13

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

 4 years ago
source link: https://zhaoyanblog.com/archives/1080.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

第一篇写在前面

  • 为什么写源码阅读笔记

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

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

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

  • 阅读习惯说明

每个人阅读源码的习惯不一样,有的人喜欢先把一款软件大体框架结构搞清楚,然后分块的去阅读。有的人喜欢先找到程序的主入口,一点点的往下调试。我的阅读习惯是找重点,找出这个软件有哪些核心的功能,比如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类。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK