J2ME中文教程 5 MIDP的持久化解决方案

来源:互联网 发布:菜鸟也能数据分析 编辑:程序博客网 时间:2024/04/30 10:05
第5章            MIDP的持久化解决方案—RMS

5.1    初识RMSRecord Management System

5.2    RecordStore的管理

5.2.1     RecordStore的打开

5.2.2     RecordStore的关闭

5.2.3     RecordStore的删除

5.2.4     其他相关操作

5.3    RecordStore的基本操作

5.3.1     增加记录

5.3.2     修改与删除记录

5.3.3     自定义数据类型与字节数组的转换技巧

5.3.4     利用RMS实现对象序列化

5.4    RecordStore的进阶操作

5.4.1     RecordEnumeration遍历接口

5.4.2     RecordFilter 过滤接口

5.4.3     RecordComparator比较接口

5.4.4     RecordListener监听器接口

 


 

5.1    初识RMSRecord Management System

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

 

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

 

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

 

记录仓储(Records Stores)类似于一般关系数据库系统中的表格(TABLE),它代表了一组记录的集合。在相同MIDlet Suite中,每个仓储都拥有自己独一无二的名字,大小不能超过32Unicode字符,同一个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;第三个参数表明了此仓储的读取权限,最后一个参数则决定了写入权限。

 

当我们使用第一个开启方法时,则表示我们选择读取权限只限于本地,并且拒绝其他MIDlet写数据到这个记录仓储上。即相当于使用第二种开启方法并分别为第三第四个参数传入了RecordStore.AUTHMODE_PRIVATEfalse

 

最后,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错误。

 

关闭方法是如此简单,以至于我们有可能忽略这样一个细节:记录仓储在调用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中中扮演着主键的角色,它由一个简单增长算法产生。例如第一条添加的记录ID0,第二个是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数组相互转换,需要ByteArrayOutputStreamDataOutputStreamByteArrayInputStreamDataInputStream 4个类的协助,在MIDP的帮助API有对于他们的详细描述,你可以在http://java.sun.com 下载或在线查阅。下面简单的介绍一下它们的使用。

 

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

 

要读入就要利用剩余的两个类ByteArrayInputStreamDataInputStream。首先利用getRecord(int)得到刚刚写入的byte数组。利用得到的byte数组构造一个ByteArrayInputStream的实例bais,然后用DataInputStream包装它,得到一个实例disDataInputStream有一组的方便的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个接口的使用:RecordComparatorRecordEnumerationRecordFilterRecordListener

 

考虑到RecordComparatorRecordFilter都是作用在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个参数:filtercomparatorkeepUpdated。前两个参数分别是过滤器和排序策略,这个后面会讲到。当传入的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比较接口

比较器定义了一个比较接口,用于比较两条记录是否匹配,或者符合一定的逻辑关系。使用比较器必须实现方法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监听器接口

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

 

l         recordAdded:当一条新的记录被添加到仓储空间的时候,该方法被触发。

l         recordChanged:当一条记录被修改时使用。

l         recordDeleted:当一条记录从记录仓储中删除时调用。

 

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

原创粉丝点击