摘要
实现一个端到端的 J2ME 应用程序并不简单,而且这种系统的体系结构和开发可能是非常复杂的。本文通过 Sun 公司的 Java Blueprints 中的范例程序 Java Smart Ticket,指导你怎样设计和实现一个基于 MIDP和J2EE 的复杂的、端到端的应用程序。我们将讨论设计模式、体系结构和创建应用程序的实现技巧。
概述
2001 年发布的第一版 Java 技术蓝图 Java Pet Store 就充分展示了 Sun 公司 J2EE 技术的优势。 蓝图不仅为多层的、数据库驱动的电子商务应用程序提供了示例代码 , 而且提供了设计准则,示范了常用的模式。自第一版发布以来,对于想学习 J2EE 最新技术的开发人员来说,Java 技术蓝图已经成为最宝贵的资源和最佳实践。
Smart Ticket 蓝图增加了一个新的特性:移动性。它示范了如何创建一个实现电影订票功能的完整的端到端的移动商务系统 ,将 J2ME MIDP 用于无线前端,而将 J2EE 应用程序服务器和关系数据库用于后端。学习这个程序如何设计和构造将能极大地提高你对移动企业应用程序的难题和它们的解决方案的理解。
文章包含 2003 年 4 月发布的 Smart Ticket 的 Early Access 2.0 版的代码。 early-access 版中的屏幕截图和示例代码在最终版本中可能有细微的更改,但你从设计中学到的经验依然是有用的。Smart Ticket 1.2 仍然有效。它与我们现在讨论的版本有相同的模型和后台实现,因此,无论对过去还是未来的版本,很多详细解释都适用。除特别说明之外,Sun Microsystems 均对本文的所有源代码保留版权。
下载和安装
Smart Ticket 应用程序可从 Sun's Blueprints网站获得。Zip 压缩文件包含源代码、Ant 构建脚本和预构建可配置的应用程序。
Smart Ticket 应用程序包含一个 J2ME 组件和一个 J2EE 组件。运行它要求一个 J2EE 应用服务器(比如 Sun 的 J2EE 参考实现,1.3 版或更高),和任一个带有 Internet 连接的兼容 MIDP 2.0的设备或者合适的仿真程序。如 Sun 的 J2ME Wireless Toolkit 2.0。 Smart Ticket 发行版包括了特别的说明,帮助构建和部署这个应用程序。现在开始:
-
确保你已经安装以下资源:
- JDK v1.4.1 或更高版本。
- J2EE v1.3.1 或更高版本。
- J2ME Wireless Toolkit 2.0 或更高版本。
-
设置以下环境变量:
JAVA_HOME
:JDK 安装目录 。J2EE_HOME
:J2EE RI 安装目录。J2MEWTK_HOME
:J2ME Wireless Toolkit 安装目录。
-
启动 J2EE 服务器:
J2EE_HOME/bin/cloudscape -start J2EE_HOME/bin/j2ee -verbose
-
配置 J2EE 应用程序。 在
setup.xml
文件中,使用以下setup
脚本调用deploy
Ant 任务:setup deploy
-
指定浏览器连接
http://localhost:8000/smartticket
,单击 Populate Database 链接,将模拟影院和电影数据导入数据库。如果用的是老式计算机,这是非常慢的过程, 所以要有耐心!模拟数据包括位于 95054 和 95130 这两个邮政编码的影院。 -
启动 J2ME Wireless Toolkit 2.0,并运行在
smart_ticket-client.jad
中指定的 MIDlet。
运行中的 Smart Ticket
运行 MIDlet 后,采用简便途径就能实现用户需求。你会发现你需要完成四项任务。
- 管理用户参数 :当第一次运行 MIDP 客户端,你需要创建一个配置文件,包含用户名、密码、用于影院搜索的首选邮政编码、一周的首选日,也可以包括信用卡号。Smart Ticket 用帐户凭证在服务器端创建用户帐户,并且将首选数据缓存在计算机中。还可以配置 MIDP 客户机使其能够缓存凭证,以便在每次购票或提交电影评级时无需手工输入。你也可以在任何时候修改用户参数。
- 搜索电影和购票:只要你登录成功,你可以搜索符合首选条件的影院、电影和放映时间。只要你选择了一部预演电影,MIDlet 就提供显示空位的座位图。这个过程包括一系列对 J2EE 服务器的实时查询。利用 MIDP 的丰富用户界面(UI),你可以选择或预订一个座位。预订信息将写入服务器端数据库,并会在下一次搜索中反映在座位图中。
3.电影评级:你可以对看过的电影评级。此操作不会立即提交到服务器。这些电影缓存在客户机,任何时候只要你评级,都能同步提交到服务器。因此,甚至在你的电话超出了网络范围也可评级(例如,在一个屏蔽的影院!)。同步代理可智能地防止你“谎服选票”:如果你对一部电影评级多次,在数据库中它只保留最后一次结果 。
缓存影院放映时间表:为避免繁琐的查看过程,你可以下载一份影院的时间表到客户应用程序,以便离线浏览。你可以在需要时删除或再次下载时间表。
Smart Ticket 的优势
较老的移动商务平台,比如基于 WAP/WML 的微型浏览器将所有的信息处理都放置在服务器端。 J2ME 的一个重要优势是它支持运行在客户机上智能客户端程序。Smart Ticket 充分体现了智能客户端应用程序范例的优势:
丰富的 UI:在 MIDP 2.0 中利用了 LCDUI 增强的特性,Smart Ticke 客户端提供一个极好的用户界面。例如,它允许用户通过交互式座位视图选择座位。当你浏览影院放映时间表并选择日期时,MIDlet 就会动态地在当前屏幕中添加放映时间。
缓存首选项:用户首选项被完全缓存以支持完全个性化 -- 这是移动商务的核心价值所在。例如,您无需输入邮政编码、信用卡号,或者甚至每次输入使用 Smart Ticket 的个人登录信息,这大大减少了击键工作量。
脱机功能:有限且不稳定的移动网络覆盖已经阻碍了基于微型浏览器的”持续连接“应用程序的发展。J2ME 智能客户端程序遵循“间断连接”范例,使用存储在客户设备上的数据并根据需要同步驻留在服务器端的数据 - Smart Ticket 支持脱机浏览下载的放映时间表和对电影进行评级就是一个很好的例子。
高性能缓:下载的放映时间表也适合于性能缓存,哪怕是在客户设备处于连接状态时。这样减少了对多次往返过程的需求,因为这种过程是非常缓慢的。
智能同步:智能客户端程序使用的程序缓存需要定期的从服务端更新。电影放映时间表可以直接由用户下载,而评级可以通过驻留在客户端和服务端的智能代理来进行同步。
怎样实现这些特性?
重要的体系结构模式
总体 MVC 模式
Smart Ticket 应用程序的总体体系结构遵循模式-视图-控制器(Model-View-Controller)模式。这个应用程序被分为多个逻辑层,因此开发人员在修改一部分时不会影响其他部分。 Smart Ticket 符合 MVC 模型,如下所示:
视图:每个视图类显示一个交互式 UI(用户界面)屏幕,等待用户输入。当用户通过按键或从列表中选择一个条目时将产生一个 UI 事件,视图类的事件处理程序捕获这个事件,并将控制传递给控制器类。在 com.sun.j2me.blueprints.smartticket.client.midp.ui 包中的大多数类都是视图类。
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());
}
}
}
}
控制器:控制器类感知用户和程序之间的所有可能的交互。在 Smart Ticket 中,UIController类有用于每个可能动作的方法,例如,purchaseRequested()。动作方法经常启动两个新的线程,一个用于执行后台的动作,另一个用于向用户显示进度条。动作线程通过 EventDispatcher 类来表示,它的 run() 方法包括一条很长的 switch 语句,此语句在模型层调用合适的方法完成事件请求。当这些方法的最后一次调用返回时,控制器初始化下一个 UI 画面并显示出来。
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
}
模式:模型层中的类包括所有的应用逻辑。事实上,整个 J2EE 服务器组件、设备中的缓存和通信类都属于模型层。模型层在客户端和服务端的复杂界面模式方面起着重要作用。
让我们看看模型层的详细内容。
客户端界面
对于大多数应用程序动作,指向模型层的控制器条目是 ModelFacade
类。为符合 MVC 模式,ModelFacade
类包含一个响应模型层中每个事件的方法。根据动作的本质,界面将它委托给以下的一个或多个模型类:
LocalModel
类处理需要对本地设备上存储的数据进行访问的动作。例如,如果一个动作需要读写首选数据,ModelFacade
调用LocalModel
中合适的动作方法。RemoteModelProxy
类,它实现了RemoteModel
接口,处理需要对 J2EE 服务器进行访问的动作,比如购票。RemoteModelProxy
中的方法对服务器端界面进行远程过程调用(remote procedure call,RPC) ,这种方式我们将在讨论后台时讲述。SynchronizationAgent
类用本地数据同步化远程服务器端数据。在 Smart Ticket 程序中,只有评级实现了同步。这个代理有两个动作方法:synchronizeMovieRatings()
同步了评级;commitMovieRatings()
方法向后台提交已分析的同步请求,并更新本地存储的内容。
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) { // ... } } // ... } |
服务端界面
应用程序的服务端使用了很多 Enterprise JavaBeans 组件 (EJB) 来封装业务逻辑和管理与关系数据库的交互。当客户端的 RemoteModelProxy
向服务器端发出 RPC 调用时,HTTP servlet SmartTicketServlet
通过业务代理对象 SmartTicketBD
调用会话 EJB 中合适的动作方法 SmartTicketFacadeBean
。根据请求性质,它进一步委托两个其他会话 bean 中的一个,TicketingBean
或 SynchronizingBean
。一组实体 bean 在需要时使用 EJB 2.0 的容器托管的持久性来更新数据库。
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."); } } // ... }
具体的 handler 类扩展了
|
在服务器端, SmartTicketServlet
首先确定在请求数据流中的第一个字节编码所表达的动作,然后立即通过界面将请求分派合适的动作方法,并传递保留在数据流中的所有 RPC 参数。
在 Smart Ticket 程序中,客户机和服务器紧密联系。这种方式可提高网络性能,因为,每次 RPC 交换都可以经过特别的设计和优化。然而,要在开发速度和健壮性之间进行权衡。 即使服务器端的微小改变也很可能逼使客户端的协议和解析码进行改变,还有很多潜在的可能因素。开发人员需要对所有可能影响的代码保持跟踪,并在必要时更新它。 他们也需要经常重新编译和重新分发客户端程序,否则将可能导致错误。
客户端线程模型
Smart Ticket 应用程序在客户端采用一个复杂的线程模型,有两个重要方面:
- MIDP 规范请求
CommandListener.commandAction()
方法“立即返回”以避免阻塞 UI,因此任何长时间的操作都必须放入其他线程。 - 正运行的线程能够显示一个指示长时操作进度的动态进度条,特别是涉及远程网络操作的线程。进度条屏幕为缺乏耐心的用户提供了一个取消按钮,可终止太长的运作。
也许你早就注意到 UIController
类中的动作方法只是 runWithProgress()
方法的简单包装,该方法设置屏幕为 ProgressObserverUI
并启动 EventDispatcher
线程。 ProgressObserverUI
屏幕显示一个进度条和一个 Stop 按钮,通过主 MIDlet 系统 UI 线程来监控它。如前所述, EventDispatcher
线程最终委托到模型层方法的请求动作。 这些方法中的每一个都在其执行的某个阶段调用 ProgressObserverUI.updateProgress()
,以告知用户的进度情况。
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; } } } |
结束语
本文介绍了全新的 Smart Ticket v2.0 蓝图。几个针对早期版本的重大改进都利用了智能客户端丰富的功能。 Smart Ticket 向你展示了如何用几个我们刚才简要讲述的重要 设计模式来实现高级功能。我们希望我们本文讲述的内容能让你在端到端设计模式领域快速起步。
参考资料
- Sun Java Wireless Blueprints
- "Developing an End to End Wireless Application Using Java Smart Ticket Demo",作者:Eric Larson (涵盖了 Smart Ticket v1.1)