[死磕 Spring 2/43] --- IOC 之 Spring 统一资源加载策略

Posted wei198621

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[死磕 Spring 2/43] --- IOC 之 Spring 统一资源加载策略相关的知识,希望对你有一定的参考价值。

[死磕 Spring] — IOC 之 Spring 统一资源加载策略
参考文章: https://www.cmsblogs.com/article/1391375268060467201

在学 Java SE 的时候我们学习了一个标准类 java.net.URL,该类在 Java SE 中的定位为统一资源定位器(Uniform Resource Locator),但是我们知道它的实现基本只限于网络形式发布的资源的查找和定位。然而,实际上资源的定义比较广泛,除了网络形式的资源,还有以二进制形式存在的、以文件形式存在的、以字节流形式存在的等等。而且它可以存在于任何场所,比如网络、文件系统、应用程序中。所以 java.net.URL 的局限性迫使 Spring 必须实现自己的资源加载策略,该资源加载策略需要满足如下要求:

  • 职能划分清楚。资源的定义和资源的加载应该要有一个清晰的界限;
  • 统一的抽象。统一的资源定义和资源加载策略。资源加载后要返回统一的抽象给客户端,客户端要对资源进行怎样的处理,应该由抽象资源接口来界定。

统一资源:Resource

类结构图如下

  • InputStreamSource
  • Resource
  • AbstractResource
  • FileSystemResource
  • ByteArrayResource
  • UrlResource
  • ClassPathResource
  • InputStreamResource
  • ContextResource

org.springframework.core.io.Resource 为 Spring 框架所有资源的抽象和访问接口,它继承 org.springframework.core.io.InputStreamSource接口。作为所有资源的统一抽象,Resource 定义了一些通用的方法,由子类 AbstractResource 提供统一的默认实现。定义如下:

    public interface Resource extends InputStreamSource 
    
        /**
         * 资源是否存在
         */
        boolean exists();
    
        /**
         * 资源是否可读
         */
        default boolean isReadable() 
            return true;
        
    
        /**
         * 资源所代表的句柄是否被一个stream打开了
         */
        default boolean isOpen() 
            return false;
        
    
        /**
         * 是否为 File
         */
        default boolean isFile() 
            return false;
        
    
        /**
         * 返回资源的URL的句柄
         */
        URL getURL() throws IOException;
    
        /**
         * 返回资源的URI的句柄
         */
        URI getURI() throws IOException;
    
        /**
         * 返回资源的File的句柄
         */
        File getFile() throws IOException;
    
        /**
         * 返回 ReadableByteChannel
         */
        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 根据资源的不同类型提供不同的具体实现,如下:

  • FileSystemResource:对 java.io.File 类型资源的封装,只要是跟 File 打交道的,基本上与 FileSystemResource 也可以打交道。支持文件和 URL 的形式,实现 WritableResource 接口,且从 Spring Framework 5.0 开始,FileSystemResource 使用NIO.2 API进行读/写交互
  • ByteArrayResource:对字节数组提供的数据的封装。如果通过 InputStream 形式访问该类型的资源,该实现会根据字节数组的数据构造一个相应的 ByteArrayInputStream。
  • UrlResource:对 java.net.URL类型资源的封装。内部委派 URL 进行具体的资源操作。
  • ClassPathResource:class path 类型资源的实现。使用给定的 ClassLoader 或者给定的 Class 来加载资源。
  • InputStreamResource:将给定的 InputStream 作为一种资源的 Resource 的实现类。

AbstractResource 为 Resource 接口的默认实现,它实现了 Resource 接口的大部分的公共实现,作为 Resource 接口中的重中之重,其定义如下:

    public abstract class AbstractResource implements Resource 
    
        /**
         * 判断文件是否存在,若判断过程产生异常(因为会调用SecurityManager来判断),就关闭对应的流
         */
        @Override
        public boolean exists() 
            try 
                return getFile().exists();
            
            catch (IOException ex) 
                // Fall back to stream existence: can we open the stream?
                try 
                    InputStream is = getInputStream();
                    is.close();
                    return true;
                
                catch (Throwable isEx) 
                    return false;
                
            
        
    
        /**
         * 直接返回true,表示可读
         */
        @Override
        public boolean isReadable() 
            return true;
        
    
        /**
         * 直接返回 false,表示未被打开
         */
        @Override
        public boolean isOpen() 
            return false;
        
    
        /**
         *  直接返回false,表示不为 File
         */
        @Override
        public boolean isFile() 
            return false;
        
    
        /**
         * 抛出 FileNotFoundException 异常,交给子类实现
         */
        @Override
        public URL getURL() throws IOException 
            throw new FileNotFoundException(getDescription() + " cannot be resolved to URL");
        
    
        /**
         * 基于 getURL() 返回的 URL 构建 URI
         */
        @Override
        public URI getURI() throws IOException 
            URL url = getURL();
            try 
                return ResourceUtils.toURI(url);
            
            catch (URISyntaxException ex) 
                throw new NestedIOException("Invalid URI [" + url + "]", ex);
            
        
    
        /**
         * 抛出 FileNotFoundException 异常,交给子类实现
         */
        @Override
        public File getFile() throws IOException 
            throw new FileNotFoundException(getDescription() + " cannot be resolved to absolute file path");
        
    
        /**
         * 根据 getInputStream() 的返回结果构建 ReadableByteChannel
         */
        @Override
        public ReadableByteChannel readableChannel() throws IOException 
            return Channels.newChannel(getInputStream());
        
    
        /**
         * 获取资源的长度
         *
         * 这个资源内容长度实际就是资源的字节长度,通过全部读取一遍来判断
         */
        @Override
        public long contentLength() throws IOException 
            InputStream is = getInputStream();
            try 
                long size = 0;
                byte[] buf = new byte[255];
                int read;
                while ((read = is.read(buf)) != -1) 
                    size += read;
                
                return size;
            
            finally 
                try 
                    is.close();
                
                catch (IOException ex) 
                
            
        
    
        /**
         * 返回资源最后的修改时间
         */
        @Override
        public long lastModified() throws IOException 
            long lastModified = getFileForLastModifiedCheck().lastModified();
            if (lastModified == 0L) 
                throw new FileNotFoundException(getDescription() +
                        " cannot be resolved in the file system for resolving its last-modified timestamp");
            
            return lastModified;
        
        protected File getFileForLastModifiedCheck() throws IOException 
            return getFile();
        
        /**
         * 交给子类实现
         */
        @Override
        public Resource createRelative(String relativePath) throws IOException 
            throw new FileNotFoundException("Cannot create a relative resource for " + getDescription());
        
    
        /**
         * 获取资源名称,默认返回 null
         */
        @Override
        @Nullable
        public String getFilename() 
            return null;
        
        /**
         * 返回资源的描述
         */
        @Override
        public String toString() 
            return getDescription();
        
    
        @Override
        public boolean equals(Object obj) 
            return (obj == this ||
                (obj instanceof Resource && ((Resource) obj).getDescription().equals(getDescription())));
        
        @Override
        public int hashCode() 
            return getDescription().hashCode();
        
    

如果我们想要实现自定义的 Resource,记住不要实现 Resource 接口,而应该继承 AbstractResource 抽象类,然后根据当前的具体资源特性覆盖相应的方法即可。

统一资源定位:ResourceLoader

其类的类结构图如下:

  • ResourceLoader
  • DefaultResourceLoader
  • ClassRelativeResourceLoader
  • FileSystemResourceLoader
  • ResourcePatternResolver
  • PathMatchingResourcePatternResolver

一开始就说了 Spring 将资源的定义和资源的加载区分开了,Resource 定义了统一的资源,那资源的加载则由 ResourceLoader 来统一定义。

org.springframework.core.io.ResourceLoader 为 Spring 资源加载的统一抽象,具体的资源加载则由相应的实现类来完成,所以我们可以将 ResourceLoader 称作为统一资源定位器。其定义如下:

    public interface ResourceLoader 
        String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX;
        Resource getResource(String location);
        ClassLoader getClassLoader();
    

ResourceLoader 接口提供两个方法:getResource()、getClassLoader()。

ResourceLoader.getResource() 方法

getResource()根据所提供资源的路径 location 返回 Resource 实例,但是它不确保该 Resource 一定存在,需要调用 Resource.exist()方法判断。该方法支持以下模式的资源加载:

URL位置资源,如”file:C:/test.dat”
ClassPath位置资源,如”classpath:test.dat”
相对路径资源,如”WEB-INF/test.dat”,此时返回的Resource实例根据实现不同而不同

该方法的主要实现是在其子类 DefaultResourceLoader 中实现,具体过程我们在分析 DefaultResourceLoader 时做详细说明。

ResourceLoader.getClassLoader() 方法

getClassLoader() 返回 ClassLoader 实例,对于想要获取 ResourceLoader 使用的 ClassLoader 用户来说,可以直接调用该方法来获取,
在分析 Resource 时,提到了一个类 ClassPathResource ,这个类是可以根据指定的 ClassLoader 来加载资源的。
作为 Spring 统一的资源加载器,它提供了统一的抽象,具体的实现则由相应的子类来负责实现,

DefaultResourceLoader 类

DefaultResourceLoader 是 ResourceLoader 的默认实现,它接收 ClassLoader 作为构造函数的参数或者使用不带参数的构造函数,在使用不带参数的构造函数时,使用的 ClassLoader 为默认的 ClassLoader(一般为Thread.currentThread().getContextClassLoader()),可以通过 ClassUtils.getDefaultClassLoader()获取。当然也可以调用 setClassLoader()方法进行后续设置。如下:

        public DefaultResourceLoader() 
            this.classLoader = ClassUtils.getDefaultClassLoader();
        
    
        public DefaultResourceLoader(@Nullable ClassLoader classLoader) 
            this.classLoader = classLoader;
        
    
        public void setClassLoader(@Nullable ClassLoader classLoader) 
            this.classLoader = classLoader;
        
    
        @Override
        @Nullable
        public ClassLoader getClassLoader() 
            return (this.classLoader != null ? this.classLoader : ClassUtils.getDefaultClassLoader());
        

ResourceLoader 中最核心的方法为 getResource(),它根据提供的 location 返回相应的 Resource,而 DefaultResourceLoader 对该方法提供了核心实现(它的两个子类都没有提供覆盖该方法,所以可以断定ResourceLoader 的资源加载策略就封装 DefaultResourceLoader中),如下:

        public Resource getResource(String location) 
            Assert.notNull(location, "Location must not be null");
    
            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 (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
                
                catch (MalformedURLException ex) 
                    // No URL -> resolve as resource path.
                    return getResourceByPath(location);
                
            
        

首先通过 ProtocolResolver 来加载资源,成功返回 Resource,否则调用如下逻辑:

  • 若 location 以 / 开头,则调用 getResourceByPath()构造 ClassPathContextResource 类型资源并返回。
  • 若 location 以 classpath: 开头,则构造 ClassPathResource 类型资源并返回,在构造该资源时,通过 getClassLoader()获取当前的 ClassLoader。
  • 构造 URL ,尝试通过它进行资源定位,若没有抛出 MalformedURLException 异常,则判断是否为 FileURL , 如果是则构造 FileUrlResource 类型资源,否则构造 UrlResource。若在加载过程中抛出 MalformedURLException 异常,则委派 getResourceByPath() 实现资源定位加载。

ProtocolResolver ,用户自定义协议资源解决策略,作为 DefaultResourceLoader 的 SPI,它允许用户自定义资源加载协议,而不需要继承 ResourceLoader 的子类。在介绍 Resource 时,提到如果要实现自定义 Resource,我们只需要继承 DefaultResource 即可,但是有了 ProtocolResolver 后,我们不需要直接继承 DefaultResourceLoader,改为实现 ProtocolResolver 接口也可以实现自定义的 ResourceLoader。 ProtocolResolver 接口,仅有一个方法 Resource resolve(String location, ResourceLoader resourceLoader),该方法接收两个参数:资源路径location,指定的加载器 ResourceLoader,返回为相应的 Resource 。在 Spring 中你会发现该接口并没有实现类,它需要用户自定义,自定义的 Resolver 如何加入 Spring 体系呢?调用 DefaultResourceLoader.addProtocolResolver() 即可,如下:

        public void addProtocolResolver(ProtocolResolver resolver) 
            Assert.notNull(resolver, "ProtocolResolver must not be null");
            this.protocolResolvers.add(resolver);
        

下面示例是演示 DefaultResourceLoader 加载资源的具体策略,代码如下(该示例参考《Spring 解密》 P89):

            ResourceLoader resourceLoader = new DefaultResourceLoader();
    
            Resource fileResource1 = resourceLoader.getResource("D:/Users/chenming673/Documents/spark.txt");
            System.out.println("fileResource1 is FileSystemResource:" + (fileResource1 instanceof FileSystemResource));
    
            Resource fileResource2 = resourceLoader.getResource("/Users/chenming673/Documents/spark.txt");
            System.out.println("fileResource2 is ClassPathResource:" + (fileResource2 instanceof ClassPathResource));
    
            Resource urlResource1 = resourceLoader.getResource("file:/Users/chenming673/Documents/spark.txt");
            System.out.println("urlResource1 is UrlResource:" + (urlResource1 instanceof UrlResource));
    
            Resource urlResource2 = resourceLoader.getResource("http://www.baidu.com");
            System.out.println("urlResource1 is urlResource:" + (urlResource2 instanceof  UrlResource));

运行结果:

    fileResource1 is FileSystemResource:false
    fileResource2 is ClassPathResource:true
    urlResource1 is UrlResource:true
    urlResource1 is urlResource:true

其实对于 fileResource1 我们更加希望是 FileSystemResource 资源类型,但是事与愿违,它是 ClassPathResource 类型。在getResource()资源加载策略中,我们知道 D:/Users/chenming673/Documents/spark.txt资源其实在该方法中没有相应的资源类型,那么它就会在抛出 MalformedURLException 异常时通过 getResourceByPath() 构造一个 ClassPathResource 类型的资源。而指定有协议前缀的资源路径,则通过 URL 就可以定义,所以返回的都是UrlResource类型。

FileSystemResourceLoader 类

从上面的示例我们看到,其实 DefaultResourceLoader 对getResourceByPath(String)方法处理其实不是很恰当,这个时候我们可以使用 FileSystemResourceLoader ,它继承 DefaultResourceLoader 且覆写了 getResourceByPath(String),使之从文件系统加载资源并以 FileSystemResource 类型返回,这样我们就可以得到想要的资源类型,如下:

        @Override
        protected Resource getResourceByPath(String path) 
            if (path.startsWith("/")) 
                path = path.substring(1);
            
            return new FileSystemContextResource(path);
        

FileSystemContextResource 为 FileSystemResourceLoader 的内部类,它继承 FileSystemResource。

        private static class FileSystemContextResource extends FileSystemResource implements ContextResource 
    
            public FileSystemContextResource(String path) 
                super(path);
            
    
            @Override
            public String getPathWithinContext() 
                return getPath();
            
        

在构造器中也是调用 FileSystemResource 的构造方法来构造 FileSystemContextResource 的。

如果将上面的示例将 DefaultResourceLoader 改为 FileSystemContextResource ,则 fileResource1 则为 FileSystemResource。

ResourcePatternResolver

ResourceLoader 的 Resource getResource(String location) 每次只能根据 location 返回一个 Resource,当需要加载多个资源时,我们除了多次调用 getResource() 外别无他法。ResourcePatternResolver 是 ResourceLoader 的扩展,它支持根据指定的资源路径匹配模式每次返回多个 Resource 实例,其定义如下:

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

ResourcePatternResolver 在 ResourceLoader 的基础上增加了 getResources(String locationPattern),以支持根据路径匹配模式返回多个 Resource 实例,同时也新增了一种新的协议前缀 classpath*:,该协议前缀由其子类负责实现。

PathMatchingResourcePatternResolver

PathMatchingResourcePatternResolver 为 ResourcePatternResolver 最常用的子类,它除了支持 ResourceLoader 和 ResourcePatternResolver 新增的 classpath*: 前缀外,还支持 Ant 风格的路径匹配模式(类似于 **/*.xml)。
PathMatchingResourcePatternResolver 提供了三个构造方法,如下:

    public PathMatchingResourcePatternResolver() 
            this.resourceLoader = new DefaultResourceLoader();
        
    
        public PathMatchingResourcePatternResolver(ResourceLoader resourceLoader) 
            Assert.notNull(resourceLoader, "ResourceLoader must not be null");
            this.resourceLoader = resourceLoader;
        
    
        public PathMatchingResourcePatternResolver(@Nullable ClassLoader classLoader) 
            this.resourceLoader = new DefaultResourceLoader(classLoader);
        

PathMatchingResourcePatternResolver 在实例化的时候,可以指定一个 ResourceLoader,如果不指定的话,它会在内部构造一个 DefaultResourceLoader。
Resource getResource(String location)

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

getResource() 方法直接委托给相应的 ResourceLoader 来实现,所以如果我们在实例化的 PathMatchingResourcePatternResolver 的时候,如果不知道 ResourceLoader ,那么在加载资源时,其实就是 DefaultResourceLoader 的过程。其实在下面介绍的 Resource[] getResources(String locationPattern) 也相同,

以上是关于[死磕 Spring 2/43] --- IOC 之 Spring 统一资源加载策略的主要内容,如果未能解决你的问题,请参考以下文章

2死磕 Spring——IoC之深入理解Spring IoC

[死磕 Spring 4/43] --- IOC 之 获取验证模型

[死磕 Spring 15/43] --- IOC 之 IOC 初始化总结

[死磕 Spring 01/43 ] 号外02 通俗解释一下Spring的IOC原理

[死磕 Spring 7/43] --- IOC 之解析Bean:解析 import 标签

[死磕 Spring 36/43] --- IOC 之分析 BeanWrapper