一、SpringBoot 默认的错误处理机制

  SpringBoot 根据浏览器的请求头返回相应的信息:

  • 浏览器,返回一个默认的错误页面。
  • 如果是其他客户端,默认响应一个 JSON 数据。

  SpringBoot 判断是浏览器还是客户端的方法:

image.png

二、SpringBoot 错误处理机制原理

ErrorMvcAutoConfiguration 类是 SpringBoot 自动配置的错误处理方式,它给容器添加了四大组件:

  • DefaultErrorAttributes
  • BasicErrorController
  • ErrorPageCustomizer
  • DefaultErrorViewResolver

  当系统出现 4xx 或者 5xx 之类的错误,ErrorPageCustomizer 就会生效(定制错误的响应规则)。

		@Override
		public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
			ErrorPage errorPage = new ErrorPage(
					this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
			errorPageRegistry.addErrorPages(errorPage);
		}

  响应的接口为 /error 。

	/**
	 * Path of the error controller.
	 */
	@Value("${error.path:/error}")
	private String path = "/error";

  接着就会请求 /error ,会被 BasicErrorController 处理。

@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, isIncludeStackTrace(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, isIncludeStackTrace(request, MediaType.ALL));
		return new ResponseEntity<>(body, status);
	}

  响应页面:去哪个页面由 ErrorViewResolver 接口的实现类 DefaultErrorViewResolver 决定的。

   	protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
   			Map<String, Object> model) {
		// 从所有的 ErrorViewResolver  得到 modelAndView 
   		for (ErrorViewResolver resolver : this.errorViewResolvers) {
			// 执行 DefaultErrorViewResolver 类的 resolveErrorView 方法
   			ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
   			if (modelAndView != null) {
   				return modelAndView;
   			}
   		}
   		return null;
   	}

  首先查看模板引擎是否能解析到指定状态码的页面,如果有则用模板引擎解析的页面,如果没有则去静态页面下查找。

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) {
		// 首先对状态码进行精准匹配,例如404
		ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
		if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
			// 如果精准匹配的状态码页面不存在,则进行模糊匹配。例如 4xx。
			modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
		}
		return modelAndView;
	}

	private ModelAndView resolve(String viewName, Map<String, Object> model) {
		// 默认SpringBoot会去找一个页面。例如 error/404
		String errorViewName = "error/" + viewName;
		// 模板引擎可以解析这个页面地址就用模板引擎解析
		TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
				this.applicationContext);
		if (provider != null) {
			// 模板引擎可用的情况下返回 errorViewName 指定的视图地址
			return new ModelAndView(errorViewName, model);
		}
		// 模板引擎不可用,就在静态资源文件夹下找 errorViewName 对应的页面,例如 error/404.html
		return resolveResource(errorViewName, model);
	}

	private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
		// 去静态资源下查找 404.html
 		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;
	}

DefaultErrorAttributes 为页面提供错误信息:

  • timestamp:时间戳
  • status:状态码
  • error:错误提示
  • exception:异常对象
  • message:异常消息
  • errors:JSR303 数据校验的错误都在这里

 在错误页面中可以通过 [[${timestamp}]] 获取指定信息的值(仅在模板引擎下有效)。

	@Override
	public 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;
	}

三、如何定制错误响应

1、如何定制错误页面

  根据上面的原理分析,我们可知:

  • 有模板引擎的情况下,将错误页面命名为 错误状态码.html 放在模板引擎文件夹里面的 error 文件夹下,发生此状态码的错误就会来到 对应的页面。
  • 没有模板引擎(模板引擎找不到这个错误页面),静态资源文件夹下找。
  • 以上都没有错误页面,就是默认来到 SpringBoot 默认的错误提示页面。

  我们可以使用 4xx 或 5xx 作为错误页面的文件名也匹配这种类型的所有错误,精确优先(优先寻找精准的状态码.html)。

2、如何定制错误的 JSON 数据

方法一:

  在 @ExceptionHandler 注解中捕获指定异常,返回特定的 JSON 数据,但是没有自适应的效果,无论是客户端还是浏览器返回的都是 JSON 数据,很显然这并不是我们想要的结果。

@ControllerAdvice
public class MyExceptionHandler {

    @ResponseBody
    @ExceptionHandler(Exception.class)
    public Map<String ,Object> handlerException(Exception e){
        Map<String,Object> map=new HashMap<>();
        map.put("code",500);
        map.put("message",e.getMessage());
        return map;
    }
}

方法二:

  将请求转发到 /error 可以实现自适应响应效果处理,但是我们的数据并没有携带出去。

@ControllerAdvice
public class MyExceptionHandler {

    @ExceptionHandler(Exception.class)
    public String handlerException(Exception e, HttpServletRequest request) {
        Map<String, Object> map = new HashMap<>();
        // 源码中获取装状态码的代码
        // Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code")
        // 设置状态码,否则状态码默认为200,不会跳转到错误页面
        request.setAttribute("javax.servlet.error.status_code",500);
        map.put("code", 500);
        map.put("message", e.getMessage());
        return "forward:/error";
    }
}
数据没有携带出去的原因

  当系统出现错误后,会来到 /error 请求,会被 BasicErrorController 处理。响应出去可以获取的数据是由 getErrorAttributes 得到的(该方法来自 ErrorAttributes 接口)。

  现在我们来看 ErrorMvcAutoConfiguration 类中的 errorAttributes 方法。当容器中没有 ErrorAttributes 时,会向容器中添加一个 DefaultErrorAttributes,所以 ErrorAttributes 接口的默认实现是 DefaultErrorAttributes

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

  DefaultErrorAttributes 中的 getErrorAttributes 只添加了基本的错误信息,并没有添加我们自定义的错误信息。

	@Override
	public 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;
	}
将我们的定制数据携带出去

  经过上面的分析我们有两种解决方案:

  1. 完全来编写一个 ErrorController 的实现类或者编写 AbstractErrorController 的子类放在容器中,重新定义 /error 接口的映射规则(过于麻烦)。
  2. 页面上能用的数据或者是 JSON 返回的数据都是通过 getErrorAttributes 得到的,容器中 DefaultErrorAttributes.getErrorAttributes() 是默认数据处理的方式。可以继承 DefaultErrorAttributes 类重写 getErrorAttributes 方法,我们自己定义数据的处理方式。

  将数据封装在 HttpServletRequest 中。

@ControllerAdvice
public class MyExceptionHandler {

    @ExceptionHandler(Exception.class)
    public String handlerException(Exception e, HttpServletRequest request) {
        Map<String, Object> map = new HashMap<>();
        // 源码中获取装状态码的代码
        // Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code")
        // 设置状态码,否则状态码默认为200,不会跳转到错误页面
        request.setAttribute("javax.servlet.error.status_code",500);
        map.put("code", 500);
        map.put("message", e.getMessage());
        request.setAttribute("ext",map);
        return "forward:/error";
    }
}

  重写 getErrorAttributes 方法,使用 webRequest 获取异常处理器封装的数据,将其添加到 map 集合中即可。

@Component
public class MyErrorAttributes extends DefaultErrorAttributes {

    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        Map<String, Object> map=super.getErrorAttributes(webRequest,includeStackTrace);
        // 在所有页面中加入以下数据
        map.put("name","Yi-Xing");
        // 我们的异常处理器携带的数据
        Map<String, Object> ext=(Map<String, Object>)webRequest.getAttribute("ext", RequestAttributes.SCOPE_REQUEST);
        map.put("ext",ext);
        return map;
    }
}

标题:SpringBoot 异常处理机制
作者:Yi-Xing
地址:http://zyxwmj.top/articles/2020/04/03/1585907905090.html
博客中若有不恰当的地方,请您一定要告诉我。前路崎岖,望我们可以互相帮助,并肩前行!