SpringBoot异常处理页面高级订制(附源码讲解)

在 SpringBoot 中,所有的默认配置都在这个包下面
在这里插入图片描述

默认跳转的静态错误页面地址

SpringBoot 会在服务器发生错误的时候,去静态资源路径下面的 error 文件夹来找对应的错误页面。比如发生了 404 错误,会去静态资源路径下面的 error 文件夹下找404.html,如果发生500错误,会去找500.html页面。SpringBoot 在没有找到对应错误码的页面时,回去找错误码第一个数字加上xx.html 页面,比如 4xx.html、5xx.html。

源码讲解

SpringBoot 的静态资源路径

SpringBoot 定义了4个静态资源路径,它们分别在:

  1. classpath:/META-INF/resources/

  2. classpath:/resources/

  3. classpath:/static/

  4. classpath:/public/

这四个地方.这是在哪里定义的呢? 可以看一看 SpringBoot 如何定义静态资源的( 这部分可以跳过,知道位置就好)
打开Spring Boot配置 web 模块的自动配置类:WebMvcAutoConfiguration。找到静态内部类 WebMvcAutoConfigurationAdapter 。可以看到,这个类是实现了WebMvcConfigurer 接口的子类。在我的上一篇文章(点这–>Java配置Spring MVC<–点这)中也提到了,这个接口是用来在纯注解的情况下配置 Spring MVC 的。
在这个内部类中可以找到这个用来配置静态资源的方法:
public void addResourceHandlers(ResourceHandlerRegistry registry)
这个方法是用来替代 dispatcher.xml 中这个标签的:< mvc :resources location=”” mapping=”“/ >

@Override
		public void addResourceHandlers(ResourceHandlerRegistry registry) {
			if (!this.resourceProperties.isAddMappings()) {
				logger.debug("Default resource handling disabled");
				return;
			}
            // 这些是用来配置 webjars 的,就是用 maven 来引入jQuery.js、Bootstrap.js 等资源的
			Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
			CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
			if (!registry.hasMappingForPattern("/webjars/**")) {
				customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
						.addResourceLocations("classpath:/META-INF/resources/webjars/")
						.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
			}
            // <mvc :resources /> 标签的 mapping 属性的值 
			String staticPathPattern = this.mvcProperties.getStaticPathPattern();
			if (!registry.hasMappingForPattern(staticPathPattern)) {
				customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
					.addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))// 这是配置静态资源的路径
						.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
			}
		}

点进this.resourceProperties.getStaticLocations() 方法中,可以看到返回值 就是:org.springframework.boot.autoconfigure.web.ResourceProperties#CLASSPATH_RESOURCE_LOCATIONS 属性的值 :

private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/",
			"classpath:/resources/", "classpath:/static/", "classpath:/public/" };

异常页面的路径

现在我们已经知道了静态资源路径在哪,以及在哪定义的了。那如何确认异常页面的路径是在静态资源下的 /error/ 文件夹下的呢?来,进一步的深入源码。
配置 Web 模块有专门的配置类,同样的,配置异常处理也有专门的配置类,它就是 :ErrorMvcAutoConfiguration

找到ErrorMvcAutoConfiguration 类,关于错误页面跳转及错误页面信息,我们先看这个 Bean。

	@Bean
	@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
	public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
			ObjectProvider<ErrorViewResolver> errorViewResolvers) {
		return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
				errorViewResolvers.orderedStream().collect(Collectors.toList()));
	}

根据这个Bean的名字可以看出,这是一个处理错误信息的Controller。

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {

可以看到类上面有个非常熟悉的注解:@RequestMapping 只不过和我们平时写的不太一样,我们平时只是写一个地址,这里却用了 ${}来取值。${server.error.path:${error.path:/error}} 这段代码的意思是:如果配置文件中有 server.error.path 这个属性,那么 @RequestMapping 的值就是这个属性的值,如果没有,就用 error.path 的值,如果 error.path 还没有,那就默认处理 /error 请求。
该类中有一个方法,它就是:
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response)
看方法名就知道它是处理异常页面的,我们打个断点试确认下
创建一个接口,在其中抛出一个异常

// 测试异常接口
	@GetMapping("/t")
    public void error1(){
        throw new NullPointerException("测试异常");
    }

启动项目,假设端口是 8080。在浏览器中访问 localhost:8080/t 即可进行断点。证明我们的猜想是正确的。现在来好好看看这个方法。

@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
	public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
		HttpStatus status = getStatus(request);
		// 这是返回的是 ModelAndView 中的 model 数据,后面会对这里做讲解。
		Map<String, Object> model = Collections
				.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
		response.setStatus(status.value());
		// 返回个 ModelAndView ,这其中包含视图对象,要着重关注这里
		ModelAndView modelAndView = resolveErrorView(request, response, status, model);
		return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
	}

ModelAndView modelAndView = resolveErrorView(request, response, status, model); 这一行打个断点,看下返回的 modelAndView 里面有什么?
在这里插入图片描述
可以看到 视图对象已经指定到了 error/5xx 了。step into 到 resolveErrorView 方法中看看。

protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
			Map<String, Object> model) {
		for (ErrorViewResolver resolver : this.errorViewResolvers) {
			ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
			if (modelAndView != null) {
				return modelAndView;
			}
		}
		return null;
	}

遍历所有的 ErrorViewResolver ,并调用其resolveErrorView 方法。在这个方法中也打上断点,可以发现this.errorViewResolvers 只有一个成员,就是在org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration.DefaultErrorViewResolverConfiguration 中注入的,是ErrorViewResolver 的实现类DefaultErrorViewResolver注入组件不是重点,就不给这的代码复制了。
再次进入 DefaultErrorViewResolverresolveErrorView 方法。

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

可以看到流程:

  1. 先调用本类的 private ModelAndView resolve(String viewName, Map<String, Object> model) 方法,将请求状态码作为视图名传入 resolve 方法中,返回 ModelAndView。

在这里插入图片描述

  1. 如果返回 ModelAndView 为空并且SERIES_VIEWS(不知道是个什么东西,先不管它)中存储了有关这个错误的信息,则用另一种方法调用resolve方法。这里的 viewName 就是错误码第一个数字+xx

在这里插入图片描述
进入 resolve(String viewName, Map<String, Object> model) 方法进行调试。

private ModelAndView resolve(String viewName, Map<String, Object> model) {
// 就是在这里规定异常资源文件夹是在 error 文件夹下的
		String errorViewName = "error/" + viewName;

		TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
				this.applicationContext);
		// 默认是在静态资源路径下找 error 文件夹,如果模板引擎也能处理,那就交给模板引擎处理。
		if (provider != null) {
			return new ModelAndView(errorViewName, model);
		}
		// 模板引擎处理不了错误资源,就接着在静态资源路径下进行处理
		return resolveResource(errorViewName, model);
	}

在模板引擎无法处理错误资源的情况下,Spring Boot 调用resolveResource方法去静态资源路径下找错误资源,该方法定义如下。

/*
 *@param (viewName) : 调用该方法时传入的视图名,也就是 4xx或5xx或者错误码
 *@param (model) : 也就是带到页面上的数据
 */
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
// 遍历所有静态资源位置
		for (String location : this.resourceProperties.getStaticLocations()) {
			try {
			// 根据静态资源位置生成 Resource 对象,该对象代表静态资源路径的 Resource
				Resource resource = this.applicationContext.getResource(location);
				// viewName + ".html" 就是 错误码.html 页面,这里是创建静态资源位置加上文件名的 Resource
				resource = resource.createRelative(viewName + ".html");
				// 判断这个资源是否存在,如果存在就返回个记录了该页面资源的 ModelAndView。
				if (resource.exists()) {
					return new ModelAndView(new HtmlResourceView(resource), model);
				}
			}
			catch (Exception ex) {
			}
		}
		return null;
	}

源码流程总结

代码看完了,现在总结下默认的异常处理流程

  1. 服务器抛出异常,跳转到 /error 接口。也就是 BaseController
  2. 遍历所有 ErrorViewResolver,看哪个能处理错误请求
  3. 先根据错误码找到对应的视图
    a. 将视图名拼成 “error/错误码”
    b. 看模板引擎能否对该视图处理,如果能直接返回视图模型
    c. 如果模板引擎处理不了,则先将viewName 后面再拼上".html"并在静态资源下找错误视图。
  4. 如果无法根据错误码找到对应的视图,则用错误码的第一个数字加上 xx 来查找对应的视图,流程和上面一样。

自定义异常视图

既然知道了 Spring Boot 会如何查找异常视图,那么扩展起来也就随心应手了。扩展异常视图有两周手段

  1. 直接在静态资源路径下创建 error 文件夹,放入错误码对应的静态页面

  2. 在模板引擎路径下创建按 error 文件夹,放入错误码对应的静态页面。在模板引擎中可以打印一些错误信息。

第一种非常简单,但是能够展示的信息过少,不做过多介绍,在静态资源路径下的/error 文件夹下创建状态码对应的html 文件即可。
第二种可以展示默认会展示如下信息:

  1. timestamp 时间戳

  2. status 错误码

  3. error 错误信息类型描述

  4. exception 异常全限定类名

  5. message 就是 Throwable 接口的 getMessage()方法的返回值

  6. path 就是哪个URI 报错了

以 Thymeleaf 模板引擎为例,默认 Thymeleaf 的模板是放在类路径下的 templates 路径下,那么就可以在 templates 路径下创建一个 error 文件夹,放入 4xx.html 和 5xx.html 用来测试。
在这里插入图片描述

先在浏览器地址栏中输入一个不存在的地址,会发现页面跳到了 4xx.html

再输入在上面定义的会抛出异常的接口,会跳到 5xx.html 。证明测试成功!

展示错误信息

4开头的错误码都是浏览器端造成的,不是地址错了就是请求参数有误,一般我们不用过于关心。我们主要是关注5开头的错误码,这才是我们服务器的问题。上面说过,在跳转至异常页面时,默认会携带6个信息,如果我们使用模板引擎处理异常视图,我们是可以直接把这些错误信息打印出来的。下面看一下 5xx 页面中的html代码

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>服务器异常页面</title>
</head>
<body>
	<h2>哎呦~服务器崩溃了</h2>
	<h2>timestamp: [[${timestamp}]]</h2>
	<h2>URI: [[${path}]]</h2>
	<h2>status: [[${status}]]</h2>
	<h2 th:if="not ${#strings.isEmpty(exception)}">Exception:[[ ${exception}]]</h2>
	<h2 th:if="not ${#strings.isEmpty(message)}">Exception Message:[[ ${message}]]</h2>
	<h2 th:if="not ${#strings.isEmpty(trace)}">Exception trace:</h2>
	<p th:if="not ${#strings.isEmpty(trace)}">[[ ${trace}]]</p>
</body>
</html>

这里先介绍下上面用到的 Thymeleaf 模板引擎语法,如果你很熟悉,可以跳过。

  1. [[]] 一个中括号内嵌一个中括号,是 Thymeleaf 的行内写法,在里面可以写 Thymeleaf 表达式而不会被 html 直接打印出来
  2. ${} 美元符 + 大括号可以取出 ModelAndView 中的 model
  3. th:if 用来做条件筛选,表达式返回 true 该行元素才会被渲染出来
  4. #strings # 号开头代表调用 Thymeleaf 内置对象,这是处理字符串的工具类

看一下页面效果
在这里插入图片描述
如果大家把我的 html 代码复制过去(前提是你用的是 Thymeleaf 模板引擎,否则会报错或者把thymeleaf的取值符号打印出来),会发现只能打印时间戳、URI和错误码。这是为什么呢?
在讲解原理之前,我们要清楚一件事:我们的项目最终都会发布到生产环境上的,在平时的开发环境我们可以把方法调用栈、异常信息打印到页面上方便定位异常产生的位置。但是这些信息绝对不能暴漏在开发环境,尤其是方法调用栈,相当于暴漏了项目结构,非常危险!所以,Spring Boot 在默认的情况下,只携带一些不重要的参数到前端的。
理解这个概念之后,我们来查看一下如何把所有错误信息都展示出来。

源码调试

再次来到 BasicErrorController 的 errorHtml 方法中,打断点在这一行

@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
	public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
		HttpStatus status = getStatus(request);
		Map<String, Object> model = Collections
		// 主要关注这个 getErrorAttributes 方法,在这里打上断点。
				.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);
	}

一层一层的调试,先看看 getErrorAttributeOptions 返回个什么东西

在这里插入图片描述
啥也没有,那就不管了,再进入 getErrorAttributes 方法看看用getErrorAttributeOptions 的返回值做了些什么操作。
进入getErrorAttributeOptions 方法后,先将 HttpServletRequest 包装成了 WebRequest ,然后调了ErrorAttributesgetErrorAttributes 方法。直接进入到ErrorAttributesgetErrorAttributes 方法中,跳过ErrorController中的getErrorAttributes 方法。(只做了包装对象和调用其他方法的操作,没必要复制过来浪费篇幅)
进入DefaultErrorAttributes 类中,它是ErrorAttributes接口的默认实现类,查看getErrorAttributes 方法

@Override
	public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
		Map<String, Object> errorAttributes = getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE));
		if (this.includeException != null) {
			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;
	}

可以看到第二个ErrorAttributeOptions类型的参数是用来确认显示哪些异常信息的,在该方法的第一行会把6个异常信息全部取出来,在后面的 if 判断中,如果不包含这些信息就给删除掉或者清空。
如果想保留一些信息,可以创建一个继承自DefaultErrorAttributes 的子类,然后重写getErrorAttributes 方法,这样我们就可以操控要打印哪些异常信息了。

@Component
public class MyErrorAttribute extends DefaultErrorAttributes {

    private ErrorAttributeOptions.Include[] includes;
    {
    	// 将想要现实的信息添加到数组中
    	includes = new ErrorAttributeOptions.Include[]{
                ErrorAttributeOptions.Include.STACK_TRACE,
                ErrorAttributeOptions.Include.EXCEPTION,
                ErrorAttributeOptions.Include.MESSAGE,
        };
    }

    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
    // 调用ErrorAttributeOptions的includeing 方法把我们要展示的信息包含进去
        Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, options.including(includes));
        return errorAttributes;
    }
}

这样就可以展示出所有的异常信息了。但是,这么做有个缺点,在任何环境中都是展示这几个信息,没办法根据环境切换。还得换个写法。
在 项目中添加一个配置类,注入一个 ErrorAttributeOptions.Include 类型的数组,再利用 @Profile 属性进行动态切换

@Configuration
public class Beans {
    @Bean("includes")
    @Profile("dev") // 开发环境显示方法调用栈、异常类名和异常信息
    public ErrorAttributeOptions.Include[] exceptionDetailIncludesDev() {
        return new ErrorAttributeOptions.Include[]{
                ErrorAttributeOptions.Include.STACK_TRACE,
                ErrorAttributeOptions.Include.EXCEPTION,
                ErrorAttributeOptions.Include.MESSAGE,
        };
    }
    @Bean("includes")
    @Profile("prod") // 生产环境只显示错误信息
    public ErrorAttributeOptions.Include[] exceptionDetailIncludesProd() {
        return new ErrorAttributeOptions.Include[]{
                ErrorAttributeOptions.Include.MESSAGE,
        };
    }
}

回到我们重写的DefaultErrorAttributes,这个ErrorAttributeOptions.Include[] 数组不能写死了,得动态注入。

@Component
// 这里比较关键,Spring 在 IOC容器初始化的时候有可能先加载这个Bean 再加载上面定义的
// 配置类 Beans ,这就会造成 includes 注入失败。所以要指明在 Beans加载之后再加载这个类
@DependsOn("beans")
public class MyErrorAttribute extends DefaultErrorAttributes {

    @Autowired
    private ErrorAttributeOptions.Include[] includes;

    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
        Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, options.including(includes));
        return errorAttributes;
    }
}

在 SpringBoot 配置文件 application.yml 中,指定当前环境为开发环境( dev )

spring:
  profiles:
    active: dev

访问抛出异常的接口,就可以看到所有的错误信息啦!!!
再将环境调整为生产环境(prod)
就只能看见三个错误信息。

结束语

上一次写博客是 6月11日,10天过去了,学了一个新知识,消化了之后就又来写博客了。之前也是知道 SpringBoot 会在静态资源下的找错误页面,但是并不了解流程。这次跟进源码调试之后才理解了具体的流程。不得不感叹到 SpringBoot 的强大,自动配置基本满足开发需求,即使需要扩展,也只需要写少量代码即可做到。配置简介,可扩展性高,真的是Java界的典范

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值