Spring 资源管理

279 阅读2分钟

Spring 资源管理

Spring 资源管理类图结构:www.processon.com/view/6311a9…

Chapter 1 概述

1 为什么 Spring 不使用 Java 标准资源管理,而选择重新发明轮子?

  • Java 标准资源管理强大,然而扩展复杂(不同的作者对资源理解不一致,导致扩展纷杂),资源存储方式并不统一
  • Spring 要自立门户,打造自己的生态系统
  • Spring “抄”、“超” 和 “潮”

Chapter 2 Java 标准资源管理

1 Java 标准资源管理的职责

职责说明
面向资源文件系统、artifact(jar、war、ear 文件)以及远程资源(HTTP、FTP 等)
API 整合java.lang.ClassLoader#getResource、java.io.File 或 java.net.URL
资源定位java.net.URL 或 java.net.URI
面向流式存储java.net.URLConnection
协议扩展java.net.URLStreamHandler 或 java.net.URLStreamHandlerFactory

java.lang.ClassLoader 可以做到文件的获取,在调用 getResource 后,会得到相应的 URL

URI: 表示的是web上每一种可用的资源,如 HTML文档、图像、视频片段、程序等都由一个URI进行标识的。

URL:统一资源定位符,是 URI 的子集

URL 的格式: protocol :// hostname[:port] / path / ;parameters#fragment (有相关协议 File、Http、Ftp)

协议 / 主机名[端口号] / 路径 / 参数

URL 和 URI 的区别 :只要能唯一标识资源的就是URI,在URI的基础上给出其资源的访问方式的就是URL

2 Java URL 协议扩展

1 、实现 URLStreamHandler 并放置在 sun.net.www.protocol.${protocol}.Handler 包下

实现类名必须为 “Handler” 实现类命名规则:

  • 默认:sun.net.www.protocol.${protocol}.Handler
  • 自定义:通过 Java Properties java.protocol.handler.pkgs 指定实现类包名, 实现类名必须为“Handler”。如果存在多包名指定,通过分隔符 “|”

实现步骤:

  1. 在项目根目录下创建 sun.net.www.protocol.${protocol}.Handler 包。

    1. ${protocol} 是自己定义的协议名称,如果为 x ,则为 x 协议,例如 sun.net.www.protocol.x
    2. Handler 则是在该包下创建的类名称,必须为这个名称。
  2. Handler 需要继承 URLStreamHandler 类并重写 openConnection 方法

public class Handler extends URLStreamHandler {
    @Override
    protected URLConnection openConnection(URL u) throws IOException {
        return new XUrlConnection(u);
    }
}

3.创建 ${protocol}UrlConnection 并继承 UrlConnection

package sun.net.www.protocol.x;
import org.springframework.core.io.ClassPathResource;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
​
public class XUrlConnection extends URLConnection {
    private final ClassPathResource classPathResource;
    protected XUrlConnection(URL url) {
        super(url);
        this.classPathResource = new ClassPathResource(url.getPath());
    }
​
    @Override
    public void connect() throws IOException {
​
    }
​
    @Override
    public InputStream getInputStream() throws IOException {
        return classPathResource.getInputStream();
    }
}
​

测试示例:

package sun.net.www.protocol.x;
import org.springframework.util.StreamUtils;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.Charset;
public class HandlerTest {
​
    public static void main(String[] args) throws IOException {
        // 类似于 classpath:/META-INF/default.properties
        URL url = new URL("x:///META-INF/default.properties");
        URLConnection urlConnection = url.openConnection();
        String s = StreamUtils.copyToString(urlConnection.getInputStream(), Charset.forName("UTF-8"));
        System.out.println(s);
    }
}
​

2 、自定义实现

  1. 在项目工程内创建协议包 org.geekbang.thinking.in.spring.resource.springx
  2. 在 springx 下创建 Handler,并实现 URLStreamHandler
  3. 实现 XUrlConnection 继承 UrlConnection,并重写 getInputStream 方法。
  4. 在项目启动参数添加指定协议包的路径,不包括 springx -Djava.protocol.handler.pkgs=org.geekbang.thinking.in.spring.resource 注意: = 号左右两边不能有空格

3、基于 URLStreamHandlerFactory 进行协议扩展

实现方案:

  1. 实现 URLStreamHandlerFactory 接口,并重写 createURLStreamHandler 方法,
public class MyUrlStreamHandlerFactory implements URLStreamHandlerFactory {
    @Override
    public URLStreamHandler createURLStreamHandler(String protocol) {
        return new Handler();
    }
}
  1. 该方法需要返回一个 URLStreamHandler 。我们创建一个 Handler 并继承自扩展一的 Handler 即可。
  1. 示例使用时,需要调用URL.setURLStreamHandlerFactory 方法替换 Factory。
public class HandlerTest {
​
    public static void main(String[] args) throws IOException {
        URL.setURLStreamHandlerFactory(new MyUrlStreamHandlerFactory());
        URL url = new URL("factory:///META-INF/dev.properties");
​
        InputStream inputStream = url.openStream();
        String s = StreamUtils.copyToString(inputStream, Charset.forName("UTF-8"));
        System.out.println(s);
    }
}

注意事项:

  • 基于 java.net.URLStreamHandlerFactory 这种扩展机制存在缺陷,单个 ClassLoader 里面只允许有一次
  • 基于 java.net.URLStreamHandler 可以根据不同的协议进行扩展

URLStreamHanlderFactory 扩展机制分析

  1. URLStreamHanlderFactory 类可以创建一个 URLStreamHanlder
  2. URLStreamHanlder 可以根据一个 URL 去打开一个连接
  3. URL 中的 URLStreamHandlerFactory 是个静态属性,一个 ClassLoader 中只会存在一个,并且,这个属性可以为空

image.png

public static void setURLStreamHandlerFactory(URLStreamHandlerFactory fac) {
    synchronized (streamHandlerLock) {
        // factory 已经设置过的话,会抛出错误
        if (factory != null) {
            throw new Error("factory already defined");
        }
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkSetFactory();
        }
        handlers.clear();
        factory = fac;
    }
}

3 、JDK 1.8 內建协议实现

filesun.net.www.protocol.file.Handler
ftpsun.net.www.protocol.ftp.Handler
httpsun.net.www.protocol.http.Handler
httpssun.net.www.protocol.https.Handler
jarsun.net.www.protocol.jar.Handler
mailtosun.net.www.protocol.mailto.Handler
netdocsun.net.www.protocol.netdoc.Handler

Chapter 3 Spring 资源接口

1 资源接口

类型接口
输入流org.springframework.core.io.InputStreamSource
只读资源org.springframework.core.io.Resource
可写资源org.springframework.core.io.WritableResource
编码资源org.springframework.core.io.support.EncodedResource
上下文资源org.springframework.core.io.ContextResource

2 输入流 StreamSource

public interface InputStreamSource {
​
    /**
     * 返回一个输入流
     */
    InputStream getInputStream() throws IOException;
​
}
​

3 只读资源 Resource

Resource 可读资源,不可写,但是存在一些漏洞的地方,可以通过 getURL 去获取URL, 然后再OpenConnection 开启一个流进行写操作。 但正常情况下,要写资源还是需要使用 WriteableResource

  • exists(); 判断资源是否存在
  • isReadable(); 资源是否可读
  • isOpen();是否可以打开
  • isFile(); 是否为文件
  • getURL(); 返回URL
  • getURI(); 返回URI
  • getFile(); 返回文件
  • readableChannel(); 创建返回一个新的 ReadableByteChannel
  • contentLength(); 返回资源的长度
  • lastModified(); 返回上次修改的时间
  • createRelative(); 创建与此资源相关的资源
  • getFilename(); 获取文件名称
  • getDescription(); 获取资源描述
public interface Resource extends InputStreamSource {
​
    boolean exists();
​
    default boolean isReadable() {
        return exists();
    }
​
​
    default boolean isOpen() {
        return false;
    }
​
​
    default boolean isFile() {
        return false;
    }
​
​
    URL getURL() throws IOException;
​
​
    URI getURI() throws IOException;
​
​
    File getFile() throws IOException;
}

4 可写资源 WritableResource

WriteableResource 可写资源

  • isWritable(); 是否可写
  • getOutputStream(); 获取写的流
  • WritableByteChannel(); 返回一个写入的管道,NIO 支持
public interface WritableResource extends Resource {
​
    default boolean isWritable() {
        return true;
    }
​
    OutputStream getOutputStream() throws IOException;
​
}
​

5 编码资源 EncodedResource

EncodedResource 可编码资源。 可编码的实现方式为 将 Resource 资源转换为 Reader。

public class EncodedResource implements InputStreamSource {
​
    private final Resource resource;
​
    @Nullable
    private final String encoding;
​
    @Nullable
    private final Charset charset;
    
    private EncodedResource(Resource resource, @Nullable String encoding, @Nullable Charset charset) {
        super();
        Assert.notNull(resource, "Resource must not be null");
        this.resource = resource;
        this.encoding = encoding;
        this.charset = charset;
    }
​
​
    public final Resource getResource() {
        return this.resource;
    }
​
    @Nullable
    public final String getEncoding() {
        return this.encoding;
    }
    
    @Nullable
    public final Charset getCharset() {
        return this.charset;
    }
​
​
    public Reader getReader() throws IOException {
        if (this.charset != null) {
            return new InputStreamReader(this.resource.getInputStream(), this.charset);
        }
        else if (this.encoding != null) {
            return new InputStreamReader(this.resource.getInputStream(), this.encoding);
        }
        else {
            return new InputStreamReader(this.resource.getInputStream());
        }
    }
​
​
    @Override
    public InputStream getInputStream() throws IOException {
        return this.resource.getInputStream();
    }
​
​
}

6 上下文资源 ContextResource

通常在 Servlet 环境下使用

public interface ContextResource extends Resource {
​
    /**
     * 返回封闭“上下文”中的路径。 
     * <p>这通常是相对于上下文特定根目录的路径,
     * 例如ServletContext 根或 PortletContext 根。
     */
    String getPathWithinContext();
​
}

Chapter 4 Spring 内建 Resource 实现

资源来源资源协议实现类
Bean 定义org.springframework.beans.factory.support.BeanDefinitionResource
数组org.springframework.core.io.ByteArrayResource
类路径classpath:/org.springframework.core.io.ClassPathResource
文件系统file:/org.springframework.core.io.FileSystemResource
URLURL 支持的协议org.springframework.core.io.UrlResource
ServletContextorg.springframework.web.context.support.ServletContex tResource

2 Bean 定义 BeanDefinitionResource

BeanDefinitionResource 该接口使用的较少,大多为 Spring 内部使用 exists() 返回不存在 isReadable() 返回不可读。 大多作用为设置 Description。

class BeanDefinitionResource extends AbstractResource {
​
    @Override
    public String getDescription() {
        return "BeanDefinition defined in " + this.beanDefinition.getResourceDescription();
    }
}
​

3 数组 ByteArrayResource

字节流数组资源:该类的 getInputStream(); 方法返回的是一个 Java 的 ByteArrayInputStream。

public class ByteArrayResource extends AbstractResource {
​
    @Override
    public InputStream getInputStream() throws IOException {
        return new ByteArrayInputStream(this.byteArray);
    }
}

4 类路径 ClassPathResource

ClassPathResource 是采用 ClassLoader 获取 InputStream 输入流。 ClassPathResource 构造方法中,如果没有传入 classLoader 则会初始化一个ClassLoader

public class ClassPathResource extends AbstractFileResolvingResource {
​
    private final String path;
​
    @Nullable
    private ClassLoader classLoader;
​
    @Nullable
    private Class<?> clazz;
​
​
    /**
     * Create a new {@code ClassPathResource} for {@code ClassLoader} usage.
     * A leading slash will be removed, as the ClassLoader resource access
     * methods will not accept it.
     * <p>The thread context class loader will be used for
     * loading the resource.
     * @param path the absolute path within the class path
     * @see java.lang.ClassLoader#getResourceAsStream(String)
     * @see org.springframework.util.ClassUtils#getDefaultClassLoader()
     */
    public ClassPathResource(String path) {
        this(path, (ClassLoader) null);
    }
​
    /**
     * Create a new {@code ClassPathResource} for {@code ClassLoader} usage.
     * A leading slash will be removed, as the ClassLoader resource access
     * methods will not accept it.
     * @param path the absolute path within the classpath
     * @param classLoader the class loader to load the resource with,
     * or {@code null} for the thread context class loader
     * @see ClassLoader#getResourceAsStream(String)
     */
    public ClassPathResource(String path, @Nullable ClassLoader classLoader) {
        Assert.notNull(path, "Path must not be null");
        String pathToUse = StringUtils.cleanPath(path);
        if (pathToUse.startsWith("/")) {
            pathToUse = pathToUse.substring(1);
        }
        this.path = pathToUse;
        // ClassPathResource 构造方法中,如果没有传入 classLoader 则会初始化一个ClassLoader 
        this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());
    }
​
    /**
     * Create a new {@code ClassPathResource} for {@code Class} usage.
     * The path can be relative to the given class, or absolute within
     * the classpath via a leading slash.
     * @param path relative or absolute path within the class path
     * @param clazz the class to load resources with
     * @see java.lang.Class#getResourceAsStream
     */
    public ClassPathResource(String path, @Nullable Class<?> clazz) {
        Assert.notNull(path, "Path must not be null");
        this.path = StringUtils.cleanPath(path);
        this.clazz = clazz;
    }
​
    /**
     * Create a new {@code ClassPathResource} with optional {@code ClassLoader}
     * and {@code Class}. Only for internal usage.
     * @param path relative or absolute path within the classpath
     * @param classLoader the class loader to load the resource with, if any
     * @param clazz the class to load resources with, if any
     * @deprecated as of 4.3.13, in favor of selective use of
     * {@link #ClassPathResource(String, ClassLoader)} vs {@link #ClassPathResource(String, Class)}
     */
    @Deprecated
    protected ClassPathResource(String path, @Nullable ClassLoader classLoader, @Nullable Class<?> clazz) {
        this.path = StringUtils.cleanPath(path);
        this.classLoader = classLoader;
        this.clazz = clazz;
    }
​
​
    /**
     * Return the path for this resource (as resource path within the class path).
     */
    public final String getPath() {
        return this.path;
    }
​
    /**
     * Return the ClassLoader that this resource will be obtained from.
     */
    @Nullable
    public final ClassLoader getClassLoader() {
        return (this.clazz != null ? this.clazz.getClassLoader() : this.classLoader);
    }
​
​
    /**
     * This implementation checks for the resolution of a resource URL.
     * @see java.lang.ClassLoader#getResource(String)
     * @see java.lang.Class#getResource(String)
     */
    @Override
    public boolean exists() {
        return (resolveURL() != null);
    }
​
    /**
     * Resolves a URL for the underlying class path resource.
     * @return the resolved URL, or {@code null} if not resolvable
     */
    @Nullable
    protected URL resolveURL() {
        if (this.clazz != null) {
            return this.clazz.getResource(this.path);
        }
        else if (this.classLoader != null) {
            return this.classLoader.getResource(this.path);
        }
        else {
            return ClassLoader.getSystemResource(this.path);
        }
    }
​
    /**
     * This implementation opens an InputStream for the given class path resource.
     * @see java.lang.ClassLoader#getResourceAsStream(String)
     * @see java.lang.Class#getResourceAsStream(String)
     */
    @Override
    public InputStream getInputStream() throws IOException {
        InputStream is;
        if (this.clazz != null) {
            // 如果类存在,就采用类的 classLoader 来获取 InputStream
            is = this.clazz.getResourceAsStream(this.path);
        }
        else if (this.classLoader != null) {
            // 采用当前对象的 classLoader 进行加载
            is = this.classLoader.getResourceAsStream(this.path);
        }
        else {
            // 调用系统的 classLoader 进行加载
            is = ClassLoader.getSystemResourceAsStream(this.path);
        }
        if (is == null) {
            // 如果还是不存在,则抛出文件找不到异常。但实际上到不了这里
            throw new FileNotFoundException(getDescription() + " cannot be opened because it does not exist");
        }
        return is;
    }
​
    /**
     * This implementation returns a URL for the underlying class path resource,
     * if available.
     * @see java.lang.ClassLoader#getResource(String)
     * @see java.lang.Class#getResource(String)
     */
    @Override
    public URL getURL() throws IOException {
        URL url = resolveURL();
        if (url == null) {
            throw new FileNotFoundException(getDescription() + " cannot be resolved to URL because it does not exist");
        }
        return url;
    }
​
    /**
     * This implementation creates a ClassPathResource, applying the given path
     * relative to the path of the underlying resource of this descriptor.
     * @see org.springframework.util.StringUtils#applyRelativePath(String, String)
     */
    @Override
    public Resource createRelative(String relativePath) {
        String pathToUse = StringUtils.applyRelativePath(this.path, relativePath);
        return (this.clazz != null ? new ClassPathResource(pathToUse, this.clazz) :
                new ClassPathResource(pathToUse, this.classLoader));
    }
​
    /**
     * This implementation returns the name of the file that this class path
     * resource refers to.
     * @see org.springframework.util.StringUtils#getFilename(String)
     */
    @Override
    @Nullable
    public String getFilename() {
        return StringUtils.getFilename(this.path);
    }
​
    /**
     * This implementation returns a description that includes the class path location.
     */
    @Override
    public String getDescription() {
        StringBuilder builder = new StringBuilder("class path resource [");
        String pathToUse = this.path;
        if (this.clazz != null && !pathToUse.startsWith("/")) {
            builder.append(ClassUtils.classPackageAsResourcePath(this.clazz));
            builder.append('/');
        }
        if (pathToUse.startsWith("/")) {
            pathToUse = pathToUse.substring(1);
        }
        builder.append(pathToUse);
        builder.append(']');
        return builder.toString();
    }
​
​
    /**
     * This implementation compares the underlying class path locations.
     */
    @Override
    public boolean equals(@Nullable Object other) {
        if (this == other) {
            return true;
        }
        if (!(other instanceof ClassPathResource)) {
            return false;
        }
        ClassPathResource otherRes = (ClassPathResource) other;
        return (this.path.equals(otherRes.path) &&
                ObjectUtils.nullSafeEquals(this.classLoader, otherRes.classLoader) &&
                ObjectUtils.nullSafeEquals(this.clazz, otherRes.clazz));
    }
​
    /**
     * This implementation returns the hash code of the underlying
     * class path location.
     */
    @Override
    public int hashCode() {
        return this.path.hashCode();
    }
​
}
​

5 文件系统 FileSystemResource

FileSystemResource 文件资源接口 具有 File、FilePath 属性。构造方法中需要传入两个属性中的一个,另外一个属性可以反推。 getInputStream(); 方法返回的是 Nio 的 File 文件。

6 URL UrlResource

UrlResource 根据 URL 读取 InputStream。 getInputStream(); 方法会开启一个 URLConnection,再获取 URL 中的输入流。

public class UrlResource extends AbstractFileResolvingResource {
​
    
    private URL getCleanedUrl(URL originalUrl, String originalPath) {
        String cleanedPath = StringUtils.cleanPath(originalPath);
        if (!cleanedPath.equals(originalPath)) {
            try {
                return new URL(cleanedPath);
            }
            catch (MalformedURLException ex) {
                // Cleaned URL path cannot be converted to URL -> take original URL.
            }
        }
        return originalUrl;
    }
​
​
    @Override
    public InputStream getInputStream() throws IOException {
        // 开启 url 中的 URLConnection
        URLConnection con = this.url.openConnection();
        ResourceUtils.useCachesIfNecessary(con);
        try {
            // 获取 URLConnection 中的输入流
            return con.getInputStream();
        }
        catch (IOException ex) {
            // Close the HTTP connection (if applicable).
            if (con instanceof HttpURLConnection) {
                ((HttpURLConnection) con).disconnect();
            }
            throw ex;
        }
    }
​
    
    @Override
    public URL getURL() {
        return this.url;
    }
​
    
    @Override
    public URI getURI() throws IOException {
        if (this.uri != null) {
            return this.uri;
        }
        else {
            return super.getURI();
        }
    }
}
​

Chapter 5 Spring Resource 接口扩展

1 概述

可写资源接口 org.springframework.core.io.WritableResource

  • org.springframework.core.io.FileSystemResource
  • org.springframework.core.io.FileUrlResource(@since 5.0.2)
  • org.springframework.core.io.PathResource(@since 4.0 & @Deprecated) 编码资源接口
  • org.springframework.core.io.support.EncodedResource

2 FileSystemResource 与 EncodedResource 演示

package org.geekbang.thinking.in.spring.resource;
​
import org.apache.commons.io.IOUtils;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.support.EncodedResource;
​
import java.io.File;
import java.io.IOException;
import java.io.Reader;
​
/**
 * @ClassName EncodedFileSystemResourceDemo
 * @Description {@link FileSystemResource} {@link EncodedResource} 示例演示
 * @Author WQ
 * @Date 2022/8/31 13:22
 * @Version 1.0
 */
public class EncodedFileSystemResourceDemo {
​
    public static void main(String[] args) throws IOException {
        String path = "C:\Users\Administrator.WIN-25IPOL1DU1L\Desktop\画图\data_2021\jk\thinking-in-spring\resource\src\main\java\org\geekbang\thinking\in\spring\resource\EncodedFileSystemResourceDemo.java";
        File file = new File(path);
        FileSystemResource fileSystemResource = new FileSystemResource(file);
        EncodedResource encodedResource = new EncodedResource(fileSystemResource, "UTF-8");
        Reader reader = encodedResource.getReader();
        String s = IOUtils.toString(reader);
        System.out.println(s);
    }
}
​

Chapter 6 Spring 资源加载器

1 Resource 加载器

org.springframework.core.io.ResourceLoader

  • org.springframework.core.io.DefaultResourceLoader

    • org.springframework.core.io.FileSystemResourceLoader
    • org.springframework.core.io.ClassRelativeResourceLoader
    • org.springframework.context.support.AbstractApplicationContext

FileSystemResourceLoader、ClassRelativeResourceLoader 、AbstractApplicationContext 都继承自 DefaultResourceLoader。 前两者主要重写 getResourceByPath 方法,差异在于,一个根据路径获取文件,一个是根据 ClassLoader 加载。 AbstractApplicationContext 则重写了其他方法。

/**
 * @ClassName EncodedFileSystemResourceLoaderDemo
 * @Description {@link FileSystemResourceLoader} 文件系统加载器示例
 * @Author WQ
 * @Date 2022/8/31 13:22
 * @Version 1.0
 */
public class EncodedFileSystemResourceLoaderDemo {
​
    public static void main(String[] args) throws IOException {
        String path = "C:\Users\Administrator.WIN-25IPOL1DU1L\Desktop\画图\data_2021\jk\thinking-in-spring\resource\src\main\java\org\geekbang\thinking\in\spring\resource\EncodedFileSystemResourceDemo.java";
        FileSystemResourceLoader loader = new FileSystemResourceLoader();
        Resource resource = loader.getResource(path);
        EncodedResource encodedResource = new EncodedResource(resource, "UTF-8");
        Reader reader = encodedResource.getReader();
        String s = IOUtils.toString(reader);
        System.out.println(s);
    }
}
​

2 ResourceLoader

资源加载器 API

public interface ResourceLoader {
​
    /** 从类路径加载的伪 URL 前缀:“classpath:”。*/
    String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX;
​
​
    /**
     * 根据路径返回资源
     */
    Resource getResource(String location);
​
    /**
     * 获取 ClassLoader
     */
    @Nullable
    ClassLoader getClassLoader();
​
}
​

3 DefaultResourceLoader

DefaultResourceLoader 具备 ResourceLoader 的能力,可以根据路径获取 Resource 资源,也可以获取 ClassLoader

public class DefaultResourceLoader implements ResourceLoader {
    
    @Override
    @Nullable
    public ClassLoader getClassLoader() {
        return (this.classLoader != null ? this.classLoader : ClassUtils.getDefaultClassLoader());
    }
​
​
    public void clearResourceCaches() {
        this.resourceCaches.clear();
    }
​
​
    @Override
    public Resource getResource(String location) {
        Assert.notNull(location, "Location must not be null");
​
        for (ProtocolResolver protocolResolver : getProtocolResolvers()) {
            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 (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : 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());
    }
}
​

4 FileSystemResourceLoader

FileSystemResourceLoader 继承 DefaultResourceLoader 类。 重写了 getResourceByPath 方法

public class FileSystemResourceLoader extends DefaultResourceLoader {
​
    
    @Override
    protected Resource getResourceByPath(String path) {
        if (path.startsWith("/")) {
            path = path.substring(1);
        }
        return new FileSystemContextResource(path);
    }
​
​
    /**
     * FileSystemResource that explicitly expresses a context-relative path
     * through implementing the ContextResource interface.
     */
    private static class FileSystemContextResource extends FileSystemResource implements ContextResource {
​
        public FileSystemContextResource(String path) {
            super(path);
        }
​
        @Override
        public String getPathWithinContext() {
            return getPath();
        }
    }
​
}

5 ClassRelativeResourceLoader

package org.springframework.core.io;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
public class ClassRelativeResourceLoader extends DefaultResourceLoader {
​
    private final Class<?> clazz;
​
    @Override
    protected Resource getResourceByPath(String path) {
        return new ClassRelativeContextResource(path, this.clazz);
    }
​
    private static class ClassRelativeContextResource extends ClassPathResource implements ContextResource {
​
        private final Class<?> clazz;
​
        public ClassRelativeContextResource(String path, Class<?> clazz) {
            super(path, clazz);
            this.clazz = clazz;
        }
​
        @Override
        public String getPathWithinContext() {
            return getPath();
        }
​
        @Override
        public Resource createRelative(String relativePath) {
            String pathToUse = StringUtils.applyRelativePath(getPath(), relativePath);
            return new ClassRelativeContextResource(pathToUse, this.clazz);
        }
    }
​
}
​

Chapter 7 Spring 通配路径资源加载器

1 概述

通配路径 ResourceLoader

  • org.springframework.core.io.support.ResourcePatternResolver

    • org.springframework.core.io.support.PathMatchingResourcePatternResolver

路径匹配器 (采用通配符表达式进行比较)

  • org.springframework.util.PathMatcher

    • Ant 模式匹配实现 - org.springframework.util.AntPathMatcher

2 ResourcePatternResolver 资源模式处理器

资源通配符处理器(采用策略模式实现)

  • getResources(): 方法传入一个通配符表达式,返回查找到的资源数组。
public interface ResourcePatternResolver extends ResourceLoader {
​
    String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
​
    Resource[] getResources(String locationPattern) throws IOException;
​
}

3 PathMatchingResourcePatternResolver

public class PathMatchingResourcePatternResolver implements ResourcePatternResolver {
​
    
    @Override
    public Resource[] getResources(String locationPattern) throws IOException {
        Assert.notNull(locationPattern, "Location pattern must not be null");
        // 路径以 classpath*: 开头? 
        if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
            // a class path resource (multiple resources for same name possible)
            // substring 截取 n 位后面的内容
            // 如果匹配
            if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {
                // 类路径资源模式
                return findPathMatchingResources(locationPattern);
            }
            else {
                // 具有给定名称的所有类路径资源
                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)};
            }
        }
    }
​
    
    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]);
    }
​

4 findAllClassPathResources

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]);
}

5 doFindAllClassPathResources

protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {
    // 建立资源列表
    Set<Resource> result = new LinkedHashSet<>(16);
    // 获取 ClassLoader
    ClassLoader cl = getClassLoader();
    // 将 path 转化为 URL
    Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path));
    while (resourceUrls.hasMoreElements()) {
        // 获取 URL
        URL url = resourceUrls.nextElement();
        // 将从 ClassLoader 返回的给定 URL 转换为 {@link Resource}。
        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;
}

Chapter 8 Spring 通配路径资源扩展

1 概述

实现 org.springframework.util.PathMatcher 重置 PathMatcher

  • PathMatchingResourcePatternResolver#setPathMatcher

在 PathMatchingResourcePatternResolver 根据路径获取资源列表的时候, 会根据 PathMatcher 的 isPattern();方法决定是否走 通配符模式进行查找

2 扩展示例

路径设置注意事项: // 读取当前 package 对应的所有的 .java 文件 // determineRootDir(locationPattern); 方法会解析根节点路径,该方法会解析 RootDirPath 的末尾下表,如果 resource 路径后面没有 / 会导致解析失败。 // PathMatchingResourcePatternResolver#retrieveMatchingFiles 方法中会判断 rootDir 路径对应的文件夹是否存在,此处可以 Debug 看看路径那里有问题,放到文件夹中进行访问。

扩展步骤主要分为两步:

  1. 自定义 PathMatcher 继承 PathMatcher,并重写 isPattern(); 与 match(); 方法
  2. 将自定义 PathMatcher 设置到 PathMatchingResourcePatternResolver 中。
/**
 * 自定义 {@link ResourcePatternResolver} 示例
 *
 * @author <a href="mailto:mercyblitz@gmail.com">Mercy</a>
 * @see ResourcePatternResolver
 * @see PathMatchingResourcePatternResolver
 * @see PathMatcher
 * @since
 */
public class CustomizedResourcePatternResolverDemo {
​
    public static void main(String[] args) throws IOException {
        // 读取当前 package 对应的所有的 .java 文件
        // determineRootDir(locationPattern); 方法会解析根节点路径,该方法会解析 RootDirPath 的末尾下表,如果 resource 路径后面没有 / 会导致解析失败。
        // PathMatchingResourcePatternResolver#retrieveMatchingFiles 方法中会判断 rootDir 路径对应的文件夹是否存在,此处可以 Debug 看看路径那里有问题,放到文件夹中进行访问。
        //构建 rootDirPath
        String currentPackagePath = "/" + System.getProperty("user.dir") + "/resource/src/main/java/org/geekbang/thinking/in/spring/resource/";
        // 构建通配符
        String locationPattern = currentPackagePath + "*.java";
        // 构建通配符资源资源匹配处理器
        PathMatchingResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(new FileSystemResourceLoader());
        // 添加路径匹配处理器
        resourcePatternResolver.setPathMatcher(new JavaFilePathMatcher());
        // 根据指定路径获取资源
        Resource[] resources = resourcePatternResolver.getResources(locationPattern);
        // 进行输出
        Stream.of(resources).map(ResourceUtils::getContent).forEach(System.out::println);
    }
​
    /**
     * 扩展 PathMatcher 的匹配规则
     */
    static class JavaFilePathMatcher implements PathMatcher {
​
        @Override
        public boolean isPattern(String path) {
            return path.endsWith(".java");
        }
​
        @Override
        public boolean match(String pattern, String path) {
            return path.endsWith(".java");
        }
​
        @Override
        public boolean matchStart(String pattern, String path) {
            return false;
        }
​
        @Override
        public String extractPathWithinPattern(String pattern, String path) {
            return null;
        }
​
        @Override
        public Map<String, String> extractUriTemplateVariables(String pattern, String path) {
            return null;
        }
​
        @Override
        public Comparator<String> getPatternComparator(String path) {
            return null;
        }
​
        @Override
        public String combine(String pattern1, String pattern2) {
            return null;
        }
    }
}
​

Chapter 9 依赖注入Spring Resource

1 概述

基于 @Value 实现 @Value(“classpath:/...”) private Resource resource;

@Value("classpath :/META-INF/ .properties") private Resource[] resourceArray;

@Value("${user.dir}") private String currentProjectPath;

2 Resource 资源注入示例

package org.geekbang.thinking.in.spring.resource;
​
import org.geekbang.thinking.in.spring.resource.util.ResourceUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.io.Resource;
​
import javax.annotation.PostConstruct;
import java.util.List;
import java.util.stream.Stream;
​
/**
 * @ClassName InjectionResourceDemo
 * @Description 注入 {@link Resource} 资源示例
 * @Author WQ
 * @Date 2022/8/31 17:40
 * @Version 1.0
 */
public class InjectionResourceDemo {
​
    @Value("classpath:/META-INF/default.properties")
    private Resource defaultPropertyResource;
​
    @Value("classpath*:/META-INF/*.properties")
    private Resource[] resourceArray;
​
    @Value("${user.dir}")
    private String currentProjectPath;
​
    @PostConstruct
    public void init() {
        System.out.println(ResourceUtils.getContent(defaultPropertyResource));
        Stream.of(resourceArray).map(ResourceUtils::getContent).forEach(System.out::println);
        System.out.println(currentProjectPath);
    }
​
    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        applicationContext.register(InjectionResourceDemo.class);
        applicationContext.refresh();
        applicationContext.close();
    }
}
​

Chapter 10 依赖注入 ResourceLoader

1 概述

  • 方法一:实现 ResourceLoaderAware 回调
  • 方法二:@Autowired 注入 ResourceLoader
  • 方法三:注入 ApplicationContext 作为 ResourceLoader

2 依赖注入 ResourceLoader 示例

public class InjectionResourceLoaderDemo implements ResourceLoaderAware {
​
    @Autowired
    private ResourceLoader resourceLoader;
​
    private ResourceLoader resourceLoaderAware;
​
    @Autowired
    private ApplicationContext applicationContext;
​
    @PostConstruct
    public void init() {
        System.out.println(resourceLoader == resourceLoaderAware);
        System.out.println(resourceLoaderAware == applicationContext);
    }
​
    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        applicationContext.register(InjectionResourceLoaderDemo.class);
        applicationContext.refresh();
        applicationContext.close();
    }
​
    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoaderAware = resourceLoader;
    }
}

\