Loading

SpringBoot自动配置学习

自动配置是SpringBoot的一大优势。比起基本的Spring框架,使用SpringBoot可以减少开发人员大量的配置

本文章对SpringBoot的自动配置记录一下学习过程

1. 相关注解

在深入了解SpringBoot的自动配置原理之前,需要了解一些底层的注解

1.1 @Configuration

在使用Spring的时候,我们需要编写配置文件(如applicationContext.xml)将Java对象放入IOC容器中,但在使用SpringBoot的时候,这些配置文件是不起作用的,那么我们可以使用@Configuration注解来将Java对象放入IOC容器中

@Configuration注解作用在一个类上,这个类被称为配置类,相当于Spring中的配置文件。在配置类中,我们可以定义要放入IOC容器的对象。另外,配置类自身也会被放入IOC容器中

@Configuration
public class MyConfig {

    @Bean
    public User user() {
        return new User("Tom", 18);
    }

    @Bean("tomcat")
    public Pet pet() {
        return new Pet("tomcat");
    }
}

上述代码中,注解@Configuration标注的类是MyConfig,那么这个类就是一个配置类。在这个类中,定义了两个函数,分别返回一个User对象和一个Pet对象,并且在方法上加了@Bean注解,表示将返回的对象作为组件添加到IOC容器中。其中,默认以方法名作为组件的id,返回类型就是组件类型,方法返回的值就是在容器中具体的对象实例。比如,第一个方法user(),表示将一个User类型的实例添加到IOC容器中,这个User实例有两个属性,一个是name,值为Tom,另一个是age,值为18,这个组件的id为user。另外,@Bean注解还可以定义组件的id,比如第二个方法中,将一个Pet类型的实例添加到IOC容器中,Pet实例有一个属性为name,值为tomcat,这个组件的id被命名为tomcat

需要注意的是,配置类中配置的组件默认是单实例的

@Configuration注解内有一个变量proxyBeanMethods,表示对配置类中的方法是否进行代理,规定是Full模式还是Lite模式。默认为true,是Full模式,表示配置类中每个被@Bean注解标注的方法被调用多少次返回的都是IOC容器中的单实例对象。如果设置@Configuration(proxyBeanMethods=false),那么是Lite模式,那么配置类中被@Bean注解标注的方法被调用的话会返回新创建的对象,而不是IOC容器中的对象。

对于Full模式和Lite模式,配置类中组件之间没有依赖关系的话用Lite模式可以加速容器启动过程,减少判断,而配置类中组件之间存在依赖关系,就必须用Full模式

public static void main(String[] args) {
    ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);
    User user1 = run.getBean("user", User.class);
    User user2 = run.getBean("user", User.class);
    System.out.println(user1 == user2);  // true

    MyConfig config = run.getBean(MyConfig.class);  // 配置类本身也放入IOC容器
    System.out.println(config);  // Full模式下,config是被CGLIB增强的代理对象,Lite模式下,config是普通对象

    User user3 = config.user();
    User user4 = config.user();
    System.out.println(user3 == user4);  // 默认Full模式下结果为true,Lite模式下结果为false
}

Full模式可以解决容器内的组件依赖问题,假设User类中有个属性是Pet类,且在容器内user组件的Pet属性是tomcat组件,那么使用Full模式可以解决这一问题

@Configuration(proxyBeanMethods=false)
public class MyConfig {

    @Bean
    public User user() {
        User user = new User("Tom", 18);
        user.setPet(pet());  // 调用pet()方法
        return user;
    }

    @Bean("tomcat")
    public Pet pet() {
        return new Pet("tomcat");
    }
}
public static void main(String[] args) {
    ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);
    User user = run.getBean("user", User.class);
    Pet pet = run.getBean("tomcat", Pet.class);
    System.out.println(user.getPet() == pet);  // Full模式下,为true,Lite模式下,为false
}

可以看到,在Full模式下,user组件内的Pet属性是容器中的tomcat组件,在Lite模式下,user组件内的Pet属性是新创建的Pet类对象

1.2 @Component,@Controller,@Service,@Repository

这些注解是Spring的原生注解,在SpringBoot环境下也可以使用,只要被标注的类在SpringBoot的包扫描路径下(默认是主程序MainApplication所在包及其子包),就能被加入IOC容器

1.3 @Import

@Import注解可以用在容器中的组件上,表示给容器导入组件

@Import(User.class, DBHelper.class)
@Configuration
public class MyConfig {
}

上述代码表示在容器中添加User组件和DBHelper组件,默认组件的名字是全类名

1.4 @Conditional

条件装配,只有满足Conditional指定的条件时,才添加组件。比如@ConditionalOnBean表示只有当某个组件在容器中时,被标注的组件才被添加到容器中

@Configuration
public class MyConfig {

    @Bean("tom")
    public Pet tomcatPet() {
        return new Pet("tomcat");
    }

    @ConditionalOnBean(name="tom")
    @Bean
    public User user01() {
        User zhangsan = new User("zhangsan", 18);
        zhangsan.setPet(tomcatPet());
        return zhangsan;
    }
}

上述代码表示当tom组件在容器中时,user01组件才会被添加到容器中,最终,两个组件都在容器中

注意点:@ConditionalOnBean注解会最晚扫描组件类,并且是从上到下扫描,如果上述代码的两个方法交换次序的话,user01组件是不会被添加到容器中的,因为user01组件需要tom组件,而这个时候tom组件还未添加到容器中

@Conditional相关注解还可以使用在配置类上,表示类中所有定义的组件要满足一定条件时才全部加入到容器中

1.5 @ImportResource

在Spring项目中,我们使用xml配置文件导入组件,但在SpringBoot中,使用配置类导入组件。但对于一些老的第三方包,只有xml配置文件,对于这些组件,我们可以使用@ImportResource注解导入配置文件,从而导入组件到容器中

@ImportResource(classpath:beans.xml)
@Configuration
public class MyConfig {
}

上述代码表示将beans.xml配置文件中定义的组件加入到IOC容器中

1.6 @ConfigurationProperties

使用@ConfigurationProperties注解可以将properties配置文件中的数据绑定到容器中的组件属性上,叫做属性配置

比如,在配置文件中有

user.username=Tom
user.age=18

另外,我们有一个User类

@ConfigurationProperties(prefix="user")
@Component
public class User {
    String name;
    int age;
    // get, set方法
}

注解内prefix属性是配置文件中定义的前缀名,这样就把配置文件中的数据注入到容器中的User组件属性上,这种做法就相当于

@Component
public class User {

    @Value("Tom")
    String name;

    @Value("18")
    int age;
    // get, set方法
}

或者

@Component
public class User {

    @Value("${user.username}")
    String name;

    @Value("${user.age}")
    int age;
    // get, set方法
}

第一种做法,数据和代码的耦合度很高,而使用配置文件存放数据耦合度很低。对于第二种做法,虽然数据和代码的耦合度降低了,@Value适用于配置较少的场景,@ConfigurationProperties适用于配置较多的场景

另外,除了使用@Component + @ConfigurationProperties注解配合的方式,还可以使用@ConfigurationProperties + @EnableConfigurationProperties注解配合的方式进行属性配置,只是@EnableConfigurationProperties注解是使用在配置类上的,而不是特定组件类上

比如

@ConfigurationProperties(prefix="user")
public class User {
    String name;
    int age;
    // get, set方法
}

我们定义了一个User类,需要绑定配置文件中的user前缀定义的数据

@EnableConfigurationProperties(User.class)
@Configuration
public class MyConfig {
}

在配置类上,我们使用@EnableConfigurationProperties来开启User类的属性配置功能,并将User组件自动添加到容器中

2. 引导加载自动配置类

每个SpringBoot项目都有个MainApplication类,并且在类上有@SpringBootApplication注解,我们从注解入手,分析SpringBoot自动配置原理

我们查看@SpringBootApplication注解的源代码,在这里使用的SpringBoot版本是2.4.1

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

可以看到,@SpringBootApplication注解内部还有多个注解,分别是@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan这三个注解

2.1 @SpringBootConfiguration

我们首先查看@SpringBootConfiguration注解的源代码

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
    @AliasFor(
        annotation = Configuration.class
    )
    boolean proxyBeanMethods() default true;
}

发现这就是一个@Configuration注解,表示当前是一个配置类,也就是说,MainApplication这个主程序也是SpringBoot中的一个配置类,只不过是一个核心配置类,也叫主配置类

2.2 @ComponentScan

这个注解表示要扫描哪些包,使用了两个自定义的扫描器TypeExcludeFilter.classAutoConfigurationExcludeFilter.class

2.3 @EnableAutoConfiguration

最后,我们查看@EnableAutoConfiguration注解的源代码

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    // ...
}

可以看到,@EnableAutoConfiguration注解是@AutoConfigurationPackage@Import({AutoConfigurationImportSelector.class})这两个注解的合成

@AutoConfigurationPackage

查看@AutoConfigurationPackage的源代码

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({Registrar.class})
public @interface AutoConfigurationPackage {
    // ...
}

可以看到,这个注解就是将Registrar.class加入到IOC容器中,我们继续查看Registrar.class的源代码

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

这是一个定义在AutoConfigurationPackages.class类中的静态内部类,我们给registerBeanDefinitions()方法加上断点,启动debug

程序运行到此处,我们查看其中的(new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames()表达式的值

发现这个值就是主类MainApplication所在的包路径,这个函数的作用就是将主类所在包名封装进一个数组中,进行注册,相当于将这个包路径下的所有组件注册到容器中

@Import({AutoConfigurationImportSelector.class})

@EnableAutoConfiguration注解中的另一个注解是@Import({AutoConfigurationImportSelector.class}),表示将AutoConfigurationImportSelector.class放入容器中

继续查看这个类的源代码,在getAutoConfigurationEntry()方法上加上断点,启动debug

运行完List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);这一行,我们可以看到debug界面的变量

表示这一个行代码是获取了所有需要导入到容器中的自动配置类,有130个,至于是如何导入的,从哪里导入的,我们可以继续分析getCandidateConfigurations()这个方法

在getCandidateConfigurations()方法中,核心部分是SpringFactoriesLoader.loadFactoryNames()方法,这是使用了Spring的工厂加载器,继续查看loadFactoryNames()源代码,可以发现这部分的核心代码是(List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());,接下来,我们查看其中的loadSpringFactories()方法,最终就是这个方法加载得到了所有的组件

在其中我们可以看到这样一行代码Enumeration urls = classLoader.getResources("META-INF/spring.factories");,表示是从META-INF/spring.factories这个位置加载所有的文件的,这样SpringBoot就会扫描当前系统内所有在META-INF/spring.factories位置的文件

我们可以查看引入的外部包,有些在META-INF/这个路径下有spring.factories文件,而有些没有。我们主要查看spring-boot-autoconfigure这个包,打开它的META-INF/spring.factories文件,我们可以看到在这个文件中,写死了所有需要导入的自动配置类

在这个文件中正好写死了130个AutoConfiguration类,也就是说,当SpringBoot启动时,就导入这个文件内写的所有自动配置类。但问题是,当我们在Springboot启动后使用getBeanDefinitionNames()方法获取容器中的组件时,发现容器中并没有文件中定义的所有自动配置类。说明这些自动配置类并不是全部生效的,至于是如何实现的,就要了解按需开启自动配置项

3. 按需开启自动配置项

从上一步可以看到,容器中并没有spring-boot-autoconfigureMETA-INF/spring.factories文件中定义的所有自动配置类。对于这种情况,我们查看这个依赖下的org.springframework.boot.autoconfigure包下,这里面都是定义好的配置类。比如,我们可以查看aop包下的AopAutoConfiguration

在类名上标注了@ConditionalOnProperty注解,表示当配置文件中指定了spring.aop.auto=true,这个组件才能加入到容器中,另外,如果配置文件中不指定,默认也为true,所以这个配置类是已经加入到容器中的。此外,在这个类中还有两个静态内部类

如果容器中没有org.aspectj.weaver.Advice类,且在配置文件中指定了spring.aop.proxy-target-class=true或者不指定默认为true,才能将ClassProxyingConfiguration组件放入容器中

如果容器中存在Advice.class这个组件(全类名是org.aspectj.weaver.Advice),AspectJAutoProxyingConfiguration组件就能被加入到容器中

可以看到,对于这个AopAutoConfiguration这个自动配置类,如果在容器中有org.aspectj.weaver.Advice组件时,会使用AspectJAutoProxyingConfiguration,不存在时,会使用ClassProxyingConfiguration

所以,虽然在SpringBoot启动时会加载spring.factories文件中写死的全部自动配置类,但是会按照条件装配规则,最终按需配置,放入IOC容器中

4. 修改默认配置

我们可以看到,在很多自动配置类中会有EnableConfigurationProperties注解,开启属性绑定,将对应的properties类放入容器中,并在自动配置类中使用这些属性

比如,我们查看WebMvcAutoConfiguration自动配置类

在类上开启了MultipartProperties.class的属性绑定,并且在类中使用了MultipartProperties对象

我们查看MultipartProperties.class源代码

这个类绑定了配置文件中spring.servlet.multipart前缀的属性值,也就是说,自动配置类中的一些属性最终是来自于配置文件的相应前缀对应的属性值。既然如此,那么,我们可以在配置文件中使用相应的前缀加上属性名来改变容器中XXXProperties组件的属性值,而这些属性值又会被相应的XXXAutoConfiguration自动配置类使用,从而达到了修改默认配置的效果

5. 总结

  1. 在SpringBoot启动时,会先加载所有的自动配置类

  2. 每个自动配置类按照条件装配进行生效,类中组件会绑定配置文件中对应前缀的值(从XXXProperties类中获得,而XXXProperties类和配置文件中相应前缀进行了绑定)

  3. 生效的配置类会给容器中添加组件

  4. 容器中存在组件,相应的功能就可以使用

  5. 定制化配置

    • 在自己的配置类中使用@Bean注解替换配置好的组件(因为自动配置类中会使用@ConditionalMissingBean注解)
    • 在配置文件中定义修改相应前缀的属性值(前缀名可以参照XXXAutoConfiguration使用的XXXProperties类中绑定的前缀名)

属性值的获取:XXXAutoConfiguration ---> 组件 ---> XXXProperties ---> application.properties/yaml

posted @ 2021-01-21 17:04  Kinopio  阅读(207)  评论(0)    收藏  举报