层间数据传输的过程就是服务的执行者将数据返回给服务的调用者的过程。在非分布式系统中由于有类似Open session in view这样的“怪胎解决方案”的存在,所以层间数据传输的问题并没有充分暴露出来,但是在分布式系统中我们就能清楚地意识到层间数据传输的问题,从而能够更合理的进行设计。为了暴露更多问题,本章讨论的层间数据传输假定的场景是“服务器将执行的数据结果如何传递给远程客户端”,尽管在实际场景中服务的提供者和服务的调用者有可能处于同一虚拟机中(比如Web端与应用服务部署在同一服务器中)。
Data Transfer Object(数据传输对象)
一个数据传输对象 (DTO),用该对象包含远程调用所需要的所有数据。修改远程方法签名,以便将 DTO 作为单个参数接受,并将单个 DTO 参数返回给客户端。在调用方应用程序收到 DTO 并将其作为本地对象存储之后,应用程序可以分别对 DTO 发出一系列单独的过程调用,而不会引发远程调用开销。
优点 减少了远程调用次数。通过在单个远程调用中传输更多的数据,应用程序可以减少远程调用次数。 提高了性能。远程调用可以使应用程序的运行速度大大降低。减少调用次数是提高性能的最佳方法之一。在大多数方案中,传输大量数据的远程调用所用的时间与仅传输少量数据的调用所用的时间几乎相等。 隐藏内部情况。在单个调用中来回传递更多的数据,还可以更有效地将远程应用程序的内部情况隐藏在粗粒度接口的背后。这就是使用 Remote Facade 模式 [Fowler03] 的主要原因。 发现业务对象。在一些情况下,定义 DTO 有助于发现有意义的业务对象。在创建用作
DTO 的自定义类时,您通常会注意到作为一组凝聚性信息而显示给用户或另一个系统的元素分组。通常,这些分组用作描述应用程序所处理的业务域的对象的有用原型。 可测试性。将所有参数封装到可序列化对象中可以提高可测试性。例如,可以从 XML 文件中读取 DTO,并调用远程函数以测试它们。同样,可以轻松地将结果再序列化为 XML 格式,并将 XML 文档与所需结果进行比较,而不必创建冗长的比较脚本。 缺点 可能需要太多的类。如果选择了使用强类型的 DTO,则可能必须为每个远程方法创建一个(如果考虑返回值,则为两个)DTO。即使在粗粒度接口中,这也可能导致大量的类。编写如此数量的类的代码并管理这些类会是很困难的。使用自动代码生成可以在一定程度上缓解此问题。
增加计算量。如果将服务器上的一种数据格式转换为可以跨网络传输的字节流,并在客户端应用程序内转换回对象格式,可以带来相当大的开销。通常,需要将来自多个源的数据聚合到服务器上的单个 DTO 中。要提高通过网络进行远程调用的效率,必须在任一端执行其他计算,才能聚合和串行化信息。 增加编码工作量。可以用一行代码完成将参数传递到方法的操作。使用 DTO 要求实例化新对象,并为每个参数调用 setters 和 getters。编写此代码可能是很乏味的。
10.1 什么是DTO
在分布式系统中,客户端和服务器端交互有两种情形:第一个是客户端从服务器端读取数据;第二个是客户端将本身的数据传递给服务器端。
当有客户端要向服务器端传输大量数据的时候,可以通过一个包含要传输的所有数据的方法调用来完成。这在小数据量的时候缺点并不明显,但是如果要传递包含有大量信息的数据的时候,这将变得难以忍受。下面的方法是任何人看了都会害怕的:
public void save(String id,String number,String name,int type,int height,
int width,BigDecimal weight,BigDecimal price,String description)
这种接口也是非常的脆弱,一旦需要添加或者删除某个属性,方法的签名就要改变。
当客户端要从服务器端取得大量数据的时候,可以使用多个细粒度的对服务器端的调用来获取数据。比如:
ISomeInterface intf = RemoteService.getSomeInterface();
System.out.println("您要查询的商品的资料为:");
System.out.println("编号:"+intf.getNumber(id));
System.out.println("姓名:"+intf.getName(id));
System.out.println("类型:"+intf.getType(id));
System.out.println("高度:"+intf.getHeight(id));
System.out.println("宽度:"+intf.getWidth(id));
System.out.println("价格:"+intf.getPrice(id));
System.out.println("描述信息:"+intf.getDescription(id));
这种方式中每一个get***方法都是一个对服务器的远程调用,都需要对参数和返回值进行序列化和反序列化,而且服务器进行这些调用的时候还需要进行事务、权限、日志的处理,这会造成性能的大幅下降。如果没有使用客户端事务的话还会导致这些调用不在一个事务中从而导致数据错误。
系统需要一种在客户端和服务器端之间高效、安全地进行数据传输的技术。DTO(Data Transfer Object,数据传送对象)是解决这个问题的比较好的方式。DTO是一个普通的Java类,它封装了要传送的批量的数据。当客户端需要读取服务器端的数据的时候,服务器端将数据封装在DTO中,这样客户端就可以在一个网络调用中获得它需要的所有数据。
还是上面的例子,服务器端的服务将创建一个DTO并封装客户端所需要的属性,然后返回给客户端:
ISomeInterface intf = RemoteService.getSomeInterface();
SomeDTOInfo info = intf.getSomeData(id);
System.out.println("您要查询的商品的资料为:");
System.out.println("编号:"+info.getNumber());
System.out.println("姓名:"+info.getName());
System.out.println("类型:"+info.getType());
System.out.println("高度:"+info.getHeight());
System.out.println("宽度:"+info.getWidth());
System.out.println("价格:"+info.getPrice());
System.out.println("描述信息:"+info.getDescription());
使用DTO 的时候,一个主要问题是选择什么样的DTO:这个DTO能够容纳哪些数据,DTO的结构是什么,这个DTO是如何产生的。DTO是服务器端和客户端进行通信的一个协议格式,合理的DTO设计将会使得服务器和客户端的通信更加顺畅。在水平开发模式(即每个开发人员负责系统的不同层,A专门负责Web表现层的开发,B专门负责服务层的开发)中,在项目初期合理的DTO设计会减少各层开发人员之间的纠纷;在垂直开发模式(即每个开发人员负责不同模块的所有层,A 专门负责库存管理模块的开发,B专门负责固定资产模块的开发)中,虽然开发人员可以自由地调整DTO的结构,但是合理的DTO设计仍然会减少返工的可能性。
实现DTO 最简单的方法是将服务端的域对象(比如Hibernate中的PO、EJB中的实体Bean)进行拷贝然后作为DTO传递。采用域对象做DTO比较简单和清晰,因为DTO与域模型一致,所以了解一个结构就够了。这样做也免去了DTO的设计,使得开发工作变得更快。这种做法的缺点是域DTO的粒度太大以至于难以满足客户端的细粒度的要求,客户端可能不需要访问那些域中的所有属性,也可能需要不是简单地被封装在域中的数据,当域DTO不能满足要求的时候就需要更加细粒度的DTO方案。目前主流的DTO解决方案有定制DTO、数据传送哈希表、数据传送行集。
10.2 域DTO
域模型是指从业务模型中抽取出来的对象模型,比如商品、仓库。在J2EE中,最常见的域模型就是可持久化对象,比如Hibernate中的PO、EJB中的实体Bean。
在分布式系统中,域模型完全位于服务器端。根据持久化对象可否直接传递到客户端,域对象可以分为两种类型:一种是服务器端的持久化对象不可以直接传递到客户端,比如EJB中的实体Bean是不能被传递到客户端的;一种是持久化对象可以直接传递到客户端,比如Hibernate中的PO变为detached object以后就可以传递到客户端。
EJB中的实体Bean不能直接传递到客户端,而且实体Bean不是一个简单的JavaBean,所以也不能通过深度克隆(deep clone)创造一个新的可传递Bean的方式产生DTO。针对这种情况,必须编写一个简单的JavaBean来作为DTO。
下面是一个系统用户的实体Bean的代码:
abstract public class SystemUserBean implements EntityBean
{
}
根据需要我们设计了如下的DTO:
public class SystemUserDto implements Serializable
{
}
为了实现DTO的生成,这里还需要一个将实体Bean转换为一个DTO的工具,我们称其为DTOAssembler:
public class SystemUserDtoAssembler
{
}
为一个实体Bean产生DTO是非常麻烦的事情,所以像JBuilder这样的IDE都提供了根据实体Bean直接生成DTO类和DTOAssembler的代码生成器。
相对于重量级的实体Bean来说,使用 Hibernate的开发人员则轻松多了,因为Hibernate中的PO就是一个普通的JavaBean对象,而且PO可以随时脱离Hibernate 被传递到客户端,不用进行复杂的DTO和DTOAssembler的开发。不过缺点也是有的,当一个PO脱离Hibernate以后如果客户端访问其并没有在服务器端加载的属性的时候就会抛出惰性加载的异常,而如果对PO不采用惰性加载的话则会导致Hibernate将此PO直接或者间接关联的对象都取出来的问题,在有的情况下这是灾难性的。在案例系统中是使用DTOGenerator的方式来解决这种问题的。
无论是哪种方式,客户端都不能直接访问服务器端的域模型,但是客户端却希望能和域模型进行协作,因此需要一种机制来允许客户端像操纵域模型一样操作DTO,这样客户端可以对DTO进行读取、更新的操作,就好像对域模型做了同样的操作一样。客户端对DTO进行新增、修改、删除等操作,然后将修改后的DTO传回服务器端由服务器对其进行处理。对于实体Bean 来讲,如果要处理从客户端传递过来的DTO,就必须编写一个DTODisassembler来将DTO解析为实体Bean:
public class SystemUserDtoDisassemble
{