Spring component-scan类扫描加载过程

 https://github.com/javahongxihtml

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

 

要看这部分源码,其实在spring 3.0以上你们都通常会配置一个Servelet,以下所示:git

 

[html]  view plain  copy
 
  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()方法,那么天然是调用DispatcherServletinit方法,经过源码一看,居然没有,可是并不带表真的没有,你会发如今父类的父类中:org.springframework.web.servlet.HttpServletBean有这个方法,以下图所示:github

 

 

[java]  view plain  copy
 
  1.     public final void init() throws ServletException {  
  2.     if (logger.isDebugEnabled()) {  
  3.         logger.debug("Initializing servlet '" + getServletName() + "'");  
  4.     }  
  5.   
  6.     // Set bean properties from init parameters.  
  7.     try {  
  8.         PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);  
  9.         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,源码片断(篇幅所限,不拷贝全部源码,仅仅截取片断)web

 


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


这里的parent基本不用管,由于Root的ApplicationContext的信息还根本没建立,因此主要是看createWebApplicationContext这个方法,进去后,该方法前面部分,都是在设置一些相关的参数,例如咱们须要将WEB容器、以及容器的配置信息设置进去,而后会调用一个refresh()方法,这个方法表面上是用来刷新的,其实也是用来作初始化bean用的,也就是配置修改后,若是你能调用它的这个方法,就能够从新装载spring的信息,咱们看看源码中的片断以下(一样,不相关的部分,咱们就不贴太多了):数组


其实这个方法,不管是经过ClassPathXmlApplicationContext仍是WEB装载都会调用这里,咱们看下ClassPathXmlApplicationContext中调用的部分:tomcat


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

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扫描中,一般会配置:

 

[html]  view plain  copy
 
  1. <context:component-scan base-package="com.xxx" />  

 

 

此时根据名称“component-scan”就会找到对应的解析器来解析,而与之对应的就是ComponentScanBeanDefinitionParserparse方法,这地方已经很明显有扫描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究竟是怎么搞的呢?就要看第二个红线部分的代码:

 

[java]  view plain  copy
 
  1. Resource[] resources = this.resourcePatternResolver.getResources(packageSearchPath);  

 

 

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

 

[java]  view plain  copy
 
  1. private ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();  

 

 

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

 

[java]  view plain  copy
 
  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() 的实现中是这样写的:

 

[java]  view plain  copy
 
  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包,你要运行的话,切记放在某个根目录来跑,由于当前目录,就是根目录也会被递归下去,你的程序会被莫名奇怪地慢。

回到上面的代码中,在findPathMatchingResources中咱们这里刚刚获取到base的路径列表,也就是全部包含相似com/abc/为前缀的路径,或classpath合并后的目录根路径;此时咱们须要下面全部的class,那么就须要的是递归,这里我就再也不跟踪了,你们能够本身去跟踪里面的几个方法调用:doFindPathMatchingJarResources、doFindPathMatchingFileResources 。

几乎不会用到:VfsResourceMatchingDelegate.findMatchingResources,因此主要是上面两个,分别是jar包中的和工程里面的class,跟踪进去会发现,代码会不断递归循环调用目录路径下的class文件的路径信息,最终会拿到相关的class列表信息,可是这些class还并无作检测是否有annotation,那是下一步作的事情,可是下一个步骤已经很简单了,由于要检测一个类的annotation,在前面的文章中:《 java之annotation与框架的那些秘密》中已经提到了。

 

这里你们还能够经过如下简单的方式来测试调用路径的问题:

 

[java]  view plain  copy
 
  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.                     + "\t" + beanDefinition.getResourceDescription()  
  6.                     + "\t" + beanDefinition.getClass());  
  7. }  

 

 

这是直接引用spring的源码部分的内容,若是这里能够获取到,且路径是正确的,通常状况下,都是能够加载到类的。

 

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

相关文章
相关标签/搜索