转载

【修炼内功】[spring-framework][1]Resource

本文已收录【修炼内功】跃迁之路

【修炼内功】[spring-framework][1]Resource

【修炼内功】[spring-framework][1]Resource

阅读源码是一件极其枯燥无比的事情,对于使用频率较高的组件,如果能做到知其然且知其所以然,这对日常工作中不论是问题排查、代码优化、功能扩展等都是利大于弊的,如同老司机开车(对,就是开车),会让你有一种参与感,而不仅仅把它当成一种工具,若能习之精髓、学以致用,那便再好不过!

从工作之初便开始接触Spring框架,时至今日也没有认真地正视过它的实现细节,今日开拔,希望能够坚持下来~

对于Spring如此“庞大”(至少与我而言)的框架,不想一上来就将level提的很高,以上帝的视角将整个Spring框架的架构图或者类图之类抛出来,对于并不特别了解的人来说,除了膜拜Spring的“宏伟”之外别无他法,依然不清楚应该如何下手

这里,希望能够循序渐进,将Spring的几个核心组件各个击破,再将各组件串联起来,以点至面

Spring系列文章

  • 默认您对Spring框架有一定的了解及使用经验
  • 既然是源码分析便不可避免地会贴一些源码,会尽量以精简代码、伪代码、额外注释等方式呈现,以减少源码所占的篇幅
  • spring-framework
  • spring-boot

言归正传,本篇就Spring的资源Resource聊起

Resource

Resource(资源)是进入Spring生态的第一道门,不敢说它是Spring的基石,但绝对是Spring的核心组件之一

Resource主要负责资源的(读写)操作,最为常见地出现在系统各初始化阶段,如

  • 指定配置文件,手动创建 ApplicationContext
new ClassPathXmlApplicationContext("classpath:spring/application.xml");
  • 在web.xml中指定配置文件,用于Spring初始化时加载
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring/application.xml</param-value>
</context-param>
  • 指定package,在扫描过程中查找指定package下class文件
@Configuration
@ComponentScan("com.manerfan")
public class AppConfiguration {}
<context:component-scan base-package="com.manerfan"></context:component-scan>
  • (Spring Boot中)指定mybatis配置,用于mybatis初始化时加载
mybatis.config-location=classpath:/mybatis/mybatis-config.xml
  • 依赖注入时加载指定资源文件
@Component
public class SomeComponent {
    @Value("classpath:in18/zh-cn.properties")
    private Resource in18ZhCn;
}
<bean id="someComponent" class="...SomeComponent">
    <property name="in18ZhCn" value="classpath:in18/zh-cn.properties"/>
</bean>
  • Spring Boot加载启动配置文件 application[-env].properties application[-env].yml ,加载 META-INF 配置文件等

Java中的 URL 通过不同的前缀(协议)已经实现了一套资源的读取,如磁盘文件 file:///var/log/system.log 、网络文件 https://some.host/some.file.txt 甚至jar中的class jar:file:///spring-core.jar!/org/springframework/core/io/Resource.class ,然而Spring并没有采用 URL 的方案,其官方文档给出了一定的解释

https://docs.spring.io/spring...

Java’s standard java.net.URL class and standard handlers for various URL prefixes, unfortunately, are not quite adequate enough for all access to low-level resources. For example, there is no standardized URL implementation that may be used to access a resource that needs to be obtained from the classpath or relative to a ServletContext . While it is possible to register new handlers for specialized URL prefixes (similar to existing handlers for prefixes such as http: ), this is generally quite complicated, and the URL interface still lacks some desirable functionality, such as a method to check for the existence of the resource being pointed to.

其一, URL 扩展复杂;其二, URL 功能有限

Spring将不同类型的资源统一抽象成了 Resource ,这有点类似Linux系统的“一切皆文件”(磁盘文件、目录、硬件设备、套接字、网络等),资源的抽象屏蔽了不同类型资源的差异性,统一了操作接口

⇪Resource 的定义非常简洁明了,方法的命名已经足够清晰,不再统一解释

public interface Resource extends InputStreamSource {
    boolean exists();
    boolean isReadable();
    boolean isOpen();
    boolean isFile()
    URL getURL() throws IOException;
    URI getURI() throws IOException;
    File getFile() throws IOException;
    ReadableByteChannel readableChannel() throws IOException;
    long contentLength() throws IOException;
    long lastModified() throws IOException;
    Resource createRelative(String relativePath) throws IOException;
    String getFilename();
    String getDescription();
    // ...
}

Resource 继承自更为抽象的 ⇪InputStreamSource

public interface InputStreamSource {
    String CLASSPATH_URL_PREFIX = "classpath:";
    InputStream getInputStream() throws IOException;
}

其只有一个方法 getInputStream ,用于获取资源的 InputStream

对于 Resouce 的具体实现可参考下图(莫被错综复杂的类关系扰乱了思路),一层层剥离解析

【修炼内功】[spring-framework][1]Resource

WritableResource

⇪WritableResource 派生自 Resource ,其在 Resource 的基础上增加了'写'相关的能力

public interface WritableResource extends Resource {
    boolean isWritable();
    OutputStream getOutputStream() throws IOException;
    WritableByteChannel writableChannel() throws IOException;
}

这里重点关注 Resource '读'能力的实现

AbstractResource

⇪AbstractResource 实现了大部分 Resource 中公共的、无底层差异的逻辑,实现较为简单,不再详述

AbstractResource 的具体实现类则是封装了不同类型的资源类库,使用具体的类库函数实现 Resource 定义的一系列接口

⇪FileSystemResource 封装了 java.io.File (或 java.io.Path )的能力实现了 Resource 的一些细节

public class FileSystemResource extends AbstractResource implements WritableResource {
    @Override
    public InputStream getInputStream() throws IOException {
        // ...
        // 将File/Path封装为InputStream
        return Files.newInputStream(this.filePath);
        // ...
    }
    
    @Override
    public OutputStream getOutputStream() throws IOException {
        // 将File/Path封装为OutputStream
        return Files.newOutputStream(this.filePath);
    }
    
    @Override
    public URL getURL() throws IOException {
        return (this.file != null ? this.file.toURI().toURL() : this.filePath.toUri().toURL());
    }

    @Override
    public URI getURI() throws IOException {
        return (this.file != null ? this.file.toURI() : this.filePath.toUri());
    }
    
    // ...
}

同理, ⇪ByteArrayResource 封装了 ByteArray 的能力, ⇪InputStreamResource 封装了 InputSream 的能力,等等,不再一一介绍

AbstractFileResolvingResource

⇪AbstractFileResolvingResource 则把中心放在 java.net.URL 上,其使用 URL 的能力重写了其父类 AbstractResource 的大部分实现

AbstractFileResolvingResource 的实现类只有两个, ⇪UrlResource⇪ClassPathResource

UrlResource

UrlResource 同样简单地封装了 URL 的能力来实现 Resource 中定义的接口

public class UrlResource extends AbstractFileResolvingResource {
    @Override
    public InputStream getInputStream() throws IOException {
        URLConnection con = this.url.openConnection();
        ResourceUtils.useCachesIfNecessary(con);
        try {
            return con.getInputStream();
        }
        catch (IOException ex) {
            // Close the HTTP connection (if applicable).
            if (con instanceof HttpURLConnection) {
                ((HttpURLConnection) con).disconnect();
            }
            throw ex;
        }
    }
    
    // ...
}

ClassPathResource

ClassPathResource 则是借助 ClassLoader 的能力来实现 Resource 中定义的接口

public class ClassPathResource extends AbstractFileResolvingResource {
    @Override
    public InputStream getInputStream() throws IOException {
        InputStream is;
        if (this.clazz != null) {
            is = this.clazz.getResourceAsStream(this.path);
        }
        else if (this.classLoader != null) {
            is = this.classLoader.getResourceAsStream(this.path);
        }
        else {
            is = ClassLoader.getSystemResourceAsStream(this.path);
        }
        if (is *** null) {
            throw new FileNotFoundException(getDescription() + " cannot be opened because it does not exist");
        }
        return is;
    }
}

EncodedResource

细心的可能会发现,这里还有一个 ⇪EncodedResource ,其在 Resource 的基础上加入了编码信息,并提供了额外的 getReader 接口

public class EncodedResource implements InputStreamSource {
    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());
        }
    }
}

这对于获取编码格式有要求的资源来讲十分受用

@Component
public class MyComponent {
    private final Properties properties;
    public MyComponent(@Value("classpath:/config/my-config.properties") Resource resource) {
        // Properties读取配置默认编码为ISO-8859-1
        this.properties = PropertiesLoaderUtils.loadProperties(new EncodedResource(resource, "utf-8"));
    }
    
    // ...
}

小结

  • ⇪Resource 将资源的操作抽象,屏蔽不同类型资源差异性,统一操作接口
  • ⇪FileSystemResource⇪ByteArrayResource⇪InputStreamResource⇪UrlResource⇪ClassPathResource 等,借助相应的资源类库能力,实现 Resource 中定义的接口

    • FileSystemResourceFile or Path
    • ByteArrayResourceByteArray
    • InputStreamResourceInputStream
    • UrlResourceUrl
    • ClassPathResourceClassLoader
    • ...

使用上,针对不同的资源类型创建不同的 Resource 即可

new FileSystemResource("/var/log/system.log"); // 文件系统中的文件
new ClassPathResource("/config/my-config.properties"); // classpath中的文件
new UrlResource("http://oss.manerfan.com/config/my-config.properties"); // 网络上的文件

ResourceLoader

针对不同的资源类型创建不同的 Resource ”,如上例中的硬编码并不符合开闭原则,对于开发者来说其实并不那么友好,还好Spring提供了 ⇪ResourceLoader (接下来,你会发现Spring中提供了各种各样的 Loader Resolver Aware 等等)

public interface ResourceLoader {
    Resource getResource(String location);
}

ResourceLoader 中定义了 getResource 方法用于创建 合适 类型的 Resource ,至于应该创建哪种类型以及如何创建,则交由 ResourceLoader 处理

ResourceLoader 的实现类主要有两种,其一为 ⇪DefaultResourceLoader ,其二为 ⇪PathMatchingResourcePatternResolver

【修炼内功】[spring-framework][1]Resource

DefaultResourceLoader

⇪DefaultResourceLoaderResourceLoader 的默认实现,其实现极为简单

public class DefaultResourceLoader implements ResourceLoader {
    @Override
    public Resource getResource(String location) {
        Assert.notNull(location, "Location must not be null");

        // 1. 如果存在自定义的解析器,优先使用自定义解析器
        
        for (ProtocolResolver protocolResolver : getProtocolResolvers()) {
            Resource resource = protocolResolver.resolve(location, this);
            if (resource != null) {
                return resource;
            }
        }

        // 2. 如果没有自定义解析器,或者自定义解析器无法解析,则使用默认实现
        
        if (location.startsWith("/")) {
            // 构造ClassPathResource
            return getResourceByPath(location);
        }
        else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
            // 取"classpath:"后的内容,构造ClassPathResource
            return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
        }
        else {
            try {
                // 尝试构造URLResource
                URL url = new URL(location);
                return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
            }
            catch (MalformedURLException ex) {
                // 降级到ClassPathResource
                return getResourceByPath(location);
            }
        }
    }
    
    protected Resource getResourceByPath(String path) {
        return new ClassPathContextResource(path, getClassLoader());
    }
}

首先会尝试使用自定义解析器解析(通过 addProtocolResolver 方法添加),如果没有或者解析失败才会使用默认实现

默认实现逻辑中,如果是以 /classpath: 开头的,会直接构造 ClassPathResource ,否则会尝试构造为 UrlResource

了解 ClassPathResource 实现的会注意到,如果在所有的classpath路径中同时存在多个文件匹配(如,同时在 a.jar b.jar 中存在 /config/my-config.properties 文件),则只会返回首个匹配到的(取决于JVM加载顺序),这也是有别于 PathMatchingResourcePatternResolver 的一个地方

PathMatchingResourcePatternResolver

⇪PathMatchingResourcePatternResolver 实现自 ⇪ResourcePatternResolver 接口

public interface ResourcePatternResolver extends ResourceLoader {
    String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
    Resource[] getResources(String locationPattern) throws IOException;
}

ResourcePatternResolverResourceLoader 的基础上,增加了批量获取的接口 getResources

默认情况下, PathMatchingResourcePatternResolvergetResource 实现其实是使用了 DefaultResourceLoader (当然你也可以自己指定默认的 ResourceLoader 实现)

public class PathMatchingResourcePatternResolver implements ResourcePatternResolver {
    public PathMatchingResourcePatternResolver() {
        this.resourceLoader = new DefaultResourceLoader();
    }
    
    @Override
    public Resource getResource(String location) {
        return getResourceLoader().getResource(location);
    }
}

PathMatchingResourcePatternResolver 的一大特点在于 PathMatching (默认使用 AntPathMatcher ,也可以指定),对于类似 classpath:/config/my-*.properties classpath*:/config/my-*.properties 等Ant风格的资源进行匹配,其基本思路大致为

  1. 找到Ant风格资源的父目录 classpath:/config/ classpath*:/config/
  2. 在该目录下查找匹配 my-*.properties 的资源(具体实现集中在 ⇪findPathMatchingResources 方法)

classpath: classpath*: 的区别主要在于父目录的查找逻辑

classpath:

父目录的查找借助 DefaultResourceLoader 的能力(归根结底使用了 ClassLoader.getResource ),上文也有提到,这里只会返回首个匹配到的目录资源

@Override
public Resource[] getResources(String locationPattern) throws IOException {
    // ... 各种 if-else 之后
    // a single resource with the given name
    return new Resource[] {getResourceLoader().getResource(locationPattern)};
}

classpath*:

父目录的查找则直接使用 ClassLoader.getResources ,返回所有classpath中的目录资源

@Override
public Resource[] getResources(String locationPattern) throws IOException {
    // ... 各种 if-else 之后
    // all class path resources with the given name
    return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
}

protected Resource[] findAllClassPathResources(String location) throws IOException {
    // ...
    Set<Resource> result = doFindAllClassPathResources(path);
    // ...
}

protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {
    // ...
    Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path));
    // ...
}

所以, classpath:/config/my-*.properties 只会返回首个匹配到的 /config/ 目录中所有的 my-*.properties 资源,而 classpath*:/config/my-*.properties 则会返回所有匹配到的 /config/ 目录中所有的 my-*.properties 资源

AbstractApplicationContext

AbstractApplicationContext 同时实现了 ResourcePatternResolver 接口并继承了 DefaultResourceLoader

public abstract class AbstractApplicationContext extends DefaultResourceLoader
        implements ConfigurableApplicationContext {
    public AbstractApplicationContext() {
        this.resourcePatternResolver = getResourcePatternResolver();
    }

    protected ResourcePatternResolver getResourcePatternResolver() {
        return new PathMatchingResourcePatternResolver(this);
    }
}

所以,如开篇的几个例子里,在 ApplicationContext 中获取 Resource 资源,大多数情况下使用的都是 PathMatchingResourcePatternResolver

小结

  • Spring提供了 ResourceLoader 根据不同的前缀(协议)生成相对应的 Resource (Spring中提供了各种各样的 Loader Resolver Aware 等等)
  • DefaultResourceLoader 只能获取单个资源,且只能获取classpath中首次匹配到的资源( ClassLoader.getResource
  • PathMatchingResourcePatternResolver 可以使用Ant风格匹配并返回多个资源

    • classpath: 前缀,只会返回首个匹配到的 根目录 中( ClassLoader.getResource )所有的满足给定Ant规则的资源
    • classpath*: 前缀,则会返回所有匹配到的 根目录 中( ClassLoader.getResources )所有的满足给定Ant规则的资源
  • ApplicationContext 默认使用 PathMatchingResourcePatternResolver 获取 Resource 资源

总结

  • ⇪Resource 将资源的操作抽象,屏蔽不同类型资源差异性,统一操作接口
  • Resource 的不同实现,均借助相应的资源类库能力,来实现 Resource 中定义的接口
  • ResourceLoader 根据不同的前缀(协议)生成相对应的 Resource

    • DefaultResourceLoader 只能获取单个资源,且只能获取classpath中首次匹配到的资源
    • PathMatchingResourcePatternResolver 可以使用Ant风格匹配并返回多个资源, classpath: classpath*: 的区别在于如何获取 根目录 以在其中查找匹配Ant风格的资源
  • ApplicationContext 默认使用 PathMatchingResourcePatternResolver 获取 Resource 资源

【修炼内功】[spring-framework][1]Resource

原文  https://segmentfault.com/a/1190000021176074
正文到此结束
Loading...