SpringMVC异常处理原理

Spring Boot 默认的异常处理规则

  • 默认情况下Spring Boot提供/error处理所有错误映射
  • 对于非浏览器的客户端请求会返回Json数据,包含错误信息,Http状态码,异常信息,堆栈信息. 对于浏览器客户端会响应一个 whitelabel 默认HTML白页错误视图

image-20210207112434493

image-20210207112442085

自定义错误处理

  • 自定义错误页 当前项目静态路径 或者 模板引擎查找路径下 创建 error目录,该目录下放入4xx.html 5xx.html 404.html等错误页面 如果使用了模板引擎还可以在页面中获取请求域中的数据

    image-20210207112906686

  • 使用@ControllerAdvice + @ExceptionHandler 定义异常处理逻辑 底层是因为有ExceptionHandlerExceptionResolver的支持

    @Slf4j
    @ControllerAdvice
    public class MyExceptionHandler {
    
        @ExceptionHandler(value = {NullPointerException.class})
        public Map<String, Object> handlerNEP(Throwable e) {
            log.info("null pointer exceptioin !");
            return Collections.singletonMap("msg", e.getMessage());
        }
    
        @ExceptionHandler(value = ArithmeticException.class)
        public Map<String, Object> handlerArithmetic(Throwable e) {
            log.info("handlerArithmetic !");
            return Collections.singletonMap("msg", e.getMessage());
        }
    }
    
  • @ResponseStatus + 自定义异常 底层使用ResponseStatusExceptionResolver ,把ResponseStatus的注解信息拿到,底层调用response.sendError(statusCode, resolvedReason);tomcat发送的/error

  • SpringMVC 底层的异常 如参数转换异常 DefaultHandlerExceptionResolver 来处理底层框架的异常 底层调用response.sendError(statusCode, resolvedReason);tomcat发送的/error

  • 自定义HandlerExceptionResolver

    @Slf4j
    public class MyCustomerHandlerExceptionResolver implements HandlerExceptionResolver, Ordered {
        @Override
        public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
            if(ex instanceof  MyRuntimeException){
                log.error("出现了异常! ", ex);
                try {
                    response.getWriter().write(ex.getMessage()+" [MyCustomerHandlerExceptionResolver!]");
                } catch (IOException e) {
                    e.printStackTrace();
                }
                // 可以自定义往哪里跳 一旦返回了mav 之后 就确定用这个HandlerExceptionResolver 处理
                 ModelAndView mav = new ModelAndView();
                return mav;
            }
            return null;
        }
    
        @Override
        public int getOrder() {
            return Ordered.HIGHEST_PRECEDENCE;
        }
    }
    
        /**
         * 扩展自己的resolver
         * @param resolvers
         */
        @Override
        public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
            resolvers.add(new MyCustomerHandlerExceptionResolver());
        }
    
  • ErrorViewResolver 自定义处理异常

    • response.sendError error请求转给controller
    • 异常没人能处理 Tomcat底层 response.sendError error请求会转给controller
    • basicErrorController 要去的页面地址是ErrorResolver

异常处理默认配置原理

ErrorMvcAutoConfiguration 类管理 SpringMVC的异常处理的默认配置 下面核心分析该配置类中做了哪些工作

添加了如下的组件

  • DefaultErrorAttributes 组件 默认的异常处理包含的属性信息 他实现了ErrorAttributes, HandlerExceptionResolver接口,说明他本身就是一个HandlerExceptionResolver SpringMVC发生错误之后默认的处理返回的数据项就定义在这个类中

    image-20210207150657696

image-20210207150729776

  • BasicErrorController 组件 本质就是一个Controller组件 用于接收/error 请求响应

    @Controller
    @RequestMapping("${server.error.path:${error.path:/error}}") // 获取默认的错误路径
    public class BasicErrorController extends AbstractErrorController{
        // 返回html错误页面
        	@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
    	public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
    		HttpStatus status = getStatus(request);
    		Map<String, Object> model = Collections
    				.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
    		response.setStatus(status.value());
            // 本质是遍历所有的ErrorViewResolver 找到一个合适返回MAV
            // 默认Spring MVC 只有一个DefaultErrorViewResolver 他里面实现了自定去找我们404.html 4xx.html 5xx.html的逻辑
    		ModelAndView modelAndView = resolveErrorView(request, response, status, model);
    		return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
    	}
    	// 返回 json 数据
    	@RequestMapping
    	public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
    		HttpStatus status = getStatus(request);
    		if (status == HttpStatus.NO_CONTENT) {
    			return new ResponseEntity<>(status);
    		}
    		Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
    		return new ResponseEntity<>(body, status);
    	}
    }
    
  • DefaultErrorViewResolver 默认的错误视图解析器 该解析器实现了 404.html 5xx.html等相关逻辑页面跳转的逻辑

    	@Override
    	public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
            // 创建一个MAV返回 传入HttpStatus的 value 如404
    		ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
    		if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
    			modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
    		}
    		return modelAndView;
    	}
    
    	private ModelAndView resolve(String viewName, Map<String, Object> model) {
            // 拼接 如error/404
    		String errorViewName = "error/" + viewName;
            // 查看是否有模板引擎
    		TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
    				this.applicationContext);
            // 如果项目中有模板引擎交给模板引擎处理
    		if (provider != null) {
    			return new ModelAndView(errorViewName, model);
    		}
            // 遍历寻找 解析视图的地址 errorViewName => /error/404
    		return resolveResource(errorViewName, model);
    	}
    
    	private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
            // 遍历项目/error/目录下的xxx.html文件 是否有跟HttpStatus匹配的 如果没有返回null 此时回到resolveErrorView 方法的第一行执行完毕 如果精准匹配到状态码的新页面 进入下一个逻辑 拿SERIES_VIEWS 再进resolve方法再来匹配
            /**
            ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
            // 继续那SERIES_VALUE -> 4xx 5xx
    		if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
    			modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
    		}
    		return modelAndView;
            */
    		for (String location : this.resourceProperties.getStaticLocations()) {
    			try {
    				Resource resource = this.applicationContext.getResource(location);
    				resource = resource.createRelative(viewName + ".html");
    				if (resource.exists()) {
    					return new ModelAndView(new HtmlResourceView(resource), model);
    				}
    			}
    			catch (Exception ex) {
    			}
    		}
    		return null;
    	}
    
    	private static final Map<Series, String> SERIES_VIEWS;
    
    	static {
    		Map<Series, String> views = new EnumMap<>(Series.class);
    		views.put(Series.CLIENT_ERROR, "4xx");
    		views.put(Series.SERVER_ERROR, "5xx");
    		SERIES_VIEWS = Collections.unmodifiableMap(views);
    	}
    
    

异常处理步骤

  • 当调用mv = ha.handle(processedRequest, response, mappedHandler.getHandler());方法之后如果出现异常会进去catch 代码块 将异常包装一下 再次进入processDispatchResult 转发解析逻辑

    image-20210207155531113

  • processDispatchResult 一上来就判断 有没有出现异常 如果出现了异常 调用processHandlerException方法进行处理

    	private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
    			@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
    			@Nullable Exception exception) throws Exception {
    
    		boolean errorView = false;
    
    		if (exception != null) {
                // 如果是ModelAndViewDefiningException 特殊处理
    			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);
    			}
    		}
            // ... 省略
    
  • processHandlerException 方法 循环遍历出所有的handlerExceptionResolver调用他的resolveException方法 哪个Resolver返回ModelAndView 就用这个MAV对象进行返回

    	protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
    			@Nullable Object handler, Exception ex) throws Exception {
    
    		// Success and error responses may use different content types
    		request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
    
    		// Check registered HandlerExceptionResolvers...
    		ModelAndView exMv = null;
    		if (this.handlerExceptionResolvers != null) {
    			for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
    				exMv = resolver.resolveException(request, response, handler, ex);
    				if (exMv != null) {
    					break;
    				}
    			}
    		}
    		if (exMv != null) {
    			if (exMv.isEmpty()) {
    				request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
    				return null;
    			}
    			// We might still need view name translation for a plain error model...
    			if (!exMv.hasView()) {
    				String defaultViewName = getDefaultViewName(request);
    				if (defaultViewName != null) {
    					exMv.setViewName(defaultViewName);
    				}
    			}
    			if (logger.isTraceEnabled()) {
    				logger.trace("Using resolved error view: " + exMv, ex);
    			}
    			else if (logger.isDebugEnabled()) {
    				logger.debug("Using resolved error view: " + exMv);
    			}
    			WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
    			return exMv;
    		}
    
    		throw ex;
    	}
    

    默认Spring MVC有4个 HandlerExceptionResolver

    image-20210207160830366\

  • 分别来看看他们4个HandlerExceptionResolver干了什么事情

  • DefaultErrorAttributes 基本啥也没干 就在 请求域中设置了一个属性 因为他返回为null进入下一个resolver

    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler,
          Exception ex) {
       storeErrorAttributes(request, ex);
       return null;
    }
    
    private void storeErrorAttributes(HttpServletRequest request, Exception ex) {
       request.setAttribute(ERROR_ATTRIBUTE, ex);
    }
    
  • 第二个Resolver 是HandlerExceptionResolverComposite 其实是一个组合的Resolver 他里面包含了3个 resolver ,默认我们自己定义的 HandlerExceptionResolver 通过WebMVCConfigurer的extendHandlerExceptionResolvers方法添加会被加到这个集合中,调用add方法传入 索引可以指定我们自定义resovler的顺序 image-20210207161116782

        @Override
        public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
            resolvers.add(0,new MyCustomerHandlerExceptionResolver());
        }
    

    这个组合ResolverComposite 底层也是遍历他所有的resolver

    	@Override
    	@Nullable
    	public ModelAndView resolveException(
    			HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
    
    		if (this.resolvers != null) {
                // 遍历他所有包装的resolver
    			for (HandlerExceptionResolver handlerExceptionResolver : this.resolvers) {
    				ModelAndView mav = handlerExceptionResolver.resolveException(request, response, handler, ex);
    				if (mav != null) {
    					return mav;
    				}
    			}
    		}
    		return null;
    	}
    
  • ExceptionHandlerExceptionResolver 是@ControllerAdvice + @ExceptionHanlder的底层原理核心逻辑在该类中的 doResolveHandlerMethodException方法中,该方法内部的getExceptionHandlerMethod是去找哪个@ExceptionHandler方法来处理

    	@Override
    	@Nullable
    	protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request,
    			HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) {
    		// 最核心的方法 找到哪个处理器来处理
    		ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
    		// 省略非核心逻辑...
    
    		ModelAndViewContainer mavContainer = new ModelAndViewContainer();
    		// MAVC 的处理逻辑
    		if (mavContainer.isRequestHandled()) {
    			return new ModelAndView();
    		}
    		else {
    			ModelMap model = mavContainer.getModel();
    			HttpStatus status = mavContainer.getStatus();
    			ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, status);
    			mav.setViewName(mavContainer.getViewName());
    			if (!mavContainer.isViewReference()) {
    				mav.setView((View) mavContainer.getView());
    			}
                // 如果是重定向页面如何处理
    			if (model instanceof RedirectAttributes) {
    				Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes();
    				RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
    			}
    			return mav;
    		}
    	}
    

    image-20210207163502226

  • 进一步研究getExceptionHandlerMethod 如何帮我们找到具体的HandlerMethod ,先去各种缓存中查询,最核心的在于 最后的遍历循环所有的ExceptionHandlerAdvice 这一个Map Key是ControllerAdviceBean相当于是标记了@ControllerAdvice的类 value是 ExceptionHandlerMethodResolver 相当于是包装了所有该类下标记了@ExceptionHandler注解的方法

    	@Nullable
    	protected ServletInvocableHandlerMethod getExceptionHandlerMethod(
    			@Nullable HandlerMethod handlerMethod, Exception exception) {
    
    		Class<?> handlerType = null;
    
    		// 省略各种从缓存中查询
    
    		for (Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) {
    			ControllerAdviceBean advice = entry.getKey();
    			if (advice.isApplicableToBeanType(handlerType)) {
    				ExceptionHandlerMethodResolver resolver = entry.getValue();
    				Method method = resolver.resolveMethod(exception);
    				if (method != null) {
    					return new ServletInvocableHandlerMethod(advice.resolveBean(), method);
    				}
    			}
    		}
    
    		return null;
    	}
    

    底层核心查找逻辑getMappedMethod方法判断异常类型是不是@ExceptionHandler中定义的异常image-20210207164543885

  • 以上逻辑走完只是找到了 应该用哪个@ExceptionHandler方法来处理 还没有真正执行,真正执行的逻辑在ExceptionHnadlerExceptionResolver后面的exceptionHandlerMethod.invokeAndHandle方法

    image-20210207170620653

    逻辑跟正常流程类似也是调用反射去执行@ExceptionHandler的方法,此时的returnValue 就是调用异常处理器的方法的返回值,后面逻辑一样调用 返回值处理器 进行返回值的处理

    image-20210207170749270

  • 此时才返回到了DispatcherServlet# processHandlerException方法拿到了ModelAndView,而且如果ModelAndView没有View会默认设置一个根据请求路径得到的默认值 继续走页面渲染逻辑

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值