Spring Boot实现文件上传的两种方式总结
作者:Java技术攻略
最近的一个小项目里使用到了文件上传、下载功能,今天我打算梳理一下文件上传所涉及的技术及实现。 内容主要包括两部分,如何通过纯 Servlet 的形式进行文件上传、保存(不通过 Spring 框架);另一部分是如何在 Spring Web MVC 中进行文件上传。
01-从 HTTP 协议角度分析文件上传
HTTP 协议传输文件一般都遵循 RFC 1867 规范,即客户端通过 POST 请求,Context-Type 为 "multipart/form-data"。 前端提交页面一般为:
<form method="post" action="${user_upload_service_url}" enctype="multipart/form-data"> Choose a file: <input type="file" name="image" accept="image/*" /> <input type="submit" value="Upload" /> </form>
通过 Wireshark 对 POST 请求进行抓包,发现发送的请求格式为:
POST /upload HTTP/1.1
Host: localhost:8080
Content-Length: 197624
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarynIbwtdWznj6QLu52
First boundary: ------WebKitFormBoundarynIbwtdWznj6QLu52
Encapsulated multipart part: (image/png)
Content-Disposition: form-data; name="image"; filename="Snipaste_2023-01-05_13-35-11.png"
Content-Type: image/png
Portable Network Graphics
Boundary: ------WebKitFormBoundarynIbwtdWznj6QLu52
Encapsulated multipart part: (image/png)
Content-Disposition: form-data; name="image"; filename="Snipaste_2023-01-05_13-35-12.png"
Content-Type: image/png
Portable Network Graphics
Last boundary: ------WebKitFormBoundarynIbwtdWznj6QLu52--
对上述过程有了基本的理解后,就可以动手来写上传功能(本文以图片为例,当然你也可以实现支持上传其他类型的文件的版本)。 接下来我会展示两种实现文件上传功能的代码,第一种是使用纯 Servlet API 实现,不依赖 Spring 框架,当你的程序是一个简单的基于 Servlet 的应用时,可以参考这种方式。 第二种,借助了 Spring 提供的 MultipartFile 以及 MultipartResolver 实现的文件上传。
02-Servlet 处理上传请求
首先,需要先实现一个 Servlet。
@MultipartConfig(fileSizeThreshold = 5 * 1024 * 1024, maxFileSize = 1024 * 1024 * 5, maxRequestSize = 1024 * 1024 * 5) @WebServlet(name = "MultipartServlet", urlPatterns = "/servlet-upload") public class MultipartServlet extends HttpServlet { private File uploadDir = null; @Override public void init(ServletConfig config) throws ServletException { super.init(config); // 检查存储文件的路径是否存在,若不存在,则创建一个 String uploadPath = System.getProperty("user.dir") + File.separator + "uploads"; uploadDir = new File(uploadPath); if (!uploadDir.exists()) { uploadDir.mkdir(); } } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 第一节中介绍过,文件上传是通过 POST 方法完成的,所以这里我们要重写 doPost 方法 try { final Collection<Part> parts = req.getParts(); // 从请求中获取 multipart 内容 for (Part part : parts) { if (part.getSize() <= 0) { // 判断上传的内容是否空文件 System.out.println("part is empty, skip it!"); continue; } String fileName = getFileName(part); // 从请求中获取文件的名 // or //final String fileName = part.getSubmittedFileName(); // fileName 是前端提供的,并不十分可靠 // 后端应该自己生成一个文件名 fileName = genNewFileName(fileName); String uploadedFilePath = uploadDir + File.separator + fileName; part.write(uploadedFilePath); // 存储到指定目录 System.out.println("saved to " + uploadedFilePath); resp.getWriter().write("saved to " + uploadedFilePath); } } catch (ServletException se) { // request is not of type multipart/form-data } resp.setStatus(HttpServletResponse.SC_OK); resp.getWriter().flush(); resp.getWriter().close(); } private String getFileName(Part part) { for (String s : part.getHeader("Content-Disposition").split(";")) { if (s.trim().startsWith("filename")) { return s.substring(s.indexOf("=") + 2, s.length() - 1); } } // 默认文件名 return "foo.txt"; } private String genNewFileName(String filename) { String filenameFormat = "%s.%s"; return String.format(filenameFormat, UUID.randomUUID().toString().replace("-", "").substring(8), FilenameUtils.getExtension(filename) ); } }
这里面有几个地方需要解释一下;
- 其一,getFileName 为什么要这么实现?参考第一节给出的 HTTP 报文,发现每个 Part,即两个 boundary 之间的内容,通过 Content-Disposition 给出了内容类型、文件名等信息。 getFileName 中的逻辑就是从这个格式里获得文件名的。 不过,这个文件名是由前端提供的,它其实也可以不提供,所以这个值就不是那么可靠。 所以,在我们将上传文件保存到磁盘上时,最好重新生成一个文件名,这就使 genNewFileName 的动机。
- 其二,根据 HttpServletRequest 接口的文档,getParts 方法在请求不是 multipart 类型时会抛异常。 而且,Part 的内容有可能是为空的,如果我们不做判断,可能会在服务端创建一个空文件。
- Servlet 类上的注解,@WebServlet 不再介绍,@MultipartConfig 是对请求、请求中文件大小的限制条件,当请求或文件超过这个限制时会抛对应的异常。
有了上面的定义,我们就可以测试下上传功能了。
服务启动后,访问 页面能够得到上传页面。 选择文件,提交后,服务端响应成功,并将新名字传给前端。例如:
注:这里 会返回 Thymeleaf 实现的上传界面。
@GetMapping("/servlet-upload-page") public String uploadImageByServlet(Model model) { model.addAttribute("message", "please choose file to be uploaded"); return "upload/servlet-upload"; }
界面内容为:
<body> <h2>Upload Image Example</h2> <p th:text="${message}" th:if="${message ne null}" class="alert alert-primary"></p> <form method="post" th:action="@{/servlet-upload}" enctype="multipart/form-data"> <div class="form-group"> <input type="file" name="image" accept="image/*" class="form-control-file"> <input type="file" name="image" accept="image/*" class="form-control-file"> </div> <button type="submit" class="btn btn-primary">Upload image</button> </form> <span th:if="${msg != null}" th:text="${msg}"></span> </body> </html>
其中,@{/servlet-upload}
指向的是 @WebServlet(name = "MultipartServlet", urlPatterns = "/servlet-upload")
中将 Servlet 注册到的 url。
03-通过 Spring Boot 中的 MultipartFile 处理上传请求
通过 Spring Boot 来实现文件上传功能会更简单,它的自动化配置机制已经做了大部分的工作。 开发人员的工作就是定义一个 Controller,处理文件上传请求就可以了。
@Controller public class UploadController { public static String UPLOAD_DIRECTORY = System.getProperty("user.dir") + File.separator + "uploads"; @GetMapping("/upload") // 主要返回文件上传页面 public String uploadImage(Model model) { model.addAttribute("message", "please choose file to be uploaded"); return "upload/index"; } @PostMapping("/upload") // 处理文件上传 POST 请求 public String upload(@RequestParam("image")MultipartFile[] files, Model model) throws IOException { StringBuilder sb = new StringBuilder(); for (MultipartFile file : files) { if (file.getSize() <= 0) { continue; } final String newFileName = save(file); final String msg = String.format("uploaded file %s, and new filename is %s%n", file.getOriginalFilename(), newFileName); sb.append(msg); } model.addAttribute("msg", sb.toString()); return "upload/index"; } private String save(MultipartFile file) throws IOException{ String newFileName = genNewFileName(file.getOriginalFilename()); final Path filePath = Paths.get(UPLOAD_DIRECTORY, newFileName); Files.write(filePath, file.getBytes()); System.out.println("file saved to: " + filePath); return newFileName; } }
Spring Boot 中,文件上传请求(multipart request)被 StandardServletMultipartResolver 进一步封装为 StandardMultipartHttpServletRequest。 解析原请求的过程与我在前面介绍 Servlet 的方式时基本类似:
private void parseRequest(HttpServletRequest request) { try { Collection<Part> parts = request.getParts(); this.multipartParameterNames = new LinkedHashSet<>(parts.size()); MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<>(parts.size()); for (Part part : parts) { String headerValue = part.getHeader(HttpHeaders.CONTENT_DISPOSITION); ContentDisposition disposition = ContentDisposition.parse(headerValue); String filename = disposition.getFilename(); if (filename != null) { // 把文件添加到 files if (filename.startsWith("=?") && filename.endsWith("?=")) { filename = MimeDelegate.decode(filename); } // part 被封装为 StandardMultipartFile,它是 MultipartFile 的一个实现类 files.add(part.getName(), new StandardMultipartFile(part, filename)); } else { // 把不是文件的属性添加到 multipartParameterNames 中 this.multipartParameterNames.add(part.getName()); } } setMultipartFiles(files); } catch (Throwable ex) { handleParseFailure(ex); } }
通过上面的代码可以了解到,Client 提交的 POST 请求中,上传的文件被封装称 MultipartFile。 所以,我们在 Controller 中的处理方法中,可以通过 @RequestParam 的方式拿到这个文件列表进行处理,就像我们的 UploadController 实现的那样。
04-总结
在今天的文章中,我介绍了文件上传的两种实现方式,从纯 Servlet 实现,到基于 Spring Boot MVC 实现。 并且分析了 Spring Boot 中对 Multipart 请求的封装过程。
到此这篇关于Spring Boot实现文件上传的两种方式的文章就介绍到这了,更多相关SpringBoot文件上传方式内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!