在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處理的流程就是這樣,這個設計還是非常靈活的。
