PathResourceResolver和ClassPathResource源码解析
先来看一下 org.springframework.web.servlet.resource.PathResourceResolver
的继承关系图:
- ResourceResolver 接口需要实现 resolveUrlPath(返回值为String) 和 resolveResource(返回值为 org.springframework.core.io.Resource)
- AbstractResourceResolver 则是实现了 ResourceResolver,并声明了继承类需要实现的对应方法 resolveUrlPathInternal 和 resolveResourceInternal
- PathResourceResolver 继承了 AbstractResourceResolver,在实现 resolveUrlPathInternal 和 resolveResourceInternal 都调用了核心方法 getResource
PathResourceResolver源码解析
点开查看PathResourceResolver 的 resolveUrlPathInternal & resolveResourceInternal 方法源码
protected Resource resolveResourceInternal(@Nullable HttpServletRequest request, String requestPath,
List locations, ResourceResolverChain chain) {
return getResource(requestPath, request, locations);
}
protected String resolveUrlPathInternal(String resourcePath, List locations,
ResourceResolverChain chain) {
// 调用 getResource 方法,就是想查询一个对应路径下,是否存在该资源?
// 如果存在,则返回该 resourcePath,如果不存在,就返回 null
return (StringUtils.hasText(resourcePath) &&
getResource(resourcePath, null, locations) != null ? resourcePath : null);
}
getResource源码解析
这里调用的 locations 是个数组,因此逐一查看是否某个 location 下。
encodeIfNecessary 这个方法,功能也比较简单,就是先用分隔符
/
进行分词,然后对分词后的每一部分进行编码,最后再用/
拼接还原。
目前主要研究访问静态资源时用到PathResourceResolver的场景。在这种场景下, encodeIfNecessary 常常是在执行 shouldEncodeRelativePath 方法直接放回 false,即不编码直接返回了,所以不重点分析了~
// PathResourceResolver.java
protected Resource getResource(String resourcePath, Resource location) throws IOException {
// 第一步创建相对路径
Resource resource = location.createRelative(resourcePath);
// 第二步判断是否存在,是否可读
if (resource.isReadable()) {
// 第三步进一步验证资源是在相对于它被发现的位置下,以及是否在 {@link #setAllowedLocations 允许的位置} 之下。
if (checkResource(resource, location)) {
return resource;
}
else if (logger.isWarnEnabled()) {
Resource[] allowedLocations = getAllowedLocations();
logger.warn("Resource path \"" + resourcePath + "\" was successfully resolved " +
"but resource \"" + resource.getURL() + "\" is neither under the " +
"current location \"" + location.getURL() + "\" nor under any of the " +
"allowed locations " + (allowedLocations != null ? Arrays.asList(allowedLocations) : "[]"));
}
}
return null;
}
checkResource源码解析
// PathResourceResolver.java
protected boolean checkResource(Resource resource, Resource location) throws IOException {
// 确认resource确实在location之下
// 也就是location.path就是resource.path的前缀部分
if (isResourceUnderLocation(resource, location)) {
return true;
}
Resource[] allowedLocations = getAllowedLocations();
if (allowedLocations != null) {
for (Resource current : allowedLocations) {
if (isResourceUnderLocation(resource, current)) {
return true;
}
}
}
return false;
}
allowedLocations来源解析
WebMvcConfigurer=>ResourceHandlerRegistration
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
/**
* 静态资源处理
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 从WebMvcConfigurer设置到ResourceHandlerRegistration
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
}
}
ResourceHandlerRegistration=>ResourceHttpRequestHandler
// ResourceHandlerRegistration.java
public class ResourceHandlerRegistration {
private final List<String> locationValues = new ArrayList<>();
// 对应上一段代码addResourceLocations的方法
public ResourceHandlerRegistration addResourceLocations(String... resourceLocations) {
this.locationValues.addAll(Arrays.asList(resourceLocations));
return this;
}
protected ResourceHttpRequestHandler getRequestHandler() {
ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
if (this.resourceChainRegistration != null) {
handler.setResourceResolvers(this.resourceChainRegistration.getResourceResolvers());
handler.setResourceTransformers(this.resourceChainRegistration.getResourceTransformers());
}
// 设置到 ResourceHttpRequestHandler 中
handler.setLocationValues(this.locationValues);
if (this.cacheControl != null) {
handler.setCacheControl(this.cacheControl);
}
else if (this.cachePeriod != null) {
handler.setCacheSeconds(this.cachePeriod);
}
return handler;
}
}
ResourceHttpRequestHandler=>PathResourceResolver
// ResourceHttpRequestHandler.java
public void setLocationValues(List<String> locationValues) {
Assert.notNull(locationValues, "Location values list must not be null");
this.locationValues.clear();
// 数据来自于ResourceHandlerRegistration设置
this.locationValues.addAll(locationValues);
}
// 把List<String>类型的locationValues转化为
// List<Resource>类型的locations
private void resolveResourceLocations() {
if (CollectionUtils.isEmpty(this.locationValues)) {
return;
}
else if (!CollectionUtils.isEmpty(this.locations)) {
throw new IllegalArgumentException("Please set either Resource-based \"locations\" or " +
"String-based \"locationValues\", but not both.");
}
ApplicationContext applicationContext = obtainApplicationContext();
for (String location : this.locationValues) {
if (this.embeddedValueResolver != null) {
String resolvedLocation = this.embeddedValueResolver.resolveStringValue(location);
if (resolvedLocation == null) {
throw new IllegalArgumentException("Location resolved to null: " + location);
}
location = resolvedLocation;
}
Charset charset = null;
location = location.trim();
if (location.startsWith(URL_RESOURCE_CHARSET_PREFIX)) {
int endIndex = location.indexOf(']', URL_RESOURCE_CHARSET_PREFIX.length());
if (endIndex == -1) {
throw new IllegalArgumentException("Invalid charset syntax in location: " + location);
}
String value = location.substring(URL_RESOURCE_CHARSET_PREFIX.length(), endIndex);
charset = Charset.forName(value);
location = location.substring(endIndex + 1);
}
Resource resource = applicationContext.getResource(location);
this.locations.add(resource);
if (charset != null) {
if (!(resource instanceof UrlResource)) {
throw new IllegalArgumentException("Unexpected charset for non-UrlResource: " + resource);
}
this.locationCharsets.put(resource, charset);
}
}
}
protected void initAllowedLocations() {
if (CollectionUtils.isEmpty(this.locations)) {
return;
}
for (int i = getResourceResolvers().size() - 1; i >= 0; i--) {
if (getResourceResolvers().get(i) instanceof PathResourceResolver) {
PathResourceResolver pathResolver = (PathResourceResolver) getResourceResolvers().get(i);
if (ObjectUtils.isEmpty(pathResolver.getAllowedLocations())) {
// 从ResourceHttpRequestHandler.locations设置到PathResourceResolver.allowLocations中去
pathResolver.setAllowedLocations(getLocations().toArray(new Resource[0]));
}
if (this.urlPathHelper != null) {
pathResolver.setLocationCharsets(this.locationCharsets);
pathResolver.setUrlPathHelper(this.urlPathHelper);
}
break;
}
}
}
ClassPathResource源码解析
接下来主要针对 Resource 的子类 ClassPathLocation 进行分析:
ClassPathResource 是类路径资源的 {@link Resource} 实现。
使用给定的 {@link ClassLoader} 或给定的 {@link Class} 来加载资源。
如果类路径资源位于文件系统中,则支持解析为 {@code java.io.File},但不支持 JAR 中的资源(解析为 File)。始终支持解析为 URL。
createRelative源码解析
该方法作用就是解析出文件在 classpath 下的相对路径:
比如这个文件 classes/static/lib/jquery-3.6.0.min.js
在类路径下的相对路径就是 static/lib/jquery-3.6.0.min.js
// ClassPathResource.java
public Resource createRelative(String relativePath) {
String pathToUse = StringUtils.applyRelativePath(this.path, relativePath);
return (this.clazz != null ? new ClassPathResource(pathToUse, this.clazz) :
new ClassPathResource(pathToUse, this.classLoader));
}
// StringUtils.java
public static String applyRelativePath(String path, String relativePath) {
// 比如参数 path="static/",relativePath="lib/jquery-3.6.0.min.js"
int separatorIndex = path.lastIndexOf("/");
if (separatorIndex != -1) {
String newPath = path.substring(0, separatorIndex); // 结果为 newPath="static",去掉了结尾的"/"
if (!relativePath.startsWith("/")) {
newPath += FOLDER_SEPARATOR; // newPath="static/",看似无意义,其实是考虑到 relativePath 和父文件夹之间还是要有一个"/"作为分隔
}
return newPath + relativePath; 返回值为 static/lib/jquery-3.6.0.min.js
}
else {
return relativePath;
}
}
isReadable&exists源码解析
// 接口 Resource.java
public interface Resource extends InputStreamSource {
boolean exists();
// isReadable默认和exists是同一个实现,ClassPathResource没有对此方法进行覆写
default boolean isReadable() {
return exists();
}
}
继续来看 exists 方法
// ClassPathResource.java
@Override
public boolean exists() {
return (resolveURL() != null);
}
protected URL resolveURL() {
// 到了这一层,再往下就是调用 Jdk 的方法了
// this.path 的类型是String,所以以下三个方法的入参类型也是String
// 输入一个字符串url,返回一个URL对象
try {
if (this.clazz != null) {
return this.clazz.getResource(this.path);
}
else if (this.classLoader != null) {
return this.classLoader.getResource(this.path);
}
else {
return ClassLoader.getSystemResource(this.path);
}
}
catch (IllegalArgumentException ex) {
// Should not happen according to the JDK's contract:
// see https://github.com/openjdk/jdk/pull/2662
return null;
}
}