前言

相信大家在刚开始体验 Springboot 的时候一定会经常碰到这个页面,也就是访问一个不存在的页面的默认返回页面。

如果是其他客户端请求,如接口测试工具,会默认返回JSON数据。

{
"timestamp":"2019-01-06 22:26:16",
"status":404,
"error":"Not Found",
"message":"No message available",
"path":"/asdad"
}

很明显,SpringBoot 根据 HTTP 的请求头信息进行了不同的响应处理。HTTP 相关知识可以参考此处

1. SpringBoot 异常处理机制

追随 SpringBoot 源码可以分析出默认的错误处理机制。

// org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration
// 绑定一些错误信息 记为 1
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes(
this.serverProperties.getError().isIncludeException());
}
// 默认处理 /error 记为 2
@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
this.errorViewResolvers);
}
// 错误处理页面 记为3
@Bean
public ErrorPageCustomizer errorPageCustomizer() {
return new ErrorPageCustomizer(this.serverProperties, this.dispatcherServletPath);
}
@Configuration
static class DefaultErrorViewResolverConfiguration { private final ApplicationContext applicationContext; private final ResourceProperties resourceProperties; DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext,
ResourceProperties resourceProperties) {
this.applicationContext = applicationContext;
this.resourceProperties = resourceProperties;
}
// 决定去哪个错误页面 记为4
@Bean
@ConditionalOnBean(DispatcherServlet.class)
@ConditionalOnMissingBean
public DefaultErrorViewResolver conventionErrorViewResolver() {
return new DefaultErrorViewResolver(this.applicationContext,
this.resourceProperties);
} }

结合上面的注释,上面代码里的四个方法就是 Springboot 实现默认返回错误页面主要部分。

1.1. errorAttributes

errorAttributes直译为错误属性,这个方法确实如此,直接追踪源代码。

代码位于:

// org.springframework.boot.web.servlet.error.DefaultErrorAttributes

这个类里为错误情况共享很多错误信息,如。

errorAttributes.put("timestamp", new Date());
errorAttributes.put("status", status);
errorAttributes.put("error", HttpStatus.valueOf(status).getReasonPhrase());
errorAttributes.put("errors", result.getAllErrors());
errorAttributes.put("exception", error.getClass().getName());
errorAttributes.put("message", error.getMessage());
errorAttributes.put("trace", stackTrace.toString());
errorAttributes.put("path", path);

这些信息用作共享信息返回,所以当我们使用模版引擎时,也可以像取出其他参数一样轻松取出。

1.2. basicErrorControll

直接追踪 BasicErrorController 的源码内容可以发现下面的一段代码。

// org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController
@Controller
// 定义请求路径,如果没有error.path路径,则路径为/error
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController { // 如果支持的格式 text/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());
// 解析错误视图信息,也就是下面1.4中的逻辑
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
// 返回视图,如果没有存在的页面模版,则使用默认错误视图模版
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
} @RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
// 如果是接受所有格式的HTTP请求
Map<String, Object> body = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.ALL));
HttpStatus status = getStatus(request);
// 响应HttpEntity
return new ResponseEntity<>(body, status);
}
}

由上可知,basicErrorControll 用于创建用于请求返回的 controller类,并根据HTTP请求可接受的格式不同返回对应的信息,所以在使用浏览器和接口测试工具测试时返回结果存在差异。

1.3. ererrorPageCustomizer

直接查看方法里的new ErrorPageCustomizer(this.serverProperties, this.dispatcherServletPath);

//org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration.ErrorPageCustomizer
/**
* {@link WebServerFactoryCustomizer} that configures the server's error pages.
*/
private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered { private final ServerProperties properties; private final DispatcherServletPath dispatcherServletPath; protected ErrorPageCustomizer(ServerProperties properties,
DispatcherServletPath dispatcherServletPath) {
this.properties = properties;
this.dispatcherServletPath = dispatcherServletPath;
}
// 注册错误页面
// this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath())
@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
//getPath()得到如下地址,如果没有自定义error.path属性,则去/error位置
//@Value("${error.path:/error}")
//private String path = "/error";
ErrorPage errorPage = new ErrorPage(this.dispatcherServletPath
.getRelativePath(this.properties.getError().getPath()));
errorPageRegistry.addErrorPages(errorPage);
} @Override
public int getOrder() {
return 0;
} }

由上可知,当遇到错误时,如果没有自定义 error.path 属性,则请求转发至 /error.

1.4. conventionErrorViewResolver

根据上面的代码,一步步深入查看 SpringBoot 的默认错误处理实现,查看看 conventionErrorViewResolver方法。下面是 DefaultErrorViewResolver 类的部分代码,注释解析。

// org.springframework.boot.autoconfigure.web.servlet.error.DefaultErrorViewResolver

// 初始化参数,key 是HTTP状态码第一位。
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) {
// 使用HTTP完整状态码检查是否有页面可以匹配
ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
// 使用 HTTP 状态码第一位匹配初始化中的参数创建视图对象
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
} private ModelAndView resolve(String viewName, Map<String, Object> model) {
// 拼接错误视图路径 /eroor/[viewname]
String errorViewName = "error/" + viewName;
// 使用模版引擎尝试创建视图对象
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
.getProvider(errorViewName, this.applicationContext);
if (provider != null) {
return new ModelAndView(errorViewName, model);
}
// 没有模版引擎,使用静态资源文件夹解析视图
return resolveResource(errorViewName, model);
} private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
// 遍历静态资源文件夹,检查是否有存在视图
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;
}

而 Thymeleaf 对于错误页面的解析实现。

//org.springframework.boot.autoconfigure.thymeleaf.ThymeleafTemplateAvailabilityProvider
public class ThymeleafTemplateAvailabilityProvider
implements TemplateAvailabilityProvider {
@Override
public boolean isTemplateAvailable(String view, Environment environment,
ClassLoader classLoader, ResourceLoader resourceLoader) {
if (ClassUtils.isPresent("org.thymeleaf.spring5.SpringTemplateEngine",
classLoader)) {
String prefix = environment.getProperty("spring.thymeleaf.prefix",
ThymeleafProperties.DEFAULT_PREFIX);
String suffix = environment.getProperty("spring.thymeleaf.suffix",
ThymeleafProperties.DEFAULT_SUFFIX);
return resourceLoader.getResource(prefix + view + suffix).exists();
}
return false;
}
}

从而我们可以得知,错误页面首先会检查模版引擎文件夹下的 /error/HTTP状态码 文件,如果不存在,则检查去模版引擎下的/error/4xx或者 /error/5xx 文件,如果还不存在,则检查静态资源文件夹下对应的上述文件。

2. 自定义异常页面

经过上面的 SpringBoot 错误机制源码分析,知道当遇到错误情况时候,SpringBoot 会首先返回到模版引擎文件夹下的 /error/HTTP状态码 文件,如果不存在,则检查去模版引擎下的/error/4xx或者 /error/5xx 文件,如果还不存在,则检查静态资源文件夹下对应的上述文件。并且在返回时会共享一些错误信息,这些错误信息可以在模版引擎中直接使用。

errorAttributes.put("timestamp", new Date());
errorAttributes.put("status", status);
errorAttributes.put("error", HttpStatus.valueOf(status).getReasonPhrase());
errorAttributes.put("errors", result.getAllErrors());
errorAttributes.put("exception", error.getClass().getName());
errorAttributes.put("message", error.getMessage());
errorAttributes.put("trace", stackTrace.toString());
errorAttributes.put("path", path);

因此,需要自定义错误页面,只需要在模版文件夹下的 error 文件夹下防止4xx 或者 5xx 文件即可。

<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>[[${status}]]</title>
<!-- Bootstrap core CSS -->
<link href="/webjars/bootstrap/4.1.3/css/bootstrap.min.css" rel="stylesheet">
</head>
<body >
<div class="m-5" >
<p>错误码:[[${status}]]</p>
<p >信息:[[${message}]]</p>
<p >时间:[[${#dates.format(timestamp,'yyyy-MM-dd hh:mm:ss ')}]]</p>
<p >请求路径:[[${path}]]</p>
</div> </body>
</html>

随意访问不存在路径得到。

3. 自定义错误JSON

根据上面的 SpringBoot 错误处理原理分析,得知最终返回的 JSON 信息是从一个 map 对象中转换出来的,那么,只要能自定义 map 中的值,就可以自定义错误信息的 json 格式了。直接重写 DefaultErrorAttributes类的 getErrorAttributes 方法即可。

import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.WebRequest; import java.util.HashMap;
import java.util.Map; /**
* <p>
* 自定义错误信息JSON值
*
* @Author niujinpeng
* @Date 2019/1/7 15:21
*/
@Component
public class ErrorAttributesCustom extends DefaultErrorAttributes { @Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
String code = map.get("status").toString();
String message = map.get("error").toString();
HashMap<String, Object> hashMap = new HashMap<>();
hashMap.put("code", code);
hashMap.put("message", message);
return hashMap;
}
}

使用 postman 请求测试。

4. 统一异常处理

使用 @ControllerAdvice 结合@ExceptionHandler 注解可以实现统一的异常处理,@ExceptionHandler 注解的类会自动应用在每一个被 @RequestMapping 注解的方法。当程序中出现异常时会层层上抛

import lombok.extern.slf4j.Slf4j;
import net.codingme.boot.domain.Response;
import net.codingme.boot.enums.ResponseEnum;
import net.codingme.boot.utils.ResponseUtill;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.HttpServletRequest; /**
* <p>
* 统一的异常处理
*
* @Author niujinpeng
* @Date 2019/1/7 14:26
*/ @Slf4j
@ControllerAdvice
public class ExceptionHandle { @ResponseBody
@ExceptionHandler(Exception.class)
public Response handleException(Exception e) {
log.info("异常 {}", e);
if (e instanceof BaseException) {
BaseException exception = (BaseException) e;
String code = exception.getCode();
String message = exception.getMessage();
return ResponseUtill.error(code, message);
}
return ResponseUtill.error(ResponseEnum.UNKNOW_ERROR);
}
}

请求异常页面得到响应如下。

{
"code": "-1",
"data": [],
"message": "未知错误"
}

文章代码已经上传到 GitHub Spring Boot Web开发 - 错误机制

<完>

欢迎点赞关注!

本文原发于个人博客:https://www.codingme.net 转载请注明出处

Springboot 系列(七)Spring Boot web 开发之异常错误处理机制剖析的更多相关文章

  1. Springboot 系列(五)Spring Boot web 开发之静态资源和模版引擎

    前言 Spring Boot 天生的适合 web 应用开发,它可以快速的嵌入 Tomcat, Jetty 或 Netty 用于包含一个 HTTP 服务器.且开发十分简单,只需要引入 web 开发所需的 ...

  2. Springboot 系列(六)Spring Boot web 开发之拦截器和三大组件

    1. 拦截器 Springboot 中的 Interceptor 拦截器也就是 mvc 中的拦截器,只是省去了 xml 配置部分.并没有本质的不同,都是通过实现 HandlerInterceptor ...

  3. spring boot系列(二)spring boot web开发

    json 接口开发 在以前的spring 开发的时候需要我们提供json接口的时候需要做如下配置: 1 添加jackjson等jar包 2 配置spring controller扫描 3 对接的方法添 ...

  4. SpringBoot系列:Spring Boot使用模板引擎FreeMarker

    一.Java模板引擎 模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的HTML文档. 在jav ...

  5. SpringBoot系列:Spring Boot使用模板引擎Thymeleaf

    一.Java模板引擎 模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的HTML文档. 在jav ...

  6. SpringBoot系列:Spring Boot使用模板引擎JSP

    一.Java模板引擎 模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的HTML文档. 在jav ...

  7. Spring Boot Web 开发注解篇

    本文提纲 1. spring-boot-starter-web 依赖概述 1.1 spring-boot-starter-web 职责 1.2 spring-boot-starter-web 依赖关系 ...

  8. 四、Spring Boot Web开发

    四.Web开发 1.简介 使用SpringBoot: 1).创建SpringBoot应用,选中我们需要的模块: 2).SpringBoot已经默认将这些场景配置好了,只需要在配置文件中指定少量配置就可 ...

  9. SpringBoot系列:Spring Boot集成Spring Cache,使用RedisCache

    前面的章节,讲解了Spring Boot集成Spring Cache,Spring Cache已经完成了多种Cache的实现,包括EhCache.RedisCache.ConcurrentMapCac ...

随机推荐

  1. oracle常用系统函数

    一.字符类函数 字符类函数是专门用于字符处理的函数,处理的对象可以是字符或者字符串常量,也可以是字符类型的列. 1.ASCII(c)和CHR(i) ASCII(c)函数用于返回一个字符的ASCII码, ...

  2. Python从入门到放弃

    计算机基础 01 计算机基础之编程 02 计算机组成原理 03 计算机操作系统 04 编程语言分类 Python解释器 05 Python和Python解释器 06 执行Python程序的两种方式 0 ...

  3. FreeSql 如何实现 Sqlite 跨库查询

    FreeSql 是 .NetFramework 4.6+..NetCore 下的 ORM 功能库,提供了丰富的功能,支持五种流行数据库 MySql/SqlServer/PostgreSQL/Oracl ...

  4. 学习 JavaScript (八) 引用类型之 Object

    在JavaScript中,引用类型是一种数据结构.包括对象(Obejct).数组(Array).日期(Date).正则表达式(RegExp).函数(Function).基本包装类型(new Boole ...

  5. 卷积神经网络之AlexNet

    由于受到计算机性能的影响,虽然LeNet在图像分类中取得了较好的成绩,但是并没有引起很多的关注. 知道2012年,Alex等人提出的AlexNet网络在ImageNet大赛上以远超第二名的成绩夺冠,卷 ...

  6. LeetCode重建二叉树系列问题总结

    二叉树天然的递归特性,使得我们可以使用递归算法对二叉树进行遍历和重建.之前已经写过LeetCode二叉树的前序.中序.后序遍历(递归实现),那么本文将进行二叉树的重建,经过对比,会发现二者有着许多相似 ...

  7. 第15章 时间常数字符串比较 - IdentityModel 中文文档(v1.0.0)

    在比较安全上下文中的字符串(例如比较键)时,应尽量避免泄漏时序信息. 该TimeConstantComparer类可以帮助: var isEqual = TimeConstantComparer.Is ...

  8. 常见文本类css属性

    学习web的第六天 p{ font-size:18px;  /*文本尺寸*/ font-family:"隶书";  /*文本字体*/ font-weight:bold;    /* ...

  9. 使用Fiddler获取OAuth2认证的access token时候返回502

    微软动态CRM专家罗勇 ,回复322或者20190402可方便获取本文,同时可以在第一间得到我发布的最新博文信息,follow me! 我这里Fiddler的Composer功能来获取OAuth2 认 ...

  10. 积极拥抱.NET Core开源社区

    潘正磊在上海的Tech Summit 2018 大会上给我们的.NET Core以及开源情况带来了最新信息. .Net Core 开源后取得了更加快速的发展,目前越活跃用户高达400万人,每月新增开发 ...