注:該源碼分析對應SpringBoot版本為2.1.0.RELEASE
1 前言
本篇接
如何分析SpringBoot源碼子產品及結構?--SpringBoot源碼(二)上一篇分析了SpringBoot源碼結構及各個子產品pom之間的關系後,那麼此篇開始就開始解開SpringBoot新特性之一--自動配置的神秘面紗了。因為SpringBoot自動配置原理是基于其大量的條件注解
ConditionalOnXXX
,是以,本節我們先來撸下Spring的條件注解的相關源碼。
2 SpringBoot的派生條件注解
我們都知道,SpringBoot自動配置是需要滿足相應的條件才會自動配置,是以SpringBoot的自動配置大量應用了條件注解
ConditionalOnXXX
。如下圖:
那麼上圖的條件注解如何使用呢?
舉個栗子,我們來看下如何使用和
@ConditionalOnClass
這兩個注解,先看下圖代碼:
@ConditionalOnProperty
HelloWorldEnableAutoConfiguration
這個自動配置類應用了
@ConditionalOnClass
ConditionalOnProperty
兩個條件注解,那麼隻有在滿足:
classpath
中存在
HelloWorldComponent.class
和配置了
hello.world.name
hello.world.age
屬性這兩個條件的情況下才會建立
HelloWorldComponent
這個
bean
。
其實SpringBoot的
@ConditionalOnXXX
等條件注解都是派生注解,那麼什麼是派生注解呢?
就拿上面的栗子來說,以
@ConditionalOnClass(HelloWorldComponent.class)
為例,我們打開
ConditionalOnClass
注解源碼,如下:
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnClassCondition.class)
public @interface ConditionalOnClass {
Class<?>[] value() default {};
String[] name() default {};
}
可以看到
@ConditionalOnClass
注解上面又标注了
@Conditional(OnClassCondition.class)
注解,是以
@ConditionalOnClass
是
@Conditional
的派生注解,
@Conditional(OnClassCondition.class)
@ConditionalOnClass
注解是等價的,即這兩個注解标注在某個配置類上的效果是等價的。
而SpringBoot的自動配置原理正是建立在這些大量的派生條件注解
@ConditionalOnXXX
之上,而這些條件注解的原理跟Spring的Condition接口有關。是以我們先來研究下Condition接口的相關源碼。
3 Condition接口
3.1 Condition接口源碼分析
分析Condition接口源碼前先看下如何自定義
ConditionalOnXXX
注解,舉個栗子,比如自定義一個
@ConditionalOnLinux
注解,該注解隻有在其屬性
environment
是"linux"才會建立相關的bean。定義了以下代碼:
/**
* 實作spring 的Condition接口,并且重寫matches()方法,如果@ConditionalOnLinux的注解屬性environment是linux就傳回true
*
*/
public class LinuxCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// 獲得注解@ConditionalOnLinux的所有屬性
List<AnnotationAttributes> allAnnotationAttributes = annotationAttributesFromMultiValueMap(
metadata.getAllAnnotationAttributes(
ConditionalOnLinux.class.getName()));
for (AnnotationAttributes annotationAttributes : allAnnotationAttributes) {
// 獲得注解@ConditionalOnLinux的environment屬性
String environment = annotationAttributes.getString("environment");
// 若environment等于linux,則傳回true
if ("linux".equals(environment)) {
return true;
}
}
return false;
}
}
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(LinuxCondition.class)
public @interface ConditionalOnLinux {
// 标注是哪個環境
String environment() default "";
}
@Configuration
public class ConditionConfig {
// 隻有`@ConditionalOnLinux`的注解屬性`environment`是"linux"時才會建立bean
@Bean
@ConditionalOnLinux(environment = "linux")
public Environment linuxEnvironment() {
return new LinuxEnvironment();
}
}
上面的代碼我們捋一下:
-
實作了LinuxCondition
接口并實作了Condition
方法,而matches
方法則判斷matches
的注解屬性@ConditionalOnLinux
是否"linux",是則傳回true,否則false。environment
- 然後我們再定義了一個注解
,這個注解是@ConditionalOnLinux
的派生注解,與@Conditional
等價,注意@Conditional(LinuxCondition.class)
注解定義了一個屬性@ConditionalOnLinux
。而我們最終可以利用environment
的LinuxCondition
方法中的參數matches
來擷取AnnotatedTypeMetadata
@ConditionalOnLinux
的值,進而用來判斷值是否為linux"。environment
- 最後我們又定義了一個配置類
,在ConditionConfig
方法上标注了linuxEnvironment
。是以,這裡隻有@ConditionalOnLinux(environment = "linux")
LinuxCondition
方法傳回true才會建立matches
bean
學會了如何自定義
@ConditionalOnXXX
注解後,我們現在再來看下
Condition
接口的源碼:
@FunctionalInterface
public interface Condition {
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
Condition接口主要有一個
matches
方法,該方法決定了是否要注冊相應的
bean
對象。其中
matches
方法中有兩個參數,參數類型分别是
ConditionContext
AnnotatedTypeMetadata
,這兩個參數非常重要。它們分别用來擷取一些環境資訊和注解中繼資料進而用在
matches
方法中判斷是否符合條件。
,顧名思義,主要是跟
ConditionContext
的上下文有關,主要用來擷取
Condition
,
Registry
BeanFactory
Environment
ResourceLoader
等。那麼擷取這些用來幹什麼呢?舉個栗子,比如
ClassLoader
需要靠
OnResourceCondition
ConditionContext
來加載指定資源,
ResourceLoader
OnClassCondition
ConditionContext
來加載指定類等,下面看下其源碼:
ClassLoader
public interface ConditionContext {
BeanDefinitionRegistry getRegistry();
@Nullable
ConfigurableListableBeanFactory getBeanFactory();
Environment getEnvironment();
ResourceLoader getResourceLoader();
@Nullable
ClassLoader getClassLoader();
}
,這個跟注解中繼資料有關,利用
AnnotatedTypeMetadata
可以拿到某個注解的一些中繼資料,而這些中繼資料就包含了某個注解裡面的屬性,比如前面的栗子,利用
AnnotatedTypeMetadata
可以拿到
AnnotatedTypeMetadata
@ConditionalOnLinux
的值。下面看下其源碼:
environment
public interface AnnotatedTypeMetadata {
boolean isAnnotated(String annotationName);
Map<String, Object> getAnnotationAttributes(String annotationName);
Map<String, Object> getAnnotationAttributes(String annotationName, boolean classValuesAsString);
MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationName);
MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationName, boolean classValuesAsString);
}
回到剛才的栗子,我們知道
@ConditionalOnLinux
注解真正起作用的是
Condition
接口的具體實作類
LinuxCondition
matches
方法,那麼這個
matches
方法是在何時被調用的呢?
通過idea調試看調用的棧幀,如下圖:
發現是在
ConditionEvaluator
shouldSkip
方法中調用了
LinuxCondition
matches
方法,自然我們再去看看
ConditionEvaluator
shouldSkip
的方法執行了什麼邏輯。
// 這個方法主要是如果是解析階段則跳過,如果是注冊階段則不跳過
public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) {
// 若沒有被@Conditional或其派生注解所标注,則不會跳過
if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {
return false;
}
// 沒有指定phase,注意phase可以分為PARSE_CONFIGURATION或REGISTER_BEAN類型
if (phase == null) {
// 若标有@Component,@Import,@Bean或@Configuration等注解的話,則說明是PARSE_CONFIGURATION類型
if (metadata instanceof AnnotationMetadata &&
ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) {
return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION);
}
// 否則是REGISTER_BEAN類型
return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN);
}
List<Condition> conditions = new ArrayList<>();
// TODO 獲得所有标有@Conditional注解或其派生注解裡面的Condition接口實作類并執行個體化成對象。
// 比如@Conditional(OnBeanCondition.class)則獲得OnBeanCondition.class,OnBeanCondition.class往往實作了Condition接口
for (String[] conditionClasses : getConditionClasses(metadata)) {
// 将類執行個體化成對象
for (String conditionClass : conditionClasses) {
Condition condition = getCondition(conditionClass, this.context.getClassLoader());
conditions.add(condition);
}
}
// 排序,即按照Condition的優先級進行排序
AnnotationAwareOrderComparator.sort(conditions);
for (Condition condition : conditions) {
ConfigurationPhase requiredPhase = null;
if (condition instanceof ConfigurationCondition) {
// 從condition中獲得對bean是解析還是注冊
requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase();
}
// 若requiredPhase為null或擷取的階段類型正是目前階段類型且不符合condition的matches條件,則跳過
if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) {
return true;
}
}
return false;
}
shouldSkip
這個方法執行的邏輯主要是如果是解析階段則跳過,如果是注冊階段則不跳過;如果是在注冊階段即
REGISTER_BEAN
階段的話,此時會得到所有的
Condition
接口的具體實作類并執行個體化這些實作類,然後再執行下面關鍵的代碼進行判斷是否需要跳過。
if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) {
return true;
}
上面代碼最重要的邏輯是調用了
Condition
接口的具體實作類的
matches
方法,若
matches
傳回
false
,則跳過,不進行注冊
bean
的操作;若
matches
true
,則不跳過,進行注冊
bean
的操作;
好了,
Condition
的源碼分析就到此為止,再往上翻調用方法的話應該就是Spring加載
bean
定義的相關源碼了,不屬于這裡的分析範圍。
3.2 Spring的内置Condition接口實作類
前面我們學會了如何自定義條件注解及
Condition
的源碼分析,那麼我們不禁好奇,Spring究竟内置了哪些
Condition
接口的實作類呢?
那麼看下Spring的
Condition
接口的具體實作類的類圖:
發現Spring内置的
Condition
接口的具體實作類雖然有多個,但隻有
ProfileCondition
不是測試相關的,是以可以說真正的内置的
Condition
接口的具體實作類隻有
ProfileCondition
一個,非常非常少,這跟SpringBoot的大量派生條件注解形成了鮮明的對比。
ProfileCondition
大家都知道,是跟環境有關,比如我們平時一般有
dev
test
prod
環境,而
ProfileCondition
就是判斷我們項目配置了哪個環境的。下面是
ProfileCondition
的源碼,很簡單,這裡就不分析了。
class ProfileCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
if (attrs != null) {
for (Object value : attrs.get("value")) {
if (context.getEnvironment().acceptsProfiles(Profiles.of((String[]) value))) {
return true;
}
}
return false;
}
return true;
}
}
4 SpringBootCondition源碼解析
前面看到Spring對
Condition
的内置注解可以說隻有
ProfileCondition
一個,但是我們都知道,SpringBoot則内置了大量的條件注解
ConditionalOnXXX
。在分析前,我們先來看一下
SpringBootCondition
的整體類圖來個整體的了解,如下圖:
SpringBootCondition
作為SpringBoot條件注解的基類,處于整個類圖的中心,它實作了
Condition
接口,然後又有很多具體的子類
OnXXXCondition
,這些
OnXXXCondition
其實就是
@ConditionalOnXXX
的條件類。
我們先來看下
SpringBootCondition
這個父類是主要做了哪些事情,抽象了哪些共有的邏輯?
SpringBootConditon
Condition
接口,作為SpringBoot衆多條件注解
OnXXXCondtion
的父類,它的作用主要就是列印一些條件注解評估報告的日志,比如列印哪些配置類是符合條件注解的,哪些是不符合的。列印的日志形式如下圖:
因為
SpringBootConditon
Condition
接口,也實作了
matches
方法,是以該方法同樣也是被
ConditionEvaluator
shouldSkip
方法中調用,是以我們就以
SpringBootConditon
matches
方法為入口去進行分析。直接上代碼:
// SpringBootCondition.java
public final boolean matches(ConditionContext context,
AnnotatedTypeMetadata metadata) {
// 得到metadata的類名或方法名
String classOrMethodName = getClassOrMethodName(metadata);
try {
// 判斷每個配置類的每個條件注解@ConditionalOnXXX是否滿足條件,然後記錄到ConditionOutcome結果中
// 注意getMatchOutcome是一個抽象模闆方法,交給OnXXXCondition子類去實作
ConditionOutcome outcome = getMatchOutcome(context, metadata);
// 列印condition評估的日志,哪些條件注解@ConditionalOnXXX是滿足條件的,哪些是不滿足條件的,這些日志都列印出來
logOutcome(classOrMethodName, outcome);
// 除了列印日志外,這些是否比對的資訊還要記錄到ConditionEvaluationReport中
recordEvaluation(context, classOrMethodName, outcome);
// 最後傳回@ConditionalOnXXX是否滿足條件
return outcome.isMatch();
}
catch (NoClassDefFoundError ex) {
throw new IllegalStateException(
"Could not evaluate condition on " + classOrMethodName + " due to "
+ ex.getMessage() + " not "
+ "found. Make sure your own configuration does not rely on "
+ "that class. This can also happen if you are "
+ "@ComponentScanning a springframework package (e.g. if you "
+ "put a @ComponentScan in the default package by mistake)",
ex);
}
catch (RuntimeException ex) {
throw new IllegalStateException(
"Error processing condition on " + getName(metadata), ex);
}
}
上面代碼的注釋已經非常詳細,我們知道了
SpringBootCondition
抽象了所有其具體實作類
OnXXXCondition
的共有邏輯--
condition
評估資訊列印,最重要的是封裝了一個模闆方法
getMatchOutcome(context, metadata)
,留給各個
OnXXXCondition
具體子類去覆寫實作屬于自己的判斷邏輯,然後再傳回相應的比對結果給
SpringBootCondition
用于日志列印。
是以我們知道了
SpringBootCondition
其實就是用來列印
condition
評估資訊的,對于其他枝節方法我們不必追究過深,免得丢了主線。我們現在的重點是放在交給
OnXXXCondition
子類實作的模闆方法上
getMatchOutcome(context, metadata);
,因為這個方法将會由很多
OnXXXCondition
覆寫重寫判斷邏輯,這裡是我們接下來分析的重點。
SpringBootCondition
有衆多具體實作類,下面隻挑
OnResourceCondition
,
OnBeanCondition
OnWebApplicationCondition
進行講解,而
AutoConfigurationImportFilter
跟自動配置有關,則留到自動配置源碼解析的時候再進行分析。
4.1 OnResourceCondition源碼分析
現在先來看下一個邏輯及其簡單的注解條件類
OnResourceCondition
OnResourceCondition
繼承了
SpringBootCondition
父類,覆寫了其
getMatchOutcome
方法,用于
@ConditionalOnResource
注解指定的資源存在與否。
OnResourceCondition
的判斷邏輯非常簡單,主要拿到
@ConditionalOnResource
注解指定的資源路徑後,然後用
ResourceLoader
根據指定路徑去加載看資源存不存在。下面直接看代碼:
先來看下
@ConditionalOnResource
的代碼,
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnResourceCondition.class)
public @interface ConditionalOnResource {
/**
* The resources that must be present.
* @return the resource paths that must be present.
*/
String[] resources() default {};
}
再來看
OnResourceCondition
的代碼:
@Order(Ordered.HIGHEST_PRECEDENCE + 20)
class OnResourceCondition extends SpringBootCondition {
private final ResourceLoader defaultResourceLoader = new DefaultResourceLoader();
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
// 獲得@ConditionalOnResource注解的屬性中繼資料
MultiValueMap<String, Object> attributes = metadata
.getAllAnnotationAttributes(ConditionalOnResource.class.getName(), true);
// 獲得資源加載器,若ConditionContext中有ResourceLoader則用ConditionContext中的,沒有則用預設的
ResourceLoader loader = (context.getResourceLoader() != null)
? context.getResourceLoader() : this.defaultResourceLoader;
List<String> locations = new ArrayList<>();
// 将@ConditionalOnResource中定義的resources屬性值取出來裝進locations集合
collectValues(locations, attributes.get("resources"));
Assert.isTrue(!locations.isEmpty(),
"@ConditionalOnResource annotations must specify at "
+ "least one resource location");
// missing集合是裝不存在指定資源的資源路徑的
List<String> missing = new ArrayList<>();
// 周遊所有的資源路徑,若指定的路徑的資源不存在則将其資源路徑存進missing集合中
for (String location : locations) {
// 這裡針對有些資源路徑是Placeholders的情況,即處理${}
String resource = context.getEnvironment().resolvePlaceholders(location);
if (!loader.getResource(resource).exists()) {
missing.add(location);
}
}
// 如果存在某個資源不存在,那麼則報錯
if (!missing.isEmpty()) {
return ConditionOutcome.noMatch(ConditionMessage
.forCondition(ConditionalOnResource.class)
.didNotFind("resource", "resources").items(Style.QUOTE, missing));
}
// 所有資源都存在,那麼則傳回能找到就提的資源
return ConditionOutcome
.match(ConditionMessage.forCondition(ConditionalOnResource.class)
.found("location", "locations").items(locations));
}
// 将@ConditionalOnResource中定義的resources屬性值取出來裝進locations集合
private void collectValues(List<String> names, List<Object> values) {
for (Object value : values) {
for (Object item : (Object[]) value) {
names.add((String) item);
}
}
}
}
OnResourceCondition
getMatchOutcome
方法非常簡單,這裡不再詳述。
4.2 OnBeanCondition源碼分析
OnBeanCondition
同樣繼承了
FilteringSpringBootCondition
父類,覆寫了父類
FilteringSpringBootCondition
getOutcomes
方法。而
FilteringSpringBootCondition
又是
SpringBootCondition
的子類,
FilteringSpringBootCondition
跟自動配置類過濾有關,這裡先不分析。值得注意的是
OnBeanCondition
同樣重寫了
SpringBootCondition
getMatchOutcome
方法,用來判斷Spring容器中是否存在指定條件的
bean
。同時是
OnBeanCondition
@ConditionalOnBean
@ConditionalOnSingleCandidate
ConditionalOnMissingBean
同樣,先來看
OnBeanCondition
複寫父類
SpringBootCondition
getMatchOutcome
方法的代碼:
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
ConditionMessage matchMessage = ConditionMessage.empty();
// (1),配置類(metadata)标注@ConditionalOnBean注解的情況
if (metadata.isAnnotated(ConditionalOnBean.class.getName())) {
// 将@ConditionalOnBean注解屬性封裝進BeanSearchSpec對象中
// 注意BeanSearchSpec是一個靜态内部類,用來存儲@ConditionalOnBean和@ConditionalOnMissingBean注解的屬性值
BeanSearchSpec spec = new BeanSearchSpec(context, metadata,
ConditionalOnBean.class);
// 調用getMatchingBeans得到符合條件的bean
MatchResult matchResult = getMatchingBeans(context, spec);
// 如果不比對
if (!matchResult.isAllMatched()) {
String reason = createOnBeanNoMatchReason(matchResult);
return ConditionOutcome.noMatch(ConditionMessage
.forCondition(ConditionalOnBean.class, spec).because(reason));
}
// 如果比對
matchMessage = matchMessage.andCondition(ConditionalOnBean.class, spec)
.found("bean", "beans")
.items(Style.QUOTE, matchResult.getNamesOfAllMatches());
}
// (2),配置類(metadata)标注@ConditionalOnSingleCandidate注解的情況
if (metadata.isAnnotated(ConditionalOnSingleCandidate.class.getName())) {
BeanSearchSpec spec = new SingleCandidateBeanSearchSpec(context, metadata,
ConditionalOnSingleCandidate.class);
MatchResult matchResult = getMatchingBeans(context, spec);
if (!matchResult.isAllMatched()) {
return ConditionOutcome.noMatch(ConditionMessage
.forCondition(ConditionalOnSingleCandidate.class, spec)
.didNotFind("any beans").atAll());
}
else if (!hasSingleAutowireCandidate(context.getBeanFactory(),
matchResult.getNamesOfAllMatches(),
spec.getStrategy() == SearchStrategy.ALL)) {
return ConditionOutcome.noMatch(ConditionMessage
.forCondition(ConditionalOnSingleCandidate.class, spec)
.didNotFind("a primary bean from beans")
.items(Style.QUOTE, matchResult.getNamesOfAllMatches()));
}
matchMessage = matchMessage
.andCondition(ConditionalOnSingleCandidate.class, spec)
.found("a primary bean from beans")
.items(Style.QUOTE, matchResult.getNamesOfAllMatches());
}
// (3),配置類(metadata)标注@ConditionalOnMissingBean注解的情況
if (metadata.isAnnotated(ConditionalOnMissingBean.class.getName())) {
BeanSearchSpec spec = new BeanSearchSpec(context, metadata,
ConditionalOnMissingBean.class);
MatchResult matchResult = getMatchingBeans(context, spec);
if (matchResult.isAnyMatched()) {
String reason = createOnMissingBeanNoMatchReason(matchResult);
return ConditionOutcome.noMatch(ConditionMessage
.forCondition(ConditionalOnMissingBean.class, spec)
.because(reason));
}
matchMessage = matchMessage.andCondition(ConditionalOnMissingBean.class, spec)
.didNotFind("any beans").atAll();
}
// 最終傳回matchMessage
return ConditionOutcome.match(matchMessage);
}
我們可以看到
OnBeanCondition
類覆寫的
getMatchOutcome
方法分别處理了标注
@ConditionalOnBean
@ConditionalOnSingleCandidate
@ConditionalOnMissingBean
注解的情況,分别對應上面代碼注釋的
(1)
(2)
(3)
處。
現在我們隻看針對
@ConditionalOnBean
注解的處理邏輯,從上面代碼中可以看到若配置類(metadata)标注
@ConditionalOnBean
注解的話,主要做了以下事情:
- 将該注解屬性提取出來封裝進
對象中;BeanSearchSpec
- 然後調用
方法來擷取是否有比對的getMatchingBeans(context, spec)
;bean
- 最後傳回
的比對情況;bean
可以看到最重要的邏輯是第2步,那麼我們再來看下
getMatchingBeans
方法,直接上代碼:
protected final MatchResult getMatchingBeans(ConditionContext context,
BeanSearchSpec beans) {
// 獲得Spring容器的beanFactory
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
// 判斷bean的搜尋政策是否是SearchStrategy.ANCESTORS政策
if (beans.getStrategy() == SearchStrategy.ANCESTORS) {
BeanFactory parent = beanFactory.getParentBeanFactory();
Assert.isInstanceOf(ConfigurableListableBeanFactory.class, parent,
"Unable to use SearchStrategy.PARENTS");
beanFactory = (ConfigurableListableBeanFactory) parent;
}
// MatchResult用來存儲bean的比對結果
MatchResult matchResult = new MatchResult();
// 如果bean的搜尋政策不是SearchStrategy.CURRENT的話,則置considerHierarchy為true
boolean considerHierarchy = beans.getStrategy() != SearchStrategy.CURRENT;
// 擷取TypeExtractor,TypeExtractor是用來判斷bean的類型的
TypeExtractor typeExtractor = beans.getTypeExtractor(context.getClassLoader());
// 擷取是否有被忽略bean類型,若有的話将該bean類型的名稱裝進beansIgnoredByType集合
// 這裡主要是針對@ConditionalOnMissingBean的ignored屬性
List<String> beansIgnoredByType = getNamesOfBeansIgnoredByType(
beans.getIgnoredTypes(), typeExtractor, beanFactory, context,
considerHierarchy);
// 周遊bean的所有類型
for (String type : beans.getTypes()) {
// 調用getBeanNamesForType方法根據bean類型得到所有符合條件的bean類型,并放到typeMatches集合
Collection<String> typeMatches = getBeanNamesForType(beanFactory, type,
typeExtractor, context.getClassLoader(), considerHierarchy);
// 移除掉Ignored的類型
typeMatches.removeAll(beansIgnoredByType);
// 若typeMatches為空,那麼則說明正在周遊的這個type類型不符合比對條件,此時用matchResult記錄一下這個不符合條件的類型
if (typeMatches.isEmpty()) {
matchResult.recordUnmatchedType(type);
}
// 若typeMatches不為空,那麼則說明正在周遊的這個type類型符合比對條件,此時用matchResult記錄一下這個符合條件的類型
else {
matchResult.recordMatchedType(type, typeMatches);
}
}
// 這裡針對@ConditionalOnBean等注解的annotation屬性的處理
for (String annotation : beans.getAnnotations()) {
List<String> annotationMatches = Arrays
.asList(getBeanNamesForAnnotation(beanFactory, annotation,
context.getClassLoader(), considerHierarchy));
annotationMatches.removeAll(beansIgnoredByType);
if (annotationMatches.isEmpty()) {
matchResult.recordUnmatchedAnnotation(annotation);
}
else {
matchResult.recordMatchedAnnotation(annotation, annotationMatches);
}
}
// 這裡針對@ConditionalOnBean等注解的name屬性的處理
for (String beanName : beans.getNames()) {
// beansIgnoredByType集合不包含beanName且beanFactory包含這個bean,則比對
if (!beansIgnoredByType.contains(beanName)
&& containsBean(beanFactory, beanName, considerHierarchy)) {
matchResult.recordMatchedName(beanName);
}
// 否則,不比對
else {
matchResult.recordUnmatchedName(beanName);
}
}
// 最後傳回比對結果
return matchResult;
}
上面的邏輯主要是從spring容器中搜尋有無指定條件的
bean
,搜尋Spring容器搜尋bean的話有三種搜尋政策,分别是
CURRENT
ANCESTORS
ALL
,分表表示隻從目前的
context
中搜尋
bean
,隻從父
context
bean
和從整個
context
bean
;定義了搜尋政策後,然後再根據
BeanSearchSpec
對象封裝的注解屬性分别取指定的容器中查找有無符合條件的
bean
,然後再進行一些過濾。比如
@ConditionalOnMissingBean
注解有定義
ignored
屬性值,那麼從容器中搜尋到有符合條件的
bean
時,此時還要移除掉
ignored
指定的
bean
好了,上面就已經分析了
OnBeanCondition
這個條件類了,我們堅持主線優先的原則,具體的細節代碼不會深究。
4.3 OnWebApplicationCondition
OnWebApplicationCondition
FilteringSpringBootCondition
FilteringSpringBootCondition
getOutcomes
FilteringSpringBootCondition
SpringBootCondition
FilteringSpringBootCondition
OnWebApplicationCondition
SpringBootCondition
getMatchOutcome
方法,用來判斷目前應用是否web應用。同時是
OnWebApplicationCondition
@ConditionalOnWebApplication
OnWebApplicationCondition
重寫
SpringBootCondition
getMatchOutcome
方法:
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
// 配置類是否标注有@ConditionalOnWebApplication注解
boolean required = metadata
.isAnnotated(ConditionalOnWebApplication.class.getName());
// 調用isWebApplication方法傳回比對結果
ConditionOutcome outcome = isWebApplication(context, metadata, required);
// 若有标注@ConditionalOnWebApplication但不符合條件,則傳回不比對
if (required && !outcome.isMatch()) {
return ConditionOutcome.noMatch(outcome.getConditionMessage());
}
// 若沒有标注@ConditionalOnWebApplication但符合條件,則傳回不比對
if (!required && outcome.isMatch()) {
return ConditionOutcome.noMatch(outcome.getConditionMessage());
}
// 這裡傳回比對的情況,TODO 不過有個疑問:如果沒有标注@ConditionalOnWebApplication注解,又不符合條件的話,也會執行到這裡,傳回比對?
return ConditionOutcome.match(outcome.getConditionMessage());
}
上面代碼的邏輯很簡單,主要是調用
isWebApplication
方法來判斷目前應用是否是web應用。是以,我們再來看下
isWebApplication
方法:
private ConditionOutcome isWebApplication(ConditionContext context,
AnnotatedTypeMetadata metadata, boolean required) {
// 調用deduceType方法判斷是哪種類型,其中有SERVLET,REACTIVE和ANY類型,其中ANY表示了SERVLET或REACTIVE類型
switch (deduceType(metadata)) {
// SERVLET類型
case SERVLET:
return isServletWebApplication(context);
// REACTIVE類型
case REACTIVE:
return isReactiveWebApplication(context);
default:
return isAnyWebApplication(context, required);
}
}
在
isWebApplication
方法中,首先從
@ConditionalOnWebApplication
注解中擷取其定義了什麼類型,然後根據不同的類型進入不同的判斷邏輯。這裡我們隻看下
SERVLET
的情況判斷處理,看代碼:
private ConditionOutcome isServletWebApplication(ConditionContext context) {
ConditionMessage.Builder message = ConditionMessage.forCondition("");
// 若classpath中不存在org.springframework.web.context.support.GenericWebApplicationContext.class,則傳回不比對
if (!ClassNameFilter.isPresent(SERVLET_WEB_APPLICATION_CLASS,
context.getClassLoader())) {
return ConditionOutcome.noMatch(
message.didNotFind("servlet web application classes").atAll());
}
// 若classpath中存在org.springframework.web.context.support.GenericWebApplicationContext.class,那麼又分為以下幾種比對的情況
// session
if (context.getBeanFactory() != null) {
String[] scopes = context.getBeanFactory().getRegisteredScopeNames();
if (ObjectUtils.containsElement(scopes, "session")) {
return ConditionOutcome.match(message.foundExactly("'session' scope"));
}
}
// ConfigurableWebEnvironment
if (context.getEnvironment() instanceof ConfigurableWebEnvironment) {
return ConditionOutcome
.match(message.foundExactly("ConfigurableWebEnvironment"));
}
// WebApplicationContext
if (context.getResourceLoader() instanceof WebApplicationContext) {
return ConditionOutcome.match(message.foundExactly("WebApplicationContext"));
}
// 若以上三種都不比對的話,則說明不是一個servlet web application
return ConditionOutcome.noMatch(message.because("not a servlet web application"));
}
對于是
SERVLET
的情況,首先根據
classpath
中是否存在
org.springframework.web.context.support.GenericWebApplicationContext.class
,如果不存在該類,則直接傳回不比對;若存在的話那麼又分為以下幾種比對的情況:
- session
- ConfigurableWebEnvironment
- WebApplicationContext
若上面三種情況都不比對,則說明不是一個servlet web application。
4.4 其他
由于springboot的
OnXXXCondition
類實作太多,不可能每個條件類都分析一遍,是以上面隻分析了
OnResourceCondition
OnBeanCondition
onWebApplicationCondition
的源碼。我們分析源碼不可能把所有代碼都通讀一遍的,閱讀源碼的話,隻要了解了某個子產品的類之間的關系及挑幾個有代表性的類分析下就行,不可能一網打盡。
若有時間的話,推薦看下幾個我們常用的條件類的源碼:
OnPropertyCondition
OnClassCondition
OnExpressionCondition
等。
5 如何擴充SpringBootCondition
前文我們知道了如何擴充Spring的
Condition
接口,那麼我們該如何擴充SpringBoot的
SpringBootCondition
類呢?
推薦閱讀
springboot之使用SpringBootCondition獲得答案
好了,本篇文章是SpringBoot自動配置源碼分析的前置文章,這裡分析了條件注解源碼,那麼下篇文章我們就來看看SpringBoot自動配置的源碼了。
下節預告:
SpringBoot新特性:SpringBoot是如何自動配置的?--SpringBoot源碼(四)
原創不易,幫忙點個贊呗!
參考:
1,
spring 自動配置(上) 配置檔案和插件解讀2,
SpringBoot内置條件注解3,
spring boot 系列之六:深入了解spring boot的自動配置