问题描述
遇到一个关于资源加载的问题,因此简单的记录一下,对Spring
资源加载也做一个记录。
问题起因是使用了@PropertySource
来进行配置文件加载,配置路径时,没有使用关键字classpath
来指明从classpath
下面来查找配置文件。具体配置如下
@PropertySource("config/application-download.yml", factory=YamlPropertySourceFactory)
这种方式在启动应用时,是没问题的,正常。但是在build时,跑单元测试,出了问题,说无法从ServletContext
中找到/config/application-download.yml
,然后加上了classpath
,再跑了下就没错误了。
于是找到了处理@PropertySource
的位置,跟踪代码找到了差异的原因。
源码解释
Spring
对于资源,做了一个抽象,那就是Resource
,资源的加载使用资源加载器来进行加载,ResourceLoader
就是这样一个接口,用于定义对资源的加载行为的。
Spring
中几乎所有的ApplicationContext
都实现了它,应用十分的广泛。
除了各个ApplicationContext
实现了它,它还有个可以独立使用的实现,也就是一会要提到的。
DefaultResourceLoader
这个实现类,是一个在框架外部独立使用版本,一般默认的都不简单 ,这个也不例外。
无论从哪里加载资源,使用DefaultResourceLoader
来加载就行了
// org.springframework.core.io.DefaultResourceLoader#getResource
@Override
public Resource getResource(String location) {
Assert.notNull(location, "Location must not be null");
// 这个是提供的SPI使用的,没有采用子类实现的方式
for (ProtocolResolver protocolResolver : getProtocolResolvers()) {
Resource resource = protocolResolver.resolve(location, this);
if (resource != null) {
return resource;
}
}
// 如果以/开头,使用纯路径的方式,比如./config.properties
if (location.startsWith("/")) {
return getResourceByPath(location);
}
// 如果以classpath:开头,创建一个ClassPathResource资源对象
// 底层使用的是Class#getResourceAsStream,ClassLoader#getResourceAsStream
// 或者 ClassLoader#getSystemResourceAsStream,具体有机会再详细解释下这些
else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
}
else {
try {
// Try to parse the location as a URL...
// 如果上面的判断不满足,直接使用java.net.URL来生成一个URL对象,
// 如果location为null,或者location没有指定协议,或者协议不能被识别
// 就会抛出异常
URL url = new URL(location);
//file:开头的 会使用创建一个FileUrlResource
return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
}
catch (MalformedURLException ex) {
// 没有指定协议
return getResourceByPath(location);
}
}
}
protected Resource getResourceByPath(String path) {
// ClassPathResource的子类
return new ClassPathContextResource(path, getClassLoader());
}
这个类在Spring
中被广泛使用,或者更具体的说,这个类的getResource
方法,几乎遇到资源相关的加载动作都会调用到它。
各个ApplicationContext
应该是加载资源最多的地方了,而AbstractApplicationContext
正是继承了DefaultResourceLoader
,才有了这中加载资源的能力。
不过DefaultResourceLoader
也留给了子类的扩展点,主要是通过重写getResourceByPath
这个方法。这里是继承的方式,也可以重写 getResource
方法,这个方法在GenericApplicationContext
中被重写了, 不过也没有做过多的操作,这里主要是可以在一个context
中设置自己的资源加载器,一旦设置了,会将 ApplicationContext
中所有的资源委托给它加载,一般不会有这个操作 。
遇到的问题 ,正是因为子类对 getResourceByPath
的重写 ,导致了不一样的行为。
经过跟踪源码发现,正常启动应用的时候,实例化的是一个 AnnotationConfigServletWebServerApplicationContext
实例 ,这个类继承自ServletWebServerApplicationContext
,在ServletWebServerApplicationContext
中重写了getResourceByPath
// org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#getResourceByPath
@Override
protected Resource getResourceByPath(String path) {
if (getServletContext() == null) {
// ServletContext为null,从classpath去查找
return new ClassPathContextResource(path, getClassLoader());
}
// 否则从ServletContext去查找
return new ServletContextResource(getServletContext(), path);
}
而通过 Debug
发现,在使用SpirngBootTest
执行单元测试,它实例化的是org.springframework.web.context.support.GenericWebApplicationContext
/**
* This implementation supports file paths beneath the root of the ServletContext.
* @see ServletContextResource
* 这里就是直接从ServletContext中去查找资源,一般就是webapp目录下。
*/
@Override
protected Resource getResourceByPath(String path) {
Assert.state(this.servletContext != null, "No ServletContext available");
return new ServletContextResource(this.servletContext, path);
}
并且这里ServletContext
不为null
,SpringBootTest
实例化一个SpringBootMockServletContext
对象。
而正常情况下,在处理@PropertySource
时,还没能初始化一个ServletContext
,因为 @PropertySource
的处理是在BeanDefinitionRegistryPostProcessor
执行时处理的,早于SpringBoot
去初始化Servlet
容器。SpringBoot创建Servlet容器是在这里org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#onRefresh
,它的执行时机是晚于处理 BeanFactoryPostProcessor
的org.springframework.context.support.AbstractApplicationContext#invokeBeanFactoryPostProcessors
,所以 正常运行应用,肯定只会创建一个ClassPathContextResource
资源对象,而配置文件在classpath
下是存在的,所以可以搜索到。
结论
结论就是不知道SpringBootTest
是故意为之呢还是出于什么别的考虑,也不知道除了加上classpath
前缀外是否有别的方式能解决这个问题。
不过现在看来,偷懒是不可能的呢了 ,老老实实的 把前缀classpath
给加上,就不会有问题了