Spring Boot Of Autoconfiguration Intro


一、SpringBoot 特点

1、依赖管理

采用 Maven 架构工具的 Springboot 工程的 pom 文件中,有这样的标签:

<!-- 依赖管理 -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.6.0</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<!-- 点进 parent 工程后里面还有一层父项目 -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-dependencies</artifactId>
    <version>2.6.0</version>
</parent>

<!-- spring-boot-dependencies 包中声明几乎开发中常用的所有 jar 的版本号,自动版本仲裁机制 -->
  • 开发导入 starter 场景启动器

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

<!-- 
	1. springboot 官方 starter 命名规则:spring-boot-starter-*
	2. 在某个开发场景中只需引入对应的 starter,那么所需依赖都会引进来 
	3. 某些情况需要自定义 starter,官方推荐自定义的 starter 命名规则:*-spring-boot-starter 
	4. 所有场景启动器最底层的依赖 
-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <version>2.6.0</version>
    <scope>compile</scope>
</dependency>
  • 无需关注版本号,自动版本仲裁
<!-- 
	1. 引入默认依赖都可以不写版本号
	2. 引入非版本仲裁的 jar,要写版本号
-->
  • 可以修改版本号

原理见:https://naivekyo.github.io/2021/11/12/maven-common-operation

<!-- 1. 查看 spring-boot-dependencies 包中规定了当前依赖的版本(在 dependencyManagement 和  properties 标签中) -->

<!-- 2. 在当前项目里面重写配置 (具体原理是 Maven 的依赖调解原则) -->
<properties>
	<mysql.version>5.1.49</mysql.version>
</properties>

2、自动配置

spring-boot-starter-web 场景为例:

  • 自动装配好 Tomcat
    • 自动引入 Tomcat 依赖
    • 配置 Tomcat
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
    <version>2.6.0</version>
    <scope>compile</scope>
</dependency>
  • 自动配置好 SpringMVC
    • 引入 SpringMVC 全套组件
    • 自动配置好 SpringMVC 常用组件(功能)
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>5.3.13</version>
    <scope>compile</scope>
</dependency>
  • 自动配置好 Web 常用功能,如:字符编码问题
    • SpringBoot 已经帮我们配置好了所有 web 开发的常见容器
  • 默认的包结构
  • 各种配置拥有默认值
    • 默认配置最终都是映射到某一个类上,这个类的对象存在于 IOC 容器中
  • 按需加载所有自动配置项
    • 引入了哪些场景,这些场景的自动配置才会生效
    • SpringBoot 的所有自动配置功能都在
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure</artifactId>
    <version>2.6.0</version>
    <scope>compile</scope>
</dependency>
  • ……

二、容器功能

1、组件添加

(1) @Configuration

  • 基本使用
  • Full 模式和 Lite 模式
    • 实例
    • 最佳实战
      • 配置类中注册的组件之间无依赖关系用 Lite 模式加速容器启动过程,减少判断
      • 配置类中注册的组件之间有依赖关系,方法会被调用得到 IOC 容器中的单实例组件,用 Full 模式

基本使用

测试类:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {

    private static final long serialVersionUID = -2918082554678902109L;
    
    private Integer id;
    
    private String name;
    
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Pet implements Serializable {

    private static final long serialVersionUID = 3176227278246712244L;
    
    private String name;
    
}

/**
 * 1、配置类里面使用 @Bean 注解在方法上给容器注册组件,默认也是单实例的
 * 2、配置类本身就是组件
 * 3、从 spring5.2 后, @Configuration 注解中多了一个属性 proxyBeanMethods 默认值 true
 *    Full 模式: proxyBeanMethods = true
 *    Lite 模式: proxyBeanMethods = false
 *      解决组件依赖问题.
 */
@Configuration(proxyBeanMethods = true)
public class MyConfig {

    /**
     * 给容器中添加一个组件:
     * 方法名就是 Bean 的 id;
     * 返回类型就是 Bean 的类型;
     * 返回的值就是组件在容器中的实例;
     * 默认还是 singleton 模式的
     */
    @Bean
    public User user() {
        return new User(22, "NaiveKyo");
    }
    
    @Bean
    public Pet pet() {
        return new Pet("Milo");
    }
}

上面的代码中介绍了测试使用的 User 和 Pet 类,我们通过配置类将其注册到 IOC 容器中,用于替代以前的 xml。

测试代码:

// 测试 3: 获取 IOC 容器并获取指定配置类, 然后调用其中被 @Bean 注解标注的方法
@RequestMapping(value = "/test2", method = RequestMethod.GET)
@ResponseBody
public String test2(HttpServletRequest request) {

    WebApplicationContext rootContext = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());

    // 获取到的是被 CGLIB 代理的配置类(原因: proxyBeanMethods = true)
    MyConfig myConfig = rootContext.getBean(MyConfig.class);
    // com.example.springboot1one.config.MyConfig$$EnhancerBySpringCGLIB$$1225388a@1a3ebb1c
    System.out.println(myConfig); 

    // 下面获取的都是 IOC 容器中的实例
    User userBean = rootContext.getBean(User.class);
    System.out.println(userBean); // User(id=22, name=NaiveKyo)

 	/**
    * 如果在配置类中声明 @Configuration(proxyBeanMethods = true), 
    * 那么 SpringBoot 总会检查这个组件是否已经注册到容器中, 如果已注册, 就生成该配置类的代理对象,
    * 通过代理对象调用被代理类的@Bean修饰的方法, 然后会去容器中寻找目标实例。
    * 
    * 目的: 保持组件单实例
    * 
    * 反之, 如果 proxyBeanMethods = false, 那么通过全局上下文获取到的配置类实例就不会被代理,
    * 而是直接拿到容器中的配置类对象本身;
    * 此时调用@Bean修饰的方法获取到的对象就是 new 出来的和容器中不同的对象。
    */
    User user1 = myConfig.user();
    User user2 = myConfig.user();

    System.out.println(user1 == user2);   // true

    return "ok2";
}

不难看出其中重点就在于:@Configuration 注解的 proxyBeanMethods 属性,看下面的讲解。

组件依赖问题

假如现在测试条件变成这样:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {

    private static final long serialVersionUID = -2918082554678902109L;
    
    private Integer id;
    
    private String name;
    
    private Pet pet;
    
}

可以看到 User 类中持有一个 Pet 类型的对象。

@Configuration(proxyBeanMethods = true)
public class MyConfig {

    @Bean
    public User user() {
        return new User(22, "NaiveKyo", pet());
    }
    
    @Bean
    public Pet pet() {
        return new Pet("Milo");
    }
}

现在的情况:proxyBeanMethods = true

// 测试 4: 组件依赖问题
@RequestMapping(value = "/test3", method = RequestMethod.GET)
@ResponseBody
public String test3(HttpServletRequest request) {

    WebApplicationContext rootContext = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());

    User user = rootContext.getBean(User.class);
    Pet pet = rootContext.getBean(Pet.class);

    Pet pet1 = user.getPet();

    System.out.println(pet == pet1);    // true

    return "ok3";
}

可以看到此时 User 对象中持有的 Pet 类型的对象其实就是容器中的 Pet 实例。

那么如果把 proxyBeanMethods 改为 false 会发生什么呢?

System.out.println(pet == pet1); // false

此时容器中 User 对象持有的 Pet 类型的对象就不是容器中注册的 Pet 实例了。

Full 模式和 Lite 模式

两种模式的选取取决于在配置类中注册的组件之间是否有依赖关系,看下面:

// MyConfig.class

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

// 无依赖关系
@Bean
public User user() {
    // User 类中没有 Pet 类型成员
    return new User(22, "NaiveKyo");
}

// 有依赖关系
@Bean
public User user() {
    // User 类中有 Pet 类型成员
    return new User(22, "NaiveKyo", pet());
}
  • 配置类中注册的组件之间无依赖关系时可以给配置类设置 @Configuration(proxyBeanMethods = false)Lite 模式,这样容器在启动过程中加载配置类调用其 @Bean 修饰的方法注册组件时就不会判断容器中是否已经存在该组件
  • 配置类中注册的组件之间有依赖关系时,使用 @Configuration 默认的 proxyBeanMethods = true ,即 Full 模式,这样通过配置类的 @Bean 修饰的方法注册组件时就会进行判断,保证容器中实例都是 singleton 模式。

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

除了可以在配置类中向 IOC 容器中注册组件之外,(在 SpringBoot 工程能够扫描到的包下)使用如下注解也可以向容器中注册组件:

@Component、@Controller、@Service、@Repository

(3) @ComponentScan、@Import

@Import 注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {

	/**
	 * {@link Configuration @Configuration}, {@link ImportSelector},
	 * {@link ImportBeanDefinitionRegistrar}, or regular component classes to import.
	 */
	Class<?>[] value();

}

该注解可以标注在任意类上,但是在 Spring 工程中一般标注在组件上(@Component 修饰的类,很多注解的底层都引入了 @Component,例如 @Controller、@Service、@Repository 等等)。

这是因为 SpringBoot 启动时会扫描该注解修饰的类,然后通过其 value 属性中的 Class 对象调用无参构造器生成实例。

看例子:

public class TestImport {
    
}

@Import({TestImport.class, User.class})
@Configuration(proxyBeanMethods = true)
public class MyConfig {
    
    @Bean
    public User user() {
        return new User(22, "NaiveKyo", pet());
    }
    
    @Bean
    public Pet pet() {
        return new Pet("Milo");
    }
}

此时我们通过两种方式向容器中注入了 User 类型的不同实例。

测试代码:

// 测试 5: @Import 注解
@RequestMapping(value = "/test4", method = RequestMethod.GET)
@ResponseBody
public String test4(HttpServletRequest request) {

    WebApplicationContext rootContext = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());

    String[] beanNamesForType = rootContext.getBeanNamesForType(User.class);

    // 找到 IOC 容器中注入的两个类型为 User 的实例的名称
    for (String s : beanNamesForType) {
        System.out.println(s);
        // com.example.springboot1one.bean.User
        // user
    }

    // 找到 TestImport 类型的实例
    String[] testImportNames = rootContext.getBeanNamesForType(TestImport.class);
    for (String testImportName : testImportNames) {
        System.out.println(testImportName);
        // com.example.springboot1one.bean.TestImport
    }

    // 找到 HelloController
    String[] helloBeanNames = rootContext.getBeanNamesForType(HelloController.class);
    for (String helloBeanName : helloBeanNames) {
        System.out.println(helloBeanName);
        // helloController
    }

    return "ok4";
}

可以看到在中使用 @Import 注解注入的组件实例的名称是全类名,而通过 @Bean 和其他例如 @Controller 等注解注入的组件实例的名称是方法名或者类名称首字母缩写。

(4) @Conditional

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

在 idea 中搜索到这个注解,使用快捷键 ctrl + H 打开它的 type hierarchy,可以看到该注解有很多衍生注解。

假如说我们要找一个容器中不存在的组件,可以这样找:

// 测试 6: @Conditional 注解
@RequestMapping(value = "/test5", method = RequestMethod.GET)
@ResponseBody
public String test5(HttpServletRequest request) {

    WebApplicationContext rootContext = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());

    boolean testConditional = rootContext.containsBean("testConditional");
    System.out.println("容器中是否存在名为 testConditional 的组件? " + (testConditional ? "是" : "否"));
    // 容器中是否存在名为 testConditional 的组件? 否

    boolean testUser = rootContext.containsBean("user");
    System.out.println("容器中是否存在名为 user 的组件? " + (testUser ? "是" : "否"));
    // 容器中是否存在名为 user 的组件? 是

    return "ok5";
}

可知名为 testConditional 的组件没有在容器中注册,那么可以这样在配置类中设置:

@Configuration
public class MyConfig {

    @ConditionalOnBean(value = {TestConditional.class})
    @Bean
    public User user() {
        return new User(22, "NaiveKyo");
    }
    
}

此时再运行前面的测试代码就会出现:

// 容器中是否存在名为 testConditional 的组件? 否
// 容器中是否存在名为 user 的组件? 否

可知 @ConditionalOnBean 注解的作用就是容器中存在指定的组件实例时,被该注解修饰的部分才会生效。

源码:

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnBeanCondition.class)
public @interface ConditionalOnBean {
    …………
}

可知该注解可以修饰类和方法。

2、原生配置文件引入

@ImportResource 注解用于将 spring 的 xml 文件中注册的 bean 转换为配置类注入。

application-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:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="testImportResource" class="com.example.springboot1one.bean.TestImportResource" />
    
</beans>

MyConfig.java

@Import({TestImport.class, User.class})
@ImportResource("classpath:application-beans.xml")
@Configuration
public class MyConfig {

    @ConditionalOnBean(value = {TestConditional.class})
    @Bean
    public User user() {
        return new User(22, "NaiveKyo", pet());
    }
    
    @Bean
    public Pet pet() {
        return new Pet("Milo");
    }
}

测试:

// 测试 7: @ImportResource 注解
@RequestMapping(value = "/test6", method = RequestMethod.GET)
@ResponseBody
public String test6(HttpServletRequest request) {

    WebApplicationContext rootContext = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());

    boolean testImportResource = rootContext.containsBean("testImportResource");
    System.out.println("容器中是否存在名为 testImportResource 的组件? " + (testImportResource ? "是" : "否"));
    // 容器中是否存在名为 testImportResource 的组件? 是

    return "ok6";
}

3、配置绑定

举个简单的例子,在 Java 中从 properties 配置文件中读取配置封装到 JavaBean 中:

@Test
public void testProperties() throws IOException {

    Properties properties = new Properties();

    properties.load(new FileInputStream("a.properties"));

    Enumeration<?> enumeration = properties.propertyNames();
    while (enumeration.hasMoreElements()) {
        String key = (String) enumeration.nextElement();
        String value = properties.getProperty(key);

        System.out.println("key: " + key + " value: " + value);
        // 封装到 JavaBean ......
    }
}

现在在 SpringBoot 中使用注解可以很方便做到配置绑定。

(1) @ConfigurationProperties

@ConfigurationProperties 注解的作用是给 Spring IOC 容器中注册的组件绑定一个外部配置文件,用于将相关属性注入到该组件中。

查看其源码可以发现,该注解可以用到类或方法上,Spring 工程中一般建议用在 @Configuration 标注的类或者 @Bean 标注的方法上。

绑定的原理是调用属性的 setter 方法或者使用了 @ConstructorBinding 注解的构造方法。

创建一个测试类:

@Data
@AllArgsConstructor
@NoArgsConstructor
@Component
@ConfigurationProperties(prefix = "mycar")
public class Car implements Serializable {
    
    private static final long serialVersionUID = -1096069076931448275L;
    
    private String brand;
    
    private Integer price;
    
}

配置文件

mycar.brand = BYD
mycar.price = 100000

此时 IOC 容器中名为 car 的组件就和配置文件绑定到一块儿去了,而且会去找配置文件中以 mycar 开头的属性。

测试:

// 测试 8: @ConfigurationProperties 注解
@RequestMapping(value = "/test7", method = RequestMethod.GET)
@ResponseBody
public String test7(HttpServletRequest request) {

    WebApplicationContext rootContext = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());

    Car car = rootContext.getBean(Car.class);

    // Car(brand=BYD, price=100000)
    System.out.println(car);

    return "ok7";
}

(2) @EnableConfigurationProperties

@EnableConfigurationProperties 需要结合 @ConfigurationProperties 一起使用。

查看源码可知前者是标注在类上的,在 Spring 工程中一般都是标注在配置类上,它有两个作用:

  • 将指定类注册到 IOC 容器中
    • 注册的 Bean 名称为类名的全路径(和 @Import 注册的组件名称格式一样)
  • 为该组件开启配置绑定功能

测试类:(注意需要结合 @ConfigurationProperties 使用

@Data
@AllArgsConstructor
@NoArgsConstructor
@ConfigurationProperties(prefix = "mycar")
public class Car implements Serializable {
    
    private static final long serialVersionUID = -1096069076931448275L;
    
    private String brand;
    
    private Integer price;
    
}

配置类:

@Import({TestImport.class, User.class})
@ImportResource("classpath:application-beans.xml")
@Configuration
@EnableConfigurationProperties({Car.class})
public class MyConfig {

    @ConditionalOnBean(value = {TestConditional.class})
    @Bean
    public User user() {
        return new User(22, "NaiveKyo", pet());
    }
    
    @Bean
    public Pet pet() {
        return new Pet("Milo");
    }
}

测试代码:

// 测试 9: @EnableConfigurationProperties 结合 @ConfigurationProperties
@RequestMapping(value = "/test9", method = RequestMethod.GET)
@ResponseBody
public String test9() {

    // Car(brand=BYD, price=100000)
    return this.applicationContext.getBean(Car.class).toString();
}

三、自动装配原理入门

1、引导加载自动装配类

在 SpringBoot 项目的主启动类上有一个注解 @SpringBootApplication

查看其源码:

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })

下面依次介绍该组合注解的重要部分。

(1)@SpringBootConfiguration

查看源码:

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

	@AliasFor(annotation = Configuration.class)
	boolean proxyBeanMethods() default true;

}

可知该注解其实就是一个 @Configuration 注解,那么标注了 @SpringBootConfiguration 注解的类就代表着整个 SpringBoot 项目的核心配置类。

(2)@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(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {}

可以看到该注解主要功能就是向容器中注入一个组件:AutoConfigurationPackages.Registrar,这是一个静态内部类:

static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
    }

    @Override
    public Set<Object> determineImports(AnnotationMetadata metadata) {
        return Collections.singleton(new PackageImports(metadata));
    }

}

而这个静态类的功能是 向容器中注册一系列组件。(注册一系列组件其实就是主启动类所在的包下的所有组件,以及需要使用的 jar 包中的组件。)

方法:registerBeanDefinitions

  • AnnotationMetadata :注解的元信息,该类的作用是得到标注在类上的注解信息,在这里得到的就是主启动类上的注解信息。
  • PackageImports(metadata).getPackageNames().toArray(new String[0])):这段代码的作用是得到主启动类所在的包的名称。
  • register(BeanDefinitionRegistry registry, String... packageNames):该方法的作用就是使用 BeanDefinitionRegistry 为默认的包配置生成 BeanDefinition,从而进一步将默认包下所有能够注册到容器的组件注册进去。

@Import(AutoConfigurationImportSelector.class)

@EnableAutoConficuration 注解的第二个作用就是向容器中注册名为 AutoConfigurationImportSelector 的组件。

其中 selectImports 方法是关键。

// org.springframework.boot.autoconfigure.AutoConfigurationImportSelector#selectImports()

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

利用 getAutoConfigurationEntry(annotationMetadata) 方法给容器批量导入一些组件。

// org.springframework.boot.autoconfigure.AutoConfigurationImportSelector#getAutoConfigurationEntry()

protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
    
    // 这里的 annotationMetadata 依旧是主启动类上的注解信息
    if (!isEnabled(annotationMetadata)) {
        return EMPTY_ENTRY;
    }
    
    AnnotationAttributes attributes = getAttributes(annotationMetadata);
    List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
    configurations = removeDuplicates(configurations);
    Set<String> exclusions = getExclusions(annotationMetadata, attributes);
    checkExcludedClasses(configurations, exclusions);
    configurations.removeAll(exclusions);
    configurations = getConfigurationClassFilter().filter(configurations);
    fireAutoConfigurationImportEvents(configurations, exclusions);
    return new AutoConfigurationEntry(configurations, exclusions);
}

通过 getCandidateConfigurations 获取到所有场景下需要使用的自动装配类:

可以看到目前使用的 SpringBoot 版本下一共有 137 个开发场景(即 137 个自动装配类)。

// org.springframework.boot.autoconfigure.AutoConfigurationImportSelector#getCandidateConfigurations()

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    // 使用 Spring 的工厂加载器获取配置
    List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
                                                                         getBeanClassLoader());
    Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
                    + "are using a custom packaging, make sure that file is correct.");
    return configurations;
}
// org.springframework.core.io.support.SpringFactoriesLoader#loadFactoryNames()

public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
    ClassLoader classLoaderToUse = classLoader;
    if (classLoaderToUse == null) {
        classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
    }
    String factoryTypeName = factoryType.getName();
    
    // 通过下面的方法获取信息
    return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}

// 实际处理是在这个方法中
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
    // 下面这个 result 就是需要加载的所有组件的相关信息
    Map<String, List<String>> result = cache.get(classLoader);
    if (result != null) {
        return result;
    }

    result = new HashMap<>();
    try {
        // 可以看到有个属性 FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"
        Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
        while (urls.hasMoreElements()) {
            URL url = urls.nextElement();
            UrlResource resource = new UrlResource(url);
            Properties properties = PropertiesLoaderUtils.loadProperties(resource);
            for (Map.Entry<?, ?> entry : properties.entrySet()) {
                String factoryTypeName = ((String) entry.getKey()).trim();
                String[] factoryImplementationNames =
                    StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
                for (String factoryImplementationName : factoryImplementationNames) {
                    result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
                        .add(factoryImplementationName.trim());
                }
            }
        }

        // Replace all lists with unmodifiable lists containing unique elements
        result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
                          .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
        cache.put(classLoader, result);
    }
    catch (IOException ex) {
        throw new IllegalArgumentException("Unable to load factories from location [" +
                                           FACTORIES_RESOURCE_LOCATION + "]", ex);
    }
    return result;
}

这里可以发现我们能够从 FACTORIES_RESOURCE_LOCATION = “META-INF/spring.factories” 这个属性代表的文件中获取到所有的自动装配类信息。

在 springboot 的自动装配包下可以找到这个文件。

该文件中在标有 # Auto Configure 注释下方写有所有的 xxxAutoConfiguration 类所在的路径。

可知 SpringBoot 项目一旦启动首先一定会收集并加载所有的自动装配类。

2、按需开启自动配置项

虽然一上来就将所有的自动装配类都加载类,但是在 getAutoConfigurationEntry 方法后续还会进行一些筛选:

// org.springframework.boot.autoconfigure.AutoConfigurationImportSelector#getAutoConfigurationEntry()

protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
    if (!isEnabled(annotationMetadata)) {
        return EMPTY_ENTRY;
    }
 
    // 获取主启动类上注解的相关信息
    AnnotationAttributes attributes = getAttributes(annotationMetadata);
    
    // 获取所有自动装配类的信息(当期 SpringBoot 版本一共 137 个)
    List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
    
    // 去除 configurations 中重复的字符串
    configurations = removeDuplicates(configurations);
    
    // 根据主启动类上注解的信息获取不需要的自动装配信息
    Set<String> exclusions = getExclusions(annotationMetadata, attributes);
    checkExcludedClasses(configurations, exclusions);
    
    // 去除不需要的自动装配类
    configurations.removeAll(exclusions);
    
    // 通过配置类过滤器过滤掉当期 SpringBoot 项目不需要的场景(具体是通过 @Conditional 及其衍生注解处理的)
    configurations = getConfigurationClassFilter().filter(configurations);
    
    // 去除主启动类上标注的不需要的场景
    fireAutoConfigurationImportEvents(configurations, exclusions);
    
    return new AutoConfigurationEntry(configurations, exclusions);
}

通过上面的处理后,我们就可以得到当前 SpringBoot 项目所需要使用的开发场景了(就是用到了哪些 starter),然后就会根据相应的 xxxAutoConfiguration 类根据其中的 @Conditional 及其衍生注解向容器中注册所需要使用的组件(包括 @Import@Bean 等注解注册的组件)。

总结

这样当 SpringBoot 工程启动后,IOC 容器中应该包括当前项目需要使用到的自动装配类,通过自动装配类注册的组件、其他诸如@Controller等注解注入的组件、各种配置属性类 等等。

3、修改默认配置

我们以 springmvc 的自动装配类为例:org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration

// 表明这是一个配置类,而且使用的是 Lite 模式
@Configuration(proxyBeanMethods = false)
// 表面当前 web 开发环境为基于 Servlet 的原生 web 应用,而不是 webflux 响应式开发应用
@ConditionalOnWebApplication(type = Type.SERVLET)
// 要求 IOC 容器中必须存在 Servlet 开发需要的核心类
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
// WebMvcConfigurationSupport 必须没有被注册
@ConditionalOnMissingBean(WebMvcConfigurationSupport 必须没有被注册.class)
// springmvc 的自动装配的优先级
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
// 要求在当前装配类装配之前必须已经存在的装配类
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
		ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {}

从中找到装配 DispatcherServlet 的配置类 DispatcherServletAutoConfiguration

可以看到其中对 DispatcherServlet 的处理。

@Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {
    DispatcherServlet dispatcherServlet = new DispatcherServlet();
    dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());
    dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest());
    dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound());
    dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents());
    dispatcherServlet.setEnableLoggingRequestDetails(webMvcProperties.isLogRequestDetails());
    return dispatcherServlet;
}

下面看个有趣的:

@Bean
// 容器中 MultipartResolver 类型的组件必须存在
@ConditionalOnBean( MultipartResolver 类型的组件必须存在.class)
// 容器中名为 multipartResolver 的组件必须不存在
@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
// @Bean 修饰的方法的参数,spring 会从 IOC 容器中找到对应的组件传递给该方法
public MultipartResolver multipartResolver(MultipartResolver resolver) {
    
    // 作用:如果用户定制了自己的 MultipartResolver 类型的组件,但是注册到容器中的名称不是 multipartResolver
    // 这里就会将其重命名为 multipartResolver
    // Detect if the user has created a MultipartResolver but named it incorrectly
    return resolver;
}

同时还有很多地方用到了 @ConditionalOnMissingBean 注解,要么是为了规范化,要么是用户没有配置spring会自动配置。这也是 SpringBoot 的一种设计模式。

4、总结

  • SpringBoot 先加载所有的自动装配类
  • 每个自动装配类会按照条件自动生效,默认都会绑定配置文件相关联的配置类
  • 生效的配置类会给容器中装配很多组件
  • 只要容器中有这些组件,相当于这些功能就有了
  • 只要用户有自己的配置就以用户的优先
  • 定制化配置:
    • 用户直接使用 @Bean 替换底层组件
    • 用户修改指定组件绑定的配置文件

xxxAutoConfiguration –> 注册组件 –> 组件从 xxxProperties 中获取相关的配置属性 —> application.properties

实践

四、自定义 starter

初步了解了 SpringBoot 自动装配原理后,我们可以定制我们自己的 starter,官方建议第三方提供的 starter 命名规范是 *-spring-boot-starter

1、创建 Maven 工程

首先需要创建一个空 Maven 工程,然后提供 SpringBoot starter 所需要的核心依赖:

注意这里的版本最好和使用的 SpringBoot 版本一致

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

Maven 工程目录结构如下:

pom.xml

<dependencies>

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

</dependencies>

代码如下:

NaiveKyoAutoConfiguration.java

// 声明这是一个配置类,采用 Lite 模式加载
@Configuration(proxyBeanMethods = false)
// 将 PersonProperties 注册到 IOC 容器并将其绑定的配置文件中相关属性注入该组件中
@EnableConfigurationProperties(PersonProperties.class)
// 如果类路径下有 Person 的 class 文件,就加载当前配置类
@ConditionalOnClass(Person.class)
public class NaiveKyoAutoConfiguration {
    
    // 向容器中注入 Person 组件 (参数中的 personProperties 是从 spring 容器中获取的)
    @Bean
    @ConditionalOnMissingBean(Person.class) // 如果容器中没有 Person 就注册它
    public Person person(PersonProperties personProperties) {
        
        return new Person(personProperties.getName(), personProperties.getAge(), personProperties.getDesc());
    }
}

Person.java

这个自定义 stater 最主要的目的就是将 Person 注册到 IOC 容器中。

public class Person {
    
    private String name;
    
    private Integer age;
    
    private String desc;

    public Person() {
    }

    public Person(String name, Integer age) {
        this(name, age, null);
    }

    public Person(String name, Integer age, String desc) {
        this.name = name;
        this.age = age;
        this.desc = desc;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", desc='" + desc + '\'' +
                '}';
    }
}

PersonProperties.java

配置类如下:

@ConfigurationProperties(prefix = "naivekyo")
public class PersonProperties {
    
    private String name = "NaiveKyo";
    
    private Integer age = 22;
    
    private String desc = "Hello Developer! This is NaiveKyo.";

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }
}

spring.factories

我们知道 SpringBoot 项目启动时会从 spring-boot-autoconfigure 这个 jar 包的 META-INF/spring.factories 文件中找到所有的自动装配类的全类名路径。

可以仿照其规则,在当前自定义 stater 的 resource 目录下新建 META-INF 目录,同时新建 spring.factories 文件:

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.naivekyo.config.NaiveKyoAutoConfiguration

2、打包并导入依赖

首先将我们定制的 stater 打成 jar 包:

首先打开 stater 工程的 pom 文件所在的文件夹,打开命令行:

先输入:mvn clean

然后输入:mvn install

打包成功后就会输出到本机安装的 maven 配置文件指定的本地仓库中:

然后直接在测试工程中导入依赖:

<!-- 引入自定义的 stater -->
<dependency>
    <groupId>org.naivekyo</groupId>
    <artifactId>customstater-spring-boot-stater</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

3、测试

测试代码:

// 测试 10: 测试自定义 stater jar 包
@RequestMapping(value = "/test10", method = RequestMethod.GET)
@ResponseBody
public String test10() {

    NaiveKyoAutoConfiguration autoConfiguration = this.applicationContext.getBean(NaiveKyoAutoConfiguration.class);

    PersonProperties properties = this.applicationContext.getBean(PersonProperties.class);

    Person person = this.applicationContext.getBean(Person.class);

    System.out.println(" ========================= ");

    System.out.println(autoConfiguration);
    System.out.println(properties);
    System.out.println(person);

    System.out.println("===========================");

    return "ok10";
}

此处如果我们没有在 application.properties 文件中配置以 naivekyo 开头的属性就会使用默认的属性:

/**
	没有自定义配置时显示:
    com.naivekyo.config.NaiveKyoAutoConfiguration@3d672003
    com.naivekyo.PersonProperties@2f70d39e
    Person{name='NaiveKyo', age=22, desc='Hello Developer! This is NaiveKyo.'}
*/

在配置文件中设置后:

naivekyo.name = NaiveKyo
naivekyo.age = 23
naivekyo.desc = This is NaiveKyo.

再次调用测试代码:

/**
    com.naivekyo.config.NaiveKyoAutoConfiguration@555c6ae0
    com.naivekyo.PersonProperties@378b7e36
    Person{name='NaiveKyo', age=23, desc='This is NaiveKyo.'}
*/

Author: NaiveKyo
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source NaiveKyo !
  TOC