前言
本文源于 2005 年底一個(gè)真實(shí)的手機項目。很早就想為那個(gè)項目寫(xiě)點(diǎn)什么了,至今才提筆,也算是了卻一個(gè)心愿。雖然時(shí)隔兩年,但技術(shù)本身并沒(méi)有發(fā)生什么太大的變化,我想本文應該能為廣大開(kāi)發(fā)人員提供幫助吧。
受朋友之托,他們接到一個(gè)手機應用項目(以下簡(jiǎn)稱(chēng) dbMobile )。 dbMobile 項目主要服務(wù)于零擔物流運輸,為廣大的貨主和司機建立一個(gè)暢通的交流平臺,實(shí)現便利的貨主找車(chē),車(chē)主找貨功能。只要貨主或車(chē)主的手機支持 Java ,安裝注冊之后以用戶(hù)身份登錄上去,就能免費查詢(xún)自己想要的信息。本文講貫穿整個(gè) dbMobile 項目,并重點(diǎn)介紹開(kāi)發(fā)者最關(guān)注的內容。
手機端實(shí)現
(由于我是做 Java EE 應用的,為了讓自己以后參考,所以關(guān)于手機端實(shí)現寫(xiě)得較啰嗦。)要進(jìn)行 Java ME 開(kāi)發(fā),首先到 http://java.sun.com/products/sjwtoolkit/download-2_5.html 下載 WTK 2.5 ,然后一步步安裝好(發(fā)現安裝界面比 2.2 漂亮了)。接著(zhù)下載 IDE 插件,我用的開(kāi)發(fā)環(huán)境是 Eclipse ,在 http://eclipseme.org/ 找到 EclipseME 的安裝包 eclipseme.feature_1.7.5_site ,解壓縮之后(也可以不解壓縮,只是安裝方式稍有不同)在 Eclipse 里面新建一個(gè) “New Local Site…” ,定位到剛才插件解壓縮之后的位置,一步步安裝即可。重啟 Eclipse 之后可以在 “Preferences” 選項中發(fā)現 “J2ME” 菜單,現在開(kāi)始配置 “WTK Root” ,如圖一所示。
圖一: EclipseME 配置 1
配置好 WTK Root 之后,我們還要為 dbMobile 配置設備。如圖二所示,點(diǎn)擊 “Device Management” ,在 “Specify search directory” 中選中 WTK 根目錄,然后點(diǎn)擊右下位置的 “Refresh” ,稍等片刻, WTK 默認的四個(gè)模擬設備就被找到了。
圖二: EclipseME 配置 2
完成了這些,如果沒(méi)有特殊要求,其他選項就不用再配置了。
接著(zhù)新建一個(gè)名為 dbMobile 的 J2ME 項目(既新建 “J2ME Midlet Suit” ),如果你沒(méi)有安裝多個(gè) WTK 版本或者不想使用默認的彩色模擬器的的話(huà),在新建項目的時(shí)候,無(wú)需進(jìn)行過(guò)多的配置。
MIDlet 是 MIDP 的基本執行單元,如同 Servlet 繼承自 javax.servlet.http.HttpServlet 一樣, MIdlet 必須繼承自 javax.microedition.midlet.MIDlet 抽象類(lèi)。該類(lèi)定義了三個(gè)抽象方法, startApp() 、 pauseApp() 、 destroyApp() ,應用程序管理器通過(guò)上面這三個(gè)方法控制著(zhù) MIdlet 的生命周期。在編寫(xiě) MIDlet 時(shí)必須實(shí)現這三個(gè)方法。如圖三所示,我為 dbMobile 創(chuàng )建了 HttpCli 類(lèi),該類(lèi)不屬于任何的包。
圖三:創(chuàng )建 Midlet
我們來(lái)看看,類(lèi)里面怎樣實(shí)現抽象方法的,并以如何在啟動(dòng)時(shí)進(jìn)入菜單畫(huà)面(登錄前)這個(gè)功能切入。
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 );
}
/**
* 啟動(dòng)方法
*/
public void startApp(){
Navigator.current = Navigator.MAIN_SCREEN;
Navigator.show();
}
/**
* 暫停方法
*/
protected void pauseApp() {
// TODO Auto-generated method stub
}
/**
* 銷(xiāo)毀方法
*/
protected void destroyApp( boolean arg0) throws MIDletStateChangeException {
this .notifyDestroyed();
}
}
我們在構造函數 “ HttpCli() ” 中 用到了名叫 Navigator 的導航類(lèi),該類(lèi)的主要作用是把 dbMobile 中所有的頁(yè)面管理起來(lái)、統一進(jìn)行頁(yè)面跳轉控制(稍后我會(huì )把 Navigator 類(lèi)代碼列出來(lái))。接著(zhù)看構造函數, “Navigator.midlet = this” 的作用是把整個(gè) MIDlet 實(shí)例交給導航類(lèi),以便在退出程序時(shí)觸發(fā)。 “Navigator.display = Display.getDisplay(this)” ,在手機屏幕上顯示一幅畫(huà)面就是一個(gè) Display 對象要實(shí)現的功能,從 MIDlet 實(shí)例中獲取 Display 對象實(shí)例,也就是在向導航類(lèi)授予一個(gè)進(jìn)行畫(huà)面切換的控制權。接著(zhù)看 “startApp () ” 啟動(dòng)方法,同樣調用了導航類(lèi),并設置啟動(dòng)后首先進(jìn)入的頁(yè)面是菜單畫(huà)面(登錄前)。
接下來(lái)我們看看 Navigator 導航類(lèi)都有些什么。
package com.forbidden.screen;
import javax.microedition.midlet.MIDlet;
import javax.microedition.lcdui. * ;
/*
* 導航類(lèi)
* @author rosen jiang
* @since 2005-12
*/
public class Navigator{
// 菜單畫(huà)面(登錄前)
final public static int MAIN_SCREEN = 1 ;
// 用戶(hù)注冊
final public static int USER_REG = 2 ;
// 車(chē)主找貨
final public static int AUTO_FIND_GOODS = 3 ;
// 用戶(hù)登錄
final public static int USER_LOGIN = 4 ;
// 菜單畫(huà)面(登錄后)
final public static int MENU_SCREEN = 5 ;
// 貨主找車(chē)
final public static int GOODS_FIND_AUTO = 6 ;
// 空車(chē)信息發(fā)布
final public static int AUTO_PUB = 7 ;
// 貨物信息發(fā)布
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( " 離開(kāi) " )){
midlet.notifyDestroyed();
} else if (cmd.equals( " 注冊 " )){
current = USER_REG;
show ();
} else if (cmd.equals( " 車(chē)主找貨 " )){
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( " 貨主找車(chē) " )){
current = GOODS_FIND_AUTO;
show ();
} else if (cmd.equals( " 空車(chē)信息發(fā)布 " )){
current = AUTO_PUB;
show ();
} else if (cmd.equals( " 貨物信息發(fā)布 " )){
current = GOODS_PUB;
show ();
} else if (cmd.equals( " 修改注冊信息 " )){
current = REG_UPD;
show ();
}
}
}
該類(lèi)對每個(gè)畫(huà)面進(jìn)行了編號處理, “show()” 方法是整個(gè)導航類(lèi)的關(guān)鍵,當符合條件的畫(huà)面編號被找到時(shí),調用 “display.setCurrent()” 方法設置被顯示畫(huà)面的實(shí)例,同時(shí)手機上也會(huì )切換到相應畫(huà)面。 “flow()” 方法做用是捕獲用戶(hù)的控制命令,并把命令轉換成內部的畫(huà)面編號,和 “show()” 聯(lián)合使用就能響應用戶(hù)操作了。
下面是菜單畫(huà)面(登錄前)類(lèi)。
package com.forbidden.screen;
import javax.microedition.lcdui. * ;
/*
* 菜單畫(huà)面(登錄前)
* @author rosen jiang
* @since 2005-12
*/
public class MainScreen extends List implements CommandListener{
// 對象實(shí)例
private static Displayable instance;
/**
* 獲取對象實(shí)例
*/
synchronized public static Displayable getInstance(){
if (instance == null )
instance = new MainScreen();
return instance;
}
/**
* 畫(huà)面內容
*/
private MainScreen(){
super ( " 菜單 " , Choice.IMPLICIT);
append ( " 注冊 " , null );
append ( " 登陸 " , null );
addCommand( new Command( " 進(jìn)入 " ,Command.OK, 1 ));
addCommand( new Command( " 離開(kāi) " ,Command.EXIT, 1 ));
setCommandListener( this );
}
/**
* 對用戶(hù)輸入命令作出反應
* @param c 命令
* @param s Displayable 對象
*/
public void commandAction(Command c, Displayable s){
String cmd = c.getLabel();
if (cmd.equals( " 進(jìn)入 " )){
String comd = getString(getSelectedIndex());
Navigator.flow(comd);
} else if (cmd.equals( " 離開(kāi) " )) {
Navigator.flow(cmd);
}
}
}
圖四:高級界面類(lèi)圖
Displayable 是所有高級(Screan)、低級(Canvas)界面的父類(lèi),在 dbMobile 項目中,由于專(zhuān)注于數據而不是界面,所以我決定采用高級界面。圖四列出了高級界面的類(lèi)、接口關(guān)系,可以對整個(gè)高級界面開(kāi)發(fā)有個(gè)概括。關(guān)于高級界面編程基礎的話(huà)題就不多說(shuō)了,請參考其他資料。
在整個(gè)程序加載的時(shí)候會(huì )首先實(shí)例化HttpCli類(lèi),接著(zhù)觸發(fā)”Navigator.MAIN_SCREEN”,最后實(shí)例化MainScreen類(lèi),在手機屏幕上顯示如圖五的畫(huà)面。MainScreen類(lèi)的“getInstance()”方法返回 MainScreen 唯一對象實(shí)例。在“MainScreen()”構造函數中,“super ("菜單", Choice.IMPLICIT)”創(chuàng )建名為“菜單”的單選列表,然后分別用“append("注冊",null)”和“append ("登陸",null)”追加兩個(gè)選項,接著(zhù)追加兩個(gè)命令“addCommand(new Command("進(jìn)入",Command.OK,1))”和“addCommand(new Command("離開(kāi)",Command.EXIT,1))”,最后針對當前對象實(shí)例設置命令監聽(tīng)器“setCommandListener(this)”。在手機上一切都已正確顯示后就可以監聽(tīng)用戶(hù)的操作了,“commandAction()”方法捕捉用戶(hù)點(diǎn)擊的是”離開(kāi)”還是”進(jìn)入”。如果是”離開(kāi)”,直接利用Navigator類(lèi)退出整個(gè)程序,如果是”進(jìn)入”則通過(guò)”String comd = getString(getSelectedIndex())”代碼獲取用戶(hù)選擇的菜單,然后再通過(guò)Navigator類(lèi)的”flow()”方法實(shí)例化相應的畫(huà)面實(shí)例,就像進(jìn)入菜單畫(huà)面(登錄前)一樣。
可能你非常熟悉以上這些調用流程,本文到這里開(kāi)始轉到如何與 Java EE服務(wù)器端通訊的部分。
圖六:主菜單(登錄后)
登錄成功以后進(jìn)入主菜單(登錄后)如圖六所示,現在我重點(diǎn)介紹貨主找車(chē)這個(gè)功能,首先要創(chuàng )建貨主找車(chē)界面,GoodsFindAuto類(lèi)代碼如下:
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;
/* 貨主找車(chē)輸入查詢(xún)條件頁(yè)面
* @author rosen jiang
* @since 2005-12
*/
public class GoodsFindAuto extends Form implements CommandListener {
//車(chē)輛出發(fā)地
private TextField autoFromField;
//車(chē)輛目的地
private TextField autoTargetField;
//發(fā)布時(shí)間
private DateField pubDateField;
//對象實(shí)例
private static Displayable instance;
/**
* 獲取對象實(shí)例
*/
synchronized public static Displayable getInstance(){
if (instance==null)
instance = new GoodsFindAuto("貨主找車(chē)");
return instance;
}
/**
* 畫(huà)面內容
*/
public GoodsFindAuto(String arg0) {
super(arg0);
autoFromField = new TextField("車(chē)輛出發(fā)地", "28", 25, TextField.NUMERIC);
autoTargetField = new TextField("車(chē)輛目的地", null, 25, TextField.NUMERIC);
pubDateField = new DateField("發(fā)布日期", DateField.DATE);
pubDateField.setDate(new Date());
append(autoFromField);
append(autoTargetField);
append(pubDateField);
Command backCommand = new Command("功能列表", Command.BACK, 1);
Command sendCommand = new Command("查詢(xún)", Command.SCREEN, 1);
addCommand(backCommand);
addCommand(sendCommand);
setCommandListener(this);
}
/**
* 對用戶(hù)輸入命令作出反應
* @param c 命令
* @param s Displayable 對象
*/
public void commandAction(Command c, Displayable s) {
String cmd = c.getLabel();
if (cmd.equals("查詢(xún)")){
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()+"";
//發(fā)送查詢(xún)
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);
}
}
}
對于手機用戶(hù)來(lái)說(shuō),要用最簡(jiǎn)單的界面實(shí)現查詢(xún)功能那是最好不過(guò)了。在構造函數里面添加了三個(gè)輸入框"車(chē)輛出發(fā)地"、"車(chē)輛目的地"、”發(fā)布日期”,為了更進(jìn)一步減少用戶(hù)輸入,在"車(chē)輛出發(fā)地"和”車(chē)輛目的地"是按照當地的去掉0的電話(huà)區號來(lái)作為條件,默認的以成都(28)為車(chē)輛出發(fā)地,運行效果如圖七所示。
圖七:貨主找車(chē)
當用戶(hù)完成查詢(xún)并點(diǎn)擊”查詢(xún)之后”,要對用戶(hù)的輸入信息進(jìn)行判斷,根據業(yè)務(wù)上的要求,”車(chē)輛目的地”是必填項,如果為空,在”commandAction()”方法中會(huì )通過(guò)Alert對象進(jìn)行提示。接下來(lái)將與服務(wù)器進(jìn)行數據交互,交互之前先把查詢(xún)條件構造成TransAuto車(chē)輛對象實(shí)例并進(jìn)行序列化,然后再通過(guò)HTTP GET方法請求服務(wù)器,服務(wù)器收到序列化的數據后抽取查詢(xún)條件。手機端和服務(wù)器端通訊的策略是:從手機端到服務(wù)器端是通過(guò)拼接字符串然后GET過(guò)去,而從服務(wù)器端到手機端則通過(guò)UTF-8編碼后的數據流送回來(lái),否則容易出現亂碼。如果你要問(wèn)為什么不使用GBK、GB2312編碼輸出,我的回答是DataOutputStream/ DataInputStream類(lèi)原生支持”writeUTF()/readUTF()”方法,無(wú)論是在服務(wù)器端還是手機端,轉換起來(lái)很輕松,盡管UTF-8三字節編碼會(huì )產(chǎn)生更多的通訊流量。”GoodsFindAutoThread(1,20,ta)”構造函數來(lái)自GoodsFindAutoThread線(xiàn)程類(lèi),該線(xiàn)程類(lèi)用于遠程HTTP連接,由于GPRS連接非常慢,為了提高網(wǎng)絡(luò )利用率,要一次多傳些查詢(xún)結果到手機端,這就涉及到了分頁(yè),我定義的分頁(yè)策略是:一次從服務(wù)器端取最多20條記錄,然后在手機上分成4頁(yè)顯示(每頁(yè)5條);如果總記錄數超過(guò)20條,當手機將要閱讀第5頁(yè)的時(shí)候再取下20條。那么上面的構造函數實(shí)際上是發(fā)出了獲取從1—4頁(yè)共20條數據的分頁(yè)請求。在進(jìn)入線(xiàn)程類(lèi)的話(huà)題之前,先看看TransAuto車(chē)輛類(lèi)。
package com.forbidden.vo;
import java.io.DataInputStream;
import java.io.IOException;
import com.forbidden.util.Split;
/* 車(chē)輛
* @author rosen jiang
* @since 2005-12
*/
public class TransAuto{
//車(chē)主名
private String name;
//車(chē)牌號
private String autoNo;
//聯(lián)系電話(huà)
private String phone;
//車(chē)輛容積
private String autoCap;
//發(fā)布時(shí)間
private String pubDate;
//車(chē)輛出發(fā)地
private String autoFrom;
//車(chē)輛目的地
private String autoTarget;
//備注
private String memo;
/**
* 構造函數
*
* @param name 車(chē)主名
* @param autoNo 車(chē)牌號
* @param phone 聯(lián)系電話(huà)
* @param autoCap 車(chē)輛容積
* @param pubDate 發(fā)布時(shí)間
* @param autoFrom 車(chē)輛出發(fā)地
* @param autoTarget 車(chē)輛目的地
* @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 車(chē)輛出發(fā)地
* @param rows 條數
* @param din 輸入流
* @return TransAuto[] 車(chē)輛數組
* @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]+"時(shí)";
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()很簡(jiǎn)單,僅僅是按照HTTP GET方法的格式進(jìn)行查詢(xún)條件組裝,就像之前說(shuō)到的,這里因為沒(méi)有涉及到中文字符,所以無(wú)需進(jìn)行UTF-8編碼轉換。”deserializes()”方法進(jìn)行多對象的反序列化,并構造成TransAuto數組在手機上顯示,需要強調的是根據分頁(yè)策略,進(jìn)行一次反序列化的TransAuto對象實(shí)例條數最多20條。服務(wù)器端返回的數據并不是按照HTTP GET方法的格式,而是根據表中字段的順序再在中間加”&”進(jìn)行分割的字符串,遺憾的是手機上String對象未提供”split()”方法,”Split.split(recString,"&")”方法化解了這一問(wèn)題,下面是實(shí)現代碼(來(lái)自于互聯(lián)網(wǎng))。
/**
* 分割字符串
*
* @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;
}
一旦所有前期工作都準備好,將是最關(guān)鍵的GoodsFindAutoThread線(xiàn)程類(lèi)連接服務(wù)器返回數據,為了讓界面美觀(guān),在進(jìn)入線(xiàn)程類(lèi)之后GoodsFindAuto類(lèi)馬上通過(guò)”Navigator.display.setCurrent(WaitForm.getInstance())”代碼把畫(huà)面轉移到WaitForm,一個(gè)等待畫(huà)面,上面寫(xiě)著(zhù)“正在連接服務(wù)器,請稍等...”,直到線(xiàn)程類(lèi)和服務(wù)器交互完畢,這個(gè)畫(huà)面將由線(xiàn)程類(lèi)負責切換。
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;
/* 貨主找車(chē)線(xiàn)程
* @author rosen jiang
* @since 2005-12
*/
public class GoodsFindAutoThread extends Thread{
private String url; //服務(wù)器地址
private String autoFrom; //車(chē)輛出發(fā)地
private String autoTarget; //車(chē)輛目的地
private String pubDate; //發(fā)布日期
/**
* 構造函數
*
* @param page 請求頁(yè)數
* @param perPage 每頁(yè)條數
* @param ta 構造好的條件對象
*/
public GoodsFindAutoThread(int page,int perPage,TransAuto ta){
String userName=null;
try {
//取出當前用戶(hù)名,以便進(jìn)行數據庫端日志記錄。
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();
//未找到符合條件的車(chē)輛
if (recData[0]+recData[1]!=0) {
ByteArrayInputStream bin = new ByteArrayInputStream(recData);
DataInputStream din = new DataInputStream(bin);
Alert al = new Alert ("查詢(xún)結果",din.readUTF(),null,AlertType.CONFIRMATION);
al.setTimeout(Alert.FOREVER);
din.close();
bin.close();
Navigator.display.setCurrent(al,GoodsFindAuto.getInstance());
} else {
//找到符合條件的車(chē)輛
//丟掉兩個(gè)錯誤信息字節數據
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()); //總頁(yè)數
int current = Integer.parseInt(dis.readUTF());//當前頁(yè)碼
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 ("查詢(xún)發(fā)生錯誤",e.toString(),null,AlertType.ERROR);
al.setTimeout(Alert.FOREVER);
Navigator.display.setCurrent(al,GoodsFindAuto.getInstance());
}
}
}
在GoodsFindAutoThread()構造函數中,首先取出了記錄在RMS中的當前登陸者名,以便服務(wù)器記錄用戶(hù)的查詢(xún)條件,以后為不同的客戶(hù)提供個(gè)性化服務(wù),接著(zhù)把用戶(hù)輸入的檢索條件記錄下來(lái),取第5頁(yè)的時(shí)候要用到,然后按照之前構造好的TransAuto實(shí)例進(jìn)行序列化生成URL字符串。在”run()”方法中,為了判斷是否返回了正確數據,我在服務(wù)器端的代碼中設置了標識,既:如果查詢(xún)結果為0條或服務(wù)器端發(fā)生任何錯誤,那么返回數據的前個(gè)兩字節不為空,反之則亦然。收到正確的查詢(xún)結果之后,先丟棄前兩個(gè)字節,然后轉換成輸入流,”int count = Integer.parseInt(dis.readUTF())”第一組字節流是總記錄數,”int total = Integer.parseInt(dis.readUTF())”第二組字節流是總頁(yè)數,”int current = Integer.parseInt(dis.readUTF())”第三組字節流是當前頁(yè)碼。取完基本信息后,接著(zhù)取車(chē)輛數據,通過(guò)”tas = ta.deserializes(autoFrom,20,dis)”反序列化之后得到TransAuto數組對象。最后進(jìn)入GFAList類(lèi)進(jìn)行數據列表,代碼” Navigator.display.setCurrent(GFAList.getInstance(count,total,current,autoFrom,autoTarget,pubDate,tas))”把檢索條件全部記錄下來(lái),其做用也是為了再次分頁(yè)的需要。
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;
/* 貨主找車(chē)列表
* @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; //車(chē)輛出發(fā)地
private static String autoTarget; //車(chē)輛目的地
private static String pubDate; //發(fā)布日期
private static int count; //總數
private static int total; //總頁(yè)數
private int current; //當前頁(yè)碼
private TransAuto[] newTas; //車(chē)輛數組
public static Displayable instance;//對象實(shí)例
/**
* 獲取對象實(shí)例
*
* @param coun //總數
* @param tot //總頁(yè)數
* @param curr //當前頁(yè)碼
* @param from //車(chē)輛出發(fā)地
* @param target //車(chē)輛目的地
* @param date //發(fā)布日期
* @param tas //車(chē)輛數組
* @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 //當前頁(yè)碼
* @param tas //車(chē)輛數組
* @param i //表現方式
*/
public GFAList(int curr,TransAuto[] tas,int i){
super(curr+"/"+total+"頁(yè) 共"+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("貨主找車(chē)", Command.BACK, 1);
funCommand = new Command("詳情", Command.SCREEN, 1);
preCommand = new Command("上一頁(yè)", Command.SCREEN, 1);
nextCommand = new Command("下一頁(yè)", Command.SCREEN, 1);
addCommand(backCommand);
addCommand(funCommand);
addCommand(preCommand);
addCommand(nextCommand);
setCommandListener(this);
}
/**
* 對用戶(hù)輸入命令作出反應
* @param c 命令
* @param s Displayable 對象
*/
public void commandAction(Command c, Displayable s) {
String cmd = c.getLabel();
if (cmd.equals("貨主找車(chē)")){
Navigator.flow(cmd);
//翻頁(yè)(下一頁(yè))處理
} else if (cmd.equals("下一頁(yè)")){
if(total == current){
Alert al = new Alert ("提示","已到達最后一頁(yè)。",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));
//如果當前頁(yè)已經(jīng)是4的倍數頁(yè),那么連接服務(wù)器取下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();
}
//翻頁(yè)(上一頁(yè))處理
} else if (cmd.equals("上一頁(yè)")){
if(current == 1){
Alert al = new Alert ("提示","這是第一頁(yè)。",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));
//如果當前頁(yè)已經(jīng)是4的倍數頁(yè),那么連接服務(wù)器取上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除了創(chuàng )建幾個(gè)命令以外,最主要的工作是顯示本頁(yè)數據,通過(guò)當前頁(yè)碼與4取模之后再從TransAuto數組得到準確數據并顯示在屏幕上。還是讓我們先睹為快吧,這次截圖不是模擬器上,而是運行在我的索愛(ài)K700c上真實(shí)環(huán)境,如圖八所示。
圖八:車(chē)輛列表
看完截圖,回到”commandAction()”方法上,當用戶(hù)進(jìn)行”下一頁(yè)”操作的時(shí)候,首先判定當前頁(yè)是不是最后一頁(yè),如果是”Alert al = new Alert ("提示","已到達最后一頁(yè)。",null,AlertType.CONFIRMATION)”代碼給用戶(hù)一個(gè)Alert提示。如果當前頁(yè)是4的倍數頁(yè),”GoodsFindAutoThread gfat = new GoodsFindAutoThread(current+1,20,ta)”代碼連接服務(wù)器取回下20條數據,這個(gè)時(shí)候要進(jìn)入WaitForm畫(huà)面進(jìn)行等待,就像首次進(jìn)行查詢(xún)一樣,之前一直保存著(zhù)的查詢(xún)參數也就派上用場(chǎng)了。如果還不是4的倍數,”Navigator.display.setCurrent(getInstance(count,total,current+1,autoFrom,autoTarget,pubDate,newTas))”這行代碼就把修改后的數據再次送到GFAList對象的構造函數中,重新實(shí)例化畫(huà)面,也就在手機上實(shí)現了不用連接服務(wù)器的快速翻頁(yè)。”上一頁(yè)”操作和剛才談到的”下一頁(yè)”操作類(lèi)似,就不多說(shuō)了。如果用戶(hù)點(diǎn)擊”詳情”看某條車(chē)輛信息的時(shí)候,需要計算被點(diǎn)擊信息所處的絕對位置,比如當前頁(yè)碼正好是4的倍數,那么可以斷定該條信息處于整個(gè)TransAuto數組的末端,也就是最后一頁(yè),然后取得這條記錄的相對位置(例如3)再加上15,就是這條記錄的絕對位置了,其他位置以此類(lèi)推,”Navigator.display.setCurrent(GFAInfo.getInstance(newTas[getSelectedIndex()+15]))”代碼把某個(gè)選中的車(chē)輛信息送入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;
/* 車(chē)輛詳情
* @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;
/**
* 獲取對象實(shí)例
* @param ta 車(chē)輛對象
* @return
*/
synchronized public static Displayable getInstance(TransAuto ta){
instance = new GFAInfo(ta);
return instance;
}
/**
* 構造函數
*
* @param ta 車(chē)輛對象
*/
public GFAInfo(TransAuto ta){
super("車(chē)輛詳情");
backCommand = new Command("返回", Command.BACK, 1);
nameField = new TextField("車(chē)主", ta.getName(), 25, TextField.ANY);
autoNoFiled = new TextField("車(chē)牌號", ta.getAutoNo(), 25, TextField.ANY);
phoneFiled = new TextField("電話(huà)", ta.getPhone(), 25, TextField.ANY);
autoCapField = new TextField("重量", ta.getAutoCap(), 25, TextField.ANY);
pubDateField = new TextField("發(fā)布日期", 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);
}
/**
* 對用戶(hù)輸入命令作出反應
* @param c 命令
* @param s Displayable 對象
*/
public void commandAction(Command c, Displayable s) {
Navigator.display.setCurrent(GFAList.instance);
}
}
GFAInfo 類(lèi)代碼顯得很簡(jiǎn)單了,”getInstance()”方法每次都根據數據的不同重新構造實(shí)例,”GFAInfo()”構造函數則把TransAuto對象屬性一一呈現出來(lái),如圖九所示的畫(huà)面是dbMobile最底層畫(huà)面,手機端實(shí)現也就告一段落了。
圖九:車(chē)輛詳情
服務(wù)器端實(shí)現
相比手機端,服務(wù)器端實(shí)現起來(lái)容易多了。采用的數據庫是MySQL,并使用Proxool連接池,去年在我的blog上也做了引見(jiàn),可參考《Proxool 0.9.0RC1 發(fā)布》一文。目前最新版本是0.9.0RC3,Proxool的作者基本上一年才僅僅更新一次RC后面的版本,可能明年才更新到0.9.0RC4,可謂慢工出細活。下面是服務(wù)器端的GoodsFindAuto Servlet類(lèi),由于是2年前的代碼,那時(shí)為了簡(jiǎn)便起見(jiàn),我把需要在DAO里做的事情都搬到這個(gè)Servlet了,另外代碼里面應該用事務(wù)保證,我也偷懶了。
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;
/* 貨主找車(chē)Servlet
* @author rosen jiang
* @since 2005-12
*/
public class GoodsFindAuto extends HttpServlet {
/**
* 構造函數
*/
public GoodsFindAuto() {
super();
}
/**
* 銷(xiāo)毀方法
*/
public void destroy() {
super.destroy();
}
/**
* 貨主找車(chē)功能
*
* @param request HttpServletRequest
* @param response HttpServletResponse
* @throws ServletException
* @throws IOException
*/
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
//電話(huà)區號實(shí)際城市名互轉類(lèi)
RegCode rc = new RegCode();
//用戶(hù)名,用于記載查詢(xún)日志。
String userName = request.getParameter("userName");
//頁(yè)數
int page = 0;
String strPage = request.getParameter("page");
//每頁(yè)條數
int perPage = 0;
String strPerPage = request.getParameter("perPage");
//車(chē)輛出發(fā)地
String autoFrom = request.getParameter("autoFrom");
//車(chē)輛目的地
String autoTarget = request.getParameter("autoTarget");
//發(fā)布日期
String pubDate = request.getParameter("pubDate");
//查詢(xún)日志
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(?)";
//查詢(xún)結果
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);
//電話(huà)區號轉換到實(shí)際地址
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{
//進(jìn)行查詢(xún)
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(""); //設置前兩個(gè)字節為空
dos.writeUTF(row+""); //記錄總數
if(row%perPage!=0){
dos.writeUTF(Integer.toString(1+(row/5))); //總頁(yè)數
}else{
dos.writeUTF(Integer.toString((row/5))); //總頁(yè)數
}
dos.writeUTF(Integer.toString(page)); //當前頁(yè)
while(rs.next()){
//創(chuàng )建業(yè)務(wù)數據輸出流
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);
}
}
}
//數據長(cháng)度
response.setContentLength(dos.size());
} catch (Exception e) {
e.printStackTrace();
dos.writeUTF("服務(wù)器異常!"+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()”構造一個(gè)電話(huà)區號到實(shí)際城市名互轉的實(shí)現,實(shí)現方式也就是在數據庫建立一個(gè)“電話(huà)區號—城市名”的一一對應關(guān)系。接著(zhù)通過(guò)”response.setContentType("application/octet-stream")”代碼聲明輸出類(lèi)型。可以搞破壞了,我在瀏覽器中寫(xiě)上” http://127.0.0.1:8080/AirTransport/servlet/GoodsFindAuto”,GoodsFindAuto Servlet發(fā)現沒(méi)有收到正確的查詢(xún)條件,馬上通過(guò)”dos.writeUTF("非法請求!")”代碼拋出消息。
業(yè)務(wù)上要求無(wú)論有沒(méi)有符合條件的結果,都要記錄用戶(hù)查詢(xún)信息;在記錄完查詢(xún)信息后開(kāi)始根據條件去數據庫匹配,如果匹配結果為0就通過(guò)”dos.writeUTF("未找到你要匹配的數據。")”代碼拋出消息;如果>0就取出詳細數據,然后通過(guò)”dos.writeUTF(resString)”代碼循環(huán)輸出數據流,這和之前在手機端實(shí)現中說(shuō)到的“根據表中字段的順序再在中間加”&”進(jìn)行分割的字符串”完全匹配,另外還不能忘了通過(guò)”dos.writeUTF("")”設置前兩個(gè)字節為空,讓手機端明白有業(yè)務(wù)數據返回。最后,無(wú)論服務(wù)器端發(fā)生任何異常,都可以通過(guò)”dos.writeUTF("服務(wù)器異常!"+e.toString())”代碼告訴手機端。
如何做得更好
開(kāi)發(fā)Java ME應用給我最大的感受是“模擬器是不可靠的(只能相信它70%)”,模擬器運行成功,但是手機上卻出現千奇百怪的問(wèn)題,遇到這樣的困惑只有自己多花時(shí)間、多請教他人才能解決了。關(guān)于Java ME程序的調試,網(wǎng)上有一大把說(shuō)明,我就不拿來(lái)引用了,不過(guò)我照著(zhù)說(shuō)明修改了Eclipse,始終有點(diǎn)問(wèn)題。另外,索愛(ài)和諾基亞都能正常運行,但摩托羅拉手機始終有中文亂碼,這個(gè)問(wèn)題我還沒(méi)解決。如果你很在意勞動(dòng)成果被他人竊取,那么推薦使用ProGuard一個(gè)很好用的壓縮、優(yōu)化、混肴器,在EclipseME插件里有相應的配置,在這里引用一句話(huà)“混淆只是延緩了反編譯的時(shí)間”,自己想想也行,至少有那么一點(diǎn)安全感。另外還有安全性的問(wèn)題,既然以HTTP方式提供服務(wù),那么就應該實(shí)現一種安全策略,我考慮的是類(lèi)似HTTP Session的方式,在登錄成功后服務(wù)器端生成以時(shí)間戳為參考的隨機數返回給手機端,然后接下來(lái)的交互手機端必須提供這個(gè)隨機數進(jìn)行服務(wù)器端驗證,成功后才能獲取業(yè)務(wù)數據,另外還要提供超時(shí)機制。
我們知道連接GPRS很慢,但也要關(guān)注這樣的用戶(hù)提問(wèn):“為什么不能訂閱本月內成都到上海的所有車(chē)輛呢?每天沒(méi)事的時(shí)候打開(kāi)程序讓它在后臺運行并自動(dòng)同步數據保存在手機上,當符合條件的車(chē)輛出現時(shí),能自動(dòng)提醒我;不光如此,我還要在月底翻閱這些所有符合條件的車(chē)輛。”
依照現有方式,短信推送可實(shí)現自動(dòng)提醒功能,但是無(wú)法實(shí)現數據瀏覽(短信字數有限);WAP方式可以實(shí)現數據瀏覽,但是無(wú)法實(shí)現提醒功能,除非WAP瀏覽器定時(shí)刷新頁(yè)面或支持Ajax,就算支持也不能實(shí)現離線(xiàn)數據瀏覽!
問(wèn)題的解決辦法——“在手機上建立分布式數據庫。”目前db4o西班牙團隊領(lǐng)導的db4oME項目正在開(kāi)發(fā)中,該項目能幫助實(shí)現手機分布式數據庫的遐想。我們可以再想得更遠點(diǎn),是不是有了db4oME就能解決上面的問(wèn)題?我認為可以解決,不過(guò)又會(huì )產(chǎn)生更多的問(wèn)題,比如Java能管理手機上多大的存儲空間,是否可以把數據存放在例如TF卡上,如果可以放在TF卡上那么檢索效率如何,數據文件是否安全……
要實(shí)現遐想并解釋上面的問(wèn)題有待于廣大開(kāi)發(fā)者的共同努力,把自己的業(yè)余時(shí)間都投入在類(lèi)似db4oME這樣的技術(shù)上,如果這樣的分布式數據庫能成功實(shí)現,我想這會(huì )給移動(dòng)計算帶來(lái)一場(chǎng)革命,更多依托網(wǎng)絡(luò )、數據庫的手機應用程序將會(huì )誕生,發(fā)揮Java強大的優(yōu)勢!