本文基于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 {@Beanpublic 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属性中可包含四种,代表它可以导入四种不同的组件,它们分别是:
- 普通类
- @Configuration标记的配置类
- ImportSelector的实现类
- 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 {@Beanpublic Cashier zhangsan(){return new Cashier("张三");}@Beanpublic 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 {@Beanpublic 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 {@Overridepublic 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 {@Overridepublic void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {//第一个参数是bean的名称(即id),第二个参数是通过RootBeanDefinition指定要注册bean的字节码(.class),这种方式相当于是向IOC容器注册了一个普通的单实例beanregistry.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 {@Overridepublic 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 {@Overridepublic 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 {@Overridepublic 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 {@Beanpublic Cashier zhangsan(){return new Cashier("张三");}@Beanpublic 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 {@Overridepublic 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 机制的实现示例
工作流程:
- 定义服务接口:首先定义一个服务接口,这是应用需要的功能定义。
- 实现服务接口:然后,服务提供者实现该接口,提供具体的服务实现。
- 提供服务描述文件:每个服务提供者还需要在
META-INF/services
目录下创建一个描述文件,文件名为接口的完全限定名,文件内容是服务提供者的实现类的全限定类名。 - 加载服务实现:通过
ServiceLoader
类,服务消费者可以在运行时动态加载符合接口规范的实现类。
假设我们有一个接口 PaymentService
,它表示一种支付服务的接口,并且我们提供了两个实现类 PaypalPaymentService
和 StripePaymentService
。
定义服务接口(PaymentService
):
public interface PaymentService {void processPayment(double amount);
}
提供服务实现类(PaypalPaymentService
和 StripePaymentService
)
public class PaypalPaymentService implements PaymentService {@Overridepublic void processPayment(double amount) {System.out.println("Processing payment of " + amount + " through PayPal.");}
}
public class StripePaymentService implements PaymentService {@Overridepublic 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
内容:
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
方法。假设 PaypalPaymentService
和 StripePaymentService
两个实现都在描述文件中,输出如下:
Processing payment of 100.0 through PayPal.
Processing payment of 100.0 through Stripe.
SPI 的优势:
- 解耦:SPI 允许服务的实现与调用解耦,服务消费者并不知道具体的实现类,具体的实现类是由 SPI 动态加载的。
- 可扩展性:如果需要扩展应用的功能,只需要添加新的服务提供者(即实现类)和描述文件,而不需要修改原有代码。
- 插件化:SPI 可以用于实现插件机制,服务消费者只需要通过接口与服务进行交互,不关心具体的插件实现。
常见的 Java SPI 应用场景:
- JDBC 驱动程序:不同数据库的驱动程序通过 SPI 提供,Java 在运行时可以根据需要加载不同的数据库驱动。
- 日志框架:许多日志框架(如 SLF4J、Log4j、Logback)都使用 SPI 机制来加载不同的日志实现。
- Java 服务框架(如 Spring):Spring 使用 SPI 来加载不同的持久化框架(如 JPA 或 MyBatis)的实现。
- 序列化框架: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
文件路径位置及内容截图:
其实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) {//第二个参数传当前类的ClassLoaderList<PaymentService> paymentServices =SpringFactoriesLoader.loadFactories(PaymentService.class, PaymentProcessor.class.getClassLoader());paymentServices.forEach(paymentService -> System.out.println(paymentService));System.out.println("------------------分割线---------------------");//第二个参数传当前类的ClassLoaderList<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获取的加载器
- 流程:
- 确定类加载器。
- 调用
loadSpringFactories
方法加载所有META-INF/spring.factories
文件中的配置。 - 根据
factoryType
的名称(全限定名)从配置中获取对应的实现类列表。 - 如果找不到对应的配置,返回空列表。
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的valuefor (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>>
的形式。 - 流程:
- 缓存检查:首先检查缓存中是否已经加载过该
ClassLoader
对应的配置,如果有则直接返回。 - 加载文件:通过
ClassLoader.getResources
方法加载类路径下所有META-INF/spring.factories
文件。 - 解析配置:
- 将文件内容解析为
Properties
对象。 - 遍历
Properties
,将键(要加载类的全限定名)和值(实现类的全限定名列表)存入result
中。 - 使用
computeIfAbsent
方法确保每个键对应的值是一个List
。
- 将文件内容解析为
- 去重和不可变处理:
- 对每个键对应的实现类列表进行去重。
- 将列表转换为不可变列表(
unmodifiableList
)。
- 缓存结果:将解析结果存入缓存,避免重复加载。
- 异常处理:如果加载过程中发生
IOException
,抛出IllegalArgumentException
。
- 缓存检查:首先检查缓存中是否已经加载过该
总结起来,SpringFactoriesLoader
会将项目路径下的每一个spring.factories
都当作properties
进行解析,提取出内容中的每一对映射关系,然后将其存入到全局缓存中。
Spring Boot的装配原理
Spring Boot的自动装配其实就是模块装配+条件装配+SPI机制的组合使用,这一切都体现在主启动类的注解@SpringBootApplication
上。而其中关键的就是它内部的另外三个注解:
下面逐个讲解三个注解
@ComponentScan
将@ComponentScan
放在主启动类注解里的作用就是扫描主启动类所在的包及其子包下的所有组件,这也是主启动类要放在所有类所在包最外层的原因。
不同于平时使用该注解,这里多了两个过滤条件,TypeExcludeFilter
与AutoConfigurationExcludeFilter
:
TypeExcludeFilter
是一个用于类型排除的过滤器,主要用于在组件扫描过程中排除特定类型的类。它的作用是方便开发者排除一些特定的类不被注册。因为开发者可以继承TypeExcludeFilter
并重写match
方法,实现自定义的排除逻辑,满足排除规则的类将不会被注册到IOC容器中。AutoConfigurationExcludeFilter
则是一个用于排除自动配置类的过滤器。它所排除的类要满足两个条件,一是被@Configuration
标注(即是一个配置类),二是被定义在spring.factories
文件的EnableAutoConfiguration
之中(即是一个自动配置类)。因为自动配置类会通过spring.factories文件被加载,如果某一些被错误地包含在组件扫描路径中,可能会导致重复加载或配置冲突,所以用此类来进行排除。
@SpringBootConfiguration
@SpringBootConfiguration
其内部又标注了@Configuration
注解,所以可以简单理解标注了@SpringBootConfiguration
注解也就是标注了一个@Configuration
,代表这个类是一个注解配置类。
@EnableAutoConfiguration
@EnableAutoConfiguration
是自动装配中最核心最重要的注解。它的作用是启用 Spring Boot 的自动配置机制。通过该注解,Spring Boot 可以根据应用的依赖和配置,自动配置 Spring 应用上下文中的 Bean 和组件。
@EnableAutoConfiguration
自身又是一个组合注解,其内部包含了两个重要的注解:
-
@AutoConfigurationPackage
该注解导入了一个AutoConfigurationPackages.Registrar.class:
所以,该注解的实际作用是将主启动类所在的包记录下来并注册到AutoConfigurationPackages中,而AutoConfigurationPackages的内部类Registrar会将默认主启动类所在包路径作为bean对象注册到IOC容器中,存放包路径的类为
BasePackages
(存入其名为packages的集合中)。除了提供给spring boot内部使用,保存包路径的另一个作用就是整合其他第三方技术使用,例如MyBatis,MyBatis内部的AutoConfiguredMapperScannerRegistrar会提取保存的包路径来进行后续的Mapper接口扫描工作。
-
@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
及其衍生注解,实现了开发者配置优先的原则,同时保留了自动配置的灵活性。