Spring實戰第七章————SpringMVC配置的替代方案
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配置的替代方案