zoukankan      html  css  js  c++  java
  • 补习系列(11)-springboot 文件上传原理

    目录

    一、文件上传原理

    一个文件上传的过程如下图所示:

    1. 浏览器发起HTTP POST请求,指定请求头:
      Content-Type: multipart/form-data

    2. 服务端解析请求内容,执行文件保存处理,返回成功消息。

    RFC1867 定义了HTML表单文件上传的处理机制。
    通常一个文件上传的请求内容格式如下:

    POST /upload HTTP/1.1 
    Host:xxx.org 
    Content-type: multipart/form-data, boundary="boundaryStr"
    
    --boundaryStr
    content-disposition: form-data; name="name"
    
    Name Of Picture
    --boundaryStr
    Content-disposition: attachment; name="picfile"; filename="picfile.gif"
    Content-type: image/gif
    Content-Transfer-Encoding: binary
    
    ...contents of picfile.gif...

    其中boundary指定了内容分割的边界字符串;
    Content-dispostion 指定了这是一个附件(文件),包括参数名称、文件名称;
    Content-type 指定了文件类型;
    Content-Transfer-Encoding 指定内容传输编码;

    二、springboot 文件机制

    springboot 的文件上传处理是基于Servlet 实现的。
    在Servlet 2.5 及早期版本之前,文件上传需要借助 commons-fileupload 组件来实现。
    Servlet 3.0规范之后,提供了对文件上传的原生支持,进一步简化了应用程序的实现。
    Tomcat 为例,在文件上传之后通过将写入到临时文件,最终将文件实体传参到应用层,如下:

    Tomcat 实现了 Servlet3.0 规范,通过ApplicationPart对文件上传流实现封装,
    其中,DiskFileItem 描述了上传文件实体,在请求解析时生成该对象,
    需要关注的是,DiskFileItem 声明了一个临时文件,用于临时存储上传文件的内容,
    SpringMVC 对上层的请求实体再次封装,最终构造为MultipartFile传递给应用程序。

    临时文件

    临时文件的路径定义:

    {temp_dir}/upload_xx_xxx.tmp

    temp_dir是临时目录,通过 系统属性java.io.tmpdir指定,默认值为:

    操作系统 路径
    windows C:Users{username}AppDataLocalTemp|
    Linux /tmp

    定制配置

    为了对文件上传实现定制,可以在application.properties中添加如下配置:

    //启用文件上传
    spring.http.multipart.enabled=true 
    //文件大于该阈值时,将写入磁盘,支持KB/MB单位
    spring.http.multipart.file-size-threshold=0 
    //自定义临时路径
    spring.http.multipart.location= 
    //最大文件大小(单个)
    spring.http.multipart.maxFileSize=10MB
    //最大请求大小(总体)
    spring.http.multipart.maxRequestSize=10MB

    其中 maxFileSize/maxRequestSize 用于声明大小限制,
    当上传文件超过上面的配置阈值时,会返回400(BadRequest)的错误;
    file-size-threshold是一个阈值,用于控制是否写入磁盘;
    location是存储的目录,如果不指定将使用前面所述的默认临时目录。

    这几个参数由SpringMVC控制,用于注入 Servlet3.0 的文件上传配置,如下:

    public class MultipartConfigElement {
    
        private final String location;// = "";
        private final long maxFileSize;// = -1;
        private final long maxRequestSize;// = -1;
        private final int fileSizeThreshold;// = 0;

    三、示例代码

    接下来以简单的代码展示文件上传处理

    A. 单文件上传

        @PostMapping(value = "/single", consumes = {
                MediaType.MULTIPART_FORM_DATA_VALUE }, produces = MediaType.TEXT_PLAIN_VALUE)
        @ResponseBody
        public ResponseEntity<String> singleUpload(@RequestParam("file") MultipartFile file) {
            logger.info("file receive {}", file.getOriginalFilename());
    
            // 检查文件内容是否为空
            if (file.isEmpty()) {
                return ResponseEntity.badRequest().body("no file input");
            }
    
            // 原始文件名
            String fileName = file.getOriginalFilename();
    
            // 检查后缀名
            if (!checkImageSuffix(fileName)) {
                return ResponseEntity.badRequest().body("the file is not image");
            }
    
            // 检查大小
            if (!checkSize(file.getSize())) {
                return ResponseEntity.badRequest().body("the file is too large");
            }
    
            String name = save(file);
    
            URI getUri = ServletUriComponentsBuilder.fromCurrentContextPath().path("/file/get").queryParam("name", name)
                    .build(true).toUri();
    
            return ResponseEntity.ok(getUri.toString());
    
        }

    在上面的代码中,我们通过Controller方法传参获得MultipartFile实体,而后是一系列的检查动作:
    包括文件为空、文件后缀、文件大小,这里不做展开。
    save 方法实现了简单的本地存储,如下:

        private String save(MultipartFile file) {
    
            if (!ROOT.isDirectory()) {
                ROOT.mkdirs();
            }
            try {
                String path = UUID.randomUUID().toString() + getSuffix(file.getOriginalFilename());
                File storeFile = new File(ROOT, path);
                file.transferTo(storeFile);
                return path;
    
            } catch (IllegalStateException e) {
                throw new RuntimeException(e);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

    B. 多文件上传

    与单文件类似,只需要声明MultipartFile数组参数即可:

        @PostMapping(value = "/multi", consumes = {
                MediaType.MULTIPART_FORM_DATA_VALUE }, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
        @ResponseBody
        public ResponseEntity<List<String>> multiUpload(@RequestParam("file") MultipartFile[] files) {
    
            logger.info("file receive count {}", files.length);
    
            List<String> uris = new ArrayList<String>();
            for (MultipartFile file : files) {

    C. 文件上传异常

    如前面所述,当文件上传大小超过限制会返回400错误,为了覆盖默认的行为,可以这样:

        @ControllerAdvice(assignableTypes = FileController.class)
        public class MultipartExceptionHandler {
            @ExceptionHandler(MultipartException.class)
            public ResponseEntity<String> handleUploadError(MultipartException e) {
                return ResponseEntity.badRequest().body("上传失败:" + e.getCause().getMessage());
            }
        }

    D. Bean 配置

    SpringBoot 提供了JavaBean配置的方式,前面提到的几个配置也可以这样实现:

        @Configuration
        public static class FileConfig {
            @Bean
            public MultipartConfigElement multipartConfigElement() {
                MultipartConfigFactory factory = new MultipartConfigFactory();
                factory.setMaxFileSize("10MB");
                factory.setMaxRequestSize("50MB");
                return factory.createMultipartConfig();
    
            }
        }

    四、文件下载

    既然解释了文件上传,自然避免不了文件下载,
    文件下载非常简单,只需要包括下面两步:

    1. 读文件流;
    2. 输出到Response;

    这样,尝试写一个Controller方法:

        @GetMapping(path = "/get")
        public ResponseEntity<Object> get(@RequestParam("name") String name) throws IOException {
    
            ...
            File file = new File(ROOT, name);
            if (!file.isFile()) {
                return ResponseEntity.notFound().build();
            }
    
            if (!file.canRead()) {
                return ResponseEntity.status(HttpStatus.FORBIDDEN).body("no allow to access");
            }
            Path path = Paths.get(file.getAbsolutePath());
    
            ByteArrayResource resource = new ByteArrayResource(Files.readAllBytes(path));
            return ResponseEntity.ok().contentLength(file.length()).body(resource);
        }

    这段代码通过参数(name)来指定访问文件,之后将流写入到Response。

    接下来,我们访问一个确实存在的文件,看看得到了什么?

    ...

    !! 没错,这就是文件的内容,浏览器尝试帮你呈现了。
    那么,我们所期望的下载呢? 其实,真实的下载过程应该如下图:

    区别就在于,我们在返回响应时添加了Content-Disposition头,用来告诉浏览器响应内容是一个附件。
    这样根据约定的协议,浏览器会帮我们完成响应的解析及下载工作。
    修改上面的代码,如下:

        @GetMapping(path = "/download")
        public ResponseEntity<Object> download(@RequestParam("name") String name) throws IOException {
    
            if (StringUtils.isEmpty(name)) {
                return ResponseEntity.badRequest().body("name is empty");
            }
    
            if (!checkName(name)) {
                return ResponseEntity.badRequest().body("name is illegal");
            }
    
            File file = new File(ROOT, name);
            if (!file.isFile()) {
                return ResponseEntity.notFound().build();
            }
    
            if (!file.canRead()) {
                return ResponseEntity.status(HttpStatus.FORBIDDEN).body("no allow to access");
            }
    
            Path path = Paths.get(file.getAbsolutePath());
            ByteArrayResource resource = new ByteArrayResource(Files.readAllBytes(path));
    
            return ResponseEntity.ok().header("Content-Disposition", "attachment;fileName=" + name)
                    .contentLength(file.length()).contentType(MediaType.APPLICATION_OCTET_STREAM).body(resource);
        }

    继续尝试访问文件,此时应该能看到文件被正确下载了。

    码云同步代码

    小结

    文件上传开发是Web开发的基础课,从早期的Servlet + common_uploads组件到现在的SpringBoot,文件的处理已经被大大简化。

    这次除了展示SpringBoot 文件上传的示例代码之外,也简单介绍了文件上传相关的协议知识点。对开发者来说,了解一点内部原理总是有好处的。

    本文来自"美码师的补习系列-springboot篇" ,如果觉得老司机的文章还不赖,欢迎关注分享^-^

    作者:美码师

  • 相关阅读:
    hihoCoder 1148 2月29日
    Java 之常用运算符(3)
    Java 之变量和常量(2)
    Codeforces Round #414 A. Bank Robbery
    Codeforces Round #413 B. T-shirt buying
    C++中 set(集合容器)的用法
    Codeforces Round #411 B. 3-palindrome
    Codeforces Round #411 A. Fake NP
    Codeforces Round #413 A. Carrot Cakes
    Codeforces Round #412 B. T-Shirt Hunt
  • 原文地址:https://www.cnblogs.com/huaweicloud/p/11861521.html
Copyright © 2011-2022 走看看