一、文件上传原理

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

  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}\AppData\Local\Temp\
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篇" ,如果觉得老司机的文章还不赖,欢迎关注分享-

补习系列(11)-springboot 文件上传原理的更多相关文章

  1. Web攻防系列教程之文件上传攻防解析(转载)

    Web攻防系列教程之文件上传攻防解析: 文件上传是WEB应用很常见的一种功能,本身是一项正常的业务需求,不存在什么问题.但如果在上传时没有对文件进行正确处理,则很可能会发生安全问题.本文将对文件上传的 ...

  2. Spring Boot 文件上传原理

    首先我们要知道什么是Spring Boot,这里简单说一下,Spring Boot可以看作是一个框架中的框架--->集成了各种框架,像security.jpa.data.cloud等等,它无须关 ...

  3. php文件上传原理详解(含源码)

    1.文件上传原理 将客户端的文件上传到服务器,再将服务器的临时文件上传到指定目录 2.客户端配置 提交表单 表单的发送方式为post 添加enctype="multipart/form-da ...

  4. Java Web文件上传原理分析(不借助开源fileupload上传jar包)

    Java Web文件上传原理分析(不借助开源fileupload上传jar包) 博客分类: Java Web   最近在面试IBM时,面试官突然问到:如果让你自己实现一个文件上传,你的代码要如何写,不 ...

  5. HTTP文件上传原理

    前言 对于这块知识点,我一直都是模糊的,不是非常清楚的.在平时的工作中,遇到上传的问题,也没有深入的去研究过,也都是直接用别人封装好的类来完成自己的工作.某一天,看了本书,说到这个知识点,一脸茫然,觉 ...

  6. 【SpringBoot】07.SpringBoot文件上传

    SpringBoot文件上传 1.编写html文件在classpath下的static中 <!DOCTYPE html> <html> <head> <met ...

  7. SpringBoot文件上传异常之提示The temporary upload location xxx is not valid

    原文: 一灰灰Blog之Spring系列教程文件上传异常原理分析 SpringBoot搭建的应用,一直工作得好好的,突然发现上传文件失败,提示org.springframework.web.multi ...

  8. SpringBoot文件上传与POI的使用

    1.使用springboot上传文件 本文所要源码在一个项目中,源码:https://github.com/zhongyushi-git/springboot-upload-download.git. ...

  9. SpringBoot 文件上传临时文件路径问题

    年后放假回来,一向运行OK的项目突然图片上传不了了,后台报错日志如下: java.io.IOException: The temporary upload location [/tmp/tomcat. ...

随机推荐

  1. python爬取珞珈1号卫星数据

    首先登录珞珈一号数据系统查询想要的数据 利用浏览器审查元素获取包含下载信息的源码 将最右侧的table相关的网页源码copy到剪切板备用 利用python下载数据 ## utf-8 import re ...

  2. 无线路由WMM,Short GI,AP隔离功能介绍(转)

    无线路由器中有开启WMM.开启Short GI和开启AP隔离分别代表什么呢?这是我在我的TP-LINK无线路由器TL-WR841N中的无线高级设置中看到的三个选项,下面三点对这三项无线高级设置做了下解 ...

  3. web基础要点记录

    最近公司项目做完了,不怎么忙,翻看了一些基础的资料,文章.就做了个简单的记录. 1.Chrome 中文界面下默认会将小于 12px 的文本强制按照 12px 显示, 可通过加入 CSS 属性  -we ...

  4. java方法的调用

    各种方法的调用实例 package cn.edu.fhj.day004; public class FunctionDemo { // 定义全局的变量 public int a = 3; public ...

  5. zookeeper使用和原理探究

    转:http://www.blogjava.net/BucketLi/archive/2010/12/21/341268.html zookeeper介绍 zookeeper是一个为分布式应用提供一致 ...

  6. 封装axios

    import axios from 'axios' // import store from '@/vuex/store.js' import router from '../router' impo ...

  7. node06

    1.数据库: server端:数据存在 client端:管理工具,node mysql内有两个单位: 库:类似文件夹,容纳表 表:存储数据 行:一条数据 列(字段,域):一个数据项 主键:数据的唯一标 ...

  8. elasticsearch目录

    快速入门篇(基于版本5.4) Elasticsearch入门 Elasticsearch和Kibana安装 Elasticsearch索引和文档操作 Elasticsearch文档查询 安装和配置(基 ...

  9. 如何让浏览器支持ES6语法,步骤详细到小学生都能看懂!

    为什么ES6会有兼容性问题? 由于广大用户使用的浏览器版本在发布的时候也许早于ES6的定稿和发布,而到了今天,我们在编程中如果使用了ES6的新特性,浏览器若没有更新版本,或者新版本中没有对ES6的特性 ...

  10. [Swift]LeetCode190. 颠倒二进制位 | Reverse Bits

    Reverse bits of a given 32 bits unsigned integer. Example 1: Input: 00000010100101000001111010011100 ...