SpringBoot异常处理原理&定制错误页面/数据

一 、原理

可以参照ErrorMvcAutoConfiguration类,错误处理的自动配置类:
主要是给容器添加了以下组件:

ErrorPageCustomizer

@Override
// 注册错误页面
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
	// 主要是这里的getPath方法,返回了下面/error的路径
	ErrorPage errorPage = new ErrorPage(
			this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
	errorPageRegistry.addErrorPages(errorPage);
}
// 系统出现错误后来到error请求进行处理(就是web.xml注册的错误页面规则)
@Value("${error.path:/error}")
private String path = "/error";

BasicErrorController(处理默认/error请求)

浏览器和客户端出现异常返回的数据是不一样的,浏览器会返回错误的页面,客户端会返回一个json数据
浏览器发送请求中请求头Accept的数据为 text/html客户端(PostMan)请求头中Accept的数据为 /

Accept : 位于请求头中,表明客户端想要接受的数据类型
Content-Type : 位于请求体中,表明客户端发送的数据类型
// 标注是一个Controller
@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);
		// getErrorAttributes 方法内部会用到 DefaultErrorAttributes
		// 主要就是获取响应出去的数据
		Map<String, Object> model = Collections
				.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
		response.setStatus(status.value());
		// 去那个页面作为错误页面:包含页面地址和页面内容
		// 方法内部会遍历所有的异常视图解析器
		ModelAndView modelAndView = resolveErrorView(request, response, status, model);
		// 找不到视图解析器时,就返回SpringBoot默认的页面
		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);
		}
		// getErrorAttributes主要就是获取响应出去的数据
		Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
		return new ResponseEntity<>(body, status);
	}
// 遍历所有的ErrorViewResolver异常视图解析器
// ErrorViewResolver其实就是 DefaultErrorViewResolver
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
			Map<String, Object> model) {
		for (ErrorViewResolver resolver : this.errorViewResolvers) {
			// 这里调用每个 DefaultErrorViewResolver 的resolveErrorView方法
			ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
			if (modelAndView != null) {
				return modelAndView;
			}
		}
		return null;
	}

DefaultErrorViewResolver

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

private ModelAndView resolve(String viewName, Map<String, Object> model) {
	// 默认Springboot可以去找到某个页面
	// 例如 errpr/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);
}

DefaultErrorAttributes

// 帮我们在页面共享信息
@Override
@Deprecated
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
	// 向错误页面存放错误数据 errorAttributes
	Map<String, Object> errorAttributes = new LinkedHashMap<>();
	// timestamp 时间戳
	errorAttributes.put("timestamp", new Date());
	// 方法内部添加状态码和错误提示
	addStatus(errorAttributes, webRequest);
	// 添加异常对象exception 和异常消息message ,以及 jsr303数据校验的错误errors
	addErrorDetails(errorAttributes, webRequest, includeStackTrace);
	// 
	addPath(errorAttributes, webRequest);
	return errorAttributes;
}

方法中放到页面的数据都可以在页面中使用,例如使用模版引擎可以直接获取到这些数据

thymeleaf模版引擎:
[[${status}]]
[[${timestamp}]]
...
timestamp : 时间戳
status :  状态码
error  : 错误消息
excetyipn : 异常对象
message : 异常消息
errors : jsr303数据校验的错误

二、步骤

  • 一旦系统出现错误后: ErrorPageCustomizer 就会生效(定制错误的相应机制),然后来到/error请求,被BasicErrorController处理,根据不同的客户端,返回不同的数据(页面或者json数据)
  • 响应出去的数据都是由getErrorAttributes得到的(是AbstractErrorController规定的方法)

三、定制错误页面(前后端不分离)

  • 有模版引擎的情况下会去找: error/状态码
  1. templates目录下创建error目录,在error目录下创建错误页面(以错误状态码命名的html,例如 404.html),发生此状态码的错误时就会来到对应的页面
  2. 可以使用 4xx.html 和 5xx.html 作为错误页面的名字来匹配这种类型的所有错误(精准优先与这种方式)
  • 没有模版引擎(模版引擎找不到这个错误页面)的情况,静态资源文件夹下找相应的页面
    只需在静态资源文件夹下创建相关的页面即可(这种情况是无法在后台渲染的,只能返回静态的页面)
  • 以上都没有错误页面的情况下,就是SpringBoot默认的错误页面

四、定制错误数据(后端统一返回json,没有页面)

/**
 * 异常处理器
 */
@ControllerAdvice
public class MyExceptionHandler {
    // 出现指定异常后,返回设定的数据
    @ResponseBody
    // 捕获 UserNotExistException 自定义的异常
    @ExceptionHandler(UserNotExistException.class)
    // 这里的返回值可以定义一个通用的类来封装
    public Map<String,Object> userNotExist(){
        Map<String,Object> result = new Hashtable<>();
        result.put("code","userNotExist");
        result.put("message","用户不存在");
        return result;
    }
}
  • 上面的方法,设定后不管是浏览器还是客户端,当Controller抛出指定异常后,返回的都是我们设定的数据(如果没有捕获这个异常则会出现500页面)
  • 如果用户手动输入一些不存在的url(正常返回404页面),SpringBoot中有它自己的一套404异常的返回数据格式,例如
{
    "timestamp": "2018-09-26T17:03:41.161+0800",
    "status": 404,
    "error": "Not Found",
    "message": "No message available",
    "path": "/process-instance/overview1"
}

但是这样的结果并不是我们想要的,因此需要我们来设定修改返回值:
(04错误是不经过Controller的,所以使用@ControllerAdvice或@RestControllerAdvice无法获取到404错误) (当前端的请求url不存在的时候404,会抛出NoHandlerFoundException异常)

  1. 捕获NoHandlerFoundException异常
@ResponseBody
@ExceptionHandler(NoHandlerFoundException.class)
public Map<String,Object> noHandlerFound(){
    Map<String,Object> result = new Hashtable<>();
    result.put("code","404");
    result.put("message","请求错误");
    return result;
}
  1. 修改配置文件
# true时 当url不存在时抛出NoHandlerFoundException
spring.mvc.throw-exception-if-no-handler-found=true
#不要为我们工程中的资源文件建立映射
spring.resources.add-mappings=false

这种方式不太适用实际开发,例如如果项目使用swagger时,访问/swagger-ui.html会出现404异常