ExceptionHandler的实践
1、ExceptionHandler中ModelAndView返回一个不存在的view,会发生什么
背景: 公司的项目中经常会抛异常,然后就排查为什么会经常有异常,最终通过异常堆栈等信息了解到工程中使用了全局异常捕获,返回的是ModelAndView对象,但是这里指定的view工程上找不到。
结论: 在全局异常捕获方法【ExceptionHandler】中,如果返回的是ModelAndView,但是view返回一个不存在view,会发生什么?
- 很明显会直接抛异常给调用端。
接下来看,为什么会抛异常?
2、验证
代码复现:
// http接口
@RestController
@RequestMapping(value = "/api")
public class SecondFloorController {
@GetMapping("/list")
@ResponseBody
public 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 afterCompletion
if (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 forward
return;
}
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;
}
@ExceptionHandler
public BaseResponse exceptionHandler(Exception e) {
BaseResponse response = new BaseResponse(-1, e.getMessage());
return response;
}
}
顺便说句:
如果Controller中增加了@MdpExceptionHandler(EofException.class),那么Controller会优先使用自己controller下的异常捕获。
标签:mappedHandler,request,ExceptionHandler,实践,ModelAndView,processedRequest,response From: https://blog.csdn.net/m0_50149847/article/details/143616938