背景
上篇最后給大家了一個建議,建議配置bean掃描包時使用如下寫法:
spring-mvc.xml
spring.xml
文中提到通過以上配置,就可以在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. 流程分析
2.1 Java注解
不是說好的講嗎,怎么注解亂入了。
放心,雖然看源碼累,寫讓大家看懂的文章更累,但是我還沒瘋。
為什么講注解,因?yàn)镾pring中很多地方用到注解,本文及前幾篇文章大家或多或少也都有看到。
因此在這里加個小灶,和大家一起回顧一下注解的知識點(diǎn)。
先查看官方文檔:
https://docs.oracle.com/javase/tutorial/java/annotations/predefined.html
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文件。* 在程序運(yùn)行期用來做一些檢查。
2.2 Java元注解
JAVA為了開發(fā)者能夠靈活定義自己的注解,因此在java.lang.annotation包中提供了4種元注解,用來注解其它注解。
查看官方文檔對這4種元注解的介紹:
1.@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)記的注解存儲范圍??蛇x范圍是原文件、class文件、運(yùn)行期。
2.@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.
翻譯:因?yàn)樽⒔饽J(rèn)是不會被JavaDoc工具處理的,因此@Documented用來要求注解能被JavaDoc工具處理并生成到API文檔中 。
3.@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ù)、類的任意元素。
4.@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種元注解的介紹,其實(shí)大部分同學(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@Componentpublic @interface Controller { String value() default “”;}
可以看到Controller聲明為注解類型,類上的@Target({ElementType.TYPE}) 注解表明@Controller可以用到任意元素上,@Retention(RetentionPolicy.RUNTIME)表明注解可以保存到運(yùn)行期,@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)@Documentedpublic @interface Component { String value() default “”;}
可以看到@Component注解可以用在任意類型上,保留在運(yùn)行期,能生成到API文檔中。
再回到@Controller注解,正是因?yàn)锧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@Componentpublic @interface Service { String value() default “”;}@Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@Componentpublic @interface Repository { String value() default “”;}
2.4 源碼剖析
鋪墊都結(jié)束了,現(xiàn)在開始重頭戲。
和元素一樣, 也屬于自定義命名空間,對應(yīng)的解析器是ComponentScanBeanDefinitionParser。
自定義命名空間的解析過程可以參考上篇,此處不再介紹。
我們進(jìn)入CommponentScanBeanDefinitionParser類的parse()方法。
@Overridepublic BeanDefinition parse(Element element, ParserContext parserContext) { //此處 BASE_PACKAGE_ATTRIBUTE = “base-package”; //1.獲取要掃描的包 String basePackage = element.getAttribute(BASE_PACKAGE_ATTRIBUTE); //此處CONFIG_LOCATION_DELIMITERS = “,; “, //把,或者;分割符分割的包放到數(shù)組里面 String[] basePackages = StringUtils.tokenizeToStringArray(basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); //2.創(chuàng)建掃描器 ClassPathBeanDefinitionScanner scanner = configureScanner(parserContext, element); //3.掃描包并注冊bean Set 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)注的類會被注冊了吧,因?yàn)檫@些注解都用@Component標(biāo)注了。
我們再進(jìn)入第3步的parseTypeFilters()方法,看如何設(shè)置過濾器。
protected void parseTypeFilters(Element element, ClassPathBeanDefinitionScanner scanner, ParserContext parserContext) { //解析exclude-filter和include-filter元素 //獲取元素所有子節(jié)點(diǎn) NodeList nodeList = element.getChildNodes(); //遍歷元素子節(jié)點(diǎn) 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()方法查看實(shí)現(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) ClassUtils.forName(expression, classLoader)); }}
上面就是創(chuàng)建掃描器的過程,主要是將XML文件中設(shè)置的類型添加到白名單和黑名單中。
2.4.2 掃描注冊bean
得到掃描器后,開始掃描注冊流程。
進(jìn)入doScan()方法。
protected Set doScan(String… basePackages) { Set beanDefinitions = new LinkedHashSet(); //遍歷所有需要掃描的包 for (String basePackage : basePackages) { //1.在該包中找出用@Component注解的類,放到候選列表中 Set 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 findCandidateComponents(String basePackage) { Set candidates = new LinkedHashSet(); //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是否能實(shí)例化 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信息判斷類是否能實(shí)例化。
(7)如果可以實(shí)例化,將beanDefinition信息加入到候選列表中。
(8)返回保存beanDefinition信息的候選列表。
還記得BeanDefinition是什么吧,主要是保存bean的信息。如果不記得看看Spring注冊流程。
因?yàn)槠渌壿嫳容^簡單,在此我們重點(diǎn)分析第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(); //判斷是否可以實(shí)例化 return (metadata.isIndependent() && (metadata.isConcrete() || (metadata.isAbstract() && metadata.hasAnnotatedMethods(Lookup.class.getName()))));}
可以看到上面是根據(jù)該類是不是接口、抽象類、嵌套類等信息來判斷能否實(shí)例化的。
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的實(shí)例化等流程,大家同樣可以參考Spring解析注冊流程一文。
3.小結(jié)
看完上面的分析,相信大家對有了深入的了解。
現(xiàn)在回到開頭的那段代碼。會不會有“誠不我欺也”的感覺。
最后,我再把那段代碼貼出來,大家對著代碼在腦海里想象一下其解析流程,檢驗(yàn)一下掌握程度。
如果有哪一步卡住了,建議再回頭看看我的文章,直至能在腦海中有一個完整的流程圖,甚至能想到對應(yīng)的源代碼段。
如果能做到這樣,說明你真正理解了,接下來就可以愉快的和小伙伴炫技或者和面試官去侃大山了。
spring-mvc.xml
spring.xml
本文完。
推薦閱讀: