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

Spring 相关代码分析

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

下面以一个最简单的 Spring Boot 项目为例,调试并观察源代码
1@SpringBootApplication
2@ComponentScan(basePackages = "cc.unmi")
3public class DemoApplication {
4    public static void main(String[] args) {
5        SpringApplication.run(DemoApplication.class, args);
6    }
7}

还是直奔主题吧,不一步一步的去探寻到底是哪个实现类去扫描资源的,用 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 的方法
 1protected void registerDefaultFilters() {
 2    this.includeFilters.add(new AnnotationTypeFilter(Component.class));
 3    ClassLoader cl = ClassPathScanningCandidateComponentProvider.class.getClassLoader();
 4    try {
 5        this.includeFilters.add(new AnnotationTypeFilter(
 6                ((Class<? extends Annotation>) ClassUtils.forName("javax.annotation.ManagedBean", cl)), false));
 7        logger.debug("JSR-250 'javax.annotation.ManagedBean' found and supported for component scanning");
 8    }
 9    catch (ClassNotFoundException ex) {
10        // JSR-250 1.1 API (as included in Java EE 6) not available - simply skip.
11    }
12    try {
13        this.includeFilters.add(new AnnotationTypeFilter(
14                ((Class<? extends Annotation>) ClassUtils.forName("javax.inject.Named", cl)), false));
15        logger.debug("JSR-330 'javax.inject.Named' annotation found and supported for component scanning");
16    }
17    catch (ClassNotFoundException ex) {
18        // JSR-330 API not available - simply skip.
19    }
20}

说明了它会扫描 @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

w 等着,还没完, 其实我们是绕了一个大大的弯又回来了,早发现 ApplicationContext 有 getResources(String)/getResource(String) 方法便无需看那么多源代码,也就根本不会有本文的出现。 巡着 PathMatchingResourcePatternResolver, 找到它的一个父接口 ResourcePatternResolver,其中定义了
Resource[] getResources(java.lang.String locationPattern)
方法,并且发现它有一个子接口竟然是 ApplicationContext, 多么熟悉的味道。事情就变得简单多了,想要获得 ApplicationContext 还不容易, 任意一个 SpringBean 实现 ApplicationContextAware 就行。请看下方的例子
 1@Named
 2public class UserService implements ApplicationContextAware {
 3
 4    @Override
 5    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
 6        try {
 7            Resource[] resources = applicationContext.getResources("classpath*:**/springframework/**/*.xml");
 8            Stream.of(resources).forEach(resource -> System.out::println);
 9        } catch (IOException e) {
10            e.printStackTrace();
11        }
12    }
13}

输出为
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)
那么相应的代码就是
 1@Named
 2public class UserService implements ResourceLoaderAware {
 3
 4    @Override
 5    public void setResourceLoader(ResourceLoader resourceLoader) {
 6        ResourcePatternResolver resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader);
 7        try {
 8            Resource[] resources = resourcePatternResolver.getResources("classpath*:**/springframework/**/*.xml");
 9            Stream.of(resources).forEach(System.out::println);
10        } catch (IOException e) {
11            e.printStackTrace();
12        }
13    }
14}

其实上面 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's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。