Spring中資源的載入原來是這麼一回事啊!

来源:https://www.cnblogs.com/i-code/archive/2020/05/07/12845329.html
-Advertisement-
Play Games

1. 簡介 在JDK中 適用於載入資源的類,但是 的實現類都是訪問網路資源的,並沒有可以從類路徑或者相對路徑獲取文件及 , 雖然可以通過自定義擴展URL介面來實現新的處理程式,但是這是非常複雜的,同時 介面中定義的行為也並不是很理想的 ,如檢測資源的存在等的行為,這也是 為什麼自己全新的開發一套自己 ...


1. 簡介

在JDK中 java.net.URL 適用於載入資源的類,但是 URL 的實現類都是訪問網路資源的,並沒有可以從類路徑或者相對路徑獲取文件及 ServletContext , 雖然可以通過自定義擴展URL介面來實現新的處理程式,但是這是非常複雜的,同時 URL 介面中定義的行為也並不是很理想的 ,如檢測資源的存在等的行為,這也是 spring 為什麼自己全新的開發一套自己的資源載入策略, 同時它也滿足下麵的特點:

  • 單一職責原則,將資源的定義和資源的載入的模型界限劃的非常清晰
  • 採用高度抽象,統一的資源定義和資源載入的策略和行為,資源載入返回給客戶端的是抽象的資源,客戶端根據資源的行為定義對其進行具體化的處理

2. Resource 介面

spring 中的 Resource 介面目的在於成為一種功能更加強大的介面,用於抽象化對具體資源的訪問,它繼承了 org.springframework.core.io.InputStreamSource 介面,作為資源定義的頂級介面, Resource 內部定義了通用的方法,並且有它的子類 AbstractResource 來提供統一的預設實現,

Resouerce 介面定義:

//資源定義介面
public interface Resource extends InputStreamSource {

	/**
	 * 檢驗資源是否是物理存在
	 */
	boolean exists();

	/**
	 * 判斷資源是否是可讀的
	 */
	default boolean isReadable() {
		return exists();
	}

	/**
	 * 判斷資源是否是打開的,true為打開
	 */
	default boolean isOpen() {
		return false;
	}

	/**
	 * 判斷該資源是否是文件 true為是
	 */
	default boolean isFile() {
		return false;
	}

	/**
	 * 返回該資源的URL句柄
	 */
	URL getURL() throws IOException;

	/**
	 * 返回該資源的URI句柄
	 */
	URI getURI() throws IOException;

	/**
	 * 獲取該資源的File句柄
	 */
	File getFile() throws IOException;

	/**
	 * 返回一個ReadableByteChannel 作為NIO中的可讀通道
	 */
	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();

}

InputStreamSource 介面定義:

public interface InputStreamSource {

	/**
	 * Return an {@link InputStream} for the content of an underlying resource.
	 * <p>It is expected that each call creates a <i>fresh</i> stream.
	 * <p>This requirement is particularly important when you consider an API such
	 * as JavaMail, which needs to be able to read the stream multiple times when
	 * creating mail attachments. For such a use case, it is <i>required</i>
	 * that each {@code getInputStream()} call returns a fresh stream.
	 * @return the input stream for the underlying resource (must not be {@code null})
	 * @throws java.io.FileNotFoundException if the underlying resource doesn't exist
	 * @throws IOException if the content stream could not be opened
	 */
	InputStream getInputStream() throws IOException;

}

Resource 中一些最重要的方法:

  • getInputStream() :找到並打開資源,並返回一個資源以 InputStream 供讀取,每次調用都會返回一個新的 InputStream ,調用者有責任關閉流

  • exists() :返回 boolean 指示此資源是否實際以物理形式存在。

  • isOpen() :返回, boolean 指示此資源是否表示具有打開流的句柄, 如果為 trueInputStream 則不能多次讀取,必須只讀取一次,然後將其關閉以避免資源泄漏。返回 false 所有常用資源實現(除外) InputStreamResource 可讀

  • getDescription() :返回對此資源的描述,以便在使用該資源時用於錯誤輸出。這通常是標準文件名或資源的實際 URL

** Resource 實現**

Resource類圖

  • UrlResource : 包裝一個 java.net.URL ,可用於訪問通常可以通過 URL 訪問的任何對象,例如文件, HTTP 目標, FTP 目標等。所有 URL 都有一個標準化的 String 表示形式,因此適當的標準化首碼可用於指示另一種 URL 類型。如: file : 訪問文件系統路徑, http : 通過 HTTP 協議 ftp : 訪問資源,通過 FTP 訪問資源等

  • ClassPathResource : 此類表示應從類路徑獲取的資源。它使用線程上下文類載入器( ClassLoader ),給定的類載入器或給定的類來載入資源

  • FileSystemResource : 是一個 Resource 執行 java.io.Filejava.nio.file.Path 類型資源的封裝,它支持 FileURL , 實現 WritableResource 介面,且從 Spring Framework 5.0 開始, FileSystemResource 使用 NIO2 API 進行讀/寫交互

  • ServletContextResource : 該 ServletContex t資源解釋相關 Web 應用程式的根目錄內的相對路徑。

  • InputStreamResource : 將給定的 InputStream 作為一種資源的 Resource 的實現類

  • ByteArrayResource : 這是Resource給定位元組數組的實現。它為給定的位元組數組創建一個 ByteArrayInputStream

3. ResourceLoader 介面

ResourceLoader 主要是用於返回(即載入) Resource 對象,主要定義:

public interface ResourceLoader {

	/** Pseudo URL prefix for loading from the class path: "classpath:". */
	String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX;

	/**
	 * 返回指定路徑的資源處理器
	 * 必須支持完全限定的網址: "file:C:/test.dat"
	 * 必須支持ClassPath 的 URL  :"classpath:test.dat"
	 * 必須支持相對路徑  : "WEB-INF/test.dat"
	 * 並不能保證資源是否物理存在,需要自己去檢測通過existence
	 * 再spring中所有的應用上下文都去實現這個介面,可以進行資源的載入
	 */
	Resource getResource(String location);

	/**
	 * 返回當前類的 ClassLoader 對象
	 */
	@Nullable
	ClassLoader getClassLoader();

}
  • 應用上下文即容器都有實現 ResourceLoader 這個介面,所有的上下文都可以用於獲取 Resource 實例對象

  • 我們可以在特定的應用上下文中通過 getResource() 來獲取特定類型的 Resource 實例,但是的保證 location 路徑沒有特殊的首碼,如 classpatch: 等,如果有特定首碼慢麽會強制使用相應的資源類型,與上下文無關。

Prefix Example Explanation
classpath: classpath:com/myapp/config.xml 從類路徑載入
file: file:///data/config.xml 從文件系統作為 URL 載入
http: https://myserver/logo.png 按照URL形式載入
(none) /data/config.xml 取決於應用上下文

ResourceLoader 的子類結構:

3.1 DefaultResourceLoader

這個類是 ResourceLoader 的預設實現類,與 Resource 介面的 AbstractResource 一樣,

3.1.1. 構造函數

  • 提供有參和無參的構造函數,有參構造函數接受 ClassLoader 類型,如不帶參數則使用預設的 ClassLoader , Thread.currentThread()#getContextClassLoader()

核心代碼代碼,部分省去:

public class DefaultResourceLoader implements ResourceLoader {

	@Nullable
	private ClassLoader classLoader;

	private final Set<ProtocolResolver> protocolResolvers = new LinkedHashSet<>(4);

	private final Map<Class<?>, Map<Resource, ?>> resourceCaches = new ConcurrentHashMap<>(4);

	/**
	 * 無參構造函數
	 * @see java.lang.Thread#getContextClassLoader()
	 */
	public DefaultResourceLoader() {
		this.classLoader = ClassUtils.getDefaultClassLoader();
	}

	/**
	 * 帶ClassLoader的有參構造函數
	 */
	public DefaultResourceLoader(@Nullable ClassLoader classLoader) {
		this.classLoader = classLoader;
	}

	/**
     * 設置 ClassLoader
	 */
	public void setClassLoader(@Nullable ClassLoader classLoader) {
		this.classLoader = classLoader;
	}

	/**
	 * Return the ClassLoader to load class path resources with.、
	 * @see ClassPathResource
	 */
	@Override
	@Nullable
	public ClassLoader getClassLoader() {
		return (this.classLoader != null ? this.classLoader : ClassUtils.getDefaultClassLoader());
	}

	/**
	 * Obtain a cache for the given value type, keyed by {@link Resource}.
	 * @param valueType the value type, e.g. an ASM {@code MetadataReader}
	 * @return the cache {@link Map}, shared at the {@code ResourceLoader} level
	 * @since 5.0
	 */
	@SuppressWarnings("unchecked")
	public <T> Map<Resource, T> getResourceCache(Class<T> valueType) {
		return (Map<Resource, T>) this.resourceCaches.computeIfAbsent(valueType, key -> new ConcurrentHashMap<>());
	}

	/**
	 * Clear all resource caches in this resource loader.
	 * @since 5.0
	 * @see #getResourceCache
	 */
	public void clearResourceCaches() {
		this.resourceCaches.clear();
	}
}

3.1.2 getResource() 核心方法

ResourceLoader 中最核心的方法,他根據傳入的 location 來返回相應的Resource,而 DefaultResourceLoader 對其做了核心實現, 子類都沒覆蓋該方法,所以我們可以斷定 ResourceLoader 載入資源的核心策略都在 DefaultResourceLoader

核心代碼:

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

		//1. 通過 ProtocolResolver 協議解析器來記載資源
		for (ProtocolResolver protocolResolver : getProtocolResolvers()) {
			Resource resource = protocolResolver.resolve(location, this);
			if (resource != null) {
				return resource;
			}
		}

		//2.如果location是以 / 開頭則返回 ClassPathContextResource 類型的 資源
		if (location.startsWith("/")) {
			return getResourceByPath(location);
		}//3.如果是以 classpath: 開頭,則返回 ClassPathResource 類型的資源
		else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
			return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
		}
		else {
			try {
				//4.如果不是以上兩種,則判斷是否是 File URL ,如果是返回FileUrlResource 否則 返回UrlResource
				// 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.
				//5.最後則返回ClassPathContextResource
				return getResourceByPath(location);
			}
		}
	}

上述代碼中具體說明瞭執行的流程,其中 getResourceByPath(location) 的具體實現代碼如下:

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

3.1.3 ProtocolResolver

全限定類名: org.springframework.core.io.ProtocolResolver ,是一個介面,用於用戶自定義協議資源解析策略,是 DefaultResourceLoaderSPI ,允許處理自定義協議而無需將載入程式實現(或應用程式上下文實現)為子類,即不需要繼承 ResourceLoader 的子類 DefaultResourceLoader , 而直接實現 ProtocolResolver 介面就可以自定義 ResourceLoader

@FunctionalInterface
public interface ProtocolResolver {

	/**
     * 使用指定的ResourceLoader  來解析location路徑的 資源
	 * Resolve the given location against the given resource loader
	 * if this implementation's protocol matches.
	 * @param location the user-specified resource location
	 * @param resourceLoader the associated resource loader
	 * @return a corresponding {@code Resource} handle if the given location
	 * matches this resolver's protocol, or {@code null} otherwise
	 */
	@Nullable
	Resource resolve(String location, ResourceLoader resourceLoader);

}

在spring中該類並沒有任何實現類,他需要用戶自己實現,那麼自定義的 ProtocolResolver 如何載入到spring中呢?在我們 DefaultResourceLoader 類中有一個方法 addProtocolResolver(ProtocolResolver resolver) 則是用來添加的

	/**
	 * Register the given resolver with this resource loader, allowing for
	 * additional protocols to be handled.
	 * <p>Any such resolver will be invoked ahead of this loader's standard
	 * resolution rules. It may therefore also override any default rules.
	 * @since 4.3
	 * @see #getProtocolResolvers()
	 */
	public void addProtocolResolver(ProtocolResolver resolver) {
		Assert.notNull(resolver, "ProtocolResolver must not be null");
		this.protocolResolvers.add(resolver);
	}

3.2 FileSystemResourceLoader

DefaultResourceLoadergetResourceByPath() 方法的處理是直接返回了一個 ClassPathContextResource 類型的資源,這其實是不完善的,在spring中 FileSystemResourceLoader 類繼承了 DefaultResourceLoader ,同時重寫了 getResourceByPath() 方法,使用標準的文件系統讀入,並且返回 FileSystemContextResource 類型

public class FileSystemResourceLoader extends DefaultResourceLoader {

	/**
	 * Resolve resource paths as file system paths.
	 * <p>Note: Even if a given path starts with a slash, it will get
	 * interpreted as relative to the current VM working directory.
	 * @param path the path to the resource
	 * @return the corresponding Resource handle
	 * @see FileSystemResource
	 * @see org.springframework.web.context.support.ServletContextResourceLoader#getResourceByPath
	 */
	@Override
	protected Resource getResourceByPath(String path) {
		if (path.startsWith("/")) {
			path = path.substring(1);
		}
		return new FileSystemContextResource(path);
	}

	/**
	 * FileSystemResource that explicitly expresses a context-relative path
	 * through implementing the ContextResource interface.
	 */
	private static class FileSystemContextResource extends FileSystemResource implements ContextResource {

		public FileSystemContextResource(String path) {
			super(path);
		}

		@Override
		public String getPathWithinContext() {
			return getPath();
		}
	}

}
  • 我們可以從 上面的代碼中看到 在 FileSystemResourceLoader 中有一個私有的內部類 FileSystemContextResource , 這個類繼承了 FileSystemResource ,同時實現了 ContextResource 介面

  • FileSystemContextResource 通過構造函數調用 FileSystemResource 的構造函數,創建 FileSystemResource 類型資源定義,同時實現 ContextResource 是為了實現其中的 getPathWithinContext() 方法,這個方法是用來獲取上下文根路徑的, 源碼中這樣寫的 :

/**

 * Return the path within the enclosing 'context'.  
 * This is typically path relative to a context-specific root directory,  
 * e.g. a ServletContext root or a PortletContext root.  
 */  

3.3 ClassRelativeResourceLoader

org.springframework.core.io.ClassRelativeResourceLoader 類也是 DefaultResourceLoader 的另一個實現子類,與 FileSystemResourceLoader 類似,也同樣重寫了 getResourceByPath() 方法,也內部維護了一個私有的內部類 ClassRelativeContextResource , 具體代碼如下:

/**
 * 從給定的 class 下載入資源
 * {@link ResourceLoader} implementation that interprets plain resource paths
 * as relative to a given {@code java.lang.Class}.
 *
 * @author Juergen Hoeller
 * @since 3.0
 * @see Class#getResource(String)
 * @see ClassPathResource#ClassPathResource(String, Class)
 */
public class ClassRelativeResourceLoader extends DefaultResourceLoader {

	private final Class<?> clazz;

	/**
	 * Create a new ClassRelativeResourceLoader for the given class.
	 * @param clazz the class to load resources through
	 */
	public ClassRelativeResourceLoader(Class<?> clazz) {
		Assert.notNull(clazz, "Class must not be null");
		this.clazz = clazz;
		setClassLoader(clazz.getClassLoader());
	}

	/**
	 * 重寫getResourceByPath 方法 , 返回一個ClassRelativeContextResource 資源類型
	 * @param path the path to the resource
	 * @return
	 */
	@Override
	protected Resource getResourceByPath(String path) {
		return new ClassRelativeContextResource(path, this.clazz);
	}

	/**
	 * 繼承 ClassPathResource  定義資源類型,實現ContextResource 中的 getPathWithinContext 方法,
	 *
	 * ClassPathResource that explicitly expresses a context-relative path
	 * through implementing the ContextResource interface.
	 */
	private static class ClassRelativeContextResource extends ClassPathResource implements ContextResource {

		private final Class<?> clazz;

		/**
		 * 調用父類 ClassPathResource 對資源進行初始化
		 * @param path
		 * @param clazz
		 */
		public ClassRelativeContextResource(String path, Class<?> clazz) {
			super(path, clazz);
			this.clazz = clazz;
		}

		@Override
		public String getPathWithinContext() {
			return getPath();
		}

        /**
		 * 重寫 ClassPathContext 中方法, 通過給定的路徑返回一個ClassRelativeContextResource資源
		 * @param relativePath the relative path (relative to this resource)
		 * @return
		 */
		@Override
		public Resource createRelative(String relativePath) {
			String pathToUse = StringUtils.applyRelativePath(getPath(), relativePath);
			return new ClassRelativeContextResource(pathToUse, this.clazz);
		}
	}

}

3.4 ResourcePatternResolver

org.springframework.core.io.support.ResourcePatternResolver 是對 ResourceLoader 的一個擴展,我們在 ResourceLoader 中通過 getResource 方法獲取 Resource 實例時,只能通過一個 location 來獲取一個 Resource , 而不能獲取到多個 Resource , 當我們需要載入多個資源時,只能通過調用多次的該方法來實現,所以spring 提供了 ResourcePatternResolver 對其進行了擴展,實現了通過 location 來載入多個資源,類的定義如下:

public interface ResourcePatternResolver extends ResourceLoader {

	/**
	 * Pseudo URL prefix for all matching resources from the class path: "classpath*:"
	 * This differs from ResourceLoader's classpath URL prefix in that it
	 * retrieves all matching resources for a given name (e.g. "/beans.xml"),
	 * for example in the root of all deployed JAR files.
	 * @see org.springframework.core.io.ResourceLoader#CLASSPATH_URL_PREFIX
	 */
	String CLASSPATH_ALL_URL_PREFIX = "classpath*:";

	/**
	 * Resolve the given location pattern into Resource objects.
	 * <p>Overlapping resource entries that point to the same physical
	 * resource should be avoided, as far as possible. The result should
	 * have set semantics.
	 * @param locationPattern the location pattern to resolve
	 * @return the corresponding Resource objects
	 * @throws IOException in case of I/O errors
	 */
	Resource[] getResources(String locationPattern) throws IOException;

}
  • 可以看到 ResourcePatternResolver 新增加了一個方法 getResources ,返回一個 Resource 數組

  • 這裡我們要註意, ResourcePatternResolver 增加了一個新的協議首碼 classpath*: , 看到這裡是不是大家可以很熟悉的想起我們在平時配置路徑時經常會寫 classpath:classpath*: ,那麼他們的區別就在這裡,他們的資源載入方式時不一樣的

3.5 PathMatchingResourcePatternResolver

org.springframework.core.io.support.PathMatchingResourcePatternResolverResourcePatternResolver 的一個主要實現類,也是使用較多的一個實現類,我們可以來看一下,它主要實現了 新增首碼的解析,同時還支持 Ant 風格的路徑匹配模式(如 : "**/*.xml" )

3.5.1 構造函數

PathMatchingResourcePatternResolver 提供了三個構造函數:

	/**
	 * 內置 資源定位載入器
	 */
	private final ResourceLoader resourceLoader;

	/**
	 * Ant路徑匹配器
	 */
	private PathMatcher pathMatcher = new AntPathMatcher();

	/**
	 * 無參構造函數,當不指定內部載入器類型時,預設是 DefaultResourceLoader
	 * Create a new PathMatchingResourcePatternResolver with a DefaultResourceLoader.
	 * <p>ClassLoader access will happen via the thread context class loader.
	 * @see org.springframework.core.io.DefaultResourceLoader
	 */
	public PathMatchingResourcePatternResolver() {
		this.resourceLoader = new DefaultResourceLoader();
	}

	/**
	 * 指定特定的資源定位載入器
	 * Create a new PathMatchingResourcePatternResolver.
	 * <p>ClassLoader access will happen via the thread context class loader.
	 * @param resourceLoader the ResourceLoader to load root directories and
	 * actual resources with
	 */
	public PathMatchingResourcePatternResolver(ResourceLoader resourceLoader) {
		Assert.notNull(resourceLoader, "ResourceLoader must not be null");
		this.resourceLoader = resourceLoader;
	}

	/**
	 * 使用預設的資源載入器,但是傳入 classLoader ,使用特定的類載入
	 * Create a new PathMatchingResourcePatternResolver with a DefaultResourceLoader.
	 * @param classLoader the ClassLoader to load classpath resources with,
	 * or {@code null} for using the thread context class loader
	 * at the time of actual resource access
	 * @see org.springframework.core.io.DefaultResourceLoader
	 */
	public PathMatchingResourcePatternResolver(@Nullable ClassLoader classLoader) {
		this.resourceLoader = new DefaultResourceLoader(classLoader);
	}
  • 我們可以看到,當構造函數不提供 ResourceLoader 時,預設是 DefaultResourceLoader

3.5.2 getResource

PathMatchingResourcePatternResolver 中的 getResource 方法的實現是調用了 傳入的 ResourceLoader 或者預設的 DefaultResourceLoader , 具體的代碼實現如下:

/**
	 * 調用getResourceLoader 獲取當前的 ResourceLoader 
	 * @param location the resource location
	 * @return
	 */
	@Override
	public Resource getResource(String location) {
		return getResourceLoader().getResource(location);
    }
    
    	/**
	 * Return the ResourceLoader that this pattern resolver works with.
	 */
	public ResourceLoader getResourceLoader() {
		return this.resourceLoader;
	}

3.5.3 getResources

實現了 ResourcePatternResolvergetResources 方法,可以通過 location 載入多個資源,進行分類處理,如果是沒有 classpath*: 首碼以及不包含通配符的情況下直接調用當前類的 ResourceLoader 來進行處理,其他按具體來處理,主要涉及兩個方法 #findPathMatchingResources(...)#findAllClassPathResources(...)

	@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)) {
			//1.1.進行路徑匹配校驗 是否包含通配符
			// 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 {
				//1.2 不包含通配符
				// all class path resources with the given name
				return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
			}
		}
		else {
			// 2. 不是classpath首碼開頭
			// 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);
			//2.1 校驗是否包含通配符
			if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
				// a file pattern
				return findPathMatchingResources(locationPattern);
			}
			else {
				//2.2 不包含通配符 使用內部 ResourceLoader 進行資源載入 預設是 DefaultReourceLoader
				// a single resource with the given name
				return new Resource[] {getResourceLoader().getResource(locationPattern)};
			}
		}
	}

3.5.4 findPathMatchingResources

上面代碼中我們可以看到,當存在通配符時都會執行 #findPathMatchingResources(...) 方法,我們來看一下方法的定義:

	/**
	 * 通過ant解析器來對給定的路徑下的所有模糊資源進行解析和匹配
	 * 支持jar和zip以及系統中的文件資源
	 * Find all resources that match the given location pattern via the
	 * Ant-style PathMatcher. Supports resources in jar files and zip files
	 * and in the file system.
	 * @param locationPattern the location pattern to match
	 * @return the result as Resource array
	 * @throws IOException in case of I/O errors
	 * @see #doFindPathMatchingJarResources
	 * @see #doFindPathMatchingFileResources
	 * @see org.springframework.util.PathMatcher
	 */
	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.isTraceEnabled()) {
			logger.trace("Resolved location pattern [" + locationPattern + "] to resources " + result);
		}
		//轉換為數組返回
		return result.toArray(new Resource[0]);
	}
  • spring 中很多真真做操作的方法命名都是以 do 開頭,我們從上面可以看到核心方法 #doFindPathMatchingFileResources(...)#doFindPathMatchingJarResources(...) 這兩個基本一樣知識解析不懂的文件類型,另外還有一個方法 #determineRootDir(...) 方法實現了路徑的解析,下麵我們簡單看一這兩個實現。
3.5.4.1 determineRootDir(... )

determineRootDir 方法主要用於根路徑的獲取,解析路徑中的通配符,代碼如下:

	/**
	 * 通過給定的路徑來獲取根目錄路徑
	 * Determine the root directory for the given location.
	 * <p>Used for determining the starting point for file matching,
	 * resolving the root directory location to a {@code java.io.File}
	 * and passing it into {@code retrieveMatchingFiles}, with the
	 * remainder of the location as pattern.
	 * <p>Will return "/WEB-INF/" for the pattern "/WEB-INF/*.xml",
	 * for example.
	 * @param location the location to check
	 * @return the part of the location that denotes the root directory
	 * @see #retrieveMatchingFiles
	 */
	protected String determineRootDir(String location) {
		//1. 找到最後 路徑中出現的 : 的索引 +1 ,這裡註意我們的路徑時 類似 : classpath*: /web-inf/*.xml
		int prefixEnd = location.indexOf(':') + 1;
		//2. 獲取跟路徑長度
		int rootDirEnd = location.length();
		//3.判斷冒號後面的路徑是否包含通配符 如果包含,則截斷最後一個由”/”分割的部分。
		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);
	}

舉例看一下:

原路徑 獲取跟路徑
classpath*:/test/aa*/app-*.xml classpath*:/test/
classpath*:/test/aa/app-*.xml classpath*:/test/aa
3.5.4.2 doFindPathMatchingFileResources(... )

#doFindPathMatchingFileResources(...)#doFindPathMatchingJarResources(...) 方法的的內部基本一致,只是解析不同的類型文件,我們這裡只看其中一個則可,大家可以自行比對兩者的區別。

  • 我們跟一下 #doFindPathMatchingFileResources(...) 方法,方法內部調用較深,所以下麵我主要把代碼貼出來,註釋已有,相信可以看的懂

  • #doFindPathMatchingFileResources(...) 代碼:

 	/**
	 * 查找文件系統符合給定的location的資源, 路徑符合 ant 樣式的通配符
	 * Find all resources in the file system that match the given location pattern
	 * via the Ant-style PathMatcher.
	 * @param rootDirResource the root directory as Resource
	 * @param subPattern the sub pattern to match (below the root directory)
	 * @return a mutable Set of matching Resource instances
	 * @throws IOException in case of I/O errors
	 * @see #retrieveMatchingFiles
	 * @see org.springframework.util.PathMatcher
	 */
	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);
	}
  • 上面方法中主要調用了核心流程 #doFindMatchingFileSystemResources(...) , 代碼如下:
 /**
	 * 通過ant通配符的subPattern與已經獲取的根目錄rootDir來組合獲取所有在文件系統中的資源
	 * 如:我們本來的 url: 'classpath*:/test/aa/app-*.xml'
	 * 那麼這裡rootDir:classpath*:/test/aa/   subPattern :app-*.xml
	 * Find all resources in the file system that match the given location pattern
	 * via the Ant-style PathMatcher.
	 * @param rootDir the root directory in the file system
	 * @param subPattern the sub pattern to match (below the root directory)
	 * @return a mutable Set of matching Resource instances
	 * @throws IOException in case of I/O errors
	 * @see #retrieveMatchingFiles
	 * @see org.springframework.util.PathMatcher
	 */
	protected Set<Resource> doFindMatchingFileSystemResources(File rootDir, String subPattern) throws IOException {
		if (logger.isTraceEnabled()) {
			logger.trace("Looking for matching resources in directory tree [" + rootDir.getPath() + "]");
		}
		//調用真實處理方法,獲取set集合的File
		Set<File> matchingFiles = retrieveMatchingFiles(rootDir, subPattern);
		Set<Resource> result = new LinkedHashSet<>(matchingFiles.size());
		//將獲取的File轉換為 FileSystemResource 同時添加到result結果集中
		for (File file : matchingFiles) {
			result.add(new FileSystemResource(file));
		}
		return result;
	}

上面方法主要是將獲取的 Set<File> 的結果進行轉換,將資源類型轉換為 FileSystemResource , 上面方法的核心方法是 #retrieveMatchingFiles(...)

  • #retrieveMatchingFiles(...) 代碼如下:
 	/**
	 *
	 * Retrieve files that match the given path pattern,
	 * checking the given directory and its subdirectories.
	 * @param rootDir the directory to start from
	 * @param pattern the pattern to match against,
	 * relative to the root directory
	 * @return a mutable Set of matching Resource instances
	 * @throws IOException if directory contents could not be retrieved
	 */
	protected Set<File> retrieveMatchingFiles(File rootDir, String pattern) throws IOException {
		//1.不存在直接返回空集合
		if (!rootDir.exists()) {
			// Silently skip non-existing directories.
			if (logger.isDebugEnabled()) {
				logger.debug("Skipping [" + rootDir.getAbsolutePath() + "] because it does not exist");
			}
			return Collections.emptySet();
		}
		//2.不是目錄直接返回空
		if (!rootDir.isDirectory()) {
			// Complain louder if it exists but is no directory.
			if (logger.isInfoEnabled()) {
				logger.info("Skipping [" + rootDir.getAbsolutePath() + "] because it does not denote a directory");
			}
			return Collections.emptySet();
		}
		//3/判斷是否可讀
		if (!rootDir.canRead()) {
			if (logger.isInfoEnabled()) {
				logger.info("Skipping search for matching files underneath directory [" + rootDir.getAbsolutePath() +
						"] because the application is not allowed to read the directory");
			}
			return Collections.emptySet();
		}
		//4.將所有的系統分割器轉換為 /
		String fullPattern = StringUtils.replace(rootDir.getAbsolutePath(), File.separator, "/");
		//5.若子路徑開頭沒有 / 則父路徑要最後添加 /
		if (!pattern.startsWith("/")) {
			fullPattern += "/";
		}
		fullPattern = fullPattern + StringUtils.replace(pattern, File.separator, "/");
		Set<File> result = new LinkedHashSet<>(8);
		//真真處理的方法
		doRetrieveMatchingFiles(fullPattern, rootDir, result);
		return result;
	}

我們可以看到方法中主要是做了一些校驗和轉換,真真的處理是調用了 #doRetrieveMatchingFiles(...) 方法,

  • #doRetrieveMatchingFiles(...) 方法定義:
 	/**
	 * 遞歸遍歷 dir 目錄 結合fullpattern 進行路徑匹配,將符合的資源全部放入result中
	 * Recursively retrieve files that match the given pattern,
	 * adding them to the given result list.
	 * @param fullPattern the pattern to match against,
	 * with prepended root directory path
	 * @param dir the current directory
	 * @param result the Set of matching File instances to add to
	 * @throws IOException if directory contents could not be retrieved
	 */
	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, "/");
			//如果是目錄 同時和 fullPattern匹配 則進遞歸
			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);
			}
		}
	}

3.5.5 findAllClassPathResources

上面分析了當有通配符時的方法調用過程,那麼這裡我們來分析當沒有通配符時的方法調用

  • #findAllClassPathResources(...) 方法代碼:
	/**
	 * 通過ClassLoader 來 載入所有的 class location
	 * Find all class location resources with the given location via the ClassLoader.
	 * Delegates to {@link #doFindAllClassPathResources(String)}.
	 * @param location the absolute path within the classpath
	 * @return the result as Resource array
	 * @throws IOException in case of I/O errors
	 * @see java.lang.ClassLoader#getResources
	 * @see #convertClassLoaderURL
	 */
	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]);
	}
  • #doFindAllClassPathResources(...) 方法代碼:
	/**
	 * Find all class location resources with the given path via the ClassLoader.
	 * Called by {@link #findAllClassPathResources(String)}.
	 * @param path the absolute path within the classpath (never a leading slash)
	 * @return a mutable Set of matching Resource instances
	 * @since 4.1.1
	 */
	protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {
		Set<Resource> result = new LinkedHashSet<>(16);
		ClassLoader cl = getClassLoader();
		//1.通過ClassLoader獲取所有的URl
		Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path));
		while (resourceUrls.hasMoreElements()) {
			//將URl轉換為  UrlResource
			URL url = resourceUrls.nextElement();
			result.add(convertClassLoaderURL(url));
		}
		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...
			//添加所有的jar包
			addAllClassLoaderJarRoots(cl, result);
		}
		return result;
	}

方法內相對簡單,主要是通過ClassLoader來載入目錄下的jar資源,詳細不再貼出來,可以自行查看

本文由AnonyStar 發佈,可轉載但需聲明原文出處。
仰慕「優雅編碼的藝術」 堅信熟能生巧,努力改變人生
歡迎關註微信公賬號 :coder簡碼 獲取更多優質文章
更多文章關註筆者博客 :IT簡碼


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 數據很重要 在介紹MyBatis事務之前,先普及下資料庫事務相關知識 事務(Transaction)是訪問並可能更新資料庫中各種數據項的一個程式執行單元(unit)。事務通常由高級資料庫操縱語言或編程語言(如SQL,C++或Java)書寫的用戶程式的執行所引起,並用形如begin transacti ...
  • 原創聲明:本文轉載自公眾號【胖滾豬學編程】​ 某日,胖滾豬寫的代碼導致了一個生產bug,奮戰到凌晨三點依舊沒有解決問題。胖滾熊一看,只用了一個volatile就解決了。並告知胖滾豬,這是併發編程導致的坑。這讓胖滾豬堅定了要學好併發編程的決心。。於是,開始了我們併發編程的第一課。 序幕 BUG源頭之一 ...
  • 1、創建聊天消息表,其表的欄位有消息內容,發送時間和發送者的名稱; SQL: CREATE TABLE `guanhui`.`message` ( `id` INT(10) NOT NULL AUTO_INCREMENT COMMENT '消息ID' , `content` VARCHAR(255) ...
  • 官網:http://www.axios-js.com/zh-cn/docs/ 什麼是 axios? Axios 是一個基於 promise 的 HTTP 庫,可以用在瀏覽器和 node.js 中。 特性 從瀏覽器中創建 XMLHttpRequests 從 node.js 創建 http 請求 支持  ...
  • 如果熟悉C 語言的小伙伴們一般都會知道委托、事件的好處,只需在某個類中提前定義好公開的委托或事件(委托的特殊表現形式)變數,然後在其它類中就可以很隨意的訂閱該委托或事件,當委托或事件被觸發執行時,會自動通知所有的訂閱者進行消費處理。(觀察者模式用委托來實現是最好不過了,DDD所提倡的事件驅動其根本理 ...
  • 案例故事: Testlink是我們常用的用例管理工具,很多公司其實都在用, Testlink 支持制定測試計劃,支持多人線上同時管理維護/執行測試用例,自動生成測試報告等。 我個人也非常非常不推薦Excel線下管理測試用例, 但是官方提供的Testlink版本,是不支持Excel導入的,只能進行Xm ...
  • flutter sdk安裝包 【下載地址】https://flutter.dev/docs/development/tools/sdk/releases#macos 下載完成後,操作步驟: cd ~/目標路徑 unzip ~/安裝包路徑 設置&更新環境變數 打開 $HOME/.bash_profil ...
  • 前言 本文的文字及圖片來源於網路,僅供學習、交流使用,不具有任何商業用途,版權歸原作者所有,如有問題請及時聯繫我們以作處理。 作者:William Mannard 歡迎點擊右上角關註小編,除了分享技術文章之外還有很多福利,私信學習資料可以領取包括不限於Python實戰演練、PDF電子文檔、面試集錦、 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...