Spring Boot自动装配原理

本文基于Spring Boot-2.5.3版本进行讲解及演示

Spring Boot基于Spring Framework实现,Spring Framework的两大核心是IOC控制反转AOP面向切面编程。Spring Framework本身有一个IOC容器,该容器会统一管理其中的bean对象,bean对象可以理解为组件

基于Spring Framework的应用在整合第三方技术时,要把第三方框架中的核心API配置到Spring Framework的配置文件或注解配置类中,以供 Spring Framework统一管理。此处的配置是关键,通过编写 XML 配置文件或注解配置类,将第三方框架中的核心API以对象的形式注册到IOC容器中。这些核心API对象会在适当的位置发挥其作用,以支撑项目的正常运行。IOC容器中的核心API对象本身就是一个个的bean对象,即组件;将核心 API配置到 XM 配置文件或注解配置类的行为称为组件装配

Spring Framework本身只有一种组件装配方式,即手动装配,而 Spring Boot基于原生的手动装配,通过 ‘模块装配+条件装配+SPI机制’ ,可以完美实现组件的自动装配。下面分别就手动装配和自动装配的概念简单展开解释。


手动装配

手动装配指的就是开发者在项目中通过编写XML配置文件、注解配置类、配合特定注解等方式,将所需组件注册到IOC容器中。

下面是手动装配的代码示例:

基于xml配置文件的手动配置

<bean id="person"  class="com.principle.TestDemo">

基于注解配置类的手动装配

@Configuration
public class ExampleConfiguration {
    
    @Bean
    public Person person(){
        return new Person();
    }
    
}

基于组件扫描的手动装配

@Component
public class DemoService {
}

@Configuration
@ComponentScan("com.principle")//扫描的是包
public class ExampleConfiguration {
}

从上面的示例代码中得出一个共性:手动装配都需要亲自编写配置信息,将组件注册到IOC容器中


自动装配

自动装配是Spring Boot的核心特性之一,其核心是,本应由开发者编写的配置,转为框架自动根据项目中整合的场景依赖,合理的做出判断,并装配合适的Bean到IOC容器中。

Spring Boot利用模块装配+条件装配的机制,可以在开发者不进行任何干预的情况下注册默认所需的组件,也可以基于开发者自定义注册的组件装配其他必要的组件,并合理的替换默认的组件注册。

同时,SpringBoot的自动装配可以实现配置禁用,通过在@SpringBootApplication或者@EnableAutoConfiguration注解上标注exclude/excludeName属性,可以禁用默认的自动配置类。这种禁用方式在Spring Boot的全局配置文件中声明spring.autoconfigure.exclude属性时同样适用。


模块装配

模块装配是自动装配的核心,它可以把一个模块所需的核心功能组件都装配到IOC容器中。表现形式为大量的@EnableXXX注解。

使用模块装配的核心原则为:自定义注解+@Import导入组件

下面列举一个场景,场景为一个市场mall,其中包含老板boss、收银员Cashiers、收银台Till、货物goods,下面将通过模块装配,使用一个注解将老板boss、收银员Cashiers、收银台Till、货物goods都填充到市场mall中。

开始代码演示前,先查看 @Import 注解源码,它的value属性中可包含四种,代表它可以导入四种不同的组件,它们分别是:

  1. 普通类
  2. @Configuration标记的配置类
  3. ImportSelector的实现类
  4. ImportBeanDefinitionRegistrar的实现类
@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();

}

下面将结合这4种方式来导入市场组件。

首先,声明一个模块注解@EnableMall,代表开启市场模块配置

import org.springframework.context.annotation.Import;
import java.lang.annotation.*;

@Documented
@Target(ElementType.TYPE)//元注解,代表@EnableMall生效在类上
@Retention(RetentionPolicy.RUNTIME)//元注解,代表@EnableMall生效在运行期
public @interface EnableMall {
}

导入普通类方式

开始导入老板类,这里使用 @Import 导入普通类的方式

自定义一个老板类Boss,老板类作为一个普通java类,无需添加任何注解

public class Boss {}

修改模块注解,将boss类导入其中:

import org.springframework.context.annotation.Import;
import java.lang.annotation.*;

@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(Boss.class)
public @interface EnableMall {
}

然后通过配置类开启模块配置,结合@EnableMall将老板类注入市场mall:

import org.springframework.context.annotation.Configuration;

@Configuration
@EnableMall
public class MallConfiguration {
}

编写测试类,向容器中注入配置,并观察效果:

package com.principle.demo;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.util.Arrays;

public class MallMain {

    public static void main(String[] args) {
        //ApplicationContext就是IOC容器
        ApplicationContext context = new AnnotationConfigApplicationContext(MallConfiguration.class);
        //注入配置后尝试获取Boss的bean对象
        Boss boss = context.getBean(Boss.class);
        System.out.println(boss);
    }

}

输出结果如下,通过getBean方法可以获取Boss对象,说明Boss类已被注册到IOC容器中,并创建了一个对象,以此完成了一个最简单的模块装配:

com.principle.demo.Boss@5906ebcb

导入注解配置类方式

通过 @Import 注解导入 @Configuration 配置类的方式,来装配收银员Cashier类

在上面代码基础上,增加收银员Cashier类

public class Cashier {

    private String name;

    public Cashier(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

}

收银员可以有多个,这里通过配置类添加

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class Cashiers {

    @Bean
    public Cashier zhangsan(){
        return new Cashier("张三");
    }

    @Bean
    public Cashier lisi(){
        return new Cashier("李四");
    }

}

修改模块注解,添加收银员的配置类Cashiers

import org.springframework.context.annotation.Import;
import java.lang.annotation.*;

@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import({Boss.class,Cashiers.class})
public @interface EnableMall {
}

修改MallMain测试类,添加收银员bean的获取:

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Stream;

public class MallMain {

    public static void main(String[] args) {
        //ApplicationContext就是容器类
        ApplicationContext context = new AnnotationConfigApplicationContext(MallConfiguration.class);
        //输出所有的baen名
        Stream.of(context.getBeanDefinitionNames()).forEach(name -> System.out.println(name));
        System.out.println("---------------------");
        //通过类型获取所有的收银员bean并打印
        Map<String, Cashier> cashiers = context.getBeansOfType(Cashier.class);
        cashiers.forEach((key,value) -> System.out.println(value));

    }

}

运行MallMain测试类:

org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
mallConfiguration
com.principle.demo.Boss
com.principle.demo.Cashiers
zhangsan
lisi
---------------------
com.principle.demo.Cashier@3901d134
com.principle.demo.Cashier@14d3bc22

org.springframework.context开头的是IOC容器自带的bean,后面是我们装配的bean,可以看到多出了两个收银员bean对象。需要注意的是,MallConfiguration这个配置类也会作为一个bean被注册到IOC容器中。


导入ImportSelector方式

接着,我们通过@Import注解导入ImportSelector的方式,导入收银台类

ImportSelector是一个接口,它既可以导入配置类,也可以导入普通类。被ImportSelector导入的类,最终会在IOC容器中以单例bean的形式创建并保存。

在上面代码基础上增加收银台类:

public class Till {}

然后在声明一个收银台的配置类:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class TillConfiguration {
    
    @Bean
    public Till tillOne(){
        return new Till();
    }
    
}

接着编写一个ImportSelector接口的实现类,因为ImportSelector接口的selectImports方法返回值为字符串数组。这里传入要注册的bean的全限定类名,并且传入了一个收银台普通类和一个它的配置类,来观察最后的效果是否为注册了两个收银台bean:

import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;

public class MallImportSelector implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        
        return new String[]{Till.class.getName(),TillConfiguration.class.getName()};
        
        //等同于上面的class.getName(),getName()获取的就是这种字符串全限定名
        //return new String[]{"com.principle.demo.Till","com.principle.demo.TillConfiguration"};
    }
}

修改@EnableMall添加ImportSelector接口实现类

import org.springframework.context.annotation.Import;
import java.lang.annotation.*;

@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import({Boss.class,Cashiers.class,MallImportSelector.class})
public @interface EnableMall {
}

修改MallMain测试类,只输出所有bean名字:

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Stream;

public class MallMain {

    public static void main(String[] args) {
        //ApplicationContext就是容器类
        ApplicationContext context = new AnnotationConfigApplicationContext(MallConfiguration.class);
        //输出所有的baen名
        Stream.of(context.getBeanDefinitionNames()).forEach(name -> System.out.println(name));
    }

}

运行结果:

org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
mallConfiguration
com.principle.demo.Boss
com.principle.demo.Cashiers
zhangsan
lisi
com.principle.demo.Till
com.principle.demo.TillConfiguration
tillOne

末尾倒数第一行和倒数第三含看到打印了两个收银台bean,注册成功并说明ImportSelector既可以导入配置类,也可以导入普通类。

注意: ImportSelector接口实现类MallImportSelector本身并没有注册到IOC容器中。

ImportSelector的实现方式拥有很大的灵活性,可以把需要注册到容器中类的全限定名字符串写入一个文件中,并把此文件放到项目中一个可被读取的位置,来避免硬编码问题。事实上Spring Boot的自动装配就是这么做的,它通过spring.factories文件实现,这里暂不细述。


导入ImportBeanDefinitionRegistrar方式

最后,使用ImportBeanDefinitionRegistrar的方式来导入货物类Goods

ImportBeanDefinitionRegistrar不同于ImportSelector,它实际导入的是bean定义。在上面代码基础上,增加货物类

public class Goods {}

然后编写ImportBeanDefinitionRegistrar实现类MallBeanRegistrar

import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;

public class MallBeanRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

        //第一个参数是bean的名称(即id),第二个参数是通过RootBeanDefinition指定要注册bean的字节码(.class),这种方式相当于是向IOC容器注册了一个普通的单实例bean
        registry.registerBeanDefinition("goods", new RootBeanDefinition(Goods.class));

        // 可以注册多个 Bean
		// registry.registerBeanDefinition("goods", new RootBeanDefinition(Goods.class));
    }

}

修改模块注解,添加带有注册货物类的MallBeanRegistrar

@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import({Boss.class,Cashiers.class,MallImportSelector.class,MallBeanRegistrar.class})
public @interface EnableMall {
}

运行如下打印所有bean的MallMain:

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Stream;

public class MallMain {

    public static void main(String[] args) {
        //ApplicationContext就是容器类
        ApplicationContext context = new AnnotationConfigApplicationContext(MallConfiguration.class);
        //输出所有的baen名
        Stream.of(context.getBeanDefinitionNames()).forEach(name -> System.out.println(name));
    }

}

运行结果,最后一行多出了货物bean对象goods:

org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
mallConfiguration
com.principle.demo.Boss
com.principle.demo.Cashiers
zhangsan
lisi
com.principle.demo.Till
com.principle.demo.TillConfiguration
tillOne
goods

注意ImportBeanDefinitionRegistrar接口的实现类MallBeanRegistrar也没有注册到容器中。


导入DeferredImportSelector方式

这里在列举一种@Import的扩展方式,即DeferredImportSelector接口

ImportSelector接口的处理时机是在注解配置类的解析期间,此时配置类中的@Bean方法等还没有被解析,而DeferredImportSelector接口的处理时机是在注解配置类完全解析后,此时配置类的解析工作已全部完成,这样做的目的主要是为了配合Spring Boot的条件装配

我们在为场景添加一个顾客类customer:

public class Customer {}

然后编写DeferredImportSelector接口的实现类用以注册顾客,并在其方法内添加打印语句以便监控效果:

import org.springframework.context.annotation.DeferredImportSelector;
import org.springframework.core.type.AnnotationMetadata;

public class CustomerDeferredImportSelector implements DeferredImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        System.out.println("顾客类的DeferredImportSelector执行");
        return new String[]{Customer.class.getName()};
    }
}

然后在另外两个货物类与收银台类的接口实现中添加打印语句:

ImportSelector方式的收银台类

import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;

public class MallImportSelector implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {

        System.out.println("收银台类的ImportSelector执行");

        return new String[]{"com.principle.demo.Till","com.principle.demo.TillConfiguration"};

        //class.getName()等同于上面直接写全限定名的方式,二者用一个即可
        //return new String[]{Till.class.getName(),TillConfiguration.class.getName()};
    }
}

ImportBeanDefinitionRegistrar的货物类

import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;

public class MallBeanRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        System.out.println("货物类的ImportBeanDefinitionRegistrar执行");
        registry.registerBeanDefinition("goods", new RootBeanDefinition(Goods.class));

        // 可以注册多个 Bean
		// registry.registerBeanDefinition("goods", new RootBeanDefinition(Goods.class));
    }

}

然后别忘记在模块装配注解添加DeferredImportSelector的实现类CustomerDeferredImportSelector

import org.springframework.context.annotation.Import;
import java.lang.annotation.*;


@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import({Boss.class,Cashiers.class,MallImportSelector.class,MallBeanRegistrar.class,CustomerDeferredImportSelector.class})
public @interface EnableMall {
}

运行如下的MallMain来输出结果:

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.util.stream.Stream;

public class MallMain {

    public static void main(String[] args) {
        //ApplicationContext就是容器类
        ApplicationContext context = new AnnotationConfigApplicationContext(MallConfiguration.class);
        //输出所有的baen名
        Stream.of(context.getBeanDefinitionNames()).forEach(name -> System.out.println(name));

    }

}

打印结果:

收银台类的ImportSelector执行
顾客类的DeferredImportSelector执行
货物类的ImportBeanDefinitionRegistrar执行
...............................................省略无效信息及IOC自带bean
mallConfiguration
com.principle.demo.Boss
com.principle.demo.Cashiers
zhangsan
lisi
com.principle.demo.Till
com.principle.demo.TillConfiguration
tillOne
goods
com.principle.demo.Customer

可以发现结果为DeferredImportSelector执行晚于ImportSelector,但比ImportBeanDefinitionRegistrar要早,并且顾客bean被成功注册并获取。

注意DeferredImportSelector的源码中还存在一个分组方法,可以对不同的DeferredImportSelector加以区分,不过使用它的地方非常的少:

@Nullable
default Class<? extends Group> getImportGroup() {
	return null;
}


条件装配

只靠模块装配,不足以实现完整的组件装配。比如,想要根据环境的不同来装配不同的组件,或者一个组件需要另一个组件存在才能向容器注册自己等场景。Spring Framework提供了两种条件装配方式:基于Profile和Conditional

Profile

Profile提供了一种基于环境的配置,根据当前项目的不同运行环境,来动态的注册与当前运行环境向匹配的组件。

假设收银员只在白天工作,为其添加@Profile注解并指定条件为白天"day":

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

@Configuration
@Profile("day")
public class Cashiers {

    @Bean
    public Cashier zhangsan(){
        return new Cashier("张三");
    }

    @Bean
    public Cashier lisi(){
        return new Cashier("李四");
    }

}

然后运行MallMain将不会在控制台打印张三和李四:

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.util.stream.Stream;

public class MallMain {

    public static void main(String[] args) {
        //ApplicationContext就是容器类
        ApplicationContext context = new AnnotationConfigApplicationContext(MallConfiguration.class);
        //输出所有的baen名
        Stream.of(context.getBeanDefinitionNames()).forEach(name -> System.out.println(name));
    }

}
........省略IOC自带bean打印
mallConfiguration
com.principle.demo.Boss
com.principle.demo.Till
com.principle.demo.TillConfiguration
tillOne
goods
com.principle.demo.Customer

这是因为ApplicationContext中的Profile默认为“default”,与 @Profile(“day”) 并不相同,下面修改MallMain,为其指定环境:

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.util.stream.Stream;

public class MallMain {

    public static void main(String[] args) {
        //AnnotationConfigApplicationContext的构造传入配置类会立即初始化,这里不传,因为要配置环境
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        //设置当前激活的环境
        context.getEnvironment().setActiveProfiles("day");
        context.register(MallConfiguration.class);
        //在这里初始化IOC容器
        context.refresh();
        //输出所有的baen名
        Stream.of(context.getBeanDefinitionNames()).forEach(name -> System.out.println(name));
    }

}

然后在运行主方法观看结果:

.........
mallConfiguration
com.principle.demo.Boss
com.principle.demo.Cashiers
zhangsan
lisi
com.principle.demo.Till
com.principle.demo.TillConfiguration
tillOne
goods
com.principle.demo.Customer

发现张三和李四又成功被打印出来,证明他们已被注册到容器中,条件配置成功。

在实际的Spring Boot开发中,需要激活Profile的环境配置都在配置文件中进行,要避免写在代码中的这种硬编码方式,例如在application.properties 中使用:

spring.profiles.active=day

或在application.yml使用:

spring:
  profiles:
    active: day

除此之外,还有命令行参数、环境变量激活、在开发工具 IDE 中激活等多种方式。


Conditional

使用@Conditional()注解的条件装配

实际开发中,仍有很多Profile无法满足的条件装配情况。比如只有当老板Boss存在时,才能设置收银台Till,这种情况下Profile无法满足,但可以通过@Conditional()注解实现。

@Conditional()注解的作用:被此注解标注的Bean想要注册到IOC容器中时,必须满足此注解上设定的条件才可以注册

下面展示@Conditional()注解的使用方法,实现只有老板类存在于容器中时,才能注册收银台类的效果。首先写一个Condition接口的实现类,该类需要作为判定条件传入@Conditional()注解中:

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

public class ExistBossCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        //这里使用BeanDefinition即bean定义作为判断依据,是因为当前条件匹配时Boss对象可能还没有被创建,导致条件匹配出现偏差
        return context.getBeanFactory().containsBeanDefinition(Boss.class.getName());
    }
}

然后再TillConfiguration配置类的方法上标记@Conditional(ExistBossCondition.class),注解参数就是上面的实现类:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;

@Configuration
public class TillConfiguration {

    @Bean
    @Conditional(ExistBossCondition.class)
    public Till tillOne(){
        return new Till();
    }

}

我们去除EnableMall@Import注解的Boss类和MallImportSelector类(回顾上面,此类注册了Till),即去掉Boss类的注册:

import org.springframework.context.annotation.Import;
import java.lang.annotation.*;

@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import({Cashiers.class,MallImportSelector.class,MallBeanRegistrar.class,CustomerDeferredImportSelector.class})
public @interface EnableMall {
}

当我们去除Boss类后,再运行MallMain时的打印结果,发现老板类Boss与收银台类Till均没有出现:

.............省略IOC自带bean
mallConfiguration
com.principle.demo.Cashiers
zhangsan
lisi
goods
com.principle.demo.Customer

然后在EnableMall@Import注解中重新导入Boss类和MallImportSelector类:

@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import({Boss.class,Cashiers.class,MallImportSelector.class,MallBeanRegistrar.class,CustomerDeferredImportSelector.class})
public @interface EnableMall {
}

再运行MallMain的打印结果:

.............省略IOC自带bean
mallConfiguration
com.principle.demo.Boss
com.principle.demo.Cashiers
zhangsan
lisi
com.principle.demo.Till
com.principle.demo.TillConfiguration
tillOne
goods
com.principle.demo.Customer

这时老板类Boss与收银台类Till成功注册回来了。

在实际开发中,@Conditional还有许多的派生注解可以使用,他们也出现在了很多第三方配置场景中:

  • @ConditionalOnProperty:只有在指定的属性存在并且具有指定的值时,Bean 才会被创建。会检查配置文件中是否存在指定的属性,存在才会创建bean。
  • @ConditionalOnClass:它会检查类路径上是否存在某个类,如果存在该类则创建 Bean。
  • @ConditionalOnMissingBean:只有在容器中没有指定类型的 Bean 的情况下,才会创建当前 Bean。
  • @ConditionalOnBean:会在 Spring 容器中存在特定类型的 Bean 时才会创建该 Bean。
  • @ConditionalOnMissingClass:是 @ConditionalOnClass 的反向注解,只有在类路径中缺少指定的类时,才会创建对应的 Bean。
  • @ConditionalOnExpression:允许基于 SpEL (Spring Expression Language) 表达式来控制是否加载一个 Bean。
  • @ConditionalOnEnvironmentVariable:用于基于环境变量的值来决定是否创建 Bean。

Spring Boot 中正是通过@Conditional实现了开发者配置优先的原则,Spring Boot 加载的很多自动配置类中,都含有@Conditional及其派生注解,因此开发者自定义的配置类可以覆盖很多第三方场景的配置类。


SPI机制

SPI(Service Provider Interface)是 Java 中的一种服务发现机制,用于解耦接口的定义与实现。它可以在运行时动态的加载接口或抽象类的具体实现类,它把接口或抽象类的具体实现类的定义和声明权交给了外部化的配置文件

原生SPI

java原生 SPI 机制的实现示例

工作流程:

  1. 定义服务接口:首先定义一个服务接口,这是应用需要的功能定义。
  2. 实现服务接口:然后,服务提供者实现该接口,提供具体的服务实现。
  3. 提供服务描述文件:每个服务提供者还需要在 META-INF/services 目录下创建一个描述文件,文件名为接口的完全限定名,文件内容是服务提供者的实现类的全限定类名。
  4. 加载服务实现:通过 ServiceLoader 类,服务消费者可以在运行时动态加载符合接口规范的实现类。

假设我们有一个接口 PaymentService,它表示一种支付服务的接口,并且我们提供了两个实现类 PaypalPaymentServiceStripePaymentService

定义服务接口(PaymentService):

public interface PaymentService {
    void processPayment(double amount);
}

提供服务实现类(PaypalPaymentServiceStripePaymentService

public class PaypalPaymentService implements PaymentService {
    @Override
    public void processPayment(double amount) {
        System.out.println("Processing payment of " + amount + " through PayPal.");
    }
}
public class StripePaymentService implements PaymentService {
    @Override
    public void processPayment(double amount) {
        System.out.println("Processing payment of " + amount + " through Stripe.");
    }
}

创建服务描述文件,在 META-INF/services 目录下,创建一个文件 com.principle.spi.PaymentService(这里注意文件名一定要和接口的全限定名一致),并在文件中写入具体的实现类的全限定名:

文件路径:META-INF/services/com.principle.spi.PaymentService

image-20250206204306318

内容:

com.principle.spi.PaypalPaymentService
com.principle.spi.StripePaymentService

使用 ServiceLoader 加载服务,消费者使用 ServiceLoader 加载 PaymentService 接口的实现类,并进行调用:

import java.util.ServiceLoader;

public class PaymentProcessor {
    public static void main(String[] args) {
        ServiceLoader<PaymentService> loader = ServiceLoader.load(PaymentService.class);

        // 遍历所有的服务实现
        for (PaymentService paymentService : loader) {
            paymentService.processPayment(100.0); // 执行支付
        }
    }
}

最后输出,运行上述代码时,ServiceLoader 会自动加载 META-INF/services/com.principle.spi.PaymentService 中列出的所有服务实现类,并调用它们的 processPayment 方法。假设 PaypalPaymentServiceStripePaymentService 两个实现都在描述文件中,输出如下:

Processing payment of 100.0 through PayPal.
Processing payment of 100.0 through Stripe.

SPI 的优势:

  1. 解耦:SPI 允许服务的实现与调用解耦,服务消费者并不知道具体的实现类,具体的实现类是由 SPI 动态加载的。
  2. 可扩展性:如果需要扩展应用的功能,只需要添加新的服务提供者(即实现类)和描述文件,而不需要修改原有代码。
  3. 插件化:SPI 可以用于实现插件机制,服务消费者只需要通过接口与服务进行交互,不关心具体的插件实现。

常见的 Java SPI 应用场景:

  1. JDBC 驱动程序:不同数据库的驱动程序通过 SPI 提供,Java 在运行时可以根据需要加载不同的数据库驱动。
  2. 日志框架:许多日志框架(如 SLF4J、Log4j、Logback)都使用 SPI 机制来加载不同的日志实现。
  3. Java 服务框架(如 Spring):Spring 使用 SPI 来加载不同的持久化框架(如 JPA 或 MyBatis)的实现。
  4. 序列化框架:Java 提供 SPI 来加载不同的序列化实现,比如默认的 Java 序列化或第三方的序列化库。

Spring的SPI

Spring Framework的SPI演示

Spring Framework的SPI 比原生SPI更加高级且实用,因为它不局限于接口或抽象类,而可以是任何一个类、接口或注解。Spring Boot中正是大量使用了SPI机制来加载自动配置类和特殊组件等。

Spring Framework的SPI也需要将文件放在META-INF目录下,且SPI文件名必须为spring.factories,沿用上面的示例,此时spring.factories文件内容应如下:

com.principle.spi.PaymentService=\
com.principle.spi.PaypalPaymentService,\
com.principle.spi.StripePaymentService

文件路径位置及内容截图:

image-20250206211526483

其实spring.factories文件就是一个properties文件,编写内容的规则为 被检索的接口或类或注解的全限定名为key,其具体要检索的类为value,多个类之间逗号分隔。这里以PaymentService接口为key,它的两个实现类为value,还需要用斜杠分割。

注意:spring.factories文件中的key和value可以毫无关联,因此其强于原生SPI机制,更加灵活。

然后修改PaymentProcessor类,这里使用SpringFactoriesLoader,因为它是负责加载spring.factories的API,代码如下,输出对象和名称:

import org.springframework.core.io.support.SpringFactoriesLoader;
import java.util.List;

public class PaymentProcessor {
    public static void main(String[] args) {

        //第二个参数传当前类的ClassLoader
        List<PaymentService> paymentServices =
                SpringFactoriesLoader.loadFactories(PaymentService.class, PaymentProcessor.class.getClassLoader());
        paymentServices.forEach(paymentService -> System.out.println(paymentService));

        System.out.println("------------------分割线---------------------");

        //第二个参数传当前类的ClassLoader
        List<String> paymentServicesName =
                SpringFactoriesLoader.loadFactoryNames(PaymentService.class, PaymentProcessor.class.getClassLoader());
        paymentServicesName.forEach(name -> System.out.println(name));

    }
}

然后运行上面的主方法,输出:

com.principle.spi.PaypalPaymentService@51016012
com.principle.spi.StripePaymentService@29444d75
------------------分割线---------------------
com.principle.spi.PaypalPaymentService
com.principle.spi.StripePaymentService

发现可以输出对象及实现类全限定名,证明Spring Framework的SPI 机制已被正确使用。

Spring SPI机制的实现原理

Spring SPI机制实现的核心方法为SpringFactoriesLoader.loadFactoryNames方法,它通过加载 META-INF/spring.factories 文件,根据要加载类的全限定名从配置中获取对应的实现列表。

进入SpringFactoriesLoader中,首先看它有两个重要的属性:

//规定SPI的文件名必须是spring.factories
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
//存储SPI机制加载的类及其映射,作为缓存
static final Map<ClassLoader, Map<String, List<String>>> cache = new ConcurrentReferenceHashMap<>();

然后再查看其loadFactoryNames 方法的源码,如下:

public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
    
    //确定类加载器,如果为空就用SpringFactoriesLoader加载器
    ClassLoader classLoaderToUse = classLoader;
    if (classLoaderToUse == null) {
        classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
    }
    
    //获取需要加载的类的名称
    String factoryTypeName = factoryType.getName();
    
    //加载所有`META-INF/spring.factories`文件中的配置,根据要加载类的名称(全限定名)从配置中获取对应的实现类列表。
    //如果找不到对应的配置,返回空列表
    return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
  • 作用:根据指定的类型(factoryType)加载其实现类的全限定名。
  • 参数
    • factoryType:需要加载的接口或抽象类的 Class 对象。断点运行时,这里传入的就是PaymentService接口
    • classLoader:类加载器,如果为 null,则使用 SpringFactoriesLoader 的类加载器。断点运行时,这里传入的就是PaymentProcessor获取的加载器
  • 流程
    1. 确定类加载器。
    2. 调用 loadSpringFactories 方法加载所有 META-INF/spring.factories 文件中的配置。
    3. 根据 factoryType 的名称(全限定名)从配置中获取对应的实现类列表。
    4. 如果找不到对应的配置,返回空列表。

loadFactoryNames 方法中又通过调用loadSpringFactories 方法来加载spring.factories 文件,接下来看loadSpringFactories 方法源码:

private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
    
    //检查缓存中是否已经加载过对应的配置,如果有则直接返回
    Map<String, List<String>> result = cache.get(classLoader);
    if (result != null) {
        return result;
    }

    result = new HashMap<>();
    try {
        //真正触发加载的地方,利用类加载器加载所有的spring.factories文件,并挨个解析
        Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
        while (urls.hasMoreElements()) {
            
            //获取每个spring.factories文件
            URL url = urls.nextElement();
            UrlResource resource = new UrlResource(url);
            
            //用properties的方式来读取spring.factories文件
            Properties properties = PropertiesLoaderUtils.loadProperties(resource);
            
            for (Map.Entry<?, ?> entry : properties.entrySet()) {
                //按key收集配置项,这里获取的就是'com.principle.spi.PaymentService'
                String factoryTypeName = ((String) entry.getKey()).trim();
                //这里获取的是value值,即PaymentService的两个实现类名
                String[] factoryImplementationNames =
                        StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
                
                //对一个key对应多个value的情况进行处理:key作为result的key,value存入一个集合
                //result是一个HashMap,这里把接口'com.principle.spi.PaymentService'作为result的key,把它的两个实现类放入一个list中作为result的value
                for (String factoryImplementationName : factoryImplementationNames) {
                    result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
                            .add(factoryImplementationName.trim());
                }
            }
        }

        // 去重处理,去除重复的实现类(就是去重spring.factories中重复的value),并将列表转换为不可变列表
        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;
}
  • 作用:加载所有 META-INF/spring.factories 文件中的配置,并将其解析为 Map<String, List<String>> 的形式。
  • 流程
    1. 缓存检查:首先检查缓存中是否已经加载过该 ClassLoader 对应的配置,如果有则直接返回。
    2. 加载文件:通过 ClassLoader.getResources 方法加载类路径下所有 META-INF/spring.factories 文件。
    3. 解析配置
      • 将文件内容解析为 Properties 对象。
      • 遍历 Properties,将键(要加载类的全限定名)和值(实现类的全限定名列表)存入 result 中。
      • 使用 computeIfAbsent 方法确保每个键对应的值是一个 List
    4. 去重和不可变处理
      • 对每个键对应的实现类列表进行去重。
      • 将列表转换为不可变列表(unmodifiableList)。
    5. 缓存结果:将解析结果存入缓存,避免重复加载。
    6. 异常处理:如果加载过程中发生 IOException,抛出 IllegalArgumentException

总结起来,SpringFactoriesLoader 会将项目路径下的每一个spring.factories 都当作properties 进行解析,提取出内容中的每一对映射关系,然后将其存入到全局缓存中。



Spring Boot的装配原理

Spring Boot的自动装配其实就是模块装配+条件装配+SPI机制的组合使用,这一切都体现在主启动类的注解@SpringBootApplication上。而其中关键的就是它内部的另外三个注解:

image-20250207192955728

下面逐个讲解三个注解

@ComponentScan

@ComponentScan放在主启动类注解里的作用就是扫描主启动类所在的包及其子包下的所有组件,这也是主启动类要放在所有类所在包最外层的原因。

不同于平时使用该注解,这里多了两个过滤条件,TypeExcludeFilterAutoConfigurationExcludeFilter

  • TypeExcludeFilter 是一个用于类型排除的过滤器,主要用于在组件扫描过程中排除特定类型的类。它的作用是方便开发者排除一些特定的类不被注册。因为开发者可以继承 TypeExcludeFilter 并重写 match 方法,实现自定义的排除逻辑,满足排除规则的类将不会被注册到IOC容器中。
  • AutoConfigurationExcludeFilter则是一个用于排除自动配置类的过滤器。它所排除的类要满足两个条件,一是被@Configuration标注(即是一个配置类),二是被定义在spring.factories 文件的EnableAutoConfiguration之中(即是一个自动配置类)。因为自动配置类会通过spring.factories文件被加载,如果某一些被错误地包含在组件扫描路径中,可能会导致重复加载或配置冲突,所以用此类来进行排除。

@SpringBootConfiguration

@SpringBootConfiguration其内部又标注了@Configuration注解,所以可以简单理解标注了@SpringBootConfiguration注解也就是标注了一个@Configuration,代表这个类是一个注解配置类。

image-20250207195108723


@EnableAutoConfiguration

@EnableAutoConfiguration是自动装配中最核心最重要的注解。它的作用是启用 Spring Boot 的自动配置机制。通过该注解,Spring Boot 可以根据应用的依赖和配置,自动配置 Spring 应用上下文中的 Bean 和组件。

@EnableAutoConfiguration自身又是一个组合注解,其内部包含了两个重要的注解:

image-20250207195803880

  1. @AutoConfigurationPackage

    该注解导入了一个AutoConfigurationPackages.Registrar.class

    image-20250207195954932

    所以,该注解的实际作用是将主启动类所在的包记录下来并注册到AutoConfigurationPackages中,而AutoConfigurationPackages的内部类Registrar会将默认主启动类所在包路径作为bean对象注册到IOC容器中,存放包路径的类为BasePackages(存入其名为packages的集合中)。

    除了提供给spring boot内部使用,保存包路径的另一个作用就是整合其他第三方技术使用,例如MyBatis,MyBatis内部的AutoConfiguredMapperScannerRegistrar会提取保存的包路径来进行后续的Mapper接口扫描工作。

  2. @Import(AutoConfigurationImportSelector.class)

    此处作用是导入一个AutoConfigurationImportSelector,而该类是DeferredImportSelector的实现类,上面模块装配的例子中已讲解此类的作用,目的就是用来确保自动配置类在所有开发者定义的配置类之后加载,避免自动配置类覆盖用户自定义的配置,这也是自动配置类在开发者自定义配置类之后加载生效的原因。

    AutoConfigurationImportSelector实际发挥作用是在其重写的getImportGroup方法中:

    @Override
    public Class<? extends Group> getImportGroup() {
    	return AutoConfigurationGroup.class;
    }
    

    getImportGroup方法返回的AutoConfigurationGroup类中的process方法,是真正负责加载所有自动配置类的入口:

    @Override
    public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) {
        //断言,确保deferredImportSelector是由AutoConfigurationImportSelector导入而来
    	Assert.state(deferredImportSelector instanceof AutoConfigurationImportSelector,
    			() -> String.format("Only %s implementations are supported, got %s",
    					AutoConfigurationImportSelector.class.getSimpleName(),
    					deferredImportSelector.getClass().getName()));
    	
        //加载自动配置类,移除被去掉的自动配置类,然后封装为Entry返回
        AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector).getAutoConfigurationEntry(annotationMetadata);
        
        //最后将自动配置类名存储起来,存储在名为entries的LinkedHashMap中
    	this.autoConfigurationEntries.add(autoConfigurationEntry);
    	for (String importClassName : autoConfigurationEntry.getConfigurations()) {
    		this.entries.putIfAbsent(importClassName, annotationMetadata);
    	}
    }
    

    上面的getAutoConfigurationEntry方法是加载自动配置类的核心逻辑,其分为三步:加载自动配置类、移除被去掉的自动配置类、最后封装为Entry返回,源码如下:

    protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
    	
        //判断自动配置是否启用
        if (!isEnabled(annotationMetadata)) {
    		return EMPTY_ENTRY;
    	}
        //获取注解中的配置属性。这些属性是决定是否启用某些自动配置类的关键因素。
    	AnnotationAttributes attributes = getAttributes(annotationMetadata);
        //真正加载自动配置类的步骤,会从spring.factories文件中获取自动配置类
    	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);
        //用于触发事件通知,告知 Spring 系统哪些自动配置类将被导入,哪些被排除。这有助于 Spring 管理和记录自动配置的过程。
    	fireAutoConfigurationImportEvents(configurations, exclusions);
        //返回一个包含了所有经过处理的配置类和排除的配置类的对象
    	return new AutoConfigurationEntry(configurations, exclusions);
    }
    

    上面源码中的getCandidateConfigurations方法和getExclusions方法是两个非常重要的处理步骤,

    getCandidateConfigurations方法是执行SPI机制的方法,可以看到其内部有SpringFactoriesLoader,它用来读取解析spring.factories文件:

    protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
        
    	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;
     }   
    

    getExclusions是获取被显式移除的自动配置类的方法,显式移除的方式包括 @SpringBootApplication(exclude = {…})@SpringBootApplication(excludeName = {…}),以及在配置文件配置spring.autoconfigure.exclude等:

    protected Set<String> getExclusions(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    	Set<String> excluded = new LinkedHashSet<>();
    	excluded.addAll(asList(attributes, "exclude"));
    	excluded.addAll(Arrays.asList(attributes.getStringArray("excludeName")));
    	excluded.addAll(getExcludeAutoConfigurationsProperty());
    	return excluded;
    }
    

    在经过上述的处理过程后,需要加载的自动配置类会全部收集完成,存储AutoConfigurationGroup类中名为entries的LinkedHashMap中,后续IOC容器会从中提取自动配置类并解析,完成自动装配的加载。

总结

Spring Boot通过@SpringBootApplication注解实现自动装配机制:

  • @SpringBootApplication中的@ComponentScan,可以默认扫描当前包及其子包下所有的组件。
  • @SpringBootApplication中的@EnableAutoConfiguration包含@AutoConfigurationPackage注解,可以记录最外层根包的位置,以便于整合第三方技术框架使用。
  • @EnableAutoConfiguration导入的AutoConfigurationImportSelector会利用Spring的SPI机制从spring.factories文件中获取自动配置类并进行加载
  • 另外,spring.factories文件EnableAutoConfiguration中的很多自动配置类都包含@Conditional及其衍生注解,实现了开发者配置优先的原则,同时保留了自动配置的灵活性。

补充

Spring Boot 2.7 后的版本增加了imports文件实现自动配置类的读取,对旧版本spring.factories的实现方式进行了优化

该文件在自动配置包的spring文件夹下,全名org.springframework.boot.autoconfigure.AutoConfiguration.imports,其内容的到简化,直接是每行的全类名,不再是key-value键值对形式:

image-20250509095332555

相比旧版的getCandidateConfigurations方法,增加了对imports文件的处理,代码在AutoConfigurationImportSelector类中,如下:

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    
    //读取解析spring.factories文件
	List<String> configurations = new ArrayList<>(
				SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()));
    
    //读取解析imports文件
	ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader()).forEach(configurations::add);
	
    Assert.notEmpty(configurations,
					"No auto configuration classes found in META-INF/spring.factories nor in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you "
					+ "are using a custom packaging, make sure that file is correct.");
	return configurations;
}

进入ImportCandidates.load方法内部,查看imports文件的处理过程:

public static ImportCandidates load(Class<?> annotation, ClassLoader classLoader) {
    
    //校验注解非空
	Assert.notNull(annotation, "'annotation' must not be null");
    
    //如果传入的 classLoader 为 null,则使用当前类的类加载器-ImportCandidates.class.getClassLoader(),目的是为确保后续资源加载时使用正确的类加载器
	ClassLoader classLoaderToUse = decideClassloader(classLoader);
    
    //构建资源文件路径:LOCATION 是常量,值为 "META-INF/spring/%s.imports",传入的注解是@AutoConfiguration,所以此处获取的值为META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
	String location = String.format(LOCATION, annotation.getName());
    
	//通过类加载器扫描所有类路径(包括 JAR 包)中匹配的 AutoConfiguration.imports 文件。会返回多个文件的 URL 枚举
    Enumeration<URL> urls = findUrlsInClasspath(classLoaderToUse, location);
    
    //将url添加到集合中用于后续加载
	List<String> importCandidates = new ArrayList<>();
	while (urls.hasMoreElements()) {
		URL url = urls.nextElement();
		importCandidates.addAll(readCandidateConfigurations(url));
	}
    
	return new ImportCandidates(importCandidates);
}

ImportCandidates中的内容断点截图如下,对应的就是imports文件中的内容:

image-20250509100407411

imports文件对比旧版本spring.factories的实现方式:

  1. 简化配置
    无需重复写键名(如 EnableAutoConfiguration=),直接列出类名即可。
  2. 性能优化
    减少文件解析开销(无需解析键值对)。
  3. 明确性
    文件名直接关联 @AutoConfiguration 注解,语义更清晰。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值