一文讀懂SpringMVC中的檔案上傳與下載

Wizey發表於2018-09-22

這兩天研究了一下 SpringMVC 中檔案上傳與下載,也遇到了一些坑,這裡做個總結。

1、檔案上傳下載的原理

Web 中檔案上傳下載是和 HTTP 協議分不開的,想要更加深入的理解檔案上傳和下載,必須要對 HTTP 協議有充分認識。

1.1 檔案上傳

在 TCP/IP 中,最早出現的檔案上傳機制是 FTP,這是將檔案由客戶端傳送到伺服器的標準機制。而在 Web 開發中,使用應用層協議 HTTP,通過在請求頭中設定傳輸的內容型別 Content-Type 為 multipart/form-data; boundary=流分隔符值 來上傳檔案,這個流分隔符用來區分一個檔案上傳的開始和結束,下面的是我在火狐瀏覽器中擷取的多個檔案上傳時的訊息頭和引數。

檔案上傳訊息頭.jpg
檔案上傳引數.jpg

對應在 HTML 中就是為 form 元素設定 Method = "post" enctype="`multipart/form-data" 屬性,為 input 元素設定 type = "file" 以及多個檔案上傳時設定 "multiple" 屬性,程式碼示例如下。

<form action="" method="post" enctype="multipart/form-data" onsubmit="return check()">
    <input type="file" name="file" id="file" multiple="multiple"><br>
    <input type="submit" value="上傳">
</form>
複製程式碼

對錶單中的 enctype 屬性做個詳細的說明:

  • application/x-www=form-urlencoded:預設方式,只處理表單域中的 value 屬性值,採用這種編碼方式的表單會將表單域中的值處理成 URL 編碼方式。
  • multipart/form-data:這種編碼方式會以二進位制流的方式來處理表單資料,這種編碼方式會把檔案域指定檔案的內容也封裝到請求引數中,不會對字元編碼。
  • text/plain:除了把空格轉換為 "+" 號外,其他字元都不做編碼處理,這種方式適用直接通過表單傳送郵件。

1.2 檔案下載

通過在響應訊息頭中設定 Content-Disposition 和 Content-Type 使得瀏覽器無法使用某種方式或者啟用某個程式來處理 MIME 型別的檔案,來讓瀏覽器提示是否儲存檔案,也就是檔案的下載。Content-Disposition 的值為 attachment;filename=檔名,Content-Type 的值為 application/octet-stream 或者 application/x-msdownload。檔案中的中文注意編碼問題,不同瀏覽器之間是有差異的。

檔案下載.jpg

2、SpringMVC中的檔案上傳與下載

本文涉及的所以程式碼,都可以在我的 GitHub 上找到,傳送門

2.1 檔案上傳

檔案在上傳時注意前後端最好都做下檢查,如檔案的大小,檔案的型別等等,我這裡就只做了後端的驗證。

檔案上傳頁面程式碼:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html lang="zh-cn">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>檔案上傳與下載</title>
    <noscript>
        <style>
            #main {
                display: none !important;
            }
        </style>
        <p align="center">您的瀏覽器禁止了JS,請先啟動指令碼</p>
    </noscript>
    <script>
        function check() {
            var file = document.getElementById("file");
            if (file.value == "") {
                alert("上傳的檔案為空")
                return false;
            }
            return true;
        }
    </script>
</head>
<body>
    <div id="main" style="width:500px; margin: 0 auto;">
        <span style="color:red;">${msg}</span>
        <form action="" method="post" enctype="multipart/form-data" onsubmit="return check()">
            <input type="file" name="file" id="file" multiple="multiple"><br>
            <input type="submit" value="上傳">
        </form>
    </div>
</body>
</html>
複製程式碼

在做限制檔案上傳的大小時,注意不要在 SpringMVC 中直接限制,尤其是大檔案(2M以上的),否則在上傳時 Tomcat 會關閉接收流,瀏覽器會失去響應。這個地方困擾的不止我一個人,這個 BUG 和 SpringMVC 無關,和 Tomcat 的一個屬性有關係,請看下圖,網上有人說 Tomcat7 就沒有這個問題,但這不是推薦的解決問題方式。

Tomcat檔案上傳大小限制.jpg

經過一些研究,我的方案是用攔截器來做檔案上傳的大小限制。當攔截器攔截檔案超過設定的值時就丟擲異常,在 Controller 中處理異常,這裡要在配置中延遲異常的解析時間。在攔截器的配置中,對攔截器的屬性做限制,在攔截器中獲取這個配置值,不要在攔截器中直接寫死。Controller 中捕獲這個異常,提示上傳檔案超過了限制。

SpringMVC 中的配置:

<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    <!--設定請求編碼-->
    <property name="defaultEncoding" value="UTF-8"/>
    <property name="uploadTempDir" value="WEB-INF/tmp"/>
    <!--設定允許單個上傳檔案的最大值,不要在這裡配置-->
    <!--<property name="maxUploadSizePerFile" value="31457280"/>-->
    <!--延遲解析,在Controller中丟擲異常-->
    <property name="resolveLazily" value="true"/>
</bean>
<mvc:interceptors>
    <mvc:interceptor>
        <mvc:mapping path="/*upload*"/>
        <bean class="com.wenshixin.interceptor.FileUploadInterceptor">
            <property name="maxSize" value="31457280"/>
        </bean>
    </mvc:interceptor>
</mvc:interceptors>
複製程式碼

攔截器:

public class FileUploadInterceptor implements HandlerInterceptor {
    private long maxSize;

    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
        if (httpServletRequest != null && ServletFileUpload.isMultipartContent(httpServletRequest)) {
            ServletRequestContext servletRequestContext = new ServletRequestContext(httpServletRequest);
            long requestSize = servletRequestContext.contentLength();
            if (requestSize > maxSize) {
                // 丟擲異常
                throw new MaxUploadSizeExceededException(maxSize);
            }
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {

    }

    public void setMaxSize(long maxSize) {
        this.maxSize = maxSize;
    }
}
複製程式碼

Controller 中的異常處理方法:

@ExceptionHandler(MaxUploadSizeExceededException.class)
public String handException(MaxUploadSizeExceededException e, HttpServletRequest request) {
    request.setAttribute("msg", "檔案超過了指定大小,上傳失敗!");
    return "fileupload";
}
複製程式碼

SpringMVC 中使用 MultipartFile 物件來接收上傳的檔案,通過這個物件可以得到檔案的檔名和檔案型別,通過 transferTo() 方法將檔案寫入到磁碟上。檔案上傳時,給檔案重新命名來防止上傳檔案重名產生覆蓋,我這裡採取是 UUID值 + 檔名,中間用下劃線隔開。

Controller 中的檔案上傳方法:

@PostMapping(value = "/fileupload")
public String fileUpload(@RequestParam(value = "file") List<MultipartFile> files, HttpServletRequest request) {
    String msg = "";
    // 判斷檔案是否上傳
    if (!files.isEmpty()) {
        // 設定上傳檔案的儲存目錄
        String basePath = request.getServletContext().getRealPath("/upload/");
        // 判斷檔案目錄是否存在
        File uploadFile = new File(basePath);
        if (!uploadFile.exists()) {
            uploadFile.mkdirs();
        }
        for (MultipartFile file : files) {
            String originalFilename = file.getOriginalFilename();
            if (originalFilename != null && !originalFilename.equals("")) {
                try {
                    // 對檔名做加UUID值處理
                    originalFilename = UUID.randomUUID() + "_" + originalFilename;
                    file.transferTo(new File(basePath + originalFilename));
                } catch (IOException e) {
                    e.printStackTrace();
                    msg = "檔案上傳失敗!";
                }
            } else {
                msg = "上傳的檔案為空!";
            }
        }
        msg = "檔案上傳成功!";
    } else {
        msg = "沒有檔案被上傳!";
    }
    request.setAttribute("msg", msg);
    return "fileupload";
}
複製程式碼

檔案上傳的效果圖:

檔案下載效果圖.gif

2.2 檔案下載

下載頁面我使用了 Jquery 動態生成下載列表對 url 提前做了編碼處理,防止檔名中 # 號等特殊字元的干擾,並對顯示的檔名做了去除 UUID 值的處理,對 IE 瀏覽器也做了特殊的中文處理。

下載頁面:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html lang="zh-cn">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>檔案上傳與下載</title>
    <script src="${pageContext.request.contextPath}/js/jquery-1.12.4.min.js"></script>
    <script>
        $(function(){
            var targer = $("#main")
            $.ajax({
                url: "fileList",
                dataType: "json",
                success: function (data) {
                    data = JSON.parse(data)
                    for (var i in data) {
                        var a = $("<a></a><br>").text(data[i].substring(data[i].indexOf("_")+1))
                        a.attr("href", "${pageContext.request.contextPath}/download?filename="+encodeURIComponent(data[i]))
                        targer.append(a)
                    }
                }
            })
        })
    </script>
</head>
<body>
    <div id="main" style="width:500px; margin: 0 auto;">
    </div>
</body>
</html>
複製程式碼

Controller 中的下載方法:

@RequestMapping(value = "/download")
public ResponseEntity<byte[]> fileDownload(String filename, HttpServletRequest request) throws IOException {
    String path = request.getServletContext().getRealPath("/upload/");
    File file = new File(path + filename);
    //        System.out.println("轉碼前" + filename);
    filename = this.getFilename(request, filename);
    //        System.out.println("轉碼後" + filename);
    // 設定響應頭通知瀏覽器下載
    HttpHeaders headers = new HttpHeaders();
    // 將對檔案做的特殊處理還原
    filename = filename.substring(filename.indexOf("_") + 1);
    headers.setContentDispositionFormData("attachment", filename);
    headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
    return new ResponseEntity<byte[]>(FileUtils.readFileToByteArray(file), headers, HttpStatus.OK);
}

// 根據不同的瀏覽器進行編碼設定,返回編碼後的檔名
public String getFilename(HttpServletRequest request, String filename) throws UnsupportedEncodingException {
    String[] IEBrowerKeyWords = {"MSIE", "Trident", "Edge"};
    String userAgent = request.getHeader("User-Agent");
    for (String keyword : IEBrowerKeyWords) {
        if (userAgent.contains(keyword)) {
            return URLEncoder.encode(filename, "UTF-8");
        }
    }
    return new String(filename.getBytes("UTF-8"), "ISO-8859-1");
}
複製程式碼

下載檔案的效果圖(谷歌、火狐、IE、360瀏覽器):

檔案上傳效果圖.gif

檔案上傳下載是 Web 開發中很常見的功能,但是要想做好也並不容易,瀏覽器的相容性要考慮,如果追求使用者體驗,還可以在上傳檔案時給出進度條、AJAX實現頁面無重新整理上傳,深感自己的前端水平還需要提高 ?,不說了學習去了。

歡迎關注下方的微信公眾號哦,裡面有各種學習資料免費分享哦!

程式設計心路

相關文章