Spring boot 为了解决Bean的复杂配置,引入了自动装配机制;那么什么是自动装配,它的原理又是什么呢?我们先通过以下例子来了解以一下什么是自动装配。
引入依赖包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.6.0</version>
</dependency>
配置相关参数
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=
spring.redis.database=0
controller
@RestController
@RequestMapping(value = "/redis")
public class RedisController {
@Autowired
private RedisTemplate redisTemplate;
@RequestMapping("/save")
public String save(@RequestParam String key, @RequestParam String value) {
redisTemplate.opsForValue().set(key, value);
return "ok";
}
@RequestMapping("/get")
public String get(@RequestParam String key) {
return (String) redisTemplate.opsForValue().get(key);
}
}
这是一个很简单的集成 redis 的案例,通过这个案例我们可以看出:RedisTemplate
这个类的 bean 对象,我们并没有通过 xml 的形式或者 注解 的形式注入到 ioc 容器中,但是我们可以直接通过 Autowired
注解自动从容器里面拿到相应的 bean 对象,从而进行属性的注入。
这就是Spring boot 中的自动装配,那这是怎么做到的呢?我们来分析一下它的原理,从而来理解RedisTemplate
是如何注入的。
我们先来分析下上面案例的流程:
首先我们在集成的时候只做了一件事:引入依赖包;然后启动项目后,对象就自动进入到 IOC 容器中了。
那它是如何自动注入的呢?我们平常手动注入对象的时候,是通过 component
或者就是configuration
中的bean
注入。那它会不会也是呢?如果是这样,那按照理论来说应该也有一个componentscan
来扫描对应路径,从而将对象注入;但是 spring boot 中可以引入各种各样的依赖,扫描的路径肯定也是千奇百怪的,那我们猜测应该是有一个固定的路径让其去扫描,从而能将依赖自动注入到容器中。
这个逻辑是不是有点眼熟,这不就是SPI机制吗?接下来我们从源码中逐步看下是如何实现的。
@SpringBootApplication
注解是入口:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
}
其中比较重要的是有以下三个注解:
@SpringBootConfiguration
继承了 Configuration
,表示当前是注解类
@EnableAutoConfiguration
开启 spring boot 的注解功能,自动装配机制,借助 @import
注解的功能。
@ComponentScan(excludeFilters = {})
自动扫描并加载符合条件的组件(比如component
,controller
等),并将这些对象加载到 ioc 容器中。
我们可以通过basepackages
等属性来定制其自动扫描的范围,如果不指定,则默认会从声明。@ComponentScan
所在类的 package 进行扫描,这就是为什么我们希望启动类放在根目录下的原因。
这个注解我们看名字就是可以自动装配,显而易见就是最重要的那个了。
@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({AutoConfigurationImportSelector.class})
我们先来看这个注解,看看这个注解做了那些事情:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({Registrar.class})
public @interface AutoConfigurationPackage {
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
}
首先通过 import
注解导入了Registrar
类:
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
Registrar() {
}
// 注册当前启动类的根 package
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
AutoConfigurationPackages.register(registry, (String[])(new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames().toArray(new String[0]));
}
public Set<Object> determineImports(AnnotationMetadata metadata) {
return Collections.singleton(new AutoConfigurationPackages.PackageImports(metadata));
}
}
我们可以这样理解它的作用:将添加该注解的类所在的package 作为自动配置package进行管理。就是我们上文所说的当Spring boot应用启动时会将启动类所在的package作为自动配置的package。
接下来我们来看@Import({AutoConfigurationImportSelector.class})
,借助AutoConfigurationImportSelector
,EnableAutoConfiguration
可以帮助spring boot 项目将所有符合条件(Spring.factories)的bean定义都加载到当前IOC容器中。
该类最大的作用就是帮助我们找相关的配置类,而找配置类的过程就是我们上面所说的SPI机制。
关于SPI机制可以看这篇文章:SPI 机制详解
我们继续来看整体流程:首先看selectImports
方法,该方法是找配置文件的入口。
为什么是这个方法呢?因为这个类继承了DeferredImportSelector
接口,接口又继承了ImportSelector
接口,所以在spring的流程中会进行调用。
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
public AutoConfigurationImportSelector() {
}
// 找配置文件
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return NO_IMPORTS;
} else {
AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
}
}
然后进入getAutoConfigurationEntry
方法:
protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
} else {
AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
// 获取配置信息
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
configurations = this.removeDuplicates(configurations);
Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
this.checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = this.getConfigurationClassFilter().filter(configurations);
this.fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
}
}
继续往下,我们来看看它是如何获取配置信息的getCandidateConfigurations
方法:
该方法主要是去加载各个组件jar下的 spring.factories
文件
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.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;
}
看到SpringFactoriesLoader.loadFactoryNames
是不是很眼熟?和我们SPI机制中的ServiceLoader.load
是不是很类似?看来我们找对地方了,SpringFactoriesLoader
的底层原理其实就是借鉴于JDK的SPI机制。
我们知道SPI机制都是从classpath下的service目录查找对应的文件?那么SpringFactoriesLoader
从哪里查找呢?我们进入该类查看:
一进来我们就看到路径也被写死了,这样我们上面的问题就被解决了。
public final class SpringFactoriesLoader {
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
private static final Log logger = LogFactory.getLog(SpringFactoriesLoader.class);
static final Map<ClassLoader, Map<String, List<String>>> cache = new ConcurrentReferenceHashMap();
}
我们到spring boot 的自动装配jar中查看一下:
整个流程是不是很符合SPI机制,这样redis对象自动装配是不是能稍微想通了?
既然找到了文件,接下来肯定就是读取配置了,我们打断点来看下效果:
可以很清楚的看到spring.factories文件中的配置信息都读取到了。
我们来看下loadFactoryNames
方法,它的入参为工厂类名称和对应的类加载器,方法会根据指定的类加载器,加载该类加载器搜索路径下的指定文件,即spring.factories
文件。
传入的工厂类为接口,而文件中对应的类则是接口的实现类,或最终作为实现类。
配合@EnableAutoConfiguration
使用的话,它更多是提供一种配置查找的功能支持,即根据@EnableAutoConfiguration
的完整类名:org.springframework.boot.autoconfigure.EnableAutoConfiguration
作为查找的KEY,获取对应的一组Configuration
类。
看过源码之后,我们对Spring boot 的装配原理进行一个简单的总结,方便我们记忆:
首先 spring boot 启动
扫描 @SpringBootApplication
注解
扫描@EnableAutoConfiguration
注解
这个注解里面带有一个@import
注解,导入了AutoConfigurationImportSelector
类,这个类实现了DeferredImportSelector
接口,这个接口又实现了ImportSelector
接口,在这个接口里面有一个叫做selectImports
的方法,这个方法实现了配置类的寻找。
整个寻找的过程就是Springboot 的SPI机制。