美文网首页
Spring IoC资源管理之ResourceLoader

Spring IoC资源管理之ResourceLoader

作者: 虾米咬小米 | 来源:发表于2020-07-29 16:50 被阅读0次

    概述

    在上一章节Spring IoC资源管理之Resource提到 Spring 将资源的定义和资源的加载区分开了,Resource 定义了统一的资源,那资源的加载则由 ResourceLoader 来统一定义。

    ResourceLoader

    org.springframework.core.io.ResourceLoader 为 Spring 资源加载的统一抽象,具体的资源加载则由相应的实现类来完成,所以我们可以将 ResourceLoader 称作为统一资源定位器。其定义如下:

    public interface ResourceLoader {
    String CLASSPATH_URL_PREFIX = "classpath:";   
    Resource getResource(String var1);    
    @Nullable   
    ClassLoader getClassLoader();
    }
    
    

    作为 Spring 统一的资源加载器,它提供了统一的抽象,具体的实现则由相应的子类来负责实现,其类的类结构图如下:


    image.png

    ResourceLoader 接口提供两个方法:getResource()、getClassLoader()。

    getResource() 根据所提供的路径 location 返回 Resource 实例,但是它不确保该 Resource 一定存在,需要调用 Resource.exist() 方法判断。该方法支持以下模式的资源加载:

    • URL位置资源,如“file:D:/conf.xml”
    • ClassPath位置资源,如“classpath:conf.xml”
    • 相对路径资源,如"conf/conf.xml",此时返回的 Resource 实例根据实现不同而不同

    该方法的主要实现是在其子类 DefaultResourceLoader 中实现,具体过程我们在分析 DefaultResourceLoader 时做详细说明。

    getClassLoader() 返回 ClassLoader 实例,对于想要获取 ResourceLoader 使用的 ClassLoader 用户来说,可以直接调用该方法来获取。

    DefaultResourceLoader

    DefaultResourceLoader 是 ResourceLoader 的默认实现,它接收 ClassLoader 作为构造函数的参数或者使用不带参数的构造函数,在使用不带参数的构造函数时,使用的 ClassLoader 为默认的 ClassLoader(一般为Thread.currentThread().getContextClassLoader()),可以 通过 ClassUtils.getDefaultClassLoader()获取。当然也可以调用 setClassLoader()方法进行后续设置。如下:

    public DefaultResourceLoader() {
            this.classLoader = ClassUtils.getDefaultClassLoader();
        }
    
        /**
         * Create a new DefaultResourceLoader.
         * @param classLoader the ClassLoader to load class path resources with, or {@code null}
         * for using the thread context class loader at the time of actual resource access
         */
        public DefaultResourceLoader(@Nullable ClassLoader classLoader) {
            this.classLoader = classLoader;
        }
    
    
        /**
         * Specify the ClassLoader to load class path resources with, or {@code null}
         * for using the thread context class loader at the time of actual resource access.
         * <p>The default is that ClassLoader access will happen using the thread context
         * class loader at the time of this ResourceLoader's initialization.
         */
        public void setClassLoader(@Nullable ClassLoader classLoader) {
            this.classLoader = classLoader;
        }
    
        /**
         * Return the ClassLoader to load class path resources with.
         * <p>Will get passed to ClassPathResource's constructor for all
         * ClassPathResource objects created by this resource loader.
         * @see ClassPathResource
         */
        @Override
        @Nullable
        public ClassLoader getClassLoader() {
            return (this.classLoader != null ? this.classLoader : ClassUtils.getDefaultClassLoader());
        }
    

    ResourceLoader 中最核心的方法为 getResource(),它根据提供的 location 返回相应的 Resource,而 DefaultResourceLoader 对该方法提供了核心实现(它的两个子类都没有提供覆盖该方法,所以可以断定ResourceLoader 的资源加载策略就封装 DefaultResourceLoader中),如下:

        @Override
        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);
                }
            }
    
    
    
    

    在分析该方法,我们需要认识一下 ProtocolResolver,该类与 DefaultResourceLoader 密不可分。ProtocolResolver 翻译过来叫做"协议解析器", 允许用户自定义协议资源解决策略,作为 DefaultResourceLoader 的 SPI,它允许用户自定义资源加载协议,而不需要继承 ResourceLoader 的子类。 在这个接口类中只有一个方法:

    
    @FunctionalInterface
    public interface ProtocolResolver {
    
        /**
         * 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);
    
    }
    

    这个方法就是根据传入的 location 字符串,解析出对应的 Resource 资源。
    在介绍 Resource 时,提到如果要实现自定义 Resource,我们只需要继承 AbstractResource 即可,但是有了 ProtocolResolver 后,我们不需要直接继承 DefaultResourceLoader,改为实现 ProtocolResolver 接口也可以实现自定义的 ResourceLoader。

    然后我们再来查看 getResource() 方法,首先通过 getProtocolResolvers()获取 ProtocolResolver 的集合,如果存在 ProtocolResolver,则直接用它来加载资源,并返回 Resource,否则调用如下逻辑:

    • 若 location 以/开头,则调用 getResourceByPath()构造 ClassPathContextResource 类型资源并返回;
    • 若 location 以 classpath:开头, 则构造 ClassPathResource 类型资源并返回,在构造该资源时,通过 getClassLoader()获取当前的 ClassLoader;
    • 构造 URL,尝试通过它进行资源定位,若没有抛出 MalformedURLException 异常,然后判断是否为 FileURL,如果是则构造 FileUrlResource 类型资源,否则构造 UrlResource。若在加载过程中抛出 MalformedURLException 异常,则 委派 getResourceByPath() 实现资源定位加载。
      接下来看一个测试案例,代码如下:
    
        @Test
        public void useResourceLoader() {
            ResourceLoader resourceLoader = new DefaultResourceLoader();
            Resource resource = null;
            String location = "";
    
            //location以/开头
            location = "/applicationContext.xml";
            resource = resourceLoader.getResource(location);
            System.out.println(resource.getClass());
    
            //location以classpath开头
            location = "classpath:applicationContext.xml";
            resource = resourceLoader.getResource(location);
            System.out.println(resource.getClass());
    
            //抛出MalformedURLException异常,进而执行getResourceByPath方法
            location = "target/classes/applicationContext.xml";
            resource = resourceLoader.getResource(location);
            System.out.println(resource.getClass());
    
            //同上
            location = "F:/workspace/Spmvc_Learn/spring_study/spring-chap1/target/classes/applicationContext.xml";
            resource = resourceLoader.getResource(location);
            System.out.println(resource.getClass());
    
            //ResourceUtils.isFileURL(url)为true,返回FileUrlResource
            location = "file:/target/classes/applicationContext.xml";
            resource = resourceLoader.getResource(location);
            System.out.println(resource.getClass());
    
            //ResourceUtils.isFileURL(url)为false,返回UrlResource
            location = "https://cn.bing.com/";
            resource = resourceLoader.getResource(location);
            System.out.println(resource.getClass());
        }
    

    执行结果为:

    class org.springframework.core.io.DefaultResourceLoader$ClassPathContextResource
    class org.springframework.core.io.ClassPathResource
    class org.springframework.core.io.DefaultResourceLoader$ClassPathContextResource
    class org.springframework.core.io.DefaultResourceLoader$ClassPathContextResource
    class org.springframework.core.io.FileUrlResource
    class org.springframework.core.io.UrlResource
    

    FileSystemResourceLoader

    在上一章节中,我们还提到了 FileSystemResource 类型,但是 DefaultResourceLoader 中并没有对此类型的处理,包括 getResourceByPath(String) 方法只是构建 ClassPathResource 类型。所以我们可以使用 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。

        private static class FileSystemContextResource extends FileSystemResource implements ContextResource {
    
            public FileSystemContextResource(String path) {
                super(path);
            }
    
            @Override
            public String getPathWithinContext() {
                return getPath();
            }
        }
    

    在构造器中也是调用 FileSystemResource 的构造方法来构造 FileSystemContextResource 的。
    除了对getResourceByPath(String)方法的重写之外,其他方法都是复用 DefaultResourceLoader 中的方法。此时将上面的示例稍作修改:

    
        @Test
        public void useResourceLoaders() {
            ResourceLoader resourceLoader = new DefaultResourceLoader();
            Resource resource = null;
            String location = "";
            resourceLoader = new FileSystemResourceLoader();
            //抛出MalformedURLException异常,进而执行getResourceByPath方法
            location = "target/classes/applicationContext.xml";
            resource = resourceLoader.getResource(location);
            System.out.println(resource.getClass());
            //同上
            location = "C:\\project\\github\\spring-framework\\spring-demo\\build\\resources\\main\\applicationContext.xml";
            resource = resourceLoader.getResource(location);
            System.out.println(resource.getClass());
        }
    

    执行结果为:

    class org.springframework.core.io.FileSystemResourceLoader$FileSystemContextResourceclass org.springframework.core.io.FileSystemResourceLoader$FileSystemContextResource
    
    

    ResourcePatternResolver

    ResourceLoader 的 Resource getResource(String location) 每次根据 location 返回一个 Resource,当需要加载多个资源时,必须要多次调用 getResource()方法,这样很不合理,所以引申而来了 ResourcePatternResolver。该类是 ResourceLoader 的扩展,它支持根据指定的资源路径匹配模式每次返回多个 Resource 实例,其定义如下:

    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 在 ResourceLoader 的基础上增加了 getResources(String locationPattern),以支持根据路径匹配模式返回多个 Resource 实例,同时也新增了一种新的协议前缀 classpath:,该协议前缀由其子类负责实现。
    PathMatchingResourcePatternResolver 为 ResourcePatternResolver 最常用的子类(在后续构建应用上下文调试的过程中,会发现该类的使用),它除了支持 ResourceLoader 和 ResourcePatternResolver 新增的 classpath
    : 前缀外,还支持 Ant 风格的路径匹配模式(类似于 */.xml)。
    PathMatchingResourcePatternResolver 提供了三个构造方法,如下:

    
        /**
         * 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;
        }
    
        /**
         * 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);
        }
    
    

    PathMatchingResourcePatternResolver 在实例化的时候,可以指定一个 ResourceLoader,如果不指定的话,它会在内部构造一个 DefaultResourceLoader。
    getResource()
    该方法在当前类中的定义如下:

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

    getResource() 方法直接委托给相应的 ResourceLoader 来实现,所以如果我们在实例化 PathMatchingResourcePatternResolver 的时候,如果不知道 ResourceLoader ,那么在加载资源时,其实就是 DefaultResourceLoader 的过程。
    getResources()
    查看该方法的定义:

        @Override
        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)};
                }
            }
        }
    
    

    处理逻辑作图如下:


    image.png

    下面就 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.isTraceEnabled()) {
                logger.trace("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)) {
                // 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;
        }
    

    doFindAllClassPathResources() 根据 ClassLoader 加载路径下的所有资源。 如果在构造 PathMatchingResourcePatternResolver 实例时未传入 resourceLoader 或 classLoader 参数,则默认调用 DefaultResourceLoader 的 classLoader(默认为Thread.currentThread().getContextClassLoader())。然后调用 classLoader 的 getResources()方法,只有当 getClassLoader()返回为 null 时才会 调用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);
        }
    

    若 path 为 空(“”)时,则调用 addAllClassLoaderJarRoots()方法。该方法主要是加载路径下得所有 jar 包,方法较长也没有什么实际意义就不贴出来了。
    通过上面的分析,我们知道 findAllClassPathResources() 其实就是利用 ClassLoader 来加载指定路径下的资源,不管它是在 class 路径下还是在 jar 包中。如果我们传入的路径为空或者 /,则会调用 addAllClassLoaderJarRoots() 方法加载所有的 jar 包。

    findPathMatchingResources

    当 locationPattern 以 classpath*: 开头且当中包含了通配符,则调用该方法进行资源加载。如下:

        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]);
        }
    

    方法有点儿长,但是思路还是很清晰的,主要分两步:

    确定目录,获取该目录下得所有资源
    在所获得的所有资源中进行迭代匹配获取我们想要的资源。

    在这个方法里面我们要关注两个方法,一个是 determineRootDir(),一个是 doFindPathMatchingFileResources()。
    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;
            }
            if (rootDirEnd == 0) {
                rootDirEnd = prefixEnd;
            }
            return location.substring(0, rootDirEnd);
        }
    
    

    该方法一定要给出一个确定的根目录。该根目录用于确定文件的匹配的起始点,将根目录位置的资源解析为 java.io.File 并将其传递到 retrieveMatchingFiles(),其余为知用于模式匹配,找出我们所需要的资源。

    确定根路径如下:

    原路径 根路径
    classpath:test/cc/spring-*.xml classpath*:test/
    classpath:test/aa/spring-.xml classpath*:test/aa/
    file:F:/workspace/test/target/classes/*/beans.xml file:F:/workspace/test/target/classes/

    确定根路径后,则调用 getResources() 方法获取该路径下得所有资源,然后迭代资源获取符合条件的资源。

    下面给出一个案例,代码如下:

        @Test
        public void usePatternResolver() throws IOException {
            ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
            Resource[] resources = null;
            String location = "";
            //使用DefaultResourceLoader的情况下,改变location的内容
            location = "applicationContext.xml";
            resources = resolver.getResources(location);//调用DefaultResourceLoader.getResource()方法
            sout(resources);
            location = "classpath*:config/beans.xml";
            resources = resolver.getResources(location);//findAllClassPathResources
            sout(resources);
            location = "classpath*:*/beans.xml";
            resources = resolver.getResources(location);//findPathMatchingResources
            sout(resources);
            location = "file:F:/workspace/Spmvc_Learn/spring_study/spring-chap1/target/classes/*/beans.xml";
            resources = resolver.getResources(location);//findPathMatchingResources
            sout(resources);
            location = "F:/workspace/Spmvc_Learn/spring_study/spring-chap1/target/classes/applicationContext.xml";
            resources = resolver.getResources(location);//调用resourceLoader.getResource()方法
            sout(resources);  //更新ClassLoader
            location = "F:/workspace/Spmvc_Learn/spring_study/spring-chap1/target/classes/applicationContext.xml";
            resolver = new PathMatchingResourcePatternResolver(new FileSystemResourceLoader());
            resources = resolver.getResources(location);//调用FileSystemResourceLoader.getResource()方法
            sout(resources);
        }
    
        public void sout(Resource[] resources) {
            for (Resource resource : resources) {
                System.out.println(resource);
            }
        }
    

    执行结果为:

    class path resource [applicationContext.xml]
    14:51:47.412 [Test worker] DEBUG org.springframework.core.io.support.PathMatchingResourcePatternResolver - Skipping [F:\workspace\Spmvc_Learn\spring_study\spring-chap1\target\classes] because it does not exist
    class path resource [F:/workspace/Spmvc_Learn/spring_study/spring-chap1/target/classes/applicationContext.xml]
    file [F:\workspace\Spmvc_Learn\spring_study\spring-chap1\target\classes\applicationContext.xml]
    

    总结
    经过两章节的讲述,关于 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)。

    参考;
    Spring IoC资源管理之ResourceLoader

    相关文章

      网友评论

          本文标题:Spring IoC资源管理之ResourceLoader

          本文链接:https://www.haomeiwen.com/subject/ecfqrktx.html