Spring源码阅读(一):Spring是如何扫描某个包下面所有的类

第一篇写在前面

  • 为什么写源码阅读笔记

随着工作的年份越来越长,我越来越觉得我对程序员那种最初的热爱和兴趣越来越淡了,更多的是凭借着经验为了工作而工作。我最近一直在思考,我的追求是什么,一个技术专家? 还是一个方案专家?可能有人要反驳我说不懂设计的程序员说最低级别的程序员,程序员的发展目标应该是架构师。我不这么认为,架构师有架构师的牛逼,程序员应该也有程序员的使命。我就愿意做一个极客程序员。

好了,这就是我的初衷,要做一个极客程序员,那就要掌握每一个技术细节。掌握技术细节最好的方式就是阅读源码。刚入职的时候,一个有着丰富经验的老员工就劝戒我,平常要多读读开源软件的源码,开源软件里有很多编程思想都是值得借鉴的。最初的几年还可以,越到后面越感觉自己“浮于表面”。看源码的原因仅限于为了解决恰好遇到的问题。

记得有人说过怎么验证你完全掌握了一个知识呢,那就是讲给别人听,你能把别人讲明白,就代表你真的掌握了,而且你在讲的过程中也会发现问题反问自己。写笔记的目的就是验证自己真的搞清楚了。

  • 阅读习惯说明

每个人阅读源码的习惯不一样,有的人喜欢先把一款软件大体框架结构搞清楚,然后分块的去阅读。有的人喜欢先找到程序的主入口,一点点的往下调试。我的阅读习惯是找重点,找出这个软件有哪些核心的功能,比如Spring如何对xml的解析,Spring对事务的抽象,Spring如何对注解的解析等等这样独立的功能,通过阅读源码理清它们的原理,最后汇总起来,基本上对整个框架也全面掌握了。

  • 为什么选择Spring

Spring从最初的Spring Framework到Spring Boot到Spring Cloud,其实已经成为了Java 开发行业的标准,Spring应该是当前Java开发的首选,Spring全家桶可以说包含了绝大部分Java编程方面的知识,Spring里的知识丰富浩繁,值得深入阅读学习。后面同时还会有很多开源软件的穿插学习。

Spring扫描注解的功能

我们知道在Spring中可以使用注解声明Bean,可以让我们不用再去配置繁琐的xml文件,确实让我们的开发简便不少。只要在Spring xml里配置如下,就开启了这项功能。

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

Spring就会自动扫描该包下面所有打了Spring注解的类,帮你初始化病注册到Spring容器中。

@Service
public class UserController {

    @Autowired
    private UserDao userDao;

    //TODO
}

上述行为,就和在xml里进行下面的配置是等价的。

<bean class="com.zhaoyanblog.UserController">
    <property name="userDao" ref="userDao" />
</bean>

那么问题来了,Spring是怎么扫描到com.zhaoyanblog包下面的所有带注解的类的呢?

context:component-scan标签的处理者

要弄清楚为什么在xml里配置了context:component-scan就可以实现这样的功能,就要现找到这个标签的处理者。我们很容易联想到Spring解析xml的基本原理,就是遇到这个标签以后,交给一个类处理,这个类扫描包下带注解的类,初始化成对象。我们就是要找到这个关键的类。

Spring对Xml的解析功能后面阅读,这里先简要描述。Spring的jar包里有两个重要的配置文件:spring.schemas和spring.handlers,在META-INF目录下。

spring.schemas记录了xml的每个命名空间,对应的Schema校验XSD文件在哪个目录。

http\://www.springframework.org/schema/context/spring-context-4.2.xsd=org/springframework/context/config/spring-context.xsd
http\://www.springframework.org/schema/context/spring-context-4.3.xsd=org/springframework/context/config/spring-context.xsd
http\://www.springframework.org/schema/context/spring-context.xsd=org/springframework/context/config/spring-context.xsd

spring.handlers里配置了每个命名空间的标签都由哪个类处理

http\://www.springframework.org/schema/context=org.springframework.context.config.ContextNamespaceHandler
http\://www.springframework.org/schema/jee=org.springframework.ejb.config.JeeNamespaceHandler
http\://www.springframework.org/schema/lang=org.springframework.scripting.config.LangNamespaceHandler
http\://www.springframework.org/schema/task=org.springframework.scheduling.config.TaskNamespaceHandler
http\://www.springframework.org/schema/cache=org.springframework.cache.config.CacheNamespaceHandler

我们可以看到context这个命名空间由org.springframework.context.config.ContextNamespaceHandler这个类处理。打开这个类

public class ContextNamespaceHandler extends NamespaceHandlerSupport {

   @Override
   public void init() {
      registerBeanDefinitionParser("property-placeholder", new PropertyPlaceholderBeanDefinitionParser());
      registerBeanDefinitionParser("property-override", new PropertyOverrideBeanDefinitionParser());
      registerBeanDefinitionParser("annotation-config", new AnnotationConfigBeanDefinitionParser());
      registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser());
      registerBeanDefinitionParser("load-time-weaver", new LoadTimeWeaverBeanDefinitionParser());
      registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser());
      registerBeanDefinitionParser("mbean-export", new MBeanExportBeanDefinitionParser());
      registerBeanDefinitionParser("mbean-server", new MBeanServerBeanDefinitionParser());
   }

}

这个类为每个标签都给出了一个处理类,component-scan的处理类是ComponentScanBeanDefinitionParser

继续打开ComponentScanBeanDefinitionParser

private static final String BASE_PACKAGE_ATTRIBUTE = "base-package";
public BeanDefinition parse(Element element, ParserContext parserContext) {
        String basePackage = element.getAttribute(BASE_PACKAGE_ATTRIBUTE);
        basePackage = parserContext.getReaderContext().getEnvironment().resolvePlaceholders(basePackage);
        String[] basePackages = StringUtils.tokenizeToStringArray(basePackage,
                ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);

        // Actually scan for bean definitions and register them.
        ClassPathBeanDefinitionScanner scanner = configureScanner(parserContext, element);
        Set<BeanDefinitionHolder> beanDefinitions = scanner.doScan(basePackages);
        registerComponents(parserContext.getReaderContext(), beanDefinitions, element);

        return null;
    }

看到这里整明白了,扫描某个包下面的所有类的工作,就是ClassPathBeanDefinitionScanner干的。入口是doScan方法。

查找所有的包路径

ClassPathBeanDefinitionScanner有很多细节,比如可以设置class的filter, 设置classloader等等,我们先关注最主要的功能,就是怎么找到一个包下面所有的类的。

通过调用关系,一路找下去

doScan->findCandidateComponents(super)->scanCandidateComponents(super)

private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
   Set<BeanDefinition> candidates = new LinkedHashSet<>();
   try {
      String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
            resolveBasePackage(basePackage) + '/' + this.resourcePattern;
      Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);

我们发现,原来Spring是在classpath下面,通过查找所有classpath*:com/zhaoyanblog/**/*.class的文件来实现的啊。

那这个问题就变了,Spring是如何在Classpath下面查找满足classpath*:com/zhaoyanblog/**/*.class 匹配条件的文件的呢?

这个就需要看类PathMatchingResourcePatternResolver的源码了。入口getResources方法。

这里需要讲个特别知识点:classpath*: 前缀表达式,用于描述类路径匹配。*表示所有classpath,不带*表示查到的第一个类路径。后面的匹配表达式有个学名: “Ant风格路径表达式”(Ant-style patterns)。这个表达式很简单,可以自行google一下。 这里我们这里只分析下

Spring是如何查找 classpath*:com/zhaoyanblog/**/*.class 这个表达式的,其它以此类推。

PathMatchingResourcePatternResolver代码一路读下去

getResources->findPathMatchingResources
protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
   String rootDirPath = determineRootDir(locationPattern);
   String subPattern = locationPattern.substring(rootDirPath.length());
   Resource[] rootDirResources = getResources(rootDirPath);

看到Spring会把路径分成两部分,一部分是rootDir=classpath*:com/zhaoyanblog/,一部分是subPatter=**/*.class

查找rootDir,又递归getResources,你会发现这次走的是下述分支

getResources->findAllClassPathResources->doFindAllClassPathResources

过程代码很简单,通过classloader.getResources(“com/zhaoyanblog/”) 找到了所以的com.zhaoyanblog包路径。

查找包路径下的类

既然找到classpath的所有com/zhaoyanblog/路径,怎么列举下面的class文件呢?

一个classpath路径,可能是jar包,也可能是个磁盘目录,还有可能是其它形式。

没错,Spring就是这样勤劳的递归遍历每种类型的路径,直到找遍所有满足**/*.class条件的文件。

if (equinoxResolveMethod != null && rootDirUrl.getProtocol().startsWith("bundle")) {
   URL resolvedUrl = (URL) ReflectionUtils.invokeMethod(equinoxResolveMethod, null, rootDirUrl);
   if (resolvedUrl != null) {
      rootDirUrl = resolvedUrl;
   }
   rootDirResource = new UrlResource(rootDirUrl);
}
if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
   result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher()));
}
else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) {
   result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPattern));
}
else {
   result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));
}

包括osgi的boundle文件路径、jar包文件路径、vfs虚拟文件路径以及最后是普通文件路径。

读取类文件

找到类文件 假设是你,你会怎么办?

识别文件名,根据文件路径,得到类的完整名称:com.zhaoyanblog.UserController

然后Class.forName(“com.zhaoyanblog.UserController”) ,得到class对象是吗?

显然这样做是不对的,也是不道德的。你怎么敢确认这个类就是有Spring注解的类呢?假设这个类没有Spring注解,那么当你执行了Class.forName, 这个类就会被classloader加载,它的静态属性,静态代码段就会被执行。这个后果很有可能不是Spring使用者想看到的。

那怎么办呢?

我们回到ClassPathBeanDefinitionScanner类的最开始的地方

doScan->findCandidateComponents(super)->scanCandidateComponents(super)
private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
      ...
      for (Resource resource : resources) {
      ...
         if (resource.isReadable()) {
            try {
               MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
               if (isCandidateComponent(metadataReader)) {
                  ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
                  sbd.setResource(resource);
                  sbd.setSource(resource);

在得到每一个类的resource文件路径以后,会使用一个MetadataReader读取它的元数据信息。

没错,Spring就是自己解析了class文件,主要读取了它的类名,注解信息。

阅读代码路径

CachingMetadataReaderFactory(SimpleMetadataReaderFactory)->SimpleMetadataReader->ClassReader

最终就是一个字节一个字节的解析class文件。至此“Spring扫描注解”的功能代码阅读完毕。

题外小知识点

  • PathMatchingResourcePatternResolver查找classpath*.*xml 和查找classpath*.spring/*xml 的过程是不同的。 在Spring 官方文档里,你会看到这样一句这样的警告:

  Note that `classpath*:`, when combined with Ant-style patterns, only works reliably with at least one root directory before the pattern starts, unless the actual target files reside in the file system. This means that a pattern such as `classpath*:*.xml` might not retrieve files from the root of jar files but rather only from the root of expanded directories.

  Spring’s ability to retrieve classpath entries originates from the JDK’s `ClassLoader.getResources()` method, which only returns file system locations for an empty string (indicating potential roots to search). Spring evaluates `URLClassLoader` runtime configuration and the `java.class.path` manifest in jar files as well, but this is not guaranteed to lead to portable behavior.

具体是说Spring查找classpath下的文件,依赖于JDK的ClassLoader.getResources,但是如果你写成类似classpath*:*.xml的路径,那么Spring就会先去查询ClassLoader.getResources(“”). 但是JDK的该方法对于“”路径,只会返回一个(你可以试一下哟~)。所以你查找跟路径的文件,有可能查不全。当然Spring也做了一点努力,就是把classpath下面的所以jar包都进行了搜索,但是还是保不齐能搜全。所以你想通过扫描类路径加载文件,最好不要放在跟路径下面。

具体你可以看下面类和方法对“”的处理

org.springframework.core.io.support.PathMatchingResourcePatternResolver#doFindAllClassPathResources
  • @Repository @Service @Component 等注解都可以注册一个Bean,有啥区别呢?

看下这些注解的java doc, 比如@Service

* Indicates that an annotated class is a "Service", originally defined by Domain-Driven
* Design (Evans, 2003) as "an operation offered as an interface that stands alone in the
* model, with no encapsulated state."

我靠,这个概念来自2003年Evans的大作《领域驱动设计(DDD)》, 又是一个新的知识领域。后面需要学习。

也就是说这些注解本质上基本没有区别,仅是用于标识该类在DDD模型中的位置。

Spring官方的解释是,准确的使用这些注解,可以让一些开发工具更好的分析你的代码,比如我们最好的开发工具IDEA等。Spring未来很有可能对不同的注解带来不同的附加含义。

比如@Repository,就已经被特殊处理了,Spring会认为@Repository属于持久层的类,针对它的方法抛出的异常,会统一转换成Spring 的数据库异常类DataAccessException

可以阅读PersistenceExceptionTranslationPostProcessor类。

留言

提示:你的email不会被公布,欢迎留言^_^

*

验证码 *