02【SpringBoot静态处理、错误处理】

目录

一、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 {@Overrideprotected 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 {@Overridepublic void init(FilterConfig filterConfig) throws ServletException {System.out.println("test filter...");}@Overridepublic 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...");}@Overridepublic 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 {@Overridepublic void contextInitialized(ServletContextEvent sce) {System.out.println("contextInitialized...");}@Overridepublic 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*/@Beanpublic ServletRegistrationBean testServlet(){ServletRegistrationBean testServletBean =new ServletRegistrationBean(new TestServlet(),"/test");return testServletBean;}/*** 注册Filter* @return*/@Beanpublic FilterRegistrationBean testFilter(){FilterRegistrationBean testFilterBean = new FilterRegistrationBean();testFilterBean.setFilter(new TestFilter());testFilterBean.setUrlPatterns(Arrays.asList("/*"));return testFilterBean;}/*** 注册Listener* @return*/@Beanpublic 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 {@Overrideprotected 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.classResourceProperties.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 WebMvcAutoConfigurationAdapterimplements WebMvcConfigurer, ResourceLoaderAware {// 配置信息转换的转换器@Overridepublic void configureMessageConverters(List<HttpMessageConverter<?>> converters) {converters.addAll(this.messageConverters.getConverters());}// 配置请求异步处理@Overridepublic void configureAsyncSupport(AsyncSupportConfigurer configurer) {Duration timeout = this.mvcProperties.getAsync().getRequestTimeout();if (timeout != null) {configurer.setDefaultTimeout(timeout.toMillis());}}// 配置统一路径@Overridepublic void configurePathMatch(PathMatchConfigurer configurer) {configurer.setUseSuffixPatternMatch(this.mvcProperties.getPathmatch().isUseSuffixPattern());configurer.setUseRegisteredSuffixPatternMatch(this.mvcProperties.getPathmatch().isUseRegisteredSuffixPattern());}// 配置内容协商处理@Overridepublic 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@ConditionalOnMissingBeanpublic 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)@ConditionalOnMissingBeanpublic BeanNameViewResolver beanNameViewResolver() {BeanNameViewResolver resolver = new BeanNameViewResolver();resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);return resolver;}// 注册Converter@Overridepublic 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();}// 添加资源映射@Overridepublic 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.staticLocationsspring.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());}// 欢迎页面映射@Beanpublic 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;}@Overridepublic void setResourceLoader(ResourceLoader resourceLoader) {this.resourceLoader = resourceLoader;}@Beanpublic SimpleUrlHandlerMapping faviconHandlerMapping() {SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();mapping.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);mapping.setUrlMap(Collections.singletonMap("**/favicon.ico",faviconRequestHandler()));return mapping;}@Beanpublic 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;@Overridepublic 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_LOCATIONSString[] 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")@ResponseBodypublic String hello() {return "hello";}@GetMapping("/demo01")@ResponseBodypublic String demo01() {int i = 1 / 0;return "demo01...";}@PostMapping("/demo02")			// post请求,如果不是post请求则出现405@ResponseBodypublic 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 {@Overridepublic 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;}@Overridepublic 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);}// 错误页面定制化器@Beanpublic 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;}@Overridepublic 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@ResponseBodypublic 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就返回trueif (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) {// 状态码为500ModelAndView 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/500String 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: 8080error: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@ResponseBodypublic 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;}@Overridepublic 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();// 添加timestamperrorAttributes.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 reasonerrorAttributes.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);}}// 获取触发的异常对象@Overridepublic 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*/@Overridepublic 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;}// 添加追踪信息时异常的类型@Overridepublic Throwable getError(WebRequest webRequest) {// 获取实际触发的异常NestedServletException attribute = (NestedServletException) webRequest.getAttribute("javax.servlet.error.exception", WebRequest.SCOPE_REQUEST);Throwable exception = attribute.getCause();return exception;}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/204021.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

java学习part17final

110-面向对象(高级)-关键字final的使用及真题_哔哩哔哩_bilibili 1.概念 tips&#xff1a;java里有const关键字&#xff0c;但是用于保留字&#xff0c;不会使用&#xff0c;目前没有意义。 final变量没有默认赋值&#xff0c;只能在以下三个地方赋值&#xff0c;且只能赋值一…

03、K-means聚类实现步骤与基于K-means聚类的图像压缩(1)

03、K-means聚类实现步骤与基于K-means聚类的图像压缩&#xff08;1&#xff09; 03、K-means聚类实现步骤与基于K-means聚类的图像压缩&#xff08;1&#xff09; 03、K-means聚类实现步骤与基于K-means聚类的图像压缩&#xff08;2&#xff09; 开始学习机器学习啦&#xf…

【数据库】基于排序算法的去重,集合与包的并,差,交,连接操作实现原理,执行代价以及优化

基于两趟排序的其它操作 ​专栏内容&#xff1a; 手写数据库toadb 本专栏主要介绍如何从零开发&#xff0c;开发的步骤&#xff0c;以及开发过程中的涉及的原理&#xff0c;遇到的问题等&#xff0c;让大家能跟上并且可以一起开发&#xff0c;让每个需要的人成为参与者。 本专栏…

【Android】Android Framework系列--Launcher3各启动场景源码分析

Android Framework系列–Launcher3各启动场景源码分析 Launcher3启动场景 Launcher3是Android系统提供的默认桌面应用(Launcher)&#xff0c;它的源码路径在“packages/apps/Launcher3/”。 Launcher3的启动场景主要包括&#xff1a; 开机后启动&#xff1a;开机时&#xff…

【开源】基于JAVA的开放实验室管理系统

项目编号&#xff1a; S 013 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S013&#xff0c;文末获取源码。} 项目编号&#xff1a;S013&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、研究内容2.1 实验室类型模块2.2 实验室模块2.3 实…

Java网络爬虫实战

List item 文章目录 ⭐️写在前面的话⭐️&#x1f4cc;What is it?分类网络爬虫按照系统结构和实现技术&#xff0c;大致可以分为以下几种类型&#xff1a;通用网络爬虫&#xff08;General Purpose Web Crawler&#xff09;、聚焦网络爬虫&#xff08;Focused Web Crawler&a…

计算机丢失vcomp140.dll是什么意思,如何解决与修复(附教程)

vcomp140.dll缺失的5种解决方法以及vcomp140.dll缺失原因 引言&#xff1a; 在日常使用电脑的过程中&#xff0c;我们可能会遇到一些错误提示&#xff0c;其中之一就是“vcomp140.dll缺失”。这个错误提示通常出现在运行某些程序或游戏时&#xff0c;给使用者带来了困扰。本文…

github访问失败

1. 问题场景 今天了解到notepad可以安装许多插件&#xff0c;但是自动下载插件时总是失败&#xff0c;这些插件的下载源都是github&#xff0c;将地址复制到浏览器也打不开&#xff0c;所以查了下github的访问问题&#xff0c;目前插件已正常下载。 2. 解决方法 gitee上搜索…

大数据数据仓库,Sqoop--学习笔记

数据仓库介绍 1. 数据仓库概念 数据仓库概念创始人在《建立数据仓库》一书中对数据仓库的定义是&#xff1a;数据仓库&#xff08;Data Warehouse&#xff09;是一个面向主题的&#xff08;Subject Oriented&#xff09;、数据集成的&#xff08;Integrated&#xff09;、相对…

【MATLAB源码-第86期】基于matlab的QC-LDPC码性能仿真,输出误码率曲线。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 QC-LDPC&#xff08;准循环低密度奇偶校验&#xff09;编码是一种高效的错误校正编码方式&#xff0c;广泛应用于通信系统和数据存储中以提高数据的可靠性。它是低密度奇偶校验&#xff08;LDPC&#xff09;编码的一种特殊形…

Zookeeper分布式锁实现Curator十一问

前面我们通过Redis分布式锁实现Redisson 15问文章剖析了Redisson的源码&#xff0c;理清了Redisson是如何实现的分布式锁和一些其它的特性。这篇文章就来接着剖析Zookeeper分布式锁的实现框架Curator的源码&#xff0c;看看Curator是如何实现Zookeeper分布式锁的&#xff0c;以…

wpf使用CefSharp.OffScreen模拟网页登录,并获取身份cookie,C#后台执行js

目录 框架信息&#xff1a;MainWindow.xamlMainWindow.xaml.cs爬取逻辑模拟登录拦截请求Cookie获取 CookieVisitorHandle完整代码参考项目 框架信息&#xff1a; CefSharp.OffScreen.NETCore 109.1.110 .NET 7 CefSharp.OffScreen.NETCore v114.2.100 第一版使用这个&#x…

shiro整合redis

shiro整合redis 前言&#xff1a;shiro默认的session是存储在jvm内存中的&#xff0c;这样会导致java服务内存占用更大以及一旦服务器宕机或者版本迭代需要重启服务时&#xff0c;缓存中的数据不能恢复&#xff0c;导致用户需要重新登录认证&#xff0c;体验很差。因此利用第三…

C++ 泛型编程,函数模版和类模版

1.泛型编程 泛型编程&#xff1a;编写与类型无关的通用代码&#xff0c;是代码复用的一种手段。模板是泛型编程的基础 就比如说活字印刷术&#xff0c;就是提供一个模具&#xff0c;然后根据模具来印刷出不同的字。 泛型编程跟着类似&#xff0c;提供一个模版&#xff0c;根据这…

哈希表——闭散列表

该哈希表实现是闭散列实现法。 闭散列表&#xff1a; 闭散列&#xff1a;也叫开放定址法&#xff0c;当发生哈希冲突时&#xff0c;如果哈希表未被装满&#xff0c;说明在哈希表中必然还有空位置&#xff0c;那么可以把key存放到冲突位置中的“下一个” 空位置中去。 那如何寻…

96.STL-遍历算法 transform

目录 transform 语法&#xff1a; 功能描述&#xff1a; 函数原型&#xff1a; 代码示例&#xff1a; transform 是 C 标准模板库&#xff08;STL&#xff09;中的一个算法&#xff0c;用于对一个范围内的元素进行转换并将结果存储到另一个范围。以下是简要解释和一个示例…

【开源】基于JAVA的衣物搭配系统

项目编号&#xff1a; S 016 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S016&#xff0c;文末获取源码。} 项目编号&#xff1a;S016&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、研究内容2.1 衣物档案模块2.2 衣物搭配模块2.3 衣…

成为AI产品经理——TPR、FPR、ROC、AUC

目录 一、PR图、BEP 1.PR图 2.BEP 二、灵敏度、特异度 1.灵敏度 2.特异度 三、真正率、假正率 1.真正率 2.假正率 三、ROC、AUC 1.ROC 2.AUC 四、KS值 一、PR图、BEP 1.PR图 二分类问题模型通常输出的是一个概率值&#xff0c;我们需要设定一个阈值&#xff…

手机爬虫用Fiddler详细教程

如果你正在进行手机爬虫的工作&#xff0c;那么一款强大而又实用的网络调试工具Fiddler将会是你的好帮手。今天&#xff0c;我将和大家分享一份详细的Fiddler教程&#xff0c;教你如何使用它来轻松捕获和分析手机App的网络请求。让我们一起来探索Fiddler的功能和操作&#xff0…