Java EE規格雖然是針對網路應用開發的,但直到 Servlet 3.0 前都沒有提供檔案上傳的功能
Servlet 3.0的上傳機制可以參考這篇
因為還沒打算轉換到Servlet 3.0 的打算下,先研究使用套件的解決方案
時下較為流行的上傳套件共有 Cos、FileUpload、SmartUpload
Cos套件是 O'reilly 公司提供,可至 http://www.servlets.com/cos 下載套件(cos-26Dec2008.zip)
FileUpload 的下載位置: http://commons.apache.org/fileupload/
FileUpload 使用須具備的套件:http://commons.apache.org/io/
SmartUpload 的網站已關閉,大概未來也不會繼續開發了
FileUpload 具有較多的功能,也有提供方便用來做 Ajax 應用的 listener
不過純以上傳效率來說Cos是最優秀的,贏過其他套件不少
這次實作選擇使用Cos
先提到上傳的介紹
撰寫上傳的功能必須先撰寫好一個頁面
並由表單以POST的形式將資料傳送到上傳處理的頁面
表單的編碼方式也與一般有所不同,enctype屬性共有三種值
1.application/x-www-form-urlencoded 是預設的編碼方式,它只處理表單域裡的value值
採用這種編碼方式的表單會將表單域的值處理成URL編碼方式
2.multipart/form-data 編碼會以二進制流的方式來處理表單數據
它把文件域指定文件的內容也封裝到請求參數里
一旦設置了這種方式,就無法透過HttpServletRequest.getParameter()請求獲取請求參數
3.text/plain 編碼方式當表單的action屬性為mailto:URL的形式時比較方便
這種方式主要適用於直接通過表單發送郵件的方式
這次實作的內容為撰寫一個提供圖片及附件上傳的功能
先寫好提供表單的網頁
designUploader.jsp======
.....
<form action="UploadHandler" method="POST" enctype="multipart/form-data">
<label>正面:</label><input type="file" id="f_pic" name="f_pic" value="" width="20" /><br />
<label>背面:</label><input type="file" id="b_pic" name="b_pic" value="" width="20" /><br />
<label>附件:</label><input type="file" id="append" name="append" value="" width="20" /><br />
<input type="submit" value="Upload" /><br />
<span>${uploadInfo.message}</span>
</form>
....
解壓縮下載好的 Cos套件,並將 lib 裡的 cos.jar 複製到 WEB_INF 目錄底下的 lib 裡
接著撰寫處理上傳的頁面
使用Cos時,可以使用兩個類別來進行上傳工作:
1. MultipartRequest 2. MultipartParser
一般情況下使用MultipartRequest即可,不需要複雜的設定就可以輕鬆上傳物件
實際上MultipartRequest封裝了MultipartParser
在構造MultipartRequest實例時,建構了MultipartParser實例
建構 parser 的過程取得了上傳的 InputStream,但並不會真正讀取
然後透過 MultipartParser的readNextPart() method,從request流中讀取數據
區別出 InputStream 中的參數域和文件域
如果是參數的話用ParamPart類封裝;如果是文件的話用FilePart封裝
此時如果設置了重新命名策略,則將在Server端新建一個新命名的空白物件
接著用FilePart的writeTo(saveDir)方法將流數據寫到硬碟中,文件上傳完成
MultipartRequest 範例:
UploadHandler.java=========
package tw.vencs;
import java.io.IOException;
import java.util.Enumeration;
import javax.servlet.http.*;
import com.oreilly.servlet.MultipartRequest;
public class UploadHandler extends HttpServlet{
public void doPost(HttpServletRequest request, HttpServletResponse response)throws IOException{
String saveDirectory = .....; //uploaded files' store dictionary
int maxPostSize=5*1024*1024; //limit of file capacity
String FileName=null;
String ContentType=null; //declare file type
int count = 0; //count numbers of file uploaded
MultipartRequest multi = new MultipartRequest(request,saveDirectory,maxPostSize, "UTF-8");
//get all types and descriptions from uploaded files
Enumeration filename = multi.getFileNames();
Enumeration filesdc = multi.getParameterNames();
while(filename.hasMoreElements()){
String name = (String)filename.nextElement();
String dc = (String)filesdc.nextElement();
FileName = multi.getFilesystemName(name);
ContentType = multi.getContentType(name);
if(FileName != null){
count++;
}
}
............
}
}
當 MultipartRequest 建立好的時候就是檔案上傳完畢的時候
不設定檔案上傳大小時,最大上傳限制預設是 1mb
如果上傳檔案大小超過設定的大小時會丟出例外
雖然看API的說明是會丟出 ExceededSizeException,不過結果似乎是裝在IOException
命名策略是指上傳的檔案遇到了檔名重複時的處理方式
有自訂處理方式可以自行撰寫,如下:
RandomFileRenamePolicy.java===========
package tw.vencs;
import java.io.File;
import java.util.Date;
import com.oreilly.servlet.multipart.FileRenamePolicy;
public class RandomFileRenamePolicy implements FileRenamePolicy {
public File rename(File file) {
String body="";
String ext="";
Date date = new Date();
int pot=file.getName().lastIndexOf(".");
if(pot!=-1){
body= date.getTime() +"";
ext=file.getName().substring(pot);
}else{
body=(new Date()).getTime()+"";
ext="";
}
String newName=body+ext;
file=new File(file.getParent(),newName);
return file;
}
}
這個命名規則會取得時間來做為隨機命名的依據,接著將MultipartRequest改寫成:
RandomFileRenamePolicy rfrp=new RandomFileRenamePolicy();
MultipartRequest multi = new MultipartRequest(request,saveDirectory,maxPostSize, "UTF-8",rfrp);
因為我實作的需求是希望能限制上傳檔案的大小
不管是怎麼樣的上傳套件都沒辦法再接收資料前得知檔案大小
畢竟那等同於Server端直接從Client讀取資料,權限上不會被允許
JavaScript可以做到檢查檔案大小的功能
<script language="JavaScript">
var checkImageSize = true; //是否檢查圖片檔案大小
var ImageSizeLimit = 100000; //上傳上限,單位:byte
function checkFile() {
var f = document.forms[0];
var re = /\.(jpg|gif)$/i; //允許的圖片副檔名
if (checkImageType && !re.test(f.file1.value)) {
alert("只允許上傳JPG或GIF影像檔");
} else {
var img = new Image();
img.onload = checkImage;
img.src = f.file1.value;
}
...
document.forms[0].submit();
}
function checkImage() {
...
if (checkImageSize && this.fileSize > ImageSizeLimit) {
showMessage('檔案大小','kb',this.fileSize/1000,ImageSizeLimit/1000);
}
}
function showMessage(kind,unit,real,limit) {
var msg = "選擇的圖片kind為 real unit\n超過了上傳上限 limit unit\n不允許上傳!"
alert(msg.replace(/kind/,kind).replace(/unit/g,unit).replace(/real/,real).replace(/limit/,limit));
}
</script>
不過預防Client端沒開啟JavaScript的情形,我再查了些資料
java.io套件裡的 FileInputStream 有available() method可以檢查request送來的資料流大小
而不必等到資料流全傳輸完畢
可惜的是看過 FileInputStream 的API後知道available()的限制
它處理的是沒遭遇到network blocking時所送來的一串資料流
只是現實上常有blocking發生...
MultipartRequest最多是在全部的物件上傳完後再來檢查
既然傳輸資料的過程已經免不了了,那我希望至少是當一個檔案上傳時就先檢查
而省掉全傳輸完所耗費的資源及時間
所以將 UploadHandler 改寫成以 MultipartParser 處理
UploadHandler.java=========
package tw.vencs;
import java.io.File;
import java.io.IOException;
import javax.servlet.http.*;
import com.oreilly.servlet.multipart.FilePart;
import com.oreilly.servlet.multipart.MultipartParser;
import com.oreilly.servlet.multipart.Part;
public class UploadHandler extends HttpServlet{
public void doPost(HttpServletRequest request, HttpServletResponse response)throws IOException{
HttpSession session = request.getSession();
String saveDirectory = null; //uploaded files' store dictionary
File dir = new File(saveDirectory);
int pictureSize = Integer.parseInt(getServletConfig().getInitParameter("pictureSize"));
int appendSize = Integer.parseInt(getServletConfig().getInitParameter("appendSize"));
int maxPostSize = pictureSize * 4 + appendSize; //limit of file capacity
String Message = "";
MultipartParser fileMap = null;
String fileName = null;
try{
fileMap = new MultipartParser(request, maxPostSize);
fileMap.setEncoding("UTF-8");
Part part = null;
while ((part = fileMap.readNextPart()) != null){
if(part.isFile()){ //if this inputstream is from HTML file upload element
FilePart filePart = (FilePart) part;
filePart.setRenamePolicy(new DefaultFileRenamePolicy());
String name = part.getName(); //get HTML elements' name of form
fileName = filePart.getFileName(); //get name of uploaded object
if(fileName != null && !fileName.equals("")){
String fileType = fileName.substring( fileName.lastIndexOf('.')+1).toLowerCase();
....
if(name.substring(2).equals("pic")){
File dir = new File(saveDirectory);
dir.mkdirs();
long size = filePart.writeTo(dir); //write data into designated file directory
if(fileType.equals("jpg") || fileType.equals("jpeg") ||
fileType.equals("png") || fileType.equals("bmp") ||
fileType.equals("gif") || fileType.equals("ai")){
if(size > (long)pictureSize){
tempMessage = fileName + "大小超過限制!!<br />";
Message += (tempMessage);
File disposedFile = new File(
saveDirectory + "/" + filePart.getFileName());
disposedFile.delete();
}else{
... //success process
}
}else{
tempMessage = fileName + "不是圖檔格式!!<br />";
Message += tempMessage;
File disposedFile = new File(saveDirectory + "/" + fileName);
disposedFile.delete();
}
}else{ //appendix check
File appendDir = new File(saveDirectory + "/append");
appendDir.mkdirs();
int size = (int)filePart.writeTo(appendDir);
int uAppendSize = 0; //store uploaded data size info
if(session.getAttribute("append") != null &&
!session.getAttribute("append").equals("")){
File[] fList = appendDir.listFiles();
for (int j = 0; j < fList.length; j++){
FileInputStream in = new FileInputStream(fList[j]);
uAppendSize += in.available();
in.close(); //must close FileInputStream after use it
}
}
if(size > (appendSize - uAppendSize)){
tempMessage = fileName + "大小超過限制!!<br />";
Message += tempMessage;
File disposedFile = new File(
saveDirectory + "/appendix/" + filePart.getFileName());
disposedFile.delete();
}else{
.... //success process
}
}
}
}
}
}catch(IOException e){
.....
}
...
}
}
}
預設的命名規則是遇到覆蓋已存在的同名檔案
因為我想處理的只有上傳域的物件
假使表單裡還有其他文件域的資訊,如下處理:
if (part.isParam()){ //if this inputstream is from normal HTML input element
ParamPart paramPart = (ParamPart) part;
String value = paramPart.getStringValue(); //get param's value
Servlet 3.0的上傳機制可以參考這篇
因為還沒打算轉換到Servlet 3.0 的打算下,先研究使用套件的解決方案
時下較為流行的上傳套件共有 Cos、FileUpload、SmartUpload
Cos套件是 O'reilly 公司提供,可至 http://www.servlets.com/cos 下載套件(cos-26Dec2008.zip)
FileUpload 的下載位置: http://commons.apache.org/fileupload/
FileUpload 使用須具備的套件:http://commons.apache.org/io/
SmartUpload 的網站已關閉,大概未來也不會繼續開發了
FileUpload 具有較多的功能,也有提供方便用來做 Ajax 應用的 listener
不過純以上傳效率來說Cos是最優秀的,贏過其他套件不少
這次實作選擇使用Cos
先提到上傳的介紹
撰寫上傳的功能必須先撰寫好一個頁面
並由表單以POST的形式將資料傳送到上傳處理的頁面
表單的編碼方式也與一般有所不同,enctype屬性共有三種值
1.application/x-www-form-urlencoded 是預設的編碼方式,它只處理表單域裡的value值
採用這種編碼方式的表單會將表單域的值處理成URL編碼方式
2.multipart/form-data 編碼會以二進制流的方式來處理表單數據
它把文件域指定文件的內容也封裝到請求參數里
一旦設置了這種方式,就無法透過HttpServletRequest.getParameter()請求獲取請求參數
3.text/plain 編碼方式當表單的action屬性為mailto:URL的形式時比較方便
這種方式主要適用於直接通過表單發送郵件的方式
這次實作的內容為撰寫一個提供圖片及附件上傳的功能
先寫好提供表單的網頁
designUploader.jsp======
.....
<form action="UploadHandler" method="POST" enctype="multipart/form-data">
<label>正面:</label><input type="file" id="f_pic" name="f_pic" value="" width="20" /><br />
<label>背面:</label><input type="file" id="b_pic" name="b_pic" value="" width="20" /><br />
<label>附件:</label><input type="file" id="append" name="append" value="" width="20" /><br />
<input type="submit" value="Upload" /><br />
<span>${uploadInfo.message}</span>
</form>
....
解壓縮下載好的 Cos套件,並將 lib 裡的 cos.jar 複製到 WEB_INF 目錄底下的 lib 裡
接著撰寫處理上傳的頁面
使用Cos時,可以使用兩個類別來進行上傳工作:
1. MultipartRequest 2. MultipartParser
一般情況下使用MultipartRequest即可,不需要複雜的設定就可以輕鬆上傳物件
實際上MultipartRequest封裝了MultipartParser
在構造MultipartRequest實例時,建構了MultipartParser實例
建構 parser 的過程取得了上傳的 InputStream,但並不會真正讀取
然後透過 MultipartParser的readNextPart() method,從request流中讀取數據
區別出 InputStream 中的參數域和文件域
如果是參數的話用ParamPart類封裝;如果是文件的話用FilePart封裝
此時如果設置了重新命名策略,則將在Server端新建一個新命名的空白物件
接著用FilePart的writeTo(saveDir)方法將流數據寫到硬碟中,文件上傳完成
MultipartRequest 範例:
UploadHandler.java=========
package tw.vencs;
import java.io.IOException;
import java.util.Enumeration;
import javax.servlet.http.*;
import com.oreilly.servlet.MultipartRequest;
public class UploadHandler extends HttpServlet{
public void doPost(HttpServletRequest request, HttpServletResponse response)throws IOException{
String saveDirectory = .....; //uploaded files' store dictionary
int maxPostSize=5*1024*1024; //limit of file capacity
String FileName=null;
String ContentType=null; //declare file type
int count = 0; //count numbers of file uploaded
MultipartRequest multi = new MultipartRequest(request,saveDirectory,maxPostSize, "UTF-8");
//get all types and descriptions from uploaded files
Enumeration filename = multi.getFileNames();
Enumeration filesdc = multi.getParameterNames();
while(filename.hasMoreElements()){
String name = (String)filename.nextElement();
String dc = (String)filesdc.nextElement();
FileName = multi.getFilesystemName(name);
ContentType = multi.getContentType(name);
if(FileName != null){
count++;
}
}
............
}
}
當 MultipartRequest 建立好的時候就是檔案上傳完畢的時候
不設定檔案上傳大小時,最大上傳限制預設是 1mb
如果上傳檔案大小超過設定的大小時會丟出例外
雖然看API的說明是會丟出 ExceededSizeException,不過結果似乎是裝在IOException
命名策略是指上傳的檔案遇到了檔名重複時的處理方式
有自訂處理方式可以自行撰寫,如下:
RandomFileRenamePolicy.java===========
package tw.vencs;
import java.io.File;
import java.util.Date;
import com.oreilly.servlet.multipart.FileRenamePolicy;
public class RandomFileRenamePolicy implements FileRenamePolicy {
public File rename(File file) {
String body="";
String ext="";
Date date = new Date();
int pot=file.getName().lastIndexOf(".");
if(pot!=-1){
body= date.getTime() +"";
ext=file.getName().substring(pot);
}else{
body=(new Date()).getTime()+"";
ext="";
}
String newName=body+ext;
file=new File(file.getParent(),newName);
return file;
}
}
這個命名規則會取得時間來做為隨機命名的依據,接著將MultipartRequest改寫成:
RandomFileRenamePolicy rfrp=new RandomFileRenamePolicy();
MultipartRequest multi = new MultipartRequest(request,saveDirectory,maxPostSize, "UTF-8",rfrp);
因為我實作的需求是希望能限制上傳檔案的大小
不管是怎麼樣的上傳套件都沒辦法再接收資料前得知檔案大小
畢竟那等同於Server端直接從Client讀取資料,權限上不會被允許
JavaScript可以做到檢查檔案大小的功能
<script language="JavaScript">
var checkImageSize = true; //是否檢查圖片檔案大小
var ImageSizeLimit = 100000; //上傳上限,單位:byte
function checkFile() {
var f = document.forms[0];
var re = /\.(jpg|gif)$/i; //允許的圖片副檔名
if (checkImageType && !re.test(f.file1.value)) {
alert("只允許上傳JPG或GIF影像檔");
} else {
var img = new Image();
img.onload = checkImage;
img.src = f.file1.value;
}
...
document.forms[0].submit();
}
function checkImage() {
...
if (checkImageSize && this.fileSize > ImageSizeLimit) {
showMessage('檔案大小','kb',this.fileSize/1000,ImageSizeLimit/1000);
}
}
function showMessage(kind,unit,real,limit) {
var msg = "選擇的圖片kind為 real unit\n超過了上傳上限 limit unit\n不允許上傳!"
alert(msg.replace(/kind/,kind).replace(/unit/g,unit).replace(/real/,real).replace(/limit/,limit));
}
</script>
不過預防Client端沒開啟JavaScript的情形,我再查了些資料
java.io套件裡的 FileInputStream 有available() method可以檢查request送來的資料流大小
而不必等到資料流全傳輸完畢
可惜的是看過 FileInputStream 的API後知道available()的限制
它處理的是沒遭遇到network blocking時所送來的一串資料流
只是現實上常有blocking發生...
MultipartRequest最多是在全部的物件上傳完後再來檢查
既然傳輸資料的過程已經免不了了,那我希望至少是當一個檔案上傳時就先檢查
而省掉全傳輸完所耗費的資源及時間
所以將 UploadHandler 改寫成以 MultipartParser 處理
UploadHandler.java=========
package tw.vencs;
import java.io.File;
import java.io.IOException;
import javax.servlet.http.*;
import com.oreilly.servlet.multipart.FilePart;
import com.oreilly.servlet.multipart.MultipartParser;
import com.oreilly.servlet.multipart.Part;
public class UploadHandler extends HttpServlet{
public void doPost(HttpServletRequest request, HttpServletResponse response)throws IOException{
HttpSession session = request.getSession();
String saveDirectory = null; //uploaded files' store dictionary
File dir = new File(saveDirectory);
int pictureSize = Integer.parseInt(getServletConfig().getInitParameter("pictureSize"));
int appendSize = Integer.parseInt(getServletConfig().getInitParameter("appendSize"));
int maxPostSize = pictureSize * 4 + appendSize; //limit of file capacity
String Message = "";
MultipartParser fileMap = null;
String fileName = null;
try{
fileMap = new MultipartParser(request, maxPostSize);
fileMap.setEncoding("UTF-8");
Part part = null;
while ((part = fileMap.readNextPart()) != null){
if(part.isFile()){ //if this inputstream is from HTML file upload element
FilePart filePart = (FilePart) part;
filePart.setRenamePolicy(new DefaultFileRenamePolicy());
String name = part.getName(); //get HTML elements' name of form
fileName = filePart.getFileName(); //get name of uploaded object
if(fileName != null && !fileName.equals("")){
String fileType = fileName.substring( fileName.lastIndexOf('.')+1).toLowerCase();
....
if(name.substring(2).equals("pic")){
File dir = new File(saveDirectory);
dir.mkdirs();
long size = filePart.writeTo(dir); //write data into designated file directory
if(fileType.equals("jpg") || fileType.equals("jpeg") ||
fileType.equals("png") || fileType.equals("bmp") ||
fileType.equals("gif") || fileType.equals("ai")){
if(size > (long)pictureSize){
tempMessage = fileName + "大小超過限制!!<br />";
Message += (tempMessage);
File disposedFile = new File(
saveDirectory + "/" + filePart.getFileName());
disposedFile.delete();
}else{
... //success process
}
}else{
tempMessage = fileName + "不是圖檔格式!!<br />";
Message += tempMessage;
File disposedFile = new File(saveDirectory + "/" + fileName);
disposedFile.delete();
}
}else{ //appendix check
File appendDir = new File(saveDirectory + "/append");
appendDir.mkdirs();
int size = (int)filePart.writeTo(appendDir);
int uAppendSize = 0; //store uploaded data size info
if(session.getAttribute("append") != null &&
!session.getAttribute("append").equals("")){
File[] fList = appendDir.listFiles();
for (int j = 0; j < fList.length; j++){
FileInputStream in = new FileInputStream(fList[j]);
uAppendSize += in.available();
in.close(); //must close FileInputStream after use it
}
}
if(size > (appendSize - uAppendSize)){
tempMessage = fileName + "大小超過限制!!<br />";
Message += tempMessage;
File disposedFile = new File(
saveDirectory + "/appendix/" + filePart.getFileName());
disposedFile.delete();
}else{
.... //success process
}
}
}
}
}
}catch(IOException e){
.....
}
...
}
}
}
預設的命名規則是遇到覆蓋已存在的同名檔案
因為我想處理的只有上傳域的物件
假使表單裡還有其他文件域的資訊,如下處理:
if (part.isParam()){ //if this inputstream is from normal HTML input element
ParamPart paramPart = (ParamPart) part;
String value = paramPart.getStringValue(); //get param's value
留言
張貼留言