明輝手游網(wǎng)中心:是一個(gè)免費(fèi)提供流行視頻軟件教程、在線學(xué)習(xí)分享的學(xué)習(xí)平臺(tái)!

JSP編程進(jìn)度條設(shè)計(jì)案例

[摘要]許多Web應(yīng)用、企業(yè)應(yīng)用涉及到長(zhǎng)時(shí)間的操作,例如復(fù)雜的數(shù)據(jù)庫(kù)查詢或繁重的XML處理等,雖然這些任務(wù)主要由數(shù)據(jù)庫(kù)系統(tǒng)或中間件完成,但任務(wù)執(zhí)行的結(jié)果仍舊要借助JSP才能發(fā)送給用戶。本文介紹了一種通過(guò)改進(jìn)前端表現(xiàn)層來(lái)改善用戶感覺、減輕服務(wù)器負(fù)載的辦法。當(dāng)JSP調(diào)用一個(gè)必須長(zhǎng)時(shí)間運(yùn)行的操作,且該操作的結(jié)果...

許多Web應(yīng)用、企業(yè)應(yīng)用涉及到長(zhǎng)時(shí)間的操作,例如復(fù)雜的數(shù)據(jù)庫(kù)查詢或繁重的XML處理等,雖然這些任務(wù)主要由數(shù)據(jù)庫(kù)系統(tǒng)或中間件完成,但任務(wù)執(zhí)行的結(jié)果仍舊要借助JSP才能發(fā)送給用戶。本文介紹了一種通過(guò)改進(jìn)前端表現(xiàn)層來(lái)改善用戶感覺、減輕服務(wù)器負(fù)載的辦法。當(dāng)JSP調(diào)用一個(gè)必須長(zhǎng)時(shí)間運(yùn)行的操作,且該操作的結(jié)果不能(在服務(wù)器端)緩沖,用戶每次請(qǐng)求該頁(yè)面時(shí)都必須長(zhǎng)時(shí)間等待。很多時(shí)候,用戶會(huì)失去耐心,接著嘗試點(diǎn)擊瀏覽器的刷新按鈕,最終失望地離開。 本文介紹的技術(shù)是把繁重的計(jì)算任務(wù)分離開來(lái),由一個(gè)獨(dú)立的線程運(yùn)行,從而解決上述問題。當(dāng)用戶調(diào)用JSP頁(yè)面時(shí),JSP頁(yè)面會(huì)立即返回,并提示用戶任務(wù)已經(jīng)啟動(dòng)且正在執(zhí)行;JSP頁(yè)面自動(dòng)刷新自己,報(bào)告在獨(dú)立線程中運(yùn)行的繁重計(jì)算任務(wù)的當(dāng)前進(jìn)度,直至任務(wù)完成。
一、模擬任務(wù)
 首先我們?cè)O(shè)計(jì)一個(gè)TaskBean類,它實(shí)現(xiàn)java.lang.Runnable接口,其run()方法在一個(gè)由JSP頁(yè)面(start.jsp)啟動(dòng)的獨(dú)立線程中運(yùn)行。終止run()方法執(zhí)行由另一個(gè)JSP頁(yè)面stop.jsp負(fù)責(zé)。TaskBean類還實(shí)現(xiàn)了java.io.Serializable接口,這樣JSP頁(yè)面就可以將它作為JavaBean調(diào)用:
 package test.barBean;
import java.io.Serializable;
 public class TaskBean implements Runnable, Serializable {
    private int counter;
    private int sum;
    private boolean started;
    private boolean running;
    private int sleep;
     public TaskBean() {
        counter = 0;
        sum     = 0;
        started = false;
        running = false;
        sleep   = 100;
    }
}
 TaskBean包含的“繁重任務(wù)”是計(jì)算1+2+3…+100的值,不過(guò)它不通過(guò)100*(100+1)/2=5050公式計(jì)算,而是由run()方法調(diào)用work()方法100次完成計(jì)算。work()方法的代碼如下所示,其中調(diào)用Thread.sleep()是為了確保任務(wù)總耗時(shí)約10秒。
 protected void work() {
    try {
        Thread.sleep(sleep);
        counter++;
        sum += counter;
    } catch (InterruptedException e) {
        setRunning(false);
    }
}
 
status.jsp頁(yè)面通過(guò)調(diào)用下面的getPercent()方法獲得任務(wù)的完成狀況:
 public synchronized int getPercent() {
    return counter;
}如果任務(wù)已經(jīng)啟動(dòng),isStarted()方法將返回true:
public synchronized boolean isStarted() {
    return started;
}
 
 如果任務(wù)已經(jīng)完成,isCompleted()方法將返回true:
 public synchronized boolean isCompleted() {
    return counter == 100;
}
 
 如果任務(wù)正在運(yùn)行,isRunning()方法將返回true:
 public synchronized boolean isRunning() {
    return running;
}
 
SetRunning()方法由start.jsp或stop.jsp調(diào)用,當(dāng)running參數(shù)是true時(shí)。SetRunning()方法還要將任務(wù)標(biāo)記為“已經(jīng)啟動(dòng)”。調(diào)用setRunning(false)表示要求run()方法停止執(zhí)行。
 public synchronized void setRunning(boolean running) {
    this.running = running;
    if (running)
        started  = true;
}
 
任務(wù)執(zhí)行完畢后,調(diào)用getResult()方法返回計(jì)算結(jié)果;如果任務(wù)尚未執(zhí)行完畢,它返回null:
 public synchronized Object getResult() {
    if (isCompleted())
        return new Integer(sum);
    else
        return null;
}
 
當(dāng)running標(biāo)記為true、completed標(biāo)記為false時(shí),run()方法調(diào)用work()。在實(shí)際應(yīng)用中,run()方法也許要執(zhí)行復(fù)雜的SQL查詢、解析大型XML文檔,或者調(diào)用消耗大量CPU時(shí)間的EJB方法。注意“繁重的任務(wù)”可能要在遠(yuǎn)程服務(wù)器上執(zhí)行。報(bào)告結(jié)果的JSP頁(yè)面有兩種選擇:或者等待任務(wù)結(jié)束,或者使用一個(gè)進(jìn)度條。
 public void run() {
    try {
        setRunning(true);
        while (isRunning() && !isCompleted())
            work();
    } finally {
        setRunning(false);
    }
}二、啟動(dòng)任務(wù)

 start.jsp是web.xml部署描述符中聲明的歡迎頁(yè)面,web.xml的內(nèi)容是:
 <?xml version="1.0" encoding="GB2312"?>
 <!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">
 <web-app>
    <welcome-file-list>
        <welcome-file>start.jsp</welcome-file>
    </welcome-file-list>
</web-app>
 start.jsp啟動(dòng)一個(gè)專用的線程來(lái)運(yùn)行“繁重的任務(wù)”,然后把HTTP請(qǐng)求傳遞給status.jsp。
 start.jsp頁(yè)面利用<jsp:useBean>標(biāo)記創(chuàng)建一個(gè)TaskBean的實(shí)例,將scope屬性定義為session使得對(duì)于來(lái)自同一瀏覽器的HTTP請(qǐng)求,其他頁(yè)面也能提取到同一個(gè)Bean對(duì)象。start.jsp通過(guò)調(diào)用session.removeAttribute("task")確保<jsp:useBean>創(chuàng)建了一個(gè)新的Bean對(duì)象,而不是提取一個(gè)舊對(duì)象(例如,同一個(gè)用戶會(huì)話中更早的JSP頁(yè)面所創(chuàng)建的Bean對(duì)象)。
 下面是start.jsp頁(yè)面的代碼清單:
 <% session.removeAttribute("task"); %>
 <jsp:useBean id="task" scope="session"
    class="test.barBean.TaskBean"/>
 <% task.setRunning(true); %>
<% new Thread(task).start(); %>
<jsp:forward page="status.jsp"/>
 
start.jsp創(chuàng)建并設(shè)置好TaskBean對(duì)象之后,接著創(chuàng)建一個(gè)Thread,并將Bean對(duì)象作為一個(gè)Runnable實(shí)例傳入。調(diào)用start()方法時(shí)新創(chuàng)建的線程將執(zhí)行TaskBean對(duì)象的run()方法。
 現(xiàn)在有兩個(gè)線程在并發(fā)執(zhí)行:執(zhí)行JSP頁(yè)面的線程(稱之為“JSP線程”),由JSP頁(yè)面創(chuàng)建的線程(稱之為“任務(wù)線程”)。接下來(lái),start.jsp利用調(diào)用status.jsp,status.jsp顯示出進(jìn)度條以及任務(wù)的執(zhí)行情況。注意status.jsp和start.jsp在同一個(gè)JSP線程中運(yùn)行。
 start.jsp在創(chuàng)建線程之前就把TaskBean的running標(biāo)記設(shè)置成了true,這樣,即使當(dāng)JSP線程已開始執(zhí)行status.jsp而任務(wù)線程的run()方法尚未啟動(dòng),也能夠確保用戶會(huì)得到“任務(wù)已開始運(yùn)行”的狀態(tài)報(bào)告。
 將running標(biāo)記設(shè)置成true、啟動(dòng)任務(wù)線程這兩行代碼可以移入TaskBean構(gòu)成一個(gè)新的方法,然后由JSP頁(yè)面調(diào)用這個(gè)新方法。一般而言,JSP頁(yè)面應(yīng)當(dāng)盡量少用Java代碼,即我們應(yīng)當(dāng)盡可能地把Java代碼放入Java類。不過(guò)本例中我們不遵從這一規(guī)則,把new Thread(task).start()直接放入start.jsp突出表明JSP線程創(chuàng)建并啟動(dòng)了任務(wù)線程。
 在JSP頁(yè)面中操作多線程必須謹(jǐn)慎,注意JSP線程和其它線程實(shí)際上是并發(fā)執(zhí)行的,就象在桌面應(yīng)用程序中,我們用一個(gè)線程來(lái)處理GUI事件,另外再用一個(gè)或多個(gè)線程來(lái)處理后臺(tái)任務(wù)。不過(guò)在JSP環(huán)境中,考慮到多個(gè)用戶同時(shí)請(qǐng)求某一個(gè)頁(yè)面的情況,同一個(gè)JSP頁(yè)面可能會(huì)在多個(gè)線程中同時(shí)運(yùn)行;另外,有時(shí)同一個(gè)用戶可能會(huì)向同一個(gè)頁(yè)面發(fā)出多個(gè)請(qǐng)求,雖然這些請(qǐng)求來(lái)自同一個(gè)用戶,它們也會(huì)導(dǎo)致服務(wù)器同時(shí)運(yùn)行一個(gè)JSP頁(yè)面的多個(gè)線程。三、任務(wù)進(jìn)度
status.jsp頁(yè)面利用一個(gè)HTML進(jìn)度條向用戶顯示任務(wù)的執(zhí)行情況。首先,status.jsp利用<jsp:useBean>標(biāo)記獲得start.jsp頁(yè)面創(chuàng)建的Bean對(duì)象:
 <jsp:useBean id="task" scope="session"
    class="test.barBean.TaskBean"/>
 為了及時(shí)反映任務(wù)執(zhí)行進(jìn)度,status.jsp會(huì)自動(dòng)刷新。JavaScript代碼setTimeout("location='status.jsp'", 1000)將每隔1000毫秒刷新頁(yè)面,重新請(qǐng)求status.jsp,不需要用戶干預(yù)。
 <HTML>
 <HEAD>
    <TITLE>JSP進(jìn)度條</TITLE>
    <% if (task.isRunning()) { %>
        <SCRIPT LANGUAGE="JavaScript">
            setTimeout("location='status.jsp'", 1000);
        </SCRIPT>
    <% } %>
</HEAD>
 <BODY>
 進(jìn)度條實(shí)際上是一個(gè)HTML表格,包含10個(gè)單元——即每個(gè)單元代表任務(wù)總體的10%進(jìn)度。
 <H1 ALIGN="CENTER">JSP進(jìn)度條</H1>
     <H2 ALIGN="CENTER">
        結(jié)果: <%= task.getResult() %><BR>
        <% int percent = task.getPercent(); %>
        <%= percent %>%
    </H2>
     <TABLE WIDTH="60%" ALIGN="CENTER"
            BORDER=1 CELLPADDING=0 CELLSPACING=2>
        <TR>
            <% for (int i = 10; i <= percent; i += 10) { %>
                <TD WIDTH="10%" BGCOLOR="#000080">&nbsp;</TD>
            <% } %>
            <% for (int i = 100; i > percent; i -= 10) { %>
                <TD WIDTH="10%">&nbsp;</TD>
            <% } %>
        </TR>
    </TABLE>
 
任務(wù)執(zhí)行情況分下面幾種狀態(tài):正在執(zhí)行,已完成,尚未開始,已停止:
 <TABLE WIDTH="100%" BORDER=0 CELLPADDING=0 CELLSPACING=0>
        <TR>
            <TD ALIGN="CENTER">
                <% if (task.isRunning()) { %>
                    正在執(zhí)行
                <% } else { %>
                    <% if (task.isCompleted()) { %>
                        完成
                    <% } else if (!task.isStarted()) { %>
                        尚未開始
                    <% } else { %>
                        已停止
                    <% } %>
                <% } %>
            </TD>
        </TR>頁(yè)面底部提供了一個(gè)按鈕,用戶可以用它來(lái)停止或重新啟動(dòng)任務(wù):
 <TR>
            <TD ALIGN="CENTER">
                <BR>
                <% if (task.isRunning()) { %>
                    <FORM METHOD="GET" ACTION="stop.jsp">
                        <INPUT TYPE="SUBMIT" VALUE="停止">
                    </FORM>
                <% } else { %>
                    <FORM METHOD="GET" ACTION="start.jsp">
                        <INPUT TYPE="SUBMIT" VALUE="開始">
                    </FORM>
                <% } %>
            </TD>
        </TR>
    </TABLE>
</BODY></HTML>
 只要不停止任務(wù),約10秒后瀏覽器將顯示出計(jì)算結(jié)果5050:
 四、停止任務(wù)
 stop.jsp頁(yè)面把running標(biāo)記設(shè)置成false,從而停止當(dāng)前的計(jì)算任務(wù):
 <jsp:useBean id="task" scope="session"
    class="test.barBean.TaskBean"/>
 <% task.setRunning(false); %>
<jsp:forward page="status.jsp"/>
 
注意最早的Java版本提供了Thread.stop方法,但JDK從1.2版開始已經(jīng)不贊成使用Thread.stop方法,所以我們不能直接調(diào)用Thread.stop()。
 第一次運(yùn)行本文程序的時(shí)候,你會(huì)看到任務(wù)的啟動(dòng)有點(diǎn)延遲;同樣地,第一次點(diǎn)擊“停止”按鈕時(shí)也可以看到任務(wù)并沒有立即停止運(yùn)行(特別是如果機(jī)器配置較低的話,延遲的感覺更加明顯),這些延遲都是由于編譯JSP頁(yè)面導(dǎo)致的。編譯好JSP頁(yè)面之后,應(yīng)答速度就要快多了。
 五、實(shí)際應(yīng)用
 進(jìn)度條不僅使得用戶界面更加友好,而且對(duì)服務(wù)器的性能也有好處,因?yàn)檫M(jìn)度條會(huì)不斷地告訴用戶當(dāng)前的執(zhí)行進(jìn)度,用戶不會(huì)再頻繁地停止并重新啟動(dòng)(刷新)當(dāng)前的任務(wù)。另一方面,創(chuàng)建單獨(dú)的線程來(lái)執(zhí)行后臺(tái)任務(wù)也會(huì)消耗不少資源,必要時(shí)可考慮通過(guò)一個(gè)線程池來(lái)實(shí)現(xiàn)Thread對(duì)象的重用。另外,頻繁地刷新進(jìn)度頁(yè)面也增加了網(wǎng)絡(luò)通信開銷,所以務(wù)必保持進(jìn)度頁(yè)面簡(jiǎn)潔短小。
 在實(shí)際應(yīng)用中,后臺(tái)執(zhí)行的繁重任務(wù)可能不允許停止,或者它不能提供詳細(xì)的執(zhí)行進(jìn)度數(shù)據(jù)。例如,查找或更新關(guān)系數(shù)據(jù)庫(kù)時(shí),SQL命令執(zhí)行期間不允許中途停止——不過(guò)如果用戶表示他想要停止或中止任務(wù),程序可以在SQL命令執(zhí)行完畢后回退事務(wù)。
 解析XML文檔的時(shí)候,我們沒有辦法獲知已解析內(nèi)容精確的百分比。如果用DOM解析XML文檔,直到解析完成后才得到整個(gè)文檔樹;如果用SAX,雖然可以知道當(dāng)前解析的內(nèi)容,但通常不能確定還有多少內(nèi)容需要解析。在這些場(chǎng)合,任務(wù)的執(zhí)行進(jìn)度只能靠估計(jì)得到。
 估計(jì)一個(gè)任務(wù)需要多少執(zhí)行時(shí)間通常是很困難的,因?yàn)樗婕暗皆S多因素,即使用實(shí)際測(cè)試的辦法也無(wú)法得到可靠的結(jié)論,因?yàn)榉⻊?wù)器的負(fù)載隨時(shí)都在變化之中。一種簡(jiǎn)單的辦法是測(cè)量任務(wù)每次執(zhí)行所需時(shí)間,然后根據(jù)最后幾次執(zhí)行的平均時(shí)間估算。如果要提高估計(jì)時(shí)間的精確度,應(yīng)當(dāng)考慮實(shí)現(xiàn)一種針對(duì)應(yīng)用特點(diǎn)的算法,綜合考慮多種因素,例如要執(zhí)行的SQL語(yǔ)句類型、要解析的XML模式的復(fù)雜程度,等等。
 結(jié)束語(yǔ):本文例子顯示出用JSP、Java、HTML和JavaScript構(gòu)造進(jìn)度條是相當(dāng)容易的,真正困難的是如何將它用到實(shí)際應(yīng)用之中,特別是獲得后臺(tái)任務(wù)的進(jìn)度信息,但這個(gè)問題沒有通用的答案,每一種后臺(tái)執(zhí)行的任務(wù)都有它自己的特點(diǎn),必須按照具體情況具體分析。 (出處:PConline)