Spring源码系列 — Resource抽象

Posted 怀瑾握瑜

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring源码系列 — Resource抽象相关的知识,希望对你有一定的参考价值。

前言

前面两篇介绍了上下文的启动流程和Environemnt的初始化,这两部分都是属于上下文自身属性的初始化。这篇开始进入Spring如何加载实例化Bean的部分 — 资源抽象与加载。

本文主要从以下方面介绍Spring中的资源Resource:

  • 前提准备
  • Resource抽象
  • Resource加载解析
  • 何时何地触发加载解析
  • 总结

前提准备

Spring中的资源抽象使用到了很多陌生的api,虽然这些api都是JDK提供,但是日常的业务场景中很少使用或者接触不深。在阅读Resource的源码前,需要完善知识体系,减轻阅读Resource实现的难度。

1.URL和URI

URL代表Uniform Resource Locator,指向万维网上的一个资源。资源可以是文件、声音、视频等。
URI代表Uniform Resource Identifier,用于标识一个特定资源。URL是URI的一种,也可以用于标识资源。

URI的语法如下:

不在第一条直线上的部分都是可选。关于更多URL和URI的详细信息可以参考URLURI

在Java中提供了两个类分别表示URL和URI。两者在描述资源方面提供了很多相同的属性:protocol(scheme)/host/port/file(path)等等。但是URL除此还提供了建立Tcp连接,获取Stream的操作。如:

public URLConnection openConnection() throws java.io.IOException {
    return handler.openConnection(this);
}

public final InputStream openStream() throws java.io.IOException {
    return openConnection().getInputStream();
}

因为URL表示网络中的资源路径,所以它能够提供网络操作获取资源。Spring中包含UrlResource即是对URL的封装,提供获取资源的便捷性。

2.Class和ClassLoader

Class是Java中对象类型。Class对象提供了加载资源的能力,根据资源名称搜索,返回资源的URL:

public java.net.URL getResource(String name) {
	...省略
}

ClassLoader是类加载器,它同样也提供了加载资源的能力:

// 搜索单个资源
public URL getResource(String name) {
    ...省略
}

// 搜索匹配的多个资源
public Enumeration<URL> getResources(String name) throws IOException {
    ...省略
}

Class中getResource委托加载该Class的ClassLoader搜索匹配的资源。搜索规则:委托父类加载器搜索,父类加载器为空再有启动类加载器搜索。搜索结果为空,再由该类加载器的findResources搜索。

在Spring中,加载类路径上的资源就由ClassLoader.getResources完成。

3.URLClassLoader

URLClassLoader是Java中用于从搜索路径上加载类和资源,搜索路径包括JAR文件和目录。
在Spring中利用其获得所有的搜索路径—即JAR files和目录,然后从目录和JAR files中搜索匹配特定的资源。如:
Spring支持Ant风格的匹配,当搜索模式*.xml的资源时,无法通过classLoader.getResources获取,故Spring自实现获取匹配该模式的资源。

URLClassLoader提供接口获取所有的搜索路径:

/**
 * Returns the search path of URLs for loading classes and resources.
 * This includes the original list of URLs specified to the constructor,
 * along with any URLs subsequently appended by the addURL() method.
 * @return the search path of URLs for loading classes and resources.
 */
public URL[] getURLs() {
    return ucp.getURLs();
}
3.JarFile和JarEntry

对于JarFile和JarEntry类,笔者自己也未曾在工作中使用过。不过从命名上也可以看出一些猫腻。
JarFile用于表示一个Jar,可以利用其api读取Jar中的内容。
JarEntry用于表示Jar文件中的条目,比如:org/springframework/context/ApplicationContext.class

通过JarFile提供的entries接口可以获取jar文件中所有的条目:

public Enumeration<JarEntry> entries() {
    return new JarEntryIterator();
}

通过遍历条目,可以达到从Jar文件中检索需要的条目,即class。

4.JarURLConnection

同上,笔者之前也曾为接触JarURLConnection。JarURLConnection是URLConnection的实现类,表示对jar文件或者jar文件中的条目的URL连接。提供了获取输入流的能力,例外还提供获取JarFile对象的api。

语法如下:

jar:<url>!/{entry}

jar表示文件类型为jar,url表示jar文件位置,"!/"表示分隔符。

例如:

// jar条目
jar:http://www.foo.com/bar/baz.jar!/COM/foo/Quux.class

// jar文件
jar:http://www.foo.com/bar/baz.jar!/

// jar目录
jar:http://www.foo.com/bar/baz.jar!/COM/foo/

JarURLConnection提供获取JarFile和JarEntry对象的api:

public abstract JarFile getJarFile() throws IOException;

public JarEntry getJarEntry() throws IOException {
    return getJarFile().getJarEntry(entryName);
}

Tips
在Spring的Resource实现中,主要使用到了这些平时很少使用的陌生api。在阅读Resource实现前,非常有必要熟悉这些api。

Resource抽象

在Spring中的资源的抽象非常复杂,根据资源位置的不同,分别实现了纷繁复杂的资源。整体Resource的UML类图如下:

Note:
Spring中Resource模块使用了策略模式,上层实现统一的资源抽象接口,针对不同的Resource类型,分别封装各自的实现,然后在相应的场景中组合使用相应的资源类型。其中对于多态、继承的应用可谓淋漓尽致。

从以上的UML类图中可以看出:

  1. 将输入流作为源头的对象抽象为接口,可以表示输入流源;
  2. Spring统一抽象Resource接口,表示应用中的资源,如文件或者类路径上的资源。Resource继承上述的输入流源,则Resource抽象具有获取资源内容的能力;
  3. 在上图的下部,分别是Resource接口的具体实现。根据资源的表示方式不同,分为:
    文件系统Resource、字节数组Resource、URL的Resource、类路劲上的Resource等等;
1.UrlResource

UrlResource通过包装URL对象达到方位目标资源,目标资源包括:file、http网络资源、ftp文件资源等等。URL协议类型:"file:"文件系统、"http:"http协议、"ftp:"ftp协议。

pulic class UrlResource extends AbstractFileResolvingResource {
   private final URI uri;

   // 代表资源位置的url
   private final URL url;

	// 通过uri构造UrlResource对象
   public UrlResource(URI uri) throws MalformedURLException {
       Assert.notNull(uri, "URI must not be null");
       this.uri = uri;
       this.url = uri.toURL();
       this.cleanedUrl = getCleanedUrl(this.url, uri.toString());
   }
   // 通过url构造UrlResource对象
   public UrlResource(URL url) {
       Assert.notNull(url, "URL must not be null");
       this.url = url;
       this.cleanedUrl = getCleanedUrl(this.url, url.toString());
       this.uri = null;
   }
   // 通过path路径构造UrlResource对象
   public UrlResource(String path) throws MalformedURLException {
       Assert.notNull(path, "Path must not be null");
       this.uri = null;
       this.url = new URL(path);
       this.cleanedUrl = getCleanedUrl(this.url, path);
   }

   ...省略
}
2.ClassPathResource

ClassPathResource代表类路径资源,该资源可以由类加载加载。如果该资源在文件系统中,则支持使用getFile接口获取该资源对应的File对象;如果该资源在jar文件中但是无法扩展至文件系统中,则不支持解析为File对象,此时可以使用URL加载。

public class ClassPathResource extends AbstractFileResolvingResource {
    // 文件在相对于类路径的路径
    private final String path;
    // 指定类加载器
    private ClassLoader classLoader;
    private Class<?> clazz;

    public ClassPathResource(String path) {
        this(path, (ClassLoader) null);
    }
    public ClassPathResource(String path, 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;
        this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());
    }
}
3.FileSystemResource

FileSystemResource代表文件系统上的资源。可以通过getFile和getURL接口获取对应的File和URL对象。

public class FileSystemResource extends AbstractResource implements WritableResource {
    // 代表资源的File对象
    private final File file;
    // 文件系统路径
    private final String path;

    public FileSystemResource(File file) {
        Assert.notNull(file, "File must not be null");
        this.file = file;
        this.path = StringUtils.cleanPath(file.getPath());
    }
    public FileSystemResource(String path) {
        Assert.notNull(path, "Path must not be null");
        this.file = new File(path);
        this.path = StringUtils.cleanPath(path);
    }
}
4.ByteArrayResource

ByteArrayResource将字节数组byte[]包装成Resource。

5.InputStreamResource

InputStreamResource将输入流InputStream包装成Resource对象。

Spring文档Tips:
虽然Resouce为Spring框架设计和被Spring框架内部大量使用。但是Resource还可以作为通用的工具模块使用,日常的应用开发过程中涉及到资源的处理,推荐使用Resource抽象,因为Resource提供了操作资源的便捷接口,可以简化对资源的操作。虽然耦合Spring,但是在Spring产品的趋势下,还会有耦合?

Resource加载解析

Resource加载是基于XML配置Spring上下文的核心基础模块。Spring提供了强大的加载资源的能力。可以分为两种模式:

  • 根据简单的资源路径,加载资源;
  • 根据复杂的的资源路径:Ant-Style、classpath:、classpath*:等等,解析如此复杂的资源路径,加载资源;

Spring依次抽象出ResourceLoader和ResourcePatternResolver两部分实现以上的两种情况:

  • ResourceLoader纯粹的加载资源;
  • ResourcePatternResolver负责解析复杂多样的资源路径并加载资源,它本身也是加载器的一种,实现ResourceLoader接口;

Note:
策略模式是一个传神的模式,在Spring中最让我感叹的两个模式之一,在Spring中随处可见,可能源于策略模式是真的易扩展而带来的随心所欲的应对各种场景带来的效果吧。这里定义策略接口ResourceLoader,根据不同的场景实现相应的资源加载器,是典型策略的应用方式。

Spring对加载资源定义顶层接口ResourecLoader:

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);
    // ResourceLoader是利用java的类加载器实现资源的加载
    ClassLoader getClassLoader();
}

该接口是加载资源的策略接口,Spring中加载资源模块的最顶层定义。Spring应用需要配置各种各样的配置,这些决定上下文具有加载资源的能力。在Spring中所有上下文都继承了ResouceLoader接口,加载资源是Spring上下文的基本能力。

public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory,
		MessageSource, ApplicationEventPublisher, ResourcePatternResolver {

统一的上下文接口继承了ResoucePatternResolver接口,间接继承了ResourceLoader。

从ResouceLoader的接口定义上也可以看出,ResourceLoader只能加载单个资源,功能比较简单。其有个默认实现DefaultResourceLoader,在看DefaultResourceLoader之前,首先了解DefaultResourceLoader的SPI。

Spring提供了ProtocolResolver策略接口,也是策略模式的应用,为了解决特定协议的资源解析。被用于DefaultResourceLoader,使其解析资源的能力得以扩展。

public interface ProtocolResolver {
	 // 根据特定路径解析资源
    Resource resolve(String location, ResourceLoader resourceLoader);
}

应用可以自实现该接口,将其实现加入,如:

/**
 * 实现对http协议URL资源的解析
 *
 * @author huaijin
 */
public class FromHttpProtocolResolver implements ProtocolResolver {

    @Override
    public Resource resolve(String location, ResourceLoader resourceLoader) {
        if (location == null || location.isEmpty() || location.startsWith("http://")) {
            return null;
        }
        byte[] byteArray;
        InputStream inputStream = null;
        try {
            URL url = new URL(location);
            inputStream = url.openStream();
            byteArray = StreamUtils.copyToByteArray(inputStream);
        } catch (MalformedURLException e) {
            throw new RuntimeException("location isn\'t legal url.", e);
        } catch (IOException e) {
            throw new RuntimeException("w/r err.", e);
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return new ByteArrayResource(byteArray);
    }
}


// 将其加入上下文容器
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
context.addProtocolResolver(new FromHttpProtocolResolver());

DefaultResouceLoader中部分源代码如下:

public class DefaultResourceLoader implements ResourceLoader {

    // 类加载器,可以编程式设置
    private ClassLoader classLoader;

    // 协议解析器集合
    private final Set<ProtocolResolver> protocolResolvers = new LinkedHashSet<ProtocolResolver>(4);

    // 增加协议解析器
    public void addProtocolResolver(ProtocolResolver resolver) {
        Assert.notNull(resolver, "ProtocolResolver must not be null");
        this.protocolResolvers.add(resolver);
    }


    // 加载单个资源实现
    @Override
    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;
            }
        }

        // 资源路径以"/"开头,表示是类路径上下文资源ClasspathContextResource
        if (location.startsWith("/")) {
            return getResourceByPath(location);
        }
        // 资源以"classpath:"开头,表示是类路径资源ClasspathResource
        else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
            return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
        }
        else {
            // 否则认为是路径时URL,尝试作为URL解析
            try {
                // Try to parse the location as a URL...
                URL url = new URL(location);
                return new UrlResource(url);
            }
            catch (MalformedURLException ex) {
                // 如果不是URL,则再次作为类路径上下文资源ClasspathContextResource
                // No URL -> resolve as resource path.
                return getResourceByPath(location);
            }
        }
    }


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

Note:
DefaultResourceLoader实现整体比较简单,但是值得借鉴的是使用ProtocolResolver扩展机制,可以认为是预留钩子。
所有ApplicationContext都继承了ResourceLoader接口从而具有了资源加载的基本能力,但是对于ApplicationContext都去主动实现该接口无疑使ApplicationContext强耦合资源加载能力,不易加载能力的扩展。Spring这里的设计非常精妙,遵循接口隔离原则。资源加载能力单独隔离成ResourceLoader接口,使其独立演变。ApplicationContext通过继承该接口而具有资源加载能力,ApplicationContext的实现中再继承或者组合ResourceLoader的实现DefaultResourceLoader。这样ResourceLoader可以自由扩展,而不影响ApplicationContext。当然Spring这里采用了AbstractApplicationContext继承DefaultResourceLoader。

Tips:
ResourceLoader虽然在Spring框架大量应用,但是ResourceLoader可以作为工具使用,可以极大简化代码。强力推荐应用中加载资源时使用ResourceLoader,应用可以通过继承ResouceLoaderAware接口或者@Autowired注入ResouceLoader。当然也可以通过ApplicationContextAware获取ApplicationContext作为ResouceLoader使用,但是如此,无疑扩大接口范围,有违封装性。详细参考:https://docs.spring.io/spring/docs/4.3.20.RELEASE/spring-framework-reference/htmlsingle/#resources

Spring另外一种资源加载方式ResourcePatternResolver是Spring中资源加载的核心。ResourcePatternResolver本身也是ResourceLoader的接口扩张。其特点:

  • 支持解析复杂化的资源路径
  • 加载多资源
public interface ResourcePatternResolver extends ResourceLoader {

    String CLASSPATH_ALL_URL_PREFIX = "classpath*:";

    Resource[] getResources(String locationPattern) throws IOException;
}

getResources的定义决定了ResourcePatternResolver具有根据资源路径模式locationPattern加载多资源的能力。
并且提供了新的模式:在所有的类路径上"classpath*:"。

Note:
这里又使用到了策略模式,ResourcePatternResolver是策略接口,可以根据不同的路径模式封装实现相应的实现。有没有感觉到策略模式无处不在!

顶层上下文容器ApplicationContext通过继承ResourcePatternResolver使其具有按照模式解析加载资源的能力。这里不再赘述,前文接受ResourceLoader时有所描述。

ResourcePatternResolver的路径模式非常多,首先这是不确定的。根据不同的模式,有相应的实现。PathMatchingResourcePatternResolver是其标准实现。
在深入PathMatchingResourcePatternResolver的源码前,先了解下Ant—Style,因为PathMatchingResourcePatternResolver是ResourcePatternResolver在支持Ant-Style模式和classpath*模式的实现:

  1. "*"代表一个或者多个字符,如模式beans-*.xml可以匹配beans-xxy.xml;
  2. "?"代表单个字符,如模式beans-?.xml可以匹配beans-1.xml;
  3. "**"代表任意路径,如模式/**/bak.txt可以匹配/xxx/yyy/bak.txt;

在Spring中有Ant-Sytle匹配器AntPathMatcher。该匹配实现接口PathMatcher:

public interface PathMatcher {

	 // 判断给定模式路径是否为指定模式
    boolean isPattern(String path);

   	 // 判断指定模式是否能匹配指定路径path
    boolean match(String pattern, String path);
}

PathMatcher是一个策略接口,表示路径匹配器,有默认Ant-Style匹配器实现AntPathMatcher。

Note:
这里仍然使用策略模式。组合是使用策略模式的前提!

再回到PathMatchingResourcePatternResolver,其支持:

  • 解析Ant-Style路径;
  • 解析classpath*模式路径;
  • 解析classpath*和Ant-Style结合的路径;
  • 加载以上解析出来的路径上的资源;

PathMatchingResourcePatternResolver中持有AntPathMatcher实现对Ant-Style的解析,持有ResourceLoader实现对classpath*的解析。PathMatchingResourcePatternResolver中成员持有关系:

public class PathMatchingResourcePatternResolver implements ResourcePatternResolver {
    // 持有resouceLoader
    private final ResourceLoader resourceLoader;
    // 持有AntPathMatcher
    private PathMatcher pathMatcher = new AntPathMatcher();

    public PathMatchingResourcePatternResolver() {
        this.resourceLoader = new DefaultResourceLoader();
    }
    // 指定ResourceLoader构造
    public PathMatchingResourcePatternResolver(ResourceLoader resourceLoader) {
        Assert.notNull(resourceLoader, "ResourceLoader must not be null");
        this.resourceLoader = resourceLoader;
    }
}

在PathMatchingResourcePatternResolver中核心的方法数ResourcePatternResolver中定义的getResources的实现,其负责加载多样模式路径上的资源:

@Override
public Resource[] getResources(String locationPattern) throws IOException {
    Assert.notNull(locationPattern, "Location pattern must not be null");
    // 1.路径模式是否以classpath*开头
    if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
        // 路径模式是否为指定模式,这里指定模式是Ant-Style,即判断路径模式是否为Ant-Style
        // a class path resource (multiple resources for same name possible)
        if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {
            // a class path resource pattern
            // 1-1.如果是Ant-Style,则在所有的类路径上查找匹配该模式的资源
            return findPathMatchingResources(locationPattern);
        }
        else {
            // 1-2.如果不是Ant-Style,则在所有类路径上查找精确匹配该名称的的资源
            // all class path resources with the given name
            return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
        }
    }
    // 2.不是以classpath*开头
    else {
        // Generally only look for a pattern after a prefix here,
        // and on Tomcat only after the "*/" separator for its "war:" protocol.
        // tomcat的war协议比较特殊,路径模式在war协议的*/后面,需要截取*/的路径模式
        int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 :
                locationPattern.indexOf(\':\') + 1);
        // 判断路径模式是否为指定的模式,这里是Ant-Style,即判断路径模式是否为Ant-Style
        if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
            // a file pattern
            // 2-1.是指定的路径模式,根据模式查找匹配的资源
            return findPathMatchingResources(locationPattern);
        }
        // 2-2.如果不是Ant-Style,则认为是单个资源路径,使用ResourceLoader加载单个资源
        else {
            // a single resource with the given name
            return new Resource[] {getResourceLoader().getResource(locationPattern)};
        }
    }
}

这里加载资源的逻辑根据路径模式的不同,分支情况非常多,逻辑也非常复杂。为了更加详细而清晰的探索,这里分别深入每种情况,并为每种情况举例相应的资源路径模式。

1.1-1分支(类路径资源模式)

1-1分支进入条件需要满足:

  • 路径以classpath*:开头;
  • 路径时Ant-Style风格,即路径中包含通配符;

如:classpath*:/META-INF/bean-*.xml和classpath*

以上是关于Spring源码系列 — Resource抽象的主要内容,如果未能解决你的问题,请参考以下文章

Spring读源码系列01---Spring核心类及关联性介绍

AOP执行增强-Spring 源码系列

spring源码分析:resource资源定位一

Spring源码系列 —— Envoriment组件

Spring源码解析之Resource

Spring源码三千问从源码分析@Resource与@Autowired的区别