文章索引

摘要

实现一个端到端的 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 发行版包括了特别的说明,帮助构建和部署这个应用程序。现在开始:

  1. 确保你已经安装以下资源:

    1. JDK v1.4.1 或更高版本。
    2. J2EE v1.3.1 或更高版本。
    3. J2ME Wireless Toolkit 2.0 或更高版本。

     

  2. 设置以下环境变量:

    1. JAVA_HOME:JDK 安装目录 。
    2. J2EE_HOME:J2EE RI 安装目录。
    3. J2MEWTK_HOME:J2ME Wireless Toolkit 安装目录。

     

  3. 启动 J2EE 服务器:

    J2EE_HOME/bin/cloudscape -start
    J2EE_HOME/bin/j2ee -verbose 

     

  4. 配置 J2EE 应用程序。 在 setup.xml 文件中,使用以下 setup 脚本调用 deploy Ant 任务:

    setup deploy 
  5. 指定浏览器连接 http://localhost:8000/smartticket,单击 Populate Database 链接,将模拟影院和电影数据导入数据库。如果用的是老式计算机,这是非常慢的过程, 所以要有耐心!模拟数据包括位于 95054 和 95130 这两个邮政编码的影院。

  6. 启动 J2ME Wireless Toolkit 2.0,并运行在 smart_ticket-client.jad 中指定的 MIDlet。

运行中的 Smart Ticket

运行 MIDlet 后,采用简便途径就能实现用户需求。你会发现你需要完成四项任务。

  1. 管理用户参数 :当第一次运行 MIDP 客户端,你需要创建一个配置文件,包含用户名、密码、用于影院搜索的首选邮政编码、一周的首选日,也可以包括信用卡号。Smart Ticket 用帐户凭证在服务器端创建用户帐户,并且将首选数据缓存在计算机中。还可以配置 MIDP 客户机使其能够缓存凭证,以便在每次购票或提交电影评级时无需手工输入。你也可以在任何时候修改用户参数。


    Image 

  2. 搜索电影和购票:只要你登录成功,你可以搜索符合首选条件的影院、电影和放映时间。只要你选择了一部预演电影,MIDlet 就提供显示空位的座位图。这个过程包括一系列对 J2EE 服务器的实时查询。利用 MIDP 的丰富用户界面(UI),你可以选择或预订一个座位。预订信息将写入服务器端数据库,并会在下一次搜索中反映在座位图中。


    Image 

3.电影评级:你可以对看过的电影评级。此操作不会立即提交到服务器。这些电影缓存在客户机,任何时候只要你评级,都能同步提交到服务器。因此,甚至在你的电话超出了网络范围也可评级(例如,在一个屏蔽的影院!)。同步代理可智能地防止你“谎服选票”:如果你对一部电影评级多次,在数据库中它只保留最后一次结果 。


Image 

缓存影院放映时间表:为避免繁琐的查看过程,你可以下载一份影院的时间表到客户应用程序,以便离线浏览。你可以在需要时删除或再次下载时间表。

Image
 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 类包含一个响应模型层中每个事件的方法。根据动作的本质,界面将它委托给以下的一个或多个模型类:

  1. LocalModel 类处理需要对本地设备上存储的数据进行访问的动作。例如,如果一个动作需要读写首选数据,ModelFacade 调用 LocalModel中合适的动作方法。
  2. RemoteModelProxy类,它实现了 RemoteModel 接口,处理需要对 J2EE 服务器进行访问的动作,比如购票。RemoteModelProxy 中的方法对服务器端界面进行远程过程调用(remote procedure call,RPC) ,这种方式我们将在讨论后台时讲述。
  3. 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 中的一个,TicketingBeanSynchronizingBean。一组实体 bean 在需要时使用 EJB 2.0 的容器托管的持久性来更新数据库。