第1章 正则表达式.... 1
1.1 为什么要用正则表达式... 1
1.2 正则表达式入门... 3
1.2.1 正则表达式中元字符的用法... 4
1.2.2 Java中的正则表达式API 5
1.2.3 java.util.regex的使用... 6
1.3 实战正则表达式... 8
第2章 程序最优化.... 14
2.1 空间与时间... 14
2.1.1 空间与时间的概念和度量... 14
2.1.2 空间与时间的背反... 15
2.1.3 以空间换时间... 15
2.2 字典、哈希与Map. 19
2.2.1 字典的定义... 19
2.2.2 哈希表与哈希方法... 19
2.2.3 冲突与冲突的解决... 20
2.2.4 Java中的Map接口... 20
2.3 HashMap. 21
2.3.1 应用举例... 21
2.3.2 Map与HashCode. 26
2.4 使用缓存... 29
2.4.1 缓存的概念... 29
2.4.2 LRUMap类... 30
第3章 AOP. 33
3.1 AOP概论... 33
3.2 AspectJ. 35
3.3 Spring AOP. 36
3.3.1 实现Advice. 36
3.3.2 编写业务代码... 37
3.3.3 装配pointcut和advice. 38
3.3.4 运行主程序... 39
3.4 动态代理... 40
3.4.1 CGLib. 40
3.4.2 JDK Proxy. 42
第4章 Java平台下的Web开发.... 48
4.1 标记语言... 48
4.2 自定义标记库的开发... 48
4.2.1 Tag接口的生命周期... 49
4.2.2 hello标记的开发... 50
4.2.3 flash标记的开发... 52
第5章 案例系统需求.... 58
5.1 基础系统... 58
5.1.1 系统用户管理... 58
5.1.2 编码规则管理... 59
5.2 基础资料... 60
5.2.1 人员管理... 60
5.2.2 供应商管理... 61
5.2.3 客户管理... 62
5.2.4 计量单位管理... 62
5.2.5 物料管理... 63
5.3 业务单据... 64
5.3.1 入库单... 64
5.3.2 出库单... 66
5.3.3 盘点单... 68
第6章 基于Spring的多层分布式
应用.... 71
6.1 概述... 71
6.2 Spring Remoting. 72
6.2.1 Hessian使用演示... 72
6.2.2 几种Remoting实现的比较... 75
6.3 改造HttpInvoker 75
6.3.1 服务文件的分模块化... 82
6.3.2 本地服务加载器... 85
6.4 Remoting Session实现... 87
6.4.1 实现思路... 88
6.4.2 Session Id的生成... 88
6.4.3 用户信息的保存... 93
6.4.4 维护管理Session. 95
6.4.5 Session的注销... 97
6.4.6 安全问题... 100
第7章 元数据引擎.... 102
7.1 MDA概述... 102
7.2 关于元数据... 104
7.2.1 元数据示例... 105
7.2.2 元元数据... 108
7.2.3 设计时与运行时... 108
7.2.4 元数据设计的基本原则... 109
7.2.5 此“元数据”非彼
“元数据”... 109
7.3 实体元数据... 110
7.3.1 实体元数据格式... 110
7.3.2 元数据编辑器... 113
7.4 元数据引擎设计... 118
7.4.1 实体元数据运行时模型... 118
7.4.2 分包及命名规范... 119
7.4.3 元数据加载器接口... 120
7.4.4 元数据热部署... 121
7.4.5 元数据部署方式... 121
7.5 元数据引擎实现... 122
7.5.1 根据元数据路径加载
元数据... 122
7.5.2 元数据枚举器... 122
7.5.3 元数据缓存... 125
7.5.4 元数据加载器... 126
7.5.5 工具类... 132
7.5.6 待改进问题... 133
第8章 基础类与基础接口.... 135
8.1 异常处理... 135
8.1.1 异常处理的方式... 135
8.1.2 为异常“脱皮”... 140
8.1.3 枚举异常... 141
8.1.4 异常处理器... 146
8.2 工具类... 147
8.2.1 枚举... 147
8.2.2 资源管理工具类... 149
8.2.3 DateUtils. 149
8.2.4 StringUtils. 150
第9章 数据访问基础服务.... 151
9.1 多账套的实现... 151
9.1.1 配置文件的支持... 151
9.1.2 账套管理器... 154
9.2 线程变量管理器... 157
9.2.1 ThreadLocal类... 157
9.2.2 线程变量管理器的实现... 159
9.3 事务... 163
9.3.1 为什么需要事务... 163
9.3.2 什么是事务... 164
9.3.3 事务的边界划分... 164
9.3.4 声明型事务的属性... 166
9.3.5 事务的隔离... 168
9.3.6 事务的隔离级别... 168
9.3.7 不同隔离级别的差异... 169
9.3.8 Spring的声明型事务... 169
9.3.9 改造Spring事务配置方式... 172
9.4 会话服务的生命周期管理... 175
9.5 IValueObject接口... 178
第10章 层间数据传输.... 180
10.1 什么是DTO.. 180
10.2 域DTO.. 181
10.3 定制DTO.. 186
10.4 数据传送哈希表... 188
10.5 数据传送行集... 189
10.6 案例系统的层间数据传输... 191
10.7 DTO生成器... 192
10.7.1 生成器接口定义... 193
10.7.2 Hibernate的元数据... 197
10.7.3 HibernateDTO产生器... 200
10.7.4 通用DTO生成器... 207
第11章 基于AOP技术的日志系统
和权限系统.... 211
11.1 日志系统... 211
11.1.1 日志系统的设计目标... 211
11.1.2 日志记录元数据... 212
11.1.3 日志拦截器... 214
11.2 权限系统... 217
11.2.1 RBAC.. 218
11.2.2 用户模型... 219
11.2.3 权限拦截器... 222
11.2.4 取得系统中所有的权限项... 225
第12章 基于Hibernate和JDBC的
持久层.... 229
12.1 ServiceBean基类... 229
12.1.1 IBizCtrl与BizCtrlImpl 229
12.1.2 IBaseDAO与
BaseDAOImpl 230
12.2 SQL翻译器... 238
12.2.1 数据库差异比较... 239
12.2.2 LDBC.. 240
12.2.3 SwisSQL. 240
12.2.4 CowNewSQL. 241
12.2.5 案例系统SQL翻译器的
选择... 243
12.2.6 SQL语句的缓存... 243
12.2.7 LDBC异常信息的序列化
问题... 243
12.3 SQL执行器... 246
12.3.1 SQL执行器服务接口... 246
12.3.2 CachedRowSet 248
12.3.3 直接执行SQL对Hibernate的
影响... 248
第13章 Swing客户端主框架.... 253
13.1 登录服务与远程服务定位器... 253
13.1.1 登录接口... 253
13.1.2 密码的保存... 254
13.1.3 通用服务... 257
13.1.4 客户端配置... 259
13.1.5 远程服务定位器... 259
13.2 系统登录... 263
13.2.1 对话框信息的保存和加载... 263
13.2.2 未捕获异常的处理... 265
13.2.3 登录对话框... 266
13.2.4 客户端入口... 269
13.3 基于Panel的UI框架... 271
13.3.1 UIPanel 271
13.3.2 界面容器... 273
13.3.3 UI工厂... 277
13.4 主界面与可配置式菜单... 279
13.4.1 主界面... 279
13.4.2 可配置式菜单... 281
13.4.3 主菜单管理器... 284
13.4.4 主界面菜单初始化... 287
第14章 Swing客户端基础类.... 291
14.1 常用Swing控件... 291
14.1.1 JTextComponent 291
14.1.2 JTextField. 292
14.1.3 JFormattedTextField. 294
14.1.4 JPasswordField. 295
14.1.5 JScrollPane. 295
14.1.6 JProgressBar 296
14.1.7 JList 296
14.2 JTable的使用及扩展... 301
14.2.1 基本用法... 301
14.2.2 隐藏表列... 304
14.2.3 单元格渲染器... 304
14.2.4 单元格编辑器... 308
14.2.5 导出到Excel 312
14.3 数据选择器... 319
14.3.1 自定义布局管理器... 320
14.3.2 数据选择器视图... 322
14.3.3 文件选择器... 326
14.3.4 日期选择器... 328
14.3.5 数据库数据选择器设计... 330
14.3.6 数据选择对话框... 336
14.3.7 数据库数据选择器... 339
第15章 客户端数据维护框架.... 343
15.1 功能描述... 343
15.2 列表界面... 346
15.2.1 数据显示及分页支持... 347
15.2.2 增删改查... 351
15.3 编辑界面... 360
15.3.1 UIDataBinder 360
15.3.2 TableDataBinder 367
15.3.3 EditUI 371
15.4 过滤界面... 376
15.4.1 界面布局... 377
15.4.2 过滤方案持久化... 377
15.4.3 排序规则相关类... 380
15.4.4 系统预设条件面板接口... 384
15.4.5 FilterUI实现... 386
第16章 Web客户端框架.... 394
16.1 Web端部署方式与相关辅助类... 394
16.1.1 SessionId的存储... 394
16.1.2 Web端应用服务定位器... 396
16.1.3 Web端元数据加载器
工厂... 397
16.2 登录界面... 398
16.2.1 登出系统... 403
16.2.2 心跳页面... 404
16.3 主页面和主菜单... 405
16.3.1 菜单配置文件... 407
16.3.2 菜单控件... 412
16.4 数据选择器... 415
16.4.1 HTML中的模态对话框... 416
16.4.2 表格的行选效果... 418
16.4.3 数据库数据对话框... 420
16.4.4 数据库数据选择器标记... 427
16.4.5 日期选择对话框... 430
第17章 应用系统开发.... 433
17.1 日志监控和权限管理... 433
17.1.1 日志监控界面... 433
17.1.2 用户管理接口... 435
17.1.3 用户管理列表界面... 439
17.1.4 用户新增界面... 444
17.1.5 Web端修改密码... 449
17.2 用户自定义编码规则... 452
17.2.1 编码规则的持久化... 455
17.2.2 产生编码... 456
17.3 查询分析器... 460
17.3.1 生成建库SQL. 461
17.3.2 实体检索... 465
17.3.3 客户端界面... 468
17.4 WebExcel 473
17.4.1 Excel的解析... 473
17.4.2 处理文件上传... 474
17.5 客户基础资料开发... 478
17.5.1 数据校验器... 478
17.5.2 客户基础资料开发... 485
17.6 计量单位基础资料开发... 489
17.6.1 计量单位组的服务器端
实现... 492
17.6.2 计量单位列表界面... 496
17.7 库存业务单据... 502
17.7.1 入库单建模... 502
17.7.2 服务端接口及实现... 503
17.7.3 入库单编辑界面... 509
17.7.4 入库单列表界面... 513
17.7.5 入库单过滤界面... 517
17.8 库存Web报表... 523
17.8.1 报表服务接口及实现... 523
17.8.2 报表的编辑... 527
17.8.3 报表的打印... 530
17.8.4 打印控制按钮标记... 531
17.8.5 库存流水账... 533
17.8.6 销售排行榜... 538
序
现在大部分软件开发书籍都是讲解某个技术如何使用,很少有讲实战的,即使有实战案例的讲解,也是讲解网上购物、聊天室之类已经被人写烂了的系统的开发,最可怕的是书中的实现代码惨不忍睹,使得读者很容易被误导,至于如何进行合理的架构设计就更是无从谈起;少数从国外引进的高端技术书籍又大谈特谈各种在天上飞来飞去的理论,“看的时候心潮澎湃,看完之后一脸茫然”,读者不知道如何将这些理论应用到实际的开发过程当中。本书就尝试着打破这种局面,把一个真实的案例系统搭建从头讲起,不仅包含具体的实现技术,也包含一些架构方面的设计思想。
这是一本以Java开发语言为载体来讲解企业级信息系统开发的书,其中涉及到了Hibernate、Struts、Spring、JSP、Swing、JDBC等很多技术,而且案例系统在搭建过程中也较合理地使用了面向对象理念进行系统设计,但是书中不可能全面讲解这些技术的细节知识,读者可以根据需要参考与这些技术相关的资料。
在本书的序言中介绍开发框架等的概念;第1、2、3、4章介绍正则表达式、AOP、自定义JSP标记等基础知识;第5章给出案例系统的需求文档;第6章基于Spring技术搭建案例系统的Remoting部分;第7章构建一个基于MDA理念的元数据引擎;第8章对案例系统中用到的枚举异常类、工具类等进行介绍;第9、10、11、12章基于Spring、Hibernate等技术搭建事务、DTO生成器、权限控制、日志记录、多数据库支持等基础模块;第13、14章开发登录服务、Swing客户端基础模块以及数据选择器等自定义Swing控件;第15章实现列表界面、编辑界面和编辑界面的基类;第16章搭建Web客户端的登录界面、主菜单等基础模块,并开发JSP用的数据选择器等自定义标记;第17章则以前面章节搭建出的框架为基础实现第5章中的需求文档所要求的功能。
在此,我要感谢为这本书的诞生给予我帮助的所有人。首先要感谢父母对我的养育之恩,他们在我辞职写书的过程中对我无微不至的帮助更是让我永远不能忘记;其次要感谢冯仁飞、刘培德、杨勇、戴敬、张洌生等同事对我的帮助和指导;此外还要感谢CowNew开源团队的朋友们(特别是KingChou的执着精神很值得我学习);最后我要感谢清华大学出版社的彭欣编辑,她给我的帮助使得我们的合作非常圆满,使得本书能够顺利地完成创作和出版。
相对于业界很多高手来说,我的水平是很有限的,无论在实战方面还是在理论知识方面都还有不少差距,希望读者不吝指教,以便再版时改进,您可以给我发送邮件:about521@163.com,与本书相关的后续资料将会发布到CowNew开源团队的网站(http://www.cownew.com)中。
杨中科
JDK中很多类的用法我都烂熟于胸了;我已经能够使用Struts+Hibernate做出一个像样的论坛,公司很多人都称我是Hibernate高手;我做过很多上千万的大项目;我有多年的编程经验,我写的代码很多人看了都叫好;我曾经用过Delphi三年,写过很多小程序,什么远程监控呀、API劫持呀、木马呀,Windows的API里边藏着不少好东西呀,Delphi的控件也真是很丰富;我还研究了C#,用C#的WebForm做东西真是方便呀;对了,我目前正在研究很火的AJAX技术!可是……我是软件工程师吗?
1. 重剑无锋、大巧不工
很多开发人员做了一段时间开发以后经常琢磨着怎么写出精彩、巧妙的程序来,所以在程序中使用了大量的技巧,并引以为自豪,还被同事夸奖:“真是高手呀,人家独辟蹊径用这种方法解决的问题,咱都看不懂,惭愧!”
可是这样的技巧真的很好吗?
我曾经接手过一段代码,这段代码据说是一位前辈高人留下的,这段代码经常出现Bug,多少人接手过,每次试图修改Bug时就会引来更多的Bug,被人称为一块硬石头。我读过了这段代码以后倒吸了一口凉气,这确实是高人写的代码,里边嵌套了5层循环,数据在界面和数据库之间加载来保存去很多次,并且用了巧妙的方式拦截了一个框架API,实现了用很复杂的代码才能实现的功能,我读了一上午,并且用了两天时间去改Bug,可谁知仍然是越改Bug越多。一气之下我把原来的代码都删掉了,然后用最普通但是比较笨的实现方式重新实现,从此以后这个功能很少出现Bug,接手的人也再没有抱怨过代码难懂。
这件事让我想起我上学时候的一件事。大四的时候我在一个小软件公司兼职,当时做的是一个呼叫中心系统。我用了很多控件的高级特性进行开发,开发速度之快让项目经理惊讶不已,一个劲地夸我是高手,所以当功能做得差不多的时候他就放我去北京求职了。晚上12点,我正在北京的一个旅馆中整理第二天去招聘会所需要的资料的时候,项目经理的电话打了过来:“兄弟,这边你做的程序有一个间歇性Bug,现在程序跑不起来了。我让另外一个兄弟帮着改一下你的程序,他说看了半天也不知道你是怎么实现的。”我当时心里听了特别自豪,于是乎在电话中很骄傲地给改我程序的那个兄弟讲我是怎么实现的。后来这个程序又出现了一些问题,都是只有我才能搞定,以至于当我毕业要离开那个城市的时候,项目经理苦着脸对我说:“兄弟,你走了以后谁来维护你的程序呀,当初真应该让你用简单一点的技术做呀。”当时听了他这句话我心里仍然是美滋滋的,可现在想起来却好难过。
在软件开发领域中,技巧的优点在于能独辟蹊径地解决一些问题,缺点是技巧并不为大众所熟知。若在程序中使用太多的技巧,可能会造成别人难以理解。一个技巧造就的一个局部的优点对整个系统而言是微不足道的,而一个别人看不懂、改不了的Bug则可能是致命的。在团队协作开发的情况下,开发时应该强调的一个重要方面是程序的易读性,在能够保证软件的性能指标且能够满足用户需求的情况下,必须让其他程序员容易读懂你的程序。
编程时不可滥用技巧,应该用自然的方式编程,简洁是一种美。不会解决问题的人尽管是“菜鸟”,但是这种人破坏力很小;而把简单的问题用复杂的方式解决的人是“半瓶子醋”,这种人破坏力极强,就像当年的我一样!只有能够把复杂问题用简洁而又直接的方式解决的人,才有望成为高手;所以现在我们更愿意看到写得朴实无华,没有什么闪亮词汇的代码。
2. 框架与工具箱
经常听到有人说“我用某某框架做了一个东西”、“我开发了一个实现某某功能的框架”,那么到底什么是框架呢?
用《设计模式》一书中的定义来说就是:“框架(Framework)是构成一类特定软件可复用设计的一组相互协作的类。……框架规定了你的应用程序的体系结构。它定义了整体结构,类和对象的分割,各部分的主要责任,类和对象怎么协作,以及控制流程。”框架实现了对具体实现细节的反向控制(IOC),实现者无需考虑框架层已经实现好的设计,只要按照框架的要求开发就可以了,然后把开发好的东西放到框架中就可以运行。
以Java Web开发而言,任何人都知道MVC的架构方式比传统的Model1方式更容易管理和维护,可是在实际开发中很多人又禁不住Model这种简便开发方式的诱惑。那么如何强迫自己使用MVC模式呢?当然是使用MVC的开发框架了,比如Struts。采用Struts后必须写一个JSP作为View,必须从Action继承来实现Control,必须从ActionForm继承来实现Model,框架约束住了开发人员那充满幻想的大脑,让我们以最佳的设计策略进行开发。
尽管框架中经常包含具体可用的子类或者工具包,但是使用框架的好处是可复用设计而非复用实现,使用框架时,我们无需考虑一些设计策略问题,因为我们已经无形中使用了框架设计好了的“最佳实践”。
与框架相对应的是工具箱(Toolkit)。工具箱是预定义在类库中的类,它是一组相关的、可复用的、提供了通用功能类的集合。我们经常使用的ArrayList、FileOutputStream、CGLib、Dom4j等都是工具箱。这些工具箱不强迫我们采用某个特定的设计,它们只是提供了功能上的帮助,所以说工具箱的作用是实现复用(或者称代码复用)。
在Java中有一种特殊的类叫做工具类(Utils),比如java.lang包中的Math、Commons- BeanUtils中的BeanUtils。这些类并没有封装任何状态在里边,也不允许调用者实例化它们,它们只是暴露了一些静态方法供其他类调用。这种类在其他支持函数库的语言里(比如C++、Delphi)被称为“伪类”,因为这些类没有封装任何东西,没有自己的状态,只是一堆函数的集合而已,原本是应该被坚决杜绝的东西,但是由于Java不支持函数库,所以才允许它们的存在。很多人都批评说这些类是面向过程的东西,不是真正的面向对象。但是不得不承认的是,Java这样的面向过程与面向对象的混合产品正是工业级开发所需要的,面向过程与面向对象并不冲突,否则那些真正的面向对象语言为什么一直还生活在学院派的温室里面呢?
3. 再次框架
“再次框架”是我想使用的一个词汇,意思是在现有的框架基础上为了实现更多应用层次的设计复用而进行的框架设计。
很多人认为使用Struts、Spring这样的框架开发就是基于框架开发了,就是最好的设计了,岂不知这些框架只是解决了大部分通用的问题,很多具体实现上的问题还需要进行框架设计,否则做出来的东西仍然是难以理解、难以复用、难以扩展的。很多人基于某些著名框架写出来的论坛、网站的源代码里实际上充斥着大量重复的代码、糟糕的设计,这些东西确实应该被好好地“再次框架”了。
4. 企业框架
很多企业都建立了自己的一套或自用或开放的框架,比如SAP的Netweaver、用友的UAP、金蝶的BOS、浪潮的Loushang、上海普元的EOS等。开发这些框架是需要大量的资金和人力资源的投入的,但是带来的如下好处也是非常明显的。
(1) 模块复用
在企业信息系统中很多模块是通用的,比如权限管理、组织架构管理,企业框架把这些模块抽取出来,这样具体业务系统的开发者就可以专心于具体业务功能的实现。
(2) 提高产品开发效率
企业框架通常都提供了很多通用的工具箱或者辅助开发工具,业务系统的开发者使用这些工具箱或者辅助开发工具可以用最少的工作量在最短的时间内完成功能开发。试想如果做第1个功能和做第1000个功能耗时耗力一样多的话,那要这个框架有什么用呢?
(3) 保证业务系统使用了最佳实践
企业框架常常采用了非常合理的设计,如何取得远程接口、在哪个地方进行数据校验、如何储存数据等一系列问题都已经被设计好了,开发者只要按照框架的要求进行设计,就可以保证开发出来的产品是设计合理的。
(4) 对知识管理很重要
企业框架集中了公司所有软件项目的共同点,集中了对于公司最重要的知识的精华,随着框架的应用,框架本身也会随之升级优化。一个新加入企业的员工只要理解并掌握了这个框架,就可以很好地融入到团队中来;而离职的人员也已经把自己的知识留在了这个框架中。
(5) 提高产品的一致性
此处的一致性包含两个方面,一个是产品功能的一致性,另一个是产品实现的一致性。功能的一致性主要指产品的界面、操作方式等的一致性,这保证了用户可以很容易地学习和使用系统的所有模块;实现的一致性主要就是规范产品的实现方式。
开发人员是聪明且富于想象力的,不同的开发人员写出来的程序具有个体差异性。这里举一个例子。
有一个界面的功能是:用户可以往这个界面中输入人员的姓名、年龄、性别、地址等信息,然后单击“保存”按钮保存到数据库中。
如果没有一个统一的框架,那么开发人员就可以随意发挥了:有人会在单击“保存”按钮的时候去逐个读取控件的值,然后通过JDBC执行一个Insert语句把数据保存到数据库中;有人会使用开源的数据绑定框架把控件与数据库的字段绑定起来,然后让数据绑定框架处理数据的保存;有人会先为人员建立一个hibernate配置文件,然后逐个读取控件的值填充到值对象中并调用session.save()方法把数据保存到数据库。
开发人员的这种随意发挥是项目非常大的一个风险,如何保证离职人员的代码能迅速地被接手人读懂是非常重要的一个问题,如果这么一个小小的功能就有这么多种实现方式的话,较大规模功能聚合中将面临的问题就更是可想而知了。
而使用特定的开发框架以后呢?对于上述界面功能而言,系统规定只要覆盖父类的initDataBind方法,并在方法里注册界面控件与数据库字段的绑定关系就可以了,其余的如何连接数据库、如何保存都由框架处理了。相信这样做的话接手的人员几乎不用看代码就能知道程序是如何实现的。
采用企业框架带来的一个主要变化就是开发人员可随意发挥的余地小了,必须在框架的约束下进行开发,无法在开发过程中体现自己的“高超本领”。从提高管理效率角度来说,软件企业应当欢迎这种变化;而另一方面,企业中具有挑战新技术激情的优秀开发人员尚可针对企业框架不断地实施改进和完善工作,为企业的技术路线注入新的活力。
l 用框架编程思想来指导企业开发
CowNew的指导思想是为软件公司的J2EE开发提供性能优良的框架方案。他(杨中科)在书序中写道:
“再次框架”是我想使用的一个词汇,意思是在现有的框架基础上为了实现更多应用层次的设计复用而进行的框架设计。
很多人认为使用Struts、Spring这样的框架开发就是基于框架开发了,就是最好的设计了,岂不知这些框架只是解决了大部分通用的问题,很多具体实现上的问题还需要进行框架设计,否则做出来的东西仍然是难以理解、难以复用、难以扩展的。很多人基于某些著名框架写出来的论坛、网站的源代码里实际上充斥着大量重复的代码、糟糕的设计,这些东西确实应该被好好地“再次框架”了。
杨中科的“再次框架”思想是针对当前大量Java图书中的低水平开发方式而提出的。从软件工程的角度出发,像Java这样的完全面向对象的编程语言,最适合以可积累的方式来从事任何开发工作。无论是水平高超的个人还是企业都有必要以比较稳妥的方式来保存前期的工作成果。此外任何普通程序员若想从蓝领地位跃进到设计师的高度,他就必须转换思维方式,从习惯性的蓝领思维模式变化到经常能够思考设计师所关注的问题。所以仅仅学会使用现有的各种框架是不够的,那些是包装严密的“别人的框架”。还应该学会“再次框架”——即一切从企业的需要出发,以各种已有的框架为工具,把许多复杂的工作、频繁重复的简单工作统统地包装起来,构建出企业自己的框架系统,这种“再次框架”所产生的系统无疑对于提高企业的管理水平是最有帮助的。
l 具体地展示企业开发过程中的实战技术
可以这样说:《J2EE全程开发实录》是国内Java人士对J2EE研究的具有里程碑意义的一部作品。
此书的写作根由作者总结得最为确切:
现在大部分软件开发书籍都是讲解某个技术如何使用,很少有讲实战的,即使有实战案例的讲解,也是讲解网上购物、聊天室之类已经被人写烂了的系统的开发,最可怕的是书中的实现代码惨不忍睹,使得读者很容易被误导,至于如何进行合理的架构设计就更是无从谈起;少数从国外引进的高端技术书籍又大谈特谈各种在天上飞来飞去的理论,“看的时候心潮澎湃,看完之后一脸茫然”,读者不知道如何将这些理论应用到实际的开发过程当中。本书就尝试着打破这种局面,把一个真实的案例系统搭建从头讲起,不仅包含具体的实现技术,也包含一些架构方面的设计思想。
通过阅读此书可以发现,原来架构设计并非高不可攀,一些极为普通的、琐碎和细微的工作均可借助于框架设计思想而获得简化。得益于作者在企业中已有的实践经历,在此方面该书对案例系统搭建过程的描述的确令人大开眼界。
本书所讲述的实战技术并非关注于技巧(这与许多书不同),而是以分析框架需求为核心通过CowNew的设计来体现提高开发效率的过程。从这一点来看,CowNew所提供的框架不是封闭的、简单地供一般程序员编程时引用的,而是完全敞开,供与广大读者共同研究的,CowNew的框架是以培养框架思维为目的的、特殊的开放式框架。
l 在开发中以企业框架来实现再次框架的工作
本书的重要价值是向广大Java学习者传达了关于企业真实需求的信息。
企业开发是一种高度有组织化的劳动,要求以很高的效率来完成,并不懈地追求程序代码的可重用性,要求以简单、规范、易于管理的方式开展工作。而企业框架恰好体现了这种要求,CowNew开源团队站在全局的高度审视国内软件开发企业,承诺联手共建优秀企业框架的责任。
关于依照企业框架思路开展工作时的特征,作者写道:
采用企业框架带来的一个主要变化就是开发人员可随意发挥的余地小了,必须在框架的约束下进行开发,无法在开发过程中体现自己的“高超本领”。从提高管理效率角度来说,软件企业应当欢迎这种变化;而另一方面,企业中具有挑战新技术激情的优秀开发人员尚可针对企业框架不断地实施改进和完善工作,为企业的技术路线注入新的活力。
由此可见企业框架是软件企业核心竞争力的体现,是优秀的开发人员着手开展工作的基础和前提。这种认识不但对广大Java学习者具有指导意义,而且对于很多企业培训内部人才和提高管理水平也是十分重要的。基于这样的成熟认识,中国的企业有理由通过不断地积累精华资源,从积累中发挥出自己的优势。
l 精彩光盘提供珍贵的学习资料
杨中科的《J2EE全程开发实录》配书光盘也令我们开眼界。
他把CowNew框架下的企业开发活动,即蓝领的实际劳动过程用录像加解说的形式表现出来。整整一个多小时。
他用Eclips为工具,先做服务器端,然后做Swing客户端,然后做Web客户实现。
一边说,一边敲代码,喔快捷提示与JBuilder同样丰富,而且也能以可视化方式布局控件,如按钮、文本框、图片框等。
给我们印象最深的,是他不时地发现自己编码有错误、未完善。然后他就从error输出中实时地判断问题的根源,马上进行完善。再运行。通了。
之所以在编程的过程中感到得心应手,是因为本书提供了丰富的避免重复开发的技巧(做成CowNew的包)。然后蓝领每用一个企业功能,就把CowNew的包引用一下。有趣的是,本书从头到尾都在演示CowNew的框架设计思想(是设计师的工作),然后作者又在光盘里充当蓝领现身说法。
光盘中已经囊括了书中引述的全部CowNew框架包,以及全部相关源代码。当然读者还可以直接访问CowNew的网站(http://www.cownew.com)与作者本人直接交流。
l 后期编辑加工以专业的态度对待任何细节
本书由清华大学出版社资深责任编辑组稿、加工。编辑本身对Java等编程语言有很好的了解和必要的实践经验,在加工、校对过程中不敢有一丝一毫的疏忽(此书的创作成果最初经过与以往大量Java图书比较,已认定为精品)。任何编辑、修改之处均已反馈给作者杨中科本人亲自进行了核实。编辑对作者十分认真的工作态度深感敬佩。
衷心希望本书的出版能够进一步促进国内的Java学习向更高水平迈进。期待未来国产好书更加精彩,使读者倍加欢迎。期待我国的软件开发竞争力登上新的台阶。
6.1 概述
对于本书之中的案例系统来说,前面提到的业务需求比较简单,使用PB、Delphi、JBuilder等提供的数据敏感组件可以在很短时间内开发出一个可用的系统来。但是本书不会采用这种简单的开发方式,而是要构建一个多层的分布式系统框架,主要基于如下两点考虑:
l 本书是讲解J2EE开发的,所以必须以一个集成了先进设计思想的系统为案例讲解才可以使读者得到尽可能多的知识,这是最大的动因。
l 目前的案例系统的需求非常简单,但是如果架构设计合理,以后完全可以基于这个技术架构进行更多的业务扩展,最终发展成为一个集生产管理、供应链管理、财务管理、客户管理、HR管理等多业务模块,并能处理跨地区、跨组织的大型企业信息系统。
6.1 概 述
分布式技术是处理客户端与服务器之间资源分配的技术,它解决的问题包括失败转发、负载平衡、分布式事务、Session共享等。
分布式系统通常是由多台实现相同功能的服务器同时提供服务,客户端的请求可以根据一定的负载平衡算法被转发到负载较轻的服务器上去,这样就提高了各个服务器的利用率和系统的整体吞吐量;当一台服务器发生故障时,其他服务器会接管这个服务器正在执行的操作,继续为客户端提供服务。使用分布式技术带来的好处主要是提高了系统的稳定性和吞吐量。
多层架构把系统分成数据访问层,业务规则层等多个层次。每个层次都向其他层次提供服务,服务使用者无须关心服务的实现,这样各个层次之间可以责任明确地、相互协调地完成系统任务。各个层之间只通过约定的接口提供服务,服务的实现方式是调用者无须关心的,这样任何一层内部的修改不会蔓延到其他模块,从而最大限度地减少了需求变化时对系统的影响。多层架构带来的好处就是解耦了系统,使各个模块之间的依赖变小,系统更容易理解、修改和扩展。
在Java中,分布式技术常常是和多层架构同时出现的,分布式技术的使用会自然而然地导致系统的分层设计。比如使用EJB后会很自然地将系统分成三层甚至更多层。分布式系统开发中的经验也被借鉴到分层开发中来,从而使得分层更加合理,以至于很多没有采用分布式技术的多层架构系统中也可以找到分布式技术的影子。采用了分层结构设计的分布式系统就被称为多层分布式系统,在Java世界中又经常被简称为分布式系统。本书将会沿用这种说法。
经过多年的发展,分布式技术已经日趋成熟,在各个平台都有了成熟的实现,比如Windows平台下的DCom、Com+、.NET技术体系,Java平台下的EJB,跨平台的Corba、Webservice等。
在Java平台下,EJB无疑是最成熟的分布式技术,它解决了安全性、负载平衡、分布式事务、数据持久化等很多核心问题。但EJB的缺点是应用程序的运行必须依靠EJB服务器,需要编写部署描述符,并且每次修改都要重新编写部署描述符并部署到应用服务器上去,必要的时候还要重启服务器;EJB服务器运行时启动很多开发时无用的组件,占用了大量的系统内存,这给系统的开发人员带来了诸多的不便,开发效率变得非常低。
Rod Johnson在使用EJB进行开发过程中逐渐认识到了EJB的这些缺点,在《Expert One-on-One J2EE Design and Development》一书中从实用的角度重新认识了EJB等技术在J2EE开发中的作用,也催生出了优秀的J2EE框架SpringFramework。SpringFramework、Hibernate等“草根”框架的流行给了EJB这个“皇家规范”以很大的压力,EJB也在向这些框架学习,甚至邀请这些框架的作者参与EJB的改进,从EJB3的规范中就可以看到这一点。
6.2 Spring Remoting
Spring目前提供了对RMI、HttpInvoker、Hessian、Burlap及WebService等Remoting技术的集成。Spring屏蔽了这些实现技术的差异,用户只需开发简单的Java对象(Plain Old Java Objects,POJO)然后按照Spring规定的格式进行配置文件的编写即可。
6.2.1 Hessian使用演示
【例6.1】在Spring中使用Hessian Remoting技术。
下面就来演示一下在Spring中是如何使用Hessian Remoting技术的。Hessian、Burlap、HttpInvoker等是要运行在支持Servlet的Web服务器中的,因此在运行例子之前要安装配置好Web服务器。Web服务器配置完毕以后按照下面的步骤编写代码。
(1) 编写业务接口:
// IWordProcessor业务接口
public interface IWordProcessor
{
/**
* 抽取value中的中文
* @param value
* @return
*/
public String extractChinese(String value);
}
(2) 编写实现类:
// 实现类
public class WordProcessorImpl implements IWordProcessor
{
public String extractChinese(String value)
{
Pattern p = Pattern.compile("[//u4E00-//u9FFF]+");
Matcher matcher = p.matcher(value);
StringBuffer sb = new StringBuffer();
while (matcher.find())
{
sb.append(matcher.group());
}
return sb.toString();
}
}
(3) 修改Web工程中的web.xml文件:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application
2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<!-- 通过Spring的一个Servlet来完成对Hessian的代理 -->
<servlet>
<servlet-name>remote</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet<
/servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>remote</servlet-name>
<url-pattern>/remote/*</url-pattern>
</servlet-mapping>
</web-app>
(4) 在Web工程中添加remote-servlet.xml文件:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
"http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean name="/wordProcessorBean"
class="com.cownew.Char11.Sec02.WordProcessorImpl">
</bean>
<bean name="/WordProcessorService"
class="org.springframework.remoting.caucho.HessianServiceExporter">
<property name="service">
<ref bean="wordProcessorBean" />
</property>
<property name="serviceInterface">
<value>com.cownew.Char11.Sec02.IWordProcessor</value>
</property>
</bean>
</beans>
(5) 编写客户端测试代码:
// 测试代码
package com.cownew.Char11.Sec02;
import java.net.MalformedURLException;
import com.caucho.hessian.client.HessianProxyFactory;
public class MainApp
{
public static void main(String[] args)
{
HessianProxyFactory proxyFactory = new HessianProxyFactory();
try
{
IWordProcessor service = (IWordProcessor) proxyFactory.create(
IWordProcessor.class, "http://localhost:8080/
RemoteCall/remote/WordProcessorService");
System.out.println(
service.extractChinese("人来的不少,I'm very 欣慰"));
} catch (MalformedURLException e)
{
e.printStackTrace();
}
}
}
运行结果:
人来的不少欣慰
用Web服务器来实现Remoting,确实很神奇!
如果需要改用Burlap,则将上面的HessianServiceExporter改成BurlapServiceExporter,HessianProxyFactory改成BurlapProxyFactory就可以,接口和实现类的代码均不需要修改;同样如果要改用HttpInoker,只要将上面的HessianServiceExporter改成HttpInvokerService- Exporter,将HessianProxyFactory改成HttpInvokerProxyFactoryBean就可以了。
在案例系统开发的最初阶段曾经使用Hessian实现Remoting,后来逐渐发现Hessian不能传递复杂对象的缺点,因此决定切换到Http Invoker,没想到从看资料到最终修改完毕竟然用了不到1分钟时间,其他部分完全不用修改,不得不为Spring折服。
6.2.2 几种Remoting实现的比较
Spring支持的Remoting实现技术是非常多的,虽然Spring屏蔽了这些技术使用上的差异,但是选择一个合适的Remoting技术仍然对系统有非常积极的作用,下面就来讲述这些实现技术的优缺点。
(1) RMI:RMI使用Java的序列化机制实现调用及返回值的编组(marshal)与反编组(unmarshal),可以使用任何可序列化的对象作为参数和返回值。其缺点是RMI只能通过RMI协议来进行访问,无法通过HTTP协议访问,无法穿透防火墙。
(2) Hessian:Hessian也是将网络传输的对象转换为二进制流通过Http进行传递,不过它是使用自己的序列化机制实现的编组与反编组,其支持的数据类型是有限制的,不支持复杂的对象。Hessian的优点是可以透过防火墙。
(3) Burlap:Burlap是将网络传输的对象转换为XML文本格式通过Http进行传递,支持的对象与Hessian相比更少。XML一般比二进制流占用空间大,在网络上传递所需要的时间比二进制流长,XML的解析过程也会耗用更多的内存。Burlap可以穿透防火墙,而且由于传输的格式是XML文本,可以与其他系统(比如.NET)集成,从某种程度来讲,Burlap是一种不标准的WebService。
(4) HttpInvoker:HttpInvoker将参数和返回值通过Java的序列化机制进行编组和反编组,它具有RMI的支持所有可序列化对象的优点。Http Invoker是使用Http协议传输二进制流的,而同时又具有Hessian、Burlap的优点。
经过比较,并结合案例系统的特点,HttpInvoker在众
6.3 改造HttpInvoker
HttpInvoker提供了HessianServlet和HessianServiceExporter两种发布服务的方式,HessianServiceExporter比HessianServlet简单一些,只要配置一个Spring IoC风格的配置文件即可:
<beans>
<bean name="/wordProcessorBean" class="com.cownew.Char11.Sec02.WordProcessorImpl">
</bean>
<bean name="/WordProcessorService" class="org.springframework.remoting.caucho.HessianServiceExporter">
<property name="service">
<ref bean="wordProcessorBean" />
</property>
<property name="serviceInterface">
<value>com.cownew.Char11.Sec02.IWordProcessor</value>
</property>
</bean>
</beans>
这是Spring官方文档中提到的使用方法,可是这是最好的使用方法吗?想一想我们配置这个文件无非是要告诉容器3件事:向外提供的服务名字叫做WordProcessorService;服务实现了接口com.cownew.Char11.Sec02.IWordProcessor;服务的实现类是com.cownew. Char11.Sec02.WordProcessorImpl。为了实现这3件事竟然要去写13行配置文件,如果要给服务对象添加AOP代理,那么还要再添加一个<bean>…</bean>标记;如果要对外暴露100个服务,就要去写(13+n)*100行配置文件!
长篇大论、没完没了的配置文件不是Spring的本意,把本应该写在代码里的依赖关系写到配置文件中是对Spring的最大滥用,甚至Rod Johnson本人也犯这样的错误。Java是强类型语言,这也是为什么Java成为工业级语言的重要原因,强类型可以尽早发现代码的错误,借助IDE使用强类型可以提高开发效率。通过使用配置文件,可以将组件之间的依赖延迟到运行阶段,但是并不是任何依赖都需要延迟到运行阶段的。把本应该在开发阶段就组装完毕且在运行时不会轻易改变的依赖放到配置文件中,不仅会导致代码难读、编写困难,也会降低系统的运行效率。
初学Spring的人往往喜欢把本来能在代码里完成的功能都改成配置文件方式,甚至personInfo.setParent(new PersonInfo(“Smith”))这样的代码也要配置到XML文件中。因为这样看起来很酷,因为把代码写到了配置文件中,这样会与众不同!但是软件开发不是玩玩具,“酷”不是选择一个技术的理由,这个技术必须解决实际问题才可以。
Spring只是提供了一个解决问题的思路,Spring的IOC思想是非常简单易懂的,一个熟练的开发人员可以在很短的时间内重写Spring的核心。但是Spring能够发展至今,靠的不是这个核心!试想如果没有Spring MVC、Spring AOP、Spring Remoting,没有Spring ORM,没有Spring JMS,我们还会如此痴迷Spring吗?
不要滥用Spring配置文件,不要把本应该在代码中注入的依赖放到配置文件中去,Spring的本意是简化,而不是复杂化!
那么下面看一下我们想要的配置文件是什么样的:
<bean id="WordProcessorService" class="com.cownew.Char11.Sec02.WordProcessorImpl" serviceInterface="com.cownew.Char11.Sec02.IWordProcessor">
</bean>
在这里指定了输出服务的标识为“WordProcessorService”、实现该服务的类为“com.cownew.Char11.Sec02.WordProcessorImpl”,该服务对应的接口为“com.cownew. Char11.Sec02.IWordProcessor”。
这个配置文件还可以进一步简化。真实的系统中会存在大量的远程服务对象,如果每个对象都采取“WordProcessorService”这样的命名方式的话很容易重复,而且不容易管理。最好的命名方式就是模仿Java的包机制,比如“com.cownew.Char11.Sec02.WordProcessorService”。既然此服务的调用者知道此服务实现了“com.cownew.Char11.Sec02.IWordProcessor”接口,而且“com.cownew.Char11.Sec02.IWordProcessor”这个名字不会重复,服务标识为什么不直接命名为“com.cownew.Char11.Sec02.IWordProcessor”呢?这是个好注意!这样配置文件就可以被简化为:
<bean id="com.cownew.Char11.Sec02.IWordProcessor"
class="com.cownew.Char11.Sec02.WordProcessorImpl" >
</bean>
这就是我想要的!那么我们就来向着这个目标迈进吧。
HttpInvoker是如何在服务器端响应客户端的调用请求,然后把调用的结果返回给客户端的呢?HttpInvoker与客户端交互的组件是DispatcherServlet,当Web服务器接收到“http://localhost:8080/RemoteCall/remote”这个请求的时候就会将请求派发给DispatcherServlet处理。通过阅读DispatcherServlet的代码可以得知,DispatcherServlet从请求中分辨出客户端要调用的服务是“/WordProcessorService”,它就会到remote-servlet.xml中查找名称为“/WordProcessorService”的Bean,最终查找到下面的配置文件声明了“/WordProcessorService”这个服务:
<bean name="/WordProcessorService"
class="org.springframework.remoting.caucho.HessianServiceExporter">
<property name="service">
<ref bean="wordProcessorBean" />
</property>
<property name="serviceInterface">
<value>com.cownew.Char11.Sec02.IWordProcessor</value>
</property>
</bean>
DispatcherServlet调用IOC容器的方法得到这个服务。IOC容器发现这个Bean还引用了另外一个Bean:
<bean name="/wordProcessorBean"
class="com.cownew.Char11.Sec02.WordProcessorImpl">
</bean>
IOC容器首先实例化“WordProcessorImpl”为名称为“wordProcessorBean”的Bean,然后实例化“HessianServiceExporter”,设置“service”属性为“wordProcessorBean”对象,设置“serviceInterface”属性为“com.cownew.Char11.Sec02.IWordProcessor”。IOC容器将实例化完毕的“/WordProcessorService”对象返回给DispatcherServlet,DispatcherServlet把Web请求再次派发给“/WordProcessorService”。
那么“/WordProcessorService”(即HttpInvokerServiceExporter类的对象)是如何响应Web请求的呢?打开HttpInvokerServiceExporter的源码,查看其实现代码,下面的公共方法引起我们的注意:
public void handleRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
Assert.notNull(this.proxy,
"HttpInvokerServiceExporter has not been initialized");
try
{
RemoteInvocation invocation = readRemoteInvocation(request);
RemoteInvocationResult result = invokeAndCreateResult(invocation,
this.proxy);
writeRemoteInvocationResult(request, response, result);
}
catch (ClassNotFoundException ex)
{
throw new NestedServletException("Class not found during
deserialization", ex);
}
}
方法的名字和其参数以及方法内部的实现都暗示了它就是响应Web请求的核心方法,看到它的JavaDoc就更加表明我们的猜测是完全正确的:
Read a remote invocation from the request, execute it, and write the remote invocation result to the response.(从请求中读取远程调用,执行调用,然后将调用结果写到响应中去。)
handleRequest方法是HttpRequestHandler接口中定义的响应Http请求的接口,BurlapServiceExporter、HessianServiceExporter、HttpInvokerServiceExporter都实现了这个接口。request是客户端的请求,客户端的方法调用全部在request中;response是返回给客户端的响应对象,我们要把调用结果(包括返回值、异常等)通过response返回给客户端。
弄懂了HttpInvokerServiceExporter的实现原理,下面就来实现要解析的配置文件:
<bean id="com.cownew.Char11.Sec02.IWordProcessor"
class="com.cownew.Char11.Sec02.WordProcessorImpl" >
</bean>
由于上面的这个配置文件格式是自定义的,DispatcherServlet和HttpInvokerServiceExporter都无法识别它,必须写一个Servlet来处理调用请求。
【例6.2】提供Remoting服务的Servlet。
编写一个从HttpServlet继承的RemotingCallServlet:
// 提供Remoting服务的Servlet
public class RemotingCallServlet extends HttpServlet
{
private static Logger logger = Logger.getLogger(RemotingCallServlet.class);
private static BeanFactory appContext = null;
protected void doPost(HttpServletRequest httpRequest,
HttpServletResponse httpResponse) throws ServletException,
IOException
{
try
{
invokeService(httpResponse, httpRequest);
}catch (Throwable e)
{
// 注意,bean运行过程中的异常并不是通过此处抛出的
//而是通过remoting机制传递到客户端再抛出的
// 此处抛出的是非bean的异常
//由于这里的异常不会抛出到客户端,因此把异常打印出来,方便开发调试
//使用log4j把异常打印出来是一种好习惯!
logger.error(e.getMessage(), e);
throw new ServletException(e);
}
}
private void invokeService(HttpServletResponse response,
HttpServletRequest request) throws ServletException, PISException
{
String reqPath = request.getPathInfo();
String serviceId = getServiceId(reqPath);
invokeBean(request, response, serviceId);
}
private void invokeBean(HttpServletRequest request,
HttpServletResponse response, String serviceId)
throws ServletException, PISException
{
Object _service = appContext.getBean(serviceId);
//因为所有的服务都是无状态的服务,所以此处的_service无须进行同步,
//可以同时为多个调用服务
try
{
HttpInvokerServiceExporter exporter =
new HttpInvokerServiceExporter();
exporter.setService(_service);
exporter.setServiceInterface(Class.forName(serviceId));
exporter.afterPropertiesSet();
exporter.handleRequest(request, response);
} catch (ClassNotFoundException e)
{
throw new ServletException(e);
} catch (IOException e)
{
throw new ServletException(e);
}
}
//用正则表达式将Path中的服务id提取出来,比如“/com.cownew.demo.IService”
//将“com.cownew.demo.IService”解析出来
private static String getServiceId(String reqPath)
{
Pattern pattern = Pattern.compile("/(.+)");
Matcher match = pattern.matcher(reqPath);
match.matches();
match.group();
String serviceId = match.group(1);
return serviceId;
}
static
{
appContext = new ClassPathXmlApplicationContext(
"com/cownew/PIS/framework/server/springBeans.xml");
}
}
编写配置文件springBeans.xml放到和RemotingCallServlet同一级的包下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "file:///E:/保留文档/java/常用包/spring/spring-framework-1.2.8/dist/spring-beans.dtd">
<beans>
<bean id="com.cownew.Char11.Sec02.IWordProcessor"
class="com.cownew.Char11.Sec02.WordProcessorImpl" >
</bean>
</beans>
修改Web工程中的web.xml文件,将DispatcherServlet替换成RemotingCallServlet,删除remote-servlet.xml文件,然后重启服务器。
编写测试客户端:
public class MainApp
{
public static void main(String[] args)
{
HttpInvokerProxyFactoryBean proxyFactory =
new HttpInvokerProxyFactoryBean();
try
{
proxyFactory.setServiceUrl(
"http://localhost:8080/RemoteCall/remote/"
+IWordProcessor.class.getName());
proxyFactory.setServiceInterface(IWordProcessor.class);
proxyFactory.setHttpInvokerRequestExecutor(new
CommonsHttpInvokerRequestExecutor());
proxyFactory.afterPropertiesSet();
IWordProcessor service = (IWordProcessor)proxyFactory.getObject();
System.out.println(service.extractChinese(
"人来的不少,I'm very 欣慰"));
} catch (MalformedURLException e)
{
e.printStackTrace();
}
}
}
运行结果:
人来的不少欣慰
RemotingCallServlet的核心代码在invokeBean中。首先使用Spring的ClassPathXml- ApplicationContext的getBean方法得到服务,然后实例化HttpInvokerServiceExporter,把通过getBean方法得到的服务Bean对象赋值给setService方法。这里规定服务的id和服务的接口类名一致,所以调用Class.forName(serviceId)即可反射得到接口类名,把类名赋值给setServiceInterface方法。
在例子中通过Spring的配置文件来为HttpInvokerServiceExporter设置service、serviceInterface属性,而此处是直接在代码中完成的注入。Spring中大部分类都实现了InitializingBean接口,Spring在为Bean设置完属性后会调用InitializingBean接口的afterPropertiesSet方法来标识属性设置完毕,实现类常常在afterPropertiesSet方法中做属性合法性检验、数据初始化等操作,因此在这种代码注入的情况下要手动调用afterPropertiesSet方法,以防出错。代码最后调用了handleRequest来响应客户端请求。
不按照Spring推荐的配置文件的方式来使用Spring的类初看好像是对Spring的错误使用,实则是一种最佳的使用方式。此处由于服务接口和实现的动态性,在Spring中用配置文件实现起来是非常困难的,即使能够实现配置文件看起来也是非常难懂的,而通过这种代码注入的方式看起来却更简单明了。在使用Spring的时候,使用配置文件注入一定要有充分的理由,不能人云亦云。
客户端测试代码中HttpInvokerProxyFactoryBean的初始化方式也是从Spring的HttpInvoker使用手册的XML文件配置方式翻译过来的,此处由于只是得到IWordProcessor服务,我们完全可以按照配置文件的方式进行注入,但是我们后边将会将这种调用方式封装成一个能承担各种服务调用工作的RemoteServiceLocator,所以此处仍然使用代码方式进行注入。案例系统中使用Spring的配置文件方式注入的地方是非常少的,所以在后边的代码分析中再见到类似的“反Spring”的使用方式的时候就无须大惊小怪了。
经过上边的改造,实现一个新的服务所需要的工作已经减少很多了,只需完成下面的工作就可以实现一个Remoting服务:编写服务接口;编写实现类;在springBeans.xml加入<bean id="服务接口类名" class="服务实现类名" ></bean>。
这个框架还有以下两点可以进一步改进的:
l 服务文件的分模块化。每增加一个服务都要向springBeans.xml中加入一个服务的定义,如果整个系统的服务都定义在这一个文件中,当多人甚至多项目组协同开发的时候这个文件的修改将成为一个灾难,会经常出现多个人同时修改这一个文件造成冲突的问题。要对此处做改进,使得可以支持多springBeans.xml文件,每个项目组都定义自己的springBeans.xml文件。
l 抽象出本地服务加载器。在invokeBean方法中使用appContext.getBean(serviceId)方法来取得本地服务Bean,这暗示了系统是使用Spring IOC来管理服务的,但这个事实是无须RemotingCallServlet 知道的,RemotingCallServlet 只是想通过serviceId来得到服务实现,至于服务的加载方式RemotingCallServlet 无须关心。再者在服务器端,各个模块之间也要相互协作,模块之间无须知道具体的实现类是什么而是通过接口直接调用的。如果调用其他模块的时候都要使用appContext.getBean(serviceId)来加载服务的话,这无疑使得Spring IOC的使用蔓延到了整个系统。基于以上两点考虑,系统需要一个本地服务加载器。
6.3.1 服务文件的分模块化
由于每个模块都定义一个springBeans.xml文件,所以系统内部的各个包中会散布着这些文件,要通过它们加载服务的话,必须首先加载它们,要加载它们就首先要知道它们的位置。得到所有springBeans.xml文件有两种实现方式。
(1) 遍历系统每个包,发现名字为springBeans.xml的配置文件就加载进来;
(2) 在系统的一个配置文件中保存这些springBeans.xml文件的位置,只要读取这个配置文件就可以知道所有springBeans.xml文件的位置了。
第一种方式简单灵活,开发人员可以在任意位置编写springBeans.xml文件,系统都可以加载到它们。缺点就是遍历所有的包需要一定的时间,会降低系统的初始化速度;文件名只能为springBeans.xml,而且系统中不能有用作其他用途的名称为“springBeans.xml”的文件。
第二种方式比较严谨,各个模块必须严格遵守在配置文件中声明的位置建立springBeans.xml文件,文件的名字也可以改变成其他的;配置文件的加载速度也会有所提高,从而加快系统的初始化速度;缺点就是每个模块都必须到统一的配置文件中注册,加大了工作量。
在大型的团队开发中,第二种方式比第一种方式拥有更多的优势,所以此处按照第二种思路实现。
在包“/com/cownew/PIS/framework/server/”下建立文件ServerConfig.xml:
<?xml version="1.0" encoding="UTF-8"?>
<Config>
<!--remoting文件的位置-->
<BeanFiles>
<File>/com/cownew/PIS/framework/server/springBeans.xml</File>
<File>/com/cownew/PIS/framework/server/springBeans2.xml</File>
</BeanFiles>
</Config>
在BeanFiles标记中定义的就是所有的springBeans.xml文件。
【例6.3】建立一个配置文件读取类。
为了读取这个文件,下面建立一个应用服务器端配置文件读取类:
// 应用服务器端配置文件读取器
package com.cownew.PIS.framework.server.helper;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.List;
import org.dom4j.Document;
import org.dom4j.io.SAXReader;
import org.dom4j.tree.DefaultElement;
import com.cownew.ctk.common.ExceptionUtils;
import com.cownew.ctk.common.StringUtils;
import com.cownew.ctk.constant.StringConst;
import com.cownew.ctk.io.ResourceUtils;
public class ServerConfig
{
private String[] beanFiles;
private static ServerConfig instance = null;
private ServerConfig()
{
super();
};
public static ServerConfig getInstance()
{
if (instance == null)
{
instance = new ServerConfig();
try
{
instance.initConfig();
} catch (Exception e)
{
ExceptionUtils.toRuntimeException(e);
}
}
return instance;
}
protected void initConfig() throws Exception
{
InputStream beansXFStream = null;
try
{
beansXFStream = getClass().getResourceAsStream(
"/com/cownew/PIS/framework/server/ServerConfig.xml");
SAXReader reader = new SAXReader();
reader.setValidation(false);
Document doc = reader.read(new InputStreamReader(beansXFStream,
StringConst.UTF8));
loadBeanFilesDef(doc);
} finally
{
ResourceUtils.close(beansXFStream);
}
}
/**
* Remoting定义文件
*/
public String[] getBeanFiles()
{
return beanFiles;
}
/**
* 加载remoting配置文件
*/
private void loadBeanFilesDef(Document doc)
{
List beanList = doc.selectNodes("//Config/BeanFiles/File");
beanFiles = new String[beanList.size()];
for (int i = 0, n = beanList.size(); i < n; i++)
{
DefaultElement beanElement = (DefaultElement) beanList.get(i);
beanFiles[i] = beanElement.getText();
}
}
}
配置文件的解析是一个比较费时的过程,所以在这里只是在ServerConfig实例化的时候调用initConfig进行配置文件的解析,并把解析后的结果保存在beanFiles数组中。为了防止调用者实例化此读取类,所以将此类设计成单例的,并且实现为惰性加载,通过getInstance返回这个单例。这样通过ServerConfig.getInstance().getBeanFiles()就可以得到所有配置文件的位置了。
在这里规定在ServerConfig.xml定义的配置文件位置必须是以类路径形式表示的,ClassPathXmlApplicationContext有一个支持字符串数组的构造函数,所以只要修改Bean工厂的实例化方式为:
appContext = new ClassPathXmlApplicationContext(ServerConfig
.getInstance().getBeanFiles());
就可以一次性加载所有的配置文件了。
6.3.2 本地服务加载器
目前阶段的本地服务加载器的实现是非常简单的,只要在适当的时候创建Bean工厂,并调用appContext的相应方法来取得相应的服务对象即可。
【例6.4】本地服务加载器。
代码如下:
// 本地服务加载器
public class LocalServiceLocator
{
private static LocalServiceLocator instance;
private LocalServiceLocator()
{
super();
};
private static BeanFactory appContext = null;
public static LocalServiceLocator getInstance()
{
if (instance == null)
{
instance = new LocalServiceLocator();
}
return instance;
}
public Object getService(Class serviceIntfClass) throws PISException
{
String serviceId = serviceIntfClass.getName();
Object bean = appContext.getBean(serviceId);
return bean;
}
static
{
appContext = new ClassPathXmlApplicationContext(ServerConfig
.getInstance().getBeanFiles());
}
}
将RemotingCallServlet的invokeBean方法中根据serviceId得到服务的代码替换为下面的方式:
Class serviceIntfClass = Class.forName(serviceId);
Object _service = LocalServiceLocator.getInstance()
.getService(serviceIntfClass);
6.4 Remoting Session实现
由于Http协议是不保持连接、无状态的,所以HttpInvoker、Hessian、Burlap、WebService等都是无状态的,系统无法分辨本次调用者是否是上次调用的那个客户端;EJB、Com+等支持状态的Remoting实现本质上也是无状态的,只是它们内置了Remoting Session机制而已。
无状态的Remoting服务从严格意义上来说是回归到了面向过程的时代,我们面对的是服务器提供的没有任何状态的“伪类”,业界专家之所以推荐使用无状态的Remoting服务,是考虑到如果服务是有状态的,那么状态信息就会迅速地将服务器内存占满,将会降低系统的吞吐量;无状态的Remoting服务更体现了服务的概念,超市收银员只是提供收银服务,无须在服务完成后记住每个客户买了多少东西、买了什么东西,否则收银员的脑子会爆炸掉的。
不过在某些时候调用者的状态还是有用的,超市的购物积分就是一个例子。收银员无须记忆客户的购买历史,也无须客户出示所有的购物小票,这些购买历史全部记录在POS机中,标识这个客户唯一性的就是会员卡的卡号。当用户付款的时候,POS机首先读取会员卡号,根据此卡号找到对应的客户记录,然后将本次的购买情况记录到此客户的名下。
在信息系统中同样需要类似的功能,比如在业务处理过程中服务常常需要知道当前所服务的客户端的操作人员是谁、用户名是什么、密码对不对、它是否有执行此操作的权限、它要连接哪个数据库。如果这些信息全部都在每次提供服务的时候要求客户端提供(比如每个服务端方法都增加传递这些信息的参数),那么这对于双方都会很麻烦,并且也是不安全的行为,让调用者无法理解:“不是上次告诉你了吗?怎么还问?”。
解决这个问题就需要借助于Session技术了。Session中文翻译为“会话”,其本来的含义是指有始有终的一系列交互动作。随着Web技术的发展,Session已经变成在无状态的协议中在服务器端保存客户端状态的解决方案的代名词了。
当客户端第一次登录的时候,服务器分配给此客户端一个标识其唯一性的Id号码,并且询问客户端“你是谁、用户名是什么、密码是什么、要连接哪个数据库”,根据这些信息服务器再到数据库中查询这个客户端有哪些权限、密码是否正确,然后将“它是谁、用户名是什么、要连接哪个数据库、有哪些权限”等信息以唯一性的Id号码为主键保存到特定的位置。以后客户端再向服务器请求服务的时候,只要提供此Id号码即可(类似于购物时提供会员卡),服务端就可以根据这个Id号码来取它需要的信息。
要注意此Remoting Session和Web中的Session的区别,它们的作用是类似的,不过这里的Remoting Session是不在乎调用的客户端是Swing GUI程序还是Web应用的。它是一个应用服务器端技术,在Web调用的时候常常需要把应用服务器分配的唯一的Id保存在Web Session中,这个问题在后边会有专门的论述。
6.4.1 实现思路
Session的实现方式如下:在用户第一次登录的时候,系统为它分配一个唯一Id(被称为Session Id)作为标识,并且记录下这个用户的用户名、要登录的账套名、用户拥有的权限等,以Id为键,用户名、账套名等信息为值保存到一张Session哈希表中。以后客户端登录的时候只要提供此Id即可,应用服务器可以通过此Id到Session哈希表中查询到所需要的一切信息。因为Session哈希表是保存在存储器中的(通常是内存),存储过多的Session信息将会占用内存空间,所以客户端退出的时候要通知应用服务器注销此Id。
具体到细节还有一些问题需要处理:
l 如何生成唯一的Id。
l 如何保存用户名、账套名等信息,如何能在Session中放入自定义的信息。
l 如何维护管理Session。
l 如何清除Session。当系统非正常退出的时候,比如客户端机器故障,客户端是无法通知应用服务器注销Id的,这会造成应用服务器中存在垃圾Session。
l 如何防止此Id被恶意程序截获,从而冒充合法客户端登录系统。
6.4.2 Session Id的生成
客户端Session Id的生成与数据库中的主键生成面对的问题是类似的。以可移植、高效率、可靠的方式来生成主键是一个非常重要的问题。可移植指的是主键生成策略不能依赖于服务器、操作系统、数据库等;高效率是生成主键的过程必须足够快,不能让生成主键的算法成为系统的瓶颈;可靠指的是生成的主键必须保证唯一性。主键生成方式可以分为数据库相关方式和数据库无关方式两种。
数据库相关方式是通过数据库的帮助来生成主键。对于支持自增字段的数据库,可以借助其序列号发生器来产生唯一的主键;对于不支持自增字段的数据库,可以在系统中放置一张表,采用此表记录本次生成的主键,这样就保证了生成的主键与以前的不冲突。数据库相关方式的优点是实现简单,而且可以完全保证生成主键的唯一性;不过由于需要数据库来维护主键的状态和同步对主键生成器的访问,所以对数据库有依赖性,而且由于需要访问数据库,其生成速度较慢。
数据无关方式是无须依靠数据库而生成主键的方式。最典型的算法就是UUID算法。UUID是一个字符串,它被编码为包含了使生成的UUID在整个空间和时间上都完全唯一的所必需的系统信息集,不管这个UUID是何时何地被生成的。原始的UUID规范是由Paul Leach和Rich Salz在网络工作组因特网草案中定义的。
UUID字符串一般由下面信息构成:
l 系统时钟的毫秒值。这是通过System.currentTimeMillis()方法得到的。这保证了在时间维度上产生主键的唯一性。
l 网络IP或者网卡标识。这保证了在集群环境中产生主键的唯一性。
l 精确到在一个JVM内部的对象的唯一。通常是System.identityHashCode(this)所调用产生的编码,这个方法调用保证对JVM中不同对象返回不同的整数。即使在同一台机器上存在多个JVM,两个UUID生成器返回相同的UUID的情况也极不可能发生。
l 在一个对象内的毫秒级的唯一。这是由与每一个方法调用所对应的随机整数,这个随机整数是通过使用java.security.SecureRandom类生成的。这可以保证在同一毫秒内对同一个方法的多个调用都是唯一的。
上述这些部分组合在一起,就可以保证生成的UUID在所有机器中(IP不重复或者网卡地址不重复),以及在同一台机器上的JVM内部的所有UUID生成器的实例中都保持唯一,并且能精确到毫秒级甚至是一个毫秒内的单个方法调用的级别。
流行的UUID算法有很多,这些算法有的不能完全保证生成的UUID的唯一性,必须根据情况选用。下面推荐两种UUID实现算法。
【例6.5】UUID.Hex算法。
这个算法是Hibernate中主键策略为“uuid.hex”时所使用的算法,代码位于包org.hibernate.id下的UUIDHexGenerator.java文件中。
调用方法:
IdentifierGenerator gen = new UUIDHexGenerator();
for (int i = 0; i < 10; i++)
{
String id = (String) gen.generate(null, null);
System.out.println(id);
}
运行结果(UUID的生成是不重复的,每次的运行结果都会不同):
ff8080810ef0779f010ef0779f500000
ff8080810ef0779f010ef0779f500001
ff8080810ef0779f010ef0779f500002
ff8080810ef0779f010ef0779f500003
ff8080810ef0779f010ef0779f500004
ff8080810ef0779f010ef0779f500005
ff8080810ef0779f010ef0779f500006
ff8080810ef0779f010ef0779f500007
ff8080810ef0779f010ef0779f500008
ff8080810ef0779f010ef0779f500009
这个算法的特点是生成的UUID序列具有顺序性,因此生成的UUID具有一定的可预测性。前边的部分采用的是系统时钟、网络地址等拼凑的,而最后的有序部分采用的是内部维持一个同步了的计数器,每次生成UUID此计数器增加1,所以并发性能稍差。
【例6.6】Marc A. Mnich的算法。
代码如下:
// 随机GUID生成器
public class RandomGUID
{
public String valueBeforeMD5 = "";
public String valueAfterMD5 = "";
private static Random myRand;
private static SecureRandom mySecureRand;
private static String s_id;
static
{
mySecureRand = new SecureRandom();
long secureInitializer = mySecureRand.nextLong();
myRand = new Random(secureInitializer);
try
{
s_id = InetAddress.getLocalHost().toString();
} catch (UnknownHostException e)
{
e.printStackTrace();
}
}
public RandomGUID()
{
getRandomGUID(false);
}
public RandomGUID(boolean secure)
{
getRandomGUID(secure);
}
private void getRandomGUID(boolean secure)
{
MessageDigest md5 = null;
StringBuffer sbValueBeforeMD5 = new StringBuffer();
try
{
md5 = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e)
{
throw ExceptionUtils.toRuntimeException(e);
}
try
{
long time = System.currentTimeMillis();
long rand = 0;
if (secure)
{
rand = mySecureRand.nextLong();
} else
{
rand = myRand.nextLong();
}
sbValueBeforeMD5.append(s_id);
sbValueBeforeMD5.append(":");
sbValueBeforeMD5.append(Long.toString(time));
sbValueBeforeMD5.append(":");
sbValueBeforeMD5.append(Long.toString(rand));
valueBeforeMD5 = sbValueBeforeMD5.toString();
md5.update(valueBeforeMD5.getBytes());
byte[] array = md5.digest();
StringBuffer sb = new StringBuffer();
for (int j = 0; j < array.length; ++j)
{
int b = array[j] & 0xFF;
if (b < 0x10)
sb.append('0');
sb.append(Integer.toHexString(b));
}
valueAfterMD5 = sb.toString();
} catch (Exception e)
{
e.printStackTrace();
}
}
public String toString()
{
String raw = valueAfterMD5.toUpperCase();
StringBuffer sb = new StringBuffer();
sb.append(raw.substring(0, 8));
sb.append("-");
sb.append(raw.substring(8, 12));
sb.append("-");
sb.append(raw.substring(12, 16));
sb.append("-");
sb.append(raw.substring(16, 20));
sb.append("-");
sb.append(raw.substring(20));
return sb.toString().trim();
}
}
测试代码:
for(int i=0;i<10;i++)
{
System.out.println(new RandomGUID().toString());
}
运行结果:
B2FAA7E0-5E4**0D5-4757-C4C91A6A8F8E
7B8E3A34-B173-AC54-8F3D-8CAF48CECD13
62380599-EFA4-0AEF-8E03-A49018308D92
F781C6B5-55ED-D553-D1F7-43C573A00AB4
19FE1D7F-41A0-EB71-E149-9FEAD2B746C7
A0C334EA-0C31-E4C8-B7B6-64F2A4A9C35A
ED9329E2-64D2-E3D7-88FC-7EC03FA0AA54
7285B963-2BBE-45FE-A074-7CA20B496D04
F0085927-12B2-BE6C-1217-281B470E1282
9AA63E7A-61C9-2CB9-46C1-3E07B60952D1
RandomGUID算法和其他算法一样采用系统时钟、网络地址等来产生唯一编码,唯一不同的地方就是RandomGUID算法生成的UUID是随机的,由于使用SecureRandom产生随机数,所以其安全性非常高。此算法除了能快速、可移植地生成可靠的UUID之外,其最大的优势就是无法根据以前生成的UUID来推算后边要生成什么样的UUID。这在有的场合是非常有用的,比如在生成对安全性要求比较高的数据表的主键的时候,不希望有恶意企图的人能够猜测出后续要生成的UUID。这对这里要生成的客户端唯一标识也是有意义的,这个客户端唯一标识是应该只有应用服务器和相应的客户端才需要知道的,如果这个唯一标识能被猜测的话,就会对系统安全造成隐患(比如恶意清除Session等)。
基于安全和效率的考虑,我们选择RandomGUID算法作为客户唯一标识生成算法,同时案例系统中其他需要生成主键的地方也全部使用RandomGUID算法。
6.4.3 用户信息的保存
目前要保存在应用服务器端Session的信息有登录用户Id、账套名、sessionId等,并且允许把自定义的一些信息放入Session中,以实现一些特殊的功能。
【例6.7】用户信息的保存。
写一个简单的服务器端用户上下文JavaBean即可储存这些信息。代码如下:
// 服务器端用户上下文
public class ServerUserContext implements Serializable
{
private String sessionId;
//账套名
private String acName;
//登录用户id
private String curUserId;
//存储用户自定义信息用的哈希表
private Hashtable userDefAttributes = new Hashtable();
public String getACName()
{
return acName;
}
public void setACName(String acName)
{
this.acName = acName;
}
public String getSessionId()
{
return sessionId;
}
public void setSessionId(String sessionId)
{
this.sessionId = sessionId;
}
public String getCurUserId()
{
return curUserId;
}
public void setCurUserId(String curUserId)
{
this.curUserId = curUserId;
}
//得到名称为name的用户自定义信息
public Object getUserDefAttribute(String name)
{
return userDefAttributes.get(name);
}
//得到所有的用户自定义信息
public Enumeration getUserDefAttributeNames()
{
return userDefAttributes.keys();
}
//移除名称为name的用户自定义信息
public void removeUserDefAttribute(String name)
{
userDefAttributes.remove(name);
}
//在用户自定义信息中加入名称为name,值为value的用户自定义信息
public void setUserDefAttribute(String name, Object value)
{
userDefAttributes.put(name, value);
}
}
和Web中的Session一样,此处的Session中的自定义用户信息功能只是供一些极特殊的用途使用的,不应该使用此功能使得Remoting 服务变成了有状态的。比如,在A服务中将一些逻辑信息存入Session,然后到B服务中取出使用,这违反了分布式开发的无状态原则,很容易导致数据混乱并使系统状态控制变得复杂。即使确实有特殊需要向Session中放置自定义信息的,也要尽量避免放置大对象或者过多的对象进去,以免过多地占用宝贵的应用服务器内存资源。当某个问题要通过向Session中放置自定义信息的时候,要首先思考能否改用其他更好的方式解决,向Session中放置自定义信息是下下策。
6.4.4 维护管理Session
在Session的原理中曾经提到,Session通常是应用服务器中的一张哈希表。在系统中增加一个Session哈希表,这个表以Session的Id(即前边说的客户端唯一标识)为键,以ServerUserContext 的对象为值。其他的管理操作自然而然也就全部围绕此哈希表来完成,
【例6.8】Session的维护管理。
编写如下的Session管理器来进行Session的维护管理:
// Session管理器
public class SessionManager
{
private static SessionManager instance = null;
//sessionId为key,ServerUserContext为value
private Map sessionMap = Collections.synchronizedMap(new HashMap());
private SessionManager()
{
super();
}
public static SessionManager getInstance()
{
if (instance == null)
{
instance = new SessionManager();
}
return instance;
}
/**
* 根据会话id得到用户上下文
* @param sessionId
* @return
*/
public ServerUserContext getServerUserContext(String sessionId)
{
return (ServerUserContext) sessionMap.get(sessionId);
}
/**
* 得到所有会话id的集合
* @return
*/
public Set getSessionIdSet()
{
return Collections.unmodifiableSet(sessionMap.entrySet());
}
/**
* sessionId是否合法
* @param sessionId
* @return
*/
public boolean isValid(String sessionId)
{
return sessionMap.containsKey(sessionId);
}
/**
* 清除session
* @param sessionId
*/
public void removeSession(String sessionId)
{
sessionMap.remove(sessionId);
}
/**
* 清除所有session
*
*/
public void removeAll()
{
sessionMap.clear();
}
/**
* 根据账套名请求一个会话Id
* @param acName
* @return
*/
public String requestSessionId(String acName)
{
String sessionId = new RandomGUID().toString();
ServerUserContext ctx = new ServerUserContext();
ctx.setACName(acName);
ctx.setSessionId(sessionId);
sessionMap.put(sessionId, ctx);
return sessionId;
}
}
Session管理器在一个应用服务器内只存在一个实例,所以采用单例模式。客户端第一次登录的时候以要登录的账套名为参数调用requestSessionId方法得到唯一的Session Id;以后可以SessionId为参数调用getServerUserContext方法,这样就可以得到此Session的ServerUserContext信息;退出的时候以SessionId为参数调用removeSession方法以清除对应的Session。
6.4.5 Session的注销
由于Session中保存着用户名、权限、账套名称等信息,占据一定的应用服务器内存,随着登录系统的人次的增多,Session占用的存储空间也会变大。为了及时清除无用的Session,可以要求调用者在退出时调用SessionManager的removeSession方法来清除其对应的Session。
客户端有可能意外终止或者客户端和应用服务器的连接意外断开,这时客户端在应用服务器中的Session就会永远无法清除了。解决这个问题最直接的办法就是建立Session超时机制:某个Session对应的客户端如果在一定时间后还没有活动的话,就将此客户端对应的Session从应用服务器清除,这也是Web Session处理这类问题的策略。
超时时长的设置最好能够配置,这样方便实施人员或者客户根据情况进行修改以便调优,为此在ServerConfig.xml中增加下面的配置项:
<SessionTimeOut>3</SessionTimeOut>
这个配置表示当客户端3分钟之后还没有活动的话就将其对应的Session清除。在ServerConfig.java中增加读取此配置项的代码,并增加getSessionTimeOut()方法以读取配置项的值。
接着在SessionManager中增加一个私有变量Map,用来记录Session的活动信息:
private Map sessionActiveMap = Collections.synchronizedMap(new HashMap());
sessionActiveMap以sessionId为键,以会话自上次活动以来的时间(分钟)为值。
然后为SessionManager增加一个公共方法用来供外界调用,表示某Session产生活动了:
public void sessionVisit(String sessionId)
{
if (!sessionMap.containsKey(sessionId))
{
return;
}
sessionActiveMap.put(sessionId, new Integer(0));
}
当sessionVisit被调用以后,就重置此Session的未活动时间为0。那么谁来调用此方法呢?我们将会在讲解SessionServiceLifeListener类的时候介绍,此处可以认为只要客户端调用应用服务器的方法的时候sessionVisit方法就会被自动调用。
剩下的问题就是实现定时清除超时Session了。要实现这个功能,首先要设置定时任务,以使定时清除超时Session的任务每隔一段时间运行一次。设置定时任务的方式有很多种,比如Quartz就是一个非常优秀的定时任务工具。对于这个应用,使用Quartz就有点大材小用了,最方便、高效的实现方式就是使用java.util.Timer类。
Timer类的使用是非常简单的,比如下面的代码就实现了每两秒钟打印一次的功能:
package com.cownew.Char11.Sec04;
import java.util.Timer;
public class TimerTest
{
public static void main(String[] args)
{
Timer timer = new Timer(false);
timer.schedule(new java.util.TimerTask() {
public void run()
{
System.out.println("hello");
}
}, 0, 2 * 1000);
System.out.println("program end!");
}
}
使用Timer类的时候有如下几点需要特别注意:
l Timer类有一个参数为“boolean isDaemon”的构造函数,此处的isDaemon表示执行定时器任务的线程是否是后台线程。如果是后台线程,则主程序终止的时候后台线程也就终止了;如果不是后台线程,除非这个Timer任务停止,否则主程序无法停止。可以看到TimerTest类运行的时候“program end!”已经打印出来了,可程序仍然没有终止,这是因为timer已经被设置为后台线程,而timer是无限次循环执行的,所以程序就无法正常终止了。
l schedule中的时间参数是以毫秒为单位的。
l Timer中是采用Object.wait(long time)来实现定时的,由于Object.wait()不保证精确计时,所以Timer也不是一个精确的时钟。如果是实时系统,不能依赖Timer。
【例6.9】Session超时清理任务。
编写一个从TimerTask继承的SessionCleanerTimerTask类作为SessionManager的内部类,实现TimerTask的run方法,在run方法中进行Session超时的检测及处理:
// Session超时清理任务
protected class SessionCleanerTimerTask extends TimerTask
{
private int timeOut = ServerConfig.getInstance().getSessionTimeOut();
public void run()
{
Set idSet = sessionActiveMap.keySet();
Iterator idIt = idSet.iterator();
// 已经失效的Session的Id列表
List invalidIdList = new ArrayList();
while (idIt.hasNext())
{
String id = (String) idIt.next();
// 自上次访问以来的时长,即未活动时间
Integer lastSpan = (Integer) sessionActiveMap.get(id);
if (lastSpan.intValue() > timeOut)
{
invalidIdList.add(id);
}
//Session的未活动增加一分钟
sessionActiveMap.put(id, new Integer(lastSpan.intValue() + 1));
}
//清除超时的Session
for (int i = 0, n = invalidIdList.size(); i < n; i++)
{
String id = (String) invalidIdList.get(i);
removeSession(id);
sessionActiveMap.remove(id);
}
}
}
SessionCleanerTimerTask 是SessionManager的内部类,能够访问sessionActiveMap并调用removeSession等方法。此任务每隔一段时间对所有的Session进行扫描,拣出超时的Session,然后把所有Session的未活动时间增加一分钟,处理完毕后统一清除超时的Session。
为了使此任务与SessionManager一起启动,需要把任务的部署(schedule)工作放到SessionManager的构造函数中:
private SessionManager()
{
super();
//要设置成后台线程,否则会造成服务器无法正常关闭
Timer sessionClearTimer = new Timer(true);
//ONE_MINUTE是CTK的DateUtils中定义的常量,表示一分钟
//ONE_MINUTE = 60000;
int oneMin = DateUtils.ONE_MINUTE;
//1分钟以后开始,每隔一分钟探测一次
sessionClearTimer.schedule(new
SessionCleanerTimerTask(),oneMin,oneMin);
}
6.4.6 安全问题
Session机制可以防止恶意攻击者跳过登录模块直接调用服务端方法,因为对系统安全有影响的操作(查询数据、修改删除数据)都必须通过SessionId才能得到数据库连接,由于恶意攻击者得不到一个正确的SessionId,所以就无法正确调用这些方法。
Session采用了RandomGUID来防止恶意攻击者通过猜测SessionId的方式来冒充合法的用户进入系统,但这不足以防范恶意攻击者。恶意攻击者可以截获客户端发往应用服务器的数据包并从数据包中分析出SessionId,这样恶意攻击者就可以采用此SessionId来冒充合法的用户进行系统的操作了。对此进行防范的比较好的方法就是采用SSL连接,SSL连接会对客户端和应用服务器之间的数据交换过程进行加密及数字签名,恶意攻击者根本无法正确地截获数据,商业系统目前大都采用此种方式保证数据的安全,比如网上银行、银企平台等。
为了在不安全的网络上安全保密地传输关键信息,Netscape公司开发了SSL协议,后来IETF(Internet Engineering Task Force)把它标准化了,并且取名为TLS,目前TLS的版本为1.0,TLS 1.0的完整版本请参考rfc2246(www.ietf.org)。
基于TLS协议的通信双方的应用数据是经过加密后传输的,应用数据的加密采用了对称密钥加密方式,通信双方通过TLS握手协议来获得对称密钥。为了不让攻击者偷听、篡改或者伪造消息,通信的双方需要互相认证,来确认对方确实是其所声称的主体。TLS握手协议通过互相发送证书来认证对方,一般来说只需要单向认证,即客户端能确认服务器便可。但是对于对安全性要求很高的应用往往需要双向认证,以获得更高的安全性。
可以向可信的第三方认证机构(CA)申请证书,也可以自己做CA,由自己来颁发证书。如果自己做证书颁发机构,可以使用Openssl,Openssl是能用来产生CA证书、证书签名的软件,可以在其官方网站http://www.openssl.org下载最新版本。使用的时候要同时生成服务器端证书和颁发并发布个人证书。服务器端证书用来向客户端证明服务器的身份,也就是说在SSL协议握手的时候,服务器发给客户端的证书。个人证书用来向服务器证明个人的身份,即在SSL协议握手的时候,客户端发给服务器端的证书。
MDA(Model Driven Architecture)是由OMG定义的一个软件开发框架。它是一种基于UML以及其他工业标准的框架,支持软件设计和模型的可视化、存储和交换。MDA能够创建出机器可读和高度抽象的模型,这些模型独立于实现技术,以标准化的方式储存。MDA提供了一种途径来规范化一个平台独立的系统,为系统选择一个特定的实现平台,并且把系统规范转换到特定的实现平台。本章将对MDA的主要概念做一下介绍,然后开发一个体现MDA思想的元数据引擎。
7.1 MDA 概述
随着信息技术的发展,一大批信息系统,如客户关系管理系统、自动办公系统、资金管理系统等被开发出来,对提高管理效率起到了重要的作用。但是在这些系统的开发和后续的扩展过程中存在很多长期无法解决的难题,最突出的问题就是设计与实现不一致。
大多数的信息系统没有分离的定义模型。软件开发虽然有建模过程,但是很多模型仅仅仅在开发者脑中闪现,然后就消失了。开发人员经常使用单独代码的方法,依靠他们编写的代码表示他们正在建立的系统模型。他们所做的任何“建模”都是以嵌入在代码中的编程的抽象形式进行的,这些方式是通过程序库和对象层次的机制进行管理的,系统的可重用性差。
随着软件工程学的发展,越来越多的系统在开发的时候开始注意开发前的系统建模过程,而且开发人员会按着最初的设计模型进行开发。软件的需求是一直在变的,客户会源源不断地要求提供各种新的功能,开发人员对付这些新需求的手段就是修改代码。随着时间的推移,系统不断地被修改,设计模型和代码之间的距离就越来越远。修改设计模型并不会对系统有任何影响,直接修改代码就可以达到目的,所以很少有人去做设计模型与代码的同步工作。即使我们修改了设计模型,这样的工作是否有效也值得怀疑,因为我们还会不断地修改代码,难道我们要花更多的时间去不断修改设计模型吗?算了吧,设计模型不改也罢,有代码就行了。
当开发团队发生**的时候,来维护这个系统的人可能是一个新人,那么他面对的就只有一堆已经过时的设计文档和天书一般的代码,这使得系统维护极其困难。
在很多团队中,详细设计是由经验丰富的开发人员完成的,他们会根据客户的需求设计出有哪些实体对象、实体对象有哪些字段、字段的类型是什么、实体对象之间的关系是什么、实体对象对应的数据库表是什么等。开发人员拿到这个设计文档以后开始按着设计文档一步步机械地实现:设计文档说有Employee这个对象代表员工,因此我就在数据库中创建一个员工表T_Employee,建立一个EmployeeInfo JavaBean,并创建EmployeeInfo.hbm.xml配置文件;设计文档说Employee有一个类型为int、名称为age的字段代表年龄,因此我就在T_Employee表中增加一个名称为FAge的int类型字段,在EmployeeInfo 中增加age属性,并在EmployeeInfo.hbm.xml中加上age属性的映射配置……,最后还要同样机械地创建“员工”的管理界面。开发人员心里一定在抱怨:我真的是软件蓝领呀,只能做这些机械的工作!既然设计人员已经设计出来了这些模型,为什么不能把模型直接转换为数据库表、JavaBean、配置文件、维护界面呢?
为了解决这个机械性地将设计文档转化成代码的重复性劳动问题,出现了很多代码生成器。代码生成器分为两类:
l 无源的代码生成器。这些生成器一般提供了一些向导,只要在它的向导页中设置相应的参数,比如实体对象的名字是什么、有哪些字段、生成的ORM目标产品是什么,填写完毕以后就可以生成需要的代码了。这类代码生成器的优点是非常简便,使用之前无须做准备工作。缺点是过程数据无法重复利用,如果想增加一个字段的话,就必须重复上次的工作,一个字段一个字段地重新添加,如果改动不大的话大部分开发人员都是直接去修改生成的代码。这样的生成器仅仅是一个一次性用品。
l 有源的代码生成器。这些代码生成器能够利用现有的一些信息来加速代码生成的过程。比如HibernateTools、Middlegen就可以根据现有数据库表来生成代码和配置文件,Rose可以根据UML图生成Java代码。这类代码生成器能够重复利用原有的工作成果,缺点是指导思想不明确,仅仅是一个代码生成器而已。比如HibernateTool暗示开发人员要先建立数据库表,这其实是一种数据驱动的开发模式,使得开发人员先要去思考数据是如何存储的、有哪些字段,而不是先思考系统对象之间的关系是怎么样的。这类代码生成器的“源”具有随意性,可以是一张数据库表、可以是代码中的JavaDoc,“源”只是为代码生成存在的,代码生成以后就没有了任何作用。
OMG 在 2001 年7月发布了模型驱动体系结构(Model-Driven Architecture,MDA),确定了以模型驱动体系结构代替对象管理体系结构(OMA)作为对象管理联盟未来的发展方向。
MDA通过使用软件工程方法和工具,为分析、理解、设计、实现、维护、发展以及集成原有信息系统提供了方法。在MDA中,模型不再仅仅是描述系统和辅助沟通的工具,而是软件开发的核心和主要媒介。MDA采用标准模型表述方法和标准建模方法来详细描述信息系统,从业务需求描述、系统功能和体系结构设计、包含平台技术细节的系统实现等3 个层次,MDA 都给出了相应的描述模型。
(1) 计算无关模型(Computation Independent Model,CIM),在系统需求分析阶段从纯业务角度描述系统要完成的工作。
(2) 平台无关模型(Platform Independent Model,PIM),从功能设计角度描述系统的体系结构,与技术细节无关。
(3) 平台特定模型(Platform Specific Model,PSM),描述基于特定平台的解决方案。
(4) 实现相关模型(Implementation Specific Model,ISM),面向最后的编程描述系统的实现细节。
MDA是一种用于构建系统应用架构的新方法,它的最大特点是通过定义一种与具体实现技术或平台无关的应用系统规范,将系统的功能描述与基于具体平台的实现描述分离开来。建模和模型映射技术是MDA的核心,对系统的不同方面进行不同抽象水平的建模,模型之间通过模型映射机制实现模型映射,保证了模型的可追溯性。
运用MDA开发系统,开发人员可以获得最大限度的灵活性。当底层基础设施随时间发生变化或系统功能需要扩展时,开发人员能够从稳定的、平台独立的模型重新生成代码,而不必重新构建系统。模型的一些信息可以通过运行时取得,这样使得模型在系统开发中发挥了更大的作用。
MDA解决的是从需求收集到系统设计、从详细设计到系统开发、从产品测试到产品实施等产品全生命周期的问题。本书无法去探讨MDA在整个生命周期中发挥的作用,案例中最能体现MDA思想的就是元数据机制。
7.2 关于元数据
系统开发中存在各种各样的数据,比如Tom是一个年龄为30岁的男性员工、Liliy是一个21岁的女性员工、这张报表是今年第三季度的利润表、那张报表是今年上半年的销售波动图、对话框上有三个按钮控件、窗口上有一个多行文本控件和一个保存按钮、这个WebService提供了股票实时情况查询的服务、那个WebService提供了查询天气预报的服务。
以上数据存在很多共性的特征,这些特性都可以通过某种形式进行抽象。
对于“Tom是一个年龄为30岁的男性员工”、“Liliy是一个21岁的女性员工”,在数据库级别就会抽象成含有FId Varchar(50)、FName Varchar(50)、FAge(int)、FSex(int)四个字段的数据库表T_Employee,在Hibernate中就被抽象成含有id、name、age、sex四个字段的JavaBean以及对应的hbm配置文件。
这些数据是平台无关的,在描述“Tom是一个年龄为30岁的男性员工”这条数据的时候,它即可以是保存在数据库中的,也可以是保存在XML配置文件中的,甚至有可能只是写在一张便条上的。与此相反的是,对这些数据的抽象方式大都是与特定平台相关的,是无法移植的。比如要把数据的存储方式由数据库改为XML文档,那么就必须针对XML文件的存取特点重新进行抽象。由于抽象方式是平台相关的,这些抽象出来的模型就不具有通用性,无法通过统一的方式来读取它们。比如要读懂T_Employee这张表中的字段的含义就要去查阅数据字典,要读懂便条上的“Tom 30 m”就要去询问写便条的人。
元数据(MetaData)是MDA中非常重要的概念。它通过统一的、平台无关的、规范的方式对数据的模式特征进行描述,通过一个模型结构来表达通用的信息,它集设计模型、开发模型与运行模型为一体。元数据具有如下几个作用。
(1) 元数据是独立于平台的,无论使用什么技术平台,元数据本身是不受影响的,这保证了先期工作成果的效用最大化。
(2) 元数据是生成平台相关模型的基础,可以使用代码生成器等工具将元数据转换成平台相关代码。
(3) 元数据为运行时系统提供了统一的可读的系统模型,系统运行时可以使得实体对象通过运行时元数据模型来得知自身的结构、自身的特征、在系统模型中的位置以及与其他对象之间的关系等。这样就可以从一个新的角度来观察、设计、开发系统。
(4) 元数据模型是系统运行不可或缺的部分,如果直接修改平台相关代码而不修改元数据,就会造成系统运行异常,这就强迫保证元数据模型与代码同步,保证了设计模型和实现代码的一致性。
(5) 元数据本身就是一个设计模型。系统设计人员可以使用元数据进行系统建模,在某种程度上元数据可以取代UML图等传统的设计模型。设计人员将设计完成的元数据模型交给开发人员,开发人员使用代码生成器将元数据转换成平台相关代码,然后就可以基于这些平台相关代码进行开发了。元数据起到了设计人员和开发人员沟通桥梁的作用,设计人员的工作立即就可以转换为可以运行的平台相关代码。
7.2.1 元数据示例
枚举类型在不同的系统中有不同的表示方式,而且有不同的模型描述方式(即枚举有哪些项、项的值是多少等信息),有的平台还没有提供足够的模型描述方式。客户类型包括:普通客户、会员客户、VIP客户。
在JDK 1.5中可以表示为enum CustomerTypeEnum{Normal, Member, VIP},取得CustomerTypeEnum枚举类型中定义的所有枚举项的方法为CustomerTypeEnum.values(),取得“Normal”这个字符串对应的枚举项的方法为Enum.valueOf(CustomerTypeEnum.class, "Normal")。
在JDK 1.4中使用Apache Commons包提供的Enum类可以表示为:
public class CustomerTypeEnum extends org.apache.commons.lang.enums.Enum
{
public static DataTypeEnum Normal= new DataTypeEnum("Normal");
public static DataTypeEnum Member= new DataTypeEnum("Member");
public static DataTypeEnum VIP= new DataTypeEnum("VIP");
private DataTypeEnum(String name)
{
super(name);
}
}
取得CustomerTypeEnum枚举类型中定义的所有枚举项的方法为EnumUtils.get- EnumList(CustomerTypeEnum.class),取得“Normal”这个字符串对应的枚举项的方法为EnumUtils.getEnum(CustomerTypeEnum.class, "Normal")。
在C#中,可以表示为enum CustomerTypeEnum{Normal, Member, VIP},取得Customer- TypeEnum枚举类型中定义的所有枚举项的方法为Enum.GetNames(typeof(CustomerTypeEnum)),取得“Normal”这个字符串对应的枚举项的方法为Enum.Parse(typeof(CustomerTypeEnum), "Normal")。
在Delphi中,可以表示为type CustomerTypeEnum=(Normal, Member, VIP);没有提供取得CustomerTypeEnum枚举类型中定义的所有枚举项的方法,取得“Normal”这个字符串对应的枚举项的方法也没有直接提供,必须借助RTTI。
要将一个平台上的CustomerTypeEnum移植到另一个平台,必须用目标平台的枚举语法重新改写,而且使用的取得枚举类描述信息的方式也要发生变化,这都给系统的移植带来了很大的工作量。
【例7.1】元数据示例。
为了解决这个问题,我们设计一个元数据模型:
<Enum>
<Name>CustomerTypeEnum</Name>
<Items>
<Item name="Normal" displayName="普通客户"></Item>
<Item name="Member" displayName="会员客户"></Item>
<Item name="VIP" displayName="VIP客户"></Item>
</Items>
</Enum>
提供一个描述这个元数据模型的描述类:
//枚举描述类
public class EnumInfo
{
…
//得到所有的枚举项
public EnumItemInfo[] getEnumItems();
//得到名字为name的枚举项的信息
public EnumItemInfo getEnumItem(String name);
}
//枚举项描述类
public class EnumItemInfo()
{
…
//枚举项的名字
public String getName();
//枚举项的显示信息
public String getDisplayName();
}
提供一个读取元数据模型的API:
public class EnumMetaDataLoader
{
…
//加载元数据类型enumTypeName对应的元数据模型
public EnumInfo loadEnum(String enumTypeName)
{
…
}
}
枚举元数据模型的描述类和读取元数据模型的API的实现代码仍然是平台相关的,因为这些类都是要被特定平台使用的。因为XML解析在各个平台是大同小异的,所以这些描述类和API的实现方式的移植是非常简单的。
使用这样的元数据模型我们还可以定义其他的枚举类型,比如:
<Enum>
<Name>SexEnum</Name>
<Items>
<Item name="Male" displayName="男"></Item>
<Item name="Female" displayName="女"></Item>
</Items>
</Enum>
在JDK 1.4平台下,使用代码生成器将SexEnum的元数据模型转换成JDK 1.4下的枚举代码:
public class SexEnum extends org.apache.commons.lang.enums.Enum
{
public static SexEnum Male= new DataTypeEnum("Male");
public static SexEnum Female= new DataTypeEnum("Female");
private SexEnum String name)
{
super(name);
}
}
当要得到所有SexEnum 定义的枚举项的时候,按如下方式调用:
EnumInfo enumInfo = EnumMetaDataLoader.getInstance().loadEnum("SexEnum");
EnumItemInfo[] itemInfos = enumInfo.getgetEnumItems();
for(int i=0,n=itemInfos.length;i<n;i++)
{
EnumItemInfo itemInfo = itemInfos[i];
System.out.println("项名称:"+itemInfo.getName()
+";显示名称:"+getDisplayName());
}
这样我们不用再去调用特定平台的API实现了,元数据信息提供了比平台API更多的功能,并且写出的代码不会受平台API的限制。
若某天客户提出要增加一种“不详”的性别类别,如果开发人员直接修改生成的SexEnum 类,在其中加入“不详”的性别类别的枚举定义的话,系统就会工作不正常,因为没有修改SexEnum 元数据。这样就限制了开发人员直接修改SexEnum 类,这样开发人员只能去修改SexEnum 元数据,然后用代码生成器来重新生成SexEnum 类代码。这规范了开发人员的行为,保证了设计模型与实现代码的一致性。
若某一天,公司决定将开发平台从Java迁移到C#,那么对于枚举这部分需要做的改造工作就是用C#重写元数据模型描述类和元数据读取API,并开发一个针对C#枚举的元数据转换器,系统所有的枚举就都可以自动转换成C#下的了。这保证了前期对于枚举元数据模型的设计开发成本利用的最大化。
由于枚举在各个平台之间差异并不算大,而且一个平台整体从Java迁移到C#的可能性也非常小,所以元数据在这里起到的作用并不大。但是在Java平台上ORM工具的迁移倒是很有可能的,要想体会元数据的更重要的作用就要看案例系统的实体元数据了。
7.2.2 元元数据
元数据是对数据共性的抽象,而不同的元数据本身也是具有共性的,以上一节的两个枚举元数据来说,客户类型枚举元数据与性别枚举元数据为共同的模式:
<Enum>
<Name>枚举名称</Name>
<Items>
<Item name="名称" displayName="显示名称"></Item>
…
</Items>
</Enum>
可以定义一种模型来描述所有枚举元数据的共性特征,也就是枚举元数据的元数据(Metadata of metadata)。这种对元数据进行抽象描述的形式被称为元元数据(MetaMetaData)。
7.2.3 设计时与运行时
元数据的直接表示形式被称为设计时元数据,而在运行的时候能被系统读取的形式(比如上边的EnumInfo)被称为运行时元数据。通常,运行时元数据描述的特性是设计时元数据的特性的子集。
系统承担着设计模型与运行时模型的多重责任,而且元数据还作为代码生成器的“源”,承载着描述目标代码的作用。这些责任之间有相交的部分,也有自己独特的部分。举例来说,一个描述实体对象的元数据,它描述这个实体对象有哪些字段、字段的类型是什么、和其他实体对象之间有什么关系等信息,而作为代码生成器的“源”,它还要描述一些目标平台特有的东西,比如当目标平台为Hibernate的时候,就需要指定主键字段的生成策略、关联字段的LazyLoad策略、Casade策略等。从严格意义上来讲,为了维持元数据的平台无关性,这些平台相关的特性是不能放在元数据中的,而应该放在一个描述平台相关属性的地方,不过这样就使得元数据模型过于复杂。一个较好的策略是在元数据中增加一个专门存放这些平台相关属性的区域。
运行时的元数据是要被平台相关代码访问的,如果运行时元数据中包含平台相关特性的话,就会导致以后平台移植难度加大,而且也混淆了设计时语义与运行时语义之间的界限。所以运行时的元数据中一定不能包含平台相关特性。
7.2.4 元数据设计的基本原则
除了上边提到的运行时的元数据中一定不能包含平台相关特性之外,在元数据的设计中,“适可而止”也是需要铭记在心的核心原则。对元数据描述的范围要适可而止,不要试图包罗万象。运行时元数据是能够给运行时的系统提供元数据的信息的,这在一定程度上简化了系统的开发,但是切不可把应该写在代码中或者写到配置文件中的信息写到元数据中。比如在实体对象元数据中,给字段增加了“allowNull”特性来表示此字段是否允许为空。系统保存实体对象的时候,可以读取此实体对象对应的元数据,进而取得所有字段的是否为空的特性,从而对数据进行校验。这是对运行时元数据非常合理的运用。但是如果试图把字段为空时提示什么样的信息、字段最大长度是多少、字段是否进行加密操作等特性加入元数据的话就会使得元数据模型过于庞大,这也违反了“适可而止”这一基本原则。如果元数据直接驱动系统的运行过程,并且有取代程序代码的趋势的话,就说明设计人员对元数据概念理解错误了,用元数据驱动系统运行虽然减少了代码的编写,但是这些本不应该放在元数据中的特性是不完备的,一旦需要扩展就会遇到难以逾越的鸿沟。
由于客户需求的复杂性,模型结构不能表达出所有业务的处理过程,仍然存在需要利用编程语言才能完成的业务功能。元数据模型解决大多数通用的问题,而对于具有差异性的问题还是要通过编码来完成的,不应该让运行时元数据承担过多的运行时语义。
7.2.5 此“元数据”非彼“元数据”
元数据这个词汇并不是MDA发明的,在其他领域“元数据”早已经被使用了,在软件开发领域,“元数据”也不是MDA中才有的。
JDK 5.0的annotation机制也被称为元数据,它为属性的物理驻留位置提供了新的选择。annotation使得代码具有自解释的能力,代码变成能同时提供行为及自我描述能力的实体,也就是说代码从一维变成二维的了。使用JDK提供的API就可以从代码中读取到这些描述信息。
/**
*@ filedName = "FName" type = "Varchar(44)"
*/
public String getName(){…}
这段代码中类似JavaDoc的东西就是annotation,它描述了name这个属性对应着数据库中的类型为“Varchar(44)”名称为“FName”的字段。Hibernate3、EJB3都推荐并且支持这种方式。
在Hibernate本身也有元数据机制,在Hibernate的包org.hibernate.metadata中的类就是提供元数据支持的API,通过它们能读取到一个实体对象有哪些字段、字段的类型是什么、是否允许为空、是否关联其他的实体对象等。
JDK的annotation、Hibernate中的元数据都符合元数据的定义,它们也是真正的元数据。它们与MDA中的元数据的最主要区别就是是否具有平台无关性。很显然JDK的annotation、Hibernate中的元数据都不能脱离它们所依赖的平台,元数据中有很多描述平台专有属性的东西,无法作为一个跨平台的元数据引擎使用。不过这些元数据能反应平台的更多的细节,如果合理利用将极大地提高开发效率。在后边关于HibernateDTOGenerator的分析中读者将会看到我们是如何使用Hibernate的元数据来实现DTO产生器的。
7.3 实体元数据
为了使用MDA思想进行系统的设计开发,在案例系统中为在系统中处于核心的数据实体引入了元数据机制,系统建模、代码生成、系统开发、系统运行全部基于此元数据机制。
7.3.1 实体元数据格式
实体元数据中定义了实体的别名、对应的表名、实体的字段列表、字段的名称、字段的别名、字段类型等,基本包含了数据实体的公共特征,实体元数据文件的扩展名为“.emf”。下面是人员元数据的内容,各个标记的含义见注释:
<EnityModel>
<!--实体的名称为Person-->
<Name>Person</Name>
<!--实体的别名为Person-->
<Alias>人员</Alias>
<!--此实体定义在com.cownew.PIS.basedata包下-->
<PackageName>com.cownew.PIS.basedata</PackageName>
<!--实体对应的数据库表名为T_BD_Person-->
<DbTableName>T_BD_Person</DbTableName>
<!--实体的主键为id-->
<PrimaryKey>id</PrimaryKey>
<!--下面是字段定义列表-->
<FieldList>
<Field>
<Name>id</Name>
<Alias>id</Alias>
<DbFieldName>FId</DbFieldName>
<DataType>STRING</DataType>
<AllowNull>false</AllowNull>
<Length>50</Length>
<IsLinkProperty>false</IsLinkProperty>
</Field>
<Field>
<!--字段名为age-->
<Name>age</Name>
<!--字段别名为年龄-->
<Alias>年龄</Alias>
<!--字段对应的数据库字段名为FAge-->
<DbFieldName>FAge</DbFieldName>
<!--字段类型为INTEGER-->
<DataType>INTEGER</DataType>
<!--字段不允许为空-->
<AllowNull>false</AllowNull>
<!—字段不是关联属性(不关联其他实体)-->
<IsLinkProperty>false</IsLinkProperty>
</Field>
<Field>
<Name>name</Name>
<Alias>姓名</Alias>
<DbFieldName>FName</DbFieldName>
<DataType>STRING</DataType>
<AllowNull>false</AllowNull>
<Length>100</Length>
<IsLinkProperty>false</IsLinkProperty>
</Field>
<Field>
<Name>number</Name>
<Alias>编码</Alias>
<DbFieldName>FNumber</DbFieldName>
<DataType>STRING</DataType>
<AllowNull>false</AllowNull>
<Length>50</Length>
<IsLinkProperty>false</IsLinkProperty>
</Field>
</FieldList>
</EnityModel>
实体元数据不仅能定义简单的字段,而且能定义实体之间的关联关系,下面是一个定义了关联类型的系统操作员数据实体:
<EnityModel>
<Name>User</Name>
<Alias>系统用户</Alias>
<PackageName>com.cownew.PIS.base.permission</PackageName>
<DbTableName>T_BS_User</DbTableName>
<PrimaryKey>id</PrimaryKey>
<FieldList>
<Field>
<Name>id</Name>
<Alias>主键</Alias>
<DbFieldName>FId</DbFieldName>
<DataType>STRING</DataType>
<AllowNull>false</AllowNull>
<Length>50</Length>
<IsLinkProperty>false</IsLinkProperty>
</Field>
<Field>
<Name>number</Name>
<Alias>账号</Alias>
<DbFieldName>FNumber</DbFieldName>
<DataType>STRING</DataType>
<AllowNull>false</AllowNull>
<Length>50</Length>
<IsLinkProperty>false</IsLinkProperty>
</Field>
<Field>
<Name>password</Name>
<Alias>密码</Alias>
<DbFieldName>FPassword</DbFieldName>
<DataType>STRING</DataType>
<AllowNull>false</AllowNull>
<Length>50</Length>
<IsLinkProperty>false</IsLinkProperty>
</Field>
<Field>
<Name>person</Name>
<Alias>对应人</Alias>
<DbFieldName>FPersonId</DbFieldName>
<DataType>STRING</DataType>
<AllowNull>false</AllowNull>
<Length>50</Length>
<IsLinkProperty>true</IsLinkProperty>
<LinkType>MANYTOONE</LinkType>
<LinkEntity>/com/cownew/PIS/basedata/Person.emf</LinkEntity>
<CascadeType>none</CascadeType>
<Inverse>false</Inverse>
</Field>
<Field>
<Name>isFreezed</Name>
<Alias>是否被冻结</Alias>
<DbFieldName>FIsFreezed</DbFieldName>
<DataType>BOOLEAN</DataType>
<AllowNull>false</AllowNull>
</Field>
</FieldList>
</EnityModel>
这个元数据的定义和Person类似,唯一的区别在于这里定义了一个“person”字段关联到“Person”实体:
<Name>person</Name>
<Alias>对应人</Alias>
<DbFieldName>FPersonId</DbFieldName>
<DataType>STRING</DataType>
<AllowNull>false</AllowNull>
<Length>50</Length>
<!—此字段是关联字段-->
<IsLinkProperty>true</IsLinkProperty>
<!—与被关联实体是“多对一”的关系-->
<LinkType>MANYTOONE</LinkType>
<!—关联实体路径是/com/cownew/PIS/basedata/Person.emf-->
<LinkEntity>/com/cownew/PIS/basedata/Person.emf</LinkEntity>
<CascadeType>none</CascadeType>
<Inverse>false</Inverse>
对于关联字段只要设置IsLinkProperty为true,在LinkType标记内指定关联的类型,在LinkEntity中指定关联的实体路径(注意实体路径以“/”分割,并且全部是相对于根包的相对路径)即可。对于“一对多(ONETOMANY)”类型的字段还需要添加“<KeyColumn>***</KeyColumn>”标记表示被关联实体通过哪个字段反向关联本实体。
能够定义实体、定义字段、字段类型、实体关联、并定义了一些平台特有属性,这就是一个比较完备的实体元数据模型了。
7.3.2 元数据编辑器
虽然元数据模型是比较简单易懂的,但是手工编写这样的元数据文件仍然是低效且易出错的,直接查看元数据源文件也是非常烦琐的,为此我们开发了一个元数据文件的编辑器,使用此编辑器就可以通过可视化的界面编辑和查看实体元数据文件。编辑器还内置了代码生成功能,可以根据实体元数据文件生成JavaBean文件和ORM配置文件,目前仅支持Hibernate,不过由于设计时考虑到了可扩展问题,所以可以很轻松地支持其他ORM工具的代码和配置文件的生成。
这个元数据文件的编辑器是基于Eclipse的插件机制进行开发的。本书不假定也不强迫用户使用任何IDE,所以这里不介绍这个插件的实现原理。这里只简单介绍一下这个插件的使用,读者可以将此插件移植到当前使用的IDE上,当然也可以将其开发成一个独立的应用程序。
【例7.2】一个销售小票的建模过程(元数据编辑器的使用)。
下面以一个销售小票的建模过程来演示一下元数据编辑器的使用,图 7.1是销售小票的类图。
图7.1 销售小票类图
(1) 安装Eclipse,安装CowNewStudio插件。
在工程根目录下创建一个名字为“metadata”的文件夹,也可以直接打开案例工程,这个工程已经建立好了“metadata”文件夹以及常用的实体元数据。本例子中假定您使用的是案例工程。
(2) 在metadata/com/cownew/目录下创建文件夹demo。
在demo文件夹上右击,在弹出的快捷菜单中选择【新建】|【其他】命令,弹出向导对话框,如图7.2所示,选中CownewStudio节点下的Entity Model File creation wizard,单击【下一步】按钮。
(3) 进入如图7.3所示的新建界面,在【文件名】文本框中输入Goods.emf,然后单击【完成】按钮。
图7.2 选择向导
图7.3 选择所在文件夹
(4) 然后系统会自动用实体元数据编辑器打开此元数据文件,如图7.4所示。
编辑器的主要选项卡有两个,其中config选项卡为元数据文件的可视化编辑界面,而Goods.emf选项卡为元数据文件的源码编辑器,可以直接在此处编辑元数据文件的源码。
在可视化编辑选项卡中,Name、PackageName因为是系统预设的,所以是不可编辑的。在Alias中输入“商品”,在DBTableName中输入“T_Demo_Goods”。
编辑器左下方的空白区域是字段列表区,实体定义的字段在此展示,可以单击add按钮新增字段,单击remove按钮删除选定的字段。字段属性的编辑在eclipse的属性视图中进行,可以通过选择【窗口】|【显示视图】|【属性】命令打开此视图,可以通过单击编辑器中的快捷按钮open properties views来打开属性视图。
图7.4 元数据编辑器
(5) 单击add按钮增加id字段,在如图7.5所示的属性视图中编辑字段属性。
图7.5 属性视图
(6) 按照同样方式增加number、name字段。
在PrimaryKey下拉列表框中选择id作为主键。然后单击Eclipse的保存图标完成商品元数据的建模。
按照同样的步骤建立SaleBill实体元数据,增加id、number、saleDate属性。在增加saler属性的时候,此属性关联着系统中已经建立的Person元数据,因此设置isLinkProperty为true,设置完毕后属性视图中的属性比普通属性多了一些内容,主要是linkEntity、linkType、casadeType等。单击linkEntity属性右边的浏览 按钮,如图7.6所示,选择系统中已经定义好的“Person元数据”。
选择linkType属性为MANYTOONE。SaleBillDetail元数据没有建立,所以暂时不增加details属性。
(7) 按照同样步骤增加“SaleBillDetail”实体元数据。
回到SaleBill实体元数据编辑界面,增加details属性,设置linkedEntity指向SaleBillDetail实体,设定linkType属性为OneToMany,从keycolumn属性的下拉列表框中选择FHeadId属性,表示SaleBillDetail实体通过FHeadId字段指向SaleBill实体。
图7.6 选择关联元数据
(8) 建模完毕,下面开始生成代码和配置文件。同时选中Goods.emf、SaleBill.emf、SaleBillDetail.emf三个文件,右击,在弹出的快捷菜单中选择CownewStudio∣Generate Code from Model File命令,弹出如图7.7所示的界面。
图7.7 代码生成选项
(9) Target ORM为生成的文件对应的ORM类型,目前支持Hibernate2和Hibernate3。按照图7.7进行设置,单击【完成】按钮,然后在Eclipse中就可以看到生成的文件了,如图7.8所示。hbm配置文件生成在bizLayer包下,JavaBean生成在common包下。
图7.8 生成的代码和配置文件
7.4 元数据引擎设计
前边介绍了元数据在系统设计和平台相关代码生成中的应用,本节介绍元数据的运行时模型,这部分也是整个元数据中最复杂的部分。
7.4.1 实体元数据运行时模型
元数据中定义了很多丰富的属性,但并不是元数据所有的属性都对运行时系统有用,而且有的属性也不应该放到运行时模型中,图7.9是运行时的元数据模型。
图7.9 运行时的元数据模型
DataTypeEnum是数据类型枚举,LinkTypeEnum是关联类型枚举,EntityFieldModelInfo为实体字段模型,EntityModelInfo为整体的实体元数据模型。可以看到实体字段模型中去掉了实体元数据中的“CascadeType”、“Constrained”、“Inverse”等属性,这些字段是Hibernate平台特有的,所以是不能放到运行时模型中的。
为了解析元数据文件以生成运行时元数据模型,我们开发了元数据解析器类EntityMetaDataParser(在包com.cownew.PIS.framework.common.metaDataMgr下),此类对外提供了一个静态方法:public static EntityModelInfo xmlToBean(Document doc)。将元数据XML文件的Dom4j对象Document 作为参数调用,返回值就是这个实体元数据的模型EntityModelInfo。因为XML文件中包含运行时元数据模型不需要的东西,所以此处不能使用XStream等OXMapping工具,而是使用Dom4j完成XML的解析。
7.4.2 分包及命名规范
在多层架构中,各个层之间有自己独立的不应被其他层访问的类,也有在层之间共享的类。对这些不同共享层次的类进行分包可以保证清晰的系统分层,也可以简化各层的配置安装。在案例系统中分为4种包:应用服务器包、Swing Client端包,Web Client端包,公共包。比如,有一个提供天气预报服务的Remoting Service,定义了服务接口IWeatherForecast,此服务接口是应用服务器、Swing Client、Web Client都要访问的,所以要将它放到公共包中;此接口的实现类WeatherForecastImpl只有应用服务器需要,因此定义在应用服务器包中;如果编写了一个Swing客户端WeatherForecastUI访问此服务,WeatherForecastUI就要定义在Swing Client端包中;如果编写一个Web页面,此页面有WeatherForecastForm和WeatherForecastAction两个类,这两个类就要放到Web Client端包中。在案例系统中,应用服务器包命名为bizLayer、
为了简化系统的开发及规范文件管理,案例系统对文件目录结构做了如下约定,如果实体元数据DemoEntity的名称(name)为DemoEntity,包名(packageName)为com.cownew. Demo,那么:
l 生成的hbm配置文件DemoEntity.hbm.xml保存在com.cownew.Demo.bizLayer目录下。
l 生成的JavaBean文件必须以实体名加“Info”命名,即DemoEntityInfo.java,它保存在com.cownew.Demo.common目录下。
l 此实体对应的DAO实现接口IDemoEntityDAO保存在com.cownew.Demo.bizLayer目录下。
l 此实体对应的DAO实现类DemoEntityDAOImpl保存在com.cownew.Demo.bizLayer目录下。
l 访问此接口的Swing客户端文件保存在com.cownew.Demo.client目录下。
l 访问此接口的Web端源码保存在com.cownew.Demo.web目录下。
前三条是必须遵守的规范,其他是建议遵守的规范。
系统运行时可能需要根据实体的JavaBean得到其对应的接口或者根据元数据的位置得知接口的位置,只要根据上述规则就可以在JavaBean、接口、元数据之间互相转换。为了简化此操作,系统提供了一个工具类NameUtils,位于包com.cownew.PIS. framework.common.metaDataMgr下,各个方法的作用如下。
l public static String getVOClassName(String entityPath):将实体路径名转化为VO类名,比如getVOClassName("/com/cownew/Person.emf")将返回“com.cownew.common. PersonInfo ”。
l public static String getPackageName(String entityPath):从实体路径名得到包路径名,比如getPackageName("/com/cownew/Person.emf")将返回“com.cownew”。
l public static String getEntityName(String entityPath):从实体路径名得到实体名,比如getEntityName("/com/cownew/Person.emf")将返回“Person”。
l public static String getEntityPath(String infoClassName):将VO类名转化为实体路径名,比如getEntityPath("com.cownew.common.PersonInfo")将会返回“/com/cownew/ Person.emf”。
7.4.3 元数据加载器接口
元数据加载器最基本的功能就是根据给出的元数据路径返回对应的元数据运行时模型,比如调用者想得到“人员”元数据运行时模型,只要将“/com/cownew/PIS/basedata/ Person.emf”作为参数传递给元数据加载器,加载器就可以返回对应于“人员”的EntityModelInfo类的实例。
为了方便调用,元数据加载器还提供一些辅助功能,比如根据VO类加载元数据、根据VO类名加载元数据、一次性加载系统中所有的元数据模型。元数据的解析是非常耗时且耗用内存的,最好提供元数据缓存以及缓存的持久化功能。
【例7.3】设计元数据加载器接口。
代码如下:
// 元数据加载器接口
public interface IMetaDataLoader
{
/**
* 根据元数据路径加载元数据运行时模型
*/
public EntityModelInfo loadEntityByEntityPath(String path) throws
MetaDataException;
/**
* 根据VO类加载元数据运行时模型
*/
public EntityModelInfo loadEntityByVOClass(Class voClass) throws
MetaDataException;
/**
* 根据VO类名加载元数据运行时模型
*/
public EntityModelInfo loadEntityByVOClass(String voClass) throws
MetaDataException;
/**
* 加载所有的实体元数据的path,List中元素的类型为String
*/
public List loadAllEntityPath() throws MetaDataException;
/**
* 保存元数据缓存
*/
public void saveCache();
}
7.4.4 元数据热部署
元数据的热部署指的是当对元数据进行修改以后,无须重启服务器就能在系统中得到修改以后的运行时元数据模型。元数据的热部署功能主要是考虑到应用服务器的启动过程以及Spring、Hibernate等的初始化过程都是需要一定的时间的,如果开发人员每次对元数据进行修改都需要重启服务器就会降低开发效率。
7.4.5 元数据部署方式
元数据在系统中存在的方式可以有3种:①元数据和.class二进制字节码文件放到一起;②把metadata文件夹直接放到系统的目录下;③把所有元数据单独打包成一个jar文件。
元数据是和二进制字节码文件用途不同的文件,如果将它们混放则不利于管理,第一种方式很自然地被否决了。
第二种方式将元数据单独管理,比第一种方式具有优势。当元数据数量变多以后,metadata文件夹占据的空间也会变大,从文件夹中检索文件的速度也会较慢。
第三种方式将元数据打包,减小了元数据占据的空间并且加快了文件的检索速度。但是在开发环境中采用此方式的话,开发人员每次新增、删除或者修改元数据以后都要对元数据重新打包,这是非常低效和不现实的。
为了同时兼顾开发环境和运行环境的要求,元数据引擎将同时支持第二、第三种方式。当处于开发环境时就采用第三种方式,当处于正式运行环境的时候则采用第二种方式。具体采用哪种方式可以在配置文件中配置。
为了提高元数据的加载速度,元数据在客户端和服务器端各存在一份,并且客户端和服务器端都有自己的元数据加载器实例。
7.5 元数据引擎实现
前面我们设计了元数据引擎,这一节将看一下元数据引擎的具体实现。
7.5.1 根据元数据路径加载元数据
IMetaDataLoader接口中定义了根据元数据路径加载元数据运行时模型的方法public EntityModelInfo loadEntityByEntityPath(String path),实现此方法有两种思路:
l 编写一个专用的文件加载器,可以按照元数据路径从文件夹(开发环境部署模式)或者Jar文件(正式运行环境部署模式)中加载元数据文件,然后再对元数据文件进行解析。
l 将元数据所在的文件夹(开发环境部署模式)或者jar文件(正式运行环境部署模式)放入类路径(ClassPath)中,这样在程序中就可以通过getClass().getResource- AsStream("/com/cownew/PIS/demo/Person.emf")这样的方式来加载元数据文件了。
很显然,第二种方式实现起来最简单,所以在系统中将元数据所在的文件夹或者jar文件放入类路径(ClassPath)中。
7.5.2 元数据枚举器
IMetaDataLoader接口中定义了加载所有的实体元数据的path的方法public List loadAllEntityPath()。JDK没有提供枚举类路径中某一类文件的方法,我们必须开发这样的功能,我们称其为元数据枚举器。其接口定义如下:
public interface IMetaDataEnumerator
{
public List listAll();
}
接口只定义了一个listAll方法,它返回所有的元数据的路径列表。
元数据有文件夹和Jar文件两种部署模式,为了同时支持这两种方式的元数据枚举,可以编写一个元数据枚举器,在这个枚举器的listAll方法中判断是哪种部署模型,然后进行不同的处理。这样实现违背了面向对象开发的基本原则,应该将这两种不同的行为分别都定义在不同的枚举器中:DirectoryMetaDataEnumerator类负责在文件夹中进行元数据枚举,而JarFileMetaDataEnumerator类负责在Jar文件中进行元数据枚举。
【例7.4】元数据枚举器抽象类。
为了抽象出这两种枚举器的公共行为,首先编写一个实现了IMetaDataEnumerator接口的抽象类AbstractMetaDataEnumerator:
// 元数据枚举器抽象类
public abstract class AbstractMetaDataEnumerator implements
IMetaDataEnumerator
{
protected List list;
public List listAll()
{
// 进行惰性初始化处理,子类是要实现fillList方法,
// 在这个方法中向list中填充元数据路径即可,不用管惰性初始化问题
if (list == null)
{
fillList();
}
return list;
}
protected abstract void fillList();
}
抽象类中进行了惰性初始化处理,子类只要实现fillList方法即可。
【例7.5】Jar文件元数据枚举器。
接着编写从AbstractMetaDataEnumerator继承的JarFileMetaDataEnumerator类:
// Jar文件元数据枚举器
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.jar.JarFile;
public class JarFileMetaDataEnumerator extends AbstractMetaDataEnumerator
{
protected JarFile jarFile;
protected String suffix;
public JarFileMetaDataEnumerator(JarFile jarFile, String suffix)
{
super();
this.jarFile = jarFile;
this.suffix = suffix;
}
public void fillList()
{
list = new ArrayList();
Enumeration entries = jarFile.entries();
while (entries.hasMoreElements())
{
Object obj = entries.nextElement();
String e = obj.toString();
if (e.endsWith(suffix))
{
list.add("/" + e);
}
}
}
}
JarFileMetaDataEnumerator 的构造函数接受两个参数jarFile为元数据所在的Jar包,suffix为待匹配文件的扩展名,一般传递“.emf”。
通过JarFile 类的public Enumeration entries()方法即可得到遍历Jar文件中所有文件的枚举器,在遍历文件过程中判断文件扩展名,若等于suffix的话,就认为它是元数据文件。
【例7.6】目录元数据枚举器。
接着编写目录元数据枚举器,代码如下:
// 目录元数据枚举器
public class DirectoryMetaDataEnumerator extends AbstractMetaDataEnumerator
{
private File dir;
private String suffix;
public DirectoryMetaDataEnumerator(File dir, String suffix)
{
super();
this.dir = dir;
this.suffix = suffix;
}
public void fillList()
{
list = new ArrayList();
list.addAll(getChidren(dir, suffix));
}
private List getChidren(File f, String suffix)
{
List list = new ArrayList();
File[] listFiles = f.listFiles();
if (listFiles == null)
{
return list;
}
for (int i = 0, n = listFiles.length; i < n; i++)
{
File file = listFiles[i];
if (file.isFile())
{
if (file.getPath().endsWith(suffix))
{
String path = file.getPath().substring(
dir.toString().length());
path = path.replace(File.separatorChar, '/');
if (!path.startsWith("/"))
{
path = "/" + path;
}
list.add(path);
}
} else
{
list.addAll(getChidren(file, suffix));
}
}
return list;
}
}
此类的核心代码就是getChidren方法,此方法采用递归的方式遍历一个目录下的所有文件。当File的实例调用一个目录的时候,就调用其listFiles()方法得到其下的所有直接子文件(或者目录)。使用File类的isFile()方法判断子文件是文件还是文件夹,如果是文件夹则继续递归。文件的分隔符在不同的操作系统中有不同的形式,在UNIX系统中为“/”而在Windows中则为“/”,元数据路径遵守Java中的跨平台的要求,分隔符使用“/”,使用path = path.replace(File.separatorChar, '/')方法将路径中的平台相关的文件分隔符替换成“/”。
7.5.3 元数据缓存
元数据的解析是非常耗时且耗用内存的,如果每次加载元数据运行时模型都要去解析XML文件的话,会大大降低系统的运行效率,因此需要建立元数据的缓存机制。在loadEntityByEntityPath方法中首先判断要加载的元数据是否已经解析过并放在缓存中了,如果已经放在缓存中则只需到缓存中去取就可以;如果没有在缓存中,则解析元数据,然后将解析结果放入缓存。
由于缓存是建立在内存哈希表中的,当系统重启(包括客户端重启或者服务器端重启)以后,缓存就消失了,元数据必须被再次加载才能放到缓存中。既然上次运行的时候已经解析过元数据了,为什么不把上次的缓存保存下来呢?由于缓存是以哈希表的形式存在的,而哈希表是可以序列化的,所以在系统即将关闭的时候将缓存保存到硬盘中,下次系统重启的时候只要读取这个缓存文件并重建缓存即可。这项技术被称为“元数据延迟预编译”。更近一步,在系统正式安装运行的之前就将所有元数据解析一遍然后保存到缓存中,进而将缓存保存到文件中,系统运行的时候根本不用再去解析元数据,只要从缓存中读取就行了,这种技术被称为“元数据预编译”,和JSP页面预编译技术类似。这种技术在处理大数据量的不可变XML文件时很有用处。
元数据缓存在加快系统运行的同时也给开发人员带来了麻烦。举例来说:开发人员开发了实体元数据Person.emf,然后在系统中运行并加载了此元数据,这样元数据的运行时模型就保存到缓存中了。测试Person.emf的时候,开发人员发现要对Person.emf做一下修改,于是他修改了Person.emf,并保存了修改。当他再次调用元数据加载器加载此元数据的时候,由于Person.emf的元数据模型已经在缓存中存在了,所以他得到的是未修改之前的元数据模型。这种情况下,必须重启服务器和客户端,删除缓存文件。可以想象这是多么烦琐的过程,为了解决这个问题,我们在配置文件中增加一个缓存开关,在开发环境下关闭缓存开关,在正式运行时则打开缓存开关。
在ClientConfig.xml和ServerConfig.xml文件中同时增加下面针对元数据的配置项:
<MetaData>
<!--元数据缓存开关-->
<CacheEnabled>false</CacheEnabled>
<!--元数据路径-->
<MetaDataPath>F:/我的程序/java/CowNewPIS/metadata</MetaDataPath>
<!--元数据缓存文件的位置-->
<EntityCacheFile>C:/cownew/entity.cache</EntityCacheFile>
</MetaData>
并在ClientConfig.java和ServerConfig.java文件中增加读取这些配置项的代码。
7.5.4 元数据加载器
【例7.7】实现元数据加载器。
有了上面的这些类作为基础,下面就来看一下最核心的元数据加载器的实现代码:
// 元数据加载器
package com.cownew.PIS.framework.common.metaDataMgr;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.JarFile;
import org.apache.log4j.Logger;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.io.SAXReader;
import com.cownew.ctk.common.ExceptionUtils;
import com.cownew.ctk.io.ResourceUtils;
public class MetaDataLoader implements IMetaDataLoader
{
//实体路径到元数据模型的缓存
private static Map pathEntityMap;
//Vo类名到实体路径的缓存
private static Map voClassPathEntityMap;
//元数据的位置
private File metaDataPathFile;
//元数据缓存文件的位置
private File entityCacheFile;
//元数据枚举器
private IMetaDataEnumerator metaDataScanner;
//元数据缓存开关
private boolean cacheEnable;
/**
* @param metaDataPath 元数据所在的路径
* @param entityCacheFile 元数据缓存文件位置
*/
public MetaDataLoader(String metaDataPath, String entityCacheFile)
{
super();
metaDataPathFile = new File(metaDataPath);
if (!metaDataPathFile.exists())
{
throw new IllegalArgumentException("path:" + metaDataPath
+ " not found!");
}
//如果元数据所在的路径是目录,则使用目录元数据扫描器
//否则使用Jar文件元数据扫描器
if (metaDataPathFile.isDirectory())
{
metaDataScanner = new
DirectoryMetaDataEnumerator(metaDataPathFile,
"." + NameUtils.EMFEXT);
} else
{
try
{
metaDataScanner = new JarFileMetaDataEnumerator(new JarFile(
metaDataPathFile), "." + NameUtils.EMFEXT);
} catch (IOException e)
{
throw ExceptionUtils.toRuntimeException(e);
}
}
voClassPathEntityMap = new HashMap();
this.entityCacheFile = new File(entityCacheFile);
cacheEnable = false;
loadMetaCache();
}
public void setCacheEnable(boolean cacheEnable)
{
this.cacheEnable = cacheEnable;
}
//从元数据缓存文件中加载缓存
private void loadMetaCache()
{
if (!this.entityCacheFile.exists())
{
pathEntityMap = new HashMap();
}
ObjectInputStream ois = null;
try
{
ois = new ObjectInputStream(new FileInputStream(
this.entityCacheFile));
pathEntityMap = (Map) ois.readObject();
} catch (Exception e)
{
//只要发生异常就认为元数据缓存不可用,因此就不加载缓存
//发生异常一般是由于类版本不一致造成的
//这种异常一般只在开发环境或者正式运行环境的系统
//升级过程中出现
pathEntityMap = null;
Logger.getLogger(MetaDataLoader.class).error(e);
} finally
{
ResourceUtils.close(ois);
}
if (pathEntityMap == null)
{
pathEntityMap = new HashMap();
}
}
public EntityModelInfo loadEntityByEntityPath(String path)
throws MetaDataException
{
// 只有元数据缓存打开的时候才从缓存中读取
if (cacheEnable)
{
EntityModelInfo eInfo = (EntityModelInfo)
pathEntityMap.get(path);
if (eInfo != null)
{
return eInfo;
}
}
InputStream inStream = null;
try
{
inStream = getClass().getResourceAsStream(path);
Document doc = new SAXReader().read(inStream);
EntityModelInfo info = EntityMetaDataParser.xmlToBean(doc);
pathEntityMap.put(path, info);
return info;
} catch (DocumentException e)
{
throw new MetaDataException(
MetaDataException.LOADENTITYMETADATAERROR, e);
} finally
{
ResourceUtils.close(inStream);
}
}
public EntityModelInfo loadEntityByVOClass(Class voClass)
throws MetaDataException
{
return loadEntityByVOClass(voClass.getName());
}
public EntityModelInfo loadEntityByVOClass(String voClass)
throws MetaDataException
{
String entityPath = (String) voClassPathEntityMap.get(voClass);
if (entityPath == null)
{
entityPath = NameUtils.getEntityPath(voClass);
voClassPathEntityMap.put(voClass, entityPath);
}
EntityModelInfo info = loadEntityByEntityPath(entityPath);
return info;
}
public List loadAllEntityPath() throws MetaDataException
{
return metaDataScanner.listAll();
}
//保存缓存
public void saveCache()
{
ObjectOutputStream oos = null;
try
{
//无论缓存文件是否存在,重新创建文件
entityCacheFile.createNewFile();
oos = new ObjectOutputStream(new
FileOutputStream(entityCacheFile,false));
oos.writeObject(pathEntityMap);
} catch (Exception e)
{
Logger.getLogger(MetaDataLoader.class).error(e);
} finally
{
ResourceUtils.close(oos);
}
}
}
在初始化元数据枚举器的时候体现了基于接口编程的好处:
if (metaDataPathFile.isDirectory())
{
metaDataScanner = new DirectoryMetaDataEnumerator(metaDataPathFile,
"." + NameUtils.EMFEXT);
} else
{
metaDataScanner = new JarFileMetaDataEnumerator(
new JarFile(metaDataPathFile), "." + NameUtils.EMFEXT);
}
如果元数据文件是目录,则将metaDataScanner 变量初始化为DirectoryMetaDataEnumerator的实例;如果元数据文件是文件,则将metaDataScanner变量初始化为JarFileMetaDataEnumerator的实例。后面使用metaDataScanner的时候都是使用IMetaDataEnumerator接口声明的方法,而不管是哪个实现类的。
这里保存缓存的方式是直接将pathEntityMap对象序列化到文件中,这样做的优点是简单,缺点是当缓存中的对象对应类版本发生变化的时候(在开发环境中对类进行修改或者正式运行环境进行版本升级),反序列化就会失败。只要从缓存文件中反序列化pathEntityMap的时候发生任何异常,就重建缓存:
try
{
ois = new ObjectInputStream(new FileInputStream(
this.entityCacheFile));
pathEntityMap = (Map) ois.readObject();
} catch (Exception e)
{
pathEntityMap = null;
}
…
if (pathEntityMap == null)
{
pathEntityMap = new HashMap();
}
7.5.5 工具类
为了方便使用元数据引擎,系统中还内置了方便客户端和服务器端访问元数据的工具类。
【例7.8】内置方便客户端和服务器端访问元数据的工具类。
ClientMetaDataLoaderFactory是客户端元数据加载器工厂,它位于com.cownew. PIS.framework.client包中。
具体代码如下:
// 客户端元数据加载器工厂
public class ClientMetaDataLoaderFactory
{
private static MetaDataLoader loader;
public static IMetaDataLoader getLoader()
{
if (loader != null)
{
return loader;
}
ClientConfig config = ClientConfig.getInstance();
String entityCacheFile = config.getEntityCacheFile();
String metaDataPath = config.getMetaDataPath();
loader = new MetaDataLoader(metaDataPath, entityCacheFile); loader.setCacheEnable(
ClientConfig.getInstance().isMetaCacheEnabled());
return loader;
}
}
ServerMetaDataLoaderFactory是服务器端元数据加载器工厂,它位于com.cownew. PIS.framework.server.helper包中。
具体代码如下:
// 服务器端元数据加载器工厂
public class ServerMetaDataLoaderFactory
{
private static MetaDataLoader loader;
public static IMetaDataLoader getLoader()
{
if (loader != null)
{
return loader;
}
ServerConfig config = ServerConfig.getInstance();
String entityCacheFile = config.getEntityCacheFile();
String metaDataPath = config.getMetaDataPath();
loader = new MetaDataLoader(metaDataPath, entityCacheFile);
loader.setCacheEnable(
ServerConfig.getInstance().isMetaCacheEnabled());
return loader;
}
static
{
Runtime.getRuntime().addShutdownHook(new Thread() {
public void run()
{
super.run();
ServerMetaDataLoaderFactory.getLoader().saveCache();
}
});
}
}
客户端的元数据缓存的保存动作是放到注销方法中的。与客户端不同的是,服务器关闭的时候没有一个准确的响应服务器关闭的入口,必须借助其他手段来实现服务器关闭时保存元数据缓存的功能。
java.lang.Runtime类有一个addShutdownHook方法,使用它我们可以把一个线程对象注册到虚拟机中。当虚拟机正常关闭时,虚拟机会调用注册的所有线程对象,并运行它们。这样把保存元数据缓存的方法写到一个线程对象中,然后调用addShutdownHook将其注册到JVM即可。
MetaDataHelper位于com.cownew.PIS.framework.common.metaDataMgr包中,这个助手类的构造函数要求一个IMetaDataLoader接口的实例。类中有一个方法:public String getPropertyAlias(Class voClass, String property),通过这个方法可以得知一个实体值对象的某个字段的别名。
例如调用metaDataHelper.getPropertyAlias(PersonInfo.class, "age")就会返回“年龄”。通过元数据引擎就能够得到数据的一些模型信息,这就是元数据的神奇之处。
MetaDataHelper类目前只有这一个方法,读者可以根据需要增加更多的方法,比如得到实体值对象的所有关联实体、得到实体值对象的某个字段的类型等。
7.5.6 待改进问题
到了这里,元数据引擎已经基本可用了。任何事情都不可能是完美的,这个元数据引擎还可以在如下几方面优化:
l 实现“元数据预编译”。目前实现的是“元数据延迟预编译”,在系统第一次运行的时候要进行元数据的解析,所以运行速度会比较慢,采用“元数据预编译”以后就可以避免此问题。
l 优化元数据缓存策略。现阶段系统中业务模块较少,元数据量较少,所以对所有访问过的元数据都进行了缓存。当系统的业务模块发展到一定规模以后,系统中会存在大量元数据,如果把这些元数据都加载到缓存中必将大量地占用内存。说到这里,第一个反应就是将目前的缓存改造成固定大小的采用LRU淘汰算法的缓存,但是这种淘汰算法只能解决客户端的问题。在真实的业务系统中,一个登录的客户端通常大部分时间只运行一部分业务功能,比如会计登录系统的时候只会登录财务模块、库管只会登录仓库管理模块,即使会访问其他模块也是暂时和短暂的;而应用服务器端则不同,应用服务器是为所有客户端提供服务的,它会几率均等地访问系统的各个模块。在客户端只有部分元数据会被频繁访问,而在应用服务器端大部分元数据都会被频繁地访问,所以说客户端的元数据访问具有局部性,而应用服务器端元数据访问的局部性则不明显。对访问局部性很强的客户端采用LRU淘汰算法能够起到非常良好的作用,而如果对应用服务器端元数据采用LRU淘汰算法则会导致缓存的抖动。基于此,我们建议对客户端元数据采用LRU淘汰算法,而对应用服务器端则采取增加内存容量的方式来解决问题。 采用非递归算法改造DirectoryMetaDataEnumerator类。当目录结构过深或者目录数量过多的话,此实现算法会导致系统性能急剧下降,甚至使机器发生故障。
10.1 什么是DTO
层间数据传输的过程就是服务的执行者将数据返回给服务的调用者的过程。在非分布式系统中由于有类似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是无法将案例系统的元数据机制剥离的。