场景

在项目中一般来说都需要进行文件的上传和下载,文件存储分为两种情况,一种为直接存储在服务器后端中,另一种则是可以存储在其他的文件服务中,如Minio,下篇博客将会介绍如何使用。

文件上传

文件上传可以通过File类以及各种文件流和缓冲流进行读取和写入,文件的存储路径,可以使用一个Properties类读取配置文件。

    @RequestMapping(value = "/upload", method = RequestMethod.POST)
    // 接口返回类型随意,接口参数固定为MultipartFile类型,注解的参数一般为file,因为前端的input文件标签会以file为名,如要修改需和前端同步
    public Result<Map> fileUpload(@RequestParam("file") MultipartFile file) throws IOException {
        String dirPath = sysCommonProperties.getPath();
        File dirFile = new File(dirPath);
        String fileName = null;
        if (!dirFile.exists()) {
            dirFile.mkdirs();
        }
        else {
            // MultipartFile可以直接获取文件的Stream流,放入缓冲流中快速读取
            BufferedInputStream bis = new BufferedInputStream(file.getInputStream());
            // 通过UUID为文件重命名,以防同名文件覆盖,但是不能改变原文件后缀名,通过getOriginalFilename方法获得原始名称
            fileName = UUID.randomUUID() + Objects.requireNonNull(file.getOriginalFilename()).substring(file.getOriginalFilename().indexOf("."));
            // 创建新文件的out流
            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(dirPath + "/" + fileName));
            // 以byte[]方式读取文件
            byte[] buffer = new byte[1024];
            int readCount = 0;
            while((readCount=bis.read(buffer))!=-1)
                bos.write(buffer, 0, readCount);
            // 写完后记得flush,不然会导致最后一次数据丢失
            bos.flush();
            bos.close();
            bis.close();
        }
        Map<String, String> r = new HashMap<String, String>();
        r.put("fileName", fileName);
        return Result.ok(r);
    }

文件下载

    // 请求方式为GET
    @RequestMapping(value = "/download", method = RequestMethod.GET)
    // 接口参数根据需求自定义,接口返回类型一般使用ResponseEntity<byte[]>,响应体数据类型是byte[]
    public ResponseEntity<byte[]> downloadFile(@RequestParam(value = "name") String name) throws IOException {
        String filePath = sysCommonProperties.getPath() + "/" + name;
        // 使用文件系统类FileSystemResource
        Resource resource = new FileSystemResource(filePath);
        if (name == null || name.isEmpty()) {
            throw new CustomException(ResultCodeEnum.DATA_ERROR);
        }
        if (resource.exists()) {
            File file = resource.getFile();
            // 设置响应头内容,ContentType和attachment是必填项,attachment为文件名字
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.MULTIPART_FORM_DATA);
            headers.setContentDispositionFormData("attachment", file.getName());
            // 通过Files工具类获得文件的所有byte
            return new ResponseEntity<>(Files.readAllBytes(file.toPath()), headers, HttpStatus.OK);
        }
        return new ResponseEntity<>(HttpStatus.NOT_FOUND);
    }

Resource类和ResourceLoader类复习

Spring 定义了一个 org.springframework.core.io.Resource 接口,Resource 接口是为了统一各种类型不同的资源而定义的,Spring 提供了若干 Resource 接口的实现类,这些实现类可以轻松地加载不同类型的底层资源,并提供了获取文件名、URL 地址以及资源内容的操作方法。

假设有一个文件地位于 Web 应用的类路径下,您可以通过以下方式对这个文件资源进行访问: 通过 FileSystemResource 以文件系统绝对路径的方式进行访问; 通过 ClassPathResource 以类路径的方式进行访问; 通过 ServletContextResource 以相对于Web应用根目录的方式进行访问。 相比于通过 JDK 的 File 类访问文件资源的方式,Spring 的 Resource 实现类无疑提供了更加灵活的操作方式,您可以根据情况选择适合的 Resource 实现类访问资源。下面,我们分别通过 FileSystemResource 和 ClassPathResource 访问同一个文件资源:

Resource

    String filePath = "D:/masterSpring/chapter23/webapp/WEB-INF/classes/conf/file1.txt";
    // 使用系统文件路径方式加载文件
    Resource res1 = new FileSystemResource(filePath); 
    // 使用类路径方式加载文件
    Resource res2 = new ClassPathResource("conf/file1.txt");
    InputStream ins1 = res1.getInputStream();
    InputStream ins2 = res2.getInputStream();

访问类路径下的资源还可以使用类加载器方式

Thread.currentThread().getContextClassLoader().getResourceAsStream("classpath:/***.**");

ResourceLoader

而Spring框架为了更方便的获取资源,尽量弱化程序员对各个Resource接口实现类的感知与分辨,降低学习与使用成本,定义了另一个接口,就是:ResourceLoader接口。 (1)此接口有一个特别重要的方法:Resource getResource(String location)。返回的对象,就是Spring容器中Resource接口的实例 (2)Spring内所有的ApplicationContext实例(包括Spring自启动容器或者用户手动创建的其他容器),都实现了这个方法

public class ResourceTest implements ApplicationContextAware{
	
	ApplicationContext applicationContext ;
	
	public void getResourceTest() {
		//通过applicationContext,只一步getResource(),就可以获取资源
		Resource resource = applicationContext.getResource("spring-mvc.xml");
		//TODO: 用此resource来获取想要的资源
		//......
	}
	
	@Override
	public void setApplicationContext(ApplicationContext applicationContext)
			throws BeansException {		
		this.applicationContext = applicationContext;		
	}
}

因为ApplicationContext在内置的Resource实现类的类型是根据ApplicationContext创建容器的方式决定的。 如果用ClassPathXmlApplicationContext启动的Spring容器,则底层Resource是ClassPathResource实例
如果用FileSystemXmlApplicationContext启动的Spring容器,则底层Resource是FileSystemResource实例 如果用XmlWebApplicationContext启动的Spring容器,则底层Resource是ServletContextResource实例 如果要强制使用不同的方式加载文件:

//强制使用ClassPathResource
Resource resource = applicationContext.getResource("classpath:spring-mvc.xml");
//强制使用UrlResource
Resource resource = applicationContext.getResource("file:book.xml");

与BeanNameAware、ApplicationContextAware这些接口类似,Spring会自动调用:implements了ResourceLoaderAware接口类的实现方法:setResourceLoader(),将ApplicationContext的ResourceLoader注入进去。之后对它getResource(),就可以获取到系统的Resource了

public class ResourceBean implements ResourceLoaderAware {  
 
    private ResourceLoader resourceLoader;  
 
    public ResourceLoader getResourceLoader() {  
        return resourceLoader;  
    } 
 
    @Override  
    public void setResourceLoader(ResourceLoader resourceLoader) {  
        this.resourceLoader = resourceLoader;  
    }  
}