Spring Boot 自動裝配原始碼分析
基於 Spring Boot 開發的應用中,經常需要引用別的 starter 元件或者自定義公司內部使用的 starter。而 starter 的基礎是 Spring Boot的自動裝配。
什麼是自動裝配
自動裝配,簡單說就是自動將 Bean 裝配到 IoC 容器中。下面通過 Spring Boot 整合 Redis 的案例感受一下自動裝配的用法。
- 專案中引入 starter 依賴:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>2.1.7.RELEASE</version> </dependency>
- 新增配置:在 application.properties 中新增:
spring.redis.host=localhost
spring.redis.port=6379
- 使用:新建測試類 RedisTestController 中使用 RedisTemplate
@RestController public class RedisTestController { @Autowired RedisTemplate<String, String> redisTemplate; @GetMapping("/hello") public String hello() { redisTemplate.opsForValue().set("key", "value"); return "Hello World"; } }
這是我們一般的使用方式。在這個案例中我們並沒有通過 xml 或者註解的方式將 RedisTemplate 注入 IoC 容器中,為什麼在 Controller 中可以直接通過 @Autowired 註解注入 RedisTemplate 例項呢?這說明,我們使用該例項之前,容器中已經存在 RedisTemplate 了。這就是 Spring Boot 自動裝配的結果。
自動裝配的實現
@EnableAutoConfiguration
自動裝配在 Spring Boot 中是通過 @EnableAutoConfiguration 註解實現的,這個註解的宣告是在啟動類註解 @SpringBootApplication 內。
@SpringBootApplication
public class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
}
點進 @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 註解
@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 {};
}
@EnableAutoConfiguration 中 @AutoConfigurationPackage 的作用是把使用了該註解的類所在的包和子包下所有元件掃描到 Spring IoC 容器中。同時,@Import 匯入了 @AutoConfigurationImportSelector,這個類會匯入需要自動配置的類。
@AutoConfigurationImportSelector
點進 @AutoConfigurationImportSelector 中:
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return NO_IMPORTS;
} else {
// 獲取需要自動配置的類
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
}
該註解中重寫了頂級介面 ImportSelector 中的 selectImports 方法來匯入自動配置類,其中又呼叫了自身的 getAutoConfigurationEntry 方法:
protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, 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);
// 排除:根據exclude屬性把不需要自動裝配的配置類排除
Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
this.checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = this.filter(configurations, autoConfigurationMetadata);
// 廣播事件
this.fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
}
}
ImportSelector 可以實現批量裝配,並且還可以通過邏輯處理來實現 Bean 的選擇性裝配,也就是可以根據上下文來決定哪些類能夠被 IoC 容器初始化。
總的來說,getAutoConfigurationEntry 方法先獲取所有配置類,再通過去重,exclude 排除等操作,得到最終需要自動配置的配置類。這裡著重看如何獲取所有的配置類,也就是 getCandidateConfigurations 方法:
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;
}
protected Class<?> getSpringFactoriesLoaderFactoryClass() {
return EnableAutoConfiguration.class;
}
protected ClassLoader getBeanClassLoader() {
return this.beanClassLoader;
}
這裡呼叫 SpringFactoriesLoader.loadFactoryNames 來載入配置,兩個引數,一個是自動配置類的 Class 型別,
EnableAutoConfiguration.class ,另一個是本類的一個類載入器 this.beanClassLoader。
檢視 SpringFactoriesLoader.loadFactoryNames 方法:
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
// 獲取Class檔案的全限定名,這裡對於自動配置,即EnableAutoConfiguration.class.getName()
String factoryTypeName = factoryType.getName();
// 根據下面的方法,通過給定的類載入器載入
return (List)loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);
// 先從快取中取
if (result != null) {
// 如果快取已經載入過,直接返回結果
return result;
} else {
try {
// 載入META-INF/spring.factories檔案,獲取URL集合
Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
LinkedMultiValueMap result = new LinkedMultiValueMap();
// 遍歷URL集合
while(urls.hasMoreElements()) {
URL url = (URL)urls.nextElement();
UrlResource resource = new UrlResource(url);
// 讀取配置
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
Iterator var6 = properties.entrySet().iterator();
while(var6.hasNext()) {
Entry<?, ?> entry = (Entry)var6.next();
String factoryTypeName = ((String)entry.getKey()).trim();
String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
int var10 = var9.length;
for(int var11 = 0; var11 < var10; ++var11) {
String factoryImplementationName = var9[var11];
// 結果轉成集合
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}
cache.put(classLoader, result);
return result;
} catch (IOException var13) {
throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13);
}
}
}
這裡用到了 SpringFactoriesLoader ,它是 Spring 內部提供的一種約定俗成的載入方式,類似於Java 中的 SPI。簡單說,它會掃描 classpath 下的 META-INF/spring.factories 檔案,spring.factories 檔案中的資料以 key = value 形式儲存,而 SpringFactoriesLoader.loadFactoryNames 會根據 key 得到對應的 value 值。因此,這個場景中key 對應為 EnableAutoConfiguration ,value 是多個配置類,也就是 getCandidateConfigurations 方法返回值。
spring.factories
找到依賴中的 spring.factories
該配置檔案以鍵值對給出需要自動配置的配置類,通過上述的程式碼獲取 配置類的 String 陣列。
隨便找個值如 org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration 作為示例:
// 配置類
@Configuration(proxyBeanMethods = false)
// 條件判斷,只有指定類載入後,才載入找個配置
@ConditionalOnClass({RabbitTemplate.class, Channel.class})
// 繫結的配置檔案
@EnableConfigurationProperties({RabbitProperties.class})
// 匯入其他配置
@Import({RabbitAnnotationDrivenConfiguration.class})
public class RabbitAutoConfiguration {
public RabbitAutoConfiguration() {
}
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({RabbitMessagingTemplate.class})
@ConditionalOnMissingBean({RabbitMessagingTemplate.class})
@Import({RabbitAutoConfiguration.RabbitTemplateConfiguration.class})
protected static class MessagingTemplateConfiguration {
protected MessagingTemplateConfiguration() {
}
@Bean
@ConditionalOnSingleCandidate(RabbitTemplate.class)
public RabbitMessagingTemplate rabbitMessagingTemplate(RabbitTemplate rabbitTemplate) {
return new RabbitMessagingTemplate(rabbitTemplate);
}
}
可以看到這裡對配置做了一定的約束,比如載入順序、載入條件、額外的元件等。
@ConditionalOnClass :該系列註解用於條件判斷,之前寫過關於該註解的筆記。
另外,該配置類還綁定了一個配置檔案 @EnableConfigurationProperties({RabbitProperties.class}) ,可以根據繫結的這個配置檔案做個性化修改:
@ConfigurationProperties(
prefix = "spring.rabbitmq"
)
public class RabbitProperties {
private String host = "localhost";
private int port = 5672;
private String username = "guest";
private String password = "guest";
private final RabbitProperties.Ssl ssl = new RabbitProperties.Ssl();
private String virtualHost;
private String addresses;
@DurationUnit(ChronoUnit.SECONDS)
private Duration requestedHeartbeat;
private boolean publisherReturns;
private ConfirmType publisherConfirmType;
private Duration connectionTimeout;
private final RabbitProperties.Cache cache = new RabbitProperties.Cache();
private final RabbitProperties.Listener listener = new RabbitProperties.Listener();
private final RabbitProperties.Template template = new Rab
@ConfigurationProperties 標註當前類是配置檔案,它繫結字首為 ”spring.rabbitmq“ 的配置,我們新增到配置會被傳遞到具體的元件覆蓋預設配置。
總結
簡單總結 Spring Boot 的自動裝配流程:
- 通過 @EnableAutoConfiguration 註解開啟自動配置。
- 自動載入類路徑下 META-INF/spring.factories 檔案,讀取以 EnableAutoConfiguration 的全限定類名對應的值,作為候選配置類。這裡預設給出了很多元件的自動配置類。
- 自動配置類可能會再匯入一些依賴(比如 @Import),或者給出一些配置條件,並且會通過 @Bean 註解把該元件所包含的元件注入到 spring 容器中以供使用。
- 自動配置類還可能會繫結 xxxProperties 配置檔案類,該類又會和應用程式中的
application.properties
中的指定字首繫結。第 3 步注入元件的時候,元件可能還會獲取配置檔案類中的內容,所以使用者可以在 application.properties 修改指定配置,來制定自己的個性化服務。