springboot笔记

雷神SpringBoot学习笔记

提示:边看视频边做的笔记,可能有错别字,请见谅。视频地址:https://www.bilibili.com/video/BV19K4y1L7MT?p=86&spm_id_from=pageDriver


提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录


# 一、时代背景

1、微服务时代

  • 微服务是一种架构风格

  • 一个应用拆分为一组小型服务

  • 每个服务运行在自己的进程内,也就是可独立部署和升级

  • 服务之间使用轻量级HTTP交互(轻量级HHTP主要指:是指REST API))

  • 服务围绕业务功能拆分

  • 可以由全自动部署机制独立部署

  • 去中心化,服务自治。服务可以使用不同的语言、不同的存储技术

2、分布式

  1. 分布式的困难:

    • 远程调用 :一般使用http进行服务交互

    • 服务发现 :就是看哪些服务是可用的

    • 负载均衡 :让多台服务器动起来

    • 服务容错 :各种错误情况下的处理方式

    • 配置管理 : 配置中心,修改配置让服务们自己同步

    • 服务监控 : 多个服务以及云平台的资源消耗和健康状况

    • 链路追踪 :一个复杂的业务流程可能需要连续调用多个微服务,我们需要记录一个完整业务逻辑涉及的每一个微服务的运行状态,再通过可视化链路图展现,帮助软件工程师在系统出错时分析解决问题,常见的解决方案有Zipkin,SkyWalking。

    • 日志管理 : 微服务架构默认将应用日志分散保存在每一个微服务节点上,当系统进行用户行为分析、数据统计时必须收集所有节点日志数据,非常不方便。这时候我们需要一个独立的日志平台,收集所有节点的日志数据并可方便对其进行汇总分析,然后进行可视化展示,常见的解决方案有:

      ELK(Elasticsearch+Logstash+Kibana),EFK(Elasticsearch+Fluentd+Kibana)。

    • 任务调度

  2. 分布式的解决

    SpringBoot + SpringCloud

3、云原生

  1. 原生应用如何上云。 Cloud Native

  2. 上云的困难:

    • 服务自愈:其中一个服务出现错误怎么复原

    • 弹性伸缩:根据不同的性能需求分配更多的服务器

    • 服务隔离:服务之间不相互影响

    • 自动化部署:自动化部署

    • 灰度发布:同样的服务有多台服务器运行,先把服务部署在其中一两台上看运行效果,没有问题了再慢慢全部升级

    • 流量治理:控制流量

二、springboot

  1. springboot官网地址:https://spring.io/projects/spring-boot#overview
  2. 查看版本新特性地址:https://github.com/spring-projects/spring-boot/wiki#release-notes

1、SpringBoot2入门

1.系统要求
  • Java 8 & 兼容java14 .
  • Maven 3.3+

修改maven 的配置文件 settings.xml

<mirrors>
    <!-- mirror
     | Specifies a repository mirror site to use instead of a given repository. The repository that
     | this mirror serves has an ID that matches the mirrorOf element of this mirror. IDs are used
     | for inheritance and direct lookup purposes, and must be unique across the set of mirrors.
     |
    <mirror>
      <id>mirrorId</id>
      <mirrorOf>repositoryId</mirrorOf>
      <name>Human Readable Name for this Mirror.</name>
      <url>http://my.repository.com/repo/path</url>
    </mirror>
     -->
      <!--只需在mirrors标签下加入以下内容,加入后导入下载jar速度更快-->
	  <mirror> 
	  <id>alimaven</id>
	  <name>aliyun maven</name>
	  <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
	  <mirrorOf>central</mirrorOf>
	  </mirror>
  </mirrors>

<profiles>
    <!--只需在profiles标签下加入以下内容-->
    <profile>    
        <id>jdk-1.8</id>    
         <activation>    
            <activeByDefault>true</activeByDefault>    
            <jdk>1.8</jdk>    
          </activation>    
        <properties>    
        <maven.compiler.source>1.8</maven.compiler.source>    
        <maven.compiler.target>1.8</maven.compiler.target>    
        <maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>    
        </properties>    
    </profile>
  </profiles>
2、HelloWorld

需求:浏览发送/hello请求,响应 Hello,Spring Boot 2

1、创建maven工程
2、引入依赖
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.5.3</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<dependencies>
    <dependency>
        <!--springboot web场景的依赖-->
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <!--springboot测试依赖-->
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
3、创建主程序
/**
 * 主程序类
 * @SpringBootApplication:标记这是一个SpringBoot应用
 */
@SpringBootApplication
public class Springboot_1Application {
    public static void main(String[] args) {
        SpringApplication.run(Springboot_1Application.class, args);
    }	
}
4、编写业务代码
@RestController //标记为controller 且该类返回为 json
public class HelloWordController {
    @RequestMapping("/hello")
    public String handle01(){
        return "Hello, Spring Boot 2!";
    }
}
5、运行&测试
  • 运行MainApplication
  • 浏览器输入http://localhost:8080/hello,将会输出Hello, Spring Boot 2!
6、springboot配置

完成后,可以对springboot项目进行配置 在maven工程的resource文件夹中创建application.properties文件。

所有配置的地址:https://docs.spring.io/spring-boot/docs/2.3.7.RELEASE/reference/html/appendix-application-properties.html#common-application-properties-server

# 设置端口号  访问需要 http://localhost:8888/hello
server.port=8888
7、简化部署(把项目打成jar包,直接在目标服务器执行即可)

在IDEA的Maven插件上点击运行 clean 、package,把helloworld工程项目的打包成jar包,

打包好的jar包被生成在helloworld工程项目的target文件夹内。

用cmd运行java -jar boot-01-helloworld-1.0-SNAPSHOT.jar,既可以运行helloworld工程项目。

将jar包直接在目标服务器执行即可。

<!-- 导入依赖插件 -->
<build>
	<plugins>
         <!--2.3.4.RELEASE 版本的springboot可以不用导  2.5.3 需要导入-->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-resources-plugin</artifactId>
            <version>3.1.0</version>
        </plugin>
        
		<plugin>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-maven-plugin</artifactId>
		</plugin>
	</plugins>
</build>

image-20210825172401306

2、了解自动配置原理

1、SpringBoot特点
1.1、依赖管理
1、父项目做依赖管理
<!--  springboot 依赖的父项目  父项目一般就是来做依赖管理的-->
<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.3</version>
        <relativePath/> <!-- lookup parent from repository -->
</parent>

<!-- 上面的父项目依赖于下面的父项目  它几乎声明了所有开发中常用的依赖的版本号,自动版本仲裁机制-->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-dependencies</artifactId>
    <version>2.5.3</version>
</parent>

2、开发导入starter场景启动器
  • ​ 1、见到很多 spring-boot-starter-* : *就某种场景 (jdbc、tomcat、web、json、thymeleaf等)

  • ​ 2、只要引入starter,这个场景的所有常规需要的依赖我们都自动引入

  • 3、SpringBoot所有支持的场景 https://docs.spring.io/spring-boot/docs/current/reference/html/using-spring-boot.html#using-boot-starter

  • ​ 4、见到的 *-spring-boot-starter: 第三方为我们提供的简化开发的场景启动器。

    • 5、所有场景启动器最底层的依赖
    <dependency>
    	<groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
        <version>2.5.3</version>
    </dependency>
    
3、无需关注版本号,自动版本仲裁

​ 1、引入依赖默认都可以不写版本

​ 2、引入非版本仲裁的jar,要写版本号。(可以在 spring-boot-dependencies.xml文件搜索版本号,如果没有则需要自己配置,如果有则无需自己配置)

4、可以修改默认版本号

​ 1、查看spring-boot-dependencies里面规定当前依赖的版本 用的 key。

​ 2、在当前项目里面重写配置

<!-- 在自己从的项目xml中配置一个propertys  版本就近依赖原则  就近原则,在pom中修改之后,先按照pom中的版本号,否则遵循父类中的版本。-->
<properties>
     <mysql.version>5.1.43</mysql.version>  
</properties>
2.1自动配置
1、自动配好Tomcat
  • 引入Tomcat依赖。 想要自动配置好Tomcat就需要引入Tomcat依赖,但是我们在依赖管理引入web场景的时候,就已经引入了Tomcat场景。
  • 配置Tomcat
<!--spring-boot-dependencies 自动引入了Tomact依赖-->
<groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-tomcat</artifactId>
        <version>2.5.3</version>
</dependency>
2、自动配好SpringMVC
  • 引入SpringMVC全套组件

  • 自动配好SpringMVC常用组件(功能)

  • 如下:

    前端控制器DispatcherServlet:拦截所有的前端的请求;

    字符编码characterEncodingFilter:解决返回中文字符串乱码问题;

    视图解析器viewResolver:对返回的视图进行渲染呈现;

    文件上传解析器multipatResolver:文件上传;

3、自动配好Web常见功能,如:字符编码问题
  • SpringBoot帮我们配置好了所有web开发的常见场景
@SpringBootApplication
public class Springboot_1Application {

    public static void main(String[] args) {
        //返回IOC容器
        ConfigurableApplicationContext run =
                SpringApplication.run(Springboot_1Application.class, args);
        //获取容器中的所有组件
        String[] names = run.getBeanDefinitionNames();
        for (String name : names) {
            System.out.println(name);
        }
    }
}
image-20210825181832519
4、默认的包结构
  1. 主程序所在包及其下面的所有子包里面的组件都会被默认扫描进来 这点特别重要,一定要将主程序放对位置,不然一定扫描不到 如主程序在 com.pj.boot包下,该包下或子包下的所有类标注了组件注解的都会自动扫描进来

  2. 无需以前的包扫描配置

  3. 想要改变扫描路径
    @SpringBootApplication(scanBasePackages=“com.lun”)
    @ComponentScan 指定扫描路径

  4. @SpringBootApplication等同于

    @SpringBootConfiguration(声明为一个配置类,会被扫描到容器中)

    @EnableAutoConfiguration(自动载入应用程序所需的所有Bean) @EnableConfigurationProperties(MultipartProperties.class) 就会将 MultipartProperties 就会载入到当前类

    @ComponentScan(“com.lun”)(扫描路径)

5、各种配置拥有默认值
    • 默认配置最终都是映射到某个类上,如:MultipartProperties

    • 配置文件的值最终会绑定某个类上,这个类会在容器中创建对象

      image-20210825185345564

6、按需加载所有自动配置项
    • 非常多的 starter 如:starter-web、starter-batch

    • 引入了哪些场景这个场景的自动配置才会开启

      <!--引入批处理场景-->
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-batch</artifactId>
          <version>2.4.4</version>
      </dependency>
      
    • SpringBoot所有的自动配置功能都在 spring-boot-autoconfigure 包里面(SpringBoot所有的自动配置都依赖

      @EnableAutoConfiguration)

3、容器功能 (com.pj.boot.container 代码全在改包下)

2.1、组件添加
1、@Configuration
  1. ​ 基本使用
    ​ Full模式与Lite模式:

  2. ​ Full模式和Lite模式是针对spring配置而言的,和xml配置无关。

  3. ​ 何时为Lite模式:

  • 1.类上有@Component注解
  • 2.类上有@ComponentScan注解
  • 3.类上有@Import注解
  • 4.类上有@ImportResource注解
  • 5.类上没有任何注解,但是类中存在@Bean方法
  • 6.类上有@Configuration(proxyBeanMethods = false)注解

Lite总结:运行时不用生成CGLIB子类,提高运行性能,降低启动时间,可以作为普通类使用。但是不能声明@Bean之间的依赖

  1. 何时为Full模式:

标注有@Configuration或者@Configuration(proxyBeanMethods = true)的类被称为Full模式的配置类。

Full模式总结:单例模式能有效避免Lite模式下的错误。性能没有Lite模式

  1. 代码

    1. TestMyConfigConfiguration启动类
//@SpringBootApplication  这个注解相当于下面三个注解  @SpringBootApplication(exclude = {RedisAutoConfiguration.class}) //排出莫项自动配置
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(basePackages = "com.pj.boot")
public class Springboot_1Application {

    public static void main(String[] args) {
        //返回IOC容器
        ConfigurableApplicationContext run =
                SpringApplication.run(Springboot_1Application.class, args);
        //获取容器中的所有组件
        String[] names = run.getBeanDefinitionNames();
        for (String name : names) {
            System.out.println(name);
        }

        System.out.println("======container======");
        //从容器中获取组件
        User user01 = run.getBean("user01", User.class);
        User user02 = run.getBean("user01", User.class);
        //如果@Configuration(proxyBeanMethod = true)代理对象调用方法,springboot总会检查这个组件是否在容器中有  保持组件的单实例
        //在 full下  在lite下 都为 true  因为容器中的对象都是同一个对象
        System.out.println("container:user01 == user02;"+ (user01 == user02));
            
            MyConfig myconfig = run.getBean(MyConfig.class);
            /*
               full : myconfigcom.pj.boot.container.config.MyConfig$$EnhancerBySpringCGLIB$$ead26534@5308decf  代理类
                lite : myconfigcom.pj.boot.container.config.MyConfig@4e2eb16  不是代理对象  会直接调用方法  重新new一个对象
             */
            System.out.println("myconfig"+myconfig);
    
           User user011 = myconfig.user01();
            User user012 = myconfig.user01();
            //在 full下 为true ,在lite下 为false   配置类对象调用方法 才会产生不同效果
            System.out.println("container:user011 == user012:"+ (user011 == user012));
  
            Pet pet = run.getBean("pet", Pet.class);
            Pet pet2 = run.getBean("pet", Pet.class);
           //在 full下  在lite下 都为 true  因为容器中的对象都是同一个对象          							System.out.println("container:pet == pet2:"+ (pet == pet2));
    
            //在 full下 为true ,在lite下 为false  当类之间有依赖关系使用full模式保证对象的单实例(会使用代理类会检查是否有该对象) 如果没有依赖关系,使用lite模式 保证springboot启动更快
           System.out.println("container:user01.getPet()==pet:"+(user01.getPet()==pet));
          }
    }
  1. MyConfigConfiguration 配置类
/*
  @Configuration : 标记为一个配置类
  自定义配置配置类
     1、配置类里面使用@bean标注在方法上给容器注册组件,默认是单实例的(标记了@bean的方法,在容器中只会产生一个对象 无论 proxyBeanMethods 为true还是false)
     2、配置类本身也是组件 会被扫描进入容器中
     3、proxyBeanMethods:代理bean的方法  容器对象调用方法才不同  即MyConfig类调用方法就不同
          full:(proxyBeanMethods = true) 保证每个@bena方法被调用多少次返回的组件都是单实例的
          lite:(proxyBeanMethods = false) 每个@bean方法被调用多少次返回的组件都是新创建的
*/
@Configuration(proxyBeanMethods = true) //告诉springboot这是一个配置类==配置文件
public class MyConfig {
  /**
   * full (proxyBeanMethods = true): 外部无论对配置类中的这个组件注册方法调用对少次获取的都是之前注册容器中的单实例对象
   * lite (proxyBeanMethods = false): 外部获取一次就是一个新对象.
   * @return
   */
  @Bean //给容器中添加组件。以方法名作为组件的id。返回类型就是组件类型,返回值,就是组件在容器中的实例
  public User user01(){
      User zs = new User(1 ,"张三",12);
      //容器user依赖组件 pet
      zs.setPet(pet());
      return zs;
  }

  @Bean(value = "pet") //默认是以方法名作为组件的id , 也可以自定义组件id value ="组件名"
  public Pet pet(){
      Pet pet = new Pet(1,"tom");
      return pet;
  }
}

image-20210826120230571

​ 3. 示例

​ 最佳实战配置
​ 1、配置类组件之间无依赖关系用Lite模式加速容器启动过程,减少判断(当某个pojo依赖了默认pojo,某个对象可能需要用到另一个对象,会检查容器中是否有已经注册了的实例)
​ 2、配置类组件之间有依赖关系,方法会被调用得到之前单实例组件,用Full模式(Full每次都要检查,会慢Lite每次不检查,快)

2、@Bean、@Component、@Controller、@Service、@Repository

​ 它们是Spring的基本标签,在Spring Boot中并未改变它们原来的功能。

3、@ComponentScan、@Import
  1. ​ @ComponentScan: @ComponentScan(basePackages = “com.pj.boot”) 扫描指定包下的所有组件

  2. ​ @Import的好处是可以引入外部类 给容器中导入一个组件

  3. 代码

    1. MyConfigImport 配置类

      /*
          @Import({User.class, DBHelper.class}) : 会将 User 和 DBHelper 加载到容器中
       */
      @Import({User.class, DBHelper.class})
      @Configuration
      public class MyConfigImport {
      }
      
    2. TestMyConfigConfiguration启动类(添加如下代码)

      System.out.println("======TestMyConfigImport======");
      DBHelper dbHelper = run.getBean(DBHelper.class);
      //TestMyConfigImport:dbHelper:ch.qos.logback.core.db.DBHelper@4a81af81
      System.out.println("TestMyConfigImport:dbHelper:"+dbHelper);
      
    3. 导入的类

image-20210826122326524

4、@Conditional
  1. 条件装配:满足Conditional指定的条件,则进行组件注入

  2. 放在配置类上表示,当容器中满足条件时,配置类中的组件才生效;放在配置方法上的时候,表示的意思是当满足条件的时候配置方法才生效;

  3. image-20210826165322618

  4. 代码及结果

    1. MyConfigImport 测试 Conditional

      /*
          @Import({User.class, DBHelper.class}) : 会将 User 和 DBHelper 加载到容器中
       */
      @Import({User.class, DBHelper.class})
      @Configuration
      @ConditionalOnMissingBean(name = "pet") //容器中没有pet名字的bean MyConfigImport配置类的才能生效  在MyConfigConfiguration 中注入了pet组件id  所以当前类中不会有 DBHelper、User  如果将 MyConfigConfiguration注入的pet1组件id  就会有DBHelper、User
      public class MyConfigImport {
      }
      
    2. MyConfigConfiguration 是否给容器中放入组件id为 pet

      @Configuration(proxyBeanMethods = true) //告诉springboot这是一个配置类==配置文件
      public class MyConfigConfiguration {
          @Bean(value = "pet") //给容器中注入pet 
      //    @Bean(value = "pet1")  //给容器中注入pet1 
          public Pet pet(){
              Pet pet = new Pet(1,"tom");
              return pet;
          }
      }
      
    3. 主程序测试类

      System.out.println("======TestConditional======");
      DBHelper dbHelper = run.getBean(DBHelper.class); //获取DBHlper
      System.out.println("TestMyConfigImport:dbHelper:"+dbHelper);
      
    4. 容器中有组件id为pet的结果

    5. 容器中没有组件id为pet的结果

2.2、原生配置文件引入
1、含义:

​ 指以.xml结尾的配置文件,通过@ImportResource导入后SpringBoot进行解析,完成对应的组件注册位置:在主配置类的上方

2、@ImportResource导入Spring配置文件
  1. @ImportResource(“classpath:beans.xml”):导入spring的配置文件来进行生效,比如:公司使用bean.xml文件生成配置bean,然而你为了省事,想继续复用bean.xml,@ImportResource粉墨登场。

  2. 代码及结果

    1. MyConfigImport 导入配置文件

      @ImportResource("classpath:beans.xml")  //会自动将类路径下的beans.xml配置文件里面的内容加载给容器
      //@ImportResource("classpath:com/pj/boot/container/resource/beans.xml") 
      public class MyConfigImport {
      }
      
    2. TestMyConfigConfiguration 启动类测试

      System.out.println("======TestImportResource======");
      User haha = run.getBean("haha", User.class);
      System.out.println(haha);
      System.out.println(run.getBean("hehe", Pet.class));
      
    3. bean.xml

      <?xml version="1.0" encoding="UTF-8"?>
      <beans xmlns="http://www.springframework.org/schema/beans"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
          <bean id="haha" class="com.pj.boot.container.pojo.User">
              <property name="name" value="zhangsan"></property>
              <property name="age" value="18"></property>
          </bean>
          <bean id="hehe" class="com.pj.boot.container.pojo.Pet">
              <property name="name" value="tomcat"></property>
          </bean>
      </beans>
      
    4. 结果

      image-20210826173308873

2.3、配置绑定
1、含义:

​ 如何使用Java读取到properties文件中的内容,并且把它封装到JavaBean中,以供随时使用。场景例子:我们习惯将经常爱变化的东西写在.properties配置文件中,比如与数据库相关的信息(连接池、URL等)配置到配置文件中,为了方便我们会将配置文件中的内容解析到JavaBean中。这个过程使用java原生代码较为麻烦。

2、原生传统的获取配置文件的方式
/*
    使用原生的方式获取 properties文件
 */
public class TestProperties {
    public static void main(String[] args) throws IOException {
        Properties properties = new Properties();
        properties.load(new FileInputStream("a.properties"));
        Enumeration<String > enumeration = (Enumeration<String>) properties.propertyNames();
        while (enumeration.hasMoreElements()){
            String  key = enumeration.nextElement();
            String value = properties.getProperty(key);
            System.out.println(key +"="+value);
            //封装到bean
        }
    }
}
3、Spring Boot一种配置配置绑定:第一种实现方式:@Component + @ConfigurationProperties
  1. 实体类 Car

    /*
        配置绑定测试pojo
            只有在容器中的组件,才会拥有SpringBoot提供的强大功能
     */
    @Component //必须注入到容器中  不然下面注解会报错
    @ConfigurationProperties(prefix = "mycar") //将配置文件中 以mycar开头的项 注入到改pojo中  开启Car配置绑定功能
    public class Car {
        private int id;
        private String name;
        private int age;
    }
    
  2. 测试类

    System.out.println("======Test自动配置ConfigurationProperties======");
    /*
    ======Test自动配置ConfigurationProperties======
    Test自动配置ConfigurationProperties:carcom.pj.boot.container.pojo.Car@4258c97a
    */
    System.out.println("Test自动配置ConfigurationProperties:car"+run.getBean(Car.class));
    

    image-20210826182918356

  3. application.properties 配置文件

    #给 pojo car注入属性值
    mycar.id=1
    mycar.name=xiaohu
    mycar.age=12
    
4、Spring Boot一种配置配置绑定:第二种实现方式:@EnableConfigurationProperties + @ConfigurationProperties
  1. 实体类 Car

    /*
        配置绑定测试pojo
            只有在容器中的组件,才会拥有SpringBoot提供的强大功能
     */
    @ConfigurationProperties(prefix = "mycar") //将配置文件中 以mycar开头的项 注入到改pojo中  开启Car配置绑定功能0
    public class Car {
        private int id;
        private String name;
        private int age;
    }
    
  2. 配置类 MyConfigImport

    @Configuration
    @EnableConfigurationProperties({Car.class}) //  把这个Car这个组件自动注册到容器中  必须标记为配置类或者启动类才能使用  因为这样才会被加载到器中  如果未加载到容器中 获取时会抛出异常
    public class MyConfigImport {
    }
    
2.4、自动配置原理
1、引导加载自动配置类(SpringBootApplication)
  1. @SpringBootApplication源码

    @SpringBootConfiguration //标记为一个配置类 注解源码中使用了 @Configuration @ SpringBootConfiguration只是Spring标准@Configuration批注的替代方法。 两者之间的唯一区别是@SpringBootConfiguration允许自动找到配置。
    @EnableAutoConfiguration
    @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
          @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) //指定扫描那些,spring注解
    public @interface SpringBootApplication {}
    
  2. @SpringBootConfiguration源码

    @Configuration //标记为一个配置类
    @Indexed
    public @interface SpringBootConfiguration {}
    
  3. @EnableAutoConfiguration源码

    @AutoConfigurationPackage 
    @Import(AutoConfigurationImportSelector.class)
    public @interface EnableAutoConfiguration {}
    
    • 重点分析

      @AutoConfigurationPackage@Import(AutoConfigurationImportSelector.class)`。

    • @AutoConfigurationPackage 标签名直译为:自动配置包,指定了默认的包规则。自动包规则原理

      @Target(ElementType.TYPE)
      @Retention(RetentionPolicy.RUNTIME)
      @Documented
      @Inherited
      @Import(AutoConfigurationPackages.Registrar.class) //给容器中导入一个组件 使用@import将AutoConfigurationPackages包下的Registrar类作为组件导入到容器中,然后使用Registrar中的方法批量完成组件的注册。
      public @interface AutoConfigurationPackage {
         String[] basePackages() default {};
         Class<?>[] basePackageClasses() default {};
      }
      
      1. 利用Registrar给容器中导入一系列组件

      2. AutoConfigurationPackages类 重要源码

        //该类将扫描启动类所在包下的所有组件
        static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
           @Override
            /*
            metadate参数指的是注解源
            new PackaImports(metadata)导入包中的组件
            getPackageNames()获得包名
            toArray封装到数组中。
        	最终注册进去
            */
           public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
              register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
           }
        }
        
      3. 将指定的一个包下的所有组件导入进MainApplication所在包下。

    • @Import(AutoConfigurationImportSelector.class)`。

      • 利用getAutoConfigurationEntry(annotationMetadata);给容器中批量导入一些组件 初始加载自动配置类

      • 调用List configurations = getCandidateConfigurations(annotationMetadata, attributes)获取到所有需要导入到容器中的配置类 134个配置类自动装配

      • 利用工厂加载 Map<String, List> loadSpringFactories(@Nullable ClassLoader classLoader);得到所有的组件

      • 从META-INF/spring.factories位置来加载一个文件。

        • 默认扫描我们当前系统里面所有META-INF/spring.factories位置的文件

        • spring-boot-autoconfigure-2.3.4.RELEASE.jar包里面也有META-INF/spring.factories

          # 文件里面写死了spring-boot一启动就要给容器中加载的所有配置类
          # spring-boot-autoconfigure-2.3.4.RELEASE.jar/META-INF/spring.factories
          # Auto Configure
          org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
          org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
          org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
          ...
          
      • 总流程

        image-20210827102717050
2、按需开启自动配置项

​ 虽然我们134(版本不同加载个数不同)个场景的所有自动配置启动的时候默认全部加载,但是xxxxAutoConfiguration按照条件装配规则(@Conditional),如AopAutoConfiguration类:,最终会按需配置。

 #Auto Configure 默认装载的所有类  131个 可能文件中也有其他的自动配置类
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration,\
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\
org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration,\
org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ReactiveElasticsearchRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ReactiveElasticsearchRestClientAutoConfiguration,\
org.springframework.boot.autoconfigure.data.jdbc.JdbcRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.ldap.LdapRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.neo4j.Neo4jDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.neo4j.Neo4jReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.neo4j.Neo4jReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.neo4j.Neo4jRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.r2dbc.R2dbcRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration,\
org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAutoConfiguration,\
org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration,\
org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration,\
org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration,\
org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration,\
org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration,\
org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration,\
org.springframework.boot.autoconfigure.hazelcast.HazelcastJpaDependencyAutoConfiguration,\
org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration,\
org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration,\
org.springframework.boot.autoconfigure.influx.InfluxDbAutoConfiguration,\
org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration,\
org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration,\
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.JndiDataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration,\
org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.JndiConnectionFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration,\
org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration,\
org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration,\
org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration,\
org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration,\
org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration,\
org.springframework.boot.autoconfigure.ldap.embedded.EmbeddedLdapAutoConfiguration,\
org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration,\
org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration,\
org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration,\
org.springframework.boot.autoconfigure.mail.MailSenderValidatorAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration,\
org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfiguration,\
org.springframework.boot.autoconfigure.netty.NettyAutoConfiguration,\
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\
org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration,\
org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration,\
org.springframework.boot.autoconfigure.r2dbc.R2dbcTransactionManagerAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketRequesterAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketServerAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketStrategiesAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration,\
org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration,\
org.springframework.boot.autoconfigure.security.rsocket.RSocketSecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyAutoConfiguration,\
org.springframework.boot.autoconfigure.sendgrid.SendGridAutoConfiguration,\
org.springframework.boot.autoconfigure.session.SessionAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.client.reactive.ReactiveOAuth2ClientAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.ReactiveOAuth2ResourceServerAutoConfiguration,\
org.springframework.boot.autoconfigure.solr.SolrAutoConfiguration,\
org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration,\
org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration,\
org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration,\
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration,\
org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration,\
org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration,\
org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration,\
org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.function.client.ClientHttpConnectorAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.reactive.WebSocketReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.servlet.WebSocketMessagingAutoConfiguration,\
org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration,\
org.springframework.boot.autoconfigure.webservices.client.WebServiceTemplateAutoConfiguration

image-20210827105711532

@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(  //有下面的条件配置了才会生效
    prefix = "spring.aop",  //绑定配置文件中以 spring.aop开头的
    name = "auto",  
    havingValue = "true",
    matchIfMissing = true //默认是开启的
)
public class AopAutoConfiguration {}

CacheAutoConfiguration类:条件未成立所以不会进行加载

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(CacheManager.class)  //有这个CacheManager类在容器中 下面配置才生效  CacheManager spirng核心包下的类默认加载了  条件成立
@ConditionalOnBean(CacheAspectSupport.class) //测试类中可以测试当前类是否存在  结果不存在  所以该类不会被加载
@ConditionalOnMissingBean(value = CacheManager.class, name = "cacheResolver") //容器中没存在CacheManager类 下面才生效
@EnableConfigurationProperties(CacheProperties.class)
@AutoConfigureAfter({ CouchbaseDataAutoConfiguration.class, HazelcastAutoConfiguration.class,
      HibernateJpaAutoConfiguration.class, RedisAutoConfiguration.class })
@Import({ CacheConfigurationImportSelector.class, CacheManagerEntityManagerFactoryDependsOnPostProcessor.class })
public class CacheAutoConfiguration {}

测试 CacheAspectSupport在容器中是否存在 :不存在

/*
 判断 CacheAspectSupport 是否存在  返回为0
*/
String[] beanNamesForType = run.getBeanNamesForType(CacheAspectSupport.class);
System.out.println("======"+beanNamesForType.length); //0

如DispatcherServletConfiguration类:条件成立所以会进行加载

@Configuration(proxyBeanMethods = false)
@Conditional(DefaultDispatcherServletCondition.class)
@ConditionalOnClass(ServletRegistration.class)//判断ServletRegistration.class存在  是tomcat核心包下的 成立
@EnableConfigurationProperties(WebMvcProperties.class) //开启配置绑定  绑定WebMvcProperties.class存在
protected static class DispatcherServletConfiguration {}

测试 DispatcherServletConfiguration在容器中是否存在 :存在

/*
    从容器中获取组件 WebMvcProperties  判断是否存在  返回1  存在
 */
String[] beanNamesForType1 = run.getBeanNamesForType(WebMvcProperties.class);
System.out.println("======"+beanNamesForType1.length); //1
3、修改自动默认配置
  1. DispatcherServletAutoConfiguration的内部类DispatcherServletConfiguration为例子:主要体现在@ConditionalOnMissingBean,如果没有存在这个bean,那么springboot就会自动帮你配置,存在就是用我们自己的

    @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)  //优先级 
    @Configuration(proxyBeanMethods = false) //标记为配置类
    @ConditionalOnWebApplication(type = Type.SERVLET) //判断当前项目是原生的 SERVLET severlet  是就加会加载这个类 不是会加载
    @ConditionalOnClass(DispatcherServlet.class)  //判断容器中是否有 DispatcherServlet  导入了springmvc肯定有 生效
    @AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class) //在配置完ServletWebServerFactoryAutoConfiguration这个之后才来配置这个类
    public class DispatcherServletAutoConfiguration {  //总类满足上述条件下面才能生效  现在是满足的
        
        @Configuration(proxyBeanMethods = false)
        @Conditional(DefaultDispatcherServletCondition.class)  
        @ConditionalOnClass(ServletRegistration.class) //是有ServletRegistration这个组件 没有则不会配置  是Tomact核心包下的类  导入了Tomact核心包就会有
        @EnableConfigurationProperties(WebMvcProperties.class) //开启配置绑定的WebMvcProperties 放入容器中
        protected static class DispatcherServletConfiguration {
            
            //配置DispatchServlet 一些初始化工作 
            @Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
            public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {
                DispatcherServlet dispatcherServlet = new DispatcherServlet();
                dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());
                dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest());
                dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound());
                dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents());
                dispatcherServlet.setEnableLoggingRequestDetails(webMvcProperties.isLogRequestDetails());
                return dispatcherServlet;
            }
            
            @Bean  //注入到容器中 组件id默认为 multipartResolver
            @ConditionalOnBean(MultipartResolver.class) //容器中有这类型的组件  有才生效
            @ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)//容器中没有这个名字的 multipartResolver 的组件      MULTIPART_RESOLVER_BEAN_NAME = multipartResolver  如果容器中有id为multipartResolver的组件就不会使用默认的自动配置  下面就不生效  有就使用自己配置
            public MultipartResolver multipartResolver(MultipartResolver resolver) {
                //给@Bean标注的方法传入了对象参数,multipartResolver这个参数的值就会从容器中找。
                //SpringMVC multipartResolver。防止有些用户配置的文件上传解析器不符合规范		即使没有这个名为multipartResolver组件  也会给你在容器中自动配置
                return resolver;//给容器中加入了文件上传解析器;
            }
        }
    }
    
    
  2. SpringBoot默认会在底层配好所有的组件,但是如果用户自己配置了以用户的优先。约定大于配置

    //controller发请求测试
    @Controller
    public class TestController {
        /**
         * 测试自动配置的 字符编码过滤器
         */
        @ResponseBody
        @GetMapping("/testCharacterEncodingFilter")
        public String testCharacterEncodingFilter(){
            return "你好···springboot";
        }
    }
    
    
    //配置类
    @Configuration
    public class MyConfigTest {
        /*
            自定义字符编码过滤器
         */
        @Bean
        //    @ConditionalOnMissingBean //如果容器中没有就帮你配
        public CharacterEncodingFilter characterEncodingFilter() {
            CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
            filter.setEncoding("ISO-8859-1");
            return filter;
        }
    
    }
    
4、自动配置总结:
  1. SpringBoot先加载所有的自动配置类 xxxxxAutoConfiguration 组件中经常变化的值是从application.properties中来的
  2. 每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值。(xxxxProperties里面读取,xxxProperties和配置文件进行了绑定)
  3. 生效的配置类就会给容器中装配很多组件
  4. 只要容器中有这些组件,相当于这些功能就有了
  5. 定制化配置
    1. 用户直接自己@Bean替换底层的组件
    2. 用户去看这个组件是获取的配置文件什么值就去修改。可以去@ConfigurationPropeties中的xxxProperties.class中的中找到相关的propeties.class,再在application中去配置
  6. 大致总流程:xxxxxAutoConfiguration —> 组件 —> xxxxProperties里面拿值 ----> application.properties (通过配置文件进行自定义配置的思路,当然也可以通过自定义Configuration类实现对应接口(如实现WebMvcConfigurer接口进行MVC定制化配置)进行配置,稍复杂。)
5、最佳实践-SpringBoot应用如何编写
1、引入场景依赖

​ https://docs.spring.io/spring-boot/docs/current/reference/html/using-spring-boot.html#using-boot-starter

2、查看自动配置了哪些(选做)判断场景自动配置那些生效那些没生效.
  1. 自己分析,引入场景对应的自动配置一般都生效了

  2. 配置文件中debug=true开启自动配置报告。Negative(不生效)Positive(生效)

    #查看哪些自动配置类注入的生效或者不生效
    debug=true
    

    在application.properties配置文件中新增一行 debug=true,当应用程序运行时,可以在控制台看到哪些组件生效(Positive matches),那些组件不生效(Negative matches)

    image-20210827123702544

    image-20210827123721357

3、是否需要修改某些配置项
  1. 参照文档修改配置项

    1. https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-application-properties.html#common-application-properties

      #修改springboot的默认图片
      spring.banner.image.location=classpath:banner.txt
      
    2. 自己分析。xxxxProperties绑定了配置文件的哪些。通过查看XxxProperties.java类的属性,查看可以绑定application.properties配置文件中的哪些值

4、自定义加入或者替换组件,可以使用@bean添加新的组件或者替换原来默认的组件

​ @Bean、@Component…

5、自定义器 XXXXXCustomizer;
6、开发小技巧
1、Lombok

步骤:

  1. 导入依赖 idea 下载lombok插件

    不需要引入版本号  springboot 自动仲裁了版本号
    <dependency>    
        <groupId>org.projectlombok</groupId>  
        <artifactId>lombok</artifactId>
    </dependency>
    
  2. 使用

    /*=======================简化JavaBean开发========================
        @Data---帮助生产getset方法
        @ToString---帮助生成ToString方法
        @AllArgsConstructor---生成有参构造器
        @NoArgsConstructor---生成无参构造方法
        @EqualsAndHashCode---生成Equals、HashCode方法	*/			
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    //@ToString
    //@EqualsAndHashCodepublic
    class User {   
        private  int id;   
        private String name;  
        private int age;   
        private Pet pet;    
        public User(int id, String name, int age) {    
            this.id = id;        
            this.name = name;     
            this.age = age;  
        }}
    
2、dev-tools 虚假的热更新工具(需重启项目)

导入依赖即可 在IDEA中,项目或者页面修改以后:Ctrl+F9 会自动重启项目。

<dependencies>  
    <dependency>    
        <groupId>org.springframework.boot</groupId>    
        <artifactId>spring-boot-devtools</artifactId>     
        <optional>true</optional>  
    </dependency>
</dependencies>
3、Spring Initailizr(项目初始化向导 快速开发springboot项目)

自动创建项目结构、自动编写好主配置类、自动依赖引入

image-20210827170703689

4、配置文件

1.1、文件类型
1、properties

​ 同以前的properties用法

2、yaml
  1. 简介:YAML 是 “YAML Ain’t Markup Language”(YAML 不是一种标记语言(标记语言:标签比xml, 语法更简洁,更轻量级))的递归缩写。在开发的这种语言时,YAML 的意思其实是:“Yet Another Markup Language”(仍是一种标记语言)。非常适合用来做以数据为中心的配置文件

  2. 基本语法

    • key: value;kv之间有空格

    • 大小写敏感

    • 使用缩进表示层级关系

    • 缩进不允许使用tab,只允许空格

    • 缩进的空格数不重要,只要相同层级的元素左对齐即可

    • ‘#’ 表示注释

    • 字符串无需加引号,如果要加,''与""表示字符串内容 会被 转义/不转义

  3. 数据类型

    • 字面量:单个的、不可再分的值。date、boolean、string、number、null kv之间有空格

      k: v
      
    • 对象:键值对的集合。map、hash、set、object

      #行内写法:  
      k: {k1:v1,k2:v2,k3:v3}
      #或
      k: 
       k1: v1
       k2: v2 
       k3: v3
      
    • 数组:一组按次序排列的值。array、list、queue

      #行内写法: 
      k: [v1,v2,v3]
      #或者
      k: - v1 - v2 - v3
      
  4. 案例

    pojo类

    /*
        测试 yaml配置文件
     */
    @Data
    @Component
    @ConfigurationProperties("my.person") //绑定properties配置文件  以my.person开头的
    public class Person {
        private String userName;
        private Boolean boss;
        private Date birth;
        private Integer age;
        private Pet pet;
        private String[] interests;
        private List<String> animal;
        private Map<String, Object> score;
        private Set<Double> salarys;
        private Map<String, List<Pet>> allPets;
    }
    
    
    
    @Data
    public class Pet {
        private String name;
        private Double weight;
    }
    

    启动类;

    @SpringBootConfiguration@EnableAutoConfiguration@ComponentScan(basePackages = "com.pj.boot.yaml")
    public class TestMyYaml {    
        public static void main(String[] args) {     
            //返回IOC容器        
        ConfigurableApplicationContext run =                SpringApplication.run(Springboot_1Application.class, args);    
    }}
    

    Controller类

    @Controller
    public class TestYamlController {
    
        //自动注入person
        @Autowired
        Person person;
        /**
         * 测试yaml配置文件
         * @return
         */
        @ResponseBody
        @GetMapping("testYaml")
        public Person testYaml(){
            //单引号输入:zhangsan \n aaa
            //双引号输出:zhangsan
            // aaa
            System.out.println(person.getUserName());
            return person;
        }
    }
    
    

    application.yaml

    my:
      person:
        userName: "zhangsan \n aaa"
        #单引号输出 "userName": "zhangsan \\n aaa",  会将 \n 以字符串输出
        #双引号输出 "userName": "zhangsan \n aaa",  会将 换行输出  双引号不会转义  单引号会转义
        boss: true
        birth: 2019/12/9
        age: 18
        #private String[] interests;
        #interests:[篮球,游泳]  两种方式都可以
        interests:
          - 篮球
          - 游泳
        #private List<String> animal;
        animal: [jerry,mario]
        #private Pet pet;
        pet:
          name: 阿毛
          weight: 20.02
        #private Map<String, Object> score;
        #    score:
        #      english: 80
        #      math: 90
        score: { english: 80,math: 90}
        #private Set<Double> salarys;
        salarys:
          - 999
          - 7788
        #private Map<String, List<Pet>> allPets;
        allPets:
          sick:
            - {name: a,weight: 12}
            - name: 阿猫
              weight: 13
            - name: 啊虫
              weight: 14
          health:
            - {name: b,weight: 15}
            - {name: c,weight: 16}
    
1.2、自定义类绑定的配置提示

自定义的类和配置文件绑定一般没有提示。若要提示,添加如下依赖:配置对应的pom文件,且打包时排除

 <!--自定义类配置提示-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>


<plugins>
    <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <!--打包时排出即可自定义配置类的提示-->
          <configuration>
              <excludes>
                  <exclude>
                      <groupId>org.springframework.boot</groupId>
                      <artifactId>spring-boot-configuration-processor</artifactId>
                  </exclude>
              </excludes>
          </configuration>
    </plugin>
</plugins>

5、WEB开发

1、SpringMVC自动配置概览
1.1、SpringMVC大多场景我们都无需自定义配置
  • 内容协商视图解析器和BeanName视图解析器
  • 静态资源(包括webjars)
  • 自动注册 Converter,GenericConverter,Formatter(转换器、格式化器)
  • 支持 HttpMessageConverters (后来我们配合内容协商理解原理)
  • 自动注册 MessageCodesResolver (国际化用)
  • 静态index.html (欢迎页)页支持
  • 自定义 Favicon(网站图标)
  • 自动使用 ConfigurableWebBindingInitializerDataBinder负责将请求数据绑定到JavaBean上)
1.2、自订化配置
  • 不用@EnableWebMvc注解。使用 @Configuration + WebMvcConfigurer 自定义规则(WebMvcConfigurer是一个接口。里面的方法都是default的方法,可以有选择的实现。我们是通过重写里面的方法来修改mvc的配置的。所以可以实现这个接口,配合@Configuration注解自定义mvc组件。)(向容器中添加该类型的组件或者配置类实现该接口都可以达到定制Mvc的目的)
  • 声明 WebMvcRegistrations 改变默认底层组件
  • 使用 @EnableWebMvc+@Configuration+DelegatingWebMvcConfiguration 全面接管SpringMVC
2、简单功能分析
2.1、静态资源
  1. 静态资源目录(静态资源一般包括图片、视频、js、css文件)

    1. 只要静态资源放在类路径下: resources/static (or /public or /resources or /META-INF/resources

      静态资源访问路径优先级:

      1. META-INF/resources
      2. resources
      3. static
      4. public

      image-20210829091931836

    2. 访问 : 当前项目根路径/ + 静态资源名 如:http://localhost:8888/a2.jpg

    3. 原理: 静态映射/**。

    4. /** 是Ant风格的路径配置,两个星号代表匹配任意层级的路径

    5. 请求进来,先去找Controller看能不能处理。不能处理的所有请求又都交给静态资源处理器。静态资源也找不到则响应404页面。(先去找动态资源,找不到后在去找静态资源

      image-20210829091811183

      Conroller请求类:TestWebController

      @Controller
      public class TestWebController {
      	//测试静态资源(有个同名的静态资源a.jpg),测试先访问请求还是先访问静态资源    
          //结果:先访问请求 
          @ResponseBody
          @GetMapping("a.jpg")
          public String testGetStaticResource(){
              return "Hello a.jpg";
          }
      }
      

      启动类:TestWeb

      @SpringBootConfiguration
      @EnableAutoConfiguration
      @ComponentScan(basePackages = "com.pj.boot.web")
      public class TestWeb {
          public static void main(String[] args) {
              //返回IOC容器
              ConfigurableApplicationContext run =
                      SpringApplication.run(Springboot_1Application.class, args);
          }
      
      }
      
    6. 也可以改变默认的静态资源路径,默认配置的资源路径/static/public,/resources, /META-INF/resources失效

      application.yaml:

      spring:  
        web:    
         resources:     
          #配置静态资源默认路径  可配置多个 数组形式的  配置后  默认的其他的会失效  
          #点击查看源码  能发现默认配置路径的规则 只改变存储的路径
           static-locations: [classpath:/haha/,classpath:/hehe/]
      
2.2、静态资源访问前缀

当前项目 + static-path-pattern(/res/**) + 静态资源名 = 静态资源文件夹下找 static-path-patten是虚拟的路径 不是真实存在的 如:http://localhost:8888/res/a2.jpg 不需要创建文件夹

为了让拦截时能区分出静态资源和动态资源,所以规定静态资源前面加个前缀,拦截器在看到指定前缀时就放行,从而达到动态静态分开的目的。

application.yaml:

#配置静态资源访问的前缀  
spring:  
 mvc:    
  static-path-pattern: /res/**
2.3、webjar
  1. ​ 可用jar方式添加css,js等资源文件

  2. ​ 官方地址:https://www.webjars.org/

  3. 例如,添加jquery依赖,jQuery相关的js文件都会自动给你导入

    访问地址:http://localhost:8888/webjars/jquery/3.5.1/jquery.js 即可访问到js文件(后面地址要按照依赖里面的包路径。)

    image-20210829094635522

    pom.xml

    <!--webjars 导入jquery-->
    <dependency>  
        <groupId>org.webjars</groupId>   
        <artifactId>jquery</artifactId>
        <version>3.5.1</version>
    </dependency>
    
2.4、欢迎页支持

官网地址:https://docs.spring.io/spring-boot/docs/2.3.8.RELEASE/reference/htmlsingle/#boot-features-spring-mvc-welcome-page

1、静态资源路径下 index.html

  • 可以配置静态资源路径

  • 但是不可以配置静态资源的访问前缀。否则导致 index.html不能被默认访问

    spring:
      mvc:
       #静态资源访问前缀  不可以配置 否则欢迎页会失效#    
       static-path-pattern: /res/**  
     web:    
       resources:      
      #配置静态资源默认路径 以配置  但欢迎页需要在以下文件夹中 hahaha或者hehe或者static   	  
        static-locations: [classpath:/hahaha/,classpath:/hehe/,classpath:/static]
    

2、controller能处理/index。

​ 有一种方法:把映射路径设为*@RequestMapping*(“/”),后面return要访问的欢迎页

2.5、自定义Favicon

1、指网页标签上的小图标。favicon.ico 放在静态资源目录下即可。

  • 可以配置静态资源路径 (指定静态文件所在的文件夹)
  • 但是不可以配置静态资源的访问前缀。否则导致favicon.ico 失效

image-20210829101544408image-20210829101833595image-20210829102122304

3、静态资源配置原理
3.1、SpringBoot启动默认加载 xxxAutoConfiguration 类(自动配置类)
3.2、SpringMVC功能的自动配置类**WebMvcAutoConfiguration**,生效
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)//是原生的Type.SERVLET 应用嘛  是就生效 不是不生效  当前的是
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer .class })  //导入了springmvc的依赖  自然会导入DispatcherServlet、WebMvcConfigurer  容器中有这三个类才会生效
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class) //容器中没有这个组件的时候才生效  可以全面接管springmvc,会需要这个类
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,      ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {}
3.3、WebMvcAutoConfiguration 给容器中配置了什么
  1. rest风格过滤器和格式转换器等

    image-20210829105240225

  2. WebMvcAutoConfiguration的内部类WebMvcAutoConfigurationAdapter

    @Configuration(proxyBeanMethods = false)  //是一个配置类
    @Import(EnableWebMvcConfiguration.class) 
    @EnableConfigurationProperties({ WebMvcProperties.class,      org.springframework.boot.autoconfigure.web.ResourceProperties.class, WebProperties.class }) //当前配置类 绑定了  WebMvcProperties和ResourceProperties和WebProperties 三个类  并将三个类放到容器中
    @Order(0)
    public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {}
    
  3. 配置文件的相关属性和xxx前缀进行了绑定: WebMvcProperties绑定配置文件前缀为:spring.mvc,ResourceProperties绑定配置文件前缀为spring.resources,WebProperties绑定配置文件前缀为spring.web

  4. 和静态资源相关的方法,都会有缓存策略(WebMvcAutoConfigurationAdapter 中的方法)

      @Override    
    public void addResourceHandlers(ResourceHandlerRegistry registry) {        
        if (!this.resourceProperties.isAddMappings()) {  //resourceProperties中有个属性为 addMappings  可以禁用默认静态资源配置 可在properties或者yaml中配置 addMappings设置为false  进入if  if后面的配置就不生效了  访问静态资源就会报404            
            logger.debug("Default resource handling disabled"); 
            return;        
        }       
        addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/"); //如果导入了 webjars的依赖  就会将 访问 /webjars/** 下的所有请求映射到 /META-INF/resources/webjars/下 就可以访问到下面的静态资源  解释了如何访问 webjars下的资源 是要以http://localhost:8888/webjars开头 
        
        /* 
        getStaticPathPattern()方法获取 mvcProperties类中属性 为staticPathPattern(默认值:private String staticPathPattern = "/**";) 属性的中 可在properties或者yaml中配置 staticPathPattern(静态资源访问前缀)值  配置了则会获取我们配置的  将staticPathPattern 映射到  resourceProperties类中属性为  staticLocations(默认值 private String[] staticLocations = { "classpath:/META-INF/resources/","classpath:/resources/", "classpath:/static/", "classpath:/public/" }; 解释了静态资源为什么可以放到以上目录,就可以直接访问了) , 可在properties或者yaml中配置 staticLocations(静态资源访问路径)值  配置了则会获取我们配置的  也有缓存策略        */   
        addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
            registration.addResourceLocations(this.resourceProperties.getStaticLocations());
            if (this.servletContext != null) {             
                ServletContextResource resource = new ServletContextResource(this.servletContext, SERVLET_LOCATION);            
                registration.addResourceLocations(resource);   
            }     
        });   
    }    
    private void addResourceHandler(ResourceHandlerRegistry registry, String pattern, String... locations) {
    			addResourceHandler(registry, pattern, (registration) -> registration.addResourceLocations(locations));
    		}
    private void addResourceHandler(ResourceHandlerRegistry registry, String pattern,
    				Consumer<ResourceHandlerRegistration> customizer) {
    			if (registry.hasMappingForPattern(pattern)) {
    				return;
    			}
    			ResourceHandlerRegistration registration = registry.addResourceHandler(pattern);
    			customizer.accept(registration);
    			registration.setCachePeriod(getSeconds(this.resourceProperties.getCache().getPeriod()));//加入到缓存中  可以在properties或者yaml中配置  resourceProperties 中属性的值period 设置缓存时间
    			registration.setCacheControl(this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl());
    			registration.setUseLastModified(this.resourceProperties.getCache().isUseLastModified());
    			customizeResourceHandlerRegistration(registration);
    		}
    

    application.yaml:

    spring:  
     mvc:  
    #  静态资源访问前缀    
       static-path-pattern: /res/** 
     web:    
       resources:     
    	#配置静态资源默认路径   
        static-locations: [classpath:/hahaha/,classpath:/hehe/,classpath:/static]	 
    	#设置静态资源缓存时间     
        cache:        
          period: 1100      
          #关闭默认静态资源的配置      
    	  add-mappings: false
    

    image-20210829114048147

  5. 欢迎页的配置

    • WebMvcAutoConfiguration的内部类EnableWebMvcConfiguration
    //=============EnableWebMvcConfiguration===============
    @Configuration(proxyBeanMethods = false)
    @EnableConfigurationProperties(WebProperties.class)
    public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware {
        //HandlerMapping:处理器映射。保存了每一个Handler能处理哪些请求
        @Bean  //在容器中放入WelcomePageHandlerMapping
        public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
                                                                   FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
            /*
            	this.mvcProperties.getStaticPathPattern() 也会获取 静态资源前缀的值
            	WelcomePageHandlerMapping 源码如下
            */
            WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
                new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(),this.mvcProperties.getStaticPathPattern());
            
            welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
            welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations());
            return welcomePageHandlerMapping;
        }
    }
    
    //=============WelcomePageHandlerMapping源码如下===============
    WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders,
    			ApplicationContext applicationContext, Resource welcomePage, String staticPathPattern) {
    		if (welcomePage != null && "/**".equals(staticPathPattern))  //如果静态访问资源前缀不是/** 则欢迎页会失效 解释了  欢迎页时不能在配置文件中配置 staticPathPattern 
    			logger.info("Adding welcome page: " + welcomePage);
    			setRootViewName("forward:index.html"); //重定向到 欢迎页
    		}
    		else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
                //调用Controller /index
    			logger.info("Adding welcome page template: index");
    			setRootViewName("index");
    		}
    	}
    
  6. favicon

    浏览器会发送 /favicon 请求获取到图标,整个session期间不再获取 ,不归springboot管

  7. 扩展:

    • 配置类 WebMvcAutoConfigurationAdapter 只有一个有参构造器
    //有参构造器所有参数的值都会从容器中确定(前提条件 只有一个有参构造器)
    /*
    	resourceProperties: 获取和spring.resources绑定的所有的对象的
    	webProperties:获取和spring.web绑定的所有的对象的
    	mvcProperties:获取和spring.mvc绑定的所有的值的对象
    	ListableBeanFactory: Spring的beanFactory(bean工厂)
    	messageConvertersProvider:找到所有的HttpMessageConverters
    	resourceHandlerRegistrationCustomizerProvider:找到资源处理器的自定义器。(和静态资源相关)
    	dispatcherServletPath: 前端控制器
    	servletRegistrations: 给应用注册原生的Servlet、Filter… 需要自定义配置改类
    */
    public WebMvcAutoConfigurationAdapter(
    	org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties,
    				WebProperties webProperties,
                     WebMvcProperties mvcProperties,
                     ListableBeanFactory beanFactory,
    				ObjectProvider<HttpMessageConverters> messageConvertersProvider,
    				ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,
    				ObjectProvider<DispatcherServletPath> dispatcherServletPath,
    				ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {
    			this.resourceProperties = resourceProperties.hasBeenCustomized() ? resourceProperties
    					: webProperties.getResources();
    			this.mvcProperties = mvcProperties;
    			this.beanFactory = beanFactory;
    			this.messageConvertersProvider = messageConvertersProvider;
    			this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
    			this.dispatcherServletPath = dispatcherServletPath;
    			this.servletRegistrations = servletRegistrations;
    			this.mvcProperties.checkConfiguration();
    		}
    
    
    • 结论:某个配置类如果只有一个有参构造器,有参构造器所有参数的值都会从容器中确定
4、请求参数处理
4.1、请求映射
1、 rest使用与原理
  • @xxxMapping: @GetMapping、@PostMapping、@PutMapping、@DeleteMapping

  • Rest风格支持(使用HTTP请求方式动词来表示对资源的操作)

    • 以前:
      • /getUser 获取用户
      • /deleteUser 删除用户
      • /editUser 修改用户
      • /saveUser保存用户
    • 现在: /user
      • GET-获取用户
      • DELETE-删除用户
      • PUT-修改用户
      • POST-保存用户
    • 核心Filter;HiddenHttpMethodFilter
  • 用法

    • 开启页面表单的Rest功能(默认是不开启的 需要在配置文件中开启

      spring:  
      #开启rest风格的过滤器  
       mvc:    
        hiddenmethod:     
         filter:        
           enabled: true #默认值为false
      
    • 页面 form的属性method=post,隐藏域 _method=put、delete等(如果直接get或post,无需隐藏域)

      ========index.html=======
      <h2>Rest风格测试</h2>
      <form action="/user" method="get">   
          <input value="REST-GET提交" type="submit"/>
      </form>
      <form action="/user" method="post">    
          <input value="REST-POST提交" type="submit"/>
      </form>
      <form action="/user" method="post">  
          <input name="_method" type="hidden" value="DELETE"/>   
          <input value="REST-DELETE 提交" type="submit"/>
      </form>
      <form action="/user" method="post">  
          <input name="_method" type="hidden" value="PUT"/>
          <input value="REST-PUT提交" type="submit"/>
      </form>
      
    • 编写请求映射

      @RestControllerpublic class TestWebController {
          @PostMapping("/user")
          //@RequestMapping(value = "/user",method = RequestMethod.POST)  
          public String saveUser(){        
              return "POST-张三";    
          }    
          @PutMapping("/user")
          //@RequestMapping(value = "/user",method = RequestMethod.PUT)   
          public String putUser(){      
              return "PUT-张三"; 
          }    
          @DeleteMapping("/user")
          //@RequestMapping(value = "/user",method = RequestMethod.DELETE)  
          public String deleteUser(){ 
              return "DELETE-张三";  
          }
      }
      
  • Rest原理(表单提交要使用REST的时候)

    • 表单提交会带上_method=PUT
    • 请求过来被HiddenHttpMethodFilter拦截
      • 请求是否正常,并且是POST
      • 获取到_method的值。
      • 兼容以下请求;PUT.DELETE.PATCH
      • 原生request(post),包装模式requesWrapper重写了getMethod方法,返回的是传入的值。
      • 过滤器链放行的时候用wrapper。以后的方法调用getMethod是调用requesWrapper的。
    //====== WebMvcAutoConfiguration  类下的方法===================
        @Bean
        @ConditionalOnMissingBean(HiddenHttpMethodFilter.class)  //绑定了 HiddenHttpMethodFilter类
        @ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled"  /*, matchIfMissing = false*/)//以spring.mvc.hiddenmethod.filter 为前缀的配置都会映射到HiddenHttpMethodFilter类 的属性上 没配置matchIfMissing属性的值,默认为false, 默认是不开启的  需要在配置文件中开启
        public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {  
        return new OrderedHiddenHttpMethodFilter();
    }
    
    
    //====== HiddenHttpMethodFilter  类下的方法===================    
    public class HiddenHttpMethodFilter extends OncePerRequestFilter {
    	private static final List<String> ALLOWED_METHODS =
    			Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(),
    					HttpMethod.DELETE.name(), HttpMethod.PATCH.name()));
    	/** Default method parameter: {@code _method}. */
    	public static final String DEFAULT_METHOD_PARAM = "_method";
    
    	private String methodParam = DEFAULT_METHOD_PARAM;
    	/**
    	 * Set the parameter name to look for HTTP methods.
    	 * @see #DEFAULT_METHOD_PARAM
    	 */
    	public void setMethodParam(String methodParam) {
    		Assert.hasText(methodParam, "'methodParam' must not be empty");
    		this.methodParam = methodParam;
    	}
    
    	@Override
    	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
    			throws ServletException, IOException {
    		HttpServletRequest requestToUse = request;
    		if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {//是不是POST方式提交和是否有异常
    			String paramValue = request.getParameter(this.methodParam);//获取 _method 的参数的值	
    			if (StringUtils.hasLength(paramValue)) {//判断是获取的 _method 参数的值长度是否大于0	
    				String method = paramValue.toUpperCase(Locale.ENGLISH);//转成大写
    				if (ALLOWED_METHODS.contains(method)) {//判断获取的参数值是不是允许的 请求方式 允许的请求方式有 :PUT ,DELETE,PATCH  
    					requestToUse = new HttpMethodRequestWrapper(request, method); //使用构造器的改变请求方式,包装了request,重写了里面的get方法 ,包请求方式method值 改成了自己传入的值			
    				}
    			}
    		}
    		filterChain.doFilter(requestToUse, response); //放行的是自己包装之后的request,以后使用的就是这个request对象	
    	}
    	/**
    	 * Simple {@link HttpServletRequest} wrapper that returns the supplied method for
    	 * {@link HttpServletRequest#getMethod()}.
    	 */
    	private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {
    		private final String method;
    		public HttpMethodRequestWrapper(HttpServletRequest request, String method) {
    			super(request);
    			this.method = method;
    		}
    		@Override
    		public String getMethod() {
    			return this.method;
    		}
    	}
    }
    
    
  • Rest使用客户端工具。

    • 如PostMan可直接发送put、delete等方式请求。不会走上述的包装 request,直接可发送。
  • 怎么改变默认的携带的_method

    • @ConditionalOnMissingBean(HiddenHttpMethodFilter.class)意味着在没有HiddenHttpMethodFilter时,才执行hiddenHttpMethodFilter()。因此,我们可以自定义filter,改变默认的_method

      /*    web的配置类 */
      @Configuration(proxyBeanMethods = false)
      public class WebConfig {    
          @Bean    
          public HiddenHttpMethodFilter hiddenHttpMethodFilter(){ 
              //这样不会执行hiddenHttpMethodFilter()方法          
              HiddenHttpMethodFilter filter = new HiddenHttpMethodFilter();     				 			    filter.setMethodParam("_m"); //将获取的参数值设置为自己的值        
              return filter;  
          }
      }
      
      <form action="/user" method="post">  
          <input name="_m" type="hidden" value="DELETE"/> //能获取到delete   
          <input value="REST-DELETE 提交" type="submit"/>
      </form>
      <form action="/user" method="post">   
          <input name="_method" type="hidden" value="PUT"/>//不能获取到put  因为没改变参数名 "_m"  
          <input value="REST-PUT提交" type="submit"/>
      </form>
      
2、 请求映射原理
  • SpringMVC功能分析都从 org.springframework.web.servlet.DispatcherServlet -> `doDispatch()``

    image-20210829155505161

  • DispatchServlet --》doDispatch()源码的一部分

    //=========DispatchServlet类 下的方法=======
        protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {   
        HttpServletRequest processedRequest = request;  
        HandlerExecutionChain mappedHandler = null;  
        boolean multipartRequestParsed = false; //判断是不是文件上传请求  默认false   
        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); //是不是异步请求  
        try {      
            ModelAndView mv = null;    
            Exception dispatchException = null;    
            try {        
                processedRequest = checkMultipart(request); //检测是不是 文件上传请求 
                multipartRequestParsed = (processedRequest != request);         
                // 确定当前请求的处理程序。 HandlerMapping中找到一个HandlerExecutionChain 链对象,能处理请求的Handler(Controller.method())的请求的controller和获取所有的拦截器。	       
                mappedHandler = getHandler(processedRequest); //找到那个Hnadler(Controller处理器(类))可以执行当前的请求         
               // .....  
            }
        }
    
  • 图解流程

    image-20210829170023811

  • this.handlerMappings在Debug模式下展现的内容是 @RequestMappinghandler的映射规则。

    image-20210829164126849
  • 所有的请求映射都在HandlerMapping中:

    • SpringBoot自动配置欢迎页的 WelcomePageHandlerMapping 。访问 /能访问到index.html;

    • SpringBoot自动配置了默认 的 RequestMappingHandlerMapping

      image-20210829171011883
    • 请求进来,挨个尝试所有的HandlerMapping ,看当前的HandlerMapping 能否处理当前发送的请求。

      • 如果能处理就找到这个请求对应的handlerMapping,获取后,在获取能够处理当前请求的Handler(Controller类)对应的方法
      • 如果没有就循环遍历下一个 HandlerMapping
    • 我们需要一些自定义的映射处理,我们也可以自己给容器中放HandlerMapping。自定义 HandlerMapping(有的时候比如说同一组api有不同的版本,比如v1,v2我们可以在controller中写两组mapping(比如v1/user,v2/user),但同时我们也可以放在两个包下,都是/user,这个时候我们就可以自定义handlermapping,把v1/user映射到一个包下的/user,把v2/user映射到另外一个包下的/user)

4.2、普通参数与基本注解
1常用参数注解使用
  • @PathVariable 路径变量((rest风格 {…}代表一个参数)
    @RequestHeader 获取请求头
    @RequestParam 获取请求参数(指问号后的参数,url?a=1&b=2)
    @CookieValue 获取Cookie值
    @RequestBody 获取请求体[POST]
    @ModelAttribute

    参数解析大概原理:26个参数解析器,采用循环遍历的方式找对应的参数解析器,(参数解析器完成第一次加载后,会进到缓存中),15个返回值处理器,最常用的返回值类型:ModelAndView, Model, View, ResponseBody

  //  rest形如:car/2/owner/zhangsan   
@GetMapping("/car/{id}/owner/{username}") 
public Map<String ,Object> testParameter(
    @PathVariable("id") int id,                                            
    @PathVariable("username") String name,   
    @PathVariable Map<String ,String > map,//获取所以rest风格传递的参数键值对 id:值 username:值 
    @RequestHeader("User-Agent") String userAgent, //获取单个头 
    @RequestHeader Map<String ,String > heads, //获取所以请求头  
    @RequestParam("age") int age, //参数是单个值的获取          
    @RequestParam("like")List<String> likes, //参数是多个值的获取    
    @RequestParam Map<String , String > params,//获取所有的请求参数值
    @CookieValue("Idea-f4788bfe") String cookie, //获取cookie的值转成字符串 
    @CookieValue("Idea-f4788bfe") Cookie cookie2 //获取的cookie转成对象    
){    
    Map<String,Object> maps = new HashMap<>();  
    maps.put("id", id);       
    maps.put("username", name);   
    maps.put("map", map);    
    maps.put("userAgent", userAgent);   
    maps.put("heads", heads);       
    maps.put("age", age);       
    maps.put("likes", likes);    
    maps.put("params", params);    
    map.put("cookie",cookie);      
    System.out.println(cookie2);     
    map.put("cookieName",cookie2.getName());   
    return maps;    
}   
@PostMapping("/testRequestBody")   
public Map<String ,Object> testParameter2(@RequestBody String content){ 
    //获取请求体(POST请求才有体) "content": "username=dsadsa&password=dasdsa"     
    Map<String,Object> maps = new HashMap<>();  
    maps.put("content", content);    
    return maps;    
}

index.html

<h2>Test请求参数注解</h2>
<a href="car/2/owner/zhangsan?age=12&like=nv&like=启程">Test PathVariable</a><br/>
<a href="car/2/owner/zhangsan?age=12&like=nv&like=启程">Test RequestHeader</a>
<br/>
<a href="car/2/owner/zhangsan?age=12&like=nv&like=启程">Test RequestParam</a>
<br/>
<a href="car/2/owner/zhangsan?age=12&like=nv&like=启程">Test CookieValue</a>
<br/>
<form action="testRequestBody"  method="post">
    <input name="username" type="text">    
    <input name="password" type="password">   
    <input type="submit" value="Test RequestBody">
</form>
  • @RequestAttribute 获取request域属性
@GetMapping("goto")
public String goToPage(HttpServletRequest request){ 
    request.setAttribute("msg", "成功了..."); 
    request.setAttribute("code", 200); 
    return "forward:/success";//转发到 /success请求
}
@GetMapping("/params")
public String testParam(
    Map<String,Object> map,
    Model model,           
    HttpServletRequest request,    
    HttpServletResponse response){    
    map.put("hello", "world666");   
    model.addAttribute("world", "hello666");    
    request.setAttribute("message", "HelloWorld");
    Cookie cookie = new Cookie("c1", "v1");   
    response.addCookie(cookie);  
    return "forward:/success";
}
//<-----------------主角@RequestAttribute在这个方法
@ResponseBody@GetMapping("/success")
public Map success(@RequestAttribute(value = "msg",required = false) String msg, 
                   @RequestAttribute(value = "code",required = false)String code,     
                   @RequestAttribute(value = "hello" ,required = false) String hello,   
                   @RequestAttribute(value = "world" ,required = false) String world, 
                   @CookieValue("c1") String c1,                
                   HttpServletRequest request){  
    Object msg1 = request.getAttribute("msg");
    Map<String,Object> map = new HashMap<>();
    Object hello1 = request.getAttribute("hello"); 
    Object world1 = request.getAttribute("world"); 
    Object message = request.getAttribute("message");  
    map.put("annotation_msg",msg);   
    map.put("annotation_code",code);
    map.put("annotation_hello",hello);
    map.put("annotation_world",world); 
    map.put("reqMethod_msg1",msg1);    
    map.put("reqMethod_hello1",hello1);  
    map.put("reqMethod_world1",world1); 
    map.put("message",message);   
    map.put("cookie",c1);  
    return map;
}

index.html

<a href="goto">Test RequestAttribute</a>><br/><a href="params">Test RequestAttribute2</a>><br/>
  • @MatrixVariable 矩阵变量:默认是功能时关闭的需要手动开启

    • 语法: 请求路径:/cars/sell;low=34;brand=byd,audi,yd 映射路径:/cars/{path}

      ​ /boss/1;age=20/2;age=10 /boss/{path1}/{path2}

    • SpringBoot默认是禁用了矩阵变量的功能

      手动开启:原理。对于路径的处理。UrlPathHelper的removeSemicolonContent设置为false,让其支持矩阵变量的。矩阵变量必须有url路径变量才能被解析

      image-20210829190801048

    • 手动开启矩阵变量

      //========WebMvcConfigurer========
          @Configuration(proxyBeanMethods = false)
          public class WebConfig implements WebMvcConfigurer { 
              /*        手动开启矩阵变量        第一种方法: @Bean 在容器中洪注入一个    WebMvcConfigurer(一个接口)        第二种方法 实现 WebMvcConfigurer 重写需要修改的方法 configurePathMatch     */
              /*
              @Bean    
              public WebMvcConfigurer webMvcConfigurer(){  
              return new WebMvcConfigurer(){   
              @Override           
              public void configurePathMatch(PathMatchConfigurer configurer) {      
              UrlPathHelper urlPathHelper = new UrlPathHelper(); 
              urlPathHelper.setRemoveSemicolonContent(false); //修改默认值 相当于矩阵变量功能生效  即不移除分号;的内容              
              configurer.setUrlPathHelper(urlPathHelper);         
              }      
              };  
              }*/   
              @Override  
              public void configurePathMatch(PathMatchConfigurer configurer) {   
                  UrlPathHelper urlPathHelper = new UrlPathHelper();  
                  urlPathHelper.setRemoveSemicolonContent(false); //修改默认值 相当于矩阵变量功能生效  即不移除分号;的内容       
                  configurer.setUrlPathHelper(urlPathHelper);    
              }
          }
      
      //=========TestWebController 类=========== 
          //      /cars/sell;low=34;brand=byd,audi,yd    
          @GetMapping("/cars/{path}") //不能写成 cars/sell  一个sell;low=34;brand=byd,audi,yd 绑定一个{path}  否则会出现404  
          @ResponseBody    
          public Map carsSell(@MatrixVariable("low") Integer low,     
                              @MatrixVariable("brand") List<String > brand,   
                              @PathVariable("path") String path){  
          Map<String ,Object> map = new HashMap<>();
          map.put("low", low);     
          map.put("brand", brand);        
          map.put("path", path);        
          return map;   
      }   
      //     /boss/1;age=20/2;age=10    
      @GetMapping("/boss/{bossId}/{empId}") 
      @ResponseBody  
      public Map boss(@MatrixVariable(value = "age" ,pathVar = "bossId") Integer boosId,  
                      @MatrixVariable(value = "age" ,pathVar = "empId") Integer empId){
          Map<String ,Object> map = new HashMap<>(); 
          map.put("boosId", boosId);        
          map.put("empId", empId);      
          return map;   
      }
      

      index.html

      /cars/{path}?xxx=xx&aaa=ccc queryString 查询字符串:@RequestParam;<br>
      /cars/sell;low=34;brand=byd,audi,yd; 矩阵变量
      <br>
      页面开发,cookie禁用,session里面的内容怎么使用;session.set(a,b)---->jsessionid=xx--->cookie---->每次发请求携带  禁用cookie 导致整条链失效
      <br>
      url重写 : /abc;jsessionid=xxx 把cookie的值使用矩阵遍历的方式进行传递
      <br>
      <a href="/cars/sell;low=34;brand=byd,audi,yd">Test MatrixVariable</a>
      <br>
      <a href="/cars/sell;low=34;brand=byd,audi,yd;brand=aa">Test MatrixVariable</a>
      <br>
      <a href="/boss/1;age=20/2;age=10">Test MatrixVariable /boss/{bossId}/{bossId2}</a>
      <br>
      
2、请求参数注解的原理
  1. 这要从DispatcherServlet开始说起:请求访问都是从DispatcherServlet

    //====================DispatchServlet类 下的===============
        protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception 
    {   
        HttpServletRequest processedRequest = request;  
        HandlerExecutionChain mappedHandler = null;
        boolean multipartRequestParsed = false; 
        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); 
        try {      
            ModelAndView mv = null; 
            Exception dispatchException = null;  
            try {         
                processedRequest = checkMultipart(request);  
                multipartRequestParsed = (processedRequest != request);  
                //HandlerMapping中找到一个HandlerExecutionChain 链对象,能处理请求的Handler(Controller.method())的请求的controller和获取所有的拦截器。	 
                mappedHandler = getHandler(processedRequest);     
                if (mappedHandler == null) {    
                    noHandlerFound(processedRequest, response);     
                    return;       
                }       
                // 确定当前请求的处理程序适配器。     
                HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());   
                // 处理上次修改的标头(如果句柄支持)         
                String method = request.getMethod();   
                boolean isGet = HttpMethod.GET.matches(method); //判断是不是get请求 
                if (isGet || HttpMethod.HEAD.matches(method)) {    
                    long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); 
                    if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { 
                        return;        
                    }       
                }		     
                //执行拦截器的applyPreHandle方法,如果方法返回false就会被拦截
                if (!mappedHandler.applyPreHandle(processedRequest, response)) {    
                    return;         
                }        
                // 正真执行目标方法  调用我们自己写的controller中的方法   当中会进行参数解析(处理参数解析的方法) 返回值解析等流程        
                mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); 
               // ......
    
  2. 找寻合适 的适配器(前面找寻Handler的流程不在重复赘述)

    • HandlerMapping中找到能处理请求的Handler(Controller.method())。(前面的流程rest风格原理详细解析过)
      • 为当前Handler 找一个适配器 HandlerAdapter,用的最多的是RequestMappingHandlerAdapter。(默认会加载所有HandlerAdapter )
        • 0下标的支持方法上标注@RequestMapping
        • 1下标的支持函数式编程的
        • 3下标 访问静态资源
    • 使用适配器执行目标方法并确定方法参数的每一个值。
    image-20210830115436993
  3. 调用执行自己编写的controller中的请求方法(本节重点就是这个 mv = ha.handle(processedRequest, response, mappedHandler.getHandler());)

    image-20210830120427634

    1. 获取参数解析器、返回值处理器处理 上述方法的 invokeHandlerMethod()方法源码如下:

      • 获取参数解析器和返回值处理器,SpringMVC目标方法能写多少种参数类型。取决于参数解析器argumentResolvers。都有多个 参数解析器argumentResolvers

        [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9EZNqn8s-1642398938629)(https://gitee.com/pjgitee/note/raw/master/springboot%E7%AC%94%E8%AE%B0.assets/image-20210830120914785.png)]

        1. *this.argumentResolvers(参数解析器)**在 RequestMappingHandlerAdapterafterPropertiesSet()方法内初始化

          • if (this.argumentResolvers != null) {}//<-----关注点\

            public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter		implements BeanFactoryAware, InitializingBean {   
                @Nullable    
                private HandlerMethodArgumentResolverComposite argumentResolvers;
                @Override   
                public void afterPropertiesSet() {    
                    //...        
                        if (this.argumentResolvers == null) {
                            //初始化argumentResolvers           
                            List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();           
                            this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers); 
                        }     
                    //...   
                }    
                //初始化了一堆的实现HandlerMethodArgumentResolver接口的  
                private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() { 
                    List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(30);
                    //基于注释的参数解析  默认的就是添加了这些      
                    resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
                    resolvers.add(new RequestParamMapMethodArgumentResolver());     
                    resolvers.add(new PathVariableMethodArgumentResolver());       
                    resolvers.add(new PathVariableMapMethodArgumentResolver()); 
                    resolvers.add(new MatrixVariableMethodArgumentResolver());    
                    resolvers.add(new MatrixVariableMapMethodArgumentResolver());        resolvers.add(new ServletModelAttributeMethodProcessor(false));        
                    resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));       
                    resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));     
                    resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory())); 
                    resolvers.add(new RequestHeaderMapMethodArgumentResolver());   
                    resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory())); 
                    resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));  
                    resolvers.add(new SessionAttributeMethodArgumentResolver());       
                    resolvers.add(new RequestAttributeMethodArgumentResolver());     
                    // Type-based argument resolution     
                    resolvers.add(new ServletRequestMethodArgumentResolver());    
                    resolvers.add(new ServletResponseMethodArgumentResolver());    
                    resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));        
                    resolvers.add(new RedirectAttributesMethodArgumentResolver()); 
                    resolvers.add(new ModelMethodProcessor());     
                    resolvers.add(new MapMethodProcessor());     
                    resolvers.add(new ErrorsMethodArgumentResolver());    
                    resolvers.add(new SessionStatusMethodArgumentResolver());     
                    resolvers.add(new UriComponentsBuilderMethodArgumentResolver());  
                    if (KotlinDetector.isKotlinPresent()) {        
                        resolvers.add(new ContinuationHandlerMethodArgumentResolver());  
                    }       
                    // Custom arguments    
                    if (getCustomArgumentResolvers() != null) {    
                        resolvers.addAll(getCustomArgumentResolvers()); 
                    }      
                    // Catch-all       
                    resolvers.add(new PrincipalMethodArgumentResolver());
                    resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
                    resolvers.add(new ServletModelAttributeMethodProcessor(true));
                    return resolvers;    
                }
            }
            
        2. HandlerMethodArgumentResolver 参数解析器的接口定义 (当前解析器 判断 是否支持解析这种参数如果支持就调用 resolveArgument 解析)

          image-20210830121401602

          • 返回值处理器 this.returnValueHandlers 在 RequestMappingHandlerAdapterafterPropertiesSet()方法内初始化

            if (this.returnValueHandlers != null) {}//<—关注点

            public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter		implements BeanFactoryAware, InitializingBean {
                @Nullable
                private HandlerMethodReturnValueHandlerComposite returnValueHandlers;
                @Override	public void afterPropertiesSet() {
                    ...		
                        if (this.returnValueHandlers == null) {	
                            List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();			
                            this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
                        }	
                }       
                //初始化了一堆的实现HandlerMethodReturnValueHandler接口的   
                private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {	
                    List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>(20);
                    // Single-purpose return value types	
                    handlers.add(new ModelAndViewMethodReturnValueHandler());		
                    handlers.add(new ModelMethodProcessor());		
                    handlers.add(new ViewMethodReturnValueHandler());	
                    handlers.add(new ResponseBodyEmitterReturnValueHandler(getMessageConverters(),				this.reactiveAdapterRegistry, this.taskExecutor, this.contentNegotiationManager));		
                    handlers.add(new StreamingResponseBodyReturnValueHandler());	
                    handlers.add(new HttpEntityMethodProcessor(getMessageConverters(),				this.contentNegotiationManager, this.requestResponseBodyAdvice));		
                    handlers.add(new HttpHeadersReturnValueHandler());		
                    handlers.add(new CallableMethodReturnValueHandler());	
                    handlers.add(new DeferredResultMethodReturnValueHandler());	
                    handlers.add(new AsyncTaskMethodReturnValueHandler(this.beanFactory));	
                    // Annotation-based return value types		
                    handlers.add(new ServletModelAttributeMethodProcessor(false));	
                    handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(),				this.contentNegotiationManager, this.requestResponseBodyAdvice));	
                    // Multi-purpose return value types		
                    handlers.add(new ViewNameMethodReturnValueHandler());
                    handlers.add(new MapMethodProcessor());	
                    // Custom return value types	
                    if (getCustomReturnValueHandlers() != null) {	
                        handlers.addAll(getCustomReturnValueHandlers());	
                    }		
                    // Catch-all		
                    if (!CollectionUtils.isEmpty(getModelAndViewResolvers())) {		
                        handlers.add(new ModelAndViewResolverMethodReturnValueHandler(getModelAndViewResolvers()));
                    }else {		
                        handlers.add(new ServletModelAttributeMethodProcessor(true));	
                    }		return handlers;	
                }
            }
            
          • HandlerMethodReturnValueHandler 返回值处理器接口的定义

            image-20210830130931491

      • 正真执行我们自定义的方法

        image-20210830121805759

        1. 上述的 getMethodArgumentValues() 方法的源码

          //============InvocableHandlerMethod 该方法作用:确定每个参数的对应的值==============
              protected Object[] getMethodArgumentValues(
              NativeWebRequest request,
              @Nullable ModelAndViewContainer mavContainer,  
              Object... providedArgs) throws Exception {  
              MethodParameter[] parameters = getMethodParameters();
              //获取所有参数的详细信息  包括类型 标注了什么注解等  
              if (ObjectUtils.isEmpty(parameters)) { //判断是否有参数  没参数直接返回空参数   
                  return EMPTY_ARGS;   
              } 
              Object[] args = new Object[parameters.length]; //创建一个数组  用于保存各个参数的值   
              for (int i = 0; i < parameters.length; i++) { //遍历所有参数     
                  MethodParameter parameter = parameters[i]; //获取第i个参数   
                  parameter.initParameterNameDiscovery(this.parameterNameDiscoverer); 
                  args[i] = findProvidedArgument(parameter, providedArgs);  
                  if (args[i] != null) {      
                      continue;     
                  }     
                  if (!this.resolvers.supportsParameter(parameter)) {
                      //判断各个参数解析器(我的版本27个)是否支持这个参数类型的解析(前面有参数解析的截图)  为什么能够支持那么多参数类型,就是对这个接口的实现有很多种,每一种针对不同的参数类型(注解)进行支持、实现,然后解析。 
                      throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));    
                  }      
                  try {         
                      args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory); //解析参数值 这是参数解析接口中的方法  解析参数值  
                  }      catch (Exception ex) {   
                      // Leave stack trace for later, exception may actually be resolved and handled...
                      if (logger.isDebugEnabled()) {  
                          String exMsg = ex.getMessage();
                          if (exMsg != null &&  !exMsg.contains(parameter.getExecutable().toGenericString())) {    
                              logger.debug(formatArgumentError(parameter, exMsg));   
                          }        
                      }      
                      throw ex;
                  }  
              }  
              return args;
          
        2. 上述源码:if (!this.resolvers.supportsParameter(parameter)) 调用的方法源码

          image-20210830123318541

        3. getArgumentResolver()方法源码

          image-20210830174241726

          //====HandlerMethodArgumentResolverComposite的方法  作用:是寻找一个合适的参数解析器解析各个参数===========
              private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
              HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter); //第一次进去缓存为空,找缓存  能够提高效率    
              if (result == null) {   
                  for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) 
                  {
                      //argumentResolvers 27个参数解析器          
                      if (resolver.supportsParameter(parameter)) { 
                          //循环遍历每个参数解析器  每个参数解析判断能否处理当前的参数  方法中的每个参数(parameter当前的参数)都会循环判断(上述的getMethodArgumentValues()方法是对每个参数进行循环判断的)  找到一个能够处理当前参数的解析器就直接返回 27个参数解析器判断能否解析的当前参数的方法实现不同   
                          result = resolver;             
                          this.argumentResolverCache.put(parameter, result); //放入缓存中 
                          break;           
                      }   
                  }    
              }   
              return result;//返回的是能够解析当前参数的参数解析器
          }
          
      • 返回(上述的result)参数解析器后,会调用参数解析器的解析方法 resolveArgument()源码如下图:

        args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory); //解析参数值 这是参数解析接口中的方法 解析参数值(也是getMethodArgumentValues()方法中的源码) args数组中存储的是能够解析参数的解析器

        //......
            try {  
                args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);  //解析参数值 这是参数解析接口中的方法  解析参数值   
            }      catch (Exception ex) { 
                // Leave stack trace for later, exception may actually be resolved and handled...  
                if (logger.isDebugEnabled()) {    
                    String exMsg = ex.getMessage();   
                    if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) { 
                        logger.debug(formatArgumentError(parameter, exMsg));   
                    }      
                }         throw ex;    
            }  
        }   
        return args;
        
        1. resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory); 方法源码如下

          image-20210830123727844

      • 解析完成当前参数后,会解析下一个参数,依次循环上述步骤(依次解析各个参数)

    2. 上述步骤完成后,解析各个参数完成

  4. 接下面的复杂参数的源码

3、复杂的参数和原理
  1. Map、**Model(map、model里面的数据会被放在request的请求域 request.setAttribute)、**Errors/BindingResult、RedirectAttributes( 重定向携带数据)ServletResponse(response)、SessionStatus、UriComponentsBuilder、ServletUriComponentsBuilder

  2. 请求代码

    //测试复杂参数@GetMapping("/params")
    public String testParam(Map<String,Object> map, Model model, HttpServletRequest request, 
                            //三个参数类型都能往request域中放入数据 request.getAttribute()可获取到
                            HttpServletResponse response) 
    {   
        map.put("hello", "world666");    
        model.addAttribute("world", "hello666"); 
        request.setAttribute("message", "HelloWorld");  
        Cookie cookie = new Cookie("c1", "v1");   
        cookie.setDomain("localhost");   
        response.addCookie(cookie);    return "forward:/success";
    }
    
  3. 匹配到参数解析器后,调用当前解析器的解析方法(继续跟上面源码分析的流程) (这里解析参数相关)

    • Map、Model类型的参数,会返回 mavContainer.getModel();—> BindingAwareModelMap 是Model 也是Map

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BGxY8Hxq-1642398938633)(https://gitee.com/pjgitee/note/raw/master/springboot%E7%AC%94%E8%AE%B0.assets/image-20210830160544756.png)]

    • 参数解析完成后得到的 args(保存的是各个参数的值)

      image-20210830162700527

  4. 各个参数解析完成后,会执行自己编写的controller请求方法 处理完成后 会将需要放入到request域中的值存储到modelAndViewContainer中对象中 获取到返回值 在处理返回值结果

    image-20210830163109492

    • 上述的 图片中 handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest); 处理方法截图

      image-20210830163234557

    • 上述方法执行完成 mavContainer()对象的视图名也设置好了

      image-20210830163620703

  5. invocableMethod.invokeAndHandle(webRequest, mavContainer);上述是该方法完成之后,处理完调用自己的方法之后的操作,接上面3-1(3步骤的1(请求参数注解的原理模块))操作之后执行

    调用的是getModelAndView(mavContainer, modelFactory, webRequest);方法,改方法执行完成返回 ModelAndView 对象

    DispatchServlet的 mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); 执行完成

    image-20210830164339924

    //====================DispatchServlet类 下的===============
        protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {   
        HttpServletRequest processedRequest = request; 
        HandlerExecutionChain mappedHandler = null;
        boolean multipartRequestParsed = false;   
        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);  
        try {     
            ModelAndView mv = null;   
             Exception dispatchException = null;
            try {     
                processedRequest = checkMultipart(request);  
                multipartRequestParsed = (processedRequest != request);  
                //HandlerMapping中找到一个HandlerExecutionChain 链对象,能处理请求的Handler(Controller.method())的请求的controller和获取所有的拦截器。	
                mappedHandler = getHandler(processedRequest);   
                if (mappedHandler == null) {       
                    noHandlerFound(processedRequest, response);     
                    return;    
                }   
                // 确定当前请求的处理程序适配器。 
                HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
                //处理参数解析的方法      
                // 处理上次修改的标头(如果句柄支持)  
                String method = request.getMethod(); 
                boolean isGet = HttpMethod.GET.matches(method); 
                //判断是不是get请求     
                if (isGet || HttpMethod.HEAD.matches(method)) {    
                    long lastModified = ha.getLastModified(request, mappedHandler.getHandler());   
                    if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {  
                        return;       
                    }     
                }	     
                //执行拦截器的applyPreHandle方法,如果方法返回false就会被拦截    
                if (!mappedHandler.applyPreHandle(processedRequest, response)) {  
                    return;     
                }     
                // 正真执行目标方法  调用我们自己写的controller中的方法  当中会进行参数解析 返回值解析等流程 返回一个modelAndView对象     
                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
                if (asyncManager.isConcurrentHandlingStarted()) {			
                    return;			
                }			
                applyDefaultViewName(processedRequest, mv);   
                //处理拦截器的方法		
                mappedHandler.applyPostHandle(processedRequest, response, mv);	
            }		 catch (Exception ex) {	
                dispatchException = ex;	
            }	
            catch (Throwable err) {		
                // As of 4.3, we're processing Errors thrown from handler methods as well,	
                // making them available for @ExceptionHandler methods and other scenarios.	
                dispatchException = new NestedServletException("Handler dispatch failed", err);
            }   	
            //处理开发结果  会把model的数据放到域中	
            processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
        }
            //......
    
  6. 上述的 processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);的源码如下图

    image-20210830171518922

    • 上述 view.render()方法的源码如下:

      image-20210830175311271

    • renderMerOutputModel()方法源码如下:存储数据到了request域中

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bHdprxVK-1642398938636)(https://gitee.com/pjgitee/note/raw/master/springboot%E7%AC%94%E8%AE%B0.assets/image-20210830171618012.png)]

4、Servlet API:
  1. WebRequest、ServletRequest、MultipartRequest、 HttpSession、javax.servlet.http.PushBuilder、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId

  2. **ServletRequestMethodArgumentResolver **参数解析器用来处理以上的参数:可通过debug测试上述的"goto"请求

    image-20210830151856419

5、自定义参数绑定原理 POJO
  1. 实践代码

    pojo类

    @Datapublic class Person {  
        private String userName;  
        private Integer age;    
        private Date birth;   
        private Pet pet;
    }
    @Datapublic class Pet {  
        private String name;  
        private String age;
    }
    

    controller类:

    /*    测试封装pojo        数据绑定:页面提交的请求数据(GET、POST)都可以和对象属性进行绑定        @param person        @return */
    @ResponseBody@PostMapping("testPojo")
    public Person testPojo(Person person){ 
        return person;
    }
    

    index.html

    <h2>Test POJO</h2>
    <form action="testPojo" method="post">   
        姓名: <input name="userName"/> <br/>    
        年龄: <input name="age"/> <br/>   
        生日: <input name="birth"/> <br/>  
        宠物姓名:<input name="pet.name"/><br/>  
        宠物年龄:<input name="pet.age"/>  
        <input type="submit" value="提交">
    </form>
    
  2. 找到可以解析pojo类的参数解析器 参数解析器为:ServletModelAttributeMethodProcessor 解析器

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vEkTapdL-1642398938636)(C:\Users\彭 俊\AppData\Roaming\Typora\typora-user-images\image-20210831105928008.png)]

  3. 找到解析器后,调用当前解析器的解析方法 args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory); //解析参数值 这是参数解析接口中的方法 解析参数值

    ServletModelAttributeMethodProcessor 参数解析器解析参数的方法 resolveArgument() 解析源码如下:

    public final Object resolveArgument(
        MethodParameter parameter,
        @Nullable ModelAndViewContainer mavContainer,    
        NativeWebRequest webRequest,
        @Nullable WebDataBinderFactory binderFactory) throws Exception { 
        Assert.state(mavContainer != null, "ModelAttributeMethodProcessor requires ModelAndViewContainer");
        Assert.state(binderFactory != null, "ModelAttributeMethodProcessor requires WebDataBinderFactory");
        String name = ModelFactory.getNameForParameter(parameter); //通过传递过来的参数 获取参数类型的名称 
        ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);  
        if (ann != null) {//判断是否包含 ModelAttribute注解 
            mavContainer.setBinding(name, ann.binding());  
        } 
        Object attribute = null;  
        BindingResult bindingResult = null; 
        if (mavContainer.containsAttribute(name)) {
            //如果 mavContainer 包含了当前参数类型的名称 将会和mavContainer 中的整合  
            attribute = mavContainer.getModel().get(name);
        } else {     
            try {    
                // 创建一个类型为参数类型的空的实例   
                attribute = createAttribute(name, parameter, binderFactory, webRequest);
            }      catch (BindException ex) {  
                if (isBindExceptionRequired(parameter)) {  
                    // No BindingResult parameter -> fail with BindException    
                    throw ex;     
                }        
                // Otherwise, expose null/empty value and associated BindingResult 
                if (parameter.getParameterType() == Optional.class) {   
                    attribute = Optional.empty();    
                }         else {     
                    attribute = ex.getTarget();
                }         
                bindingResult = ex.getBindingResult(); 
            }   
        }  
        if (bindingResult == null) { 
            //Bean 属性绑定和验证;   
            // 在施工绑定失败的情况下跳过。   
            //binder  web数据绑定 作用:将每个传递过来的 参数转换、封装到 attribute中(这是一个循环的过程,每个参数都需要进行转换、封装)      
            //webRequest:原生的request,包含了需要封装的数据 
            WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
            if (binder.getTarget() != null) {    
                if (!mavContainer.isBindingDisabled(name)) {  
                    bindRequestParameters(binder, webRequest);//进行 转换 、封装,先将传过来的参数类型转换成目标数据类型 然后在封装到 对象中  改方法结束  attribute中的各个属性的值就已经封装完成 
                }        
                validateIfApplicable(binder, parameter);
                if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {            throw new BindException(binder.getBindingResult());         
                                                                                                         }
            }     
            // Value type adaptation, also covering java.util.Optional  
            if (!parameter.getParameterType().isInstance(attribute)) {
                attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);     
            }     
            bindingResult = binder.getBindingResult(); 
        }   
        // Add resolved attribute and BindingResult at the end of the model 
        Map<String, Object> bindingResultModel = bindingResult.getModel();  
        mavContainer.removeAttributes(bindingResultModel);   
        mavContainer.addAllAttributes(bindingResultModel);  
        return attribute;
    }
    
  4. WebDataBinder 利用它里面的 Converters 将请求数据转成指定的数据类型。再次封装到JavaBean中

    在过程当中,用到GenericConversionService:在设置每一个值的时候,找它里面的所有converter那个可以将这个数据类型(request带来参数的字符串)转换到指定的类型**(如:JavaBean 中的 Integer 类型)**

    转换器总接口:@FunctionalInterface public interface Converter<S, T> S 源数据类型 T目标数据类型

    image-20210831101056421

  5. 未来我们可以给WebDataBinder里面放自己的Converter;

    private static final class StringToNumber<T extends Number> implements Converter<String, T> Stirng(字符串)—>Number(数字)

    • 自定义 Converter

      • 实践代码

        index.html

      <form action="testPojo" method="post"> 
          姓名: <input name="userName"/> <br/> 
          年龄: <input name="age"/> <br/>    
          生日: <input name="birth"/> <br/>  
          <!--宠物姓名:<input name="pet.name"/><br/>
      		宠物年龄:<input name="pet.age"/>
      	-->    
          宠物:<input name="pet" value="阿猫,12"> 
          <input type="submit" value="提交">
      </form>
      

      上述如果不自定义类型转换器会报错如下:

      image-20210831110102662

      ​ 向容器中添加一个自定义的 Converter 转换器

      @Configuration(proxyBeanMethods = false)
      public class WebConfig   implements WebMvcConfigurer { 
          @Override    
          public void addFormatters(FormatterRegistry registry) {
              registry.addConverter(new Converter<String, Pet>(){  
                  @Override         
                  public Pet convert(String source) {   
                      if (StringUtils.hasLength(source)){   
                          Pet pet = new Pet();         
                          String[] split = source.split(",");
                          pet.setName(split[0]);           
                          pet.setAge(split[1]);        
                          return pet;        
                      }             
                      return null;    
                  }        
              });   
          }
      }
      

      image-20210831114903326

5、数据响应与内容协商
5.1、响应JSON
1、jackson.jar+@ResponseBody
  1. web依赖引入后,自动导入了json场景

    <dependency>   
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- 引入了上面的web场景 会自动导入json场景 -->
        <dependency>   
            <groupId>org.springframework.boot</groupId>  
            <artifactId>spring-boot-starter-json</artifactId>   
            <version>2.5.3</version>    
            <scope>compile</scope>
    </dependency>
    
  2. controller代码 测试

    /*    测试返回json */
    @ResponseBody@GetMapping("testJson")
    public Person testJson(){    
        Person person = new Person();    
        person.setUserName("张三");   
        person.setAge(12);    
        person.setBirth(new Date());   
        return person;
    }
    

    index.html

    <h2>Test Json</h2><a href="testJson">Test Json</a> 
    
  3. 标注了@ResponseBody ,可以直接给前端返回json数据

    image-20210831150929161

2、响应json原理解析
  1. 返回值处理器的总接口 HandlerMethodReturnValueHandler

    image-20210831154017115

  2. 获取所有的返回值处理器,调用自己写的请求方法(前面的流程和上述请求参数注解的原理一样)

    image-20210831154306028

    • 选择一个合适的返回值处理器方法(上述 handleReturnValue() 方法的源码)

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cxL7ejgC-1642398938639)(https://gitee.com/pjgitee/note/raw/master/springboot%E7%AC%94%E8%AE%B0.assets/image-20210831155048480.png)]

    • 上述找到合适的返回值处理器后,调用这个返回值处理器的处理方法

      handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);

      • 源码如下:进行返回值的处理

        image-20210831170400557

      • 消息转换器总接口 HttpMessageConverter: 看是否支持将 此 Class类型的对象,转为MediaType类型的数据。

        例子:Person对象转为JSON,或者 JSON转为Person,这将用到MappingJackson2HttpMessageConverter

      • 接上述writeWithMessageConverters()方法的内容(中间有些代码可能省略了) 重要点:MediaType 媒体(内容)类型

        内容协商:浏览器默认会以头的方式告诉服务器能够接收什么样的内容类型

        ​ 服务器会自己产生能够响应给浏览器的类型有哪些

        ​ 双方就需要协商(循环遍历),得到一个协商结果,就是浏览器既能接收的 ,服务器又可以响应的数据类型

        源码分析见 3 内容协商原理解析:

        image-20210831170424699

        image-20210831170119171

        内容协商循环遍历:

        image-20210831170107488

        循环遍历系统所有的消息转换器,找到一个能够处理当前响应类型(在上述的流程已经确定了(内容协商确定了))的消息转换器

        image-20210831170231511

        系统默认的消息转换器支持的各种数据类型如下:

        ​ 0 - 只支持Byte类型的
        ​ 1 - String
        ​ 2 - String
        ​ 3 - Resource
        ​ 4 - ResourceRegion
        ​ 5 - DOMSource.class \ SAXSource.class) \ StAXSource.class \StreamSource.class \Source.class
        ​ 6 - MultiValueMap
        ​ 7 - true
        ​ 8 - true
        ​ 9 - 支持注解方式xml处理的。

      • 找到了适合的消息转换器后,调用消息转换器write()方法 将其数据转换后写出去 (响应出去)

        image-20210831170054042

        上述图片中 writeInternal()方法的源码:最终 MappingJackson2HttpMessageConverter 把对象转为JSON(利用底层的jackson的objectMapper转换的)

        image-20210831170316863

  3. spring能支持那些返回值类型

    ModelAndView
    Model
    View
    ResponseEntity 
    ResponseBodyEmitter
    StreamingResponseBody
    HttpEntity
    HttpHeaders
    Callable
    DeferredResult
    ListenableFuture
    CompletionStage
    WebAsyncTask
    有 @ModelAttribute 且为对象类型的
    @ResponseBody 注解 ---> RequestResponseBodyMethodProcessor;
    
  4. 总结:返回值处理器 ReturnValueHandler 原理

    1. 返回值处理器判断是否支持这种类型返回值 supportsReturnType()方法判断
    2. 上述判断成功,返回值处理器调用 handleReturnValue() 进行处理
    3. RequestResponseBodyMethodProcessor 可以处理返回值标了@ResponseBody 注解的。
      1. 利用 MessageConverters 消息转换器 进行处理 将数据写为json
        1. 内容协商(浏览器默认会以请求头的方式告诉服务器他能接受什么样的内容类型)
        2. 服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据,
        3. SpringMVC会挨个遍历所有容器底层的 HttpMessageConverter ,看谁能处理?
          1. 得到MappingJackson2HttpMessageConverter可以将对象写为json
          2. 利用MappingJackson2HttpMessageConverter将对象转为json再写出去。
3、内容协商原理解析
  1. 根据客户端接(浏览器)收能力不同,返回不同媒体类型的数据。若客户端无法解析服务端返回的内容,即媒体类型未匹配,那么响应406

  2. 引入响应客服端(浏览器)xml的依赖

    可用Postman软件分别测试返回json和xml:只需要改变请求头中Accept字段(application/json、application/xml)。

    Http协议中规定的,Accept字段告诉服务器本客户端可以接收的数据类型。

    <!--引入 能够响应 xml 的依赖  版本仲裁了-->
    <dependency>    
        <groupId>com.fasterxml.jackson.dataformat</groupId>
        <artifactId>jackson-dataformat-xml</artifactId>
    </dependency>
    
  3. 引入上述依赖后 ,使用浏览器发送 请求 "testJson "请求

    image-20210831174725058

    image-20210831174753943

  4. 使用Postman发送 "testJson "请求

    image-20210831175447023

    image-20210831175617748

  5. 上述就证明了,消息转换器 会根据 客户端接收能力的不同 使用不同的消息转换器 将需要响应的数据转成客户端选择的接收的类型的数据

  6. 内容协商原理分析:

    1. acceptableTypes = getAcceptableMediaTypes(request); 源码 获取客户端(浏览器、postMan)可以接收的媒体类型(具体可见 4 小章节:基于请求参数的内容协商原理 6 小章节:自定义convert原理解析 7 小章节:浏览器与PostMan内容协商完全适配)

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sfXkNGZp-1642398938644)(https://gitee.com/pjgitee/note/raw/master/springboot%E7%AC%94%E8%AE%B0.assets/image-20210831192049564.png)]

    2. List producibleTypes = getProducibleMediaTypes(request, valueType, targetType); 源码。获取服务器能够处理当前返回值类型的消息转换器,获取到消息转换器后,获取消息转换器能够响应的媒体类型,存储到媒体类型的集合中,返回。

      image-20210831192039621

    3. mediaTypesToUse 是服务器能响应的数据类型和客户端能接收的数据类型循环最佳匹配后的得到的结果(只有响应和接收的数据类型相同就添加到这个mediaTypesToUse 媒体集合中) ,然后在 获取具体的响应数据的类型 ,只要不是 * 就选择哪一个

      image-20210831192939937

  7. 解释 使用浏览器发送 请求 "testJson "请求 返回的是xml 数据

    image-20210831195226777

  8. 总结

    • 判断当前响应头中是否已经有确定的媒体类型MediaType。
    • 获取客户端(PostMan、浏览器)支持接收的内容类型。(获取客户端Accept请求头字段application/xml)(这一步在下一节有详细介绍)
      • contentNegotiationManager 内容协商管理器 默认使用基于请求头的策略
      • HeaderContentNegotiationStrategy 确定客户端可以接收的内容类型
    • 获取服务器能够响应的媒体数据类型
      • 遍历循环所有当前系统的 MessageConverter,看谁支持操作这个对象(Person)
      • 找到支持操作Person的converter,把converter支持的媒体类型统计出来。
    • 客户端需要application/xml,服务端有10种MediaType。
      • 进行内容协商的最佳匹配媒体类型
    • 遍历循环所有当前系统的 MessageConverter,用 支持 将对象转为 最佳匹配媒体类型 的converter。调用它进行转化 。MessageConverter用了两次循环比较
4、基于请求参数的内容协商原理
  1. 未开启基于参数请求时,默认使用的是基于头的内容协商管理策略(HeaderContentNegotiationStrategy),分析如下:

    image-20210901085529955

  2. 由于浏览器无法支持我们自己发送请求头(除了ajax添加content-type字段),所以springboot提供了我们基于请求参数的内容协商管理器

    为了方便内容协商,开启基于请求参数的内容协商功能。

    spring: 
    	mvc:   
        	contentnegotiation:     
            	favor-parameter: true  #开启请求参数内容协商模式  默认值为false
    

    浏览器地址输入带format参数的URL:

    http://localhost:8080/test/person?format=json 或 http://localhost:8080/test/person?format=xml

  3. 开启基于请求参数的内容协商功能后,默认会有两个内容协商管理器

    ParameterContentNegotiationStrategy(由Spring容器注入,基于参数的内容协商管理器)

    HeaderContentNegotiationStrategy(基于请求头的内容协商管理器)

    就会循环遍历这两个内容协商管理器,由于会优先遍历 ParameterContentNegotiationStrategy,这个就会获取参数名为 format 参数的值,来确定客户端需要接收的数据类型是什么,参数值只能是:json、或者xml

    image-20210901091657441

5、自定义converter
  1. 实现多协议数据兼容。json、xml、myPj(这个是自创的)

    • @ResponseBody 响应数据出去 调用 RequestResponseBodyMethodProcessor 处理 方法返回值
    • Processor 处理方法返回值。通过 MessageConverter处理服务器响应,客户端接收的协商操作
    • 所有 MessageConverter 合起来可以支持各种媒体类型数据的操作(读、写)
    • 内容协商找到最终的 messageConverter,将其响应出去或者读到请求方法的参数中
  2. 系统默认添加的一些 convert

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NHPwxGhc-1642398938647)(https://gitee.com/pjgitee/note/raw/master/springboot%E7%AC%94%E8%AE%B0.assets/image-20210901100207974.png)]

  3. 自定义convert步骤

    • 自定义一个 convert 类,实现 消息转换器的总接口HttpMessageConverter 实现里面的方法

      /*    自定义convert    
      实现 消息转换器的总接口 HttpMessageConverter
      */
      public class MyConvert implements HttpMessageConverter<Person> {
          @Override    
          public boolean canRead(Class<?> clazz, MediaType mediaType) {
              //读 @Response 能读能写  读标注在方法的参数上  可以把前端(json、xml等类型的)的参数值封装到参数上        //false 代表不可以读浏览器发送过来的数据     
              return false;    
          }       
          public boolean canWrite(Class<?> clazz, MediaType mediaType) {  
              //服务器是否可以响应出去  返回true 代表什么类型都可以响应出去    
              //是person类型才可以服务器才可以响应出去 application/myPj 这种媒体类型   
              return clazz.isAssignableFrom(Person.class);   
          }   
          /*    
          服务器要统计所有MessageConverter都能写出哪些内容类型 
          */ 
          public List<MediaType> getSupportedMediaTypes() {   
              return MediaType.parseMediaTypes("application/myPj"); //增加媒体类型  accept头发送的是这个类型的接收  就会和这个进行匹配 ,匹配成功 就调用 canWrite()、canRead()方法 返回了true的对应的write()、read()方法 
          }     
          //服务器获取到所有的messageConvert后,会循环判断每个messageConvert能不能读,能不能写,满足了才会调用下面的方法    
          /*        
          能读 canRead()返回true 才会调用此方法  
          */    
          public Person read(Class<? extends Person> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {   
              return null;   
          }   
          /*      能读 canWrite()返回true 才会调用此方法   */ 
          public void write(Person person, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {   
              OutputStream body = outputMessage.getBody();//获取流数据        
              //自定义响应格式的内容   
              String result = person.getUserName()+";"+person.getAge()+";"+ person.getBirth(); 
              //响应出去出去        
              body.write(result.getBytes()); 
          }
      }
      
    • 向springmvc中添加这个messageConvert:SpringMVC的什么功能,一个入口给容器中添加一个 WebMvcConfigurer,重写里面的方法即可,添加、修改springmvc的特定功能

      /*    web的配置类 */
      @Configuration(proxyBeanMethods = false)
      public class WebConfig   implements WebMvcConfigurer { 
          /*    覆盖所有的 convert  这个方法重写  系统默认的所有messageConvert都会失效    
          @Override   
          public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {    }*/
          /*        在原有的基础上扩展一些convert     */    
          @Override    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) { 
              converters.add(new MyConvert()); //将自定义的convert添加到服务器中  服务器就可以响应 application/myPj 类型的请求了  
          }
      }
      
    • 编写请求方法

      /*    自定义 MessageConverter    
      1、浏览器发请求直接返回 xml    [application/xml]        jacksonXmlConverter  
      2、如果是ajax请求 返回 json   [application/json]      jacksonJsonConverter 
      3、如果硅谷app发请求,返回自定义协议数据  [appliaction/myPj]   xxxxConverter  
      自定义响应的格式:属性值1;属性值2;属性值3;...     
      步骤:    
      1、添加自定义的MessageConverter进系统底层     
      2、系统底层就会统计出所有MessageConverter能操作哪些类型  
      3、客户端和服务器内容协商 [myPj--->myPj] */
      @ResponseBody@GetMapping("testMyConvert")
      public Person testMyConvert(){ 
          Person person = new Person();   
          person.setUserName("李四");   
          person.setAge(6);    
          person.setBirth(new Date()); 
          return person;
      }
      
    • 测试

      image-20210901103322632

6、自定义convert原理解析

image-20210901110249615

  • 找到客户端接收的类型(acceptableTypes)和服务器响应的类型(producibleTypes)之后,就开始做协商(循环遍历匹配),精确匹配后,确定响应的类型就是 application/myPj ,然后就再循环所有的messageConvert寻找能够处理当前响应类型的messageConvert,匹配到了就 调用当前messageConvert的write()方法进行格式化 最后响应出去 交个客户端
7、浏览器与PostMan内容协商完全适配
  1. 浏览器 通过format 指定需要响应的内容是什么,但是format的值只能为 json或者xml 如果值是其他就响应不了(如何以参数的方式进行内容协商)

    image-20210901111157675

    ParameterContentNegotiationStrategy 内容协商器 支持 响应两种数据类型

    image-20210901113119780

  2. 改变默认浏览器请求参数的协商步骤:

    • 必须开启基于请求参数的内容协商

      spring: 
      	mvc:    
      	contentnegotiation:    
          	favor-parameter: true  #开启请求参数内容协商模式  默认值为false
      
    • 修改 内容协商 策略

      • 第一种办法:
      /*    web的配置类 */
      @Configuration(proxyBeanMethods = false)
      public class WebConfig   implements WebMvcConfigurer {   
          /*        在原有的基础上扩展一些convert     */ 
          @Override    
          public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {   
              converters.add(new MyConvert()); //将自定义的convert添加到服务器中  服务器就可以响应 application/myPj 类型的请求了   
          }  
          /**     * 配置内容协商选项     *  如何以参数的方式进行内容协商     */
          public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {  
              Map<String, MediaType> mediaTypes = new HashMap<>();     
              mediaTypes.put("json", MediaType.APPLICATION_JSON);//方法参数的值为 json 映射到json媒体类型 
              mediaTypes.put("xml", MediaType.APPLICATION_XML);//方法参数的值为 xml 映射到xml媒体类型   
              mediaTypes.put("gg", MediaType.parseMediaType("application/myPj")); //方法参数的值为 gg 映射到添加自定义的媒体类型       
              //参数需要 Map<String, MediaType> mediaTypes  
                  ParameterContentNegotiationStrategy parameterStrategy = new ParameterContentNegotiationStrategy(mediaTypes);       
              parameterStrategy.setParameterName("format"); //设置请球参数的名称  
              //参数需要  List<ContentNegotiationStrategy>     
              configurer.strategies(Arrays.asList(parameterStrategy));  
          }
      }
      
      • 第二种办法 配置文件中添加 内容协商策略

        spring:  
        	mvc:    
        		contentnegotiation: 
                	favor-parameter: true     
                    media-types:        gg: application/myPj
        
    • 修改 内容协商策略后,ParameterContentNegotiationStrategy 内容协商器 支持 响应三种数据类型

      image-20210901113215993

  3. 产生的问题,使用postMan发送请求 ,不管accept头是什么数据类型,返回都是json,通过上图也看到,没有

    HeaderContentNegotiationStrategy(基于请求头的内容协商管理器),所以导致,没有能够解析请求头的内容协商管理器,就会返回 /

    image-20210901113611842

    产生的问题原因:有可能我们添加的自定义的功能会覆盖默认很多功能,导致一些默认的功能失效。

    image-20210901114622436

    解决办法:添加的解析基于请求头的内容协商管理器

    /*    web的配置类 */
    @Configuration(proxyBeanMethods = false)
    public class WebConfig   implements WebMvcConfigurer {
        /*        在原有的基础上扩展一些convert     */
        @Override    
        public void extendMessageConverters(List<HttpMessageConverter<?>> converters) { 
            converters.add(new MyConvert());//将自定义的convert添加到服务器中  服务器就可以响应 application/myPj 类型的请求了  
        }    
        /**     * 配置内容协商选项     *  如何以参数的方式进行内容协商     */  
        public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { 
            Map<String, MediaType> mediaTypes = new HashMap<>();  
            mediaTypes.put("json", MediaType.APPLICATION_JSON);//方法参数的值为 json 映射到json媒体类型  
            mediaTypes.put("xml", MediaType.APPLICATION_XML);//方法参数的值为 xml 映射到xml媒体类型  
            mediaTypes.put("gg", MediaType.parseMediaType("application/myPj")); //方法参数的值为 gg 映射到添加自定义的媒体类型  
            //参数需要 Map<String, MediaType> mediaTypes     
            ParameterContentNegotiationStrategy parameterStrategy = new ParameterContentNegotiationStrategy(mediaTypes);      
            parameterStrategy.setParameterName("format"); //设置请球参数的名称    
            HeaderContentNegotiationStrategy headerStrategy = new HeaderContentNegotiationStrategy();//添加的解析基于请求头的内容协商管理器     
            //参数需要 List<ContentNegotiationStrategy>        
            configurer.strategies(Arrays.asList(parameterStrategy,headerStrategy));	}}
    

    image-20210901115605154

  4. 解决后的结果

    image-20210901115306806

  5. 大家考虑,上述功能除了我们完全自定义外?SpringBoot有没有为我们提供基于配置文件的快速修改媒体类型功能?怎么配置呢?【提示:参照SpringBoot官方文档web开发内容协商章节】

    spring.mvc.contentnegotiation.media-types: {gg: application/x-guigu}

    spring: 
    	mvc:  
        	contentnegotiation:   
            	favor-parameter: true 
                media-types:        gg: application/myPj
    
5.2、视图解析器与模板引擎

SpringBoot默认不支持 JSP,需要引入第三方模板引擎技术实现页面渲染。

1、模板引擎-Thymeleaf
  1. Thymeleaf官网文档使用地址:https://www.thymeleaf.org/documentation.html

    • 简介:Thymeleaf 是适用于 Web 和独立环境的现代服务器端 Java 模板引擎。
    • Thymeleaf的主要目标是将优雅的自然模板引入到您的开发工作流中——HTML可以在浏览器中正确显示,也可以作为静态原型,允许开发团队进行更强的协作。
    • 不适合高并发的项目,适合单体项目。
  2. 基本语法

    • 官网基础语法网址:https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#setting-attribute-values

    • 表达式

      表达式名字语法用途
      变量取值${…}获取请求域、session域、对象等值
      选择变量*{…}获取上下文对象值
      消息#{…}获取国际化等值
      链接@{…}生成链接
      片段表达式~{…}jsp:include 作用,引入公共页面片段
    • 字面量

      • 文本值: ‘one text’ , ‘Another one!’
      • 数字: 0 , 34 , 3.0 , 12.3
      • 布尔值: true , false
      • 空值: null
      • 变量: one,two,… 变量不能有空格
    • 文本操作

      • 字符串拼接: +
      • 变量替换: |The name is ${name}|
    • 数学运算

      • 运算符: + , - , * , / , %
    • 布尔运算

      • 运算符: and , or
      • 一元运算: ! , not
    • 比较运算

      • 比较: > , < , >= , <= ( gt , lt , ge , le )
      • 等式: == , != ( eq , ne )
    • 条件运算

      • If-then: (if) ? (then)
      • If-then-else: (if) ? (then) : (else)
      • Default: (value) ?: (defaultvalue)
    • 特殊操作

      • 无操作: _
    • 设置属性值 语法:th:attr

      • 设置单个值

        <form action="subscribe.html" th:attr="action=@{/subscribe}">  
            <fieldset>    
                <input type="text" name="email" />   
                <input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>
            </fieldset>
        </form>
        
      • 设置多个值

        <img src="../../images/gtvglogo.png"       th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />
        
    • 所有h5兼容的标签写法

      https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#setting-value-to-specific-attributes

    • 迭代(循环)

      <tr th:each="prod : ${prods}">   
          <td th:text="${prod.name}">Onions</td>   
          <td th:text="${prod.price}">2.41</td>  
          <td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
      </tr>
      <tr th:each="prod,iterStat : ${prods}" th:class="${iterStat.odd}? 'odd'">   
          <td th:text="${prod.name}">Onions</td> 
          <td th:text="${prod.price}">2.41</td>   
          <td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
      </tr>
      
    • 条件运算

      <a href="comments.html"	th:href="@{/product/comments(prodId=${prod.id})}"	th:if="${not #lists.isEmpty(prod.comments)}">view</a>
      <div th:switch="${user.role}">
          <p th:case="'admin'">User is an administrator</p>    
          <p th:case="#{roles.manager}">User is a manager</p>    
          <p th:case="*">User is some other thing</p>
      </div>
      
    • 属性优先级

      image-20210901161511090

  3. Thymeleaf的使用

    1. 引入Starter

      <!--引入Thymeleaf的依赖-->
      <dependency>  
          <groupId>org.springframework.boot</groupId>
          <artifactId>pring-boot-starter-thymeleaf</artifactId>
      </dependency>
      
    2. Thymeleaf 就已经配置好了

      image-20210901162206480

      //===========ThymeleafAutoConfiguration 自动配置类==================
      @Configuration(proxyBeanMethods = false) //配置类
      @EnableConfigurationProperties(ThymeleafProperties.class) // 自动载入应用程序所需的所有Bean 
      @ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
      @AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
      public class ThymeleafAutoConfiguration {}
      
    3. ThymeleafAutoConfiguration自动配置类中 自动配置好了以下东西

      • 绑定了类 ThymeleafProperties,所有的设置都在该类中

      • 配置好了 SpringTemplateEngine 模板引擎

      • 配好了 ThymeleafViewResolver 视图解析器

        视图解析器的前后缀

        //ThymeleafProperties 类下的属性默认值
        public static final String DEFAULT_PREFIX = "classpath:/templates/";//模板放置处
        public static final String DEFAULT_SUFFIX = ".html";//文件的后缀名
        
    4. 我们只需要直接开发页面

      1. 页面代码

        ​ controller.java

        /*
            thymeleaf 模板引擎的测试
         */
        @GetMapping("testThymeleaf")
        public String testThymeleaf(Model model){
            model.addAttribute("msg", "呵呵呵");
            model.addAttribute("link", "www.baidu.com");
            return "success"; //会频写前后缀  classpath:/templates/success.html
        }
        

        ​ success.html (必须引入的名称空间:xmlns:th=“http://www.thymeleaf.org”)

        <!DOCTYPE html>
        <html lang="en" xmlns:th="http://www.thymeleaf.org">
        <head>
            <meta charset="UTF-8">
            <title>Title</title>
        </head>
        <body>
        <!--el表达式写法:<h1>${msg}</h1>-->
        <h1 th:text="${msg}">哈哈</h1><br>
        <a href="www.aitigui.com" th:href="${link}">去百度</a><br/> <!--以存储的值的形式显示-->
        <a href="www.aitigui.com" th:href="@{link}">去百度2</a><!--已存储的键表示访问链接 会动态加上键前面的字符串 如果项目有前置访问路径,也会被添加上-->
        </body>
        </html>
        

        image-20210901164404246

      2. 给项目添加前置访问路径,添加后访问当前项目都得加上改前置路径

        server:
          servlet:
            context-path: /myPj #添加项目的前置路径
        

        image-20210901165135772

        这个设置后,URL要插入/myPj, 如 http://localhost:8888/myPj/testThymeleaf

2、后台管理系统(Springboot-2项目)
  1. 表单重复提交问题

    image-20210901172714065

    @Controller
    public class ThymeleafController {
    
        @GetMapping(value = {"/","/login"})
        public String goToLogin(){
            return "login";
        }
        /*
            直接去main页面  会导致登陆成功后,按刷新按钮 导致页面重复提交
         */
      /*  @PostMapping("/login")
        public String login(User user){
            return "main";
        }*/
    
        /*
            登陆成功  重定向到  /main.html 请求 在跳转到main页面,如果点刷新按钮  也不会刷新当前请求  只会刷新 /main.html  该请求就直接跳转到 main页面
         */
       /*  @PostMapping("/login")
        public String login(User user){
            return "redirect:/main.html";
        }
    
        /*
            重定向防止登陆重复提交
                最终去main页面的方法
                这样会导致 不用登录也能去main页面
         */
       /* @GetMapping("/main.html")
        public String goToMain(){
            return "main";
        }*/
    
    上述会导致 不用登录也能去main页面
    
    
  2. 解决不用登录去mian页面的问题

      @PostMapping("/login")   
    public String login(User user , HttpSession session, Model model){  
        //判断输入的用户名和密码是否为空  密码是 123 就登陆成功     
        if (StringUtils.hasLength(user.getPassword()) && "123".equals(user.getPassword())){  
            //不为空           
            //把登陆的用户放到session中       
            session.setAttribute("loginUser", user);          
            //跳转到主页面           
            return "redirect:/main.html";     
        }else {        
            //为空           
            //保存错误性返回登录页      
            model.addAttribute("errorMessage", "用户名密码错误");  
            return "login";      
        }   
    }    
    @GetMapping("/main.html") 
    public String goToMain(HttpSession session,Model model){
        Object loginUser = session.getAttribute("loginUser"); 
        if (loginUser!=null){          
            //登陆成功采访问 main页面  一般用拦截器‘过滤器做
            return "main";        
        }else {            //保存错误性返回登录页  
            model.addAttribute("errorMessage", "未登录");  
            return "login";        
        }   
    }
    
  3. thyeleaf行内写法,直接获取某个域中的值

    image-20210901175212740

  4. 抽取公共页面的内容(左侧导航和头部)

    • 抽取公共页官方文档:https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#template-layout

    • 将某段代码标记为公共页(第一种方式):使用 thymeleaf 的 th:fragment =" 公共模块名称" 属性 标记为公共标签

    • 引入公共标签:使用thymeleaf 的 th:insert=“页面名称 :: 公共模块名称” th:replace=“页面名称 :: 公共模块名称” th:include=“页面名称 :: 公共模块名称”

      image-20210902090059142

    • 将某段代码标记为公共页(第二种方式):使用 标签 的 id=" id名称" 属性 标记为公共标签

    • 引入公共标签:使用 thymeleaf 的 th:insert=“页面名称 :: #id名称” th:replace=“页面名称 :: #id名称” th:include=“页面名称 :: #id名称”

      image-20210902090825296

  5. 循环遍历数据 使用 thymeleaf 的 th:each=“遍历单个对象的名称1,状态名(需要时在写)😒{存储在域中的集合}”

    image-20210902094306138

3、视图解析的流程分析
  1. 执行目标方法 ,根据目标方法的返回值,判断那个返回值消息转换器能够处理当前的返回值的数据,找到返回值消息处理器后,调用改转换器的处理方法。

    (所有数据,视图信息都会存储到 ModelAndViewContainer 中 ,这个ViewNameMethodReturnValueHandler 返回值消息处理器能够处理字符串)

    image-20210902104009149

  2. 将上述返回值处理器处理的modelAndView对象封装到modelView对象中

    image-20210902104140279

  3. 根据上述返回的modelView对象,判断是否为空,为空会设置一个默认的视图地址。

    image-20210902104214079

  4. 将modelView对象进行渲染操作,会找到系统中所有的视图解析器,循环遍历,看哪个视图解析器,能够解析该modelView对象的视图名称,可能有多个能够解析 该modelView对象的视图名称 的view对象,在进行最佳匹配,获取一个 view对象 返回

    image-20210902113127425

    • 上述 解析 modelView对象的视图名称的源码,ContentNegotiatingViewResolver包含了其他四个视图解析器。就会循环遍历这个四个视图解析器,获取我们候选的视图对象,每个视图解析器对象能否将当前视图名称解析成一个view对象,能就会封装到一个存储 view的集合(候选的视图对象) 中,最后将这个候选的视图对象返回

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Iwz3T8e8-1642398938656)(https://gitee.com/pjgitee/note/raw/master/springboot%E7%AC%94%E8%AE%B0.assets/image-20210902113341778.png)]

    • 上述解析 视图名称的源码:resolveViewName()方法的源码

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UPs1cO1Z-1642398938657)(https://gitee.com/pjgitee/note/raw/master/springboot%E7%AC%94%E8%AE%B0.assets/image-20210902113520411.png)]

    • 扩展 视图名称的源码:resolveViewName()方法的源码

      返回值前缀为forward或者为普通字符串时的处理方法:

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-89BtBT8B-1642398938657)(https://gitee.com/pjgitee/note/raw/master/springboot%E7%AC%94%E8%AE%B0.assets/image-20210902121636484.png)]

      返回的字符串不是以 redirect 或者 forward 开头,返回的view对象详细情况:

      image-20210902121702777

  5. 视图对象的总接口

    image-20210902115518202

  6. 上述返回的view对象,调用返回的view自定义的渲染方法,进行转发或者重定向或者模板引擎创建HTML页面等方式进行渲染,当前请求是重定向请求,调用的是 RedirectView类 的render()方法,在渲染合并输出模型,进行重定向操作

    image-20210902115420287

    • 进行重定向操作,就是用的是原生的重定向方法 response.sendRedirect(encodedURL);进行重定向
  7. image-20210902115420287

  8. 总结

    • 目标方法处理的过程中(阅读DispatcherServlet源码),所有数据都会被放在 ModelAndViewContainer 里面,其中包括数据和视图地址。
    • 方法的参数是一个自定义类型对象(从请求参数中确定的),把他重新放在 ModelAndViewContainer 。
    • 任何目标方法执行完成以后都会返回ModelAndView(数据和视图地址)。
    • processDispatchResult()处理派发结果(页面改如何响应)
      • render(mv, request, response); 进行页面渲染逻辑
        • 根据方法的String返回值得到 View 对象【定义了页面的渲染逻辑】
        • 所有的视图解析器尝试是否能根据当前返回值得到View对象
        • 得到了 redirect:/main.html --> Thymeleaf new RedirectView()。
        • ContentNegotiationViewResolver 里面包含了下面所有的视图解析器,内部还是利用下面所有视图解析器得到视图对象。
        • view.render(mv.getModelInternal(), request, response); 视图对象调用自定义的render进行页面渲染工作。
          • RedirectView 如何渲染【重定向到一个页面】
          • 获取目标url地址
          • response.sendRedirect(encodedURL); redirect:/main.html 通过thymeleaf创建RedirectView对象,然后该对象调用原生response的sendRedirect(url)方法
  9. 不同返回值处理的方式

    • 返回值以 forward: 开始:

      new InternalResourceView(forwardUrl); --> 转发request.getRequestDispatcher(path).forward(request, response);

    • 返回值以 redirect: 开始: new RedirectView() --> render就是重定向

    • 返回值是普通字符串:new ThymeleafView()—> 之后调用模板引擎的process方法进行页面渲染(用writer输出)

6、web开发源码流程总结(4-5章节)
1、从 DispatcherServlet为入口
`1.1、以 doDispatch() 方法 为方法入口
  1. 调用 mappedHandler = getHandler(processedRequest); 找到那个Hnadler(Controller处理器(类))可以执行当前的请求,如 :访问 / 就是访问的 WelcomePageHandlerMapping

  2. HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); 确定当前请求的处理程序适配器。确定当前请求的处理程序适配器。

    为当前Handler 找一个适配器 HandlerAdapter,用的最多的是RequestMappingHandlerAdapter。(默认会加载所有HandlerAdapter )

  3. mv = ha.handle(processedRequest, response, mappedHandler.getHandler());) 使用适配器执行目标方法并确定方法参数的每一个值。调用执行自己编写的controller中的请求方法

    • invokeHandlerMethod() 方法 获取参数解析器和返回值处理器
    • invokeForRequest() 正真执行我们自定义的方法
      • getMethodArgumentValues() 获取我们所有传递过去的参数
2、
7、拦截器
7.1、拦截器的总接口定义

image-20210903090341284

7.2、拦截器的使用
1、使用步骤
  1. 编写一个拦截器实现HandlerInterceptor接口

    /*   登陆拦截器步骤      
    1、编写一个拦截器  实现handlerInterceptor接口 实现接口中的三个方法       
    2、将当前拦截器添加到容器中,并指定拦截的路径和放行的路径 ,这是配置springmvc的功能,写个配置类 ,实现WebMvcConfigurer  实现接口中的方法,定制化springmvc的功能 添加拦截器实现 接口中的 addInterceptors()方法 
    */
    @Slf4j
    public class LoginInterceptor implements HandlerInterceptor {   
        /*        
        在执行自己写的目标方法之前执行    
        */   
        @Override   
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {        
            log.info("拦截的路径有{}",request.getRequestURL());//打印拦截的路径
            HttpSession session = request.getSession();        
            Object loginUser = session.getAttribute("loginUser");    
            if (loginUser != null){          
                return true;//放行   
            }       
            //转发       
            request.setAttribute("errorMessage", "您还未登陆"); 
            request.getRequestDispatcher("/").forward(request, response);//        session.setAttribute("msg", "您还未登陆");
            //response.sendRedirect("/"); //未登录重定向到登录页      
            return false;//拦截   
        }    
        /*          
        在执行自己写的目标方法执行完毕之后执行
        */   
        @Override    
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {    
            log.info("postHandle执行",modelAndView);    
        }    
        /*            
        在渲染完成之后执行  
        */   
        @Override    
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {   
            log.info("afterCompletion执行",handler); 
        }
    }
    
  2. 拦截器注册到容器中(实现WebMvcConfigurer的addInterceptors()) 指定拦截规则(注意,如果是拦截所有,静态资源也会被拦截】

    /** * webMvc定制化功能 */
    @Configuration
    public class MyWebMvcConfig implements WebMvcConfigurer {  
        @Override    
        public void addInterceptors(InterceptorRegistry registry) { 
            registry.addInterceptor(new LoginInterceptor()) //添加拦截器    
                .addPathPatterns("/**") //添加拦截器的拦截路径 /**拦截所有  静态资源也会拦截    
                .excludePathPatterns(Arrays.asList("/","/login"))//不拦截某些路径  
                .excludePathPatterns("/css/**","/fonts/**","/images/**","/js/**","/favicon.ico");//放行静态资源  可以将静态资源放到一个目录中 放行这个静态资源目录即可   也可以配置静态资源访问前缀,放行这个前缀路径即可    }}
    
  3. 拦截方法的执行顺序

    image-20210903092216685

7.3、拦截器的源码分析
1、doDispatch()整个方法源码
//====================DispatchServlet类 下的 doDispatch()整个方法源码===============
    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {   
    HttpServletRequest processedRequest = request;   
    HandlerExecutionChain mappedHandler = null;  
    boolean multipartRequestParsed = false;   
    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);  
    try {      
        ModelAndView mv = null;
        Exception dispatchException = null;
        try {    
            processedRequest = checkMultipart(request);
            multipartRequestParsed = (processedRequest != request);
            //HandlerMapping中找到一个HandlerExecutionChain 链对象,能处理请求的Handler(Controller.method())的请求的controller和获取所有的拦截器。	    
            mappedHandler = getHandler(processedRequest);  
            if (mappedHandler == null) {    
                noHandlerFound(processedRequest, response);        
                return;     
            }     
            // 确定当前请求的处理程序适配器。
            HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); 
            //处理参数解析的方法     
            // 处理上次修改的标头(如果句柄支持)      
            String method = request.getMethod();      
            boolean isGet = HttpMethod.GET.matches(method); //判断是不是get请求   
            if (isGet || HttpMethod.HEAD.matches(method)) {     
                long lastModified = ha.getLastModified(request, mappedHandler.getHandler());    
                if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
                    return;          
                }  
            }	 
            //目标方法之前执行 执行拦截器的applyPreHandle方法,如果方法返回false就会被拦截   
            if (!mappedHandler.applyPreHandle(processedRequest, response)) {    
                return;     
            }      
            // 正真执行目标方法  调用我们自己写的controller中的方法  当中会进行参数解析 返回值解析等流程 返回一个modelAndView对象      
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());   
            if (asyncManager.isConcurrentHandlingStarted()) {
                return;		
            }		
            applyDefaultViewName(processedRequest, mv);     
            //目标方法执行之后  处理拦截器的postHandle()方法	
            mappedHandler.applyPostHandle(processedRequest, response, mv);
        }		
        catch (Exception ex) {	
            dispatchException = ex;		
        }		
        catch (Throwable err) {
            // As of 4.3, we're processing Errors thrown from handler methods as well,			
            // making them available for @ExceptionHandler methods and other scenarios.			
            dispatchException = new NestedServletException("Handler dispatch failed", err);	
        }   		
        //处理开发结果  会把model的数据放到域中  渲染处理完后会倒序执行各个拦截器的afterCompletion()方法
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);	
    }		
    catch (Exception ex) { 
        //上述那个环节 抛出异常  都会倒序执行各个拦截器的afterCompletion()方法 
        triggerAfterCompletion(processedRequest, response, mappedHandler, ex);	
    }		catch (Throwable err) {
        //上述那个环节 抛出异常  都会倒序执行各个拦截器的afterCompletion()方法 		
        triggerAfterCompletion(processedRequest, response, mappedHandler,	
                               new NestedServletException("Handler processing failed", err));
    }		
    finally {		
        if (asyncManager.isConcurrentHandlingStarted()) {
            // Instead of postHandle and afterCompletion	
            if (mappedHandler != null) {				
                mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
            }		
        }			
        else {	
            // Clean up any resources used by a multipart request.
            if (multipartRequestParsed) {			
                cleanupMultipart(processedRequest);		
            }		
        }		
    }	
}
2、获取handler的执行链
  1. ​ mappedHandler = getHandler(processedRequest); 获取执行链 包含处理请求的controller和所有拦截器

    image-20210903102128193

3、执行拦截器的applyPreHandle()方法
  1. ​ mappedHandler.applyPreHandle(processedRequest, response) 目标方法之前执行 执行拦截器的applyPreHandle方法,如果方法返回false就会被拦截, 源码如下:

    image-20210903102424874

4、执行完目标方法之后,在执行mappedHandler.applyPostHandle(processedRequest, response, mv);
  1. ​ 倒序遍历所有的拦截器,执行拦截器的POSTHandel()方法

    image-20210903102626315

5、上述方法执行完后,在渲染跳转视图,执行完后,执行拦截器的afterCompletion()方法

image-20210903102820587

6、总结
  1. 根据当前请求,找到HandlerExecutionChain(可以处理请求的handler以及handler的所有 拦截器)

  2. 先来顺序执行 所有拦截器的 preHandle()方法。

    1. 如果当前拦截器preHandle()返回为true。则执行下一个拦截器的preHandle()
    2. 如果当前拦截器返回为false。直接倒序执行所有已经执行了的拦截器的 afterCompletion();。
  3. 如果任何一个拦截器返回false,直接跳出不执行目标方法。

  4. 所有拦截器都返回true,才执行目标方法。

  5. 倒序执行所有拦截器的postHandle()方法。

  6. 前面的步骤有任何异常都会直接倒序触发 afterCompletion()。

  7. 页面成功渲染完成以后,也会倒序触发 afterCompletion()。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Auu6fNnH-1642398938661)(https://gitee.com/pjgitee/note/raw/master/springboot%E7%AC%94%E8%AE%B0.assets/Snipaste_2022-01-14_21-54-19.png)])

8、文件上传
1、使用步骤
1、编写前端页面上传请求:
  • 注意点:
    • multiple多文件上传
    • method=“post” enctype=“multipart/form-data” 文件上传必须的
<form role="form" th:action="@{/upload}" method="post" enctype="multipart/form-data">
    <!--method="post" enctype="multipart/form-data" 文件上传必须的--> 
    <div class="form-group">       
        <label for="exampleInputEmail1">Email</label> 
        <input type="email" name="email" class="form-control" id="exampleInputEmail1" placeholder="Enter email">   
    </div> 
    <div class="form-group">    
        <label for="exampleInputPassword1">姓名</label>       
        <input type="text" name="name" class="form-control" id="exampleInputPassword1" placeholder="Password"> 
    </div>   
    <div class="form-group">   
        <label for="exampleInputFile">头像</label>  
        <input type="file" name="headImage" id="exampleInputFile">   
    </div>   
    <div class="form-group">  
        <label for="exampleInputFile">生活照</label>  
        <input type="file" name="photos" multiple> <!--multiple多文件上传-->  
    </div>    
    <div class="checkbox">   
        <label>           
            <input type="checkbox"> Check me out   
        </label>  
    </div>   
    <input type="submit" class="btn btn-primary" value="提交">
</form>
2、编写处理请求的方法
  • @RequestPart(“‘名称’”) 处理文件上传请求
  @PostMapping("/upload")   
public String upload(@RequestParam("email") String email, 
                     @RequestParam("name") String name, 
                     @RequestPart("headImage")MultipartFile headImage,   
                     @RequestPart("photos") MultipartFile[] photos) throws IOException { 
    //上传的信息:email=1464847406@qq.com,name=201801421038,headImage文件大小=277362,photos文件个数=4 
    log.info("上传的信息:email={},name={},headImage文件大小={},photos文件个数={}",email,name,headImage.getSize(),photos.length);//       
    String s = ResourceUtils.getURL("classpath:").getPath() + "static/upload/";  
    if (!headImage.isEmpty()){
        //判断文件不为空
        //  InputStream inputStream = multipartFile.getInputStream();  获取流数据 可以直接操作流数据
        String originalFilename = headImage.getOriginalFilename();//获取源文件的名称   
        //保存到文件服务器,OSS服务器        
        headImage.transferTo(new File("F:\\test\\"+originalFilename));//直接指定上传到那个位置
        //  headImage.transferTo(new File(s +originalFilename));//直接指定上传到那个位置   
    }        
    if (photos.length > 0){ 
        //多文件上传的个数 大于0个      
        for (MultipartFile file : photos) {         
            if (!file.isEmpty()){ //当前文件不为空数据           
                String originalFilename = file.getOriginalFilename();//获取源文件的名称
                file.transferTo(new File("F:\\test\\"+originalFilename));//直接指定上传到那个位置
                //file.transferTo(new File(s +originalFilename));//直接指定上传到那个位置
            }        
        }    
    }        
    return "main";  
}
3、注意点
  1. 文件上传总配置类为 MultipartAutoConfiguration

    @Configuration(proxyBeanMethods = false)
    //配置类
    @ConditionalOnClass({ Servlet.class, StandardServletMultipartResolver.class, MultipartConfigElement.class })
    @ConditionalOnProperty(prefix = "spring.servlet.multipart", name = "enabled", matchIfMissing = true) //默认导入到了容器中
    @ConditionalOnWebApplication(type = Type.SERVLET)@EnableConfigurationProperties(MultipartProperties.class)//绑定 MultipartProperties 组件
    public class MultipartAutoConfiguration {}
    
  2. 文件的绑定的配置文件MultipartProperties组件,所有相关配置都在改类下

    @ConfigurationProperties(prefix = "spring.servlet.multipart", ignoreUnknownFields = false)
    public class MultipartProperties {    
        /**	 * 最大文件大小  最大文件大小不超过 1MB	 */
        private DataSize maxFileSize = DataSize.ofMegabytes(1);	
        /**	 * 最大请求文件总大小 多文件传输的文件总大小不超过 10MB	 */	
        private DataSize maxRequestSize = DataSize.ofMegabytes(10);
    }
    
  3. 配置类默认设置了 最大文件大小最大请求文件总大小 超过了会抛出以下异常

    Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.web.multipart.MaxUploadSizeExceededException: Maximum upload size exceeded; nested exception is java.lang.IllegalStateException: org.apache.tomcat.util.http.fileupload.impl.SizeLimitExceededException: the request was rejected because its size (13983920) exceeds the configured maximum (10485760)] with root cause
    
2、文件上传原理分析
1、文件上传的自动配置类 和 绑定的 bean组件的配置项

image-20210903160658911

2、检查是否是文件上传请求,是就对原生的request进行包装(MultipartHttpServletRequest)之后就使用包装的request

image-20210903160500493

image-20210903160735664

3、执行目标方法,获取全部的参数解析器、返回值解析器确定方法的各个参数值

image-20210903160901719

4、获取所有的方法参数,挨个遍历,确定参数具体的参数解析器,然后使用参数解析器解析的解析方法当前参数的值,如果是文件上传请求使用的是RequestPartMethodArgumentResolver参数解析器解析

image-20210903160914535

image-20210903161622953

5、使用 RequestPartMethodArgumentResolver参数解析器的解析方法解析参数

image-20210903160419007

image-20210903160410672

上述 multipartRequest.getFiles()方法的源码

image-20210903160430038

6、总结
  1. 文件上传自动配置类-MultipartAutoConfiguration-MultipartProperties
  2. 自动配置好了 StandardServletMultipartResolver 【文件上传解析器】
  3. 原理步骤
    1、请求进来使用文件上传解析器判断(isMultipart)并封装(resolveMultipart,返回MultipartHttpServletRequest)文件上传请求
    2、参数解析器来解析请求中的文件内容封装成MultipartFile
    3、将request中文件信息封装为一个Map;MultiValueMap<String, MultipartFile> 对应的就是参数名,和文件的multipart对象
  4. Multipart类中使用了FileCopyUtils。实现文件流的拷贝
9、异常处理
9.1、SpringBoot默认错误处理机制
1、默认规则
  • 默认情况下,Spring Boot提供/error处理所有错误的映射

  • 机器客户端,它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息。对于浏览器客户端,响应一个“ whitelabel”错误视图,以HTML格式呈现相同的数据

    image.png

    image.png

  • 要对其进行自定义,添加View解析为error

  • 要完全替换默认行为,可以实现 ErrorController并注册该类型的Bean定义,或添加ErrorAttributes类型的组件以使用现有机制但替换其内容。

  • /templates/error/下的4xx,5xx页面会被自动解析

image-20210903163247417

@GetMapping("editable_table")public String goToeDitable_table(){    int i = 10/0;    return "dataTables/editable_table";}

image-20210903163617325

image-20210903163647152

9.2、异常处理自动配置原理
1、自动配置类 ErrorMvcAutoConfiguration
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
// Load before the main WebMvcAutoConfiguration so that the error View is available
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties({ ServerProperties.class, WebMvcProperties.class })
public class ErrorMvcAutoConfiguration {}
2、自动配置异常处理规则
  1. 容器中的组件1:类型:DefaultErrorAttributes -> id:errorAttributes 自定义错误页面属性

    • public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver

    • DefaultErrorAttributes:定义错误页面中可以包含数据(异常明细,堆栈信息等)。

    • request域中存储的数据

      image-20210903173245458

      image-20210903173157538

  2. 容器中的组件2:类型:BasicErrorController --> id:basicErrorController(作用:json(postman)+白页(浏览器) 适配响应) 自定义页面跳转逻辑

    • 要么响应一个modelandvie(页面),要么响应一个responseentity(json)

    • BasicErrorController的定义

      @Controller@RequestMapping("${server.error.path:${error.path:/error}}")//默认处理错误的请求路径
      public class BasicErrorController extends AbstractErrorController {}
      
      # 默认处理错误的请求路径 可以自己在配置文件中设置
      server.error.path=/error
      
    • 处理默认 /error 路径的请求,页面响应 new ModelAndView("error", model);页面数据:new ResponseEntity<>(body, status);

    • 容器中有组件 View->id是error;(响应默认错误页)

    • 容器中放组件 BeanNameViewResolver(视图解析器);按照返回的视图名作为组件的id去容器中找View对象。

    • 如果想要返回页面;就会找error视图【StaticView】。(默认是一个白页)

    • 图解

      发送error请求,使用 BeanNameViewResolver视图解析器从容器中找到一个名为error视图的对象,将其渲染到客户端

      image-20210903171027519

      默认的错误页的渲染逻辑(StaticView视图对象):

      image-20210903171036728

  3. 容器中的组件3:类型:DefaultErrorViewResolver -> id:conventionErrorViewResolver 自定义错误页面html路径

    • 如果发生异常错误,会以HTTP的状态码 作为视图页地址(viewName),找到真正的页面(主要作用)

    • error/4xx、5xx.html

    • 解释了 /templates/error/下的4xx,5xx页面会被自动解析,发生错误会自动跳转到这两个页面

      image-20210903172217314

  4. 总结

    • 如果不满意上面的任何一个默认配置的默认功能,就可以自定义配置默认的组件。
    • 可以 自定义错误页面属性(配置 DefaultErrorAttributes类似的组件)、自定义页面跳转逻辑(配置 BasicErrorController类似的组件)、自定义错误页面html路径(配置 DefaultErrorViewResolver类似的组件)
9.3、异常处理步骤流程
1、mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); 执行目标方法,抛出异常,被catch掉,封装到 Exception dispatchException中

image-20210903182519830

2、抛出异常后,processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); 执行视图渲染解析流程,mv = processHandlerException(request, response, handler, exception); 处理handler发送的异常

image-20210903182530238

3、遍历每个异常解析器,调用每个异常解析器的解析方法(resolveException(),每个异常解析器的实现不同),发现默认的异常解析器无法解析当前异常,就将异常抛出,抛给 mv = processHandlerException(request, response, handler, exception);,会用catch捕捉

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TuLNgasr-1642398938671)(https://gitee.com/pjgitee/note/raw/master/springboot%E7%AC%94%E8%AE%B0.assets/image-20210903183008469.png)]

4、完成捕捉后,异常未得到处理,系统就会发送error请求,进行异常的处理

image-20210903183649350

5、执行 error请求的 mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); 跳转到下图,会使用自动配置类(ErrorMvcAutoConfiguration)给容器注入的组件(BasicErrorController)进行处理,循环遍历系统中默认的错误视图解析器,只有一个们也是自动配置类(ErrorMvcAutoConfiguration)给容器注入的组件(DefaultErrorViewResolver)错误视图解析器

image-20210903192053269

6、DefaultErrorViewResolver错误视图解析器,获取状态码,添加前后缀,使用模板引擎响应页面

image-20210903192059414

7、总结:

1、执行目标方法,目标方法运行期间有任何异常都会被catch、而且标志当前请求结束;并且用 dispatchException 存储异常信息

2、进入视图解析流程(页面渲染)

​ processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);有异常时,mv为空,异常保存在了dispatchException中

3、mv = processHandlerException();处理handler发生的异常,处理完成返回ModelAndView;

  • 1、遍历所有的 handlerExceptionResolvers,看谁能处理当前异常【HandlerExceptionResolver处理器异常解析器】

  • 2、系统默认的异常解析器;

    image.png

      • 1、DefaultErrorAttributes先来处理异常。把异常信息保存到rrequest域,并且返回null;
      • 2、默认没有任何异常解析器能处理异常,所以异常会被抛出
        • 1、如果没有任何人能处理最终底层就会发送 /error 请求。会被底层的BasicErrorController处理

        • 2、解析错误视图;遍历所有的 ErrorViewResolver 看谁能解析。

          image.png

        • 3、默认的 DefaultErrorViewResolver ,作用是把响应状态码作为错误页的地址,error/500.html

        • 4、模板引擎最终响应这个页面 error/500.html

        • 5、查找顺序:‘/templates/error/500.’ 、‘/static/error/500.html’、‘/templates/error/5xx.’、‘/static/error/5xx.html’

9.4、定制错误处理逻辑
1、自定义错误页
  • error/404.html error/5xx.html;有精确的错误状态码页面就匹配精确,没有就找 4xx.html;如果都没有就触发白页

    /*    不传a参数会抛出400错误,如果  templates下的 error下的错误页名称为404  就找不到改页  报错还是到原始页面                          如果  templates下的 error下的错误页名称为4xx  就会找到改页  报错就到此页面                          证明先会精确匹配在进行模糊匹配 
    */
    @GetMapping("editable_table")
    public String goToeDitable_table(@RequestParam("a") int a){ 
        int i = 10/0;    
        return "dataTables/editable_table";
    }
    
2、ControllerAdvice+@ExceptionHandler处理全局异常
  • 底层是 ExceptionHandlerExceptionResolver 支持的 解析我们标注了 @ExceptionHandler 的方法,通过该方法的返回值确定返回的视图对象

    ThymeleafController类

    /*    Web全局异常处理 controller的 */
    @Slf4j@ControllerAdvice //放在容器中  注解源码中包含了
    @Component
    public class WebException {  
        /**     * 全局异常处理器     * @param e  会将抛出的异常信息封装到这个参数中       * @return  返回的是视图的名称     */   
        @ExceptionHandler({ArithmeticException.class,NullPointerException.class}) //异常处理器 标记为处理异常信息的方法,抛出这个两个异常就会 执行下面的流程  (通常是某些已经定义好的异常)  
        public /*ModelAndView  可以自定义modelAndView返回*/ String  goToException(Exception e){   
            log.info("异常信息={}" ,e);      
            //抛出上面 ArithmeticException\NullPointerException异常就会跳转
            return "login";    
        }
    }
    
  • 效果图

    image-20210904101445721

  • 原理图,找到 exceptionHandlerExceptionResolver异常解析器解析,会找到我们自己标记的exceptionHandler注解的方法,执行之后返回 视图的名称 或者modelAndView对象

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SglLCbmj-1642398938674)(https://gitee.com/pjgitee/note/raw/master/springboot%E7%AC%94%E8%AE%B0.assets/image-20210904105053008.png)]

3、@ResponseStatus+自定义异常
  • 底层是 ResponseStatusExceptionResolver 底层会默认发送 /error请求 ,即当前异常解析器无法处理视图信息等,只能封装错误信息 把responsestatus注解的信息通过底层调用 response.sendError(statusCode, resolvedReason)发送;再由tomcat发送的/error,如没有能够处理的 /error请求的controller,就会响应tomcat的自己的错误页,当springboot有处理 /error请求的controller,就是自动导入的 BasicErrorController

    tomcat 自定义的错误页

    image.png

    /*    自定义异常2 */
    @ResponseStatus(value = HttpStatus.FORBIDDEN,reason = "用户数量过多") //抛出异常  响应403状态码  响应当前自定义异常  异常信息为 用户数量过多
    public class UserTooManyException extends RuntimeException {  
        public UserTooManyException() {    } 
        public UserTooManyException(String message) {        super(message);    }
    }
    

    ThymeleafController类

    @GetMapping("dynamic_table")
    public String goToDynamic_table(Map<String ,Object> map){
        List<User> users = Arrays.asList(new User("张三", "123"),  
                                         new User("李四", "12343"),
                                         new User("王五", "12321"),   
                                         new User("笑死", "1213223")); 
        if(users.size() > 3){    
            throw new UserTooManyException();//抛出自定义异常 
        }    map.put("users", users);   
        return "dataTables/dynamic_table";
    }
    
  • 效果图

    image-20210904103714312

  • 原理图 找到 ExceptionHandlerExceptionResolver 异常解析器,调用解析异常的方法,判断是否标记了 responseStatus注解,解析注解的信息,如:状态码,错误信息等

image-20210904110231363

resolveResponseStatus()方法源码 ,获取状态码,错误信息,应用状态码和错误信息,在直接由 tomcat发送 /error请求,返回一个空的 modelAndView对象

response.sendError(statusCode, resolvedReason)

image-20210904110204742

image-20210904110325882

4、Spring底层的异常,如 参数类型转换异常;
  • DefaultHandlerExceptionResolver 处理框架底层的异常。底层会默认发送 /error请求 ,即当前异常解析器无法处理视图信息等,只能封装错误信息 把错误信息通过底层调用 response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage()); 发出去;再由tomcat发送的/error,,如没有能够处理的 /error请求的controller,就会响应tomcat的自己的错误页,当springboot有处理 /error请求的controller,就是自动导入的 BasicErrorController

    //实验代码:
    /*    不传a参数会抛出400错误,如果  templates下的 error下的错误页名称为404  就找不到改页  报错还是到原始页面                          如果  templates下的 error下的错误页名称为4xx  就会找到改页  报错就到此页面                          证明先会精确匹配在进行模糊匹配 */
    @GetMapping("editable_table")
    public String goToeDitable_table(@RequestParam("a") int a){ 
        int i = 10/0; 
        return "dataTables/editable_table";
    }
    
  • 效果图

    image-20210904113635873

  • 原理图 找到 DefaultHandlerExceptionResolver 异常解析器,调用解析异常的方法

image-20210904113031017

该异常解析器可以处理许多springmvc底层抛出的异常,如果是这些底层抛出的异常,就会 发送 /error请求,同时把错误信息和错误状态发送过去,在返回一个空的modelAndView对象, 在由 tomcat发送 /error请求就会进行处理

response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ex5iokcl-1642398938677)(https://gitee.com/pjgitee/note/raw/master/springboot%E7%AC%94%E8%AE%B0.assets/image-20210904113123105.png)]

5、自定义异常解析器
  • 异常解析器的总接口 可以作为默认的全局异常处理规则

    image-20210904114831836

  • 编写自定义异常解析器代码,实现 HandlerExceptionResolver异常解析器总接口,实现 解析异常的方法

    /*    自定义异常解析器 */
    @Component //标记到容器中
    public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
    	@Override   
        public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {  
            try {           
                response.sendError(511, "我自定义的与异常解析器");  
            } catch (IOException e) { 
                e.printStackTrace();        
            }     
            //如果modelAndView中有视图地址和模型数据会自动跳到我们自己的视图地址去      
            return new ModelAndView();  
        }
    }
    
  • 当前抛出的是 “不传a参数会抛出参数异常错误”,会由 DefaultHandlerExceptionResolver 异常解析解析返回空的modelAndView对象 ,发送/error请求,不会使用我们自己的定义的异常解析器

    image-20210904115326390

  • 所有得给我们自定义异常的解析器加上优先级 这样一配优先级,之后所有的异常都会由这个自定义的异常解析解析,因为没有准确的解析那种异常,啥异常都直接发送 /error请求,响应 "511"和 “我自定义的与异常解析器”,返回空modelAndView对象

    /*    自定义异常解析器 */
    @Order(value = Integer.MIN_VALUE) //默认值为最大值  越小优先级越高
    @Component //标记到容器中
    public class MyHandlerExceptionResolver implements HandlerExceptionResolver {  
        @Override   
        public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {   
            try {       
                response.sendError(511, "我自定义的与异常解析器");  
            } catch (IOException e) {       
                e.printStackTrace();  
            }       
            //如果modelAndView中有视图地址和模型数据会自动跳到我们自己的视图地址去 
            return new ModelAndView();   
        }
    }
    
  • 结果 自定义的异常解析器优先被遍历了 就会调用我们自己异常解析器的解析方法 resolveException() ,就会 有tomcat发送/error请求 ,同时携带状态码和错误信息, 再返回一个 空的视图对象 modelAndView ,就不会循环其他的异常解析器了,直接返回空的 modelAndView对象,在响应 /error请求.

    image-20210904115914505

  • 效果图

    image-20210904121213525

6、ErrorViewResolver 实现自定义处理异常;
  • 系统默认的一些异常解析器就是(DefaultHandlerExceptionResolver 、ExceptionHandlerExceptionResolver) response.sendError ()。error请求就会转给controller
  • 你的异常没有任何异常解析器能够处理。tomcat底层 response.sendError()。error请求就会转给controller
  • basicErrorController 要去的页面地址是 ErrorViewResolver(系统默认的是 DefaultErrorViewResolver定义的规则)
10、web 原生的组件(servlet、filter、listener)
10.1、使用servlet API

官网地址:https://docs.spring.io/spring-boot/docs/2.4.2/reference/htmlsingle/#howto-add-a-servlet-filter-or-listener

1、提供了原生对原生注解的支持

@ServletComponentScan(basePackages = “com.atguigu.admin”) :指定原生Servlet组件都放在那里

@WebServlet(urlPatterns = “/my”):效果:直接响应,没有经过Spring的拦截器?

@WebFilter(urlPatterns={“/css/*”,“/images/*”})

@WebListener

image-20210904150904006

2、servlet的使用
/*    原生的servlet */
@WebServlet(urlPatterns = "/myServlet") //必须加上
public class MyServlet extends HttpServlet { 
    @Override    
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {        
        resp.getWriter().write("我们原生的servlet~~");   
    }
}
@ServletComponentScan(basePackages = "com.pj.springboot") //扫描改包下的所有子包 并将原生组件 servlet 扫入进去
@SpringBootApplicationpublic class Springboot2Application { 
    public static void main(String[] args) {     
        SpringApplication.run(Springboot2Application.class, args);
    }
}
3、Filter 原生组件的使用
/*    定于原生的 filter */
@Slf4j@WebFilter(urlPatterns = {"/css/*","/images/*","/myServlet"}) //拦截 css下的所有、image下的所有、myServlet请求
/*servlet 写法 /** spring家的写法  单个*是servlet的写法,双 ** 是spring家族写法*/
public class MyFilter implements Filter {  
    @Override   
    public void init(FilterConfig filterConfig) throws ServletException { 
        log.info("init 初始化完成");    
    }  
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {       
        log.info("doFilter 开始工作");  
        chain.doFilter(request, response); //放行   
    }    
    @Override   
    public void destroy() {  
        log.info("destroy 销毁完成");
    }
}

image-20210904151628121

4、Listener原生组件的使用
/*自定义监听器 */
@Slf4j@WebListenerpublic class MyListener implements ServletContextListener {  
    @Override    
    public void contextInitialized(ServletContextEvent sce) {       
        log.info("MyListener监听到项目已经初始化成功~~"); 
    }    
    @Override    
    public void contextDestroyed(ServletContextEvent sce) {
        log.info("MyListener监听到项目已经销毁成功~~");    
    }
}

image-20210904152202204

10.2、使用RegistrationBean
/*将上述的原生注册servlet组件的类的注解去掉 用配置类配置进去 通过配置类 配置原生的servlet 组件 */
@Configuration(proxyBeanMethods = true) //保证容器中组件是单实例的 不会造成容器中有冗余 的servlet对象
public class MyRegisterConfig {   
    @Bean   
    public ServletRegistrationBean servletRegistrationBean(){  
        MyServlet myServlet = new MyServlet();        
        return new ServletRegistrationBean(myServlet,"/my","/my02");//这个servlet映射多个路径 
    }   
    @Bean    public FilterRegistrationBean filterRegistrationBean(){    
        MyFilter myFilter = new MyFilter();        //第一种方式
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
        filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/css/*")); //自定义拦截 /my 和 /css下的所有        //第二种方式//        return new FilterRegistrationBean(myFilter,servletRegistrationBean());//默认拦截 servletRegistrationBean注册的servlet的路径  my 和 my02     
        return filterRegistrationBean;   
    } 
    @Bean   
    public ServletListenerRegistrationBean listenerRegistrationBean(){   
        MyListener myListener = new MyListener();  
        return new ServletListenerRegistrationBean(myListener); 
    }
}
10.3、 解释原生的servlet为什么没经过Spring的拦截器
  • 容器中现在有两个servlet 分别是 MyServlet 映射路径 为 /my 和 DispatchServlet映射路径为 /

  • 分析DispatchServlet如何配置进容器中的

    @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)@Configuration(proxyBeanMethods = false)
    @ConditionalOnWebApplication(type = Type.SERVLET) //判断当前项目是否是servlet类型的
    @ConditionalOnClass(DispatcherServlet.class) //没有DispatchServlet.class存在
    @AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class)
    public class DispatcherServletAutoConfiguration {   
        public static final String DEFAULT_DISPATCHER_SERVLET_BEAN_NAME = "dispatcherServlet";   
        public static final String DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME = "dispatcherServletRegistration";  
        @Configuration(proxyBeanMethods = false)  
        @Conditional(DefaultDispatcherServletCondition.class) 
        @ConditionalOnClass(ServletRegistration.class)   
        @EnableConfigurationProperties(WebMvcProperties.class)  
        protected static class DispatcherServletConfiguration {  
            @Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME) //给容器中注入了DispatchServlet的bean组件
            public DispatcherServlet dispatcherServlet(WebMvcProperties  webMvcProperties) { 
                //绑定 webMvcProperties bean的配置,配置项都在 WebMvcPropertiesWebMvcProperties中  配置前缀为  spring.mvc      
                DispatcherServlet dispatcherServlet = new DispatcherServlet();  
                dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());
                dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest());         dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound());
                dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents());
                dispatcherServlet.setEnableLoggingRequestDetails(webMvcProperties.isLogRequestDetails()); 
                return dispatcherServlet;    
            }  
        }        
        @Configuration(proxyBeanMethods = false)	
        @Conditional(DispatcherServletRegistrationCondition.class)	
        @ConditionalOnClass(ServletRegistration.class)	@EnableConfigurationProperties(WebMvcProperties.class)	
        @Import(DispatcherServletConfiguration.class)	
        protected static class DispatcherServletRegistrationConfiguration {
            @Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
            @ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
            public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet,				WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) {
                //通过 DispatcherServletRegistrationBean  也就是 ServletRegistrationBean注册到容器中 
                //参数的值 会从容器中注入
                DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet,webMvcProperties.getServlet().getPath());
                // private String path = "/"; path默认值	
                registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);	
                registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
                multipartConfig.ifAvailable(registration::setMultipartConfig);	
                return registration;
            }	
        }
    }
    
  • 修改DispatchServlet默认映射路径,一般不要改 ,默认映射 / 路径

    #修改DispatchServlet默认的映射路径
    spring.mvc.servlet.path=/mvc
    
  • Tomcat-Servlet;

    多个Servlet都能处理到同一层路径,精确优选原则 如果发送/my/1 就会调用 B

    A: /my/

    B: /my/1

  • 解释:根据精确优先原则,DispatcherServlet处理"/“请求,MyServlet处理”/my"请求,发送/my,MyServlet 更精确,所以由原生的servlet(Tomcat处理),而只有由DispatcherServlet(Spring)处理的请求才会经过spring的拦截器

    image-20210904155911683

11、嵌入式Servlet容器
11.1、切换嵌入式Servlet容器

官方文档:https://docs.spring.io/spring-boot/docs/2.4.2/reference/htmlsingle/#howto-use-another-web-server

1、默认支持的webServer
    • Tomcat, Jetty, or Undertow
    • ServletWebServerApplicationContext 容器启动寻找ServletWebServerFactory 并引导创建服务器
2、切换服务器
  • 底层默认支持的服务器如下 Spring Boot默认使用Tomcat服务器,若需更改其他服务器,则修改工程pom.xml:

    image-20210904171233402

  • 导入服务器的依赖

    <dependency>    
        <groupId>org.springframework.boot</groupId>    
        <artifactId>spring-boot-starter-web</artifactId>   
        <exclusions>     
            <exclusion>            
                <!--spring-boot-starter-web场景下自动导入了tomcat服务器,所以需要排出 tomcat服务器-->           
                <groupId>org.springframework.boot</groupId>         
                <artifactId>spring-boot-starter-tomcat</artifactId>      
            </exclusion>   
        </exclusions>
    </dependency>
    <!--使用自己想用的服务器  就导入该场景 jetty服务器-->
    <dependency>    
        <groupId>org.springframework.boot</groupId>   
        <artifactId>spring-boot-starter-jetty</artifactId>
    </dependency>
    

    image-20210904172400086

3、原理
    • SpringBoot应用启动发现当前是Web应用。web场景包-导入tomcat
    • web应用会创建一个web版的 IOC 容器 ServletWebServerApplicationContext
    • ServletWebServerApplicationContext 启动的时候寻找ServletWebServerFactory(Servlet 的web服务器工厂对应一个 Servlet 的web服务器) ServletWebServerFactory 在启动时,会根据导入的依赖加载相对应的 ServletWebServerFactory,相对应的 ServletWebServerFactory 就寻找 对应的 Servlet 的web服务器

      • SpringBoot底层默认有很多的WebServer工厂;TomcatServletWebServerFactory, JettyServletWebServerFactory, or UndertowServletWebServerFactory , 都会在 ServletWebServerFactoryConfiguration中根据条件进行注入容器中 ,条件为,有那个webServlet工厂的依赖jar包,就会导入那个。如:导入 tomcat 依赖 就会注入 tomcatServletWebServerFactory 工厂,导入 Jetty的依赖,就注入 JettyServletWebServerFactory 工厂

        [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ShGErXJ8-1642398938681)(https://gitee.com/pjgitee/note/raw/master/springboot%E7%AC%94%E8%AE%B0.assets/image-20210904165802569.png)]

      • 底层直接会有一个自动配置类。这个配置类会有被优先加载到容器中,ServletWebServerFactoryAutoConfiguration` WebServer工厂配置类

        @Configuration(proxyBeanMethods = false)@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)@ConditionalOnClass(ServletRequest.class)@ConditionalOnWebApplication(type = Type.SERVLET)@EnableConfigurationProperties(ServerProperties.class) //绑定了配置组件 ServerProperties 可以配置的属性都在该类中
        @Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,     ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,      ServletWebServerFactoryConfiguration.EmbeddedJetty.class,      ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })//导入了
        ServletWebServerFactoryConfigurationpublic class ServletWebServerFactoryAutoConfiguration {}	
        
      • ServletWebServerFactoryAutoConfiguration导入了ServletWebServerFactoryConfiguration(配置类)

      • ServletWebServerFactoryConfiguration 配置类 根据动态判断系统中到底导入了那个Web服务器的包。(默认是web-starter导入tomcat包),容器中就有 TomcatServletWebServerFactory

      • TomcatServletWebServerFactory 创建出Tomcat服务器并启动;TomcatWebServer 的构造器拥有初始化方法initialize()该方法中调用了this.tomcat.start();`

      • 内嵌服务器,就是手动把启动服务器的代码调用(tomcat核心jar包存在)在 factory.getWebServer(getSelfInitializer()); 源码中体现

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8gharOHe-1642398938682)(https://gitee.com/pjgitee/note/raw/master/springboot%E7%AC%94%E8%AE%B0.assets/image-20210904170004049.png)]

  • 上述 this.webServer = factory.getWebServer(getSelfInitializer()); 的源码

image-20210904170056544

4、定制Servlet容器
  • 修改配置文件 server.xxx( ConfigurableServletWebServerFactory 绑定了 ServerProperties组件) 推荐使用

    #修改 服务器的配置#修改端口号
    server.port=8081 
    #修改其他web服务的一些配置
    server.undertow.accesslog.dir=/tem  
    #设置session过期时间
    server.servlet.session.timeout=30s
    
  • 直接自定义 ConfigurableServletWebServerFactory 配置的 Servlet Web 服务器工厂

    image-20210904174657962

  • 实现 WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>

    • ServletWebServerFactoryAutoConfiguration配置类中给容器中放入一个 ServletWebServerFactoryCustomizer(Servlet Web 服务器工厂定制器)的组件

    • 是通过 ServletWebServerFactoryCustomizer(Servlet Web 服务器工厂定制器) 把配置文件的值和 ServletWebServerFactory(web服务器工厂) 进行绑定

      @Overridepublic void customize(ConfigurableServletWebServerFactory factory) {   PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();   map.from(this.serverProperties::getPort).to(factory::setPort);   map.from(this.serverProperties::getAddress).to(factory::setAddress);   map.from(this.serverProperties.getServlet()::getContextPath).to(factory::setContextPath);   map.from(this.serverProperties.getServlet()::getApplicationDisplayName).to(factory::setDisplayName);   map.from(this.serverProperties.getServlet()::isRegisterDefaultServlet).to(factory::setRegisterDefaultServlet);   map.from(this.serverProperties.getServlet()::getSession).to(factory::setSession);   map.from(this.serverProperties::getSsl).to(factory::setSsl);   map.from(this.serverProperties.getServlet()::getJsp).to(factory::setJsp);   map.from(this.serverProperties::getCompression).to(factory::setCompression);   map.from(this.serverProperties::getHttp2).to(factory::setHttp2);   map.from(this.serverProperties::getServerHeader).to(factory::setServerHeader);   map.from(this.serverProperties.getServlet()::getContextParameters).to(factory::setInitParameters);   map.from(this.serverProperties.getShutdown()).to(factory::setShutdown);   for (WebListenerRegistrar registrar : this.webListenerRegistrars) {      registrar.register(factory);   }}
      
    • 自定义 ServletWebServerFactoryCustomizer 配置web服务器工厂默认设置

      image-20210904175650121

  • xxxxxCustomizer:定制化器,可以改变xxxx的默认规则(默认配置)

12、定制化原理-SpringBoot定制化组件的几种方式(小结)
12.1、定制化的常见方式
1、编写自定义的配置类 xxxConfiguration + @Bean替换、增加容器中默认组件,视图解析器 推荐
  • 如:

    @Configurationpublic class WebConfig   implements WebMvcConfigurer {	
        //添加一个格式转换器
        @Override    public void addFormatters(FormatterRegistry registry) {}    
        //添加一个消息转换器   
        @Override    
        public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {}    
        @Bean 
        //替换    
        public CharacterEncodingFilter characterEncodingFilter() {        
            CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();      
            filter.setEncoding("ISO-8859-1");        
            return filter;   
        }}
    
2、修改配置文件
3、xxxxxCustomizer:定制化器,可以改变xxxx的默认规则(默认配置)
4、Web应用 编写一个配置类实现 WebMvcConfigurer 即可定制化web功能 + @Bean给容器中再扩展一些组件 推荐
@Configuration
public class AdminWebConfig implements WebMvcConfigurer{}

或者
 @Configuration
 public class AdminWebConfig implements WebMvcConfigurer{    
     //过于底层 不推荐    
     @Bean    
     public WebMvcRegistrations webMvcRegistrations(){        
         /*        用于注册  WebMvcConfigurationSupport 的关键组件的接口  Spring MVC 提供的默认组件。  所有自定义实例稍后由 Boot 和 Spring MVC 配置处理。  该组件的单个实例应该被注册,否则将无法 从冗余的 MVC 组件中进行选择         */        
         return new WebMvcRegistrations() {}; 
     }}
5、@EnableWebMvc + WebMvcConfigurer@Bean 可以全面接管SpringMVC,所有规则全部自己重新配置; 实现定制和扩展功能(高级功能,初学者退避三舍)。
  • 使用
/** *  @EnableWebMvc 全面接管mvc的配置 ,默认的都不生效了  都需要自己配置 	静态资源、视图解析器、欢迎页...全部失效 */
@EnableWebMvc
@Configuration
public class MyWebMvcConfig implements WebMvcConfigurer {}

image-20210904182902737

  • 添加静态映射规则
@EnableWebMvc
@Configuration
public class MyWebMvcConfig implements WebMvcConfigurer {    
    //添加静态资源求映射    
    @Override    
public void addResourceHandlers(ResourceHandlerRegistry registry) {       
     	registry.addResourceHandler("/aa/**") //以aa这个访问路径开头的请求都会    
        		.addResourceLocations("classpath:/static/"); //映射到 classpath下的 static目录下  
   }
}

image-20210904183749216

  • 原理:

    1. WebMvcAutoConfiguration默认的SpringMVC的自动配置功能类,如静态资源、欢迎页等功能。

    2. @EnableWebMvc注解

      @Retention(RetentionPolicy.RUNTIME)
      @Target(ElementType.TYPE)
      @Documented@Import(DelegatingWebMvcConfiguration.class) //导入了该类
      public @interface EnableWebMvc {}
      
    3. DelegatingWebMvcConfiguration的作用,只保证SpringMVC最基本的使用

      • 把所有系统中的实现了WebMvcConfigurer接口的类拿过来,所有功能的定制都是这些WebMvcConfigurer合起来一起生效。

      • DelegatingWebMvcConfiguration 自动配置了一些非常底层的组件,如RequestMappingHandlerMapping,这些组件依赖的组件都是从容器中获取如。

      • 完整的声明 导入了DelegatingWebMvcConfiguration 就会导入 WebMvcConfigurationSupport

        @Configuration(proxyBeanMethods = false)
        public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {}
        
    4. WebMvcAutoConfiguration里面的配置要能生效必须

      @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)。没有这个 WebMvcConfigurationSupport 组件才能生效 DelegatingWebMvcConfiguration继承了 WebMvcConfigurationSupport 所以它不生效 就不会使用改配置类

      @Configuration(proxyBeanMethods = false)
      @ConditionalOnWebApplication(type = Type.SERVLET)
      @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
      @ConditionalOnMissingBean(WebMvcConfigurationSupport.class) //没有这个 WebMvcConfigurationSupport 组件才能生效
      @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
      @AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,      ValidationAutoConfiguration.class })
      public class WebMvcAutoConfiguration {}
      
    5. @EnableWebMvc 导致了WebMvcAutoConfiguration 没有生效。

12.2、原理分析套路

​ 导入场景starter - 就会引入一些xxxxAutoConfiguration 配置类 - 配置类中导入xxx组件(@Bean注解) - 配置类也会绑定xxxProperties(@EnableConfigurationProperties(xxxx.class)) --绑定配置文件项。

6、数据访问

在项目springboot-1中

1、SQL
1、数据源的自动配置
1.1 、导入JDBC使用场景
  • 导入JDBC依赖

    <!--jdbc 使用场景-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jdbc</artifactId>
    </dependency>
    
  • 导入依赖后,改依赖中导入了一下依赖,唯独没有 数据库驱动的依赖

    image-20210905114338626

  • 为什么导入JDBC场景,官方不导入驱动?因为官方不知道我们接下要操作什么数据库。

  • 导入数据库驱动 官方做了版本仲裁

    <!--数据库驱动  做了版本仲裁-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    

    image-20210905114958055

  • 自己的数据库版本应该和驱动版本对应

    默认版本:<mysql.version>8.0.26</mysql.version>
    想要修改版本
    1、直接依赖引入具体版本(maven的就近依赖原则)
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
    	        <version>5.1.47</version>
            </dependency>
    
    2、重新声明版本(maven的属性的就近优先原则)
        <properties>
            <mysql.version>5.1.47</mysql.version>
        </properties>
    
  • maven的三大原则:依赖传递原则,依赖就近原则(最短路径原则),声明优先原则

1.2、自动配置
  • 数据源的自动配置类 DataSourceAutoConfiguration

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
    @ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory") //容器中没有基于响应式编程的类
    @EnableConfigurationProperties(DataSourceProperties.class) //开启配置文件绑定功能  绑定的类为 DataSourceProperties  绑定配置文件前缀为 spring.datasource
    @Import({ DataSourcePoolMetadataProvidersConfiguration.class,      DataSourceInitializationConfiguration.InitializationSpecificCredentialsDataSourceInitializationConfiguration.class,      DataSourceInitializationConfiguration.SharedCredentialsDataSourceInitializationConfiguration.class })
    public class DataSourceAutoConfiguration {
        //.....
        @Configuration(proxyBeanMethods = false)
    	@Conditional(PooledDataSourceCondition.class)
    	@ConditionalOnMissingBean({ DataSource.class, XADataSource.class }) //容器中没有 DataSource,XADataSource 响应式  就帮你配置一个连接池
    	@Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
    			DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class,
    			DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class })//DataSourceConfiguration 会根据导入的依赖 进行注入不同的数据源 默认Hikari数据源 ,也可以通过配置文件的配置项 spring.datasource.type 指定数据源
    	protected static class PooledDataSourceConfiguration {
    
    	}
       // .....
    }
    
    
    //DataSourceConfiguration类下的一个默认数据源配置
    @Configuration(proxyBeanMethods = false)
    	@ConditionalOnClass(HikariDataSource.class)
    	@ConditionalOnMissingBean(DataSource.class) //容器中没有数据源时才跟你配置数据源
    	@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource",
    			matchIfMissing = true)
    	static class Hikari {
    
    		@Bean
    		@ConfigurationProperties(prefix = "spring.datasource.hikari")
    		HikariDataSource dataSource(DataSourceProperties properties) {
    			HikariDataSource dataSource = createDataSource(properties, HikariDataSource.class);
    			if (StringUtils.hasText(properties.getName())) {
    				dataSource.setPoolName(properties.getName());
    			}
    			return dataSource;
    		}
    
    	}
    
    • 开启配置文件绑定功能 绑定的类为 DataSourceProperties 绑定配置文件前缀为 spring.datasource

      • 需要配置数据源 url、username、password、driverclass

        spring:   
        	datasource:  
            	password: 123     
                username: root    
                driver-class-name: com.mysql.jdbc.Driver     
                url: jdbc:mysql://localhost:3306/test
        

        image-20210905122129818

      • 不导入数据源配置,项目会启动失败

        image-20210905121527612

    • 数据库连接池的配置,是自己容器中没有DataSource才自动配置的

      • DataSourceConfiguration类 会根据导入的依赖 进行注入不同的数据源 默认Hikari数据源,如果有多个数据源的依赖,也可以通过配置文件的配置项 spring.datasource.type 指定数据源 ,数据源可以如下:

        org.apache.tomcat.jdbc.pool.DataSourcecom.zaxxer.hikari.HikariDataSource  默认的org.apache.commons.dbcp2.BasicDataSourceoracle.ucp.jdbc.PoolDataSource
        
      • 底层配置好的连接池是:HikariDataSource

  • 数据库的事务自动配置类 DataSourceTransactionManagerAutoConfiguration

  • dbcTemplateAutoConfiguration: JdbcTemplate的自动配置,可以来对数据库进行crud

    • 可以修改这个配置项@ConfigurationProperties(prefix = “spring.jdbc”) 来修改JdbcTemplate

      spring:
      	jdbc:
          	template:    	query-timeout: 3    #设置查询超时时间
      
    • @Bean@Primary JdbcTemplate;容器中有这个组件

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass({ DataSource.class, JdbcTemplate.class })
    @ConditionalOnSingleCandidate(DataSource.class)
    @AutoConfigureAfter(DataSourceAutoConfiguration.class)
    @EnableConfigurationProperties(JdbcProperties.class)  //开启配置文件绑定功能  绑定的类为 JdbcProperties  绑定配置文件前缀为 spring.jdbc
    @Import({ DatabaseInitializationDependencyConfigurer.class, JdbcTemplateConfiguration.class,
          NamedParameterJdbcTemplateConfiguration.class }) //JdbcTemplateConfiguration 配置了jdbc的一些基本设置
    public class JdbcTemplateAutoConfiguration {
    }
    
    //JdbcTemplateConfiguration 类
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnMissingBean(JdbcOperations.class)
    class JdbcTemplateConfiguration {
    
    	@Bean
    	@Primary //容器中导入了 jdbcTemplate
    	JdbcTemplate jdbcTemplate(DataSource dataSource, JdbcProperties properties) {
    		JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
    		JdbcProperties.Template template = properties.getTemplate();
    		jdbcTemplate.setFetchSize(template.getFetchSize());
    		jdbcTemplate.setMaxRows(template.getMaxRows());
    		if (template.getQueryTimeout() != null) {
    			jdbcTemplate.setQueryTimeout((int) template.getQueryTimeout().getSeconds());
    		}
    		return jdbcTemplate;
    	}
    }
    
  • JndiDataSourceAutoConfiguration: jndi的自动配置

  • XADataSourceAutoConfiguration: 分布式事务相关的

  • 单元测试代码

    @Slf4j
    @SpringBootTestclass 
    SpringBoot_1TestsMysql {    
        @Autowired    
        JdbcTemplate jdbcTemplate;  
        @Test    void testJdbcTemplate() {
            //        jdbcTemplate.queryForObject("", User.class); 
            List<User> users = jdbcTemplate.queryForList("select * from user", User.class);    
            log.info("数据库的数据为={}",users);  
        }
    }
    
2、自定义方式整合druid数据源

官网地址:https://github.com/alibaba/druid

2.1、Druid是什么?

它是数据库连接池,它能够提供强大的监控和扩展功能。Druid连接池为监控而生,内置强大的监控功能,监控特性不影响性能。功能强大,能防SQL注入,内置Loging能诊断Hack应用行为。

官网介绍:https://github.com/alibaba/druid/wiki/Druid%E8%BF%9E%E6%8E%A5%E6%B1%A0%E4%BB%8B%E7%BB%8D

2.2、Spring Boot整合第三方技术的两种方式:
  • 自定义
  • 找starter场景
2.3、自定义整合druid数据源
  1. 添加druid数据源的依赖

    <!--导入德鲁伊数据源驱动-->
    <dependency>  
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>  
        <version>1.1.19</version>
    </dependency>
    
  2. 配置Druid数据源

    /*配置类 */
    @Configurationpublic class MyConfig {    
        /*        
            自定义数据源  因为默认的数据源配置条件是  @ConditionalOnMissingBean(DataSource.class) 容器中没有数据源时才配置数据源       
            自己添加了数据源进容器中就使用我们自定义的数据源   
        */    
        @ConfigurationProperties("spring.datasource") //绑定配置文件spring.datasource开头的配置   
        @Bean    
        public DataSource dataSource(){ 
            DruidDataSource druidDataSource = new DruidDataSource();        //手动设置数据源的配置     
            /*  
                druidDataSource.setUrl();       
                druidDataSource.setUsername();       
                druidDataSource.setPassword();        
                druidDataSource.setDriverClassName();
            */   
            return druidDataSource;    
        }
    }
    /* 可配置的数据项
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"   destroy-method="close">    
    	<property name="url" value="${jdbc.url}" />   
    	<property name="username" value="${jdbc.username}" />    
    	<property name="password" value="${jdbc.password}" />    <property name="maxActive" value="20" />    	<property name="initialSize" value="1" />    <property name="maxWait" value="60000" />   
        <property name="minIdle" value="1" />   
        <property name="timeBetweenEvictionRunsMillis" value="60000" /> 
        <property name="minEvictableIdleTimeMillis" value="300000" />  
        <property name="testWhileIdle" value="true" />   
        <property name="testOnBorrow" value="false" />    
        <property name="testOnReturn" value="false" />    
        <property name="poolPreparedStatements" value="true" />  
        <property name="maxOpenPreparedStatements" value="20" />
        </bean>  
        */
    
  3. 单元测试进行验证 是否自定义的数据源类型

    @AutowiredDataSource 
    dataSource;
    @Testvoid testDataSource() {  
        log.info("数据源的类型为:"+dataSource.getClass()); //数据源的类型为:class com.alibaba.druid.pool.DruidDataSource
    }
    
  4. 更多配置项:https://github.com/alibaba/druid/wiki/DruidDataSource%E9%85%8D%E7%BD%AE

2.4、配置Druid的监控页功能

官方地址:https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE_StatViewServlet%E9%85%8D%E7%BD%AE

  • 提供监控信息展示的html页面
  • 提供监控信息的JSON API

image-20210905154418590

  1. 向容器中注入一个 StatViewServlet 即可开启监控页功能 但没有打开监控功能

    @Configurationpublic class MyConfig {
        /*        配置druid 数据源的监控页功能            得依靠StatViewServlet 向容器中注入该servlet     */  
        @Bean   
        public ServletRegistrationBean<StatViewServlet> statViewServletServletRegistrationBean(){  
            StatViewServlet statViewServlet = new StatViewServlet();//创建一个 StatViewServlet servlet  
            ServletRegistrationBean<StatViewServlet> registrationBean = new ServletRegistrationBean<>(statViewServlet,"/druid/*"); //该servlet映射的路径为 /druid 下的所有   
            return registrationBean;    
        }
    }
    
  2. 效果图 访问网址:http://localhost:8888/druid/index.html

    image-20210905153602806

  3. 配置监控页面访问密码

    image-20210905162826192

    @Configurationpublic class MyConfig {    
        @Bean   
        public ServletRegistrationBean<StatViewServlet> statViewServletServletRegistrationBean(){  
            StatViewServlet statViewServlet = new StatViewServlet();//创建一个 StatViewServlet servlet  
            ServletRegistrationBean<StatViewServlet> registrationBean = new ServletRegistrationBean<>(statViewServlet,"/druid/*"); //该servlet映射的路径为 /druid 下的所有     
            //设置登陆用户名密码       
            registrationBean.addInitParameter("loginUsername", "pj"); 
            registrationBean.addInitParameter("loginPassword","123");     
            return registrationBean;   
        }
    }
    

    配置后需要登陆才能查看监控页信息:

    image-20210905163434600

  4. 设置 ip 黑白名单

    image-20210905163214294

2.5、打开Druid的监控统计功能
  1. 默认是不打开监控功能的

    image-20210905155003593

  2. 官方打开监控功能的方式

    image-20210905154320753

  3. 设置druid数据源的属性

    /*
    配置类
     */
    @Configuration
    public class MyConfig {
        @ConfigurationProperties("spring.datasource") //绑定配置文件spring.datasource开头的配置
        @Bean
        public DataSource dataSource() throws SQLException {
            DruidDataSource druidDataSource = new DruidDataSource();
            //手动设置数据源的配置
          /*  druidDataSource.setUrl();
            druidDataSource.setUsername();
            druidDataSource.setPassword();
            druidDataSource.setDriverClassName();*/
    
            //开启监控功能  也可在配置文件中配置
            druidDataSource.setFilters("stat");
            return druidDataSource;
    	}
    }
    
    spring:
       datasource:
       #    开启监控功能 和防火墙功能 
    	filters: stat,wall
    	# 设置最大活跃数
    	maxActive: 10
    
  4. 效果

    每个字段的相关介绍:https://github.com/alibaba/druid/wiki/Druid%E8%BF%9E%E6%8E%A5%E6%B1%A0%E4%BB%8B%E7%BB%8D

    image-20210905155739243

  5. 系统中所有的 filter

    image-20210905174010480

  6. 慢sql

    <bean id="stat-filter" class="com.alibaba.druid.filter.stat.StatFilter">   
        <property name="slowSqlMillis" value="10000" /> 
        <property name="logSlowSql" value="true" />
    </bean>
    <!-- 使用 slowSqlMillis 定义慢SQL的时长 -->
    
2.6、内置监控中的Web关联监控配置
  1. 默认不开启web应用的监控和URL监控默认不开启

    image-20210905160208909

  2. 官方开启功能的方法

    image-20210905160313328

  3. 添加 WebStatFilter 到容器中 开启web应用监控功能和URL监控

    @Configuration
    public class MyConfig {	
        /*        开启web应用监控功能            依靠 WebStatFilter  想容器中注入该Filter     */    
        @Bean    
        public FilterRegistrationBean<WebStatFilter> webStatFilterFilterRegistrationBean(){   
            WebStatFilter webStatFilter = new WebStatFilter();       
            FilterRegistrationBean<WebStatFilter> filterRegistrationBean = new FilterRegistrationBean<>();
            filterRegistrationBean.setUrlPatterns(Arrays.asList("/*"));//拦截所有的路径  
            filterRegistrationBean.addInitParameter("exclusions","*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");//添加参数排除项参数信息  排出静态资源 和  /druid/* 的请求     
            return filterRegistrationBean;   
        }
    }
    
  4. 效果

    image-20210905161236021

2.7、配置Druid和Spring关联监控配置
  1. spring监控默认不开启 可以监控容器中有哪些组件

    image-20210905162355637

  2. 官方开启方式:https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE_Druid%E5%92%8CSpring%E5%85%B3%E8%81%94%E7%9B%91%E6%8E%A7%E9%85%8D%E7%BD%AE

2.8、配置防御SQL注入攻击
  1. SQL防火墙默认不开启

    image-20210905161721721

  2. 官方开启防火墙的方式

    image-20210905161744143

  3. 设置数据源的属性值

    /*
    配置类
     */
    @Configuration
    public class MyConfig {
        @ConfigurationProperties("spring.datasource") //绑定配置文件spring.datasource开头的配置
        @Bean
        public DataSource dataSource() throws SQLException {
            DruidDataSource druidDataSource = new DruidDataSource();
            //手动设置数据源的配置
          /*  druidDataSource.setUrl();
            druidDataSource.setUsername();
            druidDataSource.setPassword();
            druidDataSource.setDriverClassName();*/
    
            //开启监控功能  也可在配置文件中配置
            druidDataSource.setFilters("stat,wall");
            return druidDataSource;
    	}
    }
    
  4. 效果图

    image-20210905162223205

多访问几个请求的效果图:

image-20210905163655922

3、druid数据源starter整合方式

官方地址:https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter

注释掉上述的配置类 配置类失效

//@Configuration
public class MyConfig {}
3.1、导入依赖
<!--引入 druid数据源的监控功能-->
<dependency>   
    <groupId>com.alibaba</groupId> 
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.17</version>
</dependency>
  • 导入依赖后

    image-20210905164947023

3.2、自动配置类
@Configuration@ConditionalOnClass(DruidDataSource.class) // 容器中有 DruidDataSource数据源
@AutoConfigureBefore(DataSourceAutoConfiguration.class) //先配置好这个 在配置DataSourceAutoConfiguration  在官方之前配置好
@EnableConfigurationProperties({DruidStatProperties.class, DataSourceProperties.class}) //绑定配置组件  绑定以 spring.datasource.druid  spring.datasource前缀的配置
@Import({DruidSpringAopConfiguration.class,//配置Druid和Spring关联监控配置 绑定 spring.datasource.druid.aop-patterns     
         DruidStatViewServletConfiguration.class, //配置Druid的监控页功能 绑定 spring.datasource.druid.stat-view-servlet 默认值为true  开启的   
         DruidWebStatFilterConfiguration.class,//内置监控中的Web关联监控配置 绑定 spring.datasource.druid.web-stat-filter 默认值为true  开启的    
         DruidFilterConfiguration.class}  //配置防御SQL注入攻击和Druid的监控统计功能等功能       
        /*    
        private static final String FILTER_STAT_PREFIX = "spring.datasource.druid.filter.stat"; 监控统计功能    
        private static final String FILTER_CONFIG_PREFIX = "spring.datasource.druid.filter.config";   
        private static final String FILTER_ENCODING_PREFIX = "spring.datasource.druid.filter.encoding"; 
        private static final String FILTER_SLF4J_PREFIX = "spring.datasource.druid.filter.slf4j";   
        private static final String FILTER_LOG4J_PREFIX = "spring.datasource.druid.filter.log4j"; 
        private static final String FILTER_LOG4J2_PREFIX = "spring.datasource.druid.filter.log4j2";
        private static final String FILTER_COMMONS_LOG_PREFIX = "spring.datasource.druid.filter.commons-log";   
        private static final String FILTER_WALL_PREFIX = "spring.datasource.druid.filter.wall"; 防火墙 
        private static final String FILTER_WALL_CONFIG_PREFIX = FILTER_WALL_PREFIX + ".config";        */ 
       )
public class DruidDataSourceAutoConfigure {   
    private static final Logger LOGGER = LoggerFactory.getLogger(DruidDataSourceAutoConfigure.class);
    @Bean(initMethod = "init")   
    @ConditionalOnMissingBean //没有就配置这个 所以先要配置这个配置类 在配置DataSourceAutoConfiguration  DataSourceAutoConfiguration配置类会默认配置 HikariDataSource       
    public DataSource dataSource() {    
        LOGGER.info("Init DruidDataSource");      
        return new DruidDataSourceWrapper();  
    }
}
3.3、配置文件配置
spring: 
  datasource:
    #配置数据源的基本信息
    password: 123
    username: root
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/test
    druid:
      #配置监控页
      stat-view-servlet:
        #开启监控页功能
        enabled: true
        #配置用户密码
        login-username: pj
        login-password: 123
        #是否由有重置按钮
        resetEnable: false
      #配置web关联监控
      web-stat-filter:
        #开启web关联监控
        enabled: true
        #拦截路径
        url-pattern: /*
        #排出路径 有默认值 *.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*
        exclusions:  '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'
      #开启 防火墙和监控统计功能
      filters: wall,stat
      #配置单个 filters 功能的具体配置
      filter:
        #监控统计功能
        stat:
          #慢查询sql的时间 1000ms 默认 3000ms
          slow-sql-millis: 1000
          #日志记录慢查询
          log-slow-sql: true
          #开启监控统计功能
          enabled: true
        #配置防火墙功能
        wall:
          #开启防火墙
          enabled: true
          config:
            #不允许删除操作 更新操作就会被防火墙拦截
            delete-allow: false
      #开启 Druid和Spring关联监控配置  com.pj.boot下组件都监控
      aop-patterns: com.pj.boot.*

    #    开启监控功能 和防火墙功能
    #    filters: stat,wall
    # 设置最大活跃数
  #    maxActive: 10
3.4、具体的配置信息参照官方文档

https://github.com/alibaba/druid

4、整合 mybatis

官方地址:https://github.com/mybatis

4.1、导入依赖
<!--引入mybatis 场景-->
<dependency>    
    <groupId>org.mybatis.spring.boot</groupId> 
    <artifactId>mybatis-spring-boot-starter</artifactId>  
    <version>2.1.4</version>
</dependency>

image-20210905174948386

4.2、配置模式
  1. 自动配置类

    //@org.springframework.context.annotation.Configuration
        @ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class }) 
        //容器中有SqlSessionFactory 和SqlSessionFactoryBean的类  导入mybatis 依赖就会有
        @ConditionalOnSingleCandidate(DataSource.class) //整个容器中只有一个数据源
        @EnableConfigurationProperties(MybatisProperties.class) //开启绑定配置组件  以mybatis前缀开头的配置 可以修改配置文件中 mybatis 开始的所有
        @AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
        public class MybatisAutoConfiguration implements InitializingBean {  
            @Bean 
            //给容器中放入了 sqlSessionFactory  参数的值会从容器中拿到数据源和 MybatisProperties类 配置文件中的信息一起 进行获取 sqlSessionFactory  
                @ConditionalOnMissingBean 
            public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {....}
            @Bean //给容器 放置 了一个SqlSessionTemplate  SqlSessionTemplate组合了 SqlSession  
            @ConditionalOnMissingBean  
            public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {...}  
            
            
    //@org.springframework.context.annotation.Configuration    
            @Import(AutoConfiguredMapperScannerRegistrar.class)//导入  AutoConfiguredMapperScannerRegistrar  只要我们写的操作MyBatis的接口标准了 @Mapper 就会被自动扫描进来 
             @ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class }) //容器中没有MapperFactoryBean、和MapperScannerConfigurer 会导入  AutoConfiguredMapperScannerRegistrar 
                public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {}
    
    • 全局配置文件:可以通过配置文件指定

    • SqlSessionFactory: 自动配置好了

    • SqlSession:自动配置了 SqlSessionTemplate 组合了SqlSession

    • @Import(AutoConfiguredMapperScannerRegistrar.class);

    • Mapper:AutoConfiguredMapperScannerRegistrar 只要我们写的操作MyBatis的接口标准了 @Mapper 就会被自动扫描进来

  2. 使用步骤

    • 编写全局配置文件(可以省略 通过配置文件配置全局配置文件的信息)

      mybatis-config.xml

      <?xml version="1.0" encoding="UTF-8" ?>
      <!DOCTYPE configuration        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"        "http://mybatis.org/dtd/mybatis-3-config.dtd">
      <configuration>   
          <!-- 由于Spring Boot自动配置缘故,此处不必配置,只用来做做样。-->   
          <!-- <settings>         开启驼峰命名策略 可以通过配置文件开启       
       <setting name="mapUnderscoreToCamelCase" value="true"/>  
        </settings>
       -->
      </configuration>
      
    • 编写mapper接口 要标准@Mapper注解 也可以在启动类上加上@MapperScan(指定扫描的mapper包下路径)替换@Mapper

      TestBeanMapper

      @Mapper
      public interface TestBeanMapper {   
          public TestBean selectTestBeanById(int id);
      }
      
    • 编写接口的映射文件

      TestBeanMapper.xml

      <?xml version="1.0" encoding="UTF-8" ?>
      <!DOCTYPE mapper
              PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
              "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
      <mapper namespace="com.pj.boot.mysqlDate.mapper.TestBeanMapper">
      
          <select id="selectTestBeanById" resultType="com.pj.boot.mysqlDate.pojo.TestBean">
              select * from testBean where id=#{id}
          </select>
      </mapper>
      
    • 配置全局配置文件和mapper接口的映射的路径

    mybatis:
      #全局配置文件的位置  配置了  configuration: 属性 就不能配置 config-location 所以可以不用写 全局配置文件  可以不写全局;配置文件,所有全局配置文件的配置都放在configuration配置项中即可
      #config-location: classpath:mybatis/mybatis-config.xml
      #指定mapper映射文件的位置
      mapper-locations:  classpath:mybatis/mapper/*.xml
      #配置mybatis全局配置文件
      configuration:
        #映射驼峰命名
        map-underscore-to-camel-case: true
    
    • 编写service层

      TestBeanService

      @Servicepublic class TestBeanService {   
          @Autowired    
          TestBeanMapper testBeanMapper; 
          public TestBean getTestBean(int id){ 
              return testBeanMapper.selectTestBeanById(id); 
          }
      }
      
    • 编写controller层

      @Controller
      public class MySqlController { 
          @Autowired   
          TestBeanService testBeanService; 
          @ResponseBody    
          @GetMapping("/getTestBean") 
          public TestBean getTestBean(@RequestParam("id") int id){   
              return testBeanService.getTestBean(id);    
          }
      }
      
4.3、注解模式和配置模式混合
  1. mapper层

    @Mapper
    public interface TestBeanMapper {  
        public TestBean selectTestBeanById(int id);    //纯注解版 
        @Insert("insert into testBean(username,password) values(#{username},#{password})")  
        @Options(useGeneratedKeys = true,keyProperty = "id") //返回自增主键的id值 
        public int insertTestBean(TestBean testBean);
    }
    
    <!-- public int insertTestBean(TestBean testBean);-->
    <insert id="insertTestBean" useGeneratedKeys="true" keyProperty="id">
        <!--可以返回id自增之后的值--> 
        insert into testBean(username,password) values(#{username},#{password})
    </insert>
    
  2. service层

    @Service
    public class TestBeanService {   
        @Autowired    
        TestBeanMapper testBeanMapper;  
        public TestBean getTestBean(int id){ 
            return testBeanMapper.selectTestBeanById(id);  
        }    
        public int insetTestBean(TestBean testBean){ 
            return testBeanMapper.insertTestBean(testBean);
        }
    }
    
  3. controller层

    @ResponseBod
    @GetMapping("/insertTestBean")
    public int insertTestBean(@RequestParam("username") String username, 
                              @RequestParam("password") String password){
        return testBeanService.insetTestBean(new TestBean(username,password));
    }
    //访问:http://localhost:8888/insertTestBean?username=pj&password=123
    
4.4、总结
  • 最佳实战:

    • 引入mybatis-starter

    • 配置application.yaml中,指定mapper-location位置即可

    • 编写Mapper接口并标注@Mapper注解

    • 简单方法直接注解方式

    • 复杂方法编写mapper.xml进行绑定映射

    • @MapperScan(“com.atguigu.admin.mapper”) 简化,其他的接口就可以不用标注@Mapper注解

5、整合mybatis-plus (可查看是项目mybatis-plus)
5.1、导入依赖
<!--引入mybatis-plus  可以不用导入mybatis和jdbc 的依赖了-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.0.5</version>
</dependency>

image-20210905192235435

5.2、自动配置
  1. 自动配置类 MybatisPlusAutoConfiguration

    @org.springframework.context.annotation.Configuration
    @ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
    @ConditionalOnSingleCandidate(DataSource.class)
    @EnableConfigurationProperties(MybatisPlusProperties.class) //绑定了MybatisPlusProperties 配置项 绑定了前缀为 mybatis-plus 的配置项
    @AutoConfigureAfter(DataSourceAutoConfiguration.class)
    public class MybatisPlusAutoConfiguration {
        @Bean //自动配置好了 SqlSessionFactory
        @ConditionalOnMissingBean
        public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {....}
        
        @Bean //自动配置好了 SqlSessionTemplate  SqlSessionTemplate组合了 SqlSession
        @ConditionalOnMissingBean
        public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {}
        
        //扫描 @Mapper注解的mapper接口
        public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar {
    
            private BeanFactory beanFactory;
    
            @Override
            public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {....}
        }
    }
    
  2. MybatisPlusProperties 配置项类

    mapperLocations 默认值为 classpath*:/mapper/**/*.xml 任意包的类路径下的所有mapper文件夹下任意路径下的所有xml都是sql映射文件。 建议以后sql映射文件,放在 mapper下

    @ConfigurationProperties(prefix = Constants.MYBATIS_PLUS)
    public class MybatisPlusProperties {
    
        private static final ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
        ....
        /**
         * Locations of MyBatis mapper files.
         *
         * @since 3.1.2 add default value   3.1.2 增加的默认值
         */
        private String[] mapperLocations = new String[]{"classpath*:/mapper/**/*.xml"};
        ....
    }
    
  3. 总结自动配置

    • MybatisPlusAutoConfiguration配置类,MybatisPlusProperties配置项绑定。
    • SqlSessionFactory自动配置好,底层是容器中默认的数据源。
    • mapperLocations自动配置好的,有默认值classpath*:/mapper/**/*.xml,这表示任意包的类路径下的所有mapper文件夹下任意路径下的所有xml都是sql映射文件。 建议以后sql映射文件放在 mapper下。
    • 容器中也自动配置好了SqlSessionTemplate。
    • @Mapper 标注的接口也会被自动扫描,建议直接 @MapperScan(“com.lun.boot.mapper”)批量扫描。
    • MyBatisPlus优点之一:只需要我们的Mapper继承MyBatisPlus的BaseMapper 就可以拥有CRUD能力,减轻开发工作。
5.3、分页的使用(springboot-2项目中)
  1. mapper层 继承了BaseMapper,包含了基本的crud

    @Mapper
    public interface UserMapper extends BaseMapper<User> {
    }
    
  2. service层 继承了 ServiceImpl<UserMapper, User>,属于简单service,包含了基本的crud

    @Service
    public class UserService extends ServiceImpl<UserMapper, User>  {
    }
    
  3. controller层

    @Controller
    @Slf4j
    public class ThymeleafController {
    
        @Autowired
        UserService userService;
    
        //通过id删除用户
        @GetMapping("/delete/{id}")
        public String deleteUser(@PathVariable("id") Long id,
                                 @RequestParam(value = "pn",defaultValue = "1") Integer pn,
                                 RedirectAttributes redirectAttributes){
            //删除数据
            userService.removeById(id);
            //删除的那一页数据就停留在那一页 会url 的方式添加到链接上
            redirectAttributes.addAttribute("pn", pn);
            //重定向到 dynamic_table请求中
            return "redirect:/dynamic_table";
        }
        
        
        @GetMapping("dynamic_table")
        public String goToDynamic_table(Map<String ,Object> map,
                                        @RequestParam(value = "pn",defaultValue = "1") int pn){//pn 第几页
            //查询所有用户  使用是简单service的中定义方法
            //        List<User> users = userService.list();
            //current: 当前页  size:每页显示多少条数据  创建分页
            Page<User> page = new Page<>(pn,2);
            //分页查询的数据
            Page<User> userPage = userService.page(page);
            map.put("userPage", userPage);
    
            return "dataTables/dynamic_table";
        }
    }
    
  4. config配置类 相同器中注入一个分页过滤器

    @Configuration
    public class MyPagesConfig {
        @Bean
        public MybatisPlusInterceptor mybatisPlusInterceptor() {
            MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
            interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
            return interceptor;
        }
    }
    
  5. HTML层

     <table class="display table table-bordered table-striped" id="dynamic-table">
                                        <thead>
                                        <tr>
                                            <th>#</th>
                                            <th>id</th>
                                            <th>姓名</th>
                                            <th>年龄</th>
                                            <th>邮箱</th>
                                            <th>操作</th>
                                        </tr>
                                        </thead>
                                        <tbody>
                                        <tr class="gradeX" th:each="user,status:${userPage.records}">
                                            <td th:text="${status.count}">Trident</td>
                                            <td th:text="${user.id}">Internet
                                                Explorer 4.0
                                            </td>
                                            <td th:text="${user.name}">Win 95+</td>
                                            <td class="center hidden-phone" th:text="${user.age}">4</td>
                                            <td class="center hidden-phone" th:text="${user.email}">X</td>
                                            <td class="center hidden-phone">
                                                <a th:href="@{/delete/{id}(id=${user.id},pn=${userPage.current})}" class="btn btn-danger" type="button">删除</a>
                                            </td>
                                        </tr>
                                        </tbody>
                                    </table>
    
    <!--分页信息-->
     <div class="row-fluid">
                                        <div class="span6">
                                            <div class="dataTables_info" id="dynamic-table_info">当前第 [[${userPage.current}]]
                                                页 总共 [[${userPage.pages}]] 页 共计 [[${userPage.total}]]
                                                条数据
                                            </div>
                                        </div>
                                        <div class="span6">
                                            <div class="dataTables_paginate paging_bootstrap pagination">
                                                <ul>
                                                    <li class="prev disabled" th:class="${userPage.hasPrevious}? 'prev':'prev disabled'" ><a href="#" th:href="@{/dynamic_table(pn=${userPage.current-1})}">Previous</a></li>
                                                    <!--th:class="${num ==userPage.current?'active':''}  判断如果是 当前页  当前页码就高亮-->
                                                    <li class="active" th:each="num:${#numbers.sequence(1,userPage.pages)}" th:class="${num == userPage.current?'active':''}" >
                                                        <!-- th:each="${#numbers.sequence(1,userPage.pages)}" 生成 1到 userPage.pages 的个数-->
                                                        <!-- th:href="@{/dynamic_table(pn=${num})} thymeleaf 带参数的链接-->
                                                        <a href="#" th:href="@{/dynamic_table(pn=${num})}">[[${num}]]</a>
                                                    </li>
                                                    <li class="next disabled" th:class="${userPage.hasNext}? 'next':'next disabled'" ><a href="#" th:href="@{/dynamic_table(pn=${userPage.current+1})}">Next</a></li>
                                                </ul>
                                            </div>
                                        </div>
                                    </div>
    
  6. pojo

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    //@TableName("user") //指定映射的表名
    public class User {
        @TableField(exist = false) //标记为不是表中属性
        private String username;
        @TableField(exist = false)
        private String password;
    
        private Long id;
        private String name;
        private Integer age;
        private String email;
    }
    
2、NoSQL
1、Redis(见redis-springboot项目)
1、导入场景依赖
<!--springBoot 操作Redis导入的jar-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

image-20210907094156451

2、自动配置
  1. 自动配置类 RedisAutoConfiguration

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(RedisOperations.class)
    @EnableConfigurationProperties(RedisProperties.class)// 绑定配置属性 RedisProperties  以前缀 spring.redis开头的配置
    @Import({ LettuceConnectionConfiguration.class,  //连接工厂 默认使用Lettuce
              JedisConnectionConfiguration.class })
    public class RedisAutoConfiguration {
        //装入了 RedisTemplate  操作Redis的连接  k:v 都是object
        @Bean
    	@ConditionalOnMissingBean(name = "redisTemplate")
    	@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    	public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
    		RedisTemplate<Object, Object> template = new RedisTemplate<>();
    		template.setConnectionFactory(redisConnectionFactory);
    		return template;
    	}
        
         //装入了 StringRedisTemplate  操作字符串类型的Redis的连接 k:v 都是string
        @Bean
    	@ConditionalOnMissingBean
    	@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    	public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
    		StringRedisTemplate template = new StringRedisTemplate();
    		template.setConnectionFactory(redisConnectionFactory);
    		return template;
    	}
    }
    
  2. RedisProperties 配置属性,以前缀 spring.redis开头的配置

  3. LettuceConnectionConfiguration 连接工厂 默认使用Lettuce作为连接

  4. JedisConnectionConfiguration 连接工厂 导入 Jedis 的依赖jar包 指定配置 spring.redis.client-type:jedis 就是有 jedis作为连接工厂

  5. 总结

    • RedisAutoConfiguration自动配置类,RedisProperties 属性类 --> spring.redis.xxx是对redis的配置。
    • 连接工厂LettuceConnectionConfiguration、JedisConnectionConfiguration是准备好的。
    • 自动注入了RedisTemplate<Object, Object>,xxxTemplate。
    • 自动注入了StringRedisTemplate,key,value都是String
    • 底层只要我们使用StringRedisTemplate、RedisTemplate就可以操作Redis。
3、外网Redis环境搭建:
  1. 阿里云按量付费Redis,其中选择经典网络
  2. 申请Redis的公网连接地址。
  3. 修改白名单,允许0.0.0.0/0访问。
4、Redis操作与统计网站访问次数
  1. 编写基本的配置文件

    spring:
      redis:
    #   url: redis://lfy:Lfy123456@r-bp1nc7reqesxisgxpipd.redis.rds.aliyuncs.com:6379
        host: r-bp1nc7reqesxisgxpipd.redis.rds.aliyuncs.com #连接的主机地址
        port: 6379 #端口号
        password: lfy:Lfy123456  #密码
        client-type: jedis  #配置使用jedis操作Redis
        jedis:
          pool:
            max-active: 10 #设置池中的最大活跃数
    #   lettuce:# 另一个用来连接redis的java框架
    #      pool:
    #        max-active: 10
    #        min-idle: 5
    
    
  2. 测试连接

    @SpringBootTest
    public class Boot05WebAdminApplicationTests {
    
        @Autowired
        StringRedisTemplate redisTemplate;
    
    
        @Autowired
        RedisConnectionFactory redisConnectionFactory;
    
        @Test
        void testRedis(){
            ValueOperations<String, String> operations = redisTemplate.opsForValue();
    
            operations.set("hello","world");
    
            String hello = operations.get("hello");
            System.out.println(hello);
    
            System.out.println(redisConnectionFactory.getClass());
        }
    
    }
    
    
  3. URL统计拦截器:

    @Component
    public class RedisUrlCountInterceptor implements HandlerInterceptor {
    
        @Autowired
        StringRedisTemplate redisTemplate;
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            String uri = request.getRequestURI();
    
            //默认每次访问当前uri就会计数+1
            redisTemplate.opsForValue().increment(uri);
    
            return true;
        }
    }
    
    
  4. 向容器中添加拦截器

    @Configuration
    public class AdminWebConfig implements WebMvcConfigurer{
    
        @Autowired
        RedisUrlCountInterceptor redisUrlCountInterceptor;
    
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
    
            registry.addInterceptor(redisUrlCountInterceptor)
                    .addPathPatterns("/**")
                    .excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**",
                            "/js/**","/aa/**");
        }
    }
    
  5. Filter、Interceptor 几乎拥有相同的功能?使用哪个?

    • Filter是Servlet定义的原生组件,它的好处是脱离Spring应用也能使用。
    • Interceptor是Spring定义的接口,可以使用Spring的自动装配等功能。
  6. 调用Redis内的统计数据:

    @Slf4j
    @Controller
    public class IndexController {
    
    	@Autowired
        StringRedisTemplate redisTemplate;
        
    	@GetMapping("/main.html")
        public String mainPage(HttpSession session,Model model){
    
            log.info("当前方法是:{}","mainPage");
    
            ValueOperations<String, String> opsForValue =
                    redisTemplate.opsForValue();
    
            String s = opsForValue.get("/main.html");
            String s1 = opsForValue.get("/sql");
    
            model.addAttribute("mainCount",s);
            model.addAttribute("sqlCount",s1);
    
            return "main";
        }
    }
    
    

7、单元测试

1、JUnit5 的变化
  1. Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库

  2. JUnit 5官方文档

  3. 作为最新版本的JUnit框架,JUnit5与之前版本的JUnit框架有很大的不同。由三个不同子项目的几个不同模块组成。

    • JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

    • JUnit Platform: Junit Platform是在JVM上启动测试框架的基础,不仅支持Junit自制的测试引擎,其他测试引擎也都可以接入。

    • JUnit Jupiter: JUnit Jupiter提供了JUnit5的新的编程模型,是JUnit5新特性的核心。内部包含了一个测试引擎,用于在Junit Platform上运行。

    • JUnit Vintage: 由于JUint已经发展多年,为了照顾老的项目,JUnit Vintage提供了兼容JUnit4.x,JUnit3.x的测试引擎。

      img

  4. 注意:

    SpringBoot 2.4 以上版本移除了默认对 Vintage 的依赖。如果需要兼容JUnit4需要自行引入(不能使用JUnit4的功能 @Test)

    JUnit 5’s Vintage已经从spring-boot-starter-test从移除。如果需要继续兼容Junit4(@Test注解)需要自行引入Vintage依赖:

    <dependency>
        <groupId>org.junit.vintage</groupId>
        <artifactId>junit-vintage-engine</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.hamcrest</groupId>
                <artifactId>hamcrest-core</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    
  5. 使用添加JUnit 5,添加对应的starter:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    
  6. Spring的JUnit 5的基本单元测试模板(Spring的JUnit4的是@SpringBootTest+@RunWith(SpringRunner.class)):

    import org.junit.jupiter.api.Assertions;
    import org.junit.jupiter.api.Test;//注意不是org.junit.Test(这是JUnit4版本的)
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    
    @SpringBootTest
    class SpringBootApplicationTests {
    
        @Autowired
        private Component component;
        
        @Test
        //@Transactional 标注后,测试完毕,连接的数据库有自动回滚功能
        public void contextLoads() {
    		Assertions.assertEquals(5, component.getFive());
        }
    }
    
2、常用测试注解
1、官方文档 - Annotations
2、常用注解
  • @Test:表示方法是测试方法。但是与JUnit4的@Test不同,他的职责非常单一不能声明任何属性,拓展的测试将会由Jupiter提供额外测试
  • @ParameterizedTest:表示方法是参数化测试。
  • @RepeatedTest:表示方法可重复执行。
  • @DisplayName:为测试类或者测试方法设置展示名称。
  • @BeforeEach:表示在每个单元测试之前执行。
  • @AfterEach:表示在每个单元测试之后执行。
  • @BeforeAll:表示在所有单元测试之前执行。
  • @AfterAll:表示在所有单元测试之后执行。
  • @Tag:表示单元测试类别,类似于JUnit4中的@Categories。
  • @Disabled:表示测试类或测试方法不执行,类似于JUnit4中的@Ignore。
  • @Timeout:表示测试方法运行如果超过了指定时间将会返回错误。
  • @ExtendWith:为测试类或测试方法提供扩展类引用。
3、测试代码
@DisplayName("junit5测试") //给当前的测试类 添加备注
@SpringBootTest //标记为springboot的测试类  可以使用springboot容器中的功能 不加 @Autowired注解无法使用
    /*
    @BootstrapWith(SpringBootTestContextBootstrapper.class)
    @ExtendWith(SpringExtension.class)
     */
class Junit5Tests {

    @DisplayName("测试displayName") //给当前的测试方法 添加备注
    @Test
    void testDisplayName() {
        System.out.println(1);
    }


    @Disabled //运行整个类时  标记为可以不用执行的测试方法
    @DisplayName("测试test2") //给当前的测试方法 添加备注
    @Test
    void test2() {
        System.out.println(2);
    }

    @DisplayName("测试testRepeatedTest") //给当前的测试方法 添加备注
    @RepeatedTest(value = 5) //指定当前方法执行多少次
    @Test
    void testRepeatedTest() {
        System.out.println(3);
    }

    /*
        @Timeout规定方法超时时间
            java.util.concurrent.TimeoutException: testTimeout() timed out after 500 milliseconds
     */
    @Timeout(value = 500,unit = TimeUnit.MILLISECONDS) //超过500ms就会抛出异常
    @DisplayName("测试Timeout") //给当前的测试方法 添加备注
    @Test
    void testTimeout() {
        try {
            TimeUnit.MILLISECONDS.sleep(600);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
    }
    /*
        在每个测试方法之前运行  针对每个方法  执行多次
     */
    @BeforeEach
    void testBeforeEach(){
        System.out.println("测试开始了");
    }

    /*
      在每个测试方法之前运行
   */
    @AfterEach
    void testAfterEach(){
        System.out.println("测试结束了");
    }

    /*
       在整个测试类运行之前执行  针对类 只执行一次
       必须标记为static 否则会抛出异常
       org.junit.platform.commons.JUnitException: @BeforeAll method 'void com.pj.springboot.Junit5Tests.testBeforeAll()' must be static unless the test class is annotated with @TestInstance(Lifecycle.PER_CLASS).

    */
    @BeforeAll
    static void testBeforeAll(){
        System.out.println("所有的测试方法开始开始测试");
    }

    /*
      在整个测试类运行完成之后执行  针对类 只执行一次
  */
    @AfterAll
    static void testAfterAll(){
        System.out.println("所有的测试方法测试结束");
    }
}

image-20210907105416131

3、断言机制
1、含义

断言Assertion是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是org.junit.jupiter.api.Assertions的静态方法。检查业务逻辑返回的数据是否合理。所有的测试运行结束以后,会有一个详细的测试报告。

2、分类
2.1、简单断言

用来对单个值进行简单的验证

image-20210907111446458

   /*
        有一个断言失败了 后面的断言不会执行
     */
    @DisplayName("测试简单断言")
    @Test
    void testAssertions(){
        int call = call(3, 3);
//        assertEquals(6,call); // 6 预估值  call 实际值
        assertEquals(6,call,"计算错误"); //如果错误指定错误信息 如果这个计算错误  下面的断言不会执行
        Object o1 = new Object();
        Object o2 = new Object();
        assertSame(o1,o2,"两个对象不一样");
    }

image-20210907111500040

image-20210907111507551

2.2、数组断言

通过 assertArrayEquals 方法来判断两个对象或原始类型的数组是否相等。

@Test
@DisplayName("array assertion")
public void array() {
	assertArrayEquals(new int[]{1, 2}, new int[] {1, 2}); //每个下标的元素相等 就相等
    assertArrayEquals(new int[]{2,1}, new int[] {1, 2},"数组内容不相等"); //这个就会抛出异常
}
2.3、组合断言

assertAll()方法接受多个 org.junit.jupiter.api.Executable 函数式接口(空参空返回的函数式接口)的实例作为要验证的断言,可以通过 lambda 表达式很容易的提供这些断言。

/*
    所有断言全部成功组合断言才成功,有一个失败了就是失败
*/
@Test
@DisplayName("组合断言")
void all() {
    assertAll("组合断言1", //组合断言的名称
            ()-> assertTrue(true && true),//组合断言需要判断的每个断言
            ()-> assertEquals(3, 3));
    assertAll("组合断言2",
            ()-> assertTrue(true && true),
            ()-> assertEquals(2, 3,"结果不是预期"));
}

image-20210907112316228

2.4、异常断言

在JUnit4时期,想要测试方法的异常情况时,需要用@Rule注解的ExpectedException变量还是比较麻烦的。而JUnit5提供了一种新的断言方式Assertions.assertThrows(),配合函数式编程就可以进行使用。

@Test
@DisplayName("断言异常测试")
void exceptionTest() {
    /*
    	第一个参数是预期出现的异常类型,
    	第二个参数是Executable 接口,
    	第三个参数是不符合第一个异常时抛出的信息
    */
    assertThrows(ArithmeticException.class,
            ()->{int i = 10/0;}); //抛出 ArithmeticException 才能算执行成功
    assertThrows(ArithmeticException.class,
            ()->{int i = 10/2;},
            "当业务逻辑居然正常运行");
}
2.5、超时断言

Unit5还提供了Assertions.assertTimeout()为测试方法设置了超时时间。

@Test
@DisplayName("超时测试")
public void timeoutTest() {
    //如果测试方法时间超过1s将会异常
    Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(500));
}
2.6、快速失败

通过 fail 方法直接使得测试失败。

@Test
@DisplayName("快速失败")
public void shouldFail() {
    if (1==2){
        fail("值不相等");
    }
    if (2==2){
        fail("值不相等");
    }
}
3、断言的总测试报告

image-20210907113427079

4、前置条件
1、含义

Unit 5 中的前置条件(assumptions【假设】)类似于断言,不同之处在于不满足的断言assertions会使得测试方法失败,而不满足的前置条件只会使得测试方法的执行终止

前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要。

/*==================测试前置条件=====================*/
@DisplayName("测试前置条件")
void testAssumptions(){
    Assumptions.assumeTrue(true,"结果不是true");//前置条件 必须满足这个条件才能执行下面的代码 不满足条件就会跳过下面的代码
    System.out.println(111);
}
@DisplayName("前置条件")
public class AssumptionsTest {
    private final String environment = "DEV";
    /*
    assumeTrue 和 assumFalse 确保给定的条件为 true 或 false,不满足条件会使得测试执行终止。
	assumingThat 的参数是表示条件的布尔值和对应的 Executable 接口的实现对象。只有条件满足时,Executable 对象才会被执行;当条件不满足时,测试执行并不会终止。
    */
    @Test
    @DisplayName("simple")
    public void simpleAssume() {
        assumeTrue(Objects.equals(this.environment, "DEV"));
        assumeFalse(() -> Objects.equals(this.environment, "PROD"));
    }

    @Test
    @DisplayName("assume then do")
    public void assumeThenDo() {
        assumingThat(
            Objects.equals(this.environment, "DEV"),
            () -> System.out.println("In DEV")
        );
    }
}
5、嵌套测试
1、官方文档 - Nested Tests
2、含义

JUnit 5 可以通过 Java 中的内部类和@Nested 注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用@BeforeEach@AfterEach注解,而且嵌套的层次没有限制。

3、代码
@DisplayName("嵌套测试")
class TestingAStackDemo {

    Stack<Object> stack;

    @Test
    @DisplayName("new Stack()")
    void isInstantiatedWithNew() {
        new Stack<>();
        //嵌套测试情况下;外层的Test不能驱动内层的 @BeforeEach/ALL之类的方法 提前/之后运行
        assertNull(stack);
    }

    @Nested //标记为嵌套测试类
    @DisplayName("when new")
    class WhenNew {

        @BeforeEach //在所有的测试之前
        void createNewStack() {
            stack = new Stack<>();
        }

        @Test
        @DisplayName("is empty")
        void isEmpty() {
            assertTrue(stack.isEmpty()); //运行成功
        }

        @Test
        @DisplayName("throws EmptyStackException when popped")
        void throwsExceptionWhenPopped() {
            assertThrows(EmptyStackException.class, stack::pop); //运行成功  stack::pop取出栈顶元素
        }

        @Test
        @DisplayName("throws EmptyStackException when peeked")
        void throwsExceptionWhenPeeked() {
            assertThrows(EmptyStackException.class, stack::peek);
        }

        @Nested//标记为嵌套测试类
        @DisplayName("after pushing an element")
        class AfterPushing {
            String anElement = "an element";
            //嵌套测试中,内层的Test可以驱动外层的  @BeforeEach/ALL之类的方法 提前/之后运行
            @BeforeEach
            void pushAnElement() {
                //增加了一个元素  前提是 stack要被new 出来
                stack.push(anElement);
            }
            @Test
            @DisplayName("it is no longer empty")
            void isNotEmpty() {
                assertFalse(stack.isEmpty());//执行成功
            }
            @Test
            @DisplayName("returns the element when popped and is empty")
            void returnElementWhenPopped() {
                assertEquals(anElement, stack.pop()); //执行成功
                assertTrue(stack.isEmpty());
            }
            @Test
            @DisplayName("returns the element when peeked but remains not empty")
            void returnElementWhenPeeked() {
                assertEquals(anElement, stack.peek());//执行成功
                assertFalse(stack.isEmpty());
            }
        }
    }
}
6、参数化测试
1、官方文档 - Parameterized Tests
2、含义

参数化测试是JUnit5很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能,也为我们的单元测试带来许多便利。

利用@ValueSource等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。

3、参数化注解
  • @ValueSource: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型
  • @NullSource: 表示为参数化测试提供一个null的入参
  • @EnumSource: 表示为参数化测试提供一个枚举入参
  • @CsvFileSource:表示读取指定CSV文件内容作为参数化测试入参
  • @MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)
4、扩展

当然如果参数化测试仅仅只能做到指定普通的入参还达不到让我觉得惊艳的地步。让我真正感到他的强大之处的地方在于他可以支持外部的各类入参。如:CSV,YML,JSON 文件甚至方法的返回值也可以作为入参。只需要去实现ArgumentsProvider接口,任何外部文件都可以作为它的入参。

/*=================参数化测试=====================*/
@ParameterizedTest //标记为参数化测试
@DisplayName("参数化测试")
@Test
@ValueSource(ints = {1,2,34,5,6})
void testParameterizedTest(int i){ //不同的参数类型有不同的属性  int对应 ints  具体见源码中
    System.out.println(i); //i参数的值就会从 ValueSource  对应的 ints中拿
}

Stream<String > stringStream(){
    return Stream.of("a","b","c");
}
@ParameterizedTest //标记为参数化测试
@DisplayName("参数化测试")
@Test
@MethodSource("stringStream") //方法返回值类型为Stream   返回流并且必须是静态方法
void testParameterizedTest(String  s){
    System.out.println(s); //s参数的值就会从 stringStream  方法返回的流中拿
}
7、迁移指南
1、官方文档 - Migrating from JUnit 4
2、在进行迁移的时候需要注意如下的变化:
  • 注解在 org.junit.jupiter.api 包中,断言在 org.junit.jupiter.api.Assertions 类中,前置条件在 org.junit.jupiter.api.Assumptions 类中。
  • 把@Before 和@After 替换成@BeforeEach 和@AfterEach。
  • 把@BeforeClass 和@AfterClass 替换成@BeforeAll 和@AfterAll。
  • 把@Ignore 替换成@Disabled。
  • 把@Category 替换成@Tag。
  • 把@RunWith、@Rule 和@ClassRule 替换成@ExtendWith。

8、指标监控

1、SpringBoot Actuator

官方文档 - Spring Boot Actuator: Production-ready Features

1.1、简介

未来每一个微服务在云上部署以后,我们都需要对其进行监控、追踪、审计、控制等。SpringBoot就抽取了Actuator场景,使得我们每个微服务快速引用即可获得生产级别的应用监控、审计等功能。

1.2、1.x与2.x的不同

image.png

3、如何使用
3.1、引入场景
<!--引入SpringBoot Actuator 监控功能-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

image-20210907121523034

3.2、访问
  • 访问http://localhost:8080/actuator/**。监控功能的页面

image-20210907122258163

4、监控端点

默认不全部开启全部的监控断点,只有自己设置了才能够访问。

4.1常使用的端点及开启与禁用
  1. 常使用的端点

    ID描述
    auditevents暴露当前应用程序的审核事件信息。需要一个AuditEventRepository组件
    beans显示应用程序中所有Spring Bean的完整列表。
    caches暴露可用的缓存。
    conditions显示自动配置的所有条件信息,包括匹配或不匹配的原因。
    configprops显示所有@ConfigurationProperties
    env暴露Spring的属性ConfigurableEnvironment
    flyway显示已应用的所有Flyway数据库迁移。 需要一个或多个Flyway组件。
    health显示应用程序运行状况信息。
    httptrace显示HTTP跟踪信息(默认情况下,最近100个HTTP请求-响应)。需要一个HttpTraceRepository组件。
    info显示应用程序信息。
    integrationgraph显示Spring integrationgraph 。需要依赖spring-integration-core
    loggers显示和修改应用程序中日志的配置。
    liquibase显示已应用的所有Liquibase数据库迁移。需要一个或多个Liquibase组件。
    metrics显示当前应用程序的“指标”信息。
    mappings显示所有@RequestMapping路径列表。
    scheduledtasks显示应用程序中的计划任务。

tln(“In DEV”)
);
}
}


#### 5、嵌套测试

###### 1、[官方文档 - Nested Tests](https://junit.org/junit5/docs/current/user-guide/#writing-tests-nested)

###### 2、含义

JUnit 5 可以通过 Java 中的内部类和`@Nested` 注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用`@BeforeEach` 和`@AfterEach`注解,而且嵌套的层次没有限制。

###### 3、代码

```java
@DisplayName("嵌套测试")
class TestingAStackDemo {

    Stack<Object> stack;

    @Test
    @DisplayName("new Stack()")
    void isInstantiatedWithNew() {
        new Stack<>();
        //嵌套测试情况下;外层的Test不能驱动内层的 @BeforeEach/ALL之类的方法 提前/之后运行
        assertNull(stack);
    }

    @Nested //标记为嵌套测试类
    @DisplayName("when new")
    class WhenNew {

        @BeforeEach //在所有的测试之前
        void createNewStack() {
            stack = new Stack<>();
        }

        @Test
        @DisplayName("is empty")
        void isEmpty() {
            assertTrue(stack.isEmpty()); //运行成功
        }

        @Test
        @DisplayName("throws EmptyStackException when popped")
        void throwsExceptionWhenPopped() {
            assertThrows(EmptyStackException.class, stack::pop); //运行成功  stack::pop取出栈顶元素
        }

        @Test
        @DisplayName("throws EmptyStackException when peeked")
        void throwsExceptionWhenPeeked() {
            assertThrows(EmptyStackException.class, stack::peek);
        }

        @Nested//标记为嵌套测试类
        @DisplayName("after pushing an element")
        class AfterPushing {
            String anElement = "an element";
            //嵌套测试中,内层的Test可以驱动外层的  @BeforeEach/ALL之类的方法 提前/之后运行
            @BeforeEach
            void pushAnElement() {
                //增加了一个元素  前提是 stack要被new 出来
                stack.push(anElement);
            }
            @Test
            @DisplayName("it is no longer empty")
            void isNotEmpty() {
                assertFalse(stack.isEmpty());//执行成功
            }
            @Test
            @DisplayName("returns the element when popped and is empty")
            void returnElementWhenPopped() {
                assertEquals(anElement, stack.pop()); //执行成功
                assertTrue(stack.isEmpty());
            }
            @Test
            @DisplayName("returns the element when peeked but remains not empty")
            void returnElementWhenPeeked() {
                assertEquals(anElement, stack.peek());//执行成功
                assertFalse(stack.isEmpty());
            }
        }
    }
}
6、参数化测试
1、官方文档 - Parameterized Tests
2、含义

参数化测试是JUnit5很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能,也为我们的单元测试带来许多便利。

利用@ValueSource等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。

3、参数化注解
  • @ValueSource: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型
  • @NullSource: 表示为参数化测试提供一个null的入参
  • @EnumSource: 表示为参数化测试提供一个枚举入参
  • @CsvFileSource:表示读取指定CSV文件内容作为参数化测试入参
  • @MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)
4、扩展

当然如果参数化测试仅仅只能做到指定普通的入参还达不到让我觉得惊艳的地步。让我真正感到他的强大之处的地方在于他可以支持外部的各类入参。如:CSV,YML,JSON 文件甚至方法的返回值也可以作为入参。只需要去实现ArgumentsProvider接口,任何外部文件都可以作为它的入参。

/*=================参数化测试=====================*/
@ParameterizedTest //标记为参数化测试
@DisplayName("参数化测试")
@Test
@ValueSource(ints = {1,2,34,5,6})
void testParameterizedTest(int i){ //不同的参数类型有不同的属性  int对应 ints  具体见源码中
    System.out.println(i); //i参数的值就会从 ValueSource  对应的 ints中拿
}

Stream<String > stringStream(){
    return Stream.of("a","b","c");
}
@ParameterizedTest //标记为参数化测试
@DisplayName("参数化测试")
@Test
@MethodSource("stringStream") //方法返回值类型为Stream   返回流并且必须是静态方法
void testParameterizedTest(String  s){
    System.out.println(s); //s参数的值就会从 stringStream  方法返回的流中拿
}
7、迁移指南
1、官方文档 - Migrating from JUnit 4
2、在进行迁移的时候需要注意如下的变化:
  • 注解在 org.junit.jupiter.api 包中,断言在 org.junit.jupiter.api.Assertions 类中,前置条件在 org.junit.jupiter.api.Assumptions 类中。
  • 把@Before 和@After 替换成@BeforeEach 和@AfterEach。
  • 把@BeforeClass 和@AfterClass 替换成@BeforeAll 和@AfterAll。
  • 把@Ignore 替换成@Disabled。
  • 把@Category 替换成@Tag。
  • 把@RunWith、@Rule 和@ClassRule 替换成@ExtendWith。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值