浅聊Spring的资源管理 (Resource)

Spring 为什么引入资源管理?

Java 中有各种各样的资源,资源的位置包括本地文件系统、网络、类路径等,资源的形式可以包括文件、二进制流、字节流等,针对不同的资源又有不同的加载形式。本地文件系统中的文件在 Java 中使用 File 表示,使用 FileInputStream 读取。网络上的资源使用 URL 表示,使用 URLConnection 获取 InputStream 进行读取。而类路径下的资源使用 ClassLoader 进行读取。为了使用统一的方式访问资源,Spring 将资源抽象为 Resource,将资源的加载抽象为 ResourceLoader。Spring 配置文件的读取以及扫描包中的 bean 都会通过 Resource 访问资源。

资源抽象 Resource

Resource 是 Spring 对资源抽象的一个接口,具体的资源可以有不同的实现类。Resource 相关方法如下:

public interface Resource extends InputStreamSource {
    // 资源是否以物理的形式真实存在
    boolean exists();
    // 资源是否可以通过 #getInputStream() 方法进行读取
    default boolean isReadable() {
    	return exists();
    }
    // 资源是否已经被打开
    default boolean isOpen() {
    	return false;
    }
    // 资源是否为文件系统中的资源
    default boolean isFile() {
    	return false;
    }
    // 获取资源 URL 的表示形式
    URL getURL() throws IOException;
    // 获取资源 URI 的表示形式
    URI getURI() throws IOException;
    // 获取资源文件的表示形式
    File getFile() throws IOException;
    // 获取资源 Channel 的表示形式
    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 接口继承了接口 InputStreamSource ,InputStreamSource 源码如下:

public interface InputStreamSource {
    // 获取输入流
    InputStream getInputStream() throws IOException;
}

因此,每个 Resource 都可以获取到 InputStream。常见的 Resource 如下面的类图所示:

Spring 资源类图

每个 Resource 的实现都封装了具体的资源。Resource 由 AbstractResource 进行主要的抽象实现,其子类可以根据封装的资源进行重写,由于源码比较简单,这里不再进行分析,感兴趣的朋友可以自行查看相关源码。 主要的 Resource 包括如下:

  • FileSystemResource:对文件系统中 File 及 Path 的封装,除了可以读取资源,还可以对资源进行写操作。
  • ClassPathResource:类路径下资源的封装。
  • UrlResource:URL 资源的封装。
  • InputStreamResource:输入流资源的封装。
  • ByteArrayResource:字节数组的封装。
  • ServletContextResource:对 Servlet 上下文的封装。

资源加载抽象 ResourceLoader

与 Java 中的类加载相似,Java 使用 ClassLoader 加载类,而 Spring 抽象出 ResourceLoader 加载 Resource。ResourceLoader 也是一个接口,根据不同的资源可以有不同的实现。ResourceLoader 源码如下:

public interface ResourceLoader {
    //类资源位置的前缀 classpath:
    String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX;
    // 根据指定的资源位置获取资源
    Resource getResource(String location);
    // 获取当前类加载器中使用的 ClassLoader
    @Nullable
    ClassLoader getClassLoader();
}

ResourceLoader 中定义了根据资源位置获取资源的方法,相关类图见下图:

Spring ResourceLoader 类图

DefaultResourceLoader 是 ResourceLoader 的默认实现,其根据资源路径的协议进行解析为不同的 Resource 实现,但是它只能够根据资源路径获取一个 Resource。其获取资源的方法源码如下:

@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;
    	}
    }
    // 使用 Class 或 ClassLoader 获取资源
    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 {
    		// 尝试获取 URL 资源
    		// 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 先根据协议解析器获取资源,因此我们可以定义自己的协议解析器解析自定义的协议的资源。如果路径以 / 开头,它会获取到一个 ClassPathContextResource 资源,否则如果以资源位置以 classpath: 开头,会获取到一个 ClassPathResource 资源,最后会尝试获取 UrlResource 资源。

如果想要根据资源路径的模式字符串获取多个 Resource ,则只能通过 ResourcePatternResolver,ResourcePatternResolver 源码如下:

public interface ResourcePatternResolver extends ResourceLoader {
    // 类路径下资源文件的前缀
    String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
    // 根据资源路径模式字符串获取资源
    Resource[] getResources(String locationPattern) throws IOException;
}

ResourcePatternResolver 只有一个实现 PathMatchingResourcePatternResolver,它会根据 ant 风格的路径去查找资源。实现源码如下:

// ant 风格的路径匹配
private PathMatcher pathMatcher = new AntPathMatcher();

@Override
public Resource[] getResources(String locationPattern) throws IOException {
    Assert.notNull(locationPattern, "Location pattern must not be null");
    if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
    	// 处理 classpath*: 开头类路径下的资源
    	// 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)};
    	}
    }
}

获取资源时会先判断资源路径是否为类路径,然后再判断路径是否为支持的模式,默认支持 ant 风格的路径匹配,对类路径下的资源和非类路径下的资源具有不同的处理。

如何在 Spring 中获取 Resource 和 ResourceLoader

Spring 的内部有关资源的加载大量使用了 Resource 和 ResourceLoader,自然我们也同样可以使用 Resource 获取资源。
由于 Resource 与具体的资源进行绑定,Spring 并未把它作为 bean 注入到容器中,为了获取 Resource ,我们可以通过在 bean 的 成员变量中通过 @Value 注入 Resource 及其数组对象。示例如下:

// 类路径下创建文件 META-INF/dev.properties 内容为 profile=dev
// 类路径下创建文件 META-INF/prod.properties 内容为 profile=prod
public class Main {

    @Value("classpath:/META-INF/prod.properties")
    private Resource resource;

    @Value("classpath*:/META-INF/*.properties")
    private Resource[] resources;

    public static void main(String[] args) throws IOException {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Main.class);
        Main bean = context.getBean(Main.class);
        String content = FileReader.create(bean.resource.getFile()).readString();
        System.out.println(content);
        System.out.println("=========");
        for (Resource resource : bean.resources) {
            System.out.println(FileReader.create(resource.getFile()).readString());
            System.out.println("=========");
        }

    }
}

执行结果如下:

profile=prod
=========
profile=dev
=========
profile=prod
=========

通过 @Value 注入 Resource ,成功读取到了类路径下的资源文件。

ResourceLoader 作为可能会被经常使用的组件,Spring 已经将其注册为 bean,因此可以直接通过 @Autowire 注入,另外由于 ApplicationContext 继承了 ResourceLoader 接口,因此也可以直接通过 @Autowire 注入 ApplicationContext 来使用 ResourceLoader,此外 Spring 还提供了 ResourceLoaderAware 接口,在 bean 的生命周期中,如果 bean 实现了接口 ResourceLoaderAware ,则 Spring 会调用 setResourceLoader 方法,这样就拿到了 ResourceLoader,拿到后我们就可以直接用来加载资源。示例代码如下:

public class Main implements ResourceLoaderAware {

    @Autowired
    private ResourceLoader resourceLoader;

    @Autowired
    private ApplicationContext applicationContext;

    private ResourceLoader awareResourceLoader;

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.awareResourceLoader = resourceLoader;
    }

    public static void main(String[] args) throws IOException {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Main.class);
        Main bean = context.getBean(Main.class);
        System.out.println(bean.resourceLoader);
        System.out.println(bean.applicationContext);
        System.out.println(bean.awareResourceLoader);
    }

}

执行结果如下:

org.springframework.context.annotation.AnnotationConfigApplicationContext@6aaa5eb0, started on Wed Sep 02 22:36:40 CST 2020
org.springframework.context.annotation.AnnotationConfigApplicationContext@6aaa5eb0, started on Wed Sep 02 22:36:40 CST 2020
org.springframework.context.annotation.AnnotationConfigApplicationContext@6aaa5eb0, started on Wed Sep 02 22:36:40 CST 2020

三种方式都打印出来了结果,说明这三种方式都可以正常获取 ResourceLoader,并且这三种方式获取到的对象为同一个。

总结

Resource 和 ResourceLoader 作为 Spring 中资源和加载资源的抽象,在底层加载资源的地方都会被用到,通过对这两者的熟悉,在阅读 Spring 源码时,可以把精力放在其他地方,并且我们也可以使用 Resource 获取我们自己的资源。

参考:

 

posted @ 2022-01-10 23:12  残城碎梦  阅读(486)  评论(0编辑  收藏  举报