本文通过对 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)
从该截图中可看到从 AnnotationConfigApplicationContext 是如何到达 ClassPathScanningCandidateComponentProvider
的。不妨初步打量一下这个类
我们可以看到 DEFAULT_RESOURCE_PATTERN = "**/*.class
, 比如前面设置的 @ComponentScan(basePackages = "cc.unmi")
, 会转换为搜索模式 classpath*:cc/unmi/**/*.class"
.
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
及其子类型
最后干实事的方法就是那个 this.resourcePatternResolver.getResources(packageSearchPath)
, 来看下它给我们带来了什么
它返回的是 Resource 列表,有了 Resource 我们就可以读取资源的内容,如果是类的话或以转换为类的全路径加载它。而后 Spring 会通过前面的过滤条件(@Component 或 @Named) 把相应的 Bean 注册到 Spring 上下文中。
至此,我们了解到 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 有 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
, 这是不对的,会什么也找不到。
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)
方法让代码更简法些,看看这个方法的源代码就清楚了。