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

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

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

1.<servlet>
2.<servlet-name>spring</servlet-name>
3.<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
4.<load-on-startup>1</load-on-startup>
5.</servlet>

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

01.public final void init() throws ServletException {
02.if (logger.isDebugEnabled()) {
03.logger.debug("Initializing servlet '" + getServletName() + "'");
04.}
05. 
06.// Set bean properties from init parameters.
07.try {
08.PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
09.BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
10.ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
11.bw.registerCustomEditor(Resource.classnew ResourceEditor(resourceLoader));
12.initBeanWrapper(bw);
13.bw.setPropertyValues(pvs, true);
14.}
15.catch (BeansException ex) {
16.logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", 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() + "' configured successfully");
25.}
26.}

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

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

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

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

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

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

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

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

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

loadBeanDefinitions (DefaultListableBeanFactory)方法

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

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

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

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

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

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

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

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

z

这里解析了XML的信息,跟踪进去,会发现用了 NamespaceHandlerSupport 的parse方法,它会根据节点的类型,找到一种合适的解析BeanDefinitionParser(接口) ,他们预先被spring注册好了,放在一个HashMap中,例如我们在spring 的annotation扫描中,通常会配置:

1.<context:component-scan base-package="com.xxx" />

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

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

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

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

1.Resource[] resources = this.resourcePatternResolver.getResources(packageSearchPath);

它竟然神奇般的通过这个路径获取到了URL,你一旦跟踪你会发现,获取出来的全是.class的路径,包括jar包中的相关class路径,这里有些细节,我们先不说,先看下这个resourcePatternResolover是什么类型的,看到定义部分是:

1.private ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();

为此胖哥还将其做了一个测试,用一个简单main方法写了一段:

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

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

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

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

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是不是这样做的:

果然不出所料,它也是用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包,你要运行的话,切记放在某个根目录来跑,因为当前目录,就是根目录也会被递归下去,你的程序会被莫名奇怪地慢。

这里大家还可以通过以下简单的方式来测试调用路径的问题:

1.ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true);
2.Set<BeanDefinition> beanDefinitions = provider.findCandidateComponents("com/abc");
3.for(BeanDefinition beanDefinition : beanDefinitions) {
4.System.out.println(beanDefinition.getBeanClassName()
5."   " + beanDefinition.getResourceDescription()
6."   " + beanDefinition.getClass());
7.}

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

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

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

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

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

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

spring启动component-scan类扫描加载过程(转)的更多相关文章

  1. 【Spring源码分析系列】启动component-scan类扫描加载过程

    原文地址:http://blog.csdn.net/xieyuooo/article/details/9089441/ 在spring 3.0以上大家都一般会配置一个Servelet,如下所示: &l ...

  2. spring启动component-scan类扫描加载过程---源码分析

    http://blog.csdn.net/xieyuooo/article/details/9089441#comments

  3. Dubbo源码解析之SPI(一):扩展类的加载过程

    Dubbo是一款开源的.高性能且轻量级的Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用.智能容错和负载均衡,以及服务自动注册和发现. Dubbo最早是阿里公司内部的RPC框架,于 ...

  4. Java类的加载过程-重点!!

    java类的加载过程有以下几步共同完成: 加载->连接->初始化.连接又分为验证.准备.解析 一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以 ...

  5. 第42天学习打卡(Class类 Class类的常用方法 内存分析 类的加载过程 类加载器 反射操作泛型 反射操作注解)

    Class类 对象照镜子后得到的信息:某个类的属性.方法和构造器.某个类到底实现了哪些接口.对于每个类而言,JRE都为其保留一个不变的Class类型的对象.一个Class对象包含了特定某个结构(cla ...

  6. JAVA - 类的加载过程

    JAVA - 类的加载过程 JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化. 加载 加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的java.lang.Class对象 ...

  7. JVM——类的加载过程

    附一张图方便理解,一个类的执行过程 类的加载过程,简明的来说 类装饰器就是寻找类的字节码文件并构造出类在JVM内部表示的对象组件.在Java中,类装载器把一个类装入JVM中,要经过以下步骤: 装载:查 ...

  8. java 反射,类的加载过程以及Classloader类加载器

    首先自定义一个类Person package reflection; public class Person { private String name; public int age; public ...

  9. 在Spring Boot中从类路径加载文件

    介绍 创建Spring Boot Web应用程序时,有时有时需要从类路径中加载文件:war和jar的加载文件格式是不一样的 在下面,您将找到在WAR和JAR中加载文件的解决方案. 资源加载器 使用Ja ...

随机推荐

  1. Chapter 4 Invitations——11

    "Bella?" His voice shouldn't have been so familiar to me, as if I'd known the sound of it ...

  2. ES6躬行记(12)——数组

    ES6为数组添加了多个新方法,既对它的功能进行了强化,也消除了容易产生歧义的语法. 一.静态方法 1)of() ES6为Array对象新增的第一个静态方法是of(),用于创建数组,它能接收任意个参数, ...

  3. .NetCore2.1 WebAPI 根据swagger.json自动生成客户端代码

    前言 上一篇博客中我们可以得知通过Swagger插件可以很方便的提供给接口开发者在线调试,但是实际上Swagger附带的功能还有很多, 比如使用NSwag生成客户端调用代码,进一步解放接口开发者. N ...

  4. 多种Timer的场景应用

    前言 今天讲讲各种Timer的使用. 三种Timer组件 .Net框架提供了三种常规Timer组件,分别是System.Windows.Forms.Timer.System.Timers.Timer和 ...

  5. 【.NET Core项目实战-统一认证平台】第十章 授权篇-客户端授权

    [.NET Core项目实战-统一认证平台]开篇及目录索引 上篇文章介绍了如何使用Dapper持久化IdentityServer4(以下简称ids4)的信息,并实现了sqlserver和mysql两种 ...

  6. Tomcat配置文件Executor元素属性介绍

    该元素可用于Tomcat 6.0.11以及更高版本. 允许您为一个Service的所有Connector配置一个共享线程池.在运行多个Connector的状况下,这样处理非常有用,而且每个Connec ...

  7. Java开发笔记(三十九)日期工具Date

    Date是Java最早的日期工具,编程中经常通过它来获取系统的当前时间.当然使用Date也很简单,只要一个new关键字就能创建日期实例,就像以下代码示范的那样: // 创建一个新的日期实例,默认保存的 ...

  8. java8 Stream操作

    Stream操作详解:https://www.ibm.com/developerworks/cn/java/j-lo-java8streamapi/#icomments

  9. mybatis基础(中)

    数据模型分析思路 每张表记录的数据内容 分模块对每张表记录对内容进行熟悉,相当于学习系统需求(功能)的过程 每张表重要的字段设置 非空字段.外键字段 数据库级别表与表之间的关系 外键关系 表与表之间的 ...

  10. 我从来不理解JavaScript闭包,直到有人这样向我解释它...

    摘要: 理解JS闭包. 原文:我从来不理解JavaScript闭包,直到有人这样向我解释它... 作者:前端小智 Fundebug经授权转载,版权归原作者所有. 正如标题所述,JavaScript闭包 ...