Spring?Boot實(shí)現(xiàn)文件上傳的兩種方式總結(jié)
最近的一個(gè)小項(xiàng)目里使用到了文件上傳、下載功能,今天我打算梳理一下文件上傳所涉及的技術(shù)及實(shí)現(xiàn)。 內(nèi)容主要包括兩部分,如何通過(guò)純 Servlet 的形式進(jìn)行文件上傳、保存(不通過(guò) Spring 框架);另一部分是如何在 Spring Web MVC 中進(jìn)行文件上傳。
01-從 HTTP 協(xié)議角度分析文件上傳
HTTP 協(xié)議傳輸文件一般都遵循 RFC 1867 規(guī)范,即客戶(hù)端通過(guò) POST 請(qǐng)求,Context-Type 為 "multipart/form-data"。 前端提交頁(yè)面一般為:
<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>
通過(guò) Wireshark 對(duì) POST 請(qǐng)求進(jìn)行抓包,發(fā)現(xiàn)發(fā)送的請(qǐng)求格式為:
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--
對(duì)上述過(guò)程有了基本的理解后,就可以動(dòng)手來(lái)寫(xiě)上傳功能(本文以圖片為例,當(dāng)然你也可以實(shí)現(xiàn)支持上傳其他類(lèi)型的文件的版本)。 接下來(lái)我會(huì)展示兩種實(shí)現(xiàn)文件上傳功能的代碼,第一種是使用純 Servlet API 實(shí)現(xiàn),不依賴(lài) Spring 框架,當(dāng)你的程序是一個(gè)簡(jiǎn)單的基于 Servlet 的應(yīng)用時(shí),可以參考這種方式。 第二種,借助了 Spring 提供的 MultipartFile 以及 MultipartResolver 實(shí)現(xiàn)的文件上傳。
02-Servlet 處理上傳請(qǐng)求
首先,需要先實(shí)現(xiàn)一個(gè) 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); // 檢查存儲(chǔ)文件的路徑是否存在,若不存在,則創(chuàng)建一個(gè) 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 { // 第一節(jié)中介紹過(guò),文件上傳是通過(guò) POST 方法完成的,所以這里我們要重寫(xiě) doPost 方法 try { final Collection<Part> parts = req.getParts(); // 從請(qǐng)求中獲取 multipart 內(nèi)容 for (Part part : parts) { if (part.getSize() <= 0) { // 判斷上傳的內(nèi)容是否空文件 System.out.println("part is empty, skip it!"); continue; } String fileName = getFileName(part); // 從請(qǐng)求中獲取文件的名 // or //final String fileName = part.getSubmittedFileName(); // fileName 是前端提供的,并不十分可靠 // 后端應(yīng)該自己生成一個(gè)文件名 fileName = genNewFileName(fileName); String uploadedFilePath = uploadDir + File.separator + fileName; part.write(uploadedFilePath); // 存儲(chǔ)到指定目錄 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); } } // 默認(rèn)文件名 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) ); } }
這里面有幾個(gè)地方需要解釋一下;
- 其一,getFileName 為什么要這么實(shí)現(xiàn)?參考第一節(jié)給出的 HTTP 報(bào)文,發(fā)現(xiàn)每個(gè) Part,即兩個(gè) boundary 之間的內(nèi)容,通過(guò) Content-Disposition 給出了內(nèi)容類(lèi)型、文件名等信息。 getFileName 中的邏輯就是從這個(gè)格式里獲得文件名的。 不過(guò),這個(gè)文件名是由前端提供的,它其實(shí)也可以不提供,所以這個(gè)值就不是那么可靠。 所以,在我們將上傳文件保存到磁盤(pán)上時(shí),最好重新生成一個(gè)文件名,這就使 genNewFileName 的動(dòng)機(jī)。
- 其二,根據(jù) HttpServletRequest 接口的文檔,getParts 方法在請(qǐng)求不是 multipart 類(lèi)型時(shí)會(huì)拋異常。 而且,Part 的內(nèi)容有可能是為空的,如果我們不做判斷,可能會(huì)在服務(wù)端創(chuàng)建一個(gè)空文件。
- Servlet 類(lèi)上的注解,@WebServlet 不再介紹,@MultipartConfig 是對(duì)請(qǐng)求、請(qǐng)求中文件大小的限制條件,當(dāng)請(qǐng)求或文件超過(guò)這個(gè)限制時(shí)會(huì)拋對(duì)應(yīng)的異常。
有了上面的定義,我們就可以測(cè)試下上傳功能了。
服務(wù)啟動(dòng)后,訪(fǎng)問(wèn) 頁(yè)面能夠得到上傳頁(yè)面。 選擇文件,提交后,服務(wù)端響應(yīng)成功,并將新名字傳給前端。例如:
注:這里 會(huì)返回 Thymeleaf 實(shí)現(xiàn)的上傳界面。
@GetMapping("/servlet-upload-page") public String uploadImageByServlet(Model model) { model.addAttribute("message", "please choose file to be uploaded"); return "upload/servlet-upload"; }
界面內(nèi)容為:
<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 注冊(cè)到的 url。
03-通過(guò) Spring Boot 中的 MultipartFile 處理上傳請(qǐng)求
通過(guò) Spring Boot 來(lái)實(shí)現(xiàn)文件上傳功能會(huì)更簡(jiǎn)單,它的自動(dòng)化配置機(jī)制已經(jīng)做了大部分的工作。 開(kāi)發(fā)人員的工作就是定義一個(gè) Controller,處理文件上傳請(qǐng)求就可以了。
@Controller public class UploadController { public static String UPLOAD_DIRECTORY = System.getProperty("user.dir") + File.separator + "uploads"; @GetMapping("/upload") // 主要返回文件上傳頁(yè)面 public String uploadImage(Model model) { model.addAttribute("message", "please choose file to be uploaded"); return "upload/index"; } @PostMapping("/upload") // 處理文件上傳 POST 請(qǐng)求 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 中,文件上傳請(qǐng)求(multipart request)被 StandardServletMultipartResolver 進(jìn)一步封裝為 StandardMultipartHttpServletRequest。 解析原請(qǐng)求的過(guò)程與我在前面介紹 Servlet 的方式時(shí)基本類(lèi)似:
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 的一個(gè)實(shí)現(xiàn)類(lèi) files.add(part.getName(), new StandardMultipartFile(part, filename)); } else { // 把不是文件的屬性添加到 multipartParameterNames 中 this.multipartParameterNames.add(part.getName()); } } setMultipartFiles(files); } catch (Throwable ex) { handleParseFailure(ex); } }
通過(guò)上面的代碼可以了解到,Client 提交的 POST 請(qǐng)求中,上傳的文件被封裝稱(chēng) MultipartFile。 所以,我們?cè)?Controller 中的處理方法中,可以通過(guò) @RequestParam 的方式拿到這個(gè)文件列表進(jìn)行處理,就像我們的 UploadController 實(shí)現(xiàn)的那樣。
04-總結(jié)
在今天的文章中,我介紹了文件上傳的兩種實(shí)現(xiàn)方式,從純 Servlet 實(shí)現(xiàn),到基于 Spring Boot MVC 實(shí)現(xiàn)。 并且分析了 Spring Boot 中對(duì) Multipart 請(qǐng)求的封裝過(guò)程。
到此這篇關(guān)于Spring Boot實(shí)現(xiàn)文件上傳的兩種方式的文章就介紹到這了,更多相關(guān)SpringBoot文件上傳方式內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot+ruoyi框架文件上傳和下載的實(shí)現(xiàn)
- tdesign的文件上傳功能實(shí)現(xiàn)(微信小程序+idea的springboot)
- SpringBoot中的文件上傳和異常處理詳解
- springboot文件上傳時(shí)maxPostSize設(shè)置大小失效問(wèn)題及解決
- SpringBoot實(shí)現(xiàn)文件上傳下載實(shí)時(shí)進(jìn)度條功能(附源碼)
- SpringBoot簡(jiǎn)單實(shí)現(xiàn)文件上傳
- springboot大文件上傳、分片上傳、斷點(diǎn)續(xù)傳、秒傳的實(shí)現(xiàn)
- 在Spring Boot中處理文件上傳功能實(shí)現(xiàn)
相關(guān)文章
SpringBoot中實(shí)現(xiàn)文件上傳、下載、刪除功能的步驟
本文將詳細(xì)介紹如何在 Spring Boot 中實(shí)現(xiàn)文件上傳、下載、刪除功能,采用的技術(shù)框架包括:Spring Boot 2.4.2、Spring MVC、MyBatis 3.5.6、Druid 數(shù)據(jù)源、JUnit 5 等,文中有詳細(xì)的操作步驟和示例代碼供大家參考,需要的朋友可以參考下2024-01-01springboot2.3 整合mybatis-plus 高級(jí)功能及用法詳解
這篇文章主要介紹了springboot2.3 整合mybatis-plus 高級(jí)功能,本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09IntelliJ IDEA中折疊所有Java代碼,再也不怕大段的代碼了
今天小編就為大家分享一篇關(guān)于IntelliJ IDEA中折疊所有Java代碼,再也不怕大段的代碼了,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2018-10-10MyBatis-Plus動(dòng)態(tài)返回實(shí)體類(lèi)示例詳解
這篇文章主要為大家介紹了MyBatis-Plus動(dòng)態(tài)返回實(shí)體類(lèi)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07解決Java中的強(qiáng)制類(lèi)型轉(zhuǎn)換和二進(jìn)制表示問(wèn)題
這篇文章主要介紹了解決Java中的強(qiáng)制類(lèi)型轉(zhuǎn)換和二進(jìn)制表示問(wèn)題,需要的朋友可以參考下2019-05-05SpringMVC配置攔截器實(shí)現(xiàn)登錄控制的方法
這篇文章主要介紹了SpringMVC配置攔截器實(shí)現(xiàn)登錄控制的方法,SpringMVC讀取Cookie判斷用戶(hù)是否登錄,對(duì)每一個(gè)action都要進(jìn)行判斷,有興趣的可以了解一下。2017-03-03Java?map為什么不能遍歷的同時(shí)進(jìn)行增刪操作
這篇文章主要介紹了Java?map為什么不能遍歷的同時(shí)進(jìn)行增刪操作,文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-07-07Idea導(dǎo)入eureka源碼實(shí)現(xiàn)過(guò)程解析
這篇文章主要介紹了Idea導(dǎo)入eureka源碼實(shí)現(xiàn)過(guò)程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-08-08Java實(shí)現(xiàn)統(tǒng)計(jì)在線(xiàn)人數(shù)功能的方法詳解
很多人在筆試或者面試中問(wèn)到:現(xiàn)在要你實(shí)現(xiàn)一個(gè)統(tǒng)計(jì)在線(xiàn)人數(shù)的功能,你該怎么設(shè)計(jì)?不知道的朋友,這篇文章就來(lái)告訴你具體實(shí)現(xiàn)方法2022-08-08Springboot實(shí)現(xiàn)過(guò)濾器的兩種方式
今天通過(guò)本文給大家分享Springboot實(shí)現(xiàn)過(guò)濾器的兩種方式,第一種是spring容器注冊(cè)filter,第二種方式是通過(guò)@WebFilter 注解來(lái)配置,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友參考下吧2023-10-10