Spring中的资源及资源定位

作者: Coding小聪 | 来源:发表于2019-05-05 00:32 被阅读31次

Java中的资源

在Java语言中,使用URL来表示不同来源的资源,URL可以根据不同的协议定义不同类型的资源,常见资源类型有(URL支持的协议):

  • file:
  • http/https:
  • jar:

资源通过URL定义好了之后,如何对不同类型的资源进行解析呢?可以通过注册不同的handler实现,这里的handler指的是java.net.URLStreamHandler。在URL的API中有个重要的获取Connection的方法:URL#openConnection(),而该方法的实现又委托给URLStreamHandler#openCon nection。而URLStreamHandler是个抽象类,恰好其openConnection方法又是抽象方法,即留给子类自行实现。

在JDK中,URLStreamHandler的子类如下图所示:

JDK中URLStreamHandler的子类

classpath

在日常编程中,我们会常遇到classpath,但classpath究竟指的是哪个目录呢?
无论是maven项目还是普通的java工程都会有个src目录,这个目录中存放着项目的java文件和配置文件。web项目src目录下的文件在编译后会放到WEB-INF/classes,非web项目src目录下的文件在编译后会放到classes/。classpath所对应的路径指的就是WEB-INF/classes(或者classes/)

classpath 和 classpath* 区别:
classpath:只会到你的class路径中查找找文件;
classpath*:不仅包含class路径,还包括jar文件中(class路径)进行查找.

用classpath*:需要遍历所有的classpath,所以加载速度是很慢的,因此,在规划的时候,应该尽可能规划好资源文件所在的路径,尽量避免使用 classpath*

Java读取资源文件

在讲资源文件的读取之前,我们先来看看绝对路径和相对路径。

在读取某个文件时,我们常会遇到FileNotFoundException,然而这个文件明明是存在的,导致这个异常的原因往往是由于路径定位得不对。在Java编程中我们可以使用以下两种方式读取资源:

  • 使用java.lang.Class#getResourceAsStream()
  • 使用java.lang.ClassLoader#getResourceAsStream()

使用Class进行资源加载,支持相对路径和绝对路径。相对路径以当前类所在路径为参考点;绝对路径以"/"开头,classpath为根目录。
使用ClassLoader进行资源加载,不支持相对路径,都是以classpath为根目录的绝对路径,需要注意的一点是千万不能以"/"开头。
下面看看例子:

public class ResourceReadDemo {
    public static void main(String[] args) {
        ResourceReadDemo resourceRead = new ResourceReadDemo();
        resourceRead.readFromRelativePathByClass();
        resourceRead.readFromAbsolutePathByClass();
        // classloader加载资源以classpath为相对路径,且不能以"/"开头
        resourceRead.readByClassLoader();
    }

    private void readFromRelativePathByClass(){
        try (InputStream is = this.getClass().getResourceAsStream("resourceDemo.properties");) {
            Properties prop = new Properties();
            prop.load(is);
            // output : package
            System.out.println(prop.getProperty("key"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void readFromAbsolutePathByClass(){
        try (InputStream is = this.getClass().getResourceAsStream("/resourceDemo.properties");) {
            Properties prop = new Properties();
            prop.load(is);
            // output : resources
            System.out.println(prop.getProperty("key"));
        } catch (IOException e) {
            e.printStackTrace();
        }

        try (InputStream is = this.getClass().getResourceAsStream("/io/zgc/spring/features/resource/resourceDemo.properties");) {
            Properties prop = new Properties();
            prop.load(is);
            // output : package
            System.out.println(prop.getProperty("key"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void readByClassLoader() {
        try (InputStream is = this.getClass().getClassLoader().getResourceAsStream("resourceDemo.properties");) {
            Properties prop = new Properties();
            prop.load(is);
            // output : resources
            System.out.println(prop.getProperty("key"));
        } catch (IOException e) {
            e.printStackTrace();
        }

        try (InputStream is = this.getClass().getClassLoader().getResourceAsStream("io/zgc/spring/features/resource/resourceDemo.properties");) {
            Properties prop = new Properties();
            prop.load(is);
            // output : package
            System.out.println(prop.getProperty("key"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Spring中的资源

Spring对资源单独进行了抽象表示,而没有直接使用URL,原因如下:

  1. URL没有定义Classpath或ServletContext资源(这点可以通过扩展URL并定义相应的handler来解决);
  2. URL没有提供资源是否存在、当前资源是否可读等基本判断方法。

故此,Spring将资源对象抽象成Resource,Spring会将配置文件都封装成Resource,它的接口定义如下:

public interface Resource extends InputStreamSource {

    /**
     * 资源是否存在.
     */
    boolean exists();

    /**
     * 资源是否可读
     */
    default boolean isReadable() {
        return exists();
    }

    /**
     * 资源是否处于打开状态
     */
    default boolean isOpen() {
        return false;
    }

    /**
     * 是否为文件资源
     */
    default boolean isFile() {
        return false;
    }

    /**
     * 获取资源对应的URL
     */
    URL getURL() throws IOException;

    /**
     * 获取URI
     */
    URI getURI() throws IOException;

    /**
     * 转换成文件
     */
    File getFile() throws IOException;

    /**
     * 返回一个读的channel
     */
    default ReadableByteChannel readableChannel() throws IOException {
        return Channels.newChannel(getInputStream());
    }

    /**
     * 资源内容的长度
     */
    long contentLength() throws IOException;

    /**
     * 最后修改的时间
     */
    long lastModified() throws IOException;

    /**
     * 创建相对位置的资源
     */
    Resource createRelative(String relativePath) throws IOException;

    /**
     * 获取文件名称
     */
    @Nullable
    String getFilename();

    /**
     * 获取资源描述符
     */
    String getDescription();

}

Resource的部分继承类图

Spring中资源的定位

在Spring中,使用ResourceLoader对资源进行定位、加载,主要作用为:根据给定的资源文件地址返回对应的Resource。

public interface ResourceLoader {

    /** Pseudo URL prefix for loading from the class path: "classpath:" */
    String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX;


    /**
     * 通过资源路径返回Resource对象
     */
    Resource getResource(String location);

    ClassLoader getClassLoader();

}

作为 Spring 统一的资源加载器,ResourceLoader提供了统一的抽象,具体的实现则由相应的子类来负责实现,其类的部分类结构图如下:

ResourceLoader部分继承类图

DefaultResourceLoader

org.springframework.core.io.DefaultResourceLoaderResourceLoader的默认实现,并且实现了资源加载器中重要的getResource(String)方法,源码如下:

@Override
public Resource getResource(String location) {
    Assert.notNull(location, "Location must not be null");
    //通过ProtocolResolver加载资源
    for (ProtocolResolver protocolResolver : this.protocolResolvers) {
        Resource resource = protocolResolver.resolve(location, this);
        if (resource != null) {
            return resource;
        }
    }

    if (location.startsWith("/")) {
        return getResourceByPath(location);
    }
    else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
        return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
    }
    else {
        try {
            // Try to parse the location as a URL...
            URL url = new URL(location);
            return new UrlResource(url);
        }
        catch (MalformedURLException ex) {
            // No URL -> resolve as resource path.
            return getResourceByPath(location);
        }
    }
}

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

可以看到默认的资源加载逻辑:

  1. 通过自定义的ProtocolResolver加载资源;
  2. 如果资源路径以"/"开头,将资源构造成ClassPathContextResource
  3. 如果资源路径以"classpath:"开头,将资源构造成ClassPathResource
  4. 资源路径既不以"/"开头又不以"classpath:"开头,则将资源构造成UrlResource。

ProtocolResolver接口是资源加载的一个扩展点,允许用户自定义资源加载策略。Spring默认没有提供实现类,需要用户自行实现,并调用org.springframework.core.io.DefaultResourceLoader#addProtocolResolver进行添加。

ResourcePatternResolver

ResourceLoader#getResource(String location)仅支持单个资源的加载,ResourcePatternResolver为我们提供了一次加载多个资源的功能。

public interface ResourcePatternResolver extends ResourceLoader {

    String CLASSPATH_ALL_URL_PREFIX = "classpath*:";

    Resource[] getResources(String locationPattern) throws IOException;

}

ResourcePatternResolver支持Ant模式通配符匹配,具体规则为:

“?”:匹配一个字符,如“config?.xml”将匹配“configA.xml”;
“*”:匹配零个或多个字符串,如“io/*/config.xml”将匹配“io/zgc/config.xml”,但不匹配匹配“io/config.xml”;而“io/config-*.xml”将匹配“io/config-dao.xml”;
“**”:匹配路径中的零个或多个目录,如“io/**/config.xml”将匹配“io/config.xml”,也匹配“io/zgc/spring/config.xml”;而“io/zgc/config-**.xml”将匹配“io/zgc/config-dao.xml”,即把“**”当做两个“*”处理。

PathMatchingResourcePatternResolverResourcePatternResolver最常用的一个子类,它加载资源的方法如下所示:

public Resource[] getResources(String locationPattern) throws IOException {
    Assert.notNull(locationPattern, "Location pattern must not be null");
    //是否以"classpath*:"开头
    if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
        // 字符串中是否含有"*"或者"?"
        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()));
        }
    } 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 {
            // 根据ResourceLoader加载资源
            return new Resource[] {getResourceLoader().getResource(locationPattern)};
        }
    }
}

参考(扩展阅读)

【死磕 Spring】—– IOC 之 Spring 统一资源加载策略

【第四章】 资源 之 4.4 Resource通配符路径 ——跟我学spring3

相关文章

网友评论

    本文标题:Spring中的资源及资源定位

    本文链接:https://www.haomeiwen.com/subject/pqaenqtx.html