WebApplicationContext 中特殊的 bean 型別(一)--- 請求/異常處理
前言
其實 Spring 的基本思想就是“萬物都是 bean”,那麼為了滿足 spring 工程的需要,spring 中有一些預設的 bean 選項,它們用於處理請求,渲染檢視等。比如上一篇文章就用過的 viewResolver 的配置。當然,servlet 也允許你配置使用不同特定的 bean,但是,如果你沒有配置,spring 將會按照預設的 bean 進行配置。本章將會詳細說明文件中列出的 bean 的配置以及具體的使用例子,所講述的 bean 型別包括:
- HandlerMapping 和 HandlerAdapter
- HandlerExceptionResolver
- LocaleResolver & LocaleContextResolver
- ThemeResolver
- MultipartResolver
HandlerAdapter 和 HandlerMapping 解析
前期準備
本章節將基於文件實踐(一)的程式碼進行後續的操作,因此我們使用了單個 ContextConfig 來配置工程 Context 物件,也就是 root-context.xml 檔案。另一方面,為了實現 HandlerMapping 在 xml 配置的功能,我們關掉了
<mvc:annotation-driven/>
複製程式碼
的功能,使得 @Controller 註解下的類不再會被自動配置並且做 url 的對映,現在再去試一下 localhost:8080/hello.do 的話,已經是 404 Not Found 了。之後再進行後續的實踐過程。
這裡 HandlerMapping 和 HandlerAdapter 一起講是因為,HandlerMapping 需要 HandlerAdapter 的支援才能正常執行。HandlerMapping 用於將請求的 url 對映到對應的 controller 上面,如果沒有進行配置的話,@Controller 註解即為 HandlerMapping,上一篇的 ExampleController 即有著和上述相似的功能。值得注意的是,Spring MVC 4.0 之後主推 Annotation Driven,也就是註解驅動模式下的工程,因此,對應的 adapter 已經標記為 deprecated,不推薦使用,這裡只做幫助理解使用。
HandlerAdapter
由於工程中的 Controller 都是用註解配置的,因此,在 DispatcherServlet 根據 bean 的配置資訊(root-context.xml,我們用 Context 物件來配置 bean 的資訊)知道了自己所需要呼叫的 controller 之後,他需要根據註解來提取其他的所需要的資訊。這時候就需要 HandlerAdapter 來做這些解析的事情。
然而,目前的 Spring MVC 的配置都基於註解,因此,HandlerAdapter 也退居幕後,@Controller 註解包含了其中邏輯,在 Annotation-driven 被我們關掉的場景下,也只要做好 HandlerMapping,就可以成功地對映你想要的 url
HandlerMapping
HandlerMapping 本質還是一個 Bean,他在 Spring MVC 裝配完成之後,執行著將 URL 的請求轉發到對應的 Controller 執行後續檢視,資料等返回的工作。因此,在配置 HandlerMapping Bean 的時候,需要配置 property 的 mappings 欄位,並且在 欄位下面指定對應的請求對映。具體程式碼如下:
<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/handler-mapping.do">handlerMappingController</prop>
</props>
</property>
</bean>
複製程式碼
HandlerAdapter 和 HandlerMapping 的測試
為了同步一下,目前 root-context.xml (Spring Context 物件配置檔案) 的配置加入了 HandlerMapping 的配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:tx="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.2.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<context:component-scan base-package="com.test.myapp.example"/>
<!--註冊一個用於 handlerMapping 的 bean 用於檢測 handlerMapping 效果-->
<bean id="handlerMappingController" class="com.test.myapp.example.handlermapping.HandlerMappingController"/>
<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/handler-mapping.do">handlerMappingController</prop>
</props>
</property>
</bean>
<!--<bean id="simpleHandler" class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"/>-->
<!--<mvc:annotation-driven/>-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"
p:prefix="/WEB-INF/views/" p:suffix=".jsp" p:order="1">
</bean>
</beans>
複製程式碼
並且新增了 HandlerMappingController.java 的配置:
package com.test.myapp.example.handlermapping;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
/**
* Usage: 測試 handler mapping 的有效性
* @author: srfan
* Date: 10/26/18 4:11 PM
*/
@Controller
public class HandlerMappingController {
@RequestMapping(value="/handler-mapping.do", method = RequestMethod.GET)
public String helloWorld() {
return "handler_mapping_hello";
}
}
複製程式碼
我們看到,HandlerMapping 下面配置了 /handler-mapping.do 的對映。因此,在執行工程之後,輸入 localhost:8080/handler-mapping.do,就可以看到對應的 handler_mapping_hello.jsp 上的前端檢視返回。
HandlerExceptionResolver 解析
HandlerExceptionResolver 是工程中用於捕獲特定 Exception 的 Bean,可以提前設定自己需要捕獲並且定向的 Exception,並且交由 HandlerExceptionResolver 對映到特定的檢視頁上面。 目前常用的方法有:
- 實現 HandlerExceptionResolver 介面
- 在方法上使用 @ExceptionHandler 註解
實現 HandlerExceptionResolver 介面
HandlerExceptionResolver 介面只有一個待實現的方法
ModelAndView resolveException(HttpServletRequest var1, HttpServletResponse var2, Object var3, Exception var4);
複製程式碼
為了工程上面比較直觀簡便的實現,我們只需要做最簡單的實現:拿到 Exception 的具體類,並且返回對應的 error 的檢視,並且記錄下 Exception 的 message,顯示在檢視頁面上面。因此我們的工序如下:
實現一個自定義的 Exception: MyCustomException
package com.test.myapp.example.handlermapping;
public class MyCustomException extends RuntimeException {
public MyCustomException(String msg) {
super(msg);
}
}
複製程式碼
這個 Exception 類很簡單,只是把 message 放進 Exception 中,無需贅述,主要是要讓 ExceptionResolver 捕獲該 Exception。
實現 HandlerExceptionResolver 介面:ExceptionResolver
package com.test.myapp.example.handlermapping;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class ExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) {
if (e instanceof MyCustomException) {
ModelAndView modelAndView = new ModelAndView("error");
modelAndView.addObject("msg", e.getMessage());
return modelAndView;
}
return null;
}
}
複製程式碼
我們使用 ExceptionResolver 實現了 resolveException 方法,並且會解析 MyCustomException 並且在 ModelAndView 物件加入一個變數,並且返回名為 "error" 的 jsp 檢視。我們也可以在 error.jsp 上顯示這個 msg 欄位的資訊。
HandlerMappingController 新增兩個會丟擲 Exception 的介面
為了對照效果,我們實現兩個介面,一個會丟擲 MyCustomException,另一個則會丟擲普通的 IllegalArgumentException,而我們需要捕獲的則是 MyCustomException。
package com.test.myapp.example.handlermapping;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public class HandlerMappingController {
@RequestMapping(value="/handler-mapping.do", method = RequestMethod.GET)
public String helloWorld() {
return "handler_mapping_hello";
}
@RequestMapping(value="/custom-exception.do", method = RequestMethod.GET)
public String throwException() {
throw new MyCustomException("oh, you got custom exception message~!");
}
@RequestMapping(value="/argument-exception.do", method = RequestMethod.GET)
public String throwArgumentException() {
throw new IllegalArgumentException("oh, you got argument exception message~!");
}
}
複製程式碼
檢視檔案 error.jsp 配置
檢視檔案 error.jsp 比較簡單,只要體現 msg 欄位即可:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Ooooops, you meet MyCustomException</title>
</head>
<body>
<h1>${msg}</h1>
</body>
</html>
複製程式碼
測試
執行工程後,在瀏覽器分別輸入:
- http://localhost:8080/custom-exception.do:瀏覽器返回了 error.jsp 的檢視並且輸出了包裹在 MyCustomException 的資訊,符合預期
- http://localhost:8080/argument-exception.do:瀏覽器返回了 500 Internal Server Error,因為沒有用於 IllegalArgumentException 的 resolver,因此返回了預設的檢視。
使用 @ExceptionHandler 註解
另一種方法是使用 @ExceptionHandler 的註解,該註解用於 method 的簽名上面,我們可以實現一個 Controller 的基類並讓實際接收 url 請求的 Controller 繼承該基類。值得注意的是,這個方法實現的 ExceptionResolver 只會在該 Controller 內部有效,而來自其他 Controller 類的 Exception 則無法得到解析。具體程式碼步驟如下:
設定自定義 Exception: CustomExceptionForAnnotation
我們為這一次測試也設定了自定義的 Exception 類,實現方法也很簡單,可以自定義 Exception 中的資訊:
package com.test.myapp.example.exceptionresolver;
public class CustomExceptionForAnnotation extends RuntimeException {
public CustomExceptionForAnnotation(String msg) {
super(msg);
}
}
複製程式碼
實現有 @ExceptionHandler 註解的 Controller 基類
我們的 Controller 基類需要 Resolve CustomExceptionForAnnotation,需要用 @ExceptionHandler(CustomExceptionForAnnotation.class) 進行配置,具體方法如下:
package com.test.myapp.example.exceptionresolver;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;
public abstract class BaseExceptionResolver {
@ExceptionHandler({CustomExceptionForAnnotation.class})
public ModelAndView handleCustomException(CustomExceptionForAnnotation ex) {
ModelAndView modelAndView = new ModelAndView("error");
modelAndView.addObject("msg", ex.getMessage());
return modelAndView;
}
}
複製程式碼
可以看到,該類中所含有的方法僅會解析 CustomExceptionForAnnotation 類,並且將其重新導向 error.jsp 檢視,最後輸出對應的 message 資訊到前端。
實現兩個 Controller 類
為了使測試結果有對照性,我們實現了兩個 Controller 類,一個繼承自 BaseExceptionResolver,另一個則沒有。理論上說,繼承了 BaseExceptionResolver 的 Controller 將可以解析上面的 Exception,而另一個則不能。具體的配置方法如下:
-
繼承了 BaseExceptionResolver 的 Controller 類 package com.test.myapp.example.exceptionresolver;
import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class MyExceptionController extends BaseExceptionResolver { @RequestMapping("exception-for-annotation.do") public void exceptionForAnnotation() { throw new CustomExceptionForAnnotation("Oooops, you get CustomExceptionForAnnotation message"); } } 複製程式碼
-
未繼承 BaseExceptionResolver: package com.test.myapp.example.exceptionresolver;
import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class MyExceptionOutsideController { @RequestMapping("exception-for-annotation-outside.do") public void exceptionForAnnotation() { throw new CustomExceptionForAnnotation("Oooops, you get CustomExceptionForAnnotation message"); } } 複製程式碼
測試
我們仍然使用了 error.jsp 檢視來做最後的測試工作,我們看到 BaseExceptionResolver 在捕獲異常後,仍然會輸出 error.jsp 的檢視。我們將會請求兩個具體 Controller 類的 url,觀察是否會有我們想要的檢視的輸出:
- localhost:8080/exception-for-annotation.do: 成功輸出了我們放入 CustomExceptionForAnnotation 的資訊。
- localhost:8080/exception-for-annotation-outside.do: 頁面輸出了 500 的錯誤資訊,並且帶上了 Exception 中的資訊,因為其沒有繼承 BaseExceptionResolver,因此也沒有對應的 Exception 解析器了。
小結
本章主要講述了 HandlerMapping 和 HandlerExceptionResolver 的具體實現程式碼,一個是處理正常的 url 請求的對映工具,而另一個則是專門處理工程在執行過程中出現 Exception 的處理方法。下一次我將繼續介紹後面這幾個特殊 Bean 的用法。