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*😗.xml都会进入1-1分支。

// 根据给定模式通过ant风格匹配器寻找所有匹配的资源,locationPattern为路径模式。
// 支持从文件系统、jar、zip中寻找资源
protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
	// 截取根路径,即取ant通配符之前的路径部分
	String rootDirPath = determineRootDir(locationPattern);
	// 从通配符位置开始,截取路径的后续部分(子模式)
	String subPattern = locationPattern.substring(rootDirPath.length());
	// 从根路径部分获取所有的资源
	Resource[] rootDirResources = getResources(rootDirPath);
	// 在spring中对于集合使用有个良好习惯,初始化时始终指定集合大小
	Set<Resource> result = new LinkedHashSet<Resource>(16);
	// 遍历根路径下的所有资源与子模式进行匹配,如果匹配成功,则符合路径模式的资源加入result结果集中
	for (Resource rootDirResource : rootDirResources) {
		rootDirResource = resolveRootDirResource(rootDirResource);
		// 获取资源的URL
		URL rootDirUrl = rootDirResource.getURL();
		if (equinoxResolveMethod != null) {
			if (rootDirUrl.getProtocol().startsWith("bundle")) {
				rootDirUrl = (URL) ReflectionUtils.invokeMethod(equinoxResolveMethod, null, rootDirUrl);
				rootDirResource = new UrlResource(rootDirUrl);
			}
		}
		// 如果URL是vfs协议...,这里暂时不看这种协议,使用情况较少
		if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
			result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher()));
		}
		// 如果是jar协议(代表是jar包中的资源)
		else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) {
			result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPattern));
		}
		else {
			// 如果都不是,则从文件系统系统中查找
			result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));
		}
	}
	if (logger.isDebugEnabled()) {
		logger.debug("Resolved location pattern [" + locationPattern + "] to resources " + result);
	}
	return result.toArray(new Resource[result.size()]);
}

下面逐一介绍findPathMatchingResources的实现细节的方法。

首先determineRootDir决定路径模式中的通配符前缀部分路径的解析,作为搜索资源时的根部路径:

protected String determineRootDir(String location) {
	// 获取路径模式中的协议分割符":"位置
	int prefixEnd = location.indexOf(':') + 1;
	// 路径总长度
	int rootDirEnd = location.length();
	// 截取协议分割符后的路径部分,并判断是否有通配符。(需要去掉协议部分,因为存在classpath*:前缀,会影响ant模式匹配)
	// "/"代表一层目录,while循环逐层目录进行截取,判断是否匹配通配符,然后截取,直到没有通配符的前缀路径匹配到结束
	while (rootDirEnd > prefixEnd && getPathMatcher().isPattern(location.substring(prefixEnd, rootDirEnd))) {
		rootDirEnd = location.lastIndexOf('/', rootDirEnd - 2) + 1;
	}
	if (rootDirEnd == 0) {
		rootDirEnd = prefixEnd;
	}
	// 截取通配符前部分的目录路径
	return location.substring(0, rootDirEnd);
}

通过以上方式,可以获取Ant风格路径模式中的通配符之前的目录部分路径。然后以此搜索全部资源,再次递归调用getResources方法。会进入1-2分支,因为路径仍然以classpath*开头,且不包含模式。关于1-2分支后续再介绍,这里接着分析resolveRootDirResource实现:

// 主要用于子类覆盖实现
protected Resource resolveRootDirResource(Resource original) throws IOException {
	return original;
}

上述方法主要用于子类扩展实现,默认没有任何逻辑实现。findPathMatchingResources的后续逻辑主要实现已经查找的资源和子路径模式的匹配,分为三类匹配:

  • VFS协议前提下,匹配资源与子路径模式
  • Jar协议前提下,匹配资源与子路径模式
  • 文件系统中匹配资源与子路径模式

关于VFS协议使用情况较少,这里不做分析。主要分析JAR和文件系统下的实现。首先介绍Jar协议中的匹配实现。

if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
	result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher()));
}
// 如果是jar协议或者是jar资源
else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) {
	// jar资源匹配
	result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPattern));
}
else {
	// 从文件系统中匹配
	result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));
}

在spring中以下协议的资源都认为是Jar资源

/** URL protocol for an entry from a jar file: "jar" */
public static final String URL_PROTOCOL_JAR = "jar";
/** URL protocol for an entry from a war file: "war" */
public static final String URL_PROTOCOL_WAR = "war";
/** URL protocol for an entry from a zip file: "zip" */
public static final String URL_PROTOCOL_ZIP = "zip";
/** URL protocol for an entry from a WebSphere jar file: "wsjar" */
public static final String URL_PROTOCOL_WSJAR = "wsjar";
/** URL protocol for an entry from a JBoss jar file: "vfszip" */
public static final String URL_PROTOCOL_VFSZIP = "vfszip";

public static boolean isJarURL(URL url) {
	// 获取URL协议
	String protocol = url.getProtocol();
	// 如果匹配以下协议,都认为是Jar URL
	return (URL_PROTOCOL_JAR.equals(protocol) || URL_PROTOCOL_WAR.equals(protocol) ||
			URL_PROTOCOL_ZIP.equals(protocol) || URL_PROTOCOL_VFSZIP.equals(protocol) ||
			URL_PROTOCOL_WSJAR.equals(protocol));
}

再看spring中如果匹配Jar资源与子模式doFindPathMatchingJarResources的实现

@SuppressWarnings("deprecation")
protected Set<Resource> doFindPathMatchingJarResources(Resource rootDirResource, URL rootDirURL, String subPattern)
		throws IOException {
	// doFindPathMatchingJarResources即将被移除,默认也没做任何逻辑,空实现
	// Check deprecated variant for potential overriding first...
	Set<Resource> result = doFindPathMatchingJarResources(rootDirResource, subPattern);
	if (result != null) {
		return result;
	}
	// 根据URL获取URLConnection(前文已经介绍URLConnection)
	URLConnection con = rootDirURL.openConnection();
	JarFile jarFile;
	String jarFileUrl;
	String rootEntryPath;
	boolean closeJarFile;
	// 如果是JarURLConnection的实例,大多数都是该场景
	if (con instanceof JarURLConnection) {
		// Should usually be the case for traditional JAR files.
		// 转换类型
		JarURLConnection jarCon = (JarURLConnection) con;
		ResourceUtils.useCachesIfNecessary(jarCon);
		// 根据JarURLConnection获取JarFile对象
		jarFile = jarCon.getJarFile();
		jarFileUrl = jarCon.getJarFileURL().toExternalForm();
		JarEntry jarEntry = jarCon.getJarEntry();
		rootEntryPath = (jarEntry != null ? jarEntry.getName() : "");
		closeJarFile = !jarCon.getUseCaches();
	}
	else {
		// No JarURLConnection -> need to resort to URL file parsing.
		// We'll assume URLs of the format "jar:path!/entry", with the protocol
		// being arbitrary as long as following the entry format.
		// We'll also handle paths with and without leading "file:" prefix.
		String urlFile = rootDirURL.getFile();
		try {
			int separatorIndex = urlFile.indexOf(ResourceUtils.WAR_URL_SEPARATOR);
			if (separatorIndex == -1) {
				separatorIndex = urlFile.indexOf(ResourceUtils.JAR_URL_SEPARATOR);
			}
			if (separatorIndex != -1) {
				jarFileUrl = urlFile.substring(0, separatorIndex);
				rootEntryPath = urlFile.substring(separatorIndex + 2);  // both separators are 2 chars
				jarFile = getJarFile(jarFileUrl);
			}
			else {
				jarFile = new JarFile(urlFile);
				jarFileUrl = urlFile;
				rootEntryPath = "";
			}
			closeJarFile = true;
		}
		catch (ZipException ex) {
			if (logger.isDebugEnabled()) {
				logger.debug("Skipping invalid jar classpath entry [" + urlFile + "]");
			}
			return Collections.emptySet();
		}
	}
	try {
		if (logger.isDebugEnabled()) {
			logger.debug("Looking for matching resources in jar file [" + jarFileUrl + "]");
		}
		if (!"".equals(rootEntryPath) && !rootEntryPath.endsWith("/")) {
			// Root entry path must end with slash to allow for proper matching.
			// The Sun JRE does not return a slash here, but BEA JRockit does.
			rootEntryPath = rootEntryPath + "/";
		}
		result = new LinkedHashSet<Resource>(8);
		// 获取Jar文件中的条目,通过JarFile获取JarEntry对象,然后遍历每个条目即JarEntry
		for (Enumeration<JarEntry> entries = jarFile.entries(); entries.hasMoreElements();) {
			JarEntry entry = entries.nextElement();
			// 获取条目的路径
			String entryPath = entry.getName();
			if (entryPath.startsWith(rootEntryPath)) {
				String relativePath = entryPath.substring(rootEntryPath.length());
				// 对条目路径的相对部分和路径中的子模式进行模式匹配,这里使用AntPathMatcher进行匹配实现
				if (getPathMatcher().match(subPattern, relativePath)) {
					// 如果匹配成功,则表示是路径模式匹配的资源,加入结果集
					result.add(rootDirResource.createRelative(relativePath));
				}
			}
		}
		return result;
	}
	finally {
		if (closeJarFile) {
			jarFile.close();
		}
	}
}

通过Jar文件的URL获取JarURLConnection,再获取JarFile,对JarFile中的JarEntry进行遍历,匹配路径子模式,如果匹配成功,则是符合路径模式的资源。其中涉及的很多其他的细节,这里不做详述。

接下来再看文件系统资源的匹配实现doFindPathMatchingFileResources

protected Set<Resource> doFindPathMatchingFileResources(Resource rootDirResource, String subPattern)
		throws IOException {
	File rootDir;
	try {
		// 获取资源的文件系统绝对路径
		rootDir = rootDirResource.getFile().getAbsoluteFile();
	}
	catch (IOException ex) {
		if (logger.isWarnEnabled()) {
			logger.warn("Cannot search for matching files underneath " + rootDirResource +
					" because it does not correspond to a directory in the file system", ex);
		}
		return Collections.emptySet();
	}
	// 根据绝对路径和子模式进行匹配
	return doFindMatchingFileSystemResources(rootDir, subPattern);
}

doFindMatchingFileSystemResources中实现文件系统资源匹配:

protected Set<Resource> doFindMatchingFileSystemResources(File rootDir, String subPattern) throws IOException {
	if (logger.isDebugEnabled()) {
		logger.debug("Looking for matching resources in directory tree [" + rootDir.getPath() + "]");
	}
	// retrieveMatchingFiles实现匹配并获取匹配资源的File对象
	Set<File> matchingFiles = retrieveMatchingFiles(rootDir, subPattern);
	Set<Resource> result = new LinkedHashSet<Resource>(matchingFiles.size());
	// 根据匹配资源的File对象创建FileSystemResource
	for (File file : matchingFiles) {
		result.add(new FileSystemResource(file));
	}
	return result;
}

这里核心的方法就是retrieveMatchingFiles,它实现了如果根据文件系统的绝对路径查找匹配子模式的文件资源:

protected Set<File> retrieveMatchingFiles(File rootDir, String pattern) throws IOException {
	// 如果文件目录不存在,则返回空资源,表示没有搜索到匹配子模式的文件资源
	if (!rootDir.exists()) {
		// Silently skip non-existing directories.
		if (logger.isDebugEnabled()) {
			logger.debug("Skipping [" + rootDir.getAbsolutePath() + "] because it does not exist");
		}
		return Collections.emptySet();
	}
	// 如果不是目录,则返回空资源
	if (!rootDir.isDirectory()) {
		// Complain louder if it exists but is no directory.
		if (logger.isWarnEnabled()) {
			logger.warn("Skipping [" + rootDir.getAbsolutePath() + "] because it does not denote a directory");
		}
		return Collections.emptySet();
	}
	// 如果没有目录的读权限,则返回空资源
	if (!rootDir.canRead()) {
		if (logger.isWarnEnabled()) {
			logger.warn("Cannot search for matching files underneath directory [" + rootDir.getAbsolutePath() +
					"] because the application is not allowed to read the directory");
		}
		return Collections.emptySet();
	}
	// 将不同文件系统的目录分隔符统一转换成"/"分隔符
	String fullPattern = StringUtils.replace(rootDir.getAbsolutePath(), File.separator, "/");
	if (!pattern.startsWith("/")) {
		fullPattern += "/";
	}
	// 转换子模式中的分割符为"/"
	fullPattern = fullPattern + StringUtils.replace(pattern, File.separator, "/");
	// 很好的编程习惯
	Set<File> result = new LinkedHashSet<File>(8);
	// 根据路径和模式匹配
	doRetrieveMatchingFiles(fullPattern, rootDir, result);
	return result;
}

retrieveMatchingFiles主要做目录的校验和路径分隔符的转换。实现匹配的核心部分在doRetrieveMatchingFiles:

protected void doRetrieveMatchingFiles(String fullPattern, File dir, Set<File> result) throws IOException {
	if (logger.isDebugEnabled()) {
		logger.debug("Searching directory [" + dir.getAbsolutePath() +
				"] for files matching pattern [" + fullPattern + "]");
	}
	// 获取目录中的所有文件
	File[] dirContents = dir.listFiles();
	// 如果目录中没有文件,则返回空,表示没有搜索到任何匹配的资源
	if (dirContents == null) {
		if (logger.isWarnEnabled()) {
			logger.warn("Could not retrieve contents of directory [" + dir.getAbsolutePath() + "]");
		}
		return;
	}
	Arrays.sort(dirContents);
	// 遍历所有文件
	for (File content : dirContents) {
		// 转换分隔符
		String currPath = StringUtils.replace(content.getAbsolutePath(), File.separator, "/");
		// 如果是目录,且路径匹配完整模式
		if (content.isDirectory() && getPathMatcher().matchStart(fullPattern, currPath + "/")) {
			// 如果无读权限
			if (!content.canRead()) {
				if (logger.isDebugEnabled()) {
					logger.debug("Skipping subdirectory [" + dir.getAbsolutePath() +
							"] because the application is not allowed to read the directory");
				}
			}
			else {
				// 如果有读权限,则递归调用
				doRetrieveMatchingFiles(fullPattern, content, result);
			}
		}
		// 如果是文件,则判断文件路径和模式,匹配成功则加入结果集
		if (getPathMatcher().match(fullPattern, currPath)) {
			result.add(content);
		}
	}
}

以上便是spring中按照模式从文件系统搜索匹配文件资源的过程。首先根据模式中的统配符前缀路径获取文件目录,然后获取文件目录中的所有文件,遍历所有文件。如果是子目录,则判断子目录路径是否匹配模式,匹配则递归调用再匹配;如果是子文件,则判断子文件路径是否匹配模式,匹配则加入结果集。

根据类路径模式搜索资源主要是以上三种方式。接下来再分析完整类路径搜索匹配资源的详细过程。

2.1-2分支(完整类路径资源)

完整类路径搜索匹配资源主要有1-2分支负责,由findAllClassPathResources实现:

完整类路径资源搜索主要委托Java ClassLoader加载资源。关于ClassLoader加载资源的详情这里不详述。

// 截取路径,去掉路径的中类路径前缀"classpath*:"
return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
protected Resource[] findAllClassPathResources(String location) throws IOException {
	String path = location;
	// 截取路径的前导"/",表示该资源路径相对于类类路径
	if (path.startsWith("/")) {
		path = path.substring(1);
	}
	// 在所有类路径(jar、工程)寻找资源
	Set<Resource> result = doFindAllClassPathResources(path);
	if (logger.isDebugEnabled()) {
		logger.debug("Resolved classpath location [" + location + "] to resources " + result);
	}
	return result.toArray(new Resource[result.size()]);
}

在所有类路径中寻找资源的核心实现由doFindAllClassPathResources完成:

protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {
	// nice的写法
	Set<Resource> result = new LinkedHashSet<Resource>(16);
	// 获取类类加载器,这里由AbstractApplicationContext中初始化PathMatchingResourcePatternResolver指定ResourceLoader
	// ResourceLoader是AbstractApplicationContext继承DefaultResourceLoader,类加载由DefaultResourceLoader的构造方法中初始化
	// DefaultResourceLoader中使用ClassUtils.getDefaultClassLoader()获取类加载器
	ClassLoader cl = getClassLoader();
	// 使用类加载器加载指定路径的所有资源
	Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path));
	while (resourceUrls.hasMoreElements()) {
		URL url = resourceUrls.nextElement();
		// 将url转换为UrlResource,放入结果集中
		result.add(convertClassLoaderURL(url));
	}
	// 如果路径是""空串,则使用ClassLoader将无法加载到任何资源
	// 这是需要使用URLClassLoader获取所有类和资源搜索路径,然后逐一遍历查找
	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;
}

这里关键点在于使用ClassLoader类加载提供的加载类路径资源的能力,当时对于提供的""空路径(如:classpath*😗.xml),类加载器将无法加载。需要使用URLClassLoader提供的获取搜索所有类和资源路径的接口。

再介绍addAllClassLoaderJarRoots实现细节:

protected void addAllClassLoaderJarRoots(ClassLoader classLoader, Set<Resource> result) {
	// 判断当前classLoader是否为URLClassLoader实例
	if (classLoader instanceof URLClassLoader) {
		try {
			// 获取所有资源和类的搜索路径ucp
			for (URL url : ((URLClassLoader) classLoader).getURLs()) {
				try {
					// 根据jar url规则组装url并创建UrlResource对象
					UrlResource jarResource = new UrlResource(
							ResourceUtils.JAR_URL_PREFIX + url + ResourceUtils.JAR_URL_SEPARATOR);
					// 判断资源是否存在,排除非jar资源的类路径
					if (jarResource.exists()) {
						result.add(jarResource);
					}
				}
				catch (MalformedURLException ex) {
					if (logger.isDebugEnabled()) {
						logger.debug("Cannot search for matching files underneath [" + url +
								"] because it cannot be converted to a valid 'jar:' URL: " + ex.getMessage());
					}
				}
			}
		}
		catch (Exception ex) {
			if (logger.isDebugEnabled()) {
				logger.debug("Cannot introspect jar files since ClassLoader [" + classLoader +
						"] does not support 'getURLs()': " + ex);
			}
		}
	}
	// 如果类加载器为空使用系统类加载器,需要处理java.class.path引用的jar
	if (classLoader == ClassLoader.getSystemClassLoader()) {
		// "java.class.path" manifest evaluation...
		addClassPathManifestEntries(result);
	}
	// 父类加载器的资源和所有类
	if (classLoader != null) {
		try {
			// Hierarchy traversal...
			addAllClassLoaderJarRoots(classLoader.getParent(), result);
		}
		catch (Exception ex) {
			if (logger.isDebugEnabled()) {
				logger.debug("Cannot introspect jar files in parent ClassLoader since [" + classLoader +
						"] does not support 'getParent()': " + ex);
			}
		}
	}
}

对于完整类路径资源的加载主要分为两种模式:

  • 委托ClassLoader的getResources接口加载资源
  • 当资源path为空串时,使用加载器的ucp和java.class.path指定的资源和类
3.2-1分支(文件模式-文件系统"file://"或者类路径"classpath:"模式)
if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
	// a file pattern
	return findPathMatchingResources(locationPattern);
}

主要由findPathMatchingResources实现,前文中已经详述实现细节,可以对照参考。文件模式大致流程和类路径模式处理流程类似。找到文件模式的通配符前的根目录,然后再次递归调用加载根目录下的所有资源,再遍历依次匹配子模式,最终检索符合的资源。在这里递归调用加载根目录的所有资源时,会进入2-2分支

// 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 {
	// 递归调用时,路径模式通配符前的根目录非classpath*:开头并且非ant风格
	// a single resource with the given name
	return new Resource[] {getResourceLoader().getResource(locationPattern)};
}

对于单个完整文件路径资源的加载在spring中有ResourceLoader负责,PathMatchingResourcePatternResolver中组合ResourceLoader的默认实现DefaultResourceLoader。这里对于单个完整文件路径的资源(在文件系统或者类路径上)加载委托DefaultResourceLoader加载。关于其实现细节,前文中也已经介绍。

4.2-2分支(单个文件路径-类路径或者文件系统)

上节中也已经介绍,对于单个文件的加载PathMatchingResourcePatternResolver委托DefaultResourceLoader实现。ResourceLoader

Tips
Spring中大量使用接口隔离,单一职责。然后通过组合方式实现超强的处理能力和极易扩展的能力。ResourceLoader和ResourcePatternResolver接口隔离,单一职责,PathMatchingResourcePatternResolver组合DefaultResourceLoader。

何时何地触发加载解析

在介绍Spring如何加载Resource资源的细节后,需要全面了解Spring如何触发,什么时机触发资源的加载。

上图中描绘了Spring加载资源的上下文过程:

  1. 将资源抽象(路径)给上下文
  2. 上下文委托BeanDefinitonReader加载
  3. BeanDefinitonReader委托ResourceLoader资源加载资源
  4. ResourceLoader最终生产资源抽象的对象Resource

BeanDefinitonReader和ResourceLoader的完整UML图关系:

当使用XML定义Bean的元数据时,Spring将使用XmlBeanDefinitionReader。关于如何实现的细节下节BeanDefinition中介绍详细。

总结

1.资源
前缀 例子 描述
classpath: classpath:com/myapp/config.xml 从类路径加载,解析为ClassPathResource
file: file:///data/config.xml 从文件系统加载,解析文FileSystemResrouce
http: http://myserver/logo.png 从http URL加载,解析为UrlResource
(none) /data/config.xml 依赖上下文类型

Spring中对于不同的路径解析为不同类型的资源。

2.解析
  • 单个资源加载。资源可以以文件系统或者类路径方式表现,由ResourceLoader负责解析加载
  • 路径模式,主要是Ant-Style。支持classpath:*和模式匹配方式解析加载资源
参考

springframework docs

posted @ 2018-11-14 17:22  怀瑾握瑜XI  阅读(804)  评论(0编辑  收藏  举报