目录
一、SpringBoot的WEB开发
1.1 静态资源的处理
1.1.1 静态资源目录
1)SpringBoot静态资源处理
2)关于静态资源处理的配置
3)欢迎页面的处理
4)修改SpringBoot资源访问路径
1.1.2 WebJars资源
1.2 注册Servlet三大组件
1.2.1 扫描原生组件
1.2.2 使用SpringBoot方式注册
1.3 SpringBoot项目配置
1.3.1 SpringBoot部署War项目
1.3.4 SpringBoot配置JSP
1) War项目部署JSP
2) Jar项目部署JSP
1.4 静态资源目录问题
1.4.1 关于classpath目录
1.4.2 关于/目录
1.4.3 关于JSP问题
1.5 SpringBoot项目打包
1.5.1 SpringBoot的Jar项目打包
1.5.2 SpringBoot的War项目打包
1.5.3 SpringBoot的Jar项目打包JSP文件
1.6 SpringBoot静态资源处理原理
1.2.1 WebMvcAutoConfiguration类
1.6.2 关于webjars资源
1)分析源码
2)定制webjars访问路径
1.6.3 关于静态资源
1)分析源码
2)根目录问题
二、SpringBoot错误处理
2.1 SpringBoot错误处理规则
2.1.1 定制错误页面
2.2.2 定制错误json数据响应
1)定义异常处理器
2)转发error请求
2.2.3 定制错误数据封装属性类
2.2.4 定制错误处理请求处理类
2.2 SpringBoot 错误处理原理
2.2.1 ErrorMvcAutoConfiguration源码分析
2.2.2 ErrorController源码分析
1)BasicErrorController源码分析-视图解析
2)BasicErrorController源码分析-数据封装
3)自定义ErrorController
2.2.3 ErrorAttribute源码分析
1)DefaultErrorAttributes源码分析
2)自定义ErrorAttribute
一、SpringBoot的WEB开发
1.1 静态资源的处理
我们之前在学习SpringMVC时,需要配置静态资源的放行;SpringBoot的WEB场景启动器中已经集成了SpringMVC,在 SpringBoot 中则不再需要进行此项配置,SpringBoot已经默认完成了这一工作。
- SpringBoot父工程:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
1.1.1 静态资源目录
1)SpringBoot静态资源处理
当访问项目中的任意资源时,Spring Boot 会默认从以下路径中查找资源文件(优先级依次降低):
- 1)classpath:/META-INF/resources/
- 2)classpath:/resources/
- 3)classpath:/static/
- 4)classpath:/public/
- 5)/
Tips:根(/)目录比较特殊,我们后面再具体说明,暂时先分析4个目录;
当SpingBoot接收到静态资源的请求时,会默认去以上4个路径寻找需要的静态资源;
访问:http://localhost:8080/hello.html;
2)关于静态资源处理的配置
访问SpringBoot在所有的资源(/**
)时,默认会去5个静态资源目录寻找;我们可以更改静态资源目录,也可以禁用SpringBoot默认静态资源访问规则,由我们来自定义;
- 1)修改SpringBoot资源访问目录:
server:
port: 8080
spring:
resources:
# 更改静态资源目录
static-locations: # 默认为/META-INF/resources/、/resources/、/static/、/public/、/
- classpath:/aaa
- classpath:/bbb
静态资源目录修改为aaa、bbb之后,原来的4个目录只有META-INF/resources目录有效,其他三个目录均无效;
优先级为:aaa>bbb>META-INF/resources
3)欢迎页面的处理
SpringBoot默认会去4个静态资源(static-locations
)目录下寻找index.html页面;
Tips:访问静态首页或欢迎页时,其查找顺序也遵循默认静态资源的查找顺序,即先查找优先级高的目录,在查找优先级低的目录,直到找到 index.html 为止。
- 设置欢迎页面:
4)修改SpringBoot资源访问路径
server:
port: 8080
spring:
mvc:
# 访问/views/** 下面的所有请求才去静态目录下面寻找(static-locations) 默认为 /**
static-path-pattern: /views/**
注意:如果修改了SpringBoot默认的静态资源访问目录,那么欢迎页面将会失效!
- 修改SpringBoot资源访问目录:
server:
port: 8080
spring:
mvc:
# 访问/views/** 下面的所有请求才去静态目录下面寻找(static-locations) 默认为 /**
static-path-pattern: /views/**
resources:
# 更改静态资源目录 默认为/META-INF/resources/、/resources/、/static/、/public/、/
static-locations:
- classpath:/aaa
- classpath:/bbb
# 禁用SpringBoot的静态资源访问规则(static-path-pattern和static-locations都不生效了)
add-mappings: true
1.1.2 WebJars资源
为了让页面更加美观,让用户有更多更好的体验,Web 应用中通常会使用大量的 JS 和 CSS,例如 jQuery,Backbone.js 和 Bootstrap 等等。通常我们会将这些 Web 前端资源拷贝到 Java Web 项目的 webapp 相应目录下进行管理。但是 Spring Boot 项目是以 JAR 包的形式进行部署的,不存在 webapp 目录,那么 Web 前端资源该如何引入到 Spring Boot 项目中呢?
WebJars 可以将 Web 前端资源(JS,CSS 等)打成一个个的 Jar 包,然后将这些 Jar 包部署到 Maven 中央仓库中进行统一管理,当 Spring Boot 项目中需要引入 Web 前端资源时,只需要访问 WebJars 官网,找到所需资源的 pom 依赖,将其导入到项目中即可。
- WebJars官网:WebJars - Web Libraries in Jars
在SpringBoot中所有 /webjars/** 的请求 ,都去 classpath:/META-INF/resources/webjars/
找资源;
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.6.0</version>
</dependency>
将jquery的webjars资源引入到项目中后,查看项目的依赖路径:
访问:http://localhost:8080/webjars/jquery/3.6.0/jquery.js
1.2 注册Servlet三大组件
SpringBoot默认是以jar包的方式启动嵌入式的Tomcat来启动SpringBoot的web应用,并没有web.xml;并且所有的WEB资源全部被SpringBoot所接管;我们直接编写@Servlet、@WebFilter、@Listener并不会生效;
SpringBoot提供了多种注册原生Servlet组件的方法;
- Servlet:
package com.dfbz.servlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author lscl
* @version 1.0
* @intro:
*/
@WebServlet("/test")
public class TestServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("servlet running...");
resp.getWriter().write("hello servlet!");
}
}
- TestFilter:
package com.dfbz.filter;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
/**
* @author lscl
* @version 1.0
* @intro:
*/
@WebFilter("/*")
public class TestFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("test filter...");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("before testFilter...");
chain.doFilter(request,response);
System.out.println("after testFilter...");
}
@Override
public void destroy() {
System.out.println("destroy filter...");
}
}
- Listener:
package com.dfbz.listener;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
/**
* @author lscl
* @version 1.0
* @intro:
*/
@WebListener
public class TestListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("contextInitialized...");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("contextDestroyed");
}
}
1.2.1 扫描原生组件
我们可以使用原生的servlet注解来帮助我们配置三大组件;但是需要交给SpringBoot进行三大注解的组件扫描;
在启动类中标注@ServletComponentScan
:
package com.dfbz;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
/**
* @author lscl
* @version 1.0
* @intro:
*/
@SpringBootApplication
@ServletComponentScan // 扫描启动类的子包(孙包等)下的Servlet原生三大组件
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
1.2.2 使用SpringBoot方式注册
SpringBoot提供了一系列用于注册三大原生组件的Bean:
- 注册Servlet:ServletRegistrationBean
- 注册Filter:FilterRegistrationBean
- 注册Listener:ServletListenerRegistrationBean
package com.dfbz;
import com.dfbz.filter.TestFilter;
import com.dfbz.listener.RequestListener;
import com.dfbz.servlet.TestServlet;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import java.util.Arrays;
/**
* @author lscl
* @version 1.0
* @intro:
*/
@SpringBootApplication
//@ServletComponentScan // 扫描启动类的子包(孙包等)下的Servlet原生三大组件
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
/**
* 注册Servlet
* @return
*/
@Bean
public ServletRegistrationBean testServlet(){
ServletRegistrationBean testServletBean =
new ServletRegistrationBean(new TestServlet(),"/test");
return testServletBean;
}
/**
* 注册Filter
* @return
*/
@Bean
public FilterRegistrationBean testFilter(){
FilterRegistrationBean testFilterBean = new FilterRegistrationBean();
testFilterBean.setFilter(new TestFilter());
testFilterBean.setUrlPatterns(Arrays.asList("/*"));
return testFilterBean;
}
/**
* 注册Listener
* @return
*/
@Bean
public ServletListenerRegistrationBean testListener(){
ServletListenerRegistrationBean<TestListener> testListenerBean
= new ServletListenerRegistrationBean<>(new TestListener());
return testListenerBean;
}
}
Tips:使用了注册Bean的方式注册三大组件不需要添加@WebServlet等注解;
1.3 SpringBoot项目配置
1.3.1 SpringBoot部署War项目
我们之前项目并没有使用Tomcat来部署,是因为web的场景启动器中含有内置的tomcat,并且项目的打包方式为Jar。实际开发也可能会将项目打war包,将项目部署到独立的Tomcat中;
- SpringBoot项目打包成war包部署到Tomcat运行需要添加一个Tomcat引导器:
package com.dfbz;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TomcatInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
//传入SpringBoot应用的主程序
return application.sources(Application.class);
}
}
我们的静态资源可以放在webapp目录下,也可以放在SpringBoot的静态资源目录中。
项目结构如下:
从原则上来讲,我们可以使用引导类来启动项目,也可以和我们以前一样,使用IDEA中的Tomcat插件部署项目;但是由于idea的原因,如果使用引导类来启动,默认情况下是不会打包webapp目录的,而使用tomcat来启动才会打包webapp目录;
使用clean命令清空项目,然后使用引导类来启动项目,观察编译后的target目录:
我们查看target目录发现并没有打包webapp目录中的文件,这个时候使用浏览器访问:http://localhost:8080/demo02.html肯定是访问不了的;
使用clean命令清空项目,然后使用tomcat来启动项目,观察编译后的target目录:
此时在浏览器访问:http://localhost:8080/demo02.html发现可以正常访问;
1.3.4 SpringBoot配置JSP
SpringBoot配置JSP时项目的打包方式不同,配置方式也不同;
1) War项目部署JSP
如果打包方式是war,那么JSP直接放在webapp目录即可,其他4个静态目录存放的JSP不能被访问!
- 创建一个war项目:
使用Tomcat启动项目,访问:
访问:http://localhost:8080/demo01.jsp(失败)
访问:http://localhost:8080/demo02.jsp(失败)
访问:http://localhost:8080/demo03.jsp(失败)
访问:http://localhost:8080/demo04.jsp(失败)
访问:http://localhost:8080/demo05.jsp(成功)
2) Jar项目部署JSP
打包方式为Jar时只有/META-INF/resources
目录可以部署JSP文件;
创建一个新的jar项目,项目结构如下:
由于jar项目是内置的Tomcat,要引入SpringBoot对内置Tomcat的JSP解析支持:
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
JSP热部署配置:
server:
servlet:
jsp:
init-parameters:
development: true # 开启JSP热部署
访问:http://localhost:8080/demo01.jsp(成功)
访问:http://localhost:8080/demo02.jsp(失败)
访问:http://localhost:8080/demo03.jsp(失败)
访问:http://localhost:8080/demo04.jsp(失败)
1.4 静态资源目录问题
SpringBoot将访问/**
的请求对接到了以下5个目录:
classpath:/META-INF/resources
classpath:/resources
classpath:/static
classpath:/public
/
1.4.1 关于classpath目录
前面4个目录都在classpath下,那具体什么是classpath目录呢?严格来说,classpath指的并不是maven项目的resources目录,而是项目编译完成的classes目录;如下:
只不过,maven项目中的resources目录下的资源都会被打包到classes目录下;另外我们还可以通过配置让其他目录的内容也打包到classpath下。
我们可以在maven中添加打包配置:
<build>
<resources>
<resource>
<!--打包/src/main/resources/META-INF/resource目录下的资源-->
<directory>src/main/resources/abc</directory>
<!--打包到classpath目录下的static目录-->
<targetPath>static</targetPath>
</resource>
</resources>
</build>
Tips:
- 1)build标签中配置的是当项目打包时将
src/main/resources/abc
目录打包到classpath下的static
目录,而启动引导类并不会打包项目,我们必须手动执行maven的package命令才能够将项目打包;
- 2)build标签配置了之后,maven的目录打包规则会以build标签中配置的为准,以前的默认打包规则就没有了,即只会打包abc目录中的内容到static目录,其他目录都不进行打包;
这样一来,abc目录的资源将会被打包到static目录了,也就可以被SpringBoot正常访问了;
Tips:测试完毕记得注释build标签的内容,否则会影响后面的测试;
另外,classpath的目录并不是固定的,Jar项目和War项目以及idea打包和maven的package打包的classpath位置都不一样;但是不管classpath的位置在哪,只要是maven项目/src/main/resources
目录的内容总是会被打包到classpath。
1.4.2 关于/
目录
5个静态目录中,最后一个目录是/
,/
代表的是"根目录"的意思,具体根目录是哪里War项目和Jar项目不同;
- 在Jar项目中,根目录为:
classpath:/MEAT-INF/resources
- 在War项目中,根目录为:项目名
可以看到war项目将webapp目录下的资源都打包到了"根目录(/)",因此war项目中,5个目录都可以正常放置我们的静态资源;
1.4.3 关于JSP问题
前面我们部署JSP时,规则如下:
- 1)在Jar项目中,JSP必须放置在
MEAT-INF/resources
目录,这是因为在Jar项目中/MEAT-INF/resources
最终会被打包到"根目录(/)",SpringBoot对"根目录"做了特殊处理,让其可以访问JSP,但是其他4个目录并没有
- 2)在War项目中,JSP必须放置在
webapp
目录,因为在War项目中webapp
目录中的内容最终会被打包到"根目录(/)"
1.5 SpringBoot项目打包
1.5.1 SpringBoot的Jar项目打包
我们使用maven的package命令对我们刚刚项目进行打包,发现只会有我们自己编写的代码,并没有将所依赖的SpringBoot那些jar包打包到项目来,这样的项目肯定是启动不了的;
将任意一个jar项目打包,查看包的内部结构:
只有我们自己编写的代码,并没有打包SpringBoot相关jar包,这样的一个项目肯定是运行不起来的;
SpringBoot的打包插件就是帮助我们打包SpringBoot项目的,该插件不仅会将我们编写的代码打包进来,而且也会把所依赖的SpringBoot那些jar也都统统打包到我们的项目中,使我们的项目变成一个可以独立运行的项目;
引入SpringBoot打包插件:
<build>
<plugins>
<!--SpringBoot打包插件-->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>1.4.2.RELEASE</version>
</plugin>
</plugins>
</build>
Tips:Jar项目使用maven的package打成Jar包后,classpath目录位置在:BOOT-INF/classes
,根目录还是在MEAT-INF/resources
1.5.2 SpringBoot的War项目打包
接着我们尝试打包web项目,观察打包的项目结构:
我们知道"/"代表"根目录",在War项目打包时webapp目录的内容将会被打包到"根目录(/)"
Tips:Jar项目使用maven的package打成Jar包后,classpath目录位置在:WEB-INF/classes
,根目录是项目名
1.5.3 SpringBoot的Jar项目打包JSP文件
我们知道只有"根目录"放置的JSP文件才可以被正常访问,而Jar项目中,只有src/main/resources/MEAT-INF/resources
目录中的内容才可以被打包到"根目录",因此正常情况下,我们的JSP都必须放在该目录;那如果我们自己定义一个webapp目录是否会被打包到根目录呢?答案是不能的。因为webapp目录只有在war项目中才会被打包到"根目录"
- 观察下面项目打包案例:
但是我们可以告诉maven,这个/src/main/webapp目录下的资源我们也需要打包:
<build>
<resources>
<resource>
<!--打包src/main/webapp目录下的资源-->
<directory>src/main/webapp</directory>
<!--打包到jar包中的META-INF/resources目录下-->
<targetPath>META-INF/resources</targetPath>
</resource>
<!--更改了maven的打包路径之后,static目录也需要手动指定打包路径-->
<resource>
<directory>src/main/resources/static</directory>
<targetPath>BOOT-INF/classes/static</targetPath>
</resource>
</resources>
<plugins>
<!--SpringBoot打包插件-->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>1.4.2.RELEASE</version>
</plugin>
</plugins>
</build>
Tips:我们更改了Maven的打包路径之后,Maven自己的默认的打包规则将会失效,此时如果我们有其他的静态资源编写在SpringBoot提供的那4个目录中,则需要一起指定打包;
执行打包(package)命令后,查看jar包中的文件:
此时我们将项目中的webapp目录下的资源打包到了/META-INF/resource目录下,而该目录正是Jar项目的"根目录"
而实际开发,不管是/src/main/webapp目录还是SpringBoot提供的四个目录我们都会将其打包在一起:此时这5个目录不允许存在同名文件(会覆盖)
<build>
<resources>
<resource>
<!--打包src/main/webapp目录下的资源-->
<directory>src/main/webapp</directory>
<!--打包到jar包中的META-INF/resources目录下-->
<targetPath>META-INF/resources</targetPath>
</resource>
<resource>
<directory>src/main/resources/META-INF/resources</directory>
<targetPath>META-INF/resources</targetPath>
</resource>
<resource>
<directory>src/main/resources/resources</directory>
<targetPath>META-INF/resources</targetPath>
</resource>
<resource>
<directory>src/main/resources/static</directory>
<targetPath>META-INF/resources</targetPath>
</resource>
<resource>
<directory>src/main/resources/public</directory>
<targetPath>META-INF/resources</targetPath>
</resource>
</resources>
<plugins>
<!--SpringBoot打包插件-->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>1.4.2.RELEASE</version>
</plugin>
</plugins>
</build>
重新打包,观察打包之后的目录结构:
1.6 SpringBoot静态资源处理原理
1.2.1 WebMvcAutoConfiguration类
我们通过上面的章节已经学习到,SpringBoot任何的 /**
请求都会映射到其静态资源目录(4个),任何为 /webjars/**
请求都会来到项目中的 /META-INF/resources/webjars/**
目录,那么SpringBoot关于SpringMVC的配置有哪些呢?
关于SpringBoot的SpringMVC相关配置大都在WebMvcAutoConfiguration配置类中:
@Configuration
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
在WebMvcAutoConfiguration
类中有一个重要的内部类WebMvcAutoConfigurationAdapter
,在该类中配置了大量SpringMVC相关的一些基础配置;其中还关联了WebMvcProperties.class
、ResourceProperties.class
;对SpringMVC进行了大量的配置初始化;
- WebMvcAutoConfiguration源码如下:
// 标注当前类是一个配置类
@Configuration
// 必须要在WEB环境下才会生效
@ConditionalOnWebApplication(type = Type.SERVLET)
// 必须引入SpringMVC/Servlet相关依赖才会生效
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
// 当前IOC容器里面必须没有WebMvcConfigurationSupport这个Bean的时候生效
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
// 配置类顺序
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
// 必须等到DispatcherServletAutoConfiguration和ValidationAutoConfiguration加载完毕后才会生效
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
public static final String DEFAULT_PREFIX = "";
public static final String DEFAULT_SUFFIX = "";
private static final String[] SERVLET_LOCATIONS = { "/" };
// SpringMVC解决表单提交put/delete问题
@Bean
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}
// 表单内容校验
@Bean
@ConditionalOnMissingBean(HttpPutFormContentFilter.class)
@ConditionalOnProperty(prefix = "spring.mvc.formcontent.putfilter", name = "enabled", matchIfMissing = true)
public OrderedHttpPutFormContentFilter httpPutFormContentFilter() {
return new OrderedHttpPutFormContentFilter();
}
// 标注是一个配置类
@Configuration
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter
implements WebMvcConfigurer, ResourceLoaderAware {
// 配置信息转换的转换器
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.addAll(this.messageConverters.getConverters());
}
// 配置请求异步处理
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
Duration timeout = this.mvcProperties.getAsync().getRequestTimeout();
if (timeout != null) {
configurer.setDefaultTimeout(timeout.toMillis());
}
}
// 配置统一路径
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.setUseSuffixPatternMatch(
this.mvcProperties.getPathmatch().isUseSuffixPattern());
configurer.setUseRegisteredSuffixPatternMatch(
this.mvcProperties.getPathmatch().isUseRegisteredSuffixPattern());
}
// 配置内容协商处理
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
WebMvcProperties.Contentnegotiation contentnegotiation = this.mvcProperties
.getContentnegotiation();
configurer.favorPathExtension(contentnegotiation.isFavorPathExtension());
configurer.favorParameter(contentnegotiation.isFavorParameter());
if (contentnegotiation.getParameterName() != null) {
configurer.parameterName(contentnegotiation.getParameterName());
}
Map<String, MediaType> mediaTypes = this.mvcProperties.getContentnegotiation()
.getMediaTypes();
mediaTypes.forEach(configurer::mediaType);
}
// 配置视图解析器
@Bean
@ConditionalOnMissingBean
public InternalResourceViewResolver defaultViewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix(this.mvcProperties.getView().getPrefix());
resolver.setSuffix(this.mvcProperties.getView().getSuffix());
return resolver;
}
// 配置BeanNameViewResolver
@Bean
@ConditionalOnBean(View.class)
@ConditionalOnMissingBean
public BeanNameViewResolver beanNameViewResolver() {
BeanNameViewResolver resolver = new BeanNameViewResolver();
resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
return resolver;
}
// 注册Converter
@Override
public void addFormatters(FormatterRegistry registry) {
for (Converter<?, ?> converter : getBeansOfType(Converter.class)) {
registry.addConverter(converter);
}
for (GenericConverter converter : getBeansOfType(GenericConverter.class)) {
registry.addConverter(converter);
}
for (Formatter<?> formatter : getBeansOfType(Formatter.class)) {
registry.addFormatter(formatter);
}
}
private <T> Collection<T> getBeansOfType(Class<T> type) {
return this.beanFactory.getBeansOfType(type).values();
}
// 添加资源映射
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
CacheControl cacheControl = this.resourceProperties.getCache()
.getCachecontrol().toHttpCacheControl();
// 关于 /webjars/** 请求来到 classpath:/META-INF/resources/webjars/ 目录下
if (!registry.hasMappingForPattern("/webjars/**")) {
customizeResourceHandlerRegistration(registry
.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/")
.setCachePeriod(getSeconds(cachePeriod))
.setCacheControl(cacheControl));
}
// 获取到 spring.mvc.staticPathPattern 配置(默认为/**)
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
customizeResourceHandlerRegistration(
registry.addResourceHandler(staticPathPattern)
/*
将此路径映射到: spring.resources.staticLocations
spring.resources.staticLocations 默认为: {
"classpath:/META-INF/resources/", "classpath:/resources/",
"classpath:/static/", "classpath:/public/"
}
*/
.addResourceLocations(
getResourceLocations(this.resourceProperties.getStaticLocations())
).setCachePeriod(getSeconds(cachePeriod))
.setCacheControl(cacheControl));
}
}
private Integer getSeconds(Duration cachePeriod) {
return (cachePeriod == null ? null : (int) cachePeriod.getSeconds());
}
// 欢迎页面映射
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(
ApplicationContext applicationContext) {
return new WelcomePageHandlerMapping(
new TemplateAvailabilityProviders(applicationContext),
applicationContext, getWelcomePage(),
this.mvcProperties.getStaticPathPattern());
}
// 在staticLocations的基础上又加上了"/","/"代表webapp目录下的资源也可以被访问
static String[] getResourceLocations(String[] staticLocations) {
String[] locations = new String[staticLocations.length
+ SERVLET_LOCATIONS.length];
System.arraycopy(staticLocations, 0, locations, 0, staticLocations.length);
// SERVLET_LOCATIONS="/"
System.arraycopy(SERVLET_LOCATIONS, 0, locations, staticLocations.length,
SERVLET_LOCATIONS.length);
return locations;
}
private Optional<Resource> getWelcomePage() {
String[] locations = getResourceLocations(
this.resourceProperties.getStaticLocations());
// 获取staticLocations配置的4个映射路径
return Arrays.stream(locations)
.map(this::getIndexHtml) // 在四个类路径下寻找index.html
.filter(this::isReadable) // 过滤掉不存在的
.findFirst(); // 只要第一个
}
private Resource getIndexHtml(String location) {
return this.resourceLoader.getResource(location + "index.html");
}
private boolean isReadable(Resource resource) {
try {
return resource.exists() && (resource.getURL() != null);
}
catch (Exception ex) {
return false;
}
}
// 欢迎图标的映射
@Configuration
@ConditionalOnProperty(value = "spring.mvc.favicon.enabled", matchIfMissing = true)
public static class FaviconConfiguration implements ResourceLoaderAware {
private final ResourceProperties resourceProperties;
private ResourceLoader resourceLoader;
public FaviconConfiguration(ResourceProperties resourceProperties) {
this.resourceProperties = resourceProperties;
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
@Bean
public SimpleUrlHandlerMapping faviconHandlerMapping() {
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
mapping.setUrlMap(Collections.singletonMap("**/favicon.ico",
faviconRequestHandler()));
return mapping;
}
@Bean
public ResourceHttpRequestHandler faviconRequestHandler() {
ResourceHttpRequestHandler requestHandler = new ResourceHttpRequestHandler();
requestHandler.setLocations(resolveFaviconLocations());
return requestHandler;
}
private List<Resource> resolveFaviconLocations() {
String[] staticLocations = getResourceLocations(
this.resourceProperties.getStaticLocations());
List<Resource> locations = new ArrayList<>(staticLocations.length + 1);
Arrays.stream(staticLocations).map(this.resourceLoader::getResource)
.forEach(locations::add);
locations.add(new ClassPathResource("/"));
return Collections.unmodifiableList(locations);
}
}
}
}
1.6.2 关于webjars资源
1)分析源码
查看WebMvcAutoConfiguration类的addResourceHandlers方法源码如下:
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
CacheControl cacheControl = this.resourceProperties.getCache()
.getCachecontrol().toHttpCacheControl();
if (!registry.hasMappingForPattern("/webjars/**")) {
// 如果访问的是/webjars/** 则去 /META-INF/resources/webjars/ 目录下寻找资源
customizeResourceHandlerRegistration(registry
.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/")
.setCachePeriod(getSeconds(cachePeriod))
.setCacheControl(cacheControl));
}
后面代码省略....
}
2)定制webjars访问路径
package com.dfbz;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author lscl
* @version 1.0
* @intro:
*/
@Configuration
//@EnableWebMvc // 该注解表示定制SpringMVC,即SpringBoot配置的SpringMVC相关组件全部失效
public class MyWebMvcAutoConfiguration implements WebMvcConfigurer {
@Value("${spring.mvc.my-webjars-locaitons}")
private String locations;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 与SpringBoot默认的webjars目录并不是互补配置
registry.addResourceHandler("/webjars/**").addResourceLocations(locations);
}
}
Tips:配置之后,SpringBoot默认的webjars资源路径将会失效;
application.yml:
server:
port: 8080
spring:
mvc:
my-webjars-locaitons: classpath:/webjars/
分别在classpath目录下的/webjars
、``/static`目录添加资源:
访问:
http://localhost:8080/demo01.js
http://localhost:8080/demo02.js
1.6.3 关于静态资源
1)分析源码
在WebMvcAutoConfiguration类的addResourceHandlers方法下半部分代码是关于其他静态资源处理的:
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
前半部分代码省略....
// 获取SpringBoot默认的访问路径(/**)
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
customizeResourceHandlerRegistration(
registry.addResourceHandler(staticPathPattern) // 如果访问 /**
.addResourceLocations(getResourceLocations(
this.resourceProperties.getStaticLocations())) // 就去这里找资源(5个目录)
.setCachePeriod(getSeconds(cachePeriod))
.setCacheControl(cacheControl));
}
}
- getResourceLocations方法:主要是将根(/)目录加入到SpringBoot的4个静态资源目录中,成为5个静态资源目录
static String[] getResourceLocations(String[] staticLocations) {
// SERVLET_LOCATIONS
String[] locations = new String[staticLocations.length
+ SERVLET_LOCATIONS.length];
System.arraycopy(staticLocations, 0, locations, 0, staticLocations.length);
System.arraycopy(SERVLET_LOCATIONS, 0, locations, staticLocations.length,
SERVLET_LOCATIONS.length);
return locations;
}
2)根目录问题
我们之前分析过,当修改了SpringBoot的默认静态资源目录后,唯独/META-INF/resources资源仍然可以被访问;这是为什么呢?
- 修改application.yml:
server:
port: 8080
spring:
resources:
# 更改静态资源目录
static-locations: # 默认为/META-INF/resources/、/resources/、/static/、/public/、/
- classpath:/aaa
- classpath:/bbb
- 分析WebMvcAutoConfiguration源码:
- 查看jar包的targer目录:
正因为根(/)目录是classpath:/META-INF/resources
目录,这就是将SpringBoot的静态资源目录修改为aaa、bbb的时候,只有classpath:META-INF/resources
目录有效,其他三个均不生效的原因;
二、SpringBoot错误处理
2.1 SpringBoot错误处理规则
当我们访问服务器一个不存在的地址时,SpringBoot会封装一个好一个完整的响应给到客户端,并且能够根据客户端的不同来选择响应的数据:
当使用浏览器发送请求时,响应的是一个页面:
当使用Postman等接口工具时,响应的则是一个json数据:
2.1.1 定制错误页面
当SpringBoot出现错误提供错误页面时,会将请求转发到 /error/错误码.html
,我们只需要在/static/error/错误码.html
目录下提供对应的错误页面即可:
- 准备一个Controller:
package com.dfbz.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* @author lscl
* @version 1.0
* @intro:
*/
@Controller
public class HelloController {
@GetMapping("/hello")
@ResponseBody
public String hello() {
return "hello";
}
@GetMapping("/demo01")
@ResponseBody
public String demo01() {
int i = 1 / 0;
return "demo01...";
}
@PostMapping("/demo02") // post请求,如果不是post请求则出现405
@ResponseBody
public String demo02() {
return "demo02...";
}
}
在浏览器地址栏访问:
http://localhost:8080/demo01(出现500)
http://localhost:8080/demo02(出现405)
我们也可以进行错误页面的模糊匹配,即提供4xx、5xx处理4xx的错误和5xx的错误:
Tips:当4xx与404冲突时,精确优先(优先寻找精确的状态码.html)
2.2.2 定制错误json数据响应
1)定义异常处理器
第一种方法,定义异常处理器:
package com.dfbz.exceptionHandler;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.HashMap;
import java.util.Map;
/**
* @author lscl
* @version 1.0
* @intro:
*/
@ControllerAdvice
public class MyExceptionHandler {
@ResponseBody
@ExceptionHandler(ArithmeticException.class)
public Map<String,Object> handleException(Exception e){
Map<String,Object> map = new HashMap<>();
map.put("ms1","hello");
map.put("ms2","world");
return map;
}
}
上面的处理方法有个弊端:
- 1)只能处理服务器端的错误,不能处理4xx的错误
- 2)永远只能返回json数据,不能自适应客户端(如果客户端是浏览器应该响应的是一个视图)
2)转发error请求
我们可以采用SpringBoot提供的方式,如果出现了异常则转发到 /error,交给SpringBoot去处理;
- 修改异常处理器:
@ExceptionHandler(ArithmeticException.class)
public String handleException(Exception e, Model model) {
// 传入我们自己的错误状态码 4xx 5xx,否则就不会进入定制错误页面的解析流程
model.addAttribute("javax.servlet.error.status_code", 500);
// 进行业务处理.....
// 然后转发到 /error 请求,交给SpringBoot处理后续操作
return "forward:/error";
}
此时分别采用浏览器和Postman测试工具发送请求:http://localhost:8080/demo01(出现异常);发现存在自适应效果
但是我们发现SpringBoot响应Json数据时,永远是如下的数据:
{
"timestamp": "2021-09-21T10:15:28.092+0000",
"status": 500,
"error": "Internal Server Error",
"message": "/ by zero",
"path": "/hello"
}
我们能否定制化错误信息呢?当然是可以的;
2.2.3 定制错误数据封装属性类
SpringBoot的错误Json数据都是由DefaultErrorAttributes类进行封装的;我们想要携带自己的错误信息,可以继承该类,重写其方法,然后添加我们自己的错误信息:
package com.dfbz.controller;
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.WebRequest;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @author lscl
* @version 1.0
* @intro:
*/
@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest,
boolean includeStackTrace) {
// 保留SpringBoot原来的错误信息
Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
// 在原来的基础上添加我们自己的错误信息
map.put("message1","aaa");
map.put("message2","bbb");
return map;
}
}
使用Postman发送数据测试:
2.2.4 定制错误处理请求处理类
我们知道SpringBoot在处理异常时会将请求转发给/error,然后继续后续操作;我们可以定义一个Controller来接收/error请求;
- 自定义错误处理Controller:
package com.dfbz.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author lscl
* @version 1.0
* @intro: 自定义错误请求控制器
*/
@RestController
public class MyErrorController {
@RequestMapping("/error")
public String error() {
return "我是自定义的ErrorController....";
}
}
但是启动服务器后出现如下错误:
public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
to {[/error]}: There is already 'myErrorController' bean method
提示/error路径已经已经被其他Controller定义了,原因很简单,因为SpringBoot内部已经定义了一个Controller来处理/error路径,我们再定义一个路径肯定跟SpringBoot定义的冲突了;
我们可以将我们的Controller实现SpringBoot相应的规范:
package com.dfbz.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author lscl
* @version 1.0
* @intro: 自定义错误请求控制器
*/
@RestController
public class MyErrorController implements ErrorController {
// 读取server.error.path配置,如果没有就读取error.path,如果没有就为/error
@Value("${server.error.path:${error.path:/error}}")
private String errorPath;
// 如果出现了错误执行我们自定义的逻辑来处理
@RequestMapping("${server.error.path:${error.path:/error}}")
public String result() {
return "我是自定义的ErrorController...." + errorPath;
}
@Override
public String getErrorPath() {
return errorPath;
}
}
重启服务器,访问http://localhost:8080/demo01:
很显然我们自定义的Controller肯定没有自适应功能
浏览器访问:
postman访问:
2.2 SpringBoot 错误处理原理
通过前面的学习我们已经能够明白,自定义属性封装类
2.2.1 ErrorMvcAutoConfiguration源码分析
SpringBoot关于错误处理的配置都在ErrorMvcAutoConfiguration
类中有描述:
package org.springframework.boot.autoconfigure.web.servlet.error;
@Configuration
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties({ ServerProperties.class, ResourceProperties.class })
public class ErrorMvcAutoConfiguration {
private final ServerProperties serverProperties;
private final List<ErrorViewResolver> errorViewResolvers;
// 从IOC容器里面获取ServerProperties给自身赋值
public ErrorMvcAutoConfiguration(ServerProperties serverProperties,
ObjectProvider<List<ErrorViewResolver>> errorViewResolversProvider) {
// 赋值
this.serverProperties = serverProperties;
this.errorViewResolvers = errorViewResolversProvider.getIfAvailable();
}
// 默认的错误属性配置类(如果容器没有配置ErrorAttributes才配置默认的DefaultErrorAttributes)
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes(
this.serverProperties.getError().isIncludeException());
}
// 默认的错误请求处理类(如果容器没有配置ErrorController才配置默认的BasicErrorController)
@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
this.errorViewResolvers);
}
// 错误页面定制化器
@Bean
public ErrorPageCustomizer errorPageCustomizer() {
return new ErrorPageCustomizer(this.serverProperties);
}
......
}
如果我们自己定义了ErrorAttributes、ErrorController等组件,那么SpringBoot提供的将不会生效;
2.2.2 ErrorController源码分析
1)BasicErrorController源码分析-视图解析
SpringBoot出现错误后将会把请求转发到/error
,SpringBoot内部定义了ErrorController类来处理错误请求,我们之所以能看到错误自适应效果都是因为SpringBoot内部提供的ErrorController帮我们处理的:
查看BasicErrorController源码:
package org.springframework.boot.autoconfigure.web.servlet.error;
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
private final ErrorProperties errorProperties;
public BasicErrorController(ErrorAttributes errorAttributes,
ErrorProperties errorProperties) {
this(errorAttributes, errorProperties, Collections.emptyList());
}
// 从IOC容器中获取ErrorAttributes、ErrorProperties给自身属性赋值
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();
}
// 如果客户端能够处理text/html类型的响应(浏览器发送过来的请求,Accept:text/html)
@RequestMapping(produces = "text/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 ? new ModelAndView("error", model) : modelAndView);
}
// 处理除了Accept:text/html之外请求的响应
@RequestMapping
@ResponseBody
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<>(body, status);
}
protected boolean isIncludeStackTrace(HttpServletRequest request,
MediaType produces) {
// 获取ErrorProperties对象的IncludeStacktrace参数
IncludeStacktrace include = getErrorProperties().getIncludeStacktrace();
// 如果是ALWAYS就返回true
if (include == IncludeStacktrace.ALWAYS) {
return true;
}
if (include == IncludeStacktrace.ON_TRACE_PARAM) {
return getTraceParameter(request);
}
return false;
}
// 获取ErrorProperties对象
protected ErrorProperties getErrorProperties() {
return this.errorProperties;
}
}
- 响应状态码源码(AbstractErrorController):
protected HttpStatus getStatus(HttpServletRequest request) {
Integer statusCode = (Integer) request
.getAttribute("javax.servlet.error.status_code");
if (statusCode == null) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
try {
return HttpStatus.valueOf(statusCode);
}
catch (Exception ex) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
}
响应状态码是从request中获取javax.servlet.error.status_code
属性值
- 解析视图源码:
而在ErrorMvcAutoConfiguration
配置类中创建了该对象:
- 如果是来自浏览器的访问:
调用DefaultErrorViewReslover
来解析视图:
DefaultErrorViewResolver
源码如下:
// 解析视图
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
Map<String, Object> model) {
// 状态码为500
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) {
// error/500
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) {
// 获取SringBoot的四个默认目录
for (String location : this.resourceProperties.getStaticLocations()) {
try {
Resource resource = this.applicationContext.getResource(location);
resource = resource.createRelative(viewName + ".html");
// 判断哪个目录下存在500.html文件
if (resource.exists()) {
return new ModelAndView(new HtmlResourceView(resource), model);
}
}
catch (Exception ex) {
}
}
return null;
}
2)BasicErrorController源码分析-数据封装
前面我们分析BasicErrorController对于浏览器发送的请求是如何处理的,接下来我们分析BasicErrorController对于非浏览器发送的请求该如何处理;
在BasicErrorController的构造方法中对ErrorAttribute类进行了赋值(该ErrorAttribute是从IOC容器中获取的):
调用了父类(AbstractErrorController)的构造方法:
BasicErrorController对于非浏览器发送过来的请求出现异常时则是走下面的方法来处理:
- getErrorAttributes方法:
protected Map<String, Object> getErrorAttributes(HttpServletRequest request,boolean includeStackTrace) {
WebRequest webRequest = new ServletWebRequest(request);
// 调用了ErrorAttribute类的getErrorAttribute方法来获取错误信息
return this.errorAttributes.getErrorAttributes(webRequest, includeStackTrace);
}
最终错误信息的获取还是调用ErrorAttibute类的getErrorAttribute方法来获取:
而此时的ErrorAttribute类正是我们提供的MyErrorAttribute类;
- 关于ErrorProperties类:该类是关于错误信息的属性配置类;但该类上面并未标注@ConfigurationProperties注解,那我们该如何修改里面的值呢?
我们打开源码ErrorMvcAutoConfiguration:
ErrorProperties是在创建BasicErrorController时==通过ServerProperties来获取的ErrorProperties==,也就是说ErrorProperties存在与ServerProperties内部;
因此我们在application.yml中配置:
server:
port: 8080
error:
include-stacktrace: always # 开启异常追踪信息
最终把参数传递给了ErrorAttribute类:
传递给了我们自定义的ErrorAttribute:
3)自定义ErrorController
package com.dfbz.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.boot.autoconfigure.web.ResourceProperties;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.context.ApplicationContext;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
/**
* @author lscl
* @version 1.0
* @intro: 自定义错误请求控制器
*/
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class MyErrorController implements ErrorController {
private ApplicationContext applicationContext;
private ResourceProperties resourceProperties;
private ErrorAttributes errorAttributes;
private ErrorProperties errorProperties;
public MyErrorController(ApplicationContext applicationContext,
ResourceProperties resourceProperties,
ErrorAttributes errorAttributes,
ServerProperties serverProperties) {
this.applicationContext = applicationContext;
this.resourceProperties = resourceProperties;
this.errorAttributes = errorAttributes;
this.errorProperties = serverProperties.getError();
}
// 读取server.error.path配置,如果没有就读取error.path,如果没有就为/error
@Value("${server.error.path:${error.path:/error}}")
private String errorPath;
/**
* 处理浏览器发送的请求导致的异常
* @param request
* @param response
* @return
*/
@RequestMapping(produces = "text/html")
public String response_html(HttpServletRequest request, HttpServletResponse response) {
// 获取到响应状态码
Integer statusCode = (Integer) request
.getAttribute("javax.servlet.error.status_code");
// 设置响应状态码
response.setStatus(statusCode);
// 拼接视图名称
String viewName = "error/" + statusCode;
// 获取4个默认静态资源访问路径
for (String location : this.resourceProperties.getStaticLocations()) {
try {
Resource resource = this.applicationContext.getResource(location);
resource = resource.createRelative(viewName + ".html");
// 如果资源存在,就返回
if (resource.exists()) {
return viewName + ".html";
}
} catch (Exception ex) {
}
}
return null;
}
/**
* 处理非浏览器发送的请求导致的异常
* @param request
* @return
*/
@RequestMapping
@ResponseBody
public Map<String, Object> response_json(HttpServletRequest request) {
// 包装成WebRequest对象
WebRequest webRequest = new ServletWebRequest(request);
// 获取errorProperties中配置的includeStacktrace属性
ErrorProperties.IncludeStacktrace include = errorProperties.getIncludeStacktrace();
Boolean isIncludeStackTrace = false;
if (include == ErrorProperties.IncludeStacktrace.ALWAYS) {
isIncludeStackTrace = true;
}
// 调用ErrorAttributes获取错误信息
Map<String, Object> model = this.errorAttributes.getErrorAttributes(webRequest, isIncludeStackTrace);
return model;
}
@Override
public String getErrorPath() {
return errorPath;
}
}
浏览器访问:
postman访问:
达到了自适应效果;
2.2.3 ErrorAttribute源码分析
1)DefaultErrorAttributes源码分析
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {
private static final String ERROR_ATTRIBUTE = DefaultErrorAttributes.class.getName() + ".ERROR";
private final boolean includeException;
public DefaultErrorAttributes() {
this(false);
}
public DefaultErrorAttributes(boolean includeException) {
this.includeException = includeException;
}
// 省略部分代码...
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap();
// 添加timestamp
errorAttributes.put("timestamp", new Date());
// 添加状态码
this.addStatus(errorAttributes, webRequest);
// 错误信息
this.addErrorDetails(errorAttributes, webRequest, includeStackTrace);
// 添加路径信息
this.addPath(errorAttributes, webRequest);
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,
WebRequest webRequest, boolean includeStackTrace) {
// 通过getError获取触发的异常信息
Throwable error = getError(webRequest);
if (error != null) {
while (error instanceof ServletException && error.getCause() != null) {
error = ((ServletException) error).getCause();
}
// 如果为true就添加异常类信息
if (this.includeException) {
errorAttributes.put("exception", error.getClass().getName());
}
addErrorMessage(errorAttributes, error);
// 如果为true就添加异常追踪信息
if (includeStackTrace) {
addStackTrace(errorAttributes, error);
}
}
Object message = getAttribute(webRequest, "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);
}
}
// 获取触发的异常对象
@Override
public Throwable getError(WebRequest webRequest) {
Throwable exception = getAttribute(webRequest, ERROR_ATTRIBUTE);
if (exception == null) {
exception = getAttribute(webRequest, "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)自定义ErrorAttribute
package com.dfbz.controller;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.util.NestedServletException;
import java.util.HashMap;
import java.util.Map;
/**
* @author lscl
* @version 1.0
* @intro:
*/
@Component
public class MyErrorAttributes implements ErrorAttributes {
/**
* 封装错误属性遵守我们自己的规则
*
* @param webRequest: Request的封装对象,可以获取请求相关书籍
* @param includeStackTrace: 是否要打印追踪异常最终信息,通过如下配置可以开启异常信息的打印
* server:
* error:
* include-stacktrace: always
* @return
*/
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
HashMap<String, Object> body = new HashMap<>();
// 添加自己的错误信息
body.put("message1", "aaa");
body.put("message2", "bbb");
if (includeStackTrace) {
Throwable error = getError(webRequest);
// 打印错误追踪信息
error.printStackTrace();
body.put("trace", error.getMessage());
}
return body;
}
// 添加追踪信息时异常的类型
@Override
public Throwable getError(WebRequest webRequest) {
// 获取实际触发的异常
NestedServletException attribute = (NestedServletException) webRequest.getAttribute("javax.servlet.error.exception", WebRequest.SCOPE_REQUEST);
Throwable exception = attribute.getCause();
return exception;
}
}