SpringBoot - 异常处理原理

创建一个普通的SpringBoot过程,分别在浏览器和Postman请求一个内部出错的地址,例如:http://localhost:8080/test/error,然后我们看服务器给我们返回的信息:

@Controller
@RequestMapping(value = "/test")
public class TestController {


    @RequestMapping(value = "/error", produces = MediaType.TEXT_HTML_VALUE)
    public String testHTML(HttpServletRequest request, HttpServletResponse response) throws IOException {
        int i = 1 / 0;
        return "login";
    }


    @RequestMapping("/error")
    public String testError() {
        int i = 1 / 0;
        return "模拟异常!!!";
    }
}

看到这里结果你会不会有两个问题?

  1. 返回的信息从哪里来的?
  2. 为什么两个端会显示格式不一样?

然后开启debug模式(在application.properties文件中配置:debug=true,这样我们就能看到客户端请求的详细信息),照样在浏览器和Postman中请求刚刚那个内部出错的地址(http://localhost:8080/test/error),然后看控制的log信息输出

控制台log信息

================================浏览器请求==========================
2021-06-29 18:36:09.160 DEBUG 41152 --- [nio-8080-exec-5] o.s.web.servlet.DispatcherServlet        : GET "/test/error", parameters={}
2021-06-29 18:36:09.161 DEBUG 41152 --- [nio-8080-exec-5] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.booyue.tlh.controller.TestController#testHTML(HttpServletRequest, HttpServletResponse)
2021-06-29 18:36:09.161 DEBUG 41152 --- [nio-8080-exec-5] o.s.web.servlet.DispatcherServlet        : Failed to complete request: java.lang.ArithmeticException: / by zero
2021-06-29 18:36:09.161 ERROR 41152 --- [nio-8080-exec-5] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause

.....省略异常信息.....

2021-06-29 18:36:09.164 DEBUG 41152 --- [nio-8080-exec-5] o.s.web.servlet.DispatcherServlet        : "ERROR" dispatch for GET "/error", parameters={}
2021-06-29 18:36:09.164 DEBUG 41152 --- [nio-8080-exec-5] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#errorHtml(HttpServletRequest, HttpServletResponse)
2021-06-29 18:36:09.172 DEBUG 41152 --- [nio-8080-exec-5] o.s.w.s.v.ContentNegotiatingViewResolver : Selected 'text/html' given [text/html, text/html;q=0.8]
2021-06-29 18:36:09.173 DEBUG 41152 --- [nio-8080-exec-5] o.s.web.servlet.DispatcherServlet        : Exiting from "ERROR" dispatch, status 500



================================Psotman请求==========================
2021-06-29 18:34:24.854 DEBUG 41152 --- [nio-8080-exec-3] o.s.web.servlet.DispatcherServlet        : GET "/test/error", parameters={}
2021-06-29 18:34:24.855 DEBUG 41152 --- [nio-8080-exec-3] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.booyue.tlh.controller.TestController#testError()
2021-06-29 18:34:24.855 DEBUG 41152 --- [nio-8080-exec-3] o.s.web.servlet.DispatcherServlet        : Failed to complete request: java.lang.ArithmeticException: / by zero
2021-06-29 18:34:24.856 ERROR 41152 --- [nio-8080-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause

.....省略异常信息.....

2021-06-29 18:34:24.858 DEBUG 41152 --- [nio-8080-exec-3] o.s.web.servlet.DispatcherServlet        : "ERROR" dispatch for GET "/error", parameters={}
2021-06-29 18:34:24.858 DEBUG 41152 --- [nio-8080-exec-3] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
2021-06-29 18:34:24.869 DEBUG 41152 --- [nio-8080-exec-3] o.s.w.s.m.m.a.HttpEntityMethodProcessor  : Using 'application/json', given [*/*] and supported [application/json, application/*+json, application/json, application/*+json]
2021-06-29 18:34:24.870 DEBUG 41152 --- [nio-8080-exec-3] o.s.w.s.m.m.a.HttpEntityMethodProcessor  : Writing [{timestamp=Tue Jun 29 18:34:24 CST 2021, status=500, error=Internal Server Error, trace=java.lang.Ar (truncated)...]
2021-06-29 18:34:24.889 DEBUG 41152 --- [nio-8080-exec-3] o.s.web.servlet.DispatcherServlet        : Exiting from "ERROR" dispatch, status 500

看到log信息,我们大概理一下处理流程(我们拿浏览器的请求的做例子):

  1. 服务器收到一个内部出的请求 : /test/error
  2. 然后TestController的testHTML( )没有处理完成,出现了异常
  3.  dispatcherServlet中的service( ) 方法中将异常抛了出去
  4. 再次收到一个请求 : /error
  5. 然后匹配到处理器是:org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#errorHtml(HttpServletRequest, HttpServletResponse)
  6. 我们浏览器就看到了异常信息了

这里好像又多了两个问题了:

  1. /error 请求哪里来的?
  2. BasicErrorController从哪里来的?

原理

SpringBoot在启动的时候往IOC容器中添加了一些组件,其中也包括上面看到的BasicErrorController,具体的注入的位置在org.springframework.boot.autoconfigure.web.servlet.error包下面有一个自动配置类:ErrorMvcAutoConfiguration。该配置类主要往Spring的IOC容器中注入了DefaultErrorAttributes、BasicErrorController 、DefaultErrorViewResolver 这个三个类的实例。

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
// Load before the main WebMvcAutoConfiguration so that the error View is available
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties({ ServerProperties.class, WebMvcProperties.class })
public class ErrorMvcAutoConfiguration {

	private final ServerProperties serverProperties;

	public ErrorMvcAutoConfiguration(ServerProperties serverProperties) {
		this.serverProperties = serverProperties;
	}

	@Bean
	@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
	public DefaultErrorAttributes errorAttributes() {
		return new DefaultErrorAttributes();
	}

	@Bean
	@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
	public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
			ObjectProvider<ErrorViewResolver> errorViewResolvers) {
		return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
				errorViewResolvers.orderedStream().collect(Collectors.toList()));
	}

	@Configuration(proxyBeanMethods = false)
	@EnableConfigurationProperties({ org.springframework.boot.autoconfigure.web.ResourceProperties.class,
			WebProperties.class, WebMvcProperties.class })
	static class DefaultErrorViewResolverConfiguration {

		private final ApplicationContext applicationContext;

		private final Resources resources;

		DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext,
				org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties,
				WebProperties webProperties) {
			this.applicationContext = applicationContext;
			this.resources = webProperties.getResources().hasBeenCustomized() ? webProperties.getResources()
					: resourceProperties;
		}

		@Bean
		@ConditionalOnBean(DispatcherServlet.class)
		@ConditionalOnMissingBean(ErrorViewResolver.class)
		DefaultErrorViewResolver conventionErrorViewResolver() {
			return new DefaultErrorViewResolver(this.applicationContext, this.resources);
		}

	}

.....省略很多.....

}

BasicErrorController

        这里Controller就是用默认处理我们看到的 /error请求,还有在不同端返回不同格式的数据,其中errorHtml( )方法返回的是就是html格式的数据,error( )方法返回json格式的数据。SpringBoot在接受到前端的请求的时候,会看前端接受什么类型的数据和自己能生成什么类型的数据,然后做出一个最佳匹配,就是http协议中说的内容协商的实现。

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}") //默认处理 /error 请求
public class BasicErrorController extends AbstractErrorController {

	private final ErrorProperties errorProperties;

	/**
	 * Create a new {@link BasicErrorController} instance.
	 * @param errorAttributes the error attributes
	 * @param errorProperties configuration properties
	 */
	public BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties) {
		this(errorAttributes, errorProperties, Collections.emptyList());
	}

	/**
	 * Create a new {@link BasicErrorController} instance.
	 * @param errorAttributes the error attributes
	 * @param errorProperties configuration properties
	 * @param errorViewResolvers error view resolvers
	 */
	public BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties,
			List<ErrorViewResolver> errorViewResolvers) {
		super(errorAttributes, errorViewResolvers);
		Assert.notNull(errorProperties, "ErrorProperties must not be null");
		this.errorProperties = errorProperties;
	}

    //该方法返回的是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());
		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

        这个ViewResolver就是在BasicErrorController处理  /error 请求的时候返回一个ModelAndView的时候,由DefaultErrorViewResolver的resolveErrorView( )方法负责解析出具体视图的名字的。所有我们在自定义错页面的时候都会在templates的目录下面创建一个error的文件夹,然后放上4xx.html或者5xx.html。最后将这个ModelAndView交给BasicErrorController,BasicErrorController在交给视图解析器,最后出现在浏览器。

public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {

	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);
	}


    //根据返回的状态码来确定视图的名字
	@Override
	public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
		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;
	}

    //指定从/error目录下面获取对应页面的名字(所以我们子在定制错误页面的时候需要放在error文件夹下面的原因)
	private ModelAndView resolve(String viewName, Map<String, Object> model) {
		String errorViewName = "error/" + viewName;
		TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
				this.applicationContext);
		if (provider != null) {
			return new ModelAndView(errorViewName, model);
		}
		return resolveResource(errorViewName, model);
	}



}

DefaultErrorAttributes

        这里就定义了我们页面上看到的信息:exception、trace、message、errors、webRequest等等

@Order(Ordered.HIGHEST_PRECEDENCE)
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {

	private static final String ERROR_INTERNAL_ATTRIBUTE = DefaultErrorAttributes.class.getName() + ".ERROR";

	@Override
	public int getOrder() {
		return Ordered.HIGHEST_PRECEDENCE;
	}

	@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_INTERNAL_ATTRIBUTE, ex);
	}

	@Override
	public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
		Map<String, Object> errorAttributes = getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE));
		if (!options.isIncluded(Include.EXCEPTION)) {
			errorAttributes.remove("exception");
		}
		if (!options.isIncluded(Include.STACK_TRACE)) {
			errorAttributes.remove("trace");
		}
		if (!options.isIncluded(Include.MESSAGE) && errorAttributes.get("message") != null) {
			errorAttributes.remove("message");
		}
		if (!options.isIncluded(Include.BINDING_ERRORS)) {
			errorAttributes.remove("errors");
		}
		return errorAttributes;
	}

	private Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
		Map<String, Object> errorAttributes = new LinkedHashMap<>();
		errorAttributes.put("timestamp", new Date());
		addStatus(errorAttributes, webRequest);
		addErrorDetails(errorAttributes, webRequest, includeStackTrace);
		addPath(errorAttributes, webRequest);
		return errorAttributes;
	}

.....省略很多.....

}

/error 请求从哪里来的?

        这个/error请求其实是tomcat抛给SpringBoot的。当我们请求一个内部出错的请求或者请求的目标方法发生异常的时候,SpringBoot默认是不处理的直接抛给底层的Servlet容器,然后Servlet就会给我们发送一个/error的请求。

        我们来debug下DispatcherServlet的doDispatch( )-> processDispatchResult ( ) -> processHandlerException ( ) 方法,就会发现在DispatcherServlet的processHandlerException( )方法中有获取系统全部异常处理器然后挨个遍历看能否处理当前异常的方法,如果能处理的话就返回一个ModelAndView 。然而这里并没有处理,直接抛给给底层的Servlet容器,然后Servlet就抛出一个 /error的请求,在然后就被我们的BasicErrorController处理了。如果我们让processHandlerException( )方法中的处理异常的HandlerExceptionResolver实现处理 ,那么就SpringBoot就不会把异常抛给底层的Servlet容器了,我们就能自定义异常处理器了。

@Nullable
	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;
	}

我们debug发现系统支持其实有三类的异常错误解析器

我们平时定制的异常解析器就是符合了第一种ExceptionHandlerExceptionResolver规则:ControllerAdvice注解和ExceptionHandler注解

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public String handler() {
        return "这里是全局异常捕获!!!";
    }
}

上面就是大概的SpringBoot的异常原理了。

Guess you like

Origin blog.csdn.net/qq_27062249/article/details/118316320