图源:简书 (jianshu.com)
Resource
接口Spring中的资源被抽象为一个Resource接口:
public interface Resource extends InputStreamSource {
boolean exists();
boolean isReadable();
boolean isOpen();
boolean isFile();
URL getURL() throws IOException;
URI getURI() throws IOException;
File getFile() throws IOException;
ReadableByteChannel readableChannel() throws IOException;
long contentLength() throws IOException;
long lastModified() throws IOException;
Resource createRelative(String relativePath) throws IOException;
String getFilename();
String getDescription();
}
Resource
接口中最重要的一些方法是。
getInputStream()
: 定位并打开资源,返回一个用于读取资源的 InputStream
。我们期望每次调用都能返回一个新的 InputStream
。关闭该流是调用者的责任。exists()
: 返回一个 boolean
值,表示该资源是否以物理形式实际存在。isOpen()
: 返回一个 boolean
,表示该资源是否代表一个具有开放流的句柄。如果为 true
,InputStream
不能被多次读取,必须只读一次,然后关闭以避免资源泄漏。对于所有通常的资源实现,除了 InputStreamResource
之外,返回 false
。getDescription()
: 返回该资源的描述,用于处理该资源时的错误输出。这通常是全路径的文件名或资源的实际URL。Resource
实现Spring内置了一些Resource
接口的实现类:
UrlResource
ClassPathResource
FileSystemResource
PathResource
ServletContextResource
InputStreamResource
ByteArrayResource
这里介绍几个常见的Resource
实现类。
ClassPathResource
是最常见的,通过它我们可以访问ClassPath中的文件。
举例说明,假如在Spring的静态资源目录resources
下有一个文件override.properties
:
dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql:mydb
可以通过下面的示例代码将其内容打印到控制台:
package com.example.resource.controller;
// ...
@RestController
@RequestMapping("/hello")
public class HelloController {
@GetMapping("")
public String hello() throws IOException {
Resource resource = new ClassPathResource("override.properties");
printContent(resource);
return Result.success().toString();
}
private void printContent(Resource resource) throws IOException {
File file;
try {
file = resource.getFile();
} catch (FileNotFoundException e) {
String content = resource.getContentAsString(StandardCharsets.UTF_8);
System.out.println(content);
return;
}
printContent(file);
}
private void printContent(File file) throws IOException {
FileReader fr;
fr = new FileReader(file);
BufferedReader br = new BufferedReader(fr);
String line;
do {
line = br.readLine();
if (line == null) {
break;
}
System.out.println(line);
}
while (true);
br.close();
}
}
这其中下面这行代码,明确创建了一个到resources/override.properties
文件的资源:
Resource resource = new ClassPathResource("override.properties");
如果Spring项目是通过IDE运行的,那么resources
这个静态资源目录会被加入ClassPath,因此自然可以通过ClassPathResource
正确访问到,如果项目是打包成Jar包运行,该目录同样会被打包的Jar包中的/BOOT-INF/classes
目录:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CuYSJIK3-1684032458114)(D:\image\typora\image-20230513172429162.png)]
该目录同样会被加入ClassPath中,所以同样可以获取到正确的文件。
正是因为这样的特性,所以在开发中通常都会用ClassPath的方式引用静态资源,而非文件路径。因为后者可能导致部署的目标服务器上缺少相应的资源而出错。
FileSystemResource
是通过文件系统来访问资源,具体来说就是文件的相对路径和绝对路径。
同样是上面的示例,只需要稍微修改:
// ...
@GetMapping("")
public String hello() throws IOException {
Resource resource = new FileSystemResource("src/main/resources/override.properties");
printContent(resource);
return Result.success().toString();
}
// ...
当然也可以使用绝对路径:
Resource resource = new FileSystemResource("D:/workspace/learn_spring_boot/ch28/resource/src/main/resources/override.properties");
通过UrlResource
可以访问用URL定义的资源,当然最常见的是网络资源:
Resource resource = new UrlResource("https://blog.icexmoon.cn/");
要说明的是,通过UrlResource
创建的Resource
,是无法通过调用Resource.getFile()
方法获取文件的,会产生FileNotFoundException
异常。因此只能是以Resource.getInputStream()
方法获取输入流,然后再打印内容。不过Resource
接口其实已经提供了一个getContentAsString()
方法:
public interface Resource extends InputStreamSource {
// ...
default String getContentAsString(Charset charset) throws IOException {
return FileCopyUtils.copyToString(new InputStreamReader(this.getInputStream(), charset));
}
// ...
}
URL实际上也可以指定本地文件系统:
Resource resource = new UrlResource("file://D:/workspace/learn_spring_boot/ch28/resource/src/main/resources/override.properties");
其它的Resource
实现可以阅读核心技术 (springdoc.cn)。
ResourceLoader
接口可以通过接口ResourceLoader
来获取Resource
:
public interface ResourceLoader {
Resource getResource(String location);
ClassLoader getClassLoader();
}
所有的application context都实现了这个接口,可以当做ResourceLoader
来使用:
@RestController
@RequestMapping("/hello")
public class HelloController {
@Autowired
private ApplicationContext ctx;
@GetMapping("")
public String hello() throws IOException {
Resource resource = ctx.getResource("classpath:override.properties");
printContent(resource);
return Result.success().toString();
}
}
实际上在这个示例中,直接注入ResourceLoader
更为合适:
@RestController
@RequestMapping("/hello")
public class HelloController {
@Autowired
private ResourceLoader resourceLoader;
@GetMapping("")
public String hello() throws IOException {
Resource resource = resourceLoader.getResource("classpath:override.properties");
printContent(resource);
return Result.success().toString();
}
// ...
}
作为参数传入getResource
方法的classpath:override.properties
这样的被称作资源字符串:
前缀 | 示例 | 说明 |
---|---|---|
classpath: | classpath:com/myapp/config.xml |
从classpath加载。 |
file: | file:///data/config.xml |
作为 URL 从文件系统加载。另请参见FileSystemResource 注意事项. |
https: | https://myserver/logo.png |
以 URL 形式加载。 |
(none) | /data/config.xml |
取决于底层的 `ApplicationContext’。 |
如果资源字符串不带前缀(比如classpath:
),ResourceLoader
获取资源的行为根据ApplicationContext
的类型的不同而不同,比如ClassPathXmlApplicationContext
默认会以ClassPathResource
的方式获取资源,FileSystemXmlApplicationContext
默认会以FileSystemResource
的方式获取资源。
ResourcePatternResolver
接口ResourcePatternResolver
接口是ResourceLoader
的扩展:
public interface ResourcePatternResolver extends ResourceLoader {
String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
Resource[] getResources(String locationPattern) throws IOException;
}
在ResourceLoader
的基础上,getResources()
方法增加了对通配符的支持:
@RestController
@RequestMapping("/hello")
public class HelloController {
@Autowired
private ResourcePatternResolver resourcePatternResolver;
@GetMapping("")
public String hello() throws IOException {
Resource[] resources = resourcePatternResolver.getResources("classpath:*.properties");
for(Resource r: resources){
System.out.println(r.getFilename());
}
if (resources == null || resources.length == 0){
return Result.fail("没有获取到文件").toString();
}
Resource resource = resources[0];
printContent(resource);
return Result.success().toString();
}
// ...
}
这个示例中,resourcePatternResolver.getResources("classpath:*.properties")
可以匹配到resource
目录下所有以.properties
为后缀的文件作为资源对象返回。
如果需要从多个jar中检索同样的包名下的资源,可以使用
classpath*:
这样的前缀配合通配符检索。
ResourceLoaderAware
接口可以让bean通过实现ResourceLoaderAware
接口的方式获取ResourceLoader
:
@RestController
@RequestMapping("/hello")
public class HelloController implements ResourceLoaderAware {
// ...
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
Resource resource = resourceLoader.getResource("classpath:override.properties");
try {
printContent(resource);
} catch (IOException e) {
e.printStackTrace();
}
}
}
当然,相比直接注入ResourceLoader
,这样做并没有什么优势。
Resource
可以借助@Value
注解直接注入Resource
:
@RestController
@RequestMapping("/hello")
public class HelloController implements ResourceLoaderAware {
// ...
@Value("${my.properties}")
@Autowired
private Resource resource;
@GetMapping("")
public String hello() throws IOException {
printContent(resource);
return Result.success().toString();
}
// ...
}
Spring Boot默认的配置文件application.properties
:
my.properties=classpath:override.properties
当然,通过setter或构造器注入也是可以的,这里不再演示。
资源字符串中使用了通配符,可以注入所有匹配的资源:
my.all.properties=classpath:*.properties
@RestController
@RequestMapping("/hello")
public class HelloController implements ResourceLoaderAware {
// ...
@Value("${my.all.properties}")
@Autowired
private Resource[] resources;
@GetMapping("")
public String hello() throws IOException {
for (Resource r : resources) {
System.out.println(r.getFilename());
}
if (resources == null || resources.length == 0) {
return Result.fail("没有获取到文件").toString();
}
Resource resource = resources[0];
printContent(resource);
return Result.success().toString();
}
// ...
}
本文所有的示例代码可以通过learn_spring_boot/ch28/resource获取。
谢谢阅读。