在Spring里,我們可以使用@ControllerAdvice來聲明一些全局性的東西,最常見的是結合@ExceptionHandler注解用於全局異常的處理。
@ControllerAdvice是在類上聲明的注解,其用法主要有三點:
- @ExceptionHandler注解標注的方法:用於捕獲Controller中拋出的不同類型的異常,從而達到異常全局處理的目的;
- @InitBinder注解標注的方法:用於請求中注冊自定義參數的解析,從而達到自定義請求參數格式的目的;
- @ModelAttribute注解標注的方法:表示此方法會在執行目標Controller方法之前執行 。
看下具體用法:
// 這里@RestControllerAdvice等同於@ControllerAdvice + @ResponseBody @RestControllerAdvice public class GlobalHandler { private final Logger logger = LoggerFactory.getLogger(GlobalHandler.class); // 這里@ModelAttribute("loginUserInfo")標注的modelAttribute()方法表示會在Controller方法之前 // 執行,返回當前登錄用戶的UserDetails對象 @ModelAttribute("loginUserInfo") public UserDetails modelAttribute() { return (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); } // @InitBinder標注的initBinder()方法表示注冊一個Date類型的類型轉換器,用於將類似這樣的2019-06-10 // 日期格式的字符串轉換成Date對象 @InitBinder protected void initBinder(WebDataBinder binder) { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); dateFormat.setLenient(false); binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false)); } // 這里表示Controller拋出的MethodArgumentNotValidException異常由這個方法處理 @ExceptionHandler(MethodArgumentNotValidException.class) public Result exceptionHandler(MethodArgumentNotValidException e) { Result result = new Result(BizExceptionEnum.INVALID_REQ_PARAM.getErrorCode(), BizExceptionEnum.INVALID_REQ_PARAM.getErrorMsg()); logger.error("req params error", e); return result; } // 這里表示Controller拋出的BizException異常由這個方法處理 @ExceptionHandler(BizException.class) public Result exceptionHandler(BizException e) { BizExceptionEnum exceptionEnum = e.getBizExceptionEnum(); Result result = new Result(exceptionEnum.getErrorCode(), exceptionEnum.getErrorMsg()); logger.error("business error", e); return result; } // 這里就是通用的異常處理器了,所有預料之外的Exception異常都由這里處理 @ExceptionHandler(Exception.class) public Result exceptionHandler(Exception e) { Result result = new Result(1000, "網絡繁忙,請稍后再試"); logger.error("application error", e); return result; } }
@ExceptionHandler標注的多個方法分別表示只處理特定的異常。這里需要注意的是當Controller拋出的某個異常多個@ExceptionHandler標注的方法都適用時,Spring會選擇最具體的異常處理方法來處理,也就是說@ExceptionHandler(Exception.class)這里標注的方法優先級最低,只有當其它方法都不適用時,才會來到這里處理。
下面我們看看Spring是怎么實現的,首先前端控制器DispatcherServlet對象在創建時會初始化一系列的對象:
public class DispatcherServlet extends FrameworkServlet { // ...... protected void initStrategies(ApplicationContext context) { initMultipartResolver(context); initLocaleResolver(context); initThemeResolver(context); initHandlerMappings(context); initHandlerAdapters(context); initHandlerExceptionResolvers(context); initRequestToViewNameTranslator(context); initViewResolvers(context); initFlashMapManager(context); } // ...... }
對於@ControllerAdvice 注解,我們重點關注initHandlerAdapters(context)和initHandlerExceptionResolvers(context)這兩個方法。
initHandlerAdapters(context)方法會取得所有實現了HandlerAdapter接口的bean並保存起來,其中就有一個類型為RequestMappingHandlerAdapter的bean,這個bean就是@RequestMapping注解能起作用的關鍵,這個bean在應用啟動過程中會獲取所有被@ControllerAdvice注解標注的bean對象做進一步處理,關鍵代碼在這里:
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { // ...... private void initControllerAdviceCache() { // ...... List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext()); AnnotationAwareOrderComparator.sort(adviceBeans); List<Object> requestResponseBodyAdviceBeans = new ArrayList<>(); for (ControllerAdviceBean adviceBean : adviceBeans) { Class<?> beanType = adviceBean.getBeanType(); if (beanType == null) { throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean); } // 找到所有ModelAttribute標注的方法並緩存起來 Set<Method> attrMethods = MethodIntrospector.selectMethods(beanType, MODEL_ATTRIBUTE_METHODS); if (!attrMethods.isEmpty()) { this.modelAttributeAdviceCache.put(adviceBean, attrMethods); if (logger.isInfoEnabled()) { logger.info("Detected @ModelAttribute methods in " + adviceBean); } } // 找到所有InitBinder標注的方法並緩存起來 Set<Method> binderMethods = MethodIntrospector.selectMethods(beanType, INIT_BINDER_METHODS); if (!binderMethods.isEmpty()) { this.initBinderAdviceCache.put(adviceBean, binderMethods); if (logger.isInfoEnabled()) { logger.info("Detected @InitBinder methods in " + adviceBean); } } // ...... } } // ...... }
來看DispatcherServlet的initHandlerExceptionResolvers(context)方法,方法會取得所有實現了HandlerExceptionResolver接口的bean並保存起來,其中就有一個類型為ExceptionHandlerExceptionResolver的bean,這個bean在應用啟動過程中會獲取所有被@ControllerAdvice注解標注的bean對象做進一步處理,關鍵代碼在這里:
public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver implements ApplicationContextAware, InitializingBean { // ...... private void initExceptionHandlerAdviceCache() { // ...... List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext()); AnnotationAwareOrderComparator.sort(adviceBeans); for (ControllerAdviceBean adviceBean : adviceBeans) { ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(adviceBean.getBeanType()); if (resolver.hasExceptionMappings()) { // 找到所有ExceptionHandler標注的方法並保存成一個ExceptionHandlerMethodResolver類型的對象緩存起來 this.exceptionHandlerAdviceCache.put(adviceBean, resolver); if (logger.isInfoEnabled()) { logger.info("Detected @ExceptionHandler methods in " + adviceBean); } } // ...... } } // ...... }
當Controller拋出異常時,DispatcherServlet通過ExceptionHandlerExceptionResolver來解析異常,而ExceptionHandlerExceptionResolver又通過ExceptionHandlerMethodResolver 來解析異常, ExceptionHandlerMethodResolver 最終解析異常找到適用的@ExceptionHandler標注的方法是這里:
public class ExceptionHandlerMethodResolver { // ...... private Method getMappedMethod(Class<? extends Throwable> exceptionType) { List<Class<? extends Throwable>> matches = new ArrayList<Class<? extends Throwable>>(); // 找到所有適用於Controller拋出異常的處理方法,例如Controller拋出的異常 // 是BizException(繼承自RuntimeException),那么@ExceptionHandler(BizException.class)和 // @ExceptionHandler(Exception.class)標注的方法都適用此異常 for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) { if (mappedException.isAssignableFrom(exceptionType)) { matches.add(mappedException); } } if (!matches.isEmpty()) { // 這里通過排序找到最適用的方法,排序的規則依據拋出異常相對於聲明異常的深度,例如 // Controller拋出的異常是BizException(繼承自RuntimeException),那么BizException // 相對於@ExceptionHandler(BizException.class)聲明的BizException.class其深度是0, // 相對於@ExceptionHandler(Exception.class)聲明的Exception.class其深度是2,所以 // @ExceptionHandler(BizException.class)標注的方法會排在前面 Collections.sort(matches, new ExceptionDepthComparator(exceptionType)); return this.mappedMethods.get(matches.get(0)); } else { return null; } } // ...... }
整個@ControllerAdvice處理的流程就是這樣,這個設計還是非常靈活的。