超详细的实现上传文件功能教程,文件上传实现。
重要聲明:本文章僅僅代表了作者個人對此觀點的理解和表述。讀者請查閱時持自己的意見進行討論。
本文更新不及時,請到原文地址瀏覽:《超詳細的實現上傳文件功能教程,文件上傳實現。》。
一、文件上傳的方式
在程序的世界里,沒有什么功能的實現方式是單一的。上傳文件也不例外,我們有很多種能夠實現文件上傳的方法。但我們最終要采用的,必然是最熟悉、最常用的方法。文件上傳通常有下面的方法進行:
這些文件上傳方式,都是基于HTTP協議的基礎,它并沒有強制限定了我們要如何去編碼或處理數據,相反協議的存在使世界上所有的程序差異化不是那么太大。HTTP協議規定了,提交大量數據時,可以使用POST請求來進行提交。是的,大多數后臺程序都只會在POST請求下去獲取大量上傳的數據,而不會在GET請求下去獲取。你要知道,如果你不規范提交,當然也是可以不規范的從GET下獲取到提交的數據。但極其不建議這樣做。
如果你對HTTP協議還不是很了解,你可以通過這篇文章來入門對HTTP協議的了解:《【HTTP,Java】認識HTTP協議,在互聯網世界里翱翔》。
二、base64 上傳文件
1、HTML網頁內使用base64
如果你要在網頁中使用base64的方式將文件提交給后臺,那么你不得不面臨一個很嚴峻的問題就是:如何在網頁中獎文件內容讀取并轉換成base64的數據。很幸運HTML5為我們提供了解決方案。
FileReader提供了讀取文件的能力。但它可不是你想象的提供一個文件路徑就可以讀取了。它能讀取的文件需是來自下面的途徑:
下面以input選擇的文件為例進行講解。
a、選擇文件并轉換為base64
直接在html文件中寫一個input并將其type設定為file即可立即實現一個文件選擇功能。現在同時給出讀取文件的示列代碼:
<body><!-- 文件選擇和上傳按鈕 --><input type="file" id="fileSelecter"><button onclick="uploadFile()">上傳</button><!-- 上傳邏輯 --><script>function uploadFile() {// 找到文件文件選擇框var fileInput = document.querySelector("#fileSelecter");// 獲取選擇的文件// (因為input是支持選擇多個文件的,所以獲取文件通過files字段,如果單個文件也是在這個files列表里。)var file = fileInput.files.item(0);// 判斷一下if (file == null) {// 沒有選擇文件。就什么都不處理。return;}// 使用FileReader讀取文件。var fileReader = new FileReader();fileReader.addEventListener("error", function (ev) {// 文件讀取出錯時,執行此方法。// 通過 fileReader.error 可以獲取到錯誤信息。});fileReader.addEventListener("load", function (ev) {// 文件讀取成功后調用此方法。// 通過 fileReader.result 即可獲取到文件內容。});fileReader.addEventListener("loadstart", function (ev) {// 讀取開始時此方法被調用。});fileReader.addEventListener("loadend", function (ev) {// 文件讀取結束時執行此方法。// 無論讀取成功,還是讀取失敗。// 總之,在結束讀文件操作時,此方法都會調用。});fileReader.addEventListener("abort", function (ev) {// 文件讀取被中斷時,此方法調用。// 你可以通過 fileReader.abort() 方法隨時中斷文件的讀取。});fileReader.addEventListener("progress", function (ev) {// 讀取文件過程不是一次性讀完的,會進行多次讀取。// 沒讀取一次,本方法執行一次。});// 將文件內容讀取為 base64 內容。通過 fileReader.result 即可返回base64的數據內容。fileReader.readAsDataURL(file);}</script> </body>上述代碼中詳細注釋了使用FileReader如何讀取文件的使用方式。通過監聽load事件,即可獲取到文件讀取的結果數據。如果你將這個結果打印出來,你將看到類似下面的數據:
data:image/jpeg;base64,/9j/4AAQSkZJergxawffwetcwgIKFUEGNiwgefnwkef...KCAcHCg0如果你看到了類似的數據。那么你就算是成功一半了。
注意: 其中data:image/jpeg;base64,這一段屬于對base64數據體的描述。文件真實base64內容只有逗號后面部分。如果后端接口明確告知你只需要傳遞后面的數據,那么你還需要自行將前面的描述去除。
b、上傳結果到接口
現在建立請求將得到的結果提交到服務器即可。在load監聽事件里我們可以成功的獲取到base64的結果,因此只需要將提交數據代碼寫在監聽里即可。這里我直接使用JQuery的post方法進行數據提交。代碼如下:
fileReader.addEventListener("load", function (ev) {// 文件讀取成功后調用此方法。// 通過 fileReader.result 即可獲取到文件內容。var result = fileReader.result;$.post("https://www.microanswer.cn/test/uploadBase64", {base64Data: result}, function (response) {// 服務器響應了我們的上傳請求。}); });如果你對JQuery的post方法不是很熟悉,請參考:【JQuery】JQuery常用方法總結、大全。
2、Java程序內使用base64
在使用java作為后臺程序的時候,有時候也經常通過http向其他服務器上傳內容,那如果是使用base64方式,如何將數據以base64提交呢?其實非常簡單,和上一小節里提到的BASE64Decoder有點類似,Java里還有一個類BASE64Encoder,他可以方便的將文件轉為一個base64字符串,看下面的示列代碼:
// 將文件轉為base64字符串 public String file2Base64(String filePath) {// 建立文件對象File file = new File(filePath);BASE64Encoder base64Encoder = new BASE64Encoder();ByteArrayOutputStream out = null;FileInputStream fileInputStream = null;// 標準流讀取代碼模板。try {out = new ByteArrayOutputStream();fileInputStream = new FileInputStream(file);byte[] data = new byte[1024];int datasize = 0;while ((datasize = fileInputStream.read(data)) != -1) {out.write(data, 0, datasize);}out.flush();} finally {try {if (fileInputStream != null) {fileInputStream.close();}}catch (Exception ignore) {}}return base64Encoder.encode(out.toByteArray()); }有了base64字符串,就可以方便的進行上傳了。相信后臺程序一般都會有一個封裝好了的網絡請求工具類,下面示列一個典型的上傳代碼:
String base64 = file2Base64("D:/temp/test.doc"); HashMap<String, String> param = new HashMap<>(); param.put("base64Data", base64); String response = HttpUtil.postFormUrlEncode("https://www.microanswer.cn/test/uploadBase64", param);此處使用的HttpUtil工具你可以通過下面的maven依賴獲取:
<dependency><groupId>cn.microanswer</groupId><artifactId>HttpUtil</artifactId><version>1.0.1</version> </dependency>3、后臺接口
上述請求中,將數據以 base64Data 字段提交給了接口。現在只需要處理服務器響應后的事情了。先看看接口是如何獲取數據的把:
@RequestMapping("/uploadBase64") public Object uploadBase64Data(HttpServletRequest request) throws Exception {String base64Data = request.getParameter("base64Data");// 保存文件后綴名。String fileExtName = "";// 保存文件真實的base64數據String realBase64;// 判斷是否有前綴if (base64Data.startsWith("data:") && base64Data.contains("base64,")) {String[] typeAndData = base64Data.split(",");// 截取 data:image/jpeg;base64, 為=> jpeg;base64,String tempType = typeAndData[0].split("/")[1];// 拿到 ; 號前面的內容作為文件后綴。fileExtName = "." + tempType.substring(0, tempType.indexOf(";"));// 拿到真實base64數據。realBase64 = typeAndData[1];} else {// 沒有前綴,則認為所有字符串都是base64實際內容。realBase64 = base64Data;}// 對 base64 字符串進行原數據讀取byte[] bytes = new BASE64Decoder().decodeBuffer(realBase64);// 構建文件將數據保存File file = new File("D:/temp/" + UUID.randomUUID().toString() + fileExtName);FileOutputStream fo = new FileOutputStream(file);// 輸出文件內容fo.write(bytes);fo.flush();fo.close();// 返回成功JSONObject object = new JSONObject();object.put("code", 200);object.put("msg", "上傳成功");return object; }三、form表單上傳文件
對于前端來說,這種方式無疑是最簡單的上傳方式,因為甚至可以做到不寫一點JavaScript代碼就實現了。為了對前端上傳文件有更深入了解,使用form表單上傳時也有兩種上傳方案,一種是直接使用form節點的,另一種則是通過FromData對象來完成。
1、使用html的form節點
下面直接展示示列代碼:
<form method="post"action="https://www.microanswer.cn/test/uploadFile"enctype="multipart/form-data">請選擇文件:<input type="file" name="fileData"><br>請輸入文件描述:<input type="text" name="fileDescription"><br><input type="submit" value="提交"> </form>這樣,前端就實現了文件上傳了。提交給接口uploadFile要怎么去獲取里面的文件,前端不用管了。
2、使用 FormData 對象
當使用FormData完成文件上傳時,事情就變得稍微復雜一點了。但是有它的優勢。直接在頁面上使用form表單會造成頁面的跳轉,上傳成功后就不會再停留在當前頁面了。而使用 FormData 進行文件上傳則可以完成異步上傳,達到不跳頁面的效果。
首先要清除一點,FormData對象要提交數據時,其文件數據是來自于<input type="file">選擇的文件。加入現在你已經選好了文件并且拿到了file對象,那么下面示列一個如何將此數據上傳到服務端的代碼:
// 文件上傳方法。 callback 是上傳完成回調。 function uploadFile(file, callback) {var formData = new FormData();formData.append("fileData", file); // 添加文件到 formData。formData.append("fileDescription", "文件上傳描述信息。"); // 添加一個描述字段。// 進行上傳$.ajax({url: "https://www.microanswer.cn/test/uploadFile",type: 'post',data: formData,contentType: false, // 不指定contentType,這樣讓JQuery主動識別processData: false, // 不讓JQuery處理上傳的數據,dataType: 'json', // 預期返回數據格式。success: function (response) {callback(response);},error: function (xmlHttpRequest, statusStr, exception) {callback(undefined, exception);}}); }代碼中可以看到,首先使用 FormData 創建了一個實例,然后將文件和描述信息放入其中,最后通過 JQuery 的 ajax 方法將數據提交給后臺接口。
四、使用Java模擬表單
java里沒有類似formdata的上傳文件輔助類,我們需要根據表單在上傳文件時數據具體是如何進行提交的流程進行自己使用java來實現。擺在我們面前的首要任務就是要搞明白表單提交的數據結構,只有詳細了解了其結構,才能使用Java進行完美的模擬。
1、文件上傳的數據內容格式
通過表單上傳文件顯然是HTTP協議內的一部分內容,因此我們不妨直接翻開HTTP協議里針對表單上傳文件相關的定義文檔:RFC1867。下面是一些重點內容的截取部分(博主能力有限,翻譯得不好還請將就湊合):
multipart/form-data的定義
數據格式內容舉例
2、Java代碼實現表單
通過上述HTTP協議定義文檔中的描述,相信文件表單在數據提交過程中數據傳輸方式和格式已經有了大致的了解。現在是時候使用Java實現這樣一個功能了。
a、單個表單文件單元
根據定義,一個input可以支持選擇多個文件的,而在傳輸時,每個文件的傳輸需要提供一些基本信息。因此,直接為單個文件單元實現為一個類:
/*** InputFile.java* 用于放在表單 Input 里的文件單元。*/ public class InputFile {// 保存當前需要傳遞的文件引用。private File file;// 如果要傳遞的數據直接是二進制數據了,這里也提供一個變量,使支持二進制數據直接傳遞。private byte[] byteData;// 在不清楚上傳的數據是什么,但確切知道上傳的內容是一個inputstream里的內容。這里也提供一個變量,使支持輸入流直接傳遞。private InputStream inputStream;public InputFile(File file) { this.file = file; }// 對于數組,不要直接引用,而是取一份其拷貝的數據。public InputFile(byte[] byteData) {this.byteData = new byte[byteData.length];System.arraycopy(byteData, 0, this.byteData, 0, byteData.length);}public InputFile(InputStream inputStream) { this.inputStream = inputStream; }// 當進行提交時,使用此方法,此方法將會把當前單元持有的[文件\數據\流]進行上傳。public void submit(OutputStream outputStream) throws Exception{// 輸出是,首先建立基本信息輸出。StringBuilder baseInfo = new StringBuilder();if (file != null) {// 有文件,則輸出文件。baseInfo.append(" filename=\"").append(file.getName()).append("\"").append(Constant.LINE_SEPARATOR);baseInfo.append("Content-Type: ").append(new MimetypesFileTypeMap().getContentType(file)).append(Constant.LINE_SEPARATOR);baseInfo.append(Constant.LINE_SEPARATOR);inputStream = new FileInputStream(file);} else {// 輸出數據 或 流,但因為不知道名稱,因此填寫一個隨機名稱。baseInfo.append(" filename=\"").append(UUID.randomUUID().toString().replaceAll("-", "")).append("\"").append(Constant.LINE_SEPARATOR).append("Content-Type: application/octet-stream").append(Constant.LINE_SEPARATOR).append(Constant.LINE_SEPARATOR);}outputStream.write(baseInfo.toString().getBytes(Constant.charset));// 將具備的流信息輸出。if (inputStream != null) {byte[] datas = new byte[1024];int datasize = 0;while ((datasize = inputStream.read(datas))!= -1) {outputStream.write(datas, 0, datasize);}if (file != null) {inputStream.close();}} else if (byteData != null && byteData.length > 0) {// 將數據輸出。outputStream.write(byteData);}outputStream.write(Constant.LINE_SEPARATOR.getBytes(Constant.charset));} }b、Input單元
input單元里可以放置一個鍵值對,用來傳遞數據,同時,它還可以傳遞鍵和一系列文件,上一節的文件單元就可以放在這個Input單元中。則可以設計出 Input單元類如下:
/*** Input.java* 表單里面的一條input輸入單元。*/ public class Input {// 表單支持的類型枚舉public enum Type { text,file }// 此 input 要提交的name值。private String name;// 此 input 要提交的value值。當type為text時才會提交此數據。private String value;// 此 input 要提交的文件。當type為file時才會提交此數據。private ArrayList<InputFile> inputFiles;// 此表單的類型,目前只有:text 和 fileprivate Type type;// 允許直接使用鍵值對構造一個text的input。public Input(String name, String value) {this.name = name;this.value = value;this.type = Type.text;}// 允許使用name和文件列表構造一個file的input。public Input(String name, InputFile... inputFile) {this.name = name;this.inputFiles = new ArrayList<>();this.inputFiles.addAll(Arrays.asList(inputFile));this.type = Type.file;}// 在提交前,允許添加文件。public Input addFile(InputFile inputFile) {this.inputFiles.add(inputFile);return this;}// 在提交前,允許修改要提交的值。public Input setValue(String value) {this.value = value;return this;}// 將此input內的數據進行提交。public void submit(OutputStream outputStream) throws Exception {// 先構建基本信息輸出。StringBuilder baseInfo = new StringBuilder();baseInfo.append("content-disposition: form-data; name=\"").append(name).append("\";");if (type == Type.text) {baseInfo.append(Constant.LINE_SEPARATOR);baseInfo.append(Constant.LINE_SEPARATOR);baseInfo.append(value);baseInfo.append(Constant.LINE_SEPARATOR);String s = baseInfo.toString();outputStream.write(s.getBytes(Constant.charset));} else {if (inputFiles != null && inputFiles.size() > 0) {if (inputFiles.size() == 1) {outputStream.write(baseInfo.toString().getBytes(Constant.charset));inputFiles.get(0).submit(outputStream);} else {String boundary = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 6);// 此input有多個文件。先輸出內容體的基本信息。baseInfo.append(Constant.LINE_SEPARATOR);baseInfo.append("Content-Type: multipart/mixed, boundary=").append(boundary);baseInfo.append(Constant.LINE_SEPARATOR);baseInfo.append(Constant.LINE_SEPARATOR);// 循環輸出文件內容for (InputFile infile: inputFiles) {baseInfo.append("--").append(boundary).append(Constant.LINE_SEPARATOR);baseInfo.append("content-disposition: attachment;");outputStream.write(baseInfo.toString().getBytes(Constant.charset));infile.submit(outputStream);baseInfo = new StringBuilder();}// 追加上最后一個 boundary。baseInfo.append("--").append(boundary).append("--").append(Constant.LINE_SEPARATOR);outputStream.write(baseInfo.toString().getBytes(Constant.charset));}}}} }c、 form 表單
實現了 Input 之后,最后一個便是form實現了,form只需要實現其基本的數據組裝即可完成:
/*** Form.java* 模擬HTML頁面表單行為。*/ public class Form {private ArrayList<Input> inputs;public Form() {this.inputs = new ArrayList<>();}// 往表單中添加一個input數據。public Form addInput(Input input) {this.inputs.add(input);return this;}// 提交這個表單。傳入你要提交到的目標地址。// 此處代碼比較簡單,可以按需自己實現或修復其中存在的問題。public String submit(String url) throws Exception {if (inputs == null || inputs.size() == 0) {throw new NullPointerException("請至少添加一個要提交的表單數據。");}URL url1 = new URL(url);URLConnection urlConnection = url1.openConnection();HttpURLConnection c = (HttpURLConnection) urlConnection;// 設置請求方式、c.setRequestMethod("POST");c.setDoOutput(true);c.setDoInput(true);String boundary = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 6);// 設置headerc.setRequestProperty("Content-Type", "multipart/form-data, boundary=" + boundary);// 進行傳輸。OutputStream outputStream = c.getOutputStream();for (Input in : inputs) {outputStream.write(("--" + boundary + Constant.LINE_SEPARATOR).getBytes(Constant.charset));in.submit(outputStream);}outputStream.write(("--" + boundary + "--").getBytes(Constant.charset));outputStream.flush();// 獲取結果InputStream inputStream = c.getInputStream();// 讀取結果字符串。InputStreamReader reader = new InputStreamReader(inputStream, Constant.charset);char[] chars = new char[256];int charsize= 0;StringBuilder result = new StringBuilder();while ((charsize = reader.read(chars))!= -1) {result.append(chars, 0, charsize);}c.disconnect();return result.toString();} }d、測試結果
現在InputFile、Input、Form 類都已完成開發,不妨進行一下測試:
// 構建一個表單 Form form = new Form();// 加入一個普通的鍵值對。 form.addInput(new Input("name", "Jack"));// 加入一個包含多個文件的input。 form.addInput(new Input("fileData",new InputFile(new File("C:\\Users\\Micro\\Desktop\\file1.jpg")),new InputFile(new File("C:\\Users\\Micro\\Desktop\\file2.jpg")) ));// 提交到指定地址。 String result = form.submit("https://www.microanswer.cn/test/uploadFile");// 打印結果 System.out.println("提交結果:" + result);// 輸出:提交結果:{"msg":"success","code":200,"data":null}完美通過。
e、附加類
在上述幾個類中,使用了一個常量類,其代碼如下:
public class Constant {/*** 表單中上傳文件,使用的換行必須是 \r\n。*/public static final String LINE_SEPARATOR = "\r\n";/*** 表單中字符串的編碼。*/public static final Charset charset = StandardCharsets.UTF_8;}五、后臺接收表單文件
本文主要講了Java后臺如何實現獲取表單內的數據,下面將分別對spring后臺和原生的servlet后臺進行示例。
1、Spring后臺
如果你的后臺使用了Spring框架,那么你就幸運了,你可以十分方便的拿到表單上傳上來的文件。只需要通過一些簡單的注解就可以完成,下面示列了一個典型的Controller接口方法,用于獲取表單上傳上來的文件:
@RequestMapping("/uploadFile") public Object uploadFileTest(@RequestParam(value = "fileData") MultipartFile multipartFile,HttpServletRequest request ) throws Exception {// 即可拿到已上傳的文件內容。如果你不處理,這個文件就會在本次請求結束時被刪除InputStream in = multipartFile.getInputStream();return Util.buildReturnJson(WebApplication.Code.SUCCESS, "success", null); }這個方法針對input里只有一個文件時非常方便。如果某個input里包含了多個文件,這個方法似乎沒法獲取到更多的文件。(如果這個方法能的法話,還請大佬評論指正。)
當你希望獲取到某個input下的所有上傳的文件時。你可以用下面的方法:
@RequestMapping("/uploadFile") public Object uploadFileTest(HttpServletRequest request) throws Exception {// 使用 spring 提供的內置工具StandardMultipartHttpServletRequest r = new StandardMultipartHttpServletRequest(request);// 可以直接獲取到input里如果是text類型的值。String name = r.getParameter("name");// 可以通過這樣的方式獲取到某個input下的所有文件。// 這里就獲取了input的name為files時,提交上來的所有文件。List<MultipartFile> files = r.getMultiFileMap().get("files");return Util.buildReturnJson(WebApplication.Code.SUCCESS, "success", null); }2、servlet 實現
在servlet中獲取表單上傳的文件相對比spring里復雜,但其實spring只是把servlet的封裝了一下。咱們java后臺的表單解析功能是自帶就有的。下面給出一個示例:
public void doPost(HttpServletRequest request, HttpServletResponse response) throws Exception {// 獲取表單的所有 part 內容。// 一個 text 的input 是一個 part// 一個 file 的input 里可能有多個part(因為可以多選文件嘛),這些part的name都是 input的name。Collection<Part> parts = request.getParts();Iterator<Part> iterator = parts.iterator();while (iterator.hasNext()) {// 獲取到表單里的每個part。Part part = iterator.next();part.getName(); // 對應了 input 的 name 屬性。part.getSubmittedFileName(); // 對應了 input=file 時 提交的文件的真實名字。(當input不是file類型時此方法返回的null)part.getInputStream(); // 對應了 input=file 是文件時的文件輸入流。input=text時,其value值也通過此流讀取。// todo 實現自己的業務。} }六、總結
無論使用什么方式進行文件上傳,只要對HTTP協議有一定的了解,都可以輕松完成各種需求的開發。為什么要針對表單進行模擬來上傳文件,因為這是大多數服務器的上傳文件方式,許多服務端也都默認支持解析表單文件,因此客戶端的上傳也許迎合服務器所支持的使用表單進行文件上傳。
如果你還對HTTP協議不是很了解,可通過這篇文章進行入門:《【HTTP,Java】認識HTTP協議,在互聯網世界里翱翔》。
總結
以上是生活随笔為你收集整理的超详细的实现上传文件功能教程,文件上传实现。的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: imgbb图床API
- 下一篇: NEON加速