寫在前面的話
相關背景及資源:
曹工說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源碼第11篇,最近都在講解:spring解析xml文件,到底獲得了什么?獲得了什么呢,感興趣的可以挑選感興趣的看;目前呢,已經講到了context命名空間,接下來准備講解component-scan,但是吧,這個真的是一個重量級的嘉賓,且不說原理,光是用法,就夠我們感受感受啥叫主角了。
常規用法
我們在package:org.springframework.contextnamespace.componentscantest下存放了以下幾個文件:
MainClassForTestComponentScan.java 測試類,包含main方法,不是bean
PersonTestController.java 使用了@Controller注解,里面使用@Autowired自動注入了PersonService
PersonService.java 使用了@Service注解
下邊看下代碼:
//定義一個bean
package org.springframework.contextnamespace.componentscantest;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
@Slf4j
@Data
@Controller
public class PersonTestController {
@Autowired
private PersonService personService;
}
// 再一個bean
package org.springframework.contextnamespace.componentscantest;
import org.springframework.stereotype.Service;
@Service
public class PersonService {
private String personname;
}
//測試代碼
package org.springframework.contextnamespace.componentscantest;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.util.MyFastJson;
import java.util.List;
import java.util.Map;
@Slf4j
public class MainClassForTestComponentScan {
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(
new String[]{"classpath:context-namespace-test-component-scan.xml"},false);
context.refresh();
List<BeanDefinition> list =
context.getBeanFactory().getBeanDefinitionList();
// 我自己的工具類,使用json輸出bean definition
MyFastJson.printJsonStringForBeanDefinitionList(list);
Object bean = context.getBean(PersonTestController.class);
System.out.println("PersonController bean:" + bean);
}
}
xml文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="org.springframework.contextnamespace.componentscantest"/>
</beans>
輸出:
PersonController bean:PersonTestController(personService=org.springframework.contextnamespace.componentscantest.PersonService@3e11f9e9)
可以看到,注入成功。
我代碼里,其實還輸出了全部的beanDefinition
,我簡單整理了一下,一共包含了如下幾個:
beanDefinition中的beanClass |
---|
org.springframework.context.annotation.CommonAnnotationBeanPostProcessor |
org.springframework.contextnamespace.componentscantest.PersonService 我們自己的業務bean |
org.springframework.contextnamespace.componentscantest.PersonTestController 業務bean |
org.springframework.context.annotation.ConfigurationClassPostProcessor |
org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor |
org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor |
org.springframework.context.annotation.ConfigurationClassPostProcessor$ImportAwareBeanPostProcessor |
看來,一個簡單的注解,背后卻默默做了很多騷操作啊,除了自己的業務bean外,還有5個框架自帶的bean,類型呢,從命名可以看出,都是些什么PostProcessor,有興趣的,可以翻到我前一篇,里面講解了AutowiredAnnotationBeanPostProcessor
。
閱讀理解
我們從spring-context.xsd文件可以找到這個元素的官方說明。
Scans the classpath for annotated components that will be auto-registered as Spring beans. By default, the Spring-provided @Component, @Repository, @Service, and @Controller stereotypes will be detected. Note: This tag implies the effects of the 'annotation-config' tag, activating @Required, @Autowired, @PostConstruct, @PreDestroy, @Resource, @PersistenceContext and @PersistenceUnit annotations in the component classes, which is usually desired for autodetected components (without external configuration). Turn off the 'annotation-config' attribute to deactivate this default behavior, for example in order to use custom BeanPostProcessor definitions for handling those annotations. Note: You may use placeholders in package paths, but only resolved against system properties (analogous to resource paths). A component scan results in new bean definition being registered; Spring's PropertyPlaceholderConfigurer will apply to those bean definitions just like to regular bean definitions, but it won't apply to the component scan settings themselves. See Javadoc for org.springframework.context.annotation.ComponentScan for information on code-based alternatives to bootstrapping component-scanning.
我用我的425分壓線4級翻譯了一下:
掃描類路徑下的注解組件,它們將會被主動注冊為spring bean。默認情況下,可以識別以下注解:
@Component, @Repository,@Service, and @Controller。注意:這個元素隱含了context:annotation-config的作用,會默認激活bean class類里的@Required,
@Autowired, @PostConstruct, @PreDestroy, @Resource, @PersistenceContext and @PersistenceUnit 注解,這個功能也是一般默認需要的。將annotation-config屬性,設為false,可以關閉這項功能,比如想要自己定制處理這些注解的BeanPostProcessor時。注意:你可以使用在包路徑里,使用placeholder,但是只能引用system property。 component scan會導致新的bean definition被注冊,Spring的PropertyPlaceholderConfigurer對這些bean,依然生效,但是,PropertyPlaceholderConfigurer 不能對 component scan生效。
如果要基於注解啟動component-scan,請查看org.springframework.context.annotation.ComponentScan
這個只是元素本身的介紹,你知道,這個元素的屬性還是有辣么多的,我們用一個表格,來看看其屬性的意思:
annotation-config屬性的作用
這個屬性的意思是,本來,component-scan不是默認包含了context:annotation-config的功能嗎,所以才能夠識別並解析@Autowired等注解,那要是我們關了這個功能,再試試還能不能注入呢?
<context:component-scan
//這里設為false,關閉@autowired等注解的解析功能
annotation-config="false"
base-package="org.springframework.contextnamespace.componentscantest"/>
再次測試,輸出如下:
PersonController bean:PersonTestController(personService=null)
可以發現,注入沒成功。
而且,這次,我的beanDefinition輸出語句顯示,一共只有兩個beanDefinition,就是我們定義的那兩個業務bean。
這么看來,annotation-config的魔術手被我們斬斷了,當然,代價就是,不能自動注入了。
use-default-filters屬性的作用
本來這個屬性的作用吧,從字面上看是說:
Indicates whether automatic detection of classes annotated with @Component, @Repository, @Service, or @Controller should be enabled. Default is "true".
即:是否自動檢測注解了@Component, @Repository, @Service,or @Controller 的類。
后面翻看了一下源碼,更加明確了意義:
在component-scan這個元素的解析器里(ComponentScanBeanDefinitionParser),有個屬性:
private static final String USE_DEFAULT_FILTERS_ATTRIBUTE = "use-default-filters";
關鍵代碼如下:
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,下面的 2 3 4 等,代表一步一步跟代碼的跳轉順序
ClassPathBeanDefinitionScanner scanner = createScanner(readerContext, useDefaultFilters);
...
}
// 2.
protected ClassPathBeanDefinitionScanner createScanner(XmlReaderContext readerContext, boolean useDefaultFilters) {
// 3
return new ClassPathBeanDefinitionScanner(readerContext.getRegistry(), useDefaultFilters);
}
// 3
public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters) {
this(registry, useDefaultFilters, getOrCreateEnvironment(registry));
}
// 4
public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters, Environment environment) {
// 5
super(useDefaultFilters, environment);
Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
this.registry = registry;
// Determine ResourceLoader to use.
if (this.registry instanceof ResourceLoader) {
setResourceLoader((ResourceLoader) this.registry);
}
}
// 第5處,進入以下邏輯
public ClassPathScanningCandidateComponentProvider(boolean useDefaultFilters, Environment environment) {
// 如果使用默認filter,則注冊默認filter
if (useDefaultFilters) {
registerDefaultFilters();
}
this.environment = environment;
}
下邊就是核心了:
protected void registerDefaultFilters() {
/**
* 默認掃描Component注解
*/
this.includeFilters.add(new AnnotationTypeFilter(Component.class));
ClassLoader cl = ClassPathScanningCandidateComponentProvider.class.getClassLoader();
try {
// 這里可以看到,還支持 ManagedBean 注解
this.includeFilters.add(new AnnotationTypeFilter(
((Class<? extends Annotation>) cl.loadClass("javax.annotation.ManagedBean")), false));
logger.info("JSR-250 'javax.annotation.ManagedBean' found and supported for component scanning");
}
try {
// 還支持 javax.inject.Named 注解
this.includeFilters.add(new AnnotationTypeFilter(
((Class<? extends Annotation>) cl.loadClass("javax.inject.Named")), false));
logger.info("JSR-330 'javax.inject.Named' annotation found and supported for component scanning");
}
}
所以,這個屬性的作用就是:假設指定的掃描包內有20個類,其中2個class注解了@component,則這兩個類才是真正被掃描的類,至於具體的解析,這個屬性就不關心了。
context:exclude-filter屬性的作用
為什么不分析context:include-filter,因為假設某個類沒有注解@component,按理說,是不加入掃描范圍的;
如果我們的include-filter把這個類納入范圍,則還要自定義bean definition的解析邏輯才能將這個類變成bean。
我們這里有個demo,其中TeacherController和TeacherService是注解了ShouldExclude的。
xml如下:
<context:component-scan
use-default-filters="true"
base-package="org.springframework.contextnamespace">
// 我們這里使用了annotation類型,要把包含了ShouldExclude注解的,全部排除
<context:exclude-filter type="annotation" expression="org.springframework.contextnamespace.ShouldExclude"></context:exclude-filter>
// 這里使用regex類型,排除掉TestController
<context:exclude-filter type="regex" expression="org.springframework.contextnamespace.TestController"></context:exclude-filter>
</context:component-scan>
所以,上面的xml,我們可以將3個bean全部排除。
context:include-filter屬性的作用
在前面,我們說,這個屬性不好測試,但我想到也許可以這樣測:
<context:component-scan
use-default-filters="false"
base-package="org.springframework.contextnamespace">
<context:include-filter
type="annotation"
expression="org.springframework.stereotype.Component"/>
<context:exclude-filter type="regex" expression="org.springframework.contextnamespace.TestController"/>
</context:component-scan>
use-default-filters 這里設為false,排除掉默認的@component的include filter;
但是我們在下面,再通過include-filter來達到同樣效果。
<context:include-filter
type="annotation"
expression="org.springframework.stereotype.Component"/>
經過上述改造后,運行正常。
注:以上部分是前兩天寫的(代碼要后邊上傳,在家里電腦上),以下部分是公司電腦寫的,前面的代碼在家里,忘記提交了。所以demo會略微不一樣,不過不影響實驗。
前面我說不好測試,但我發現還是可以搞。我們將會單獨定義一個自定義注解:
package org.springframework.contextnamespace;
import org.springframework.stereotype.Component;
import java.lang.annotation.*;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 大家注意,這里的@component我注掉了
//@Component
public @interface DerivedComponent {
/**
* The value may indicate a suggestion for a logical component name,
* to be turned into a Spring bean in case of an autodetected component.
* @return the suggested component name, if any
*/
String value() default "";
}
然后呢,下面這兩個類我是使用上面的注解來標注了的:
@DerivedComponent
public class PersonService {
private String personname1;
}
@DerivedComponent
public class PersonTestController {
// @Autowired
@Resource
private PersonService personService;
}
xml如下:
<context:component-scan
use-default-filters="false"
base-package="org.springframework.contextnamespace.componentscan">
<context:include-filter type="annotation" expression="org.springframework.contextnamespace.DerivedComponent"/>
</context:component-scan>
有必要解釋下:
use-default-filters="false":為true時,會將注解了@component或者@controller等注解的class包含進候選bean;這里設為false,就不會進行上述行為;
context:include-filter:這里呢,類型為注解,注解類就是我們自定義的那個。
總體意思就是,掃描指定包下面的,帶有@DerivedComponent注解的類;忽略帶有@component等注解的類。
這樣設置,我們的測試程序會如何:
PersonController bean:PersonTestController(personService=org.springframework.contextnamespace.componentscan.PersonService@4f615685)
it works!沒想到,這樣都可以。
我們看看他們的bean definition:
{
"abstract":false,
"autowireCandidate":true,
"autowireMode":0,
"beanClassName":"org.springframework.contextnamespace.componentscan.PersonService",
"constructorArgumentValues":{
"argumentCount":0,
"empty":true,
"genericArgumentValues":[],
"indexedArgumentValues":{}
},
"dependencyCheck":0,
"enforceDestroyMethod":false,
"enforceInitMethod":false,
"lazyInit":false,
"lenientConstructorResolution":true,
"metadata":{
"abstract":false,
// 這里可以看到,注解確實是DerivedComponent
"annotationTypes":["org.springframework.contextnamespace.DerivedComponent"],
"className":"org.springframework.contextnamespace.componentscan.PersonService",
"concrete":true,
"final":false,
"independent":true,
"interface":false,
"interfaceNames":[],
"memberClassNames":[],
"superClassName":"java.lang.Object"
},
"methodOverrides":{
"empty":true,
"overrides":[]
},
"nonPublicAccessAllowed":true,
"primary":false,
"propertyValues":{
"converted":false,
"empty":true,
"propertyValueList":[]
},
"prototype":false,
"qualifiers":[],
"resolvedAutowireMode":0,
"resourceDescription":"file [F:\\work_java_projects\\spring-boot-first-version-learn\\all-demo-in-spring-learning\\spring-xml-demo\\target\\classes\\org\\springframework\\contextnamespace\\componentscan\\PersonService.class]",
"role":0,
"scope":"singleton",
"singleton":true,
"synthetic":false
}
具體原理,下節具體分析,主要呢,context:component-scan 的解析代碼,主要就是負責收集beandefinition,
而上面這種自定義注解收集的方式的缺點在於,不能像@component等注解那樣,有很多的屬性可以設置。我們的自定義注解,只能是使用默認的beanDefinition配置,比如默認單例,等等。當然,你也可以直接使用和@component一模一樣的屬性,不過那也沒啥必要了,對吧。
這部分的源碼,我放在了:
最后這個自定義注解的內容,小馬哥的spring boot編程思想里也提到了,在161頁,我手邊沒有電子版本,所以抱歉了。
總結
component-scan,用了這么些年,看來真的只是用,里面的原理還是一知半解,經過上面的分析,我也自己系統梳理了一遍。大家看看有啥問題的,歡迎指出來,一起進步。