IT@程序员猿媛

【Spring 笔记】资源加载策略相关整理

2019-09-14  本文已影响0人  58bc06151329

文前说明

作为码农中的一员,需要不断的学习,我工作之余将一些分析总结和学习笔记写成博客与大家一起交流,也希望采用这种方式记录自己的学习之旅。

本文仅供学习交流使用,侵权必删。
不用于商业目的,转载请注明出处。

1. 概述

2. 原理

2.1 统一的资源定义(Resource)

public interface Resource extends InputStreamSource {
    /**
     * 资源是否存在
     */
    boolean exists();
    /**
     * 资源是否可读
     */
    boolean isReadable();
    /**
     * 资源所代表的句柄是否被一个 stream 打开了
     */
    boolean isOpen();
    /**
     * 返回资源的 URL 的句柄
     */
    URL getURL() throws IOException;
    /**
     * 返回资源的 URI 的句柄
     */
    URI getURI() throws IOException;
    /**
     * 返回资源的 File 的句柄
     */
    File getFile() throws IOException;
    /**
     * 资源内容的长度
     */
    long contentLength() throws IOException;
    /**
     * 资源最后的修改时间
     */
    long lastModified() throws IOException;
    /**
     * 根据资源的相对路径创建新资源
     */
    Resource createRelative(String var1) throws IOException;
    /**
     * 资源的文件名
     */
    String getFilename();
    /**
     * 资源的描述
     */
    String getDescription();
}

2.1.1 类结构图

Resource 体系

ServletContextResource

2.1.2 AbstractResource

public abstract class AbstractResource implements Resource {

    public AbstractResource() {
    }
    /**
     * 判断文件是否存在,若判断过程产生异常(因为会调用 SecurityManager 来判断),就关闭对应的流
     */
    public boolean exists() {
        try {
          // 基于 File 进行判断
            return getFile().exists();
        }
        catch (IOException ex) {
            // Fall back to stream existence: can we open the stream?
            // 基于 InputStream 进行判断
            try {
                InputStream is = getInputStream();
                is.close();
                return true;
            } catch (Throwable isEx) {
                return false;
            }
        }
    }
    /**
     * 直接返回 true,表示可读
     */
    public boolean isReadable() {
        return true;
    }
    /**
     * 直接返回 false,表示未被打开
     */
    public boolean isOpen() {
        return false;
    }
    /**
     * 抛出 FileNotFoundException 异常,交给子类实现
     */
    public URL getURL() throws IOException {
        throw new FileNotFoundException(getDescription() + " cannot be resolved to URL");

    }
    /**
     * 基于 getURL() 返回的 URL 构建 URI
     */
    public URI getURI() throws IOException {
        URL url = getURL();
        try {
            return ResourceUtils.toURI(url);
        } catch (URISyntaxException ex) {
            throw new NestedIOException("Invalid URI [" + url + "]", ex);
        }
    }
    /**
     * 抛出 FileNotFoundException 异常,交给子类实现
     */
    public File getFile() throws IOException {
        throw new FileNotFoundException(getDescription() + " cannot be resolved to absolute file path");
    }
    /**
     * 获取资源的长度
     *
     * 这个资源内容长度实际就是资源的字节长度,通过全部读取一遍来判断
     */
    public long contentLength() throws IOException {
        InputStream is = getInputStream();
        try {
            long size = 0;
            byte[] buf = new byte[255]; // 每次最多读取 255 字节
            int read;
            while ((read = is.read(buf)) != -1) {
                size += read;
            }
            return size;
        } finally {
            try {
                is.close();
            } catch (IOException ex) {
            }
        }
    }
    /**
     * 返回资源最后的修改时间
     */
    public long lastModified() throws IOException {
        long lastModified = getFileForLastModifiedCheck().lastModified();
        if (lastModified == 0L) {
            throw new FileNotFoundException(getDescription() +
                    " cannot be resolved in the file system for resolving its last-modified timestamp");
        }
        return lastModified;
    }
    /**
     * 抛出 FileNotFoundException 异常,交给子类实现
     */
    public Resource createRelative(String relativePath) throws IOException {
        throw new FileNotFoundException("Cannot create a relative resource for " + getDescription());
    }
    /**
     * 获取资源名称,默认返回 null ,交给子类实现
     */
    public String getFilename() {
        return null;
    }
    /**
     * 返回资源的描述
     */
    public String toString() {
        return getDescription();
    }
    public boolean equals(Object obj) {
        return obj == this || obj instanceof Resource && ((Resource)obj).getDescription().equals(this.getDescription());
    }

    public int hashCode() {
        return this.getDescription().hashCode();
    }
}

// java.io.File.java
public boolean exists() {
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkRead(path);
    }
    if (isInvalid()) {
        return false;
    }
    return ((fs.getBooleanAttributes(this) & FileSystem.BA_EXISTS) != 0);
}

FileSystemResource(文件类型资源)的定义。

public class FileSystemResource extends AbstractResource implements WritableResource {
    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);
    }
    /**
     * 返回文件路径
     */
    public final String getPath() {
        return this.path;
    }
    /**
     * 判断文件是否存在,若判断过程产生异常(因为会调用 SecurityManager 来判断),就关闭对应的流
     */
    public boolean exists() {
        return this.file.exists();
    }
    /**
     * 返回 true,表示可读
     */
    public boolean isReadable() {
        return this.file.canRead() && !this.file.isDirectory();
    }
    /**
     * 返回文件输入流
     */
    public InputStream getInputStream() throws IOException {
        return new FileInputStream(this.file);
    }
    /**
     * 返回 true,表示可写
     */
    public boolean isWritable() {
        return this.file.canWrite() && !this.file.isDirectory();
    }
    /**
     * 返回文件输出流
     */
    public OutputStream getOutputStream() throws IOException {
        return new FileOutputStream(this.file);
    }
    /**
     * 返回资源的 URL 的句柄
     */
    public URL getURL() throws IOException {
        return this.file.toURI().toURL();
    }
    /**
     * 返回资源的 URI 的句柄
     */
    public URI getURI() throws IOException {
        return this.file.toURI();
    }
    /**
     * 返回资源的 File 的句柄
     */
    public File getFile() {
        return this.file;
    }
    /**
     * 资源内容的长度
     */
    public long contentLength() throws IOException {
        return this.file.length();
    }
    /**
     * 根据资源的相对路径创建新资源
     */
    public Resource createRelative(String relativePath) {
        String pathToUse = StringUtils.applyRelativePath(this.path, relativePath);
        return new FileSystemResource(pathToUse);
    }
    /**
     * 资源的文件名
     */
    public String getFilename() {
        return this.file.getName();
    }
    /**
     * 资源的描述
     */
    public String getDescription() {
        return "file [" + this.file.getAbsolutePath() + "]";
    }

    public boolean equals(Object obj) {
        return obj == this || obj instanceof FileSystemResource && this.path.equals(((FileSystemResource)obj).path);
    }

    public int hashCode() {
        return this.path.hashCode();
    }
}

2.2 统一的资源加载(ResourceLoader)

public interface ResourceLoader {
    String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX; // CLASSPATH URL 前缀。默认为:"classpath:"
    Resource getResource(String location);
    ClassLoader getClassLoader();
}

2.2.1 类结构图

ResourceLoader 体系

2.2.2 DefaultResourceLoader

@Nullable
private ClassLoader classLoader;
public DefaultResourceLoader() { // 无参构造函数
    this.classLoader = ClassUtils.getDefaultClassLoader();
}
public DefaultResourceLoader(@Nullable ClassLoader 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());
}

getResource 方法实现

// DefaultResourceLoader.java

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

    // 1. 通过 ProtocolResolver 加载资源
    for (ProtocolResolver protocolResolver : this.protocolResolvers) {
        Resource resource = protocolResolver.resolve(location, this);
        if (resource != null) {
            return resource;
        }
    }
    // 2. 以 / 开头,返回 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());
    // 4. 根据是否为文件 URL ,是则返回 FileUrlResource 类型的资源,否则返回 UrlResource 类型的资源
    } 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) {
            // 5. 返回 ClassPathContextResource 类型的资源
            // No URL -> resolve as resource path.
            return getResourceByPath(location);
        }
    }
}

ProtocolResolver(用户自定义协议资源解决策略)

/**
 * 使用指定的 ResourceLoader ,解析指定的 location 。
 * 若成功,则返回对应的 Resource 。
 *
 * 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 指定的加载器 ResourceLoader
 * @return a corresponding {@code Resource} handle if the given location
 * matches this resolver's protocol, or {@code null} otherwise 返回为相应的 Resource
 */
@Nullable
Resource resolve(String location, ResourceLoader resourceLoader);
/**
 * ProtocolResolver 集合
 */
private final Set<ProtocolResolver> protocolResolvers = new LinkedHashSet<>(4);

public void addProtocolResolver(ProtocolResolver resolver) {
    Assert.notNull(resolver, "ProtocolResolver must not be null");
    this.protocolResolvers.add(resolver);
}

2.2.3 FileSystemResourceLoader

@Override
protected Resource getResourceByPath(String path) {
    // 截取首 /
    if (path.startsWith("/")) {
        path = path.substring(1);
    }
    // 创建 FileSystemContextResource 类型的资源
    return new FileSystemContextResource(path);
}

2.2.4 ClassRelativeResourceLoader

@Controller
public class LoginController  {
            
    @RequestMapping(value="/index.html")
    public String loginPage() throws IOException {
        
        ResourceLoader resourceLoader=new ClassRelativeResourceLoader(this.getClass());
        Resource resource=resourceLoader.getResource("test.xml");
        System.out.println(resource.getFile().getPath());
        return "index";
    }   
}

2.2.5 ResourcePatternResolver

public interface ResourcePatternResolver extends ResourceLoader {
    String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
    Resource[] getResources(String locationPattern) throws IOException;
}

2.2.6 PathMatchingResourcePatternResolver

/**
 * 内置的 ResourceLoader 资源定位器
 */
private final ResourceLoader resourceLoader;
/**
 * Ant 路径匹配器
 */
private PathMatcher pathMatcher = new AntPathMatcher();
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);
}

getResource 方法实现

@Override
public Resource getResource(String location) {
    return getResourceLoader().getResource(location);
}
public ResourceLoader getResourceLoader() {
    return this.resourceLoader;
}

getResources 方法实现

@Override
public Resource[] getResources(String locationPattern) throws IOException {
    Assert.notNull(locationPattern, "Location pattern must not be null");
    // 以 "classpath*:" 开头
    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()));
        }
    // 不以 "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:” 协议
        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)};
        }
    }
}

findAllClassPathResources 方法实现

protected Resource[] findAllClassPathResources(String location) throws IOException {
    String path = location;
    // 去除首个 /
    if (path.startsWith("/")) {
        path = path.substring(1);
    }
    // 真正执行加载所有 classpath 资源
    Set<Resource> result = doFindAllClassPathResources(path);
    if (logger.isTraceEnabled()) {
        logger.trace("Resolved classpath location [" + location + "] to resources " + result);
    }
    // 转换成 Resource 数组返回
    return result.toArray(new Resource[0]);
}

doFindAllClassPathResources 方法实现

protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {
    Set<Resource> result = new LinkedHashSet<>(16);
    ClassLoader cl = getClassLoader();
    // 1. 根据 ClassLoader 加载路径下的所有资源
    Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path));
    // 2.
    while (resourceUrls.hasMoreElements()) {
        URL url = resourceUrls.nextElement();
        // 将 URL 转换成 UrlResource
        result.add(convertClassLoaderURL(url));
    }
    // 3. 加载路径下得所有 jar 包
    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;
}

protected Resource convertClassLoaderURL(URL url) {
    return new UrlResource(url);
}

// java.lang.ClassLoader.java
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);
}

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();
        // 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);
    }
    // 转换成 Resource 数组返回
    return result.toArray(new Resource[0]);
}

determineRootDir 方法实现

protected String determineRootDir(String location) {
    // 找到冒号的后一位
    int prefixEnd = location.indexOf(':') + 1;
    // 根目录结束位置
    int rootDirEnd = location.length();
    // 在从冒号开始到最后的字符串中,循环判断是否包含通配符,如果包含,则截断最后一个由”/”分割的部分。
    // 再循环判断剩下的部分,直到剩下的路径中都不包含通配符。
    while (rootDirEnd > prefixEnd && getPathMatcher().isPattern(location.substring(prefixEnd, rootDirEnd))) {
        rootDirEnd = location.lastIndexOf('/', rootDirEnd - 2) + 1;
    }
    // 如果查找完成后,rootDirEnd = 0 了,则将之前赋值的 prefixEnd 的值赋给 rootDirEnd ,也就是冒号的后一位
    if (rootDirEnd == 0) {
        rootDirEnd = prefixEnd;
    }
    // 截取根目录
    return location.substring(0, rootDirEnd);
}

doFindPathMatchingJarResources 方法实现

doFindPathMatchingFileResources 方法实现

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;
    }
    //遍历
    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);
            }
        }
        //查看当前文件路径是否满足**/*.class格式,满足则添加
        if (getPathMatcher().match(fullPattern, currPath)) {
            result.add(content);
        }
    }
}

2.2.7 ServletContextResourcePatternResolver

protected Set<Resource> doFindPathMatchingFileResources(Resource rootDirResource, String subPattern) throws IOException {
    if (rootDirResource instanceof ServletContextResource) {
        ServletContextResource scResource = (ServletContextResource)rootDirResource;
        ServletContext sc = scResource.getServletContext();
        String fullPattern = scResource.getPath() + subPattern;
        Set<Resource> result = new LinkedHashSet(8);
        this.doRetrieveMatchingServletContextResources(sc, fullPattern, scResource.getPath(), result);
        return result;
    } else {
        return super.doFindPathMatchingFileResources(rootDirResource, subPattern);
    }
}

2.3 小结

上一篇 下一篇

猜你喜欢

热点阅读