Spring原始碼研究之註解掃描
雖然在兩年前已跟隨《Spring原始碼深度解析》一書看過Spring原始碼的核心實現, 但就註解這塊的解析一直沒有時間瞭解. 導致每次碰到此類問題時心理沒有底氣. 這種感覺著實讓人不爽, 加之距離上次閱讀原始碼已過去比較長時間了, 所以也藉機再次領略下Spring裡的精妙設計, 體會OOP理念以及設計模式的實際應用。
1. 前言
我們都知道在Spring的核心配置檔案中, 通過加入以下程式碼即可實現註解配置Spring Bean.
<context:component-scan base-package="xx.yyy.zzz" />
2. 前置知識
將上面的這段標籤併入到Spring解析主流程邏輯的正是對 BeanDefinitionParser
3. ContextNamespaceHandler
通過對檢視spring-context-xx.jar中META-INF
目錄下的 spring.handlers和spring.schemas檔案就會發現自定義標籤context字首的解析工作是由ContextNamespaceHandler來負責完成的.
通過觀察ContextNamespaceHandler 中的實現邏輯, 我們可以看到下面這樣一行程式碼:
// 也就是說針對component-scan的解析工作就被全權委託給了`ComponentScanBeanDefinitionParser` 類
registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser());
4. ComponentScanBeanDefinitionParser
核心邏輯是對所實現介面 BeanDefinitionParser
定義的唯一方法 parse 的實現了.
public BeanDefinition parse(Element element, ParserContext parserContext) {
// private static final String BASE_PACKAGE_ATTRIBUTE = "base-package";
// ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS = ",; \t\n";
// 所以我們這裡在設定 base-package 的值時, 可以通過上面指示的分隔符進行多個package的指定.
String[] basePackages = StringUtils.tokenizeToStringArray(element.getAttribute(BASE_PACKAGE_ATTRIBUTE), ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
// ------------- 以下是4.3.12 的與上述功能類似的程式碼 (2017/12/8)
/*
String basePackage = element.getAttribute(BASE_PACKAGE_ATTRIBUTE);
// 注意這裡新增的程式碼使得我們的base-package屬性也可以使用Ant-style 模式的匹配符號.
basePackage = parserContext.getReaderContext().getEnvironment().resolvePlaceholders(basePackage);
String[] basePackages = StringUtils.tokenizeToStringArray(basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
*/
// Actually scan for bean definitions and register them.
// 核心掃描邏輯; 本次關注的重點
ClassPathBeanDefinitionScanner scanner = configureScanner(parserContext, element);
Set<BeanDefinitionHolder> beanDefinitions = scanner.doScan(basePackages);
// 註冊所掃描到的符合要求的Bean
// 2017/10/31 新增; 這個方法裡有些細節被忽略了, 預設情況下 annotationConfig 欄位為true, 這就會導致預設情況下會向容器中註冊針對@Configuration, @Autowired, @Value, @Inject, @Required等處理器.
// http://m.blog.csdn.net/honghailiang888/article/details/74981445
registerComponents(parserContext.getReaderContext(), beanDefinitions, element);
// 滿足介面定義的契約要求
return null;
}
5. ClassPathBeanDefinitionScanner
上面的四行程式碼中, 核心的就是第二 , 三行程式碼了. 其中第二行程式碼就是按照使用者的自定義需求構建出一個
ClassPathBeanDefinitionScanner
例項, 所以我們將關注點主要集中在第三行程式碼, 也就是scanner.doScan(basePackages);
的實現上.
首先談談這個方法doScan
的命名, 在閱讀《Spring原始碼深度解析》一書時, 作者專門談到了在Spring原始碼中, 一般真正的實現邏輯是由名為doXX的方法來完成的. 而XX只是負責進行排程處理. 這裡同樣也不例外.
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<BeanDefinitionHolder>();
for (String basePackage : basePackages) {
// 核心邏輯
Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
// 對找到的每個BeanDefinition進行屬性配置; 具體程式碼略
// xxxx
return beanDefinitions;
}
7. findCandidateComponents(basePackage)
方法
該類定義在ClassPathBeanDefinitionScanner
類的基類ClassPathScanningCandidateComponentProvider
中, 其實看看這個基類的名字我們就大概可以猜測類似過濾的操作應該就是在其內部完成的.
以下程式碼進行了一定的裁剪, 以節省篇幅.
public Set<BeanDefinition> findCandidateComponents(String basePackage) {
Set<BeanDefinition> candidates = new LinkedHashSet<BeanDefinition>();
// ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX = "classpath*:";
// 通過觀察resolveBasePackage()方法的實現, 我們可以在設定basePackage時, 使用形如${}的佔位符, Spring會在這裡進行替換
// this.resourcePattern 預設為 "**/*.class"
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
resolveBasePackage(basePackage) + "/" + this.resourcePattern;
// 使用上面拼接出的形如 "classpath*:xx/yyy/zzz/**/*.class", 將其檢索為Spring內建的Resource物件(這樣就統一化了資源的差異)
Resource[] resources = this.resourcePatternResolver.getResources(packageSearchPath);
boolean traceEnabled = logger.isTraceEnabled();
boolean debugEnabled = logger.isDebugEnabled();
for (Resource resource : resources) {
if (traceEnabled) {
logger.trace("Scanning " + resource);
}
if (resource.isReadable()) {
try {
MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(resource);
// 這個isCandidateComponent就是核心邏輯了,
// 上面將class檔案內容轉換為Resource時, 是將所有的檔案都讀取進來了
// 這顯然是不滿足我們的要求的, 我們就需要進行相應的過濾
if (isCandidateComponent(metadataReader)) {
ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
sbd.setResource(resource);
sbd.setSource(resource);
// 這裡只是判斷掃描到的這個類是否可以被例項化, 以及是否是a top-level class or a nested class (static inner class)
if (isCandidateComponent(sbd)) {
/* 列印日誌 */
candidates.add(sbd);
}
else {/* 列印日誌 */ }
}
else {/* 列印日誌 */ }
}
catch (Throwable ex) {
}
}
else {/* 列印日誌 */ }
}
return candidates;
}
8. isCandidateComponent()方法
protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
// this.excludeFilters除非使用者顯式配置, 否則預設為空
for (TypeFilter tf : this.excludeFilters) {
if (tf.match(metadataReader, this.metadataReaderFactory)) {
return false;
}
}
// 這裡就需要注意一下了
// 我在前面沒有進行講解的configureScanner方法裡有這麼一個細節,
// <context:component-scan/> 的"use-default-filters"的屬性值預設是true . 這一點可以在configureScanner方法中進行驗證
// 於是我們追蹤對useDefaultFilters欄位的呼叫來到ClassPathBeanDefinitionScanner的基類ClassPathScanningCandidateComponentProvider中就會發現
// useDefaultFilters欄位為true時, 會預設註冊如下幾個AnnotationTypeFilter到includeFilters欄位中:
// 1. new AnnotationTypeFilter(Component.class)
// 2. new AnnotationTypeFilter(((Class<? extends Annotation>) ClassUtils.forName("javax.annotation.ManagedBean", cl)), false)
// 3. new AnnotationTypeFilter(((Class<? extends Annotation>) ClassUtils.forName("javax.inject.Named", cl)), false)
// 多說一句:
// 與Component註解位於同一個package下的Repository, Controller, Service都被@Component註解所修飾
// 再多說一句(2017/10/31):
// @Configuration 也被@Component註解所修飾
for (TypeFilter tf : this.includeFilters) {
if (tf.match(metadataReader, this.metadataReaderFactory)) {
AnnotationMetadata metadata = metadataReader.getAnnotationMetadata();
if (!metadata.isAnnotated(Profile.class.getName())) {
return true;
}
AnnotationAttributes profile = MetadataUtils.attributesFor(metadata, Profile.class);
return this.environment.acceptsProfiles(profile.getStringArray("value"));
}
}
// 注意這裡預設返回是false, 也就是說要是通不過includeFilters的條件, 該Bean就不滿足要求, 不會進入Spring容器
return false;
}
上面的程式碼裡的解釋應該足夠詳細了, 再多說一句就是 我們的註解方式註冊Bean到Spring容器是通過擴充套件方式(excludeFilters和includeFilters)來完成的, 而非寫死在主邏輯裡面. 這一點非常值得借鑑!
9. 細粒度控制
這裡只放本人使用到了的; 更多的參見開濤的部落格.
使用 <context:include-filter />
, <context:exclude-filter />
filter機制在Spring3有五種type
<context:component-scan base-package="com.kq">
<!-- 排除base-package下的某個package-->
<context:exclude-filter type="regex" expression="com\.kq\.common\.singleuser.*"/>
</context:component-scan>
10. 擴充套件閱讀
本文發表後, 被CSDN推薦瞭如下連結, 遂一併合併進來.
11. 總結
- Spring原始碼讀起來很流暢, 報紙一樣的排版讓人閱讀起來有種賞心悅目的感覺. 本人曾在三年前有幸拜讀了Bob大叔的《Clean Code》, Spring原始碼的大部分都是滿足其中的規則.
- 以上一段邏輯追溯下來, 你就會發現Spring原始碼裡深刻貫徹了”多用組合,少用繼承”的思想, 其內部的很多功能都是委託了其他單獨的元件. 例如
ClassPathBeanDefinitionScanner
類中的beanNameGenerator欄位(負責生成唯一性的Bean Name)和scopeMetadataResolver欄位(負責檢索)