1. Java SE 檔案上傳和檔案下載的底層原理
@
- 1. Java SE 檔案上傳和檔案下載的底層原理
- 2. 檔案上傳
- 2.1 檔案上傳應用例項
- 2.2 檔案上傳注意事項和細節
- 3. 檔案下載
- 3.1 檔案下載應用例項
- 3.2 檔案下載注意事項和細節
- 4. 總結:
- 5. 最後:
2. 檔案上傳
- 檔案的上傳和下載,是常見的功能。說明:這裡我們的檔案上傳僅僅只是對於小檔案上的上傳 。如果是傳輸大檔案,一般用專門工具或者外掛。
檔案上傳下載需要使用到如下兩個包,需要匯入。同時記得要進行匯入載入到專案當中去。
檔案上傳原理示意圖:
檔案上傳的解讀:
- 檔案上傳還是使用的是表單 的方式提交
- 其中
action
還是按照以前規定來指定method
指定為 post ,因為檔案上傳是比較大的檔案, get 無法傳送較大的檔案。enctype:encodetype
編碼型別,要設定為:multipart/form-data
,表示進行二進位制檔案的提交,multipart/form-data: 表示表單提交的資料是有多個部分組成,也就是可以提交二進位制資料和文字資料,兩者都行。
- 注意:
enctype:encodetype
預設是:enctype="application/x-www-form-urlencoded"
即為 URL 編碼,這種編碼方式不適合對二進位制檔案資料的提交,一般適用於文字資料的提交。
操作上傳檔案流程:
- 判斷是不是一個檔案表單
- 判斷表單提交的各個表單項是什麼型別
- 如果是一個普通的表單項,就按照文字的方式來處理。
- 如果是一個檔案表單項(二進位制資料),使用 IO技術進行處理。
- 把表單提交的檔案資料,儲存到你指定的服務端的某個目錄。
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 檔案上傳注意事項和細節
-
如果將檔案都上傳到一個目錄下,當上傳檔案很多時,會造成訪問檔案速度變慢,因此 可以將檔案上傳到不同目錄 比如 一天上傳的檔案,統一放到一個資料夾 年月日, 比如:2024-7-1 ,21001010 資料夾 。
-
一個完美的檔案上傳,要考慮的因素很多,比如斷點續傳、控制圖片大小,尺寸,分片 上 傳 , 防 止 惡 意 上 傳 等 , 在 項 目 中 , 可 以 考 慮 使 用 WebUploader 組 件 ( 百 度 開 發 ) http://fex.baidu.com/webuploader/doc/index.html
-
檔案上傳功能,在專案中建議有限制的使用,一般用在頭像、證明、合同、產品展示等, 如果不加限制,會造成伺服器空間被大量佔用 [比如 b 站評論,就不能傳圖片,微信發 1 次朋友圈最多 9 張圖等..]
-
檔案上傳,建立 web/upload 的資料夾,在 tomcat 啟動時,沒有在 out 目錄下 建立 對 應的 upload 資料夾, 原因是 tomcat 對應空目錄是不會在 out 下建立相應目錄的,所以,只 需在 upload 目錄下,放一個檔案即可, 這個是 Idea + Tomcat 的問題, 實際開發不會存 在
3. 檔案下載
檔案下載的原理分析圖:
檔案下載響應頭說明:
- Content-Disposition: 表示下載的資料的展示方式,比如是內聯形式(網頁形式或者網頁一部分)或者是檔案下載方式 attachment
- Content-Type: 指定返回資料的型別 MIME ————》http 協議的內容
檔案下載響應體說明:
- 在網路傳輸時是圖片的原生資料(按照瀏覽器下載的編碼)
- 這個圖片時下載後檢視到的,也就是瀏覽器本身做了解析
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 檔案下載注意事項和細節
-
檔案下載,比較麻煩的就是不同瀏覽器檔名中文處理,因此,在程式碼中,需要針對不同的瀏覽器做處理。
-
對於網站的檔案,很多檔案使用另存為即可下載,對於大檔案(文件,影片),會使用專 業的下載工具(迅雷、百度,騰訊,華為網盤等)
-
對於不同的瀏覽器, 在把檔案下載完畢後,處理的方式不一樣, 有些是直接開啟檔案,有些是將檔案下載到 本地/下載目錄。
4. 總結:
檔案上傳的表單上的屬性上的處理:
method
指定為 post ,因為檔案上傳是比較大的檔案, get 無法傳送較大的檔案。enctype:encodetype
編碼型別,要設定為:multipart/form-data
,表示進行二進位制檔案的提交,multipart/form-data: 表示表單提交的資料是有多個部分組成,也就是可以提交二進位制資料和文字資料,兩者都行。有時-》上傳失敗了,可能是目錄的問題 ,加上 “/”
為了防止大量的目錄建立,可以增加日期時間進行建立多個目錄,這樣以日期天數進行建立目錄的話,一年最多也就是 365個目錄而已。
文字被替換覆蓋的問題,我們也一個工具類,讓檔名不重複
// UUID.randomUUID().toString() 雜湊不重複值
// System.currentTimeMillis() 獲取噹噹前系統時間毫秒級別的
// 對上傳的檔名進行處理,前面增加一個字首,保證是唯一即可
// 同時使用特定的 "_" 符號進行分割,用於後續可能需要拿到檔名,最方便使用name = UUID.randomUUID().toString() + "_" + System.currentTimeMillis() + "_" + name; String fileFullPath = fileRealPathDirectory + "/" + name;
檔案上傳功能,在專案中建議有限制的使用,一般用在頭像、證明、合同、產品展示等, 如果不加限制,會造成伺服器空間被大量佔用 [比如 b 站評論,就不能傳圖片,微信發 1 次朋友圈最多 9 張圖等..]
檔案下載:一個小細節:如果 web目錄下建立的 目錄是一個空資料夾/空目錄,就是目錄下沒有東西的話,就算重啟了 Tomcat 伺服器也是不會新增到 out 目錄下的。所以,只 需在 upload 目錄下,放一個檔案即可, 這個是 Idea + Tomcat 的問題, 實際開發不會存在。
檔案下載,比較麻煩的就是不同瀏覽器檔名中文處理,因此,在程式碼中,需要針對不同的瀏覽器做處理。這裡:火狐的 是檔名中文需要 base64 編碼,而 ie/chrome 是 URL編碼。針對不同的瀏覽器,我們需要進行不同的編碼處理。
- 關於檔案上傳和下載,這裡使用的是原生API的方式,在實際的開發中,我們這些關於檔案上傳和下載,都是被框架封裝好了的,比如 :Spring MVC,Spring Boot 等等,我們只需要呼叫對應的API即可,框架封裝的太好了,我們很難了解其中的底層原理。這裡的檔案上傳和下載就是其底層原理了。
5. 最後:
限於自身水平,其中存在的錯誤,希望大家給予指教,韓信點兵——多多益善,謝謝大家,後會有期,江湖再見 !!!