转贴:http://book.youkuaiyun.com/bookfiles/427/10042715313.shtml
层间数据传输的过程就是服务的执行者将数据返回给服务的调用者的过程。在非分布式系统中由于有类似Open session in view这样的“怪胎解决方案”的存在,所以层间数据传输的问题并没有充分暴露出来,但是在分布式系统中我们就能清楚地意识到层间数据传输的问题,从而能够更合理的进行设计。为了暴露更多问题,本章讨论的层间数据传输假定的场景是“服务器将执行的数据结果如何传递给远程客户端”,尽管在实际场景中服务的提供者和服务的调用者有可能处于同一虚拟机中(比如Web端与应用服务部署在同一服务器中)。
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
{
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可能做了很多更新或者很深层次的更新,要探查这些更新然后更新域对象是很麻烦的事情。
10.3 定制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增加了字段,那么客户端的代码也必须重新编译,否则会产生类版本不一致的问题。
10.4 数据传送哈希表
使用定制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以后的版本中不再存在此问题。
10.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实现)把所有的与数据更新相关的行集操作通过异常等方式屏蔽。
10.6 案例系统的层间数据传输
上面几节比较了常见的层间数据传输模式,这些模式都有各自的优缺点,必须根据实际情况选择合适的模式,绝对不能生搬硬套、人云亦云。
考虑到系统架构的合理性,很多人都是强调避免将域对象直接传递到客户端的,因为这样服务端的域模型就暴露给了客户端,造成客户端与服务器端的高度耦合。当域模型修改的时候,就要造成客户端代码的修改或者重新编写。建议重新建立一个定制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传递给客户端就成为一个必须解决的问题。
10.7 DTO生成器
将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是无法将案例系统的元数据机制剥离的。
虽然Swing客户端的表现力非常强,但是由于Swing客户端的部署安装非常麻烦,所以在需要无固定地点登录系统进行数据查询的场合,必须采用Web方式来实现。本章就来讨论使用Web实现客户端的一些实现技术。
16.1 Web端部署方式与相关辅助类
根据分层开发的思想,Web端通常需要使用应用服务器提供的服务,因此像Swing客户端一样需要一个部署方式以及调用应用服务器服务的方式。和Swing客户端方式不同的是运行Web端的服务器有可能和应用服务器是运行在同一个服务器的同一个JVM下的,这种方式被称为“并置应用”部署模式。当处于公网的时候这种方式是不被推荐的,因为应用服务器的所有服务接口暴露给了外部访问者,这样增加了系统被攻击的概率,比较好的方式是将应用服务器和数据库等都放在防火墙内部,对外只暴露Web服务器。不过如果应用只是供内网使用的,采用并置的方式则提高了系统的运行速度,因为同一JVM内的调用会比跨JVM的调用速度更快。
无论那种部署方式,Web端都是需要访问元数据以及应用服务器的,所以同样需要元数据加载器、远程服务定位器等,而且通常需要一个允许配置应用服务器地址、元数据地址等的配置文件。
Web端的配置文件的结构目前和ClientConfig是一致的,因此直接拷贝一份ClientConfig.xml重命名为WebConfig.xml即可,读取WebConfig.xml的WebConfig.java也是和ClientConfig.java相似的,所以同样拷贝一份ClientConfig.java并稍作修改以后保存为WebConfig.java。虽然ClientConfig.java的实现和WebConfig.java是非常相似的,但是不能通过继承等方式来实现代码重用,因为这种相似是临时的、不稳定的,在这种情况下采用“Ctrl+C”、“Ctrl+V”的“代码重用”方式是一种比较合理的选择。
16.1.1 SessionId的存储
和Swing一样,Web端在进入系统之前也首先要登录,登录成功以后服务器会分配一个SessionId,在Swing客户端中将SessionId保存到RemoteServiceLocator的一个静态变量中,但是由于HTTP服务的无状态特性并且一个Web服务器是同时为多个登录客户服务的,所以不能将这个SessionId保存到一个普通静态变量中。必须采取其他的机制将这个SessionId保存到能唯一标识这个登录用户的地方。
在Web中有如下几种方式用来存储SessionId。
(1) 隐藏字段
通过HTML的Hidden标记来保存信息:
<input type=hidden name="SessionId" value="ABCD123-CCEDMS-99818848">
这种方式需要在每个页面中都增加这个隐藏字段,在服务器端处理表单的时候也要及时读取这个字段,在每次将响应发送回客户端的时候也要将SessionId写到这个字段中。而且这种方式有保密性问题,恶意攻击者可以很容易地从HTML源代码中找到SessionId。
(2) URL重写技术
把SessionId写到页面链接的尾部,例如:
<a href="/InvReport.jsp">库存报表</a>
改为:
<a href="/InvReport.jsp?SessionId=ABCD123-CCEDMS-99818848">库存报表</a>
这种方式同样存在安全问题。
(3) Cookie
可以把一些小的信息片段保存在客户端,在以后的使用过程中,服务器端可以去读取客户端的Cookie。这种方式无须在每个页面中加入处理代码,用的时候直接调用服务端的方法就可以读取Cookie,安全性也比较好,但是由于早期用户对Cookie的误解造成很多用户将浏览器的Cookie功能关闭,这样系统就不能正常运行了。
(4) Session
这里的Session和应用服务器中的Session原理是一致的,不过是两个不同的东西,为了避免混淆,我们称这里的Session为WebSession。WebSession是Web服务器为每个登录客户端在服务器中设立的一个数据区,客户端只要将数据放入WebSession中,以后的调用中就可以直接从WebSession中取得这些数据了。通过这种方式分配的SessionId是保存在Web服务器端的,系统的安全性得到了提高,因此我们决定将分配的SessionId保存在WebSession中。
客户端浏览器登录Web服务器的时候,Web服务器同样会为此客户端分配一个SessionId以唯一标识此用户,不过这个WebSessionId是不用去关心的,Web服务器会正确处理WebSessionId的保存。如果浏览器支持Cookie的话,Web服务器就会将WebSessionId保存在Cookie中;如果浏览器不支持Cookie的话,就采用URL重写技术将SessionId通过URL传递。
和WebSession对应的是HttpSession接口,我们只要将应用服务器分配的SessionId作为一个Attribute保存到WebSession中即可,WebSession中每个Attribute必须有一个名字作为键,此处将保存SessionId的键命名为“SessionId”并定义在WebConstant类中作为常量:
public final static String SESSIONID = "SessionId";
为了简化调用,创建一个WebContextHelper类,并增加一个getSessionId静态方法用来从WebSession中取得SessionId:
public static String getSessionId(HttpServletRequest request)
{
HttpSession session = request.getSession();
return (String) session.getAttribute(WebConstant.SESSIONID);
}
16.1.2 Web端应用服务定位器
与Swing方式的远程服务定位器一样,Web端应用服务定位器使得取得应用服务器服务的过程透明化,无须考虑服务器的位置、如何连接服务器等问题,实现代码和RemoteServiceLocator非常相似。
【例11.1】在Web端定位服务的定位器。
代码如下:
// Web端服务定位器
public class WebEndServiceLocator
{
public static Object getService(HttpServletRequest request,
Class serviceIntfClass)
{
HttpSession webSession = request.getSession();
String sessionId = (String) webSession
.getAttribute(WebConstant.SESSIONID);
return getService(sessionId, serviceIntfClass);
}
public static Object getService(String sessionId,
Class serviceIntfClass)
{
WebConfig webConfig = WebConfig.getInstance();
HttpInvokerProxyFactoryBean proxyFactory =
new HttpInvokerProxyFactoryBean();
CommonsHttpInvokerRequestExecutor reqExecutor =
new CommonsHttpInvokerRequestExecutor();
proxyFactory.setHttpInvokerRequestExecutor(reqExecutor);
try
{
String serviceIntfName = serviceIntfClass.getName();
String serviceURL = webConfig.getServerURL().toString()
+ serviceIntfName;
if (!StringUtils.isEmpty(sessionId))
{
StringBuffer sb = new StringBuffer();
sb.append(serviceURL).append("?")
.append(SysConstants.SESSIONID).append("=")
.append(sessionId);
proxyFactory.setServiceUrl(sb.toString());
} else
{
proxyFactory.setServiceUrl(serviceURL);
}
proxyFactory.setServiceInterface(serviceIntfClass);
proxyFactory.afterPropertiesSet();
Object service = proxyFactory.getObject();
return service;
} catch (MalformedURLException e)
{
throw new RemoteServerException(
RemoteServerException.CONNECTSERVERERROR, e);
}
}
}
这里假设Web服务器和应用服务器处于不同的JVM中,所以和RemoteServiceLocator一样采用Spring提供的Remoting访问类来跨JVM进行方法调用。如果采用“并置应用”部署模式的话这种方式就会造成参数的返回结果的无谓的序列化与反序列化,这种情况下可以改由直接调用LocalServiceLocator来取得服务,这样的调用消耗就变成了JVM内的普通方法调用,提高了响应速度。
16.1.3 Web端元数据加载器工厂
在Web端同样需要去读取元数据,因此必须为Web端编写一个元数据加载器工厂,用来生成元数据加载器。
【例16.2】在Web端加载元数据的加载器工厂。
// Web端元数据加载器工厂
public class WebMetaDataLoaderFactory
{
private static MetaDataLoader loader;
public static IMetaDataLoader getLoader()
{
if (loader != null)
{
return loader;
}
WebConfig config = WebConfig.getInstance();
String entityCacheFile = config.getEntityCacheFile();
String metaDataPath = config.getMetaDataPath();
loader = new MetaDataLoader(metaDataPath, entityCacheFile);
loader.setCacheEnable(config.isMetaCacheEnabled());
return loader;
}
}
16.2 登 录 界 面
登录界面是系统的入口,在登录界面中用户可以选择要登录的账套以及使用的账号,登录成功以后应用服务器会分配一个SessionId,必须将此SessionId保存到WebSession中。完成登录界面需要三个文件:作为视图的index.jsp用来显示登录界面,LoginForm.java作为用户提交表单的模型,LoginAction.java则执行实际的登录动作。
登录界面由三个表单项构成:账套列表、用户名、密码。在用户单击【登录】按钮的时候要在客户端校验用户名是否为空,如果校验通过则提示“正在登录请稍候……”。
【例16.3】登录界面的JSP代码(index.jsp)。
代码如下:
<%@ page contentType="text/html; charset=UTF-8"%>
<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean"%>
<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html"%>
<%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic"%>
<%@page import="java.util.List"%>
<%@page import="java.util.ArrayList"%>
<%@page errorPage="/errorPage.jsp" %>
<html:html locale="true">
<head>
</head>
<html:base />
<title>登录</title>
<link rel="stylesheet" type="text/css" href="css/style.css">
<script>
function checkForm()
{
if(document.loginForm.userName.value=="")
{
alert("请输入用户名");
return false;
}
document.getElementById("loadingDiv").style.visibility = "visible";
return true;
}
</script>
</head>
<body bgColor=#ffffff>
<table width="379" height="228" border="0" align="center"
background="images/login.jpg">
<tr>
<td width="122"></td>
<td width="241">
<html:form action="loginAction.do" method="POST" onsubmit="return
checkForm()">账 套:<html:select property="acName">
<html:optionsCollection property="ACInfos"
label="displayName" value="name" />
</html:select><br>
账户名:<html:text property="userName" styleClass="input1" value="admin1"></html:text><br>
密 码:<html:password property="password" styleClass="input1"></html:password><br>
<html:submit styleClass="button1">登录</html:submit><br/>
<html:errors />
<div id="loadingDiv" style="visibility:hidden">正在登录请
稍候……</div>
</html:form>
</td>
</tr>
</table>
</body>
</html:html>
在表单上设置了onSubmit事件监听器,这样当用户单击“登录”按钮的时候会首先执行JavaScript代码checkForm,只有checkForm方法返回true,表单才被提交。在checkForm方法中校验用户名是否为空,如果为空则提示用户“请输入用户名”,否则将初始状态为隐藏的包含文字“正在登录请稍候……”的层loadingDiv显示出来,然后返回true。
得到账套列表的方法getACInfos定义在LoginForm中,当index.jsp初始化的时候会调用LoginForm的getACInfos方法得到账套列表,并使用<html:optionsCollection/>标记将列表显示到下拉列表中,label="displayName"、 value="name" 表示显示ACInfo的displayName属性,而内部保存的属性则为name。
图16.1是Web端的登录界面。
图16.1 登录界面
LoginForm有三个作用:一是保存index.jsp提交过来的表单中的账套名acName、用户名userName及密码password三个字段;二是提供得到账套列表的getACInfos方法;三是对提交的表单进行校验。
【例16.4】登录界面的Form类。
代码如下:
// LoginForm.java
public class LoginForm extends ActionForm
{
private String acName;
private String userName;
private String password;
public String getAcName()
{
return acName;
}
public void setAcName(String acName)
{
this.acName = acName;
}
public String getPassword()
{
return password;
}
public void setPassword(String password)
{
this.password = password;
}
public String getUserName()
{
return userName;
}
public void setUserName(String userName)
{
this.userName = userName;
}
/**
* 得到账套列表
*/
public ACInfo[] getACInfos()
{
ILoginService loginService = (ILoginService) WebEndServiceLocator
.getService((String)null,ILoginService.class);
return loginService.getAllACInfo();
}
public ActionErrors validate(ActionMapping mapping,
HttpServletRequest request)
{
ActionErrors errors = new ActionErrors();
if(StringUtils.isEmpty(acName))
{
errors.add("acName",new ActionMessage("请选择账套",false));
}
if(StringUtils.isEmpty(userName))
{
errors.add("userName",new ActionMessage(
"用户名不能为空",false));
}
return errors;
}
}
getACInfos方法直接将ILoginService接口的getAllACInfo方法的返回值作为自身的返回值。在登录之前还没有SessionId,所以在此处将SessionId参数设定为null。
在validate方法中校验用户提交的表单中账套名和用户名是否为空,虽然在客户端的JavaScript代码中校验用户名不为空了,但是客户端永远是不可信的,必须在服务端重新校验。案例系统没有多语言支持的要求,因此对于校验信息简化处理,即不定义在配置文件中,而是直接写在代码中,ActionMessage构造函数的第二个参数表示第一个参数是配置文件的键还是一个单纯的消息字符串,此处设置为false,"用户名不能为空"这样的字符串表示单纯的消息字符串,无须到配置文件中去进行消息的转换。
【例16.5】进行实际登录处理的Action类。
代码如下:
// LoginAction.java
public class LoginAction extends Action
{
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws Exception
{
LoginForm loginForm = (LoginForm) form;
String userName = loginForm.getUserName();
String password = loginForm.getPassword();
String acName = loginForm.getAcName();
ILoginService loginService = (ILoginService) WebEndServiceLocator
.getService((String)null,ILoginService.class);
try
{
String sessionId = loginService.login(userName, password, acName);
HttpSession session = request.getSession();
// 将应用sessionId放入WebSession
session.setAttribute(WebConstant.SESSIONID, sessionId);
ICommonService cs = (ICommonService) WebEndServiceLocator
.getService(sessionId, ICommonService.class);
// 将当前用户值对象放入WebSession
session.setAttribute(WebConstant.CURUSERINFO,
cs.getCurrentUser());
return mapping.findForward("mainPage");
} catch (LoginServiceException lse)
{
saveErrors(request, WebExceptionUtils.toActionMessages(lse));
return mapping.getInputForward();
}
}
}
这里调用ILoginService 接口的login实现登录操作,如果登录正确则将应用服务器分配的SessionId保存到WebSession中,并调用ICommonService 的getCurrentUser方法将当前用户的信息也保存到WebSession中,最后跳转到主页面。
如果出现密码错误、用户已被冻结等问题,则login方法中会抛出LoginServiceException 异常,所以在这里捕捉LoginServiceException 异常,并将异常消息显示给用户。WebExceptionUtils中的toActionMessages方法用来将异常对象中的异常消息转换成ActionMessages对象,其实现如下:
// Web端异常工具类
package com.cownew.PIS.framework.web.helper;
import org.apache.struts.action.ActionErrors;
import org.apache.struts.action.ActionMessage;
import org.apache.struts.action.ActionMessages;
public class WebExceptionUtils
{
public static ActionMessages toActionMessages(Exception e)
{
ActionMessages errors = new ActionMessages();
errors.add(ActionErrors.GLOBAL_MESSAGE, new ActionMessage(e
.getMessage(), false));
return errors;
}
}
index.jsp、LoginForm.java和LoginActon.java 这3个文件编写完毕后,在struts-config.xml中将三者的对应关系配置起来即可。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts-config PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 1.1//EN" "http://jakarta.apache.org/struts/dtds/struts-config_1_1.dtd">
<struts-config>
<form-beans>
<form-bean name="loginForm"
type="com.cownew.PIS.framework.web.LoginForm" />
</form-beans>
<global-forwards>
<forward name="errorPage" path="/errorPage.jsp" />
<forward name="loginPage" path="/index.jsp" />
<forward name="mainPage" path="/mainPage.jsp" />
</global-forwards>
<action-mappings>
<action input="/index.jsp" name="loginForm" path="/loginAction"
scope="request" type="com.cownew.PIS.framework.web.LoginAction"
validate="true" />
</action-mappings>
<message-resources parameter="ApplicationResources" />
</struts-config>
16.2.1 登出系统
当用户退出系统的时候如果执行退出操作就能够及早释放服务器端资源。因为登录操作涉及不到表单等问题,所以不需要编写JSP页面作为视图,也不需要Form作为模型,只要开发一个登出Action即可。Struts支持没有JSP和Form的Action,直接调用Action即可执行Action的execute方法,这时候的Action相当于一个Servlet,不过与Servlet相比能更方便地使用Struts提供的一些功能。
【例16.6】登出操作对应的Action。
代码如下:
// LogoutAction.java
public class LogoutAction extends Action
{
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws Exception
{
String sessionid = WebContextHelper.getSessionId(request);
ILoginService loginService = (ILoginService) WebEndServiceLocator
.getService(request, ILoginService.class);
loginService.logout(sessionid);
request.getSession().invalidate();
return mapping.findForward("loginPage");
}
}
在execute方法中首先调用ILoginService接口的logout方法登出应用服务器,然后调用HttpSession的invalidate方法将WebSession销毁,最后转向登录界面。
16.2.2 心跳页面
Web客户端同样存在Swing客户端的长时间无操作造成的Session失效问题,必须定时调用ICommonService接口提供的心跳操作。
可以使用HTML的元信息头实现刷新,HTML提供了一种要求浏览器定时刷新页面的方式,也就是在HTML的head标记内加入<meta http-equiv="refresh" content="60">,这样浏览器就可以每分钟刷新本页面了。
【例16.7】心跳页面(heartBeat.jsp)。
代码如下:
<%@ page language="java" contentType="text/html; charset=GB18030"
pageEncoding="GB18030"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<%@page import="com.cownew.PIS.framework.common.services.ICommonService"%>
<%@page import="com.cownew.PIS.framework.web.helper.WebEndServiceLocator"%>
<%@page import="com.cownew.ctk.common.StringUtils"%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=GB18030">
<!-- 每隔60秒刷新一次 -->
<meta http-equiv="refresh" content="60">
<title>HeartBeat</title>
</head>
<body>
<%
ICommonService cs = (ICommonService)WebEndServiceLocator
.getService(request,ICommonService.class);
try
{
cs.nop();
}
catch(Throwable t)
{
//心跳操作不能停
out.println(StringUtils.stackToString(t));
}
%>
</body>
</html>
为了防止nop方法调用出现异常造成“心跳停止”,这里将所有可能的异常全部截获并且打印出来。
这个心跳页面必须放到用户永远都会打开的页面上,后边将会讲到的主菜单页面是用户在运行期间一直打开的页面,所以将心跳页面嵌入到主菜单页面中是比较好的选择。
16.3 主页面和主菜单
主页面框架定义在页面mainPage.jsp中,这个页面由左侧的菜单框架和右侧的主界面框架组成。
<%@ page contentType="text/html; charset=UTF-8"%>
<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean"%>
<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html"%>
<%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN"
"http://www.w3.org/TR/html4/frameset.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312">
<meta http-equiv="Page-Enter" content="revealTrans(duration=1,
transition=12)">
<title>CowNew进销存</title>
</head>
<frameset cols='150,*' rows='*' frameborder="yes">
<frame name=menuFrame src='mainPageLeftMenu.jsp' MARGINWIDTH=0
MARGINHEIGHT=0>
<frame src='mainPageRightIndex.jsp' name=mainFrame scrolling='yes'>
</frameset>
<noframes><body>
</body></noframes>
</html>
文件head部分的代码:
<meta http-equiv="Page-Enter" content="revealTrans(duration=1, transition=12)">
表示页面进入时的渐进显示特效,这使得从登录界面到主界面的切换过程更圆滑。在页面中可以用类似的方式指定页面进入和页面退出时的各种特效:
进入页面<meta http-equiv="Page-Enter" content="revealTrans(duration=x, transition=y)">
退出页面<meta http-equiv="Page-Exit" content="revealTrans(duration=x, transition=y)">
duration表示特效的持续时间,以秒为单位。transition表示使用哪种特效,取值为1~23,各个值的意义如下:
0 矩形缩小
1 矩形扩大
2 圆形缩小
3 圆形扩大
4 下到上刷新
5 上到下刷新
6 左到右刷新
7 右到左刷新
8 竖百叶窗
9 横百叶窗
10 错位横百叶窗
11 错位竖百叶窗
12 点扩散
13 左右到中间刷新
14 中间到左右刷新
15 中间到上下
16 上下到中间
17 右下到左上
18 右上到左下
19 左上到右下
20 左下到右上
21 横条
22 竖条
23 以上22种随机选择一种
16.3.1 菜单配置文件
与Swing方式一样,在Web客户端同样采用可配置的菜单来组织Web客户端的菜单,这样开发人员无须在意菜单的绘制方式,只要了解菜单XML文件的配置方式即可。
Web客户端的业务并不是很复杂,而且功能点比较少,所以没有必要采用多级的菜单结构,只要支持两级即可,也就是整个菜单分为多个栏目,每个栏目下又有数个平行关系的菜单项。菜单配置文件WebMainMenu.xml的格式如下:
<?xml version="1.0" encoding="UTF-8"?>
<Config>
<Columns>
<Column name="SysTool">系统工具</Column>
<Column name="InvMgr">库存管理</Column>
</Columns>
<MenuItems>
<Item column="SysTool" link="Tools/WebExcel.jsp">WebExcel</Item>
<Item column="SysTool" link="Tools/ChangePassword.jsp">修改密码</Item>
<Item column="InvMgr" link="InvMgr/InvDetailReport.jsp">
库存流水账
</Item>
<Item column="InvMgr" link="InvMgr/InvInTimeReport.jsp">即时库存</Item>
<Item column="InvMgr" link="InvMgr/SaleRankReport.jsp">
销售排行榜
</Item>
</MenuItems>
</Config>
Columns标记中定义的是各个栏目,MenuItems标记定义的是各个菜单项,column属性表示菜单项所属的栏目,link属性表示此菜单项对应的超链接地址。
为了比较清晰地描述菜单栏目和菜单项,我们需要定义WebMenuColumnInfo和WebMenuItemInfo两个类分别表示栏目和菜单项的属性信息。
【例16.8】菜单属性定义。
Web菜单栏目属性代码如下:
package com.cownew.PIS.framework.web.menu;
public class WebMenuColumnInfo
{
private String name;
private String text;
public WebMenuColumnInfo(String name, String text)
{
super();
this.name = name;
this.text = text;
}
public String getName()
{
return name;
}
public String getText()
{
return text;
}
}
Web菜单项属性代码如下:
package com.cownew.PIS.framework.web.menu;
public class WebMenuItemInfo
{
private String text;
private String link;
private String column;
public WebMenuItemInfo(String text, String link, String column)
{
super();
this.text = text;
this.link = link;
this.column = column;
}
/**
* 得到菜单项的显示名称
*/
public String getText()
{
return text;
}
/**
* 得到菜单项的连接地址
*/
public String getLink()
{
return link;
}
/**
* 得到菜单项的所属栏目
*/
public String getColumn()
{
return column;
}
}
为了读取WebMainMenu.xml中的菜单信息,需要开发一个与MainMenuManager类似的WebMenuManager。
【例16.9】负责Web主菜单管理的类。
代码如下:
// Web主菜单管理器
package com.cownew.PIS.framework.web.menu;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.io.SAXReader;
import org.dom4j.tree.DefaultElement;
import com.cownew.ctk.common.ExceptionUtils;
import com.cownew.ctk.constant.StringConst;
import com.cownew.ctk.io.ResourceUtils;
public class WebMenuManager
{
private static WebMenuManager instance;
private WebMenuColumnInfo[] columnInfos;
private WebMenuItemInfo[] menuItems;
private WebMenuManager()
{
super();
}
public static WebMenuManager getManager()
{
if (instance == null)
{
instance = new WebMenuManager();
try
{
instance.init();
} catch (UnsupportedEncodingException e)
{
throw ExceptionUtils.toRuntimeException(e);
} catch (DocumentException e)
{
throw ExceptionUtils.toRuntimeException(e);
}
}
return instance;
}
private void init() throws UnsupportedEncodingException, DocumentException
{
InputStream beansXFStream = null;
try
{
beansXFStream = getClass().getResourceAsStream(
"/com/cownew/PIS/framework/web/menu/WebMainMenu.xml");
Document doc = new SAXReader().read(new InputStreamReader(
beansXFStream, StringConst.UTF8));
List colList = doc.selectNodes("//Config/Columns/Column");
columnInfos = new WebMenuColumnInfo[colList.size()];
for (int i = 0, n = colList.size(); i < n; i++)
{
DefaultElement beanElement = (DefaultElement) colList.get(i);
String name = beanElement.attribute("name").getText();
String text = beanElement.getText();
columnInfos[i] = new WebMenuColumnInfo(name,text);
}
List itemList = doc.selectNodes("//Config/MenuItems/Item");
menuItems = new WebMenuItemInfo[itemList.size()];
for (int i = 0, n = itemList.size(); i < n; i++)
{
DefaultElement beanElement = (DefaultElement) itemList.get(i);
String column = beanElement.attribute("column").getText();
String link = beanElement.attribute("link").getText();
String text = beanElement.getText();
menuItems[i] = new WebMenuItemInfo(text,link,column);
}
}finally
{
ResourceUtils.close(beansXFStream);
}
}
/**
* 获取配置文件的所有栏目定义
*/
public WebMenuColumnInfo[] getColumnInfos()
{
return columnInfos;
}
/**
* 获取配置文件的所有菜单项定义
*/
public WebMenuItemInfo[] getMenuItems()
{
return menuItems;
}
/**
* 得到栏目columnName下的所有菜单项
*/
public List getMenuItems(String columnName)
{
List list = new ArrayList();
for(int i=0,n=menuItems.length;i<n;i++)
{
WebMenuItemInfo item = menuItems[i];
if(item.getColumn().equals(columnName))
{
list.add(item);
}
}
return list;
}
}
调用getColumnInfos方法可以得到定义的所有栏目信息,调用getMenuItems则可以得到所有菜单项,而以栏目名为参数调用getMenuItems则可以得到此栏目下的所有菜单项信息。
16.3.2 菜单控件
Web中实现菜单有很多方式,比如模拟Swing中的级联菜单、模拟Outlook效果的滑动效果的菜单、下拉列表框菜单。在这里使用XTree控件实现树状菜单。
XTree是Fason用JavaScript编写的一个在HTML中模拟Swing中的JTree的控件。其使用非常简单,就像编写Java代码一样,用数行代码即可实现一个复杂的树。下面看一下使用演示。
【例16.10】树状控件的使用。
代码如下:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<link rel="stylesheet" type="text/css" href="css/xtree.css">
<script language="JavaScript" src="js/xtree.js"></script>
<head></head>
<body>
<div id=menuDiv></div>
<script language='JavaScript'>
var root=new treeItem('CowNew进销存','','','',icon.root.src);
var itemSysTool =
new treeItem('系统工具','','mainFrame');
root.add(itemSysTool);
var itemSysTool0=
new treeItem('网络Office','','');
itemSysTool.add(itemSysTool0);
var itemWord =
new treeItem('Word','','');
itemSysTool0.add(itemWord);
var itemExcel =
new treeItem('Excel','','');
itemSysTool0.add(itemExcel);
var itemSysTool1=
new treeItem('修改密码','Tools/ChangePassword.jsp','');
itemSysTool.add(itemSysTool1);
var itemFav =
new treeItem('常用网址','','');
root.add(itemFav);
var itemInvMgr0=
new treeItem('网易','http://www.163.com','');
itemFav.add(itemInvMgr0);
var itemInvMgr1=
new treeItem('百度','http://www.baidu.com','');
itemFav.add(itemInvMgr1);
root.setup(document.getElementById('menuDiv'));
</script>
</body>
</html>
在浏览器中打开上面的HTML文件,在浏览器中就可以看到如图16.2所示的效果。
图16.2 树状菜单演示
treeItem的构造函数方法签名如下:
function treeItem(text,action,target,title,Icon)
5个参数的含义分别为菜单项显示文字、对应的链接地址、连接打开的target、超链接的title属性、此节点的图标。
由于XTree是基于层技术的,所以在树构造完毕以后要调用根节点的setup方法将树创建到一个层中。
有了WebMenuManager和XTree控件,我们就能很容易地实现主菜单页面了。
【例16.11】显示主菜单的JSP页面。
<%@ page contentType="text/html; charset=UTF-8"%>
<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean"%>
<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html"%>
<%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<%@page import="com.cownew.PIS.framework.web.menu.WebMenuManager"%>
<%@page import="com.cownew.PIS.framework.web.menu.WebMenuColumnInfo"%>
<%@page import="java.util.List"%>
<%@page import="com.cownew.PIS.framework.web.menu.WebMenuItemInfo"%>
<html>
<link rel="stylesheet" type="text/css" href="css/xtree.css">
<script language="JavaScript" src="js/xtree.js"></script>
<style type="text/css">
body {
margin-left: 0px;
margin-top: 0px;
margin-right: 0px;
margin-bottom: 0px;
}
</style>
<head></head>
<body>
<div id=menuDiv></div>
<script language='JavaScript'>
var root=new treeItem('CowNew进销存','','mainFrame','',icon.root.src);
<%
WebMenuManager menuMgr = WebMenuManager.getManager();
WebMenuColumnInfo[] columnInfos = menuMgr.getColumnInfos();
for(int colIndex=0;colIndex<columnInfos.length;colIndex++)
{
WebMenuColumnInfo columnInfo = columnInfos[colIndex];
%>
var item<%=columnInfo.getName()%> =
new treeItem('<%=columnInfo.getText()%>','','mainFrame');
root.add(item<%=columnInfo.getName()%>);
<%
List menuItemList = menuMgr.getMenuItems(columnInfo.getName());
for(int itemIndex=0;itemIndex<menuItemList.size();itemIndex++)
{
WebMenuItemInfo menuItem =
(WebMenuItemInfo)menuItemList.get(itemIndex);
%>
var item<%=columnInfo.getName()+itemIndex%>=
new treeItem('<%=menuItem.getText()%>',
'<%=menuItem.getLink()%>','mainFrame');
item<%=columnInfo.getName()%>.add(
item<%=columnInfo.getName()+itemIndex%>);
<%
}
}
%>
var itemSystem = new treeItem('系统','','');
root.add(itemSystem);
var itemExit = new treeItem('退出','logoutAction.do','_parent');
itemSystem.add(itemExit);
root.setup(document.getElementById('menuDiv'));
</script>
<iframe src="heartBeat.jsp" height="0" width="0"
name="heartBeatFrame"></iframe>
</body>
</html>
首先创建栏目项,然后创建菜单项,并将菜单项加入栏目项,由于规定了栏目项和菜单项的命名规则,所以此处很容易地就拼凑出变量名。在脚本最后是预定义的“退出”菜单。
注意到在页面中使用<iframe/>标记定义了一个大小为0的子框架来放置心跳页面,由于主菜单页面是不会被关闭的,所以心跳页面就会一直定时刷新以维持心跳。
由于此例的运行效果要在完成第17章的开发后才明显,故在此略去效果演示。
16.4 数据选择器
在Web客户端同样需要数据库数据选择器以及日期选择器,其实现原理与Swing中的数据选择器是一致的,不过由于是以Web形式存在的,所以实现技术又略有差异。
16.4.1 HTML中的模态对话框
数据选择器和日期选择器是需要显示在模态对话框中的,选择器又需要将用户的选择结果返回给调用页面,下面就来看一下HTML中关于模态对话框的使用。
HTML中创建模态对话框的语法为:
vReturnValue = window.showModalDialog(sURL [, vFreeArgument] [, sOrnaments]);
以下是各参数的含义:
l VReturnValue:对于showModalDialog(),它表示被打开的对话框窗口的返回值。
l VFreeArgument:这个参数可用于传递某种类型的数据到打开的对话框,数据可以是一个数值、字符串、数组或者一个对象类型。在对话框中引用这个数值时,可通过dialogArguments 属性来获得。
l SOrnaments:用这个参数指定新窗口的外观。可选择的窗口属性有很多种,当有多种控制需求时,将相关内容用一个字符串连接起来,其间用分号隔开。以下是可选择的属性种类。
u dialogHeight:对话框高度。
u dialogLeft:对话框左边距。
u dialogTop:对话框上边距。
u dialogWidth:对话框宽度。
u center:是否居中( yes | no | 1 | 0 | on | off )。
u dialogHide:是否隐藏( yes | no | 1 | 0 | on | off )。
u edge:边缘风格( sunken | raised )。
u help:是否显示帮助按钮( yes | no | 1 | 0 | on | off )。
u resizable:窗口大小是否可变 ( yes | no | 1 | 0 | on | off )。
u scroll:是否有滚动条( yes | no | 1 | 0 | on | off )。
u status:是否显示状态栏( yes | no | 1 | 0 | on | off )。
【例16.12】打开一个高200px、宽800px的对话框。
下面的代码将打开一个高200px、宽800px的对话框。
window.showModalDialog('http://www.baidu.com','',
'dialogHeight:200px;dialogWidth:800px');
对话框通常是收集用户输入的数据的,因此必须能够取得对话框中的数据。读取showModalDialog方法的返回值就是得到对话框中数据的最好方式,这个返回值是由对话框调用window.parent.returnValue=“要返回的值”来设置的,下面来演示一下。
首先建立main.htm作为主窗口,代码如下:
<html>
<head>
</head>
<body>
<input type="button" value="打开窗口" onClick="javascript:var result=window.showModalDialog('dialog.htm','','dialogHeight:100px;dialogWidth:200px');window.alert(result);"/>
</body>
</html>
当【打开窗口】按钮被单击以后会调用dialog.htm作为模态对话框,并将对话框的返回值通过消息框打印出来。dialog.htm的代码如下:
<html>
<head>
</head>
<body>
<input type="text" id="inputText"/>
<input type="button" value="确定" onclick="javascript:window.parent.returnValue=document.getElementById(
'inputText').value;window.close();"/>
<input type="button" value="取消" onclick="javascript:window.parent.returnValue=undefined;window.close();"/>
</body>
</html>
在用户单击【确定】按钮的时候会将文本控件inputText中录入的文字作为返回值返回,然后调用window.close()关闭窗口;在用户单击【取消】按钮的时候会将undefined(在JavaScript中相当于Java中的null)作为返回值返回,然后调用window.close()关闭窗口。
在浏览器中打开main.htm,单击【打开窗口】按钮以后,dialog.htm就作为模态对话框被打开了,效果如图16.3所示。
图16.3 模态对话框测试
在文本框中输入一些文字后单击【确定】按钮,就可以看到main.htm中已经得到这个返回值了,如图16.4所示。
图16.4 显示对话框返回结果
16.4.2 表格的行选效果
在实现数据库数据选择器的时候可以使用HTML的table 标记实现表格效果,不过HTML的Table标记的表现力是非常弱的,比如不支持行选择效果、不支持列拖动、不支持在位编辑,为了解决这个问题,很多人使用HTC、层、DHTML等技术来模拟实现更为强大的表格。不过在这个应用中没有必要使用这么复杂的技术,我们将尝试使用普通的JavaScript来解决行选择效果问题。
【例16.13】实现Table的行选效果。
建立一个文件table.htm,内容如下:
<html>
<head>
<meta http-equiv="Content-Language" content="zh-cn">
<meta http-equiv="Content-Type" content="text/html; charset=gb2312">
</head>
<script>
function initPage()
{
selectRow(tableMain.tBodies[0].rows[0]);
}
function selectRow(row)
{
for(var i=0,n=tableMain.tBodies[0].rows.length;i<n;i++)
{
var curRow = tableMain.tBodies[0].rows[i];
if(row==curRow)
{
curRow.bgColor='#BEC5DE';
}
else
{
curRow.bgColor='#FFFFFF';
}
}
}
</script>
<body onload="javascript:initPage()">
<table border="1" width="100%" id="tableMain">
<tr onClick='javascript:selectRow(this)'>
<td>数据11</td>
<td>数据12</td>
<td>数据13</td>
</tr>
<tr onClick='javascript:selectRow(this)'>
<td>数据21</td>
<td>数据22</td>
<td>数据23</td>
</tr>
<tr onClick='javascript:selectRow(this)'>
<td>数据31</td>
<td>数据32</td>
<td>数据33</td>
</tr>
<tr onClick='javascript:selectRow(this)'>
<td>数据41</td>
<td>数据42</td>
<td>数据43</td>
</tr>
<tr onClick='javascript:selectRow(this)'>
<td>数据51</td>
<td>数据52</td>
<td>数据53</td>
</tr>
</table>
</body>
</html>
核心方法是selectRow,这个方法接受一个表格行对象,此方法会将此表格行的背景颜色改为“#BEC5DE”,而将其他表格行的背景颜色改为“#FFFFFF”,这样就达到突出显示某一行的目的了。为body添加了onload监听器,这样当页面打开的时候initPage方法就会调用selectRow来实现选中表格的第一行;同时为每个表格对象都增加onClick监听器,这样当表格任意部分被单击以后,就会调用selectRow方法来突出显示被单击的行。
运行效果如图16.5所示,当窗口刚刚打开的时候第一行被选中。
图16.5 表格初始状态
用鼠标单击第三行以后,第三行就被突出显示了,如图16.6所示。
图16.6 单击以突出显示第三行
16.4.3 数据库数据对话框
【例16.14】供选择数据库数据用的JSP页面。
下面是数据库数据对话框页面的代码:
<%@ page contentType="text/html; charset=UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean"%>
<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html"%>
<%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic"%>
<%@ taglib uri="/WEB-INF/CowNew-PIS.tld" prefix="cn"%>
<%@ taglib uri="/WEB-INF/JSTL-Core.tld" prefix="c"%>
<%@page import="com.cownew.PIS.framework.common.metaDataMgr.IMetaDataLoader"%>
<%@page import="com.cownew.PIS.framework.web.helper.WebMetaDataLoaderFactory"%>
<%@page import="com.cownew.PIS.framework.common.IBaseDAO"%>
<%@page import="com.cownew.PIS.framework.web.helper.WebEndServiceLocator"%>
<%@page import="com.cownew.PIS.framework.common.metaDataMgr.EntityModelInfo"%>
<%@page import="com.cownew.PIS.ui.ctrl.query.QueryExecutor"%>
<%@page import="java.util.List"%>
<%@page import="com.cownew.PIS.ui.ctrl.query.QueryColumnPropertyList"%>
<%@page import="com.cownew.PIS.ui.ctrl.query.QueryColumnProperty"%>
<%@page import="com.cownew.PIS.framework.web.ctrl.WebOVPickerUtils"%>
<%@page import="com.cownew.ctk.common.StringUtils"%>
<%@page import="com.cownew.ctk.common.AssertUtils"%>
<%@page import="com.cownew.PIS.framework.web.ctrl.CowNewFunction"%>
<html>
<head>
<%
String daoIntfString = (String)request.getParameter
(WebOVPickerUtils.DAOINTFCLASS);
String dispalyProperty = (String)request.getParameter
(WebOVPickerUtils.DISPALYPROPERTY);
AssertUtils.assertNotNull(daoIntfString);
AssertUtils.assertNotNull(dispalyProperty);
Class daoIntfClass = Class.forName(daoIntfString);
IBaseDAO baseDAO = (IBaseDAO)WebEndServiceLocator
.getService(request,daoIntfClass);
IMetaDataLoader metaLoader = WebMetaDataLoaderFactory.getLoader();
EntityModelInfo eInfo = metaLoader
.loadEntityByEntityPath(baseDAO.getEntityPath());
String pkProperty = eInfo.getPrimaryKey();
QueryExecutor queryExecutor = new QueryExecutor(baseDAO);
List listData = queryExecutor.executeQuery();
QueryColumnPropertyList columnList = queryExecutor.getColumnList();
int displayColIndex = WebOVPickerUtils.getColIndex(
columnList,dispalyProperty);
pageContext.setAttribute("pkProperty",pkProperty);
pageContext.setAttribute("queryExecutor",queryExecutor);
pageContext.setAttribute("listData",listData);
pageContext.setAttribute("columnList",columnList);
%>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<meta HTTP-EQUIV="expires" CONTENT="0">
<script>
function initPage()
{
selectRow(tableMain.tBodies[0].rows[0]);
}
function getSelectedData()
{
var selectIndex=-1;
for (i=0; i<(document.formDataList.selectId.length); i++)
{
if(document.formDataList.selectId[i].checked=='1')
{
selectIndex = i;
}
}
if(selectIndex<0)
{
return undefined;
}
var selectId = document.formDataList.selectId[selectIndex].value;
var selectDisplayProp = tableMain.tBodies[0].rows[selectIndex]
.cells[<%=displayColIndex%>].innerText;
var resultArray = new Array();
resultArray[0]=selectId;
resultArray[1]=selectDisplayProp;
return resultArray;
}
function selectRow(row)
{
for(var i=0,n=tableMain.tBodies[0].rows.length;i<n;i++)
{
var curRow = tableMain.tBodies[0].rows[i];
if(row==curRow)
{
curRow.bgColor='#BEC5DE';
document.formDataList.selectId[i].checked=true;
}
else
{
curRow.bgColor='#FFFFFF';
document.formDataList.selectId[i].checked=false;
}
}
}
</script>
<title><%=eInfo.getAlias() %></title>
</head>
<body onload="javascript:initPage()">
<form name="formDataList">
<table border="1" width="100%" id="tableMain"
style="border-collapse:collapse;">
<thead>
<tr>
<c:forEach var="colInfo" items="${columnList}">
<td>
<logic:notEqual value="${pkProperty}"
name="${colInfo.column}">
<c:out value="${colInfo.displayName}"></c:out>
</logic:notEqual>
</td>
</c:forEach>
</tr>
</thead>
<c:forEach var="info" items="${listData}">
<tr onClick='javascript:selectRow(this)'>
<c:set var="info" value="${info}"></c:set>
<c:forEach var="columnInfo" items="${columnList}">
<c:set var="columnInfo" value="${columnInfo}"></c:set>
<c:set var="propValue" value="${cn:getPropertySilent(info,columnInfo.column)}">
</c:set>
<td>
<logic:equal name="pkProperty" value="${columnInfo.column}" >
<input type=radio name='selectId'
value='<c:out value="${propValue}"></c:out>' />
</logic:equal>
<logic:notEqual name="pkProperty"
value="${columnInfo.column}">
<c:out value="${propValue}"></c:out>
</logic:notEqual>
</td>
</c:forEach>
</tr>
</c:forEach>
</table>
<input type="button" value="确定"
onclick="javascript:window.parent.returnValue=getSelectedData();window.
close();">
<input type="button" value="取消"
onclick="javascript:window.parent.returnValue=undefined;window.close();">
</form>
</body>
</html>
代码一开始读取调用者通过参数传递过来的要显示的数据的远程接口类型以及要显示的属性,接着通过WebEndServiceLocator获得服务接口,进而通过元数据引擎获得实例的主键字段并构造QueryExecutor实例,执行QueryExecutor实例获得系统中的数据。WebOVPickerUtils类提供的getColIndex方法用来得到一个列在属性列表中的顺序,这个顺序将用来确定要显示的字段是哪一列。
最后将一些变量添加到页面上下文中,这些变量将会被后面标记中的表达式访问到。与普通的JSP代码不同,标记中的表达式是不能直接引用JSP代码中定义的变量的,必须将变量放入Session、Request、PageContext等之中才可以,由于这里的变量只对当前页有意义,所以我们将变量添加到页面上下文中。
在构建表格的时候,首先使用forEach 标记遍历columnList中的值构造表头,接着使用嵌套的forEach 标记遍历listData变量。在填充表体单元格数据的时候使用自定义的表达式函数cn:getPropertySilent来取得记录中的属性值。
在【确定】按钮被按下的时候,我们通过getSelectedData得到返回值,getSelectedData方法构造的返回值为一个含有两个元素的数组,第一个元素为被选择行的Id,第二个元素为被选择行的显示字段的值。
为了使得对JSP标记库不熟悉的读者也能看懂上边的代码,这里给出不用JSP标记库实现的数据选择对话框的代码,代码中对部分重复的内容做了省略。
【例16.15】不使用标记实现数据选择对话框。
代码如下:
<%@ page contentType="text/html; charset=UTF-8"%>
…
<%
String daoIntfString =
(String)request.getParameter(WebOVPickerUtils.DAOINTFCLASS);
…
QueryColumnPropertyList columnList = queryExecutor.getColumnList();
int displayColIndex =
WebOVPickerUtils.getColIndex(columnList,dispalyProperty);
%>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<meta HTTP-EQUIV="expires" CONTENT="0">
<script>
…
</script>
<title><%=eInfo.getAlias() %></title>
</head>
<body onload="javascript:initPage()">
<form name="formDataList">
<table border="1" width="100%" id="tableMain" style="border-collapse:collapse;">
<%
out.println("<thead> <tr>");
for(int i=0,n=columnList.size();i<n;i++)
{
QueryColumnProperty colInfo = columnList.getQueryColumnProperty(i);
String column = colInfo.getColumn();
out.print(" <td>");
if(!pkProperty.equals(column))
{
out.print(colInfo.getDisplayName());
}
out.println("</td>");
}
out.println(" </tr></thead>");
for (int i = 0, n = list.size(); i < n; i++)
{
out.print(" <tr onClick='javascript:selectRow(this)'>");
Object info = list.get(i);
for (int j = 0,m=columnList.size();j<m;j++)
{
String column = columnList.getQueryColumnProperty(j)
.getColumn();
Object property = null;
try
{
property = PropertyUtils.getProperty(info, column);
} catch (NestedNullException ne)
{
//当表达式的中间某个嵌套值为null的时候就会抛出此异常
//因此对于其值按照null处理
property = null;
}
out.print("<td> ");
if(pkProperty.equals(column))
{
out.print("<input type=radio name='selectId' value='");
out.print(property);
out.print("'>");
}
else
{
out.print(property);
}
out.print("</td>");
}
out.println("</tr>");
}
%>
</tr>
</table>
<input type="button" value="确定" onclick="javascript:window.parent.returnValue=getSelectedData();window.
close();">
<input type="button" value="取消" onclick="javascript:window.parent.returnValue='';window.close();">
</form>
</body>
</html>
接着编写如下测试代码:
<input type="text" id="idInput"/>
<input type="text" id="displayInput"/>
<input type="button" value="选择人员" onclick="result=window.showModalDialog('/CowNewPIS/Ctrl/ValueObjectData-Picker.jsp?DAOINTFCLASS=com.cownew.PIS.basedata.common.IPersonDAO&dispalyProperty=name','','resizable:yes');idInput.value=result[0];displayInput.value=result[1];"/>
在这段测试代码中,将对话框返回的主键赋值给idInput文本框,而将返回的显示字段的值赋值给displayInput文本框。运行以后单击【选择人员】按钮就可以弹出人员选择对话框,如图16.7所示。
选择一条记录以后单击【确定】按钮,就可以将选择的主键和显示字段显示到文本框中,如图16.8所示。
图16.7 人员选择对话框
图16.8 选择人员以后的效果
16.4.4 数据库数据选择器标记
上面实现的数据库数据选择对话框加快了系统的开发速度,开发人员只需编写少量的HTML代码即可实现数据库数据选择的功能。不过如果系统中用到数据选择的地方比较多的话,开发人员就要在每个地方都重复类似的代码;而且弹出对话框的方式和数据显示的方式也不同,有的开发人员会在按钮的onClick事件中弹出对话框,而有的开发人员会在图片单击的时候弹出对话框,有的开发人员会将选择出的主键显示出来,而有的开发人员则会将主键保存到隐藏字段中,这些不同的实现方式使得产品的一致性被破坏。
Web中的数据选择器应该和Swing中的数据选择器行为一致才可以,这样我们规定Web中的数据选择器为一个文本框加一个小图片按钮,用户单击小图片按钮的时候会弹出数据选择对话框,用户选择一条数据并单击【确定】按钮以后系统将用户选择的数据的显示字段值显示在文本框中,而将主键赋值给开发人员指定的另外一个控件中,这个控件通常是隐藏字段;用户如果在单击图片的同时按住Ctrl键,那么就将选中的数据清除。
根据这个要求,我们开发一个自定义标记来简化数据库数据选择器的使用。
【例16.16】数据选择器的标记定义。
代码如下:
// 数据库数据选择器标记
package com.cownew.PIS.framework.web.ctrl;
import java.io.IOException;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.tagext.TagSupport;
import com.cownew.PIS.framework.web.helper.WebUtils;
public class ValueObjectDataPickerTag extends TagSupport
{
private String daoIntfName;
private String displayProperty;
private String forComponent;
/**
* DAO接口类名
*/
public void setDaoIntfName(String daoIntfName)
{
this.daoIntfName = daoIntfName;
}
/**
* 显示字段
*/
public void setDisplayProperty(String displayProperty)
{
this.displayProperty = displayProperty;
}
/**
* id字段要放置在哪个控件上
*/
public void setForComponent(String forComponent)
{
this.forComponent = forComponent;
}
public int doEndTag() throws JspException
{
String picURL = WebUtils.getAbsoluteURL(pageContext,
"/Ctrl/dataPicker.bmp");
StringBuffer sb = new StringBuffer();
sb.append("<INPUT id='").append(forComponent)
.append("Display' type='text' readonly/>/n");
sb.append(
"<IMG style='WIDTH: 28px; CURSOR: hand;
HEIGHT: 19px' height='19'/n");
sb.append(" src='" + picURL+ "' width='28' onclick=");
sb.append("/"").append(getImgOnClickScript()).append("/"/>/n");
try
{
pageContext.getOut().println(sb.toString());
} catch (IOException e)
{
throw new JspException(e);
}
return EVAL_PAGE;
}
private String getImgOnClickScript()
{
StringBuffer sbPickerURL = new StringBuffer();
sbPickerURL.append("/Ctrl/ValueObjectDataPicker.jsp?");
sbPickerURL.append(WebOVPickerUtils.DAOINTFCLASS).append("=")
.append(daoIntfName);
sbPickerURL.append("&");
sbPickerURL.append(WebOVPickerUtils.DISPALYPROPERTY).append("=")
.append(displayProperty);
String pickerURL = WebUtils.getAbsoluteURL(
pageContext,sbPickerURL.toString());
StringBuffer sb = new StringBuffer();
sb.append("javascript:if(!event.ctrlKey){
result=window.showModalDialog('");
sb.append(pickerURL).append("','','scroll:yes;');");
sb.append(forComponent).append(
"Display.value=result[1];").append(forComponent)
.append(".value=result[0];}else{");
sb.append(forComponent).append("Display.value='';")
.append(forComponent).append(".value='';");
sb.append("}");
return sb.toString();
}
}
在TLB标记中的声明如下:
<tag>
<description>ValueObjectDataPicker</description>
<name>VOPicker</name>
<tag-class>
com.cownew.PIS.framework.web.ctrl.ValueObjectDataPickerTag
</tag-class>
<body-content>JSP</body-content>
<attribute>
<name>daoIntfName</name>
<required>true</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<attribute>
<name>displayProperty</name>
<required>true</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<attribute>
<name>forComponent</name>
<required>true</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
</tag>
daoIntfName属性为要显示数据的远程接口类型名,displayProperty为显示字段名,forComponent为主键字段要被赋值的控件的名字。
在doEndTag方法中拼接出了图片按钮和文本框的代码,在getImgOnClickScript方法中拼接的是按钮图片被单击以后运行的JavaScript脚本,在脚本中要根据是否按下了Ctrl来做不同的处理。
使用示例:
<input type="text" id="personId"/>
<cn:VOPicker daoIntfName="com.cownew.PIS.basedata.common.IPersonDAO"
displayProperty="name" forComponent="personId" />
可以看到数据选择器的调用变得十分简化了,使用这个选择器的开发人员甚至不用了解关于JavaScript的任何知识。
16.4.5 日期选择对话框
HTML中的日期选择对话框有很多种,在案例系统中使用了一个采用VML技术实现的日期控件,界面比较美观,而且易用性很好,我们将其代码做了少许修改以后封装到了DataSelectDialog.htm中,其返回值就是用户选择的日期的本文格式(yyyy-MM-dd)。
为了方便使用,像数据库数据选择器一样,这里同样开发一个自定义标记。
【例16.17】简化日期选择器创建工作的标记。
代码如下:
// 日期选择器标记
package com.cownew.PIS.framework.web.ctrl;
import java.io.IOException;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.tagext.TagSupport;
import com.cownew.PIS.framework.web.helper.WebUtils;
public class DatePickerTag extends TagSupport
{
private String forComponent;
public void setForComponent(String forComponent)
{
this.forComponent = forComponent;
}
public int doEndTag() throws JspException
{
StringBuffer sb = new StringBuffer();
sb.append("<IMG style='CURSOR: hand' onclick=/"")
.append(getOnClick()).append("/"");
String picURL = WebUtils.getAbsoluteURL(pageContext,
"/images/datepicker.gif");
sb.append(" src='").append(picURL).append("' align=top border=0/>/n");
try
{
pageContext.getOut().println(sb.toString());
} catch (IOException e)
{
throw new JspException(e);
}
return EVAL_PAGE;
}
private String getOnClick()
{
StringBuffer sbPickerURL = new StringBuffer();
sbPickerURL.append("/Ctrl/DateSelectDialog.htm");
String pickerURL = WebUtils.getAbsoluteURL(
pageContext,sbPickerURL.toString());
StringBuffer sb = new StringBuffer();
sb.append("javascript:if(!event.ctrlKey){result=window
.showModalDialog('");
sb.append(pickerURL).append(
"','','dialogHeight:300px;dialogWidth:250px;scroll:yes;');");
sb.append(forComponent).append(".value=result;}else{");
sb.append(forComponent).append(".value='';");
sb.append("}");
return sb.toString();
}
}
从代码可以看出,其外观和行为与数据库数据选择器非常相似,其在TLB标记中的声明如下。
<tag>
<description>DatePicker</description>
<name>DatePicker</name>
<tag-class>
com.cownew.PIS.framework.web.ctrl.DatePickerTag
</tag-class>
<body-content>JSP</body-content>
<attribute>
<name>forComponent</name>
<required>true</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
</tag>
使用示例:
<input type="text" id="birthDay"/>
<cn:DatePicker forComponent="birthDay"></cn:DatePicker>
运行效果如图16.9所示。
图16.9 日期选择对话框