POST请求实现文件传输

通过POST请求以form-data形式在前后端以及后端服务间传递文件。

通过设置http请求responsecontent-type,后端以二进制流传递数据给前端。

1 以form-data传数据给后端

1.1 后端数据接收接口定义

后端接口/form-data定义

@Slf4j
@RestController
@RequestMapping(value = "/rest")
public class FileTransmit {
    @PostMapping(value = "/form-data")
    public String formData(HttpServletRequest request,
                           @RequestParam(value = "email", required = false) String email,
                           @RequestParam(value = "file_excel", required = false) MultipartFile multipartFile,
                           UserInfo userInfo) {
        StandardMultipartHttpServletRequest standardMultipartRequest = (StandardMultipartHttpServletRequest) request;
​
        log.info("multipartFile in @RequestParam={}", multipartFile.getOriginalFilename());
        MultiValueMap<String, MultipartFile> multiFileMap = standardMultipartRequest.getMultiFileMap();
        for (String paramKey : multiFileMap.keySet()) {
            List<MultipartFile> multipartFileList = multiFileMap.get(paramKey);
            log.info("MultipartFile key={}, size={}", paramKey, multipartFileList.size());
            for (MultipartFile curMultipartFile : multipartFileList) {
                log.info("MultipartFile key={}, file name={}",
                        paramKey, curMultipartFile.getOriginalFilename());
                log.info("MultipartFile in request equals to file in @RequestParam={}",
                        multipartFile.equals(curMultipartFile));
            }
        }
​
        log.info("email in @RequestParam={}", email);
        log.info("userInfo in RequestParam={}", userInfo);
        Map<String, String[]> paramMap = standardMultipartRequest.getParameterMap();
        for (String paramKey : paramMap.keySet()) {
            log.info("param key={}, size={}, value={}",
                    paramKey, paramMap.get(paramKey).length, paramMap.get(paramKey));
        }
        return "Form Data processed finished!";
    }
}
复制代码

1.2 postman调试接口

image-20220103112627238.png

设置请求body为form-data类型,可以同时传递文本数据和文件数据,文件key允许包含多个文件(file_excel),key也允许重复(id, name, email)。

从运行结果来看,@RequestParam注解可以获取form-data中指定key的数据,而HttpServletRequest request则包含了所有的form-data数据。

由于@RequestParam修饰的emailmultipartFile不是数组或者List类型,文本类型的email取拼接值,而文件类型的multipartFile则取第1个value值。修改接口定义,改为List<MultipartFile> multipartFile就可以接收多个文件。

image-20220103113051282.png

image-20220103113405153.png

1.3 前端传递form-data

以简单的html演示在页面填写数据,点击提交时调用1.1节定义的接口并把数据以form-data形式传递给后端。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Upload FormData</title>
</head>
<body>
    <form id="Form1" action="http://localhost:8080/rest/form-data" method="post" enctype="multipart/form-data">
        <div style="text-align: left">
            ID: <input name="id" type="text" /> <br />
            ID: <input name="id" type="text" /> <br />
            Name: <input name="name" type="text" /> <br />
            Name: <input name="name" type="text" /> <br />
            Email: <input name="email" type="text" /> <br />
            Email: <input name="email" type="text" /> <br />
            File: <input id="1" name="file_excel" type="file" multiple="multiple" /> <br />
            File: <input id="2" name="file_img" type="file" /> <br />
            <input type="submit" value="submit" />
            <input type="reset" value="reset" />
        </div>
    </form>
</body>
</html>
复制代码

image-20220103114333475.png

1.4 后端传递form-data

展示如何后端直接封装form-data格式的数据,然后调用1.1节定义的接口,实现后端服务间的文件数据传递。

通过HttpHeaders设置请求传递的数据类型为form-data,通过HttpEntity封装requset headerrequest body,通过RestTemplatepostForObject方法调用POST接口。

使用MultiValueMap封装form-data格式的数据,文件类型数据采用FileSystemResource

public class FileTransmitTest {
    private RestTemplate restTemplate;
    private final String URL_ROOT = "http://localhost:8080/rest";
​
    @Before
    public void init() {
        restTemplate = new RestTemplate();
    }
​
    @Test
    public void FormDataTest() {
        MultiValueMap<String, Object> requestBody = new LinkedMultiValueMap();
        requestBody.add("file_excel", 
                        new FileSystemResource(new File("E:\dataJava\data\city_info.xlsx")));
        requestBody.add("file_excel", 
                        new FileSystemResource(new File("E:\dataJava\data\user_info.xlsx")));
        requestBody.add("file_img", 
                        new FileSystemResource(new File("E:\dataJava\data\github.png")));
        requestBody.add("email", "[email protected]");
        requestBody.add("email", "[email protected]");
        requestBody.add("id", "123");
        requestBody.add("id", "12345");
        requestBody.add("name", "zhangsan");
        requestBody.add("name", "kuangtu");
​
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);
​
        HttpEntity<MultiValueMap<String, Object>> requestEntity = 
            new HttpEntity<>(requestBody, httpHeaders);
        String response = restTemplate.postForObject(URL_ROOT+"/form-data", requestEntity, String.class);
        System.out.println(response);
    }
}
复制代码

1.5 传单个文件给后端

演示后端接收单个文件并保存到指定目录。

接口定义

    @ResponseBody
    @PostMapping(value = "/receive/single")
    public String receiveSingle(MultipartFile multipartFile) throws IOException {
        if (multipartFile == null) {
            return "received file is null!";
        }
        log.info("文件content-type={}", multipartFile.getContentType());
        log.info("文件大小={}", multipartFile.getSize());
        log.info("文件名={}", multipartFile.getName());
        log.info("文件原始名={}", multipartFile.getOriginalFilename());
        // 保存接收的文件到本地
        File destFile = new File("E:\Download\"+multipartFile.getOriginalFilename());
        multipartFile.transferTo(destFile); 
        return "Form Data processed finished!";
    }
复制代码

接口调用

由于接口定义中没有使用@RequestParam注解,当form-datakey不为参数名multipartFile时,变量multipartFile无法初始化,为null。只有当form-datakey设置为multipartFile时,才能正常初始化。

image-20220103120539954.png

如果修改接口定义为

public String receiveSingle(@RequestParam(value = "file") MultipartFile multipartFile) throws IOException
复制代码

那么form-datakey只能设置为file才能完成multipartFile变量的初始化。因此建议@RequestParamvalue值和变量名保存一致,接口调用时封装参数的key也用变量名。

1.6 总结

  • form-data传输数据给后端,需要设置request headercontent-type="multipart/form-data"
  • form-data可以同时传递文本和文件给后端。
  • 对于文件类型的form-data数据,后端接口以MultiPartFile类型接收,并可以通过MultiPartFiletransferTo将文件保存到本地。
  • 对于文本类型的form-data数据,可以定义类接收,也可以用基本类型变量逐个接收。
  • Spring工程后端接口中HttpServletRequest request包含了所有的form-data数据。

2 后端传文件给前端

这里以后端传图片给前端使用为例,方法包括以下3种:

  • 后端把图片保存在文件存储服务器上,返回图片的url给前端,前端设置img标签的src=url即可。这种方式应该是主流,但是不想再搞个存储服务器,没有采用这种方式。
  • 后端以把图片的base64编码以字符串的形式返给前端,前端再解析为图片展示。这种方式虽然存在大图片有可能被截断的肯,但是来得方便,选用了这种方式。
  • 后端以把图片的二进制流以字节数组的形式返给前端,前端再解析为图片展示。返回二进制流不仅适用于图片,还适用于音视频。

2.1 以base64编码传输

后端接口定义

    @CrossOrigin  //允许跨域访问
    @GetMapping(value = "/get-img-code")
    public String getImageBase64(String imageName) throws IOException {
        log.info("request param={}", imageName);
        String imgRootPath = "E:\dataJava\data\";
        File imgFile = new File(imgRootPath + imageName + ".png");
        InputStream inStream =new FileInputStream(imgFile);
        byte[] imgBytes = new byte[(int) imgFile.length()]; //创建合适文件大小的数组
        inStream.read(imgBytes); //读取文件里的内容到b[]数组
        inStream.close();
        log.info("image size={}", imgBytes.length);
        BASE64Encoder encoder = new BASE64Encoder();
        return encoder.encodeBuffer(imgBytes);
    }
复制代码

2.2 以二进制流传输

后端接口定义

    @CrossOrigin  //允许跨域访问
    @GetMapping(value = "/get-img-byte")
    public void getImageByte(HttpServletRequest request, HttpServletResponse response, String imageName) throws IOException {
        String imgRootPath = "E:\dataJava\data\";
        File imgFile = new File(imgRootPath + imageName + ".png");
        InputStream inStream =new FileInputStream(imgFile);
        byte imgBytes[] = new byte[(int) imgFile.length()]; //创建合适文件大小的数组
        inStream.read(imgBytes); //读取文件里的内容到b[]数组
        inStream.close();
​
        response.setContentType("application/octet-stream;charSet=UTF-8");
        response.setContentLength(imgBytes.length);
        try (InputStream inputStream = new ByteArrayInputStream(imgBytes);
            OutputStream outputStream = response.getOutputStream()) {
            IOUtils.copy(inputStream, outputStream);
            outputStream.flush();
        }catch (IOException e) {
            log.error("{}", e.getMessage());
        }
    }
复制代码

2.3 html接收并展示

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js">
    </script>
</head>
<body>
    <div id="main" style="width:100%;">
        <div id="left" style="width:50%;float:left;">
            <p>base64 image</p>
            <p><button id="getBase64" onclick="getImgBase64()">获取并展示图片</button> </p>
            <img id="base64_img" src="" alt="" width="" height="">
        </div>
        <div id="right" style="width:50%;float:left;">
            <p>byte image</p>
            <img id="byte_img" src="http://localhost:8080/rest/get-img-byte?imageName=github" alt="" width="70%" height="70%">
        </div>
    </div>
​
    <script>
        function getImgBase64() {
            $.ajax({
                url : "http://localhost:8080/rest/get-img-code?imageName=github",
                type : 'GET',
                contentType : false,  //必须false才会自动加上正确的Content-Type
                success : function(result) {  //jquery请求返回的结果好像都是字符串类型
                    console.log("reponse result:", result);
                    var src = 'data:image/png;base64,' + result;
                    $("#base64_img").attr('src', src);
                    $("#base64_img").css("width", "70%");
                    $("#base64_img").css("height", "70%");
                },
                error : function(result) {
                    console.log("reponse result:", result);
                    alert("Post Faile!");
                }
            });
        }
    </script>
</body>
</html>
复制代码

运行效果:

image-20220103143611952.png

附:HTTP Headers

HTTP 消息头允许客户端和服务器通过 requestresponse传递附加信息。一个请求头由名称(不区分大小写)后跟一个冒号“:”,冒号后跟具体的值(不带换行符)组成。

根据不同上下文,可将消息头分为:

  • General headers: 同时适用于请求和响应消息,但与最终消息主体中传输的数据无关的消息头。
  • Request headers: 包含更多有关要获取的资源或客户端本身信息的消息头。
  • Response headers: 包含有关响应的补充信息,如其位置或服务器本身(名称和版本等)的消息头。
  • Entity headers: 包含有关实体主体的更多信息,比如主体长(Content-Length)度或其MIME类型。

Request headers

  • Accept:表示浏览器告诉服务端,浏览器可以接收的数据类型。
  • Accept-Encoding:表示浏览器告诉服务端,浏览器可以接收的数据的编码方式。
  • Accept-Language:表示浏览器告诉服务端,浏览器可以接收的数据语言,通常用于做国际化。
  • Content-Type:POST和PUT请求body的文件类型。

Reponse headers

  • Content-Type:接口返回的数据类型。
  • Content-Length:octets (8-bit bytes)中返回body的长度

参考资料:

HTTP Headers

List of HTTP header fields

猜你喜欢

转载自juejin.im/post/7048869287716454407