记录一下 Spring 如何扫描注解的 Bean 与资源

Spring 相关代码分析

本文通过对 Spring 的源代码来理解它是如何扫描 Bean 与资源的,因为自己有一个类似的需求,想把一堆的配置文件丢到 resources 下某个目录中,在程序启动的时候能加载它们。因为文件名是不一定的,所以不能直接指定文件名来加载,通过对 Spring 扫描资源的理解后,可以在自己的代码中手工扫描那些配置文件,以后有任何新的配置文件只需要扔到相应的配置目录即可。

下面以一个最简单的 Spring Boot 项目为例,调试并观察源代码

还是直奔主题吧,不一步一步的去探寻到底是哪个实现类去扫描资源的,用 Google 找到的是 ClassPathScanningCandidateComponentProvider, 因此直接在这个类的敏感位置上打上断点,比如它的构造函数

public ClassPathScanningCandidateComponentProvider(boolean useDefaultFilters, Environment environment) 

从该截图中可看到从 AnnotationConfigApplicationContext 是如何到达 ClassPathScanningCandidateComponentProvider 的。不妨初步打量一下这个类

我们可以看到 DEFAULT_RESOURCE_PATTERN = "**/*.class, 比如前面设置的 @ComponentScan(basePackages = "cc.unmi"), 会转换为搜索模式 classpath*:cc/unmi/**/*.class".

resourcePatternResolver 的实现类是 PathMatchingResourcePatternResolver, 所使用的匹配模式是 AntPathMatcher.

它的注册 Filter 的方法

说明了它会扫描 @Component 及它的子类型注解的类; 支持用 JSR-250 的 javax.annotation.ManagedBean 注解的类; 支持用 javax.inject.Named 注解的类。下图是 @Component 及其子类型

最后干实事的方法就是那个 this.resourcePatternResolver.getResources(packageSearchPath), 来看下它给我们带来了什么

它返回的是 Resource 列表,有了 Resource 我们就可以读取资源的内容,如果是类的话或以转换为类的全路径加载它。而后 Spring 会通过前面的过滤条件(@Component 或  @Named) 把相应的 Bean 注册到 Spring 上下文中。

方法一,直接使用 PathMatchingResourcePatternResolver

至此,我们了解到 Spring 如何加载 JavaBean, 那么资源文件是怎么加载的呢?可以用同样的办法,还是在当前断点的位置上,换个 Pattern 来试试。比如看下 Spring Boot 在 classpath 下给我们提供了什么 xml 资源,用 this.resourcePatternResolver.getResources("classpath*:**/springframework/**/*.xml")

自已的放在 resources 目录或子目录的资源文件也可以这么加载。

或者在任何时候我们都可以直接使用

new PathMatchingResourcePatternResolver().getResources("classpath*:**/springframework/**/*.xml")

获得与上图同样的结果。

现在我们假设需求是在 reources/abc 中可以旋转任意的配置文件 1.json, 2.json, 3.json ...., 那么要加载它们只需用

new PathMatchingResourcePatternResolver().getResources("classpath*:abc/*.json")

在 Spring 项目中全权交给 Spring 自己就能搞定了。

方法二:使用 ApplicationContext

等着,还没完, 其实我们是绕了一个大大的弯又回来了,早发现 ApplicationContext 有 getResources(String)/getResource(String) 方法便无需看那么多源代码,也就根本不会有本文的出现。巡着 PathMatchingResourcePatternResolver, 找到它的一个父接口 ResourcePatternResolver,其中定义了

Resource[] getResources(java.lang.String locationPattern)

方法,并且发现它有一个子接口竟然是 ApplicationContext, 多么熟悉的味道。事情就变得简单多了,想要获得 ApplicationContext 还不容易, 任意一个 SpringBean 实现 ApplicationContextAware 就行。请看下方的例子

输出为

URL [jar:file:/Users/Yanbin/.m2/repository/org/springframework/boot/spring-boot/1.4.2.RELEASE/spring-boot-1.4.2.RELEASE.jar!/org/springframework/boot/context/embedded/tomcat/empty-web.xml]
URL [jar:file:/Users/Yanbin/.m2/repository/org/springframework/boot/spring-boot/1.4.2.RELEASE/spring-boot-1.4.2.RELEASE.jar!/org/springframework/boot/logging/logback/console-appender.xml]
URL [jar:file:/Users/Yanbin/.m2/repository/org/springframework/boot/spring-boot/1.4.2.RELEASE/spring-boot-1.4.2.RELEASE.jar!/org/springframework/boot/logging/logback/defaults.xml]
URL [jar:file:/Users/Yanbin/.m2/repository/org/springframework/boot/spring-boot/1.4.2.RELEASE/spring-boot-1.4.2.RELEASE.jar!/org/springframework/boot/logging/logback/file-appender.xml]
URL [jar:file:/Users/Yanbin/.m2/repository/org/springframework/boot/spring-boot/1.4.2.RELEASE/spring-boot-1.4.2.RELEASE.jar!/org/springframework/boot/logging/logback/base.xml]
URL [jar:file:/Users/Yanbin/.m2/repository/org/springframework/boot/spring-boot/1.4.2.RELEASE/spring-boot-1.4.2.RELEASE.jar!/org/springframework/boot/logging/log4j2/log4j2-file.xml]
URL [jar:file:/Users/Yanbin/.m2/repository/org/springframework/boot/spring-boot/1.4.2.RELEASE/spring-boot-1.4.2.RELEASE.jar!/org/springframework/boot/logging/log4j2/log4j2.xml]
URL [jar:file:/Users/Yanbin/.m2/repository/org/springframework/spring-context/4.3.4.RELEASE/spring-context-4.3.4.RELEASE.jar!/org/springframework/remoting/rmi/RmiInvocationWrapperRTD.xml]

注意前面那个 Pattern 还真不能省略 classpath 后那个星号直接写成 classpath:**/springframework/**/*.xml, 这是不对的,会什么也找不到。

方法三:使用 ResourceLoader

PathMatchingResourcePatternResolver 还有一个父接口 ResourceLoader, 它有相应的 ResourceLoaderAware, 也就是说在 SpringBean 中可以方便的注入 ResourceLoader. ResourceLoader 接口只定义了一个方法

Resource getResource(java.lang.String location)

它似乎无法满足我们批量加载资源的要求,但是当我们读到 ResourceLoaderAware 的接口定义方法

void setResourceLoader(ResourceLoader resourceLoader)

其中有注释

This might be a ResourcePatternResolver, which can be checked through instanceof ResourcePatternResolver. See also the ResourcePatternUtils.getResourcePatternResolver method.

说它极可能是一个 ResourcePatternResolver, 再不济还能用 ResourcePatternUtils.getResourcePatternResolver(resourceLoader) 获得相应的 ResourcePatternResolver. 这样的话就可以调用

Resource[] getResources(java.lang.String locationPattern)

那么相应的代码就是

其实上面 setResourceLoader(ResourceLoader resourceLoader) 的参数在 Spring Boot 中是 AnnotationConfigApplicationContext, 它当然是一个 ResourcePatternResolver, 使用 ResourcePatternUtils.getResourcePatternResolver(resourceLoader) 方法让代码更简法些,看看这个方法的源代码就清楚了。

相关链接:

  1. Classpath scanning, managed components and writing configurations using Java

本文链接 https://yanbin.blog/how-spring-scan-beans-resources/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments