SpringBoot2

文章目录


1.简介

1.1 SpringBoot优缺点

优点:

  • 创建独立Spring应用
  • 内嵌web服务器(默认使用Tomcat)
  • 自动starter依赖,简化构建配置(导入一个场景依赖就行,而且版本帮我们控制好)
  • 自动配置Spring以及第三方功能(减少固定而繁琐的配置文件)
  • 提供生产级别的监控、健康检查及外部化配置(外部化配置:修改不需要打开源码重新打包部署等)
  • 无代码生成、无需编写XML

缺点:

  • 人称版本帝,迭代快,需要时刻关注变化
  • 封装太深,内部原理复杂,不容易精通

1.2 官方文档结构

环境:java8以上,Maven3.3以上

我们使用版本:SpringBoot2.3.4

文档地址:https://docs.spring.io/spring-boot/docs/2.3.4.RELEASE/reference/html/

文档结构:

1.入门:

Getting Started

(Introducing Spring Boot, System Requirements, Servlet Containers, Installing Spring Boot, and Developing Your First Spring Boot Application)

Using Spring Boot

Build Systems, Structuring Your Code, Configuration, Spring Beans and Dependency Injection, DevTools, and more.

2.核心功能:

Spring Boot Features

Profiles, Logging, Security, Caching, Spring Integration, Testing, and more.

Spring Boot Actuator

Monitoring, Metrics, Auditing, and more.

Deploying Spring Boot Applications

Deploying to the Cloud, and Installing as a Unix application.

3.小技巧

“How-to” Guides

Application Development, Configuration, Embedded Servers, Data Access, and many more.

4.配置项文档

Application Properties

Common application properties that you can use to configure your application.


2. SpringBoot入门


2.1 HelloWord

1.创建普通的maven项目

2.pom.xml

<!-- parent标签-->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.4.RELEASE</version>
</parent>

<!-- web的场景启动器(一个依赖就够了,不需要版本号springboot帮我们控制,parent父工程里面规定了)-->
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

3.编写代码

MainApp.java

package com.sutong;
/**
 * 主程序类
 * 注意包结构,要将MainApp类放在最外侧,即包含所有子包,原因:spring-boot会自动加载启动类所在包下及其子包下的所有组件!
 * SpringBootApplication注解: 告诉这是一个SpringBoot应用
 */
@SpringBootApplication   
public class MainApp {

    public static void main(String[] args) {
        SpringApplication.run(MainApp.class, args);
    }
}

HelloController.java

package com.sutong.controller;       

//@Controller
//@ResponseBody     下面所有方法都是带有ResponseBody
@RestController  // 上面两个的复合注解
public class HelloController {

    @RequestMapping("/hello")
    public String sayHello() {
        return "Hello, SpringBoot2";
    }
}

4.测试

直接运行主程序类中的main方法,输入http://localhost:8080/hello就可以看到响应的字符串!!!


5.简化配置

resources/application.properties文件

# 就这一个统一配置文件!!都可以在文档Application Properties中找到可以配置什么
# server.port=8888 修改服务器端口号

6.简化部署

pom.xml : 导入springboot准备的插件,可以直接把项目打包成可执行的jar文件(直接在目标服务器执行即可)

打包点右上角的maven的执行package命令就行

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

cmd进入jar所在目录, java -jar jar包名称 就可以直接启动!!(jar中包含了所有的环境依赖)

包结构:


2.2 依赖管理

  • 依赖管理

    <!-- 我们的项目加入的starter-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
    </parent>
    
    <!-- spring-boot-starter-parent他的父项目-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>2.3.4.RELEASE</version>
    </parent>
    
    <!-- spring-boot-dependencies里面几乎声明了我们常用的依赖以及版本,自动版本仲裁!我们也可以改版本-->
    <properties>
        <activemq.version>5.15.13</activemq.version>
    	<mysql.version>8.0.21</mysql.version>
        ....等等等
    </properties>
    
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.apache.activemq</groupId>
                <artifactId>activemq-amqp</artifactId>
                <version>${activemq.version}</version>
            </dependency>
            ...等等等
        </dependencies>   
    </dependencyManagement>
    

    修改版本。例如修改mysql驱动(上面的默认8+版本,我们改为5+版本)

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

    <dependency>
         <groupId>mysql</groupId>
         <artifactId>mysql-connector-java</artifactId>  <!-- 自动的是8.0.21版本-->
    </dependency>
    

    2、引入非版本仲裁的jar,要写版本号。(学上面父项目的写法就行了,可以先查看底层规定的key和版本再改)

    <!-- 当前项目重写配置!Maven的特性就近优先原则-->
    <properties>
            <mysql.version>5.1.25</mysql.version>
    </properties>
    
  • starter场景启动器

    (后面我们会见到很多这种:spring-boot-starter-*)我们只要引入starter,这个场景所有常规需要的依赖我们都自动引入

    1. spring-boot-starter-data-jdbc
    2. spring-boot-starter-data-redis
    3. starter

    springboot所有场景最基本的依赖spring-boot-starter ,即自动配置的核心依赖。

    也可以使用第三方 *-spring-boot-starter的 ,简化开发的场景启动器


2.3 自动配置

  1. 自动配好Tomcat

    • web场景自动引入依赖

    • 配置Tomcat

  2. 自动配好SpringMVC

    • web场景自动引入全套组件

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

      ( 例如:dispatcherServlet,charactorEncodingFilter,视图解析器,multipartReesolver… )

  3. 默认包结构

    • 主程序所在包及其下面的所有子包里面的组件都会被默认扫描进来,无需以前的包扫描配置

    • 想要改变扫描路径,主程序下@SpringBootApplication(scanBasePackages="com.sutong")

      或者:@ComponentScan指定扫描路径

      @SpringBootApplication
      等同于
      @SpringBootConfiguration
      @EnableAutoConfiguration
      @ComponentScan("com.sutong")  // 默认是主程序所在的包以及以下
      
  4. 各种配置拥有默认值

    • 默认配置最终都是映射到某个类上,如:MultipartProperties
    • 配置文件的值最终会绑定某个类上,这个类会在容器中创建对象
  5. 按需加载所有自动配置项

    • 引入了哪些场景这个场景的自动配置才会开启
    • SpringBoot所有的自动配置功能都在 **spring-boot-autoconfigure **包里面

2.4 容器功能

组件添加

  1. @Configuration + @Bean

    配置类:

    /**
     * Configuration 告诉SpringBoot这是一个配置类,等同于配置文件
     *               该注解里面Spring5.2后多了个属性proxyBeanMethods(是否是代理Bean的方法),默认true
     *               如果是false的话,则不会生成代理对象
     * 
     * Full (全配置)       proxyBeanMethods = true    可以解决组件依赖问题,可以保证依赖的组件就是容器中的组件
     * Lite (轻量级配置)    proxyBeanMethods = false   不会检查方法返回的对象是否在容器中存在,就会快一点
     * 一般组件之间不怎么依赖的话都调成false模式
     */
    @Configuration(proxyBeanMethods = true)
    public class MyConfig {
    
        /**
         * 给容器添加组件,以方法名作为组件的id,返回类型就是组件类型,
         * 外部无论对这个注册方法调用多少次获取到都是IOC容器中发单实例对象(默认proxyBeanMethods = true)
         * @return 返回对象就是组件在容器中保存的实例,默认单实例
         */
        @Bean
        public User getUser() {
            return new User("Jack", 18);
        }
    }
    

    主程序类:

    @SpringBootApplication
    public class MainApp {
    
        public static void main(String[] args) {
            // 返回的就是ioc容器
            ConfigurableApplicationContext run = SpringApplication.run(MainApp.class, args);
    
            User u1 = run.getBean("getUser", User.class);
            User u2 = run.getBean("getUser", User.class);
            System.out.println(u1 == u2); // true
    
            MyConfig config = run.getBean(MyConfig.class); // 配置类也是个组件
            // 如果proxyBeanMethods = true,
            // 则获得的对象是MyConfig的代理对象,SpringBoot总会检查这个组件是否在容器中有,保持单实例,即下面输出true
            // 如果proxyBeanMethods = false,则不生成代理对象则下面输出fasle,但上面还是输出true的
            User user01 = config.getUser();
            User user02 = config.getUser();
            System.out.println(user01 == user02); 
        }
    }
    
  2. 以前的注解也行

    @Component@Controller@Service@Repository

    @ComponentScan 指定包扫描规则

  3. 导入组件

    @Import 写在组件的类上面(配置类也是一个组件)

    @Import({User.class, DBHelper.class})  // 给容器中自动创建出这两个类型的组件,默认组件的名字就是全类名!!
    @Configuration(proxyBeanMethods = true)
    public class MyConfig {
        
    }
    
  4. 条件装配

    @Conditional 满足Conditional指定的条件,则进行组件注入

    派生注解:

举例:

@Configuration(proxyBeanMethods = true)
public class MyConfig {
    @Bean
    public User getUser() {
        return new User("Jack", 18);
    }
    
    // 有getUser这个组件名的话,才进行注册getPerson这个组件!!
    // 也可以标注在类上面,对类下所有方法都生效
    @ConditionalOnBean(name = "getUser")  
    public Person getPerson() {
        return new Person("sutong");
    }
}

原生配置文件引入

@ImportResource 如果老公司还在用xml开发,里面有很多的bean,我们不用一个一个写成方法加上@Bean,只需要在一个配置类上面加上@ImportResource注解就行了

<!-- 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"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context
                           https://www.springframework.org/schema/context/spring-context.xsd">

    <bean id="haha" class="com.sutong.bean.User">
        <property name="name" value="Jack"></property>
        <property name="age" value="18"></property>
    </bean>
</beans>
@Configuration(proxyBeanMethods = true)
@ImportResource("classpath:bean.xml")  
public class MyConfig {
    
}

2.5 配置绑定

application.properties

mycar.brand=BYD
mycar.price=100000

@ConfigurationProperties

@Component                                  // 只有在容器中的组件才会拥有SpringBoot提供的强大功能!!!
@ConfigurationProperties(prefix = "mycar")  // prefix和配置文件中的那个前缀绑定
public class Car {
    private String brand;
    private Integer price;
    // 下面有get/set/有参无参构造等
}

下面获取组件,就会有值了!!


@EnableConfigurationProperties

@Configuration(proxyBeanMethods = true)
 // 在配置类上面写,开启Car的属性配置功能!并把这个Car自动注册到容器中(这种第三方的类使用较多)
 // 这个注册的组件的名字是: prefix的值-组件全类名,例如mycar-com.sutong.bean.Car
@EnableConfigurationProperties(Car.class)  
public class MyConfig {
    
}

@ConfigurationProperties(prefix = "mycar")  // 上面就是少了个@Component
public class Car {
    private String brand;
    private Integer price;
}

2.6 自动配置原理

底层

  1. 引导加载自动配置类 @SpringBootApplication

    • @SpringBootConfiguration -> @Configuration代表当前是一个配置类

    • @ComponentScan -> 指定扫描哪些

    • @EnableAutoConfiguration

      • @AutoConfigurationPackage

        利用Registrar给容器中导入一系列组件(将主程序所在包下的所有组件导入进来)

      • @Import(AutoConfigurationImportSelector.class)

        1、利用getAutoConfigurationEntry(..)给容器中批量导入一些组件
        2、调用configurations = getCandidateConfigurations(..) 获取到所有需要导入到容器中的配置类
        3、利用工厂加载得到所有的组件
        4、从META-INF/spring.factories位置来加载一个文件。
        	默认扫描我们当前系统里面所有META-INF/spring.factories位置的文件
            spring-boot-autoconfigure-2.3.4.RELEASE.jar包里面也有META-INF/spring.factories 
            这个文件里面写死了spring-boot一启动就要给容器中加载的所有配置类 -- 一共127
  2. 按需开启自动配置项

    虽然我们127个场景的所有自动配置启动的时候默认全部加载。即所有的xxxxAutoConfiguration类,按照条件装配规则(@Conditional),最终会按需配置!!!

    例如:@ConditionalOnClass({Advice.class})

  3. 修改默认配置

    spring-boot-starter-web场景下

    DispatcherServletAutoConfiguration.java

    // 上面还有注册DispatcherServlet的方法,所以不用我们手动配置了
    
    
    // 注册/规范MultipartResolver文件上传解析器
    @Bean
    @ConditionalOnBean({MultipartResolver.class})  // 如果容器中有MultipartResolver类型的组件
    @ConditionalOnMissingBean(name = {"multipartResolver"}) // 有这个类型但名字不是multipartResolver
    public MultipartResolver multipartResolver(MultipartResolver resolver) {
        // 给@Bean标注的方法传入了对象参数,这个参数的值就会从容器中找。
        // 直接把组件直接返回,并且把这个组件的名字改为multipartResolver,防止有些用户配置的文件上传解析器不符合规范
    	return resolver;  
    }
    
    // 等等...
    

    HttpEncodingAutoConfiguration.java

    // 注册编码过滤器(解决请求编码问题)
    @Bean
    @ConditionalOnMissingBean  // 如果我们没配置,就帮我们注册
    public CharacterEncodingFilter characterEncodingFilter() {
            CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
            filter.setEncoding(...);
            filter.setForceRequestEncoding(..);
            filter.setForceResponseEncoding(..);
            return filter;
    }
    
    // ...等等
    

    SpringBoot默认会在底层配好所有的组件。但是如果用户自己配置了以用户的优先!!!即大量的@ConditionalOnMissingBean


总结

  • SpringBoot先加载所有的自动配置类 xxxAutoConfiguration

  • 每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值。都是在xxxxProperties类里面拿,而xxxProperties和配置文件进行了绑定

  • 生效的配置类就会给容器中装配很多组件 (只要容器中有这些组件,相当于这些功能就有了)

  • 定制化配置

    • 用户直接自己@Bean替换底层的组件(或者实现接口)
    • 用户去看这个组件是获取的配置文件什么值就去修改。(更常用)

SpringBoot自动配置原理流程:

xxxAutoConfiguration -> 组件 -> xxxProperties类里面拿值 -> application.properties

例如修改字符编码:直接在application.properties文件里面加上:

server.servlet.encoding.charset=GBK


最佳实践

  1. 引入场景依赖

  2. 查看自动配置了哪些(选做)

    • 自己分析,引入场景对应的自动配置一般都生效了
    • 配置文件中debug=true开启自动配置报告。Negative(不生效的)\Positive(生效的)
  3. 是否需要修改配置项

    • 自己看源码(xxxxProperties类绑定了配置文件的哪些前缀)

    • 参照文档来修改配置项

  4. 自定义加入组件或者替换组件(用户配置优先)

  5. 自定义器 xxxCustomizer


2.7 开发小技巧

Lombok

1.依赖

2.安装IDEA插件

3.使用

SpringBoot帮我们也管理了
<lombok.version>1.18.12</lombok.version>

就不用写版本号了
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

Person.java

@Data // 提供get、set、equals、hashCode、canEqual、toString方法
@NoArgsConstructor
@AllArgsConstructor
public class Person {
    private String name;
}

// 还有个 @Log4j/@Slf4j : 注在类上,提供对应的 Logger 对象,变量名为 log

dev-tools

热部署,项目里面改了内容,按ctrl+f9(重新编译,java类变化才真正重新编译,静态页面也不用)就能实时生效

就是个自动重启!!检查文件变化改变重启规则

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <optional>true</optional>
</dependency>

Spring Initailizr

new project | Spring Initailizr | 按需要填写场景就行(我选择了WEB-Spring Web ,SQL-mybatis)

(版本选择好像只能选择最新的几个,我选的2.6.3,然后在改parent标签就行了)

1.自动导入一些依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.2</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

2.自动创建项目结构(一些没用的可以删掉)
在这里插入图片描述

3.自动编写好主配置类

@SpringBootApplication
public class Springboot02Application {
    public static void main(String[] args) {
        SpringApplication.run(Springboot02Application.class, args);
    }
}

3. 核心技术


3.1 配置文件

  1. properities

    同以前学习的写法一样

  2. YAML

    也是一种标记语言,非常适合用来以数据为中心的配置文件。(比xml更简洁,更轻量级)

    语法:

    • k: v 中间有空格 ,大小写敏感

    • 缩进表述层级关系(缩进的空格数不重要,相同层级左对其即可)

    • #表示注释

    • 字符串不需要加引号,如果要加,‘ ’和" " 表示字符串内容,表示转义与不转义

      如\n,单引号中作为字符串输出,双引号会换行(常规字符串加不加都一样)

    数据类型:

    • 字面量

    • 对象

      K: {k1:v2,k1:v2,k1:v2}
      #或者
      k: 
        k1: v2
        k1: v2
        k1: v2
      
    • 数组

      k: [v1,v2,v3]
      #或者
      k:
        - v1
        - v2
        - v3
      

    例子:

    @Component
    @ConfigurationProperties(prefix = "person")
    public class Person {
        private String userName;
    	// ....
    }
    
    person:
      userName: zhangsan
      boos: true
      age: 18
      birth: 2020/10/1  # 日期默认是'/'分割
      hobby:            # array List Set一样写法
        - 篮球
        - 足球
      score:            # Map<String, Object>写法
        math: 90
        english: 80
      score: {math:90,english:80}  # 或者这样
      pet:              # 对象
        name: 小狗
        weight: 30
      allPets:          # Map<String, List<String>>
        sick:
          - 小米
          - 小猫
        health:
          - 小熊
          - 小鹿
    

    application.propertiesapplication.yml都存在都生效(properties优先)

  3. 配置提示

    我们写的没有提示,加上下面的配置处理器就有了(记得重启一下)⭐

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>
    

    自动把驼峰命名法转化为带’-'的,都行!

    这个jar包之和我们开发相关,SpringBoot建议我们在打包的时候不打包上面这个东西!!

    (在以前的那个打包插件,加上configuration配置就行了)

    <build>
        <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>
    </build>
    

3.2 Web开发⭐


ⅠSpringMVC自动配置概览

Spring Boot 大多场景我们都无需自定义配置

  • 内容协商,视图解析器和BeanName视图解析器
  • 静态资源(包括webjars)
  • 自动注册 Converter,GenericConverter,Formatter
  • 支持 HttpMessageConverters (后来我们配合内容协商理解原理)
  • 自动注册 MessageCodesResolver (国际化用)(用的不多)
  • 静态 index.html页支持
  • 自定义 Favicon
  • 自动使用 ConfigurableWebBindingInitializer ,(DataBinder负责将请求数据绑定到JavaBean上)

Ⅱ 简单功能分析

① 静态资源访问
  1. 静态资源目录

    只要静态资源放在类路径下: /static (或者/public 或者/resources 或者/META-INF/resources)

    访问 : 当前项目根路径/ + 静态资源名

    原理: 静态映射/**。(即拦截所有请求)

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

    改变静态资源路径:

    spring:
       resources:
           #其他默认的静态资源就访问不到了,haha文件夹里面才能访问到,如果要写更多,分割继续写就行
           static-locations: [classpath:/haha/]  
    
  2. 静态资源访问前缀

    默认是无前缀的

    spring:
      mvc:
        static-path-pattern: /res/**
    

    当前项目 + static-path-pattern + 静态资源名 = 静态资源文件夹下找 (这个后面用的挺多)

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

  3. webjar (用的不多)

    webjar即把js, css打包成jar(依赖)

    自动映射webjar/**

    <!-- 例如:导入JQuery-->
    <dependency>
        <groupId>org.webjars</groupId>
        <artifactId>jquery</artifactId>
        <version>3.5.1</version>
    </dependency>
    

    访问地址:http://localhost:8080/webjars/jquery/3.5.1/jquery.js 后面地址要按照依赖里面的包路径

② 欢迎页支持
  1. 静态资源路径下 index.html

    配置这个访问前缀 static-path-pattern: /res/** 这个会导致welcome page功能失效!!

  2. Controller能处理"/index" 请求的

③ Favicon功能

图片命名为 favicon.ico 放在静态资源目录下即可。(favicon就是网站上面导航栏的小图标)

配置这个访问前缀 static-path-pattern: /res/** 这个会导致 Favicon 功能失效!!

因为浏览器会发送 /favicon.ico 请求获取到图标,整个session期间不再获取!

④ 静态资源配置原理
  • SpringMVC功能的自动配置类 WebMvcAutoConfiguration,经分析是生效的

  • 给容器中配了什么组件。

  • 配置文件的相关属性和xxx进行了绑定。

    WebMvcProperties == spring.mvc、ResourceProperties == spring.resources

    public class WebMvcAutoConfiguration {
        // 有 OrderedHiddenHttpMethodFilter
        
        @EnableConfigurationProperties({WebMvcProperties.class, ResourceProperties.class})
        public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {
            // 有一个有参构造器,参数全部从容器里面拿
            
            // 资源处理的默认规则
            public void addResourceHandlers(ResourceHandlerRegistry registry) {...}
        }
        
        
        public static class EnableWebMvcConfiguration ..{   
            // 配置欢迎页的方法
            @Bean
            public WelcomePageHandlerMapping welcomePageHandlerMapping(..) {...}
            
            // WelcomePageHandlerMapping构造函数里面:
            WelcomePageHandlerMapping(...) {
    		if (welcomePage.isPresent() && "/**".equals(staticPathPattern)) {
                // 要用欢迎页功能,必须是/**,所以配置访问前缀欢迎页会失效!!
    			logger.info("Adding welcome page: " + welcomePage.get());
    			setRootViewName("forward:index.html");
    		}
    		else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
                // 调用Controller  /index
    			logger.info("Adding welcome page template: index");
    			setRootViewName("index");
    		}
        }
    }
    

一些设置:

spring:
    mvc:
       static-path-pattern: /res/**
    resources:
       cache:
         period: 1000        # 1000 静态资源浏览器缓存1000秒
       add-mappings: false   # 禁用所有静态资源访问

Ⅲ 请求参数处理

① 请求映射
  1. Rest风格(使用HTTP请求方式动词来表示对资源的操作)

    核心Filter -> HiddenHttpMethodFilter

    用法: 1.表单method=post,隐藏域 _method=put 2. SpringBoot中手动开启

    spring:
      mvc:
        hiddenmethod:
          filter:
            enabled: true  # 默认是false,true底层才会注册HiddenHttpMethodFilter组件
    
    // 底层注册Filter的方法:
    @Bean
    @ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
    @ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", 
                           name = "enabled", 
                           matchIfMissing = false)
    public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
        return new OrderedHiddenHttpMethodFilter();
    }
    

    如果使用Rest使用客户端工具,如PostMan直接发送Put、delete等方式的请求,无需Filter进行转化。

  2. 请求映射原理(了解)

    SpringMVC功能分析都从 DispatcherServlet -> doDispatch()

    RequestMappingHandlerMapping:保存了所有@RequestMapping 和handler的映射规则。

    总结:所有的请求映射都在HandlerMappings中。

    • SpringBoot自动配置欢迎页的 WelcomePageHandlerMapping ,访问 “/” 能访问到index.html

    • SpringBoot自动配置了默认的 RequestMappingHandlerMapping

    • 请求进来,遍历handlerMappings集合,挨个尝试所有的HandlerMapping看是否有请求信息。

      • 如果有就找到这个请求对应的handler
      • 如果没有就是下一个 HandlerMapping
    • 我们需要一些自定义的映射处理,我们也可以自己给容器中放HandlerMapping,自定义 HandlerMapping

② 普通参数与基本注解
  1. 注解

    @PathVariable(路径变量)

    @RequestHeadr(请求头)

    @RequestParam(获取请求参数)

    @RequestBody (获取请求体值,POST才有)

    @CookieValue(获取Cookie值)

    @RequestAttribute(获取请求域中的数据)

    @ModelAttribute (不太常用,过了)

    @Controller
    public class TestController {
        
        @RequestMapping("/car/{id}/owner/{username}")
        @ResponseBody
        public String getCar(@PathVariable("id") Integer id,
                             @PathVariable Map<String, String> pv,             
                             // Map包装所有的,k-v必须都是String类型
    
                             @RequestHeader("User-Agent") String userAgent,    
                             // 这个也可以标注Map,获取所有请求头信息
    
                             @RequestParam("hobby") String bobby,              
                             // 获取k=v&k=v,也可以标注Map,所有请求参数都放入Map
    
                             @RequestBody String reqBody,
    
                             @CookieValue("_ga") String _ga,                   
                             // 也可以标注Map,所有的Cookie信息,也可标注整个Cookie,获取Cookie对象
                             @CookieValue("_ga") Cookie cookie) {
            System.out.println("id:" + id);
            System.out.println("id:" + pv.get("id"));
            return "success";
        }
        
        @RequestMapping("/goto")
        public String gotoPage(Model model) {
            model.addAttribute("msg", "我是请求域里面的信息");
            return "forward:/success";  // 转化到/success请求
        }
    
        @RequestMapping("/success")
        @ResponseBody        // @RequestAttribute获取请求域的数据,或者用原生的Servlet (一般都是页面去获取数据)
        public String success(@RequestAttribute("msg") String msg) {  
            return msg;
        }
    }
    

    @MatrixVariable(矩阵变量)

    /cars/{path}?k1=v1&k2=v2 这种写法称为queryString,查询字符串

    /cars/path;k1=v1;k2=v2,v3,v4 这种称为 矩阵变量!!

    如果页面禁用了Cookie,怎么获取Session中数据??

    url重写:/abc;jsessionid=xxx 把cookie的值使用矩阵变量的方式进行传递,可以区分普通的请求参数

    /boos/1/2 这种怎么加矩阵变量?

    /boss/1;age=20/2;age=30 (分号前代表访问路径,后代表矩阵变量)

    @Controller
    public class TestController {
        //  /matrix/cars;low=34;brand=v2,v3,v4
        //  SpringBoot默认禁用了矩阵变量,需要手动开启
        //  原理:对于路径的处理,底层用UrlPathHelper进行解析,
        //     	 其中属性removeSemicolonContent(移除分号内容)支持矩阵变量的,默认是true
        //  矩阵变量必须有url路径变量才能和解析!!!
        @RequestMapping("/matrix/{path}")
        @ResponseBody
        public String testMatrix01(@MatrixVariable("low") Integer low,
                                   @MatrixVariable("brand") List<String> brand,
                                   @PathVariable("path") String path) {
            System.out.println(path); // cars
            return low + " " + brand.toString();
        }
    
    
        // /boss/1;age=20/2;age=30  矩阵变量重名情况
        @RequestMapping("/boss/{bossId}/{empId}")
        @ResponseBody
        public String testMatrix02(@MatrixVariable(value = "age", pathVar = "bossId") Integer bossAge,
                                   @MatrixVariable(value = "age", pathVar = "empId") Integer empAge) {
            return bossAge + " " + empAge;  // 20 30
        }
    }
    
    
    
    // 开启矩阵变量(好像用的不多!!挺麻烦的)
    @Configuration(proxyBeanMethods = false)
    public class WebConfig implements WebMvcConfigurer {
        // 可以自定义映射规则!!(或者使用@Bean注入一个WebMvcConfigurer)
        @Override
        public void configurePathMatch(PathMatchConfigurer configurer) {
            UrlPathHelper urlPathHelper = new UrlPathHelper();
            urlPathHelper.setRemoveSemicolonContent(false);
            configurer.setUrlPathHelper(urlPathHelper);
        }
        // 重写addFormatter方法,自可以定义类型转化器 Converter,然后configurer.addConverter()
        
        
        // ----------------------------------------------------------------------------
        // 往容器放入WebMvcConfigurer组件也行,就不用实现WebMvcConfigurer接口了!!
        @Bean
        public WebMvcConfigurer webMvcConfigurer() {
            return new WebMvcConfigurer() {
                @Override
                public void configurePathMatch(PathMatchConfigurer configurer) {
                    UrlPathHelper urlPathHelper = new UrlPathHelper();
                    urlPathHelper.setRemoveSemicolonContent(false);
                    configurer.setUrlPathHelper(urlPathHelper);
                }
            };
        }
    }
    

    上面的每个注解都对应一个方法参数解析器!!,xxxMethodArgumentResolver

  2. Servlet API

    WebRequest、ServletRequestMultipartRequestHttpSession、javax.servlet.http.PushBuilder、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId

    底层使用 ServletRequestMethodArgumentResolver方法参数解析器进行解析上面的所有类型参数!!

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        Class<?> paramType = parameter.getParameterType();
        return (WebRequest.class.isAssignableFrom(paramType) ||
                ServletRequest.class.isAssignableFrom(paramType) ||
                MultipartRequest.class.isAssignableFrom(paramType) ||
                HttpSession.class.isAssignableFrom(paramType) ||
                (pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) ||
                Principal.class.isAssignableFrom(paramType) ||
                InputStream.class.isAssignableFrom(paramType) ||
                Reader.class.isAssignableFrom(paramType) ||
                HttpMethod.class == paramType ||
                Locale.class == paramType ||
                TimeZone.class == paramType ||
                ZoneId.class == paramType);
    }
    
  3. 复杂参数

    Map、Model(map、model里面的数据会被放在request的请求域 request.setAttribute)、

    RedirectAttributes( 重定向携带数据)、

    ServletResponse(response)、

    Errors/BindingResult、SessionStatus、UriComponentsBuilder、ServletUriComponentsBuilder

  4. 自定义对象参数

    可以自动类型转换与格式化,可以级联封装。

    /**
     *     姓名: <input name="userName"/> <br/>
     *     年龄: <input name="age"/> <br/>
     *     生日: <input name="birth"/> <br/>
     *     宠物姓名:<input name="pet.name"/><br/>
     *     宠物年龄:<input name="pet.age"/>
     */
    @Data
    public class Person {
        private String userName;
        private Integer age;
        private Date birth;
        private Pet pet;
    }
    
    @Data
    public class Pet {
        private String name;
        private String age;
    }
    

    使用ServletModelAttributeMethodProcessor 参数处理器进行解析

    之间利用WebDataBinder中的124个Converter类型转化器,进行类型转换,将请求的数据转化为我们JavaBean属性的类型。

    我们也可以自定义Converter


Ⅳ 数据响应与内容协商

① 响应JSON

使用jackson.jar + @ResponseBoby

引入web场景会自动引入jackson场景,自动返回json数据。

原理:是使用底层的各种返回值解析器进行处理 xxxReturnValueHandler, xxxMethodProcecssor

// 支持的的返回值类型,不止这些
ModelAndView
Model
View
ResponseEntity 
ResponseBodyEmitter
StreamingResponseBody
HttpEntity
HttpHeaders
Callable
DeferredResult
ListenableFuture
CompletionStage
WebAsyncTask
方法标注    @ModelAttribute 且返回值是对象类型的
方法/类标注 @ResponseBody 注解,返回数据则使用 RequestResponseBodyMethodProcessor处理器处理!!!

标记@ResponseBody的处理

( 先RequestResponseBodyMethodProcessor处理器处理,最后利用xxxHttpMessageConverter消息转化器进行处理 )

将数据写为json流程:

  • 内容协商(浏览器默认会以请求头的方式告诉服务器他能接受什么样的内容类型)

  • 服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据,

  • SpringMVC 会挨个遍历所有容器底层所有的 HttpMessageConverter ,看谁能处理

    • 得到MappingJackson2HttpMessageConverte可以将对象写为json
    • 利用MappingJackson2HttpMessageConverter将对象转为json再写出去。

处理返回值流程:先找到能处理对应返回值的处理器,然后再找具体的消息转化器。

② 内容协商

内容协商:根据客户端接受能力不同,可能返回不同类型的媒体数据。

若客户端无法解析服务端返回的内容,即媒体类型未匹配,那么响应406

例如根据客户端接受能力返回jsonxml(xml这个要引入依赖):

<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
</dependency>

看请求头中 Accept(客户端能接受的类型):

text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9

可以看出application/xml优先级高,如果服务器支持xml,则优先返回xml类型的媒体数据。(q=0.9)

*/* 代表可以接受任意类型,包括application/json (q=0.8)

可以使用PostMan软件,改变Accept请求头字段就能看到不同的效果!!

这个就是SpringMVC底层内容协商的结果。

底层:先根据方法返回值以及注解, 找到符合的xxxHttpMessageConverter(服务器能力,可以多个,例如application/json,application/xml),然后按优先级遍历用户浏览器支持的所有类型(用户需要的),和服务器能产出的媒体类型去匹配(两个for循环,这个匹配可以说成是个最佳匹配),类型匹配成功就保存下来,然后最终选择一个最优的 媒体类型 进行转化。(第一个!?)

下面还有个匹配,再去找一下能处理这个 最优媒体类型 的消息转化器,最终用这个 消息转化器 进行转化对应的媒体类型,返回。

使用PostMan软件可以轻易改变Accept请求头,而用浏览器发送请求不能改变请求头,为了方便,可以开启浏览器参数方式内容协商功能(基于请求参数的内容协商):

spring:
  mvc:
    contentnegotiation:
      favor-parameter: true  # 开启请求参数的内容协商

如果xml,json都支持,则浏览器默认是xml,开启基于请求参数的内容协商后,加上format参数就可以自定义需要的媒体类型了,

例如:http://localhost:8080/test/person?format=json (format参数值底层只有xml,json两个值,我们可以在自定义)

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

Parameter策略(ParameterContentNegotiationStrategy)优先于Header策略(HeaderContentNegotiationStrategy)

总结:

实现多协议数据兼容。json、xml

0、标注@ResponseBody 响应数据出去 调用 RequestResponseBodyMethodProcessor 处理

1、XxxProcessor 处理方法返回值。通过 XxxHttpMessageConverter 处理

2、所有 HttpMessageConverter 合起来可以支持各种媒体类型数据的操作(读、写)

3、内容协商找到最终的 messageConverter

③ 自定义 HttpMessageConverter

例如:

  • 如果浏览器发送请求返回xml [application/xml] -> MappingJackson2XmlHttpMessageConverter
  • 如果是ajax请求返回json [application/json] -> MappingJackson2HttpMessageConverter
  • 如果是 苏瞳app 发请求返回自定义协议数据 [application/x-sutong] -> XxxHttpMessageConverter

自定义:

// 自定义的 HttpMessageConverter,处理Person类型的响应数据格式!
public class SutongHttpMessageConverter implements HttpMessageConverter<Person> {

    // 是否支持读,读:是否能解析请求来的数据
    @Override
    public boolean canRead(Class<?> aClass, MediaType mediaType) {
        return false;
    }

    // 是否支持写,写:把Person对象是否转化为对应的协议数据
    @Override
    public boolean canWrite(Class<?> aClass, MediaType mediaType) {
        return aClass.isAssignableFrom(Person.class);
    }

    // 底层有个重要的环节: 服务器要统计所有MessageConverter都能处理哪些内容类型
    // 即application/x-sutong
    @Override
    public List<MediaType> getSupportedMediaTypes() {
        return MediaType.parseMediaTypes("application/x-sutong");
    }

    // 自定义的协议读入,我们上面禁用了
    @Override
    public Person read(Class<? extends Person> aClass, HttpInputMessage httpInputMessage) 
        throws IOException, HttpMessageNotReadableException {
        return null;
    }

    // 自定义的协议写出
    @Override
    public void write(Person person, MediaType mediaType, HttpOutputMessage httpOutputMessage) 
        throws IOException, HttpMessageNotWritableException {
        String data = person.getUserName() + ";" + person.getAge() + "(我是自定义的)"; // 值之间用分号分割

        // 写出去
        OutputStream outputStream = httpOutputMessage.getBody();
        outputStream.write(data.getBytes("GBK"));  // 浏览器编码是GBK
    }
}

添加:

@Configuration(proxyBeanMethods = false)
public class WebConfig implements WebMvcConfigurer {
    // 添加我们自定义的消息处理器
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(new SutongHttpMessageConverter());
    }
}

上面这样测试用PostMan发能行,即把请求头改为Accept:appplication/x-sutong

浏览器中使用 参数的内容协商 不行!还要配置一些东西,即自定义参数策略(因为Parameter策略默认只支持xml和json)

自定义参数策略:

@Configuration(proxyBeanMethods = false)
public class WebConfig implements WebMvcConfigurer {
	// 添加我们自定义的消息处理器..
    
    // 自定义参数策略
    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        // 指定解析哪些参数对应的哪些配体内容
        Map<String, MediaType> mediaTypes = new HashMap<>();
        mediaTypes.put("json", MediaType.APPLICATION_JSON);
        mediaTypes.put("xml", MediaType.APPLICATION_XML);
        mediaTypes.put("x-sutong", MediaType.parseMediaType("application/x-sutong"));

        // 需要传入一个Map,即参数映射,json -> application/json
        ParameterContentNegotiationStrategy p = new ParameterContentNegotiationStrategy(mediaTypes);
        // p.setParameterName("ff");  // 这里还可以改变“format”参数名
        // 这个如果不添加默认的请求头策略就没了,就剩下我们自定义的参数协商策略了!
        HeaderContentNegotiationStrategy h = new HeaderContentNegotiationStrategy(); 
        configurer.strategies(Arrays.asList(p, h));
    }
}

注意:在将来我们需要自定义组件的时候,我们添加的功能会覆盖默认的很多功能!导致一些默认的一些功能失效,要小心点。


Ⅴ 视图解析与模板引擎

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

原因:SpringBoot工程的打包结果是一个jar包,是压缩包,JSP不支持在压缩包中被编译运行,所以SpringBoot默认不支持JSP。

① 视图解析
  1. 学习-模板引擎-Thymeleaf

    是现代化、服务端Java模板引擎

    表达式:

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

    字面量(’ '单引号表示字符串…),文本操作(+字符串拼接…),数学运算,布尔运算,比较运算,条件运算

    设置属性值:

    <!-- 页面没有经过服务端渲染,或页面无法获取到数据的情况下,页面仍然会保留标签的默认值用以展示。
    	 经过Thymeleaf渲染后的属性xxx的值会覆盖原生的属性值。-->
    <input type="submit" value="Subscribe!" th:value="${subscribe.submit}"/>
    <form action="subscribe.html" th:action="@{/subscribe}">
        
    <!-- 行内写法,就是不在任何标签里面-->
    [[${session.user.name}]]
    

    循环,判断:

    <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>
    <!-- 还可以这样:iterStat是当前遍历元素的状态,有下标iterStat.index,计数iterStat.count等,是不是第一个...-->
    <tr th:each="prod,iterStat : ${prods}"></tr>
    
    <a 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>
    
    <a th:href="@{'/emp/'+${emp.id}}">b</a>
    
  2. Thymeleaf使用

    • Thymeleaf的场景依赖(引入starter):

      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-thymeleaf</artifactId>
      </dependency>
      

      引入场景SpringBoot就自动配置好了Thymeleaf,底层的 ThymeleafAutoConfiguration

      @Configuration(proxyBeanMethods = false)
      @EnableConfigurationProperties(ThymeleafProperties.class)
      @ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
      @AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
      public class ThymeleafAutoConfiguration {
          // 所有thymeleaf的配置值都在 ThymeleafProperties
          // 配好了 SpringTemplateEngine 
          // 配好了 ThymeleafViewResolver 
      }
      
    • 配置视图解析器的前后缀(有默认的)等:

      public static final String DEFAULT_PREFIX = "classpath:/templates/";
      public static final String DEFAULT_SUFFIX = ".html";  // xxx.html
      

      自定义:

      spring:
        thymeleaf:
          prefix: classpath:/templates/table
          suffix: .html
      

      一般也不用自定义,跳转的时候使用默认的前缀,然后再加上中间的路径就行了 return "table/basic_table";

    • 开发页面

      开发页面前,页面上需要加上名称空间 <html lang="en" xmlns:th="http://www.thymeleaf.org">

      <!DOCTYPE html>
      <html lang="en" xmlns:th="http://www.thymeleaf.org">
      <head>
          <meta charset="UTF-8">
          <title>success</title>
      </head>
      <body>
          <h1 th:text="${msg}">哈哈</h1>            <!-- 直接写key,代表从Request域取数据-->
          <a th:href="@{/test/person}">链接</a>     <!-- 绝对路径下会帮我们拼上项目路径-->
      </body>
      </html>
      
      # 添加项目路径:
      server:
          servlet:
            context-path: /springboot02  # 项目路径,这样的话所有的请求必须在8080后面加上个 /springboot02
      

      这样的话 @{/test/person} 会帮我们在前面自动加上项目路径! 只会针对绝对路径自动拼接

    • 抽取公共部分

      使用方法:

      footer.html

      <footer th:fragment="copy">
            &copy; 2011 The Good Thymes Virtual Grocery
      </footer>
      

      其他地方引入:

      <body>
             ... 或者使用~{footer :: copy}
            <div th:insert="footer :: copy"></div>   <!--copy公共部分插入当前div中-->
      
            <div th:replace ="footer :: copy"></div>  <!--copy公共部分替换掉当前div-->
      
            <div th:include="footer :: copy"></div>  <!--当前div的内容替换为copy公共部分的内容,3.0后不推荐-->
      </body>
      

      三种的区别(基本见名知意):

      <body>
            ...
            <div>
                 <footer>
                     &copy; 2011 The Good Thymes Virtual Grocery
                 </footer>
            </div>
      
            <footer>
                 &copy; 2011 The Good Thymes Virtual Grocery
            </footer>
      
            <div>
                 &copy; 2011 The Good Thymes Virtual Grocery
            </div>
      </body>
      

      common.html

      <!DOCTYPE html>
      <html lang="en" xmlns:th="http://www.thymeleaf.org">
      <head th:fragment="common_header">
          <meta charset="UTF-8">
          <title>公共信息</title>
          <!-- 公共的link标签,引入css代码的-->
          <link/>
          <link/>
      </head>
      <body>
      
          <h1 th:fragment="header_menu">头部导航</h1>
          <h1 th:fragment="footer">页脚</h1>
      
          <!-- id也行和上面一样都可以使用th:insert/th:/replace/th:include来引用-->
          <div id="common_script">
              <!-- 公共的script标签,引入JS代码的-->
              <script></script>
              <script></script>
          </div>
      </body>
      </html>
      

      success.html

      <!DOCTYPE html>
      <html lang="en" xmlns:th="http://www.thymeleaf.org">
      <head>
          <meta charset="UTF-8">
          <title>success</title>
      
          <!-- 引入公共的common_header-->
          <div th:include="common :: common_header"></div>  <!-- common就是加上前后缀的页面-->
      </head>
      <body>
          <div th:replace="common :: header_menu"></div>  
          <div th:replace="common :: footer"></div>
          
          <!-- 引入的id的,相当于选择器,要加上#-->
          <div th:replace="common :: #common_script"></div>  <!-- 那种引入都行-->
      </body>
      </html>
      
② 视图解析原理流程

(了解)

  • 通过ViewNameMethodReturnValueHandler 处理返回的 视图名称字符串(视图地址)

  • 处理目标方法处理的过程中,所有数据都会被放在 ModelAndViewContainer 里面,包括数据和视图地址

  • 任何 目标方法执行完成 以后都会返回 ModelAndView(数据和视图地址)

  • 然后调用 processDispatchResult() 处理派发结果(页面该如何响应) -> render(mv, request, response) 页面渲染逻辑

    • 首先 使用的所有视图解析器(xxxViewResolver)尝试是否能根据当前目标方法返回值得到View对象

      View 对象,里面定义了页面的渲染逻辑)

    • 遍历后,最终得到了通过Thymeleaf 创建RedirectView对象,view = new RedirectView(..)

      ( 因为目标方法返回值是"redirect:/main"

    • ContentNegotiationViewResolver 内容协商解析器,里面包含了上面遍历的所有的视图解析器,内部还是利用下面所有视图解析器得到视图对象

    • view.render(..) 视图对象调用自定义的render逻辑进行页面渲染工作,RedirectView的render方法底层先获取目标url地址,再判断是否重定向携带数据,然后调用Servlet的原生方法,即response.sendRedirect(encodedURL)

  • 返回值以 forward: 开始: 底层会 view = new InternalResourceView(forwardUrl) ,最终调用Servlet原生转发方法:request.getRequestDispatcher(path).forward(request, response)

  • 返回值以 redirect: 开始: view = new RedirectView() render就是重定向,最终调用Servlet原生重定向方法:response.sendRedirect(encodedURL)

  • 返回值是普通字符串view = new ThymeleafView(),之后调用Thymeleaf的模板引擎的process方法进行页面渲染,然后用调用一大堆writer输出html

有能力还可以自定义视图解析器+自定义视图


Ⅵ 拦截器

① HandlerInterceptor

静态资源访问前缀:

spring:
  mvc:
    static-path-pattern: /static-res/**  # 静态资源访问前缀

拦截器:

/**
 * 登录检查:
 * 1.配置要拦截的哪些请求
 * 2.把拦截器配置放到容器中
 */
public class LoginInterceptor implements HandlerInterceptor {

    // 目标方法执行前 - preHandle
    @Override
    public boolean preHandle(HttpServletRequest request, 
                             HttpServletResponse response, Object handler) throws Exception {
        HttpSession session = request.getSession();
        Object user = session.getAttribute("user");  // 因为登录成功后会把用户信息放到session域
        if (user != null) {
            return true; // 放行
        }

        // 拦截 -> 未登录 -> 跳转到登录页面(转发,重定向都行)
        response.sendRedirect("/login");
        return false;
    }

    // 目标方法执行后 - postHandle
    // 视图渲染后 - afterCompletion
}

配置拦截器:

@Configuration(proxyBeanMethods = false)
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/**")                                  // 拦截所有请求
                .excludePathPatterns("/", "login", "/static-res/**");    // 放行主页和登录页面和静态资源
    }
}
② 拦截器原理

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

2、先来 顺序执行 所有拦截器的 preHandle方法

  • 1、如果当前拦截器preHandler()返回为true,则继续执行下一个拦截器的preHandle()
  • 2、如果当前拦截器返回为false,直接去 倒序执行 所有已经执行了的拦截器的 afterCompletion()

3、如果任何一个拦截器返回false。直接return跳出不执行目标方法。

4、如果所有拦截器都返回True,执行目标方法

5、目标方法执行完后,倒序执行 所有拦截器的postHandle()方法。

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

7、如果没有异常且没有被拦截,页面成功渲染完成以后,也会 倒序触发 所有拦截器的afterCompletion()

图解:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rvNUGF6d-1668670777080)(SpringBoot2-第一季.assets/image-20220214101745717.png)]


Ⅶ 文件上传

① 测试

修改文件上传解析器的参数:

spring:
  servlet:
    multipart:
      max-file-size: 10MB      # 单个文件最大大小,默认1MB
      max-request-size: 100MB  # 整个请求的大小,默认10MB

页面:

<form th:action="@{/upload}" method="post" enctype="multipart/form-data">
    用户名:<input type="text" name="username"/> <br>
    头像:<input type="file" name="header"/>  <br/>
    生活照:<input type="file" name="photos" multiple/> <br/>  <!-- 可上传多文件-->
    <input type="submit" value="提交"/>
</form>

控制器:

@Controller
public class FileController {
    // 这个写的简陋,看详细的去看SpringMVC的笔记!!!!
    @PostMapping("/upload")
    public String upload(@RequestParam("username") String username,
                         @RequestPart("header") MultipartFile header,
                         @RequestPart("photos") MultipartFile[] photos) throws IOException {

        if (!header.isEmpty()) {
            header.transferTo(new File("D:\\Learning\\" + header.getOriginalFilename()));
        }
        for (MultipartFile photo : photos) {
            if (!photo.isEmpty()) {
                header.transferTo(new File("D:\\Learning\\" + header.getOriginalFilename()));
            }
        }
        return "success";
    }
}
② 原理
  1. 文件上传自动配置类 -> MultipartAutoConfiguration -> MultipartProperties

  2. 类里面 配置好了 StandardServletMultipartResolver 组件 (文件上传解析器)

    @Bean(name = {"multipartResolver"})
    @ConditionalOnMissingBean({MultipartResolver.class})
    public StandardServletMultipartResolver multipartResolver() {
        StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver();
        multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily());
        return multipartResolver;
    }
    
  3. 原理步骤:

    • 请求进来使用文件上传解析器XxxMultipartResolver使用 isMultipart判断是否是多端数据,是则封装Request

      (封装返回XxxMultipartHttpServletRequest类型)

    • 使用参数解析器 RequestPartMethodArgumentResolver 来解析请求中的文件内容封装成MultipartFile

    • 将request文件信息封装为一个Map,MultiValueMap<String, MultipartFile>

    • @RequestPart的value,当作key(或者形参名),把Map中对应的值赋给形参就行了

    • 最终transferTo() 里面使用FileCopyUtils 进行文件流的拷贝


Ⅷ 异常处理

① 默认规则
  • 默认情况下,Spring Boot提供/error处理所有错误的映射

  • 对于机器客户端,它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息,

    对于浏览器客户端,响应一个whitelabel错误视图,以HTML格式呈现相同的数据

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

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

  • static/error/ 或者 templates/error/ 下的404,5xx页面会被自动解析!

    (如果命名为5xx所有的5开头的所有错误状态码就会跳转这个页面)

错误的JSON信息:

所以我们可以在错误页面显示这些信息:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
           <meta charset="UTF-8">
           <title>404</title>
    </head>
    <body>
           页面找不到了
           <h3 th:text="${message}"></h3> <br>  <!-- 错误的标题-->
           <p th:text="${trace}"></p>           <!-- 错误的堆栈信息-->
    </body>
</html>
② 异常处理原理
  1. 异常自动配置类 -> ErrorMvcAutoConfiguration -> ServerProperties, WebMvcProperties

  2. 类里面自动配置了DefaultErrorAttributes组件 id:errorAttributes

    @Bean
    @ConditionalOnMissingBean(value = {ErrorAttributes.class}, search = SearchStrategy.CURRENT)
    public DefaultErrorAttributes errorAttributes() {
        return new DefaultErrorAttributes();
    }
    
    // DefaultErrorAttributes实现了这两个接口!也是个异常解析器
    public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver {}
    

    这个组件是往请求域放错误信息的。(定义了错误页面可以包含哪些数据)

    看底层,发现key可以是:exception,trace,message,errors,timestamp,status,path…

  3. 类里面自动配置了BasicErrorController组件 id: basicErrorController

    @Bean
    @ConditionalOnMissingBean(value = {ErrorController.class}, search = SearchStrategy.CURRENT)
    public BasicErrorController basicErrorController(...) {
        return new BasicErrorController(...);
    }
    

    BasicErrorController控制器里面默认处理默认 /error/** 路径的请求。(也可以修改配置: server.error.path: /error

    这里面要不响应一个html ,即new ModelAndView("error"), model,要不响应一个json,即new ResponseEntity(...)

    • 响应html页面

      @RequestMapping(produces = {"text/html"})
      public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
          ...
          return modelAndView != null ? modelAndView : new ModelAndView("error", model);
      }
      

      还会默认往容器放一个组件 View组件 -> id: error(响应默认错误页)

      @Bean(name = {"error"})
      @ConditionalOnMissingBean(name = {"error"})
      public View defaultErrorView() {
          // defaultErrorView是个StaticView类型里面append了默认白页的错误页面的html,就是个SpringBoot默认错误页
          return this.defaultErrorView;  
      }
      

      为了解析这个视图还配置了一个视图解析器 BeanNameViewResolver(视图解析器),按照返回的视图名(即上面的"error"视图名)作为组件的id去容器中找 View 对象。

    • 响应json数据

      @RequestMapping
      public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
          ...
          return new ResponseEntity(body, status);
      }
      
  4. 类里面自动配置了DefaultErrorViewResolver组件 (错误视图解析器) id: conventionErrorViewResolver

    如果发生错误,会以HTTP的状态码作为视图页地址(即下面的形参viewName),加上 error/ 前缀找到真正的页面,即error/404、5xx.html。这就解释了为什么我们把错误页面放到 static/error/ 或者templates/error/ 文件下会自动显示!!

    // DefaultErrorViewResolver里面的方法:
    private ModelAndView resolve(String viewName, Map<String, Object> model) {
        String errorViewName = "error/" + viewName;  
        ...
        return provider != null ? new ModelAndView(errorViewName, model) 
            				    : this.resolveResource(errorViewName, model);
    }
    

    总结:如果错误页面的可取值不够,就自定义DefaultErrorAttributes组件

    如果想要修改错误跳转逻辑,修改默认的白页和json,就自定义BasicErrorController组件

    如果不想把错误状态码对应的页面放到上面默认的文件夹下,就自定义DefaultErrorViewResolver组件

③ 异常处理流程
  1. 执行目标方法,目标方法运行期间有任何异常都会被catch、而且标志当前请求结束。

    并且用 dispatchException封装异常,即用它来保存异常

  2. 进入视图解析流程(页面渲染…)当目标方法出现异常时,返回的ModelAndView为null,即下面的mv = null

    processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

  3. 处理目标方法发送的异常,重新为mv赋值

    mv = processHandlerException(…)

    • 遍历所有的 XxxHandlerExceptionResolver,看谁能处理当前异常(HandlerExceptionResolver处理器异常解析器)
    • 系统默认有四个,DefaultErrorAttributes, ExceptionHandlerExceptionResolver, ResponseStatusExceptionResolver, DefaultHandlerExceptionResolver
    • 遍历第一个,只往请求域放了错误信息,然后返回了null(继续遍历),并没有返回ModelAndView对象
    • 遍历完默认的,发现没有任何人能处理当前异常,所以异常会被抛出!

    如果没有任何人能处理,最终底层就会再次发送 /error 请求(转发,这时请求域就已经保存了错误信息了),会被底层的**BasicErrorController**控制器处理(浏览器则响应html)

    • 解析错误视图,遍历所有的 XxxErrorViewResolver 看谁能解析。(默认就一个DefaultErrorViewResolver
    • 默认的 DefaultErrorViewResolver ,作用是把响应状态码作为错误页的地址,error/500.html
    • 模板引擎最终响应这个页面 error/500.html
  4. 根据mv信息跳转对应的错误页面

④ 定制错误处理逻辑
  1. 自定义错误页

    templates/error/404.html,有精确的错误状态码页面就匹配精确,没有就找 4xx.html,都没有就触发白页

  2. @ControllerAdvice + @ExceptionHandler 处理全局异常(以后开发推荐这种)

    /**
     * 处理整个WEB的Controller异常
     */
    @ControllerAdvice
    public class GlobalException {
        @ExceptionHandler({ArithmeticException.class, NullPointerException.class})
        public String handlerArithAndNullException(Exception e) {
            return "login";  // 视图地址
        } 
    }
    
    // 这种配置上面的默认异常解析器,就有可以处理的了,
    // 即 ExceptionHandlerExceptionResolver!!!底层就不用重新发送/error请求了!!!
    
  3. @ResponseStatus + 自定义异常

    // 自定义异常  FORBIDDEN -> 403 出现就取状态码对应的页面
    @ResponseStatus(value = HttpStatus.FORBIDDEN, reason = "用户数量太多")  
    public class UserTooManyException extends RuntimeException {
        public UserTooManyException() { }
        public UserTooManyException(String message) {
            super(message);
        }
    }
    
    // 控制器
    @Controller
    public class ViewController {
        @RequestMapping("/login")
        public String login(User user) {
            List<User> users = Arrays.asList(new User(), new User(), user);
            if (users.size() > 2) {
                throw new UserTooManyException();
            }
            return "success";
        }
    }
    
    // 这种配置上面的默认异常解析器,就有可以处理的了,
    // 即 ResponseStatusExceptionResolver!是把@ResponseStatus的信息保存到statusCode,resolvedReason, 
    // 底层再调用response.sendError(statusCode, resolvedReason),
    // 即 tomcat发送的/error请求 (这次请求结束),BasicErrorController控制器进行处理,
    // DefaultErrorViewResolver进行解析,状态码就是视图名称,跳转错误页面。(再处理不了取Tomcat的默认处理也,蓝白的那种)
    
  4. Spring底层的异常,如 参数类型转换异常

    这种的话则使用DefaultHandlerExceptionResolver 异常解析器进行处理!(处理SpringMVC底层的异常)

    底层也会调用 response.sendError(statusCode, ex.getMessage()) , 即 tomcat发送的 /error 请求。

  5. 自定义实现 HandlerExceptionResolver 处理异常 (这个我们基本不自定义!)

    // 自定义的异常解析器
    // 我们配置的异常解析器,默认配置到最后一个,有时候轮不到我们自定义的处理就结束了,需要加上优先级,数越小优先级越高
    @Order(Ordered.HIGHEST_PRECEDENCE)  
    @Component
    public class CustomerHandlerExceptionResolver implements HandlerExceptionResolver {
        @Override
        public ModelAndView resolveException(HttpServletRequest request, 
                                             HttpServletResponse response, Object o, Exception e) {
            try {
                response.sendError(511, "自定义的错误");  // 学底层的写法
            } catch (IOException ex) {
                ex.printStackTrace();
            }
            return new ModelAndView();
        }
    }
    

总结:DefaultErrorViewResolver 实现自定义处理异常;

  • response.sendError(),error请求就会转给BasicErrorController
  • 你的异常没有任何人能处理,tomcat底层 response.sendError,error请求就会转给BasicErrorController
  • BasicErrorController 要去的页面地址是 DefaultErrorViewResolver 解析的

Ⅸ Web原生组件注入

即(Servlet、Filter、Listener)

① 使用Servlet API

使用:@ServletComponentScan + @WebServlet 注解

原生Servlet类:

@WebServlet(urlPatterns = "/my")  // Servlet3.0后提供的注解
public class MyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
        throws ServletException, IOException {
        resp.getWriter().write("666");
    }
}

主程序类:

@SpringBootApplication
// 指定原生Servlet放在的位置,不写这个原生的Servlet是不生效的
@ServletComponentScan(basePackages = "com.sutong.servlet") 
public class Springboot02Application {
    public static void main(String[] args) {
        SpringApplication.run(Springboot02Application.class, args);
    }
}

效果:直接响应,没有通过SpringMVC配置的拦截器

其他两个组件也一样,使用@WebFilter@WebListenr ,也要使用@ServletComponentScan指定扫描 !!

② 使用RegistrationBean

ServletRegistrationBean, FilterRegistrationBean, and ServletListenerRegistrationBean

@Configuration  // 配置类!配置Filter可能要依赖Servlet组件,所以可以不写 proxyBeanMethods = false
public class MyRegisterConfig {
    @Bean
    public ServletRegistrationBean myServlet() {
        MyServlet servlet = new MyServlet();
        // 传入Servlet,第二个参数是访问路径
        return new ServletRegistrationBean(servlet, "/my");
    }

    @Bean
    public FilterRegistrationBean myFilter() {
        MyFilter filter = new MyFilter();
        // 传入Filter,第二个参数是要拦截的Servlet对应的访问路径
        // return new FilterRegistrationBean(filter, myServlet());

        // 或者指定拦截的路径
        FilterRegistrationBean frb = new FilterRegistrationBean(filter);
        frb.setUrlPatterns(Arrays.asList("/test/**"));
        return frb;
    }
    
    // Listener也一样!
}

效果和上面一样,直接响应,没有通过SpringMVC配置的拦截器

DispatchServlet 如何注册进来:(扩展)

  • 容器中自动配置了 DispatcherServlet 属性绑定到 WebMvcProperties,对应的配置文件配置项是 spring.mvc

  • 通过 ServletRegistrationBean<DispatcherServlet>DispatcherServlet 配置进来。

  • 默认映射的是 “/” 路径。

没有通过SpringMVC配置的拦截器原因:

DispatcherServlet 处理请求 -> “/”

MyServlet 处理请求 -> “/my”

Tomcat-Servlet,如果多个多个Servlet都能处理到同一层路径,则精确优选原则!!则使用MyServlet 处理 “/my” 请求,则就不进入SpringMVC的流程了,则不经过 DispatcherServlet !!


Ⅹ 嵌入式Servlet容器

这个章节可以实用性不高,了解一下不错

① 切换嵌入式Servlet容器

原理:

  1. SpringBoot应用启动发现当前是Web应用(导入了Web场景 -> 默认导入tomcat)

  2. Web应用会创建一个Web版的ioc容器 ServletWebServerApplicationContext ,启动的时候回去找,ServletWebServerFactory (WebServer工厂,即Servlet的Web服务器

  3. SpringBoot底层默认有很多的WebServer工厂:TomcatServletWebServerFactory, JettyServletWebServerFactory, or UndertowServletWebServerFactory

  4. 上面这些工厂也不用我们配置,底层直接会有一个自动配置类,即ServletWebServerFactoryAutoConfiguration,这个绑定了ServerProperties属性类,这个自动配置类导入了 ServletWebServerFactoryConfiguration配置类,这个配置类里面选择的配置了上面的三个WebServer工厂(即动态判断系统中到底导入的那个Web的服务器的包,默认是web-starter导入tomcat包,则容器中就有 TomcatServletWebServerFactory)

  5. TomcatServletWebServerFactory 创建出Tomcat服务器并启动。

    Tomcat服务器TomcatWebServer 的构造器拥有初始化方法initialize() -> this.tomcat.start() 启动

  6. 所谓内嵌服务器,就是手动把启动服务器的代码调用(tomcat核心jar包存在)

切换服务器,即导入对应的starter就行了,默认是tomcat的服务器,即 spring-boot-starter-tomcat

(spring-boot-starter-web里面默认自动导入了spring-boot-starter-tomcat)

其他还有:spring-boot-starter-undertowspring-boot-starter-jetty

<dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-web</artifactId>
       <exclusions>
           <exclusion>  <!-- 排除掉默认的Tomcat,就不会导入多个服务器了-->
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-starter-tomcat</artifactId>  
           </exclusion>
       </exclusions>
</dependency>
<dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
② 定制Servlet容器

定制Servlet容器:

  1. 修改配置文件 server.xxx

  2. 实现 WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>

    把配置文件的值和 ServletWebServerFactory 进行绑定。

    一般 xxxxxCustomizer都是一些 定制化器,可以改变xxxx的默认规则。

  3. 直接自定义 ConfigurableServletWebServerFactory

④ SpringBoot定制化总结

定制化的常见方式 :

  1. 修改配置文件

  2. 实现一些XxxCustomizer 定制化器

  3. 编写自定义的配置类 : XxxConfiguration + @Bean 替换/增加容器中默认组件

  4. web应用,@Configuration + 实现WebMvcConfigurer ,即可完成定制化web功能

  5. @EnableWebMvc + @Configuration + WebMvcConfigurer (@Bean) 可以全面接管SpringMVC

    底层几乎所有组件失效(WebMvcAutoConfiguration自动配置类失效),例如里面配置的欢迎页,视图解析器,静态资源,Rest风格的过滤器…都失效,但会保证一些最基本的功能生效,几乎所有规则全部自己重新配置, 实现定制和扩展功能(慎用)

场景starter —— xxxxAutoConfiguration —— 导入xxx组件 —— 绑定xxxProperties —— 绑定配置文件项


3.3 数据访问


Ⅰ SQL

① 准备

导入JDBC场景

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>

<!-- 这个里面有HikariCP数据源(数据源的自动配置是HikariDataSource),spring-jdbc,spring-tx等-->

SpringBoot并没有帮我们导入驱动,因为官方不知道我们接下要操作什么数据库,(数据库版本和驱动版本对应)

<properties>
    <mysql.version>5.1.25</mysql.version>  <!-- 不加这个默认驱动是8+版本的,好像也能向下兼容-->
</properties>

<dependencies>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>  <!-- 这下面写版本也行-->
    </dependency>
</dependencies>

修改配置项:

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mybatis
    username: root
    password: 9527
    type: com.zaxxer.hikari.HikariDataSource   # 默认就是这个
    driver-class-name: com.mysql.jdbc.Driver
  jdbc:
    template:
      query-timeout: 3
② 分析自动配置
  1. 数据源的自动配置 DataSourceAutoConfiguration

    • 绑定了的DataSourceProperties 属性类,即对应spring.datasource开头的配置

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

      @Configuration(proxyBeanMethods = false)
      @Conditional(PooledDataSourceCondition.class)
      @ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
      @Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
               DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class,
               DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class })
      protected static class PooledDataSourceConfiguration() {
          protected PooledDataSourceConfiguration() { }
      }
      
    • 底层配置好的连接池是:HikariDataSource (具体配置在 DataSourceConfiguration类里面),这个数据源在市面上还算优

  2. 事务管理器的自动配置DataSourceTransactionManagerAutoConfiguration

  3. JdbcTemplate组件的自动配置 JdbcTemplateAutoConfiguration

    • 可以修改这个配置项spring.jdbc 来修改 JdbcTemplate 相关属性
  4. Jndi的自动配置JndiDataSourceAutoConfiguration

  5. 分布式事务相关的自动配置XADataSourceAutoConfiguration

③ 使用Druid数据源

官方文档:https://github.com/alibaba/druid (自带监控功能,一些企业还是喜欢用Druid数据源)

整合第三方技术的两种方式,1.自定义 2.找starter

  1. 自定义

    依赖:

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.8</version>
    </dependency>
    

    配置数据源和监控页:

    @Configuration(proxyBeanMethods = false)
    public class MyDataSourceConfig {
    
        @Bean
        @ConfigurationProperties(prefix = "spring.datasource")
        public DataSource dataSource() throws SQLException {
            // 按理说要设置url,username..等但在配置文件中已经写过了,直接绑定就行了
            DruidDataSource dataSource = new DruidDataSource();
            dataSource.setFilters("stat,wall"); // 开启SQL监控和SQL防火墙功能!!这个也能在配置文件里面写!!
            return dataSource;
        }
    
        // 配置druid监控页功能,这个是个标准的Servlet组件
        @Bean
        public ServletRegistrationBean<StatViewServlet> druidStatViewServlet() {
            StatViewServlet servlet = new StatViewServlet();
            ServletRegistrationBean<StatViewServlet> srb = 
                new ServletRegistrationBean<>(servlet, "/druid/*");
            // 添加一些初始化参数,访问监控页的密码,ip等..
            HashMap<String, String> initParameters = new HashMap<>(); 
            initParameters.put("loginUsername", "admin"); 
            initParameters.put("loginPassword", "123456");
            srb.setInitParameters(initParameters);
            return srb;
        }
    
        // 配置 WebStatFilter用于web-jdbc监控的数据
        @Bean
        public FilterRegistrationBean<WebStatFilter> webStatFilter() {
            WebStatFilter filter = new WebStatFilter();
            FilterRegistrationBean<WebStatFilter> frb = new FilterRegistrationBean<>(filter);
            frb.setUrlPatterns(Arrays.asList("/*"));
            HashMap<String, String> initParameters = new HashMap<>();
            // 不监控一些静态资源
            initParameters.put("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"); 
            frb.setInitParameters(initParameters);
            return frb;
        }
    }
    
    

    还有很多监控Spring监控,Session监控等…

  2. starter方式

    导入starter:(上面这么多的东西就不用我们写了)

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.2.8</version>
    </dependency>
    

    分析自动配置:

    • DruidDataSourceAutoConfigure -> DruidStatProperties,DataSourceProperties

      绑定了这两个配置spring.datasource.druid, spring.datasource

    • 类上面使用@Import导入了下面的四个配置类:

    • DruidSpringAopConfiguration

      配置了监控Spring中组件的的相关东西,配置项spring.datasource.druid.aop-patterns

    • DruidStatViewServletConfiguration

      开启Servlet监控页的功能,配置项spring.datasource.druid.stat-view-servlet,可设置访问监控页的密码等…

    • DruidWebStatFilterConfiguration

      配置WebStatFilter,配置项spring.datasource.druid.web-stat-filter

    • DruidFilterConfiguration

      配置了其他一些Filter,有下面这么多,绑定了下面的这些配置项

      spring.datasource.druid.filter.stat
      spring.datasource.druid.filter.config
      spring.datasource.druid.filter.encoding
      spring.datasource.druid.filter.slf4j
      spring.datasource.druid.filter.log4j
      spring.datasource.druid.filter.log4j2
      spring.datasource.druid.filter.commons-log
      spring.datasource.druid.filter.wall
      spring.datasource.druid.filter.wall.config
      

    编写配置项:(示例)更多看官方文档!!

    spring:
      datasource:
        url: jdbc:mysql://localhost:3306/mybatis
        username: root
        password: 9527
        type: com.alibaba.druid.pool.DruidDataSource  # 这个可以不写,底层会根据导入依赖选择注入
        driver-class-name: com.mysql.jdbc.Driver
    
        druid:
          stat-view-servlet:          # 配置监控页功能
            enabled: true             # 开启,默认是false
            login-username: admin
            login-password: 123456
            reset-enable: false       # 禁用重置按钮
    
          web-stat-filter:            # 监控web,Web应用和URL监控
            enabled: true
            url-pattern: /*
            exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'
            
    	  # 开启多个过滤器,即Sql监控和Sql防火墙(可以使用filter配置单个过滤器的详细信息)
          filters: stat,wall            
          filter:
            stat:
              enabled: true             # 默认true
              slow-sql-millis: 1000     # 规定是慢查询的毫秒时间
              log-slow-sql: true        # 日志记录下来慢查询
            wall:
              enabled: true             # 默认false
              config:
                update-allow: false     # 所有的数据库更新都会被防火墙拦截
                delete-allow: false     # 不允许删数据
                drop-table-allow: false # 不允许删表
    
          aop-patterns: com.sutong.*    # 这个包下的所有组件都监控,监控SpringBean
    
④ 整合Myatis

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

starter:

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId> 
    <version>2.1.3</version>  <!-- 我们学习使用的是SpringBoot2.3.4.RELEASE版本-->
</dependency>
<!-- 里面有jdbc的场景,mybatis,mybatis-spring依赖-->

要求:

  • 2.2.x: MyBatis 3.5+, MyBatis-Spring 2.0+(推荐2.0.6+), Java 8+ 和 Spring Boot 2.5+
  • 2.1.x : MyBatis 3.5+, MyBatis-Spring 2.0+(推荐2.0.6+), Java 8+ 和 Spring Boot 2.1-2.4
  1. 分析自动配置

    • MybatisAutoConfiguration -> MybatisProperties属性类 -> 配置项mybatis

    • 导入了SqlSessionFactoryBean, 自动配置好了,数据源就是用的容器中的数据源

    • 导入了SqlSessionTemplate ,这个其实就个SqlSession (组合了)

      mybatis:
        executor-type: batch    # 底层就会创建一个批量的SqlSessionTemplate(就是sqlSession)
      
    • 导入了@Import({AutoConfiguredMapperScannerRegistrar.class})

      只要我们写的操作MyBatis标注了@Mapper注解就会被自动扫描进来!!

      或者在SpringBoot的启动类上面加上@MapperScan指定,就不用每个Mapper都写@Mapper注解了

  2. 使用

    配置:

    全局配置文件mybatis/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>
        <settings>
            <setting name="mapUnderscoreToCamelCase" value="true"/> <!-- 数据库下划线命名转化驼峰命名-->
        </settings>
    </configuration>
    

    application.yml

    mybatis:
      config-location: classpath:mybatis/mybatis-config.xml     # 指定Mybatis全局配置文件的位置
      mapper-locations: classpath:mybatis/mapper/*.xml          # 指定XxxMapper.xml的文件位置
      # 上面全局配置文件中的配置也能在这里面写,前缀:mybatis.configuration !!!!(推荐这种)
      # 注意全局配置文件在这里面写的话,config-location就不要配置了,否则底层也不知道以那个配置项为准了!!
    

    接口,com.sutong.dao/EmpMapper.java

    @Mapper
    public interface EmpMapper {
        Emp getEmpById(Integer id);
    }
    

    sql映射文件,mybatis/mapper/EmpMapper.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.sutong.dao.EmpMapper">
        <select id="getEmpById" resultType="com.sutong.bean.Emp">
            select * from t_emp where id = #{id}
        </select>
    </mapper>
    

    在Service层直接注入Mapper就能直接使用了!!

  3. 纯注解(长SQL就不建议了)(还可以注解和xml映射文件混合使用,但不建议…)

    @Mapper
    public interface EmpMapper {
        @Select("select * from t_emp where id = #{id}")
        Emp getEmpById(Integer id);
        
        @Insert("..")
        @Options(useGeneratedKeys = true, keyProperty = "id") // Insert标签的设置项
        void saveEmp(Emp emp);
    }
    

最佳实践:

  • 引入mybatis-starter

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

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

  • 简单方法直接注解方式

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

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

⑤ 整合MyBatis-Plus
  1. 引入starter

    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId> 
        <version>3.5.1</version>
    </dependency>
    <!-- 引入 MyBatis-Plus 不用引入MyBatis的starter和jdbc的starter了-->
    

    自动配置类:

    MybatisPlusAutoConfiguration -> MybatisPlusProperties -> 前缀mybatis-plus

    • mapperLocations属性有默认值(Mapper映射文件的位置),classpath*:/mapper/**/*.xml

    • SqlSessionFactory 自动配置好了,数据源用的是容器中的数据源

    • SqlSessionTemplate 自动配置了

    • @Mapper标注的接口也会被自动扫描,建议批量扫描@MapperScan

  2. CRUD

    Mapper接口:

    // BaseMapper是MyBatisPlus提供的(里面对单表的增删查改方法),泛型是要操作的实体类的类型
    public interface UserMapper extends BaseMapper<User> {  }
    

    Service层直接使用就行!


Ⅱ NoSQL

① Redis

启动器:

SpringBoot1.x使用的是Jedis操作redis,SpringBoot2.x默认使用lettuce

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- 选择性引入-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

自动配置:

  • RedisAutoConfiguration 自动配置类 -> RedisProperties 属性类 -> spring.redis配置项

  • 连接工厂是准备好的:LettuceConnectionConfiguration(默认这个生效)、JedisConnectionConfiguration

  • 自动注入了RedisTemplate<Object, Object> (k:v都是Object)

  • 自动注入了StringRedisTemplate (k:v都是String)

    底层只要我们使用StringRedisTemplate、RedisTemplate就可以操作redis

Linux端配置好Redis就可以连接了

配置文件:

spring:
  redis:
    #url: redis://user:password@example.com:6379 (包含下面的三,user将会忽略)
    database: 0  				# 数据库索引,默认为0
    host: 192.168.200.xxx       # Redis服务器地址,默认localhost
    port: 6379					# 端口,默认6379
    password: 9527              # 默认是空


########################################### 其他的一些配置:
spring:
  redis:
  	# ...
    timeout: 1800000  #连接超时时间(毫秒)
    lettuce:
      pool:            #写这个配置需要:commons-pool2 依赖(选择性引入,SpringBoot也管理了版本)
        max-active: 8  #最大阻塞等待时间(负数表示没限制,默认8)
        max-idle: 8    #连接池中的最大空闲连接,默认8
        min-idle: 0    #连接池中的最小空闲连接,默认0

测试:

@SpringBootTest
public class RedisTest {

    @Autowired
    private RedisTemplate<Object, Object> redisTemplate;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Test
    public void test01() {
        System.out.println(redisTemplate);
        System.out.println(stringRedisTemplate);

        // k,v都是String的客户端
        ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
        ops.set("k1", "HelloWord");
        System.out.println(ops.get("k1"));
    }
}

StringRedisTemplate继承了RedisTemplate ,两种有相似之处,也有不同。

区别:

  • 两者的数据是不共通的。

  • 使用的序列化类不同:

    StringRedisTemplate使用的是StringRedisSerializer

    采用的是把字符串本身转化位字节数组,和对应编码下字节数组之间的转换,(默认UTF-8编码)。

    数据值是以可读的形式显示,即保存的什么,在客户端看到的就是什么

    RedisTemplate使用的是JdkSerializationRedisSerializer

    存入数据会将数据先序列化成字节数组(使用ByteArrayOutputStream)然后在存入Redis数据库(使用OutputStream)。

    数据不是以可读的形式展现的,而是以字节数组显示。

  • 使用时注意事项:

    当你的redis数据库里面本来存的就是字符串,或要取的数据就是字符串类型时,那么你就使用StringRedisTemplate即可。但是如果你的数据是复杂的对象类型,而取出的时候又不想做任何的数据转换,直接从Redis里面取出一个对象,那么使用RedisTemplate是更好的选择。

切换Jedis:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <exclusions>
        <exclusion>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<!--导入jedis,(SpringBoot也管理了jedis版本,但默认没有引入)-->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>

配置:

spring:
  redis:
	database: 0 
	host: 192.168.200.xxx
	port: 6379
	password: 9527
	client-type: jedis  # 可指定

lettucejedis两者区别:

  1. lettuce:底层是用netty实现,线程安全,默认只有一个实例

  2. jedis:可直连redis服务端,配合连接池使用,可增加物理连接


4. 单元测试


Ⅰ Junit5新变化

Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库

JUnit5与之前版本的Junit框架有很大的不同。由三个不同子项目的几个不同模块组成。

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

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

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

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

starter, 场景:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

SpringBoot 2.4+ 版本中的spring-boot-starter-test场景移除了默认对Vintage 的依赖。

如果需要兼容junit4需要自行引入(不能使用junit4的功能,例如org.junit.@Test)

SpringBoot整合了Junit以后,测试非常方便(不用再测试类上面标注好几个注解了)

import org.springframework.boot.test.context.SpringBootTest;
import org.junit.jupiter.api.Test;

@SpringBootTest
class Springboot02ApplicationTests {
    // 如果需要组件,只需要@Autowired就行了,Junit的类具有Spring的功能
    // 测试方法上加上@Transactional,测试完成后会自动回滚!!
    @Test
    void contextLoads() {
        System.out.println("haha");
    }
}

Ⅱ Junit5常用注解

  • @Test:表示方法是测试方法。但是与JUnit4的@Test不同,他的职责非常单一不能声明任何属性,拓展的测试将会由Jupiter提供额外测试

  • @RepeatedTest:重复测试,value表示次数

  • @DisplayName :为测试类或者测试方法设置展示名称

  • @BeforeEach :表示在每个单元测试之前执行

  • @AfterEach:表示在每个单元测试之后执行

  • @BeforeAll :表示在所有单元测试之前执行,一般是静态方法或者标注上@TestInstance,AfterAll也一样

  • @AfterAll:表示在所有单元测试之后执行

  • @Tag :表示单元测试类别,类似于JUnit4中的@Categories

  • @Disabled:表示测试类或测试方法不执行,类似于JUnit4中的@Ignore

  • @Timeout:表示测试方法运行如果超过了指定时间将会抛出异常,单位是可以自己定

  • @ExtendWith:为测试类或测试方法提供扩展类引用

    如果想要使用Spring组件需要在测试类上面标上@SpringBootTest,而这个里面包含了:

    @BootstrapWith(SpringBootTestContextBootstrapper.class)
    @ExtendWith({SpringExtension.class})
    
  • @ParameterizedTest:表示方法是参数化测试,下方会有详细介绍


Ⅲ 断言

断言(assertions)是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是 org.junit.jupiter.api.Assertions 的静态方法。JUnit 5 内置的断言可以分成以下几个类别

用来:检查业务逻辑返回的数据是否合理(我们就不要print然后人眼判断了)。所有的测试运行结束以后,会有一个详细的测试报告(点右上角Maven的test指令,就会跑一次全部的单元测试,生成报告)

  1. 简单断言

    方法说明
    assertEquals判断两个对象或两个原始类型是否相等
    assertNotEquals判断两个对象或两个原始类型是否不相等
    assertSame判断两个对象引用是否指向同一个对象
    assertNotSame判断两个对象引用是否指向不同的对象
    assertTrue判断给定的布尔值是否为 true
    assertFalse判断给定的布尔值是否为 false
    assertNull判断给定的对象引用是否为 null
    assertNotNull判断给定的对象引用是否不为 null
    import static org.junit.jupiter.api.Assertions.*;  // 这样导入的话就不用写前面那个Assertions了!简单点
    
    @Test
    public void testAssertions() {
        // 前面是你期望值,后面是真实的值,还可以传入第三个参数是断言失败的提示信息
        Assertions.assertEquals(2, sum(1, 1));
    }
    
    // 如果前面的断言失败了,后面的代码不会执行
    
  2. 数组断言

    通过 assertArrayEquals 方法来判断两个对象或原始类型的数组是否相等(逻辑相等即equals,不是地址相等)

  3. 组合断言

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

    @Test
    public void testAssertions() {
        // 都成功才算成功
        assertAll("test",
                  () -> assertTrue(true && true),
                  () -> assertEquals(5, sum(2, 3)));
    }
    
  4. 异常断言

    在JUnit4时期,想要测试方法的异常情况时,需要用@Rule注解的ExpectedException变量还是比较麻烦的。

    而JUnit5提供了一种新的断言方式Assertions.assertThrows() ,配合函数式编程就可以进行使用。

    // 断言业务要抛出异常(意思的断言这个业务要抛出异常,)
    assertThrows(ArithmeticException.class, () -> {int i = 10 / 0;}, "业务路径竟然正常运行");
    
  5. 超时断言

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

    //如果测试方法时间超过1s将会异常
    Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(500));
    
  6. 快速失败

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

    if (...) {
    	fail("测试失败");	    
    }
    

Ⅳ 前置条件

JUnit 5 中的前置条件(assumptions,假设)类似于断言,不同之处在于不满足的断言会使得测试方法失败,而不满足的前置条件只会使得测试方法的执行终止。前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要。

失败了在测试报告上不显示fail,而是skiped(和@disabled一样)

import static org.junit.jupiter.api.Assumptions.*;

public class Junit5Test {
    String environment = "DEV";

    @Test
    public void testAssumptions() {
        assumeTrue(Objects.equals(this.environment, "DEV")); 
        assumeFalse(() -> Objects.equals(this.environment, "PROD")); // 不满足条件会使得测试执行终止

        //只有条件满足时,Executable 对象才会被执行;当条件不满足时,测试执行并不会终止。
        assumingThat(
                Objects.equals(this.environment, "DEV"),
                () -> System.out.println("In DEV")
        );
    }
}


Ⅴ 嵌套测试

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

@Nested加在里层的测试类上面,表示嵌套测试。对于before/after只对于同层或者更里层的测试有效,对于外层不生效

@DisplayName("A stack")
public class TestAStackDemo {
    Stack<Object> stack;

    @Test
    @DisplayName("new Stack()")
    void isInstantiatedWithNew() {
        new Stack<>();
        assertNull(stack);  // 这时stack为空,外层的Test不能驱动内层的BeforeEach/All之类的方法提前或之后运行
    }

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

        @Nested
        @DisplayName("after pushing an element")
        class AfterPushing {
            String anElement = "an element";
            
            @BeforeEach
            void pushAnElement() {        // 内层的Test可以驱动外层的BeforeEach/All之类的方法提前或之后运行
                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());
            }
        }
    }
}

Ⅵ 参数化测试

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

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

@ValueSource: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型

@NullSource: 表示为参数化测试提供一个null的入参

@EnumSource: 表示为参数化测试提供一个枚举入参

@CsvFileSource:表示读取指定CSV文件内容作为参数化测试入参

@MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)

public class ParameterTest {

    @ParameterizedTest  // 当前测试方法是参数化测试
    @ValueSource(strings = {"one", "two", "three"})
    @DisplayName("参数化测试1")
    public void parameterizedTest1(String string) {
        System.out.println(string);
        Assertions.assertTrue(StringUtils.isNotBlank(string));
    }

    
    @ParameterizedTest
    @MethodSource("method")    //指定方法名
    @DisplayName("方法来源参数")
    public void testWithExplicitLocalMethodSource(String name) {
        System.out.println(name);
        Assertions.assertNotNull(name);
    }

    // 注意方法返回需要是一个流,而且是静态的
    static Stream<String> method() {
        return Stream.of("apple", "banana", "sutong");
    }
}

在进行迁移的时候需要注意如下的变化:

  • 注解在 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。


5. 指标监控


ⅠSpringBoot Actuator

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

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

1.x和2.x版本区别(SpringBoot1.x版本就是Actuator1.x版本):

使用:

  • 引入场景

  • 访问http://localhost:8080/actuator/** (有项目路径加上项目路径)

    http://localhost:8080/actuator/health 当前健康状态 ,http://localhost:8080/actuator/info 当前应用的详细信息

    health,info…称为Endpoint ,SpringBoot官方有非常多的EndPoint

  • 暴露所有监控信息为HTTP(默认大多数都不以HTTP方式暴漏)

    management:
      endpoints:
        enabled-by-default: true    # 默认开启所有监控端点(默认也是true)
        web:
          exposure:
            include: '*'            # 以web方式暴露所有端点(默认只有health,info)
    

支持的暴露方式

  • HTTP:默认只暴露healthinfo Endpoint

  • JMX:默认暴露所有Endpoint (例如Jconsole)

  • 除过health和info,剩下的Endpoint都应该进行保护访问。如果引入SpringSecurity,则会默认配置安全访问规则


Ⅱ Actuator Endpoint

health,info…称为Endpoint ,SpringBoot官方有非常多的EndPoint

官方文档:https://docs.spring.io/spring-boot/docs/2.3.4.RELEASE/reference/html/production-ready-features.html#production-ready-endpoints

最常使用的端点:

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显示应用程序中的计划任务。
sessions允许从Spring Session支持的会话存储中检索和删除用户会话。需要使用Spring Session的基于Servlet的Web应用程序。
shutdown使应用程序正常关闭。默认禁用。
startup显示由ApplicationStartup收集的启动步骤数据。需要使用SpringApplication进行配置BufferingApplicationStartup
threaddump执行线程转储。

如果您的应用程序是Web应用程序(Spring MVC,Spring WebFlux或Jersey),则可以使用以下附加端点:

ID描述
heapdump返回hprof堆转储文件。
jolokia通过HTTP暴露JMX bean(需要引入Jolokia,不适用于WebFlux)。需要引入依赖jolokia-core
logfile返回日志文件的内容(如果已设置logging.file.namelogging.file.path属性)。支持使用HTTPRange标头来检索部分日志文件的内容。
prometheus以Prometheus服务器可以抓取的格式公开指标。需要依赖micrometer-registry-prometheus

最常用的Endpoint

  • Health:健康状况

  • Metrics:运行时指标

  • Loggers:日志记录

可视化平台 https://github.com/codecentric/spring-boot-admin

  1. Health Endpoint

    健康检查端点,我们一般用于在云平台,平台会定时的检查应用的健康状况,我们就需要Health Endpoint可以为平台返回当前应用的一系列组件健康状况的集合。

    management:
      ...
      endpoint:                     # 对某个端点的具体配置
        health:
          show-details: always      # 显示健康的详细信息
    
    • health endpoint返回的结果,应该是一系列健康检查后的一个汇总报告(全健康才健康)

    • 很多的健康检查默认已经自动配置好了,比如:数据库、redis等

    • 可以很容易的添加自定义的健康检查机制

  2. Metrics Endpoint

    提供详细的、层级的、空间指标信息,这些信息可以被pull(主动推送)或者push(被动获取)方式得到;

    • 通过Metrics对接多种监控系统

    • 简化核心Metrics开发

    • 添加自定义Metrics或者扩展已有Metrics

  3. 管理Endpoints

    默认所有的Endpoint除过shutdown都是开启的。但有时候我们需要选择开启,即或者禁用所有的Endpoint然后手动开启指定的Endpoint

    management:
      endpoints:
        enabled-by-default: false   # 关掉监控所有的端点(关闭总开关)!!下面选择开启端点
        web:
          exposure:
            include: '*'          
      endpoint:                    
        health:
          enabled: true
        info:
          enabled: true
        metrics:
          enabled: true
    

Ⅲ 定制 Endpoint

  1. 定制 Health 信息

    // 后缀必须是HealthIndicator,前缀就是端点的名字,继承AbstractHealthIndicator或者实现HealthIndicator接口
    // 可以参考 磁盘监控 -> DiskSpaceHealthIndicator写法
    @Component  // 放到容器中就行了
    public class MyComHealthIndicator extends AbstractHealthIndicator {
        // 编写真实的检查方法
        @Override
        protected void doHealthCheck(Health.Builder builder) throws Exception {
            // 详细信息
            Map<String,Object> map = new HashMap<>();
            // 检查完成
            if(...) {
                builder.up(); //健康 或者builder.status(Status.UP);
                map.put("count", 1);
                map.put("ms", 100);
            } else {
                builder.down(); // 不健康 或者builder.status(Status.DOWN);
                map.put("err", "连接超时");
                map.put("ms", 3000);
            }
    
            builder.withDetail("code", 100).withDetails(map);
        }
    }
    
  2. 定制 info信息 (两种)

    1. 编写配置文件

      info:
       appName: sutong-learn                   # 自定义
       appVersion: 1.0
       mavenProjectName: @project.artifactId@  # 使用@@获取pom文件里面的值
       mavenProjectVersion: @project.version@
      
    2. 实现InfoContributor

      // 这个类后缀可以随便
      @Component
      public class ExampleInfoContributor implements InfoContributor {
      
          @Override
          public void contribute(Info.Builder builder) {
              builder.withDetail("example", "hello")
                     .withDetails(Collections.singletonMap("key", "value"));
          }
      }
      
  3. 定制Metrics信息 (后面也经常需要定制的)

    默认支持是很多…看文档

    增加定制Metrics:

    @Service
    public class EmpService {
        @Autowired  
        private EmpMapper empMapper;
    
        private Counter counter;
        
        // 构造器自动注入
        public EmpService(MeterRegistry meterRegistry) {
            counter = meterRegistry.counter("empService.getEMpById被调用的次数"); // 参数name是指标名
        }
        
        public Emp getEMpById(Integer id) {
            counter.increment(); // 每调用一次增加一次
            return empMapper.getEmpById(id);
        }
    }
    
    //------------------------------也可以使用下面的方式--------------------------------------
    @Bean
    MeterBinder queueSize(Queue queue) {
        return (registry) -> Gauge.builder("queueSize", queue::size).register(registry);
    }
    
  4. 定制自己的监控端点,EndPoint

    @Component
    @Endpoint(id = "myservice") // id是端点名
    public class MyServiceEndPoint {
        // 返回值无所谓,ReadOperation代表是个读方法,即返回端点的数据。因为是getXxx,必须是无参是方法
        // http://localhost:8080/actuator/myservice
        @ReadOperation
        public Map<String, String> getDockerInfo() {
            return Collections.singletonMap("info", "docker started...");
        }
    
        // 写操作
        @WriteOperation
        private void restartDocker() {
            System.out.println("docker restarted....");
        }
    }
    

    场景:开发ReadinessEndpoint来管理程序是否就绪,或者LivenessEndpoint来管理程序是否存活;

    当然,这个也可以直接使用https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-features.html#production-ready-kubernetes-probes


Ⅳ 监控可视化界面

AdminServer文档:https://codecentric.github.io/spring-boot-admin/2.3.1/

  1. 准备一个SpringBootServer服务器,只需要加上web场景,再加上下面的starter

    (这个就能监控我们的项目了,专门来收集其他微服务监控数据的)

    <dependency>
        <groupId>de.codecentric</groupId>
        <artifactId>spring-boot-admin-starter-server</artifactId>
        <version>2.3.1</version>
    </dependency>
    

    还要再在主程序上写上@EnableAdminServer 注解

    因为我们要监控是项目是8080端口,所以这个项目要改一下端口例如8888 server.port=8888

  2. 访问http://localhost:8888/ 就能看到监控页了(但现在还没有数据)

  3. 注册客户端

    在我们要监控的项目中引入下面的starter:

    <dependency>
        <groupId>de.codecentric</groupId>
        <artifactId>spring-boot-admin-starter-client</artifactId>
        <version>2.3.1</version>
    </dependency>
    

    配置URL:

    spring:
      application:
        name: springboot02              # 给当前应用取个名字
      boot:
        admin:
          client:
            url: http://localhost:8888  # 填写AdminServer的URL
            instance:
              prefer-ip: true           # 使用ip注册进来(写不写都行)
    management:
      endpoints:
        web:
          exposure:
            include: '*'            # 以web方式暴露所有端点
    

    如果引入一些安全框架还要所有请求数放行等…(一般是要引入的)

    没有的话直接启动我们要监控的项目就行,访问监控页就能看到数据了。


6. 原理解析


Ⅰ Profile功能

为了方便多环境适配,springboot简化了profile功能。

  1. application-profile功能

    • 默认配置文件 application.yaml,任何时候都会加载

    • 指定环境配置文件 application-{env}.yaml

    • 激活指定环境

      • 默认配置文件激活,application.yaml里面:

        # 如果这里和环境配置文件属性同名,则以指定的环境属性为准
        
        spring:
          profiles:
            active: env          # 写'-'后面的就行了
        
      • 命令行激活

        java -jar xxx.jar --spring.profiles.active=env --person.userName=haha

        这里还可以修改配置文件的任意值,命令行优先!

    • 默认配置与环境配置同时生效,同名配置项,profile配置优先

  2. @Profile条件装配功能

    @Configuration(proxyBeanMethods = false)
    @Profile("production")  // production环境才进行配置(方法上面也行,配置绑定类上也行)
    public class ProductionConfiguration {
        // ...
    }
    
  3. profile分组

    spring: 
      profiles:
        active: production 
        group:
          production[0]: proddb
          production[1]: prodmq
    # 即把production环境分成了两个配置文件
    
    # properties
    spring.profiles.group.production[0]=proddb
    spring.profiles.group.production[1]=prodmq
    

Ⅱ 外部化配置

Spring Boot 允许您外部化配置,以便您可以在不同的环境中使用相同的应用程序代码,您可以使用各种外部配置源,常用的的见下面

  1. 外部配置源

    常用:Java属性文件YAML文件环境变量(本机配置的环变量)、命令行参数

    @SpringBootApplication
    public class Springboot02Application {
        public static void main(String[] args) {
            ConfigurableApplicationContext run = 
                SpringApplication.run(Springboot02Application.class, args);
            ConfigurableEnvironment environment = run.getEnvironment();
    
            Map<String, Object> systemEnvironment = environment.getSystemEnvironment(); // 系统环境变量
            Map<String, Object> systemProperties = environment.getSystemProperties(); // 系统的属性
        }
    }
    
  2. 配置文件查找位置

    • classpath 根路径

    • classpath 根路径下config目录

    • jar包当前目录

    • jar包当前目录的config目录

    • jar包当前目录/config子的直接子目录(只能一级目录,名字随意,这条Liunx系统好像才行)

      (如果有同名的配置,后面路径下的配置会覆盖前面的,即且越往下优先级越高

  3. 配置文件加载顺序

    • 当前jar包内部的application.properties和application.yml

    • 当前jar包内部的application-{profile}.properties 和 application-{profile}.yml

    • 引用的外部jar包的application.properties和application.yml

    • 引用的外部jar包的application-{profile}.properties 和 application-{profile}.yml

指定环境优先,外部优先,后面的可以覆盖前面的同名配置项


Ⅲ 自定义starter

  1. starter启动原理

    • starter-pom引入 autoconfigurer 包 (starter -> autoconfigure -> spring-boot-starter)

    • starter里面并没有什么代码,只说明当前场景引入哪些依赖 (当然要引入autoconfigure包)

    • autoconfigure 包里面做好所有的自动配置功能。(编写自动配置类XxxAutoConfiguration -> xxxxProperties

      使用 @Configuration,@Conditional, @EnableConfigurationProperties, @Bean…等注解
      包中配置使用 META-INF/spring.factories 中 **EnableAutoConfiguration **的值,使得项目启动加载指定的自动配置类!!!

  2. starter的自定义

    sutong-hello-spring-boot-starter(启动器,只列出依赖并没什么代码,并引入下面的 xxx-autoconfigure包)

    sutong-hello-spring-boot-starter-autoconfigure(编写自动配置类,不要忘记写spring.factories文件)

    (业务中一些非常常用的场景就可以像这样抽取出来)


Ⅳ SpringBoot原理

Spring原理、SpringMVC原理、自动配置原理、SpringBoot原理

SpringBoot启动过程:(面试可能问⭐)

  • 创建 SpringApplication

    • 先保存一些信息
    • 判定当前应用的类型,是原生的Servlet编程,还是响应式编程
    • 初始启动引导器:去spring.factories文件中找org.springframework.boot.Bootstrapper 配置的类
    • 应用初始化器:去spring.factories文件中找org.springframework.context.ApplicationContextInitializer
    • 应用监听器:去spring.factories文件中找org.springframework.context.ApplicationListener
  • 运行 SpringApplication

    • StopWatch 监听整个应用程序启动停止的监听器,里面记录应用的启动时间…

    • 创建引导上下文(Context环境)DefaultBootstrapContext c = createBootstrapContext()

      里面会获取到所有之前配置的 **Bootstrappers **取挨个执行 intitialize() 来完成对引导启动器上下文环境设置

    • 让当前应用进入headless 模式。java.awt.headless (简言之就是自力更生模式)

    • 获取所有 RunListener(运行监听器),为了方便所有Listener进行事件感知

      获取方式还是取去spring.factories文件中找org.springframework.boot.SpringApplicationRunListener

    • 遍历所有运行监听器 SpringApplicationRunListener 调用 starting() 方法

      (相当于通知所有感兴趣系统正在启动过程的人,项目正在 starting)

    • 保存命令行参数,ApplicationArguments (即主方法里面的args参数)

    • 准备环境 prepareEnvironment()

      • 返回或者创建基础环境信息对象,StandardServletEnvironment
      • 配置环境信息对象,读取所有的配置源的配置属性值。
      • 绑定环境信息
      • 调用所有运行监听器 SpringApplicationRunListenerenvironmentPrepared() 通知所有的监听器当前环境准备完成
    • 创建IOC容器 createApplicationContext()

      根据项目类型创建对应容器(当前是Servlet,即AnnotationConfigServletWebServerApplicationContext

    • 准备ApplicationContext,即IOC容器的基本信息, prepareContext()

      • 保存环境信息

      • IOC容器的后置处理流程

      • 应用初始化器,applyInitializers()

        遍历所有的ApplicationContextInitializer调用其initialize() ,来对ioc容器进行初始化扩展功能

        遍历所有的 运行监听器 SpringApplicationRunListener调用 contextPrepared(),通知所有的监听器上下文准备好了

      • 再次遍历监听器 调用其contextLoaded(),通知所有的监听器 上下文加载好了

    • 刷新IOC容器,refreshContext()

      里面就是SpringIOC的关键代码,创建容器中的所有组件(去看Spring注解版视频有讲)

    • 容器刷新完成后工作,afterRefresh()

    • 遍历所有的 运行监听器 SpringApplicationRunListener调用 started(),通知所有监控器启动好了

    • 调用所有runners,callRunners()

      • 获取容器中的 ApplicationRunnerCommandLineRunner
      • 合并所有Runner,按照优先级排序
      • 遍历所有的Runner,调用 run() 方法
    • 如果以上有异常,遍历所有的 运行监听器 SpringApplicationRunListener调用 failed()

    • 再次遍历所有的 运行监听器 SpringApplicationRunListener调用 running(), 通知所有的监听器 正在运行

    • running如果有问题,继续通知 failed ,调用所有 Listener 的 failed(),通知所有的监听器 当前失败了

关键组件:ApplicationContextInitializerApplicationListenerSpringApplicationRunListenerApplicationRunnerCommandLineRunner

前三个自定义的话需要配置在spring.factories文件里面,后两个需要放到容器中


详解refreshContext() -> AbstractApplicationContext.refresh()

  1. prepareRefresh() 刷新前的预处理工作

    • 记录时间,标记状态,打印日志
    • initPropertySources() 初始化一些属性设置,进去是空的留给子类做事情的
    • 校验属性的合法性
    • earlyApplicationEvents = new ListHashSet<ApplicationEvent> 保存容器的一些早期的事件
  2. obtainFreshBeanFactory() 获取BeanFactory

    • refreshBeanFactory() -> beanFactory = new DefaultListableBeanFactory() 创建了一个BeanFactory对象,设置序列化id
    • getBeanFactory() 返回刚刚 GenericApplicationContext 创建的BeanFactory,给本类使用
  3. prepareBeanFactory(beanFactory) 预处理BeanFactory,做一些设置

    • 设置类加载器,支持的的表达式解析器…

    • 添加部分BeanPostProcessor后置处理器,例如ApplicationCotextAwareProcessor

    • 设置忽略的自动装配的接口,例如EnvironmentAware,EmbeddedValueResolverAware…

    • 注册可以解析的自动装配:我们能在任何组件中自动注入,例如BeanFactory,ResourceLoader,ApplicationEventPublisher,ApplicationContext

    • 添加默认的AspectJ支持

    • 给BeanFactoy中注册一些能用的组件,

      例如:environment(ConfigurableEnvironment),systemProperties(Map),systemEnvironment(Map)要用也可也自动注入

  4. postProcessBeanFactory(beanFactory) BeanFactory准备工作完成后进行的后置处理工作

    进去是空的,子类可以重写在BeanFactory创建并预准备完成做进一步的设置 (以上都是BeanFactory的创建以及准备工作!)

  5. invokeBeanFactoryPostProcessors(beanFactory) 执行所有的BeanFactory后置处理器

    这步不仅执行BeanFactoryPostProcessor(在BeanFactory标准初始化之后,而且Bean实例还未创建的时候执行,后), 执行时都有优先级排序先后,还要执行BeanDefinitionRegistryPostProcessor (在所有Bean定义信息将要被加载的时候执行,前)

  6. registerBeanPostProcessors(beanFactory) 注册所有的Bean后置处理器,(作用:拦截Bean的创建过程)

    BeanPostProcessor 子接口:DestructionAwareBeanPostProcessor,InstantiationAwareBeanPostProcessor,SmartInstantiationAwareBeanPostProcessor,MergedBeanDefinitionPostProcessor…不同类型的接口在Bean创建前后执行时机可能是不一样,这个执行的时候也会有优先级排序

    • 注册:把不同类型的BeanPostProcessor保存到不同的集合中,再添加到BeanFactory中
    • 注册顺序:先注册实现ProorityOrdered,再注册Ordered,再注册没有实现接口的,再注册MergedBeanDefinitionPostProcessor类型的,最终还会注册一个ApplicationListenerDetector来在Bean创建完成后检查是否是ApplicationListener,如果是则把这个Bean放到容器中保存起来
  7. initMessageSource() 初始化MessageSource组件(做国际化,消息解析,消息绑定功能…)

    • 先获取BeanFactory,判断是否有id为messageSource的组件,有则使用,没有则创建一个默认的DelegatingMessageSource,并注册到容器中,我们用可以自动注入
  8. initApplicationEventMulticaster() 初始化事件派发器

    • 先获取BeanFactory,判断是否有id为applicationEventMulticaster的组件,有则使用,没有则创建一个简单的事件派发器SimpleApplicationEventMulticaster,并注册到容器中
  9. onRefresh() 进去是空的,留给子类(子容器)重写,在容器刷新的时候可以自定义一些逻辑

  10. registerListeners() 将项目中所有的监听器(ApplicationListener)注册进来

    • 在容器中拿到所有的ApplicationListener类型组件,添加到事件派发器中
    • 派发之前保存到容器的一些早期的事件earlyApplicationEvents
  11. finishBeanFactoryInitialization(beanFactory) 初始化剩下的所有的单实例Bean

    -> beanFactory.preInstantiateSingletons()

    • 先拿到所有Bean的定义信息,依次进行创建对象和初始化(有些Bean可能在前面以及创建完了)

    • 获取Bean的定义信息,判断,如果不是抽象的,是单实例的,不是懒加载的

      再判断是否是FactoryBean,如果是则调用FactoryBean的getObject()创建Bean。

      不是则调用getBean(beanName)创建对象 -> doGetBean(..)

      • 先获取缓存中保存的单实例Bean,如果能获取到,说明这个Bean之前以及被创建过了

        (所有的单实例Bean创建完都会保存到缓存中,就是个 Map类型,叫singletObjects)

      • 缓存中获取不到则开始Bean的创建流程

      • 先标记当前Bean已经被创建(防止多线程下重复创建),拿到Bean的定义信息

      • 获取当前Bean依赖的其他Bean,如果有的话还是调用getBean()把依赖的Bean先创建出来

      • 启动单实例的创建流程:createBean(beanName, mbd, args)

        • resolveBeforeInstantiation(..)

          让BeanPostProcessor (InstantiationAwareBeanPostProcessor类型的后置处理器)拦截Bean的创建,调用applyBeanPostProcessorsBeforeInstantiation(),applyBeanPostProcessorsAfterInitialization()方法,如果了返回代理对象,则Bean创建成功,直接return。如果没有返回对象,则进入下一步

        • doCreateBean(..)

          -> createBeanInstance(...) 创建Bean实例,里面利用工厂方法或者对象构造器创建实例

          -> applyMergedBeanDefinitionPostProcessors(..) 执行MergedBeanDefinitionPostProcessor类型的后置处理器postProcessMergedBeanDefinition()方法

          -> populateBean(..)Bean属性赋值,赋值之前拿到InstantiationAwareBeanPostProcessor类型的后置处理器,执行其postProcessAfterInstantiation()方法,再执行postProcessPropertyValues()方法,最后才进行赋值操作applyPropertyValues(),应用Bean属性的值

          -> initializeBean(..) Bean初始化,首先执行XxxAware接口的方法(回调)。然后执行后置处理器初始化之前的方法,即执行BeanPostProcessor类型的后置处理器的postProcessBeforeInitialization()方法。然后执行Bean的指定的初始化方法。然后执行后置处理器初始化之后的方法,即调用postProcessBeforeInitialization()方法。

          -> registerDisposableBeanIfNecessary(..) 注册Bean的销毁方法

      • 创建完成,则把单实例的Bean放到缓存中,singletObjects中,IOC容器就是这些Map,保存了单实例Bean,环境信息…

    • 遍历完所有的Bean信息,并利用getBean()创建成功后,来检查所有的Bean是否实现SmartInitializingSingleton 接口

      如果是则执行其afterSingletonsInstantiated()方法

  12. finishRefresh() 上面完成BeanFactory的初始化创建过程,IOC创建完成

    -> initLifecycleProcessor() 初始化和生命周期有关的后置处理器,先判断是否有id为lifecycleProcessor的组件,有则使用,没有则创建一个默认的DefaultLifecycleProcessor,并注册到容器中(可以写LifecycleProcessor实现类,可以在BeanFactory刷新完成以及关闭的时候进行调用)

    -> getLifecycleProcessor().onRefresh() 拿到到前面定义的声明周期处理器(监听BeanFactory的) 回调onReFresh()

    -> publishEvent(new ContextRefreshedEvent(this)) 发布容器刷新完成事件

简单总结:

  • Spring容器再启动的时候,先会保存所有注册进来的Bean的定义信息
  • Spring容器会再合适的时机创建这些Bean(时机1:用到这个Bean的时候。时机2:统一创建剩下所有Bean的时候)
  • 后置处理器:每一个Bean创建完成,都会使用各种后置处理器进行处理,增强Bean的功能(自动注入,AOP功能…)
    是空的,子类可以重写在BeanFactory创建并预准备完成做进一步的设置 (以上都是BeanFactory的创建以及准备工作!)
  1. invokeBeanFactoryPostProcessors(beanFactory) 执行所有的BeanFactory后置处理器

    这步不仅执行BeanFactoryPostProcessor(在BeanFactory标准初始化之后,而且Bean实例还未创建的时候执行,后), 执行时都有优先级排序先后,还要执行BeanDefinitionRegistryPostProcessor (在所有Bean定义信息将要被加载的时候执行,前)

  2. registerBeanPostProcessors(beanFactory) 注册所有的Bean后置处理器,(作用:拦截Bean的创建过程)

    BeanPostProcessor 子接口:DestructionAwareBeanPostProcessor,InstantiationAwareBeanPostProcessor,SmartInstantiationAwareBeanPostProcessor,MergedBeanDefinitionPostProcessor…不同类型的接口在Bean创建前后执行时机可能是不一样,这个执行的时候也会有优先级排序

    • 注册:把不同类型的BeanPostProcessor保存到不同的集合中,再添加到BeanFactory中
    • 注册顺序:先注册实现ProorityOrdered,再注册Ordered,再注册没有实现接口的,再注册MergedBeanDefinitionPostProcessor类型的,最终还会注册一个ApplicationListenerDetector来在Bean创建完成后检查是否是ApplicationListener,如果是则把这个Bean放到容器中保存起来
  3. initMessageSource() 初始化MessageSource组件(做国际化,消息解析,消息绑定功能…)

    • 先获取BeanFactory,判断是否有id为messageSource的组件,有则使用,没有则创建一个默认的DelegatingMessageSource,并注册到容器中,我们用可以自动注入
  4. initApplicationEventMulticaster() 初始化事件派发器

    • 先获取BeanFactory,判断是否有id为applicationEventMulticaster的组件,有则使用,没有则创建一个简单的事件派发器SimpleApplicationEventMulticaster,并注册到容器中
  5. onRefresh() 进去是空的,留给子类(子容器)重写,在容器刷新的时候可以自定义一些逻辑

  6. registerListeners() 将项目中所有的监听器(ApplicationListener)注册进来

    • 在容器中拿到所有的ApplicationListener类型组件,添加到事件派发器中
    • 派发之前保存到容器的一些早期的事件earlyApplicationEvents
  7. finishBeanFactoryInitialization(beanFactory) 初始化剩下的所有的单实例Bean

    -> beanFactory.preInstantiateSingletons()

    • 先拿到所有Bean的定义信息,依次进行创建对象和初始化(有些Bean可能在前面以及创建完了)

    • 获取Bean的定义信息,判断,如果不是抽象的,是单实例的,不是懒加载的

      再判断是否是FactoryBean,如果是则调用FactoryBean的getObject()创建Bean。

      不是则调用getBean(beanName)创建对象 -> doGetBean(..)

      • 先获取缓存中保存的单实例Bean,如果能获取到,说明这个Bean之前以及被创建过了

        (所有的单实例Bean创建完都会保存到缓存中,就是个 Map类型,叫singletObjects)

      • 缓存中获取不到则开始Bean的创建流程

      • 先标记当前Bean已经被创建(防止多线程下重复创建),拿到Bean的定义信息

      • 获取当前Bean依赖的其他Bean,如果有的话还是调用getBean()把依赖的Bean先创建出来

      • 启动单实例的创建流程:createBean(beanName, mbd, args)

        • resolveBeforeInstantiation(..)

          让BeanPostProcessor (InstantiationAwareBeanPostProcessor类型的后置处理器)拦截Bean的创建,调用applyBeanPostProcessorsBeforeInstantiation(),applyBeanPostProcessorsAfterInitialization()方法,如果了返回代理对象,则Bean创建成功,直接return。如果没有返回对象,则进入下一步

        • doCreateBean(..)

          -> createBeanInstance(...) 创建Bean实例,里面利用工厂方法或者对象构造器创建实例

          -> applyMergedBeanDefinitionPostProcessors(..) 执行MergedBeanDefinitionPostProcessor类型的后置处理器postProcessMergedBeanDefinition()方法

          -> populateBean(..)Bean属性赋值,赋值之前拿到InstantiationAwareBeanPostProcessor类型的后置处理器,执行其postProcessAfterInstantiation()方法,再执行postProcessPropertyValues()方法,最后才进行赋值操作applyPropertyValues(),应用Bean属性的值

          -> initializeBean(..) Bean初始化,首先执行XxxAware接口的方法(回调)。然后执行后置处理器初始化之前的方法,即执行BeanPostProcessor类型的后置处理器的postProcessBeforeInitialization()方法。然后执行Bean的指定的初始化方法。然后执行后置处理器初始化之后的方法,即调用postProcessBeforeInitialization()方法。

          -> registerDisposableBeanIfNecessary(..) 注册Bean的销毁方法

      • 创建完成,则把单实例的Bean放到缓存中,singletObjects中,IOC容器就是这些Map,保存了单实例Bean,环境信息…

    • 遍历完所有的Bean信息,并利用getBean()创建成功后,来检查所有的Bean是否实现SmartInitializingSingleton 接口

      如果是则执行其afterSingletonsInstantiated()方法

  8. finishRefresh() 上面完成BeanFactory的初始化创建过程,IOC创建完成

    -> initLifecycleProcessor() 初始化和生命周期有关的后置处理器,先判断是否有id为lifecycleProcessor的组件,有则使用,没有则创建一个默认的DefaultLifecycleProcessor,并注册到容器中(可以写LifecycleProcessor实现类,可以在BeanFactory刷新完成以及关闭的时候进行调用)

    -> getLifecycleProcessor().onRefresh() 拿到到前面定义的声明周期处理器(监听BeanFactory的) 回调onReFresh()

    -> publishEvent(new ContextRefreshedEvent(this)) 发布容器刷新完成事件

简单总结:

  • Spring容器再启动的时候,先会保存所有注册进来的Bean的定义信息
  • Spring容器会再合适的时机创建这些Bean(时机1:用到这个Bean的时候。时机2:统一创建剩下所有Bean的时候)
  • 后置处理器:每一个Bean创建完成,都会使用各种后置处理器进行处理,增强Bean的功能(自动注入,AOP功能…)
  • 事件驱动模式:ApplicationListener,做事件监听。ApplicationEventMulticaster:事件派发

本笔记参考视频:SpringBoot2





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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值