补习系列(11)-springboot 文件上传原理
一、文件上传原理
一个文件上传的过程如下图所示:
- 浏览器发起HTTP POST请求,指定请求头:
Content-Type: multipart/form-data - 服务端解析请求内容,执行文件保存处理,返回成功消息。
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(); } }
四、文件下载
既然解释了文件上传,自然避免不了文件下载,
文件下载非常简单,只需要包括下面两步:
- 读文件流;
- 输出到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 文件上传的示例代码之外,也简单介绍了文件上传相关的协议知识点。对开发者来说,了解一点内部原理总是有好处的。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
springboot集合jpa使用
现目前java中用较多的数据库操作框架主要有:ibatis,mybatis,hibernate;今天分享的是jpa框架,在springboot框架中能够很快并方便的使用它,就我个人而言觉得如果是做业务不复杂或者为了快速开发的话,选择它比较的不错呢。引入依赖如: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> 选择已有的数据库中的某个表,并插入一条测试数据: 再来创建一个映射实体类,这里需要注意的是就上面截图的表中的列名是驼峰法则,如果直接在实体类创建同样的属性会有问题,如: 实体属性名:fldId 会在jpa执行时候sql映射成fld_Id的列名 这样就会有问题,列名不存在,因此定义映射实体需要注意下,但用fldid能够解决;如下定义实体: @Entity public class tblvist { @Id pri...
- 下一篇
Java入门系列-23-NIO(使用缓冲区和通道对文件操作)
NIO 是什么 java.nio全称java non-blocking(非阻塞) IO(实际上是 new io),是指jdk1.4 及以上版本里提供的新api(New IO) ,为所有的原始类型(boolean类型除外)提供缓存支持的数据容器,使用它可以提供非阻塞式的高伸缩性网络。 NIO与IO的区别 IO NIO 面向流(Stream Oriented) 面向缓冲区(Buffer Oriented) 阻塞IO(Blocking IO) 非阻塞(Non Blocking IO) 无 选择器(Selectors) NIO系统的核心是:通道(Channel)和缓冲区(Buffer) 缓冲区(Buffer) 位于 java.nio 包,所有缓冲区都是 Buffer 抽象类的子类,使用数组对数据进行缓冲。 除了 boolean 类型,Buffer 对每种基本数据类型都有针对的实现类: ByteBuffer CharBuffer ShortBuffer IntBuffer LongBuffer FloatBuffer DoubleBuffer 创建缓冲区通过 xxxBuffer.allocate...
相关文章
文章评论
共有0条评论来说两句吧...