SpringBoot2——了解自动配置原理

1、SpringBoot特点

1.1、依赖管理

我们查看一下 pom.xml 文件中的项目依赖情况:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.6.6</version>
</parent>

点进去之后发现其还有父项目:

<parent>
  	<groupId>org.springframework.boot</groupId>
  	<artifactId>spring-boot-dependencies</artifactId>
 	<version>2.6.6</version>
</parent>

点进去之后:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <modelVersion>4.0.0</modelVersion>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-dependencies</artifactId>
  <version>2.6.6</version>
  <packaging>pom</packaging>
  <name>spring-boot-dependencies</name>
  <description>Spring Boot Dependencies</description>
  <url>https://spring.io/projects/spring-boot</url>
  <licenses>
    <license>
      <name>Apache License, Version 2.0</name>
      <url>https://www.apache.org/licenses/LICENSE-2.0</url>
    </license>
  </licenses>
  <developers>
    <developer>
      <name>Pivotal</name>
      <email>info@pivotal.io</email>
      <organization>Pivotal Software, Inc.</organization>
      <organizationUrl>https://www.spring.io</organizationUrl>
    </developer>
  </developers>
  <scm>
    <url>https://github.com/spring-projects/spring-boot</url>
  </scm>
  <properties>
    <activemq.version>5.16.4</activemq.version>
    <antlr2.version>2.7.7</antlr2.version>
    <appengine-sdk.version>1.9.95</appengine-sdk.version>
    <artemis.version>2.19.1</artemis.version>
      ......

父项目的主要功能就是依赖管理,几乎声明了所有开发中常用的依赖的版本号,自动版本仲裁机制。

  • 无需关注版本号,自动版本仲裁

    1. 引入依赖默认都可以不写版本;
    2. 引入非版本仲裁的jar,要写版本号。

    比如:我们想要引入mysql的依赖

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    

    在这里我们不需要指定版本号。刷新maven后,查看版本号为:8.0.28

    可以查找一下spring-boot-dependencies文件中指定的mysql版本号:

    <mysql.version>8.0.28</mysql.version>
    
  • 可以修改默认版本号

    1. 查看spring-boot-dependencies里面规定当前依赖的版本用的key;
    2. 在当前项目里面重写配置

    比如:就修改mysql的驱动版本为5.1.43

    查看spring-boot-dependencies里面规定当前依赖的版本用的key:(上边已经提到过了,使用的是<mysql.version>标签)

    在当前项目的 pom.xml 中重写:

    <properties>
        <mysql.version>5.1.43</mysql.version>
    </properties>
    

    再次刷新maven,可以看到mysql的包就是5.1.43了。

开发导入starter场景启动器

  1. spring-boot-starter-* : *就某种场景

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

  3. SpringBoot所有支持的场景

    https://docs.spring.io/spring-boot/docs/current/reference/html/using.html#using.build-systems.starters

  4. *-spring-boot-starter: 第三方为我们提供的简化开发的场景启动器

  5. 所有场景启动器最底层的依赖

    <dependency>
      	<groupId>org.springframework.boot</groupId>
      	<artifactId>spring-boot-starter</artifactId>
      	<version>2.6.6</version>
      	<scope>compile</scope>
    </dependency>
    

可以使用IDEA的功能,查看一下jar包的依赖关系:

在这里插入图片描述

1.2、自动配置

  • 自动配好Tomcat

    1. 引入Tomcat依赖

      web的场景spring-boot-starter-web中已经引入Tomcat:

      <dependency>
        	<groupId>org.springframework.boot</groupId>
        	<artifactId>spring-boot-starter-tomcat</artifactId>
        	<version>2.6.6</version>
        	<scope>compile</scope>
      </dependency>
      
    2. 配置Tomcat

      可以在统一的配置文件中进行配置即可。

  • 自动配好SpringMVC

    1. 引入SpringMVC全套组件

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

        • SpringBoot帮我们配置好了所有web开发的常见场景

      验证:在主程序中我们打印一下IOC容器中注册的Bean:

      @SpringBootApplication
      public class MainApplication {
          public static void main(String[] args) {
              //1、返回IOC容器
              ConfigurableApplicationContext run =
                      SpringApplication.run(MainApplication.class, args);
              //2、查看容器里边的组件
              String[] names = run.getBeanDefinitionNames();
              for (String name :
                      names) {
                  System.out.println(name);
              }
          }
      
      }
      

      第一、在web应用中,肯定要配置的就是前端控制器:dispatcherServlet

      org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration$DispatcherServletConfiguration dispatcherServlet
      

      第二、解决字符乱码问题,需要配置字符编码过滤器:characterEncodingFilter

      org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration
      characterEncodingFilter
      

      第三、视图解析器

      ......
      defaultViewResolver
      viewResolver
      ......
      

      第四、文件上传解析器:multipartResolver

      org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration
      multipartConfigElement multipartResolver
      
  • 默认的包结构

    • 主程序所在包及其下面的所有子包里面的组件都会被默认扫描进来

    • 无需以前的包扫描配置

    • 想要改变扫描路径,@SpringBootApplication(scanBasePackages="com.atguigu")

      • 或者 @ComponentScan 指定扫描路径

      查看 @SpringBootApplication 源码:

      @Target({ElementType.TYPE})
      @Retention(RetentionPolicy.RUNTIME)
      @Documented
      @Inherited
      @SpringBootConfiguration
      @EnableAutoConfiguration
      @ComponentScan(
          excludeFilters = {@Filter(
          type = FilterType.CUSTOM,
          classes = {TypeExcludeFilter.class}
      ), @Filter(
          type = FilterType.CUSTOM,
          classes = {AutoConfigurationExcludeFilter.class}
      )}
      )
      public @interface SpringBootApplication {
          ......
      

      是一个组合了其他注解的组合注解。

  • 各种配置拥有默认值

    • 默认配置最终都是映射到某个类上,如:MultipartProperties

      在这里插入图片描述

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

  • 按需加载所有自动配置项

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

2、容器功能

2.1、组件添加

准备两个组件User和Pet:

public class Pet {
    private String name;
    ......
public class User {
    private String name;
    private Integer age;
    ......

之前使用Spring进行组件注册,我们需要创建一个**xml配置文件**,在里边使用 <bean> 标签进行组件注册。

使用SpringBoot,则可以使用 @Configuration 注解。

2.1.1、@Configuration

使用 @Configuration 注解标注一个类(配置类),就相当于说明这是一个配置文件。

@Configuration //告诉SpringBoot,这是一个配置类 == 一个配置文件
public class MyConfig {

    @Bean //给容器中添加组件,以方法名作为组件的id,返回类型是组件类型。返回的值,就是组件在容器中的实例
    public User user01(){
        return new User("zhangsan", 18);
    }

    @Bean("tom") //自定义组件名id
    public Pet tomPet(){
        return new Pet("tomPet");
    }
}

使用 @Bean 注解给容器中注册组件:以方法名作为组件的id,返回类型是组件类型。返回的值,就是组件在容器中的实例。

也就是说,id默认为方法名,也可以自定义组件名id。

那么,如何查看容器中是否包含指定id的类呢?

在主程序中,从IOC容器中获取指定id名的Bean:

@SpringBootApplication
public class MainApplication {
    public static void main(String[] args) {
        //1、返回IOC容器
        ConfigurableApplicationContext run =
                SpringApplication.run(MainApplication.class, args);
        //2、查看容器里边的组件
        String[] names = run.getBeanDefinitionNames();
        for (String name :
                names) {
            System.out.println(name);
        }

        //3、从容器中获取组件
        Pet tom01 = run.getBean("tom", Pet.class);
        Pet tom02 = run.getBean("tom", Pet.class);

        System.out.println(tom01 == tom02); //true
    }

}

也就是说,使用 @Bean 默认就是单实例。而且配置类本身也是一个组件

一些性质:

  1. 使用 @Bean 默认就是单实例

  2. 配置类本身也是一个组件

    MyConfig myConfig = run.getBean(MyConfig.class);
    System.out.println(myConfig);
    //com.atguigu.boot.config.MyConfig$$EnhancerBySpringCGLIB$$f0cbc915@c68a5f8
    

    可以看到获取到的对象本身就是代理对象

  3. @Configuration()中的属性proxyBeanMethods(代理bean方法),默认为true,意味着外部无论对配置类中的这个组件注册方法调用多少次,取到的都是之前注册到容器中的单实例对象

    User user01 = myConfig.user01();
    User user02 = myConfig.user01();
    
    System.out.println(user01 == user02); //true
    

    从上面可知,myConfig本身就是代理对象。那么调用方法就是代理对象调用方法。SpringBoot总会检查这个组件是否在容器中有。总之,就是保持组件单实例

    如果把属性proxyBeanMethods改为false:

    @Configuration(proxyBeanMethods = false)
    
    System.out.println(user01 == user02); //false
    

    这个属性解决的最主要问题就是:组件依赖

    1. Full(proxyBeanMethods = true)【保证每个@Bean方法被调用多少次返回的组件都是单实例】
    2. Lite(proxyBeanMethods = false)【每个@Bean方法被调用多少次返回的组件都是新创建的】

    组件依赖必须使用Full模式默认。其他默认是否Lite模式。

    一个例子:

    我们想要在User类中添加一个Pet类的属性,也就是使得User组件依赖于Pet组件

    public class User {
        private String name;
        private Integer age;
    
        private Pet pet;
        ......
    

    在配置文件中,在创建User的方法中获取Pet:

    @Configuration(proxyBeanMethods = true) //告诉SpringBoot,这是一个配置类 == 一个配置文件
    public class MyConfig {
    
        @Bean //给容器中添加组件,以方法名作为组件的id,返回类型是组件类型。返回的值,就是组件在容器中的实例
        public User user01(){
            User zhangsan = new User("zhangsan", 18);
            //user组件依赖了Pet组件
            zhangsan.setPet(tomPet());
            return zhangsan;
        }
    
        @Bean("tom") //自定义组件名id
        public Pet tomPet(){
            return new Pet("tomPet");
        }
    }
    

    验证一下User类中的Pet和创建的Pet是否是同一个:

    User user = run.getBean("user01", User.class);
    Pet tom = run.getBean("tom", Pet.class);
    
    System.out.println("用户的宠物:"+(user.getPet() == tom)); //true
    

    但是如果把proxyBeanMethods改为false,程序就会报错!但可以运行:

    System.out.println("用户的宠物:"+(user.getPet() == tom)); //false
    

基于上面的测试,总结最佳实战:

  • 配置类组件之间无依赖关系用Lite模式加速容器启动过程,减少判断
  • 配置类组件之间有依赖关系,方法会被调用得到之前单实例组件,用Full模式

2.1.2、@Bean、@Component、@Controller、@Service、@Repository

以前使用的注解也都还可以使用。

  • @Bean:注册组件
  • @Component:组件
  • @Controller:控制器
  • @Service:业务逻辑组件
  • @Repository:数据库层组件

2.1.3、@ComponentScan、@Import

  • @ComponentScan:包扫描规则

  • @Import:给容器中自动创建组件、默认组件的名字就是全类名

    @Import({User.class})
    @Configuration(proxyBeanMethods = false) //告诉SpringBoot,这是一个配置类 == 一个配置文件
    public class MyConfig {
        ......
    

    测试:

    System.out.println("------------------");
    String[] beanNamesForType = run.getBeanNamesForType(User.class);
    for (String s : beanNamesForType) {
        System.out.println(s);
    }
    
    ------------------
    com.atguigu.boot.bean.User //@Import导入的,全类名
    user01 //是配置类中方法导入的
    

    @Import高级用法

2.1.4、@Conditional

条件装配:满足Conditional指定的条件,则进行组件注入

在这里插入图片描述

@ConditionalOnBean 为例(容器中有哪些组件了才注册…):

先把配置类中的@Bean("tom")注释掉,这就说明,tomPet()只是一个普通的方法了,不会向容器中注册组件。验证:

//@Bean("tom") //自定义组件名id
public Pet tomPet(){
    return new Pet("tomPet");
}
boolean tom = run.containsBean("tom");
System.out.println("容器中Tom组件:"+tom); //容器中Tom组件:false

User中还是会调用tomPet()方法,容器中还是会有User组件:

boolean user01 = run.containsBean("user01");
System.out.println("容器中User组件:"+user01); //容器中User组件:true

我们想要的效果是:如果容器中没有tom组件,那么也就不用向容器中注册User组件了。

@ConditionalOnBean(name = "tom")
@Bean //给容器中添加组件,以方法名作为组件的id,返回类型是组件类型。返回的值,就是组件在容器中的实例
public User user01(){
    User zhangsan = new User("zhangsan", 18);
    //user组件依赖了Pet组件
    zhangsan.setPet(tomPet());
    return zhangsan;
}
boolean user01 = run.containsBean("user01");
System.out.println("容器中User组件:"+user01); //容器中User组件:false

也可以将@ConditionalOnBean标注在类上,这就表明:如果没有tom组件,那么该类中的所有组件(包括该类)将不再注册

boolean tom = run.containsBean("tom");
System.out.println("容器中Tom组件:"+tom); //容器中Tom组件:false

boolean user01 = run.containsBean("user01");
System.out.println("容器中User组件:"+user01); //容器中User组件:false

boolean myConfig = run.containsBean("myConfig");
System.out.println("容器中myConfig组件:"+myConfig); //容器中myConfig组件:false

那么 @ConditionalOnMissingBean (容器中没有哪些组件了才注册…):

//@ConditionalOnBean(name = "tom")
@ConditionalOnMissingBean(name = "tom2")
@Import({User.class})
@Configuration(proxyBeanMethods = true) //告诉SpringBoot,这是一个配置类 == 一个配置文件
public class MyConfig {
    ......

验证:

boolean tom = run.containsBean("tom");
System.out.println("容器中Tom组件:"+tom); //容器中Tom组件:true

boolean user01 = run.containsBean("user01");
System.out.println("容器中User组件:"+user01); //容器中User组件:true

boolean myConfig = run.containsBean("myConfig");
System.out.println("容器中myConfig组件:"+myConfig); //容器中myConfig组件:true

2.2、原生xml配置文件引入:@ImportResource

原生的beans.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.atguigu.boot.bean.User">
        <property name="name" value="zhangsan"></property>
        <property name="age" value="18"></property>
    </bean>

    <bean id="hehe" class="com.atguigu.boot.bean.Pet">
        <property name="name" value="tomcat"></property>
    </bean>
</beans>

那么SpringBoot中是否会有这些组件呢?

boolean haha = run.containsBean("haha");
System.out.println("容器中haha组件:"+haha); //容器中haha组件:false

boolean hehe = run.containsBean("hehe");
System.out.println("容器中hehe组件:"+hehe); //容器中hehe组件:false

那么,就可以使用@ImportResource引入xml中的配置:

//@ConditionalOnBean(name = "tom")
@ConditionalOnMissingBean(name = "tom2")
@Import({User.class})
@ImportResource("classpath:beans.xml")
@Configuration(proxyBeanMethods = true) //告诉SpringBoot,这是一个配置类 == 一个配置文件
public class MyConfig {
    ......

再次测试:

boolean haha = run.containsBean("haha");
System.out.println("容器中haha组件:"+haha); //容器中haha组件:true

boolean hehe = run.containsBean("hehe");
System.out.println("容器中hehe组件:"+hehe); //容器中hehe组件:true

2.3、配置(properties)绑定

如何使用Java读取到properties文件中的内容,并且把它封装到JavaBean中,以供随时使用;

新建一个Car类:

public class Car {
    private String brand;
    private Integer price;
    ......

application.properties配置文件中配置其属性:

mycar.brand=BYD
mycar.price=100000

要想获取到application.properties配置文件的属性,有三种方法

第一种方法:使用@Component + @ConfigurationProperties注解:

@Component
@ConfigurationProperties(prefix = "mycar")
public class Car {
    private String brand;
    private Integer price;
    ......

我们在控制器中定义方法:

@RestController
public class HelloController {

    @Autowired
    Car car;

    @RequestMapping("/car")
    public Car car(){
        return car;
    }
    ......

测试:

在这里插入图片描述

第二种方法:使用@EnableConfigurationProperties + @ConfigurationProperties

//@Component
@ConfigurationProperties(prefix = "mycar")
public class Car {

在配置类中使用@EnableConfigurationProperties

@ConditionalOnMissingBean(name = "tom2")
@Import({User.class})
@ImportResource("classpath:beans.xml")
@EnableConfigurationProperties(Car.class) //1、开启属性绑定;2、把组件自动注册到容器中
@Configuration(proxyBeanMethods = true) //告诉SpringBoot,这是一个配置类 == 一个配置文件
public class MyConfig {

@EnableConfigurationProperties有两个作用:

  1. 开启属性绑定;
  2. 把组件自动注册到容器中。

3、自动配置原理入门

3.1、引导加载自动配置类

我们从主程序开始看起:

//@SpringBootApplication
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan("com.atguigu.boot")
public class MainApplication {

我们说,@SpringBootApplication注解就相当于@SpringBootConfiguration+@EnableAutoConfiguration+@ComponentScan("com.atguigu.boot")

可以查看一下@SpringBootApplication注解源码:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
    ......
  1. @SpringBootConfiguration

    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Configuration
    @Indexed
    public @interface SpringBootConfiguration {
        ......
    

    @Configuration。代表当前是一个配置类。

  2. @ComponentScan

    指定扫描哪些。

  3. @EnableAutoConfiguration

    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    @AutoConfigurationPackage
    @Import({AutoConfigurationImportSelector.class})
    public @interface EnableAutoConfiguration {
        String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
    
        Class<?>[] exclude() default {};
    
        String[] excludeName() default {};
    }
    

    @AutoConfigurationPackage+@Import。那么这两个注解分别的作用是什么呢?

    @AutoConfigurationPackage

    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    @Import({Registrar.class}) //给容器导入一个组件
    public @interface AutoConfigurationPackage {
        String[] basePackages() default {};
    
        Class<?>[] basePackageClasses() default {};
    }
    

    那么,导入的这个组件Registrar是什么?

    static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
        Registrar() {
        }
    
        public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
            AutoConfigurationPackages.register(registry, (String[])(new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames().toArray(new String[0]));
        }
    
        public Set<Object> determineImports(AnnotationMetadata metadata) {
            return Collections.singleton(new AutoConfigurationPackages.PackageImports(metadata));
        }
    }
    

    利用Registrar给容器中导入一系列组件。可以打个断点,调试:

    可以看到registerBeanDefinitions()方法传入了两个参数:AnnotationMetadata metadataBeanDefinitionRegistry registry

    1. AnnotationMetadata metadata:注解的元信息

      在这里插入图片描述

      其中就包含了注解标注的位置。位于class com.atguigu.boot.MainApplication

      然后在方法中,new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames()拿到元信息并获取其包名。

      在这里插入图片描述

      然后,将获取到的包名toArray(new String[0])封装成数组,并作为AutoConfigurationPackages.register()的参数,就相当于把该包下的所有组件批量注册进容器。

    @Import

    可以看到@Import({AutoConfigurationImportSelector.class}),导入了一个AutoConfigurationImportSelector的类。查看源码:

    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        if (!this.isEnabled(annotationMetadata)) {
            return NO_IMPORTS;
        } else {
            AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
            return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
        }
    }
    

    其中,有一个selectImports()方法,返回的是String的数组。这个方法主要是调用getAutoConfigurationEntry()方法,返回的是一个autoConfigurationEntry,之后autoConfigurationEntry.getConfigurations()打包成数组返回。因此,我们需要重点关注的就是getAutoConfigurationEntry()方法:

    protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
        if (!this.isEnabled(annotationMetadata)) {
            return EMPTY_ENTRY;
        } else {
            AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
            List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
            configurations = this.removeDuplicates(configurations);
            Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
            this.checkExcludedClasses(configurations, exclusions);
            configurations.removeAll(exclusions);
            configurations = this.getConfigurationClassFilter().filter(configurations);
            this.fireAutoConfigurationImportEvents(configurations, exclusions);
            return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
        }
    }
    

    可以看到其中有一个List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);语句(获取到所有需要导入到容器中的配置类),在获取到configurations的List集合后,下边进行了一系列的操作,最终返回。

    在这里插入图片描述

    那么它是如何进行获取配置类的呢?打上端点,调试:

    List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
    

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

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

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

    其中最重要的是:spring-boot-autoconfigure-2.6.6.jar包里面也有META-INF/spring.factories

    在这里插入图片描述

    要加载的其实是在这里边写死的。其中有一个# Auto Configure

    # Auto Configure
    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
    ......
    

    一共有133个,正好对应上面的configurations的个数。

    其实,就是写死了。SpringBoot一启动,就要给容器中加载的配置类(组件)。

    spring-boot-autoconfigure-xxx.jar/META-INF/spring.factories

    但实际中,我们在加载SpringBoot时根本没有这么多组件:

    //获取加载组件的数目
    int beanDefinitionCount = run.getBeanDefinitionCount();
    System.out.println(beanDefinitionCount); //1443
    

3.2、按需开启自动配置项

虽然spring.factories中的133个场景的所有自动配置启动的时候默认全部加载。xxxxAutoConfiguration

按照条件装配规则(@Conditional),最终会按需配置。

比如:AOP的类

在这里插入图片描述

3.3、修改默认配置

分析AOP的自动配置能否生效:

在这里插入图片描述

spring.factories中确实写上了AopAutoConfiguration。但它是不是真的能加载呢?

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnProperty(
    prefix = "spring.aop", //配置文件中是否有spring.aop的配置
    name = {"auto"}, //如果配置有auto
    havingValue = "true", //并且值为true
    matchIfMissing = true //就算没配置,也默认值为true
)
public class AopAutoConfiguration {
    ......

首先,使用@Configuration,说明这是一个配置类。接着使用了@ConditionalOnProperty,具体信息上边代码注释写明了。

在其中还定义了两个Class(类):

第一个:

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnClass({Advice.class})
static class AspectJAutoProxyingConfiguration {
    ......

这也是一个@Configuration,并且@ConditionalOnClass是否使用了Advice.class。具体看看Advice.class

import org.aspectj.weaver.Advice;

可以看到Advice.class来自于aspectj。如果我们没有导入aspectj,那么这个类就不会生效,自然其下边的也都不会生效。

第二个:

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnMissingClass({"org.aspectj.weaver.Advice"}) //没有org.aspectj.weaver.Advice注册
@ConditionalOnProperty( //同上
    prefix = "spring.aop",
    name = {"proxy-target-class"},
    havingValue = "true",
    matchIfMissing = true
)
static class ClassProxyingConfiguration {
    ClassProxyingConfiguration() {
        ......

@Configuration表名也是一个配置类。@ConditionalOnMissingClass也是按照条件注册。

分析web应用时,DispatcherServlet自动加载类时,有一段这样的代码:

@Bean
@ConditionalOnBean({MultipartResolver.class}) //容器中有这个类型组件
@ConditionalOnMissingBean( //容器中没有这个名字的组件
    name = {"multipartResolver"}
)
public MultipartResolver multipartResolver(MultipartResolver resolver) {
    //给@Bean标注的方法传入了对象参数,这个参数的值就会从容器中找
    //SpringMVC multipartResolver。防止有些用户配置的文件上传解析器不符合规范(意思是即使用户起名不是multipartResolver,但经过这个方法处理后,名字还是multipartResolver)
    return resolver;
}

给容器中添加文件上传解析器。具体的分析如注释。

我们再看一下HttpEncodingAutoConfiguration自动配置类,我们知道这个是解决请求中文乱码问题的。

做一个测试:

@RequestMapping("/hello")
public String handle01(@RequestParam("name") String name){
    return "Hello, Spring Boot 2!" + "你好:" + name;
}
@Configuration( //这是一个配置类
    proxyBeanMethods = false
)
@EnableConfigurationProperties({ServerProperties.class}) //配置绑定
@ConditionalOnWebApplication(
    type = Type.SERVLET
)
@ConditionalOnClass({CharacterEncodingFilter.class}) //是否加载了CharacterEncodingFilter过滤器
@ConditionalOnProperty(
    prefix = "server.servlet.encoding",	//配置文件中是否有server.servlet.encoding
    value = {"enabled"},	//默认是enabled
    matchIfMissing = true //即使没有,也是true
)
public class HttpEncodingAutoConfiguration {
    ......

来看看下边做了什么:

@Bean
@ConditionalOnMissingBean //如果容器中配置了,就不配了;反之,这里配置
public CharacterEncodingFilter characterEncodingFilter() {
    CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
    filter.setEncoding(this.properties.getCharset().name());
    filter.setForceRequestEncoding(this.properties.shouldForce(org.springframework.boot.web.servlet.server.Encoding.Type.REQUEST));
    filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.web.servlet.server.Encoding.Type.RESPONSE));
    return filter;
}

注册了一个CharacterEncodingFilter,解决字符编码问题。

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

总结:

  • SpringBoot先加载所有的自动配置类 xxxxxAutoConfiguration
  • 每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值。xxxxProperties里面拿。xxxProperties和配置文件进行了绑定
  • 生效的配置类就会给容器中装配很多组件
  • 只要容器中有这些组件,相当于这些功能就有了
  • 自定义配置
    • 用户直接自己@Bean替换底层的组件
    • 用户去看这个组件是获取的配置文件什么值就去修改

xxxxxAutoConfiguration —> 组件 —> xxxxProperties里面拿值 ----> application.properties

3.4、最佳实践

  • 引入场景依赖

    https://docs.spring.io/spring-boot/docs/current/reference/html/using.html#using.build-systems.starters

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

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

    • 参照文档修改配置项
      • https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#appendix.application-properties
      • 自己分析。xxxxProperties绑定了配置文件的哪些。
    • 自定义加入或者替换组件
      • @Bean@Component
    • 自定义器 XXXXXCustomizer…

4、开发小技巧

4.1、Lombok

简化JavaBean开发。

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>
@Component
@ConfigurationProperties(prefix = "mycar")
@Data //属性的getter和setter方法
@ToString //toString方法
@AllArgsConstructor //全参构造器
@NoArgsConstructor //无参构造器
@EqualsAndHashCode //使用属性重写EqualsAndHashCode方法
public class Car {
    private String brand;
    private Integer price;
}

使用到的注解:@Data+@ToString+@AllArgsConstructor+@NoArgsConstructor+@EqualsAndHashCode

@RestController
@Slf4j
public class HelloController {

    @Autowired
    Car car;

    @RequestMapping("/car")
    public Car car(){
        return car;
    }

    @RequestMapping("/hello")
    public String handle01(@RequestParam("name") String name){
        log.info("请求进来了...");
        return "Hello, Spring Boot 2!" + "你好:" + name;
    }
}

还有@Slf4j注解的日志功能。

4.2、dev-tools

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

热部署。只需要[Ctrl]+F9

不仅更新静态资源,类做了改变也会重启。

4.3、Spring Initailizr(项目初始化向导)

  1. 选择我们需要的开发场景
  2. 自动依赖引入
  3. 自动创建项目结构
  4. 自动编写好主配置类
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值