14、SpringMVC进阶:文件上传和下载

Media Type

上传文件时,需要指定媒体类型,有下面这两种。

multipart/form-data

在HTML中,表单form中的enctype 属性,指定在发送到服务器之前应该如何对表单数据进行编码。

默认地,表单数据会编码为 “application/x-www-form-urlencoded”。就是说,在发送到服务器之前,所有字符都会进行编码(空格转换为 “+” 加号,特殊符号转换为 ASCII HEX 值)。

当表单使用 POST 请求时,数据会被以 x-www-urlencoded 方式编码到 Body 中来传送,而如果 GET 请求,则是附在 url 链接后面来发送。GET 请求只支持 ASCII 字符集,因此,如果我们要发送更大字符集的内容,我们应使用 POST 请求。

enctype 有三种可配置类型:

描述
application/x-www-form-urlencoded 在发送前编码所有字符(默认)
multipart/form-data 不对字符编码。在使用包含文件上传控件的表单时,必须使用该值。
text/plain 空格转换为 “+” 加号,但不对特殊字符编码。

如果要发送大量的二进制数据(non-ASCII),application/x-www-form-urlencoded 显然是低效的,因为它需要用 3 个字符来表示一个 non-ASCII 的字符。因此,这种情况下,应该使用 “multipart/form-data” 格式。

multipart/form-data 定义在 rfc2388 中,最早的 HTTP POST 是不支持文件上传的,给编程开发带来很多问题。但是在1995年,ietf 出台了 rfc1867,也就是《RFC 1867 -Form-based File Upload in HTML》,用以支持文件上传。所以 Content-Type 的类型扩充了multipart/form-data 用以支持向服务器发送二进制数据。

application/octet-stream

application/octet-stream属于HTTP规范中Content-Type的一种,很少使用。

只能提交二进制,而且只能提交一个二进制,如果提交文件的话,只能提交一个文件,后台接收参数只能有一个,而且只能是流(或者字节数组)。

Spring MVC对文件上传的支持

multipart包

在spring-web模块中的multipart包下,提供了很多类和接口用于处理multipart请求。
*
commons包是对apache中commons-fileupload组件的支持。

MultipartFile接口

MultipartFile接口是多部分请求中接收的上传文件对象。

MultipartFile定义了一些获取上传文件信息、流、操作文件的方法。

public interface MultipartFile extends InputStreamSource {
   
     
		// 返回multipart form参数的名称
		String getName();
		// 返回客户端文件系统中的原始文件名,由客户提供,不应盲目使用。
		// 建议不要直接使用此文件名。最好生成一个唯一的并保存
		String getOriginalFilename();
		// 返回文件的ContentType
		String getContentType();
		// 返回上传的文件是否为空,即没有文件
		boolean isEmpty();
		// 返回文件的大小(以字节为单位)如果为空,则返回0
		long getSize();
		// 以字节数组的形式返回文件的内容。如果为空,则返回空字节数组,在访问错误时抛出IOException
		byte[] getBytes() throws IOException;
		// 读取文件内容返回InputStream,用户负责关闭返回的流
		// 如果为空,则返回空流,在访问错误时抛出IOException
		InputStream getInputStream() throws IOException;
		// 返回此多部分文件的Resource 表示形式。
		default Resource getResource() {
   
     
		return new MultipartFileResource(this);
		// 将接收到的文件传输到给定的目标文件,这可以在文件系统中移动文件
		// 或将内存中的内容保存到目标文件,如果目标文件已存在,将首先删除它。
		// 
		void transferTo(File dest) throws IOException, IllegalStateException;
		// 将接收到的文件传输到给定的目标文件夹
		default void transferTo(Path dest) throws IOException, IllegalStateException {
   
     
		FileCopyUtils.copy(getInputStream(), Files.newOutputStream(dest));
	}
}

MultipartResolver接口

MultipartResolver解析器,是用于解析包括文件上传在内的multipart请求。

支持两种方式:

  • 基于Commons FileUpload 的实现。
  • 基于 Servlet 3.0 multipart 请求解析。

MultipartResolver定义了三个方法:

public interface MultipartResolver {
   
     
	// 确定给定的请求是否包含multipart 内容
	boolean isMultipart(HttpServletRequest request);
	// 将给定的HTTP请求解析为多部分文件和参数,并将请求包装在multipartttpServletRequest。
	MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException;
	// 清理用于多部分处理的所有资源
	void cleanupMultipart(MultipartHttpServletRequest request);
}

MultipartResolver有两个实现类,CommonsMultipartResolver,StandardServletMultipartResolver。
*
CommonsMultipartResolver是对Apache Commons FileUpload组件的支持。

StandardServletMultipartResolver是MultipartResolver接口的标准实现,基于Servlet3.0,使用时要作为“multipartResolver”类型的bean添加到Spring DispatcherServlet上下文中(spring boot已默认配置)。

public class StandardServletMultipartResolver implements MultipartResolver {
   
     
	// 是否在执行时延迟解析多部分请求,默认值为“false”,立即解析多部分元素。
	private boolean resolveLazily = false;
	public void setResolveLazily(boolean resolveLazily) {
   
     
		this.resolveLazily = resolveLazily;
	}
	// 检查请求ContentType 是否包含multipart
	@Override
	public boolean isMultipart(HttpServletRequest request) {
   
     
		return StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/");
	}
	// 解析Multipart为MultipartHttpServletRequest 
	@Override
	public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException {
   
     
		return new StandardMultipartHttpServletRequest(request, this.resolveLazily);
	}
	// 清理
	@Override
	public void cleanupMultipart(MultipartHttpServletRequest request) {
   
     
	// others....
	}
}

MultipartRequest接口

此接口定义多部分请求对象访问操作。

public interface MultipartRequest {
   
     
	// 返回一个{@link java.util.Iterator}字符串对象,其中包含
	// 此请求中包含的多部分文件的参数名称。这些是表单的字段名(与普通参数类似)				   		
	//而不是原始文件名。
	Iterator<String> getFileNames();
	// 根据返回此请求中上传文件的MultipartFile 对象
	MultipartFile getFile(String name);
	// 根据返回此请求中上传文件的MultipartFile 对象集合
	List<MultipartFile> getFiles(String name);
	// 返回此请求中包含的多部分文件的{@link java.util.Map}。
	// @返回一个包含作为键的参数名的映射,以及{@link MultipartFile}对象作为值
	Map<String, MultipartFile> getFileMap();
	// 返回MultiValueMap
	MultiValueMap<String, MultipartFile> getMultiFileMap();
	// 返回content type
	String getMultipartContentType(String paramOrFileName);
}

MultipartRequest有如下实现类:
*
DefaultMultipartHttpServletRequest是MultipartHttpServletRequest的默认实现,提供生成参数值的相关方法。
*
StandardMultipartHttpServletRequest包装Servlet 3.0 HttpServletRequest
和它的部分对象。参数通过请求的getParameter公开。

上传文件案例

方式1.使用fileupload

1、 添加一个thymeleaf上传文件页面;

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh">
<head>
    <meta charset="UTF-8">
    <title>文件管理</title>
</head>
<body>
<form name="form1" method="post" action="/file/common/upload" enctype="multipart/form-data">
    选择文件: <input type="file" name="file">
    <input type="submit" value="点击上传"/>
</form>
</body>
</html>

添加一个跳转thymeleaf页面的控制器。


    @GetMapping("/commonView")
    public Object commonView() {
   
     
        return "file/file";
    }

1、 添加fileupload工具包;

        <dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
            <version>1.4</version>
        </dependency>

1、 添加一个处理器方法,处理上传文件逻辑;

    @PostMapping("/common/upload")
    @ResponseBody
    public Object commonUpload(@RequestParam CommonsMultipartFile file) throws IOException {
   
     
        if (file == null || file.isEmpty()) {
   
     
            return "文件不能为空";
        }
        try {
   
     
            // 1. 创建目标文件夹
            File dir = new File("D:\\tmp" + File.separator + "files");
            if (!dir.exists()) {
   
     
                // 文件夹不存在时,创建
                dir.mkdirs();
            }
            // 2. 获取文件信息
            String contentType = file.getContentType();
            System.out.println("contentType========" + contentType);
            String name = file.getName();
            System.out.println("name========" + name);
            String originalFilename = file.getOriginalFilename();
            System.out.println("originalFilename========" + originalFilename);
            long size = file.getSize();
            System.out.println("size========" + size);
            Resource resource = file.getResource();
            System.out.println("resource========" + resource.toString());
            String storageDescription = file.getStorageDescription();
            System.out.println("storageDescription========" + storageDescription);
            // 3. 创建保存文件对象
            String storeFileName = UUID.randomUUID() + "_"+originalFilename; // 文件名采用UUID
            File storeFile = new File(dir.getAbsolutePath() + File.separator + storeFileName);
            file.transferTo(storeFile);
        } catch (Exception e) {
   
     
            e.printStackTrace();
            return "上传失败";
        }
        return "success";
    }

1、 注入CommonsMultipartResolver解析器;

    @Bean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
    @ConditionalOnMissingBean(MultipartResolver.class)
    public CommonsMultipartResolver multipartResolver() {
   
     
        CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
        multipartResolver.setResolveLazily(false);
        return multipartResolver;
    }

1、 测试;

选择文件,点击上传,返回success,表示上传成功,去目标文件夹,也发现了上传的文件
*

方式2.使用Spring Multipart解析器上传

1、 注释CommonsMultipartResolver,Springboot已经自动注入了StandardServletMultipartResolver;
*
2、 编写一个控制器方法,除了访问路径和入参对象不同,其他的处理逻辑和上面一样;

    @PostMapping("/spring/upload")
    @ResponseBody
    public Object springUpload(@RequestParam MultipartFile file) throws IOException {
   
     
	// 省略。。。
    }

1、 在页面添加一个上传文件表单;

<form name="form1" method="post" action="/file/spring/upload" enctype="multipart/form-data">
    选择文件form1: <input type="file" name="file">
    <input type="submit" value="点击上传"/>
</form>

1、 测试,和上面一样;

文件下载案例

文件下载时,我们只需要将流写入响应报文就可以了。

1、 创建一个下载页面;

<form name="form3" method="post" action="/file/download" enctype="application/x-www-form-urlencoded">
    输入文件完整路径: <input type="text" name="filePath">
    <input type="submit" value="下载文件"/>
</form>

1、 创建一个下载控制器,直接取流,然后放到响应中;

    @PostMapping("/download")
    public ResponseEntity<?> download(@RequestParam String filePath) {
   
     
        try (InputStream in = new FileInputStream(filePath)) {
   
     
            // 获取文件流
            byte[] body = new byte[in.available()];
            in.read(body); // 写入到字节数组
            String fileName = filePath.substring(filePath.lastIndexOf("\\") + 1); // 截取到文件名
            MultiValueMap<String, String> headers = new HttpHeaders(); // 将文件信息写入消息头
            headers.add("Content-Disposition", "attachment;filename=" + fileName);
            return new ResponseEntity<>(body, headers, HttpStatus.OK);
        } catch (FileNotFoundException exception) {
   
     
            exception.printStackTrace();
            return new ResponseEntity<>("服务器FileNotFoundException", HttpStatus.INTERNAL_SERVER_ERROR);

        } catch (IOException e) {
   
     
            e.printStackTrace();
            return new ResponseEntity<>("服务器IOException", HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

1、 测试;
*

版权声明:本文不是「本站」原创文章,版权归原作者所有 | 原文地址: