Spring扫描Jar包小用

近来被委派了一个改造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是别的方式获取的,所以能正常使用。

更多精彩内容欢迎关注我的微信公众号:
Spring扫描Jar包小用_第1张图片

你可能感兴趣的:(Spring)