2021年12月15日 星期三

使用 Java 上傳檔案(發送 enctype=multipart/form-data 的 HttpPost)

這篇記綠下如何使用 Java 上傳檔案 (發送 enctype=multipart/form-data 的 HttpPost)。

先建立一個簡單的檔案接收伺服器以利驗證上傳功能的正確性。
再直接使用 Java 進行 enctype=multipart/form-data 的 HpptPost 上傳檔案,
順便再傳送一個文字參數測試文字參數的傳遞功能是否也正確。

以下直接上程式碼,使用 jdk11,Tomcat 9.0
首先是範例有用到的 Maven Lib Dependency 如下:
<dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
    
    <!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
	<dependency>
	    <groupId>javax.servlet</groupId>
	    <artifactId>javax.servlet-api</artifactId>
	    <version>4.0.1</version>
	    <scope>provided</scope>
	</dependency>
	
	<!-- https://mvnrepository.com/artifact/javax.servlet.jsp/javax.servlet.jsp-api -->
	<dependency>
	    <groupId>javax.servlet.jsp</groupId>
	    <artifactId>javax.servlet.jsp-api</artifactId>
	    <version>2.3.3</version>
	    <scope>provided</scope>
	</dependency>
	
	<!-- https://mvnrepository.com/artifact/javax.servlet.jsp.jstl/jstl-api -->
	<dependency>
	    <groupId>javax.servlet.jsp.jstl</groupId>
	    <artifactId>jstl-api</artifactId>
	    <version>1.2</version>
	</dependency>
 
	<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
	<dependency>
	    <groupId>org.apache.httpcomponents</groupId>
	    <artifactId>httpclient</artifactId>
	    <version>4.5.13</version>
	</dependency>
	
	<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpmime -->
	<dependency>
	    <groupId>org.apache.httpcomponents</groupId>
	    <artifactId>httpmime</artifactId>
	    <version>4.5.13</version>
	</dependency>
	
	<!-- https://mvnrepository.com/artifact/commons-httpclient/commons-httpclient -->
	<!-- not maintain anymore, can be replaced by org.apache.httpcomponents -->
	<dependency>
	    <groupId>commons-httpclient</groupId>
	    <artifactId>commons-httpclient</artifactId>
	    <version>3.0.1</version>
	</dependency>
		
	<!-- https://mvnrepository.com/artifact/com.jfinal/cos -->
	<dependency>
	    <groupId>com.jfinal</groupId>
	    <artifactId>cos</artifactId>
	    <version>2020.4</version>
	</dependency>
	
  </dependencies>
接著建立一個簡單的 Servlet 來接收檔案 httpPost:
UploadFileAction.java:
import java.io.File;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.oreilly.servlet.MultipartRequest;
import com.oreilly.servlet.multipart.DefaultFileRenamePolicy;

/**
 * Servlet implementation class UploadFileAction
 */
@WebServlet("/uploadFile.do")
public class UploadFileAction extends HttpServlet {
	private static final long serialVersionUID = 1L;

    /**
     * Default constructor. 
     */
    public UploadFileAction() {
        //
    }

	/**
	 * @see HttpServlet#service(HttpServletRequest request, HttpServletResponse response)
	 */
	protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		String dirSaveFilePath = "D:\\tempUploadedFile";
		if(!new File(dirSaveFilePath).exists()) {
			new File(dirSaveFilePath).mkdirs();
		}		
		MultipartRequest multipartRequest = new MultipartRequest(request, dirSaveFilePath, 100 * 1024 * 1024, "UTF-8", new DefaultFileRenamePolicy());
		System.out.println("ContentType: " + multipartRequest.getContentType("uploadedFile"));
		System.out.println("OriginalFileName: " + multipartRequest.getOriginalFileName("uploadedFile"));
		System.out.println("FileSystemName: " + multipartRequest.getFilesystemName("uploadedFile"));
		System.out.println("param1: " + multipartRequest.getParameter("param1"));
	}

	/**
	 * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
	 */
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		service(request, response);
	}

	/**
	 * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
	 */
	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		service(request, response);
	}

}



最後是會進行檔案上傳的一個很單純的 Java,
這邊我演示了兩個不同的檔案上傳方式,
uploadFile_1():
使用了 org.apache.httpcomponents (較新,取代 commons-httpclient) 的 CloseableHttpClient 和 PostMethod

uploadFile_2():
使用了 commons-httpclient (已不再維護,由 commons-httpclient 取代) 的 HttpClient 和 PostMethod

org.apache.httpcomponents 和 commons-httpclient 的關係可參考
The Commons HttpClient project is now end of life, and is no longer being developed. It has been replaced by the Apache HttpComponents project in its HttpClient and HttpCore modules, which offer better performance and more flexibility.


UploadFileTestjava:
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLConnection;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.multipart.FilePart;
import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity;
import org.apache.commons.httpclient.methods.multipart.Part;
import org.apache.commons.httpclient.methods.multipart.StringPart;
import org.apache.commons.httpclient.util.EncodingUtil;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.EntityUtils;

public class UploadFileTest {

	public static void main(String[] args) throws FileNotFoundException, IOException {
		uploadFile_1("C:\\Users\\Hugo\\Pictures\\未命名.png", "http://localhost:8080/uploadFile.do");
		uploadFile_2("C:\\Users\\Hugo\\Pictures\\未命名.png", "http://localhost:8080/uploadFile.do");
	}

	public static void uploadFile_1(String filePath, String uploadTo) throws FileNotFoundException, IOException {
		File file = new File(filePath);
		HttpPost httpPost = new HttpPost(uploadTo);

		MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create();
		//Use RFC6532 mode to avoid encoding Mojibake (garbled text) of fileName
		//使用 RFC6532 來避免中文等的檔案名稱在傳遞後變成亂碼
		multipartEntityBuilder.setMode(HttpMultipartMode.RFC6532); 
		
		multipartEntityBuilder.addPart("uploadedFile", new FileBody(file, ContentType.create(URLConnection.guessContentTypeFromName(file.getName())) , file.getName()));
//		multipartEntityBuilder.addPart("uploadedFile", new FileBody(file, ContentType.APPLICATION_OCTET_STREAM, file.getName()));		
//		multipartEntityBuilder.addBinaryBody("uploadedFile", file, ContentType.APPLICATION_OCTET_STREAM, file.getName());
				
		multipartEntityBuilder.addPart("param1", new StringBody("中文", ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8)));
//		multipartEntityBuilder.addTextBody("param1", "中文", ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8));

		HttpEntity httpEntity = multipartEntityBuilder.build();
		httpPost.setEntity(httpEntity);

		try (CloseableHttpClient httpClient = HttpClients.createDefault();
			) {
				CloseableHttpResponse httpResponse = httpClient.execute(httpPost);
				String responseStr = EntityUtils.toString(httpResponse.getEntity());
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	public static void uploadFile_2(String filePath, String uploadTo) {
		File f = new File(filePath);
		PostMethod filePost = new PostMethod(uploadTo);
		filePost.getParams().setContentCharset("UTF-8");
//		filePost.addRequestHeader("xx", "xxx"); // if you want to add any header

		try {
			Part[] parts = { 
					  new StringPart("param1", "中文", "UTF-8")
					, new FilePart("uploadedFile", f) {
							@Override
							protected void sendDispositionHeader(OutputStream out) throws IOException {
								// override 掉sendDispositionHeader 方法以解決中文 fileName 傳送後會變成亂碼的問題
								// 在原始碼中是用 getAsciiBytes(fileName),我們這裡改用utf-8 去 encode
								// super.sendDispositionHeader(out) is overridden, use UTF-8 instead of Ascii to
								// encode filename
								
								// run code copied form Part.sendDispositionHeader() directly
								out.write(CONTENT_DISPOSITION_BYTES);
								out.write(QUOTE_BYTES);
								out.write(EncodingUtil.getAsciiBytes(getName()));
								out.write(QUOTE_BYTES);
				
								String filename = getSource().getFileName();
								if (filename != null) {
									out.write(EncodingUtil.getAsciiBytes(FILE_NAME));
									out.write(QUOTE_BYTES);
									out.write(EncodingUtil.getBytes(filename, "utf-8"));
									out.write(QUOTE_BYTES);
								}
							}
					} 
			};
			filePost.setRequestEntity(new MultipartRequestEntity(parts, filePost.getParams()));
			HttpClient client = new HttpClient();
			int status = client.executeMethod(filePost);
			String responseStr = filePost.getResponseBodyAsString();
		}catch(IOException e) {
			e.printStackTrace();
		}
	}

}
題外話:
  1. 因為用 Eclipse 的 Maven 外掛產生出的 JAVA EE 專案 (Archetype 選 maven-archetype-webapp) 是用舊版的 servlet 2.3,如果要用新版例如 servlet 3.0, 記得去 web.xml 裡把 <web-app> 裡的設定值改對,servlet 3.0 的設定值可參考這裡, 或是直接把 web.xml 刪掉也可以,因為 web.xml 對 servlet 3.0 並不是必需的。
  2. 因為 Eclipse 可能以為還是使用舊版 servlet 的關係,所以可能無法使用 Eclipse 裡面的相關功能, 例如無法用 Tomcat 啟動專案, 這時需要去修改專案的 Project Facets 設定,將 Dynamic Web Module 設成 3.0 (或以上版本),但有可能會發現無法修改, 這時可以去專案的 .setting 資料夾下找到 org.eclipse.wst.common.project.facet.core.xml, 將裡面的 facet="jst.web" verson 改成 3.0 即可,例如:
    <?xml version="1.0" encoding="UTF-8"?>
    <faceted-project>
      <fixed facet="wst.jsdt.web"/>
      <installed facet="jst.web" version="3.0"/>
      <installed facet="wst.jsdt.web" version="1.0"/>
      <installed facet="java" version="11"/>
    </faceted-project>
參考資料:
  1. HTTPClient PostMethod 中文乱码问题解决方案(2种)
  2. HTML <form> 标签的 enctype 属性
  3. Eclipse創建Maven-Web項目及解決 jre版本和web.xml版本問題
  4. Multipart Upload with HttpClient 4
  5. Getting a File’s Mime Type in Java
  6. Eclipse| 修改dynamic web module 为3.0版本
原始碼分享:

沒有留言 :

張貼留言