近来被委派了一个改造canal-adapter的工作,如果有机会就给大家介绍一下canal。遇到一个问题:class.getClassLoader.getResource()在打成jar的时候获取不到资源。
百度了一下,得到很多结果:
Java代码打成jar后 classgetClassLoadergetResource()返回为null
Java读取jar包中的文件(与从工程中拿文件不同,不能用new File形式)
各种劝我使用getResourceAsStream来获取文件。其实如果只是单单读一个文件也够用了,或者某个目录其实也行,但是我需求是:实现类似Mybatis或者Spring那种扫描包得到yml文件的功能。(最后还有童鞋让我读取流,然后复制到某个目录,然后慢慢读。)然后想着我之前看过的Spring源码,记得Spring的也是打成jar包,不管是yml文件,class文件,还是xml文件都是能读取的。而且我前面分析过Spring扫描包的源码:
Spring是如何加载资源的
然后重点看这个PathMatchingResourcePatternResolver
。为什么呢?因为Spring具体的扫描逻辑是org.springframework.context.annotation.ClassPathBeanDefinitionScanner#doScan
中实现的。里面第一步调用栈是:
doScan
|---scanCandidateComponents
|----PathMatchingResourcePatternResolver.getResources
PathMatchingResourcePatternResolver.getResources其实也是对各种资源(jar,网络,文件夹,war等)的读取封装。
其实看到PathMatchingResourcePatternResolver.getResources
这个调用如下,是支持一个类似正则表达式的表达的。那么我的问题就很简单了。
String packageSearchPath = "classpath*:" +
resolveBasePackage(basePackage) + '/' + "**/*.class";
Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
我只需要修改一下上面的代码就能从jar之中读取出yml文件。
String packageSearchPath = "classpath*:" + path + '/' + "**/*.yml";
Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
if (resources.lenght > 0) {
Arrays.stream(resources).foreach(r -> {
if(r.isReadable()) {
System.out.println("获取得到的资源,resources: {}", r.getFilename())
}
})
}
结果如下:
非常之简单。classpath*:
表示的是解压的情况下的 target/class(以idea环境为准),打成jar的时候就是 jar里面的BOOT-INF/class。
但是有个问题需要注意,就是不要使用Resoruce.getFile()
这个方法,因为文件是在jar之中,不能获取,只能通过stream来获取。
然后只想解决问题的童鞋可以到这里就止步了,基于深入了解的原则,下面是对原理的分析。
我们先对org.springframework.core.io.support.PathMatchingResourcePatternResolver#getResources 分析一下:
@Override
public Resource[] getResources(String locationPattern) throws IOException {
Assert.notNull(locationPattern, "Location pattern must not be null");
// CLASSPATH_ALL_URL_PREFIX 这里等于 classpath*:
if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
// 输入的路径是 classpath*:的locationPattern 进入这里。
// 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
// 如果是使用resource匹配模式(resource pattern)就进入这里。
return findPathMatchingResources(locationPattern);
}
else {
// all class path resources with the given name
// 如果给的是全文件名就进入这里
return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
}
}
else {
// Generally only look for a pattern after a prefix here,
// and on Tomcat only after the "*/" separator for its "war:" protocol.
int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 :
locationPattern.indexOf(':') + 1);
if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
// a file pattern
return findPathMatchingResources(locationPattern);
}
else {
// a single resource with the given name
return new Resource[] {getResourceLoader().getResource(locationPattern)};
}
}
}
通过上面的注解可以知道,实现逻辑在findPathMatchingResources
之中。
findPathMatchingResources
的主要功能是找到匹配通过Ant风格的PathMatcher给定位置模式的所有资源。 支持资源jar文件和zip文件,文件系统。
protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
String rootDirPath = determineRootDir(locationPattern);
String subPattern = locationPattern.substring(rootDirPath.length());
Resource[] rootDirResources = getResources(rootDirPath); //获取root文件夹下的的 resources,调用的还是上面的方法,其实是一个递归。
Set<Resource> result = new LinkedHashSet<>(16);
for (Resource rootDirResource : rootDirResources) {
rootDirResource = resolveRootDirResource(rootDirResource);
URL rootDirUrl = rootDirResource.getURL(); // root 文件夹
if (equinoxResolveMethod != null && rootDirUrl.getProtocol().startsWith("bundle")) {
URL resolvedUrl = (URL) ReflectionUtils.invokeMethod(equinoxResolveMethod, null, rootDirUrl);
if (resolvedUrl != null) {
rootDirUrl = resolvedUrl;
}
rootDirResource = new UrlResource(rootDirUrl);
}
if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher())); //读取url资源
}
else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) {
result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPattern)); // 读取jar里面的资源。
}
else {
result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern)); //读取文件系统资源。
}
}
if (logger.isTraceEnabled()) {
logger.trace("Resolved location pattern [" + locationPattern + "] to resources " + result);
}
return result.toArray(new Resource[0]);
}
findAllClassPathResources
功能是找到所有类定位,通过类加载器给定的位置。 代理doFindAllClassPathResources(String) 。
protected Resource[] findAllClassPathResources(String location) throws IOException {
String path = location;
if (path.startsWith("/")) {
path = path.substring(1);
}
Set<Resource> result = doFindAllClassPathResources(path);
if (logger.isTraceEnabled()) {
logger.trace("Resolved classpath location [" + location + "] to resources " + result);
}
return result.toArray(new Resource[0]);
}
protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {
Set<Resource> result = new LinkedHashSet<>(16);
ClassLoader cl = getClassLoader();
Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path)); //文件系统这里可以加载出东西,其它的情况不行。
while (resourceUrls.hasMoreElements()) {
URL url = resourceUrls.nextElement();
result.add(convertClassLoaderURL(url));
}
if ("".equals(path)) {
// The above result is likely to be incomplete, i.e. only containing file system references.
// We need to have pointers to each of the jar files on the classpath as well...
addAllClassLoaderJarRoots(cl, result);
}
return result;
}
通过上面看出,最终调用的还是三个方法:VfsResourceMatchingDelegate.findMatchingResources
, doFindPathMatchingJarResources
, doFindPathMatchingFileResources
。我们重点了解doFindPathMatchingJarResources
。
//搜寻所有URLClassLoader的jar文件引用的URL,并将它们在指针的形式添加到特定的资源集。
protected void addAllClassLoaderJarRoots(@Nullable ClassLoader classLoader, Set<Resource> result) {
if (classLoader instanceof URLClassLoader) {
try {
for (URL url : ((URLClassLoader) classLoader).getURLs()) {
try {
UrlResource jarResource = (ResourceUtils.URL_PROTOCOL_JAR.equals(url.getProtocol()) ?
new UrlResource(url) :
new UrlResource(ResourceUtils.JAR_URL_PREFIX + url + ResourceUtils.JAR_URL_SEPARATOR));
// ResourceUtils.JAR_URL_PREFIX == "jar:"
// JAR_URL_SEPARATOR == "!/";
//上面解析的结果是: jar:url!/
//最重要的代码部分,封装成urlResource。具体的访问由UrlResource去完成,只需返回UrlResource即可。
if (jarResource.exists()) {
result.add(jarResource);
}
// 把 url 封装成 resource。
}
catch (MalformedURLException ex) {
}
}
}
catch (Exception ex) {
}
}
if (classLoader == ClassLoader.getSystemClassLoader()) {
// "java.class.path" manifest evaluation...
addClassPathManifestEntries(result);
}
if (classLoader != null) {
try {
// Hierarchy traversal...
addAllClassLoaderJarRoots(classLoader.getParent(), result);
}
catch (Exception ex) {
}
}
}
所以具体的访问逻辑在UrlResource之中,即jar里面的资源,也归到UrlResource大类之中解决。
我们先看看UrlResource注解
{@link Resource} implementation for {@code java.net.URL} locators. Supports resolution as a {@code URL} and also as a {@code File} in case of the {@code “file:”} protocol.
大概的意思就是:Resource实施java.net.URL定位器。 支持定位符为URL ,也可以作为File中的情况下, "file:"协议。
所以可以得出结论:jar中的资源是解析成UrlResource,当作一种网络资源(jar: 协议)去读取了。
UrlResource里面的具体内容只讲解一个注意点:
/**
* This implementation returns a File reference for the underlying URL/URI,
* provided that it refers to a file in the file system.
* @see org.springframework.util.ResourceUtils#getFile(java.net.URL, String)
*/
@Override
public File getFile() throws IOException {
大意是这个getFile是针对file system的。不要乱用哦,否则会抛出异常如下:
class path resource [xxx.txt] cannot be resolved to absolute file path because it does not reside in the file system: jar:file:/opt/app.jar!/BOOT-INF/classes!/xxx.txt
但是很多其它的方法,比如getFilename是别的方式获取的,所以能正常使用。