默认规则
Spring Boot 提供了一个默认的 /error
路径来处理所有错误的映射。但是,同样的错误在不同的客户端下显示的效果是完全不一样的。
比如,在做前后端分离的应用程序时,使用 Postman、OkHttp 等这类工具。通过它来发送一个不存在的路径时,它返回的错误信息是 JSON 格式的。
如果用的是浏览器来访问一个不正确的页面,那么它是这样的。它将会返回一个
自定义错误页
自定义错误页非常的简单,将错误页放到项目路径 /resources/templates/error
或者放到 /resources/public/error
这个文件夹下即可。
就拿之前的模拟的后台系统来说吧,有一个 404 错误页和 500 的错误页,我们将其放进来,放到 templates/error
这个目录下吧。放完之后,如下所示。且文件名必须是与错误码对应的 html 文件,如果想用这个 html 文件代表一类错误码,比如 5xx.html(5 开头的代表着服务器端的错误)
注意,放到
templates/error
错误页时,由于 templates 是加载模板引擎,放入 html 文件时,需要在 html 标签对中声明一下模板引擎xmlns:th="http://www.thymeleaf.org"
测试 404 页面
我们访问一个路径不存在的页面,运行效果如下。
测试 500 页面
我们在登录页面故意引发一个算数异常。
@GetMapping({"/login", "/"})
public String login() {
int k = 2 / 0;
return "login";
}
然后我在运行的时候,运行在 Tomcat 中出现的错误页面,居然是在打成 war 包时出现的错误页面。能够在 Spring Boot 中做出现这样的页面也真的是蛮幸运的。
这总要找找原因吧,这个问题后面猜测估计是拦截器写的有问题。以下是我原来的拦截器。
@Configuration
public class LoginStatusConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/", "/css/**", "/fonts/**", "/images/**",
"/js/**", "/login", "/login_do");
}
}
我把拦截器的写法改成这样,也就是排除了将根目录下请求路径(不包括子路径),也就是 /*
@Configuration
public class LoginStatusConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/*", "/css/**", "/fonts/**", "/images/**",
"/js/**", "/login", "/login_do");
}
}
现在我们重新运行一下,看到下面的效果就知道了。
异常处理的原理
异常处理中,有一个自动配置类,叫做 ErrorMvcAutoConfiguration
,它帮助我们配置好了异常处理的规则。同时有一些属性是从 ServerProperties
, WebMvcProperties
和 ResourceProperties
中获取来的。
@EnableConfigurationProperties({ ServerProperties.class, ResourceProperties.class, WebMvcProperties.class })
public class ErrorMvcAutoConfiguration {
private final ServerProperties serverProperties;
public ErrorMvcAutoConfiguration(ServerProperties serverProperties) {
this.serverProperties = serverProperties;
}
}
自动配置原理
在该自动配置类中,注册了如下组件:
- DefaultErrorAttributes
- BasicErrorController
BasicErrorController
先来谈谈 BasicErrorController
这个类,Controller 作为控制层的类,当然就是作为请求处理的类了。以下是它的部分源码。
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
private final ErrorProperties errorProperties;
}
其中,server.error.path
可以在配置文件中手动配置错误的映射路径,如果这个没有去配置,那么它会去找error.path
这个配置项是否已经配置,如果还是没有,则使用默认的错误处理请求路径 /error
,这个错误处理请求路径在页面中的 whitelabel 中可以看到。
This application has no explicit mapping for /error, so you are seeing this as a fallback.
然后,该类中还有这两个地方标注了 RequestMapping
注解,一个利用是浏览器是访问错误路径时所返回的内容,另外一个就是使用 Postman 等这种工具访问错误页时,返回的是 JSON 格式的错误信息。
/**
* 这个方法是浏览器访问错误页时,返回 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);
}
/**
* 这个方法是用 Postman 等请求工具试,通过 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);
}
默认的错误页是在自动配置类中,有一个 StaticView 的内部类,这个内部类中里面有默认的错误页。
/**
* Simple {@link View} implementation that writes a default HTML error page.
*/
private static class StaticView implements View {
private static final MediaType TEXT_HTML_UTF8 = new MediaType("text", "html", StandardCharsets.UTF_8);
private static final Log logger = LogFactory.getLog(StaticView.class);
@Override
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
throws Exception {
if (response.isCommitted()) {
String message = getMessage(model);
logger.error(message);
return;
}
response.setContentType(TEXT_HTML_UTF8.toString());
StringBuilder builder = new StringBuilder();
Object timestamp = model.get("timestamp"); // 拿到时间戳
Object message = model.get("message"); // 拿到错误信息
Object trace = model.get("trace"); // 拿到堆栈信息
if (response.getContentType() == null) {
response.setContentType(getContentType());
}
// 错误页的 html 字符串拼接部分
builder.append("<html><body><h1>Whitelabel Error Page</h1>").append(
"<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>")
.append("<div id='created'>").append(timestamp).append("</div>")
.append("<div>There was an unexpected error (type=").append(htmlEscape(model.get("error")))
.append(", status=").append(htmlEscape(model.get("status"))).append(").</div>");
// 如果有错误信息,则展示出来
if (message != null) {
builder.append("<div>").append(htmlEscape(message)).append("</div>");
}
// 如果错误中包含着堆栈的信息,则将堆栈信息展示出来
if (trace != null) {
builder.append("<div style='white-space:pre-wrap;'>").append(htmlEscape(trace)).append("</div>");
}
builder.append("</body></html>");
response.getWriter().append(builder.toString());
}
private String htmlEscape(Object input) {
return (input != null) ? HtmlUtils.htmlEscape(input.toString()) : null;
}
private String getMessage(Map<String, ?> model) {
Object path = model.get("path");
String message = "Cannot render error page for request [" + path + "]";
if (model.get("message") != null) {
message += " and exception [" + model.get("message") + "]";
}
message += " as the response has already been committed.";
message += " As a result, the response may have the wrong status code.";
return message;
}
@Override
public String getContentType() {
return "text/html";
}
}
DefaultErrorAttributes
就是使用 Postman 这个工具类的时候,返回的错误信息是 JSON 格式的。错误信息都放在 DefaultErrorAttributes
这个类中。
这里面有个 getErrorAttributes 的属性,里面就放着于 JSON 中对应的一些属性。
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> errorAttributes = getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE));
if (Boolean.TRUE.equals(this.includeException)) {
options = options.including(Include.EXCEPTION);
}
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.put("message", "");
}
if (!options.isIncluded(Include.BINDING_ERRORS)) {
errorAttributes.remove("errors");
}
return errorAttributes;
}
堆栈信息
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());
}
如果是直接访问 /error
这个错误页,那么它便会返回 999 状态码。对应的源码如下。
private void addStatus(Map<String, Object> errorAttributes, RequestAttributes requestAttributes) {
Integer status = getAttribute(requestAttributes, RequestDispatcher.ERROR_STATUS_CODE);
if (status == null) {
errorAttributes.put("status", 999); // 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);
}
}
请勿发布违反中国大陆地区法律的言论,请勿人身攻击、谩骂、侮辱和煽动式的语言。