曹工說Spring Boot源碼(28)– Spring的component-scan機制,讓你自己來進行簡單實現,怎麼辦
- 2020 年 4 月 4 日
- 筆記
寫在前面的話
相關背景及資源:
曹工說Spring Boot源碼(1)– Bean Definition到底是什麼,附spring思維導圖分享
曹工說Spring Boot源碼(2)– Bean Definition到底是什麼,咱們對着接口,逐個方法講解
曹工說Spring Boot源碼(3)– 手動註冊Bean Definition不比遊戲好玩嗎,我們來試一下
曹工說Spring Boot源碼(4)– 我是怎麼自定義ApplicationContext,從json文件讀取bean definition的?
曹工說Spring Boot源碼(5)– 怎麼從properties文件讀取bean
曹工說Spring Boot源碼(6)– Spring怎麼從xml文件里解析bean的
曹工說Spring Boot源碼(7)– Spring解析xml文件,到底從中得到了什麼(上)
曹工說Spring Boot源碼(8)– Spring解析xml文件,到底從中得到了什麼(util命名空間)
曹工說Spring Boot源碼(9)– Spring解析xml文件,到底從中得到了什麼(context命名空間上)
曹工說Spring Boot源碼(10)– Spring解析xml文件,到底從中得到了什麼(context:annotation-config 解析)
曹工說Spring Boot源碼(11)– context:component-scan,你真的會用嗎(這次來說說它的奇技淫巧)
曹工說Spring Boot源碼(12)– Spring解析xml文件,到底從中得到了什麼(context:component-scan完整解析)
曹工說Spring Boot源碼(13)– AspectJ的運行時織入(Load-Time-Weaving),基本內容是講清楚了(附源碼)
曹工說Spring Boot源碼(14)– AspectJ的Load-Time-Weaving的兩種實現方式細細講解,以及怎麼和Spring Instrumentation集成
曹工說Spring Boot源碼(15)– Spring從xml文件里到底得到了什麼(context:load-time-weaver 完整解析)
曹工說Spring Boot源碼(16)– Spring從xml文件里到底得到了什麼(aop:config完整解析【上】)
曹工說Spring Boot源碼(17)– Spring從xml文件里到底得到了什麼(aop:config完整解析【中】)
曹工說Spring Boot源碼(18)– Spring AOP源碼分析三部曲,終於快講完了 (aop:config完整解析【下】)
曹工說Spring Boot源碼(19)– Spring 帶給我們的工具利器,創建代理不用愁(ProxyFactory)
曹工說Spring Boot源碼(20)– 碼網恢恢,疏而不漏,如何記錄Spring RedisTemplate每次操作日誌
曹工說Spring Boot源碼(21)– 為了讓大家理解Spring Aop利器ProxyFactory,我已經拼了
曹工說Spring Boot源碼(22)– 你說我Spring Aop依賴AspectJ,我依賴它什麼了
曹工說Spring Boot源碼(23)– ASM又立功了,Spring原來是這麼遞歸獲取註解的元註解的
曹工說Spring Boot源碼(24)– Spring註解掃描的瑞士軍刀,asm技術實戰(上)
曹工說Spring Boot源碼(25)– Spring註解掃描的瑞士軍刀,ASM + Java Instrumentation,順便提提Jar包破解
曹工說Spring Boot源碼(26)– 學習位元組碼也太難了,實在不能忍受了,寫了個小小的位元組碼執行引擎
曹工說Spring Boot源碼(27)– Spring的component-scan,光是include-filter屬性的各種配置方式,就夠玩半天了
工程結構圖:
概要
本講相對獨立,我們也不愛說廢話,直接說本講要做啥。
大家知道,@Component-scan註解,在註解時代,最主要的用法就是指定一個package的名稱,然後spring就會去對應的包下面,掃描註解了@Controller、@Service、@Repository等註解的類,然後註冊為bean。
spring boot時代,這個註解則站到了幕後,如下:
@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 { }
總之,這個註解大家再熟悉不過了。我們本講,最核心的目標是,讓大家更懂spring,方法呢,就是讓我們自己來實現以下目標:
-
定義main類
@MyConfiguration @MyComponentScan(value = "org.springframework.test") public class BootStrap { public static void main(String[] args) { ... }
這個上面定義了2個自定義的註解,都加了個前綴"My"。下面簡單看看。
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface MyConfiguration { }
這個的效果,類似於@configuration,表示我們是一個配置類,配置類上一般會有一堆其他註解,來引入其他bean definition。
然後是MyComponentScan 註解:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface MyComponentScan { /* * 要掃描的包名 */ String value(); }
-
我們的target包下,有一個需要我們掃描的bean,如下:
package org.springframework.test; @MyComponent @MyComponentScan(value = "org.springframework.test1") public class PersonService { private String personname1; }
這裡使用MyComponent註解,這個註解,用來註解我們要掃描為bean的那些類。比如這裡,我們希望PersonService這個類,被掃描為bean。
其次,我們還定義了一個
@MyComponentScan(value = "org.springframework.test1")
,這個主要是:我們要能夠支持遞歸處理。當然,現在可以先忽略,權且當它不存在。
-
最終的測試效果如下:
import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.custom.MyConfigurationClassPostProcessor; import org.springframework.custom.annotation.MyComponentScan; import org.springframework.custom.annotation.MyConfiguration; import org.springframework.test1.AnotherPersonService; @MyConfiguration @MyComponentScan(value = "org.springframework.test") public class BootStrap { public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); AnnotatedGenericBeanDefinition beanDefinition = new AnnotatedGenericBeanDefinition(BootStrap.class); context.registerBeanDefinition(BootStrap.class.getName(), beanDefinition); /** * 註冊一個beanFactoryPostProcessor,用來處理MyComponentScan註解 */ RootBeanDefinition def = new RootBeanDefinition(MyConfigurationClassPostProcessor.class); def.setSource(null); context.registerBeanDefinition(MyConfigurationClassPostProcessor.class.getName(), def); context.refresh(); PersonService bean = context.getBean(PersonService.class); System.out.println(bean); } }
輸出如下:
12:39:27.259 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory – Returning cached instance of singleton bean ‘personService’
org.springframework.test.PersonService@71ba5790
可以看到,我們的目標就是以上這樣。為了實現這個目標,其實我們是仿照spring的實現,自己ctrl c/v了一把,其中進行了大量的簡化。
實現思路
思路基本等同於spring的實現,因為本系列教程的目的就是讓大家更懂Spring,所以沒必要另闢蹊徑。
用@MyConfiguration註解一個起點配置類
指定一個用 @MyConfiguration 註解的類,這個類一開始就會被註冊為bean definition,類似於spring boot中的啟動類,大家知道,spring boot中,啟動類也是間接註解了 @SpringBootConfiguration,而 @SpringBootConfiguration呢,大家看看:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Configuration public @interface SpringBootConfiguration { }
該註解,是註解了@configuration。
其實就是手動指定一個配置類,這個配置類,源碼里好像是叫做startUp config,作為一個起點,通過這個起點,我們能發現在這個起點類上的,更多的元註解,比如,在我們公司的真實項目中,啟動類就配置了一堆東西:
@SpringBootApplication @EnableTransactionManagement @EnableAspectJAutoProxy(exposeProxy = true) @MapperScan("com.xxx.cad.mapper") @ComponentScan("com.xxx") @EnableFeignClients //@Slf4j @Controller @EnableScheduling public class CadWebService {
這個類,就是我們這裡的起點類。這個起點類,然後被註冊為一個bean。
註冊一個BeanDefinitionRegistryPostProcessor,用來處理配置類上的@MyComponentScan,以發現更多bean
大家知道@configuration配置類,是怎麼被處理的呢?就是通過org.springframework.context.annotation.ConfigurationClassPostProcessor
。
這個類呢,實現了如下接口:
public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor { /** * Modify the application context's internal bean definition registry after its * standard initialization. All regular bean definitions will have been loaded, * but no beans will have been instantiated yet. This allows for adding further * bean definitions before the next post-processing phase kicks in. * @param registry the bean definition registry used by the application context * @throws org.springframework.beans.BeansException in case of errors */ void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException; }
這個接口的這個方法的調用時機,是在:截止目前,通過配置(不論是xml,還是像上面第一步這樣,去手動註冊bean definition)的方式,能夠找到的bean definition都已經找到了;本來,下一步就是找出其中的要eager-init的單例bean,去初始化了。
但是呢,在這之前,我們還有一個步驟,也算是一個擴展點吧,可以讓我們去修改目前的bean definition集合。
如果舉個通俗的例子,大概是這樣的,比如,一個公司,組織大家出去玩,大家自願報名,一開始假設報了10個人,預定周六出發;在此之前呢,公司再讓大家確認一下,大家可以
- 增,帶家屬,帶男女朋友;
- 刪,自己不去了
- 改,我周六好像還要寫bug,但我另一個同事沒啥事,他可以去
而要實現這些,只要你實現前面我們提到的那個接口的方法即可,大家可以再觀察下這個方法:
void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException;
入參是registry,也就是當前的報名表,報名表都給你了,你還有啥不能幹的?
public interface BeanDefinitionRegistry extends AliasRegistry { void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) throws BeanDefinitionStoreException; void removeBeanDefinition(String beanName) throws NoSuchBeanDefinitionException; BeanDefinition getBeanDefinition(String beanName) throws NoSuchBeanDefinitionException; ... }
就拿上面那幾個方法舉例:
- registerBeanDefinition,這個就是往名單上加人
- removeBeanDefinition,這個就是自己不去了
- BeanDefinition getBeanDefinition(String beanName),這個可以用自己名字查到報名信息,改個名字,沒問題吧?
@Configuration註解的處理,就是依賴於一個實現了BeanDefinitionRegistry
接口的類,在這個類里,它幹了很多事:
假如現在的名單里,只有我們的啟動類,對吧,上面標了一坨註解,什麼各種Enable,什麼Component-scan啦,都有。但我們標註這些,比如component-scan,不是擺着玩的,是要去對應的package下,幫我們掃描bean的;這就是相當於說,要往名單上加人。
大致的邏輯可以理解為:
-
拿到初始名單,這裡就是啟動類的bean definition,以及一些其他手動弄進去的bean definition
-
通過實現了
BeanDefinitionRegistry
的ConfigurationClassPostProcessor
,來看看第一步的初始名單中,有沒有註解@Component-scan,如果沒註解,直接返回;如果註解了,進入第三步; -
拿到@component-scan里配置的要掃描的package名,然後獲取這個package下的全部class,然後看看這些class滿不滿足條件(比如,只認註解了controller、service等註解的)
-
第三步篩出來的,滿足條件的class,它們本身合格了,可以作為bean了;然後看看它們有沒有作為配置類的資格,我拿下面的舉例:
@Component @ComponentScan(value = "xxxx.xxxx") public class PersonService { private String personname1; }
本身,上面這個例子中,PersonService因為component-scan的功勞,已經被收為bean了,但是,這不是結束,因為它自己上面還註解了@ComponentScan註解,而這,就需要去遞歸處理。
有些同學會覺得有點極端,maybe,但是,下面的例子極端嗎:
@Component @Import({MainClassForTestAnnotationConfig.class}) public class PersonService { private String personname1;
如果覺得@Import極端,那麼@ImportResource去導入xml文件里的bean,這個場景,有些時候還是會遇到吧,比如,要兼容老程序的時候。
而我要說的就是,在
ConfigurationClassPostProcessor
處理@configuration註解的過程中,如果發現這個類上有以下行為,都會遞歸處理:- 內部類先解析
PropertySource
註解ComponentScan
註解Import
註解ImportResource
註解- 有
Bean
註解的方法 - 處理superClass
總體來說,這個類還是比較難的,而且會遞歸處理。
我們今天的demo,為了聚焦,也為了實現簡單,先只處理了@component-scan本身的遞歸。
接下來,就來看看具體實現。
具體實現
測試類,主邏輯,驅動整體流程
@MyConfiguration @MyComponentScan(value = "org.springframework.test") public class BootStrap { public static void main(String[] args) { // 1 AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); AnnotatedGenericBeanDefinition beanDefinition = new AnnotatedGenericBeanDefinition(BootStrap.class); context.registerBeanDefinition(BootStrap.class.getName(), beanDefinition); /** * 2 註冊一個beanFactoryPostProcessor */ RootBeanDefinition def = new RootBeanDefinition(MyConfigurationClassPostProcessor.class); def.setSource(null); context.registerBeanDefinition(MyConfigurationClassPostProcessor.class.getName(), def); // 3 context.refresh(); // 4 PersonService bean = context.getBean(PersonService.class); System.out.println(bean); AnotherPersonService anotherPersonService = context.getBean(AnotherPersonService.class); System.out.println(anotherPersonService); } }
- 1處,使用spring默認的註解驅動上下文,設置:config的起點類為當前類,註冊到spring容器
- 2處,註冊一個 MyConfigurationClassPostProcessor 到spring 容器,這個和前面講的ConfigurationClassPostProcessor 效果類似,用於解析我們自己的@MyComponentScan
- 3處,加載上下文
- 4處,獲取bean,檢測效果。
MyConfigurationClassPostProcessor,解析@MyComponentScan
@Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { log.info("postProcessBeanDefinitionRegistry..."); /** * 1: 找到標註了{@link org.springframework.custom.annotation.MyConfiguration}註解的類 * 這些類就是我們的配置類 * 我們通過這些類,可以發現更多的bean definition */ Set<BeanDefinitionHolder> beanDefinitionHolders = new LinkedHashSet<BeanDefinitionHolder>(); for (String beanName : registry.getBeanDefinitionNames()) { BeanDefinition beanDef = registry.getBeanDefinition(beanName); if (MyConfigurationUtils.checkConfigurationClassCandidate(beanDef)) { beanDefinitionHolders.add(new BeanDefinitionHolder(beanDef, beanName)); } } // 2 if (CollectionUtils.isEmpty(beanDefinitionHolders)) { return; } // 3 MyConfigurationClassParser parser = new MyConfigurationClassParser(environment,registry); parser.parse(beanDefinitionHolders); }
- 1處,找到目前的,標註了 MyConfiguration註解的全部bean definition
- 2處,如果不存在,返回
- 3處,對第一步找到的集合,進行下一步處理
具體解析的工作,落在了第三步的身上,具體說,是MyConfigurationClassParser類的身上。
MyConfigurationClassParser具體執行者
public void parse(Set<BeanDefinitionHolder> configCandidates) { for (BeanDefinitionHolder holder : configCandidates) { BeanDefinition bd = holder.getBeanDefinition(); String className = bd.getBeanClassName(); try { processConfigurationClass(className); } catch (IOException ex) { throw new BeanDefinitionStoreException("Failed to load bean class: " + bd.getBeanClassName(), ex); } } }
這裡就是對每個找到的配置類進行處理。比如,我們這裡的demo,找到的就是啟動類。
然後調用了下面的方法:
protected void processConfigurationClass(String className) throws IOException { MetadataReader metadataReader = MyConfigurationUtils.getMetadataReader(className); AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata(); /** * 1. 判斷該類上,是否有標註{@link MyComponentScan} */ Map<String, Object> annotationAttributes = annotationMetadata.getAnnotationAttributes(MyComponentScan.class.getName(), true); AnnotationAttributes componentScan = AnnotationAttributes.fromMap(annotationAttributes); /** * 2. 如果類上有這個{@link MyComponentScan},則需要進行處理 */ if (componentScan != null) { /** * 3. 馬上掃描這個base package路徑下的bean,在裏面,會註冊beanDefinition到bean registry */ Set<BeanDefinitionHolder> scannedBeanDefinitions = this.componentScanParser.parse(componentScan, annotationMetadata.getClassName()); /** * 4. 如果掃描回來的bean definition不為空,遞歸處理 */ if (!CollectionUtils.isEmpty(scannedBeanDefinitions)) { this.parse(scannedBeanDefinitions); } } }
- 1處,判斷該類上,是否有標註 @MyComponentScan
- 2處,如果類上有這個 @MyComponentScan ,則需要進行處理
- 3處,馬上掃描這個base package路徑下的bean,在裏面,會註冊beanDefinition到bean registry
- 4處,對第三步掃描,得到bean,遞歸處理,查找更多bean
重點是這裡的第三步,交給了一個叫componentScanParser的去處理,這個componentScanParser是在本類初始化的時候賦值的:
public MyConfigurationClassParser(Environment environment, BeanDefinitionRegistry registry) { this.environment = environment; this.registry = registry; this.componentScanParser = new MyComponentScanParser(componentScanBeanNameGenerator, environment,registry); }
MyComponentScanParser的處理過程
public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, String className) { // 1 String basePackage = componentScan.getString("value"); // 2 includeFilters.add(new AnnotationTypeFilter(MyComponent.class)); // 3 Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<BeanDefinitionHolder>(); /** * 4 獲取包下的全部bean definition */ Set<BeanDefinition> candidates = findCandidateComponents(basePackage); /** * 5 對掃描回來的bean,進行一定的處理,然後註冊到bean registry */ for (BeanDefinition candidate : candidates) { String generateBeanName = componentScanBeanNameGenerator.generateBeanName(candidate, registry); if (candidate instanceof AbstractBeanDefinition) { ((AbstractBeanDefinition)candidate).applyDefaults(this.beanDefinitionDefaults); } if (candidate instanceof AnnotatedBeanDefinition) { AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate); } boolean b = checkCandidate(generateBeanName, candidate); if (b) { // 6 beanDefinitions.add(new BeanDefinitionHolder(candidate,generateBeanName)); } } /** * 7 註冊到bean definition registry */ for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitions) { registry.registerBeanDefinition(beanDefinitionHolder.getBeanName(),beanDefinitionHolder.getBeanDefinition()); } return beanDefinitions; }
- 1處,獲取MyComponentScan註解中value信息,表示要掃描的package
- 2處,設置識別bean的規則,這裡是把註解了@MyComponent的,認為是自己人
- 3處,定義變量,用於存放返回的結果
- 4處,掃描包下的全部滿足條件的,bean definition
- 5處,處理第4步拿到的bean definition集合
- 6處,加到待返回的結果集
- 7處,註冊到spring容器
以上,只有第4處需要再次說明,其他都比較簡單。
獲取滿足條件的bean definition的過程
public Set<BeanDefinition> findCandidateComponents(String basePackage) { Set<BeanDefinition> candidates = new LinkedHashSet<BeanDefinition>(); try { // 1 String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + resolveBasePackage(basePackage) + "/" + this.resourcePattern; Resource[] resources = this.resourcePatternResolver.getResources(packageSearchPath); // 2 for (Resource resource : resources) { log.info("Scanning " + resource); if (!resource.isReadable()) { continue; } // 3 MetadataReader metadataReader = MyConfigurationUtils.getMetadataReader(resource); // 4 if (isCandidateComponent(metadataReader)) { ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader); sbd.setResource(resource); sbd.setSource(resource); // 5 candidates.add(sbd); } else { log.info("Ignored because not matching any filter: " + resource); } } } ... return candidates; }
- 1處,掃描包下的全部class
- 2處,遍歷class
- 3處,獲取該class的註解信息
- 4處,利用註解信息,判斷是否是自己人(註解了@MyComponent)
- 5處,自己人,準備帶走
其中,第4處,實現如下:
protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException { for (TypeFilter tf : this.includeFilters) { if (tf.match(metadataReader, MyConfigurationUtils.getMetadataReaderFactory())) { return true; } } return false; }
就是用includeFilters去匹配,大家還記得前面,我們設置了吧:
includeFilters.add(new AnnotationTypeFilter(MyComponent.class));
大致的過程,就是這樣了。
dubbo中的實現,粗淺分析
我發現,好像和我上面說的,差得不太多,我也沒用過dubbo,確實沒參考dubbo的實現。
比如,它也定義了一個BeanDefinitionRegistryPostProcessor的實現類,叫:
public class ServiceAnnotationBeanPostProcessor implements BeanDefinitionRegistryPostProcessor, EnvironmentAware, ResourceLoaderAware, BeanClassLoaderAware {
其實現如下:
@Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { registerBeans(registry, DubboBootstrapApplicationListener.class); // 1 Set<String> resolvedPackagesToScan = resolvePackagesToScan(packagesToScan); if (!CollectionUtils.isEmpty(resolvedPackagesToScan)) { // 2 registerServiceBeans(resolvedPackagesToScan, registry); } else { if (logger.isWarnEnabled()) { logger.warn("packagesToScan is empty , ServiceBean registry will be ignored!"); } } }
- 1處,找到要掃描的package
- 2處,掃描指定包
上面的2處,實現如下:
private void registerServiceBeans(Set<String> packagesToScan, BeanDefinitionRegistry registry) { DubboClassPathBeanDefinitionScanner scanner = new DubboClassPathBeanDefinitionScanner(registry, environment, resourceLoader); BeanNameGenerator beanNameGenerator = resolveBeanNameGenerator(registry); scanner.setBeanNameGenerator(beanNameGenerator); // 1 scanner.addIncludeFilter(new AnnotationTypeFilter(Service.class)); // 2 scanner.addIncludeFilter(new AnnotationTypeFilter(com.alibaba.dubbo.config.annotation.Service.class)); // 3 for (String packageToScan : packagesToScan) { // 4 Registers @Service Bean first scanner.scan(packageToScan); // 5 Set<BeanDefinitionHolder> beanDefinitionHolders = findServiceBeanDefinitionHolders(scanner, packageToScan, registry, beanNameGenerator); // 6 if (!CollectionUtils.isEmpty(beanDefinitionHolders)) { for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitionHolders) { // 7 registerServiceBean(beanDefinitionHolder, registry, scanner); } } } }
- 1,設置includeFilters,註解類型,註解了
org.apache.dubbo.config.annotation.Service
類型就算 - 2,還是設置includeFilters,只是為了兼容以前的
- 3,遍歷要掃描的package
- 4,掃描指定的包
- 5,掃描包,獲取到滿足條件的集合
- 6,第五步返回不為空,則開始在下面的第7步去註冊到spring
- 7,註冊到spring
總結
經過前面的講解,大家應該立即更清楚一些了吧,如果還是有點懵,那最好把demo拉下來試試。
如果大家覺得還有點幫助,幫忙點個贊吧。