Spring MVC 異常解析器,原理就是這么簡單
使用介紹
一般自定義異常處理策略有兩種方式
- 使用@ExceptionHandler注解
- 實現HandlerExceptionResolver接口
因為@ExceptionHandler注解的方式已經足夠強大,所以我們一般也很少通過實現HandlerExceptionResolver來自定義異常處理策略。
簡單介紹一下@ExceptionHandler的使用,后面會結合這些例子進行源碼分析
- @RestController
- @RequestMapping("location")
- public class LocationController {
- @RequestMapping("getLocationInfo")
- public String index() {
- int sum = 10 / 0;
- return "locationInfo";
- }
- @ExceptionHandler(RuntimeException.class)
- public String processRuntimeException() {
- return "LocationController -> 發生RuntimeException";
- }
- @ExceptionHandler(Exception.class)
- public String processException() {
- return "LocationController -> 發生Exception";
- }
- }
訪問如下鏈接,返回結果為
- http://localhost:8080/location/getLocationInfo
- LocationController -> 發生RuntimeException
把processRuntimeException方法注釋掉以后,再次訪問上面的鏈接,結果為
- LocationController -> 發生Exception
如果在每個Controller里面都寫異常解析器還是很麻煩的,能不能在一個地方統一處理異常呢?當然可以,這時候就不得不用到@RestControllerAdvice或者@ControllerAdvice
寫如下的全局異常解析器
- @RestControllerAdvice
- public class MyExceptionHandler {
- @ExceptionHandler(RuntimeException.class)
- public String processRuntimeException() {
- return "MyExceptionHandler -> 發生RuntimeException";
- }
- @ExceptionHandler(Exception.class)
- public String processException() {
- return "MyExceptionHandler -> 發生RuntimeException";
- }
- }
訪問上面的鏈接,返回結果為
- LocationController -> 發生Exception
我們把LocationController類的processException方法也注釋掉,此時LocationController類里面已經沒有被@ExceptionHandler注解標記的方法了
訪問上面的鏈接,返回結果為
- MyExceptionHandler -> 發生RuntimeException
把MyExceptionHandler中的processRuntimeException方法注釋掉訪問上面的鏈接,返回結果為
- MyExceptionHandler -> 發生Exception
通過以上的例子,我們可以得出如下結論
- @RestControllerAdvice或者@ControllerAdvice類內的解析器的優先級低于@RequestMapping類的解析器的優先級
- 如果一個異常能被多個解析器所處理,則選擇繼承關系最近的解析器
假設BizException繼承自NullPointException A方法解析BizException B方法解析NullPointException C方法解析Exception
BizException會被A方法解析 NullPointException會被B方法解析 如果沒有A方法,則BizException會被B方法解析,如果B方法也沒有,則被C方法解析,不難理解哈
@RestControllerAdvice和@ControllerAdvice有什么區別呢?
名字上就可以猜出@RestControllerAdvice只是在@ControllerAdvice的基礎上加了@ResponseBody注解,看一波源碼也確實如此。所以@RestControllerAdvice類最終返回的是JSON,@ControllerAdvice最終返回的是視圖。如果你不明白為什么加了@ResponseBody注解最終返回的內容為JSON,建議看一下返回值處理器相關的內容
源碼分析
異常解析器接口定義如下
- public interface HandlerExceptionResolver {
- // 將異常封裝為ModelAndView后返回
- @Nullable
- ModelAndView resolveException(
- HttpServletRequest request, HttpServletResponse response,
- @Nullable Object handler, Exception ex);
- }
Spring MVC默認的異常解析器存放在如下屬性中
- @Nullable
- private List<HandlerExceptionResolver> handlerExceptionResolvers;
順序依次為
- ExceptionHandlerExceptionResolver
- ResponseStatusExceptionResolver
- DefaultHandlerExceptionResolver
UML圖如下
Order接口是用來排序的哈,Spring MVC默認的解析器不是通過Order接口來控制順序的,因為默認的解析器都繼承自AbstractHandlerExceptionResolver,并且都沒有重寫getOrder方法
對Spring MVC比較清楚的小伙伴應該都知道DispatcherServlet屬性的默認實現都定義在源碼包的DispatcherServlet.properties文件中,List的順序也是按這個來的。放一部分內容
- org.springframework.web.servlet.HandlerAdapter=
- org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
- org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
- org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter
- org.springframework.web.servlet.HandlerExceptionResolver=
- org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\
- org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
- org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionReso
接下來分析這3個默認的HandlerExceptionResolver
ExceptionHandlerExceptionResolver
ExceptionHandlerExceptionResolver用于支持@ExceptionHandler,而@ExceptionHandler應該是我們最常的,方便我們自定義異常處理策略,比通過實現HandlerExceptionResolver接口的方式簡單
從AbstractHandlerMethodExceptionResolver#shouldApplyTo可以看到
- @Override
- protected boolean shouldApplyTo(HttpServletRequest request, @Nullable Object handler) {
- if (handler == null) {
- // handler為空,交給父類去判斷
- // 默認該邏輯返回true
- return super.shouldApplyTo(request, null);
- }
- else if (handler instanceof HandlerMethod) {
- HandlerMethod handlerMethod = (HandlerMethod) handler;
- handler = handlerMethod.getBean();
- // 交給父類判斷
- return super.shouldApplyTo(request, handler);
- }
- else {
- // 不支持
- return false;
- }
- }
只有當handler為空或者handler的類型為HandlerMethod時(@RequestMapping返回的類型為HandlerMethod)才會執行后面的異常解析邏輯。所以你通過實現Controller接口或者實現HttpRequestHandler接口定義的Handler,這個注解是不起作用的
@ExceptionHandler的處理過程主要和下面2個類有關系ExceptionHandlerExceptionResolver,ExceptionHandlerMethodResolver
用幾個成員變量說一下處理過程,就不貼過多的代碼了
ExceptionHandlerExceptionResolver
- // 省略了繼承和實現關系
- public class ExceptionHandlerExceptionResolver {
- @Nullable
- private HandlerMethodArgumentResolverComposite argumentResolvers;
- @Nullable
- private HandlerMethodReturnValueHandlerComposite returnValueHandlers;
- private List<HttpMessageConverter<?>> messageConverters;
- // 被@RequestMapping標記的類 -> ExceptionHandlerMethodResolver
- private final Map<Class<?>, ExceptionHandlerMethodResolver>
- exceptionHandlerCache = new ConcurrentHashMap<>(64);
- // 被@ControllerAdvice注解標記的類 -> ExceptionHandlerMethodResolver
- private final Map<ControllerAdviceBean, ExceptionHandlerMethodResolver>
- exceptionHandlerAdviceCache = new LinkedHashMap<>();
- }
可以看到ExceptionHandlerExceptionResolver類定義了自己的參數處理器,返回值處理器,消息轉換器。所以你可以通過這些組件反向知道@ExceptionHandler方法支持的參數類型
例如從如下方法可以知道,支持的參數類型為@SessionAttribute,@RequestAttribute等 如果你寫個@RequestParam是肯定不會注入進來的
- protected List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
- List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();
- // Annotation-based argument resolution
- resolvers.add(new SessionAttributeMethodArgumentResolver());
- resolvers.add(new RequestAttributeMethodArgumentResolver());
- // Type-based argument resolution
- resolvers.add(new ServletRequestMethodArgumentResolver());
- resolvers.add(new ServletResponseMethodArgumentResolver());
- resolvers.add(new RedirectAttributesMethodArgumentResolver());
- resolvers.add(new ModelMethodProcessor());
- // Custom arguments
- if (getCustomArgumentResolvers() != null) {
- resolvers.addAll(getCustomArgumentResolvers());
- }
- return resolvers;
- }
最重要的4個map來了,ExceptionHandlerExceptionResolver的工作過程主要就是操作這4個map
- // 省略了繼承和實現關系
- public class ExceptionHandlerExceptionResolver {
- // 被@RequestMapping標記的類 -> ExceptionHandlerMethodResolver
- private final Map<Class<?>, ExceptionHandlerMethodResolver>
- exceptionHandlerCache = new ConcurrentHashMap<>(64);
- // 被@ControllerAdvice注解標記的類 -> ExceptionHandlerMethodResolver
- private final Map<ControllerAdviceBean, ExceptionHandlerMethodResolver>
- exceptionHandlerAdviceCache = new LinkedHashMap<>();
- }
exceptionHandlerCache保存了@RequestMapping對應的ExceptionHandlerMethodResolver,是在執行異常解析的過程中被賦值的
exceptionHandlerAdviceCache保存了@ControllerAdvice對應的 ExceptionHandlerMethodResolver,是在ExceptionHandlerExceptionResolver被初始化的過程中賦值的
而ExceptionHandlerMethodResolver你可以認為只是封裝了一下Exception及其對應的Method
以最開始的例子演示,ExceptionHandlerExceptionResolver初始化后
此時exceptionHandlerCache是沒有值的 訪問如下鏈接后
- http://localhost:8080/location/getLocationInfo
exceptionHandlerCache中的值如下,LocationController及其對應的ExceptionHandlerMethodResolver被放了進來追一下以下方法的執行 ExceptionHandlerExceptionResolver#doResolveHandlerMethodException ExceptionHandlerExceptionResolver#getExceptionHandlerMethod
可以得出我們測試的結論@RestControllerAdvice或者@ControllerAdvice類內的解析器的優先級低于@RequestMapping類的解析器的優先級
總體實現也不難,從exceptionHandlerCache中能找到解析器就返回執行,找不到就從exceptionHandlerAdviceCache中找,這不是就實現了優先級了嗎?
接著來看剩下的2個Map
- public class ExceptionHandlerMethodResolver {
- // 異常 -> 對應的處理方法
- private final Map<Class<? extends Throwable>, Method>
- mappedMethods = new HashMap<>(16);
- // 異常 -> 對應的處理方法
- // 這個是基于mappedMethods又做了一次緩存
- // 為什么要再做一次緩存呢?
- // 是因為根據異常類型獲取處理方法的時候,一個異常可能有多個處理方法,即一個異常會從mappedMethods中查出多個處理方法
- // 最后返回的是繼承關系最近的異常對應的處理方法,所以在查找的時候又做了一次緩存,避免每次查mappedMethods然后取最優值
- // 從exceptionLookupCache中就可以直接查到最優的處理方法
- private final Map<Class<? extends Throwable>, Method>
- exceptionLookupCache = new ConcurrentReferenceHashMap<>(16);
- }
@ControllerAdvice的mappedMethods是在ExceptionHandlerExceptionResolver初始化的過程中賦值的
@RequestMapping的mappedMethods是在執行異常解析的過程中被賦值的
而exceptionLookupCache是在異常解析過程中,通過Exception查找Method的過程中基于mappedMethods做的緩存
為什么在查找過程中要再做一次緩存呢?
是因為根據異常類型獲取處理方法的時候,一個異常可能有多個處理方法,即一個異常會從mappedMethods中查出多個處理方法,最后返回的是繼承關系最近的異常對應的處理方法,所以在查找的時候又做了一次緩存,避免每次查mappedMethods然后取最優值。從exceptionLookupCache中就可以直接查到最優的處理方法
以LocationController為例,查找一次異常后,exceptionLookupCache的值如下
這樣當再次發生ArithmeticException異常時就能從exceptionLookupCache找到對應的處理方法
ResponseStatusExceptionResolver
ResponseStatusExceptionResolver和DefaultHandlerExceptionResolver的實現都不是很難,就不進行過多的分析了
ResponseStatusExceptionResolver主要用來處理如下異常
拋出的異常類型繼承自ResponseStatusException
拋出的異常類型被@ResponseStatus標記
以一個例子來演示這個處理器的功能
- @ResponseStatus(HttpStatus.UNAUTHORIZED)
- public class UnauthorizedException extends RuntimeException {
- }
- @RestController
- @RequestMapping("shoppingCar")
- public class ShoppingCarController {
- @RequestMapping("getCarInfo")
- public String index() {
- throw new UnauthorizedException();
- }
- }
訪問
- http://localhost:8080/shoppingCar/getCarInfo
顯示如下
DefaultHandlerExceptionResolver
用來處理一些常見的Http異常,如
400:請求無效 405:請求方法不支持 500:內部服務器錯誤
執行入口
- # DispatcherServlet#processDispatchResult的部分方法
- // 處理過程發生了異常
- if (exception != null) {
- if (exception instanceof ModelAndViewDefiningException) {
- logger.debug("ModelAndViewDefiningException encountered", exception);
- // 直接使用異常中封裝的ModelAndView作為最終的ModelAndView結果
- mv = ((ModelAndViewDefiningException) exception).getModelAndView();
- }
- else {
- // 其他異常類型,先獲取解析器
- Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
- // 通過異常解析器將異常解析為一個錯誤視圖
- mv = processHandlerException(request, response, handler, exception);
- errorView = (mv != null);
- }
- }
如果整個處理過程發生異常,依次調用DispatcherServlet的成員變量handlerExceptionResolvers的resolveException方法,找到第一個不為null的ModelAndView,然后返回
- @Nullable
- private List<HandlerExceptionResolver> handlerExceptionResolvers;
本文轉載自微信公眾號「Java識堂」,可以通過以下二維碼關注。轉載本文請聯系Java識堂公眾號。