jsp檔案上傳並利用Ajax製作ProgressBar監控上傳進度

之前寫過一個jsp的檔案上傳,把檔案上傳的動作製成一個class。而現在要提供的這個範例就比較亂一點了,重點會在增加監控的ProgressListener及Ajax取得上傳進度之資料。

程式需要一個bean放入session裡,專門來存放目前上傳的bytes數及其它資訊,選擇好檔案按下上傳時,會開始上傳動作(使用iframe),並啟動Ajax與Server要求目前上傳進度狀況,把資訊顯示在browser上。

FileUpload2.jpg

這個專案需要使用二個額外的jar framework,分別為Apache FiluploadApache common io,而Fileupload需要1.2以上版本才支援ProgressListener。

存入上傳資訊的bean

package com.yslifes.file.upload;

public class UploadStatus {
    private long bytesRead;// 目前上傳byte數
    private long totBytes;// 總共的byte數
    private int item;// 目前上傳的item
    private long startTime;// 啟始時間

    public UploadStatus() {
        startTime = System.currentTimeMillis();
    }

    public long getBytesRead() {
        return bytesRead;
    }

    public void setBytesRead(long bytesRead) {
        this.bytesRead = bytesRead;
    }

    public long getTotBytes() {
        return totBytes;
    }

    public void setTotBytes(long totBytes) {
        this.totBytes = totBytes;
    }

    public int getItem() {
        return item;
    }

    public void setItem(int item) {
        this.item = item;
    }

    public long getStartTime() {
        return startTime;
    }

    public void setStartTime(long startTime) {
        this.startTime = startTime;
    }

}

監控的ProgressListener

在上傳時,會一直呼叫ProgressListener的update函數。

package com.yslifes.file.upload;

import org.apache.commons.fileupload.ProgressListener;

public class UploadListener implements ProgressListener {
    // 存放upload狀態的bean
    private UploadStatus status;

    public UploadListener(UploadStatus status) {
        this.status = status;
    }

    // override,上傳時會被呼叫
    @Override
    public void update(long arg0, long arg1, int arg2) {

        status.setBytesRead(arg0);// 目前已上傳的byte數
        status.setTotBytes(arg1);// 總byte數
        status.setItem(arg2);// 正在上傳的item編號
    }

}

上傳畫面html

onsubmit時順便啟動Ajax動作來監控上傳進度,而ProgressBar則是由取得的資料利用css的width來設定目前上傳量。

<body>

<form action="UploadAction.jsp" method="post"
    enctype="multipart/form-data" target="hiddenFrame"
    onsubmit="showStatus()">
<div align="center">
<table>
    <tr>
        <td>上傳檔案1</td>
        <td><input type="file" name="file1" /></td>
        <td>描述1</td>
        <td><input type="text" name="desc1" /></td>
    </tr>
    <tr>
        <td>上傳檔案2</td>
        <td><input type="file" name="file2" /></td>
        <td>描述2</td>
        <td><input type="text" name="desc2" /></td>
    </tr>
    <tr>
        <td colspan="4" align="left"><input id="button" type="submit"
            name="submit" value="確認" /></td>
    </tr>
</table>
<div id="status" style="display: none" align="left">
<div id="progressBar"
    style="width: 400px; height: 12px; background: #fff; border: 1px solid #00ff00; padding: 3px;">
<div id="barItem" style="width: 30%; height: 100%; background: #ff0000"></div>
</div>
<div id="msg"></div>
</div>
</div>
</form>

<iframe name="hiddenFrame" width="0" height="0" frameborder="0"></iframe>
</body>

Ajax的javascript內容

reuqest取得資料後,如果還沒上傳完成,則再利用setTimeout,一秒後重新再要求資料。

var success = true;//完成上傳與否
    //取得上傳資訊
    function showStatus() {
        success = false;
        //顯示ProgressBar
        document.getElementById("status").style.display = "block";
        //初始1%
        document.getElementById("barItem").style.width = '1%';
        //上傳按鈕使不能使用
        document.getElementById("button").disabled = true;
        //一秒檢查一次上傳狀態
        setTimeout("requestStatus()", 1000);
    }
    function requestStatus() {
        if (!success) {
            var ajax;
            if (window.XMLHttpRequest)//ajax request Object,非ie
                ajax = new XMLHttpRequest();
            else//ie
            {
                try {
                    ajax = new ActiveXObject("Mxsml2.XMLHTTP");
                } catch (e) {
                    ajax = new ActiveXObject("Microsoft.XMLHTTP");
                }
            }

            ajax.open("GET", "UploadStatus.jsp");
            ajax.onreadystatechange = function() {
                callback(ajax);
                
            };
            ajax.send(null);

        }
    }

    function callback(ajax) {
        if (ajax.readyState == 4) {
            if (ajax.status != 200)//有錯誤產生
            {
                document.getElementById("msg").innerHTML = "回傳錯誤!";
            } else {
                var arr = ajax.responseText.split("/");
                if (arr.length < 2) {
                    document.getElementById("msg").innerHTML = ajax.responseText;
                } else {
                    //進度
                    document.getElementById("barItem").style.width = ""
                            + arr[0] + "%";
                    document.getElementById("msg").innerHTML = '完成百分比(%):'
                            + arr[0] + '</br>'+
                    '已完成數(M):' + arr[1] + '</br>'+
                    '檔案總長度(M):' + arr[2] + '</br>'+
                    '傳輸速率(K):' + arr[3] + '</br>'+
                    '已使用時間(s):' + arr[4] + '</br>'+
                    '估計總時間(s):' + arr[5] + '</br>'+
                    '估計剩餘時間(s):' + arr[6] + '</br>'+
                    '正在上傳文件:' + arr[7];

                    if (arr[1] == arr[2]) {
                        success = true;
                        document.getElementById("msg").innerHTML = "完成上傳!";
                        //把上傳按鈕設可使用
                        document.getElementById("button").disabled = false;

                    }else
                        setTimeout("requestStatus()", 1000);
                }
            }
        }
    }

上傳動作

因為在form表單時已經設定target為iframe,所以並不會把目前頁面帶離,只會在iframe裡進行上傳動作,而iframe本身設定為看不到。

<%@ page language="java" contentType="text/html; charset=utf-8"
    pageEncoding="utf-8"%><%
    com.yslifes.file.upload.UploadStatus status = new com.yslifes.file.upload.UploadStatus();
    com.yslifes.file.upload.UploadListener listener = new com.yslifes.file.upload.UploadListener(status);
    
    //放到session裡
    //再來listener就會依上傳狀況去更動status了
    session.setAttribute("status",status);
    
    org.apache.commons.fileupload.servlet.ServletFileUpload upload = new org.apache.commons.fileupload.servlet.ServletFileUpload(new org.apache.commons.fileupload.disk.DiskFileItemFactory());
    upload.setProgressListener(listener);//設定監控
    
    try
    {
        java.util.List<org.apache.commons.fileupload.FileItem> items = upload.parseRequest(request);//把request parse成FileItem List
        java.util.Iterator<org.apache.commons.fileupload.FileItem> it = items.iterator();
        while(it.hasNext())
        {
            org.apache.commons.fileupload.FileItem item = it.next();
            if(item.isFormField())//一般的表單資料
            {
                System.out.println("表單資料名稱:"+item.getFieldName()+",表單資料內容:"+item.getString());
                
            }else//檔案上傳
            {
                System.out.println("檔案名稱:"+item.getName());
                String fileName = item.getName();
                
                //要存放檔案的路徑及目錄
                java.io.File saveFile = new java.io.File("./upload",fileName);
                //如果目錄不存在.則會自動建立
                saveFile.getParentFile().mkdirs();
                
                //讀出輸入串流
                java.io.InputStream in = item.getInputStream();
                
                //寫入輸出串流
                java.io.OutputStream writer = new java.io.FileOutputStream(saveFile);
                
                
                //寫入檔案
                byte[] tmp = new byte[1024];
                int len ;
                while((len=in.read(tmp))!=-1)
                {
                    writer.write(tmp,0,len);
                }
                
                writer.close();
                in.close();
                out.write("上傳成功!");
            }
        }
        
        
    }catch(org.apache.commons.fileupload.FileUploadException e)
    {
        e.printStackTrace();
        out.println("上傳時發生錯誤:"+e);
    }
    
    
    %>

取得監控的上傳資料

利用ajax要求這個程式來取得目前上傳的進度

<%@ page language="java" contentType="text/html; charset=utf-8"
    pageEncoding="BIG5"%>
<%
    //不讓browser使用cache資料
    response.setHeader("Cache-Control", "on-store");
    response.setHeader("Pagrma", "no-cache");
    response.setDateHeader("Expires", 0);

    //從session裡把資料取出來
    com.yslifes.file.upload.UploadStatus status = (com.yslifes.file.upload.UploadStatus) session
            .getAttribute("status");

    if (status == null) {
        out.println("未有上傳資料");
    } else {
        long startTime = status.getStartTime();
        long nowTime = System.currentTimeMillis();
        long time = (nowTime - startTime) / 1000 + 1;//上傳時間秒

        //總共的上傳資料bytes
        double totalLength = status.getTotBytes();

        //上傳送率 byte/s
        double velocity = status.getBytesRead() / (double) time;

        //估計總時間秒
        double totalTime = totalLength / velocity;
        //估計剩飲時間秒
        double guessTime = totalTime - time;

        //完成百分比
        int percent = (int) Math.round((100 * (double) status
                .getBytesRead()) / totalLength);
        //已讀取Mb數
        double M_Readlength = (double) status.getBytesRead() / 1024 / 1024;
        //總Mb數
        double m_TotalLength = (double) totalLength / 1024 / 1024;

        out.print(percent + "/");
        out.print(M_Readlength + "/");
        out.print(m_TotalLength + "/");
        out.print(velocity + "/");
        out.print(time + "/");
        out.print(totalTime + "/");
        out.print(guessTime + "/");
        out.print(status.getItem());

    }
%>

執行畫面,一開始狀況及按下確認後畫面

FileUpload1.jpg

FileUpload2.jpg

原始碼下載

Xuite WebHd|

13 thoughts to “jsp檔案上傳並利用Ajax製作ProgressBar監控上傳進度”

  1. 写的真好

    最近有个项目正好能用上了

    但是,有个问题啊
    session.setAttribute(“status”,status);
    这里,resin报错了,说status 一定要序列化哦。

    可能的话,加我q聊聊吧。。。二九601899

    1. 不好意思…不使用QQ
      你可以把status implements java.io.Serializable
      應該就可以

  2. public class UploadStatus implements Serializable

    已经可以了 ^_^

    THX

  3. public class UploadStatus implements Serializable

    請問這行要在哪邊改呢?

  4. 另外 我執行時候有發生一個問題

    第一次上傳 progress bar 不會動 但檔案會上傳

    refresh一次後 就都沒有問題

    請問這個要怎麼解決呢?

      1. @yku, 這個問題我也有看到, 好像傳40MB以上的檔案時, Ajax那邊的UploadStatus.jsp會變成另一個session, 以致於getAttribute回傳都為null, 所以status bean就沒有資料, progressbar就沒有資料可以進一步處理, 但檔案還是可上傳成功,
        如果是較小Size的檔案, 就不會出現這個問題, session也會跟做上傳動作的jsp是同一個session.
        還不知道為什麼@@….

        1. @Pati, 抱歉現在才回你
          以你的描述其實看不出來是什麼問題
          基本上session的控制在web container上,不太可能發生這種狀況
          是否您的browser有裝自動清除cookie或記憶體功能呢?
          再check您的程式看看,是否有動作reset session to null
          還是您可以提供您的程式碼呢?

          1. @yku, 感謝回覆, 你用大檔案可以複製我看到的現象嗎?下面是我的囉哩囉嗦, 呵~
            這個狀況,我用你的範例跟自己改的例子(不透過jsp調用自訂的Servlet Class, 直接利用HttpServlet的doGet/dePost),都會出現, 以下是用你提供的範例做的觀察,
            環境: Windows7 + Apache Tomcat/7.0.25
            Browser: IE 9/Chrome 22/Firefox 15

            1. 在第一次使用傳檔時, 選檔案大一點,40MB以上, 現象就會出現,
            在你的例子就會跑到out.println(“未有上傳資料”);
            傳檔的部份是沒問題的,還是可以成功跑完, 但就Progress狀態有問題,是null,
            這時到Tomcat的App Manager看, Application的session會出現兩個, 兩個創建時間幾乎同時,
            但其中一個的被使用時間(Used Time)為0, 也就是未被使用,
            這時再去使用傳檔, 一切看起來就都會正常, Progress狀態也有出現, 而且Progress狀態應該是用到剛剛那個沒有被用到的session, 因為Used Time有值了, 大概就是傳檔花費的時間, 而另一個第一次傳檔用到的session, 這次及之後也就沒有更新。

            2. 而當上傳檔案大小40MB以下時, 不會有這個問題,
            這時到Tomcat的App Manaer看, Application的session只有一個, 一切看來正常。

          2. @yku, 感謝回覆, 你用大檔案可以複製我看到的現象嗎?下面是我的囉哩囉嗦, 呵~
            這個狀況,我用你的範例跟自己改的例子(不透過jsp調用自訂的Servlet Class, 直接利用HttpServlet的doGet/dePost),都會出現,以下是用你提供的範例做的觀察,
            環境: Windows7 + Apache Tomcat/7.0.25
            Browser: IE 9/Chrome 22/Firefox 15

            1. 在第一次使用傳檔時, 選檔案大一點,40MB以上, 現象就會出現,
            在你的例子就會跑到out.println(“未有上傳資料”);
            傳檔的部份是沒問題的,還是可以成功跑完, 但就Progress狀態有問題,是null,
            這時到Tomcat的App Manager看, Application的session會出現兩個, 兩個創建時間幾乎同時,
            但其中一個的被使用時間(Used Time)為0, 也就是未被使用,
            這時再去使用傳檔, 一切看起來就都會正常, Progress狀態也有出現, 而且Progress狀態應該是用到剛剛那個沒有被用到的session, 因為Used Time有值了, 大概就是傳檔花費的時間, 而另一個第一次傳檔用到的session, 這次及之後也就沒有更新。

            2. 而當上傳檔案大小40MB以下時, 不會有這個問題,
            這時到Tomcat的App Manaer看, Application的session只有一個, 一切看來正常。

發表迴響