通过springBoot加载配置文件了解资源加载
首先,preparedEvent事件触发
org.springframework.boot.context.config.ConfigFileApplicationListener#onApplicationEnvironmentPreparedEvent
加载方法org.springframework.boot.context.config.ConfigFileApplicationListener.Loader#load()
void load() {
FilteredPropertySource.apply(this.environment, DEFAULT_PROPERTIES, LOAD_FILTERED_PROPERTY,
(defaultProperties) -> {
this.profiles = new LinkedList<>();
this.processedProfiles = new LinkedList<>();
this.activatedProfiles = false;
this.loaded = new LinkedHashMap<>();
initializeProfiles();
while (!this.profiles.isEmpty()) {
Profile profile = this.profiles.poll();
if (isDefaultProfile(profile)) {
addProfileToEnvironment(profile.getName());
}
load(profile, this::getPositiveProfileFilter,
addToLoaded(MutablePropertySources::addLast, false));
this.processedProfiles.add(profile);
}
load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
addLoadedPropertySources();
applyActiveProfiles(defaultProperties);
});
}
initializeProfiles
**
* Initialize profile information from both the {@link Environment} active
* profiles and any {@code spring.profiles.active}/{@code spring.profiles.include}
* properties that are already set.
*/
private void initializeProfiles() {
// The default profile for these purposes is represented as null. We add it
// first so that it is processed first and has lowest priority.
this.profiles.add(null);
Binder binder = Binder.get(this.environment);
Set<Profile> activatedViaProperty = getProfiles(binder, ACTIVE_PROFILES_PROPERTY);
Set<Profile> includedViaProperty = getProfiles(binder, INCLUDE_PROFILES_PROPERTY);
List<Profile> otherActiveProfiles = getOtherActiveProfiles(activatedViaProperty, includedViaProperty);
this.profiles.addAll(otherActiveProfiles);
// Any pre-existing active profiles set via property sources (e.g.
// System properties) take precedence over those added in config files.
this.profiles.addAll(includedViaProperty);
addActiveProfiles(activatedViaProperty);
if (this.profiles.size() == 1) { // only has null profile
for (String defaultProfileName : this.environment.getDefaultProfiles()) {
Profile defaultProfile = new Profile(defaultProfileName, true);
this.profiles.add(defaultProfile);
}
}
}
initializeProfiles读取配置spring.profiles.active,spring.profiles.include,spring.config.location等参数
其值在启动时通过参数指定
load(profile, this::getPositiveProfileFilter,
addToLoaded(MutablePropertySources::addLast, false))
private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
getSearchLocations().forEach((location) -> {
boolean isDirectory = location.endsWith("/");
Set<String> names = isDirectory ? getSearchNames() : NO_SEARCH_NAMES;
names.forEach((name) -> load(location, name, profile, filterFactory, consumer));
});
}
getSearchLocations方法 如果没有指定就从默认位置查找配置文件
同样,没指定,默认为application
load方法
private void load(String location, String name, Profile profile, DocumentFilterFactory filterFactory,
DocumentConsumer consumer) {
if (!StringUtils.hasText(name)) {
for (PropertySourceLoader loader : this.propertySourceLoaders) {
if (canLoadFileExtension(loader, location)) {
load(loader, location, profile, filterFactory.getDocumentFilter(profile), consumer);
return;
}
}
throw new IllegalStateException("File extension of config file location '" + location
+ "' is not known to any PropertySourceLoader. If the location is meant to reference "
+ "a directory, it must end in '/'");
}
Set<String> processed = new HashSet<>();
for (PropertySourceLoader loader : this.propertySourceLoaders) {
for (String fileExtension : loader.getFileExtensions()) {
if (processed.add(fileExtension)) {
loadForFileExtension(loader, location + name, "." + fileExtension, profile, filterFactory,
consumer);
}
}
}
}
Set processed = new HashSet<>();
for (PropertySourceLoader loader : this.propertySourceLoaders) {
for (String fileExtension : loader.getFileExtensions()) {
if (processed.add(fileExtension)) {
loadForFileExtension(loader, location + name, “.” + fileExtension, profile, filterFactory,
consumer);
}
}
}
这里有两种loader(这两个loader不是我们要说的ResourceLoader)
PropertiesPropertySourceLoader
YamlPropertySourceLoader
到了关键方法org.springframework.boot.context.config.ConfigFileApplicationListener.Loader#load
首先根据location获取**Resource,**org.springframework.boot.context.config.ConfigFileApplicationListener.Loader#getResources
private Resource[] getResources(String location) {
try {
if (location.contains("*")) {
return getResourcesFromPatternLocation(location);
}
return new Resource[] { this.resourceLoader.getResource(location) };
}
catch (Exception ex) {
return EMPTY_RESOURCES;
}
}
this.resourceLoader为DefaultResourceLoader
进入org.springframework.core.io.DefaultResourceLoader#getResource
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);
}
}
}
DefaultResourceLoader能构建三种Resource
文件相对路径 ClassPathContextResource
classpath路径ClassPathResource
url路径 UrlResource
这里可以看出来Resource是对要加载资源的封装
if (resource == null || !resource.exists())
如果资源存在
进入org.springframework.boot.context.config.ConfigFileApplicationListener.Loader#loadDocuments
org.springframework.beans.factory.config.YamlProcessor#process(org.springframework.beans.factory.config.YamlProcessor.MatchCallback, org.yaml.snakeyaml.Yaml, org.springframework.core.io.Resource)加载资源
private boolean process(MatchCallback callback, Yaml yaml, Resource resource) {
int count = 0;
try {
if (logger.isDebugEnabled()) {
logger.debug("Loading from YAML: " + resource);
}
try (Reader reader = new UnicodeReader(resource.getInputStream())) {
for (Object object : yaml.loadAll(reader)) {
if (object != null && process(asMap(object), callback)) {
count++;
if (this.resolutionMethod == ResolutionMethod.FIRST_FOUND) {
break;
}
}
}
if (logger.isDebugEnabled()) {
logger.debug("Loaded " + count + " document" + (count > 1 ? "s" : "") +
" from YAML resource: " + resource);
}
}
}
catch (IOException ex) {
handleProcessError(resource, ex);
}
return (count > 0);
}
至此,配置文件已经加载到内存里了,后面如何注入到bean总就暂不深入研究
通过加载配置文件的过程可以看出来Resource是spring对资源的抽象封装,ResourceLoader是用来生成Resource对象
Resource接口
org.springframework.core.io.Resource 为 Spring 框架所有资源的抽象和访问接口,它继承 org.springframework.core.io.InputStreamSource接口。作为所有资源的统一抽象,Source 定义了一些通用的方法,由子类 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 的实现类。
这样我们在加载资源时,不需要写具体实现,通过提供的resocurce就能很方便的加载资源
AbstractResource 为 Resource 接口的默认实现,它实现了 Resource 接口的大部分的公共实现,作为 Resource 接口中的重中之重,其定义如下:
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.io;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.NestedIOException;
import org.springframework.lang.Nullable;
import org.springframework.util.ResourceUtils;
/**
* Convenience base class for {@link Resource} implementations,
* pre-implementing typical behavior.
*
* <p>The "exists" method will check whether a File or InputStream can
* be opened; "isOpen" will always return false; "getURL" and "getFile"
* throw an exception; and "toString" will return the description.
*
* @author Juergen Hoeller
* @author Sam Brannen
* @since 28.12.2003
*/
public abstract class AbstractResource implements Resource {
/**
* This implementation checks whether a File can be opened,
* falling back to whether an InputStream can be opened.
* This will cover both directories and content resources.
*/
@Override
public boolean exists() {
// Try file existence: can we find the file in the file system?
if (isFile()) {
try {
return getFile().exists();
}
catch (IOException ex) {
Log logger = LogFactory.getLog(getClass());
if (logger.isDebugEnabled()) {
logger.debug("Could not retrieve File for existence check of " + getDescription(), ex);
}
}
}
// Fall back to stream existence: can we open the stream?
try {
getInputStream().close();
return true;
}
catch (Throwable ex) {
Log logger = LogFactory.getLog(getClass());
if (logger.isDebugEnabled()) {
logger.debug("Could not retrieve InputStream for existence check of " + getDescription(), ex);
}
return false;
}
}
/**
* This implementation always returns {@code true} for a resource
* that {@link #exists() exists} (revised as of 5.1).
*/
@Override
public boolean isReadable() {
return exists();
}
/**
* This implementation always returns {@code false}.
*/
@Override
public boolean isOpen() {
return false;
}
/**
* This implementation always returns {@code false}.
*/
@Override
public boolean isFile() {
return false;
}
/**
* This implementation throws a FileNotFoundException, assuming
* that the resource cannot be resolved to a URL.
*/
@Override
public URL getURL() throws IOException {
throw new FileNotFoundException(getDescription() + " cannot be resolved to URL");
}
/**
* This implementation builds a URI based on the URL returned
* by {@link #getURL()}.
*/
@Override
public URI getURI() throws IOException {
URL url = getURL();
try {
return ResourceUtils.toURI(url);
}
catch (URISyntaxException ex) {
throw new NestedIOException("Invalid URI [" + url + "]", ex);
}
}
/**
* This implementation throws a FileNotFoundException, assuming
* that the resource cannot be resolved to an absolute file path.
*/
@Override
public File getFile() throws IOException {
throw new FileNotFoundException(getDescription() + " cannot be resolved to absolute file path");
}
/**
* This implementation returns {@link Channels#newChannel(InputStream)}
* with the result of {@link #getInputStream()}.
* <p>This is the same as in {@link Resource}'s corresponding default method
* but mirrored here for efficient JVM-level dispatching in a class hierarchy.
*/
@Override
public ReadableByteChannel readableChannel() throws IOException {
return Channels.newChannel(getInputStream());
}
/**
* This method reads the entire InputStream to determine the content length.
* <p>For a custom sub-class of {@code InputStreamResource}, we strongly
* recommend overriding this method with a more optimal implementation, e.g.
* checking File length, or possibly simply returning -1 if the stream can
* only be read once.
* @see #getInputStream()
*/
@Override
public long contentLength() throws IOException {
InputStream is = getInputStream();
try {
long size = 0;
byte[] buf = new byte[256];
int read;
while ((read = is.read(buf)) != -1) {
size += read;
}
return size;
}
finally {
try {
is.close();
}
catch (IOException ex) {
Log logger = LogFactory.getLog(getClass());
if (logger.isDebugEnabled()) {
logger.debug("Could not close content-length InputStream for " + getDescription(), ex);
}
}
}
}
/**
* This implementation checks the timestamp of the underlying File,
* if available.
* @see #getFileForLastModifiedCheck()
*/
@Override
public long lastModified() throws IOException {
File fileToCheck = getFileForLastModifiedCheck();
long lastModified = fileToCheck.lastModified();
if (lastModified == 0L && !fileToCheck.exists()) {
throw new FileNotFoundException(getDescription() +
" cannot be resolved in the file system for checking its last-modified timestamp");
}
return lastModified;
}
/**
* Determine the File to use for timestamp checking.
* <p>The default implementation delegates to {@link #getFile()}.
* @return the File to use for timestamp checking (never {@code null})
* @throws FileNotFoundException if the resource cannot be resolved as
* an absolute file path, i.e. is not available in a file system
* @throws IOException in case of general resolution/reading failures
*/
protected File getFileForLastModifiedCheck() throws IOException {
return getFile();
}
/**
* This implementation throws a FileNotFoundException, assuming
* that relative resources cannot be created for this resource.
*/
@Override
public Resource createRelative(String relativePath) throws IOException {
throw new FileNotFoundException("Cannot create a relative resource for " + getDescription());
}
/**
* This implementation always returns {@code null},
* assuming that this resource type does not have a filename.
*/
@Override
@Nullable
public String getFilename() {
return null;
}
/**
* This implementation compares description strings.
* @see #getDescription()
*/
@Override
public boolean equals(@Nullable Object other) {
return (this == other || (other instanceof Resource &&
((Resource) other).getDescription().equals(getDescription())));
}
/**
* This implementation returns the description's hash code.
* @see #getDescription()
*/
@Override
public int hashCode() {
return getDescription().hashCode();
}
/**
* This implementation returns the description of this resource.
* @see #getDescription()
*/
@Override
public String toString() {
return getDescription();
}
}
如果我们想要实现自定义的 Resource,记住不要实现 Resource 接口,而应该继承 AbstractResource 抽象类,然后根据当前的具体资源特性覆盖相应的方法即可。
ResourceLoader接口
一开始就说了 Spring 将资源的定义和资源的加载区分开了,Resource 定义了统一的资源,那资源的加载则由 ResourceLoader 来统一定义。
org.springframework.core.io.ResourceLoader 为 Spring 资源加载的统一抽象,具体的资源加载则由相应的实现类来完成,所以我们可以将 ResourceLoader 称作为统一资源定位器。其定义如下:
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.io;
import org.springframework.lang.Nullable;
import org.springframework.util.ResourceUtils;
/**
* Strategy interface for loading resources (e.. class path or file system
* resources). An {@link org.springframework.context.ApplicationContext}
* is required to provide this functionality, plus extended
* {@link org.springframework.core.io.support.ResourcePatternResolver} support.
*
* <p>{@link DefaultResourceLoader} is a standalone implementation that is
* usable outside an ApplicationContext, also used by {@link ResourceEditor}.
*
* <p>Bean properties of type Resource and Resource array can be populated
* from Strings when running in an ApplicationContext, using the particular
* context's resource loading strategy.
*
* @author Juergen Hoeller
* @since 10.03.2004
* @see Resource
* @see org.springframework.core.io.support.ResourcePatternResolver
* @see org.springframework.context.ApplicationContext
* @see org.springframework.context.ResourceLoaderAware
*/
public interface ResourceLoader {
/** Pseudo URL prefix for loading from the class path: "classpath:". */
String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX;
/**
* Return a Resource handle for the specified resource location.
* <p>The handle should always be a reusable resource descriptor,
* allowing for multiple {@link Resource#getInputStream()} calls.
* <p><ul>
* <li>Must support fully qualified URLs, e.g. "file:C:/test.dat".
* <li>Must support classpath pseudo-URLs, e.g. "classpath:test.dat".
* <li>Should support relative file paths, e.g. "WEB-INF/test.dat".
* (This will be implementation-specific, typically provided by an
* ApplicationContext implementation.)
* </ul>
* <p>Note that a Resource handle does not imply an existing resource;
* you need to invoke {@link Resource#exists} to check for existence.
* @param location the resource location
* @return a corresponding Resource handle (never {@code null})
* @see #CLASSPATH_URL_PREFIX
* @see Resource#exists()
* @see Resource#getInputStream()
*/
Resource getResource(String location);
/**
* Expose the ClassLoader used by this ResourceLoader.
* <p>Clients which need to access the ClassLoader directly can do so
* in a uniform manner with the ResourceLoader, rather than relying
* on the thread context ClassLoader.
* @return the ClassLoader
* (only {@code null} if even the system ClassLoader isn't accessible)
* @see org.springframework.util.ClassUtils#getDefaultClassLoader()
* @see org.springframework.util.ClassUtils#forName(String, ClassLoader)
*/
@Nullable
ClassLoader getClassLoader();
}
ResourceLoader 接口提供两个方法:getResource()、getClassLoader()。
getResource()根据所提供资源的路径 location 返回 Resource 实例,但是它不确保该 Resource 一定存在,需要调用 Resource.exist()方法判断。该方法支持以下模式的资源加载:
- URL位置资源,如”file:C:/test.dat”
- ClassPath位置资源,如”classpath:test.dat”
- 相对路径资源,如”WEB-INF/test.dat”,此时返回的Resource实例根据实现不同而不同
该方法的主要实现是在其子类 DefaultResourceLoader 中实现,具体过程我们在分析 DefaultResourceLoader 时做详细说明。
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 加载资源的具体策略,代码如下
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。
在构造器中也是调用 FileSystemResource 的构造方法来构造 FileSystemContextResource 的。
如果将上面的示例将 DefaultResourceLoader 改为 FileSystemContextResource ,则 fileResource1 则为 FileSystemResource。
ResourcePatternResolver
先看spring扫描类路径的匹配过程
class文件扫描过程
org.springframework.context.annotation.ClassPathBeanDefinitionScanner#doScan方法
Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
根据basePackage扫描寻找BeanDefinition,basePackage默认为Main类所在package
最终进入org.springframework.core.io.support.PathMatchingResourcePatternResolver#getResources
public Resource[] getResources(String locationPattern) throws IOException {
Assert.notNull(locationPattern, "Location pattern must not be null");
if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
// 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
return findPathMatchingResources(locationPattern);
}
else {
// all class path resources with the given name
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)};
}
}
}
如果进入org.springframework.core.io.support.PathMatchingResourcePatternResolver#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]);
}
org.springframework.core.io.support.PathMatchingResourcePatternResolver#doFindAllClassPathResources方法
protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {
Set<Resource> result = new LinkedHashSet<>(16);
ClassLoader cl = getClassLoader();
Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path));
while (resourceUrls.hasMoreElements()) {
URL url = resourceUrls.nextElement();
result.add(convertClassLoaderURL(url));
}
if (!StringUtils.hasLength(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 在所有jar包定位basePackage所有路径,后封装成UrlResource
成功获取到Reasource之后org.springframework.core.io.support.PathMatchingResourcePatternResolver#findPathMatchingResources
protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
String rootDirPath = determineRootDir(locationPattern);
String subPattern = locationPattern.substring(rootDirPath.length());
Resource[] rootDirResources = getResources(rootDirPath);
Set<Resource> result = new LinkedHashSet<>(16);
for (Resource rootDirResource : rootDirResources) {
rootDirResource = resolveRootDirResource(rootDirResource);
URL rootDirUrl = rootDirResource.getURL();
if (equinoxResolveMethod != null && rootDirUrl.getProtocol().startsWith("bundle")) {
URL resolvedUrl = (URL) ReflectionUtils.invokeMethod(equinoxResolveMethod, null, rootDirUrl);
if (resolvedUrl != null) {
rootDirUrl = resolvedUrl;
}
rootDirResource = new UrlResource(rootDirUrl);
}
if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher()));
}
else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) {
result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPattern));
}
else {
result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));
}
}
if (logger.isTraceEnabled()) {
logger.trace("Resolved location pattern [" + locationPattern + "] to resources " + result);
}
return result.toArray(new Resource[0]);
}
进入org.springframework.core.io.support.PathMatchingResourcePatternResolver#doFindPathMatchingFileResources
protected Set<Resource> doFindPathMatchingFileResources(Resource rootDirResource, String subPattern)
throws IOException {
File rootDir;
try {
rootDir = rootDirResource.getFile().getAbsoluteFile();
}
catch (FileNotFoundException ex) {
if (logger.isDebugEnabled()) {
logger.debug("Cannot search for matching files underneath " + rootDirResource +
" in the file system: " + ex.getMessage());
}
return Collections.emptySet();
}
catch (Exception ex) {
if (logger.isInfoEnabled()) {
logger.info("Failed to resolve " + rootDirResource + " in the file system: " + ex);
}
return Collections.emptySet();
}
return doFindMatchingFileSystemResources(rootDir, subPattern);
}
获取basePackage文件夹,然后basePackage递归查找所有匹配class文件
进入org.springframework.core.io.support.PathMatchingResourcePatternResolver#doRetrieveMatchingFiles
protected void doRetrieveMatchingFiles(String fullPattern, File dir, Set<File> result) throws IOException {
if (logger.isTraceEnabled()) {
logger.trace("Searching directory [" + dir.getAbsolutePath() +
"] for files matching pattern [" + fullPattern + "]");
}
for (File content : listDirectory(dir)) {
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);
}
}
}
递归查找所有匹配class文件,后续会把扫描到的class文件包装成BeanDefinition,暂不深入研究
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 为 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) 也相同,只不过返回的资源时多个而已。
Resource[] getResources(String locationPattern)
public Resource[] getResources(String locationPattern) throws IOException {
Assert.notNull(locationPattern, "Location pattern must not be null");
// 以 classpath*: 开头
if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
// 路径包含通配符
if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {
return findPathMatchingResources(locationPattern);
}
else {
// 路径不包含通配符
return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
}
}
else {
int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 :
locationPattern.indexOf(':') + 1);
// 路径包含通配符
if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
return findPathMatchingResources(locationPattern);
}
else {
return new Resource[] {getResourceLoader().getResource(locationPattern)};
}
}
}
下面就 findAllClassPathResources()做详细分析。
findAllClassPathResources()
当 locationPattern 以 classpath*: 开头但是不包含通配符,则调用findAllClassPathResources() 方法加载资源。该方法返回 classes 路径下和所有 jar 包中的所有相匹配的资源。
protected Resource[] findAllClassPathResources(String location) throws IOException {
String path = location;
if (path.startsWith("/")) {
path = path.substring(1);
}
Set<Resource> result = doFindAllClassPathResources(path);
if (logger.isDebugEnabled()) {
logger.debug("Resolved classpath location [" + location + "] to resources " + result);
}
return result.toArray(new Resource[0]);
}
真正执行加载的是在 doFindAllClassPathResources()方法,如下:
protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {
Set<Resource> result = new LinkedHashSet<>(16);
ClassLoader cl = getClassLoader();
Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path));
while (resourceUrls.hasMoreElements()) {
URL url = resourceUrls.nextElement();
result.add(convertClassLoaderURL(url));
}
if ("".equals(path)) {
addAllClassLoaderJarRoots(cl, result);
}
return result;
}
doFindAllClassPathResources() 根据 ClassLoader 加载路径下的所有资源。在加载资源过程中如果,在构造 PathMatchingResourcePatternResolver 实例的时候如果传入了 ClassLoader,则调用其 getResources(),否则调用ClassLoader.getSystemResources(path)。 ClassLoader.getResources()如下:
public Enumeration<URL> getResources(String name) throws IOException {
@SuppressWarnings("unchecked")
Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2];
if (parent != null) {
tmp[0] = parent.getResources(name);
} else {
tmp[0] = getBootstrapResources(name);
}
tmp[1] = findResources(name);
return new CompoundEnumeration<>(tmp);
}
看到这里是不是就已经一目了然了?如果当前父类加载器不为 null,则通过父类向上迭代获取资源,否则调用 getBootstrapResources()。这里是不是特别熟悉,(▽)。
若 path 为 空(“”)时,则调用 addAllClassLoaderJarRoots()方法。该方法主要是加载路径下得所有 jar 包,方法较长也没有什么实际意义就不贴出来了。
通过上面的分析,我们知道 findAllClassPathResources() 其实就是利用 ClassLoader 来加载指定路径下的资源,不管它是在 class 路径下还是在 jar 包中。如果我们传入的路径为空或者 /,则会调用 addAllClassLoaderJarRoots() 方法加载所有的 jar 包。
findAllClassPathResources()
当 locationPattern 以 classpath*: 开头且当中包含了通配符,则调用该方法进行资源加载。如下:
查找到了basePackage资源路径之后,需要根据basePackage查找到所有class资源
protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
// 确定跟路径
String rootDirPath = determineRootDir(locationPattern);
String subPattern = locationPattern.substring(rootDirPath.length());
// 获取根据路径下得资源
Resource[] rootDirResources = getResources(rootDirPath);
Set<Resource> result = new LinkedHashSet<>(16);
for (Resource rootDirResource : rootDirResources) {
rootDirResource = resolveRootDirResource(rootDirResource);
URL rootDirUrl = rootDirResource.getURL();
// bundle 资源类型
if (equinoxResolveMethod != null && rootDirUrl.getProtocol().startsWith("bundle")) {
URL resolvedUrl = (URL) ReflectionUtils.invokeMethod(equinoxResolveMethod, null, rootDirUrl);
if (resolvedUrl != null) {
rootDirUrl = resolvedUrl;
}
rootDirResource = new UrlResource(rootDirUrl);
}
// VFS 资源
if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher()));
}
// 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[0]);
}
方法有点儿长,但是思路还是很清晰的,主要分两步:
- 确定目录,获取该目录下得所有资源
- 在所获得的所有资源中进行迭代匹配获取我们想要的资源。
进入org.springframework.core.io.support.PathMatchingResourcePatternResolver#doFindPathMatchingFileResources
protected Set<Resource> doFindPathMatchingFileResources(Resource rootDirResource, String subPattern)
throws IOException {
File rootDir;
try {
rootDir = rootDirResource.getFile().getAbsoluteFile();
}
catch (FileNotFoundException ex) {
if (logger.isDebugEnabled()) {
logger.debug("Cannot search for matching files underneath " + rootDirResource +
" in the file system: " + ex.getMessage());
}
return Collections.emptySet();
}
catch (Exception ex) {
if (logger.isInfoEnabled()) {
logger.info("Failed to resolve " + rootDirResource + " in the file system: " + ex);
}
return Collections.emptySet();
}
return doFindMatchingFileSystemResources(rootDir, subPattern);
}
获取basePackage文件夹,然后basePackage递归查找所有匹配class文件
进入org.springframework.core.io.support.PathMatchingResourcePatternResolver#doRetrieveMatchingFiles
protected void doRetrieveMatchingFiles(String fullPattern, File dir, Set<File> result) throws IOException {
if (logger.isTraceEnabled()) {
logger.trace("Searching directory [" + dir.getAbsolutePath() +
"] for files matching pattern [" + fullPattern + "]");
}
for (File content : listDirectory(dir)) {
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 整个资源加载过程已经分析完毕。下面简要总结下:
- Spring 提供了 Resource 和 ResourceLoader 来统一抽象整个资源及其定位。使得资源与资源的定位有了一个更加清晰的界限,并且提供了合适的 Default 类,使得自定义实现更加方便和清晰。
- AbstractResource 为 Resource 的默认实现,它对 Resource 接口做了一个统一的实现,子类继承该类后只需要覆盖相应的方法即可,同时对于自定义的 Resource 我们也是继承该类。
- DefaultResourceLoader 同样也是 ResourceLoader 的默认实现,在自定 ResourceLoader 的时候我们除了可以继承该类外还可以实现 ProtocolResolver 接口来实现自定资源加载协议。
- DefaultResourceLoader 每次只能返回单一的资源,所以 Spring 针对这个提供了另外一个接口 ResourcePatternResolver ,该接口提供了根据指定的 locationPattern 返回多个资源的策略。其子类 PathMatchingResourcePatternResolver 是一个集大成者的 ResourceLoader ,因为它即实现了 Resource getResource(String location) 也实现了 Resource[] getResources(String locationPattern)。
AbstractApplicationContext为什么继承DefaultResourceLoader
AbstractApplicationContext还引用了PathMatchingResourcePatternResolver可以使用
Resource[] getResources(String locationPattern) throws IOException;
同时又具备了DefaultResourceLoader的
Resource getResource(String location);
然后再创建引用时
protected ResourcePatternResolver getResourcePatternResolver() {
return new PathMatchingResourcePatternResolver(this);
}
新建PathMatchingResourcePatternResolver并把自己传进去,PathMatchingResourcePatternResolver.getResource又调用 getResourceLoader().getResource(location);
@Override
public Resource getResource(String location) {
return getResourceLoader().getResource(location);
}
也就是说,饶了一圈,getResource还是DefaultResourceLoader中的那个getResource