暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

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

蹲厕所的熊 2018-07-15
628

蹲厕所的熊 转载请注明原创出处,谢谢!

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

资源抽象接口Resource

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

Spring的Resource设计是一种典型的策略模式,通过使用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. // 1、文件系统资源

  2. Resource res1 = new FileSystemResource("/Users/benjamin/Desktop/a.txt");

  3. // 2、类路径下的资源

  4. Resource res2 = new ClassPathResource("conf/a.txt");

  5. // 3、web应用资源

  6. Resource res3 = new ServletContextResource("/WEB-INF/classes/conf/a.txt");

资源加载

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

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

资源加载器

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

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

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

  1. public class ResourceTest {


  2.    public static void main(String[] args) throws IOException {

  3.        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();

  4.        // 获取单个文件

  5.        Resource resource = resolver.getResource("classpath:spring.xml");

  6.        System.out.println(resource.getDescription());

  7.        // 获取多个文件

  8.        Resource[] resources = resolver.getResources("classpath*:spring*.xml");

  9.        for (Resource r : resources) {

  10.            System.out.println(r.getDescription());

  11.        }

  12.    }

  13. }

getResource()

  1. @Override

  2. public Resource getResource(String location) {

  3.    return getResourceLoader().getResource(location);

  4. }

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

  1. public PathMatchingResourcePatternResolver() {

  2.    this.resourceLoader = new DefaultResourceLoader();

  3. }

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

  1. public DefaultResourceLoader() {

  2.    this.classLoader = ClassUtils.getDefaultClassLoader();

  3. }


  4. public static ClassLoader getDefaultClassLoader() {

  5.    // 1、首先使用TCCL

  6.    ClassLoader cl = Thread.currentThread().getContextClassLoader();

  7.    if (cl == null) {

  8.        // 2、TCCL比存在,取得当前类的ClassLoader

  9.        cl = ClassUtils.class.getClassLoader();

  10.        if (cl == null) {

  11.            // 3、最后取得系统ClassLoader,默认AppClassLoader

  12.            cl = ClassLoader.getSystemClassLoader();

  13.        }

  14.    }

  15.    return cl;

  16. }

最后定位到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
     。

  1. public Resource getResource(String location) {

  2.    // 先走自定义protocol的resolver,默认protocolResolvers为空

  3.    for (ProtocolResolver protocolResolver : this.protocolResolvers) {

  4.        Resource resource = protocolResolver.resolve(location, this);

  5.        if (resource != null) {

  6.            return resource;

  7.        }

  8.    }


  9.    if (location.startsWith("/")) {

  10.        return getResourceByPath(location);

  11.    }

  12.    // 以 classpath: 开头

  13.    else if (location.startsWith(CLASSPATH_URL_PREFIX)) {

  14.        return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());

  15.    }

  16.    else {

  17.        try {

  18.            // 解析URL类型的资源,ftp: http: file:

  19.            URL url = new URL(location);

  20.            return new UrlResource(url);

  21.        }

  22.        catch (MalformedURLException ex) {

  23.            // 没有前缀的资源

  24.            return getResourceByPath(location);

  25.        }

  26.    }

  27. }


  28. protected Resource getResourceByPath(String path) {

  29.    return new ClassPathContextResource(path, getClassLoader());

  30. }

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

  1. public ClassPathResource(String path, Class<?> clazz) {

  2.    this.path = StringUtils.cleanPath(path);

  3.    this.clazz = clazz;

  4. }

自定义ProtocolResolver解析资源

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

  1. public interface ProtocolResolver {

  2.    Resource resolve(String location, ResourceLoader resourceLoader);

  3. }

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

  1. public class ResourceProtocolResolver implements ProtocolResolver {


  2.    private static final String RESOURCE_PREFIX = "resource";


  3.    @Override

  4.    public Resource resolve(String location, ResourceLoader resourceLoader) {

  5.        if (location.startsWith(RESOURCE_PREFIX)) {

  6.            return resourceLoader.getResource(location.replace(RESOURCE_PREFIX, "classpath"));

  7.        }

  8.        return null;

  9.    }

  10. }

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

  1. public class ResourceTest {


  2.    public static void main(String[] args) throws IOException {

  3.        DefaultResourceLoader resourceLoader = new DefaultResourceLoader();

  4.        // 增加自定义资源解析器

  5.        resourceLoader.addProtocolResolver(new MyProtocolResolver());

  6.        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(resourceLoader);

  7.        resolver.getResource("my:spring.xml");

  8.    }

  9. }

getResources()

  1. public Resource[] getResources(String locationPattern) throws IOException {

  2.    if (locationPattern.startsWith("classpath*:")) {

  3.        // classpath*:后的路径存在*或者?

  4.        if (getPathMatcher().isPattern(locationPattern.substring("classpath*:".length()))) {

  5.            return findPathMatchingResources(locationPattern);

  6.        }

  7.        else {

  8.            // 返回classpath下所有匹配路径的资源

  9.            return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));

  10.        }

  11.    }

  12.    else {

  13.        // 得到路径的前缀。war:是专门针对tomcat解析用的

  14.        int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 : locationPattern.indexOf(":") + 1);

  15.        // 路径存在*或者?

  16.        if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {

  17.            return findPathMatchingResources(locationPattern);

  18.        }

  19.        else {

  20.            // 单个资源使用 getResource 方法加载

  21.            return new Resource[] {getResourceLoader().getResource(locationPattern)};

  22.        }

  23.    }

  24. }

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

  • 解析后的路径中存在Ant风格的资源路径表达式走 findPathMatchingResources
     方法查找所有资源。

  • 以 classpath*:
     开头并且不存在Ant风格的资源路径表达式的走 findAllClassPathResources
     方法查找所有资源。

  • 其他路径走getResource的查找单个资源逻辑。

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

  1. protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {

  2.    // 返回第一个不包含Ant通配符的目录,比如/WEB-INF/*.xml 返回 /WEB-INF/

  3.    String rootDirPath = determineRootDir(locationPattern);

  4.    // 路径除去rootDirPath的另外一部分

  5.    String subPattern = locationPattern.substring(rootDirPath.length());

  6.    // 获取匹配到目录的所有资源

  7.    Resource[] rootDirResources = getResources(rootDirPath);

  8.    Set<Resource> result = new LinkedHashSet<Resource>(16);

  9.    // 拿到所有资源挨个去匹配,把匹配到的资源放入result中

  10.    for (Resource rootDirResource : rootDirResources) {

  11.        rootDirResource = resolveRootDirResource(rootDirResource);

  12.        URL rootDirURL = rootDirResource.getURL();

  13.        if (equinoxResolveMethod != null) {

  14.            if (rootDirURL.getProtocol().startsWith("bundle")) {

  15.                rootDirURL = (URL) ReflectionUtils.invokeMethod(equinoxResolveMethod, null, rootDirURL);

  16.                rootDirResource = new UrlResource(rootDirURL);

  17.            }

  18.        }

  19.        if (rootDirURL.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {

  20.            result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirURL, subPattern, getPathMatcher()));

  21.        }

  22.        // jar资源

  23.        else if (ResourceUtils.isJarURL(rootDirURL) || isJarResource(rootDirResource)) {

  24.            result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirURL, subPattern));

  25.        }

  26.        // 其他资源

  27.        else {

  28.            result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));

  29.        }

  30.    }

  31.    return result.toArray(new Resource[result.size()]);

  32. }

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

  1. if (getPathMatcher().match(fullPattern, currPath)) {

  2.    result.add(content);

  3. }

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

小结

通过源码我们可以知道对于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种匹配符:

  • ?
     :匹配文件名中的一个字符。

  • *
     :匹配文件名中的任意个字符。

  • **
     :匹配多层路径。



如果读完觉得有收获的话,欢迎点赞、关注、加公众号【蹲厕所的熊】,查阅更多精彩历史!!!

文章转载自蹲厕所的熊,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论