从Java EE到Java ME的通讯(1)

2007-10-09     浏览:1368     来源:SOFT6
关键词:  Java      通讯     Java EE     Jav  

  前言

  本文源于 2005 年底一个真实的手机项目。很早就想为那个项目写点什么了,至今才提笔,也算是了却一个心愿。虽然时隔两年,但技术本身并没有发生什么太大的变化,我想本文应该能为广大开发人员提供帮助吧。

  受朋友之托,他们接到一个手机应用项目(以下简称 dbMobile )。 dbMobile 项目主要服务于零担物流运输,为广大的货主和司机建立一个畅通的交流平台,实现便利的货主找车,车主找货功能。只要货主或车主的手机支持 Java ,安装注册之后以用户身份登录上去,就能免费查询自己想要的信息。本文讲贯穿整个 dbMobile 项目,并重点介绍开发者最关注的内容。

  手机端实现

  (由于我是做 Java EE 应用的,为了让自己以后参考,所以关于手机端实现写得较啰嗦。)要进行 Java ME 开发,首先到 http://java.sun.com/products/sjwtoolkit/download-2_5.html 下载 WTK 2.5 ,然后一步步安装好(发现安装界面比 2.2 漂亮了)。接着下载 IDE 插件,我用的开发环境是 Eclipse ,在 http://eclipseme.org/ 找到 EclipseME 的安装包 eclipseme.feature_1.7.5_site ,解压缩之后(也可以不解压缩,只是安装方式稍有不同)在 Eclipse 里面新建一个 “New Local Site…” ,定位到刚才插件解压缩之后的位置,一步步安装即可。重启 Eclipse 之后可以在 “Preferences” 选项中发现 “J2ME” 菜单,现在开始配置 “WTK Root” 。

  配置好 WTK Root 之后,我们还要为 dbMobile 配置设备。点击 “Device Management” ,在 “Specify search directory” 中选中 WTK 根目录,然后点击右下位置的 “Refresh” ,稍等片刻, WTK 默认的四个模拟设备就被找到了。

  完成了这些,如果没有特殊要求,其他选项就不用再配置了。

  接着新建一个名为 dbMobile 的 J2ME 项目(既新建 “J2ME Midlet Suit” ),如果你没有安装多个 WTK 版本或者不想使用默认的彩色模拟器的的话,在新建项目的时候,无需进行过多的配置。

  MIDlet 是 MIDP 的基本执行单元,如同 Servlet 继承自 javax.servlet.http.HttpServlet 一样, MIdlet 必须继承自 javax.microedition.midlet.MIDlet 抽象类。该类定义了三个抽象方法, startApp() 、 pauseApp() 、 destroyApp() ,应用程序管理器通过上面这三个方法控制着 MIdlet 的生命周期。在编写 MIDlet 时必须实现这三个方法。我为 dbMobile 创建了 HttpCli 类,该类不属于任何的包。

  我们来看看,类里面怎样实现抽象方法的,并以如何在启动时进入菜单画面(登录前)这个功能切入。

  import javax.microedition.lcdui.Display;

  import javax.microedition.midlet.MIDlet;

  import javax.microedition.midlet.MIDletStateChangeException;

  import com.forbidden.screen.Navigator;

  /*

  * MIDlet 主程序

  * @author rosen jiang

  * @since 2005-12

  */

  public class HttpCli extends MIDlet {

  /**

  * 构造函数

  */

  public HttpCli() {

  Navigator.midlet = this ;

  Navigator.display = Display.getDisplay( this );

  }

  /**

  * 启动方法

  */

  public void startApp(){

  Navigator.current = Navigator.MAIN_SCREEN;

  Navigator.show();

  }

  /**

  * 暂停方法

  */

  protected void pauseApp() {

  // TODO Auto-generated method stub

  }

  /**

  * 销毁方法

  */

  protected void destroyApp( boolean arg0) throws MIDletStateChangeException {

  this .notifyDestroyed();

  }

  }

  我们在构造函数 “ HttpCli() ” 中 用到了名叫 Navigator 的导航类,该类的主要作用是把 dbMobile 中所有的页面管理起来、统一进行页面跳转控制(稍后我会把 Navigator 类代码列出来)。接着看构造函数, “Navigator.midlet = this” 的作用是把整个 MIDlet 实例交给导航类,以便在退出程序时触发。 “Navigator.display = Display.getDisplay(this)” ,在手机屏幕上显示一幅画面就是一个 Display 对象要实现的功能,从 MIDlet 实例中获取 Display 对象实例,也就是在向导航类授予一个进行画面切换的控制权。接着看 “startApp () ” 启动方法,同样调用了导航类,并设置启动后首先进入的页面是菜单画面(登录前)。

  接下来我们看看 Navigator 导航类都有些什么。

  package com.forbidden.screen;

  import javax.microedition.midlet.MIDlet;

  import javax.microedition.lcdui. * ;

  /*

  * 导航类

  * @author rosen jiang

  * @since 2005-12

  */

  public class Navigator{

  // 菜单画面(登录前)

  final public static int MAIN_SCREEN = 1 ;

  // 用户注册

  final public static int USER_REG = 2 ;

  // 车主找货

  final public static int AUTO_FIND_GOODS = 3 ;

  // 用户登录

  final public static int USER_LOGIN = 4 ;

  // 菜单画面(登录后)

  final public static int MENU_SCREEN = 5 ;

  // 货主找车

  final public static int GOODS_FIND_AUTO = 6 ;

  // 空车信息发布

  final public static int AUTO_PUB = 7 ;

  // 货物信息发布

  final public static int GOODS_PUB = 8 ;

  // 注册信息更新

  final public static int REG_UPD = 9 ;

  public static MIDlet midlet;

  public static Display display;

  // 当前位置

  public static int current;

  /**

  * 转向要显示的菜单

  */

  public static void show (){

  switch (current){

  case MAIN_SCREEN:

  display.setCurrent(MainScreen.getInstance());

  break ;

  case USER_REG:

  display.setCurrent(UserReg.getInstance());

  break ;

  case AUTO_FIND_GOODS:

  display.setCurrent(AutoFindGoods.getInstance());

  break ;

  case USER_LOGIN:

  display.setCurrent(LoginScreen.getInstance());

  break ;

  case MENU_SCREEN:

  display.setCurrent(MenuScreen.getInstance( null ));

  break ;

  case GOODS_FIND_AUTO:

  display.setCurrent(GoodsFindAuto.getInstance());

  break ;

  case AUTO_PUB:

  display.setCurrent(AutoPub.getInstance());

  break ;

  case GOODS_PUB:

  display.setCurrent(GoodsPub.getInstance());

  break ;

  case REG_UPD:

  display.setCurrent(RegUpd.getInstance( null , null , null , null ));

  break ;

  }

  }

  /**

  * 导航器定位目标表单

  *

  * @param String cmd 输入的命令

  */

  public static void flow(String cmd){

  if (cmd.equals( " 离开 " )){

  midlet.notifyDestroyed();

  } else if (cmd.equals( " 注册 " )){

  current = USER_REG;

  show ();

  } else if (cmd.equals( " 车主找货 " )){

  current = AUTO_FIND_GOODS;

  show ();

  } else if (cmd.equals( " 登陆 " )){

  current = USER_LOGIN;

  show ();

  } else if (cmd.equals( " 功能列表 " )){

  current = MENU_SCREEN;

  show ();

  } else if (cmd.equals( " 返回菜单 " )){

  current = MAIN_SCREEN;

  show ();

  } else if (cmd.equals( " 货主找车 " )){

  current = GOODS_FIND_AUTO;

  show ();

  } else if (cmd.equals( " 空车信息发布 " )){

  current = AUTO_PUB;

  show ();

  } else if (cmd.equals( " 货物信息发布 " )){

  current = GOODS_PUB;

  show ();

  } else if (cmd.equals( " 修改注册信息 " )){

  current = REG_UPD;

  show ();

  }

  }

  }

  该类对每个画面进行了编号处理, “show()” 方法是整个导航类的关键,当符合条件的画面编号被找到时,调用 “display.setCurrent()” 方法设置被显示画面的实例,同时手机上也会切换到相应画面。 “flow()” 方法做用是捕获用户的控制命令,并把命令转换成内部的画面编号,和 “show()” 联合使用就能响应用户操作了。

  下面是菜单画面(登录前)类。

  package com.forbidden.screen;

  import javax.microedition.lcdui. * ;

  /*

  * 菜单画面(登录前)

  * @author rosen jiang

  * @since 2005-12

  */

  public class MainScreen extends List implements CommandListener{

  // 对象实例

  private static Displayable instance;

  /**

  * 获取对象实例

  */

  synchronized public static Displayable getInstance(){

  if (instance == null )

  instance = new MainScreen();

  return instance;

  }

  /**

  * 画面内容

  */

  private MainScreen(){

  super ( " 菜单 " , Choice.IMPLICIT);

  append ( " 注册 " , null );

  append ( " 登陆 " , null );

  addCommand( new Command( " 进入 " ,Command.OK, 1 ));

  addCommand( new Command( " 离开 " ,Command.EXIT, 1 ));

  setCommandListener( this );

  }

  /**

  * 对用户输入命令作出反应

  * @param c 命令

  * @param s Displayable 对象

  */

  public void commandAction(Command c, Displayable s){

  String cmd = c.getLabel();

  if (cmd.equals( " 进入 " )){

  String comd = getString(getSelectedIndex());

  Navigator.flow(comd);

  } else if (cmd.equals( " 离开 " )) {

  Navigator.flow(cmd);

  }

  }

  }

  Displayable 是所有高级(Screan)、低级(Canvas)界面的父类,在 dbMobile 项目中,由于专注于数据而不是界面,所以我决定采用高级界面。图四列出了高级界面的类、接口关系,可以对整个高级界面开发有个概括。关于高级界面编程基础的话题就不多说了,请参考其他资料。

  在整个程序加载的时候会首先实例化HttpCli类,接着触发”Navigator.MAIN_SCREEN”,最后实例化MainScreen类,在手机屏幕上显示如图五的画面。MainScreen类的“getInstance()”方法返回 MainScreen 唯一对象实例。在“MainScreen()”构造函数中,“super ("菜单", Choice.IMPLICIT)”创建名为“菜单”的单选列表,然后分别用“append("注册",null)”和“append ("登陆",null)”追加两个选项,接着追加两个命令“addCommand(new Command("进入",Command.OK,1))”和“addCommand(new Command("离开",Command.EXIT,1))”,最后针对当前对象实例设置命令监听器“setCommandListener(this)”。在手机上一切都已正确显示后就可以监听用户的操作了,“commandAction()”方法捕捉用户点击的是”离开”还是”进入”。如果是”离开”,直接利用Navigator类退出整个程序,如果是”进入”则通过”String comd = getString(getSelectedIndex())”代码获取用户选择的菜单,然后再通过Navigator类的”flow()”方法实例化相应的画面实例,就像进入菜单画面(登录前)一样。

  可能你非常熟悉以上这些调用流程,本文到这里开始转到如何与 Java EE服务器端通讯的部分。

  登录成功以后进入主菜单,现在我重点介绍货主找车这个功能,首先要创建货主找车界面,GoodsFindAuto类代码如下:

  package com.forbidden.screen;

  import java.util.Date;

  import javax.microedition.lcdui.Alert;

  import javax.microedition.lcdui.AlertType;

  import javax.microedition.lcdui.Command;

  import javax.microedition.lcdui.CommandListener;

  import javax.microedition.lcdui.DateField;

  import javax.microedition.lcdui.Displayable;

  import javax.microedition.lcdui.Form;

  import javax.microedition.lcdui.TextField;

  import com.forbidden.thread.GoodsFindAutoThread;

  import com.forbidden.vo.TransAuto;

  /* 货主找车输入查询条件页面

  * @author rosen jiang

  * @since 2005-12

  */

  public class GoodsFindAuto extends Form implements CommandListener {

  //车辆出发地

  private TextField autoFromField;

  //车辆目的地

  private TextField autoTargetField;

  //发布时间

  private DateField pubDateField;

  //对象实例

  private static Displayable instance;

  /**

  * 获取对象实例

  */

  synchronized public static Displayable getInstance(){

  if (instance==null)

  instance = new GoodsFindAuto("货主找车");

  return instance;

  }

  /**

  * 画面内容

  */

  public GoodsFindAuto(String arg0) {

  super(arg0);

  autoFromField = new TextField("车辆出发地", "28", 25, TextField.NUMERIC);

  autoTargetField = new TextField("车辆目的地", null, 25, TextField.NUMERIC);

  pubDateField = new DateField("发布日期", DateField.DATE);

  pubDateField.setDate(new Date());

  append(autoFromField);

  append(autoTargetField);

  append(pubDateField);

  Command backCommand = new Command("功能列表", Command.BACK, 1);

  Command sendCommand = new Command("查询", Command.SCREEN, 1);

  addCommand(backCommand);

  addCommand(sendCommand);

  setCommandListener(this);

  }

  /**

  * 对用户输入命令作出反应

  * @param c 命令

  * @param s Displayable 对象

  */

  public void commandAction(Command c, Displayable s) {

  String cmd = c.getLabel();

  if (cmd.equals("查询")){

  String autoFrom = autoFromField.getString();

  String autoTarget = autoTargetField.getString();

  if (autoTarget.length()==0) {

  Alert a = new Alert("提示信息", "目的城市不能为空!", null, AlertType.ERROR);

  a.setTimeout(Alert.FOREVER);

  Navigator.display.setCurrent(a);

  return;

  }

  String pubDate = pubDateField.getDate().getTime()+"";

  //发送查询

  TransAuto ta = new TransAuto(null,null,null,null,

  pubDate,autoFrom,autoTarget, null);

  GoodsFindAutoThread gfat = new GoodsFindAutoThread(1,20,ta);

  Navigator.display.setCurrent(WaitForm.getInstance());

  gfat.start();

  }else{

  Navigator.flow(cmd);

  }

  }

  }

  对于手机用户来说,要用最简单的界面实现查询功能那是最好不过了。在构造函数里面添加了三个输入框"车辆出发地"、"车辆目的地"、”发布日期”,为了更进一步减少用户输入,在"车辆出发地"和”车辆目的地"是按照当地的去掉0的电话区号来作为条件,默认的以成都(28)为车辆出发地。

  当用户完成查询并点击”查询之后”,要对用户的输入信息进行判断,根据业务上的要求,”车辆目的地”是必填项,如果为空,在”commandAction()”方法中会通过Alert对象进行提示。接下来将与服务器进行数据交互,交互之前先把查询条件构造成TransAuto车辆对象实例并进行序列化,然后再通过HTTP GET方法请求服务器,服务器收到序列化的数据后抽取查询条件。手机端和服务器端通讯的策略是:从手机端到服务器端是通过拼接字符串然后GET过去,而从服务器端到手机端则通过UTF-8编码后的数据流送回来,否则容易出现乱码。如果你要问为什么不使用GBK、GB2312编码输出,我的回答是DataOutputStream/ DataInputStream类原生支持”writeUTF()/readUTF()”方法,无论是在服务器端还是手机端,转换起来很轻松,尽管UTF-8三字节编码会产生更多的通讯流量。”GoodsFindAutoThread(1,20,ta)”构造函数来自GoodsFindAutoThread线程类,该线程类用于远程HTTP连接,由于GPRS连接非常慢,为了提高网络利用率,要一次多传些查询结果到手机端,这就涉及到了分页,我定义的分页策略是:一次从服务器端取最多20条记录,然后在手机上分成4页显示(每页5条);如果总记录数超过20条,当手机将要阅读第5页的时候再取下20条。那么上面的构造函数实际上是发出了获取从1—4页共20条数据的分页请求。在进入线程类的话题之前,先看看TransAuto车辆类。

  package com.forbidden.vo;

  import java.io.DataInputStream;

  import java.io.IOException;

  import com.forbidden.util.Split;

  /* 车辆

  * @author rosen jiang

  * @since 2005-12

  */

  public class TransAuto{

  //车主名

  private String name;

  //车牌号

  private String autoNo;

  //联系电话

  private String phone;

  //车辆容积

  private String autoCap;

  //发布时间

  private String pubDate;

  //车辆出发地

  private String autoFrom;

  //车辆目的地

  private String autoTarget;

  //备注

  private String memo;

  /**

  * 构造函数

  *

  * @param name 车主名

  * @param autoNo 车牌号

  * @param phone 联系电话

  * @param autoCap 车辆容积

  * @param pubDate 发布时间

  * @param autoFrom 车辆出发地

  * @param autoTarget 车辆目的地

  * @param memo 备注

  */

  public TransAuto(String name, String autoNo,

  String phone,String autoCap, String pubDate,

  String autoFrom,String autoTarget,String memo) {

  this.name=name;

  this.autoNo=autoNo;

  this.phone=phone;

  this.autoCap=autoCap;

  this.pubDate=pubDate;

  this.autoFrom=autoFrom;

  this.autoTarget=autoTarget;

  this.memo=memo;

  }

  /**

  * 序列化

  *

  * @return 字符串

  */

  public String serialize() {

  String outStrings = "pubDate="+pubDate+"&autoFrom="

  +autoFrom+"&autoTarget="+autoTarget;

  return outStrings;

  }

  /**

  * 多对象的反序列化

  *

  * @param from 车辆出发地

  * @param rows 条数

  * @param din 输入流

  * @return TransAuto[] 车辆数组

  * @throws IOException

  */

  public TransAuto[] deserializes(String from,int rows,DataInputStream din) throws IOException {

  TransAuto[] tas = new TransAuto[rows];

  for (int i = 0; i < rows; i++) {

  String recString = null;

  try{

  recString = din.readUTF();

  if(recString.equals("")){

  break;

  }

  }catch(Exception e){

  break;

  }

  String[] recStrings = Split.split(recString,"&");

  try{

  name = recStrings[0];

  autoNo = recStrings[1];

  phone = recStrings[2];

  autoCap = recStrings[3];

  pubDate = recStrings[4]+"时";

  autoFrom = from;

  autoTarget = recStrings[5];

  memo = recStrings[6];

  }catch(ArrayIndexOutOfBoundsException e){

  break;

  }

  TransAuto ta = new TransAuto(name,autoNo,phone,

  autoCap,pubDate,autoFrom,autoTarget,memo);

  tas[i]=ta;

  }

  return tas;

  }

  public String getAutoCap() {

  return autoCap;

  }

  public void setAutoCap(String autoCap) {

  this.autoCap = autoCap;

  }

  public String getAutoFrom() {

  return autoFrom;

  }

  public void setAutoFrom(String autoFrom) {

  this.autoFrom = autoFrom;

  }

  public String getAutoNo() {

  return autoNo;

  }

  public void setAutoNo(String autoNo) {

  this.autoNo = autoNo;

  }

  public String getAutoTarget() {

  return autoTarget;

  }

  public void setAutoTarget(String autoTarget) {

  this.autoTarget = autoTarget;

  }

  public String getName() {

  return name;

  }

  public void setName(String name) {

  this.name = name;

  }

  public String getMemo() {

  return memo;

  }

  public void setMemo(String memo) {

  this.memo = memo;

  }

  public String getPhone() {

  return phone;

  }

  public void setPhone(String phone) {

  this.phone = phone;

  }

  public String getPubDate() {

  return pubDate;

  }

  public void setPubDate(String pubDate) {

  this.pubDate = pubDate;

  }

  }