動機:
2006年底Google了一下AJAX Upload實現,結果沒有發現很完整的Java實現。碩果僅存的就是TELIO公司的Pierre-Alexandre發表的《AJAX Upload progress monitor for Commons-FileUpload Example》文中提供的ajax-upload-1.0.war。
雖然上文中完成Upload工作的是Apache的Common-FileUpload組件,但在其代碼中所使用的FileUpload1.1版本並沒有1.2版本所提供的上傳處理Listener功能,這就對檢測文件上傳情況造成了困難。我想正是這個原因致使Pierre-Alexandre使用了DWR+MonitoredDiskFileItem、MonitoredDiskFileItemFactory類(分別繼承DiskFileItem、DiskFileItemFactory)的方式:前者負責在web客戶端進行Remote Call;後者在進行文件數據讀取時統計數據總量、讀取數據量、處理文件總數,並保存於Session中,以供web客戶端通過DWR遠程調用UploadMonitor類的getUploadInfo方法進行輪詢(Poll)。
從本人觀點出發,Pierre-Alexandre實現的不足之處:
1.沒有用戶取消上傳功能;
2.完全的DWR實現,沒有使用Prototype,對於不會使用DWR的開發者來講有一定的知識局限性,而且由於DWR的個性而造成不便將此實現集成到項目中。
Prototype+Servlet的實現:
Prototype+Servlet的Example
所以出於研究Prototype之目的,本人經過仔細思考,嘗試實現了一個Prototype+Servlet的簡單Example。其工作流程很簡單:
1.在Form提交上傳文件Field的同時,使用AJAX周期性地從Servlet輪詢上傳狀態信息;
2.然後,根據此信息更新進度條和相關文字,及時反映文件傳輸狀態;
3.如果用戶取消上傳操作,則進行相應的現場清理工作:刪除已經上傳的文件,在Form提交頁面中顯示相關信息;
4.如果上傳完畢,在Form提交頁面中顯示已經上傳的文件內容(或鏈接),也可以與一些AJAX SlideShow應用結合在一起。
服務器端代碼: Bean序列化/反序列化工作:XmlUnSerializer這個類雖然不能夠通吃任何模樣的Bean,但應付一般的Bean、具有Collection類型屬性的Bean和Bean List來講還是夠用的。
{XmlUnSerializer類的核心方法serializeBean和serializeBeanList}:
/**
* 將bean系列化為UTF-8編碼的xml
* @param beanObj
* @return
* @throws IOException
*/
public static String serializeBean(Object beanObj) throws IOException{
…
}
/**
* 將bean列表序列化為UTF-8編碼的xml
* @param beanObj
* @return
* @throws IOException
*/
public static String serializeBeanList(Object beanListObj) throws IOException{
…
}
文件上傳狀態Bean:使用FileUploadStatus這個類記錄文件上傳狀態,並將其作為服務器端與web客戶端之間通信的媒介物:通過對這個類對象進行XML序列化作為服務器回應發送給web客戶端,web客戶端使用JavaScript對其進行反序列化處理獲得JavaScript版本的文件上傳狀態對象。
{FileUploadStatus的屬性}:
//上傳總量
private long uploadTotalSize=0;
//讀取上傳總量
private long readTotalSize=0;
//當前上傳文件號
private int currentUploadFileNum=0;
//成功讀取上傳文件數
private int successUploadFileCount=0;
//狀態
private String status="";
//處理起始時間
private long processStartTime=0l;
//處理終止時間
private long processEndTime=0l;
//處理執行時間
private long processRunningTime=0l;
//上傳文件URL列表
private List uploadFileUrlList=new ArrayList();
//取消上傳
private boolean cancel=false;
//上傳base目錄
private String baseDir="";
文件上傳狀態監視工作:使用Common-FileUpload 1.2版本(20070103)。此版本與1.1版的區別在於提供了能夠監視文件上傳情況的ProcessListener接口,使開發者通過FileUploadBase類對象的setProcessListener方法植入自己的Listener,而且實現這個Listener很簡單。
{FileUploadListener主要方法update}:
/**
* 更新狀態
* @param pBytesRead 讀取字節總數
* @param pContentLength 數據總長度
* @param pItems 當前正在被讀取的field號
*/
public void update(long pBytesRead, long pContentLength, int pItems){
FileUploadStatus fuploadStatus=BackGroundService.takeOutFileUploadStatusBean(this.session);
logger.debug("當前正在處理第" + pItems+"個文件");
fuploadStatus.setUploadTotalSize(pContentLength);
//讀取完成
if (pContentLength == -1) {
logger.debug("讀取完成:讀取了 " + pBytesRead + " bytes.");
fuploadStatus.setStatus("完成對" + pItems+"個文件的讀取:讀取了 " + pBytesRead + " bytes.");
fuploadStatus.setReadTotalSize(pBytesRead);
fuploadStatus.setSuccessUploadFileCount(pItems);
fuploadStatus.setProcessEndTime(System.currentTimeMillis());
fuploadStatus.setProcessRunningTime(fuploadStatus.getProcessEndTime());
//讀取中
} else {
logger.debug("讀取進行中:已經讀取了 " + pBytesRead + " / " + pContentLength+ " bytes.");
fuploadStatus.setStatus("當前正在處理第" + pItems+"個文件:已經讀取了 " + pBytesRead
+ " / " + pContentLength+ " bytes.");
fuploadStatus.setReadTotalSize(pBytesRead);
fuploadStatus.setCurrentUploadFileNum(pItems);
fuploadStatus.setProcessRunningTime(System.currentTimeMillis());
}
BackGroundService.storeFileUploadStatusBean(this.session,fuploadStatus);
}
很清楚,我也把FileUploadStatus這個Bean存取於Session中。
Servlet實現:BackGroundService這個Servlet類負責接收Form Post數據、回應狀態輪詢請求、處理取消文件上傳的請求。盡管可以把這些功能相互分離開來(比如構造一個FileUploadManager類),但出於簡單明了、便於閱讀之目的,還是將它們放到Servlet中,只是由不同的方法進行分割。
{BackGroundService中的processFileUpload方法用於處理文件上傳請求}:
/**
* 處理文件上傳
* @param request
* @param response
* @throws IOException
* @throws ServletException
*/
private void processFileUpload(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException{
DiskFileItemFactory factory = new DiskFileItemFactory();
//設置內存閥值,超過後寫入臨時文件
factory.setSizeThreshold(10240000);
//設置臨時文件存儲位置
factory.setRepository(new File(request.getRealPath("/upload/temp")));
ServletFileUpload upload = new ServletFileUpload(factory);
//設置單個文件的最大上傳size
upload.setFileSizeMax(10240000);
//設置整個request的最大size
upload.setSizeMax(10240000);
upload.setProgressListener(new FileUploadListener(request.getSession()));
//保存初始化後的FileUploadStatus Bean
storeFileUploadStatusBean(request.getSession(),initFileUploadStatusBean(request));
String forwardURL="";
try {
List items = upload.parseRequest(request);
//獲得返回url
for(int i=0;i<items.size();i++){
FileItem item=(FileItem)items.get(i);
if (item.isFormField()){
logger.debug("form Field["+item.getFieldName()+"]="+item.getString());
forwardURL=item.getString();
break;
}
}
//處理文件上傳
for(int i=0;i<items.size();i++){
FileItem item=(FileItem)items.get(i);
//取消上傳
if (takeOutFileUploadStatusBean(request.getSession()).getCancel()){
deleteUploadedFile(request);
break;
}
//保存文件
else if (!item.isFormField() && item.getName().length()>0){
String fileName=takeOutFileName(item.getName());
logger.debug("處理文件["+fileName+"]:保存路徑為"
+request.getRealPath(UPLOAD_DIR)+File.separator+fileName);
File uploadedFile =
new File(request.getRealPath(UPLOAD_DIR)+File.separator+fileName);
item.write(uploadedFile);
//更新上傳文件列表
FileUploadStatus fUploadStatus=takeOutFileUploadStatusBean
(request.getSession());
fUploadStatus.getUploadFileUrlList().add(fileName);
storeFileUploadStatusBean(request.getSession(),fUploadStatus);
Thread.sleep(500);
}
}
} catch (FileUploadException e) {
logger.error("上傳文件時發生錯誤:"+e.getMessage());
e.printStackTrace();
uploadExceptionHandle(request,"上傳文件時發生錯誤:"+e.getMessage());
} catch (Exception e) {
// TODO Auto-generated catch block
logger.error("保存上傳文件時發生錯誤:"+e.getMessage());
e.printStackTrace();
uploadExceptionHandle(request,"保存上傳文件時發生錯誤:"+e.getMessage());
}
if (forwardURL.length()==0){
forwardURL=DEFAULT_UPLOAD_FAILURE_URL;
}
request.getRequestDispatcher(forwardURL).forward(request,response);
}
{BackGroundService中的responseFileUploadStatusPoll方法用於處理對文件上傳狀態的輪詢請求}:
/**
* 回應上傳狀態查詢
* @param request
* @param response
* @throws IOException
*/
private void responseFileUploadStatusPoll(HttpServletRequest request,HttpServletResponse response)
throws IOException{
response.setContentType("text/xml");
response.setCharacterEncoding("UTF-8");
response.setHeader("Cache-Control", "no-cache");
logger.debug("發送上傳狀態回應");
response.getWriter().write(XmlUnSerializer.serializeBean(
request.getSession().getAttribute(UPLOAD_STATUS)));
}
{BackGroundService中的processCancelFileUpload方法用於處理取消文件上傳的請求}:
/**
* 處理取消文件上傳
* @param request
* @param response
* @throws IOException
*/
private void processCancelFileUpload(HttpServletRequest request,HttpServletResponse response)
throws IOException{
FileUploadStatus fUploadStatus=(FileUploadStatus)request.getSession().getAttribute(UPLOAD_STATUS);
fUploadStatus.setCancel(true);
request.getSession().setAttribute(UPLOAD_STATUS, fUploadStatus);
responseFileUploadStatusPoll(request,response);
}
Web客戶端代碼:
Prototype給開發者更多的自由選擇
web客戶端使用了基於Prototype的AjaxWrapper類和XMLDomForAjax類,前者實現了對Ajax.Request功能的封裝,而後者實現了對來自服務器的XML Response的反序列化(反序列化為JavaScript對象)。
為了避免在AjaxWrapper的回調方法中發生this被重寫的問題,我使用了ClassUtils類給任何類的每個方法注冊一個對類對象自身引用,詳見《解開JavaScript生命的達芬奇密碼》和《Prototype.AjaxRequest的調用堆棧重寫問題》:
{ClassUtils類代碼}:
//類工具
var ClassUtils=Class.create();
ClassUtils.prototype={
_ClassUtilsName:'ClassUtils',
initialize:function(){
},
/**
* 給類的每個方法注冊一個對類對象的自我引用
* @param reference 對類對象的引用
*/
registerFuncSelfLink:function(reference){
for (var n in reference) {
var item = reference[n];
if (item instanceof Function)
item.$ = reference;
}
}
}
{將XML反序列化為JavaScript對象的XMLDomForAjax類代碼}:
var XMLDomForAjax=Class.create();
XMLDomForAjax.prototype={
isDebug:false,
//dom節點類型常量
ELEMENT_NODE:1,
ATTRIBUTE_NODE:2,
TEXT_NODE:3,
CDATA_SECTION_NODE:4,
ENTITY_REFERENCE_NODE:5,
ENTITY_NODE:6,
PROCESSING_INSTRUCTION_NODE:7,
COMMENT_NODE:8,
DOCUMENT_NODE:9,
DOCUMENT_TYPE_NODE:10,
DOCUMENT_FRAGMENT_NODE:11,
NOTATION_NODE:12,
initialize:function(isDebug){
new ClassUtils().registerFuncSelfLink(this);
this.isDebug=isDebug;
},
/**
* 建立跨平台的dom解析器
* @param xml xml字符串
* @return dom解析器
*/
createDomParser:function(xml){
// code for IE
if (window.ActiveXObject){
var doc=new ActiveXObject("Microsoft.XMLDOM");
doc.async="false";
doc.loadXML(xml);
}
// code for Mozilla, Firefox, Opera, etc.
else{
var parser=new DOMParser();
var doc=parser.parseFromString(xml,"text/xml");
}
return doc;
},
/**
* 反向序列化xml到javascript Bean
* @param xml xml字符串
* @return javascript Bean
*/
deserializedBeanFromXML:function (xml){
var funcHolder=arguments.callee.$;
var doc=funcHolder.createDomParser(xml);
// documentElement總表示文檔的root
var objDomTree=doc.documentElement;
var obj=new Object();
for (var i=0; i<objDomTree.childNodes.length; i++) {
//獲得節點
var node=objDomTree.childNodes[i];
//取出其中的field元素進行處理
if ((node.nodeType==funcHolder.ELEMENT_NODE) && (node.tagName == 'field')) {
var nodeText=funcHolder.getNodeText(node);
if (funcHolder.isDebug){
alert(node.getAttribute('name')+' type:'+node.getAttribute('type')+' text:'+nodeText);
}
var objFieldValue=null;
//如果為列表
if (node.getAttribute('type')=='java.util.List'){
if (objFieldValue && typeof(objFieldValue)=='Array'){
if (nodeText.length>0){
objFieldValue[objFieldValue.length]=nodeText;
}
}
else{
objFieldValue=new Array();
}
}
else if (node.getAttribute('type')=='long'
|| node.getAttribute('type')=='java.lang.Long'
|| node.getAttribute('type')=='int'
|| node.getAttribute('type')=='java.lang.Integer'){
objFieldValue=parseInt(nodeText);
}
else if (node.getAttribute('type')=='double'
|| node.getAttribute('type')=='float'
|| node.getAttribute('type')=='java.lang.Double'
|| node.getAttribute('type')=='java.lang.Float'){
objFieldValue=parseFloat(nodeText);
}
else if (node.getAttribute('type')=='java.lang.String'){
objFieldValue=nodeText;
}
else{
objFieldValue=nodeText;
}
//賦值給對象
obj[node.getAttribute('name')]=objFieldValue;
if (funcHolder.isDebug){
alert(eval('obj.'+node.getAttribute('name')));
}
}
else if (node.nodeType == funcHolder.TEXT_NODE){
if (funcHolder.isDebug){
//alert('TEXT_NODE');
}
}
else if (node.nodeType == funcHolder.CDATA_SECTION_NODE){
if (funcHolder.isDebug){
//alert('CDATA_SECTION_NODE');
}
}
}
return obj;
},
/**
* 獲得dom節點的text
*/
getNodeText:function (node) {
var funcHolder=arguments.callee.$;
// is this a text or CDATA node?
if (node.nodeType == funcHolder.TEXT_NODE || node.nodeType == funcHolder.CDATA_SECTION_NODE) {
return node.data;
}
var i;
var returnValue = [];
for (i = 0; i < node.childNodes.length; i++) {
//采用遞歸算法
returnValue.push(funcHolder.getNodeText(node.childNodes[i]));
}
return returnValue.join('');
}
}
{AjaxWrapper類的主要方法putRequest和callBackHandler}:
/**
* 以get的方式向server發送request
* @param url
* @param params
* @param callBackFunction 發送成功後回調的函數或者函數名
*/
putRequest:function(url,params,callBackFunction){
var funcHolder=arguments.callee.$;
var xmlHttp = new Ajax.Request(url,
{
method: 'get',
parameters: params,
requestHeaders:['my-header-encoding','utf-8'],
onFailure: function(){
alert('對不起,網絡通訊失敗,請重新刷新!');
},
onSuccess: function(transport){
},
onComplete: function(transport){
funcHolder.callBackHandler.apply(funcHolder,[transport,callBackFunction]);
}
});
},
/**
* 遠程調用的回調處理
* @param transport xmlhttp的transport
* @param callBackFunction 回調時call的方法,可以是函數也可以是函數名
*/
callBackHandler:function(transport,callBackFunction){
var funcHolder=arguments.callee.$;
if(transport.status!=200){
alert("獲得回應失敗,請求狀態:"+transport.status);
}
else{
funcHolder.xml_source=transport.responseText;
if (funcHolder.debug_flag)
alert('call callback function');
if (typeof(callBackFunction)=='function'){
if (funcHolder.debug_flag){
alert('invoke callbackFunc');
}
callBackFunction(transport.responseText);
}
else{
if (funcHolder.debug_flag){
alert('evalFunc callbackFunc');
}
new execute().evalFunc(callBackFunction,transport.responseText);
}
if (funcHolder.debug_flag)
alert('end callback function');
}
}
{頁面中主要的JavaScript方法:refreshUploadStatus和startProcess/cancelProcess}:
//刷新上傳狀態
function refreshUploadStatus(){
var ajaxW = new AjaxWrapper(false);
ajaxW.putRequest(
'./uploadStatus.action',
'uploadStatus=',
function(responseText){
var deserialor=new XMLDomForAjax(false);
var uploadInfo=deserialor.deserializedBeanFromXML(responseText);
var progressPercent = Math.ceil(
(uploadInfo.readTotalSize) / uploadInfo.uploadTotalSize * 100);
$('progressBarText').innerHTML = ' 上傳處理進度: '+progressPercent+'% ['+
(uploadInfo.readTotalSize)+'/'+uploadInfo.uploadTotalSize + ' bytes]'+
' 正在處理第'+uploadInfo.currentUploadFileNum+'個文件'+
' 耗時: '+(uploadInfo.processRunningTime-uploadInfo.processStartTime)+' ms';
$('progressStatusText').innerHTML=' 反饋狀態: '+uploadInfo.status;
$('totalProgressBarBoxContent').style.width = parseInt(progressPercent * 3.5) + 'px';
}
);
}
//上傳處理
function startProgress(){
Element.show('progressBar');
$('progressBarText').innerHTML = ' 上傳處理進度: 0%';
$('progressStatusText').innerHTML=' 反饋狀態:';
$('uploadButton').disabled = true;
var periodicalExe=new PeriodicalExecuter(refreshUploadStatus,2);
return true;
}
//取消上傳處理
function cancelProgress(){
$('cancelUploadButton').disabled = true;
var ajaxW = new AjaxWrapper(false);
ajaxW.putRequest(
'./uploadStatus.action',
'cancelUpload=true',
//因為form的提交,這可能不會執行
function(responseText){
var deserialor=new XMLDomForAjax(false);
var uploadInfo=deserialor.deserializedBeanFromXML(responseText);
$('progressStatusText').innerHTML=' 反饋狀態: '+uploadInfo.status;
if (msgInfo.cancel=='true'){
alert('刪除成功!');
window.location.reload();
};
}
);
}
運行界面:
起始頁面
上傳進行中…
上傳完成後的文件列表
用戶取消上傳後顯示的頁面
上傳過程中出錯(上傳文件過大)頁面源代碼下載:
AjaxUpload.zip相關鏈接: AJAX Upload progress monitor for Commons-FileUpload Example Apache Common FileUpload組件 Prototype官方網站 IBM的AJAX SlideShow應用 解開JavaScript生命的達芬奇密碼 Prototype.AjaxRequest的調用堆棧重寫問題感謝閱讀此文 請支持
cleverpig發起的