1. 程式人生 > >Spring實戰第七章————SpringMVC配置的替代方案

Spring實戰第七章————SpringMVC配置的替代方案

cte 推薦 http請求 spa 自動註冊 這樣的 resp 發生 star

SpringMVC配置的替代方案

自定義DispatherServlet配置

我們之前在SpittrWebAppInitializer所編寫的三個方法僅僅是必須要重載的abstract方法。但還有更多的方法可以進行重載,從而實現額外的配置。

例如customizeRegistration()。在AbstractAnnotationConfigDispatcherServletInitializer將DispatcherServlet主車道Servlet容器後,就會調用該方法,並將Servlet註冊後得到的Registration.Dynamic傳遞進來。例如稍後我們將要計劃使用Servlet3.0對multipart的支持,那麽需要使用DispatcherServlet的registration來啟用multipart請求。

@Override
protected void customizeRegistration(Dynamic registration) {
    registration.setMultipartConfig(
        new MultipartConfigElement("/tmp/spittr/uploads"));
}

借助customizeRegistration()方法的ServletRegistration.Dynamic來設置MultipartConfigElement。

添加其它的Servlet和Filter

基於Java的初始化器(initializer)的一個好處就在於我們可以定義任意數量的初始化器類。
因此,如果需要定義額外的組件,只需新建相應的初始化類即可。最簡單的方法就是實現Spring的WebApplicationInitializer接口。

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration.Dynamic;
import org.springframework.web.WebApplicationInitializer;
import com.myapp.MyServlet;
public class MyServletInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        // 定義servlet
        Dynamic myServlet = servletContext.addServlet("myServlet", MyServlet.class);
        // 映射servlet
        myServlet.addMapping("/custom/**");
    }
}

這段代碼註冊了一個Servlet並將其映射到一個路徑上。我們也可以用這個方式來手動註冊DispatcherServlet。類似的我們也可以這樣註冊Filter和Listener。

@Override
public void onStartup(ServletContext servletContext) throws ServletException {
    // 註冊一個filter
    javax.servlet.FilterRegistration.Dynamic filter = servletContext.addFilter("myFilter", MyFilter.class);
    // 添加映射
    filter.addMappingForUrlPatterns(null, false, "/custom/*");
}

WebApplicationInitializer是一個在註冊servlet、filter、listener時比較推薦的方式,當然你是使用基於Java的配置方式並將應用部署在Servlet3.0容器上的。如果你僅僅需要註冊一個filter並將其映射到DispatcherServlet,那麽也可以使用AbstractAnnotationConfigDispatcherServletInitializer。要註冊多個filter並將它們映射到DispatcherServlet,你所要做的僅僅是重寫getServletFilters()方法。比如:

@Override
protected Filter[] getServletFilters() {
    return new Filter[] { new MyFilter() };
}

如你所見,該方法返回了一個javax.servlet.Filter的數組,這裏僅僅返回了一個filter,但是它可以返回很多個。同時這裏不再需要為這些filter去聲明映射,因為通過getServletFilters()返回的filter會自動地映射到DispatcherServlet。

在web.xml中聲明DispatcherServlet

在之前我們是使用AbstractAnnoatationConfigDispatcherServletInitializer自動註冊DispatcherServlet和ContextLoaderListener。但也可以按傳統方法在web.xml中註冊。

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/spring/root-context.xml</param-value>
    </context-param>
    <listener>
        <!-- 註冊ContextLoaderListener -->
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <servlet>
        <servlet-name>appServlet</servlet-name>
        <!-- 註冊DispatcherServlet -->
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <!-- DispatcherServlet映射 -->
    <servlet-mapping>
        <servlet-name>appServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

設置web.xml使用基於Java的配置

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
    <!-- 使用Java配置 -->
    <context-param>
        <param-name>contextClass</param-name>
        <param-value>
            org.springframework.web.context.support.AnnotationConfigWebApplicationContext
        </param-value>
    </context-param>
    
    <!-- 指定所使用的Java配置類 -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>spittr.config.RootConfig</param-value>
    </context-param>
    <listener>
        <listener-class>
            org.springframework.web.context.ContextLoaderListener
        </listener-class>
    </listener>
    <servlet>
        <servlet-name>appServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!-- 使用Java配置 -->
        <init-param>
            <param-name>contextClass</param-name>
            <param-value>
                org.springframework.web.context.support.AnnotationConfigWebApplicationContext
            </param-value>
        </init-param>
        <!-- 指定DispatcherServlet的配置類 -->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>
                spittr.config.WebConfigConfig
            </param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>appServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

處理multipart形式的數據

在WEB應用中用戶經常會上傳內容。而Spittr應用在兩個地方需要文件上傳。當新用戶註冊應用的時候,會需要他們上傳一張圖片。而當他們提交新的Spittle時可能會上傳圖片。一般表單提交形成的請求結果很簡單,就是以&為分割符的多個name-value。盡管這種編碼形式很簡單,但對像圖片這樣的二進制數據就不合適了。而multipart格式的數據會將一個表單後拆分為多個部分,每個部分對應一個輸入域。下面展現mulrtipart的請求體:

------WebKitFormBoundaryqgkaBn8IHJCuNmiW
Content-Disposition: form-data; name="firstName"
Charles
------WebKitFormBoundaryqgkaBn8IHJCuNmiW
Content-Disposition: form-data; name="lastName"
Xavier
------WebKitFormBoundaryqgkaBn8IHJCuNmiW
Content-Disposition: form-data; name="email"
[email protected]
------WebKitFormBoundaryqgkaBn8IHJCuNmiW
Content-Disposition: form-data; name="username"
professorx
------WebKitFormBoundaryqgkaBn8IHJCuNmiW
Content-Disposition: form-data; name="password"
letmein01
------WebKitFormBoundaryqgkaBn8IHJCuNmiW
Content-Disposition: form-data; name="profilePicture"; filename="me.jpg"
Content-Type: image/jpeg
[[ Binary image data goes here ]]
------WebKitFormBoundaryqgkaBn8IHJCuNmiW--

盡管multipart看起來復雜,但在SpringM中處理卻很容易。而首先需要要我們配置一個multipart解析器。

配置multipart解析器

DispatcherServlet並沒有任何實現解析multipart請求數據的功能。他將這任務委托給Spring中MultipartResolver策略接口的實現,從Spring3.1開始,Spring內置了兩個MultipartResolver的實現供我們選擇:

  • CommonMultipartResolver:使用Jakarta Commons FileUpload解析multipart請求;
  • StandardServletMultipartResolver:依賴於Servlet3.0對multipart請求的支持(始於Spring3.1)

一般選用StandardServletMultipartResolver。兼容Servlet3.0的StandardServletMultipartResolver沒有構造器參數,也沒有要設的屬性。因此在Spring上下文中將其聲明為bean會非常簡單,如下所示:

 @Bean
    public MultipartResolver multipartResolver() throws IOException {
        return new StandardServletMultipartResolver();
    }

那麽如何配置StandardServletMultipartResolver的限制條件呢?我們會在Servlet中指定multipart的限定條件。至少也要寫入文件上傳的過程中所寫入得臨時文件路徑。如果不設定這個最基本配置的話,StandardServletMultipartResolver就無法正常工作。所以我們會在web.xml或Servlet初始化類中將multipart的具體細節作為DispatcherServlet配置的一部分。采用Servlet初始化類的方式來配置:

DispatcherServlet ds=new DispatcherServlet();
Dynamic registration=context.addServlet("appServlet",ds);
registration.addMapping("/");
registration.asetMultipartConfig(
    new MultipartCofigElement("/tmp/spittr/upolads"));

如果配置的Servlet初始化類繼承了AbstractAnnotationConfigDispatcherServletInitializer或AbstractDispatcherServletInitializer的話,可以重載customizeRegistration()方法來配置multipart的具體細節。

//設置multipart上傳配置,路徑,文件不超過2MB,請求不超過4MB
    @Override
    protected void customizeRegistration(ServletRegistration.Dynamic registration){
        registration.setMultipartConfig(
                new MultipartConfigElement("C:/test",2097152,4194304,0));
    }

MultipartConfigElement構造器也可以進行其他一些設置:

  • 文件上傳的最大值(byte),默認沒有限制;
  • 所有multipart請求的文件最大值(byte),不管有多少個請求,默認無限制;
  • 直接上傳文件(不需存儲到臨時目錄)的最大值(byte),默認是0,也就是所有的文件都要寫入硬盤;

如果你是使用的傳統的web.xml的方式來設置的DispatcherServlet,那麽就需要使用多個

<servlet>
    <servlet-name>appServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
    <multipart-config>
        <location>/tmp/spittr/uploads</location>
        <max-file-size>2097152</max-file-size>
        <max-request-size>4194304</max-request-size>
    </multipart-config>
</servlet>

處理multipart請求

當配置好了對multipart請求的處理,接下來要編寫控制器方法來接受上傳的文件。實現這點最常見的方法就是在某個控制器方法參數上添加@Requestpart註解。
假設你想讓用戶可以在註冊時上傳圖像,那麽就需要對註冊表單進行更改從而用戶可以選擇一個圖片,同時還需要更改SpitterController中的processRegistration()方法以獲取上傳的文件。現在所需做的就是更新processRegistration()方法:

@RequestMapping(value = "/register", method = RequestMethod.POST)
public String processRegistration(@RequestPart("profilePicture") byte[] profilePicture, @Valid Spitter spitter,
        Errors errors) {
            ···
        }

當註冊表單提交時,請求部分的數據就會賦予到profilePicture屬性中,如果用戶沒有選中一個文件,那麽該數組就會是一個空值(不是null)。既然已經獲取到上傳的文件,下面所需要的就是將文件保存。

處理異常

不管發生什麽事情Servlet請求的輸出都是一個Servlet響應,所以如果出現異常,那麽它的輸出依舊是Servlet響應,一場必須是以某種方式轉換為響應。Spring提供了多種方式將一場轉換為響應:

  • 某些Spring異常會自動的映射為特定的HTTP狀態碼;
  • 使用@ResponseStatus註解將一個異常映射為HTTP狀態碼;
  • 使用ExceptionHandler註解的方法可以用來處理異常

將異常映射為HTTP狀態碼

在默認情況下,Spring將自身的一些異常轉換為合適的狀態碼。

Spring異常 HTTP狀態碼
BindException 400 - Bad Request
ConversionNotSupportedException 500 - Internal Server Error
HttpMediaTypeNotAcceptableException 406 - Not Acceptable
HttpMediaTypeNotSupportedException 415 - Unsupported Media Type
HttpMessageNotReadableException 400 - Bad Request
HttpMessageNotWritableException 500 - Internal Server Error
HttpRequestMethodNotSupportedException 405 - Method Not Allowed
MethodArgumentNotValidException 400 - Bad Request
MissingServletRequestParameterException 400 - Bad Request
MissingServletRequestPartException 400 - Bad Request
NoSuchRequestHandlingMethodException 404 - Not Found
TypeMismatchException 400 - Bad Request

將異常映射為特定的狀態碼

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(value=HttpStatus.NOT_FOUND, reason="Spittle Not Found")
public class SpittleNotFoundException extends Exception {

}

編寫異常處理的方法

將異常映射為狀態碼大多數情況下是比較簡單有效的,但是如果想讓響應不僅僅只有一個狀態碼呢?也許你想對異常進行一些處理,就行處理請求一樣。

例如,SpittleRepository的save()方法在用戶重復創建Spittle時拋出了一個DuplicateSpittleException,那麽SpittleController的saveSpittle()方法就需要處理該異常。如下面的代碼所示,saveSpittle()方法可以直接處理該異常:

@RequestMapping(method = RequestMethod.POST)
public String saveSpittle(SpittleForm form, Model model) {
    try {
        spittleRepository.save(new Spittle(null, form.getMessage(), 
                new Date(), form.getLongitude(), form.getLatitude()));
        return "redirect:/spittles";
    } catch (DuplicateSpittleException e) {
        return "error/duplicate";
    }
}

這個方法有兩個路徑,我們可以用別的方法處理異常,那這個方法可以簡單點。首先處理正確路徑的saveSpittle方法:

@RequestMapping(method = RequestMethod.POST)
public String saveSpittle(SpittleForm form, Model model) {
        spittleRepository.save(new Spittle(null, form.getMessage(), 
                new Date(), form.getLongitude(), form.getLatitude()));
        return "redirect:/spittles";
}

現在在SpittleController中添加一個新的方法:

@ExceptionHandler(DuplicateSpittleException.class)
public String handleDuplicateSpittle() {
    return "error/duplicate";
}

@ExceptionHandler註解應用在handleDuplicateSpittle()方法上,用來指定在有DuplicateSpittleException異常拋出時執行。而值得註意的是,@ExceptionHandler註解的方法在同一個控制器裏是通用的額,即無論SpittleController的哪一個方法拋出DuplicateSpittleException異常,handleDuplicateSpittle()方法都可以對其進行處理,而不再需要在每一個出現異常的地方進行捕獲。那麽,@ExceptionHandler註解的方法能不能捕獲其他controller裏的異常啊?在Spring3.2裏是可以的,但僅僅局限於定義在控制器通知類裏的方法。
那什麽是控制器通知類呢?這就是接下來要介紹的

為控制器添加通知

如果controller類的特定切面可以跨越應用的所有controller進行使用,那麽這將會帶來極大的便捷。例如,@ExceptionHandler方法就可以處理多個controller拋出的異常了。如果多個controller類都拋出同一個異常,也許你會在這些controller進行重復的@ExceptionHandler方法編寫。或者,你也可以編寫一個異常處理的基類,供其他@ExceptionHandler方法進行繼承。

Spring3.2帶來了另外一種解決方法:控制器通知。控制器通知是任意帶有@ControllerAdvice註解的類,這個類會包含一個或多個如下類型的方法:

  • @ExceptionHandler註解的
  • @InitBinder註解的
  • @ModelAttribute註解的

@ControllerAdvice註解的類中的這些方法會在整個應用中的所有controller的所有@RequestMapping註解的方法上應用。
@ControllerAdvice註解本身是使用了@Component註解的,因此,使用@ControllerAdvice註解的類會在組件掃描時進行提取,就行使用@Controller註解的類一樣。@ControllerAdvice的最實用的一個功能就是將所有的@ExceptionHandler方法集成在一個類中,從而可以在一個地方處理所有controller中的異常。例如,假設你想處理應用中所有的DuplicateSpittleException異常,可以采用下面的方法:

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

// 聲明控制器增強
@ControllerAdvice
public class AppWideExceptionHandler {

    // 定義異常處理方法
    @ExceptionHandler(DuplicateSpittleException.class)
    public String handleDuplicateSpittle() {
        return "error/duplicate";
    }

    @ExceptionHandler(SpittleNotFoundException.class)
    public String handleSpittleNotFound() {
        return "error/duplicate";
    }

}

跨重定向請求傳遞數據

在處理完POST請求過後通常應該執行重定向。這樣可以避免用戶點擊瀏覽器的刷新按鈕或後退按鈕時,客戶端重新執行危險的POST請求。在第五章中,已經在控制器方法返回的視圖名稱中使用了redirect:前綴,這時返回的String不是用來尋找視圖,而是瀏覽器進行跳轉的路徑:

return "redirect:/spitter/" + spitter.getUsername();

也許你認為Spring處理重定向只能這樣了,但是:Spring還可以做得更多。
特別是一個重定向方法如何向處理重定向的方法發送數據呢?一般的,當一個處理函數結束後,方法中的model數據都會作為request屬性復制到request中,並且request會傳遞到視圖中進行解析。因為控制器和視圖面對的是同一個request,因此request屬性在forward時保留了下來。
但是,當一個控制器返回的是一個redirect時,原來的request會終止,並且會開啟一個新的HTTP請求。原來request中所有的model數據都會清空。新的request不會有任何的model數據。
技術分享圖片

明顯的,現在不能再redirect時使用model來傳遞數據了。但是還有其他方法用來從重定向的方法中獲取數據:

  • 將數據轉換為路徑參數或者查詢參數
  • 在flash屬性中發送數據

通過URL模版進行重定向

@RequestMapping(value="/",method=POST)
public String processRegistration(Spitter spitter,Model model){
  spitterRepository.save(spitter);
  model.addAttribute("username",spitter.getUsername());
  model.addAttribute("spitterId",spitter.getId());
  return "redirect:/spitter/{username}";
}

返回的重定向String並沒有什麽變化,但是由於model中的spitterId屬性並沒有映射到URL中的占位符,它會自動作為查詢參數。
如果username是habuma,spitterId是42,那麽返回的重定向路徑將是/spitter/habuma?spitterId=42。
使用路徑參數和查詢參數傳遞數據比較簡單,但是它也有局限性。它只適用於傳遞簡單值,比如String和數字,不能傳遞比較復雜的東西,那麽我們就需要flash屬性來幫忙。

使用flash屬性

@RequestMapping(value="/",method=POST)
public String processRegistration(Spitter spitter,RedirectAttributes model){
  spitterRepository.save(spitter);
  model.addAttribute("username",spitter.getUsername());
  model.addFlashAttribute("spitter",spitter);
  return "redirect:/spitter/{username}";
}

我們傳遞了一個Spitter對象給addFlashAttribute()方法,在重定向之前,所有的flash屬性都會復制到會話中,在重定向之後,存在會話中的flash屬性會被取出,並從會話轉移到模型之中。處理重定向的方法就能從模型中訪問Spitter對象了

Spring實戰第七章————SpringMVC配置的替代方案