Many web and enterprise applications must perform CPU-intensive operations, such as complex database queries or heavy XML processing. These tasks are handled by database systems or middleware components, but the results are presented to the user with the help of JSP. This article shows how to implement the front tier in order to improve the user experience and reduce the server load.
When a JSP invokes an expensive operation whose result cannot be cached (on the server side), the user will have to wait each time he requests the page. Eventually, the user will lose his patience and he'll click the browser's Refresh or Reload button. This leads to ignored page requests that do heavy processing.
This article's example solves the problem described above by starting the heavy task in a separate thread. The JSP page returns an immediate response indicating that the task was started and is running. From time to time, the page is reloaded automatically in order to report the progress of the heavy task that continues to run in its own thread until it is completed. The user may stop the task only if the application allows such an action. In the end, the JSP page reports the result.
This section presents the TaskBean class, which implements
java.lang.Runnable so that its run() method can be
executed in a thread that is started from a JSP page (named
start.jsp). Another JSP page (named stop.jsp) will
let the user stop the run() method's execution. The
TaskBean class also implements the
java.io.Serializable interface so that it can be used as a
JavaBean in the JSP pages:
package com.devsphere.articles.progressbar;
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;
}
}
The "heavy task" implemented by TaskBean computes 1 + 2 + ... +
100. Instead of using the well-known formula for computing this sum -- 100 * (100
+ 1) / 2 = 5050 -- the run() method calls work() 100 times. See its code below. The Thread.sleep() call makes
sure that the task will take about 10 seconds.
protected void work() {
try {
Thread.sleep(sleep);
counter++;
sum += counter;
} catch (InterruptedException e) {
setRunning(false);
}
}
The status.jsp page calls the following methods to find out the
task status. The getPercent() method tells how much of the task
was executed:
public synchronized int getPercent() {
return counter;
}
The isStarted() method returns true if the task
was started:
public synchronized boolean isStarted() {
return started;
}
The isCompleted() method returns true if the task
was completed:
public synchronized boolean isCompleted() {
return counter == 100;
}
The isRunning() method returns true if the task is
still running:
public synchronized boolean isRunning() {
return running;
}
The setRunning() method is called by start.jsp and
stop.jsp. When the running parameter is
true, the task is also marked as started. Calling
setRunning(false) will tell the run() method to stop
its execution.
public synchronized void setRunning(boolean running) {
this.running = running;
if (running)
started = true;
}
The getResult() method returns the computed sum if the task was
completed. Otherwise, it returns null:
public synchronized Object getResult() {
if (isCompleted())
return new Integer(sum);
else
return null;
}
The run() method calls work() as long as the
running flag is true and completed is
false. A real application's run() method would execute a complex
SQL query, parse a large XML document, or invoke a CPU-intensive EJB method.
Note that the heavy task might be executed on a remote server. The JSP page
reporting the result has two choices: to wait for the task completion or to use
a progress bar.
public void run() {
try {
setRunning(true);
while (isRunning() && !isCompleted())
work();
} finally {
setRunning(false);
}
}
To run the example, download the article's source code archive, unzip it,
deploy the progressbar.war web application and open
http://localhost:8080/progressbar/ in your browser. This will
invoke start.jsp, which is the application's welcome page declared
in the web.xml descriptor:
<?xml version="1.0" encoding="ISO-8859-1"?>
<!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>
The start.jsp page starts a thread to run the "heavy task," and then it forwards the HTTP request to status.jsp, whose response looks like Figure 1:
Figure 1. A running task
The start.jsp page creates a TaskBean instance
using the <jsp:useBean> tag. The session scope
allows other pages to retrieve the same bean object when the HTTP requests are
coming from the same browser.
The page calls session.removeAttribute("task") to make sure
that <jsp:useBean> creates a new bean object rather than
retrieving an old object, which might have been created by an earlier JSP
request during the same user session.
Here is the code of the start.jsp page:
<% session.removeAttribute("task"); %>
<jsp:useBean id="task" scope="session"
class="com.devsphere.articles.progressbar.TaskBean"/>
<% task.setRunning(true); %>
<% new Thread(task).start(); %>
<jsp:forward page="status.jsp"/>
|
Related Reading
|
After creating the TaskBean object, start.jsp
creates a Thread, passing the bean object as a
Runnable instance. The start() method causes the
newly created thread to execute the run() method of the
TaskBean object.
There are now two threads running concurrently: the thread that executes the
JSP page (the "JSP thread") and the thread created by the JSP page (the "task
thread"). The start.jsp page uses <jsp:forward>
to invoke status.jsp, which shows the progress bar and the task
status. Both JSP pages are executed in the same JSP thread.
Setting the running flag to true in
start.jsp makes sure that the "running" status is reported to the
user even if the task thread's run() method doesn't get started by
the time when the status.jsp page is executed in the JSP
thread.
The two lines of Java code that set the running flag to
true and start the task thread could be moved into the
TaskBean class to form a new method that would be called from the
JSP page. It usually is a good idea to move as much code as possible into the
Java classes. In the case of this example, placing new
Thread(task).start() into start.jsp makes very clear that
the JSP thread creates and starts the task thread.
You must be careful when dealing with threads in JSP pages. Keep in mind that your threads and the JSP threads are executed concurrently. This is similar to desktop programming, when you have a thread processing GUI events and other threads executing application-specific tasks. However, in the JSP environment, the same page may be executed in multiple threads on behalf of multiple users. There are also situations when the same user makes multiple requests to the same page and those HTTP requests may be served concurrently, even though they are coming from the same user.
|
The status.jsp page uses an HTML-based progress bar to inform
the user about the status of the task. The <jsp:useBean> tag
gets the bean object created by start.jsp:
<jsp:useBean id="task" scope="session"
class="com.devsphere.articles.progressbar.TaskBean"/>
The status.jsp page is refreshed automatically from time to
time in order to reflect the status changes. The
setTimeout("location='status.jsp'", 1000) JavaScript code changes
the browser's location after 1000 milliseconds. The browser will request the
status.jsp page without any user intervention.
<html>
<head>
<title>JSP Progress Bar</title>
<% if (task.isRunning()) { %>
<SCRIPT LANGUAGE="JavaScript">
setTimeout("location='status.jsp'", 1000);
</SCRIPT>
<% } %>
</head>
<body>
The progress bar is built as an HTML table with 10 cells. Each cell represents 10% of the time that is necessary to execute the task.
<h1 align="center">JSP Progress Bar</h1>
<h2 align="center">
Result: <%= 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"> </td>
<% } %>
<% for (int i = 100; i > percent; i -= 10) { %>
<td width="10%"> </TD>
<% } %>
</tr>
</table>
The task may be in one of the following states: "Running," "Completed," "Not Started," and "Stopped."
<table width="100%" border="0" cellpadding="0" cellspacing="0">
<tr>
<td align="center">
<% if (task.isRunning()) { %>
Running
<% } else { %>
<% if (task.isCompleted()) { %>
Completed
<% } else if (!task.isStarted()) { %>
Not Started
<% } else { %>
Stopped
<% } %>
<% } %>
</td>
</tr>
At the end of the page, there is a button that lets the user stop or restart the task:
<tr>
<td align="center">
<br />
<% if (task.isRunning()) { %>
<form method="get" action="stop.jsp">
<input type="submit" value="Stop">
</form>
<% } else { %>
<form method="get" action="start.jsp">
<input type="submit" value="Start">
</form>
<% } %>
</td>
</tr>
</table>
</body>
</html>
If you don't stop the task, you'll see the 5050 result in about 10 seconds:

Figure 2. The completed task
The stop.jsp page stops the execution of the task by setting
the running flag to false:
<jsp:useBean id="task" scope="session"
class="com.devsphere.articles.progressbar.TaskBean"/>
<% task.setRunning(false); %>
<jsp:forward page="status.jsp"/>
Note that it is not okay to call the Thread.stop() method,
which was provided by the first Java version but was deprecated by JDK 1.2.
The task should get the chance to stop itself by returning from the
run() method when it is appropriate. Otherwise, the
TaskBean object might remain in an invalid state.

Figure 3. A stopped task
When you execute the application the first time, you'll notice some delay when the task is started. Also, when you click the "Stop" button for the first time, the task won't be stopped immediately. The JSP page compilation causes these delays. Once the JSP pages are compiled, the application will respond very quickly to the user requests (assuming that the server isn't overloaded and the network is working properly).
A progress bar makes the user interface friendlier and may have a good
effect on the server performance, because users won't cancel and restart an
operation that tells them to be patient and shows them the progress.
However, the threads created to execute the tasks consume system resources. You
should consider using a thread pool that allows you to reuse the
Thread objects. Refreshing the status page from time to time also
increases the network traffic. Keep that page small and simple.
In many real-world cases, the heavy task cannot be stopped or its progress cannot be tracked. For example, when accessing or updating a relational database, you cannot stop an SQL statement in the middle of its execution. However, if the user has signaled that she wants to stop or to cancel the task, you could roll back the database transaction after the SQL statement is executed.
When you parse an XML document, there's no way to show the exact percentage of the parsed content. If you use DOM, you get the whole document tree after the parsing is completed. If you use SAX, you know what was parsed, but you usually don't know much about the remaining content that has to be parsed. In such a case, you can only try to estimate the progress of the heavy task.
Estimating the time necessary to execute a heavy task can be difficult, because it depends on the available CPU resources. You can't just do a few tests in the production environment because the server load changes in time. A simple solution is to measure the execution time of the heavy task every time it is invoked, and then use the average time of the last few executions as an estimate. For more accurate estimate, you should think of an algorithm that takes into account variables specific to your application, such as the type of the SQL statement that is executed, or the complexity of the parsed document's XML schema.
The web application of this article has shown you how easy is to implement a progress bar based on JSP, Java, HTML, and JavaScript. The hard part is to integrate it within existing real applications, taking into account the issues described above. There is no general integration solution. Each real heavy task needs to be analyzed separately.
Andrei Cioroianu is the founder of Devsphere and an author of many Java articles published by ONJava, JavaWorld, and Java Developer's Journal.
Return to ONJava.com.
Copyright © 2009 O'Reilly Media, Inc.