ExceptionHandler的实践
ExceptionHandler的实践
- 1、ExceptionHandler中ModelAndView返回一个不存在的view,会发生什么
- 2、验证
- 3、view不存在为什么会抛异常
- 4、正确写全局异常捕获类实例
1、ExceptionHandler中ModelAndView返回一个不存在的view,会发生什么
背景: 公司的项目中经常会抛异常,然后就排查为什么会经常有异常,最终通过异常堆栈等信息了解到工程中使用了全局异常捕获,返回的是ModelAndView对象,但是这里指定的view工程上找不到。
结论: 在全局异常捕获方法【ExceptionHandler】中,如果返回的是ModelAndView,但是view返回一个不存在view,会发生什么?
- 很明显会直接抛异常给调用端。
接下来看,为什么会抛异常?
2、验证
代码复现:
// http接口@RestController
@RequestMapping(value = "/api")
public class SecondFloorController {@GetMapping("/list")@ResponseBodypublic BaseResponse getData(HttpServletRequest request) throws Exception {RequestParam requestParam = new RequestParam(request);long userId = requestParam.getUserId();// 只是为了测试,并不是真正的业务逻辑if(userId == 0){return new BaseResponse(0, "success");} else if (userId == 1) {throw new EofException();} else {throw new IOException();}}
}// 全局异常捕获
@ControllerAdvice
public class ExceptionHandle {private static Logger LOGGER = LoggerFactory.getLogger(ExceptionHandle.class);@ExceptionHandler(IOException.class)public ModelAndView errorPage(Exception e) {ModelAndView mav = new ModelAndView("error"); // 这里工程中没有error.index、error.jsp等文件mav.addObject("status", ResponseStatusEnum.TIMEOUT.getCode());mav.addObject("error", ResponseStatusEnum.TIMEOUT.getMessage());return mav;}@ExceptionHandler(EofException.class)public BaseResponse error(Exception e) {BaseResponse response = new BaseResponse(-2, "EofException");return response;}}
全局异常捕获方法中返回不同数据,运行结果现象:
返回数据 | 请求结果 | 现象 |
---|---|---|
返回modelAndView在遇到指定view但是不存在 | ||
返回自定义结构体 | 包装成ModelAndView,view是请求路径,model就是返回的自定义结构体 | |
在全局异常捕获类中增加ResponseBody注解 | 只要在全局异常捕获类里面增加ResponseBody注解,上述情况也没问题【但不要返回ModelAndView】 |
结论:
- 排查发现:ExceptionHandle类中没有添加@ResponseBody注解:
- 如果ExceptionHandle方法返回的自定义数据都会被Spring包装成ModelAndView,view默认是请求路径,但是view不存在就会抛异常
- 如果ExceptionHandle方法返回的是ModelAndView,如果view不存在也会抛异常
3、view不存在为什么会抛异常
一言不合,再打开源码瞅瞅:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {HttpServletRequest processedRequest = request;HandlerExecutionChain mappedHandler = null;boolean multipartRequestParsed = false;WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);try {ModelAndView mv = null;Exception dispatchException = null;try {processedRequest = checkMultipart(request);multipartRequestParsed = (processedRequest != request);// Determine handler for the current request.mappedHandler = getHandler(processedRequest);if (mappedHandler == null) {noHandlerFound(processedRequest, response);return;}// Determine handler adapter for the current request.HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());// Process last-modified header, if supported by the handler.String method = request.getMethod();boolean isGet = "GET".equals(method);if (isGet || "HEAD".equals(method)) {long lastModified = ha.getLastModified(request, mappedHandler.getHandler());if (logger.isDebugEnabled()) {logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);}if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {return;}}if (!mappedHandler.applyPreHandle(processedRequest, response)) {return;}// Actually invoke the handler.// 这里会执行业务代码,就是你的controller,并且将返回结果通过jetty发送给客户端【这时候可能会出现EofException,但是会被封装成HttpMessageNotWritableException】mv = ha.handle(processedRequest, response, mappedHandler.getHandler());if (asyncManager.isConcurrentHandlingStarted()) {return;}applyDefaultViewName(processedRequest, mv);mappedHandler.applyPostHandle(processedRequest, response, mv);}// 通过这里可以看出,业务执行的代码不管怎么抛异常都会被捕获,所以doDispatch怎么才会抛异常给上层呢catch (Exception ex) {dispatchException = ex;}catch (Throwable err) {// As of 4.3, we're processing Errors thrown from handler methods as well,// making them available for @ExceptionHandler methods and other scenarios.dispatchException = new NestedServletException("Handler dispatch failed", err);}// 无论业务是否正常返回还是给客户端发送数据是否成功都会被执行,唯一有可能抛异常的就是这里了processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);}// 这里虽然catch了,但是可以看下triggerAfterCompletion会原封不动的继续抛出来catch (Exception ex) {triggerAfterCompletion(processedRequest, response, mappedHandler, ex);}catch (Throwable err) {triggerAfterCompletion(processedRequest, response, mappedHandler,new NestedServletException("Handler processing failed", err));}finally {if (asyncManager.isConcurrentHandlingStarted()) {// Instead of postHandle and afterCompletionif (mappedHandler != null) {mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);}}else {// Clean up any resources used by a multipart request.if (multipartRequestParsed) {cleanupMultipart(processedRequest);}}}}
processDispatchResult方法:
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,@Nullable Exception exception) throws Exception {boolean errorView = false;if (exception != null) {if (exception instanceof ModelAndViewDefiningException) {logger.debug("ModelAndViewDefiningException encountered", exception);mv = ((ModelAndViewDefiningException) exception).getModelAndView();}else {Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);// 这里会按照优先级获取执行全局异常捕获方法,会封装成ModelAndView// 全局异常捕获类中没有增加ResponseBody注解,你返回自定义结构体,这里会把结果封装为model,view为路径// 全局异常捕获类中增加ResponseBody注解,你却返回ModelAndView,这里是返回的结果【等价于ResponseBody注解没生效】mv = processHandlerException(request, response, handler, exception);errorView = (mv != null);}}// Did the handler return a view to render?if (mv != null && !mv.wasCleared()) {// 这里会对view进行渲染,如果view不存在直接抛异常【根因,到此结束】render(mv, request, response);if (errorView) {WebUtils.clearErrorRequestAttributes(request);}}else {if (logger.isDebugEnabled()) {logger.debug("Null ModelAndView returned to DispatcherServlet with name '" + getServletName() +"': assuming HandlerAdapter completed request handling");}}if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {// Concurrent handling started during a forwardreturn;}if (mappedHandler != null) {mappedHandler.triggerAfterCompletion(request, response, null);}}
4、正确写全局异常捕获类实例
@ControllerAdvice
@ResponseBody
public class ExceptionHandle {private static Logger LOGGER = LoggerFactory.getLogger(ExceptionHandle.class);/*** 虽然可行 但是不要这么干!!!*/@ExceptionHandler(IOException.class)public ModelAndView errorPage(Exception e) {ModelAndView mav = new ModelAndView("error");mav.addObject("status", ResponseStatusEnum.TIMEOUT.getCode());mav.addObject("error", ResponseStatusEnum.TIMEOUT.getMessage());return mav;}@ExceptionHandler(EofException.class)public BaseResponse eofExceptionHandler(Exception e) {BaseResponse response = new BaseResponse(-1, "EofException");return response;}@ExceptionHandlerpublic BaseResponse exceptionHandler(Exception e) {BaseResponse response = new BaseResponse(-1, e.getMessage());return response;}
}
顺便说句:
如果Controller中增加了@MdpExceptionHandler(EofException.class),那么Controller会优先使用自己controller下的异常捕获。