1.背景
上篇最后給大家了一個建議,建議配置bean掃描包時使用如下寫法:
spring-mvc.xml
<!-- 只掃描@Controller注解 -->
<context:component-scan base-package="com.xxx.controller" use-default-filters="false"
>
<context:include-filter type="annotation"
expression="org.springframework.stereotype.Controller" />
</context:component-scan>
spring.xml
<!-- 配置掃描注解,不掃描@Controller注解 -->
<context:component-scan base-package="com.xxx">
<context:exclude-filter type="annotation"
expression="org.springframework.stereotype.Controller" />
</context:component-scan>
文中提到通過以上配置,就可以在Spring MVC容器中只注冊有@Controller注解的bean,Spring容器注冊除了@Controller的其它bean。
有的同學(xué)留言問為什么這樣寫就達(dá)到這種效果了呢?
也有人可能認(rèn)為我是無腦從網(wǎng)上抄來的,我有什么依據(jù),憑什么這么說蜕提?經(jīng)過ISO 9000認(rèn)證了嗎?
為了維護(hù)文章的權(quán)威性以及我的臉面靶端,本篇我就繼續(xù)帶大家從官網(wǎng)和源碼兩方面進(jìn)行分析谎势。
2. < context:component-scan/>流程分析
2.1 Java注解
不是說好的講< context:component-scan>嗎凛膏,怎么注解亂入了。
放心脏榆,雖然看源碼累猖毫,寫讓大家看懂的文章更累,但是我還沒瘋须喂。
為什么講注解鄙麦,因為Spring中很多地方用到注解,本文及前幾篇文章大家或多或少也都有看到镊折。
因此在這里加個小灶,和大家一起回顧一下注解的知識點介衔。
先查看官方文檔:
Annotations, a form of metadata, provide data about a program that is not part of the program itself. Annotations have no direct effect on the operation of the code they annotate.
Annotations have a number of uses, among them:
* Information for the compiler — Annotations can be used by the compiler to detect errors or suppress warnings.
* Compile-time and deployment-time processing — Software tools can process annotation information to generate code, XML files, and so forth.
* Runtime processing — Some annotations are available to be examined at runtime.
上面一段話翻譯過來:
注解是原數(shù)據(jù)的一種形式恨胚,對標(biāo)注的代碼邏輯上沒有直接的影響,只是用來提供程序的一些信息炎咖。
主要用處如下:
* 為編譯器提供信息赃泡,比如錯誤檢測或者警告提示。
* 在編譯和部署期處理期乘盼,程序可以根據(jù)注解信息生成代碼升熊、xml文件。
* 在程序運行期用來做一些檢查绸栅。
2.2 Java元注解
JAVA為了開發(fā)者能夠靈活定義自己的注解级野,因此在java.lang.annotation包中提供了4種元注解,用來注解其它注解粹胯。
查看官方文檔對這4種元注解的介紹:
- @Retention
@Retention annotation specifies how the marked annotation is stored:
* RetentionPolicy.SOURCE – The marked annotation is retained only in the source level and is ignored by the compiler.
* RetentionPolicy.CLASS – The marked annotation is retained by the compiler at compile time, but is ignored by the Java Virtual Machine (JVM).
* RetentionPolicy.RUNTIME – The marked annotation is retained by the JVM so it can be used by the runtime environment.
翻譯:指定標(biāo)記的注解存儲范圍蓖柔。可選范圍是原文件风纠、class文件况鸣、運行期。
- @Documented
@Documented annotation indicates that whenever the specified annotation is used those elements should be documented using the Javadoc tool. (By default, annotations are not included in Javadoc.) For more information, see the Javadoc tools page.
翻譯:因為注解默認(rèn)是不會被JavaDoc工具處理的竹观,因此@Documented用來要求注解能被JavaDoc工具處理并生成到API文檔中
镐捧。
- @Target
@Target annotation marks another annotation to restrict what kind of Java elements the annotation can be applied to. A target annotation specifies one of the following element types as its value:
* ElementType.ANNOTATION_TYPE can be applied to an annotation type.
* ElementType.CONSTRUCTOR can be applied to a constructor.
* ElementType.FIELD can be applied to a field or property.
* ElementType.LOCAL_VARIABLE can be applied to a local variable.
* ElementType.METHOD can be applied to a method-level annotation.
* ElementType.PACKAGE can be applied to a package declaration.
* ElementType.PARAMETER can be applied to the parameters of a method.
* ElementType.TYPE can be applied to any element of a class.
翻譯:用來標(biāo)識注解的應(yīng)用范圍〕粼觯可選的范圍是注解懂酱、構(gòu)造函數(shù)、類屬性誊抛、局部變量玩焰、包、參數(shù)芍锚、類的任意元素昔园。
- @Inherited
@Inherited annotation indicates that the annotation type can be inherited from the super class. (This is not true by default.) When the user queries the annotation type and the class has no annotation for this type, the class' superclass is queried for the annotation type. This annotation applies only to class declarations.
翻譯:默認(rèn)情況下注解不會被子類繼承蔓榄,被@Inherited標(biāo)示的注解可以被子類繼承。
上面就是對4種元注解的介紹默刚,其實大部分同學(xué)都知道甥郑,這里只是一起做個回顧,接下來進(jìn)入正體荤西。
2.3 @Controller介紹
查看官方文檔
Indicates that an annotated class is a "Controller" (e.g. a web controller).
This annotation serves as a specialization of @Component, allowing for implementation classes to be autodetected through classpath scanning. It is typically used in combination with annotated handler methods based on the RequestMapping annotation.
翻譯一下:
@Controller注解用來標(biāo)明一個類是Controller澜搅,使用該注解的類可以在掃描過程中被檢測到。通常@Controller和@RequestMapping注解一起使用來創(chuàng)建handler函數(shù)邪锌。
我們在來看看源碼勉躺,在org.springframework.stereotype包下找到Controller類。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
String value() default "";
}
可以看到Controller聲明為注解類型觅丰,類上的@Target({ElementType.TYPE}) 注解表明@Controller可以用到任意元素上饵溅,@Retention(RetentionPolicy.RUNTIME)表明注解可以保存到運行期,@Documented表明注解可以被生成到API文檔里妇萄。
除定義的幾個元注解外我們還看到有個@Component注解蜕企,這個注解是干什么的呢?
查看官方文檔
Indicates that an annotated class is a "component". Such classes are considered as candidates for auto-detection when using annotation-based configuration and classpath scanning.
翻譯一下:被@Component注解標(biāo)注的類代表該類為一個component冠句,被標(biāo)注的類可以在包掃描過程中被檢測到轻掩。
再看源碼:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Component {
String value() default "";
}
可以看到@Component注解可以用在任意類型上,保留在運行期懦底,能生成到API文檔中唇牧。
再回到@Controller注解,正是因為@Controller被@Component標(biāo)注聚唐,因此被@Controller標(biāo)注的類也能在類掃描的過程中被發(fā)現(xiàn)并注冊奋构。
另外Spring中還用@Service和@Repositor注解定義bean,@Service用來聲明service類拱层,@Repository用來聲明DAO累弥臼。
其源碼如下:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {
String value() default "";
}
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Repository {
String value() default "";
}
2.4 <component-scan>源碼剖析
鋪墊都結(jié)束了,現(xiàn)在開始重頭戲根灯。
和< annotation-driven/>元素一樣径缅, < component-scan/>也屬于自定義命名空間,對應(yīng)的解析器是ComponentScanBeanDefinitionParser烙肺。
自定義命名空間的解析過程可以參考上篇纳猪,此處不再介紹。
我們進(jìn)入CommponentScanBeanDefinitionParser類的parse()方法桃笙。
@Override
public BeanDefinition parse(Element element, ParserContext parserContext) {
//此處 BASE_PACKAGE_ATTRIBUTE = "base-package";
//1.獲取要掃描的包
String basePackage = element.getAttribute(BASE_PACKAGE_ATTRIBUTE);
//此處CONFIG_LOCATION_DELIMITERS = ",; \t\n"氏堤,
//把,或者;分割符分割的包放到數(shù)組里面
String[] basePackages = StringUtils.tokenizeToStringArray(basePackage,
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
//2.創(chuàng)建掃描器
ClassPathBeanDefinitionScanner scanner = configureScanner(parserContext, element);
//3.掃描包并注冊bean
Set<BeanDefinitionHolder> beanDefinitions = scanner.doScan(basePackages);
return null;
}
上面掃描注冊過程可以分為3步。
(1)獲取要掃描的包搏明。
(2)創(chuàng)建掃描器鼠锈。
(3)掃描包并注冊bean闪檬。
第1步邏輯比較簡單,就是單純的讀取配置文件的"base-package"屬性得到要掃描的包列表购笆。
我們從第2步開始分析粗悯。
2.4.1 創(chuàng)建掃描器
進(jìn)入configureScanner方法()。
protected ClassPathBeanDefinitionScanner configureScanner(ParserContext parserContext, Element element) {
//useDefaultFilters默認(rèn)為true同欠,即掃描所有類型bean
boolean useDefaultFilters = true;
//1.此處USE_DEFAULT_FILTERS_ATTRIBUTE = "use-default-filters"样傍,獲取其XML中設(shè)置的值
if (element.hasAttribute(USE_DEFAULT_FILTERS_ATTRIBUTE)) {
useDefaultFilters = Boolean.valueOf(element.getAttribute(USE_DEFAULT_FILTERS_ATTRIBUTE));
}
//2.創(chuàng)建掃描器
ClassPathBeanDefinitionScanner scanner = createScanner(parserContext.getReaderContext(), useDefaultFilters);
//3.解析過濾類型
parseTypeFilters(element, scanner, parserContext);
//4.返回掃描器
return scanner;
}
創(chuàng)建掃描器的方法分為4步。
(1)獲取掃描類范圍铺遂。
(2)根據(jù)掃描范圍初始化掃描器衫哥。
(3)設(shè)置掃描類的過濾器。
(4)返回創(chuàng)建的掃描器襟锐。
第1步也比較簡單撤逢,從配置文件中獲得“use-default-filters”屬性的值,默認(rèn)是true捌斧,即掃描所有類型的注解。
我們進(jìn)入第2步的createScanner()方法泉沾,看看如何創(chuàng)建掃描器捞蚂。
protected ClassPathBeanDefinitionScanner createScanner(XmlReaderContext readerContext, boolean useDefaultFilters) {
//新建一個掃描器
return new ClassPathBeanDefinitionScanner(readerContext.getRegistry(), useDefaultFilters,
readerContext.getEnvironment(),readerContext.getResourceLoader());
}
沿調(diào)用棧進(jìn)入ClassPathBeanDefinitionScanner()方法。
public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters,
//如果useDefaultFilters為true跷究,注冊默認(rèn)過濾器
if (useDefaultFilters) {
//注冊默認(rèn)過濾器
registerDefaultFilters();
}
}
進(jìn)入registerDefaultFilters()方法姓迅。
protected void registerDefaultFilters() {
this.includeFilters.add(new AnnotationTypeFilter(Component.class));
}
可以看到上面方法把Component注解類型加入到了掃描白名單中,因此被@Component標(biāo)注的類都會被掃描注冊俊马。
在此丁存,大家也明白為什么@Controller、@service柴我、@Repository標(biāo)注的類會被注冊了吧解寝,因為這些注解都用@Component標(biāo)注了。
我們再進(jìn)入第3步的parseTypeFilters()方法艘儒,看如何設(shè)置過濾器聋伦。
protected void parseTypeFilters(Element element, ClassPathBeanDefinitionScanner scanner, ParserContext parserContext) {
//解析exclude-filter和include-filter元素
//獲取元素所有子節(jié)點
NodeList nodeList = element.getChildNodes();
//遍歷元素子節(jié)點
for (int i = 0; i < nodeList.getLength(); i++) {
Node node = nodeList.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE) {
String localName = parserContext.getDelegate().getLocalName(node);
//解析include-filter元素 ,此處 INCLUDE_FILTER_ELEMENT = "include-filter"
if (INCLUDE_FILTER_ELEMENT.equals(localName)) {
//創(chuàng)建類型過濾器
TypeFilter typeFilter = createTypeFilter((Element) node, classLoader, parserContext);
//把解析出來的類型加入白名單
scanner.addIncludeFilter(typeFilter);
}
//解析exclude-filter元素,此處EXCLUDE_FILTER_ELEMENT = "exclude-filter"
else if (EXCLUDE_FILTER_ELEMENT.equals(localName)) {
//創(chuàng)建類型過濾器
TypeFilter typeFilter = createTypeFilter((Element) node, classLoader, parserContext);
//把解析出來的類型加入黑名單
scanner.addExcludeFilter(typeFilter);
}
}
}
進(jìn)入createTypeFilter()方法查看實現(xiàn)邏輯界睁。
protected TypeFilter createTypeFilter(Element element, ClassLoader classLoader, ParserContext parserContext) {
//獲取xml中type屬性值觉增,此處FILTER_TYPE_ATTRIBUTE = "type"
String filterType = element.getAttribute(FILTER_TYPE_ATTRIBUTE);
//獲取xml中expression屬性值,此處FILTER_EXPRESSION_ATTRIBUTE = "expression"翻斟,獲取xml中該屬性值
String expression = element.getAttribute(FILTER_EXPRESSION_ATTRIBUTE);
expression = parserContext.getReaderContext().getEnvironment().resolvePlaceholders(expression);
//如果是注解類型逾礁,創(chuàng)建注解類型過濾器,并把需要過濾的注解類設(shè)置進(jìn)去
if ("annotation".equals(filterType)) {
return new AnnotationTypeFilter((Class<Annotation>) ClassUtils.forName(expression, classLoader));
}
}
上面就是創(chuàng)建掃描器的過程访惜,主要是將XML文件中設(shè)置的類型添加到白名單和黑名單中嘹履。
2.4.2 掃描注冊bean
得到掃描器后腻扇,開始掃描注冊流程。
進(jìn)入doScan()方法植捎。
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<BeanDefinitionHolder>();
//遍歷所有需要掃描的包
for (String basePackage : basePackages) {
//1.在該包中找出用@Component注解的類衙解,放到候選列表中
Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
for (BeanDefinition candidate : candidates) {
//2.判斷容器中是否已經(jīng)有bean信息,如果沒有就注冊
if (checkCandidate(beanName, candidate)) {
//生成bean信息
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
//添加bean信息到bean定義列表中
beanDefinitions.add(definitionHolder);
//3.把bean注冊到IOC容器中
registerBeanDefinition(definitionHolder, this.registry);
}
}
}
掃描注冊過程分為3步焰枢。
(1)從包中找出需要注冊的bean并放到候選列表中蚓峦。
(2)遍歷候選列表中的所有bean,判斷容器中是否已經(jīng)存在bean济锄。
(3)如果不存在bean暑椰,就把bean信息注冊到容器中。
接下來依次分析上面掃描注冊流程荐绝。
2.4.2.1 查找候選bean
我們先看第1步一汽,查找候選bean的過程。進(jìn)入findCandidateComponents()方法低滩。
public Set<BeanDefinition> findCandidateComponents(String basePackage) {
Set<BeanDefinition> candidates = new LinkedHashSet<BeanDefinition>();
//1.獲取包的classpath
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
resolveBasePackage(basePackage) + '/' + this.resourcePattern;
//2.把包下的所有class解析成resource資源
Resource[] resources = this.resourcePatternResolver.getResources(packageSearchPath);
//遍歷所有類resource
for (Resource resource : resources) {
if (resource.isReadable()) {
//3.獲取類的元信息
MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(resource);
//4.判斷是否候選component
if (isCandidateComponent(metadataReader)) {
//5.根據(jù)類元信息生成beanDefinition
ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
sbd.setResource(resource);
sbd.setSource(resource);
//6.判斷該bean是否能實例化
if (isCandidateComponent(sbd)) {
//7.加入候選類列表
candidates.add(sbd);
}
//8.返回候選components選列表
return candidates;
}
查找bean的流程比較繁瑣召夹,可以分為以下8步。
(1)獲取包掃描路徑恕沫。
(2)把包路徑下的所有類解析成resource類监憎。
(3)解析resource類,獲取類的元信息婶溯。
(4)根據(jù)類元信息判斷該類是否在白名單中鲸阔。
(5)如果在白名單中,生成beanDefinition信息迄委。
(6)根據(jù)beanDefinition信息判斷類是否能實例化褐筛。
(7)如果可以實例化,將beanDefinition信息加入到候選列表中叙身。
(8)返回保存beanDefinition信息的候選列表渔扎。
還記得BeanDefinition是什么吧,主要是保存bean的信息信轿。如果不記得看看Spring注冊流程赞警。
因為其它邏輯比較簡單,在此我們重點分析第4步和第6步虏两。
先看第4步愧旦,進(jìn)入isCandidateComponent()方法。
protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
//1.遍歷黑名單定罢,若傳入的類元信息在黑名單中返回false
for (TypeFilter tf : this.excludeFilters) {
//判斷是否和傳入的類匹配
if (tf.match(metadataReader, this.metadataReaderFactory)) {
return false;
}
}
//2.遍歷白名單寞射,若傳入的類元信息在白名單中返回true
for (TypeFilter tf : this.includeFilters) {
if (tf.match(metadataReader, this.metadataReaderFactory)) {
//根據(jù)@Conditional注解判斷是否注冊bean劫谅,如果沒有@Conditional注解哎榴,返回true.
return isConditionMatch(metadataReader);
}
}
return false;
}
可以看到上面主要邏輯是判斷該類是否在白名單或黑名單列表中,如果在白名單酬凳,則返回true,在黑名單返回false遭庶。黑宁仔、白名單的值就是創(chuàng)建掃描流程中通過parseTypeFilters()方法設(shè)置進(jìn)去的。
再稍微提一下上面@Conditional注解峦睡,此注解是Spring 4中加入的翎苫,作用是根據(jù)設(shè)置的條件來判斷要不要注冊bean,如果沒有標(biāo)注該注解榨了,默認(rèn)注冊煎谍。我們在這里不展開細(xì)說,有興趣的同學(xué)可以自己查閱相關(guān)資料龙屉。
我們再看第6步呐粘,進(jìn)入isCandidateComponent()方法。
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
//獲取元類信息
AnnotationMetadata metadata = beanDefinition.getMetadata();
//判斷是否可以實例化
return (metadata.isIndependent() && (metadata.isConcrete() ||
(metadata.isAbstract() && metadata.hasAnnotatedMethods(Lookup.class.getName()))));
}
可以看到上面是根據(jù)該類是不是接口转捕、抽象類作岖、嵌套類等信息來判斷能否實例化的。
2.4.2.2 判斷bean是否已經(jīng)注冊
候選bean列表信息已經(jīng)得到五芝,再看看如何對列表中的bean做進(jìn)一步判斷痘儡。
進(jìn)入checkCandiates()方法。
protected boolean checkCandidate(String beanName, BeanDefinition beanDefinition) {
if (!this.registry.containsBeanDefinition(beanName)) {
return true;
}
return false;
}
上面方法比較簡單与柑,主要是查看容器中是否已經(jīng)有bean的定義信息谤辜。
2.4.2.3 注冊bean
對bean信息判斷完成后蓄坏,如果bean有效价捧,就開始注冊bean。
進(jìn)入registerBeanDefinition()方法涡戳。
protected void registerBeanDefinition(BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) {
BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, registry);
}
再進(jìn)入registerBeanDefinition()方法结蟋。
public static void registerBeanDefinition(
BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) {
//得到beanname
String beanName = definitionHolder.getBeanName();
//注冊bean信息
registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());
//注冊bean的別名
String[] aliases = definitionHolder.getAliases();
if (aliases != null) {
for (String alias : aliases) {
registry.registerAlias(beanName, alias);
}
}
上面流程大家有沒有似曾相識,和Spring解析注冊流程
文中注冊bean的邏輯一樣渔彰。
到此就完成了掃描注冊bean流程的分析嵌屎。接下來就是bean的實例化等流程,大家可以參考Spring解析注冊流程一文恍涂。
3.小結(jié)
看完上面的分析宝惰,相信大家對< context:component-scan/>有了深入的了解。
現(xiàn)在回到開頭的那段代碼再沧。會不會有“誠不我欺也”的感覺尼夺。
最后,我再把那段代碼貼出來,大家對著代碼在腦海里想象一下其解析流程淤堵,檢驗一下掌握程度寝衫。
如果有哪一步卡住了,建議再回頭看看我的文章拐邪,直至能在腦海中有一個完整的流程圖慰毅,甚至能想到對應(yīng)的源代碼段。
如果能做到這樣扎阶,說明你真正理解了< context:component-scan/>汹胃,接下來就可以愉快的和小伙伴炫技或者和面試官去侃大山了。
spring-mvc.xml
<!-- 只掃描@Controller注解 -->
<context:component-scan base-package="com.xxx.controller" use-default-filters="false"
>
<context:include-filter type="annotation"
expression="org.springframework.stereotype.Controller" />
</context:component-scan>
spring.xml
<!-- 配置掃描注解,不掃描@Controller注解 -->
<context:component-scan base-package="com.xxx">
<context:exclude-filter type="annotation"
expression="org.springframework.stereotype.Controller" />
</context:component-scan>
本文完乘陪。
如果想獲得更多统台,歡迎關(guān)注公眾號:七分熟pizza
公眾號里我會分享更多技術(shù)以及職場方面的經(jīng)驗,大家有什么問題也可以直接在公眾號向我提問交流啡邑。