曹工說Spring Boot原始碼(27)-- Spring的component-scan,光是include-filter屬性的各種配置方式,就夠玩半天了.md
阿新 • • 發佈:2020-04-01
# 寫在前面的話
相關背景及資源:
[曹工說Spring Boot原始碼(1)-- Bean Definition到底是什麼,附spring思維導圖分享](https://www.cnblogs.com/grey-wolf/p/12044199.html)
[曹工說Spring Boot原始碼(2)-- Bean Definition到底是什麼,咱們對著介面,逐個方法講解](https://www.cnblogs.com/grey-wolf/p/12051957.html )
[曹工說Spring Boot原始碼(3)-- 手動註冊Bean Definition不比遊戲好玩嗎,我們來試一下](https://www.cnblogs.com/grey-wolf/p/12070377.html)
[曹工說Spring Boot原始碼(4)-- 我是怎麼自定義ApplicationContext,從json檔案讀取bean definition的?](https://www.cnblogs.com/grey-wolf/p/12078673.html)
[曹工說Spring Boot原始碼(5)-- 怎麼從properties檔案讀取bean](https://www.cnblogs.com/grey-wolf/p/12093929.html)
[曹工說Spring Boot原始碼(6)-- Spring怎麼從xml檔案裡解析bean的](https://www.cnblogs.com/grey-wolf/p/12114604.html )
[曹工說Spring Boot原始碼(7)-- Spring解析xml檔案,到底從中得到了什麼(上)](https://www.cnblogs.com/grey-wolf/p/12151809.html)
[曹工說Spring Boot原始碼(8)-- Spring解析xml檔案,到底從中得到了什麼(util名稱空間)](https://www.cnblogs.com/grey-wolf/p/12158935.html)
[曹工說Spring Boot原始碼(9)-- Spring解析xml檔案,到底從中得到了什麼(context名稱空間上)](https://www.cnblogs.com/grey-wolf/p/12189842.html)
[曹工說Spring Boot原始碼(10)-- Spring解析xml檔案,到底從中得到了什麼(context:annotation-config 解析)](https://www.cnblogs.com/grey-wolf/p/12199334.html)
[曹工說Spring Boot原始碼(11)-- context:component-scan,你真的會用嗎(這次來說說它的奇技淫巧)](https://www.cnblogs.com/grey-wolf/p/12203743.html)
[曹工說Spring Boot原始碼(12)-- Spring解析xml檔案,到底從中得到了什麼(context:component-scan完整解析)](https://www.cnblogs.com/grey-wolf/p/12214408.html)
[曹工說Spring Boot原始碼(13)-- AspectJ的執行時織入(Load-Time-Weaving),基本內容是講清楚了(附原始碼)](https://www.cnblogs.com/grey-wolf/p/12228958.html)
[曹工說Spring Boot原始碼(14)-- AspectJ的Load-Time-Weaving的兩種實現方式細細講解,以及怎麼和Spring Instrumentation整合](https://www.cnblogs.com/grey-wolf/p/12283544.html)
[曹工說Spring Boot原始碼(15)-- Spring從xml檔案裡到底得到了什麼(context:load-time-weaver 完整解析)](https://www.cnblogs.com/grey-wolf/p/12288391.html)
[曹工說Spring Boot原始碼(16)-- Spring從xml檔案裡到底得到了什麼(aop:config完整解析【上】)](https://www.cnblogs.com/grey-wolf/p/12314954.html)
[曹工說Spring Boot原始碼(17)-- Spring從xml檔案裡到底得到了什麼(aop:config完整解析【中】)](https://www.cnblogs.com/grey-wolf/p/12317612.html)
[曹工說Spring Boot原始碼(18)-- Spring AOP原始碼分析三部曲,終於快講完了 (aop:config完整解析【下】)](https://www.cnblogs.com/grey-wolf/p/12322587.html)
[曹工說Spring Boot原始碼(19)-- Spring 帶給我們的工具利器,建立代理不用愁(ProxyFactory)](https://www.cnblogs.com/grey-wolf/p/12359963.html)
[曹工說Spring Boot原始碼(20)-- 碼網恢恢,疏而不漏,如何記錄Spring RedisTemplate每次操作日誌](https://www.cnblogs.com/grey-wolf/p/12375656.html)
[曹工說Spring Boot原始碼(21)-- 為了讓大家理解Spring Aop利器ProxyFactory,我已經拼了](https://www.cnblogs.com/grey-wolf/p/12384356.html)
[曹工說Spring Boot原始碼(22)-- 你說我Spring Aop依賴AspectJ,我依賴它什麼了](https://www.cnblogs.com/grey-wolf/p/12418425.html)
[曹工說Spring Boot原始碼(23)-- ASM又立功了,Spring原來是這麼遞迴獲取註解的元註解的](https://www.cnblogs.com/grey-wolf/p/12535152.html)
[曹工說Spring Boot原始碼(24)-- Spring註解掃描的瑞士軍刀,asm技術實戰(上)](https://www.cnblogs.com/grey-wolf/p/12571217.html)
[曹工說Spring Boot原始碼(25)-- Spring註解掃描的瑞士軍刀,ASM + Java Instrumentation,順便提提Jar包破解](https://www.cnblogs.com/grey-wolf/p/12584861.html)
[曹工說Spring Boot原始碼(26)-- 學習位元組碼也太難了,實在不能忍受了,寫了個小小的位元組碼執行引擎](https://www.cnblogs.com/grey-wolf/p/12600097.html)
[工程程式碼地址](https://gitee.com/ckl111/spring-boot-first-version-learn ) [思維導圖地址](https://www.processon.com/view/link/5deeefdee4b0e2c298aa5596)
工程結構圖:
![](https://img2018.cnblogs.com/blog/519126/201912/519126-20191215144930717-1919774390.png)
# 概要
前面三講,主要涉及了ASM的一些內容,為什麼要講ASM,主要是因為spring在進入到註解時代後,掃描註解也變成了一項必備技能,現在一個大系統,業務類就動不動大幾百個,掃描註解也是比較耗時的,所以催生了利用ASM來快速掃描類上註解的需求。
但是,掃描了那麼多類,比如,component-scan掃描了100個類,怎麼知道哪些要納入spring管理,變成bean呢?
這個問題很簡單,對吧?component註解、controller、service、repository、configuration註解了的類,就會掃描為bean。
那,假如現在面試官問你,不使用這幾個註解,讓你自定義一個註解,比如@MyComponent,你要怎麼才能把@MyComponent註解的類,掃描成bean呢?
# 核心原理
因為xml版本的component-scan,和註解版本的@Component-scan,內部複用了同樣的程式碼,所以我這裡還是以xml版本來講。
xml版本的,一般如下配置:
```xml
```
該元素的處理器為:
`org.springframework.context.annotation.ComponentScanBeanDefinitionParser`.
該類實現了`org.springframework.beans.factory.xml.BeanDefinitionParser`介面,該介面只有一個方法:
```java
BeanDefinition parse(Element element, ParserContext parserContext);
```
方法核心,就是傳入要解析的xml元素,和上下文資訊,然後你憑藉這些資訊,去解析bean definition出來。
假設交給我們來寫,大概如下思路:
1. 獲取component-scan的base-package屬性
2. 獲取第一步的結果下的全部class,獲取class上的註解資訊,儲存起來
3. 依次判斷class上,是否註解了controller、service、configuration等註解,如果是,則算是合格的bean definition。
spring的實現也差不多,但是複雜的多,核心倒是差不多。比如,spring中:
獲取component-scan的base-package屬性,可能是個list,所以要遍歷;其中,迴圈內部,呼叫了ClassPathScanningCandidateComponentProvider#findCandidateComponents。
```java
for (String basePackage : basePackages) {
/**
* 掃描候選的component,注意,這裡的名稱叫CandidateComponent,所以這裡真的就只掃描了 * @component或者基於它的那幾個。(service、controller那些)
* 這裡是沒包含下面這些:
* 1、propertysource註解的
*/
Set candidates = findCandidateComponents(basePackage);
```
如下所示,在獲取某個包下面的滿足條件的bean時,程式碼如下:
```java
public Set findCandidateComponents(String basePackage) {
Set candidates = new LinkedHashSet();
try {
// 1
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
resolveBasePackage(basePackage) + "/" + this.resourcePattern;
Resource[] resources = this.resourcePatternResolver.getResources(packageSearchPath);
// 2
for (Resource resource : resources) {
if (resource.isReadable()) {
try {
// 3
MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(resource);
// 4
if (isCandidateComponent(metadataReader)) {
...
```
我們逐個講解每個程式碼點:
* 1處,獲取包下面的全部resource,型別為Resource
* 2處,遍歷Resource陣列
* 3處,獲取資源的metadataReader,這個metadataReader,可以用來獲取資源(一般為class檔案)上的註解
* 4處,呼叫方法isCandidateComponent,判斷是否為候選的bean
接下來,我們看看 isCandidateComponent 怎麼實現的:
```java
protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
// 1
for (TypeFilter tf : this.excludeFilters) {
if (tf.match(metadataReader, this.metadataReaderFactory)) {
return false;
}
}
for (TypeFilter tf : this.includeFilters) {
// 2
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"));
}
}
return false;
}
```
* 1處,遍歷excludeFilters,如果引數中的class,匹配excludeFilter,則返回false,表示不合格;
* 2處,遍歷includeFilters,如果引數中的class,匹配includeFilter,則基本可以斷定合格了,但是因為@profile註解的存在,又加了一層判斷,如果class上不存在profile,則返回true,合格;
否則,判斷profile是否和當前激活了的profile匹配,如果匹配,則返回true,否則flase。
敲黑板,這裡的excludeFilters和includeFilters,其實就是@component-scan中的如下屬性:
```java
public @interface ComponentScan {
...
/**
* Indicates whether automatic detection of classes annotated with {@code @Component}
* {@code @Repository}, {@code @Service}, or {@code @Controller} should be enabled.
*/
boolean useDefaultFilters() default true;
/**
* Specifies which types are eligible for component scanning.
*
```
被掃描的類路徑下,一個測試類,註解了Controller:
```java
@Controller
public class TestController {
}
```
然後我們執行測試程式碼:
```java
public static void testDefaultFilter() {
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("classpath:component-scan-default-filter.xml");
TestController bean = context.getBean(TestController.class);
System.out.println(bean);
}
```
在如下地方,debug斷點可以看到:
![](https://img2020.cnblogs.com/blog/519126/202003/519126-20200331220831131-1745165470.png)
如上的includeFilters,大家看到了,包含了一個TypeFilter,型別為`org.springframework.core.type.filter.AnnotationTypeFilter`,其類繼承結構為:
![](https://img2020.cnblogs.com/blog/519126/202003/519126-20200331221020440-903599829.png)
這個TypeFilter,就一個方法:
```java
public interface TypeFilter {
/**
* Determine whether this filter matches for the class described by
* the given metadata.
* @param metadataReader the metadata reader for the target class
* @param metadataReaderFactory a factory for obtaining metadata readers
* for other classes (such as superclasses and interfaces)
* @return whether this filter matches
* @throws IOException in case of I/O failure when reading metadata
*/
boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory)
throws IOException;
}
```
方法很好理解,引數是:當前的被掃描到的那個類的元資料reader,通過這個reader,可以取到class檔案中的各種資訊,底層就是通過ASM方式來實現;第二個引數,可以先跳過。
返回值呢,就是:這個filter是否匹配,我們前面的includeFilters和excludeFilters陣列,其元素型別都是這個,所以,這個typeFilter是隻管匹配與否,不分是非,不管對錯。
我們這裡這個org.springframework.core.type.filter.AnnotationTypeFilter,就是根據註解來匹配,比如,我們前面這裡的filter,就要求是@Componnet註解標註了的類才可以。
但是,我們的TestController,沒有標註Component註解,只標註了Controller註解。對,是這樣,但是因為Controller是被@Component標註了的,所以,你標註Controller,就相當於同時標註了下面這一坨:
```java
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
```
同時,由於我們的AnnotationTypeFilter,在匹配演算法上,做的比較漂亮,不止檢測直接標註在類上的註解,如Controller,還會去檢測:Controller上的註解(俗稱:元註解,即,註解的註解)。這塊實現邏輯在:
```java
org.springframework.core.type.filter.AnnotationTypeFilter#matchSelf
@Override
protected boolean matchSelf(MetadataReader metadataReader) {
AnnotationMetadata metadata = metadataReader.getAnnotationMetadata();
return metadata.hasAnnotation(this.annotationType.getName()) ||
(this.considerMetaAnnotations && metadata.hasMetaAnnotation(this.annotationType.getName()));
}
```
這裡的considerMetaAnnotations,預設為true,此時,就會去檢測@Controller上的元註解,發現標註了@Component,所以,這裡的檢測就為true。
所以,標註了Controller的類,就被掃描為Bean了。
##includeFilters,什麼時候添加了這麼一個AnnotationTypeFilter
在xml場景下,是在如下位置:
```java
org.springframework.context.annotation.ComponentScanBeanDefinitionParser#parse
public BeanDefinition parse(Element element, ParserContext parserContext) {
String[] basePackages = StringUtils.tokenizeToStringArray(element.getAttribute(BASE_PACKAGE_ATTRIBUTE),
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
// Actually scan for bean definitions and register them.
// 1
ClassPathBeanDefinitionScanner scanner = configureScanner(parserContext, element);
Set beanDefinitions = scanner.doScan(basePackages);
registerComponents(parserContext.getReaderContext(), beanDefinitions, element);
return null;
}
```
上述程式碼,就是負責解析`component-scan`這個標籤時,被呼叫的;程式碼1處,configureScanner程式碼如下:
```java
protected ClassPathBeanDefinitionScanner configureScanner(ParserContext parserContext, Element element) {
XmlReaderContext readerContext = parserContext.getReaderContext();
boolean useDefaultFilters = true;
if (element.hasAttribute(USE_DEFAULT_FILTERS_ATTRIBUTE)) {
useDefaultFilters = Boolean.valueOf(element.getAttribute(USE_DEFAULT_FILTERS_ATTRIBUTE));
}
// 1.
ClassPathBeanDefinitionScanner scanner = createScanner(readerContext, useDefaultFilters);
...
}
```
如上,程式碼1處,createScanner時,傳入useDefaultFilters,這是個boolean值,預設為true,來自於component-scan的如下屬性,即use-default-filters:
```xml
```
跟蹤進去後,最終會呼叫如下位置的程式碼:
```java
protected void registerDefaultFilters() {
/**
* 預設掃描Component註解
*/
this.includeFilters.add(new AnnotationTypeFilter(Component.class));
...
}
```
ok,一切就水落石出了。
# 自定義typeFilter--掃描指定註解
說了那麼多,我們完全可以禁用掉預設的typeFilter,配置自己想要的typeFilter,比如,我想要定義如下註解:
```java
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyComponent {
}
```
標註了這個註解的,我們就要把它掃描為bean,那麼可以如下配置:
```xml
```
注意,禁用掉預設的filter,避免干擾,可以看到,如下我們的測試類,是隻註解了@MyComponent的:
```java
@MyComponent
public class Teacher {
}
```
測試程式碼:
```java
public static void testAnnotationFilter() {
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("classpath:component-scan-annotation-filter.xml");
Teacher bean = context.getBean(Teacher.class);
System.out.println(bean);
}
```
輸出如下:
> 22:34:01.574 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'teacher'
> org.springframework.test.annotation.Teacher@2bd7cf67
# 自定義typeFilter--掃描指定註解
事實上,component-scan允許我們定義多種型別的typeFilter,如AspectJ:
```xml
```
只要滿足這個路徑的,都會被掃描為bean。
測試路徑下,有如下類:
```java
package org.springframework.test.assignable;
public interface TestInterface {
}
public class TestInterfaceImpl implements TestInterface {
}
```
測試程式碼:
```java
static void testAspectj() {
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext(
"classpath:component-scan-aspectj-filter.xml");
TestInterface bean = context.getBean(TestInterface.class);
System.out.println(bean);
}
```
輸出如下:
> 22:37:22.347 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'testInterfaceImpl'
> org.springframework.test.assignable.TestInterfaceImpl@3dea2f07
這個背後使用的typefilter,型別為:
org.springframework.core.type.filter.AspectJTypeFilter。
```java
public class AspectJTypeFilter implements TypeFilter {
private final World world;
private final TypePattern typePattern;
public AspectJTypeFilter(String typePatternExpression, ClassLoader classLoader) {
this.world = new BcelWorld(classLoader, IMessageHandler.THROW, null);
this.world.setBehaveInJava5Way(true);
PatternParser patternParser = new PatternParser(typePatternExpression);
TypePattern typePattern = patternParser.parseTypePattern();
typePattern.resolve(this.world);
IScope scope = new SimpleScope(this.world, new FormalBinding[0]);
this.typePattern = typePattern.resolveBindings(scope, Bindings.NONE, false, false);
}
// 1
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory)
throws IOException {
String className = metadataReader.getClassMetadata().getClassName();
ResolvedType resolvedType = this.world.resolve(className);
return this.typePattern.matchesStatically(resolvedType);
}
}
```
程式碼1處,即:使用aspectj的方式,來判斷是否候選的class是否匹配。
# 自定義typeFilter--指定型別的子類或實現類被掃描為bean
我們也可以這樣配置:
```xml
```
這裡的型別是assignable,只要是`TestInterface`的子類,即可以被掃描為bean。
其實現:
```java
public class AssignableTypeFilter extends AbstractTypeHierarchyTraversingFilter {
private final Class targetType;
/**
* Create a new AssignableTypeFilter for the given type.
* @param targetType the type to match
*/
public AssignableTypeFilter(Class targetType) {
super(true, true);
this.targetType = targetType;
}
@Override
protected boolean matchClassName(String className) {
return this.targetType.getName().equals(className);
}
@Override
protected Boolean matchSuperClass(String superClassName) {
return matchTargetType(superClassName);
}
@Override
protected Boolean matchInterface(String interfaceName) {
return matchTargetType(interfaceName);
}
protected Boolean matchTargetType(String typeName) {
if (this.targetType.getName().equals(typeName)) {
return true;
}
else if (Object.class.getName().equals(typeName)) {
return Boolean.FALSE;
}
else if (typeName.startsWith("java.")) {
try {
Class clazz = getClass().getClassLoader().loadClass(typeName);
return Boolean.valueOf(this.targetType.isAssignableFrom(clazz));
}
catch (ClassNotFoundException ex) {
// Class not found - can't determine a match that way.
}
}
return null;
}
}
```
總體來說,邏輯不復雜,反正就是:只要是我們指定的型別的子類或者介面實現,就ok。
# 自定義typeFilter--實現自己的typeFilter
我這裡實現了一個typeFilter,如下:
```java
/**
* 自定義的型別匹配器,如果註解了我們的DubboExportService,就匹配;否則不匹配
*/
public class CustomTypeFilterByName implements TypeFilter {
@Override
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
boolean b = metadataReader.getAnnotationMetadata().hasAnnotation(DubboExportService.class.getName());
if (b) {
return true;
}
return false;
}
}
```
判斷很簡單,註解了DubboExportService就行。
看看怎麼配置:
```xml
```
# 總結
好了,說了那麼多,大家都理解沒有呢,如果沒有,建議把程式碼拉下來一起跟著學。
其實dubbo貌似就是通過如上的自定義typeFilter來實現的,回頭我找找相關原始碼,佐證一下,補上。
demo的原始碼在:
Further narrows the set of candidate components from everything in
* {@link #basePackages()} to everything in the base packages that matches
* the given filter or filters.
* @see #resourcePattern()
*/
Filter[] includeFilters() default {};
/**
* Specifies which types are not eligible for component scanning.
* @see #resourcePattern()
*/
Filter[] excludeFilters() default {};
...
}
```
# spring 為什麼認識@Component註解的類
大家看了前面的程式碼,大概知道了,判斷一個類,是否足夠榮幸,被掃描為一個bean,是依賴於兩個屬性,一個includeFilters,一個excludeFilters。
但是,我們好像並不能知道:為什麼@Component註解的類、@controller、@service註解的類,就能成為一個bean呢?
我們先直接做個黑盒實驗,按照如下配置:
```xml