关于无法加载依赖jar中properties文件的原因分析

我们经常把spring需要加载的properties文件放在java/resources下面,这样存放的问题导致properties在打包后就在jar的根目录下,所以我们的spring的配置路径就是classpath*:xxx.properties,但是这样的jar我们在被其他项目引用的时候会发现properties文件老是无法加载,就这个问题从spring的源码来找找为什么会这样.

首先properties是当做一个resource来加载的,实现加载的是org.springframework.core.io.ResourceLoader接口的一个实现,其中org.springframework.core.io.support.PathMatchingResourcePatternResolver用于解析classpath*:为前缀的资源定义.

PathMatchingResourcePatternResolver的getResources方法里面有以下内容

1
2
3
4
5
6
7
8
9
10
11
12
String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
// a class path resource (multiple resources for same name possible)
if(getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {
// a class path resource pattern
return findPathMatchingResources(locationPattern);
}
else {
// all class path resources with the given name
return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
}
}

如果我们用classpath*:config-*.properties来查找资源,会进入findPathMatchingResources方法,我们看看这个方法前几行怎么写的

1
2
3
4
protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
String rootDirPath = determineRootDir(locationPattern);
String subPattern = locationPattern.substring(rootDirPath.length());
Resource[] rootDirResources = getResources(rootDirPath);

这里经过determineRootDir(locationPattern)产生的rootDirPath会变成classpath*:然后就会调用到
public Resource[] getResources(String locationPattern) throws IOException
这个方法里面,(看代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
public Resource[] getResources(String locationPattern) throws IOException {
Assert.notNull(locationPattern, "Location pattern must not be null");
if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
// a class path resource (multiple resources for same name possible)
if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {
// a class path resource pattern
return findPathMatchingResources(locationPattern);
}
else {
// all class path resources with the given name
return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
}
}

,接下来并不会再调用findPathMatchingResources方法而是调用findAllClassPathResources这个方法,参数会把classpath*:这个subString掉,这个时候神奇的地方来了,我们看看findAllClassPathResources这里面有什么神奇的,上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
protected Resource[] findAllClassPathResources(String location) throws IOException {
String path = location;
if (path.startsWith("/")) {
path = path.substring(1);
}
Enumeration<URL> resourceUrls = getClassLoader().getResources(path);
Set<Resource> result = new LinkedHashSet<Resource>(16);
while (resourceUrls.hasMoreElements()) {
URL url = resourceUrls.nextElement();
result.add(convertClassLoaderURL(url));
}
return result.toArray(new Resource[result.size()]);
}

代码很简单,关键就在

1
Enumeration<URL> resourceUrls = getClassLoader().getResources(path);

(注意这个时候path=“”)这一行,这里的classloader决定了我们能获取的资源,如果在tomcat下面.这个classloader是Tomcat的WebappClassloader,这个时候我们查找资源的path是一个空的字符串.这个class返回了两个URl,一个是${webapps}/WEB-INF/classes,还有一个就是${tomcat-home}/lib,奇怪了,居然${webapps}/WEB-INF/lib并不在返回的URL中,有点神奇了,let’s look!

1
2
3
4
5
6
7
8
9
10
11
public Enumeration<URL> getResources(String name) throws IOException {
Enumeration[] tmp = new Enumeration[2];
if (parent != null) {
tmp[0] = parent.getResources(name);
} else {
tmp[0] = getBootstrapResources(name);
}
tmp[1] = findResources(name);
return new CompoundEnumeration<>(tmp);
}

这个方法实际是WebappClassloader直接继承了ClassLoad过来的,parentClassLoad实际查找的的时tomcat容器的classpath关键在于findResources(name)这个调用,这个调用webappCLassPath有自己的实现,继续look:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
public Enumeration<URL> findResources(String name) throws IOException {
…..这里省略N多代码
// Looking at the JAR files
synchronized (jarFiles) {
if (openJARs()) {
for (i = 0; i < jarFilesLength; i++) {
JarEntry jarEntry = jarFiles[i].getJarEntry(name);
if (jarEntry != null) {
try {
String jarFakeUrl = getURI(jarRealFiles[i]).toString();
jarFakeUrl = "jar:" + jarFakeUrl + "!/" + name;
result.add(new URL(jarFakeUrl));
} catch (MalformedURLException e) {
// Ignore
}
}
}
}
}
……这里也省略掉N多代码
return Collections.enumeration(result);
}

我们可以看到这个for循环里面获取JarEntry这一句,
JarEntry jarEntry = jarFiles[i].getJarEntry(name);
这个name其实是一个””字符串,所以返回的jarEntry就是null,这样所有WEB-INF/lib下jar包里面的配置文件全部都不会进入下一步的匹配过程中.

所以这个问题的根源在于classpath*:这种配置,在tomecat下并不适用于文件放在根目录下匹配加载的情况,所以想加载lib/xxx.jar包里面的porpertis文件的话,需要将porperties文件放在一个包路径下,我是放在config这个包路径下解决,可以参考解决一下.

特别说明,在Jetty下面有没有这种情况出现我没有去实验,有兴趣的人可以去试试看.

坚持原创技术分享,您的支持将鼓励我继续创作!