您现在的位置是:首页 >技术交流 >从零开始 Spring Boot 28:资源网站首页技术交流

从零开始 Spring Boot 28:资源

魔芋红茶 2024-06-17 10:19:28
简介从零开始 Spring Boot 28:资源

从零开始 Spring Boot 28:资源

spring boot

图源:简书 (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,表示该资源是否代表一个具有开放流的句柄。如果为 trueInputStream 不能被多次读取,必须只读一次,然后关闭以避免资源泄漏。对于所有通常的资源实现,除了 InputStreamResource 之外,返回 false
  • getDescription(): 返回该资源的描述,用于处理该资源时的错误输出。这通常是全路径的文件名或资源的实际URL。

内置的Resource实现

Spring内置了一些Resource接口的实现类:

这里介绍几个常见的Resource实现类。

ClassPathResource

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 yporaimage-20230513172429162.png)]

该目录同样会被加入ClassPath中,所以同样可以获取到正确的文件。

正是因为这样的特性,所以在开发中通常都会用ClassPath的方式引用静态资源,而非文件路径。因为后者可能导致部署的目标服务器上缺少相应的资源而出错。

FileSystemResource

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

通过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.pngURL 形式加载。
(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获取。

谢谢阅读。

参考资料

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。