在分布式系统中,客户端和服务器端交互有两种情形:第一个是客户端从服务器端读取数据;第二个是客户端将本身的数据传递给服务器端。
当有客户端要向服务器端传输大量数据的时候,可以通过一个包含要传输的所有数据的方法调用来完成。这在小数据量的时候缺点并不明显,但是如果要传递包含有大量信息的数据的时候,这将变得难以忍受。下面的方法是任何人看了都会害怕的:
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、数据传送哈希表、数据传送行集。
域模型是指从业务模型中抽取出来的对象模型,比如商品、仓库。在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
{
EntityContext entityContext;
public java.lang.String ejbCreate(java.lang.String userId)
throws CreateException
{
setUserId(userId);
return null;
}
public void ejbPostCreate(java.lang.String userId)
throws CreateException
{
}
public void ejbRemove() throws RemoveException
{
}
public abstract void setUserId(java.lang.String userId);
public abstract void setName(java.lang.String name);
public abstract void setPassword(java.lang.String password);
public abstract void setRole(java.lang.Integer role);
public abstract java.lang.String getUserId();
public abstract java.lang.String getName();
public abstract java.lang.String getPassword();
public abstract java.lang.Integer getRole();
public void ejbLoad()
{
}
public void ejbStore()
{
}
public void ejbActivate()
{
}
public void ejbPassivate()
{
}
public void unsetEntityContext()
{
this.entityContext = null;
}
public void setEntityContext(EntityContext entityContext)
{
this.entityContext = entityContext;
}
}
根据需要我们设计了如下的DTO:
public class SystemUserDto implements Serializable
{
private String userId;
private String name;
private String password;
private Integer role;
public void setUserId(String userId)
{
this.userId = userId;
}
public String getUserId()
{
return userId;
}
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return name;
}
public void setPassword(String password)
{
this.password = password;
}
public String getPassword()
{
return password;
}
public void setRole(Integer role)
{
this.role = role;
}
public Integer getRole()
{
return role;
}
}
为了实现DTO的生成,这里还需要一个将实体Bean转换为一个DTO的工具,我们称其为DTOAssembler:
public class SystemUserDtoAssembler
{
public static SystemUserDto createDto(SystemUser systemUser)
{
SystemUserDto systemUserDto = new SystemUserDto();
if (systemUser != null)
{
systemUserDto.setUserId(systemUser.getUserId());
systemUserDto.setName(systemUser.getName());
systemUserDto.setPassword(systemUser.getPassword());
systemUserDto.setRole(systemUser.getRole());
}
return systemUserDto;
}
public static SystemUserDto[] createDtos(Collection systemUsers)
{
List list = new ArrayList();
if (systemUsers != null)
{
Iterator iterator = systemUsers.iterator();
while (iterator.hasNext())
{
list.add(createDto((SystemUser) iterator.next()));
}
}
SystemUserDto[] returnArray = new SystemUserDto[list.size()];
return (SystemUserDto[]) list.toArray(returnArray);
}
}
为一个实体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 SystemUserDtoDisassembler
{
public static SystemUser fromDto(SystemUserDto aDto)
throws ServiceLocatorException, CreateException,
FinderException
{
SystemUser systemUser = null;
ServiceLocator serviceLoc = ServiceLocator.getInstance();
SystemUserHome systemUserHome = (SystemUserHome) serviceLoc
.getEjbLocalHome("SystemUserHome");
boolean bFind = false;
try
{
systemUser = systemUserHome.findByPrimaryKey(aDto.getPkId());
bFind = (systemUser != null);
} catch (FinderException fe)
{
bFind = false;
}
if (bFind != true)
systemUser = systemUserHome.create(aDto.getPkId());
systemUser.setName(aDto.getName());
systemUser.setPassword(aDto.getPassword());
systemUser.setRole(aDto.getRole());
return systemUser;
}
}
Hibernate在这方面的处理就又比实体Bean简单了,主要把从客户端传来的DTO重新纳入Hibernate的管理即可,唯一需要注意的就是版本问题。
(1) 使用域DTO会有如下好处:
l 域模型结构可以在一次网络调用中复制到客户端,客户端可以读取、更新这个DTO而不需要额外的网络调用开销,而且客户端还可以通过将更新后的DTO回传到服务器端以更新数据。
l 易于实现快速开发。通过使用域DTO可以直接将域模型在层间传输,减少了工作量,可以快速地构建出一个应用。
(2) 但它也有如下的缺点:
l 将客户端和服务器端域对象耦合在一起。如果域模型变了,那么相应的DTO也会改变,即使对于Hibernate这种PO、DTO一体的系统来说也会同样导致客户端的代码要重新编译或者修改。
l 不能很好地满足客户端的要求。客户端可能只需要域对象的20个属性中的一两个,采用域DTO则会将20个属性都传递到客户端,浪费了网络资源。
l 更新域对象很烦琐。客户端对DTO可能做了很多更新或者很深层次的更新,要探查这些更新然后更新域对象是很麻烦的事情。
域DTO解决了在客户端和服务器端之间传递大量数据的问题,但是客户端往往需要更细粒度的数据访问。
例如,一件商品可能有很多属性:名称、编码、重量、型号、大小、颜色、生产日期、生产厂家、批次、保质期等。而客户端只对其中一部分属性有要求,如果将包含所有属性的商品对象到客户端的话,将会即浪费时间又浪费网络带宽,并对系统的性能有不同程度的影响。
我们需要一种可定制的DTO,使它仅封装客户端需要的数据的任意组合,完全与服务器端的域模型相分离。定制DTO与域DTO的区别就是它不映射到任何服务器端的域模型。
从上述的商品例子,设想客户端只需要一些与产品质量有关的属性,在这种情况下,应该创造一个封装了这些特定属性的DTO并传送给客户端。这个DTO是商品属性的一个子集:
public class GoodsCustomDTO implements Serializable
{
private Date productDate;
private Date expireDate;
private String batchNumber;
public GoodsCustomDTO(Date productDate, Date expireDate, String
batchNumber)
{
super();
this.productDate = productDate;
this.expireDate = expireDate;
this.batchNumber = batchNumber;
}
public String getBatchNumber()
{
return batchNumber;
}
public Date getExpireDate()
{
return expireDate;
}
public Date getProductDate()
{
return productDate;
}
}
一般来说,如果客户端需要n个属性,那么应该创造一个包含且仅包含这n个属性的DTO。使用这种方法,域模型的细节被隐藏在服务器中。这样开发人员把DTO仅当做普通的数据,而不是任何像PO那样的服务端的业务数据。当然采用定制DTO系统中会有越来越多的DTO,所以很多开发者情愿使用粗糙一些的DTO(即包含比需要的属性多的属性),而不是重新编写一个新的DTO,只要是返回的冗余数据不是太多,还是可以接受的。毕竟对于任何一种技术,都需要寻求一个兼顾方便和性能的折衷点。
定制DTO主要用于只读操作,也就是DTO只能用来显示,而不能接受改变。既然定制DTO对象仅仅是一个数据的集合,和任何服务端对象没有必然的关系,那么对定制DTO进行更新就是没有意义的了。
定制DTO的缺点如下:
l 需要创建大量的DTO。使用定制DTO会爆炸式地产生大量的对象。
l 客户端DTO的版本必须和服务器端的版本一致。由于客户端和服务器端都通过定制DTO通信,所以一旦服务器端的DTO增加了字段,那么客户端的代码也必须重新编译,否则会产生类版本不一致的问题。
使用定制DTO可以解决域DTO的数据冗余等问题,但是我们需要编写大量的DTO以便返回给客户端它们所需要的数据,但是仍然有对象骤增、代码版本等问题。解决这一问题的方法就是使用数据传送哈希表。
JDK中的哈希表(HashMap、HashTable等)提供了一种通用的、可序列化的、可容纳任意数据集合的容器。若使用哈希表作为DTO客户端和服务器端代码之间数据传送载体的话,唯一的依赖关系就是置于键中用于表示属性的命名。
比如:
ISomeInterface intf = RemoteService.getSomeInterface();
Map info = intf.getSomeData(id);
System.out.println("您要查询的商品的资料为:");
System.out.println("编号:"+info.get("Number"));
System.out.println("姓名:"+info.get("Name"));
System.out.println("类型:"+info.get("Type"));
System.out.println("高度:"+info.get("Height"));
System.out.println("宽度:"+info.get("Width"));
System.out.println("价格:"+info.get("Price"));
使用数据传送哈希表而不是域DTO或者定制DTO意味着增加了额外的实现复杂性,因为客户端需要知道作为键的字符串,以便在哈希表中取得感兴趣的属性。
(1) 使用数据传送哈希表来进行数据传递的好处在于:
l 有很好的可维护性。不必像定制DTO那样需要额外的类和重复的逻辑,取而代之的是通用的哈希表访问。
l 维护代价低。无须任何服务器端编程就可以创建新的服务器端数据的视图,这样客户端可以动态地决定需要哪些数据。
(2) 当然它也是有缺点的:
l 需要服务器和客户端就键的命名达成一个约定。
l 无法使用强类型的编译时检查。当使用定制DTO或者域DTO的时候,传递给set的值或者从get方法得到的值总是正确的,任何错误都能在编译时被发现。而使用数据传送哈希表时,属性访问的问题只有运行时才能发现,而且读取数据的时候也要进行类型转换,这使得系统性能降低。
l 需要对基本类型进行封装。Java中的基本数据类型,比如int、double、boolean等不能保存在哈希表中,因为它们不是对象,所以在放入哈希表之前需要采用Wrapper类封装,不过在JDK 1.5以后的版本中不再存在此问题。
当开发报表或者开发大数据量的客户端的时候,直接用JDBC访问数据库是更好的方式,但是如何将查询结果传递给客户端呢?最普通的解决方法是使用DTO。例如,用JDBC查询每种商品的销售总量:
select sum(saleBillDetail.FQty) as FTotalQty,saleBillDetail.FGoodsName,saleBillDetail.FGoodsNumber as FGoodsName from T_SaleBillDetail as saleBillDetail group by saleBillDetail.FgoodsId
我们可以创建一个定制DTO来传送这个查询的结果集:
public class SomeDTO implements Serializable
{
private BigDecimal totalQty;
private String goodsNumber;
private String goodsName;
public SomeDTO (BigDecimal totalQty,String goodsNumber,String goodsName)
{
super();
this.totalQty = totalQty;
this.goodsNumber = goodsNumber;
this.goodsName = goodsName;
}
public BigDecimal getTotalQty
{
return totalQty;
}
public String getGoodsNumber()
{
return goodsNumber;
}
public String getGoodsName()
{
return goodsName;
}
}
服务器会执行报表SQL语句得到一个包含每种商品销量的结果集,然后服务器将结果集填装DTO,结果集中的每一行都被转换成DTO并加入一个集合中,填装完毕,这个DTO集合就被传递到客户端供客户端显示报表用。
SQL查询语句是千变万化的,因此对于每种不同的查询结果都要创建不同的DTO。而且数据已经表示在结果集的数据表的行中,将数据转换到一个对象集合中,然后在客户端又将对象集合转换回由行和列组成的数据表显然是多余的。使用行集将原始的SQL查询结果从服务器端直接返回给客户端是更好的做法。
javax.sql.RowSet是 java.sql.ResultSet的子接口,并且在JDBC 3.0中它被作为核心接口取代ResultSet。使用RowSet可以将结果集封装并传递到客户端,由于RowSet是ResultSet的子接口,所以客户端可以像操纵结果集一样对RowSet进行操作。这允许开发人员将查询结果与数据库相分离,这样就无须手工将结果集转换成DTO然后又在客户端重新转换为表格形式。
要将行集传递到客户端,那么这种行集必须是非连接的行集,也就是行集无须保持与数据库的连接,完全可以脱离数据库环境。Sun提供了一个实现如此功能的缓冲行集(Cached RowSet),这个实现在Sun JDK 1.5以后的版本中是包含在安装包中的,如果使用其他公司的JDK或者Sun JDK 1.4,则需要单独到Sun的网站上去下载对应的Jar包。
在商品销售总量报表的例子中,可以用行集获得查询的整个结果集,并将其传递到客户端。为了创建这个行集,可以在服务端编写如下的代码:
ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery();
RowSet crs = new CachedRowSet();
crs.populate(rs);
return crs;
这样客户端就可以得到这个RowSet了。
(1) 用行集作为跨层数据传输的方法的好处是:
l 行集对所有查询操作都提供了统一的接口。使用行集,所有的客户端都可以使用相同的接口满足所有的数据查询需要。当客户端要访问的数据发生改变时行集接口是不变的。
l 消除了无谓的转换。行集可以直接从SQL执行的结果集中创建,而不用从结果集转换为DTO,再由DTO转换为表格。
(2) 使用行集的缺点是:
l 客户端必须知道查询结果集中列的名字。如果查询SQL是隐藏在服务器端的话,表名、表之间的关系等对客户端是透明的,但是客户端仍然需要知道结果集中列的名字,这样才能获得相关的值。
l 直接跳过了域模型。这是一种非面向对象的方式,有悖于基本的J2EE架构。这和Delphi中的“ClientDataSet伪三层”、.Net中的 “WebService返回DataSet”一样,当使用行集的时候并没有反映出来任何业务的概念,它们只是一堆数据而已。Scott Hanselman说:“从WebService返回DataSet,是撒旦的产物,代表了世界上一切真正邪恶的东西”。采用行集使得客户端与服务器端的域模型绑定得更加紧密,当需要对系统重构的时候增加了工作量。
l 无法使用强类型的编译检查。客户端必须调用行集上的getString、getBoolean、getBigDecimal等方法来获取数据,而不是调用DTO上的getName,getNumber。这使得客户端的开发容易出现在运行时才能发现的错误。
l 行集接口定义了可以修改行集数据并与数据库同步的机制,但是开发人员应该避免使用这种手段在客户端更新数据。为了从根本上杜绝这种情况的发生。可以编写一个子集的行集实现类(或者简单地封装一个CachedRowSet实现)把所有的与数据更新相关的行集操作通过异常等方式屏蔽。
上面几节比较了常见的层间数据传输模式,这些模式都有各自的优缺点,必须根据实际情况选择合适的模式,绝对不能生搬硬套、人云亦云。
考虑到系统架构的合理性,很多人都是强调避免将域对象直接传递到客户端的,因为这样服务端的域模型就暴露给了客户端,造成客户端与服务器端的高度耦合。当域模型修改的时候,就要造成客户端代码的修改或者重新编写。建议重新建立一个定制DTO类来传输必要的数据,这样DTO与域模型就可以独立变化。
在大部分业务系统中,很多情况下DTO与域模型是无法独立变化的,比如客户要求为一个商品增加一个“跟货员”的属性,并且要能在客户端显示、编辑这个属性。这种情况下我们能做到只修改域模型而不修改DTO 吗?如果客户想去掉“批次”属性,那么如果只从域模型中去掉这个属性的话,客户端保留编辑这个属性的控件还有什么意义吗?
在大部分业务系统的普通逻辑中客户端界面通常反映的就是域模型,所以没必要进行屏蔽,这样做只能增加无谓的工作量,降低开发效率。案例系统中在大部分情况下可以直接将域模型当做DTO直接传递给客户端,只有在特殊的逻辑中才采用其他的层间数据传输模式。
前面提到对于EJB我们只能编写一个和实体Bean 含有相同属性的JavaBean作为DTO,而由于Hibernate的强大功能,PO的状态管理可以脱离Session。问题的关键是我们不能把一个脱了Session管理的PO直接传递到客户端,因为如果不采取LazyLoad的话,我们会把服务器端所有与此PO相关联的对象都传递到客户端,这是任何人都无法忍受的。而如果采用LazyLoad的话如何取得客户端要的所有数据呢?一个方法是在服务器端把客户端需要的所有数据采用BeanUtils之类的工具一次性都装载好,然后传递给客户端:
PersonInfo p = intf.getPersonByPK(id);
BeanUtils.getProperty(p,"age");
BeanUtils.getProperty(p,"parent.name");
BeanUtils.getProperty(p,"parent.company.name");
return p;
采用LazyLoad以后,对象的类型其实是域对象的子类,其中包含了CGLib、Hibernate为实现LazyLoad而添加的代码(也就是上边的p其实是类似于PersonInfo$CGLib$ Proxy的类型)。如果使用Hessian、Burlap等传递的话会导致序列化问题,因为它们没有能力序列化如此复杂的对象;如果使用RMI、 HttpInvoker虽然可以将对象传递到客户端,但是由于反序列化的需要,CGLib、Hibernate的包是需要安装在客户端的,而且客户端的代码中一旦访问了没有在服务端加载到的属性就会发生“Session已关闭”的异常。那么采用一种更合理的形式把PO传递给客户端就成为一个必须解决的问题。
将PO经过一定形式的转换,传递给客户端,使得客户端能够方便地使用传过来的DTO,这就是DTO生成器要解决的问题。把问题具体分解,我们发现DTO生成器的功能如下:
l 允许客户端指定加载哪些属性,这样DTO生成器就只加载客户端指定的属性,其他属性不予以加载,这减小了网络流量。
l 屏蔽CGLib、Hibernate等的影响,客户端可以把DTO当成一个没有任何副作用的普通JavaBean使用。
l 允许客户端将修改后的DTO传递回服务器端进行更新。
采用简单的对象克隆方法无法得到满足要求的DTO,因为克隆以后的对象仍然是和PO一样的被代理对象。更好的解决方法就是重新生成一个与PO的原有类型(比如PersonInfo,而非 PersonInfo$CGLib$Proxy)一致的JavaBean作为DTO,然后将客户端需要的PO中的属性赋值到DTO中。在复制过程中,因为 PO以及关联的对象的信息已经被LazyLoad破坏得乱七八糟了,所以我们必须要通过一种机制知道对象的字段有哪些、字段的类型是什么、字段是否是关联对象、关联的类型是什么。了解这些信息的最好方式就是通过元数据,案例系统的元数据机制就可以满足这个要求,而且Hibernate也有元数据机制能提供类似的信息,下面就分别介绍通过这两种元数据机制实现DTO生成器的方法。
10.7.1 生成器接口定义
DTO生成器要允许用户指定转换哪些属性,指定的属性的粒度精确到关联属性。下面假定有如下的员工域模型:员工有自己的上司(manager)、部门(department)、电脑设备(computer),本身还有工号、姓名等属性。类图如图10.1所示。
图10.1 员工类图
类图中的两个“0..*—1”的关联关系分别表示:一个部门可以有0到多个员工,一个员工只属于一个部门;一台电脑可以被0到多个员工同时占用,但一个员工必须有且只有一台电脑(这个假设比较特殊)。
假如客户端想获得员工的所有属性、所属部门、间接上级、间接上级的上级,那么只要指定类似于下面的格式就可以了:department、manager.manager、manager.managermanager。
【例10.1】定义一个Selectors。
定义一个Selectors类来表示这些格式,代码如下:
// 关联字段选择器
package com.cownew.PIS.framework.common.db;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class Selectors implements Serializable
{
private Set set;
public Selectors()
{
set = new HashSet();
}
public Selectors(int capacity)
{
set = new HashSet(capacity);
}
public boolean add(String string)
{
return set.add(string);
}
public boolean remove(String string)
{
return set.remove(string);
}
public Iterator iterator()
{
return set.iterator();
}
public String toString()
{
return set.toString();
}
/**
* 产生以property为根的新的Selectors
*/
public Selectors generateSubSelectors(String property)
{
property = property+".";
Selectors newSelector = new Selectors();
Iterator it = this.iterator();
while(it.hasNext())
{
String item = it.next().toString();
if(item.startsWith(property))
{
String subItem = item.substring(property.length());
newSelector.add(subItem);
}
}
return newSelector;
}
/**
* property属性是否被定义在Seletors中了
*/
public boolean contains(String property)
{
Iterator it = this.iterator();
while(it.hasNext())
{
String item = it.next().toString();
if (item.startsWith(property))
{
return true;
}
}
return false;
}
}
调用add方法向Selectors中添加要取得的属性,支持级联方式,比如manager.department;调用generateSubSelectors方法产生以property为根的新的 Selectors,比如Selectors中有manager.department、manager.manager、computer三项,调用 generateSub- Selectors("manager")以后就产生了department、manager两项;调用contains判断一个property属性是否被定义在Seletors中了,比如Selectors中有manager.department、manager.manager、computer 三项,那么调用contains("manager")返回true,调用contains("manager.computer")返回false。
代码示例:
Selectors s = new Selectors();
s.add("department");
s.add("manager.manager");
s.add("manager.manager.manager");
System.out.println(s.generateSubSelectors("manager"));
System.out.println(s.contains("computer"));
System.out.println(s.contains("manager.manager"));
运行结果:
[manager.manager, manager]
false
true
接下来我们来定义DTO生成器的接口,这个接口将能够转换单个PO为DTO,也可以批量转换多个PO为DTO,而且这个接口还应该允许用户指定转换哪些属性。
【例10.2】定义DTO生成器的接口。
代码如下:
// DTO生成器接口
public interface IDTOGenerator
{
/**
* 为多个PO产生DTO
* @param list DTO列表
* @param selectors 哪些复合属性需要转换
*/
public List generateDTOList(List list, Selectors selectors);
/**
* @see List generateDTOList(List list, Selectors selectors)
* @param list DTO列表
*/
public List generateDTOList(List list);
/**
* 为单个PO产生DTO
* @param srcBean
* @param selectors 哪些复合属性需要转换
*/
public Object generateDTO(Object srcBean, Selectors selectors);
public Object generateDTO(Object srcBean);
}
对于没指定Selectors 参数的generateDTO、generateDTOList方法则不返回关联属性的值,只返回根一级的属性。
大部分DTOGenerator的子类都将会直接循环调用generateDTO来完成generateDTOList方法,所以定义一个抽象基类来抽象出这个行为。
【例10.3】DTO生成器抽象基类。
代码如下:
// DTO生成器抽象基类
package com.cownew.PIS.framework.bizLayer;
import java.util.ArrayList;
import java.util.List;
import com.cownew.PIS.framework.common.db.Selectors;
abstract public class AbstractDTOGenerator implements IDTOGenerator
{
public List generateDTOList(List list, Selectors selectors)
{
List retList = new ArrayList(list.size());
for (int i = 0, n = list.size(); i < n; i++)
{
Object srcOV = list.get(i);
retList.add(generateDTO(srcOV, selectors));
}
return retList;
}
public List generateDTOList(List list)
{
List retList = new ArrayList(list.size());
for (int i = 0, n = list.size(); i < n; i++)
{
Object srcOV = list.get(i);
retList.add(generateDTO(srcOV));
}
return retList;
}
}
10.7.2 Hibernate的元数据
Hibernate中有一个非常丰富的元数据模型,含有所有的实体和值类型数据的元数据。
Hibernate提供了ClassMetadata接口、CollectionMetadata接口和Type层次体系来访问元数据。可以通过SessionFactory获取元数据接口的实例。
ClassMetadata catMeta = sessionfactory.getClassMetadata(Cat.class);
Object[] propertyValues = catMeta.getPropertyValues(fritz);
String[] propertyNames = catMeta.getPropertyNames();
Type[] propertyTypes = catMeta.getPropertyTypes();
Map namedValues = new HashMap();
for (int i = 0; i < propertyNames.length; i++)
{
if (!propertyTypes[i].isEntityType()
&& !propertyTypes[i].isCollectionType())
{
namedValues.put(propertyNames[i], propertyValues[i]);
}
}
通过将持久化对象的类作为参数调用SessionFactory的getClassMetadata方法就可以得到关于此对象的所有元数据信息的接口ClassMetadata。下面是ClassMetadata接口的主要方法说明。
l public String getEntityName():获取实体名称。
l public String getIdentifierPropertyName():得到主键的名称。
l public String[] getPropertyNames():得到所有属性名称(不包括主键)。
l public Type getIdentifierType():得到主键的类型。
l public Type[] getPropertyTypes():得到所有属性的类型(不包括主键)。
l public Type getPropertyType(String propertyName):得到指定属性的类型。
l public boolean isVersioned():实体是否是版本化的。
l public int getVersionProperty():得到版本属性。
l public boolean[] getPropertyNullability():得到所有属性的“是否允许为空”属性。
l public boolean[] getPropertyLaziness():得到所有属性的“是否LazyLoad”属性。
l public boolean hasIdentifierProperty():实体是否有主键字段。
l public boolean hasSubclasses():是否有子类。
l public boolean isInherited():是否是子类。
ClassMetadata 接口有getPropertyTypes()、getPropertyNullability()这样平面化的访问所有字段属性的方法,这些方法是供 Hibernate内部实现用的,在外部使用的时候我们常常需要深入每个属性的内部,这样借助于getPropertyNames()、 getPropertyType(String propertyName)两个方法就可以满足要求了。
ClassMetadata entityMetaInfo = sessionFactory
.getClassMetadata(destClass);
String[] propertyNames = entityMetaInfo.getPropertyNames();
for (int i = 0, n = propertyNames.length; i < n; i++)
{
String propertyName = propertyNames[i];
Type propType = entityMetaInfo.getPropertyType(propertyName);
…
}
getPropertyType(String propertyName)方法返回的类型为Type,这个类型包含了字段的元数据信息。Type接口只是一个父接口,它有很多子接口和实现类,图10.2是它的主要的子接口和实现类的结构图。
图10.2 Type接口层次图
Hibernate中的集合类型的基类是 CollectionType,其子类分别对应着数组类型(ArrayType)、Bag类型(BagType)、List类型(ListType)、 Map类型(MapType)、Set类型(SetType)。而“多对一”和“一对一”类型分别为ManyToOneType和 OneToOneType,它们的基类为EntityType。BigDecimal、Boolean、String、Date等类型则属于 NullableType的直接或者间接子类。
Type接口的主要方法列举如下。
l public boolean isAssociationType():此类型是否可以转型为AssociationType,并不表示此属性是关联属性。
l public boolean isCollectionType():是否是集合类型。
l public boolean isComponentType():是否是Component类型,如果是的话必须能转型为AbstractComponentType类型。
l public boolean isEntityType():是否是实体类型。
l public boolean isAnyType():是否是Any类型。
l public int[] sqlTypes(Mapping mapping):取得实体各个字段的SQL类型,返回值的类型遵守java.sql.Types中的定义。
l public Class getReturnedClass():返回值类型。
l public String getName():返回类型名称。
【例10.4】Hibernate元数据接口调用。
示例代码如下:
package com.cownew.Char15;
import org.hibernate.SessionFactory;
import org.hibernate.metadata.ClassMetadata;
import org.hibernate.type.Type;
import com.cownew.PIS.base.permission.common.UserInfo;
import com.cownew.PIS.framework.bizLayer.hibernate.HibernateConfig;
public class HibernateMetaTest
{
public static void main(String[] args)
{
SessionFactory sessionFactory =
HibernateConfig.getSessionFactory();
ClassMetadata entityMetaInfo = sessionFactory
.getClassMetadata(UserInfo.class);
String[] propertyNames = entityMetaInfo.getPropertyNames();
for (int i = 0, n = propertyNames.length; i < n; i++)
{
String propertyName = propertyNames[i];
Type propType = entityMetaInfo.getPropertyType(propertyName);
System.out.println(propertyName + "字段类型为"
+ propType.getReturnedClass().getName());
}
if (entityMetaInfo.hasIdentifierProperty())
{
String idPropName = entityMetaInfo.getIdentifierPropertyName();
Type idPropType = entityMetaInfo.getIdentifierType();
System.out.println("主键字段为:" + idPropName + "类型为"
+ idPropType.getReturnedClass().getName());
} else
{
System.out.println("此实体无主键");
}
}
}
运行结果:
number字段类型为java.lang.String
password字段类型为java.lang.String
person字段类型为com.cownew.PIS.basedata.common.PersonInfo
permissions字段类型为java.util.Set
isSuperAdmin字段类型为java.lang.Boolean
isFreezed字段类型为java.lang.Boolean
主键字段为:id类型为java.lang.String
10.7.3 HibernateDTO产生器
【例10.5】HibernateDTO产生器示例。
代码如下:
// HibernateDTO产生器
package com.cownew.PIS.framework.bizLayer.hibernate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.hibernate.SessionFactory;
import org.hibernate.metadata.ClassMetadata;
import org.hibernate.proxy.HibernateProxyHelper;
import org.hibernate.type.ArrayType;
import org.hibernate.type.CollectionType;
import org.hibernate.type.EntityType;
import org.hibernate.type.ListType;
import org.hibernate.type.MapType;
import org.hibernate.type.SetType;
import org.hibernate.type.Type;
import com.cownew.PIS.framework.bizLayer.AbstractDTOGenerator;
import com.cownew.PIS.framework.common.db.Selectors;
import com.cownew.ctk.common.PropertyUtils;
import com.cownew.ctk.common.ExceptionUtils;
public class HibernateDTOGenerator extends AbstractDTOGenerator
{
private SessionFactory sessionFactory;
public HibernateDTOGenerator(SessionFactory sessionFactory)
{
super();
this.sessionFactory = sessionFactory;
}
public Object generateDTO(Object srcBean, Selectors selectors)
{
try
{
return copyValueObject(srcBean, selectors);
} catch (InstantiationException e)
{
throw ExceptionUtils.toRuntimeException(e);
} catch (IllegalAccessException e)
{
throw ExceptionUtils.toRuntimeException(e);
}
}
private Object copyValueObject(Object srcVO, Selectors selectors)
throws InstantiationException, IllegalAccessException
{
// 取得被代理之前的类型
Class destClass = HibernateProxyHelper
.getClassWithoutInitializingProxy(srcVO);
Object newBean = destClass.newInstance();
ClassMetadata entityMetaInfo = sessionFactory
.getClassMetadata(destClass);
String[] propertyNames = entityMetaInfo.getPropertyNames();
for (int i = 0, n = propertyNames.length; i < n; i++)
{
String propertyName = propertyNames[i];
Type propType = entityMetaInfo.getPropertyType(propertyName);
// 如果不是实体类型也不是集合类型,即普通类型,则直接拷贝这些属性
if (!(propType instanceof EntityType)
&& !(propType instanceof CollectionType))
{
Object value = PropertyUtils.getProperty(srcVO,
propertyName);
PropertyUtils.setProperty(newBean, propertyName, value);
} else if (selectors != null)
{
Selectors subSelector = selectors
.generateSubSelectors(propertyName);
// 如果是集合属性,并且用户在selectors中声明要求此属性,
// 则复制这些属性
if (propType instanceof CollectionType
&& selectors.contains(propertyName))
{
Object collValue = generateCollectionValue(srcVO,
(CollectionType) propType, propertyName,
subSelector);
PropertyUtils.setProperty(newBean, propertyName,
collValue);
}
// 如果是实体属性,并且用户在selectors中声明要求此属性
// 则复制这些属性
else if (selectors.contains(propertyName))
{
Object oldVO = PropertyUtils.getProperty(srcVO,
propertyName);
if (oldVO != null)
{
Object obj = copyValueObject(oldVO, subSelector);
PropertyUtils.setProperty(newBean, propertyName, obj);
}
}
}
}
// 由于主键字段没有在getPropertyNames中,所以要复制主键
String idPropName = entityMetaInfo.getIdentifierPropertyName();
Object value = PropertyUtils.getProperty(srcVO, idPropName);
PropertyUtils.setProperty(newBean, idPropName, value);
return newBean;
}
/**
* 生成srcVO的副本,关联属性由subSelector指定
*/
private Object generateCollectionValue(Object srcVO, CollectionType
type,String propertyName, Selectors subSelector)
throws InstantiationException, IllegalAccessException
{
if (type instanceof SetType)
{
Set valueSet = new HashSet();
Set oldSet = (Set) PropertyUtils.getProperty(srcVO,
propertyName);
Iterator oldIt = oldSet.iterator();
while (oldIt.hasNext())
{
Object oldValue = oldIt.next();
if (oldValue != null)
{
Object obj = copyValueObject(oldValue, subSelector);
valueSet.add(obj);
}
}
return valueSet;
} else if (type instanceof ArrayType)
{
Object[] oldArray = (Object[]) PropertyUtils.getProperty(srcVO,
propertyName);
Object[] valueArray = new Object[oldArray.length];
for (int i = 0, n = oldArray.length; i < n; i++)
{
Object oldValue = oldArray[i];
if (oldValue != null)
{
valueArray[i] = copyValueObject(oldValue, subSelector);
}
}
return valueArray;
} else if (type instanceof ListType)
{
List oldList = (List) PropertyUtils
.getProperty(srcVO, propertyName);
List valueList = new ArrayList(oldList.size());
for (int i = 0, n = oldList.size(); i < n; i++)
{
Object oldValue = oldList.get(i);
if (oldValue != null)
{
valueList.add(copyValueObject(oldValue, subSelector));
}
}
return valueList;
} else if (type instanceof MapType)
{
Map oldMap = (Map) PropertyUtils.getProperty(srcVO,
propertyName);
Map valueMap = new HashMap(oldMap.size());
Set keySet = oldMap.keySet();
Iterator keyIt = keySet.iterator();
while (keyIt.hasNext())
{
Object key = keyIt.next();
Object oldValue = oldMap.get(key);
if (oldValue != null)
{
valueMap.put(key, copyValueObject(oldValue,
subSelector));
}
}
return valueMap;
} else if (type instanceof SetType)
{
Set oldSet = (Set) PropertyUtils.getProperty(srcVO,
propertyName);
Set valueSet = new HashSet(oldSet.size());
Iterator it = oldSet.iterator();
while (it.hasNext())
{
Object oldValue = it.next();
if (oldValue != null)
{
Object copyValue = copyValueObject(oldValue,
subSelector);
valueSet.add(copyValue);
}
}
return valueSet;
}
throw new IllegalArgumentException("unsupport Type:"
+ type.getClass().getName());
}
public Object generateDTO(Object srcBean)
{
try
{
return copyValueObject(srcBean);
} catch (InstantiationException e)
{
throw ExceptionUtils.toRuntimeException(e);
} catch (IllegalAccessException e)
{
throw ExceptionUtils.toRuntimeException(e);
}
}
/**
* 得到srcVO的副本
*/
private Object copyValueObject(Object srcVO) throws
InstantiationException,IllegalAccessException
{
Class destClass = HibernateProxyHelper
.getClassWithoutInitializingProxy(srcVO);
Object newBean = destClass.newInstance();
ClassMetadata entityMetaInfo = sessionFactory
.getClassMetadata(destClass);
String[] propNames = entityMetaInfo.getPropertyNames();
for (int i = 0, n = propNames.length; i < n; i++)
{
String propName = propNames[i];
Type fType = entityMetaInfo.getPropertyType(propName);
if (!(fType instanceof EntityType)
&& !(fType instanceof CollectionType))
{
Object value = PropertyUtils.getProperty(srcVO, propName);
PropertyUtils.setProperty(newBean, propName, value);
}
}
String idPropName = entityMetaInfo.getIdentifierPropertyName();
Object value = PropertyUtils.getProperty(srcVO, idPropName);
PropertyUtils.setProperty(newBean, idPropName, value);
return newBean;
}
}
类的核心方法就是copyValueObject、generateCollectionValue,它们分别负责生成关联实体和集合属性。
在copyValueObject中首先调用Hibernate的工具类HibernateProxyHelper提供的getClassWithoutInitializingProxy方法来得到被LazyLoad代理之前的类名,比如:
getClassWithoutInitializingProxy(session.load(PersonInfo.class, id))返回PersonInfo.class。
getClassWithoutInitializingProxy(new PersonInfo())也将返回PersonInfo.class。
这是去掉LazyLoad这个包袱的最重要的一步。
接着用反射的方法得到getClassWithoutInitializingProxy方法返回的类型的实例。
最后使用Hibernate的元数据API逐个判断实体的各个字段的属性,如果字段是普通字段(既不是实体类型也不是集合类型)则直接使用PropertyUtils来拷贝字段属性;如果字段是集合属性,并且用户在selectors中声明要求此属性,则调用generateCollectionValue方法来生成新的集合属性;如果是实体属性,并且用户在selectors中声明要求此属性,则递归调用copyValueObject方法来取得这个实体属性。需要注意的是在字段是非普通属性的时候,需要调用Selectors的generateSubSelectors方法来更换Selectors的相对根,这就达到了从左到右的逐级深入地取得关联属性值的目的。
generateCollectionValue方法用来根据源bean生成新的集合属性。因为Hibernate中集合字段的类型都是基于接口的,所以此处我们使用这些接口的任意实现类就可以。
调用代码示例:
SessionFactory sessionFactory = HibernateConfig.getSessionFactory();
Session session = sessionFactory.openSession();
UserInfo userInfo = (UserInfo) session.load(UserInfo.class,
"1111111111111111111-88888888");
HibernateDTOGenerator dtoGenerator = new HibernateDTOGenerator(
sessionFactory);
Selectors selectors = new Selectors();
selectors.add("person");
UserInfo newUser1 = (UserInfo) dtoGenerator.generateDTO(userInfo);
System.out.println(newUser1.getNumber());
UserInfo newUser2 = (UserInfo) dtoGenerator.generateDTO(userInfo,
selectors);
System.out.println(newUser2.getPerson().getName());
10.7.4 通用DTO生成器
HibernateDTOGenerator比较完美地解决了DTO的产生的问题,由于使用Hibernate本身的元数据机制,所以这个DTOGenerator可以脱离案例系统使用。并不是所有的 ORM工具都提供了像Hibernate一样的元数据机制,所以对于这样的ORM就必须使用案例系统的元数据机制。代码的实现和 HibernateDTOGenerator非常类似,不过由于根据PO得到DTO的方式在各个ORM之间的差异非常大,比如在Hibernate中PO 的类名就是DTO的类名,而在EJB的实体Bean中PO和DTO的类名没有直接关系,这就需要使用某种命名约定来决定DTO的类名(比如DTO类名为实体Bean类名加“DTO”)。CommonDTOGenerator只能是一个抽象类,把根据PO得到DTO等不能确定的逻辑留到具体的子类中实现。
【例10.6】通用DTO生成器示例。
通用DTO生成器的代码如下:
// 通用DTO生成器
abstract public class CommonDTOGenerator extends AbstractDTOGenerator
{
public Object generateDTO(Object srcBean, Selectors selectors)
{
try
{
return copyValueObject((IValueObject) srcBean, selectors);
} catch (InstantiationException e)
{
throw ExceptionUtils.toRuntimeException(e);
} catch (IllegalAccessException e)
{
throw ExceptionUtils.toRuntimeException(e);
}
}
public Object generateDTO(Object srcBean)
{
try
{
return copyValueObject((IValueObject) srcBean);
} catch (InstantiationException e)
{
throw ExceptionUtils.toRuntimeException(e);
} catch (IllegalAccessException e)
{
throw ExceptionUtils.toRuntimeException(e);
}
}
/**
* 得到bean的真实类,也就是剥离了lazyload等AOP方面以后的类,
* 比如在hibernate中就是:
* return HibernateProxyHelper
* .getClassWithoutInitializingProxy(bean)
*/
protected abstract Class getRealClass(Object bean);
private IValueObject copyValueObject(IValueObject srcVO, Selectors
selectors)throws InstantiationException, IllegalAccessException
{
Class destClass = getRealClass(srcVO);
IValueObject newBean = (IValueObject) destClass.newInstance();
EntityModelInfo eInfo = ServerMetaDataLoaderFactory.getLoader()
.loadEntityByVOClass(destClass);
List fields = eInfo.getFields();
for (int i = 0, n = fields.size(); i < n; i++)
{
EntityFieldModelInfo fInfo = (EntityFieldModelInfo) fields.get(i);
if (!fInfo.isLinkProperty())
{
Object value = PropertyUtils.getProperty(srcVO,
fInfo.getName());
PropertyUtils.setProperty(newBean, fInfo.getName(), value);
} else if (selectors != null)
{
Selectors subSelector = selectors.generateSubSelectors
(fInfo.getName());
if (fInfo.getLinkType().equals(LinkTypeEnum.ONETOMANY)
&& selectors.contains(fInfo.getName()))
{
//TODO:支持其他集合属性,比如List
Set valueSet = new HashSet();
Set oldSet = (Set) PropertyUtils.getProperty(srcVO, fInfo
.getName());
Iterator oldIt = oldSet.iterator();
while (oldIt.hasNext())
{
IValueObject oldValue = (IValueObject) oldIt.next();
if (oldValue != null)
{
IValueObject obj = copyValueObject(oldValue,
subSelector);
valueSet.add(obj);
}
}
PropertyUtils.setProperty(newBean, fInfo.getName(),
valueSet);
} else if (selectors.contains(fInfo.getName()))
{
Object oldVO = PropertyUtils
.getProperty(srcVO, fInfo.getName());
if (oldVO != null)
{
IValueObject obj = copyValueObject(
(IValueObject) oldVO, subSelector);
PropertyUtils.setProperty(newBean, fInfo.getName(),
obj);
}
}
}
}
return newBean;
}
private IValueObject copyValueObject(IValueObject srcVO)
throws InstantiationException, IllegalAccessException
{
Class destClass = getRealClass(srcVO);
IValueObject newBean = (IValueObject) destClass.newInstance();
EntityModelInfo eInfo = ServerMetaDataLoaderFactory.getLoader()
.loadEntityByVOClass(destClass);
List fields = eInfo.getFields();
for (int i = 0, n = fields.size(); i < n; i++)
{
EntityFieldModelInfo fInfo = (EntityFieldModelInfo)
fields.get(i);
if (!fInfo.isLinkProperty())
{
Object value = PropertyUtils.getProperty(srcVO,
fInfo.getName());
PropertyUtils.setProperty(newBean, fInfo.getName(), value);
}
}
return newBean;
}
}
在CommonDTOGenerator中将getRealClass方法设为抽象方法等待子类实现。在copyValueObject方法中目前支持的集合类型仅支持Set类型的属性,以后可以增加对List、Map、数组等类型的支持。
如果规定DTO类名为实体Bean类名加“DTO”,就可以编写下面的EJBDTOGenerator:
public class EJBDTOGenerator extends CommonDTOGenerator
{
protected Class getRealClass(Object bean)
{
String entityBeanClassName = bean.getClass().getName();
String dtoClassName = entityBeanClassName + "DTO";
try
{
return Class.forName(dtoClassName);
} catch (ClassNotFoundException e)
{
throw ExceptionUtils.toRuntimeException(e);
}
}
}
采用案例系统的元数据来实现DTOGenerator就可以保证不依赖于具体ORM,这就是元数据的好处,坏处就是这个EJBDTOGenerator是无法将案例系统的元数据机制剥离的。