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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

银升玻璃能源数据采集监控方案

项目背景 监控配电房电表的电压、电流、功率、总电能等数据。 监控配电房变压器的电流、温度数据&#xff0c;在数据超出额定指标时&#xff0c;进行报警推送。 现场调研情况 经过现场调研&#xff0c;共有3个变压器房&#xff0c;有不同类型的电表。具体如下表&#xff1a…

Day27|Leetcode 39. 组合总和 Leetcode 40. 组合总和 II Leetcode131. 分割回文串

Leetcode 39. 组合总和 题目链接 39 组合总和 本题目和前面的组合问题差不多&#xff0c;只不过这里能重复选取数字&#xff0c;还是要注意组合的定义&#xff0c;交换数字顺序还是算一个组合&#xff0c;所以这里还是用我们的startIndex来记录取的数字到哪里了&#xff0c;下…

PowerQuery领域的经典之作“猴子书“中文版来啦!

与数据打交道&#xff0c;还在纠结于Excel、SQL、VBA、Python&#xff1f;数据处理领域经典之作PowerQuery"猴子书"让你用更聪明的方法处理数据。学完这本书&#xff0c;你就掌握了Power Query的一切&#xff0c;想要学Power Query&#xff0c;只需要这一本就够啦&am…

城市管理实景三维:打造智慧城市的新引擎

城市管理实景三维&#xff1a;打造智慧城市的新引擎 在城市管理领域&#xff0c;实景三维技术正逐渐成为推动城市发展的新引擎。通过以精准的数字模型呈现城市真实场景&#xff0c;实景三维技术为城市决策提供了全新的思路和工具。从规划设计到交通管理&#xff0c;从环境保护到…

代码随想录二刷 | 链表 |链表相交

代码随想录二刷 &#xff5c; 链表 &#xff5c;链表相交 题目描述解题思路 & 代码实现 题目描述 160.链表相交 给你两个单链表的头节点 headA 和 headB &#xff0c;请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点&#xff0c;返回 null 。 题目数据 保…

【5k字长文 | Vue学习笔记】#1 认识Vue对象和基础语法

Vue是一个非常流行的渐进式JavaScript框架&#xff0c;渐进式指的是自底向上&#xff0c;从小组件逐渐向上构成整个项目&#xff0c;渐进式还可以理解为&#xff1a;用什么就拿什么&#xff0c;每个组件只做自己的事&#xff0c;尽可能解耦合。 本节我们将学习简单的Vue实例&a…

实验4.数据全量、增量、比较更新

【实验目的】 1.利用Kettle的“表输入”&#xff0c;“表输入出”&#xff0c;”JavaScript代码”组件&#xff0c;实现数据全量更新。 2.熟练掌握“JavaScript代码”&#xff0c;“表输入”&#xff0c;“表输入出”组件的使用&#xff0c;实现数据全量更新。 【实验原理】 …

2024法定节假日|除夕不放假?企业这样做员工更满意

国务院办公厅发布了 关于2024年部分节假日安排的通知 全文如下 各省、自治区、直辖市人民政府&#xff0c;国务院各部委、各直属机构&#xff1a; 经国务院批准&#xff0c;现将2024年元旦、春节、清明节、劳动节、端午节、中秋节和国庆节放假调休日期的具体安排通知如下。 …

【OpenCV实现图像:使用OpenCV进行图像处理之透视变换】

文章目录 概要计算公式举个栗子实际应用小结 概要 透视变换&#xff08;Perspective Transformation&#xff09;是一种图像处理中常用的变换手段&#xff0c;它用于将图像从一个视角映射到另一个视角&#xff0c;常被称为投影映射。透视变换可以用于矫正图像中的透视畸变&…

CSM32RV003:国产高精度16位ADC低功耗RISC-V内核MCU

目录 高精度ADC工业应用工业数据采集应用CSM32RV003简介主要特性 高精度ADC工业应用 高精度ADC即高精度模数转换器&#xff0c;是一种能够将输入模拟信号转换为数字信号的芯片&#xff0c;在多种消费电子、工业、医疗和科研领域都有广泛应用。高精度ADC的主要特点是能够提供高…

echarts 几千条分钟级别在小时级别图标上展示

需求背景解决效果ISQQW代码地址strategyChart.vue 需求背景 需要实现 秒级数据几千条在图表上显示&#xff0c;(以下是 设计图表上是按小时界别显示数据&#xff0c;后端接口为分钟级别数据) 解决效果 ISQQW代码地址 链接 strategyChart.vue <!--/** * author: liuk *…

02房价预测

目录 代码 评分算法&#xff1a; 代码 import numpy as np from sklearn import datasets from sklearn.linear_model import LinearRegression# 指定版本才有数据集 # C:\Users\14817\PycharmProjects\pythonProject1\venv\Scripts\activate.bat # pip install scikit-le…

webpack项目 index.html 根据不同的变量引入不同的js

项目 webpack搭建 问题&#xff1a;在入口文件index.html中根据不同的变量引入不同的js 使用插件HtmlWebpackPlugin HtmlWebpackPlugin 项目里用来生成静态文件的 这个插件每个项目基本都要用到的&#xff0c;只要全局搜一下位置 根据配置文件的指令找到执行的文件&#xff0…

[点云分割] 区域增长分割

效果&#xff1a; 原始数据 分割结果 代码&#xff1a; #include <iostream> #include <vector> #include <pcl/io/pcd_io.h> #include <pcl/point_types.h> // 各种点云数据类型 #include <pcl/search/search.h> #include <pcl/search/kdtr…

【Java】基于SaaS模式的Java基层医院卫生健康云HIS系统源码

一、模板管理 模板分为两种&#xff1a;病历模板和报表模板。模板管理是运营管理的核心组成部分&#xff0c;是基层卫生健康云中各医疗机构定制电子病历和报表的地方&#xff0c;各医疗机构可根据自身特点特色定制电子病历和报表&#xff0c;制作的电子病历及报表可直接在业务…

CSS画一条线

<p style"border: 1px solid rgba(0, 0, 0, 0.1);"></p> 效果&#xff1a;

Power Apps-Timer

插入一个计时器 右侧属性面板&#xff0c;持续时间的单位是毫秒&#xff0c;60000就是60秒&#xff08;一分钟&#xff09;&#xff1b;开启重复是指60秒结束后重新开始计时&#xff1b;自动启动是指当从其他页面进入时是否自动开始计时&#xff1b;自动暂停是指当离开这个页面…

Python语言:猜数字游戏案例讲解

猜数字游戏题目要求如下&#xff1a;该程序随机生成一个1到100之间的整数&#xff0c;然后要求玩家在有限的次数内猜出这个数字。如果玩家猜对了&#xff0c;游戏结束并显示成功信息&#xff1b;如果玩家猜错了&#xff0c;程序会提示玩家猜的数字是偏大还是偏小&#xff0c;并…

VsCode连接远程Linux编译环境的便捷处理

1.免输登录密码 免输命令的正确方法是使用公钥和私鈅在研发设备&#xff0c;和linux服务器上校验身份。公钥和私钥可在windows系统上生成。公钥要发送到linux服务器。私钥需要通知给本地的ssh客户端程序&#xff0c;相关的操作如下&#xff1a; 生成 SSH Key&#xff1a; 打开…

如何编辑WordPress配置文件wp-config.php

目录 wp-config.php文件全部内容&#xff1a; 修改wp-config.ph文件中的数据库设置&#xff1a; 设置wp-config.ph文件中的密钥部分 修改数据库表前缀 设置绝对路径 WordPress会把数据库的相关信息存在wp-config.php文件中。如果编辑有问题&#xff0c;则会出现建立数据库连…
最新文章