我们在写web应用时,有一个很常见的需求就是:业务层可能会抛出各种各样的异常,我们希望可以在最终接口返回时,根据异常类型自动映射为一个errorCode以及errorMsg,下发给客户端。
具体看下:
以登录为例,我们有一个userService,可以通过客户端的token反解出user信息。如果token不合法,则抛出一个业务异常,终止当前请求:
@Service
public class UserService {
public Object getUserInfo(String token) {
if (token == null || token.length() > 3) {
return "userInfo";
}
throw new BizException(TOKEN_NOT_EXIST, "用户token不存在");
}
}
当然,这里作为例子,就省去了查询token的过程,如果token不为空且长度大于3,就认为其存在。
public class BizException extends RuntimeException {
private int code;
private String errorMsg;
public BizException(int code, String errorMsg) {
this.code = code;
this.errorMsg = errorMsg;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getErrorMsg() {
return errorMsg;
}
public void setErrorMsg(String errorMsg) {
this.errorMsg = errorMsg;
}
}
义务异常类,所有的义务都抛这个异常,通过code来表明不同的异常类型,可以避免类爆炸。
public class ErrorCode {
public static final int SUCCESS = 1;
public static final int TOKEN_NOT_EXIST = 2;
}
code常量类。
最后看下controller:
@RequestMapping("/login")
public Object login(@RequestParam String token) {
Map<String, Object> result = new HashMap<>();
Object userInfo = userService.getUserInfo(token);
result.put("code", 1);
result.put("user", userInfo);
return result;
}
这里如果不处理异常,那么就会直接将异常信息返回给客户端:
这样客户端也不知道如何处理,可能导致崩溃,所以还是需要try-catch住,做一层转换。
变成这样:
@RequestMapping("/login")
public Object login(@RequestParam String token) {
Map<String, Object> result = new HashMap<>();
try {
Object userInfo = userService.getUserInfo(token);
result.put("code", 1);
} catch (BizException e) {
result.put("code", e.getCode());
result.put("errorMsg", e.getErrorMsg());
} catch (Exception e) {
result.put("code", 999);
result.put("errorMsg", "serverError");
}
return result;
}
如果用户不存在,捕获BizException,转为对应的errorCode,下发给客户端:
{"code":2,"errorMsg":"用户token不存在"}
这样客户端就可以根据不同的code来做不同的处理,比如说提醒用户登录等等。
这样看上去已经好了很多,但是,我们会有很多控制器,每一个都在控制器层面try-catch,转为code,这其实是一个重复的过程,而且,过多的try-catch样板代码使得这个类结构很臃肿,代码可读性较差,显然需要寻找更好的办法,可以将这个行为定义到一个抽象的框架层面里,业务控制器就不需要关心这个转换了。
这个办法就是使用今天的主角@ControllerAdvice注解。
这个注解可以用在任意的类上,被这个注解标注的类会被spring扫包扫到,这个类的实例会被作为控制器的增强类,有点类似动态代理的感觉。我们可以在这个类里定义各种控制器的增强行为,比如全局异常处理。
看下例子:
@ControllerAdvice(basePackages = "com.liyao.controller")
public class GlobalExceptionHandler {
@ResponseBody
@ExceptionHandler(value = BizException.class)
public Map<String, Object> generalProcess(BizException e) {
Map<String, Object> result = new HashMap<>();
result.put("code", e.getCode());
result.put("errorMsg", e.getErrorMsg());
return result;
}
}
这里我们定义了一个全局异常处理增强类,会自动将BizException转为对应的code以及msg,下发给客户端。每一种异常对需要一个@Exceptionhandler注解,如果有多个,会做最近匹配,根据异常类型选出最合适的处理器。
另外,对于json类型的返回值(非modelandview),需要加@Responsebody注解序列化为json,否则会报错。
这时,我们的控制器就不需要try-catch了:
@RequestMapping("/login")
public Object login(@RequestParam String token) {
Map<String, Object> result = new HashMap<>();
Object userInfo = userService.getUserInfo(token);
result.put("code", 1);
result.put("user", userInfo);
return result;
}
如果不存在,会自动捕获转为code:
{"code":2,"errorMsg":"用户token不存在"}
是不是解放了很多??
下面看下原理,主要分为两步,一是初始化,二是异常处理。
初始化:
在DispatcherServlet的init方法中,调用了一个initHandlerExceptionResolvers方法,来加载全局异常处理器:
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
在这个init方法内部,有一段代码是这样的:
if (this.detectAllHandlerExceptionResolvers) {
// Find all HandlerExceptionResolvers in the ApplicationContext, including ancestor contexts.
Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils
.beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerExceptionResolvers = new ArrayList<HandlerExceptionResolver>(matchingBeans.values());
// We keep HandlerExceptionResolvers in sorted order.
OrderComparator.sort(this.handlerExceptionResolvers);
}
}
查找classpath下所有的HandlerExceptionResolver类型的bean,存在DispatcherServlet的一个实例变量中:
/** List of HandlerExceptionResolvers used by this servlet */
private List<HandlerExceptionResolver> handlerExceptionResolvers;
所以说,在springmvc中,全局异常的处理是由HandlerExceptionResolver来完成的。
简单看下这个接口的定义:
public interface HandlerExceptionResolver {
ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);
}
和普通的handler比较像,只是多了exception参数。
debug代码看下实际上会有哪些实现类被加载类:
我们用到的一般是是第一个(debug代码发现的),这里我们重点看下ExceptionHandlerExceptionResolver类:
这个类实例化时,这个方法会被spring回调:
@Override
public void afterPropertiesSet() {
// Do this first, it may add ResponseBodyAdvice beans
initExceptionHandlerAdviceCache();
if (this.argumentResolvers == null) {
List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
if (this.returnValueHandlers == null) {
List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
}
}
第一步就是初始化一些Advice,后面两步比较熟悉,加载argumentResolver和returnValueHandler,与普通的控制器一样。
看下Advice相关的:
List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
Collections.sort(adviceBeans, new OrderComparator());
for (ControllerAdviceBean adviceBean : adviceBeans) {
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(adviceBean.getBeanType());
if (resolver.hasExceptionMappings()) {
this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
logger.info("Detected @ExceptionHandler methods in " + adviceBean);
}
if (ResponseBodyAdvice.class.isAssignableFrom(adviceBean.getBeanType())) {
this.responseBodyAdvice.add(adviceBean);
logger.info("Detected ResponseBodyAdvice implementation in " + adviceBean);
}
}
public static List<ControllerAdviceBean> findAnnotatedBeans(ApplicationContext applicationContext) {
List<ControllerAdviceBean> beans = new ArrayList<ControllerAdviceBean>();
for (String name : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(applicationContext, Object.class)) {
if (applicationContext.findAnnotationOnBean(name, ControllerAdvice.class) != null) {
beans.add(new ControllerAdviceBean(name, applicationContext));
}
}
return beans;
}
核心代码如上,会将容器中所有的被注解ControllerAdvice标注的bean拿出来,然后构建一个ExceptionHandlerMethodResolver类型的实例,这个ExceptionHandlerMethodResolver类的作用就是处理一个Advice类中各种ExceptionHandler注解。会将解析后的adviceBean与resovler放到一个map的cache中。这里补充一点,为什么可以扫描到ControllerAdvice注解的bean,是因为该注解默认被@Component注解标记了,所以会被扫包:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
好,至此,ControllerAdvice以及内部的ExceptionHandler已经被加载完毕。
再看下处理过程:
入口还是在DispatcherServlet中的doDispatch方法中:
try {
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
mv = ha.handle(processedRequest, response,
} catch (Exception ex) {
dispatchException = ex;
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
可以看到,整个doDispatch方法其实是放在一个大的try-catch块中的,所以我们编写的controller的各种异常才能在这里被捕捉到,并得到统一的处理。catch块所做的事情就是记录异常,然后有异常和无异常的处理都会在后面的processDispatcheResult方法中执行:
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
这个方法中,如果exception不为空,那么就调用processHandlerException方法处理异常:
for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) {
exMv = handlerExceptionResolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
}
这里是不是比较熟悉?调用了之前加载的handlerExceptionResolver链来依次处理。通过debug可以看到,是被ExceptionHandlerExceptionResolver来处理的:
@Override
protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod, Exception exception) {
ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
if (exceptionHandlerMethod == null) {
return null;
}
exceptionHandlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
exceptionHandlerMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
ServletWebRequest webRequest = new ServletWebRequest(request, response);
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
try {
if (logger.isDebugEnabled()) {
logger.debug("Invoking @ExceptionHandler method: " + exceptionHandlerMethod);
}
exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception);
}
catch (Exception invocationEx) {
if (logger.isErrorEnabled()) {
logger.error("Failed to invoke @ExceptionHandler method: " + exceptionHandlerMethod, invocationEx);
}
return null;
}
if (mavContainer.isRequestHandled()) {
return new ModelAndView();
}
else {
ModelAndView mav = new ModelAndView().addAllObjects(mavContainer.getModel());
mav.setViewName(mavContainer.getViewName());
if (!mavContainer.isViewReference()) {
mav.setView((View) mavContainer.getView());
}
return mav;
}
}
首先第一步就是查找合适的exceptionHandler:
protected ServletInvocableHandlerMethod getExceptionHandlerMethod(HandlerMethod handlerMethod, Exception exception) {
Class<?> handlerType = (handlerMethod != null ? handlerMethod.getBeanType() : null);
if (handlerMethod != null) {
ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(handlerType);
if (resolver == null) {
resolver = new ExceptionHandlerMethodResolver(handlerType);
this.exceptionHandlerCache.put(handlerType, resolver);
}
Method method = resolver.resolveMethod(exception);
if (method != null) {
return new ServletInvocableHandlerMethod(handlerMethod.getBean(), method);
}
}
for (Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) {
if (entry.getKey().isApplicableToBeanType(handlerType)) {
ExceptionHandlerMethodResolver resolver = entry.getValue();
Method method = resolver.resolveMethod(exception);
if (method != null) {
return new ServletInvocableHandlerMethod(entry.getKey().resolveBean(), method);
}
}
}
return null;
}
这里会从之前的advice缓存cache中根据异常类型查找出最合适的handler,里面的匹配规则和普通的handler路径匹配类似,先是直接匹配然后是最优匹配。
匹配到以后,就将handler配置上argumentResolver和returnValueHandler,来处理入参和返回值,因为异常处理的返回值也可能被@Responsebody之类的注解标注,所以需要处理。
整个处理流程和普通的handler几乎一模一样。
所以整个springmvc的dispatcherServlet处理流程是放在一个大的try-catch块中的,有异常和无异常的处理几乎类似,都是匹配出对应的handler,设置argumentResolver和returnValueHandler,然后invoke。