从Java EE到Java ME的通讯
前言
本文源于 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” ,如图一所示。
图一: EclipseME 配置 1
配置好 WTK Root 之后,我们还要为 dbMobile 配置设备。如图二所示,点击 “Device Management” ,在 “Specify search directory” 中选中 WTK 根目录,然后点击右下位置的 “Refresh” ,稍等片刻, WTK 默认的四个模拟设备就被找到了。
图二: EclipseME 配置 2
完成了这些,如果没有特殊要求,其他选项就不用再配置了。
接着新建一个名为 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 类,该类不属于任何的包。
图三:创建 Midlet
我们来看看,类里面怎样实现抽象方法的,并以如何在启动时进入菜单画面(登录前)这个功能切入。
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(newCommand("进入",Command.OK,1))”和“addCommand(newCommand("离开",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;
}
}
序列化方法serialize()很简单,仅仅是按照HTTP GET方法的格式进行查询条件组装,就像之前说到的,这里因为没有涉及到中文字符,所以无需进行UTF-8编码转换。”deserializes()”方法进行多对象的反序列化,并构造成TransAuto数组在手机上显示,需要强调的是根据分页策略,进行一次反序列化的TransAuto对象实例条数最多20条。服务器端返回的数据并不是按照HTTP GET方法的格式,而是根据表中字段的顺序再在中间加”&”进行分割的字符串,遗憾的是手机上String对象未提供”split()”方法,”Split.split(recString,"&")”方法化解了这一问题,下面是实现代码(来自于互联网)。
/**
* 分割字符串
*
* @param original 需要分割的字符串
* @paran regex 表达式
* @return 分割后生成的字符串数组
*/
public static String[] split(String original, String regex) {
int startIndex = 0;
Vector v = new Vector();
String[] str = null;
int index = 0;
startIndex = original.indexOf(regex);
while (startIndex < original.length() && startIndex != -1) {
String temp = original.substring(index, startIndex);
v.addElement(temp);
index = startIndex + regex.length();
startIndex = original.indexOf(regex, startIndex + regex.length());
}
v.addElement(original.substring(index + 1 - regex.length()));
str = new String[v.size()];
for (int i = 0; i < v.size(); i++) {
str[i] = (String) v.elementAt(i);
}
return str;
}
一旦所有前期工作都准备好,将是最关键的GoodsFindAutoThread线程类连接服务器返回数据,为了让界面美观,在进入线程类之后GoodsFindAuto类马上通过”Navigator.display.setCurrent(WaitForm.getInstance())”代码把画面转移到WaitForm,一个等待画面,上面写着“正在连接服务器,请稍等...”,直到线程类和服务器交互完毕,这个画面将由线程类负责切换。
package com.forbidden.thread;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.InputStream;
import javax.microedition.io.Connector;
import javax.microedition.io.HttpConnection;
import javax.microedition.lcdui.Alert;
import javax.microedition.lcdui.AlertType;
import javax.microedition.rms.RecordStore;
import com.forbidden.screen.*;
import com.forbidden.util.Split;
import com.forbidden.vo.TransAuto;
/* 货主找车线程
* @author rosen jiang
* @since 2005-12
*/
public class GoodsFindAutoThread extends Thread{
private String url; //服务器地址
private String autoFrom; //车辆出发地
private String autoTarget; //车辆目的地
private String pubDate; //发布日期
/**
* 构造函数
*
* @param page 请求页数
* @param perPage 每页条数
* @param ta 构造好的条件对象
*/
public GoodsFindAutoThread(int page,int perPage,TransAuto ta){
String userName=null;
try {
//取出当前用户名,以便进行数据库端日志记录。
RecordStore rs = RecordStore.openRecordStore("app",false);
userName = new String(rs.getRecord(1));
rs.closeRecordStore();
} catch (Exception e) {
}
this.url = "http://127.0.0.1:8080/AirTransport/servlet/GoodsFindAuto?userName="
+ userName+"&page="+page+"&perPage="+perPage+"&"+ta.serialize();
this.autoFrom = ta.getAutoFrom();
this.autoTarget = ta.getAutoTarget();
this.pubDate = ta.getPubDate();
}
public void run(){
InputStream is = null;
HttpConnection c = null;
try {
c = (HttpConnection)Connector.open(url);
//接收输出流
is = new DataInputStream(c.openInputStream());
int i = (int) c.getLength();
byte [] recData = new byte[i];
is.read(recData);
is.close();
c.close();
//未找到符合条件的车辆
if (recData[0]+recData[1]!=0) {
ByteArrayInputStream bin = new ByteArrayInputStream(recData);
DataInputStream din = new DataInputStream(bin);
Alert al = new Alert ("查询结果",din.readUTF(),null,AlertType.CONFIRMATION);
al.setTimeout(Alert.FOREVER);
din.close();
bin.close();
Navigator.display.setCurrent(al,GoodsFindAuto.getInstance());
} else {
//找到符合条件的车辆
//丢掉两个错误信息字节数据
byte[] reRecData = new byte[recData.length-2];
System.arraycopy(recData,2,reRecData,0,recData.length-2);
ByteArrayInputStream bin = new ByteArrayInputStream(reRecData);
//结果数据输入流
DataInputStream dis = new DataInputStream(bin);
int count = Integer.parseInt(dis.readUTF()); //总数
int total = Integer.parseInt(dis.readUTF()); //总页数
int current = Integer.parseInt(dis.readUTF());//当前页码
TransAuto ta = new TransAuto(null, null, null, null, null, null, null, null);
TransAuto[] tas = null;
tas = ta.deserializes(autoFrom,20,dis);
dis.close();
bin.close();
Navigator.display.setCurrent(GFAList.getInstance(count,total,current,autoFrom,autoTarget,pubDate,tas));
}
} catch(Exception e){
Alert al = new Alert ("查询发生错误",e.toString(),null,AlertType.ERROR);
al.setTimeout(Alert.FOREVER);
Navigator.display.setCurrent(al,GoodsFindAuto.getInstance());
}
}
}
在GoodsFindAutoThread()构造函数中,首先取出了记录在RMS中的当前登陆者名,以便服务器记录用户的查询条件,以后为不同的客户提供个性化服务,接着把用户输入的检索条件记录下来,取第5页的时候要用到,然后按照之前构造好的TransAuto实例进行序列化生成URL字符串。在”run()”方法中,为了判断是否返回了正确数据,我在服务器端的代码中设置了标识,既:如果查询结果为0条或服务器端发生任何错误,那么返回数据的前个两字节不为空,反之则亦然。收到正确的查询结果之后,先丢弃前两个字节,然后转换成输入流,”int count = Integer.parseInt(dis.readUTF())”第一组字节流是总记录数,”int total = Integer.parseInt(dis.readUTF())”第二组字节流是总页数,”int current = Integer.parseInt(dis.readUTF())”第三组字节流是当前页码。取完基本信息后,接着取车辆数据,通过”tas = ta.deserializes(autoFrom,20,dis)”反序列化之后得到TransAuto数组对象。最后进入GFAList类进行数据列表,代码”Navigator.display.setCurrent(GFAList.getInstance(count,total,current,autoFrom,autoTarget,pubDate,tas))”把检索条件全部记录下来,其做用也是为了再次分页的需要。
package com.forbidden.screen;
import javax.microedition.lcdui.Alert;
import javax.microedition.lcdui.AlertType;
import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.List;
import com.forbidden.thread.GoodsFindAutoThread;
import com.forbidden.vo.TransAuto;
/* 货主找车列表
* @author rosen jiang
* @since 2005-12
*/
public class GFAList extends List implements CommandListener{
private Command backCommand;
private Command funCommand;
private Command preCommand;
private Command nextCommand;
private static String autoFrom; //车辆出发地
private static String autoTarget; //车辆目的地
private static String pubDate; //发布日期
private static int count; //总数
private static int total; //总页数
private int current; //当前页码
private TransAuto[] newTas; //车辆数组
public static Displayable instance;//对象实例
/**
* 获取对象实例
*
* @param coun //总数
* @param tot //总页数
* @param curr //当前页码
* @param from //车辆出发地
* @param target //车辆目的地
* @param date //发布日期
* @param tas //车辆数组
* @return
*/
synchronized public static Displayable getInstance(int coun,int tot,int curr,String from,String target,String date,TransAuto[] tas){
autoFrom = from;
count = coun;
autoTarget = target;
pubDate = date;
total = tot;
instance = new GFAList(curr,tas,List.IMPLICIT);
return instance;
}
/**
* 构造函数
* @param curr //当前页码
* @param tas //车辆数组
* @param i //表现方式
*/
public GFAList(int curr,TransAuto[] tas,int i){
super(curr+"/"+total+"页 共"+count+"条记录",i);
this.current = curr;
this.newTas = tas;
for(int j=0;j<5;j++){
try{
TransAuto ta = null;
if(curr%4==0){
ta = tas[j+15];
}else if(curr%4==1){
ta = tas[j];
}else if(curr%4==2){
ta = tas[j+5];
}else if(curr%4==3){
ta = tas[j+10];
}
append(ta.getName()+","
+ta.getPhone()+".",null);
}catch(java.lang.NullPointerException e){
break;
}
}
backCommand = new Command("货主找车", Command.BACK, 1);
funCommand = new Command("详情", Command.SCREEN, 1);
preCommand = new Command("上一页", Command.SCREEN, 1);
nextCommand = new Command("下一页", Command.SCREEN, 1);
addCommand(backCommand);
addCommand(funCommand);
addCommand(preCommand);
addCommand(nextCommand);
setCommandListener(this);
}
/**
* 对用户输入命令作出反应
* @param c 命令
* @param s Displayable 对象
*/
public void commandAction(Command c, Displayable s) {
String cmd = c.getLabel();
if (cmd.equals("货主找车")){
Navigator.flow(cmd);
//翻页(下一页)处理
} else if (cmd.equals("下一页")){
if(total == current){
Alert al = new Alert ("提示","已到达最后一页。",null,AlertType.CONFIRMATION);
al.setTimeout(Alert.FOREVER);
Navigator.display.setCurrent(al);
} else if (current % 4 != 0){
Navigator.display.setCurrent(getInstance(count,total,current+1,autoFrom,autoTarget,pubDate,newTas));
//如果当前页已经是4的倍数页,那么连接服务器取下20条数据。
} else if (current % 4 == 0){
TransAuto ta = new TransAuto(null,null,null,null,
pubDate,autoFrom,autoTarget, null);
GoodsFindAutoThread gfat = new GoodsFindAutoThread(current+1,20,ta);
Navigator.display.setCurrent(WaitForm.getInstance());
gfat.start();
}
//翻页(上一页)处理
} else if (cmd.equals("上一页")){
if(current == 1){
Alert al = new Alert ("提示","这是第一页。",null,AlertType.CONFIRMATION);
al.setTimeout(Alert.FOREVER);
Navigator.display.setCurrent(al);
} else if ((current - 1) % 4 != 0){
Navigator.display.setCurrent(getInstance(count,total,current-1,autoFrom,autoTarget,pubDate,newTas));
//如果当前页已经是4的倍数页,那么连接服务器取上20条数据。
} else if ((current - 1) % 4 == 0){
TransAuto ta = new TransAuto(null,null,null,null,
pubDate,autoFrom,autoTarget, null);
GoodsFindAutoThread gfat = new GoodsFindAutoThread(current-1,20,ta);
Navigator.display.setCurrent(WaitForm.getInstance());
gfat.start();
}
//详情处理
} else if (cmd.equals("详情")){
if(current % 4 == 0){
Navigator.display.setCurrent(GFAInfo.getInstance(newTas[getSelectedIndex()+15]));
} else if (current % 4 == 1){
Navigator.display.setCurrent(GFAInfo.getInstance(newTas[getSelectedIndex()]));
} else if (current % 4 == 2){
Navigator.display.setCurrent(GFAInfo.getInstance(newTas[getSelectedIndex()+5]));
} else if (current % 4 == 3){
Navigator.display.setCurrent(GFAInfo.getInstance(newTas[getSelectedIndex()+10]));
}
}
}
}
构造函数GFAList除了创建几个命令以外,最主要的工作是显示本页数据,通过当前页码与4取模之后再从TransAuto数组得到准确数据并显示在屏幕上。还是让我们先睹为快吧,这次截图不是模拟器上,而是运行在我的索爱K700c上真实环境,如图八所示。
图八:车辆列表
看完截图,回到”commandAction()”方法上,当用户进行”下一页”操作的时候,首先判定当前页是不是最后一页,如果是”Alert al = newAlert ("提示","已到达最后一页。",null,AlertType.CONFIRMATION)”代码给用户一个Alert提示。如果当前页是4的倍数页,”GoodsFindAutoThread gfat = new GoodsFindAutoThread(current+1,20,ta)”代码连接服务器取回下20条数据,这个时候要进入WaitForm画面进行等待,就像首次进行查询一样,之前一直保存着的查询参数也就派上用场了。如果还不是4的倍数,”Navigator.display.setCurrent(getInstance(count,total,current+1,autoFrom,autoTarget,pubDate,newTas))”这行代码就把修改后的数据再次送到GFAList对象的构造函数中,重新实例化画面,也就在手机上实现了不用连接服务器的快速翻页。”上一页”操作和刚才谈到的”下一页”操作类似,就不多说了。如果用户点击”详情”看某条车辆信息的时候,需要计算被点击信息所处的绝对位置,比如当前页码正好是4的倍数,那么可以断定该条信息处于整个TransAuto数组的末端,也就是最后一页,然后取得这条记录的相对位置(例如3)再加上15,就是这条记录的绝对位置了,其他位置以此类推,”Navigator.display.setCurrent(GFAInfo.getInstance(newTas[getSelectedIndex()+15]))”代码把某个选中的车辆信息送入GFAInfo对象以显示详细信息。
package com.forbidden.screen;
import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.Form;
import javax.microedition.lcdui.TextField;
import com.forbidden.vo.TransAuto;
/* 车辆详情
* @author rosen jiang
* @since 2005-12
*/
public class GFAInfo extends Form implements CommandListener{
private Command backCommand;
private static Displayable instance;
private TextField nameField;
private TextField autoNoFiled;
private TextField phoneFiled;
private TextField autoCapField;
private TextField pubDateField;
private TextField autoFromField;
private TextField autoTargetField;
private TextField memoField;
/**
* 获取对象实例
* @param ta 车辆对象
* @return
*/
synchronized public static Displayable getInstance(TransAuto ta){
instance = new GFAInfo(ta);
return instance;
}
/**
* 构造函数
*
* @param ta 车辆对象
*/
public GFAInfo(TransAuto ta){
super("车辆详情");
backCommand = new Command("返回", Command.BACK, 1);
nameField = new TextField("车主", ta.getName(), 25, TextField.ANY);
autoNoFiled = new TextField("车牌号", ta.getAutoNo(), 25, TextField.ANY);
phoneFiled = new TextField("电话", ta.getPhone(), 25, TextField.ANY);
autoCapField = new TextField("重量", ta.getAutoCap(), 25, TextField.ANY);
pubDateField = new TextField("发布日期", ta.getPubDate(), 25, TextField.ANY);
autoFromField = new TextField("起始地", ta.getAutoFrom(), 25, TextField.ANY);
autoTargetField = new TextField("目的地", ta.getAutoTarget(), 25, TextField.ANY);
memoField = new TextField("备注", ta.getMemo(), 300, TextField.ANY);
append(nameField);
append(autoNoFiled);
append(phoneFiled);
append(autoCapField);
append(pubDateField);
append(autoFromField);
append(autoTargetField);
append(memoField);
addCommand(backCommand);
setCommandListener(this);
}
/**
* 对用户输入命令作出反应
* @param c 命令
* @param s Displayable 对象
*/
public void commandAction(Command c, Displayable s) {
Navigator.display.setCurrent(GFAList.instance);
}
}
GFAInfo 类代码显得很简单了,”getInstance()”方法每次都根据数据的不同重新构造实例,”GFAInfo()”构造函数则把TransAuto对象属性一一呈现出来,如图九所示的画面是dbMobile最底层画面,手机端实现也就告一段落了。
图九:车辆详情
服务器端实现
相比手机端,服务器端实现起来容易多了。采用的数据库是MySQL,并使用Proxool连接池,去年在我的blog上也做了引见,可参考《Proxool 0.9.0RC1 发布》一文。目前最新版本是0.9.0RC3,Proxool的作者基本上一年才仅仅更新一次RC后面的版本,可能明年才更新到0.9.0RC4,可谓慢工出细活。下面是服务器端的GoodsFindAuto Servlet类,由于是2年前的代码,那时为了简便起见,我把需要在DAO里做的事情都搬到这个Servlet了,另外代码里面应该用事务保证,我也偷懒了。
package com.forbidden.airtransport.servlet;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.Date;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.forbidden.airtransport.util.RegCode;
/* 货主找车Servlet
* @author rosen jiang
* @since 2005-12
*/
public class GoodsFindAuto extends HttpServlet {
/**
* 构造函数
*/
public GoodsFindAuto() {
super();
}
/**
* 销毁方法
*/
public void destroy() {
super.destroy();
}
/**
* 货主找车功能
*
* @param request HttpServletRequest
* @param response HttpServletResponse
* @throws ServletException
* @throws IOException
*/
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
//电话区号实际城市名互转类
RegCode rc = new RegCode();
//用户名,用于记载查询日志。
String userName = request.getParameter("userName");
//页数
int page = 0;
String strPage = request.getParameter("page");
//每页条数
int perPage = 0;
String strPerPage = request.getParameter("perPage");
//车辆出发地
String autoFrom = request.getParameter("autoFrom");
//车辆目的地
String autoTarget = request.getParameter("autoTarget");
//发布日期
String pubDate = request.getParameter("pubDate");
//查询日志
String writeLog = "insert into search_log (userName,flag,f_rom,target,serDate)" +
" values (?,?,?,?,?)";
//计算总记录数
String countRow = "select count(*) from auto_back where autoFrom=? and autoTarget=? " +
"and TO_DAYS(regDate)=TO_DAYS(?)";
//查询结果
String finSql = "select * from auto_back where autoFrom=? and autoTarget=? " +
"and TO_DAYS(regDate)=TO_DAYS(?) limit ?,?";
//连接信息
Connection conn = null;
PreparedStatement stm = null;
ResultSet rs = null;
//构建输出流
response.setContentType("application/octet-stream");
OutputStream ops = response.getOutputStream();
DataOutputStream dos = new DataOutputStream(ops);
try {
if(userName==null || strPage==null || strPerPage==null || autoFrom==null ||
autoTarget==null || pubDate==null){
dos.writeUTF("非法请求!");
}else{
page = Integer.parseInt(strPage);
perPage = Integer.parseInt(strPerPage);
//电话区号转换到实际地址
autoFrom = rc.convent(autoFrom);
autoTarget = rc.convent(autoTarget);
//获取连接
conn = DriverManager.getConnection("proxool.automy");
//记录请求信息
stm = conn.prepareStatement(writeLog);
stm.setString(1,userName);
stm.setString(2,"2");
stm.setString(3,autoFrom);
stm.setString(4,autoTarget);
Date sDate = new Date();
stm.setTimestamp(5,new Timestamp(sDate.getTime()));
stm.executeUpdate();
//计算结果集总数
stm = conn.prepareStatement(countRow);
Date date = new Date(Long.parseLong(pubDate));
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
String Fdate = formatter.format(date);
stm.setString(1,autoFrom);
stm.setString(2,autoTarget);
stm.setString(3,Fdate);
rs = stm.executeQuery();
rs.next();
int row = rs.getInt(1);
if(row==0){
dos.writeUTF("未找到你要匹配的数据。");
}else{
//进行查询
stm = conn.prepareStatement(finSql);
int rows = 0;
if(page==1){
rows = 0;
}else{
rows = perPage*((page-1)/4);
}
stm.setString(1,autoFrom);
stm.setString(2,autoTarget);
stm.setString(3,Fdate);
stm.setInt(4,rows);
stm.setInt(5,perPage);
rs = stm.executeQuery();
dos.writeUTF(""); //设置前两个字节为空
dos.writeUTF(row+""); //记录总数
if(row%perPage!=0){
dos.writeUTF(Integer.toString(1+(row/5))); //总页数
}else{
dos.writeUTF(Integer.toString((row/5))); //总页数
}
dos.writeUTF(Integer.toString(page)); //当前页
while(rs.next()){
//创建业务数据输出流
String resString = rs.getString(3)+"&"+rs.getString(4)
+"&"+rs.getString(6)+"&"+rs.getString(5)
+"&"+rs.getString(9) +"&"
+rs.getString(8)+"&"+rs.getString(10);
dos.writeUTF(resString);
}
}
}
//数据长度
response.setContentLength(dos.size());
} catch (Exception e) {
e.printStackTrace();
dos.writeUTF("服务器异常!"+e.toString());
}finally{
try {
rs.close();
stm.close();
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
dos.flush();
dos.close();
ops.close();
}
}
/**
* post方法
*
* @param request HttpServletRequest
* @param response HttpServletResponse
* @throws ServletException
* @throws IOException
*/
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
}
/**
* 初始化方法
*
* @throws ServletException
*/
public void init() throws ServletException {
}
}
Servlet 应该都很熟悉了,那就直奔”doGet()”方法。”RegCode rc = new RegCode()”构造一个电话区号到实际城市名互转的实现,实现方式也就是在数据库建立一个“电话区号—城市名”的一一对应关系。接着通过”response.setContentType("application/octet-stream")”代码声明输出类型。可以搞破坏了,我在浏览器中写上” http://127.0.0.1:8080/AirTransport/servlet/GoodsFindAuto”,GoodsFindAuto Servlet发现没有收到正确的查询条件,马上通过”dos.writeUTF("非法请求!")”代码抛出消息。
业务上要求无论有没有符合条件的结果,都要记录用户查询信息;在记录完查询信息后开始根据条件去数据库匹配,如果匹配结果为0就通过”dos.writeUTF("未找到你要匹配的数据。")”代码抛出消息;如果>0就取出详细数据,然后通过”dos.writeUTF(resString)”代码循环输出数据流,这和之前在手机端实现中说到的“根据表中字段的顺序再在中间加”&”进行分割的字符串”完全匹配,另外还不能忘了通过”dos.writeUTF("")”设置前两个字节为空,让手机端明白有业务数据返回。最后,无论服务器端发生任何异常,都可以通过”dos.writeUTF("服务器异常!"+e.toString())”代码告诉手机端。
如何做得更好
开发Java ME应用给我最大的感受是“模拟器是不可靠的(只能相信它70%)”,模拟器运行成功,但是手机上却出现千奇百怪的问题,遇到这样的困惑只有自己多花时间、多请教他人才能解决了。关于Java ME程序的调试,网上有一大把说明,我就不拿来引用了,不过我照着说明修改了Eclipse,始终有点问题。另外,索爱和诺基亚都能正常运行,但摩托罗拉手机始终有中文乱码,这个问题我还没解决。如果你很在意劳动成果被他人窃取,那么推荐使用ProGuard一个很好用的压缩、优化、混肴器,在EclipseME插件里有相应的配置,在这里引用一句话“混淆只是延缓了反编译的时间”,自己想想也行,至少有那么一点安全感。另外还有安全性的问题,既然以HTTP方式提供服务,那么就应该实现一种安全策略,我考虑的是类似HTTP Session的方式,在登录成功后服务器端生成以时间戳为参考的随机数返回给手机端,然后接下来的交互手机端必须提供这个随机数进行服务器端验证,成功后才能获取业务数据,另外还要提供超时机制。
我们知道连接GPRS很慢,但也要关注这样的用户提问:“为什么不能订阅本月内成都到上海的所有车辆呢?每天没事的时候打开程序让它在后台运行并自动同步数据保存在手机上,当符合条件的车辆出现时,能自动提醒我;不光如此,我还要在月底翻阅这些所有符合条件的车辆。”
依照现有方式,短信推送可实现自动提醒功能,但是无法实现数据浏览(短信字数有限);WAP方式可以实现数据浏览,但是无法实现提醒功能,除非WAP浏览器定时刷新页面或支持Ajax,就算支持也不能实现离线数据浏览!
问题的解决办法——“在手机上建立分布式数据库。”目前db4o西班牙团队领导的db4oME项目正在开发中,该项目能帮助实现手机分布式数据库的遐想。我们可以再想得更远点,是不是有了db4oME就能解决上面的问题?我认为可以解决,不过又会产生更多的问题,比如Java能管理手机上多大的存储空间,是否可以把数据存放在例如TF卡上,如果可以放在TF卡上那么检索效率如何,数据文件是否安全……
要实现遐想并解释上面的问题有待于广大开发者的共同努力,把自己的业余时间都投入在类似db4oME这样的技术上,如果这样的分布式数据库能成功实现,我想这会给移动计算带来一场革命,更多依托网络、数据库的手机应用程序将会诞生,发挥Java强大的优势!