蹲厕所的熊

benjaminwhx

说说Spring中的资源文件的读取

2018-07-14 作者: 吴海旭


  1. 资源抽象接口Resource
  2. 资源加载
    1. 资源加载器
      1. getResource()
      2. 自定义ProtocolResolver解析资源
      3. getResources()
    2. 小结

在上一篇 说说Java中资源文件的读取 中我们知道了如何加载类路径下的资源文件,对于其他的资源访问通常使用 java.net.URL 和文件IO来完成,在web项目里,我们也可以通过ServletContext来获取到资源,那么Spring为什么要搞出来一个Resource呢?

资源抽象接口Resource

Spring为了统一资源的访问,定义了Resource接口。为了针对不同的底层资源,Spring提供了不同资源的实现类来负责不同的资源访问逻辑。

Spring的Resource设计是一种典型的策略模式,通过使用Resource接口,客户端程序可以在不同的资源访问策略之间自由切换。

spring_resource

这其中有一些比较常见的资源实现类:

  • ByteArrayResource:二进制数组表示的资源,二进制数组资源可以在内存中通过程序构造。
  • ClassPathResource:类路径下的资源,资源以相对于类路径的方式表示。
  • FileSystemResource:文件系统资源,资源以文件系统路径的方式表示,如 /Users/benjamin/Desktop/a.txt
  • InputStreamResource:对应一个InputStream的资源。
  • ServletContextResource:为访问Web容器上下文中的资源而设计的类,负责以相对于Web应用根目录的路径加载资源,它支持以流和URL的方式访问,在WAR解包的情况下,也可以通过File的方式访问,该类还可以直接从JAR包中访问资源。
  • UrlResource:封装了java.net.URL,它使用户能够访问任何可以通过URL表示的资源,如文件系统的资源、HTTP资源、FTP资源等。
// 1、文件系统资源
Resource res1 = new FileSystemResource("/Users/benjamin/Desktop/a.txt");
// 2、类路径下的资源
Resource res2 = new ClassPathResource("conf/a.txt");
// 3、web应用资源
Resource res3 = new ServletContextResource("/WEB-INF/classes/conf/a.txt");

资源加载

为了访问不同类型的资源,必须使用相应的Resource实现类,这是比较麻烦的。是否可以在不显示使用Resource实现类的情况下,仅通过资源地址的特殊标识就可以加载相应的资源呢?

Spring提供了一个强大的加载资源的机制,不但能通过 “classpath:”、”file:” 等资源地址前缀识别不同的资源类型,还支持Ant风格贷通配符的资源地址。

资源加载器

Spring定义了一套以 ResourceLoader 为顶层的资源加载接口和实现类。

spring_resource2

ResourceLoader 接口仅有一个 getResource() 方法,可以根据一个资源地址加载文件资源,不过,资源地址仅支持带资源类型前缀的表达式,不支持Ant风格的资源路径表达式。 ResourcePatternResolver 扩展自 ResourceLoader 接口,定义了一个新的接口方法:getResources() ,该方法支持贷资源类型前缀及Ant风格的资源路径表达式。

PathMatchingResourcePatternResolver是Spring提供的标准实现类,看个例子:

public class ResourceTest {

    public static void main(String[] args) throws IOException {
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        // 获取单个文件
        Resource resource = resolver.getResource("classpath:spring.xml");
        System.out.println(resource.getDescription());
        // 获取多个文件
        Resource[] resources = resolver.getResources("classpath*:spring*.xml");
        for (Resource r : resources) {
            System.out.println(r.getDescription());
        }
    }
}

getResource()

@Override
public Resource getResource(String location) {
    return getResourceLoader().getResource(location);
}

默认情况下,getResourceLoader()会拿到 DefaultResourceLoader ,这个赋值操作是在类初始化的时候完成的。

public PathMatchingResourcePatternResolver() {
    this.resourceLoader = new DefaultResourceLoader();
}

DefaultResourceLoader 会使用ClassLoader来加载资源,如果不指定ClassLoader,会使用默认的ClassLoader,更多ClassLoader的知识可以参考:理解TCCL:线程上下文加载器JVM类加载的那些事

public DefaultResourceLoader() {
    this.classLoader = ClassUtils.getDefaultClassLoader();
}

public static ClassLoader getDefaultClassLoader() {
    // 1、首先使用TCCL
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    if (cl == null) {
        // 2、TCCL比存在,取得当前类的ClassLoader
        cl = ClassUtils.class.getClassLoader();
        if (cl == null) {
            // 3、最后取得系统ClassLoader,默认AppClassLoader
            cl = ClassLoader.getSystemClassLoader();
        }
    }
    return cl;
}

最后定位到getResource方法,它会根据传入的路径解析protocol,解析逻辑如下:

  1. 如果用户自定义了protocol的解析器,直接用它来进行解析资源,默认没有自定义解析器。
  2. 如果资源以 / 开头,使用 ClassPathContextResource 来解析资源,最终使用的就是JDK的ClassLoader.getResource()。
  3. 如果资源以 classpath: 开头,解析出 classpath: 后面的内容,使用 ClassPathResource 解析资源。
  4. URL类型的资源都可以使用 UrlResource 来解析资源,如file: http: ftp:
  5. 没有前缀的资源使用 ClassPathContextResource 来解析资源,如:com/xx/a.xml
public Resource getResource(String location) {
    // 先走自定义protocol的resolver,默认protocolResolvers为空
    for (ProtocolResolver protocolResolver : this.protocolResolvers) {
        Resource resource = protocolResolver.resolve(location, this);
        if (resource != null) {
            return resource;
        }
    }

    if (location.startsWith("/")) {
        return getResourceByPath(location);
    }
    // 以 classpath: 开头
    else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
        return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
    }
    else {
        try {
            // 解析URL类型的资源,ftp: http: file:
            URL url = new URL(location);
            return new UrlResource(url);
        }
        catch (MalformedURLException ex) {
            // 没有前缀的资源
            return getResourceByPath(location);
        }
    }
}

protected Resource getResourceByPath(String path) {
    return new ClassPathContextResource(path, getClassLoader());
}

可能有的同学对第二点的 / 开头使用ClassLoader.getResource()解析有点疑问,因为之前我说过ClassLoader解析的资源不能以 / 开头啊。其实ClassPathResource对路径做了一层处理,把 / 去掉了:

public ClassPathResource(String path, Class<?> clazz) {
    this.path = StringUtils.cleanPath(path);
    this.clazz = clazz;
}

自定义ProtocolResolver解析资源

资源解析器 ProtocolResolver 提供了一个resolve方法供我们自己解析特定protocol的资源:

public interface ProtocolResolver {
    Resource resolve(String location, ResourceLoader resourceLoader);
}

我们可以定义 resource: 开头的protocol代表自己工程下的资源文件目录:

public class ResourceProtocolResolver implements ProtocolResolver {

    private static final String RESOURCE_PREFIX = "resource";

    @Override
    public Resource resolve(String location, ResourceLoader resourceLoader) {
        if (location.startsWith(RESOURCE_PREFIX)) {
            return resourceLoader.getResource(location.replace(RESOURCE_PREFIX, "classpath"));
        }
        return null;
    }
}

最后,我们把自定义的解析器add到资源加载器中:

public class ResourceTest {

    public static void main(String[] args) throws IOException {
        DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
        // 增加自定义资源解析器
        resourceLoader.addProtocolResolver(new MyProtocolResolver());
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(resourceLoader);
        resolver.getResource("my:spring.xml");
    }
}

getResources()

public Resource[] getResources(String locationPattern) throws IOException {
    if (locationPattern.startsWith("classpath*:")) {
        // classpath*:后的路径存在*或者?
        if (getPathMatcher().isPattern(locationPattern.substring("classpath*:".length()))) {
            return findPathMatchingResources(locationPattern);
        }
        else {
            // 返回classpath下所有匹配路径的资源
            return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
        }
    }
    else {
        // 得到路径的前缀。war:是专门针对tomcat解析用的
        int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 : locationPattern.indexOf(":") + 1);
        // 路径存在*或者?
        if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
            return findPathMatchingResources(locationPattern);
        }
        else {
            // 单个资源使用 getResource 方法加载
            return new Resource[] {getResourceLoader().getResource(locationPattern)};
        }
    }
}

该方法把资源的查找分为3种类型:

  • 解析后的路径中存在Ant风格的资源路径表达式走 findPathMatchingResources 方法查找所有资源。
  • classpath*: 开头并且不存在Ant风格的资源路径表达式的走 findAllClassPathResources 方法查找所有资源。
  • 其他路径走getResource的查找单个资源逻辑。

我们重点来看一下 findPathMatchingResources 方法,看看它是怎么通过Ant风格的资源路径表达式匹配到资源的:

protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
    // 返回第一个不包含Ant通配符的目录,比如/WEB-INF/*.xml 返回 /WEB-INF/
    String rootDirPath = determineRootDir(locationPattern);
    // 路径除去rootDirPath的另外一部分
    String subPattern = locationPattern.substring(rootDirPath.length());
    // 获取匹配到目录的所有资源
    Resource[] rootDirResources = getResources(rootDirPath);
    Set<Resource> result = new LinkedHashSet<Resource>(16);
    // 拿到所有资源挨个去匹配,把匹配到的资源放入result中
    for (Resource rootDirResource : rootDirResources) {
        rootDirResource = resolveRootDirResource(rootDirResource);
        URL rootDirURL = rootDirResource.getURL();
        if (equinoxResolveMethod != null) {
            if (rootDirURL.getProtocol().startsWith("bundle")) {
                rootDirURL = (URL) ReflectionUtils.invokeMethod(equinoxResolveMethod, null, rootDirURL);
                rootDirResource = new UrlResource(rootDirURL);
            }
        }
        if (rootDirURL.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
            result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirURL, subPattern, getPathMatcher()));
        }
        // jar资源
        else if (ResourceUtils.isJarURL(rootDirURL) || isJarResource(rootDirResource)) {
            result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirURL, subPattern));
        }
        // 其他资源
        else {
            result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));
        }
    }
    return result.toArray(new Resource[result.size()]);
}

因流程较长,我们直接来看重点的方法 doRetrieveMatchingFiles 中有一段拿到AntPathMatcher来进行匹配的逻辑。

if (getPathMatcher().match(fullPattern, currPath)) {
    result.add(content);
}

至此,资源加载器中重要的部分都已经讲完了。

小结

通过源码我们可以知道对于Spring支持以下几种前缀的资源地址表达式:

地址前缀 示例 对应资源类型
classpath: classpath:com/xx/a.xml 从类路径中加载资源,classpath:和classpath:/是等价的,都是相对于类的根路径。资源文件可以在标准的文件系统中,也可以在jar或zip的类包中。
file: file:/conf/com/xx/a.xml 使用UrlResource从文件系统目录中装载资源,可采用绝对或相对路径
http:// http://www.xx.com/a.xml 使用UrlResource从web服务器中装载资源
ftp:// ftp://www.xx.com/a.xml 使用UrlResource从FTP服务器中装载资源
没有前缀 com/xx/a.xml 从类路径中加载资源

其中 classpath:classpath*: 的区别为:classpath: 只会在第一个包下查找,而 classpath*: 会扫描所有这些JAR包及类路径下出现的包。

Ant风格资源地址支持3种匹配符:

  • ? :匹配文件名中的一个字符。
  • * :匹配文件名中的任意个字符。
  • ** :匹配多层路径。


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



分享

评论