Java对象序列化 系统解耦与序列化、持久化

来源:互联网 发布:宁波数据工场 编辑:程序博客网 时间:2024/04/30 20:56

系统解耦与序列化、持久化
      从开始学习和实践系统设计起就一直围绕着这样一个问题——怎样降低系统的耦合度,使系统更加健壮、更加灵活?无论桌面应用还是网络应用,确实它的系统中无非包含如下几方面的内容——如何获取及存储数据、如何处理事务逻辑、如何用用户交互。在早期的系统内,由于其本身复杂性比较低,同时使用面较窄又基本不涉及移植或升级的问题,就算有也是波及面很窄,所以以上这些内容大多被混杂在一次,形成了现在我们称之为高耦合度的系统。这样的系统存在非常多的众所周知的缺点:除错难、测试难、升级难、维护难、部署难、移植难……随着用户要求的不断提升及应用环境的日渐复杂,系统的软件复杂度也与日俱增,如何解决以上这些难题呢?那么只有一条路可以走,那就是——“系统解耦”。

      很多人都已经熟悉了MVC的基本原理和方法,也知道如何将显示层与业务逻辑层解耦,例如桌面应用中的View/Doc方法、UI控件设计方法、Java中的AWT和Swing方法或者网络应用中的CSS方法、Ajax方法、各种面向对象或模版的Web开发语言方法和工具(像ASP Template、Zend、Komodo等)。以上这些方法很好的解决了显示层和业务逻辑层之间的解耦,甚至可以完全实现从桌面应用向网络应用转移时根本不需要修改任何业务逻辑代码即可实现,但是问题解决了吗?恐怕远远还没有!

      让我们来假设这样一个场景:按通常的系统设计,使用 JDBC/DAO/ADO 操作数据库,业务处理逻辑和数据存取逻辑是混杂在一起的。一般基本都是如下几个步骤:
1、建立数据库连接,获得 Connection 对象。
2、根据用户的输入组装查询 SQL 语句。
3、根据 SQL 语句建立 Statement 对象 或者 PreparedStatement 对象。
4、用 Connection 对象执行 SQL语句,获得结果集 ResultSet 对象。
5、然后一条一条读取结果集 ResultSet 对象中的数据。
6、根据读取到的数据,按特定的业务逻辑进行计算。
7、根据计算得到的结果再组装更新 SQL 语句。
8、再使用 Connection 对象执行更新 SQL 语句,以更新数据库中的数据。
7、最后依次关闭各个 Statement 对象和 Connection 对象。

      由上可看出代码逻辑非常复杂,这还不包括某条语句执行失败的处理逻辑,而一个完整的系统要包含成千上万个这样重复的而又混杂的处理过程,假如要对其中某些业务逻辑或者一些相关联的业务流程做修改,要改动的代码量将不可想象。另一方面,假如要换数据库产品或者运行环境也可能是个不可能完成的任务。而用户的运行环境和要求却千差万别,我们不可能为每一个用户每一种运行环境设计一套一样的系统。究竟是什么让我们从事如此之多的工作而收获甚微呢?其实就是没有将业务逻辑和数据逻辑解耦的原因,在MVC的Model中包含了复杂的业务逻辑和数据逻辑,以及数据存取机制(例如 JDBC/DOA/ADO的连接、SQL生成和Statement创建、还有ResultSet结果集的读取)等。将这些复杂的业务逻辑和数据逻辑分离,以将系统的紧耦合关系转化为松耦合关系(即解耦合),是降低系统耦合度迫切要做的,也是持久化要做的工作。另一方面,关系型数据库中的数据基本都是以一行行的数据进行存取的,而程序运行却是一个个对象进行处理,而目前大部分数据库驱动技术(如ADO.NET、JDBC、ODBC等等)均是以行集的结果集一条条进行处理的。所以为解决这一困难,就出现 ORM 这一个对象和数据之间映射技术。

何谓“持久化”
持久(Persistence),即把数据(如内存中的对象)保存到可永久保存的存储设备中(如磁盘)。持久化的主要应用是将内存中的数据存储在关系型的数据库中,当然也可以存储在磁盘文件中、XML数据文件中等等。

何谓“持久层”
持久层(Persistence Layer),即专注于实现数据持久化应用领域的某个特定系统的一个逻辑层面,将数据使用者和数据实体相关联。

何谓“对象数据映射(ORM)”
ORM-Object/Relational Mapper,即“对象-关系型数据映射组件”。对于O/R,即 Object(对象)和 Relational(关系型数据),表示必须同时使用面向对象和关系型数据进行开发。

举例来说,比如要完成一个购物打折促销的程序,用 ORM 思想将如下实现(引自《深入浅出Hibernate》):
业务逻辑如下:
public Double calcAmount(String customerid, double amount)
{
    // 根据客户ID获得客户记录
    Customer customer = CustomerManager.getCustomer(custmerid);
    // 根据客户等级获得打折规则
    Promotion promotion = PromotionManager.getPromotion(customer.getLevel());
    // 累积客户总消费额,并保存累计结果
    customer.setSumAmount(customer.getSumAmount().add(amount);
    CustomerManager.save(customer);
    // 返回打折后的金额
    return amount.multiply(protomtion.getRatio());
}
      这样代码就非常清晰了,而且与数据存取逻辑完全分离。设计业务逻辑代码的时候完全不需要考虑数据库JDBC的那些千篇一律的操作,而将它交给 CustomerManager 和 PromotionManager 两个类去完成。这就是一个简单的 ORM 设计,实际的 ORM 实现框架比这个要复杂的多。

目前有哪些流行的 ORM 产品
目前众多厂商和开源社区都提供了持久层框架的实现,常见的有
Apache OJB (http://db.apache.org/ojb/)
Cayenne (http://objectstyle.org/cayenne/)
Jaxor (http://jaxor.sourceforge.net)
Hibernate (http://www.hibernate.org)
iBatis (http://www.ibatis.com)
jRelationalFramework (http://ijf.sourceforge.net)
mirage (http://itor.cq2.org/en/oss/mirage/toon)
SMYLE (http://www.drjava.de/smyle)
TopLink (http://otn.oracle.com/products/ias/toplink/index.html)
其中TopLink 是 Oracle 的商业产品,其他均为开源项目。

      其中 Hibernate 的轻量级 ORM 模型逐步确立了在 Java ORM 架构中领导地位,甚至取代复杂而又繁琐的 EJB 模型而成为事实上的 Java ORM 工业标准。

除了ORM 技术,还有以下几种持久化技术:

主动域对象模式
它是在实现中封装了关系数据模型和数据访问细节的一种形式。在 J2EE 架构中,EJB 组件分为会话 EJB 和实体 EJB。会话 EJB 通常实现业务逻辑,而实体 EJB 表示业务实体。实体 EJB 又分为两种:由 EJB 本身管理持久化,即 BMP(Bean-Managed Persistence);有 EJB 容器管理持久化,即 CMP(Container-Managed Persistence)。BM P就是主动域对象模式的一个例子,BMP 表示由实体 EJB 自身管理数据访问细节。
主动域对象本身位于业务逻辑层,因此采用主动域对象模式时,整个应用仍然是三层应用结构,并没有从业务逻辑层分离出独立的持久化层。

JDO 模式
Java Data Objects(JDO)是 SUN 公司制定的描述对象持久化语义的标准API。严格的说,JDO 并不是对象-关系映射接口,因为它支持把对象持久化到任意一种存储系统中,包括 关系数据库、面向对象的数据库、基于 XML 的数据库,以及其他专有存储系统。由于关系数据库是目前最流行的存储系统,许多 JDO 的实现都包含了对象-关系映射服务。

CMP 模式
在 J2EE 架构中,CMP(Container-Managed Persistence)表示由 EJB 容器来管理实体 EJB 的持久化,EJB 容器封装了对象-关系的映射及数据访问细节。CMP 和 ORM 的相似之处在于,两者都提供对象-关系映射服务,都把对象持久化的任务从业务逻辑中分离出来。区别在于 CMP 负责持久化实体 EJB 组件,而 ORM 负责持久化 POJO,它是普通的基于 Java Bean 形式的实体域对象。POJO(Plain Old Java Object),意为又普通又古老的 Java 对象的意思,它和基于 CMP 的实体 EJB 相比,即简单又具有很高的可移植性,因此联合使用 ORM 映射工具和 POJO,已经成为一种越来越受欢迎的且用来取代 CMP 的持久化方案。POJO 的缺点就是无法做远程调用,不支持分布式计算。

      看了以上的分析,大致知道了怎样实现业务逻辑与数据逻辑的分离,或者至少有了一条思路,但更加关键问题是为什么自己以前一直都没有考虑过这方面的问题,以至于作了一大堆现在看起来垃圾透顶、无可救药的系统呢?思前想后终于明白了,这是人的思维惯性使然呀!以前的系统设计中总是围绕以数据库为核心,任何系统需求来了就先设计数据库的Schema,而不是去考虑如何用UML分析场景和用户需求并给出合理的高层设计方案。现在再看,其实数据库只能作为一种具体实现技术而不是一种系统设计的核心,就其存储本质来讲,数据库与文件系统有什么本质上的差别吗?没有!衡量以前的某些系统没有达到完全OO的原因,除了自己的功力不够之外,就是太多了迁就数据库设计,把它当作了系统设计的核心,正所谓一山不容二虎:如果你重视数据库,你的系统就无法完全OO,只有忽视数据库为一种具体实现技术,系统才有可能完全迈向OO,至于数据库性能调优等特定功能都可交由ORM工具实现。

其实换个角度来思考以下三个问题:

1、当业务逻辑层功能不再依赖数据逻辑层,即实现解耦时,是否意味着数据库已经不再是设计的重点了呢?

2、相对于内存缓存状态而言,无论数据库还是文件系统都只是当内存断电情况下能永久保存状态数据的一种保证,但是如果应用服务器是7X24小时集群运行几乎永不当机并且在64位运算能力的支持下提供几乎无限大的内存空间,在忽略金字塔式存储体系带来的成本优势情况下,是否一定要有数据库的存在呢?

3、面对扑面而来的互联网计算时代,所有的数据是分布式地存储在所有网络节点上的,在XML逐渐统一互联网数据格式的今天,是否有必要将所有数据以一种近乎备份的方式存储在数据库系统中再加以应用呢?除了建立索引提高计算速度之外,数据库还能为我们带来更多吗?更何况在数据格式完全统一且实现自描述之后,这种索引机制似乎也不再一定需要借助于数据库索引了吧~

      回答了以上三个问题之后,必然得到这样一个结论——数据库已经变为与操作系统中的文件系统同样的层面,以数据库为中心的系统设计时代真的该结束了。当然这里绝不是在散播数据库无用论,毕竟在现有条件下我们还是要依赖于数据库系统提供给我们的种种方便,这里只是提出一种远景,况且我也相信面对这样一种趋势数据库系统本身也会发生一些变化来适应它,从而让自己更好的发展下去。

      最后说说持久化(Persistence)与序列化(Serialization),我以前总是将他们混淆,其实两者的差距确实并不大,它们相似的地方都是企图将对象和其他东西进行转化,不同之处在于持久化是希望能把它持久保存起来并在需要的时候再得到而序列化关心的是如何把对象变为字节流,实质上从实用角度上来讲,两者有很大程度的相互覆盖,序列化机制是将类的值转化为一个一般的(即连续的)字节流,然后将该流写到磁盘文件或任何其他流化目标上的一个过程,这本身也可以称之为持久化。当然所谓反序列化,顾名思义就是将序列化过程颠倒过来,由流目标得到对象的过程了。

序列化的过程就是对象写入字节流和从字节流中读取对象。将对象状态转换成字节流之后,

可以用java.io包中的各种字节流类将其保存到文件中,管道到另一线程中或通过网络连接

将对象数据发送到另一主机。对象序列化功能非常简单、强大,在RMI、Socket、JMS、EJB

都有应用。对象序列化问题在网络编程中并不是最激动人心的课题,但却相当重要,具有

许多实用意义。

一:对象序列化可以实现分布式对象。主要应用例如:RMI要利用对象序列化运行远程主机

上的服务,就像在本地机上运行对象时一样。

二:java对象序列化不仅保留一个对象的数据,而且递归保存对象引用的每个对象的数据。

可以将整个对象层次写入字节流中,可以保存在文件中或在网络连接上传递。利用对象序

列化可以进行对象的"深复制",即复制对象本身及引用的对象本身。序列化一个对象可能

得到整个对象序列。

从上面的叙述中,我们知道了对象序列化是java编程中的必备武器,那么让我们从基础开始

,好好学习一下它的机制和用法。


java序列化比较简单,通常不需要编写保存和恢复对象状态的定制代码。实现java.io.Seri
alizable接口的类对象可以转换成字节流或从字节流恢复,不需要在类中增加任何代码。只
有极少数情况下才需要定制代码保存或恢复对象状态。这里要注意:不是每个类都可序列化,
有些类是不能序列化的,例如涉及线程的类与特定JVM有非常复杂的关系。

序列化机制:

序列化分为两大部分:序列化和反序列化。序列化是这个过程的第一部分,将数据分解成字
节流,以便存储在文件中或在网络上传输。反序列化就是打开字节流并重构对象。对象序列
化不仅要将基本数据类型转换成字节表示,有时还要恢复数据。恢复数据要求有恢复数据的
对象实例。ObjectOutputStream中的序列化过程与字节流连接,包括对象类型和版本信息。
反序列化时,JVM用头信息生成对象实例,然后将对象字节流中的数据复制到对象数据成员中。
下面我们分两大部分来阐述:


处理对象流:
(序列化过程和反序列化过程)


java.io包有两个序列化对象的类。ObjectOutputStream负责将对象写入字节流,ObjectInp
utStream从字节流重构对象。
    我们先了解ObjectOutputStream类吧。ObjectOutputStream类扩展DataOutput接口。
writeObject()方法是最重要的方法,用于对象序列化。如果对象包含其他对象的引用,则
writeObject()方法递归序列化这些对象。每个ObjectOutputStream维护序列化的对象引用表,
防止发送同一对象的多个拷贝。(这点很重要)由于writeObject()可以序列化整组交叉引用的
对象,因此同一ObjectOutputStream实例可能不小心被请求序列化同一对象。这时,进行反引用
序列化,而不是再次写入对象字节流。
下面,让我们从例子中来了解ObjectOutputStream这个类吧。

// 序列化 today's date 到一个文件中.
    FileOutputStream f = new FileOutputStream("tmp");
    ObjectOutputStream s = new ObjectOutputStream(f);
    s.writeObject("Today");
    s.writeObject(new Date());
    s.flush();
现在,让我们来了解ObjectInputStream这个类。它与ObjectOutputStream相似。它扩展Dat
aInput接口。ObjectInputStream中的方法镜像DataInputStream中读取Java基本数据类型的
公开方法。readObject()方法从字节流中反序列化对象。每次调用readObject()方法都返回
流中下一个Object。对象字节流并不传输类的字节码,而是包括类名及其签名。readObject()
收到对象时,JVM装入头中指定的类。如果找不到这个类,则readObject()抛出
ClassNotFoundException,如果需要传输对象数据和字节码,则可以用RMI框架。
ObjectInputStream的其余方法用于定制反序列化过程。
例子如下: //从文件中反序列化 string 对象和 date 对象
    FileInputStream in = new FileInputStream("tmp");
    ObjectInputStream s = new ObjectInputStream(in);
    String today = (String)s.readObject();
    Date date = (Date)s.readObject();
定制序列化过程:

序列化通常可以自动完成,但有时可能要对这个过程进行控制。java可以将类声明为serial
izable,但仍可手工控制声明为static或transient的数据成员。
例子:一个非常简单的序列化类。 public class simpleSerializableClass implements Serializable{
    String sToday="Today:";
    transient Date dtToday=new Date();
}

...

序列化时,类的所有数据成员应可序列化除了声明为transient或static的成员。将变量声明为transient告诉JVM我们会负责将变元序列化。将数据成员声明为transient后,序列化过程就无法将其加进对象字节流中,没有从transient数据成员发送的数据。后面数据反序列化时,要重建数据成员(因为它是类定义的一部分),但不包含任何数据,因为这个数据成员不向流中写入任何数据。记住,对象流不序列化static或transient。我们的类要用writeObject()与readObject()方法以处理这些数据成员。使用writeObject()与readObject()方法时,还要注意按写入的顺序读取这些数据成员。


关于如何使用定制序列化的部分代码如下:

//重写writeObject()方法以便处理transient的成员。
public void writeObject(ObjectOutputStream outputStream) throws IOException...{
    outputStream.defaultWriteObject();//使定制的writeObject()方法可以
                        利用自动序列化中内置的逻辑。
    outputStream.writeObject(oSocket.getInetAddress());
    outputStream.writeInt(oSocket.getPort());
}
//重写readObject()方法以便接收transient的成员。
private void readObject(ObjectInputStream inputStream) throws
IOException,ClassNotFoundException...{
    inputStream.defaultReadObject();//defaultReadObject()补充自动序列化
    InetAddress oAddress=(InetAddress)inputStream.readObject();
    int iPort =inputStream.readInt();
    oSocket = new Socket(oAddress,iPort);
    iID=getID();
    dtToday =new Date();
}

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/achun2050/archive/2007/05/16/1611005.aspx

原创粉丝点击