七、Spring Boot 错误处理原理 & 定制错误页面

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Java_Road_Far/article/details/82914170

【1】错误默认处理机制

1)浏览器,返回一个默认的错误页面
在这里插入图片描述
请求头:
在这里插入图片描述

2) 其他客户端访问,默认响应 JSON 数据
在这里插入图片描述
请求头:
在这里插入图片描述

为什么会产生这样的默认效果?

原理:可以参照 ErrorMvcAutoConfiguration;错误处理的自动配置;

ErrorMvcAutoConfiguration 给容器中添加了以下组件

1)、DefaultErrorAttributes

@Bean
//@ConditionalOnMissingBean:  容器中没有  ErrorAttributes 组件 时,会添加 DefaultErrorAttributes 组件
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
	return new DefaultErrorAttributes();
}

ErrorMvcAutoConfiguration 源码:

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

	private static final String ERROR_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_ATTRIBUTE, ex);
	}
	
	@Override
	public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes,
			boolean includeStackTrace) {
		//页面能获取的信息
		Map<String, Object> errorAttributes = new LinkedHashMap<String, Object>();
		errorAttributes.put("timestamp", new Date());
		addStatus(errorAttributes, requestAttributes);
		addErrorDetails(errorAttributes, requestAttributes, includeStackTrace);
		addPath(errorAttributes, requestAttributes);
		return errorAttributes;
	}

	private void addStatus(Map<String, Object> errorAttributes,
			RequestAttributes requestAttributes) {
		Integer status = getAttribute(requestAttributes,
				"javax.servlet.error.status_code");
		if (status == null) {
			errorAttributes.put("status", 999);
			errorAttributes.put("error", "None");
			return;
		}
		errorAttributes.put("status", status);
		try {
			errorAttributes.put("error", HttpStatus.valueOf(status).getReasonPhrase());
		}
		catch (Exception ex) {
			// Unable to obtain a reason
			errorAttributes.put("error", "Http Status " + status);
		}
	}

	private void addErrorDetails(Map<String, Object> errorAttributes,
			RequestAttributes requestAttributes, boolean includeStackTrace) {
		Throwable error = getError(requestAttributes);
		if (error != null) {
			while (error instanceof ServletException && error.getCause() != null) {
				error = ((ServletException) error).getCause();
			}
			errorAttributes.put("exception", error.getClass().getName());
			addErrorMessage(errorAttributes, error);
			if (includeStackTrace) {
				addStackTrace(errorAttributes, error);
			}
		}
		Object message = getAttribute(requestAttributes, "javax.servlet.error.message");
		if ((!StringUtils.isEmpty(message) || errorAttributes.get("message") == null)
				&& !(error instanceof BindingResult)) {
			errorAttributes.put("message",
					StringUtils.isEmpty(message) ? "No message available" : message);
		}
	}

	private void addErrorMessage(Map<String, Object> errorAttributes, Throwable error) {
		BindingResult result = extractBindingResult(error);
		if (result == null) {
			errorAttributes.put("message", error.getMessage());
			return;
		}
		if (result.getErrorCount() > 0) {
			errorAttributes.put("errors", result.getAllErrors());
			errorAttributes.put("message",
					"Validation failed for object='" + result.getObjectName()
							+ "'. Error count: " + result.getErrorCount());
		}
		else {
			errorAttributes.put("message", "No errors");
		}
	}

	private BindingResult extractBindingResult(Throwable error) {
		if (error instanceof BindingResult) {
			return (BindingResult) error;
		}
		if (error instanceof MethodArgumentNotValidException) {
			return ((MethodArgumentNotValidException) error).getBindingResult();
		}
		return null;
	}

	private void addStackTrace(Map<String, Object> errorAttributes, Throwable error) {
		StringWriter stackTrace = new StringWriter();
		error.printStackTrace(new PrintWriter(stackTrace));
		stackTrace.flush();
		errorAttributes.put("trace", stackTrace.toString());
	}

	private void addPath(Map<String, Object> errorAttributes,
			RequestAttributes requestAttributes) {
		String path = getAttribute(requestAttributes, "javax.servlet.error.request_uri");
		if (path != null) {
			errorAttributes.put("path", path);
		}
	}

	@Override
	public Throwable getError(RequestAttributes requestAttributes) {
		Throwable exception = getAttribute(requestAttributes, ERROR_ATTRIBUTE);
		if (exception == null) {
			exception = getAttribute(requestAttributes, "javax.servlet.error.exception");
		}
		return exception;
	}

	@SuppressWarnings("unchecked")
	private <T> T getAttribute(RequestAttributes requestAttributes, String name) {
		return (T) requestAttributes.getAttribute(name, RequestAttributes.SCOPE_REQUEST);
	}

}

2)、BasicErrorController: 处理默认/error请求

@Controller
@RequestMapping("${server.error.path:${error.path:/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.<ErrorViewResolver>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;
	}

	@Override
	public String getErrorPath() {
		return this.errorProperties.getPath();
	}

	@RequestMapping(produces = "text/html")//产生html类型的数据;浏览器发送的请求来到这个方法处理
	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);


		//此处为 ① resolveErrorView(request, response, status, model); 里执行的代码
		protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
			//拿到所有的 异常视图解析器 得到 ModelAndView
			for (ErrorViewResolver resolver : this.errorViewResolvers) {
				ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
				if (modelAndView != null) {
					return modelAndView;
				}
			}
			return null;
		}
	}

	@RequestMapping
	@ResponseBody  //产生json数据,其他客户端来到这个方法处理;
	public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
		Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
		HttpStatus status = getStatus(request);
		return new ResponseEntity<Map<String, Object>>(body, status);
	}

	/**
	 * Determine if the stacktrace attribute should be included.
	 * @param request the source request
	 * @param produces the media type produced (or {@code MediaType.ALL})
	 * @return if the stacktrace attribute should be included
	 */
	protected boolean isIncludeStackTrace(HttpServletRequest request,
			MediaType produces) {
		IncludeStacktrace include = getErrorProperties().getIncludeStacktrace();
		if (include == IncludeStacktrace.ALWAYS) {
			return true;
		}
		if (include == IncludeStacktrace.ON_TRACE_PARAM) {
			return getTraceParameter(request);
		}
		return false;
	}

	/**
	 * Provide access to the error properties.
	 * @return the error properties
	 */
	protected ErrorProperties getErrorProperties() {
		return this.errorProperties;
	}

}

3)、ErrorPageCustomizer: 主要是注册错误页面的相应规则

private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {

	private final ServerProperties properties;

	protected ErrorPageCustomizer(ServerProperties properties) {
		this.properties = properties;
	}

	@Override
	public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
		//发生错误以后, 来到 error 请求进行处理 --> this.properties.getError().getPath() == "/error"
		ErrorPage errorPage = new ErrorPage(this.properties.getServletPrefix() + this.properties.getError().getPath());
		errorPageRegistry.addErrorPages(errorPage);
	}

	@Override
	public int getOrder() {
		return 0;
	}

}

在这里插入图片描述

4)、DefaultErrorViewResolver

public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {

	private static final Map<Series, String> SERIES_VIEWS;

	static {
		Map<Series, String> views = new HashMap<Series, String>();
		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), 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) {
		//Spring Boot  可以找到 error/404.html
		String errorViewName = "error/" + viewName;
		//如果模板引擎可以解析地址就用模板引擎解析
		TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext);
		if (provider != null) {
			//模板引擎可用的情况下返回到 errorViewName 指定的视图
			return new ModelAndView(errorViewName, model);
		}
		//模板引擎不可用,在静态资源文件夹下找 errorViewName 对应的页面
		return resolveResource(errorViewName, model);
	}

步骤:
  一但系统出现4xx或者5xx之类的错误;ErrorPageCustomizer就会生效(定制错误的响应规则);就会来到/error请求;就会被BasicErrorController处理;

【2】定制错误响应

1.如何定制错误页面
  1)、有模板引擎的情况下,  error/404.html 【将错误页面命名为 错误状态码.html 放在模板引擎文件夹里面的error文件夹下】,发生此状态码的错误就会来到 对应的页面;
  路径图:
  在这里插入图片描述
  效果图
   在这里插入图片描述

我们可以使用4xx和5xx作为错误页面的文件名来匹配这种类型的所有错误,精确优先(优先寻找精确的状态码.html);
页面能获取的信息;
timestamp时间戳
status状态码
error错误提示
exception异常对象
message异常消息
errorsJSR303数据校验的错误都在这里
在这里插入图片描述
在这里插入图片描述
  2)、没有模板引擎(模板引擎找不到这个错误页面),静态资源文件夹下找;
  在这里插入图片描述
在这里插入图片描述
  3)、以上都没有错误页面,就是默认来到SpringBoot默认的错误提示页面;
Spring Boot 默认提示页面源码:

@Configuration
@ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
@Conditional(ErrorTemplateMissingCondition.class)
protected static class WhitelabelErrorViewConfiguration {

    private final SpelView defaultErrorView = new SpelView(
            "<html><body><h1>Whitelabel Error Page</h1>"
                    + "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>"
                    + "<div id='created'>${timestamp}</div>"
                    + "<div>There was an unexpected error (type=${error}, status=${status}).</div>"
                    + "<div>${message}</div></body></html>");

    @Bean(name = "error")
    @ConditionalOnMissingBean(name = "error")
    public View defaultErrorView() {
        return this.defaultErrorView;
    }

    // If the user adds @EnableWebMvc then the bean name view resolver from
    // WebMvcAutoConfiguration disappears, so add it back in to avoid disappointment.
    @Bean
    @ConditionalOnMissingBean(BeanNameViewResolver.class)
    public BeanNameViewResolver beanNameViewResolver() {
        BeanNameViewResolver resolver = new BeanNameViewResolver();
        resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
        return resolver;
    }
}

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

第一种,使用SpringMVC的异常处理器

@ControllerAdvice
public class MyExceptionHandler {
    @ResponseBody
    @ExceptionHandler(UserNotExistException.class)
    public Map<String,Object> handlerException(Exception e, HttpServletRequest request) {
        Map<String,Object> map = new HashMap<>();
        map.put("code","user.notexist");
        map.put("message","用户出错啦");
        return map;
    }
 }

这样无论浏览器还是客户端返回的都是JSON!
在这里插入图片描述

第二种,转发到/error请求进行自适应效果处理

  @ExceptionHandler(UserNotExistException.class)
  public String handleException(Exception e, HttpServletRequest request){
       Map<String,Object> map = new HashMap<>();
       //传入我们自己的错误状态码  4xx 5xx
       /**
        * Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
        */
       request.setAttribute("javax.servlet.error.status_code",500);
       map.put("code","user.notexist");
       map.put("message","用户出错啦");
       //转发到/error
       return "forward:/error";
   }

在这里插入图片描述
但是此时没有将自定义 code message传过去!

第三种,注册MyErrorAttributes继承自DefaultErrorAttributes(推荐)

BasicErrorController 中对 /error 请求有两种方式,两种方式的错误数据都是通过DefaultErrorAttributes.getErrorAttributes()方法获取,如下所示:

//BasicErrorController 类部分代码
@RequestMapping(produces = "text/html")
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
				request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
}
@RequestMapping
@ResponseBody
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request,
				isIncludeStackTrace(request, MediaType.ALL));
}

//DefaultErrorAttributes 类部分代码
@Override
public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {
     Map<String, Object> errorAttributes = new LinkedHashMap<String, Object>();
     errorAttributes.put("timestamp", new Date());
     addStatus(errorAttributes, requestAttributes);
     addErrorDetails(errorAttributes, requestAttributes, includeStackTrace);
     addPath(errorAttributes, requestAttributes);
     return errorAttributes;
 }

我们可以编写一个MyErrorAttributes继承自DefaultErrorAttributes重写其getErrorAttributes方法将我们的错误数据添加进去。

示例如下:

//给容器中加入我们自己定义的ErrorAttributes
@Component
public class MyErrorAttributes extends DefaultErrorAttributes {

    //返回值的map就是页面和json能获取的所有字段
    @Override
    public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {
        //DefaultErrorAttributes的错误数据
        Map<String, Object> map = super.getErrorAttributes(requestAttributes, includeStackTrace);
        map.put("company","SpringBoot");
        //① 我们的异常处理器携带的数据
        Map<String,Object> ext = (Map<String, Object>) requestAttributes.getAttribute("ext", 0);
        map.put("ext",ext);
        return map;
    }
}


①我们的异常处理器携带的数据
@ExceptionHandler(UserNotExistException.class)
public String handleException(Exception e, HttpServletRequest request){
     Map<String,Object> map = new HashMap<>();
     //传入我们自己的错误状态码  4xx 5xx
     /**
      * Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
      */
     request.setAttribute("javax.servlet.error.status_code",500);
     map.put("code","user.notexist");
     map.put("message","用户出错啦");
    //将自定义错误数据放入request中
     request.setAttribute("ext",map);
     //转发到/error
     return "forward:/error";
 }

5xx.html页面代码如下:
在这里插入图片描述

浏览器测试效果如下:
在这里插入图片描述

客户端测试效果如下:
在这里插入图片描述

展开阅读全文

没有更多推荐了,返回首页