spring启动component-scan类扫描加载过程(转)

文章转自 http://www.it165.net/pro/html/201406/15205.html

有朋友最近问到了 spring 加载类的过程,尤其是基于 annotation 注解的加载过程,有些时候如果由于某些系统部署的问题,加载不到,很是不解!就针对这个问题,我这篇博客说说spring启动过程,用源码来说明,这部分内容也会在书中出现,只是表达方式会稍微有些区别,我将使用spring 3.0的版本来说明(虽然版本有所区别,但是变化并不是特别大),另外,这里会从WEB中使用spring开始,中途会穿插自己通过new ClassPathXmlApplicationContext 的区别和联系。

要看这部分源码,其实在spring 3.0以上大家都 一般 会配置一个Servelet,如下所示:
view source print ?
1.

2. <servlet-name>spring</servlet-name>

3. <servlet-< code><code class="keyword">class</code><code class="plain">>org.springframework.web.servlet.DispatcherServlet</code></servlet-<><code class="plain"><code class="keyword">class</code><code class="plain">></code></code>

4. <load-on-startup></load-on-startup> 1

5.

当然 servlet 的名字决定了,你自己获取 SpringContext 的方式,在前面文章:《spring里头各种获取ApplicationContext的方法 》有详细的说明,这里就不细说了,我们就通过DispatcherServlet来说明和跟踪(注意我们这里不说请求转发,就说bean的加载过程),我们知道servlet的规范中,如果load-on-startup被设定了,那么就会被初始化的时候装载,而servlet装载时会调用其 init ()方法,那么自然是调用 DispatcherServlet 的 init 方法,通过源码一看,竟然没有,但是并不带表真的没有,你会发现在父类的父类中:org.springframework.web.servlet.HttpServletBean有这个方法,如下图所示:
view source print ?
01. public final void init()&#xA0; throws ServletException {

02. if (logger.isDebugEnabled()) {

03. logger.debug( "Initializing servlet '" + getServletName() +&#xA0; "'" );

04. }

05.

06. // Set bean properties from init parameters.

07. try {

08. PropertyValues pvs =&#xA0; new ServletConfigPropertyValues(getServletConfig(),&#xA0; this .requiredProperties);

09. BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess( this );

10. ResourceLoader resourceLoader =&#xA0; new ServletContextResourceLoader(getServletContext());

11. bw.registerCustomEditor(Resource. class ,&#xA0; new ResourceEditor(resourceLoader));

12. initBeanWrapper(bw);

13. bw.setPropertyValues(pvs,&#xA0; true );

14. }

15. catch (BeansException ex) {

16. logger.error( "Failed to set bean properties on servlet '" + getServletName() +&#xA0; "'" , ex);

17. throw ex;

18. }

19.

20. // Let subclasses do whatever initialization they like.

21. initServletBean();

22.

23. if (logger.isDebugEnabled()) {

24. logger.debug( "Servlet '" + getServletName() +&#xA0; "' configured successfully" );

25. }

26. }

注意代码: initServletBean(); 其余的都和加载bean关系并不是特别大,跟踪进去会发I发现这个方法是在类:org.springframework.web.servlet. FrameworkServlet类中(是 DispatcherServlet 的父类、 HttpServletBean 的子类 ),内部通过调用 initWebApplicationContext ()来初始化一个 WebApplicationContext ,源码片段(篇幅所限,不拷贝所有源码,仅仅截取片段)

spring启动component-scan类扫描加载过程(转)

接下来需要知道的是如何初始化这个context的(按照使用习惯,其实只要得到了ApplicationContext,就得到了bean的信息,所以在初始化ApplicationCotext的时候,就已经初始化好了bean的信息,至少至少,它初始化好了bean的路径,以及描述信息),所以我们一旦知道 ApplicationCotext 是怎么初始化的,就基本知道 bean 是如何加载的了。

spring启动component-scan类扫描加载过程(转)

这里的parent基本不用管,因为Root的 ApplicationContext 的信息还根本没创建,所以主要是看createWebApplicationContext这个方法,进去后,该方法前面部分,都是在设置一些相关的参数,例如我们需要将WEB容器、以及容器的配置信息设置进去,然后会调用一个 refresh() 方法,这个方法表面上是用来刷新的,其实也是用来做初始化bean用的,也就是配置修改后,如果你能调用它的这个方法,就可以重新装载 spring 的信息,我们看看源码中的片段如下(同样,不相关的部分,我们就不贴太多了):

spring启动component-scan类扫描加载过程(转)

其实这个方法,不论是通过 ClassPathXmlApplicationContext 还是WEB装载都会调用这里,我们看下 ClassPathXmlApplicationContext 中调用的部分:

spring启动component-scan类扫描加载过程(转)

他们的区别在于, web 容器中,用 servlet 装载了, servlet 中包装了一个 XmlWebApplicationContext 而已,而 ClassPathXmlApplicationContext 是直接调用的,他们共同点是,不论是 XmlWebApplicationContext 、还是 ClassPathXmlApplicationContext 都继承了类(间接继承):

AbstractApplicationContext ,这个类中的 refresh() 方法是共用的,也就是他们都调用的这个方法来加载 bean 的,在这个方法中,通过obtainFreshBeanFactory方法来构造 beanFactory 的,如下图所示:

spring启动component-scan类扫描加载过程(转)

是不是看到一层调用一层很烦人,其实回过头来想一想,它没一层都有自己的处理动作,毕竟spring不是简单的做一个bean加载,即使是这样,我们最少也需要做xml解析、类装载和实例化的过程,每个步骤可能都有很多需求,因此分离设计,使得代码更加具有扩展性,我们继续来看 obtainFreshBeanFactory 方法的描述:

spring启动component-scan类扫描加载过程(转)

这里很多人可能会不太注意 refreshBeanFactory ()这个方法,尤其是第一遍看这个代码的,如果你忽略掉,你可能会找不到bean在哪里加载的,前面提到了 refresh 其实可以用以初始化,这里也是这样, refreshBeanFactory 如果没有初始化 beanFactory 就是初始化它了,后面你看到的都是 getBeanFactory 的代码,也就是已经初始化好了,这个refreshBeanFactory方法类 AbstractRefreshableApplicationContext 中的方法,它是 AbstractApplicationContext 的子类,同样 不论是 XmlWebApplicationContext、还是 ClassPathXmlApplicationContext 都继承了它,因此都能调用到这个一样的初始化方法,来看看body部分的代码:

spring启动component-scan类扫描加载过程(转)

注意第一个红圈圈住的地方,是创建了一个beanFactory,然后下面的方法可以通过名称就能看出是” 加载bean的定义 “,将beanFactory传入,自然要加载到beanFactory中了,createBeanFactory就是实例化一个beanFactory没别的,我们要看的是bean在哪里加载的,现在貌似还没看到重点,继续跟踪

loadBeanDefinitions (DefaultListableBeanFactory)方法

它由 AbstractXmlApplicationContext 类中的方法实现,web项目中将会由类: XmlWebApplicationContext 来实现,其实差不多,主要是看启动文件是在那里而已,如果在非web类项目中没有自定义的XmlApplicationContext,那么其实功能可以参考 XmlWebApplicationContext ,可以认为是一样的功能。那么看看loadBeanDefinitions方法如下:

spring启动component-scan类扫描加载过程(转)

这里有一个XmlBeanDefineitionReader,是读取XML中spring的相关信息(也就是解析SpringContext.xml的),这里通过 getConfigLocations() 获取到的就是这个或多个文件的路径,会循环,通过 XmlBeanDefineitionReader 来解析,跟踪到loadBeanDefinitions方法里面,会发现方法实现体在 XmlBeanDefineitionReader的父类:AbstractBeanDefinitionReader中,代码如下:

spring启动component-scan类扫描加载过程(转)

这里大家会疑惑,为啥里面还有一个 loadBeanDefinitions ,大家要知道,我们目前只解析到我们的springContext.xml在哪里,但是还没解析到 springContext.xml的内容是什么,可能有多个spring的配置文件,这里会出现多个Resource,所以是一个数组(这里如何通过location找到文件部分,在我们找class的时候自然明了,大家先不纠结这个问题)。

接下来有很多层调用,会以此调用:

AbstractBeanDefinitionReader.loadBeanDefinitions(Resources []) 循环Resource数组,调用方法:

XmlBeanDefinitionReader.loadBeanDefinitions(Resource ) 和上面这个类是父子关系,接下来会做: doLoadBeanDefinitions、registerBeanDefinitions 的操作,在注册beanDefinitions的时候,其实就是要真正开始解析XML了

它调用了 DefaultBeanDefinitionDocumentReader 类的registerBeanDefinitions方法,如下图所示:

spring启动component-scan类扫描加载过程(转)

中间有解析XML的过程,但是貌似我们不是很关心,我们就关系类是怎么加载的,虽然已经到XML解析部分了,所以主要看parseBeanDefinitions这个方法,里面会调用到BeanDefinitionParserDelegate类的parseCustomElement方法,用来解析bean的信息:

spring启动component-scan类扫描加载过程(转)z

这里解析了XML的信息,跟踪进去,会发现用了 NamespaceHandlerSupport 的parse方法,它会根据节点的类型,找到一种合适的解析 BeanDefinitionParser(接口) ,他们预先被spring注册好了,放在一个HashMap中,例如我们在spring 的annotation扫描中,通常会配置:
view source print ?
1. <context:component-scan base-< code><code class="keyword">package</code><code class="plain">=</code><code class="string">"com.xxx"</code> <code class="plain">/></code></context:component-scan>

此时根据名称” component-scan “就会找到对应的解析器来解析,而与之对应的就是 ComponentScanBeanDefinitionParserparse 方法,这地方已经很明显有扫描bean的概念在里面了,这里的parse获取到后,中间有一个非常非常关键的步骤那就是定义了 ClassPathBeanDefinitionScanner 来扫描类的信息,它扫描的是什么?是加载的类还是class文件呢?答案是后者,为何,因为有些类在初始化化时根本还没被加载,ClassLoader根本还没加载,只是ClassLoader可以找到这些class的路径而已:

spring启动component-scan类扫描加载过程(转)

注意这里的scanner创建后,最关键的是 doScan 的功能,解析XML我想来看这个的不是问题,如果还不熟悉可以先看看,那么我们得到了类似: com.xxx 这样的信息,就要开始扫描类的列表,那么再哪里扫描呢?这里的doScan返回了一个 Set 我们感到希望就在不远处,进去看看 doScan 方法。

spring启动component-scan类扫描加载过程(转)

我们看到这么大一坨代码,其实我们目前不关心的代码,暂时可以不管,我们就看怎么扫描出来的,可以看出最关键的扫描代码是: findCandidateComponents(String basePackage) 方法,也就是通过每个 basePackage 去找到有那些类是匹配的,我们这里假如配置了 com.abc ,或配置了 * 两种情况说明。

spring启动component-scan类扫描加载过程(转)

主要看红线部分,下面非红线部分,是已经拿到了类的定义,红线部分,会组装信息,如果我们配置了 com.abc会组装为: classpath*:com/abc//.class ,如果配置是 * ,那么将会被组装为 classpath://.class** ,但是这个好像和我们用的东西不太一样,java中也没见这种URL可以获取到,spring到底是怎么搞的呢?就要看第二个红线部分的代码:
view source print ?
1. Resource[] resources =&#xA0; this .resourcePatternResolver.getResources(packageSearchPath);

它竟然神奇般的通过这个路径获取到了URL,你一旦跟踪你会发现,获取出来的全是.class的路径,包括jar包中的相关class路径,这里有些细节,我们先不说,先看下这个resourcePatternResolover是什么类型的,看到定义部分是:
view source print ?
1. private ResourcePatternResolver resourcePatternResolver =&#xA0; new PathMatchingResourcePatternResolver();

为此胖哥还将其做了一个测试,用一个简单main方法写了一段:
view source print ?
1. ResourcePatternResolver resourcePatternResolver =&#xA0; new PathMatchingResourcePatternResolver();

2.

3. Resource[] resources = resourcePatternResolver.getResources( "classpath*:com/abc/**/*.class" );

获取出来的果然是那样,胖哥开始猜测,这个和ClassLoader的getResource方法有关系了,因为太类似了,我们跟踪进去看下:

spring启动component-scan类扫描加载过程(转)

这个 CLASSPATH_ALL_URL_PREFIX 就是字符串 classpath*: , 我们传递参数进来的时候,自然会走第一个红圈圈住部分的代码,但是第二个红圈圈住部分的代码是干嘛的呢,胖哥告诉你先知道有这个,然后回头会有用,继续找 findPathMatchingResources 方法,好了,越来越接近真相了。

spring启动component-scan类扫描加载过程(转)

这里有一个 rootDirPath ,这个地方有个容易出错的,是如果你配置的是 com.abc,那么 rootDirPath 部分应该是: classpath:com/abc/ 而如果配置是 * 那么 classpath: 只有这个结果,而不是 classpath: (这里我就不说截取字符串的源码了),回到上一段代码,这里再次调用了getResources(String)方法,又回到前面一个方法,这一次,依然是以classpath*:开头,所以第一层 if 语句会进去,而第二层不会,为什么?在里面的isPattern() 的实现中是这样写的:
view source print ?
1. public boolean isPattern(String path) {

2. return (path.indexOf( '*' ) != - 1 || path.indexOf( '?' ) != - 1 );

3. }

在匹配前,做了一个 substring 的操作,会将” classpath*: “这个字符串去掉,如果是配置的是com.abc就变成了”com/abc/”,而如果配置为,那么得到的就是”” ,也就是长度为0的字符串,因此在我们的这条路上,这个方法返回的是false,就会走到代码段 findAllClassPathResources* 中,这就是为什么上面提到会有用途的原因,好了,最最最最关键的地方来了哦。例如我们知道了一个com/abc/为前缀,此时要知道相关的classpath下面有哪些class是匹配的,如何做?自然用ClassLoader,我们看看Spring是不是这样做的:

spring启动component-scan类扫描加载过程(转)

果然不出所料,它也是用ClassLoader,只是它自己提供的getClassLoader()方法,也就是和spring的类使用同一个加载器范围内的,以保证可以识别到一样的classpath,自己模拟的时候,可以用一个类

类名.class.getClassLoader().getResources(“”)

如果放为空,那么就是获取classpath的相关的根路径(classpath可能有很多,但是根路径,可以被合并),也就是如果你配置的*,获取到的将是这个,也许你在web项目中,你会获取到项目的根路径(classes下面,以及tomcat的lib目录)。

如果写入一个: com/abc/ 那么得到的将是扫描相关classpath下面所有的class和jar包中与之匹配的类名(前缀部分)的路径信息,但是需要注意的是,如果有 两层jar包 ,而你想要扫描的类或者说想要通过spring加载的类在 第二层jar包中 ,这个方法是获取不到的,这不是spring没有去做这个事情,而是,java提供的getResources方法就是这样的,有朋友问我的时候,正好遇到了类似的事情,另外需要注意的是, getResources 这个方法是包含当前路径的一个递归文件查找(一般环境变量中都会配置 . ),所以如果是一个jar包,你要运行的话,切记放在某个根目录来跑,因为当前目录,就是根目录也会被递归下去,你的程序会被莫名奇怪地慢。

这里大家还可以通过以下简单的方式来测试调用路径的问题:
view source print ?
1. ClassPathScanningCandidateComponentProvider provider =&#xA0; new ClassPathScanningCandidateComponentProvider( true );

2. Set<beandefinition> beanDefinitions = provider.findCandidateComponents(</beandefinition> "com/abc" );

3. for (BeanDefinition beanDefinition : beanDefinitions) {

4. System.out.println(beanDefinition.getBeanClassName()

5. +&#xA0; "&#xA0;&#xA0; " + beanDefinition.getResourceDescription()

6. +&#xA0; "&#xA0;&#xA0; " + beanDefinition.getClass());

7. }

这是直接引用spring的源码部分的内容,如果这里可以获取到, 且路径是正确的,一般情况下,都是可以加载到类的。

看了这么多,是不是有点晕,没关系,谁第一回看都这样,当你下一次看的时候,有个思路就好了,我这里并没有像UML一样理出他们的层次关系,和调用关系,仅仅针对代码调用逐层来说明,大家如果初步看就是,由Servlet初始化来创建ApplicationContext,在设置了Servelt相关参数后,获取servlet的配置文件路径或自己指定的配置文件路径(applicationContext.xml或其他的名字,可以一个或多个),然后通过系列的XML解析,以及针对每种不同的节点类型使用不同的加载方式,其中 component-scan 用于指定扫描类的对应有一个Scanner,它会通过ClassLoader的getResources方法来获取到class的路径信息,那么class的路径都能获取到,类的什么还拿不到呢?呵呵!

好,本文基本内容就说到这里,接下来我会提到 spring MVC 的中的简单跳转的解析,其中有部分源码是这里看过的,只是还不是这里的重点而已。

而我想说的也是这点,其实本文虽然在说启动,其实有很多代码也没说,因为那样的话我就是一个复制咱贴机了;

其实看源码,要带着目的,大家要知道主体情况或实现的功能,不要就看源码而看源码,一个是根本记不下来,另一个这样看代码没有太大的意义。

当你有了疑问,遇到了难题不知道原因,或发现了新大陆,很有兴趣,那么去看看,也许看之前你会思考下如果我来实现会怎么做?再看看别人是怎么做的,有何区别,不断吸取这些开源框架中优秀的品质,包括代码的设计层次,了解它用到了什么,为何要这样设计,那么你的代码相信会越来越漂亮,你对开源界的代码也会越来越熟悉,熟悉得像自己亲人一样,呵呵。

Original: https://www.cnblogs.com/shuaiandjun/p/10345874.html
Author: 帅LOVE俊
Title: spring启动component-scan类扫描加载过程(转)

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/541546/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 设计模式——结构性设计模式

    结构性设计模式 针对类与对象的组织结构。(白话:类与对象之间的交互的多种模式 类/对象适配器模式 当需要传入一个A类型参数,但只有B类型类时,就需要一个A类型的适配器装入B类的数据…

    Java 2023年6月14日
    074
  • Linux 搭建Apollo

    简介 Apollo(阿波罗)是携程框架部门研发的分布式配置中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用…

    Java 2023年6月5日
    090
  • 20220809-Java的接口和实现interface&implements

    1.接口的语法 2.接口随版本的变化 3.接口注意事项 4.实现接口 VS 继承类 5.接口的多态特性: 6.接口代码示例 今天抽空学习了接口相关的基础知识,学习了一些新的名词:接…

    Java 2023年6月15日
    086
  • 云原生系列1 pod基础

    成组资源调度问题的解决。 mesos采用的资源囤积策略容易出现死锁和调度效率低下问题;google采用的乐观调度技术难度非常大; 而k8s使用pod优雅的解决了这个问题。 pod的…

    Java 2023年6月8日
    083
  • Golang实现二维数组的排序

    一、通常的实现方法 实现sort.Interface接口中的3个方法:Len方法、Less方法以及Swap方法,即可通过调用sort包中的Sort方法实现结构体数组的排序。(二维数…

    Java 2023年6月13日
    071
  • 细品 Spring Boot+Thymeleaf,还有这么多好玩的细节!

    @ * – 1. Thymeleaf 简介 – 2. 整合 Spring Boot + 2.1 基本用法 + 2.2 手动渲染 – 3. Thy…

    Java 2023年5月30日
    096
  • FreeMarker 去掉循环末尾的符号

    在使用 FreeMarker 模板引擎来生成文件时,经常会使用到 list 标签用于循环生成。 有时会遇到需要处理末尾符号的情况,比如 Json 文件,循环生成的标签中末尾是不需要…

    Java 2023年6月6日
    080
  • day04-1群聊功能

    多用户即时通讯系统04 4.编码实现03 4.5功能实现-群聊功能实现 4.5.1思路分析 群聊的实现思路和私聊的实现非常类似。 不同的是:私聊时,服务端接收到消息后,只需要找出接…

    Java 2023年6月15日
    082
  • java基础篇—-类的方法常见错误

    预备知识梳理 什么是类? 首先先了解类与对象的关系 打个比方,制作一件衣服,得先有它的设计图,然后市场部在根据客户需求来确认数量,最后员工按照设计图来制作衣服. 在这个例子中,设计…

    Java 2023年6月8日
    0161
  • java 遍历Map的4种方法

    在Java中如何遍历Map对象 How to Iterate Over a Map in Java 在java中遍历Map有不少的方法。我们看一下最常用的方法及其优缺点。 既然ja…

    Java 2023年5月29日
    069
  • 按部就班的写作方法

    1.计划 阶段目标:拟定一份大纲 ① 收集想法。 ② 选择布局。 ③ 选择引言和结论。 ④ 留意那些观点需要证实,以及你将怎么证实他们。 2.起草 阶段目标:写出一份草稿 ① 在起…

    Java 2023年6月5日
    081
  • 【Java面试】听说Java求职者/面试官都关注了我,这道面试题一个空Object对象的占多大空间?你答的上来吗

    “一个空Object对象的占多大空间?”一个工作了5年的Java程序员直接被搞蒙了。大家好,我是Mic,一个工作了14年的Java程序员。我把这个问题的文字…

    Java 2023年6月16日
    088
  • 01java大数据开发_Linux安装

    大数据开发01——linux环境安装配置 1.1软件包和资料 需要安装:VMware、CentOs6.5、Xfth5、Xshell5、Xmind; 需要可加V:zhanjiquan…

    Java 2023年6月8日
    0105
  • 草履虫都能看懂的系统环境变量配置

    超详细的环境变量配置教学(Windows10) 很多刚刚入坑计算机的小伙伴可能对环境变量的配置不太熟悉,如果你还在找教程,那么看到这里,你就不用继续找了(嘿嘿~),废话不多说,让我…

    Java 2023年6月5日
    091
  • 第四周(实际是n+周)

    报错内容:ERROR RUNNING ‘TOMCAT’: UNABLE TO OPEN DEBUGGER PORT (127.0.0.1:38667): J…

    Java 2023年6月7日
    098
  • 【Elasticsearch】ES选主流程分析

    Raft协议 Raft是分布式系统中的一种共识算法,用于在集群中选举Leader管理集群。Raft协议中有以下角色: Leader(领导者):集群中的领导者,负责管理集群。 Can…

    Java 2023年6月8日
    082
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球