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 如下面的类图所示。
每个 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 中定义了根据资源位置获取资源的方法,相关类图见下图。
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 获取我们自己的资源。