当前页面: 开发资料首页 → J2ME 专题 → End-to-End J2ME Application Development by Example - Introducing Smart Ticket
摘要: End-to-End J2ME Application Development by Example - Introducing Smart Ticket
by Norman Richards and Michael Yuan
June 2003
The first Java technology blueprint, Java Pet Store, was released in 2001 as a showcase for Sun's J2EE technologies. The blueprint not only provides sample code for a multilayered, database-driven e-commerce application, it also furnishes design guidelines and demonstrates commonly used patterns. Since that first release, the Java blueprints have become one of the most important resources for developers wanting to learn the latest J2EE technologies and best practices.
The Smart Ticket blueprint adds a new dimension: mobility. It demonstrates how to build a complete end-to-end mobile commerce system for ordering movie tickets, using J2ME MIDP for a wireless front end and a J2EE application server and a relational database at the back end. Studying how this application is designed and built will greatly enhance your understanding of the problems of mobile enterprise applications – and their solutions.
This article covers version 2.0 Early Access of the Smart Ticket code, released in April 2003. The screen shots and code samples in the early-access version may change slightly in the final release, but the lessons you learn from the design should still hold. Smart Ticket 1.2 is still available. It has the same model and back-end implementation as the version discussed here, so many of the details are applicable to both past and future releases. Unless otherwise noted, all source code in this article is copyrighted by Sun Microsystems.
The Smart Ticket application is available from Sun's Blueprints web site. The .zip archive contains source code, Ant build scripts, and pre-built, deployable applications.
Smart Ticket contains a J2ME component and a J2EE component. Running it requires a J2EE application server (such as the Sun J2EE reference implementation, version 1.3 or higher), and either a MIDP 2.0-compatible device with Internet connectivity or a suitable emulator, such as the one in Sun's J2ME Wireless Toolkit 2.0. The Smart Ticket distribution contains specific instructions for building and deploying the application. To get started:
Make sure you have these resources installed:
Set the following environment variables:
JAVA_HOME
: JDK installation directory
J2EE_HOME
: J2EE RI installation directory
J2MEWTK_HOME
: J2ME Wireless Toolkit installation directory Start the J2EE server:
<table class=grey4 cellSpacing=0 cellPadding=10 width="100%" border=0> <tr> <td>J2EE_HOME/bin/cloudscape -start J2EE_HOME/bin/j2ee -verbose</td></tr></table>
Deploy the J2EE application. Use the
setup
script as follows to invoke thedeploy
Ant task insetup.xml
:<table class=grey4 cellSpacing=0 cellPadding=10 width="100%" border=0> <tr> <td>setup deploy</td></tr></table>
Point your browser to
http://localhost:8000/smartticket
and click on the Populate Database link to import mock theater and movie data into the database. This is a very slow process on older computers, so be patient! The mock data includes theaters in two Zip codes: 95054 and 95130.Start J2ME Wireless Toolkit 2.0 and run the MIDlet described in
smart_ticket-client.jad
.
When you have the MIDlet running, take a brief tour to get the user's perspective. You'll find you can perform four kinds of tasks.
Older mobile-commerce platforms such as the WAP/WML-based micro-browsers put all the intelligence on the server side. A key benefit of J2ME is that it supports smart clients that run on devices. Smart Ticket capitalizes fully on the advantages of the smart-client application paradigm:
How are those features implemented?
The overall architecture of the Smart Ticket application follows the Model-View-Controller pattern. The application is separated into several logical layers, so developers can change one part without affecting others. Smart Ticket adopts the MVC model as follows:
View: Each view class displays an interactive UI screen and waits for user input. When the user generates a UI event by pressing a button, or selecting an item from a list, the view class's event handler captures the event and passes control to the controller class. Most classes in the com.sun.j2me.blueprints.smartticket.client.midp.ui
package are view classes.
<table class=grey4 cellSpacing=0 cellPadding=10 width="100%" border=0> <tr> <td>public class ChooseMovieUI extends Form implements CommandListener, ItemStateListener, ItemCommandListener { private UIController uiController; // ... public void commandAction(Command command, Displayable displayable) { uiController.commandAction(command, displayable); } public void commandAction(Command command, Item item) { if (command == selectSeatsCommand) { if (numOfTickets.getString().length() == 0 || Integer.parseInt(numOfTickets.getString()) < 1) { uiController.showErrorAlert( uiController.getString( UIConstants.NUM_OF_TICKET_ERR)); } else { uiController.selectSeatsSelected( movieSchedules[movieList.getSelectedIndex()], getShowTimes()); } } } }</td></tr></table>
Controller: The controller class knows all the possible interactions between the user and the program. In Smart Ticket, the
UIController
class has one method for each possible action; for example,purchaseRequested()
. The action method often starts two new threads, one to perform the action in the background and the other to display a progress bar for the user. The action thread is represented by theEventDispatcher
class, whoserun()
method contains a long switch statement that performs the action requested by invoking appropriate methods in the model layer. When the last-called of these methods returns, the controller initiates and displays the next UI screen.<table class=grey4 cellSpacing=0 cellPadding=10 width="100%" border=0> <tr> <td>package com.sun.j2me.blueprints.smartticket.client.midp.ui; public class UIController { // references to all UI classes // ... public UIController(MIDlet midlet, ModelFacade model) { this.display = Display.getDisplay(midlet); this.model = model; } // ... public void selectSeatsSelected(TheaterSchedule.MovieSchedule movieSchedule, int[] showTime) { selectedShowTime = showTime; selectedMovie = movieSchedule.getMovie(); selectedMovieSchedule = movieSchedule; runWithProgress( new EventDispatcher(EventIds.EVENT_ID_SELECTSEATSSELECTED, mainMenuUI), getString(UIConstants.PROCESSING), false); } class EventDispatcher extends Thread { private int taskId; private Displayable fallbackUI; EventDispatcher(int taskId, Displayable fallbackUI) { this.taskId = taskId; this.fallbackUI = fallbackUI; return; } public void run() { try { switch (taskId) { // cases ... case EventIds.EVENT_ID_SELECTSEATSSELECTED: { SeatingPlan seatingPlan = selectedMovieSchedule.getSeatingPlan(selectedShowTime); String movieName = selectedMovie.getTitle(); seatingPlanUI.init(selectedTheater.getName(), movieName, seatingPlan, selectedShowTime); display.setCurrent(seatingPlanUI); break; } case EventIds.EVENT_ID_SEATSSELECTED: { reservation = model.reserveSeats(selectedTheater.getPrimaryKey(), selectedMovie.getPrimaryKey(), selectedShowTime, selectedSeats); purchaseTicketsUI.init(model.getAccountInfo()); display.setCurrent(purchaseTicketsUI); break; } case EventIds.EVENT_ID_PURCHASEREQUESTED: { model.purchaseTickets(reservation); purchaseCompleteUI.init(reservation.getId(), selectedTheater.getName(), selectedMovie.getTitle(), selectedShowTime); display.setCurrent(purchaseCompleteUI); break; } // Other cases ... } } catch (Exception exception) { // handle exceptions } } // end of run() method } // end of EventDispatcher class }</td></tr></table>
- Model: Classes in the model layer contain all the application logic. In fact, the entire J2EE server component, the on-device caches, and the communication classes all belong to the model layer. The model layer features sophisticated façade patterns on both the client side and the server side.
Let's look at the details of the model layer.
For most application actions, the controller's entry point into the model layer is the ModelFacade
class. In keeping with the MVC pattern, ModelFacade
contains one method for each action in the model layer. Depending on the nature of the action, the façade delegates it to one or more of the following model classes:
LocalModel
class handles actions that need access to data stored locally, on the device. For example, if an action entails reading or writing preference data, ModelFacade
calls the appropriate action method in LocalModel
.
RemoteModelProxy
class, which implements the RemoteModel
interface, handles actions that require access to the J2EE server, such as ticket purchases. Action methods in RemoteModelProxy
make remote procedure calls (RPCs) to the façade on the server side, in a format we'll disuss when we look at the back end.
SynchronizationAgent
class synchronizes data stored on the remote server with local data. In Smart Ticket, only movie ratings are synchronized. This agent has two action methods: synchronizeMovieRatings()
synchronizes the ratings; commitMovieRatings()
commits the resolved synchronization requests to the back end and updates the content of the local store. <table class=grey4 cellSpacing=0 cellPadding=10 width="100%" border=0> <tr> <td>package com.sun.j2me.blueprints.smartticket.client.midp.model; public class ModelFacade { private SynchronizationAgent syncAgent; private RemoteModelProxy remoteModel; private LocalModel localModel; // Action methods ... public Reservation reserveSeats(String theaterKey, String movieKey, int[] showTime, Seat[] seats) throws ApplicationException { try { return remoteModel.reserveSeats(theaterKey, movieKey, showTime, seats); } catch (ModelException me) { // ... } } public void purchaseTickets(Reservation reservation) throws ApplicationException { try { remoteModel.purchaseTickets(reservation.getId()); localModel.addMovieRating( new MovieRating(remoteModel.getMovie(reservation.getMovieId()), reservation.getShowTime())); } catch (ModelException me) { // ... } return; } public void synchronizeMovieRatings(int conflictResolutionStrategyId) throws ApplicationException { try { syncAgent.synchronizeMovieRatings(conflictResolutionStrategyId); return; } catch (ModelException me) { // ... } } // ... }</td></tr></table>
The Server-Side Facade
The server side of the application uses a number of Enterprise JavaBeans components (EJBs) to encapsulate the business logic and manage interaction with a relational database. When the
RemoteModelProxy
on the client side makes an RPC call to the server side, the HTTP servletSmartTicketServlet
invokes the appropriate action method in a session EJB,SmartTicketFacadeBean
, through a business delegate objectSmartTicketBD
. Depending on the nature of the request, it will be further delegated to one of two other session beans,TicketingBean
orSynchronizingBean
. An array of entity beans uses EJB 2.0 container-managed persistence to update the database as needed.<table class=grey4 cellSpacing=0 cellPadding=10 width="100%" border=0> <tr> <td>package com.sun.j2me.blueprints.smartticket.server.web.midp; public class SmartTicketServlet extends HttpServlet { public static final String SESSION_ATTRIBUTE_SMART_TICKET_BD = "com.sun.j2me.blueprints.smartticket.server.web.midp.SmartTicketBD"; protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { HttpSession session = request.getSession(true); SmartTicketBD smartTicketBD = (SmartTicketBD) session.getAttribute(SESSION_ATTRIBUTE_SMART_TICKET_BD); // Calls handleCall() method and encodes the URL for // session tracking } public int handleCall(SmartTicketBD smartTicketBD, InputStream in, OutputStream out) throws IOException, ApplicationException { // Identifies the requested action method // Executes the method, as selected in a switch statement switch (method) { // cases ... case MessageConstants.OPERATION_GET_MOVIE: { getMovie(smartTicketBD, call, successfulResult); break; } // more cases ... } } } package com.sun.j2me.blueprints.smartticket.server.web.midp; public class SmartTicketBD implements RemoteModel { public static final String EJB_REF_FACADE = "ejb/SmartTicketFacade"; private SmartTicketFacadeLocal facade; private ServletContext servletContext = null; public SmartTicketBD(ServletContext servletContext) throws ApplicationException { this.servletContext = servletContext; try { Context context = (Context) new InitialContext().lookup("java:comp/env"); facade = ((SmartTicketFacadeLocalHome) context.lookup(EJB_REF_FACADE)).create(); return; } catch (Exception e) { throw new ApplicationException(e); } } public Movie getMovie(String movieKey) throws ModelException, ApplicationException { try { MovieLocal movieLocal = facade.getMovie(movieKey); Movie movie = new Movie(movieLocal.getId(), movieLocal.getTitle(), movieLocal.getSummary(), movieLocal.getRating()); return movie; } catch (SmartTicketFacadeException stfe) { throw new ModelException(ModelException.CAUSE_MOVIE_NOT_FOUND); } catch (Exception e) { throw new ApplicationException(e); } } // Other action methods in RemoteModel interface ... } package com.sun.j2me.blueprints.smartticket.server.ejb; public class SmartTicketFacadeBean implements SessionBean { // ... public MovieLocal getMovie(String movieId) throws SmartTicketFacadeException { try { return movieHome.findByPrimaryKey(movieId); } catch (FinderException fe) { throw new SmartTicketFacadeException("No matching movie."); } } // ... }</td></tr></table>
This diagram illustrates the overall MVC-plus-façade architecture:
<table cellSpacing=0 cellPadding=10 width=image_width align=center border=0> <tr> <td align=middle> </td></tr></table>Implementation Patterns
The MVC and façade patterns define the overall architecture of the application. In addition, Smart Ticket also showcases some important behavior patterns that could help developers improve productivity.
Chain of Handlers
The
RemoteModelProxy
class delegates each requested action to a chain of handler classes that transparently work out the dirty plumbing of the RMS serialization and HTTP connection. The chained-handler architecture is based on theRequestHandler
interface and on theRemoteModelRequestHandler
abstract class that implements it:<table class=grey4 cellSpacing=0 cellPadding=10 width="100%" border=0> <tr> <td>public interface RequestHandler { RequestHandler getNextHandler(); void init() throws ApplicationException; void destroy() throws ApplicationException; } abstract public class RemoteModelRequestHandler implements RequestHandler, RemoteModel { private RemoteModelRequestHandler nextHandler; private Preferences preferences; protected static ProgressObserver progressObserver; public RemoteModelRequestHandler( RemoteModelRequestHandler nextHandler) { this.nextHandler = nextHandler; } public RequestHandler getNextHandler() { return nextHandler; } public void init() throws ApplicationException { if (nextHandler != null) { nextHandler.init(); } return; } public void destroy() throws ApplicationException { if (nextHandler != null) { nextHandler.destroy(); } return; } public void login(String userName, String password) throws ModelException, ApplicationException { getRemoteModelRequestHandler().login(userName, password); return; } public void createAccount(AccountInfo accountInfo) throws ModelException, ApplicationException { getRemoteModelRequestHandler().createAccount(accountInfo); return; } // Other action methods declared in RemoteModel // ... }</td></tr></table>
Concrete handler classes extend the
RemoteModelRequestHandler
class. Nested constructors establish a chain of handlers. Smart Ticket makes two handler classes available:RMSCacheHandler
andHTTPCommunicationHandler
. The chain is assembled thus:<table class=grey4 cellSpacing=0 cellPadding=10 width="100%" border=0> <tr> <td>public class RemoteModelProxy extends ModelObjectLoader implements RemoteModel { private RemoteModelRequestHandler requestHandlerChain; private Preferences preferences = null; private Hashtable movies = new Hashtable(); public RemoteModelProxy(String serviceURL) throws ApplicationException { requestHandlerChain = new RMSCacheHandler( new HTTPCommunicationHandler(null, serviceURL)); return; } // ... public Movie getMovie(String movieKey) throws ModelException, ApplicationException { Movie movie = (Movie) movies.get(movieKey); if (movie == null) { movie = requestHandlerChain.getMovie(movieKey); movies.put(movieKey, movie); } return movie; } // Other methods ... }</td></tr></table>
A handler can implement any action methods in the
RemoteModel
interface selectively, in either of two ways:
- If a
RemoteModelProxy
class calls an action method not implemented by the first handler class, the baseRemoteModelRequestHandler
class ensures that the call is passed to the next handler in the chain.- If a handler in a chain decides it has finished processing an action, it returns directly. Otherwise, it invokes the same action method in the superclass, to pass it to the next handler in the chain
<table class=grey4 cellSpacing=0 cellPadding=10 width="100%" border=0> <tr> <td>public class RMSCacheHandler extends RemoteModelRequestHandler { // ... public Movie getMovie(String movieKey) throws ModelException, ApplicationException { IndexEntry indexEntry = rmsAdapter.getIndexEntry(movieKey, IndexEntry.TYPE_MOVIE, IndexEntry.MODE_ANY); if (indexEntry != null) { return rmsAdapter.loadMovie(indexEntry.getRecordId()); } return super.getMovie(movieKey); } // ... }</td></tr></table>
Binary Remote Procedure Call over HTTP
In the model layer, the
HTTPCommunicationHandler
class in theRemoteModelProxy
class invokes remote procedures on the server side through a binary RPC protocol over an HTTP connection. The protocol is defined as follows:All RPC requests from the client to the server follow the same basic pattern. The first byte in the stream specifies the action method that the façade session bean on the server side must execute, and the remaining bytes encode a sequence of UTF strings that represent the parameters to be passed to the remote method. The response HTTP stream contains the RPC return value. The formats of the requests and responses are unique to each method, and you have to look at the source code for each to figure out the exact format.
The RPC codes that go into the first byte of the request stream are defined in the
MessageConstants
class:<table class=grey4 cellSpacing=0 cellPadding=10 width="100%" border=0> <tr> <td>package com.sun.j2me.blueprints.smartticket.shared.midp; public final class MessageConstants { public static final byte OPERATION_LOGIN_USER = 0; public static final byte OPERATION_CREATE_ACCOUNT = 1; public static final byte OPERATION_UPDATE_ACCOUNT = 2; public static final byte OPERATION_GET_THEATERS = 3; public static final byte OPERATION_GET_THEATER_SCHEDULE = 4; public static final byte OPERATION_GET_MOVIE = 5; public static final byte OPERATION_GET_MOVIE_POSTER = 6; public static final byte OPERATION_GET_MOVIE_SHOWTIMES = 7; public static final byte OPERATION_GET_SEATING_PLAN = 8; public static final byte OPERATION_RESERVE_SEATS = 9; public static final byte OPERATION_PURCHASE_TICKETS = 10; public static final byte OPERATION_CANCEL_SEAT_RESERVATION = 11; public static final byte OPERATION_GET_LOCALES = 12; public static final byte OPERATION_GET_RESOURCE_BUNDLE = 13; public static final byte OPERATION_INITIATE_SYNCHRONIZATION = 14; public static final byte OPERATION_SYNCHRONIZE_MOVIE_RATINGS = 15; public static final byte OPERATION_COMMIT_MOVIE_RATINGS = 16; public static final byte ERROR_NONE = 0; public static final byte ERROR_UNKNOWN_OPERATION = 1; public static final byte ERROR_SERVER_ERROR = 2; public static final byte ERROR_MODEL_EXCEPTION = 3; public static final byte ERROR_REQUEST_FORMAT = 4; private MessageConstants() {} }</td></tr></table>
The two classes that follow illustrate an RPC round trip; an action method of the
HTTPCommunicationHandler
requests information about a specified movie, and invokes a method of theMovie
class to extract the return values from the response stream.<table class=grey4 cellSpacing=0 cellPadding=10 width="100%" border=0> <tr> <td>package com.sun.j2me.blueprints.smartticket.client.midp.model; public class HTTPCommunicationHandler extends RemoteModelRequestHandler { // ... public Movie getMovie(String movieKey) throws ModelException, ApplicationException { HttpConnection connection = null; DataOutputStream outputStream = null; DataInputStream inputStream = null; try { connection = openConnection(); updateProgress(); outputStream = openConnectionOutputStream(connection); // The RPC request outputStream.writeByte(MessageConstants.OPERATION_GET_MOVIE); outputStream.writeUTF(movieKey); outputStream.close(); updateProgress(); // unmarshal the return values inputStream = openConnectionInputStream(connection); Movie movie = Movie.deserialize(inputStream); updateProgress(); return movie; } catch (IOException ioe) { throw new ApplicationException(ErrorMessageCodes.ERROR_CANNOT_CONNECT); } finally { closeConnection(connection, outputStream, inputStream); } } // Other action methods ... } package com.sun.j2me.blueprints.smartticket.shared.midp.model; public class Movie { private String primaryKey; private String title; private String summary; private String rating; private boolean alreadySeen = false; transient private byte[] poster = null; public static Movie deserialize(DataInputStream dataStream) throws ApplicationException { try { Movie movie = new Movie(); // Read the RPC response stream movie.primaryKey = dataStream.readUTF(); movie.title = dataStream.readUTF(); movie.summary = dataStream.readUTF(); movie.rating = dataStream.readUTF(); try { movie.alreadySeen = dataStream.readBoolean(); } catch (IOException ioe) { movie.alreadySeen = false; } try { return ModelObjectLoader.getInstance().getMovie(movie); } catch (ModelException me) { throw new ApplicationException(); } } catch (IOException ioe) { throw new ApplicationException(ioe); } } // Other methods ... }</td></tr></table>
On the server side, the
SmartTicketServlet
first determines the action desired from the code in the first byte in the request stream. It then dispatches the request to the appropriate action method through the façade, passing all the RPC parameters remaining in the stream.In Smart Ticket, the client and server are tightly coupled. This approach can improve network efficiency because each RPC exchange can be specially designed and optimized. The trade-off, however, is development speed and robustness. Even small changes to the server are likely to force changes in the protocol and the parsing code on the client side too, and potentially in multiple places. Developers need to keep track of all code that might be affected, and update it when necessary. They also need to recompile and redistribute clients oftener than they'd like, which could also lead to errors.
The Client-Side Thread Model
The Smart Ticket application uses a sophisticated threading model on the client side, with two important aspects:
- The MIDP specification requires the
CommandListener.commandAction()
method to "return immediately" to avoid blocking the UI, so any lengthy operation must be put into another thread.- One of the running threads can display a moving gauge indicating the progress of a long action, particularly any that involves remote network operations. The gauge screen can provide impatient users with a button to cancel actions that take too long.
<table cellSpacing=0 cellPadding=10 width=image_width align=center border=0> <tr> <td align=middle>
(Click image to enlarge.) </td></tr></table>You probably noticed earlier that action methods in the
UIController
class are simply wrappers of therunWithProgress()
method, which sets the display toProgressObserverUI
and starts theEventDispatcher
thread. TheProgressObserverUI
screen displays a gauge and a Stop button which is monitored by the main MIDlet system UI thread. As we described, theEventDispatcher
thread eventually delegates the requested action to methods in the model layer. Each of these methods calls theProgressObserverUI.updateProgress()
at certain stages in its execution to tell the user it's making progress.<table class=grey4 cellSpacing=0 cellPadding=10 width="100%" border=0> <tr> <td>public class UIController { // Action methods ... public void chooseMovieRequested() { runWithProgress( new EventDispatcher( EventIds.EVENT_ID_CHOOSEMOVIEREQUESTED, mainMenuUI), getString(UIConstants.PROCESSING), false); } // Action methods ... public void runWithProgress(Thread thread, String title, boolean stoppable) { progressObserverUI.init(title, stoppable); getDisplay().setCurrent(progressObserverUI); thread.start(); } class EventDispatcher extends Thread { // ... public void run() { // Switch -- case statements to delegate // actions to the model layer } } } public class ProgressObserverUI extends Form implements ProgressObserver, CommandListener { private UIController uiController; private static final int GAUGE_MAX = 8; private static final int GAUGE_LEVELS = 4; int current = 0; Gauge gauge; Command stopCommand; boolean stoppable; boolean stopped; public ProgressObserverUI(UIController uiController) { super(""); gauge = new Gauge("", false, GAUGE_MAX, 0); stopCommand = new Command(uiController.getString(UIConstants.STOP), Command.STOP, 10); append(gauge); setCommandListener(this); } public void init(String note, boolean stoppable) { gauge.setValue(0); setNote(note); setStoppable(stoppable); stopped = false; } public void setNote(String note) { setTitle(note); } public boolean isStoppable() { return stoppable; } public void setStoppable(boolean stoppable) { this.stoppable = stoppable; if (stoppable) { addCommand(stopCommand); } else { removeCommand(stopCommand); } } /** * Indicates whether the user has stopped the progress. * This message should be called before calling update. */ public boolean isStopped() { return stopped; } public void updateProgress() { current = (current + 1) % GAUGE_LEVELS; gauge.setValue(current * GAUGE_MAX / GAUGE_LEVELS); } public void commandAction(Command c, Displayable d) { if (c == stopCommand) { stopped = true; } } }</td></tr></table>
Conclusion
This article introduced the all-new Smart Ticket blueprint v2.0. Several key improvements over the previous versions take advantage of the rich capability of smart clients. Smart Ticket shows you how to implement advanced features using several important design patterns, which we explored briefly. We hope our presentation will get you off to a fast start in the world of end-to-end design patterns!
Resources
- Sun Java Wireless Blueprints
- "Developing an End to End Wireless Application Using Java Smart Ticket Demo" by Eric Larson (covers Smart Ticket v1.1)
<table class=grey4 cellSpacing=0 cellPadding=0 width="100%" border=0> <tr> <td></td></tr></table>