Spring Boot 自动装配原理
- 使用Spring Boot最方便的一点体验在于我们可以几零配置的搭建一个Spring Web项目,那么他是怎么做到不通过配置来对Bean完成注入的呢。这就要归功于Spring Boot的自动装配实现,他也是Spring Boot中各个Starter的实现基础,Spring Boot的核心。
- 自动装配,就是Spring Boot会自动的寻找Bean并且装配到IOC容器中,如下,我们通过一个Spring Boot项目说明,案例如下:
- 添加pom.xml文件依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ljm</groupId>
<artifactId>spring-cloud-alibaba-learn</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<java.version>8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!--Spring Boot autoConfig start-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin><!-- 指定 JDK 版本 -->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<skip>true</skip>
<source>${java.version}</source>
<target>${java.version}</target>
<compilerVersion>${java.version}</compilerVersion>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
- application.properties
spring.redis.host=localhost
spring.redis.port=6379
- HelloController.java
package com.springcloud.mystart;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author liaojiamin
* @Date:Created in 10:50 2022/3/7
*/
@RestController
public class HelloController {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@GetMapping("helloRedis")
public String helloRedis(){
redisTemplate.opsForValue().set("helloRedis", "RedisTemplateAutoConfig");
return "helloRedis";
}
}
- SpringBoot启动类 Application.java
package com.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author liaojiamin
* @Date:Created in 11:08 2022/3/7
*/
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
- Redis操作界面结果
- 如上案例中,我们并没有通过XML文件的形式注入Redis Template到IOC容器中,但是HelloController中却可以直接用@Autowired注入Redis Template实例,说明,IOC容器中已经存在了Redis Template,这个是Spring Boot自动装配实现的自动加载机制。
- 在针对Redis的配置以及jar来说,我们只添加了一个Start依赖,就完成了依赖组件相关的Bean自动注入,
自实现自动装配标签
- 自动装配在Spring Boot中通过@EnableAutoConfiguration 注解来开启的,这个注解我们没有在项目中显示的声明,他是包含在@SpringBootApplication注解中
- 如下我们可以看到@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 {......}
- 在理解EnableAutoConfiguration之前,我们能想到之前在用SpringMvc的时候用到过一个@Enable注解,他的主要作用就是吧相关的组件Bean装配到IOC容器中。@Enable注解对JavaConfig的进一步的优化,目的是为了减少配置,其实Spring从3.X开始就一直在做优化,减少配置,降低上手的难度,比如常见的有@EnableWebMvc,@EnableScheduling,如下代码
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({DelegatingWebMvcConfiguration.class})
public @interface EnableWebMvc {
}
- 如果我们通过JavaConfig的方式来注入Bean的时候,离不来@Configuration和@Bean,@Enable就是对这两个注解的封装,比如在上面的@EnableWebMvc注解代码中我们能看到@Import,Spring会解析@Import倒入的配置类,通过对这个配置类的实现来找到需要装配的Bean。
EnableAutoConfiguration
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
/**
* Exclude specific auto-configuration classes such that they will never be applied.
* @return the classes to exclude
*/
Class<?>[] exclude() default {};
/**
* Exclude specific auto-configuration class names such that they will never be
* applied.
* @return the class names to exclude
* @since 1.3.0
*/
String[] excludeName() default {};
}
- 以上是@EnableAutoConfiguration的注解,在除了@Import之外还有一个@AutoConfigurationPackage
注解,作用在于用了该注解的类所在的包以及子包下所有的组件扫描到Spring IOC容器中
- @Import注解倒入类一个Configuration结尾的配置类,和上面不同,AutoConfigurationImportSelector类就是本注解的特殊地方
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {......}
- AutoConfigurationImportSelect 实现了DeferredImportSelector,他是ImportSelector的子类,其中有一个selectImports方法返回类一个String数组,这个数组中就是我们需要制定装配到IOC容器中的所有类
- 也就是说他在ImportSelector 的实现类之后,将实现类中返回的class名称都装配进IOC容器里
- 与@Configuration不同的是,ImportSelector是批量的,并且还可以通过逻辑处理来选择对于的Bean,那么我们用一个Demo来验证
- 创建两个类:
/**
* @author liaojiamin
* @Date:Created in 11:49 2022/3/7
*/
public class FilterFirstObj {
}
/**
* @author liaojiamin
* @Date:Created in 11:49 2022/3/7
*/
public class FilterSecondObj {
}
- 创建一个ImportSelect的实现类,直接返回我们新建的类的名字,如下:
/**
* @author liaojiamin
* @Date:Created in 11:49 2022/3/7
*/
public class GpImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
return new String[]{FilterFirstObj.class.getName(), FilterSecondObj.class.getName()};
}
}
- 定义我们自己的注解,这个可以直接抄@EnableAutoConfiguration
/**
* @author liaojiamin
* @Date:Created in 11:52 2022/3/7
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({GpImportSelector.class})
public @interface EnableAutoImport {
}
- 在之前Demo的启动类中从IOC容器中获取我们定义的类:
/**
* @author liaojiamin
* @Date:Created in 11:08 2022/3/7
*/
@SpringBootApplication
@EnableAutoImport
public class Application {
public static void main(String[] args) {
ConfigurableApplicationContext ca = SpringApplication.run(Application.class, args);
System.out.println(ca.getBean(FilterFirstObj.class));
}
}
- 以上的实现方式相比@Import(*Configuration.class)来说,好处在于灵活性更高,还可以实现批量的注入,我们还有在以上的Demo中GpImportSelector添加N个,由于一个Configuration表示某一个技术组件中的一批Bean,所以自动装配的过程只需要扫描置顶路径对于的配置类即可。
自动装配源码分析
- 基于以上Demo的实现,我们找到AutoConfigurationImportSelector的实现,找到其中的selectImports方法,他是ImportSelector接口的实现,如下:
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,
annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
-
如上源码中有两个功能:
- 从META-INF/spring-autoconfigure-metadata.properties中加载自动装配的条件元数据
- 手机所有符合条件的配置类,autoConfigurationEntry.getConfigurations(),完成自动装配
-
在AutoConfigurationImportSelector 类中并不直接执行对呀selectImport方法,其中有一个process方法,这个方法最中还是会调用getAutoConfigurationEntry方法获取自动装配的所有配置类,我们可以看如下源码
-
process方法
@Override
public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) {
Assert.state(deferredImportSelector instanceof AutoConfigurationImportSelector,
() -> String.format("Only %s implementations are supported, got %s",
AutoConfigurationImportSelector.class.getSimpleName(),
deferredImportSelector.getClass().getName()));
AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector)
.getAutoConfigurationEntry(getAutoConfigurationMetadata(), annotationMetadata);
this.autoConfigurationEntries.add(autoConfigurationEntry);
for (String importClassName : autoConfigurationEntry.getConfigurations()) {
this.entries.putIfAbsent(importClassName, annotationMetadata);
}
}
- getAutoConfigurationEntry方法
protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
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 = filter(configurations, autoConfigurationMetadata);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
-
通过Debug代码可以看每个步骤得到的返回值,简单分析代码的作用
- getAttributes 获取@EnableAutoCOnfiguration注解中的属性exclude, excludeName
- getCandidateConfigurations获得所有自动装配的配置类。
- removeDuplicates 用LinkedHashSet实现去重
- getExclusions更具@EnableAutoConfiguration注解中配置的exclude等属性吧不需要自动装配的配置类移除
- fireAutoConfigurationImportEvents广播事件
- 最后返回经过过滤之后的配置类集合
-
以上步骤中,核心在于获取自动装配的配置类getCandidateConfigurations,其他只是在最筛选等其他步骤,我们看该方法实现
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;
}
-
如上代码用到了SpringFactoriesLoader,他是Spring内部提供的一种类加载方式,类似java的SPI,他会扫描classpath目录下的META-INF/spring.factories文件,spring.factories文件中的数据以 key=value 的形式存储,也就是getCandidateConfigurations 方法的返回值
-
如下图是spring-boot-autoconfiguration对应的jar包中文件,我们可以在jar中找到对应的spring.factories
-
内容如下
......
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,\
......
- 里面包含了多种Spring支持的组件的加载,我们这以Redis为案例,通过Debug,我们查看getCandidateConfigurations所扫描到的所有类,如下图所示,其中就包括我们上图中找到的Redis的支持:
- 我们打开RedisAutoConfiguration可以看到,他是一个基于JavaConfig形式的配置类,如下:
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
- 除了基本的Configuration 和Bean两个注解,还有一个COnditionalOnClass,这个条件控制机制在这里用途是判断classpath下是否存在RedisOperations这个类,我们找一下这个类属于那个jar中,如下图
- 如上图,所示,他在spring-data-redis中,而我们只引入了一个有关redis的jar就是那个redis-start,那么结论显而易见了,在Spring Boot自动装配的时候,他能扫描到所有支持的组件,但是他实际加载到IOC中的会依据每个组件的condition进行第一次筛选,只有找到对应的资源文件他才会去加载。
- @EnableConfigurationProperties注解也是我们需要关注的,他说有关属性配置的,也就是我们按照约定在application.properties中配置的Redis的参数,如下Redis.properties
@ConfigurationProperties(prefix = "spring.redis")
public class RedisProperties {
/**
* Database index used by the connection factory.
*/
private int database = 0;
/**
* Connection URL. Overrides host, port, and password. User is ignored. Example:
* redis://user:password@example.com:6379
*/
private String url;
/**
* Redis server host.
*/
private String host = "localhost";
/**
* Login password of the redis server.
*/
private String password;
/**
* Redis server port.
*/
private int port = 6379;
/**
* Whether to enable SSL support.
*/
private boolean ssl;
/**
* Connection timeout.
*/
private Duration timeout;
/**
* Client name to be set on connections with CLIENT SETNAME.
*/
private String clientName;
private Sentinel sentinel;
private Cluster cluster;
......
}
- 我们的properties中的配置样式的由来就是由此得出的
spring.redis.host=127.0.0.1
spring.redis.port=6379
- 由此自动装配的基本原理就完结了,总结过程如下:
- 通过@Import(AutoConfigurationImportSelector)实现配置类的导入,但是这里并不是传统意义上的单个配置类装配
- AutoConfigurationImportSelector实现了ImportSelector接口,重写了selectImports,他用来实现选择性批量配置类的装配
- 通过Spring提供的SpringFactoriesLoader机制,扫描classpath路径下的META-INF/spring.factories读取自动装配的配置类
- 通过筛选条件,吧不符合的配置类移除,最中完成自动装配