转载

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

Spring 相关代码分析

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

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

@SpringBootApplication
@ComponentScan(basePackages = "cc.unmi")
public class DemoApplication implements EnvironmentAware {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

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

public ClassPathScanningCandidateComponentProvider(boolean useDefaultFilters, Environment environment)

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

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

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

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

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

它的注册 Filter 的方法

protected void registerDefaultFilters() {
    this.includeFilters.add(new AnnotationTypeFilter(Component.class));
    ClassLoader cl = ClassPathScanningCandidateComponentProvider.class.getClassLoader();
    try {
        this.includeFilters.add(new AnnotationTypeFilter(
                ((Class<? extends Annotation>) ClassUtils.forName("javax.annotation.ManagedBean", cl)), false));
        logger.debug("JSR-250 'javax.annotation.ManagedBean' found and supported for component scanning");
    }
    catch (ClassNotFoundException ex) {
        // JSR-250 1.1 API (as included in Java EE 6) not available - simply skip.
    }
    try {
        this.includeFilters.add(new AnnotationTypeFilter(
                ((Class<? extends Annotation>) ClassUtils.forName("javax.inject.Named", cl)), false));
        logger.debug("JSR-330 'javax.inject.Named' annotation found and supported for component scanning");
    }
    catch (ClassNotFoundException ex) {
        // JSR-330 API not available - simply skip.
    }
}

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

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

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

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

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

方法一,直接使用 PathMatchingResourcePatternResolver

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

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

自已的放在 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 就行。请看下方的例子

@Named
public class UserService implements ApplicationContextAware {
 
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        try {
            Resource[] resources = applicationContext.getResources("classpath*:**/springframework/**/*.xml");
            Stream.of(resources).forEach(resource -> System.out::println);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

输出为

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)

那么相应的代码就是

@Named
public class UserService implements ResourceLoaderAware {
 
    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        ResourcePatternResolver resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader);
        try {
            Resource[] resources = resourcePatternResolver.getResources("classpath*:**/springframework/**/*.xml");
            Stream.of(resources).forEach(System.out::println);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

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

原文  https://unmi.cc/how-spring-scan-beans-resources/
正文到此结束
Loading...