我们在写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。
