WELCOME to the Java 2, Enterprise Edition
(J2EE) Tech Tips, January 14, 2003. Here you'll get tips on using enterprise Java technologies and APIs, such as those in Java 2 Platform,
Enterprise Edition (J2EE).
This issue covers:
JavaServer Pages (JSP) Error Pages
Preventing Repeated Operations
These tips were developed using Java 2 SDK, Standard Edition, v 1.4 and Java 2 SDK, Enterprise Edition, v 1.3.1 (Reference Implementation).
This issue of the J2EE Tech Tips is written by Mark Johnson, president of elucify technical communications, and co-author of Designing Enterprise Applications with the Java 2, Enterprise Edition, 2nd Edition. Mark Johnson runs an open forum for discussion of the tips.
You can download the sample archive
for these tips. The context root for the application is ttjan2003, and the index.html welcome file indicates how to use the sample code. Any use of this code and/or information below is subject to the license terms: http://developer.java.sun.com/berkeley_license.html
JavaServer Pages (JSP) Error Pages
Few things make an application look less polished and
professional than a server's default exception page. Even the
most well-designed page usually shows the stack trace and
exception name, and makes your application look broken. Your
application might indeed have a bug, but that's no reason for it
to look bad, as well.
The J2EE platform offers a great deal of control over how to
format error information when a Web component throws an
unexpected exception. Ideally, every Web component properly
catches and handles all application and system exceptions that
can occur. But in the real world, accidents happen. The JSP error
page lets you catch, present, and report exceptions gracefully,
instead of presenting the user with a technical, and possibly
confusing, stack trace.
Defining An HTML Error Page
Web components such as servlets and JSP pages may throw
exceptions to indicate error conditions. Usually, a Web
component catches and handles its own exceptions, as well as
exceptions from the other components it uses. When a Web
component receives or generates an exception that it doesn't
catch, the exception is passed to the Web container (part of the
J2EE server). If an error page is defined for that exception, the
request is directed to the error page's URL.
The Web application deployment descriptor uses the
<error-page> tag to define Web components that handle errors. The
simplest way to handle an error is to define an HTML page for the
exception, and associate that HTML page with the exception it
reports. This example, from this month's sample code, shows how
to define an HTML page that reports a NullPointerException:
<!-- Catch a system error using an HTML page -->
<error-page>
<exception-type>java.lang.NullPointerException
</exception-type>
<location>/NPEerror.html</location>
</error-page>
|
This deployment descriptor entry means that whenever a Web
component throws a NullPointerException, the Web container serves
the document NPerror.html, which simply reports the error. The
page can be designed to have the same look and feel as the rest
of your application, so the user isn't confused or annoyed by a
sudden change in the interface.
The Web application isn't limited to system exceptions such as
NullPointerException, though. The next example in the sample Web
application deployment descriptor defines an error page for a
developer-defined application exception:
<!-- Catch a system error using an HTML page -->
<error-page>
<exception-type>
com.elucify.tips.jan2003.AppException1a
</exception-type>
<location>/AE1Aerror.html</location>
</error-page>
|
Note that in the Web deployment descriptor, you have to provide a
fully-specified class name for the exception. (In the sample
code, the application exception classes are all subclasses of
class "ApplicationException1", which extends java.lang.Exception.)
The source code for ApplicationException1a used above looks like
this:
package com.elucify.tips.jan2003;
public class AppException1a extends AppException1 {
public AppException1a() {
super("AppException1a");
}
}
|
A simple HTML page is fine for simple errors, but most of the
time, you want to provide information about what the problem
actually was. The way to do that is to report the error using
a JSP page.
Using JSP Pages As Error Pages
JSP pages, being Web components, allow you to report not only
that an error occurred, but also what went wrong. The deployment
descriptor definition for a JSP error page looks like this:
<!-- Report an error using a JSP page -->
<error-page>
<exception-type>
com.elucify.tips.jan2003.AppException1b
</exception-type>
<location>/AE1Berror.jsp</location>
</error-page>
|
The definition of the page is identical to the definition for an
HTML error page, except that the <location> defines a JSP page
instead. The JSP page that formats the error uses the <% @page %>
shown below:
<%@page isErrorPage="true" %>
<HTML>
<HEAD>
<TITLE>Application Error 1b</TITLE>
...
|
The string isErrorPage="true" causes the Web container to define
a new implicit variable, "exception", that the JSP page can use
in scriptlets or in custom tags. For example, the error page for
the current example uses the "exception" object to report the
name of the class that caused the exception:
<%= exception.getClass().getName() %>
Servlets and custom tags may access the exception object by
calling PageContext.getException(). (You'll learn more about
directing errors to servlets later in this tip.)
Letting A Page Define Its Error Page
Sometimes you want to take complete control of a page's exception
handling. The errorPage attribute of the @page directive provides a URL to a Web component that handles the exception reporting for
an individual page. If a JSP page defines errorPage, all
exceptions thrown are directed to that page, even if web.xml
specifically defines an error page for that page. In the example,
the JSP page ThrowAppException1c.jsp defines the file
"AE1Cerror.jsp" as its error page, as shown below. Keep in mind
that the page shown here throws the exception. The page that
reports the exception is AE1Cerror.html. (For the purposes of
formatting this tip, the <%@page ...> tag below is shown on two
lines. For JSP versions before 2.0, it needs to be on one line.)
<%@page import="com.elucify.tips.jan2003.AppException1c"
errorPage="AE1Cerror.html" %>
<%
if (true) {
throw new AppException1c();
}
%>
|
The @page attribute overrides all other exception handling except
explicit try/catch clauses in the source page.
Reporting Exception Superclasses
The J2EE platform documentation defines a large number of
exceptions. Writing a Web page to handle each one would quickly
become tedious. Fortunately, the J2EE platform allows you define
a single error page for a superclass of a group of extensions.
When the Web container receives an exception, it looks for error
page for that specific exception. If the Web container doesn't
find that error page, it looks for an error page for the
exception's superclass. It continues looking up the inheritance
tree until it finds an error page, or until it reaches Throwable.
In the the sample code, the Web deployment descriptor doesn't
define an error page for ApplicationException1d, but it does
define an error page for ApplicationException1:
<!-- Note absence of an error page -->
<!-- for Application Errors 1c and 1d -->
<!-- Catch a whole class of errors -->
<error-page>
<exception-type>
com.elucify.tips.jan2003.AppException1
</exception-type>
<location>/AE1error.jsp</location>
</error-page>
|
The code above defines the error page /AE1error.jsp for any
exception of type AppException1. This includes all subclasses of
AppException1 (such as AppException1d) that do not define an error page elsewhere. The error page here simply reports the
exception and exception type that occurred:
<%@page isErrorPage="true"%>
<HTML>
...
An Application Error 1 has occurred.<br>
The error class is
<%= exception.getMessage(); %>
...
|
Using Servlets As Error Pages
For maximum flexibility, even a servlet can serve as an error
page. A servlet error page has the same syntax as a JSP error
page. The <location> is just the servlet's <servlet-path>, as
defined in web.xml:
<!-- Catch errors by error code, -->
<!-- redirecting to a servlet -->
<error-page>
<error-code>404</error-code>
<location>/errorServlet</location>
</error-page>
|
Notice above that instead of <exception-type>, this tag defines
<error-code>. The Web can identify system errors in the Web tier
either by Java exception type, as you've seen before, or by HTTP
error code. The example above defines servlet /errorServlet as
the exception handler for 404 ("File Not Found") errors. Both the
<exception-type> and the <error-code> tags can be used with any Web resource in the application.
When the Web container receives or generates an exception or
system error, it initializes several variables as request
attributes. The sample code exception handler servlet
ErrorServlet reports the values of these attributes:
public class ErrorServlet extends HttpServlet {
...
protected static String[] vars = {
"javax.servlet.error.status_code",
"javax.servlet.error.exception_type",
"javax.servlet.error.message",
"javax.servlet.error.exception",
"javax.servlet.error.request_uri"
};
// Handle post request
public void doPost(HttpServletRequest req,
HttpServletResponse res)
throws IOException, ServletException {
...
// Print vars
for (int i = 0; i < vars.length; i++) {
out.println(
"<TR><TD>" + vars[i] + "</TD><TD>" +
req.getAttribute(vars[i]) +
"</TD></TR>");
}
...
}
}
|
The meanings of these attributes are defined in the Servlet 2.3
specification. If the source of the exception was a JSP page, the
Web container also stores the exception as a request attribute
called "javax.servlet.jsp.jspException" (ErrorServlet reports these, too.)
The ErrorServlet shown here might be better implemented as a
JSP page that uses custom tags instead of scriptlets. A more
common use of an error servlet is as an "error controller". An
error controller servlet could examine the exception or error
information it receives, log the information, send an email or
page to maintenance staff, and then forward the request to a
logical place in the application.
Using Error Pages From Servlets
So far, all of the examples shown demonstrate how to use error
pages when the source of the exception is a JSP page. However,
servlets can use existing error pages to format their exceptions
and errors, as well. The trick is to set the request attribute
"javax.servlet.jsp.jspException" to the exception you want
reported, and then forward the HTTP request to the URL for the
error page. The sample code example below forwards an exception
from one servlet (called ForwardingErrorServlet) to the
ErrorServlet from the previous example:
public class ForwardingErrorServlet
extends HttpServlet {
...
// Handle post request
public void doPost(HttpServletRequest req,
HttpServletResponse res)
throws IOException, ServletException {
// Create an exception to forward
// to an error page
try {
Object o = null;
Class c = o.getClass();
} catch (NullPointerException npe) {
req.setAttribute(
"javax.servlet.jsp.jspException",
npe);
RequestDispatcher rd =
req.getRequestDispatcher(
"/errorServlet");
rd.forward(req, res);
}
}
}
|
This example could just as easily forward to an HTML or JSP
page. You need to be careful when forwarding, though. If you open
a PrintWriter in a servlet and then try to forward, the forward
will fail.
There are several key points to remember when using the JSP error
page. The Web container always uses the most specific error page
defined for the page that caused the error. The error page may
be any Web component (not just HTML or JSP pages). Consider using
a servlet to log and/or report errors, and then forward to a JSP
page that formats the exceptions or errors.
Preventing Repeated Operations
Many online operations, especially those involving money, must
always happen either zero or one times. It's common for naive
users to click things in the user interface twice. For example,
an impatient user might click a Purchase button repeatedly. This
does not mean that the user wants to make the same purchase
multiple times. And consider the case where a user opens a new
browser window. To which window should the session belong?
This tip presents a simple strategy for ensuring that once the
server receives a POST request from an HTML form, it processes
that request exactly once. The key is to generate a token, that
is, a unique symbol that can be embedded as a hidden input in the
form being processed. The token may be a string or a simple
random number (as in the case shown in the sample code), it may
also include information such as the originating URL and the name
of the form. The servlet puts a copy of the token in a hidden
field in the form, and keeps a copy on the server. Later, when
the user POSTs the form to the server, the servlet tries to look
up the token. If it has a copy of the token, it deletes the
server-side copy of the token and performs the operation. If the
servlet can't find the token, then some other invocation of the
servlet must already have serviced that request, and the servlet
produces an error message.
The sample code, OneShotServlet.java, demonstrates this
technique. Its doGet method generates a token (a random number),
saves the token as a session attribute, and serves a simple form.
public void doGet(HttpServletRequest req,
HttpServletResponse res)
throws IOException, ServletException {
PrintWriter pw = res.getWriter();
res.setContentType("text/html");
int token = _random.nextInt();
int value = _random.nextInt(1000);
// Store token in session scope
HttpSession session = req.getSession();
String stoken = String.valueOf(token);
session.setAttribute(
stoken, String.valueOf(value));
// Create form, putting token in hidden field
pw.println(
"<HTML><HEAD><TITLE>One-Shot Servlet</TITLE></HEAD>");
pw.println("<BODY BACKGROUND=white>");
pw.println(
"Please type the following number into the box and " +
"click <i>Submit</i>: " + value);
pw.println(
"<form action=\"oneShotServlet\" " +
"method=\"post\">");
pw.println(
"<input type=\"hidden\" name=\"_token\" value=\"" +
stoken + "\">");
pw.println("<B>Input:</B>");
pw.println(
"<input type=text name=\"num\" width=\"40\"><br>");
pw.println("<input type=submit value=Submit>");
pw.println("</form></BODY></HTML>");
}
|
In the form, the servlet includes a hidden field that contains
the token string. The value of the session attribute is another
random number. The form gives the user a random number between
0 and 999 (the "value") and asks the user to copy the number into
a text input element in an HTML form. When the user clicks
Submit, the form calls back to the servlet's doPost method, shown
below:
// Handle post request
public void doPost(HttpServletRequest req,
HttpServletResponse res)
throws IOException, ServletException {
res.setContentType("text/html");
PrintWriter out = res.getWriter();
out.println(
"<HTML><HEAD><title>Demonstrate One Shot Servlet" +
"</TITLE></HEAD>");
out.println("<BODY>");
// Get the token, if it exists
String stoken = req.getParameter("_token");
HttpSession session = req.getSession();
String stored_value = null;
String value = req.getParameter("num");
// Remove stored token from session
synchronized (this) {
stored_value =
(String)session.getAttribute(stoken);
// Remove token from session
// so others can't use it,
// then get the number posted by the user
if (stored_value != null) {
session.removeAttribute(stoken);
}
}
// If the token is not there,
// it has already been "consumed"
if (stored_value == null) {
out.println(
"Sorry, you can't use that form more than once.");
} else {
if (stored_value.equals(value)) {
out.println(
"You copied the number correctly");
} else {
out.println(
"That wasn't the original number!");
}
out.println(
"<p>Now hit the BACK button on your browser and " +
"try to re-post the form");
}
out.println("</BODY></HTML>");
}
}
|
The doPost method gets the token from the hidden form field
(called "_token") and gets the value provided by the user
("num"). It then checks to see if the token named in the hidden
form variable still exists as a session attribute. If it does
exist, the servlet immediately removes it, and continues with the
business operation (comparing the original number with the number
the user typed). If the token does not exist, the servlet
responds with an error message. Notice that the combined
operation of finding and removing the token from session state is
synchronized. This avoids race conditions between multiple
clients using the same session.
Be aware that this servlet might not operate properly if the
servlet is marked "distributed", or if the servlet implements
SingleThreadModel. The "synchronized" block only prevents the code from being executed by multiple threads within the same JVM.
Distributed servlets can run in multiple JVMs, so in some cases,
race conditions can occur that would cause the servlet to operate
incorrectly. Implementing SingleThreadModel is discouraged, for
reasons explained in the Enterprise Java BluePrints.
This example uses a single servlet to demonstrate the concept in
a single source file. A real implementation would probably use
JSPs and custom tags, because that solution would be much more
maintainable, loggable, and reusable. Keeping the tokens in
session scope works well for small applications with low load,
that only run on one machine. If more than one machine is
involved, this solution requires distributed session state.
Another option would be to store the keys as entity beans with
exclusive locking.
Running the sample code
Download the
sample archive for these tips. The application's context
root is ttjan2003. The downloaded EAR file also contains the complete source code for the sample.
You can deploy the application archive (ttjan2003.ear) in the
J2EE Reference Implementation using the deploytool program:
$J2EE_HOME/deploytool -deploy ttjan2003.ear localhost
Replace localhost with the name of the host on which the server
is installed. For a standard installation on a single machine,
the hostname typically (and literally) is localhost. You
can access the application at http://localhost:8000/ttjan2003.
For a J2EE-compliant implementation other than the Reference
Implementation, use your J2EE product's deployment tools to
deploy the application on your platform.
See the index.html welcome file for instructions on running the application.
IMPORTANT: Please read our Terms of Use, Privacy, and Licensing policies:
http://www.sun.com/share/text/termsofuse.html
http://www.sun.com/privacy/
http://developer.java.sun.com/berkeley_license.html
Comments? Send your feedback on the Enterprise Java Technologies
Tech Tips to: jdc-webmaster@sun.com
Subscribe to other Java developer Tech Tips:
- Core Java Technologies Tech Tips. Get tips on using core
Java technologies and APIs, such as those in the Java 2
Platform, Standard Edition (J2SE).
- Wireless Developer Tech Tips. Get tips on using wireless
Java technologies and APIs, such as those in the Java 2
Platform, Micro Edition (J2ME).
To subscribe to these and other JDC publications:
- Go to the JDC Newsletters and Publications page,
choose the newsletters you want to subscribe to and click "Update".
- To unsubscribe, go to the subscriptions page, uncheck the appropriate checkbox, and click "Update".
ARCHIVES: You'll find the Enterprise Java Technologies Tech Tips archives at:
http://developer.java.sun.com/developer/EJTechTips/index.html
Copyright 2003 Sun Microsystems, Inc. All rights reserved. 4150 Network Circle, Santa Clara, CA 95054 USA.
This document is protected by copyright.
Sun, Sun Microsystems, Java, Java Developer Connection,
JavaServer Pages, JSP, J2SE, J2EE, and J2ME are trademarks or
registered trademarks of Sun Microsystems, Inc. in the
United States and other countries.
|