Java SE 檔案上傳和檔案下載的底層原理

Rainbow-Sea發表於2024-07-27

1. Java SE 檔案上傳和檔案下載的底層原理

@

目錄
  • 1. Java SE 檔案上傳和檔案下載的底層原理
  • 2. 檔案上傳
    • 2.1 檔案上傳應用例項
    • 2.2 檔案上傳注意事項和細節
  • 3. 檔案下載
    • 3.1 檔案下載應用例項
    • 3.2 檔案下載注意事項和細節
  • 4. 總結:
  • 5. 最後:


2. 檔案上傳

  1. 檔案的上傳和下載,是常見的功能。說明:這裡我們的檔案上傳僅僅只是對於小檔案上的上傳 。如果是傳輸大檔案,一般用專門工具或者外掛。

檔案上傳下載需要使用到如下兩個包,需要匯入。同時記得要進行匯入載入到專案當中去。

在這裡插入圖片描述

在這裡插入圖片描述

檔案上傳原理示意圖:

在這裡插入圖片描述

在這裡插入圖片描述

檔案上傳的解讀:

  1. 檔案上傳還是使用的是表單 的方式提交
  2. 其中 action 還是按照以前規定來指定
  3. method 指定為 post ,因為檔案上傳是比較大的檔案, get 無法傳送較大的檔案。
  4. enctype:encodetype 編碼型別,要設定為:multipart/form-data ,表示進行二進位制檔案的提交,multipart/form-data: 表示表單提交的資料是有多個部分組成,也就是可以提交二進位制資料和文字資料,兩者都行。在這裡插入圖片描述
  1. 注意:enctype:encodetype 預設是:enctype="application/x-www-form-urlencoded" 即為 URL 編碼,這種編碼方式不適合對二進位制檔案資料的提交,一般適用於文字資料的提交。

操作上傳檔案流程:

  1. 判斷是不是一個檔案表單
  2. 判斷表單提交的各個表單項是什麼型別
  3. 如果是一個普通的表單項,就按照文字的方式來處理。
  4. 如果是一個檔案表單項(二進位制資料),使用 IO技術進行處理。
  5. 把表單提交的檔案資料,儲存到你指定的服務端的某個目錄。

2.1 檔案上傳應用例項

在這裡插入圖片描述

對應檔案上傳的前端頁面程式碼 : jsp

在這裡插入圖片描述

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <base href="<%=request.getContextPath()+"/"%>>">
    <style type="text/css">
        input[type="submit"] {
            outline: none;
            border-radius: 5px;
            cursor: pointer;
            background-color: #31B0D5;
            border: none;
            width: 70px;
            height: 35px;
            font-size: 20px;
        }

        img {
            border-radius: 50%;
        }

        form {
            position: relative;
            width: 200px;
            height: 200px;
        }

        input[type="file"] {
            position: absolute;
            left: 0;
            top: 0;
            height: 200px;
            opacity: 0;
            cursor: pointer;
        }
    </style>
    <script type="text/javascript">
        function prev(event) {
//獲取展示圖片的區域
            var img = document.getElementById("prevView");
//獲取檔案物件
            let file = event.files[0];
//獲取檔案閱讀器
            let reader = new FileReader();
            reader.readAsDataURL(file);
            reader.onload = function () {
//給 img 的 src 設定圖片 url
                img.setAttribute("src", this.result);
            }
        }
    </script>
</head>
<body>
<!-- 表單的 enctype 屬性要設定為 multipart/form-data -->
<form action="fileUploadServlet" method="post" enctype="multipart/form-data">
    家居圖: <img src="2.jpg" alt="" width="200" height="200" id="prevView"> <input type="file" name="pic" id=""
                                                                                value="2xxx.jpg" onchange="prev(this)"/>
    家居名: <input type="text" name="name"><br/> <input type="submit" value="上傳"/>
</form>
</body>
</html>

對應檔案上傳的Servlet 的編寫。

在這裡插入圖片描述

package com.rainbowsea.servlet;


import com.rainbowsea.utils.WebUtils;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;


import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;

import java.nio.file.attribute.FileTime;
import java.util.List;
import java.util.UUID;

public class FileUploadServlet extends HttpServlet {

    /*
    1. 判斷是不是一個檔案傳單
    2. 判斷表單提交的各個表單項是什麼型別
    3. 如果是一個普通的表單項,就按照文字的方式來處理
    4. 如果是一個檔案表單項(二進位制資料),使用 IO技術進行處理
    5. 把表單提交的檔案資料,儲存到你指定的服務端的某個目錄
     */
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException,
            IOException {
        //System.out.println("被呼叫了");


        // 1. 判斷是不是 檔案表單(enctype="multipart/form-data")
        if (ServletFileUpload.isMultipartContent(request)) {
            //System.out.println("OK");
            // 2. 建立 DiskFileItemFactory 物件,用於構建一個解析上傳資料的工具物件
            DiskFileItemFactory diskFileItemFactory = new DiskFileItemFactory();
            // 3. 建立一個解析上傳資料的工具物件
            ServletFileUpload servletFileUpload = new ServletFileUpload(diskFileItemFactory);

            // 4. 關鍵的地方 servletFileUpload 物件可以把表單提交的資料 text/ 檔案
            // 將其封裝到 FileItem 檔案項中
            // 韓老師的程式設計心得體會:如果我們不知道一個物件是什麼結構
            // 可以:1.輸出該物件,2 debug 測試
            try {
                List<FileItem> list = servletFileUpload.parseRequest(request);
                //System.out.println("List ==>" + list);
                // 遍歷,並分別處理=> 自然思路
                for (FileItem fileItem : list) {
                    // java.lang.ClassCastException: org.apache.commons.fileupload.disk.DiskFileItem cannot be cast to java.nio.file.attribute.FileTime
                    //System.out.println("fileItem == >" + fileItem);
                    if (fileItem.isFormField()) { // 如果是 true 就是文字 input text
                        String name = fileItem.getString("utf-8");
                        System.out.println("圖片名稱: " + name);

                    } else { // 是一個檔案
                        // 獲取上傳的檔案的名字:
                        String name = fileItem.getName();
                        System.out.println("上傳的檔名: " + name);

                        // 把這個上傳到伺服器的 temp 下的檔案儲存到你指定的目錄
                        // 1. 指定一個目錄,就是我們網站工作目錄下
                        String filePath = "/upload/";

                        // 2. 獲取到完整目錄[io/servlet基礎]
                        String fileRealPath = request.getServletContext().getRealPath(filePath);

                        System.out.println("fileRealpath = " + fileRealPath);

                        // 3. 建立這個上傳的目錄=> 建立目錄 => Java物件
                        // 為了防止大量的目錄建立,可以更加日期時間進行建立多個目錄
                        File fileRealPathDirectory = new File(fileRealPath + WebUtils.getYearMonthDay());
                        if (!fileRealPathDirectory.exists()) {  // 不存在建立
                            fileRealPathDirectory.mkdirs(); // 建立

                        }

                        // 解決接收到檔名是中文亂碼問題
                        servletFileUpload.setHeaderEncoding("utf-8");
                        // 4. 將檔案複製到 fileRealPathDirectory 目錄
                        // 構建一個上傳檔案的完整路徑:目錄 + 檔名
                        // 有時-》上傳失敗了,可能是目錄的問題 ,加上 “/”
                        // 文字被替換覆蓋的問題,我們也一個工具類,讓檔名不重複
                        // 對上傳的檔名進行處理,前面增加一個字首,保證是唯一即可
                        name = UUID.randomUUID().toString() + "_" + System.currentTimeMillis() + "_" + name;
                        String fileFullPath = fileRealPathDirectory + "/" + name;
                        fileItem.write(new File(fileFullPath));

                        // 提示資訊
                        response.setContentType("text/html;charset=utf-8");
                        response.getWriter().write("上傳成功");

                    }
                }
            } catch (FileUploadException e) {
                throw new RuntimeException(e);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        } else {
            System.out.println("不是檔案表單...");
        }
    }

}

上傳檔案操作的 Servlet 補充說明講解:

有時-》上傳失敗了,可能是目錄的問題 ,加上 “/”

為了防止大量的目錄建立,可以增加日期時間進行建立多個目錄,這樣以日期天數進行建立目錄的話,一年最多也就是 365個目錄而已。

在這裡插入圖片描述

File fileRealPathDirectory = new File(fileRealPath+ WebUtils.getYearMonthDay());
String fileFullPath =  fileRealPathDirectory +"/"+ WebUtils.getYearMonthDay();

    public static String  getYearMonthDay() {
        // 如何得到當前的日期-》Java基礎 日期,三代類
        LocalDateTime localDateTime = LocalDateTime.now();
        int year = localDateTime.getYear();
        int monthValue = localDateTime.getMonthValue();
        int dayOfMonth = localDateTime.getDayOfMonth();
        String yearMonthDay = year + "-" + monthValue + "-" + dayOfMonth;

        return yearMonthDay;
    }

文字被替換覆蓋的問題,我們也一個工具類,讓檔名不重複

// UUID.randomUUID().toString() 雜湊不重複值
// System.currentTimeMillis() 獲取噹噹前系統時間毫秒級別的
// 對上傳的檔名進行處理,前面增加一個字首,保證是唯一即可
// 同時使用特定的 "_" 符號進行分割,用於後續可能需要拿到檔名,最方便使用

name = UUID.randomUUID().toString() + "_" + System.currentTimeMillis() + "_" + name;
String fileFullPath = fileRealPathDirectory + "/" + name;

在這裡插入圖片描述

執行測試:看看檔案是否能夠上傳成功

在這裡插入圖片描述

在這裡插入圖片描述

在這裡插入圖片描述

2.2 檔案上傳注意事項和細節

  1. 如果將檔案都上傳到一個目錄下,當上傳檔案很多時,會造成訪問檔案速度變慢,因此 可以將檔案上傳到不同目錄 比如 一天上傳的檔案,統一放到一個資料夾 年月日, 比如:2024-7-1 ,21001010 資料夾 。

  2. 一個完美的檔案上傳,要考慮的因素很多,比如斷點續傳、控制圖片大小,尺寸,分片 上 傳 , 防 止 惡 意 上 傳 等 , 在 項 目 中 , 可 以 考 慮 使 用 WebUploader 組 件 ( 百 度 開 發 ) http://fex.baidu.com/webuploader/doc/index.html 在這裡插入圖片描述

  3. 檔案上傳功能,在專案中建議有限制的使用,一般用在頭像、證明、合同、產品展示等, 如果不加限制,會造成伺服器空間被大量佔用 [比如 b 站評論,就不能傳圖片,微信發 1 次朋友圈最多 9 張圖等..]

  4. 檔案上傳,建立 web/upload 的資料夾,在 tomcat 啟動時,沒有在 out 目錄下 建立 對 應的 upload 資料夾, 原因是 tomcat 對應空目錄是不會在 out 下建立相應目錄的,所以,只 需在 upload 目錄下,放一個檔案即可, 這個是 Idea + Tomcat 的問題, 實際開發不會存 在

3. 檔案下載

檔案下載的原理分析圖:

在這裡插入圖片描述

檔案下載響應頭說明:

  1. Content-Disposition: 表示下載的資料的展示方式,比如是內聯形式(網頁形式或者網頁一部分)或者是檔案下載方式 attachment
  2. Content-Type: 指定返回資料的型別 MIME ————》http 協議的內容

檔案下載響應體說明:

  1. 在網路傳輸時是圖片的原生資料(按照瀏覽器下載的編碼)
  2. 這個圖片時下載後檢視到的,也就是瀏覽器本身做了解析

在這裡插入圖片描述

3.1 檔案下載應用例項

對應檔案上傳的前端頁面程式碼 : jsp

在這裡插入圖片描述

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>檔案下載</title>
    <base href="<%=request.getContextPath()+"/"%>>">
</head>
<body>
<h1>檔案下載</h1>
<a href="fileDownLoadServlet?name=java.png">點選下載Java圖片</a><br/><br/>
<a href="fileDownLoadServlet?name=13-第十二章網路程式設計.pptx">點選下載 13-第十二章 網路程式設計.pptx</a><br/><br/>
</body>
</html>

注意:我們下載是,客戶端從伺服器端下載內容的了,所以我們需要模擬伺服器,在伺服器上新增上,我們客戶端可以下載到的內容檔案(這裡:我們在 web 目錄下,建立一個 download 目錄,用於存放我們客戶端(瀏覽器)可以下載到的檔案)。

在這裡插入圖片描述

注意:建立好目錄,新增好檔案之後,要重新啟動一下 Tomcat 伺服器,讓 這個我們新增的 download 資源目錄,新增到 out 工作目錄當中去。在這裡插入圖片描述

如果你重啟了 Tomcat 伺服器,也沒有看到你建立的 download在工作目錄 out下,則點選 rebuild project -> restart project

在這裡插入圖片描述

一個小細節:如果 web目錄下建立的 目錄是一個空資料夾/空目錄,就是目錄下沒有東西的話,就算重啟了 Tomcat 伺服器也是不會新增到 out 目錄下的。所以,只 需在 upload 目錄下,放一個檔案即可, 這個是 Idea + Tomcat 的問題, 實際開發不會存在

對應檔案下載的Servlet 的編寫。

在這裡插入圖片描述

package com.rainbowsea.servlet;

import org.apache.commons.io.IOUtils;
import sun.misc.BASE64Encoder;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;

public class FileDownLoadServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        System.out.println("被呼叫");

        // 1. 先準備要下載的檔案(假定這些檔案時公共的資源)
        // 重要: 保證當我們的 tomcat 啟動後,在工作目錄下 有 out 有 download 資料夾,並且
        // 有可供下載的檔案!!
        // 再次說明:如果你沒有看到你建立的 download在工作目錄 out下 rebuild project -> restart proj

        // 2. 獲取到要下載的檔案的名字
        request.setCharacterEncoding("utf-8");
        String downLoadFileName = request.getParameter("name");
        System.out.println("downLoadFileName = " + downLoadFileName);

        // 3. 給 http 響應,設定響應頭 Content-Type,就是檔案的MIME
        // 透過 servletContext 來獲取
        ServletContext servletContext = request.getServletContext();
        String downLoadPath = "/download/";  // 伺服器資源圖片,存放路徑
        String downLoadFileFullPath = downLoadPath + downLoadFileName;
        String mimeType = servletContext.getMimeType(downLoadFileFullPath);
        System.out.println("mimeType = " + mimeType);
        response.setContentType(mimeType);

        // 4. 給http響應,設定響應頭Content-Dispostion
        // 這裡考慮的細節比較多,比如不同的瀏覽器寫法不一樣,考慮編碼
        // ff 是檔名中文需要 base64, 而 ie/chrome 是 URL編碼
        // 這裡我們不需要同學門機制,只需知道原理
        if(request.getHeader("User-Agent").contains("Firefox")) {
            // 火狐瀏覽器的設定 為 Base64編碼
            response.setHeader("Content-Disposition","attachment; filename==?UTF-8?B?" +
                    new BASE64Encoder().encode(downLoadFileName.getBytes("UTF-8")));
        } else {
            // 其他(主流ie/chrome) 使用 URL編碼操作
            response.setHeader("Content-Disposition","attachment; filename=" +
                    URLEncoder.encode(downLoadFileName,"UTF-8"));
        }

        // 5. 讀取下面的檔案資料,返回給客戶端
        // (1)建立一個和要下載的檔案,關聯的輸入流
        InputStream resourceAsStream = servletContext.getResourceAsStream(downLoadFileFullPath);

        // (2) 得到返回資料的輸出流{因為返回檔案大多數是二進位制(位元組),IO Java基礎}
        ServletOutputStream outputStream = response.getOutputStream();

        // (3) 使用工具類,將輸入流關聯的檔案,對拷到輸出流,並返回給客戶端/瀏覽器
        // 注意是: import org.apache.commons.io.IOUtils; 包下的
        IOUtils.copy(resourceAsStream,outputStream);

    }
}

上傳下載操作的 Servlet 補充說明講解:

檔案下載,比較麻煩的就是不同瀏覽器檔名中文處理,因此,在程式碼中,需要針對不同的瀏覽器做處理。這裡:火狐的 是檔名中文需要 base64 編碼,而 ie/chrome 是 URL編碼。針對不同的瀏覽器,我們需要進行不同的編碼處理。

在這裡插入圖片描述

// 4. 給http響應,設定響應頭Content-Dispostion
        // 這裡考慮的細節比較多,比如不同的瀏覽器寫法不一樣,考慮編碼
        // ff 是檔名中文需要 base64, 而 ie/chrome 是 URL編碼
        // 這裡我們不需要同學門機制,只需知道原理
        if(request.getHeader("User-Agent").contains("Firefox")) {
            // 火狐瀏覽器的設定 為 Base64編碼
            response.setHeader("Content-Disposition","attachment; filename==?UTF-8?B?" +
                    new BASE64Encoder().encode(downLoadFileName.getBytes("UTF-8")));
        } else {
            // 其他(主流ie/chrome) 使用 URL編碼操作
            response.setHeader("Content-Disposition","attachment; filename=" +
                    URLEncoder.encode(downLoadFileName,"UTF-8"));
        }

執行測試:

在這裡插入圖片描述

在這裡插入圖片描述

3.2 檔案下載注意事項和細節

  1. 檔案下載,比較麻煩的就是不同瀏覽器檔名中文處理,因此,在程式碼中,需要針對不同的瀏覽器做處理。在這裡插入圖片描述

  2. 對於網站的檔案,很多檔案使用另存為即可下載,對於大檔案(文件,影片),會使用專 業的下載工具(迅雷、百度,騰訊,華為網盤等)

  3. 對於不同的瀏覽器, 在把檔案下載完畢後,處理的方式不一樣, 有些是直接開啟檔案,有些是將檔案下載到 本地/下載目錄。

4. 總結:

  1. 檔案上傳的表單上的屬性上的處理:method 指定為 post ,因為檔案上傳是比較大的檔案, get 無法傳送較大的檔案。enctype:encodetype 編碼型別,要設定為:multipart/form-data ,表示進行二進位制檔案的提交,multipart/form-data: 表示表單提交的資料是有多個部分組成,也就是可以提交二進位制資料和文字資料,兩者都行。

  2. 有時-》上傳失敗了,可能是目錄的問題 ,加上 “/”

  3. 為了防止大量的目錄建立,可以增加日期時間進行建立多個目錄,這樣以日期天數進行建立目錄的話,一年最多也就是 365個目錄而已。

  4. 文字被替換覆蓋的問題,我們也一個工具類,讓檔名不重複

    // UUID.randomUUID().toString() 雜湊不重複值
    // System.currentTimeMillis() 獲取噹噹前系統時間毫秒級別的
    // 對上傳的檔名進行處理,前面增加一個字首,保證是唯一即可
    // 同時使用特定的 "_" 符號進行分割,用於後續可能需要拿到檔名,最方便使用

    name = UUID.randomUUID().toString() + "_" + System.currentTimeMillis() + "_" + name;
    String fileFullPath = fileRealPathDirectory + "/" + name;
    
    
  5. 檔案上傳功能,在專案中建議有限制的使用,一般用在頭像、證明、合同、產品展示等, 如果不加限制,會造成伺服器空間被大量佔用 [比如 b 站評論,就不能傳圖片,微信發 1 次朋友圈最多 9 張圖等..]

  6. 檔案下載:一個小細節:如果 web目錄下建立的 目錄是一個空資料夾/空目錄,就是目錄下沒有東西的話,就算重啟了 Tomcat 伺服器也是不會新增到 out 目錄下的。所以,只 需在 upload 目錄下,放一個檔案即可, 這個是 Idea + Tomcat 的問題, 實際開發不會存在。

  7. 檔案下載,比較麻煩的就是不同瀏覽器檔名中文處理,因此,在程式碼中,需要針對不同的瀏覽器做處理。這裡:火狐的 是檔名中文需要 base64 編碼,而 ie/chrome 是 URL編碼。針對不同的瀏覽器,我們需要進行不同的編碼處理。

在這裡插入圖片描述

  1. 關於檔案上傳和下載,這裡使用的是原生API的方式,在實際的開發中,我們這些關於檔案上傳和下載,都是被框架封裝好了的,比如 :Spring MVC,Spring Boot 等等,我們只需要呼叫對應的API即可,框架封裝的太好了,我們很難了解其中的底層原理。這裡的檔案上傳和下載就是其底層原理了。

5. 最後:

限於自身水平,其中存在的錯誤,希望大家給予指教,韓信點兵——多多益善,謝謝大家,後會有期,江湖再見 !!!

在這裡插入圖片描述

相關文章