`
huiqiCHEN1989
  • 浏览: 1346 次
  • 性别: Icon_minigender_1
  • 来自: 广西
社区版块
存档分类
最新评论

J2ME RMS 很基础很全面 [转]

阅读更多

作者:yefeng17 文章来源:http://www.j2medev.com/Article/ShowArticle.asp?ArticleID=274

5.1
初识RMS(Record Management System)
记得曾经有人说,数据库程序员是世界上最不愁找不到工作的职业了。虽然此话无从考究J,不过也从一个方面说明了不论开发什么类型的应用,数据库几乎是一个永恒的话题!在java的体系结构里,我们现在已经有了JDBC这个技术,还有许多就此衍生的概念,许多耳熟能详的术语,EJB, JDO等等,只是,这些都是针对桌面平台或者企业用户的,对于处理能力和存储空间都十分有限的无线设备而言,必须有一种特殊的机制与之适应,MIDP2.0规范里不支持全面的树型文件系统,但为我们提供了这样一种数据持久化机制——记录管理系统(Record Management System RMS)。


记录管理系统就是一个小型的数据库管理系统,它以一种简单的,类似表格的形式组织信息,并存储起来形成持久化存储,以供应用程序在重新启动后继续使用。


RMS提供了Records(记录)和Records Stores(记录仓储)两个概念。


记录仓储(Records Stores)类似于一般关系数据库系统中的表格(TABLE),它代表了一组记录的集合。在相同MIDlet Suite中,每个仓储都拥有自己独一无二的名字,大小不能超过32个Unicode字符,同一个Suite下的MIDlet都可以共享这些记录仓储。


记录是记录仓储的组成元素。记录仓储中含有很多条记录,就如同记录表格是由一行行组成的一样。每条记录代表了一条数据信息。一条记录(Record)由一个整型的RecordID与一个代表数据的byte[]数组两个子元素组成。RecordID是每条记录的唯一标志符,利用这个标志符可以用于从记录仓储中找到对应的一条记录。请注意,由于产生记录号RecordID使用的是一种简单的单增算法。当一条数据记录被分配的时候,它的记录号也就唯一分配了。并且该条记录被删除后,RecordID也不会被使用。所以,仓储中相邻的记录并不一定会有连续的RecordID。


MIDP Suite所使用的RMS空间图可由下图所示:

每一个MIDlet Suite都会有属于自己的一个用于RMS的私有空间。可以通过jad描述文件事先规定Midlet Suite运行所必需的RMS空间大小,在手机内部存储空间中预存的一个空间,供由jad指定的jar包文件使用。在MIDP 2.0 以后,只要MIDlet开放了属于自己RMS空间的相应RecordStore的使用权限,那么这个RecordStore可以被Suite外部的其他MIDlet访问。


所有与RMS相关的API都集中在javax.microedition.rms包下,包括了一个主类,四个接口,以及五个可能的被抛出异常。既然RMS在结构上就分为记录与记录仓储两个部分,那么对他们的操作当然也是有所区别的,下一小节“RecordStore的管理”将首先阐述记录仓储的自身操作。
5.2
RecordStore的管理5.2.1
RecordStore的打开当你查阅MIDP API文档的时候,你将略为惊讶的发现,RecordStore并不能通过new来打开或创建一个实例。事实上, RecordStore提供了一组静态方法openRecordStore()来取得实例。这里,你有三个选择:


openRecordStore (String recordStoreName, boolean createIfNecessary)
openRecordStore (String recordStoreName, boolean createIfNecessary,
int authmode), boolean writable))
openRecordStore (String recordStoreName, String vendorName, String suiteName)


显然,最复杂(当然,也是最灵活的),是第二种开启方法。它的第一个参数是记录仓储的名称,第二个参数表明了当我们请求的仓储不存在时,是否新建一个Record Store;第三个参数表明了此仓储的读取权限,最后一个参数则决定了写入权限。


当我们使用第一个开启方法时,则表示我们选择读取ㄏ拗幌抻诒镜兀⑶揖芫渌?/span>MIDlet写数据到这个记录仓储上。即相当于使用第二种开启方法并分别为第三第四个参数传入了RecordStore.AUTHMODE_PRIVATE和false。


最后,MIDP API中还提供了一个专门用来读取其他MIDlet Suite记录仓储的开启方法;它的3个传入参数分别为记录仓储名,发布商名以及MIDlet Suite套件名。请注意,如果该记录仓储的读取权限为AUTHMODE_PRIVATE的话,此方法将返回安全错误。


下面,我们给出一个使用第一种方法的示例:

private RecordStore rs = null;
try {
           //打开一个RMS,如果打开失败,则创建一个
           rs = RecordStore.openRecordStore(“testRMS”, true);
       } catch (RecordStoreNotFoundException e) {
           e.printStackTrace();
       } catch (RecordStoreFullException e) {
           e.printStackTrace();
       } catch (RecordStoreException e) {
           e.printStackTrace();
       }






5.2.2
RecordStore的关闭当你不在使用一个打开的RecordStore时,记得要关闭它以节约资源。这时,你就需要RecordStore的关闭方法closeRecordStore()。


在记录仓储关闭期间,加载在当前记录仓储上的所有监听器将被消除,记录仓储本身也不能再被调用或遍历,任何试图对RMS采取的操作都会抛出RecordStoreNotOpenException错误。


关闭方法是如此简单,以至于我们有可能忽略这样一鱿附冢杭锹疾执⒃诘饔?/span>closeRecordStore()不会立即被关闭,除非你确信你关闭方法的调用次数与开启方法openRecordStore()的调用次数一样多。也就是说,MIDlet套件需要在RMS的开启与关闭之间保持平衡。


下面给出了在关闭仓储的一个示例:
try {
       rs = RecordStore.openRecordStore(“testRMS”, true);
       //DO YOUR WORK HERE
           rs.closeRecordStore();
       } catch (RecordStoreNotOpenException e) {
           e.printStackTrace();
       } catch (RecordStoreException e) {
           e.printStackTrace();
       }


5.2.3
RecordStore的删除当我们不再需要一个记录仓储的时候,我们就需要deleteRecordStore()来执行删除。一个MIDlet套件只能够删除它自己的记录仓储。在删除之前,我们需要确保当前的仓储是处于关闭状态,如果改仓储仍旧处于开启状态(被自身的MIDlet或者其他套件调用),那么删除记录将导致RecordStoreException异常抛出;如果要删除的仓储记录本身不存在,那么这将会引起RecordStoreNotFoundException异常抛出。


//假定rs是已经存在的记录仓储,并已经打开
try {
           rs.closeRecordStore();
           RecordStore.deleteRecordStore(“testRMS”);


       } catch (RecordStoreNotOpenException e) {
           e.printStackTrace();
       } catch (RecordStoreNotFoundException e) {
           e.printStackTrace();




       } catch (RecordStoreFullException e) {
           e.printStackTrace();
       } catch (RecordStoreException e) {
           e.printStackTrace();
       }


5.2.4
其他相关操作RecordStore中除去以上介绍的3种最常用的方法,还包括了一些很有用的操作,可以取得对记录仓储的信息,包括:
l
getLastModified():返回记录仓储最后更新时间。
l
getName():返回一个已经打开了的记录仓储名称。
l
getNumRecords():返回当前仓储中记录总数。
l
getSizeAvailable():返回当前仓储中可用的字节数。
l
getVersion():返回记录仓储版本号。
l
listRecordStores():获取该MIDlet套件中所有的记录仓储列表。


以上所说的都是针对记录仓储的操作,就如同一个关系型数据库系统,RMS也有包括插入,删除在内的等单条记录基本操作,这将在下一节中介绍给大家。


5.3
RecordStore的基本操作5.3.1
增加记录我们可以通过方法addRecord(byte[] data, int offset, int numBytes)添加byte数组类型的数据。在addRecord中,我们需要提供3个有效参数:byte[]数组,传入byte[]数组起始位置,传入数据的长度)。


当数据添加成功后,addRecord将返回记录ID号(RecordID),RecordID在一个RecordStore中中扮演着主键的角色,它由一个简单增长算法产生。例如第一条添加的记录ID是0,第二个是1,以此类推……


如果试图将数据添加到一个未经打开的记录仓储中,将产生RecordStoreNotOpenException异常;任何添加错误都将最终抛出异常RecordStoreException,所以,将它作为最后一个catch块将是一个很好的选择。


添加操作是一个阻塞操作,直到数据被持久的写到了存储器上,对addRecord方法的调用才会返回。同时这个操作也是个原子操作,这意味着多个线程中同时调用addRecord不会产生数据写丢失。但这并不保证读取和写入同时发生时能够读取的自动同步。复杂应用中对应的同步机制是必须的。
5.3.2
修改与删除记录通过方法deleteRecord(int recordId),传入目标记录的ID以后,可以从记录仓储中删除记录。需要注意的是,正如前面提过的记录ID号是不能够被复用的。
如果试图从一个尚未开启的记录仓储中删除记录,将会抛出RecordStoreNotOpenException异常;如果传入的记录ID是无效的,将得到InvalidRecordIDException异常;而异常RecordStoreException则可以用来捕获一般性的仓储错误发生。
通过方法setRecord(int recordId, byte[] newData, int offset, int numBytes)可以修改一个指定ID的记录值。
修改记录的传入参数包括记录号,其余的与addRecord相同。当仓储没有打开或者传入ID无效时,你会得到与deleteRecord一致的抛出错误. 而异常RecordStoreException同样可以用来捕获一般性的仓储异常。


5.3.3
自定义数据类型与字节数组的转换技巧我们在上面已经提到,针对RecordStore的操作只提供对针对byte数组的服务.而在日常处理中,大部分时候我们遇到的都将是非byte类型。当然,我们可以撰写一些工具方法来完成基本类型(int, char)与byte的相互转换,但我们又将如何解决另一种更常见的问题:自定义数据类型与byte的转换?在这里,我们向大家介绍一种已经被广泛采用的方法。


要将自定义数据与byte数组相互转换,需要ByteArrayOutputStream,DataOutputStream,ByteArrayInputStream,DataInputStream 4个类的协助,在MIDP的帮助API有对于他们的详细描述,你可以在http://java.sun.com 下载或在线查阅。下面简单的介绍一下它们的使用。


要写入数据首先要建立一个ByteArrayOutputStream的实例baos,然后将它作为参数传入DataOutputStream的构造函数来产生一个实例dos。dos由一组方便的I/O方法writeXXX方便我们将不同的数据写入流。例如writeInt用于写入int型、writeChar用于写入字符型、writeUTF用于写入一个String等。更多请参考API手册。当写入操作完成后,可以利用baos的toByteArray方法的得到一个byte[]数组,这个数组含有我们刚刚写入的数据,将它传给addRecord就可以增加一条记录了。最后记住关闭打开的流。


要读入就要利用剩余的两个类ByteArrayInputStream、DataInputStream。首先利用getRecord(int)得到刚刚写入的byte数组。利用得到的byte数组构造一个ByteArrayInputStream的实例bais,然后用DataInputStream包装它,得到一个实例dis。DataInputStream有一组的方便的I/O方法用于读入DataOutputStream对应方法写入的数据。应该注意的是读入顺序和写入顺序应保持一至。同样的不再使用流时,应关闭流以节约资源。


以下是一段代码示范,首先写入一组自定义数据,然后在读出:
ByteArrayOutputStream baos=new ByteArrayOutputStream();
DataOutputStream dos=new DataOutputStream(baos);
dos.writeBoolean(false);
dos.writeInt(15);
dos.writeUTF("abcde");
byte [] data=baos.toByteArray();//取得byte数组
dos.close();
baos.close();
ByteArrayInputStream bais=new ByteArrayInputStream(data);
DataInputStream dis=new DataInputStream(bais);
boolean flag=dis.readBoolean();
int intValue=dis.readInt();
String strValue=dis.readUTF();
dis.close();
bais.close();


5.3.4
利用RMS实现对象序列化有了上一节的基础,现在我们可以很容易的利用RMS来实现对象的序列化。下面,我以一个单词记录本为示例,为大家展示如何序列化对象。


单词记录本的基本类是一个Word,包括英文单词enWord,解释cnWord,上次读取时间dateTime以及备注信息detail:
public class Word {
   private String  enWord;
   private String  cnWord;
   private long   dateTime;
   private String  detail;
}


我们将数据转换作为Word类的内置方法,serialize用于序列化对象数据,返回byte数组类型;deserialize完成的则是相反的工作。对象中成员变量的读取方法writeXXX要与变量的类型相吻合,并且完成操作以后,要将流及时关闭!


/**生成序列化的byte数组数据
*/
   public byte[] serialize() throws IOException{


       //Creates a new data output stream to write data
       //to the specified underlying output stream
       ByteArrayOutputStream baos = new ByteArrayOutputStream();
       DataOutputStream dos = new DataOutputStream(baos);


       dos.writeUTF(this.enWord);
       dos.writeUTF(this.cnWord);
       dos.writeLong(this.dateTime);
       dos.writeUTF(this.detail);


       baos.close();
       dos.close();
       return baos.toByteArray();
   }


   /**将传入的byte类型数据反序列化为已知数据结构
   */
   public static Word deserialize(byte[] data) throws IOException{
       ByteArrayInputStream bais = new ByteArrayInputStream(data);
       DataInputStream dis = new DataInputStream(bais);


       Word word = new Word();
       word.enWord = dis.readUTF();
       word.cnWord = dis.readUTF();
       word.dateTime = dis.readLong();
       word.detail = dis.readUTF();


       bais.close();
       dis.close();
       return word;
   }


5.4
RecordStore的进阶操作记录仓储类似于一个简单的数据库,不仅仅体现在最基本添加删除操作上,RecordStore同样为开发者提供一定程度的进阶功能,这主要体现在4个接口的使用:RecordComparator,RecordEnumeration,RecordFilter与RecordListener。


考虑到RecordComparator、RecordFilter都是作用在RecordEnumeration上的,我们先来介绍这个接口。
5.4.1
RecordEnumeration遍历接口当我们需要走访一个记录仓储中所有记录时,通常的想法是使用一个for循环,利用RecordID来实现遍历。但是,很遗憾,在RMS中,这不是一个好方法。首先,当我们往往无从了解之前存入数据的RecordID;另外,也是最重要的一点,即便RecordID还存在,记录本身也可能已经被删除了,即我们不能保证RecordID对应记录的有效性。


这样的话,我们就需要一些更为有效的方法来走访记录仓储。MIDP规范中提供了一种安全,可靠的走访方式——RecordEnumeration接口。
RecordEnumeration内部没有存放任何记录仓储数据的副本,因此在使用RecordEnumeration读取数据的时候,实际上仍旧是抓取RecordStore之中的数据。RecordEnumeration如同是一个可用RecordID的集合,
他甚至可以按我们指定的方式排列记录。


下面介绍和RecordEnumeration相关的几个主要方法:


l
enumerateRecords():
通过对RecordStore实例对象调用enumerateRecords方法来取得一个RecordEnumeration的实例。


在enumerateRecords方法中我们可以传入3个参数:filter,comparator与keepUpdated。前两个参数分别是过滤器和排序策略,这个后面会讲到。当传入的filter不为空时,它将用于决定记录仓储中的哪些记录将被使用;当comparator不为空时,RecordStore将按照我们指定的排列顺序返回。第三个参数决定了当遍历器建立起来以后,是否对记录仓储新的改变做出回应。如果传入true,那么将有一个RecordListener被加入到RecordStore中,使得记录仓储的内容与RecordEnumeration随时保持同步。如果传入false,则可以使得当前遍历更有效率,但所取得的RecordID集合仅仅是调用此方法这个时刻的RecordStore快照。此后对RecordStore所有更改都不会反应在这个集合上。请读者根据要求在访问数据完整性和访问速度之间进行取舍。


l
numRecords():
numRecords返回了在当前遍历集合中,可用记录数目。这里所指的可用,不仅仅是说RecordID对应的记录存在;当filter存在时,也需要符合过滤条件。


l
hasNextElement():
hasNextElement用于判断在RecordEnumeration当前指向的下一个位置,还有没有剩余记录了。


l
hasPreviousElement():
hasPreviousElement用于判断在RecordEnumeration当前指向的前一个位置,还有没有剩余记录了。

l
nextRecord():
nextRecord返回了遍历器下一位置的记录拷贝,由于返回的是拷贝,所以任何对返回记录的修改都不会影响到记录仓储的实际内容。


l
nextRecordId():
nextRecordId返回当前遍历器下一位置记录的RecordID,当下一位置没有可用的记录时,继续调用nextRecordId将抛出异常InvalidRecordIDException。


依旧以我们的单词本为例,添加一个方法ViewAll(),用来返回当前记录仓储中的所有单词。演示了如何利用RecordEnumeration来遍历记录。在示例中,使用到了RecordComparator接口的一个实例WordComparator,后面会讲到。

public Word[] viewAll() throws IOException {
       Word[] words = new Word[0];
       RecordEnumeration re = null;
       //rs是之前创建的RecordStore类型实例变量
       if (rs == null)
           return words;
       try {
           re = rs.enumerateRecords(null, new WordComparator(), false);//无过滤器、但有一个排序策略
           words = new Word[re.numRecords()];
           int wordRecords = 0;
           while (re.hasNextElement()) {
               byte[] tmp = re.nextRecord();
               words[wordRecords] = Word.deserialize(tmp);


           wordRecords++;
           }
       } catch (RecordStoreNotOpenException e1) {
           e1.printStackTrace();
       } catch (InvalidRecordIDException e1) {
           e1.printStackTrace();
       } catch (RecordStoreException e1) {
           e1.printStackTrace();
       } finally {
           if (re != null)
               re.destroy();
       }
       return words;
   }


5.4.2
RecordFilter 过滤接口过滤接口是用来过滤不满足条件的记录的。使用RecordFilter接口必须实现match(byte[] candidate)方法,当传入byte数据符合筛选条件时,返回true。


下面的示例建立一个静态类WordFilter,它实现RecordFilter接口。
   public class WordFilter implements RecordFilter{
       private String  enWord;
       private int     type;
       public WordFilter(String enword, int type){
           //传入要比较的项,type指向一个自定义的内部事件标记
          //表现为整形
           this.enWord = enword;
           this.type   = type;
       }


       public boolean matches(byte[] word) {
           //matches方法中传入的参数是RMS中的各个候选值(元素)
           try {
               if(type == EventID.SEARCH_EQUAL){
                   return Word.matchEN(word, enWord);
               }else{
                   return Word.matchEN_StartWith(word, enWord);
               }
           } catch (IOException e) {
               e.printStackTrace();
               return false;
           }
       }
   }


以上示例中的EventID.SEARCH_EQUAL为一个定义好的整型数据;同时,这里涉及到了Word类的两个对应方法:
public static boolean matchEN_StartWith(byte[] data, String enword) throws IOException{
       ByteArrayInputStream bais = new ByteArrayInputStream(data);
       DataInputStream dis = new DataInputStream(bais);
       try{
           return (dis.readUTF().startsWith(enword));
           }catch(IOException e){
           e.printStackTrace();
           return false;


       }
   }



public static boolean matchCN(byte[] data, String cnword) throws IOException{
       ByteArrayInputStream bais = new ByteArrayInputStream(data);
       DataInputStream dis = new DataInputStream(bais);
       try{
           dis.readUTF();
           return (dis.readUTF().equals(cnword));
           }catch(IOException e){
           e.printStackTrace();
           return false;
       }
   }


5.4.3
RecordComparator比较接口比较器定义了一个比较接口,用于比较两条记录是否匹配,或者符合一定的逻辑关系。使用比较器必须实现方法, byte[])]compare(byte[] rec1, byte[] rec2),当rec1在次序上领先于rec2时,返回RecordComparator.PRECEDES;反之则返回RecordComparator.FOLLOWS;如果两个传入参数相等,RecordComparator将返回RecordComparator.EQUIVALENT。


如同过滤器一样,我们设计一个静态类——WordComparator,用以实现RecordComparator接口。
private static class WordComparator implements RecordComparator{
       public int compare(byte[] word_1, byte[] word_2) {
           try {
               Word word1 = Word.deserialize(word_1);
               Word word2 = Word.deserialize(word_2);
               long dateTime1 = word1.getDateTime();
               long dateTime2 = word2.getDateTime();


               if(dateTime1 < dateTime2){
                   return RecordComparator.FOLLOWS;
               }
               if(dateTime1 > dateTime2){
                   return RecordComparator.PRECEDES;
               }
               return RecordComparator.EQUIVALENT;
           } catch (IOException e) {
               e.printStackTrace();
           }
           return 0;
       }
   }


正如你看到的为了使程序更加的优雅,我们都是利用序列化/反序列化技术取得完整的对象后,通过对对象自身方法的调用来实现,过滤或者排序的核心逻辑。这样做占用了大量的处理器时间。所以在使用这项技术时,请注意优化你的算法并确保使用它们是必要的。


5.4.4
RecordListener监听器接口最后一起来关注一下RecordListener。RecordListener是用于接受监听记录仓储中记录添加,更改或删除记录等事件的接口。它是作用在RecordStore上的。利用RecordStore的addRecordListener方法来注册一个监听器。使用监听器必须实现3个方法:recordAdded,recordChanged与recordDeleted,他们都需要传入两个参数:记录仓储名称recordStroe与记录号recordId。


l
recordAdded:当一条新的记录被添加到仓储空间的时候,该方法被触发。
l
recordChanged:当一条记录被修改时使用。
l
recordDeleted:当一条记录从记录仓储中删除时调用。


需要注意的是,RecordListener是在对记录仓储的操作动作完成以后被调用的!特别在recordDeleted方法中,由于传入的记录已经删除,所在如果再使用getRecord()试图取得刚刚被删除记录的话,将会抛出InvalidRecordIDException异常。

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics