轻量级J2EE企业应用实战

mmvw

贡献于2015-06-14

字数:0 关键词: J2EE Java开发

李刚 著 全面介绍 J2EE 的流行握. 详细介绍时下全部架构模式 包含多达7 个实体关联的实用察倒 光盘包含. 本书所有实例代码 国 3辛劳艇在-.' ‘ ‘ 、‘ .二" ‘.. .d:kJt 李刚著 常手王芳自 k 版社· Publishing House ofElectronics Indus盯F 北京 -BEIJING内容简介 本书所介绍的内容是作者多年J2EE 开发经验的总结,内容涉及 Struts 、 Hibernate 和 Spring 三个开源 框架,还介绍了 Tomcat 和 Jetty 两个开源 Web 服务器的详细用法,以及 J2EE 应用的几种常用架构。 本书不仅是一本 J2EE 入门图书,还详尽而细致地介绍了 JSP 各个方面,包括 JSP 2.0 的规范、 Struts 的各种用法、 Hibernate 的详细用法,以及 Spring 的基本用法。书中所介绍的轻量级 J2EE 应用,是目前 最流行、最规范的J2EE 架构,分层极为清晰,各层之间以松精合的方法组织在一起。书的最后配备了两 个实例,均采用了贫血模式的架构设计,以便于读者更快地进入J2EE 应用开发。而第 8 章所介绍的其他 架构模式则可作为读者对架构有更好把握后的提高部分。本书配套光盘包括各章内容所用的代码,以及 整个应用所需要的开源类库等相关项目文件。 本书适用于有较好的 Java 编程基础,有初步的 J2EE 编程基础的读者。本书既可以作为 J2EE 初学者 的入门书籍,也可作为 J2EE 应用开发者的提高指导。 未经许可,不得以任何方式复制或抄袭本书之部分或全部内容。 版权所有,侵权必究。 图书在版编目 (CIP) 数据 轻量级 J2EE 企业应用实战: Struts+Spring+Hibernate 整合开发/李刚著.一北京:电子工业出版社,2007.4 (Java 技术大系) ISBN 978-7-121-03998-0 I.轻… II. 李… III.①JAVA 语言一程序设计②软件工具一程序设计 四 τ'P312 TP31 1.56 中国版本图书馆 CIP 数据核字 (2007) 第 033300 号 责任编辑:高洪霞裴杰 印 刷:北京天宇星印刷厂 装 订:三河市鹏成印业有限公司 出版发行:电子工业出版社 北京市海淀区万寿路 173 信箱 邮编 100036 开 本: 787 X 1092 1116 印张: 34 字数: 765 千字 印 次: 2007 年 4 月第 1 次印刷 印 数: 5∞0 册 定价: 65.00 元(含光盘 1 张) 凡所购买电子工业出版社图书有缺损问题,请向购买书店调换。若书店售缺,请与本社发行部联系, 联系电话: (010) 68279077; 邮购电话: (010) 88254888。 质量投诉请发邮件至zlts@phei. com.cn,盗版侵权举报请发邮件至dbqq@phei.com.cn。 服务热线: (010) 88258888。目 IJ -晶- E司 h 目前, J2EE 应用确实很流行,从银行、证券系统,到企业信息化平台,甚至一些小 公司,都争相使用J2EE 应用。几年前, J2 EE 应用是很"贵族"的产品,那时候使用 EJB 作为J2EE 的核心,开发成本高,部署成本也高,开发者的学习曲线也陡峭。今天,轻 量级J2EE 应用的流行,让J2EE 应用开始进入寻常百姓家。当然,轻量级J2EE 应用是 对经典J2EE 应用的简化,在保留经典J2EE 应用的架构、良好的可扩展性、可维护性的 基础上,简化了J2EE 应用的开发,降低了J2EE 应用的部署成本。 轻量级J2EE 应用让J2EE 平台以更快的速度占领电子商务、电子政务等各种信息化 平台市场。笔者从不否认对经典J2EE 应用架构的喜爱,那种严谨的架构、全方位考虑 的设计、优秀的分布式架构,无疑是一种编程的艺术。但它们太豪华了,以致于限制了 它的市场占有率。可以这样说:经典J2EE 应用是面向开发者的,而轻量级J2EE 应用则 面向用户。优秀的开发者会感慨并喜欢经典J2EE 应用的设计,但市场则喜欢轻量级J2EE 应用。轻量级J2EE 应用模仿了经典J2EE 应用的架构,保留了经典J2EE 应用的各种优 点,降低了学习难度和开发、部署成本,是一种更实际的信息化平台架构。 为什么写作本书 几年前,笔者主要从事实际的开发时,从未想过写一本书,忙是一个原因,更多原 因是没有感触。'如今,笔者已经在新东方 IT 培训中心担任J2EE 培训讲师一年多,现已 成为广州新东方软件工程师培训讲师的负责人。培训过程中,看到学生们求知若渴的眼 睛,以及他们热切的需要: "老师,出一本关于某技术的书吧! "回想起 1999 年底时,笔 者刚刚开发J2EE 学习,完成一个简单的 EJB ,居然花了将近一个月时间,其间苦痛只 有程序员才懂。如今看到学生们的苦楚,想起更多J2EE 学习者正备受煎熬,笔者愿意 将多年的经验与大家一起分享,这些经验包含笔者多年废寝忘食后的恍然醒悟,也包含 笔者跌落后艰难爬出的陷阱。希望这些经验能缩短读者朋友们的学习周期。 需要提醒读者朋友的是,所有的代码必须自己敲过,才是真正属于自己的代码。不 要指望光看看本书,就可以成为一个编程高手,一定要踏踏实实地独立完成书中所有应 用。学习编程是很辛苦却很有乐趣的事情,记得电影《阿甘正传》中有句话"偶尔雨停 了,可以看见星星。"这种场景很适合编程的境界,大部分时候都在埋头辛苦写代码,调 试错误,只在当应用真正运行成功时,获得瞬间的快乐一一这种快乐弥足珍贵,也是真 正的快乐。 有时候我的学生会拿着他刚买的图书问我,这本书如何?很不幸,有时会发现名为 J2EE 的图书,居然在 JSP 页面中有 Hibernate 的 API 。于是我无言以对,这样的图书到 底想使读者成为怎样的开发者?阅读这样的图书不仅浪费时间,而且会造成错误的积累。 • III •有感于此,笔者创作了本书,愿与各位读者共享多年的实践经验。即使今天,笔者依然 与珠三角很多软件公司联系紧密,很多学生己在华为、立信、中企动力和京华网络等企 业就业,有的学生己成长为技术经理,他们依然会就实际开发中的问题与笔者一起探讨, 这些经验,都将出现在笔者的J2EE 系列图书中。 本书有什么特点 与市面上已经存在的介绍J2EE 应用的图书相比,本书有如下儿个特色: 1.经验丰富,针对性强 笔者既担任过软件开发的技术经理,也担任过软件公司的培训导师,也从事过职业 培训的专职讲师。这些经验影响了笔者写书的目的,不是一本学院派的理论读物,而是 一本实际的开发指南。 2. 内窑实际,实用性强 本书所介绍的J2EE 应用范例,规模可能并不大,但绝对是目前企业流行的开发架 构,绝对严格遵守J2EE 开发规范。而不是将各种技术杂乱地揉合在一起号称J2EE 。读 者参考本书的架构,完全可以身临其境地感受企业实际开发。 3. 高屋建钮,启发性强 本书介绍的几种架构模式,几乎是时下最全面的J2EE 架构模式。这些架构模式可 以直接提升读者对系统架构设计的把握。 本书写给谁看 本书适用于有较好的 Java 编程基础,有初步的 J2EE 编程基础的读者。本书既可以 作为J2EE 初学者的入门书籍,也可作为 J2EE 应用开发者的提高指导。 李刚 2007-1-12 • IV·目录[;时TENTS 第 1 章 J2EE 应用运行及开发 环境的安装与配置….........…......... 1 1. 1 JDK 的 F载和安装… ·······················2 1.1.1 Windows 下 JDK 的下载和安装… 2 1.1.2 Linux 下 JDK 的下载和安装 ........·5 1.2 Tomcat 的下载和安装.......……........ 6 1.2.1 Tomcat 的下载和安装 ..................·7 1.2.2 Tomcat 的基本配置........….........… 8 1.2.3 Tomcat 的数据源配置..............… 13 1.3 Jetty 的下载和安装.........…........…· 17 1.3.1 Jetty 的下载和安装…................... 17 1.3.2 Jetty 的基本配置…....................... 18 1.4 Eclipse 的安装和使用.........… ......·25 1.4.1 Eclipse 的下载和安装.................. 25 1.4.2 Eclipse 插件的安装.................…· 26 1.4.3 Eclipse 的简单使用..................... 28 本章小结..............................….......….......… 31 第 2 章传统表现层 JSP 32 2.1 JSP 的技术原理…..............…..·......·33 2.2 JSP 注释 H …......................….......……36 2.3 JSP 声明......................….......… ..··....·37 2.4 JSP 表达式….........….......…............… 38 2.5 JSP 脚本................................…........·38 2.6 JSP 的三个编译指令..........…........41 2.6.1 page 指令….........….........………… 41 2.6.2 include 指令.........……………...… 44 2.7 JSP 的 7 个动作指令..........…........45 2.7.1 forw 缸d 指令…….......................… 46 2.7.2 include 指令….......….........… ........48 2.7.3 useBean, setProperty, getProperty 指令…....................... 49 2.7.4 plugin 指令.........….........…........... 52 2.7 .5 param 指令….........…·….........……· 53 2.8 JSP 的 9 个内置对象….......…........54 2.8.1 application 对象........................… '55 2.8.2 config 对象.................…..........….. 58 2.8.3 exception 对象.................… ....··....59 2.8.4 out 对象.........…….................…… '60 2.8.5 pageContext 对象......................… 61 2.8.6 request 对象….......…............… ......62 2.8.7 response 对象............................… 67 2.8.8 session 对象.......…… ....................·70 2.9 Serv1et 介绍…..…..........................…'72 2.9.1 Servlet 的开发.............................. 72 2.9.2 Servlet 的配置.........… ..................74 2.9.3 Servlet 的生命周期.................….. 75 2.9.4 使用 Servlet 作为控制器 .............76 2.9.5 load-on-startup Servlet..·..·..…...... 80 2.9.6 访问 Servlet 的配置参数 .............81 2.10 自定义标签库…..….........……·..··..··83 2.10.1 开发自定义标签类 ............·......··83 2.10.2 建立 TLD 文件.........….........…… 84 2.10.3 在 web.xml 文件中增加 标签库定义..................… ..··..··..·84 2.10.4 使用标签库…............................ 85 2.10.5 带属性的标签…...........…........… 86 2.10.6 带标签体的标签 ..·..··..............·..90 2.11 Filter 介绍 ·········..··..······..·..···..·····..·94 2.1 1.1 创建 Filter 类.........…........…..… "94 2.1 1.2 配置 Filter" ….............................. 95 2.12 Listener 介绍….........……....·..··..·..··96 2.12.1 创建Li stener 类…...........… ........96 2.12.2 配置 Listener 98 2.13 JSP 2.0 的新特性.......…....·..........·98 2.13.1 JSP 定义............................… H … 99 2.13.2 表达式语言.............................. 101 • V·2.13.3 简化的自定义标签 ··················108 2.13 .4 Tag File 支持… ··························111 本章小结...............…...............................… 113 第 3 章经典 MVC 框架 Struts' … ··········114 3.1 MVC 简介.........…….......…..............·115 3. 1.1 传统的 Modell 和 ModeI2·..·....115 3. 1.2 MVC 及其优势........................… 116 3.2 Struts 概述 ·····..································117 3.2.1 Struts 的起源....................… ········117 3.2.2 Struts 的体系结构....................… 117 3.3 Struts 的下载和安装.........…··..·····118 3.4 Struts 入门.......… ···..····..····..·····..··..·119 3.4.1 基本的 MVC 示例.................… "119 3.4.2 Struts 的基本示例…................... 126 3.4.3 Struts 的流程…........................... 129 3.5 Struts 的配置................,..........……130 3.5.1 配置 ActionServlet 130 3.5.2 配置 ActionForm.......... … ..·....·..·132 3.5 .3 配置 Action ….........…..............… 133 3.5.4 配置 Forw 缸d …......................….. 134 3.6 Struts 程序的国际化….................135 3.6.1 Java 程序的国际化.................… 136 3.6.2 Struts 的国际化….......…............. 139 3.7 使用动态 ActionForm….............. 143 3.7.1 配置动态 ActionForm....·..·........ 143 3.7.2 使用动态 ActionForm....·..·..·....· 144 3.8 Struts 的标签库........................…..145 3.8.1 使用 Struts 标签的基本配置..... 145 3.8.2 使用 htn咀标签库....................... 146 3.8.3 使用 bean 标签库….................… 148 3.8.4 使用 logic 标签库… ..........·....·..·155 3.9 Struts 的数据校验.........….............164 3.9.1 Actio nForm 的代码校验 H …....... 165 3.9.2 Action 的代码校验…...........…… 169 3.9.3 结合 commons-v a1idator.jar 的校验........................................ 169 3.10 Struts 的异常框架….......…·....·..·177 • VI· 3.11 几种常用的 Action....·....… ·..·....·180 3.1 1.1 Di spatchAction 及其子类......... 181 3.1 1.2 使用 ForwardAction 185 3.1 1.3 使用 Inc1udeAction 185 3.1 1.4 使用 SwitchAction .........…·......·186 3.12 Struts 的常见扩展方法..........…187 3.12.1 实现 Plug In接口.........….......... 187 3.12.2 继承 RequestProcessor .........…'188 3.12.3 继承 ActionServlet ...............…. 190 本章小结.........…........…................……....... 191 第 4 章使用 Hibernate 完成持久化….......…........… ..·......·192 4.1 ORM 简介 ......·........................·......·193 4. 1.1 什么是 ORM..·....···......· … ......·..·193 4.1.2 为什么需要 ORM.................. …. 193 4. 1.3 流行的 ORM 框架介绍…........... 193 4.2 Hibernate 概述…........................…'194 4.2.1 Hibernate 的起源 ..·........·............194 4.2.2 Hibernate 与其他 ORM 框架的对比…..........................… 195 4.3 Hibernate 的安装和使用.........…'195 4.3.1 Hibernate 下载和安装….........… '195 4.3.2 传统 JDBC 的数据库操作 ·..·....·196 4.3.3 Hibernate 的数据库操作........… 197 4.4 Hibernate 的基本映射 ....·............200 4.4.1 映射文件结构 ·....·..·........·....·....·200 4.4.2 主键生成器 H … ............·......··..·..·200 4.4.3 映射集合属性…......................... 201 4.4.4 映射引用属性…...........….......… '208 4.5 Hibernate 的关系映射 ........·..·....·216 4.5.1 单向 N -1 的关系映射… ......·....217 4.5.2 单向 1-1 的关系映射…… ........·220 4.5.3 单向 l-N 的关系映射… ·..........222 4.5.4 单向 N-N 的关系映射..........… 225 4.5.5 双向 l-N 的关系映射… ........·..226 4.5.6 双向 N-N 关联........…… ..·..·....230 4.5.7 双向 1-1 关联......................… '2324.6 Hibernate 查询体系....··················237 4.6.1 HQL 查询 ································..·237 4.6.2 条件查询...............….................. 247 4.6.3 SQL 查询.........….........….........… 249 4.6.4 数据过滤 ······..·······..·············..····253 4.7 事件框架..............................…......·255 4.7.1 拦截器.......….........… ·................·256 4.7.2 事件系统.................................... 259 本章小结.......….........….......................…… 263 第 5 章 Spring 介绍….............................. 264 5.1 Spring 的起源和背景..,.............…265 5.2 Spring 的下载和安装..................265 5.3 Spring 实现两种设计模式.........266 5.3.1 单态模式的回顾….................…· 266 5.3.2 工厂模式的回顾 H …................… 268 5.3.3 Spring 对单态与工厂 模式的实现…..........................… 270 5.4 Spring 的依赖注入.......….........…· 271 5.4.1 理解依赖注入..............…........… 272 5.4.2 设值注入.........…........................ 273 5.4.3 构造注入......................… ..........·276 5.4.4 两种注入方式的对比................ 277 5.5 bean 和 BeanFactory…................. 278 5.5.1 Spring 容器 ·......·..............·........·278 5.5.2 bean 的基本定义 ..·....·..........·....·280 5.5.3 定义 bean 的行为方式............... 281 5.5.4 深入理解 bean·....·...................... 282 5 五 5 创建 bean 实f9iJ .........…..............·284 5.6 依赖关系配置…........................…..291 5.6.1 配置依赖.........…........................ 292 5.6.2 注入属性值..........… ....·....·........·297 5.6.3 注入 field 值 ........·..............·......·300 5.6.4 注入方法返回值…..................… 301 5.6.5 强制初始化 bean" ….......… ........·304 5.6.6 自动装配..........….......… ............·304 5.6.7 依赖检查................................…· 307 5.7 bean 的生命周期.........…..............·309 5.7.1 了解 bean 的生命周期............... 309 5.7.2 定制 bean 的生命周期行为 ......·309 5.7.3 协调不同步的 bean 313 5.8 bean 的继承….................................315 5.8.1 使用 abstract 属性…… ..........·....·315 5.8.2 定义子 bean....·..·........................ 317 5.8.3 Spring bean 的继承与 Java 中继承的区别 ............·......·318 5.9 bean 后处理器...............….......……319 5.10 容器后处理器…...........................322 5.10.1 属性占位符配置器................… 323 5.10.2 另一种属性占位符配置器 (PropertyOverrideConfigurer) 324 5.11 与容器交互….......….......….......…..325 5.1 1.1 工厂 bean 简介与配置.........…. 325 5.1 1.2 FactoryBean 接口 ....................·327 5.1 1.3 实现 BeanFactory Aware 接口获取 BeanFactory 329 5.1 1.4 使用 BeanNameAware 回调本身 ......................·..........·330 5.12 ApplicationContext介绍...........331 5.12.1 国际化支持….........….........….. 332 5.12.2 事件处理...............…..…........… 334 5.12.3 Web 应用中自动力日载 ApplicationContext......·........·..· 335 5.13 加载多个 XML 配置文件 ·......·337 5.13.1 ApplicationContext 加载多个配置文件 ......·........·..·337 5.13.2 Web 应用启动时加载 多个配置文件 ..·..·....·..·..·..·..·..·337 5.13.3 XML 配置文件中导入 其他配置文件.......................... 338 本章小结....................….........................… '338 第 6 章 Spring 与 Hibernate 的整合 ....339 6.1 Spring 对Hibernate 的支持 ....·..340 6.2 管理 SessionFacto可 ............·..·..·..340 6.3 Spring 对 Hibernate 的简化 ..·....342 ·VII·6.4 使用 HibernateTemplate 343 6.4.1 HibernateTemp1ate 的 常规用法........................… ........·346 6.4.2 Hibernate 的复杂用法 HibernateCal1back .................…........…… 347 6.5 Hibernate 的 DAO 实现.........…..349 6.5.1 DAO 模式简介........................… 349 6.5.2 继承 HibernateDaoSupport 实现 DAO..···....· …..….......… ·······350 6.5.3 基于 Hibernate 3.0 实现 DAO...353 6.6 事务管理·..····..······..··················..····354 6.6.1 编程式的事务管理.................... 355 6.6.2 声明式事务管理........................ 357 6.6.3 事务策略的思考........................ 366 本章小结…......................… ··..·····....···..··..·366 第 7 章 Spring 与 Struts 的整合 ···..··..·367 7.1 Spring 整合第三方 MVC 框架的通用配置.......….................368 7. 1.1 采用 Contex tLo aderLi stener 创建 ApplicationContext..·..·...... 368 7. 1.2 采用 1oad-on-startup Serv1et 创建 ApplicationContext............ 370 7.2 Spring 与 MVC 框架 整合的思考…......................…......··372 7.3 利用 Spring 的 IoC 特性整合…. 374 7.3.1 使用 De1egatingRequest Processor …........….........…....·....·375 7.3.2 使用 De1egatingAction Proxy ….. 380 7.4 使用 ActionSupport 代替 Action............·….....................…......·382 7.5 实用的整合策略...........................385 本章小结…...................…..........… ....·....·..·388 第 8 章企业应用开发的 思考与策略.................…… ..·..···..·389 8.1 企业应用开发面临的挑战........·390 8. 1.1 可扩展性、可伸缩性… ···....·....·390 8. 1.2 快捷、可控的开发…................. 392 ·vm· 8. 1.3 稳定性、高效性........................ 392 8. 1.4 花费最小化,利益最大化......... 393 8.2 如何面对挑战...............…..…·....·..·393 8.2.1 使用建模工具.................… ..·....·393 8.2.2 利用优秀的框架…..........…........ 394 8.2.3 选择性地扩展….......… ..·..·........·396 8.2.4 使用代码生成器 ................·....·..396 8.3 常用的设计模式及应用..............397 8.3.1 单态模式的使用…..........…........ 397 8.3.2 代理模式的使用 ..·......···....··......400 8.3.3 Spring AOP 介绍.................…… '403 8.4 常见的架构设计策略… ........·......408 8.4.1 贫血模式..............…...............… '408 8.4.2 Rich Domain Object 模式.......... 413 8.4.3 抛弃业务逻辑层……..............… '418 本章小结.......…...............… ..·....·........·......·419 第 9 章 完整实例:消息发布系统 ......·420 9.1 系统架构说明................................421 9. 1.1 系统架构说明...............….......… 421 9. 1.2 采用架构的优势.........… ............421 9.2 Hibernate 持久层H … ..........·........·..422 9.2.1 编写 PO 类.....................…… H … 423 9.2.2 编写 PO 的映射配置文件 ··........428 9.2.3 连接数据库…................…… ....·..431 9.3 DAO 组件层...............… ·........·....·..434 9.3.1 DAO 组件的结构.................…… 434 9.3.2 编写 DAO 接口.........… ....·........·435 9.3.3 编写 DAO 的具体实现…… ........437 9.3.4 用 Spring 容器代替 DAO 工厂 '441 9.4 业务逻辑层 ..··..··....·..··..···..···..·····..·442 9.4.1 业务逻辑组件的结构 ..·..··..··..·..·442 9.4.2 业务逻辑组件的接口 H ….......… "442 9.4.3 业务逻辑组件的实现类.........… '444 9.4.4 业务逻辑组件的配置.......… ·..··..447 9.5 Web 层设计 ··..·..···..·..··..··..····..·..··..450 9.5.1 Action 的实现….......… ....··..··..·..450 9.5.2 Spring 容器管理 Action·..........·· 4539.5.3 数据校验的选择….........… ..·······456 9.5.4 访问权限的控制 ························459 9.5.5 解决中文编码问题................…'460 9.5.6 JSP 页面输出… ··························462 9.6 系统最后的思考.................…......·464 9.6.1 传统 EJB 架构的实现 ............·..·464 9.6.2 日B 架构与轻量级 架构的对比 ............·..................·466 本章小结 ·····..····..·····....····..····..······..·········468 第 10 章完整应用:简单 工作流系统...........................….469 10.1 项目背景及系统结构................470 10.1.1 应用背景 ······..····..······..··..····..··470 10.1.2 系统功能介绍 ..·........·..............470 10.1.3 相关技术介绍….........……........ 471 10. 1.4 系统结构 H ….........................…. 472 10.1.5 系统的功能模块................…… 473 10.2 Hibernate 持久层........................473 10.2.1 设计持久化对象 (PO) .........473 10.2.2 创建持久化类.......................… 474 10.2.3 映射持久化类… ................·......480 10.3 实现 DAO 层.......……..................485 10.3.1 DAO 组件的定义.........… ........·486 10.3.2 实现 DAO 组件 ........................492 10.3.3 部署 DAO 层…..… ............·....·..502 10.4 实现 Service 层 H ……....….......… "505 10.4. 1 Service 组件设计..................… 505 10.4.2 Service 组件的实现….........… "506 10.5 任务调度的实现...............… ·······516 10.5.1 Ql阳tz 的使用........................... 516 10.5.2 在 Spring 中使用 Qu缸但 ..........520 10.6 MVC 层实现...........…..............…'522 10.6.1 解决中文编码........… H … ........·522 10.6.2 Struts 与 Spring 的整合 ..·....·..·523 10.6.3 创建 Action ...........................… 524 10.6.4 异常处理...............…… ..........·..524 10.6.5 权限控制…................……........ 525 10.6.6 控制器配置 ........................·....·527 本章小结............……......................…..…..才30 •IX·本章要点 辛基 JDK 的下载和安装 > 环境变量的设置 没 Tomcat 的安装和配置 王瑞 在 Tomcat 中部署 Web 应用 被 Jettγ 的安装和配置 刻在 Jettγ 中部署 Web 应用 被 Eclipse 的安装 滋 Eclipse 插件的安装 J2 EE 应用以其稳定的性能、良好的开放性及严格的安全性,深受企业应用开 发者的青睐。实际上,对于信息化要求较高的行业,如银行、电信、证券及电子 商务等行业,都会选择使用 J2EE 作为企豆豆的信息乎台。 对于一个企业而言,选择 J2EE 构建信息化平台,更体现了一种长远的规划: 企业的信息化是不断整合的过程,在未来的日子里,经常会有不同乎台、不同系 统的异构系统需要整合) J2EE 应用提供的跨平台性、开放性及各种远程访问的技 术,为异构系统的良好整合提供了保证。 本书介绍的不是基于 EJB 的J2EE 应用的开发,因为 EJB 应用的开发周期过 长,且必须运行在J2EE 容器中 3 而本书介绍的轻量级J2EE 应用,完全可以运行 在 Web 容器中,无需 EJB 容器的支持,但其应用的稳定性及效果都可以得到保证已轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 1.1 JDK 的下载和安装 J2 EE 应用的开发及运行都离不开 JDK 的支持。虽然 Java 程序是跨平台的,但 JDK 不是跨平台的。因此,在不同的平台上需要安装不同的 JDK 。下面分别介绍在 Windows 和Li nux 下 JDK 的安装。 1.1.1 Windows 下 JDK 的下载和安装 目前, JDK 主要有如下三个版本。 J2SE: Java 标准版本,包括开发桌面应用的系列类库。 J2EE: 包含 Java 标准版,还增加了企业应用开发所需的类库。 J2ME: Java 2 平台微型版,被使用在资源受限制的小型消费型电子设备上。 本书介绍的是J2EE 应用的开发,推荐使用J2EE 的 JDK。下载和安装请按如下步骤 进行。 (1)登录 http://www.sun.com站点,根据所使用的操作系统,选择J2EE 的最新版本, 笔者使用的是j2eesdk-1_4_02_2005Q2,推荐读者也使用该版本的J2EE 。 下载完成后,得到一个名为j2eesdk-C 4_02_2005Q2-windows-m1 .exe 的可执行文件, 该文件就是J2EE 的安装文件。 (2)双击下载的可执行性文件,出现如图1. 1 所示的安装向导对话框,表明 JDK 开 始安装。 图1.1 J2EEJDK 安装界面 (3)安装过程与安装其他 Windows 软件并没有太大的不同,同样是多次单击【下一 步】按钮,需要选择安装路径等步骤。笔者建议不要修改 JDK 的安装路径,若要修改也 仅仅修改其盘符,程序出现如图1. 2 所示的安装路径选择对话框。 笔者将安装路径选择在 D 盘(因为笔者将所有的工具软件都放在 D 盘),而建议不 要修改后面的 Sun'飞AppSever 路径。直到出现如图1. 3 所示的对话框。 2J2EE 应用运行及开发环境的安装与配置 图1.2 安装路径选择界面 图1. 3 管理员口令窗口 (4) 选择"提示输入管理员用户名"单选框,然后在口令和重新输入口令的密码框 中输入两个系统的口令,口令的长度最少必须为 8 个字符。然后单击【下一步】按钮, 在下一个对话框中再次单击【下一步】按钮,程序开始安装。 (5) 安装成功后,增加编译和运行必需的环境变量。编译和运行 Java 程序必须增加 CLASSPATH 环境变量。在编译和运行 Java 程序时,需要 JOK 的系统类,如 java .l ang.String 等, Java 程序会根据 CLASSPATH 环境变量指定的路径搜索这些类。因此该环境变量就 是系列的搜索路径,编译和运行J2EE 的应用主要需要如下三个 jar 文件: • Sun飞AppServer\j dk\l ib\tools.jar • Sun'飞AppServer\j dk\l ib\dt.jar • Sun\AppServer\lib\j2ee.jar 这些 jar 文件表面看起来是一个文件,其实是一系列的路径,选择WinRAR 文件打 开其中任意一个文件,看到如图1.4 所示的窗口。 3轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 接着,在系统中增加 CLASSPATH 的环境变量,并将这三个文件的路径添加进去, 添加的方法如下:在桌面上"我的电脑"图标上单击右键,出现右键菜单,单击"属性" 菜单项,出现"系统特性"对话框,单击该对话框的"高级"选项卡,出现如图1. 5 所 示的对话框。 transaction i与阳 viet L二JsecLKity i二Jreso四 ce wm田 00阳ent 二二 mail :.:.J im, wenterprise l.Jejb 资料夹 资剌夹 资料夹 资制夹 资制夹 资料夹 资料夹 资刺夹 资剌夹 图 1 .4 j2ee.jar 文件的内部结构 图1. 5 【系统特性】对话框 单击图1. 5 中的【环境变量】按钮,将弹出如图1. 6 所示的【环境变量】对话框。 图1. 6 环境变量设置对话框 单击图1. 6 中的【新建】按钮,出现如图1. 7 所示的对话框,读者可以看到增加的 CLASSPATH 环境变量已将 d t.jar , tools.jar, j2ee.jar 三个文件设置到该环境变量中。因为 Windows 多个环境变量中的间隔符号是";",所以多个路径之间以英文分号隔开。增加 后效果如图1. 7 所示。 4J2EE 应用运行及开发环境的安装与配置 图1. 7 增加 CLASSPATH 环境变量后的效果 用户在编译和运行 Java 程序时,需要用到 java 和 javac 两个命令。由于 Windows 对于外部命令,都按 PATH 环境变量指定的路径搜索可执行性程序,因此为了可以执行 java 和 javac 等命令,应将 java 和 javac 所在的路径添加到 PATH 中。 java 和 javac 的路 径为 0: 飞Sun\AppServer\j dk\bin ,通常系统已经有了 PATH 环境变量,因此只需将该路径 添加到 PATH 变量中即可。 按上面的步骤修改 PATH 环境变量,修改后的效果如图1. 8 所示。 图1. 8 PATH 环境变量的设置 注意:在两个环境变量的设直中,都包含了一个" "的路径,这个" "代表系统 的当前路径,如果没有增加该路径,可能导致运行 Java 程序时, class 文件已在当前路径, 但在系统提供的文件中找不到该文件。 1.1.2 Linux 下 JDK 的下载和安装 Linux 下 JOK 的下载和安装与 Windows 下并没有太大的不同,只是对一些环境的设 置稍有不同。下载和安装Li nux 下的 JOK 请按如下步骤进行。 (1)登录 http://www.sun.com 站点,同样选择 JOK 的 j2eesdk-l_4_02_2005Q2 的版 本,区别只是在下载Li nux 版本成功后,得到的是 j2eesdk-l_4_02_2005Q2-linux-m1.bin 文件。 (2) 进入 j2eesdk- L 4_02_2005Q2-1inux-m1 .bin 所在的路径,执行如下命令: chmod 777 j2eesdk-l_4_02_2005Q2-1inux-ml.bin 该命令修改j2eesdk-L 4_02_2005Q2-linux-m1 .bin 文件为可执行文件,然后输入如下 命令: .j j2eesdk-l_4_02_2005Q2-1工 nux-ml.bin 按回车键后,出现与图1.1 类似的【安装向导】对话框。 (3)等待安装结束后,设置环境变量。环境变量的设置与在Windows 下完全相同, 5轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 只是设置的方式稍有区别(以 RedHat Linux Fedora Core 4 为例)。 (4) 登录Linux 系统后,打开 shell 窗口并输入如下命令: cd (5) 该命令将进入当前用户的home 路径,然后在home 路径下输入如下命令: Is -a (6) 该命令将列出当前路径下所有的文件(包括隐藏文件),环境变量的设置是通 过.bash_profile文件设置的。 (7)使用无格式编辑器编辑该文件来增加CLASSPATH环境变量,并修改PATH 环 境变量,修改后的.bash_profile 文件如下(文件中以#开头的行是注释): # Get the aliases and functions #如果 .bashrc 文件存在,执行该文件的内容 #因此,也可以在.bashrc 文件中设置变量 if [ -f -/.bashrc ]; then -/ .bashrc fi # User specific environment and startup programs #下面用于设置环境变量 #JAVA_HOME环境变量是 Tomcat 运行需要的环境变量 JAVA_HOME=/home/yeeku/su口 /jdk #其中 $JAVA_HOME. 表示引用宏变量,第二条路径实际就是 # /home/yeeku/sun/jdk/bin PATH=.:$PATH:$HOME/bin:$JAVA_HOME/bin #设置 CLASSPATH环境变量 CLASSPATH=.:$JAVA_HOME/lib/dt.jar:JAVA_HOME/lib/tools.jar #导出环境变量 export ANT_HOME export JAVA_HOME export CLASSPATH export PATH unset USERNAME export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH (8) 在上面中的环境变量中,路径之间以"."分隔,因为Linux 以"."作为路 径分隔符。 (9) 重新登录,或者执行如下命令: source .bash-profile 两种方式都是为了运行该文件,使文件中关于环境变量的设置起作用。 1.2 Tomcat 的下载和安装 Tomcat 是 Java 领域最著名的开源 Web 容器,简单、易用且稳定性极好。既可以作 为个人学习之用,也可以作为商业产品发布。 Tomcat 不仅提供了 Web 容器的基本功能,还支持 JAAS 和 JNDI 绑定等。 6发环的装与配置 E 1.2.1 Tomcat 的下载和安装 因为 Tomcat 完全以 Java 编写,因此与平台无关,既可以运行在 Windows 平台上, 也可以运行在Li nux 平台上。两个平台上的安装和配置也基本相同,只是环境变量的设 置稍有差别而己,关于Li nux 下环境变量的设置请参考 1.1.2 节。本节将以 Windows 平 台为例。 下载和安装 Tomcat 按如下步骤进行。 (1)登录 http://tomca t. apache.org 站点,下载 Tomcat 合适的版本,如果使用 JDK 1. 4 , 则建议使用 Tomcat 5.0.x 系列,而不是使用 Tomcat 5.5.x 系列。 Tomcat 5.0.x 目前最新的稳定版本是 5.0.28 ,建议下载该版本。在 Windows 平台下 载 zip 包, Linux 下载阳包。建议不要下载其安装文件。 (2) 解压缩刚下载到的压缩包,解压缩后应有如下文件结构。 • bin: 存放启动和关闭 Tomcat 的命令的路径。 • common: 存放所有的 Web 应用都需要的类库等。 • conf: 存放 Tomcat 的配置,所有的 Tomcat 的配置都在该路径下设置。 • log: 这是一个空路径,该路径用于保存 Tomcat 每次运行后产生的日志。 • server: 存放 Tomcat 运行所需要的基础类库,该路径是 Tomcat 运行的基础。该路 径下还包含一个 webapps 路径,并存放 Tomcat 两个控制台。 • shared: 该路径也是一个空路径,用于系统共享的类库,该路径下包括 classes 和 lib 两个路径,其中 classes 用于存放 class 文件,而 lib 用于存放 J缸文件。 • temp: 保存 Web 应用运行过程中生成的临时文件。 • webapps: 该路径用于部署 Web 应用,将 Web 应用复制在该路径下, Tomcat 会将 该应用自动部署在容器中。 • work: 保存 Web 应用运行过程中编译生成的 class 文件。该文件夹可以删除,但 每次应用启动时将自动建立该路径。 • LICENSE 等相关文档。 (3)将解压缩后的文件夹放在到任意路径下。 (4) Tomcat 的运行需要一个环境变量:JAVA_HOME。不管是 Windows 还是 Linux , 只需要增加该环境变量即可,该环境变量的值指向JDK 安装路径。 (5) 启动 Tomcat,对于 Windows 平台,只需要双击Tomcat 安装路径下 bin 路径中 的 startup.bat 文件即可。对于Linux,进入 Tomcat 安装路径的 bin 路径下,然后运行如 下命令: chmod 777 startup.sh 该命令将 startup血文件变成可执行性文件,接着用如下命令运行startup.sh 即可: ./startup.sh 启动 Tomcat 之后,打开浏览器,在地址栏输入http://localhost:8080,然后回车,浏 7轻量级 J2EE 企业应用实战一一-Struts+Spring+Hibernate 整合开发 览器出现如图1. 9 所示界面,即表示 Tomcat 安装成功。 It you're … Ing 刷'p啕. via.web browser,It means you'y…!up Tomcat lucc.,Wlly. Congratulatlonsl As you mayh制e guessed by now, this is the default Tomcet hOme page 忧 can be found on the local filesystem at $CA.TALINA_HOME/、帽bapps!ROOTIindeK. jsp where "$CATAUNA_HOME" is !tIeroot aftha Tomcat installation directory , If you're seeing this page, and you don't thinkyotl 由 auld be, then ωtheryou're .,阳 rauser V'而 o has arrived at new inst甜甜。 n ofTomcat. oryou're an administrator" 由 ohasn 飞 got 川sJh erseωp quite right Providing the 阳往'eristhe case , please refer to [he Tm\~itt且且~Uloon皿.QO for more detailed saωρand administration information than is found in the INSTALL file NOlE: For.,curtly rea.onl,utlng the admln惰'罩tlon webapp II restrlc徊dto use"'W忧h role "admln". The manager 呐"bapp II r..trtcted to U"I'I W时1 role "manager", Users are defined in $CATALINA HOMEleontl飞。meat-users. Kml 图1. 9 Tomcat 安装成功的界面 1.2.2 Tomcat 的基本配置 Tomcat 作为一个 Web 服务器,默认的服务端口是 8080 ,但该端口完全可以自己控 制。虽然 Tomcat 是免费的 Web 服务器,但也提供了两个图形界面的控制台。用户可以 使用控制台方便地部署 Web 应用、配置数据源及监控服务器中的 Web 应用等。 下面介绍如何修改 Tomcat 的 Web 服务端口,并进入其控制台来部署 Web 应用。 1. 修改端口 Tomcat 的配置文件都放在 conf 路径下,控制端口的配置文件也放在该路径下。打 开 conf 下的 se凹er.xrnl 文件,务必使用记事本或 vi 等无格式的编辑器,不要使用如写字 板等有格式的编辑器。在定位 se凹er.xrnl 文件的 92 行处看到如下代码: 其中 port="8080",就是 Tomcat 提供 Web 服务的端口。将8080 修改成任意的端口, 建议使用 1000 以上的端口,避免与公用端口冲突。笔者将此处修改为8888,即 Tomcat 的 Web 服务的提供端口为8888 。 修改成功后,重新启动Tomcat,在浏览器中输入http://loca1host:8888,回车将再次 看到如图1.9 所示的界面,即显示Tomcat 安装成功的界面。 2. 进入控制台 在图1.9 中的红色标识处,显示有两个控制台:一个是Administration控制台:另一 8发环的 与配置 I~ 个是 Manager 控制台。红色标记的是 Administration 控制台,下面是 Manager 控制台, 单击 Administration 控制台将出现如图1. 10 所示的登录界面。 图 1.10 Tomcat Administration 控制台登录界面 单击 Manager 控制台,将出现如图1. 11 所示的登录界面。 图 1.11 Tomcat Manager 控制台登录界面 两个控制台都需要增加用户名和密码才可以登录,这两个控制台都是通过 Tomcat 的 JAAS 控制的,下面介绍如何增加这两个控制台的用户和密码。 回忆前面关于 Tomcat 文件结构的介绍, server/webapps 下存放这两个控制台的 Web 应用,先进入 server/webapps/manager 路径下,该文件夹对应了 Manager 控制台。再进 入该 Web 应用的 WEB-INF 路径下,该路径存放了 Web 应用的配置文件,用无格式编辑 , 9轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 器打开 web.xml 文件。在该文件的最后部分,看到如下配置片段: <~-- 确定登录该应用所需的安全角色一〉 The role that is required to log in to the Manager Applicatio丑 manager 通过查看该文件可知, Manager 控制台需要 manager 角色才可以登录。 同样,在 server/webapps/adminIWEB-INF 路径下,打开 web.xml 文件,在该文件的 最后部分,发现如下代码: <'… FORM 表明使用表单提交的方式登录一〉 FORM Tomcat Server Configuration Form-Based Authenticat工 0口 Area adm工且 通过查看该文件可知, Administration控制台需要 admin 角色才可以登录。下面将增 加 Tomcat 的用户, Tomcat 的用户通过 conf 路径下的 tomcat-users.xml文件控制,打开该 文件,发现有如下代码: 10J2EE 应用运行及开发环境的安装与配置 国 <'… 配置第二个用户,名为rolel. 密码为 tomcat. 角色为 rolel--> 系统的配置文件默认提供了三个用户,但这三个用户没有一个是admin 角色,也没 有一个属于 manager 角色。在这里增加manager 和 admin 两个用户,增加后的配置文件 代码如下: <'… 配置登录 Administration控制台的用户 admin .密码也是 admin--> 再次重启 Tomcat,进入 Administration 控制台,在用户名和密码的文本框中输入 admin 和 admin (刚才在 tomcat-users.xml 文件中配置的),单击【登录】按钮,将进入 Administration控制台,如图1.12 所示界面。 '蝇 Tome翩 s.睛, ""Serviet(e.aline) @函'司&Source雹 eo嗣 Soure回 国 MailSllssions ..岳阳ronment E网".. IItu阳 Dalabases , 12且JSlr Defin~ion 公 Unrs 句 Oro叩S 幡 Roles 图1.12 Tomcat 的 Administration控制台 在该控制台的左边,可看到关于DataSource, MailSessions, U serDifintion 等项,这 表明可通过控制台配置数据源、邮件 Session 及 Tomcat 用户等。该控制台的使用相当简 单,此处不再赘述。 现在也可以进入 Manager 控制台了,在弹出的登录对话框的用户名和密码框中分别 11轻量级、J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 输入 manager 和 manager ,即可登录 Manager 控制台,看到如图1. 13 所示的界面。 图1. 13 Tomcat 的 Manager 控制台 该控制台可监控部署在该服务器下的所有 Web 应用:左边的红色区标识出了所有部 署在该 Web 容器内的全部应用:右边的红色区标识出了对 Web 应用的控制,包括启动、 停止及重启等;而下面的红色标识区,则用于部署 Web 应用。 3. 部署 Web 应用 在 Tomcat 中部署 Web 应用的方式非常多,主要有如下方式: ·使用控制台部署: ·利用 Tomcat 的自动部署: ·修改 server.xml 文件部署 Web 应用: .增加用户的 Web 部署文件。 通过控制台的部署方式实质上和修改 se凹er.xml 文件的部署方式相同。所有在控制 台作的修改,最终都由服务器转变为修改 server.xml 文件。 笔者不推荐采用修改 server.xml 的配置方式。因为 server.xml 文件是一个系统文件, 通常对于系统的文件应尽量避免修改,可通过增加自己的配置文件即可。 下面主要介绍用自动部署来增加用户的 Web 部署文件。 自动部署非常简单,只需将 Web 应用复制到 Tomcat 的 webapps 路径下, Tomcat 就 会自动加载该 Web 应用。 增加用户的 Web 部署文件后,为了避免复制 Web 应用,只需简单地增加一个配置 文件即可。进入 Tomcat 的 cont\Catalina飞localhost 路径下,该路径下默认有三个配置文件, 分别对应系统自带的三个 Web 应用(包括两个控制台应用)。 复制三个文件中的任意一个,假设复制了 balancer.xml 文件,并将该文件重命名, 12发环的装与配置~ 重命名的文件名并不重要,但为了更好的可读性,建议使该文件的文件名与部署的 Web 应用同名。 打开该文件,发现如下内容: 其中,每个 Context 元素都对应一个Web 应用,该元素的path 属性确定 Web 应用的 虚拟路径,而docBase 则是 Web 应用的文档路径。 假如 E:/webroot 是一个 Web 应用,若想将该应用部署在Itest 虚拟路径下,只需将该 文件的内容作如下修改: maxIdle 30 maxWait lOOOO user口ame root password 32147 driverClassName com.mysql.jdbc.Driver 〈口ame>url jdbc:mysql:lllocalhost:3306/j2ee?autoReconnect=true 再次启动 Tomcat,该 Web 应用即可通过JNDI 访问数据源,下面是访问该数据源的 代码: II 初始化 Context. 使用工nitialContext初始化 Context Context ctx=new 工 nitialContext(); 1* 通过 JNDI 查找数据源,该JNDI 为 java:comp/env/jdbc/dstest.分成两个部分 java:comp/env是 Tomcat 固定的. Tomcat 提供的 JNDI 绑定都必须加该前缀 jdbc/dstest是定义数据源时的数据源名 *1 DataSource ds=(DataSource)ctx.lookup("java:comp/env/jdbc/dstest"); II 获取数据库连接 Connection conn=ds.getConnection(); II 获取 Statement Statement stmt=conn.createStateme口t (); II 执行查询,返回ResulteSet对象 ResultSet rs=stmt.executeQuery("select * from newsinf"); while(rs.next()) 上面配置了局部数据源,如还需要配置全局数据源,可通过Administration控制台, 或者修改 server.xm1配置文件来实现。 通过控制台的配置方式相当简单,因为所作的任何修改,最终依然会变成对 se凹er.xm1文件的修改。因此笔者建议使用直接修改server.xm1的方式。 注意:使用全局数据源会破坏Tomcat 原有的配直文件。 在 se凹er.xm1文件中找到GlobalNamingResources元素,该元素下负责配置所有的全 局资源,因此只需在该元素增加数据源的配置即可。增加数据源配置的配置片段如下: <1 一配置全局命名资源一〉 <1- 配置一个资源,资源的名称为jdbc/dstest. 类型为 DataSource 数据源--> 15轻量级 J2EE 企业应用实战一-St阳ts+Spring+Hibernate 整合开发 factory org.apache.commons.dbcp.BasicDataSourceFactory maxActive 100 removeAbandonedTimeout 60 maxldle 30 maxWait 10000 username root password 32147 driverClassName com.mysql.jdbc.Driver 16 url jdbc:mysql://localhost:3306/j2Be?autoReconnect=true 通过这种方式配置的数据源可以被所有的Web 应用访问。J2EE 应用运行及开发环境的安装与配置 1.3 Jetty 的下载和安装 Jetty 是 Java 领域另一个出色的 Web 服务器,这个服务器同样是开源项目。相对于 Tomcat, Jetty 有更大的优点一一可作为一个嵌入式服务器,即如果在应用中加入 Jetty 的 Jar 文件,则应用可在代码中对外提供 Web 服务。 下面主要介绍 Jetty 的下载、安装和基本配置。 1.3.1 Jetty 的下载和安装 Jetty 也是与平台无关的 Java Web 服务器,既可以在 Windows 平台上运行,也可以 在 Linux 平台上运行,下载和安装 Jetty 请按如下步骤进行。 (0 登录 http://jetty.mortbay.comljetty /index.htm1站点,下载 Jetty 的最新版本。笔者 成书之时, Jetty 的最新版本是 jetty-6.0.0rcO ,下载 jetty-6.0.0rcO 且p 文件,该文件是与平 台无关的压缩包,不管是 Windows 还是Linux 都可使用该压缩包。 (2) 解压缩 jetty-6.0. OrcO.zip 文件,应得到如下的文件结构。 • etc: 该路径用于存放 Jetty 的配置文件。 • examples: 该路径用于存放 Jetty 的示例。 • legal: 该路径用于存放该项目的 Lisence 信息。 • lib: 该路径用于存放运行 Jetty 必需的 J缸文件。 • modules: 该路径用于存放 Jetty 的模块,包括 API 文档。 • patches: 包含一些补丁说明。 • pom.xm1:是 Jetty 的 build 文件,该文件不是 Ant 的 build 文件,而是 mavaen2 的 build 文件。 • project-site: 包含 Jetty 的网站的必需的样式文件。 • readme.txt: 包含最基本的使用信息。 • start.jar: 启动 Jetty 的启动文件。 • version.txt: Jetty 版本更新日志的简单版本。 • webapps: 该路径用于存放自动部署的 Web 应用,只要将用户的 Web 应用复制到 该路径下, Web 应用将自动部署。 • webapps-plus: 存放一些用于演示 Jetty 扩展属性的 Web 应用,该路径下的 Web 应用也可自动部署。 (3)将解压缩后的文件放在任意路径即可。运行 Jetty 需要使用如下命令: java -jar start.jar 建议将上面命令写成脚本,如在Windows 下写成批处理命令,Linux 下写成一个 shell 脚本。每次直接运行其脚本文件即可。 运行成功后,启动浏览器并在地址栏输入http://localhost: 8080/,然后回车,出现如 17轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 图1. 14 所示的界面,即表示 Jetty 安装成功。 Error 404 - Not Found. No context on this server matched or h四dIed this request. Contexts known to this server are: • Li笠旦oc 二二"\ Cor豆豆口日 andler@15a0305{/i3vadoc. file:lD: liettv­ fi. O. OrcO/modules/iettv/t <'1l'时吃 Isite/aDidocs/} ·/test 一-\COJ吐白 tHandlerif!l '{ 1. 配置 Jetty 服务端口 Configure 元素里的各种子元素,即对该Server 实例的操作。在Configure 元素下有 如下代码所示的 Set 子元素, Set 子元素的 name 属性为 connectors,效果等同于调用 setConnectors 方法,用于设置Web 服务的提供端口。该方法需要Connector 数组,其包 含的 Array 子元素则用于设置该方法的参数。Array 元素里的 Item 子元素,则是数组的 数据项,每个Connector对应一个连接提供者。 <'一类似于调用setConnectors方法--> 〈工 tern> 8080 30000 3000 l 8081 50000 --> 8443 30000 /etc/keystore OBF:lvnylzlolx8elvnwlvn61x8g1zlulvn4<1Set> OBF:lu2ulwmllz7s1z7alwnllu2g --> 在上面的配置片段中,默认第一个Connector 是有效的,该Connector就是常规 Web 服务的 Connector,其中的 8080 就是 Jetty 的默认端口。 笔者将该片段修改如下: <' 下面的 Connector提供常见的 Web 服务 > 〈工 tern> 8886 30000 3000 l 修改成上面所示的样例后, Jetty 的服务端口为 88860 这也是笔者所使用的端口。 2. 部署 Web 应用 Jetty 也支持自动部署和配置文件部署。 如果使用默认的配置文件启动,webapps 会自动部署目录。即所有存放在webapps 路径的 Web 应用将自动部署在Jetty 容器中。 如果使用带Jetty 扩展功能来启动,~P 增加 jetty-plus.xrnl文件来启动,则webapps-plus 也会自动部署目录,将所有放在该路径的Web 应用自动部署在Jetty 容器中。 下面看如何使用配置文件来部署Web 应用。 部署 Web 应用需使用 or咆g.mortba叮y予.扣t即ty.we讪ba叩pp.We由bAppCo∞nt优ex刘t ,该类的实例即对应 一个 Web 应用,并且该类还包含多个静态的重载方法:addWebApplications。该方法用 于同时部署多个Web 应用,即用于配置一个自动部署目录。 jetty.xrnl 配置文件的片段如下: <1 一 下面用于为方法传入参数--> <'一 指定自动部署目录一〉 ./webapps 20-行开发环境内置 E /etc/webdefault.刀nl <'一 指定自动部署目录--> ./webapps-plus org/mortbay/jetty/webapp/webdefault.xml <'一是否解压缩 > True False 通过查看该配置文件不难发现,在每次调用addWebApplications方法后,即可增加 一个 Web 应用的自动部署路径。如有必要,用户完全可以增加自己的自动部署路径,如 果增加了自动部署路径,则所有在该路径下的Web 应用将自动部署。 如果仅需要部署一个Web 应用,可以有如下两种方法: ·修改 jetty.xml 文件。 ·增加自己的配置文件。 根据前面的介绍,对于Web 服务器,应尽量避免修改默认的配置文件。如果读者真 需要通过修改jetty.xml 文件来部署 Web 应用,则应在jetty 的 Configure 元素下增加如下 片段: G:/StrutsTest/js / /etc/webdefault. 刀nl localhost 21轻量级 J2EE 企业应用实战 Struts+Spring+Hibernate 整合开发 <'一设置 Sess工 on 的超时时长-> 600 注意:该代码片段在jetty.xml 文件仅仅被注释,只要取消该代码片段注释即可。但 需要注意: jetty.xml 文件默认有个小错误,它的设直超时时长的Set 元素的 name 属性值 为 maxInactivelntervale。实际上 HashSessionManager并没有 setMaxInactiveIntervale方法, 通过查看 API 文档发现,它包含一个setMaxInactiveInterval方法(最后少一个e) ,读者 将原有的 e 删除即可。 通常建议增加自己的配置文件,应尽量避免修改系统原有的配置文件。 增加的配置文件如下: <1- 三个构造参数 > G:/StrutsTest/js <1-- 设置 Web 应用的 url--> / /etc/webdefault .xml 〈工 tem>localhost <1-- 类似于调用 getSessionHandler方法…〉 600 将该配置文件保存在etc 路径下,以后每次启动Jetty 时,可直接加载该配置文件, 使用如下启动命令即可(假设该配置文件的文件名为jetty-yeeku.xml): java -jar startup.jar etc/jetty.xml etc/jetty-yeeku.xml 22J2EE 应用运行及开发环境的安装与配置 电命 垂 3. 配置 JNDI 绑定 Jetty 同样可以整合 DBCP 、 C3PO 等数据源来提供容器管理的数据源。提供容器管理 的数据源,只是 Jetty JNDI 绑定功能之一。 下面介绍如何在 Jetty 绑定 JNDI,以及 JNDI 的使用。 增加 JNDI 绑定必须使用 Jetty 的 plus 功能。因此,启动时必须增加 jetty-plus.xml 文件。增加 JNDI 的绑定同样有两个方法: ·修改系统默认的 jetty.xml 文件。 ·增加自己的配置文件。 两种配置方式大同小异,区别是前者需要修改系统默认的配置文件,此处仅介绍增 加自己的配置文件方式。 在 Jetty 的 plus 中,有如下包。 or唔g.mo创r口tba町y予.扣t町ty抖u山1附s.namin吨1沼g: 执行 JNDI 绑定的包。 该包下有如下四个类。 EnvEntry: 绑定简单值。 NamingEntry: 抽象类,是另外三个类的父类。 Resource: 用于绑定数据源等资源。 Transaction: 用于绑定事务。 增加数据源绑定请按如下步骤进行。 (1)此处绑定的数据源依然以 DBCP 为实现,当然也可以绑定 C3PO 数据源,但必 须将 DBCP 所需要的 jar 文件复制到 Jetty 可以使用的路径中。根据前面介绍 DBCP 主要 需要如下三个文件: • commons-dbcp.jar • commons-pool.jar • commons-collections.jar 将这三个文件复制到 Jetty 的 lib 路径下即可, Jetty 启动时会自动加载该路径的 jar 文件。当然,还需将数据库驱动复制到该路径下。 (2) 增加如下配置文件: <'一 Jetty 配置文件的根元素一〉 wogg1e 4000 jdbc:mysql://localhost:3306/j2ee <1 一设置数据库用户名--> root 1000 <1 一 设置数据库是否自动提交一〉 true true 60 true <1 一 将实际的数据源绑定到jdbc/mydatasource这个 JNDI 名…〉 jdbc/mydatasource 在上面的配置文件中,绑定了三个JNDI 值,下面测试该JNDI 的 Se凹let: public class TestServlet extends HttpServlet 工 nitialContext ic; //Servlet 的初始化方法,该方法完成Context 的初始化 public void init(ServletConfig config) throws ServletException super.init(conf工 g) ; try ic = new InitialContext(); catch (Exception e) { throw new ServletException(e); //serv工 ce 方法是 Servlet 的服务方法 public void service(HttpServletRequest request , HttpServletResponse response) throws ServletException,工OException II 获取 JSP 页面输出流 PrintStream out = new Pr工 ntStream(response.getOutputStream()); tryJ2EE 应用运行及开发环境的安装与配置 国 II 在控制台输出 w工 ggle 的绑定值 System.out.printl口 (ic.lookup("wiggle")); /I:在控制台输出 woggle 的绑定值 System.out.pr 工口 tl 口 (ic .lookup ("woggle") ); II 获取绑定的数据源 DataSource ds = (DataSource)ic.lookup("jdbc/mydatasource"); II 通过数据源获取数据库连接 Co口口ection conn = ds.getConnection(); II 通过数据库连接创建Statement对象 Statement stmt = conn.createStatement(); II 通过 Statement对象执行 SQL 查询,返回 ResultSet对象 ResultSet rs = stmt.executeQuery("select * from news"); II 遍历记录集 wh工 le(rs.next()) out.pr工 ntl口 (rs.getString(2)); } catch (Exception e) e.printStackTrace() ; 在 web.xml 文件中增加如下片段: aa lee.TestServlet aa /aa 启动 Jetty ,访问该 Se凹let ,即看到数据库的访问结果。 1.4 Eclipse 的安装和使用 Eclipse 是一个免费的 IDE (集成开发环境)工具,它支持多种开发语言,并不仅仅 用于 Java 应用的开发。在免费的 Java 开发工具中, Eclipse 是最受欢迎的。 Eclipse 本身所提供的开发功能非常有限,但它的插件则大大提高了它的功能。 Eclipse 的插件非常多,比如 Synchronizer, Lomboz, MyEclipse 等。下面就简单介绍 Eclipse 的安装和使用。 1.4.1 Eclipse 的下载和安装 登录 http://www.eclipse.org 站点,下载 Eclipse 的最新版本。Ec lipse 当前的最新版本 25轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 Feature 情"'''M choose the way you w5lt to sc.:rch For Fed:ures to install 是 3.2 ,笔者建议下载 Eclipse 3.20 根据所使用的操作系统,在 Windows 平台上下载 eclipse-SDK-3.2-win32 且 p; 在Li nux 平台上下载 eclipse-SDK-3.2-1inux-gtk.tar.gz 文件。将下载到的压缩文件解压缩,解压缩后 的文件夹可放在任何目录中。 直接双击 eclipse.exe 文件,即看到 Eclipse 的启动界面,表明 Eclipse 已经安装成功。 1.4.2 Eclipse 插件的安装 Eclipse 本身的开发能力非常有限,但它的插件功能非常强大。 Eclipse 的插件的安装 方式分为如下两种: ·在线安装。 ·于动安装。 1. 在线安装 在线安装简单方便,适合网络畅通的场景。在线安装是个较好的安装方式,请按如 下步骤进行。 (1)单击 "Help" 菜单,然后将光标移动到 "Software Updates" 菜单项上,单击 Software Updates 菜单项的 "Find And Install" 子菜单项,如图1. 15 所示。 (2)此时将弹出如图1. 16 所示的对话框,该对话框用于选择安装新插件,或升级已 有插件。该对话框中有两个单选按钮:上面一个用于升级已有插件:下面一个用于安装 新插件。 图1.1 5 在钱安装插件的菜单 图1.1 6 选择升级或安装新插件 (3)如果需要升级己有插件,则选择第一个单选框,单击【Finish 】按钮即可,等 待 Eclipse 完成升级。 (4) 如果需要安装新插件,则选择第二个单选按钮,【Next 】按钮将变成可用,单击 【 Next 】按钮,弹出如图1. 17 所示的对话杠。 26J2EE 应用运行及开发环境的安装与配置 捆擅自 Updates比 estovis位 Select update sites to v耐 while lool 第」个 JSP 页面 〈宅 for(int i = 0 ; i < 10; i++) { out.print1n(i) ; 毡〉
〈毡}在〉 当启动 Tomcat 之后,可以在 Tomcat 的 Catalina\localhost\j sptest'飞org飞apache\j sp 目录 下找到如下文件(假如Web 应用名为 jsptest,上面 JSP 页的名为 test1扣p): test1.jsp.java 和 testtjsp.class 。这两个文件都是 Tomcat 生成的, Tomcat 根据 JSP 页面生成对应 Servlet 的 Java 文件及 class 文件。 下面是 test1.j sp.java 文件的源代码,这是一个特殊的 Java 类,是一个 Se凹 let 类: //JSP 页面经过 Tomcat 编译后默认的包 package org.apache.jsp; import javax.serv1et.*; 33轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 34 import javax.servlet.http.*; import javax.servlet.jsp.*; II 继承 HttpJspBase类,该类其实是个Servlet 的子类 public final class testl_jsp extends org.apache.jasper.runtime.HttpJspBase implements org.apache.jasper.runtime.JspSourceDependent private static java.ut工 I.Vector _jspx_dependants; public java.util.List getDependants() { return _jspx_dependants; } II 用于响应用户的方法 publ 工 c void _jspServ工 ce(HttpServletRequest request , HttpServletResponse response) throws java.io.IOException, ServletException II 获得页面输出流 JspFactory _j 日 pxFactory = null; PageContext pageContext = null; HttpSession session = null; ServletContext application = null; ServletConfig config = null; II 获得页面输出流 JspWriter out = null; Object page = this; JspWriter _jspx_out = null; PageContext _jspx-page_context = null; II 开始生成响应 try _jspxFactory = JspFactory.getDefaultFactory(); II 设置输出的页面格式 response.setContentType("text/html; charset=gb2312"); pageContext = _jspxFactory.getPageContext(this , request , response , null , true, 8192 , true); _jspx-page_context = pageContext; application = pageContext.getServletContext(); config = pageContext.getServletConf 工 g() ; sess 工 on = pageContext.getSession(); II 页面输出流 out = pageCo口 text.getOut(); _jspx_out = out; II输出流,开始输出页面文裆 out.write("\r\nil) i II 下面输出 HTML 标签 out.wr 工 te("\r\nil) ; out.write("\r\ 丑 n ); out.write ("\r\nn) ; out.write("first Jsp\r\n") ; out.write("\r\nil) ; out.write("\r\nil} ; II 页面中的循环,在此处循环输出 for(int i = 0 ; i < 10; i++) out.println(工) ; out.write("\r\n") ; out.write ( "
\r\ 丑") ;out.write("\r\ 口") ; out.write("c/BODY>\r\ 口 II) ; out.write("c/HTML>\r\ 丑") ; out.wr 工 te ("\r\ 丑") ; } catch (Throwable t) { if (! (t 工 nstanceof SkipPageExceptio口) ) { 传统表现层 JSP 国 out = _jspx_out; if (out ! = null && out.getBufferSize () ! = 0) out.clearBuffer() ; if (_jspx-page_context !=口ull) _jspx-page_context.handle PageException(t); } finally 工 f (_jspxFactory != null) _j 日pxFactory.releasePageContext(_jspx_ page_context) ;] } } 即使读者不了解上面提供的 Java 代码,也依然不会影 响 JSP 页面的编写,因为这都是由 Web 容器负责生成的。 图 2.1 显示了 testl.jsp 的执行效果。 根据图 2.1 的执行效果,再次对比 test 1.jsp 和 tes t1-.i sp 扣va 文件,可得到一个结论:该 JSP 页面中的每 个字符都由 test1-.i sp.java 文件的输出流生成。图 2.2 显示 了 JSP 页面的工作原理。 根据上面的 JSP 页面工作原理图,可以得到如下四个 结论: • JSP 文件必须在 JSP 服务器内运行。 图 2.1 t巳st l.jsp 的执行效果 • JSP 文件必须生成 Se凹let 才能执行。 ·每个 JSP 页面的第一个访问者速度很慢,因为必须等待 JSP 编译成 Se凹leta 图 2.2 JSP 页面的工作原理 35轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 • JSP 页面的访问者无须安装任何客户端,甚至不需要可以运行 Java 的运行环境, 因为 JSP 页面输送到客户端的是标准 HTML 页面。 JSP 的出现,大大提高了 Java 动态网站的开发效率,曾一度风靡 Java Web 应用开发 者。 2.2 JSP 注释 JSP 注释用于表明在程序开发过程中的开发提示,它不会输出到客户端。 JSP 注释的格式如下: 〈在 注释内容--%> 与 JSP 注释形成对比的是 HTML 注释, HTML 注释的格式是 看下面的 JSP 页面: 〈革@ page contentType="text/html; charset=gb2312" language="java" 革> 〈革 =info ()鲁〉 该页面的执行效果与前一个页面的执行效果没有区别。 2.5 JSP 脚本 JSP 脚本的应用非常广泛,可通过 Java 代码镶嵌在 HTML 代码中,即使用 JSP 脚本。 因此,所有能在 Java 程序中执行的代码,都可以通过 JSP 脚本执行。 看下面的代码: 〈毡@ page contentType="text/html; charset=gb2312" language="java" 奄〉 小脚本测试</T工 TLE> </HEAD> <BODY> <table bgcolor="9999dd" border="l"> <! -- Java 脚本,这些脚本会对HTML 的标签产生作用一〉 〈在 for(int i = 0 ; i < 10 ; i++) 毡〉 38舰层 JSP~ <!… 上面的循环将使<tr>标签循环一〉 <tr> <td>循环值 :</td> <td><毡 =i 苦 ></td> </tr> 〈革 屯〉 <table> </BODY> </HTML> 对于上面的 JSP 页面,其简单的循环将导致<tr/>标签循环 10 次,即生成一个10 行 的表格,并在表格中输出表达式值。 在浏览器中浏览该页面,浏览器中页面的源代码如下: <!DOCTYPE HTML PUBLIC "-IIW3CIIDTD HTML 4.0 TransitionalIIEN"> <HTML> <HEAD> <TITLE>小脚本测试<IT工 TLE> </HEAD> <BODY> <!… JSP 页面原有的 table 标签-→ <table bgcolor="9999dd" border="l"> <'一 下面的 10 行 tr 标签都是由 JSP 脚本控制生成的。--> <tr> <td>循环值 :</td> <td>O</td> </tr> <tr> <td>循环值 :</td> <td>l</td> </tr> <tr> <td>循环值 :</td> <td>2</td> </tr> <tr> <td>循环值 :</td> <td>3</td> </tr> <tr> <td>循环值 :</td> <td>4</td> </tr> <tr> <td>循环值 :</td> <td>S</td> </tr> <tr> <td>循环值 :</td> <td>6</td> </tr> <tr> <td>循环值 :</td> <td>7</td> </tr> 39轻量级 J2EE 企业应用实战一-Struts+Spring+Hibemate 整合开发 <tr> <td>循环值 :</td> <td>8</td> </tr> <tr> <td>循环值 :</td> <td>9</td> </tr> <table> </BODY> </HTML> 在 JSP 脚本中,可以完成 Java 代码中的任何功能,包括连接数据库和执行数据库操 作。看下面的 JSP 页面源代码: 〈革@ page contentType="text/html; charset=gb2312" language="java" 毡〉 〈屯@ page import="java.sql.食"屯〉 <lDOCTYPE HTML PUBL 工 C "-IIW3CIIDTD HTML 4.0 TransitionalIIEN"> <HTML> <HEAD> <TITLE> 小脚本测试 〈革 II 注册数据库驱动 Class. forName ("com.mysql. jdbc.Driver") ; II 获取数据库连接 Connection conn = DriverManager.getConnection("jdbc:mysql:lllocalhost:33061 j2ee" , "root" , "32147"); II 创建 Statement Statement stmt = conn.createStatement(); II 执行查询 ResultSet rs = stmt.executeQuery("select * from userinf"); 毡〉 〈宅 I l il!i历结果集 while(rs.next(» 毡〉 <屯 =rs.getString(2)革 > 〈毡 } 毡〉
<屯 =rs.getString(3}毡 >
上面的 JSP 页面在 Java 脚本中执行数据库查 询,并通过循环将查询结果输出到JSP 页面。此页 面的执行效果如图 2.3所示。 图 2.3 小脚本连接数据库的效果 40表现层 J叫~ 2.6 JSP 的三个编译指令 JSP 的编译指令是通知 JSP 引擎的消息,它不直接生成输出。编译指令都有默认值, 因此开发人员无须为每个指令设置值。 常见的编译指令有三个。 • page: 该指令是针对当前页面的指令。 • include: 用于指定如何包含另一个页面。 • tablib: 用于定义和访问自定义标签。 编译指令的格式如下: <%@编译指令名属性名="属性值"… b 下面主要介绍 page 和 include 指令,关于 taglib 指令,将在自定义标签库处详细讲解。 2.6.1 page 指令 page 指令,通常位于 JSP 页面的顶端,对同一个页面可以有多个 page 指令。 page 指令的语法格式如下: 〈屯 @page [language="Java"] [extends="package.class"] [import="package.class I package. *},…"] [session=吨 rue I false"] [buffer="none I 8kb I size kb" 1 [autoFlush="true I false"] [isThread8afe="true I false"] [info="text"] [errorPage="relativeURL"] [contentType= 呗ime飞'Pe[ ;charset=character8et]" I "text/html;char8et= 工 808859-1"] [工 sErrorPage=" true I false"] 毡〉 下面依次介绍page 的各个属性。 • language: 声明当前 JSP 页面使用的脚本语言的种类,因为页面是 JSP 页面,该 属性的值通常都是 java 。 • extends: 确定 JSP 程序编译时所产生的 Java 类,需要继承的父类,或者需要实现 的接口的全限定类名。 • import: 用来导入包,下面几个包是默认自动导入的,不需要显式导入。默认导 入的包有: java .l ang. 飞 javax.servlet.飞 javax.se凹let.jsp. 飞 javax.servle t. http 入 • session: 设定这个 JSP 页面是否需要 HTIP session 。 • buffer: 指定输出缓冲区的大小。输出缓冲区的 JSP 内部对象: out 用于缓存 JSP 页面对客户浏览器的输出,默认值为 8胁,可以设置为 none ,也可以设置为其他 41轻量级 J2EE 企业应用实战-一-Struts+Spring+Hibernate 整合开发 的值,单位为 kb 。 • autoFlush: 当输出缓冲区即将溢出时,是否需要强制输出缓冲区的内容。设置为 true 时为正常输出:如果设置为 false ,会在 buffer 溢出时产生一个异常。 • info: 设置该 JSP 程序的信息,也可以看做其说明,可以通过 Se凹 le t. getServletInfoO 方法获取该值。如果在 JSP 页面中,可直接调用 getServ letI nfoO 方法获取该值, 因为 JSP 页面的实质就是 Se凹 let 。 • eηorPage: 指定错误处理页面。如果本程序产生了异常或者错误,而该 JSP 页面 没有对应的处理代码,则会自动调用该指令所指定的 JSP 页面。使用 JSP 页面时, 可以不处理异常,即使是 checked 异常。 • isErroePage: 设置本 JSP 页面是否为错误处理程序。如果该页面本身己是错误处 理页面,则无须使用 errorPage 属性。 • contentType: 用于设定生成网页的文件格式和编码方式,即 MIME 类型和页面宇 符集类型,默认的 MIME 类型是 textlhtml; 默认的字符集为 ISO-8859-1 。 从 2.5 节中执行数据库操作的 JSP 页面中可以看出,在 script1 et2.jsp 页面的头部,使 用了两个 page 指令: 〈毛@ page contentType="text/html; charset=gb2312" language="java" 革〉 〈毡@ page import="java.sql.*"毡〉 其中第二条指令用于导入本页面中使用的类,如果没有通过page 指令的 import 指 令导入这些类,则需在脚本中使用全限定类名一一即必须带包名。可见,此处的import 属性类似于 Java 程序中的 import 关键字的作用。 如果删除第二条page 指令,则执行效果如图2.4所示。 Generated l'ervlet error D 、 torncat502B、 work、 Catalina、 localbo :5 t、 jOD巳 not飞 or.飞 'D'巳..飞 )OD飞:5 cr1ptlet2町jsp.java:5 -4: c 巴 nnct 0'"曲。 1 class !;I ts巳四>o n巳 location: cl~~ orQ'町ache. jsp. se口 Ptlil! t2_j !!l P 5tatellll!nt stm也. conn.crea巳 eStatem@ nt 川, An error DC巳 urred at lin!!: 9 in the j !l P 立 iIe: Iscriptlet2. j :!!l P Generated servll!!:巳 error: D' 、巳 omcat502B飞,。由、Catalini!l飞 localhost飞 jOD巳 not飞。町、 ap5C he飞 )OD飞!!I criptle巳 Z jsp.jsva:S5: cannot oy霄'001 01_8 Resul巳 Snt location: elu!! orQ' .apache.jsp.scr1ptlet2_jsp Re8UltSet rs • stmt. 四四 ute Qw! ry("sel l! ct ... :t rom 四町 int 川, 图 2 .4 不使用 import 属性的出错效果 看下面的 JSP 页面,该页面使用 page 指令的 info 方法指定了 JSP 页面的描述信息, 又使用 getServ le tInfoO 方法输出该描述信息。 〈毡@ page contentType="text/html; charset=gb2312" language="java" 革〉 〈屯@ page info= 町 this is a jsp" 在〉 42传统表现层 JSP EE 测试 page 指令的 info 属性 该页面的执行效果如图2.5 所示。 图 2.5 page 指令的 info 属性效果 errorPage 属性的实质是JSP 的一种异常处理机制, JSP 不要求强制处理异常,即使 该异常是 checked 异常。如果 JSP 页面在运行中抛出未处理的异常,系统将自动跳转到 errorPage 属性指定的页面:如果errorPage 没有指定错误页面,系统则将异常信息呈现 给客户端浏览器一一这是所有的开发者都不愿意见到的场景。 看下面的 JSP 页面,该页面使用了page 指令的 errorPage 属性,该属性指定了对页 面发生异常时的异常处理页面。 〈毡@ page contentType="text/html; charset=gb2312" language="java" errorPage= "error. j Spll 毛〉 测试 page 指令的 errorPage属性 〈屯 II 下面代码将出现运行时异常 int a = 6; int b = 0; int c = a / b; 毡〉
下面是 error.jsp 页面,该页面本身是错误处理页面,因此将isErrorPage 设置成 true 。 〈屯 @page contentType="text/html; charset=gb2312" language="java" iSErrorPage= "true"毡〉 43轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 出错页面 <IT 工 TLE> </HEAD> <BODY> <! 提醒客户端系统出现异常 > 系统出现异常 <br> </BODY> </HTML> 在浏览器中浏览前一个页面的效果如图 2.6 所示。 如果将前一个页面中 page 指令的 errorPage 属性删除,再次通过浏览器浏览该页面, 执行效果如图 2.7 所示。 jav~且. lang. Ar ithrnet主 c!xception: J by zero org.apache. j !lp. page_002de: rrorPaq雹_j :ll p. _j!lpService (page_ 002derr org.apache. juper.runtir回 HttpJl!!l pB al!lI! .service{耻tpJ !l pB_e.jave. org.apache:. j a8per. Ja~perException: I by zero org.apache. jal'per. !u!!:rvll!t. J !lp5ervletWrapper.!ll!rvice (Jl!!l pServlet' org.apache. ja!!lper. !l ervle~二 J l!!l pServll! t. !lerviceJsprile (JspServlet户 org. apache. ja!lper. !lervlet. J !lpServlo!: t. !ll!rvice (JspServlet. java: 2 反 j 5VI!lX.l!!lervlet. http.H1: tpServll!!巳. !lerv1ce (H巳巳pServll!! t. js'四 :602) 总S 苟明 。。细 细翻擅自黯 辈革 图 2.6 errorPage 属性控制异常处理效果 图 2.7 没有 errorPage 属性控制异常处理的效果 可见,使用 errorPage 属性控制异常处理的效果在表现形式上要好得多。 2.6.2 include 指令 使用 include 指令,可以将一个外部文件嵌入到当前 JSP 文件中,同时解析这个页面 中的 JSP 语句(如果有的话)。这是个静态的 include 语旬,不会检查所包含 JSP 页面的 变化。 include 既可以包含静态的文本,也可以包含动态的 JSP 页面。静态的编译指令 include ,是将被包含的页面加入进来,生成一个完整的页面。 include 编译指令的语法: 〈革 @include file="relativeURLSpec" 革〉 如果被嵌入的文件经常需要改变,建议使用<j sp:include>操作指令,因为它是动态 的 include 语句。 看下面的页面: 〈幸自 page contentType="text/html; charset=gb2312" language="java" 毡〉 <!DOCTYPE HTML PUBLIC "-IIW3CIIDTD HTML 4.0 Transitional//EN"> <HTML> <HEAD> <TITLE>include 测试 <IT 工 TLE> </HEAD> 44表现层 JSP~ <BODY> 〈革 @include file="scriptlet1.jsp" 毡〉 </BODY> </HTML> 该页面的执行效果与scriptlet1.jsp 的执行效果相同,该页面的执行效果如图2.8 所示。 图 2.8 include 执行效果 查看 Tomcat 的 work飞Catalina飞localhost~sptest\org飞apache~sp路径下的 include..j sp扣va 文件,从 in町Iωclud也e.扣p 编译后的源代码可看到, inc1ude.jsp 页面已经完全将scriptlet1.jsp 的 代码融入进来,下面是inc1udejsp扣va 文件的片段: II 输出流生成表格 out.write("<table bgcolor=\"9999dd\" border=\"1\" align=\"center\" width= \"200\">\r\nil) ; II 循环输出表格的行 for(int i = 0 ; i < 10 ; i++) 。ut.write("\r\口" ); out.write("<tr>\r\ 口") ; out .write ("<td>循环值</td>\r\n") ; out.wr 工 te("<td>") ; out.print(i); out.write("</td>\r\口" ); out.write("</tr>\r\n") ; } out.write("\r\n") ; 。ut.write("<table>\r\n") ; 这就是静态包含意义:包含页面在编译时已经完全包含了被包含页面的代码。 2.7 JSP 的 7 个动作指令 动作指令与编译指令不间,编译指令是通知 Servlet 引擎的处理消息,而动作指令只 是运行时的脚本动作。编译指令在将 JSP 编译成 Servlet 时起作用:处理指令通常可替换 成 Java 脚本,是 JSP 脚本的标准化写法。 45轻量级 J2EE 企业应用实战一-8t阳ts+8pring+Hibernate 整合开发 JSP 动作指令主要有如下 7 个。 • jsp:forward: 执行页面转向,将请求的处理转发到下一个页面。 • jsp:param: 用于传递参数,必须与其他支持参数曲标签一起使用。 • jsp:include: 用于动态引入一个 JSP 页面。 • jsp:p1ugin: 用于下载 JavaBean 或 Applet 到客户端执行。 • jsp:useBean: 使用 JavaBean 。 • jsp:setProperty: 修改 JavaBean 实例的属性值。 • jsp:getProperty: 获取 JavaBean 实例的属性值。 下面依次讲解这些动作指令。 2工 1 forward 指令 forward 指令用于将页面响应控制转发给另外的页面。既可以转发给静态的 HTML 页面,也可以转发到动态的 JSP 页面,或者转发到容器中的 Se凹leto JSP 的 forward 指令的格式如下: 对于 JSP 1. 0 ,使用如下语法: <jsp: forward page=" {relativeURL I <草 =expression%>}"/> 对于 JSP 1. 1 以上,可使用如下语法: <jsp:forward page=" {relat工 veURL I <奄 =expression 毡>}"> {<jsp:param.../>} </jsp:forward> 第二种语法用于在转发时增加额外的请求参数。增加的请求参数的值可以通过 HttpServ1etRequest 类的 getParameter方法获取。 看下面 JSP 页面: <jsp:forward page="forward-result.jsp"> <jsp:param name="age" value="29"/> </jsp:forward> 这个 JSP 页面非常简单,它不作任何处理,仅将所有的客户端请求转发到 forward-result.jsp页面,在转发时,增加了一个请求参数:参数名为age ,参数值为 29 。 在 forward-resu1t.j sp 页面中,使用 request 内置对象 (request 内置对象是 HttpServ1etRequest 的实例,关于 request 的详细信息参看下一节)来获取增加的请求参数值。 forward-resu1t.j sp 的代码如下: 〈毡@ page contentType="text/html; charset=gb2312" language="java" 毡〉 <lDOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <HTML> <HEAD> <TITLE>forward结果页 在浏览器中访问jsp-forward.jsp页面的执行效果如图2.9 所示。 图 2.9 jsp:forward增加的请求参数 从页面的执行效果可看出, forward 指令增加的参数可以在转发的新页面中访问到。 jsp:forward 指令转发请求时,客户端的请求参数不会丢失,看下面表单提交页面的 例子,该页面没有任何动态的内容,是一个静态的HTML 页面,仅将表单域的值提交到 jsp-forward页。 提交 cjsp:useBean id="pI" class="lee.Person" scope="page"/> cjsp:setProperty name="pI" property="age" value="23"/> c! 一输出 pi 的 name 属性值一〉 cjsp:getProperty name="pl" property="name"/>cbr> c! 一输出 pi 的 age 属性值一〉 cjsp:getProperty name="pI" property="age"/> c/BODY> c/HTML> 对于上面的 JSP 页面中的 setProperty 和 getProperty 标签,都包含了 name 和 property 属性。 property 属性确定需要访问的属性名,对于这些属性名,可以无须对应的JavaBean 中有同名的属性,但必须有对应的getter 和 setter 方法。 即如果 property 属性确定了 name 属性,则要求 JavaBean 中必须有 setName 和 getName方法。事实上,当使用setProperty和 getProperty标签时,系统将自动调用setName 和 getName 方法来访问 Person 实例的属性。 下面是 Person 类的源代码: 50统表现层 JSP~ public class Person private String name; private 工 nt age; //name 属性对应的 setter 方法 public void setName(String name) { this.name =口arne; //age 属性对应的 setter 方法 public void setAge(工 nt age) this.age = age; //name 属性对应的 getter 方法 publ工 c String getName() return name; //age 属性对应的 getter 方法 public int getAge() return age; wawa 23 该页面的执行效果如图2.12 所示。 对于上面三个标签完全可以不使用,将javabean.jsp 修改成如下代码,其内部的执行是完全一样的: 图 2.12 JavaBean 页面的执行效果 〈毡@ page contentType="text/html; charset=gb23l2" language="java" 在〉 javabean 〈屯 Person pl = new Person(); <'一设置pl 的 name 属性值--> pl. setName ( "wawa" ); pl.setAge(23); 在〉
<1- 输出 pl 的 age 属性值-> 〈革 =pl. getAge ( )屯〉 如果 useBean 的 scope 属性值是 page ,则完全可以使用 Java 脚本来代替这几个标签。 但如果指定的 scope 属性值不是 page ,则还需要将该 JavaBean 实例放入特定的生存范围, 如下面代码片段所示: 51轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 II 将 pI 放入 request 的生存范围 request. setAttribute ("pI" , pI) ; II 将 pI 放入 session 的生存范围 session.setAttribute("pl" , pl); II将 pI 放入 applicatio口的生存范围 application. setAttribute ("pI" , p1) ; 2工4 plugin 指令 plugin 指令主要用于下载服务器端的 Jav aB ean 或 Applet 到客户端执行。由于程序在 客户端执行,因此客户端必须安装虚拟机。 plugin 的语法格式如下: [ [jsp:param name="parameterName" value="parameterValue"I>] ] [text message for user that can no see the plugin ] 关于这些属性的说明如下。 • type: 指定被执行的 Java 程序的类型。 • code: 指定被执行的文件名,该属性值必须以" .class" 扩展名结尾。 • codebase: 指定被执行文件所在的目录。 • name: 给该程序起一个名字用来标识该程序。 • archive: 指向一些要预先载入的将要使用到的类的路径。 • hspace , vspace: 显示左右,上下的留白。 • jreversion: 能正确运行该程序必需的 JRE 版本,默认值是1.2 • ns臼sp抖lu吨1珞昭g旦in民町l , i抬ep抖lu鸣gin: Netscape Navigator, Internet Exploer 下载运行所需 JRE 的地址。 • 指令:当不能正确显示该 Applet 时,代替显示的提示信息。 看下面的 JSP 页面代码: 〈草@ page contentType="text/html; charset=gb2312" language="java" 革〉 jsp:plugin 测试 52表现层 JSP~ driver com.mysql.jdbc.Driver url jdbc:mysql://localhost:3306/j2ee user root pass 32l47 在浏览器中浏览该页面时,可看到数据库连接成功,数据查询完全成功。可见,使 用 application 可'以访问 Web 应用的配置参数。 注意:通过这种方式,可以将一些配直信息放在web.xml 文件中配直,避免使用硬 编码方式写在代码中,从而更好地提高程序解辑。 2.8.2 config 对象 config 对象代表当前 JSP 配置信息,但 JSP 页面通常无须配置,因此也就不存在配 置信息。该对象在 JSP 页面中非常少用,但在 Se凹 let 则用处相对较大。因为 Servlet 需 要配置在 web.xml 文件中,可以指定配置参数。关于 Servlet 的使用将在 2.9 节介绍。 看如下 JSP 页面代码,该 JSP 代码使用了 config 的一个方法 getServletNameO: 〈毡@ page contentType="text/html; charset=gb2312" language="java" 毡〉 〈毡@ page import="java.sql.*"毛〉 小脚本测试 〈毡 =config.getServletName() 屯〉 该页面的输出是 JSP 文件。所有的 JSP 页面都有相同的名字: jsp 。 58统表现层 JSP~ 2.8.3 exception 对象 exception 对象是 Throwable 的实例,代表 JSP 页面产生的错误和异常,是 JSP 页面 异常框架的一部分。 在 JSP 页面中无须处理异常,即使该异常是 checked 异常。事实上, JSP 页面包含 的所有异常都由错误页面处理了。 看下面的异常处理结构: try { II 代码处理段 catch (Exception exception) { II 异常处理段 这是典型的异常捕捉处理块。在 JSP 页面中,普通的 JSP 页面只执行第一个部 分一一代码处理段。而出错的页面负责第二个部分一一异常处理段。在异常处理段中, 可以看到有个异常对象,该对象就是内置对象 exception 。 注意: exception 对象仅在错误处理页面中才有效。结合前面的异常处理结构,读者 可以非常清晰地看出这点。 在 JSP 的异常处理体系中,一个出错页面可以处理多个 JSP 页面的异常。指定的异 常处理页面通过 page 指令的 errorPage 属性确定。 看下面的页面: <'一 通过 errorPage 属性指定异常处理页面-→ 〈屯@ page contentType="text/html; charset=gb2312" language="java" errorPage= "error.jsp"革〉 测试 page 指令的 errorPage 属性 〈毡 int a = 6; int b = 0; int c = a I b; 毛〉
当该页面出现异常时, error.jsp 页面将负责处理该异常。 注意:将异常处理页面中 page 指令的 isErrorPage 属性应设置为 trueo 59轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 2.8.4 out 对象 out 对象代表一个页面输出流,通常用于在页面上输出变量值及常量。一般在使用 输出表达式值的地方,都可以使用 out 对象来达到同样效果。 看下面的 JSP 页面代码: 〈屯@ page contentType="text/html; charset=gb2312" language="java" 毡〉 〈革@ page import="java.sql.*"毛〉 〈日TML> out 测试 〈毡 II 注册数据库驱动 Class. forName ("com.mysql.jdbc.Driver"); II 获取数据库连接 Connection conn = DriverManager.getConnection("jdbc:mysql:lllocalhost:33061 j2ee" , "root" , "32147"); II 创建 Statement 对象 Statement stmt = con口 .createStatement(); II 执行查询,获取 ResultSet 对象 ResultSet rs = stmt.executeQuery("select * from userinf"); 毡〉
〈毡 II遍历结果集 while(rs.next()) II 输出表格行 out.println(""); II 输出表格列 out.println(""); II 开始表格列 out.println(""); } 毡〉
"); II输出结果集的第二列的值 out.println(getString(2)); II 关闭表格列 out.println(""); II 输出结果集的第三列的值 out.println(rs.getString(3)); II 关闭表格列 out.pr工 ntln ("") ; II 关闭表格行 out.println("
从 Java 的语法上看,上面的程序更容易理解, out 是个页固输出流,在页面中输出 表格及其内容。 60传统表现层 JSP 国 注意:所有使用 out 的地方,都可以使用表达式输出的方式代替,而且使用这种方 式更加简洁。通过 out 对象的系统介绍,读者可以更好地理解表达式输出的原理。 2.8.5 pageContext 对象 这个对象代表页面上下文,该对象主要用于访问页面共享数据。使用 pageContext 可以直接访问 request , session, application 范围的属性,看下面的 JSP 页面: 〈毡@ page contentType="text/html; charset=gb23l2" language="java" 毡〉 clDOCTYPE HTML PUBLIC "-IIW3CIIDTD HTML 4.0 TransitionalIIEN"> cHTML> cHEAD> cTITLE>pageContext ~U试c/TITLE> c/HEAD> cBODY> 〈毡 II 使用 pageContext 设置属性,该属性默认在 page 范围内 pageContext. setAttribute ("page" , "hello") ; II 使用 request 设置属性,该属性默认在 request 范围内 request. setAttribute ("request" , "hello"); II 使用 pageContext将属性设置在request 范围中 pageContext.setAttribute("request2" , "hello" , pageContext.REQUEST_SCOPE); II 使用 sess~on将属性设置在 session 范围中 session.setAttribute("session" , "hello"l; II使用 pageContext将属性设置在session范围中 pageContext.setAttribute("session2" , "hello" , pageContext.SESSION_SCOPE); II 使用 application将属性设置在application范围中 application.setAttribute ("app" , "hello") ; II 使用 pageContext 将属性设置在 application 范围中 pageContext.setAttribute("app2" , "hello" , pageContext.APPL 工 CATION_SCOPE) ; II 以此获取各属性所在的范围 g out.println("page 变量所在范围: " + pageContext.getAttributesScope("page") + II
") ; out.println("request 变量所在范围: " +pageContext.getAttributesScope("request") + "
"); out.println("request2变量所在范围: "+pageContext.getAttributesScope("request2" I + "
") ; out.println("session 变量所在范围: " +pageContext.getAttributesScope("session") + "
")i out.println(町 session2 变量所在范围: " +pageContext.getAttributesScope("session2") + "
") i 。ut.println( 飞pp 变量所在范围: "+pageContext.getAttributesScope ( "app" I +气 br>") ; out.println("app2 变量所在范围: " +pageContext.getAttributesScope("app2") + "
"); 毡〉 c/BODY> c/HTML> 上面的 JSP 页面使用 pageContext 对象多次设置属性,在设置属性时,如果没有指 定属性存在的范围,则属性默认在page 范围内:如果指定了属性所在的范围,则属性可 以被存放在 application, session, request 等范围中。 图 2.17 显示了使用 pageContext 访问属性的效果。 61轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 page变量所在黯围, 1 request 变量所在范围. 2 request2:变量所在范围, 2 session变量所在范围. 3 session2变量所在范围, 3 .pp变量所在范围 4 app2变量所在范围. 4 图 2.17 pageContext 访问属性的示例 图 2.17 中显示了使用 pageContext 获取各属性所在的范围,其中这些范围获取的都 是整型变量,这些整型变量分别对应几个生存范围。 1 :对应 page 生存范围。 2: 对应 request 生存范围。 3: 对应 session 生存范围。 4: 对应 application 生存范围。 2.8.6 request 对象 request 对象是 JSP 中重要的对象,每个 request 对象封装着一次用户请求,并且所 有的请求参数都被封装在 request 对象中。因此 request 对象也是获取客户端请求参数的 方法。 request 对象不仅封装了表单域值,还可以封装地址栏传递的参数。因此用户也可在 request 对象中增加请求属性。 下面依次介绍这几种情况。 1. 封装表单域值 封装表单域值是最常见的情况,几乎每个网站都会大量使用表单。表单用于收集用 户信息,一旦用户提交请求,表单的信息将会提交给对应的处理程序。 看下面的表单页面: 〈革@ page contentType="text/html; charset=gb2312" language="java" 奄〉 out 测试
用户名:

性别:
男: 女:
喜欢的颜色:
62加 JSP~ 红: 绿: 蓝: <工NPUT TYPE="checkbox" NAME="color" value="蓝1I >
来自的国家:

〈工NPUT TYPE="reset" value="重置"> 这个页面没有动态的JSP 部分,仅仅包含 1 个文本框、 2 个单选框、 3 个复选框及 1 个下拉列表框,另外包括【提交】和【重置】2 个按钮。页面的执行效果如图2.18 所示。 喜欢的颜色E 红, r 绿,广蓝, r 高唱京E 遗墨每遥望国 图 2.18 表单页 在该页面中输入相应信息后,单击【提交】按钮,表单域的信息被封装成 HttpServletRequest 对象,该对象包含了所有的请求参数,可通过getParameter 方法获取 请求参数的值。 该表单页提交到 requestl.j sp ,该页面的代码如下: <%@ page contentType="text/html; charset=gb2312" language="java" %> request 测试 〈毡 II 设置解码方式,对于中文,使用 GBK 解码 request.setCharacterEncoding("GBK"); II 下面依次获取表单域的值 String name = request.getParameter("name"); String gender = request.getParameter("gender"); II 如果表单域是复选框,将使用该方法获取多个值 String[] color = request.getPararneterValues("color"); String national = request.getPararneter("country"); 63轻量级 J2EE 企业应用实战一一St阳ts+Spring+Hibernate 整合开发 毡〉
您的性别: <%=gender%>
II 输出复选框获取的数组值 您喜欢的颜色: <%for(String c: color) (out.println(c +" ");}%>
您来自的国家: <屯 =natio口al 苦 >
在页面中可大量使用 request 对象来获取表单域的值,获取表单域的值有如下两个 方法。 • String getParamete(String paramName): 获取表单域的值。 • String getParameterVa1ues(String paramName): 获取表单域的数组值。 在获取表单域的值之前,先设置request 的解码方式,因为获取的参数是简体中文, 因此使用 GBK 的解码方式,设置解码方式时使用如下方法。 • setCharacterEncoding( 飞BK"): 设置解码方式。 在表单提交页的各个输入域内输入对应的值,然后单击【提交】按钮, requestl.jsp 就会出现如图 2.19 所示的效果。 您的性别E 另 您喜欢的颜色,红绿草 您来自的国家z 申国 图 2.19 f'吨uest 获取请求参数 2. 封装地址栏参数 如果需要传递的参数是普通字符串,而且在传递少量参数时,可以通过地址栏传递 参数。地址栏传递参数的格式是urI ?paraml=va1ue1&p缸am2=value2& … 请求的 urI和参数之间以"?"分隔,而多个参数之间以"&"分隔。 看下面的 JSP 页面: 〈毡@ page contentType="text/html; charset=gb2312" language="java" 革〉 request 测试 〈毡 II 获取 name 请求参数的值 String name = request.getParameter(" 口 ameli) ; II 获取 gender 请求参数的值 String gender = request.getParameter("gender"); 64传…~ 屯〉 〈毡 革> first.jsp 页面首先获取请求的取钱数,然后对请求的钱数进行判断。如果请求的钱数 小于 500 ,则允许直接取钱:否则将请求转发到second.jsp。转发之前,在请求中封装了 info 属性,并在该属性中存入了一个 List 对象。 因此在 s臼ec∞ond叫d.扣p 页面中,不仅获取了请求的 balance 参数,而且还会获取请求中的 info 属性。 second.jsp 页面的代码如下: 〈毡@ page contentType= "text/html; charset=gb2312" language=" java" import=" java. util.*"毡〉 request 处理 〈屯 String bal = request.getParameter("balance"); double qian = Double.parseDouble(bal); List 工 nfo = (List"); out.println("取钱 11 + qian + n 块") ; out.println(" 账户减少 II + qian); 在〉 66表现层 JSP~ 如果页面请求的钱数大于500 时,请求将被转发到second.jsp 页面处理。当 forward 发出请求时,请求参数和请求属性都不会丢失,即在second.jsp 页面可以获取请求参数 和请求属性的值。 如果请求取钱的钱数为654,则页面的执行效果如图2.21 所示。 1111111 2222222 3333333 取钱 654.0 块帐户减少 654. 。 图 2.21 request 访问 a伽ibute 2.8.7 response 对象 response 代表服务器对客户端的响应。大部分的时候,程序无须使用 response 来响 应客户端请求,因为有个更简单的响应对象一-out ,它是页面输出流,是 JstWriter 的实 例。 JspWriter 是 Writer 的子类, Writer 是字符流,无法输出非字符内容一一即无法输出 字节流。 假如需要在 JSP 页面中动态生成一幅位图,使用 out 作为响应将无法完成,此时必 须使用 response 作为响应输出。 除此之外,还可以使用 response 来重定向请求,以及用于向客户端增加 Cookie 。 1. response 晌应生成图片 对于需要生成非字符响应的情况,就应该使用response 来响应客户端请求。下面的 JSP 页面将在客户端生成一张图片。 〈屯@ page import="java.awt.image.*, javax.imageio.*, java.io.* , java.awt.食"革〉 〈草 II 创建 BufferedImage对象 Buffered工mage image = new BufferedImage(400 , 400 , BufferedImage.TYPE_INT_ RGB) ; II 以工mage 对象获取 Graphics 对象 Graphics g = image.getGraphics(); II使用 Graphics 画图,所画的图像将会出现在image 对象中 g.fillRect(0, 0, 400 , 400); II 设置颜色:红 g.setColor(new Color(255 , 0 , 0)); II 画出一段弧 g.fillArc(20, 20 , 100 ,100, 30 , 120); II 设置颜色:绿 g.setColor(new Color(O , 255 , 0)); II 画出一段弧 67轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 g.fillArc(20 , 20 , 100 ,100 , 150 , 120): II 设置颜色:蓝 g.setColor(口ew Color(O , 0 , 255)): II画出一段弧 g.fillArc(20, 20 , 100, 100 , 270 , 120): II 设置颜色:黑 g.setColor(new Color(O , O, O)): II 画出三个字符串 g.drawString("red:climb" , 300 , 80): g.drawString("green:swim" , 300 , 120): g.drawString("blue:jump" , 300 , 160): g.dispose(): II将图像输出到页面的响应 ImageIO.write(image , "bmp" , response.getOutputStream(»): 毡〉 也可以在其他页面中使用如下标签来显示该图片页面,该JSP 的代码如下: 在对 JSP 页面发出请求后,在浏览器中将会看到如图2.22 所示效果。 • red:tllmb gteen:81硝m blue:Jump 图 2.22response 生成图片响应 2. 重定向 重定向是 response 的另外一个用处,与 forw 缸d 不同的是,重定向会丢失所有的请 求参数及请求属性。 看下面的 JSP 页面代码: 〈奄@ page language="java" 毡〉 〈革 II 生成页面响应 out.println("====") ; II 重定向到 forward-result.jsp 页面 response.sendRedirect("forward-result.jsp"): 毛〉 当该页面输出页面响应后,就会发送重定向命令,页面控制将会重定向到 forward-result.jsp 页面。 在地址栏中输入http:tnocalhost:8888/jsptβst/redirect.jsp?usemame="aaa",然后回车, 看到如图 2.23 所示效果。 68制层 JSP~ 图 2.23 redirect 效果 注意地址栏的改变,使用重定向指令时,地址栏的地址会变成重定向的地址。 注意:重定向会丢失所有的请求参数,使用重定向的效果,与在地址栏里重新输入 新地址再回车的效果完全一样。 3. 增加 Cookie Cookie 通常用于网站记录客户的某些信息,比如客户的用户名及客户的喜好等。一 旦用户下次登录,网站可以获取到客户的相关信息,根据这些客户信息,网站可以对客 户提供更友好的服务。 Cookie 与 session 的不同之处在于: session 关闭浏览器后就失效, 但 Cookie 会一直存放在客户端机器上,除非超出 Cookie 的生命期限。 增加 Cookie 也是使用 response 内置对象完成的, response 对象提供了一个方法。 • void addCookie(Cookie cookie): 增加 Cookie。 正如在上面方法中见到的,在增加Cookie 之前,必须先创建Cookie对象,增加Cookie 请按如下步骤进行: (1)创建 Cookie 实例: (2) 设置 Cookie 的生命期限: (3)向客户端写Cookie。 看如下 JSP 页面,该页面可以用于向客户端写一个usemame 的 Cookie。 〈屯@ page contentType="text/html; charset=gb23l2" language="java" 毡〉 增加 Cookie 〈革 II 获取请求参数 String name = request.getParameter(" 口 arne") ; II 以获取到的请求参数为值,创建一个 Cookie 对象 Cookie c = new Cookie("username" , name); II 设置 Cookie 对象的生存期限 c.setMaxAge(24 * 3600); II 向客户端增加 Cookie 对象 response.addCookie(c); 毡〉 69轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 如果浏览器没有阻止 Cookie ,在地址栏输入 http: //1 ocalhos t: 8888/jsptes t/addcookie.jsp? name=waa ,执行该页面后,网站己经将客户端的 username 的 Cookie 写入客户端机器。 该 Cookie 将在客户端硬盘上一直存在,直到超出该 Cookie 的生存期限(本 Cookie 设置 为 24 小时)。 通过 request 对象的 getCookiesO方法来访问 Cookie ,该方法将返回 Cookie 的数组, 遍历该数组的每个元素,找出希望访问的 Cookie 即可。 下面是访问 Cookie 的 JSP 页面的代码: 〈在@ page contentType="text/html; charset=gb23l2" language="java" 毡〉 增加 Cookie 〈毡 II 获取本站在客户端上保留的所有 Cookie Cookie[] cookies = request.getCookies(); II J.l!i 历客户端上的每个Cookie for (Cookie c : cookies) II 如果 Cookie 的名为 username. 表明该 Cookie 是我们需要访问的Cookie if(c.getName() .equals("username"))out.println(c.getValue()); } 毡〉 使用该页面将可读出刚才写在客户端的Cookie 。 注意:使用 Cookie 对象必须设直其生存期限,否则Cookie 将会随浏览器的关闭而 自动消失。 2.8.8 session 对象 session 对象也是一个非常常用的对象,这个对象代表一次用户会话。一次用户会话 的含义是:从客户端浏览器连接服务器开始,到客户端浏览器与服务器断开为止,这个 过程就是一次会话。 session 通常用于跟踪用户的会话信息,如判断用户是否登录系统,或者在购物车应 用中,系统是否跟踪用户购买的商品等。 session 里的属性可以在多个页面的跳转之间共享。一旦关闭浏览器,即 session 结 束, session 里的属性将全部清空。 session 对象的两个常用方法如下。 • setAttribute(String attName , Object attValue): 设置一个 session 属性。 • getAttribute(String attName): 返回一个 session 属性的值。 下面的示例演示了-个购物车应用,以下是商品陈列的JSP 页面代码: 70现层 JSP~ 〈幸自 page contentType="text/html; charset=gb2312" 1anguage="java" 老〉 选择物品购买
书籍:
汽车: <工NPUT TYPE="checkbox" NAME="item" value="car">
这个页面几乎没有动态的JSP 部分,仅提供静态的HTML。表单里包含三个复选按 钮,用于提交想购买的物品,表单由processBuy.jsp 页面处理,其页面的代码如下: 〈革@ page contentType="text/html; charset=gb2312" language="java" import="java. util.*"毡〉 〈奄 II 从 session 对象中取出 Map itemMap = (Map(); itemMap.put("书籍, , 0); itemMap.put("电脑 " I 0); itemMap.put("汽车" 0) ; II 获取上个页面的请求参数 String[) buys = request.getParameterVa1ues(" 工 tern") ; I lilii 历数组的各元素 for (String item : buys) II 如果 item 为 book. 表示选择购买书籍 if (item. equals ("book")) int num1 = itemMap.get(" 书籍,,) .intVa1ue(); II 将书籍 key 对应的数量加 1 itemMap.put("书籍, , num1 + 1); II 如果 item 为 computer,表示选择购买电脑 else if (item.equa1s("computer")) int num2 = itemMap.get(" 电脑") .intValue(); II将电脑 key 对应的数量加 1 itemMap.put("电脑, , num2 + 1); II 如果 item 为 car ,表示选择购买汽车 else if (item.equa1s("car")) int num3 = itemM ap.get(" 汽车") .intVa1ue(); II 将汽车 key 对应的数量加 1 itemMap.put( 阴汽车, , num3 + 1); 71轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 II 将 itemM ap 对象放到 session 中 session.setAttribute("itemMap" , itemMap); 奄〉 购买的物品列表 您所购买的物晶, 书籍· 1 本 电脑· 2 台 汽车· 2辆 垦盘盟Hi 图 2.24 session 购物车效果 2.9 Servlet 介绍 您所购买的物品:
书籍: <毡=工temMap.get("书籍") %>本
电脑: <%=工temMap.get("电脑")%>台
汽车: <%=itemMap.get("汽车") %>辆

再次购买 利用购物车系统可以反复购买,只要浏览器不关闭, 购买的物品信息就不会丢失,图2.24 显示的是多次购买后 的效果。 Servlet 是一种比 JSP 更早的动态网页编程技术。在没有JSP 之前, Se凹let 也是同时 充当视图层、业务逻辑层及持久层角色。 Servlet 的开发效率非常低,特别是当使用Servlet 生成表现层页面时,页面中所有的 HTML 标签,都需采用 Servlet 的输出流来输出,因此极其烦琐。由于Se凹let 是个标准 的 Java 类,因此必须由程序员开发,其修改难度大,美工人员根本无法参与Servlet 页 面的开发。这一系列的问题,都阻碍了Servlet 作为表现层的使用。 自 MVC 规范出现后, Servlet 的责任开始明确下来,仅仅作为控制器使用,不再需 要生成页面标签,也不再作为视图层角色使用。 2.9.1 Servlet 的开发 Servlet ,通常称为服务器端小程序,是运行在服务器端的程序,用于处理及响应客 户端的请求。 Servlet 是个特殊的 Java 类,这个 Java 类必须继承 HttpServlet 。每个 Servlet 可以响 应客户端的请求。 Se凹 let 提供不同的方法用于响应客户端请求。 • doGet: 用于响应客户端的 get 请求。 • doPost: 用于响应客户端的 post 请求。 • doPu t: 用于响应客户端的 put 请求。 • doDelete: 用于响应客户端的 delete 请求。 72制层 JSP~ 事实上,客户端的请求通常只有 get 和 post 两种; Se凹let 为了响应这两种请求,必 须重写 doGet 和 doPost 两个方法。如果 Servlet 为了响应四个方法,则需要同时重写上 面的四个方法。 大部分时候, Servlet 对于所有请求的响应都是完全一样的。此时,可以采用重写一 个方法来代替上面的几个方法, Servlet 只需重写 service 方法即可响应客户端的所有请求。 另外, HttpServlet 还包含两个方法。 • init(ServletConfig config): 创建 Servlet 实例时,调用的初始化方法。 • destroyO: 销毁 Servlet 实例时,自动调用的资源回收方法。 通常无须重写 initO 和 destroyO两个方法,除非需要在初始化 Servlet 时,完成某些资 源初始化的方法,才考虑重写 init 方法。如果需要在销毁 Servlet 之前,先完成某些资源 的回收,比如关闭数据库连接等,才需要重写 destroy 方法。 注意:如果重写了 init(ServletConfig config) 方法,则应在重写该方法的第一行调用 super.init(config) 。该方法将调用 HttpSe凹let 的 init 方法。 下面提供一个 Servlet 的示例,该 Servlet 将获取表单请求参数,并将请求参数显示 给客户端: IIServlet 必须继承 HttpServlet 类 public class FirstServlet extends HttpServlet II 客户端的响应方法,使用该方法可以响应客户端所有类型的请求 public void service(HttpServletRequest request , HttpServletResponse response) throws ServletException, java. 工 o.IOException II 设置解码方式 request.setCharacterEncoding("GBK") ; II 获取 name 的请求参数值 String name = request.getParameter("name"); II 获取 gender 的请求参数值 String gender = request.getParameter("gender"); II 获取 color 的请求参数值 String[] color = request.getParameterValues("color"); II 获取 country 的请求参数值 String national = request.getParameter("country"); II 获取页面输出流 Pr工 ntStream out = new PrintStream(response.getOutputStream()); II输出 HTML 页面标签 out.pri口 tl口( "< !DOCTYPE HTML PUBL 工 C\ "-IIW3CIIDTDHTML 4.0 Transitional liEN\">") ; out.println ( "") ; out.println( ""); out.println("Servlet测试"); out .println ("") ; out.println( 町 ") ; II 输出请求参数的值: name out .println ("您的名字: n + name + 11


II ); II 输出请求参数的值: gender out.println("您的性别: " + gender + 'I
"); II 输出请求参数的值: color 73轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 out.println(" 您喜欢的颜色: "); for(String c : color) out.println(c + " "); out.println("chr>"); out.println("您喜欢的颜色:叮; II 输出请求参数的值: national out.println("您来自的国家: " + national +气hr>") ; out .println ("c/BODY>") ; out .println ("c/HTML>") ; 对比该 Servlet 和 2.8.6 节中的 request1 .jsp 页面。该 Servlet 和 request1.jsp 页面的效 果完全相同,都通过 HttpServletRequest 获取客户端的 form 请求参数,并显示请求参数 的值。 Servlet 和 JSP 的区别在于: • Servlet 中没有内置对象,原来 JSP 中的内置对象都必须通过 HttpServletRequest 对象,或由 HttpServletResponse 对象生成: ·对于静态的 HTML 标签, Se凹let 都必须使用页面输出流逐行输出。 这也正是笔者在前面介绍的: JSP 是 Servlet 的一种简化,使用 JSP 只需要完成程序 员需要输出到客户端的内容,至于 JSP 中的 Java 脚本如何镶嵌到一个类中,由 JSP 容器 完成。而 Servlet 则是个完整的 Java 类,这个类的 service 方法用于生成对客户端的响应。 2.9.2 Servlet 的配置 编辑好的 Servlet 源文件并不能响应用户请求,还必须将其编译成 class 文件。将编 译后的 FirstServle t. class 文件放在 WEB- INF/classes 路径下,如果 Se凹let 有包,则还应该 将 class 文件放在对应的包路径下。 为了让 Se凹 let 能响应用户请求,还必须将 Servlet 配置在 Web 应用中。配置 Se凹let 时,需要修改 web.xrnl 文件。 配置 Servlet 需要配置两个部分。 ·配置 Se凹let 的名字:对应 web且nl 文件中的 元素。 ·配置 Servlet 的 URL: 对应 web.xrnl 文件中的 元素。 因此,配置一个能响应客户请求的 Servlet ,至少需要配置两个元素,关于上面 FirstServlet 的配置如下: c! … 配置 Servlet 的名字 > cservlet> c! 一指定 Servlet 的名字--> cservlet-name>firstServletc/servlet-name> cservlet-mapping> 74表现层 JSP~ firstServlet <'一指定 Servlet 映射的 URL 地址 > /firstServlet 对 2.8.6 节中的 form 进行简单修改,将form 表单元素的 action 修改成/firstServlet, 在表单域中输入相应的数据,然后单击【提交】按钮,效果如图2.25 所示。 您的名字,娃娃 您的性别,女 您喜欢的颜色z 红绿 您来自的国家,申国 Servlet 处理用户请求图 2.25 在这种情况下, Servlet 与 JSP 的作用效果完全相同。 Servlet 的生命周期2.9.3 Servlet 在容器中运行,其实例的创建及销毁等都不是由程序员决定的,而是由容器 进行控制。 Servlet 的创建有两个选择。 ·客户端请求对应的 Se凹 let 时,创建 Servlet 实例:大部分的 Servlet 都是这种 Se凹 let 。 • Web 应用启动时,立即创建 Servlet 实例:即 load-on-startup Se凹let 。 每个 Servlet 的运行都遵循如下生命周期。 (1)创建 Servlet 实例。 (2) Web 容器调用 Servlet 的 init 方法,对 Servlet 进行初始化。 (3) Servlet 初始化后,将一直存在于容器中,用于响应客户端请求。如果客户端有 get 请求,容器调用 Se凹let 的 doGet 方法处理并响应请求。对于不同的请求,有不同的 处理方法,或者统一使用service 方法处理来响应用户请求。 (4) Web 容器角色销毁 Servlet 时,调用 Servlet 的 destroy 方法,通常在关闭Web 容器之时销毁Servlet。 Servlet 的生命周期如图 2.26 所示。 里并⑥MH应…H 「成 J -0 元一 一例一-实一一建一·, HHd­ dFhZ 』 75 Servlet 的生命周期图 2.26轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 2.9.4 使用 Servl 创作为控制器 正如前面见到,使用 Servlet 作为表现层的工作量太大,所有的 HTML 标签都需要 使用页面输出流生成。因此,使用 Servlet 作为表现层有如下三个劣势。 ·开发效率低,所有的 HTML 标签都需使用页面输出流完成。 ·不利于团队协作开发,美工人员无法参与 Se凹 let 界面的开发。 ·程序可维护性差,即使修改一个按钮的标题,都必须重新编辑 Java 代码,并重新 编译。 在标准的 MVC 模式中, Servlet 仅作为控制器使用。J2 EE 的架构也是遵循 MVC 模 式的,图 2.27 是 MVC 的示意图。 State Query •••'• ChangeZNotification State Change …… Method Invocations •••• Events 图 2.27 MVC 示意图 下面介绍一个使用 Servlet 作为控制器的 MVC 应用,该应用演示了一个简单的登录 验证: 〈革@ page language="java" contentType="text/html;charset=gb23l2" errorPage= I' error. j sp II 革〉 登录 〈毛 if (request.getAttribute ("err") ! = null) { out.println(request.getAttribute("err")); 革〉 76传表现层 JSP~ 请输入用户名和密码:
用户名:
密   码:

这是个标准的登录页面,该页面将收集的用户名及密码,提交到Se凹let 。在这里, Se凹let 充当控制器角色,控制器Servlet 的源代码如下: public class LoginServlet extends HttpServlet II 响应客户端请求 public void service(HttpServletRequest request , HttpServletResponse respo口 se) throws ServletException, java.io. 工 OException IIServlet 本身并不输出响应到客户端,因此必须将请求转发 RequestDispatcher rd; II 获取请求参数 String username = request.getParameter("username"); String pass = request.getParameter("pass"); II try IIServlet 本身,并不执行任何的业务逻辑处理,它调用JavaBean处理用户请求 DbDao dd = DbDao. 工 nstance ("com.mysql. jdbc.Driver" , "jdbc:mysql:lllocalhost:3306/liuyan" , "root" , "32147"); II 查询结果集 ResultSet rs = dd.query("select password from user_table where username = 1 II + username + "I II ); if (rs. next ()) II 用户名和密码匹配 if (rs.getString ( "password") . equals (pass) ) II 获取 session 对象 HttpSession session = request.getSession(true); II 设置 session属性,跟踪用户会话状态 session.setAttribute("name" , username); II 获取转发对象 rd = request.getRequestDispatcher("/welcome.jsp"); II 转发请求 rd.forward(request , response); else II 用户名和密码不匹配时 errMsg += "您的用户名密码不符合,请重新输入" ; else II 用户名不存在时 errMsg += "您的用户名不存在,请先注册" ; 77轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 catch (Exception e) rd = request.getRequestDispatcher("/error.jsp"); request. setAttribute ("exception" "业务异常") ; rd.forward(request , response); II 如果出错,转发到重新登录 if (errMsg !=口ull && !errMsg.equals("")) rd = request.getRequestDispatcher("/login.jsp"); request. setAttribute ("err" , errMsg); rd.forward(request , response); 控制器负责接收客户端的请求,它既不直接对客户端输出晌应,也不处理用户请求, 只将请求转发到JSP 页,通过调用 JavaBean 来处理用户请求。 下面是 Model Jav aBean 的源代码: public class DbDao { II 该 JavaBean 做成单态模式 private static DbDao op; private Connection conn; private String driver; private String urI; private String username; private String pass; II 构造器私奋 private DbDao() { } II 构造器私有 private DbDao(String driver, String url,String username,String pass) throws Exception this.driver = driver; this.url = urI; this.username = username; this.pass = pass; Class.forName(driver); conn = DriverManager.getConnection(url , username , pass); II 下面是各个成员属性的setter 和 getter 方法 public void setDriver(Str工 ng driver) { this.driver = driver; public void setUrl(String urI) { this.url = urI; public void setUsername(String username) { this.username = username; public void setPass(String pass) { this.pass = pass; 78传统表现层 J叫E } public String getDriver() { return (this.dr工 ver) ; } public String getUrl() { return (this.url); } public String getUsername() { return (this.username); } public Stri口g getPass() { return (this.pass); II 获取数据库连接 public void getConnectio口() throws Exception if (conn == null) Class.forName(this.driver); conn = DriverManager.getConnection(this.url, this.username, this. pass); II 实例化 JavaBean 的入口 public static DbDao instance() 工 f (op ==口ull) op = new DbDao(); return op; II 实例化 JavaBean 的入口 public static DbDao instance(String driver, String url, String username , Str 工口g pass) throws Exception if (op == null) op = new DbDao(driver, url , username , pass); return op; } II 插入记录 public boolean insert(String sql) throws Exceptio口 { getConnection(); Statement stmt = this.conn.createStatement(); if (stmt.executeUpdate(sql) != 1) { return false; return true; } II 执行查询 public ResultSet query(String sql) throws Exception { getConnection(); Statement stmt = this.conn.createStatement(); return stmt.executeQuery(sql); 79轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 } II 执行删除 public void delete(String sql) throws Exception getConnection(); Statement stmt = this.conn.createStatement(); stmt.executeUpdate(sql); } II 执行更新 public void update(String sql) throws Exception { getConnection(); Statement stmt = this.conn.createStatement(); stmt.executeUpdate(sql); 由此可见,其整个结构非常清晰,下面是MVC 中各个角色的对应组件。 M: Model,即模型,对应 JavaBean 。 V: View ,即视图,对应 JSP 页面。 C: Controller,即控制器,对应 Servlet。 2.9.5 load-on-startup Servlet 在 2.9.3 节中已经介绍过, Servlet 的实例化有两个时机:用户请求之时,或应用启 动之时。应用启动时就启动的 Servlet 通常是用于某些后台服务的 Se凹let ,或者拦截很多 请求的 Servlet; 这种 Servlet 通常作为应用的基础 Servlet 使用,提供重要的后台服务。 如果需要 Web 应用启动时,可使用 load-on-startup 元素完成 Servlet 的初始化。 load-on-startup 元素只接收一个整型值,这个整型值越小, Se凹let 就越优先初始化。 下面是个简单的 Servlet ,该 Se凹let 不响应用户请求,它仅仅执行计时器功能,每隔 一段时间会在控制台打印出当前时间: public class T工merServlet extends HttpServlet { public void init(ServletConf工 g config)throws ServletException { super.init(config); Timer t = new Timer(lOOO , new ActionListener() { public void actionPerforrned(ActionEvent e) { Systern.out.println(new Date()); }); t.start () ; 该 Servlet 不会响应用户请求,因此它无须配置 URL 映射,只能在应用启动时初始 化。将该 Servlet 配置成 load响。n-startup Servlet ,即可保证应用启动时初始化,创建该 Servlet 实例的配置片段如下: 80统表现层 JSP~ t工merServlet <~口~t-param> driver com.mysql.jdbc.Driver <~口it-param> url jdbc:mysql:lllocalhost:3306/j2ee user root 82传统表现层 JSP EE <' 配置 Servlet 的初始化参数: pass--> pass 32147 另外,还需要为该Servlet 配置 URL 映射: testServlet <'一配置 Servlet 映射的 URL…〉 /testServlet 在浏览器中浏览该Servlet,可看到数据库访问成功的效果图(如果数据的配置正确)。 2.10 自定义标签库 在 JSP 规范的1.1 版中增加了自定义标签库。自定义标签库是一种非常优秀的组件 技术。通过使用自定义标签库,可以在简单的标签中封装复杂的功能。 实现自定义标签按如下步骤进行: (1)开发自定义标签处理类: (2)建立一个*.t1d 文件,每个*.t1d 文件对应一个标签库,每个标签库对应多个标签: (3)在 web.xm1文件中增加自定义标签的定义: (4) 在 JSP 文件中使用自定义标签。 2.10.1 开发自定义标签类 使用标签类,可以使用简单的标签来封装复杂的功能,从而使团队更好地协作开发 (能让美工人员更好地参与 JSP 页面的开发)。 自定义标签类都必须继承一个父类: java.Servle t.jsp.tagex t. TagSupport 。除此之外, 自定义标签类还有如下要求。 ·如果标签类包含属性,每个属性都有对应的 getter 和 setter 方法。 ·重写 doStartTagO 或 doEndTagO 方法,这两个方法生成页面内容。 ·如果需要在销毁标签之前完成资源回收,则重写 re1easeO 方法。 下面提供了一个最简单的标签代码: II 标签处理类,继承 TagSupport 父类 public class HelloWorldTag extends TagSupport II 重写 doEndTag方法,该方法在标签结束生成页面内容 public int doEndTag() throws JspTagException try 83轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 II 获取页面输出流,并输出字符串 pageContext.getOut() .write("Hello World"); II 捕捉异常 catch (工 OException ex) II 抛出新异常 throw new JspTagException("错误"} ; pμnuAPιTUAVPU 值 1 回 m 返 hu /e }/r 这是个非常简单的标签,它只在页面中生成一个 "Hello World" 的字符串。该标签 没有属性,因此无须提供 setter 和 getter 方法:此外,该标签无须初始化资源,因此无须 重写 init 方法:在标签结束时无须回收资源,因此无须重写 destroy 方法。 2.10.2 建立 TLD 文件 TLD 是 Tag Library Definition 的缩写,即标签库定义,文件的后缀是tld ,每个 TLD 文件对应一个标签库,一个标签库中可包含多个标签。TLD 文件也称为标签库定义文件。 标签库定义文件的根元素是taglib ,它可以有多个 tag 子元素,每个 tag 子元素都对 应一个标签。 下面是 tes t. tld 的标签定义文件,该文件中包含了 HelloWorldTag 标签的定义代码: 1.0 1.2 helloworld mytag.HelloWorldTag empty 这个标签库配置文件非常简单,没有标签体及标签属性等。只是一个空标签。 2.10.3 在 web.xml 文件中增加标签库定义 编辑了标签库定义文件还不够, Web 容器还无法加载标签库定义文件。还必须在 84统表现层 JSP~ web.xml 文件中增加标签库的定义。 在 web.xml 文件中定义标签库时使用 taglib 元素,该元素包含两个子元素: taglib-uri 和 taglib-location,前者确定标签库的 URI; 后者确定标签库定义文件的位置。 下面是 web.xml 文件中关于 test. tld 标签库的定义片段: /tags/tldtest.tld <'一 确定标签库定义文件的位置一〉 /WEB-INF/tldtest.tld 如果需要使用多个标签库,只需要增加多个taglib 元素即可,因为每个taglib 元素 可对应一个标签库。 2.10.4 使用标签库 使用标签库分成以下两步。 (1)导入标签库:使用 taglib 编译指令导入标签。 (2) 使用标签:在 JSP 页面中使用自定义标签。 taglib 的语法格式如下: 〈鲁@ taglib uri= 町 tagliburi" prefix="tagPrefix" 屯〉 其中 uri 属性确定标签库定义文件的URI,这个 URI 就是在 web.xml 文件中为标签 库定义的 URI。而 prefix 属性确定的是标签前缀,即在JSP 页面中使用标签时,该标签 库负责处理的标签前缀。 使用标签的语法格式如下: 如果该标签没有标签体,则可以使用如下语法格式: 下面的 JSP 页面使用 HelloWorldTag 标签: 〈毡@ page contentType="text/html; charset=GBK" 毡〉 自定义标签示范

下面显示的是自定义标签中的内容

<'一 使用标签,其中mytag 是标签前缀,根据taglib 的编译指令, mytag 前缀将 85轻量级 J2EE 企业应用实战一-Struts+Spring+Hibemate 整合开发 由于 uri 为 Itags/tldtest.tld 的标签库处理-->
该页面的执行效果如图2.28 所示。 下面显示的是自定义标签中的 内容 Hello World 图 2.28 简单标签 2.10.5 带属性的标签 除了前面的简单标签外,还有如下两种常用的标签。 .带属性的标签。 ·带标签体的标签。 正如前面介绍的,带属性的标签必须为每个属性提供对应的 setter 和 getter 方法。带 属性的标签的配置方法与简单标签也略有差别。 下面介绍一个带属性标签的示例: public class QueryTag extends TagSupport { II 标签的属性 private String driver; private String url; private String user; private Str工 ng pass; private String sql; II执行数据库访问的对象 private Connection conn = null; private Statement stmt = null; private ResultSet rs = null; private ResultSetMetaData rsmd = null; II 标签属性 driver 的 setter 方法 publ工 c void setDriver(String driver) ( th 工 s.driver = driver; II标签属性 url 的 setter 方法 public void setUrl(String url) ( this.url = url; II标签属性 user 的 setter 方法 public void setUser(String user) { this.user = user; 86表现层 JSP.~ II 标签属性 pass 的 setter 方法 public void setPass(String pass) ( this.pass = pass; II 标签属性 driver 的 getter 方法 public String getDriver() ( return (this.driver); II 标签属性 urI 的 getter 方法 public String getUrl() ( return (this.url); II 标签属性 user 的 getter 方法 publ工 c String getUser () { return (th工 s.user) ; II 标签属性 pass 的 getter 方法 public String getPass () ( return (this.pass); II 标签属性 sql 的 getter 方法 public String getSql () ( return (this.sql); II 标签属性 sql 的 setter 方法 public void setSql(String sql) ( this.sql = sql; II 标签处理 publ工 c int doEndTag() throws JspTagException try { II 注册驱动 Class.forName(driver); II 获取数据库连接 conn = DriverManager.getConnect 工 on(url , user , pass); II 创建 Statement 对象 stmt = conn.createStatement(); II 执行查询 rs = stmt.executeQuery(sql); rsmd = rs.getMetaData(); II 获取列数目 int columnCount = rsmd.getColumnCount(); II 获取页面输出流 Writer out = pageContext.getOut(); II 在页面输出表格 out.write("
"); ll :il!i历结果集 while (rs.next()) out.write(""); II 逐列输出查询到的数据 for (int i = 1 ;土<= columnCount ; i++ ) out.write(""); 87轻量级 J2EE 企业应用实战一一Struts+Spring+Hibernate 整合开发 } out.write("c/tr>"); catch (Exception ex) ex.printStackTrace(); throw new JspTagException("错误") ; return EVAL_PAGE; II 销毁标签前调用的方法 public void destroy() II 关闭结果集 if (rs != null) try rs.close(); } catch (SQLException sqle) { sqle.printStackTrace(); II 关闭 Statement if (stmt != null) try stmt.close(); } catch (SQLException sqle) { sqle.printStackTrace(); II 关闭数据库连接 if (conn != null) try conn.close(); } catch (SQLException sqle) { sqle.printStackTrace(); 这个标签有点复杂,它包含5 个属'性: driver, uri, user, pass, sql 。标签体将根据这 5 个属性执行查询,并将查询结果在页面上显示。由于这个标签带有 5 个属性,因此配置 文件也需要指定属性,下面是关于这个标签的配置文件片段: ctag> c! 一配置标签名-> cname>queryc/name> c! 一配置标签的处理类一〉 ctag-class>mytag.QueryTagc/tag-class> c! 一指定标签体为空 > cbody-content>emptyc/body-content> 88!Ji!~ JSP 国 driver true true 〈口ame>urltrue true user true true 〈口ame>pass true true sql true true 配置完毕后,就可在页面中使用标签,先导入标签库,然后使用标签。使用标签的 代码片段如下: 〈寄自 taglib uri="/tags/tldtest.tld" prefix="mytag" 革〉 ..< !一其他 HTML 内容〉 〈吨rtag:query driver=吧。m.mysql.jdbc.Driver" url="jdbc:mysql://localhost:3306/j2ee" user="root" pass="32147" sql="select * from products"/> 在浏览器中浏览该页面,效果如图2.29 所示。 图 2.29 带属性的标签 89轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 在 JSP 页面中只需要使用简单的标签,即可完成"复杂"的功能:执行数据库查询, 并将查询结果在页面上以表格形式显示。这也正是自定义标签库的目的一一以简单的标 签,隐藏复杂的逻辑。事实上,自定义标签的功能非常强大,很多项目都提供了相关的 标签库,比如 Struts 、 WebWork 等。 注意 :JSTL 是 Sun 提供的一套标签库,这套标签库的功能非常强大。另外, DisplayTag 是 Apache 组织下的一套开源标签库,主要用于生成页面并显示效果。 2.10.6 带标签体的标签 带标签体的标签,就是允许在标签内嵌套标签,通常可用于完成一些逻辑运算例如 判断和循环等。 带标签体的标签需要继承 BodyTagSupport,该类包含一个 bodyContent 属性,该属 性代表标签体。 BodyTagSupport 还包含两个方法。 • doAfterBodyO: 每次处理完标签体后调用该方法。 • void doInitBodyO: 开始调用标签体时调用该方法。 如果有必要,可以重写这两个方法。 下面以一个迭代器标签为示例,介绍如何开发一个带标签体的标签,该标签体包含 两个属性: bean 和 item , bean 属性代表 page 范围内的一个 List; 而 item 代表List 中的 每个元素。标签的源代码如下: public class My工 teratorTag extends BodyTagSupport II 标签需要送代的集合对象名 private String bean; II 集合对象的元素 private String item; II 集合的当前索引 private int i = 0; pr工 vate int size; prl飞rate L 工 st itemL工 st; Ilbea口属性的 setter 方法 public void setBean (String s) bean = s; Ilbean 属性的 getter 方法 public String getBea口() return bean; Ilitem 属性的 setter 方法 pUblic void setltem (String s) 工 tern = s; Ilitem 属性的 getter 方法 90制层 JSP~ public Str工 ng getItem () { return item; II 开始处理标签时,调用该方法。 public int doStartTag() throws JspTagException 1/ 从 page 范围中获取 List 对象 itemL工 at = (List= size) 1/ 将索引回零 i = 0; 1/ 不再计算标签体,直接调用doEndTag方法 return SKIP_BODY; II 将集合的当前元素值放入page 范围的 item 属性中 pageContext.setAttribute(item , itemList.get(i»); II 循环计算标签体 return EVAL_BODY_AGAIN; 1/ 标签体结束时调用该方法 public int doEndTag() throws JspTagException try { II 输出标签体内容 bodyContent.writeOut(pageContext.getOut(»); } catch (IOException ex) { throw new JspTagExcept工 on( "错误") ; return EVAL_PAGE; 下面是一个嵌套在该标签内的带属性的标签,该标签的功能非常简单,仅仅从page 范围中获取属性,然后在页面上输出该属性值。其代码如下: public class WritorTag extends TagSupport Ilitem 属性,该标签从page 中查找 item 的属性,并输出属性值 private String item; Ilitem 的 setter 方法 91轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 public void setltern (String s) { item = s; Ilitern 的 getter 方法 public String get 工 tern () return 工 tern; II 开始处理标签时的调用该方法 public int doStartTag() throws JspTagExcept工 on try { II 从 page 范围内搜索 item 的属性, pageContext.getOut() .write((String)pageContext.getAttribute(item)); catch (IOException ex) throw new JspTagException("错误 II) ; II 返回 EVAL_PAGE. 继续计算页面输出。 return EVAL_PAGE; 在处理标签类的各个方法中,不同的返回值对应不同的含义,常用的返回值有如下 几个。 • SKIP_BODY: 不处理标签体,直接调用 doEndTagO方法。 • SKIP_PAGE: 忽略标签后面的 JSP 页面。 • EVAL_PAGE: 处理标签结束,直接处理页面内容。 • EVAL_BODY_BUFFERED: 处理标签体。 • EVAL_BODY_INCLUDE: 处理标签体,但忽略 setBodyContentO和 doInitBodyO 方法。 • EVAL_BODY_AGAIN: 对标签体循环处理。 将上面两个标签配置在标签库中,标签库的配置片段如下: JSP item true true 在 JSP 中嵌套使用两个标签的代码如下: 〈屯 II 创建 List 对象 List a = new ArrayList(); a.add( "hello"); a.add( "world"); a . add (" java" ); II 将 List 放入 page 范围的属性 a pageContext. setAttribute ("a" , a); 毡〉
"); out.write(rs.getString(i)); out.write("
页面的执行效果如图2 .30 所示。 图 2 .30 迭代器标签 制层JSP~ 93轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 注意:本示例的迭代器仅对 page 范围的 List 进行迭代,用法有所局限。读者可以将 其扩展,增加一个属性,指定迭代搜索的范围,并可将迭代的目标不局限于List ,而是 Collection ,甚至包括数组。大部分的框架如 Struts 、 WebWork 都包含了自己的迭代器标签。 2.11 Filter 介绍 Filter 并不是一个标准的 Servlet ,它不能处理用户请求,也不能对客户端生成响应。 主要用于对 HttpServletRequest 进行预处理,也可以对 HttpServletResponse 进行后处理, 是个典型的处理链。 Filter 有如下几个用处。 ·在 HttpServletRequest 到达 Servlet 之前,拦截客户的 HttpServletRequest 。 ·根据需要检查 HttpServletRequest ,也可以修改 HttpServletRequest 头和数据。 ·在 HttpServletResponse 到达客户端之前,拦截 HttpServletResponse 。 ·根据需要检查 HttpServletResponse ,也可以修改 HttpServletResponse 头和数据。 Filter 有如下几个种类。 ·用户授权的 Filter: Filter 负责检查用户请求,根据请求过滤用户非法请求。 .日志 Filter: 详细记录某些特殊的用户请求。 ·负责解码的 Filter: 包括对非标准编码的请求解码。 .能改变 XML 内容的 XSLTFilter 等。 一个 Filter 可负责拦截多个请求或响应:一个请求或响应也可被多个请求拦截。 创建一个 Filter 只需两个步骤: (I)创建 Filter 处理类: (2) 在 web.xml 文件中配置 Filter 。 2.11.1 创建 Filter 类 创建 Filter 必须实现 javax.servle t. Filter 接口,在该接口中定义了三个方法。 • void init(FilterConfig config): 用于完成 Filter 的初始化。 • void destroyO: 用于 Filter 销毁前,完成某些资源的回收。 • void doFilter(ServletRequest request, ServletResponse response,FilterChain chain): 实 现过滤功能,该方法就是对每个请求及响应增加的额外处理。 下面介绍一个日志 Filter ,这个 Filter 负责拦截所有的用户请求,并将请求的信息记 录在日志中: public class LogFilter 工mplements F 工 lter IIFi 1t erConfig可用于访问 Filter 的配置信息 private FilterConfig config; II 实现初始化方法 94舰层 JSP~ public void init(FilterConfig config) { this.config = config; II 实现销毁方法 public void destroy() this.config =口ull; public void doFilter(ServletRequest request , ServletResponse response , FilterChain chain) { II 获取 ServletContext 对象,用于记录日志 ServletContext context = this.config.getServletContext(); long before = System.currentTimeMillis(); System.out.println("开始过滤...") i II 将请求转换成 HttpServletRequest 请求 HttpServletRequest hrequest = (HttpServletRequest)request; II 记录日志 context.log("Filter已经截获到用户的请求的地址: "+ hrequest. getServletPath(»; try I IFilter 只是链式处理,请求依然转发到目的地址。 chain.doFilter(request , response); } catch (Exception e) { e.printStackTrace(); long after = System.currentTimeMillis(); II 记录日志 context.log("过滤结束") ; II 再次记录日志 context.log(" 请求被定位到" + ((HttpServletRequest)request). getRequestUR工() + 所花的时间为: " + (after - before)); 在上面的请求 Fil阳中,仅在日志中记录请求的URL ,对所有的请求都执行 chain.doFilter(request, reponse)方法,当 Filter 对请求过滤后,依然将请求发送到目的地址。 如果检查权限,可以在Filter 中根据用户请求的HttpSession,判断用户权限是否足够。 如果权限不够,则调用重定向即可,无须调用chain.doFilter(request, reponse)方法。 2.11.2 配置 Filter Filter 的配置和 Servlet 的配置非常相似,都需要配置两个部分: ·配置 Filter 名。 ·配置 Filter 拦截 URL 模式。 区别在于, Servlet 通常只配置一个 URL ,而 Filter 可以同时拦截多个请求的 U虹。 因此,可以配置多个 Filter 拦截模式。 95轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 下面是该 Filter 的配置片段: lee.LogFilter <' 定义 Filter 拦截地址 > log * 在上面的配置代码中,名为log 的 Filter 负责拦截所有的客户端请求,该Filter 并未 对客户端请求进行额外的处理,仅仅在日志中简要记录请求的信息。 在该 Web 应用中执行请求时,控制台的效果如图2.31 所示。 图 2.31 Filter 过滤客户端请求 2.12 Listener 介绍 Listener 的作用非常类似于load-on-startup Servlet。用于在 Web 应用启动时,启动某 些后台程序,这些后台程序负责为系统运行提供支持。 Listener 与 load-on-startup Se凹let 的区别在于: Listener 的启动时机比 load-on-startup Se凹let 早,只是 Listener 是 Servlet 2.3 规范之后才出现的。 使用 Listener 只需要两个步骤: (1)创建 Listener 实现类。 (2) 在 web.xml 文件中配置Listemer。 2.12.1 创建 Listener 类 创建Li stener 类必须实现 ServletContex tListener 接口,该接口包含两个方法。 • eontextInitialized(ServletContextEvent see): 启动 Web 应用时,系统调用该 Filter 96表现层 JSP~ 的方法。 • eontextDestroyed(ServletContextEvent see): 关闭 Web 应用时候,系统调用 Filter 的方法。 下面是任务调度的Listener 类: public class ScheduleL工 stener implements ServletContextListener II 使用 JavaTimer 作为任务调度器 private java.ut工 I.Timer timer = null; public void contextInitialized(ServletContextEvent see) II 启动调度器 timer = new Timer(true); II 记录日志 sce.getServletContext() .log(口ew java.util.Date() + ..计时器己经启 动... "); sce.getServletContext() .log (new java.util.Date() + ..计时器己经启 动... "); II 调度任务每 4 分钟执行一次 timer. schedule (new MyTask() , 0 , 2*60 费 1000) ; II 记录日志 sce.getServ1etContext() .1og(new java.uti1.Date() + ..计时器执行一 次!!!!! "); sce.getServletContext() .log (new java.util.Date() + .. 计时器执行一 次!!!!! ") i II 销毁应用之时,调用该方法 public void contextDestroyed(ServletContextEvent see) II 取消调度器 timer.cancel(); II 记录日志 sce.getServletContext() .log(new java.util.Date() + ..计时器被销 毁!!! II); sce.getServletContext() .log (new java.util.Date() + .. 计时器被销 毁!!! "); 由于使用了 JDK 的 Timer 作为任务调度器,因此必须实现如下任务类: public class MyTask extends T 工merTask { private static boolean isRunning = false; II 继承 TimerTask抽象类,必须实现run 方法,该方法的方法体就是调度的任务 public void run() II 如果任务还没有开始运行 if (! isRunning) II任务开始运行 isRunning = true; II 在控制台输出当前时间 System.out.println(new java.util.Date() + II 执行任务 for (int i = 0 ; i < 100 ; i++ ) 任务开始 II ); 97轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 System.out.pr 工口 tln( 口 ew java.util.Date() + "任务完成" + i + "I" + 100 ); try Thread.sleep(80) ; } catch (Except工 on e) e.pr工 ntStackTrace(); II任务完成,将任务运行旗标设为flase isRu口ning = false; System.out.pr工口tln(new java.ut工 I.Date () + " /I如果任务已经在运行,即使获得调度,任务直接退出。 else System.out.println(new java.util.Date() + " 2.12.2 配置 Listener 所有任务完成 1 门; 任务退出!!! II); 正如 load-an-startup Se凹let 一样, Listener 用于启动 Web 应用的后台服务程序,但 不负责处理及响应用户请求,因此无须配置URL。 若将 Listener 配置在 Web 容器中(如果 Web 容器支持 Listener),则 Listener 将随 Web 应用的启动而启动。 配置Listener 时使用元素,下面是配置Listener 的片段: lee.ScheduleListener 在上面的配置中,既无须配置Listener 的名字,也无须配置Listener 的 URL 只需 配置它的实现类即可。此时容器将自动检测部署在容器中的Listener,并在应用启动时, 自动加载所有的Listener。 2.13 JSP 2.0 的新特性 2003 年发布的 JSP 2.0 是 JSP 1.2 的升级版,新增了一些额外的特性。 JSP 2.0 的目 标是使动态网页的设计更加容易,甚至可以无须学习 Java ,即可做出 JSP 页面,从而更 好地支持团队开发。 相比 JSP 1.2, JSP 2.0 主要增加了如下新特性。 98加 JSP~ ·表达式语言。 ·简化的自定义标签 API 。 • Tag 文件语法。 ·除此之外,还可以在 web.xm1文件中配置 JSP 属性。这些属性与以前使用 page 指令的效果相同,可避免每个页面需要重复使用 page 指令定义。 如果需要使用 JSP 2.0 语法,其 web.xm1文件必须使用 Se凹let 2 .4以上版本的配置文 件, Servlet 2 .4以上版本的配置文件格式如下: <'二 Serv1et2.4 以上版本的 Web 应用配置的根元素-> <1-- 关于 JSP 的配置信息二〉 <]sp-property-group> GBK <'一不允许使用Java 脚本一〉 true <'一 隐式导入页面头--> 〈工nclude-prelude>/inc/top.jspf <'一 隐式导入页面尾--> /inc/bottom.jspf 99轻量级 J2EE 企业应用实战-Struts+Spring+Hibernate 整合开发 <]sp-property-group> <1- 对哪个文件应用配置-> /test2.jsp false GBK false 注意:如果在不允许使用Java 脚本的页面中使用 Java 脚本,则页面将出现错误。 即 test1 .jsp 页面中不允许出现Java 脚本。 看下面的 JSP 页面代码,该页面是test!扣p: 页面配置 l

页面配置工

下面是表达式语言输出:
${l + 2) 在 web.xm1文件中, test1扣p 页面配置了隐式导入,而且页面会忽略表达式,在浏 览器中浏览该页面的效果如图2 .32 所示。 页面配置1 下面是表达式i曹盲输出z $ {t + 2) 这是通过某统配置的隐式导入页尾 图 2.32 页面配置的运行效果 从图中可以看出, test l.jsp 的表达式语言不能正常输出,因为系统忽略了表达式语 言的效果。 因此,在 test2.jsp 页面中使用隐式导入,不但可以使用表达式语言,也可以使用 Java 脚本,页面代码如下: 页面配置 2 100统表现层 JSP~

页面配置 2

下面是表达式语言输出:
$ (l + 2 l
下面是小脚本输出:
〈毡 out.println("hello Java"); 宅〉 页面运行效果如图2.33 所示。 页面配置2 下面是表示式语言输出, 下面是小脚本输出E hello Java 图 2.33 页面配置运行效果 此时,该页面允许使用表达式语言,页面正常输出了表达式语言的值。另外,也可 以使用 Java 脚本,但没有隐式导入,所以没有页头和页尾。 2.13.2 表达式语言 表达式语言 (Expression Language) 是一种简化的数据访问方式。使用表达式语言 可以以标记格式方便地访问 JSP 的隐含对象和 JavaBeans 组件,在 JSP 2.0 中,建议尽量 使用表达式语言使 JSP 文件的格式一致,避免使用 Java 脚本。 表达式语言可用于简化 JSP 页面的开发,允许美工设计人员使用表达式语言的语法 获取业务逻辑传过来的变量值。 注意:表达式语言是 JSP 的一个重要特性,它并不是一种通用的程序语言,而仅仅 是一种数据访问语言,可以方便地访问应用程序数据,避免使用 Java 脚本。 表达式语言的语法格式是: ${expressionl 1. 表达式语言使用算术和逻辑运算符 表达式语言支持的算术运算符非常多,在Java 语言里支持的算术运算符,表达式语 言都可以使用。 看下面的 JSP 页面 z 〈毛@ page contentType="text/html; charset=gb2312"毡〉 101轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 102 表达式语言 算术运算符

表达式语言-算术运算符


该页面运行了基本表达式运算符号,运行的效果如图2.34 所示。 图 2 .34 算数运算的表达式语言 注意:如需要在支持表达式语言的页面中正常输出"$"符号,则在"$"符号前加 特义字符 "γ' ,否则系统以为"$"是表达式语言的标记。 下面是逻辑运算符的JSP 页面代码: 〈革@ page contentType="text/html; charset=gb2312"毛〉 表达式语言-逻辑运算符

表达式语言-逻辑运算符


数字之间的比较:
表达式语言 计算结果
\$ {l} $ {l} < ltd>
\${ (1==2) ? 3 : 4} ${ (1==2) ? 3 : 4}
<1-- 直接比较两个数字一〉 108104 轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发
表达式语言 计算结果
\${1 < 2} ${l < 2}
\${1 lt 2} ${l it 2}
\${l > (4/2) } ${l> (4/2)}
\${100.0 eq 100} ${100.0 eq 100}
\${ (10*10) != 100} ${ (10*10) != 100}
\${ (10*10) 且e 100} ${(10*10) ne 100}
字符之间的比较: 传棚层 JSP~
表达式语言 计算结果
\${'a' < 'b'} ${'a' < 'b'}
\${'hip' > 'hit'} ${'h工 p' > 'hit'}
\${'4' > 3} ${'4' > 3}
因此,表达式语言不仅可在数字与数字之间比较,还可在字符与字符之间比较,字 符串的比较是根据其编码的数字来比较大小的。 2. 表达式语言的内置对象 使用表达式语言可以直接获取请求参数,可获取页面中某个JavaBean 的属性值,获 取请求头及获取session 属性值等,这些都得益于表达式语言的内置对象。 表达式语言包含如下11 个内置对象。 • pageContext: 代表该页面的 pageContext 对象,与 JSP 的 pageContext 内置对象相同。 • pageScope: 用于获取 page 范围的属性值。 • reques tS cope: 用于获取 request 范围的属性值。 • sessionScope: 用于获取 session 范围的属性值。 • applicationScope: 用于获取 application 范围的属性值。 • p缸am: 用于获取请求的参数值。 • paramValues: 用于获取请求的参数值,与param 的区别在于,该对象用于获取属 性值为数组的属性值。 • header: 用于获取请求头的属性值。 • headerValues: 用于获取请求头的属性值,与 header 的区别在于,该对象用于获 取属性值为数组的属性值。 • initParam: 用于获取请求 Web 应用的初始化参数。 • cookie: 用于获取应用的 Cookie 值。 看下面的 JSP 页面代码: 〈苍白 page contentType="text/html; charset=gb23l2"毡〉 表达式语言-内置对象

表达式语言"内置对象


请输入你的名字:
表达式语言 计算结果
\${param.name} ${param.name}&口bsp;
\${param[ "name"] } ${param["name"] }&口bsp;
\${initParam["author"]} 丰{工nitParam["author"] }
\${sessionScope["user"]} ${sessionScope["user"] }
该页面使用多个内置对象,来获取请求参数的值,请求头的信息,Web 应用的初始 化参数值及 session 属性。页面的执行效果如图2.35 所示。 谓输入你的名字z 你的名字= I..~..a 盟j 图 2 .35 使用内置对象 106表现层 JSP~ 3. 表达式语言的自定义函数 表达式语言除了可以使用基本的运算符外,还可以使用自定义函数。通过使用自定 义函数,加强了表达式语言的功能。 自定义函数的用法非常类似于标签的用法,同样需要定义函数处理类和使用标签库。 下面介绍定义函数的开发过程。 (1)开发函数处理类:函数处理类就是普通类,这个普通类中包含若干个静态方法, 每个静态方法都可定义成一个函数。 public class Functions { II 对字符串进行反转 public stat工 c String reverse( Str工 ng text ) return new StringBuffer( text) .reverse() .toString(); II 统计字符串的个数 public static int countChar( String text) return text.length(); (2) 使用标签库定义函数,定义函数方法与定义标签库方法相同。下面是定义函数 的配置文件: <1 一 标签库配置文件的根元素一〉 2.0 l.O <1-- 定义第一个函数 > reverse lee.Funct 工 0口吕 java.lang.String reverse( java.lang.String ) countChar lee.Functions <1- 定义函数的对应的方法-> int countChar( java.lang.String ) 107轻量级 J2EE 企业应用实战一一Struts+Spring+Hibernate 整合开发 (3)与自定义标签库相同,在 web.xml 文件中要增加标签库定义。 (4) 在 JSP 页面中使用函数时也需要先导入标签库,然后再使用函数。下面是使用 函数的 JSP 页面片段: <%@ page conte口tτγpe="text/html;charset=gb23l2" 毡〉 〈毡@ taglib prefix="my" uri="/tags/mytag"在〉 表达式语言-自定义函数

表达式语言 自定义函数


请输入---个字符串:
字符串=
表达式语言 计算结果
\S {param[ "name"] } ${param["name"]} 
\${my:reverse(param["name"])} ${my:reverse(param["name"])} 
\${my:reverse(my:reverse(param["name"]))} ${my:reverse(my:reverse(param["name"]))} 
\${my:countChar(param["name"])} ${my:countChar(param["name"])} 
注意:函数处理类的方法必须是public static ,因为这些方法是直接调用,无须实例化。 自定义函数,也提供了类似于自定义标签库的作用,使用简单的指令就可以完成复 杂的功能。与自定义标签库不同的是,可以在表达式语言中直接使用函数。 2.13.3 简化的自定义标签 JSP 2.0 的自定义标签更加简单,无须重写烦琐的doStartTag 和 doEndTag 等方法, 108传统表现层 JSP~ 即使是带标签体的标签,也与不带标签体的标签处理方式完全相同,无须重写 doAfterBody 等方法,通常只需重写 doTag 方法。 下面以一个迭代器标签为示例,介绍 JSP 2.0 的自定义标签的开发步骤。 (1)书写标签处理类。 JSP 2.0 的标签处理类继承 SimpleTagSupport 类,通常只需重写 doTag 方法,即使是 带标签体的标签,通常也只需重写 doTag 方法。下面是标签处理类的代码: II 简单标签处理类,继承 SimpleTagSupport 类 publ 工 c class MylteratorTag extends SimpleTagSupport II 标签属性 private String bean; II 标签属性必须的setter 和 getter 方法 public ‘void setBean(String bean) this.bean = bean; } public String getBean() { return bean; II 标签的处理方法,简单标签处理类只需要重写doTag 方法 public void doTag() throws JspException,工OException II 从 page scope 中获取名为 bean 的集合属性 Collection itemList = (Collection 2.0 1.0 <'一定义标签名一〉 helloWorld lee.HelloWorldSimpleTag 109轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 iterator lee.MylteratorTag 吕 cr工ptless 〈毡@ attribute name="color" 毡〉 〈屯@ attribute name="bgcolor" 革〉 〈毡@ attribute name="title" 革〉 〈毡@ attribute name="bean" 革〉 111轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发
${title}
S{var}
这个 Tag File 的命名必须遵守如下规则: tagName.tag。即 Tag File 的文件名就是标 签名,文件名后缀是tag 。将该文件存放在某个路径下,这个路径就是标签库路径。笔者 将其放在/WEB-INF/tags 下, flP 笔者的标签库路径为/WEB-INF/tags。 (2) 在页面中使用自定义标签时,需要先导入标签库,再使用标签。使用标签与普 通标签用法完全相似,只在导入标签时存在一些差异。由于此时的标签库没有URI,只 有标签库路径。因此导入标签时,使用如下语法格式: 〈毡@ taglib prefix="tagPrefix" tagdir="path "在〉 其中, prefix 与之前的 taglib 指令的 prefix 属性完全相同,用于确定标签前缀:而tagdir 标签库路径下存放很多Tag File ,每个 Tag File 对应一个标签。 下面是使用标签的JSP 页面代码: <1 一导入标签一〉 〈在@ taglib prefix="tags" tagdir="/WEB-INF/tags" 毡〉 〈在@ page contentType="text/html;charset=GBK" import="java.util.*"毡〉 法代器 tag f 工 le

迭代器 tag file


〈毡 II 创建集合对象,用于迭代 List a = new ArrayList(); a. add ("hello") ; a.add("world") ; a . add (" java" ); II 将集合对象放入页面范围 pageContext.setAttribute("a" • a); 草〉 II 使用自定义标签 在该 JSP 页面中,使用了如下代码导入标签: 〈革@ taglib prefix="tags" tagdir="/WEB-工NF/tags" 在〉 即以 tags 开头的标签,使用/WEB-INF/tags 路径下的标签文件处理,而在JSP 页面 中使用如下代码标签: 112统表现层 JSP~ tags 表明该标签使用 IWEB-INF/tags 路径下的 Tag File 处理标签:而 iterator 是标签 名,对应在标签库路径下包含 iterator.tag 文件,由该文件负责处理标签。页面最终的执 行效果如图 2 .3 7 所示。 由 图 2.37 使用 Tag File 的途代器标签 Tag File 是自定义标签的简化。事实上,就如同 JSP 文件会编译成 Se凹 let 一样, Tag File 也会编译成 Tag 处理类,自定义标签的后台依然由标签处理类完成,而这个过程由 容器完成。使用 Tag File 进一步简化了自定义标签的开发。 本章小结 本章系统介绍了 JSP 的相关知识,内容覆盖了 JSP 所有知识点,包括: JSP 的三个 编译指令,七个动作指令,九个内置对象,以及 JSP 的Li stener 和 Filter 的使用。 另外,详细介绍了 JSP 的自定义标签库的用法,包括简单标签的开发,带属性标签 的开发,迭代器标签的开发等。 最后,系统介绍了 JSP 2.0 的知识,从 JSP 2.0 的配置讲起,详细介绍了表达式语言, 简化的自定义标签 A凹,以及另一种使用 Tag File 开发自定义标签的方式。 113本章要点 王瑞 传统的 Modell 和 Model2 3道 MVC 的基本知识 法 Struts 的基本知识 主选 Struts 的下载和安装 洁白 ruts 的基本使用 到 Struts 的程序国际化 被动态 FormBean 滋数据校验 主革 异常处理框架 对 Struts 的常见扩展 从实际应用开发的角度而言, Struts 应该是 MVC 框架的第一选择。因为它 具有稳定性,以及成熟的开发群体和丰富的信息资埠,保证了企业应用的稳定开 发 ο 经过长达六年的发展, Struts 已经成长为稳定、成熟的框架,并且是所有 MVC 框架中应用最广的框架 U 近来, WebWork 也加入到 Struts 阵营,更提高了 Struts 的竞争力。 如今, Struts 作为全世界第一个开源 MVC 框架,具有高度的成熟性和广泛的 项目应用,保证了其应用的稳定性。~Struts ~ 3.1 MVC 简介 MVC 架构的核心思想是:将程序分成相对独立,而又能协同工作的三个部分。通 过使用 MVC 架构,可以降低模块之间的搞合,提供应用的可扩展性。另外, MVC 的每 个组件只关心组件内的逻辑,不应与其他组件的逻辑混合。 MVC 并不是 Java 所独有的概念,而是面向对象程序都应该遵守的设计理念。 3.1.1 传统的 Modell 和 Model2 在 JSP 技术的发展初期,由于它便于掌握,以及可以快速开发的优点,很快就成了 创建 Web 站点的热门技术。在早期的很多 Web 应用里,整个应用主要由 JSP 页面组成, 辅以少量 JavaBean 来完成特定的重复操作。在这一时期, JSP 页面同时完成显示业务逻 辑和流程控制。因此,开发效率非常高。 这种以 JSP 为主的开发模型就是 Modell 。其应用具体的实现方法如图 3.1 所示。 Application Server Enterprise ServerslData Sources 图 3.1 Modell 模型图 在 Model 1 中, JSP 页面接收处理客户端请求,对请求处理后直接作出响应。其间 可以辅以 JavaBean 处理相关业务逻辑。 Model I 这种模式的实现比较简单,适用于快速开发小规模项目。但从工程化的角度 看,它的局限性非常明显: JSP 页面身兼 View 和 Controller 两种角色,将控制逻辑和表现 逻辑混杂在一起,从而导致代码的重用性非常低,增加了应用的扩展性和维护的难度。 Model2 己经是基于 MVC 架构的设计模式。在 Model2 架构中, Servlet 作为前端控 制器,负责接收客户端发送的请求,在 Servlet 中只包含控制逻辑和简单的前端处理:然 后,调用后端 JavaBean 来完成实际的逻辑处理;最后,转发到相应的 JSP 页面处理显示 逻辑。其具体的实现方式如图 3.2 所示口 由于引入了 MVC 模式,使 Model 2 具有组件化的特点,更适用于大规模应用的开 发,但也增加了应用开发的复杂程度。原本需要一个简单的 JSP 页面就能实现的应用, 在 Model 2 中被分解成多个协同工作的部分,则需花更多时间才能真正掌握其设计和实 现过程。 115轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 Enterprise Servers! Data Sourees 图 3.2 Mode12 模型图 3.1.2 MVC 及其优势 MVC 是 Model 、 View 、 Controller 三个词的缩写,三个词分别代表应用的三个组成 部分:模型、视图与控制器。三个部分以最少的稿合,协同工作,从而提高应用的可扩 展性及可维护性。 起初, MVC 模式是针对相同的数据需要不同显示的应用而设计的,其整体的效果 如图 3.3 所示。 3.3 MVC 结构 在经典的 MVC 模式中,事件由控制器处理,控制器根据事件的类型改变模型或视 图,反之亦然。具体地说,模型维护一个视图列表,这些视图为获得模型变化通知,通 常采用观察者模式登记给模型。当模型发生改变时,模型向所有登记过的视图发送通知: 接下来,视图从对应的模型中获得信息,然后更新自己。 概括起来, MVC 有如下特点。 .多个视图可以对应一个模型。按 MVC 设计模式,一个模型对应多个视图,可以 减少代码的复制及代码的维护量,一旦模型发生改变,也易于维护。 ·模型返回的数据与显示逻辑分离。模型数据可以应用任何的显示技术,例如使用 116架 Struts ~ JSP 页面、 Velocity 模板或者直接产生 Excel 文档等。 ·应用被分隔为三层,降低了各层之间的稿合,提供了应用的可扩展性。 ·控制层的概念也很有效,由于它把不同的模型和不同的视图组合在一起,完成不 同的请求。因此,控制层可以说是包含了用户请求权限的概念。 • MVC 更符合软件工程化管理的精神。不同的层各司其职,每一层的组件具有相 同的特征,有利于通过工程化和工具化产生管理程序代码。 3.2 Struts 概述 随着 MVC 模式的广泛使用,催生了 MVC 框架的产生。在所有的 MVC 框架中,出 现最早,应用最广的就是 Struts 框架。 3.2.1 Struts 的起源 Struts 是 Apache 软件基金组织 Jakarta 项目的一个子项目, Struts 的前身是 Craig R. McClanahan 编写的 JSP Model2 架构。 Struts 在英文中是"支架、支撑"的意思,这表明了 Struts 在 Web 应用开发中的巨大作 用,采用 Struts 可以更好地遵循 MVC 模式。此外, Struts 提供了一套完备的规范,以及基 础类库,可以充分利用 JSP/Se凹let 的优点,减轻程序员的工作量,具有很强的可扩展性。 Struts 1. 0 版本于 2001 年 6 月发布,目前最新的版本是此 Struts 1. 2.9o Struts 的作者 Craig R.McClanahan 参与了 JSP 规范制定以及 Tomcat4 的开发,同时还领导制定了J2EE 平台的 Web 层架构的规范。受此影响, Struts 框架一经推出,立即引起了 Java 开发者的 广泛兴趣,并在全世界推广开来,最终成为世界上应用最广泛的MVC 框架。 3.2.2 Struts 的体系结构 Struts 作为 MVC 模式的典型实现,对 Model 、 View 和 Controller 都提供了对应的实 现组件,其具体的实现如图 3 .4所示。 HTTP Request struts-config.xml invoke HTTP Response Get 图 3 .4 Struts 框架结构图 117轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 下面结合该图对 Struts 架构的工作原理简单介绍。 1. Model 部分 Struts 的 Model 部分由 ActionForm和 lavaBean 组成。其中 ActionForm用于封装用 户请求参数,所有的用户请求参数由系统自动封装成ActionForm 对象:该对象被 ActionServlet转发给 Action; 然后 Action 根据 ActionForm里的请求参数处理用户请求。 而 lavaBean 则封装了底层的业务逻辑,包括数据库访问等。在更复杂的应用中, lavaBean所代表的绝非一个简单的lavaBean,可能是EJB 组件或者其他的业务逻辑组件。 该 Model 对应图 3 .4的 Model 部分。 2. View 部分 Struts 的 View 部分采用 lSP 实现。 Struts 提供了丰富的标签库,通过这些标签库可 以最大限度地减少脚本的使用。这些自定义的标签库可以实现与Model 的有效交互,并 增加了显示功能。对应图 3 .4的 lSP 部分。 整个应用由客户端请求驱动,当客户端请求被ActionServlet 拦截时, ActionServlet 根据请求决定是否需要调用Model 处理用户请求,当用户请求处理完成后,其处理结果 通过 lSP 呈现给用户。 3. Controller部分 Struts 的 Controller 由两个部分组成。 .系统核心控制器 ·业务逻辑控制器 其中,系统核心控制器对应图3 .4中的 ActionServlet 。该控制器由 Struts 框架提供, 继承 HttpServlet类,因此可以配置成一个标准的Servlet。该控制器负责拦截所有Hπp 请求,然后根据用户请求决定是否需要调用业务逻辑控制器,如果需要调用业务逻辑控 制器,则将请求转发给Action 处理,否则直接转向请求的lSP 页面。 业务逻辑控制器负责处理用户请求,但业务逻辑控制器本身并不具有处理能力,而 是调用 Model 来完成处理。业务逻辑控制器对应图3 .4中的 Action 部分。 3.3 Struts 的下载和安装 Struts 目前的最新版本是1. 2.9 ,笔者所有的代码都基于该版本运行通过,建议读者 也下载该版本。下载和安装 Struts 请按如下步骤进行。 (1)在浏览器的地址栏输入 http://rnirror.vmrnatrix.ne t/apache/struts /b inaries/struts- 1.2.9- bin.zip ,下载 Struts 1. 2.9 。 (2) 将下载到 zip 文件解压缩,解压缩后有如下文件结构。 • contrib: 包含了 Struts 表达式的依赖类库,如 lSTL 等类库。 • lib: 包含 Struts 的核心类库, Struts 自定义标签库文件以及数据校验的规则文件 等。该文件夹下的文件是 Struts 的核心部分。 118架 St阳ts~ • webapps: 该文件夹下包含了几个 WAR 文件,这些 WAR 文件都是一个 Web 应 用,包含了 Struts 的说明文档及范例( struts-documentation 文件夹下包含了 Struts 的 API 文档,用户指南等文档,而 struts-examples 夹下则包含了 Struts 的各种简 单范例)等。将这些文件解压缩。 .其他 license 和 readme 等文档。 (3)如果需要 Web 应用增加 Struts 的支持,则应该将 lib 文件夹下的 jar 文件全部复 制到 Web 应用的 WEB-INFllib 路径下。 (4) 如果需要使用 Struts 的标签库,应该将 lib 路径下的 TLD 文件复制到 Web 应用 的 WEB-INF 路径下,并在 Web 应用的 web.xml 文件中配置对应的标签库。 (5) 如果需要使用 Struts 的数据校验,应将 lib 路径下的 validator-rules.xml 文件复 制到 WEB-INF 路径下。 (6) 如果需要使用 Struts 表达式,则应将 contrib\struts-el\l ib 路径下的 jar 文件复制 到 WEB-INF 路径下,将对应的 TLD 文件也复制到 WEB-INF 路径下,并在 web.xml 文 件中配置对应的标签库。 经过上面的步骤, Web 应用己经增加了 Struts 支持。但如果需要编译 Java 文件时能 使用 Struts 的类库,则应将 lib 路径下的 struts.jar 文件添加到 CLASSPATH 的环境变量 中即可。 3.4 Struts 入门 在讲解 Struts 的示例之前,先看一个简单的 MVC 示例。通过两种 MVC 的实现,读 者可以看出,借助框架可以减少代码量,并可让程序开发更加规范化。 注意 :MVC 只是 Struts 的一种实现方式,不使用 Struts 也可以使用 MVC。因为 MVC 是一种模式,而 Struts 则是一种实现。 3.4.1 基本的 MVC 示例 下面的范例也完全遵循 MVC 模式:其中 Model 由 JavaBean 充当; View 由 JSP 页 面充当:而 Controller 则由 Se凹 let 充当。该示例是一个简单的登录流程。 1. View 部分 示例首先进入一个等待用户输入的登录页面,该页面的源代码如下: 〈革@ page language="java" contentType="text/html;charset=gb23l2" errorPage=" error.jsp"在〉 登录 119轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 IIJavascript 校验完成客户端校验 function check(form) II 如果没有输入用户名 if (form.username.value==null II form.username.value=="" ) alert ('请输入用户名,然后再登录'); return false; II 如果没有输入密码 else if(form.pass.value==null II form.pass.value=="" ) alert ('请输入密码,然后再登录'); return false; II 两者都已经输入 else return true; 〈毡 II 用于输出出错提示,出错提示保存在request 的 err 属性里。 if (request .getAttribute ("err") ! = null) out.println(request.getAttribute("err阴)) ; 毛〉 <'一下面是登录表单 > 请输入用户名和密码:
用户名:
密 &口bsp 码:

该页面是一个简单的JSP 页面,包含了少量的JSP 脚本,用于输出Model 处理用户 请求后返回的出错提示。 该页面的运行效果如图3.5 所示。 请输入用户名秘密哥哥‘ 麟户名,厂一一一 密码, r 遂望墨 图 3.5 登录界面 120制 tr由国 该页面也包含了基本的客户端校验,如果没有输入用户名或者密码,就直接单击【登 录】按钮,数据不会提交到服务器端,而是在客户端完成校验。客户端校验的效果如图 3.6 所示。 图 3.6 客户端校验的效果 登录成功后的页面非常简单,仅需要输出用户名。成功登录后的页面为 welcome.jsp , 该页面的源代码如下: 〈草@ page language="java" contentType="text/html;charset=gb23l2" errorPage=町 error.jsp"毛〉 登录成功 欢迎您, <%=session. getAttribute ("name") 在 >
两个页面都指定了出错页面,即如果两个页面出现未捕捉异常,则自动跳转到 error.jsp 页面。 erηTO町r汇.扣p 页面则负责输出异常信息, error.jsp 页面的源代码如下: 〈草@ page language="java" contentType="text/html;charset=gb2312" isErrorPage ="true 11 在〉 系统错误 〈草 if(exception!=null)out.println(exception.getMessage()+ "
"); 毡〉
从上面的源代码可看出,所有的JSP 页面仅包含简单的显示逻辑,主要用于收集用 户信息和显示系统处理信息,不会包含任何业务处理逻辑。 121轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 2. Controller 部分 该控制器是一个标准的 Servlet ,负责拦截用户请求,解析用户请求参数,并调用合 适的 Model.即 JavaBean 处理用户请求。 该 Se凹let 的源代码如下: IIServlet. 继承 HttpServlet public class LoginServlet extends HttpServlet IIServlet 的服务响应方法 public vo工 d service(ServletRequest request , ServletResponse response) throws ServletException, java. 工 o.IOExcept工 0口 RequestD工 spatcher rd; II 解析请求参数,获取用户名 String username = request.getParameter("username"); II 解析请求参数,获取密码 String pass = request.getParameter("pass"); Str 工 ng errMsg = II 完成服务器端校验 II 校验用户名 if (username null II username. equals ( " " ) ) errMsg += "您的用户名丢失或没有输入,请重新输入II; II 校验密码 else if (pass == null II pass. equals (" ") ) errMsg += "您的密码丢失或没有输入,请重新输入II ; II 如果用户名、密码都通过校验 else try { II 调用 JavaBean ,创建 JavaBean 实例 DbDao dd = DbDao.instance("com.mysql. jdbc.Driver" , "jdbc :mysql: I Ilocalhost: 3306lliuyan" , "root" , "32147") ; II 调用 JavaBean 的方法 ResultSet rs = dd.query("se1ect password from user_table where username = 111+ username + III"); II 判断用户是否存在 if (rs.next(» II用户名存在,密码符合,则正常登录 if (rs.getStr工 ng("password").equals(pass)) II 向 Session 中存入用户名 HttpServ1etRequest hrequest = (HttpServletRequest) request; HttpSession session = hrequest.getSession(true); sess工 on.setAttr工 bute ("name" , username); II跳转到成功后的JSP 页面 rd = request. getRequestDispatcher (" Iwelcome. j sp" ); rd.forward(request , response); 122架 Str向国 else II 用户名存在,但用户名密码不符合 errMsg +=什态的用户名密码不符合,请重新输入II i else II 用户名不存在 errMsg += "您的用户名不存在,请先注册"; } catch (Exception e) { II 如果出现异常,跳转到 error.]sp 页面 rd = request.getRequestDispatcher("/error.jsp"); request.setAttribute("exception" , "业务异常") ; rd.forward(request , response); II 如果出错提示不为空,表明无法正常登录,返回登录页面 if (errMsg != null && lerrMsg.equals("")) rd = request.getRequestDispatcher("/login.jsp"); request.setAttr工bute("err" , errMsg); rd.forward(request , response); 该控制器通过调用 DbDao 来处理用户请求,而DbDao 则是本系统中的 Model,负 责持久层访问。但是该控制器存在少许问题,因为该控制器里出现了JDBC API。在严 格的J2EE 架构分层里,这是不允许出现的。可将所有传到控制器的值都应封装成VOC 值 对象) ,而不应该与JDBC API 糯合。 3. Model 部分 本范例的 Model 由于 DbDao 充当,该 DbDao 仅是一个数据库访问的 JavaBean ,并 不包含任何业务逻辑,对于严格的J2 EE 应用而言,该 JavaBean 更像一个 DAO 对象, 而不是业务逻辑对象。 对于分层更清晰的J2 EE 应用,在 Model 下隐藏了更加清晰的分层:业务逻辑层及 DAO 层等。 下面是 DbBean 的源代码: publ 工 c class DbDao { private static DbDao op; II 数据库连接 private Connect工 on conn; II 数据库驱动 pr工 vate String dr工veri II 数据库服务的 url private Str工 ng url; II 数据库用户名 private String username; 123124 轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate整合开发 II 数据库密码 pr 工 vate String pass; II 构造器私有,准备做成单态模式 pr工vate DbDao () II 带参数的构造器 private DbDao (String driver, String urI , String username , String pass) throws Exception this.driver = driver; this.url = urI; this.username = username; this.pass = pass; Class.forName(driver); conn = DriverManager.getConnection(url , username , pass); II 各属性的 setter 方法 public void setDriver(String driver) { this.driver = driver; public void setUrl(String urI) { this.url = urI; } public void setUsername(String username) { this.username = username; public vo工 d setPass (String pass) { this.pass = pass; } II 各属性的 getter 方法 publ 工 c String getDriver() { return (this.driver); } public String getUrl() { return (this.url); public Str 工 ng getUsername() { return (this.username); } public String getPass() { return (this.pass); II 获取数据库连接 public void getConnection()throws Exception if (conn -- null) Class.forName(this.driver); conn = DriverManager.getConnection(this.url , this.username , this .pass) ; II 静态方法,返回 DbDao 实例 public static DbDao instance() if (op == null) {Struts ~ op = new DbDao(); return op; II 静态方法,返回DbDao 实例 public static DbDao instance(String dr工ver , String url , String username , String pass) throws Exception if (op == null) op = new DbDao(driver,url,username ,pass); return op; II 数据库访问操作,执行插入操作 public boolean insert(String sql) throws Exception II 初始化数据库连接 getConnection(); II 创建 Statement 对象 Statement stmt = this.conn.createStatement(); 工 f (stmt.executeUpdate(sql) != 1) return false; return true; II 数据库访问操作,执行查询操作 public ResultSet query(String sql) throws Exception II 初始化数据库连接 getConnection() ; II 创建 Statement 对象 Statement stmt = this.conn.createStatement(); return stmt.executeQuery(sql); II 数据库访问操作,执行删除操作 public void delete(String sql) throws Exception II 初始化数据库连接 getConnection(); II 创建 Statement 对象 Statement stmt = this.conn.createStatement(); stmt.executeUpdate(sql); II 数据库访问操作,执行更新操作 public void update(String sql) throws Exception II 初始化数据库连接 getConnection(); II 创建 Statement 对象 Statement stmt = this.conn.createStatement(); stmt.executeUpdate(sql); 从上面的示例中可看出MVC 模式的基本结构及其应用流程。但也可以发现,基于 MVC 模式的开发,比单纯JSP 的开发要复杂。 125轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 因此,使用框架,则可以大大减少代码的重复量,而且可以规范软件开发的行为。 下面介绍采用 Struts 来完成相同的功能。 3.4.2 Struts 的基本示例 Struts 框架的应用使开发更加规范、统一。所有的控制器都由两部分组成一一核心控 制器与业务逻辑控制器。核心控制器负责拦截用户请求,而业务逻辑控制器则负责处理 用户请求。 为了让核心控制器能拦截到所有的用户请求,应使用模式匹配的 Struts 的核心控制 器 Se凹let 的 URL 。配置 Struts 的核心控制器,需要在 web.xml 文件中增加如下代码: act工 onSevlet org.apache.struts.action.ActionServlet actionSevlet *.do 从上面的配置可出,所有以.do 结尾的请求都会被actionServlet 拦截,该 Servlet 由 Struts 提供,它将拦截到的请求转入Struts 体系内。 Struts 的视图依然采用JSP,该示例与前面MVC 的示例并无太大区别,只需将form 提交的 URL 改为 login.do 即可。以.do 结尾可以保证该请求被Struts 的核心控制器拦截, 其他并没有太多区别,此处不再赘述。 1. Controller部分 核心控制器 ActionServlet 由系统提供,负责拦截用户请求。 业务控制器用于处理用户请求, Struts 要求业务控制器继承 Action,下面是业务控 制器 LoginAction 的源代码: II 业务控制器必须继承Action public class LoginAction extends Action II 必须重写该核心方法,该方法负责处理用户请求 public ActionForward execute(ActionMapping mapping , ActionForm form , HttpServletRequest request , HttpServletResponse response) throws Exception II 解析用户请求参数 String username = request.getParameter("username"); String pass = request.getParameter("pass"); II 出错提示 String errMsg = II 进行服务器端的数据校验 if (username == null II username.equals("")) 126经典 MVC 框架 Struts 川国 errMsg += "您的用户名丢失或没有输入,请重新输入" ; else if(pass == null II pass.equals("")} errMsg += "您的密码丢失或没有输入,请重新输入"; else II 如果可以通过服务器端校验,则调用JavaBean处理用户请求 try DbDao dd = DbDao. instance ("com.mysql. jdbc. Driver" , "jdbc:mysql:lllocalhost:3306/liuyan" , "root" , "32l47"); ResultSet rs = dd.query("select password from user_table where username = + username + "'"}; II 判断用户名和密码的情况 if (rs.next ()) II 如果用户名和秘码匹配 if (rs.getString("password") .equals(pass}) HttpSession session = request.getSess 工 on(true) ; session.setAttribute("name" , username); return mapping.findForward("welcome"); } else II用户名和秘码不匹配的情况 errMsg += "您的用户名密码不符合,请重新输入"; else II用户名不存在的情况 errMsg += "您的用户名不存在,请先注册" ; } catch (Exception e) { request.setAttribute("exception" , "业务异常" ); return mapping.findForward("error"); if (errMsg != null && !errMsg.equals(帽" )} II 如果出错提示不为空,跳转到 input request.setAttribute("err" , errMsg}; return mapping.findForward("input"}; } else II否则跳转到welcome return mapping.findForward("welcome"); 上面的控制器非常类似于LoginServlet,只是将 Servlet 中响应方法的service 逻辑放 127轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 到了 Action 的 execute 方法中完成。 但注意到 execute 方法中除了包含 HttpServletRequest , HttpServletResponse 参数外, 还包括了两个类型的参数: ActionForm, ActionForward 。这两个参数分别用于封装用户 的请求参数和控制转发。可以注意到: Action 的转发无须使用 RequestDispatcher 类,而 是使用 ActionForward 完成转发。 注意:业务控制器 Action 类,应尽量声明成 public ,否则可能出现错误。并注意重 写的 execute 方法,其后面两个参数的类型是 HttpServletRequest 和 HttpServlet Response, 而不是 ServletRequest 和 SedvetResponse 。 2. Struts 的配置文件 在转发时也没有转向一个实际的JSP 页面,而是转向逻辑名error, input, welcome 等。 逻辑名并不代表实际的资源,因此还必须将逻辑名与资源对应起来。 实际上,此处的控制器没有作为 Servlet 配置在 web.xml 文件中,因此必须将该 Action 配置在 Struts 中,让 ActionServ let 了解将客户端请求转发给该 Action 处理。而这一切都 是通过 struts-config.xml 文件完成的。 下面是 struts-config.xml 文件的源代码: <' 配置该 Action 的转发一〉 actionSevlet <' 指定该 Servlet 的实现类--> org.apache.struts.action.ActionServlet 2 actionSevlet *.do 该 ActionServlet作为一个标准 Servlet,配置在 Web 应用中,负责拦截用户请求。 该 Se凹let 还有加载 Struts 配置文件的责任。但这里并未告诉它如何加载Struts 的配置文 件,以及 Struts 的配置文件放在哪里及文件名是什么。 ActionServlet默认加载 WEB-INF 下的 struts-config.xml文件。如果需要Struts 的配 置文件不在 WEB-INF路径下,或者改变了文件名,则应采用如下方式配置: actionSevlet org.apache.struts.action.ActionServlet config /WEB-INF/struts-config-user.xml 2 在上面的配置中,指定了ActionServlet 的配置文件: struts-config-user.xml文件,该 文件作为 init-param 参数载入,载入时候指定了参数名:config a config 是 Struts 固定的 参数名, Sm归负责解析该参数,并加载该参数的指定的配置文件。 Struts 支持使用多个配置文件,当有多个配置文件时,应将不同的配置文件配置成 不同的模块,并指定不同的 URI 。下面的片段配置了两个配置文件: <1 一 Act 工 onServlet 的名--> actionSevlet org.apache.struts.action.ActionServlet <'一 指定 Struts 的第一个配置文件 > config /WEB-INF/struts-conf工 gl.xml 2 上面的配置片段中指定了两个配置文件:struts-config l.xml 和 struts-config2.xml 文 件。这两个配置文件分别被配置到 config 和 configlwawa 的路径下。表明将 struts-config 1.xml 中的 Action 映射到应用的根路径下,而 struts-config2.xml 文件中的 Action 则被映射到应用的 wawa 子路径下。也就是说 wawa 将作为系统的一个模块使用。 131轻量级 J2EE 企业应用实战-Struts+Spring+Hibernate 整合开发 3.5.2 配置 Actio nForm 配置 ActionForm ,必须包含 ActionForm 类才行。 Struts 要求 ActionForm 必须继承 Struts 的基类: org.apache.struts.action.ActionFormo ActionForm 的实现非常简单,该类只 是一个普通的 JavaBean ,只要为每个属性提供对应的 setter 和 getter 方法即可。 根据前面的讲解, ActionForm 用于封装用户的请求参数,而请求参数是通过JSP 页 面的表单域传递过来的。因此应保证 ActionForm 的参数与表单域的名字相同。 注意: JavaB ean 的参数是根据 getter 、 setter 方法确定的。如果希望有一个 A 的属性, 则应该提供 getA 和 setA 的方法。 下面使用 ActionForm 对前面的示例再次改写。 1. ActionForm 的实现 ActionForm 的属性必须与JSP 页面的表单域相同。本示例的表单包含如下两个表单域: • usemame • password 因此, ActionForm 必须继承 org.apache.struts.action.ActionForm,并为这两个域提供 对应的 setter 和 getter 方法,下面是 ActionForm 的源代码: IIActionForm 必须继承 Struts 的基类 public class LoginForm extends ActionForm { pr~vate String username; private String password; II 表单域 username对应的 setter 方法 public void setUsername(String username) { this.username = username; II 表单域 password对应的 setter 方法 public void setPassword(String password) this.password = password; II 表单域 username对应的 getter 方法 public String getUsername() return (this.username); II表单域 password对应的 getter 方法 public String getPassword() return (this.password); 另外,该 ActionForm 的两个属性名可以不是usemame 和 password。只要提供了 usemame 和 password 的 setter 和 getter 方法即可。 132阳回 注意: FormB ean 类应尽量声明成 publico 2. ActionForm 的配置 所有的 ActionForm 都被配置在 struts-config.xml 文件中,该文件包括了一个 form-beans 的元素,该元素内定义了所有的ActionForm,每个 ActionForm 对应一个 form-bean 元素。 为了定义 LoginForm. 必须在 struts-config.xml文件中增加如下代码: <'一用于定义所有的ActionForm--> 3.5.4 配置 Forward 正如前面所讲, Forward 分局部 Forw缸d 和全局 Forward 两种。前者在 Action 里配 置,仅对该 Action 有效:后者单独配置,对所有的 Action 都有效。 配置 Forward 非常简单,主要需要指定以下三个属性。 • name: 该 Forward 的逻辑名。 • path: 该 Forward 映射到的 JSP 资源。 • redirect: 是否使用重定向。 局部 Forward 作为 Action 的子元素配置;全局 Forward 配置在 global-forwards 元素里。 下面是配置全局 Forw缸d 的代码: 134Struts ~ 上面的配置代码中,配置了一个全局Forward,该 Forward 可以被所有的 Action 访 问。通常,只将全局资源配置成全局Forwardo 当每个 Action 在转发时,首先在局部Forward 中查找与之对应的Forward,如果在 局部 Forward 中找不到对应的 Forward 对象,才会到全局Forward 中查找。因此,局部 Forward 可以覆盖全局Forward。 下面提供了该应用的struts-config.xm1文件的全部源代码: <' 配置 Action 的 path. type 属性 name 属性配置 Action 对应的 ActionForm--> 3.6 Struts 程序的国际化 国际化是指应用程序运行时,可根据客户端请求来自的国家/地区、语言的不同而显 示不同的界面。例如,如果请求来自于中文操作系统的客户端,则应用程序中的各种标 签、错误提示和帮助等都使用中文:如果客户端使用英文操作系统,则应用程序能自动 识别,并作出英文的响应。 引入国际化的目的是为了提供自适应、更友好的用户界面,而并未改变程序的逻辑 135轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 功能。国际化的英文单词是 Internationalization ,有时也简称I1 8N 。其中 I 是这个单词的 第一个字母, 18 表示这个单词的长度,而 N 代表这个单词的最后一个字母。 Struts 的国际化也是基于 Java 的国际化的,下面先介绍 Java 程序的国际化。 3.6.1 Java 程序的国际化 Java 程序的国际化思路是将程序中的标签、提示等信息放在资源文件中,每个程序 需要所有支持的国家飞语言,都必须提供对应的资源文件。其资源文件是 key-value 对, 每个资源文件中的 key 是不变的,但 value 则随不同国家飞语言而变化。 Java 程序的国际化主要通过如下三个类完成。 • java.util. ResourceBundle: 对应用于加载一个资源包。 • java.uti l. Locale: 对应一个特定的国家/区域及语言环境。 • java.text.MessageFormat: 用于将消息格式化。 为了实现程序的国际化,必须先提供程序所需要的资源文件。资源文件的内容是和 很多 key-value 对。其中 key 是程序使用的部分,而 value 则是程序界面的显示。 资源文件的命名可以有如下三种形式。 • baseName _language 30untry.properties。 • baseName _language.properties。 • baseNarne. properties 。 其中 baseName 是资源文件的基本名,用户可以自由定义。而 language 和 count可都 不可随意变化,必须是 Java 所支持的语言和国家。 1.国际化支持的语言和国家 事实上, Java 也不可能支持所有国家和语言,如需要获取 Java 所支持的语言和国家, 可调用 Locale 类的 getAvailableLocale 方法来获取。该方法返回一个 Locale 数组,该数 组里包含了 Java 所支持的语言和国家。 下面的程序简单地示范了如何获取 Java 所支持的国家和语言: public class LocaleList public static void main(Str工 ng[] args) { II 返回 Java 所支持的全部国家和语言的数组 Locale[] localeList = Locale.getAvailableLocales(); 1I:i!!i历数组的每个元素,依次获取所支持的国家和语言 for (int i = 0; i < localeList.length ; i++ ) II 打印出所支持的国家和语言 System.out.println(localeList[i] .getDisplayCountry() + "=" + localeList[i] .getCountry()+ " " + localeList[i] .getDisplayLanguage() + "=" + localeList[i] .getLanguage()); 程序的运行结果如图3.8 所示。 136国 经典 MVC 框架 Struts 公持稳制峭 4 叶、一指 adg 黯=法文=fr 比利时=BE 法文=fr I JJo拿大=CA 法文=fr 唰士=CH 法文=fr f去国=FR 法文=fr 庐森堡=LU 法文=fr 仨克罗地亚文=hr 克罗地亚=HR 克罗地亚文=hr 民匈牙利文=hu 同町牙利=HU 匈牙利文=hu 卡冰岛文=is 冰岛=15 冰岛文=is 卡意大利文=i t l 瑞士=CH 意大利文 =it 幢大利 =IT 意大利文=it ~立陶宛文=1t i立陶宛=LT 立陶宛文=lt 卡拉托维亚文(列宁C)=lv 国际化所支持的语言和国家 2. 编写国际化所需的资源 国际化所需的资源文件内容是 key-value 对,下面提供了两个资源文件,这两个资源 文件很简单,只包含一个 key-value 对。 下面是 MyResource.properties 的文件的内容: 图 3.8 II 资源文件的内容: key-value 对。 msg=Hello , {O} lToday is {1}. 下面是 MyResource_zh_CN.properties 文件的内容: II 资源文件的内容: key-value 对 msg=你好. {O}! 今天是{l}。 所有资源文件的 key 都是相同的,只是value 会随国家和语言的不同而变化。 对于所有的非西欧文字还必须使用native2ascii 命令转化,该命令负责将非西欧文字 转换成系统可以识别的文字。 因此必须对 MyResouce_zh_CN.properties 文件进行转化。 命令的格式如下: native2ascii MyResouce_zh_CN.properties MyResouce_x.properties 转换后新生成的文件内容会出现很多乱码,这是正常的。可将其转换成生成的 MyResouce_x.properties文件重命名为 MyResouce_zh_CN.properties 即可。 完成程序国际化 上面的资源文件实质上有如下两种。 • MyResouce_zh_CN.properties: 指定了确定的国家和语言的资源文件。 • MyResouce.properties: 没有指定国家和语言的资源文件。 下面的程序可实现程序的国际化: 3. 137 public class Hello {轻量级 J2EE 企业应用实战一-St阳 ts+Spring+Hibernate 整合开发 public static void main(String[] args) { Locale currentLocale = null; II 如果运行程序时指定了国家和语言参数,则以此创建Locale 对象 if (args.length 2) currentLocale = new Locale(args[O] , args[l]); II 否则,直接使用默认的国家和语言 else currentLocale = Locale.getDefault(); II根据 Locale 加载资源包 ResourceBundle bundle = ResourceBundle.getBundle("MyResource" currentLocale); II 根据 key 获取对应的资源 String msg = (String)bundle.getObject("msg"); II 创建 MessageFormat对象,用于获取格式化消息 MessageFormat mf = new MessageFormat(""); II 创建 Locale 对象 II 设置国际化时所使用的Locale 实例 mf.setLocale(currentLocale) ; II 设置需要格式化的消息 mf.applyPattern(msg); Date now = new Date(); II 为消息中需要的参数指定值 Obj ect [] msgParams = {"yeeku" , now} ; II 输出国际化消息 System.out.println(mf.format(msgParams)); 如果运行时没有指定表示国家和语言的参数,则程序的运行效果如图3.9 所示。当 然,笔者所运行的环境是简体中文的操作系统。 运行时,如果指定了两个参数:en Us ,则表明用于显示美国英语的环境,图3.10 显示了程序的运行效果。 图 3.9 没有指定参数时程序国际化的效果 图 3.10 指定参数时程序国际化的效果 但是,程序并没有指定MyResource_en_US. properties 文件,程序从哪里获取资源呢? 在 ResourceBundle 加载资源时按如下顺序搜索。 搜索所有国家和语言都匹配的资源文件,例如,对于简体中文的环境,先搜索如下文件: MyResource_zh_CN.properties 如果没有找到国家和语言都匹配的资源文件,则再搜索语言匹配的文件,即搜索如 下文件: MyResource_zh.properties 138V 阳rum~ 如果上面的文件依然无法搜索到,则搜索 baseNarne 匹配的文件,即搜索如下文件: MyResource.properties 4. 使用类文件代替资源文件 Java 也允许使用类文件代替资源文件,即将所有的key-value 对存入 class 文件,而 不是属性文件。 用来代替资源文件的Java 文件必须满足如下条件。 ·类的名字必须为baseNarne_language_count町,这与属性文件的命名相似。 ·该类必须继承ListResourceBundle,并重写 getContents 方法,该方法返回Object 数组,该数组的每一个项都是key蛐 value 对。 下面的类文件可以代替上面的属性文件: public class MyResource_zh_CN extends ListResourceBundle { /I 定义资源 private final Object myData[] []= {"msg" ,"{ O} .您好!今天是{l} "} II 重写方法 getContents() public Object[] [] getContents() II 该方法返回资源的key-value对 return myData; 如果系统同时存在资源文件及类文件,则系统将以类文件为主,而不会调用资源文 件。对于简体中文的Lοcale, ResourceB undle 搜索资源的顺序是: ( 1) baseName zh CN.class 。 (2) baseNarne_zh一CN.properties。 (3) baseNarne zh.class 。 (4) baseNarne_zh.properties。 (5) baseNarne.class。 (6) baseNarne.properties。 当系统按上面的顺序搜索资源文件时,如果前面的文件不存在,则会使用下一个: 如果一直找不到对应的文件,系统将抛出异常。 3.6.2 Struts 的国际化 Struts 的国际化也是通过 ResourceBundle 完成的。因此,也必须编写资源文件。下 面以前面的应用为示例,演示如何实现程序的国际化。 1. 编写资源文件 本示例程序能满足两种语言环境:简体中文和英语。当然,需要满足更多国家的语 139轻量级 J2EE 企业应用实战一-8t阳ts+8pring+Hibernate 整合开发 言也不是问题,只需提供对应的资源文件即可。 下面是两份资源文件: II 英文的资源文件 username=username pass=password login=submit noname=please enter name nopass=please enter password 下面是中文的资源文件: II 中文的资源文件 username=用户名 pass=密码 login=登录 noname=请输入用户名,然后再登录 noname=请输入密码,然后再登录 注意:对于非西欧文字的资源文件,必须使用native2ascii进行转换。 2. 加载资源文件 资源文件的加载通过struts-config.xml文件来配置,加载资源文件应从Web 应用的 WEB-INF/classes路径开始加载。因此,资源文件必须放在WEB-INF/classes 路径或该路 径的子路径下。如果直接放在WEB-INF/classes 路径下,在配置资源文件时,直接指定 资源文件的 baseName 即可。但如果放在子路径下,则必须以包的形式配置。 下面的示例程序中的资源文件放在WEB-INF/classesl1ee 下。配置资源文件的 struts-config.xml文件的源代码如下: 140C 框架 ""m~ Struts 负责加载资源文件, Struts 在应用启动时将加载该资源文件。 注意:如果需要Struts 实现程序国际化,必须将ActionServlet配直成 load-on-startup 的 Se凹let ,只有这样才可以保证在启动应用时加载该资源文件。 3. 使用 bean 标签显示国际化信息 根据前面的国际化示例程序我们知道,程序要实现国际化,则不能将标签及帮助等 提示信息以硬编码方式写在程序中,而应使用资源文件的key 。 Struts 提供了专门用于国际化的标签bean,关于 bean 标签,将在后面深入讲解。此 处仅介绍国际化支持使用的bean 标签。 为了可以在 Web 应用中使用 bean 标签,在应该将 struts-bean.tid 复制到 WEB-INF/ 路径下,并在web.xml 文件中增加该Struts 标签库的配置。 下面的 web.xml 文件增加了 Struts struts-bean 标签库,源代码如下: actionSevlet org.apache.struts.action.ActionServlet l <1 一配置 ActionServlet映射的 URL一〉 actionSevlet *.do <'… 配置标签库的uri--> /tags/struts-bean /WEB-工NF/struts-bean.tld
经过上面的配置, JSP 页面可以使用 bean 标签了,从而可以通过bean 标签显示国 际化提示。 下面是 login扣p 文件的源代码,该文件中不再以硬编码的方式输出提示,而是输出 141轻量级 J2EE 企业应用实战 St阳 ts+Spring+Hibernate 整合开发 的资源文件的 key: 〈草@ page language="java" contentτ'ype="text/html;charset=gb2312"errorPage=""毛〉 〈幸自 taglib uri="/tags/struts-bean" prefix="bean"毛〉 <bean:message key="login"/> funct工 on check(form) 工 f (form.username.value==null II form.username.value=="" ) alert ( '' ); return false; else if(form.pass.value==null II form.pass.value=="" ) { alert (',); return false; else return true; } 〈革 if (request.getAttr工 bute("err") != null) out.println(request.getAttr工 bute ("err") ) ; 在〉
<'一 使用国际化资源文件的key 输出用户名标签一〉 <工口put type="text" name="username"/>
<'一 使用国际化资源文件的key 输出密码标签二〉
'/>
该页面运行效果如图3.11 所示。 因此,实现国际化只需要编写资源文件,然后使用bean 标签输出国际化信息。使用 bean 标签,可以大大降低程序国际化的复杂度,这就是使用框架的优势。 142框…国 图 3.11 Struts 的程序国际化效果 3.7 使用动态 ActionForm Struts 的 ActionForm 虽然比较简单,但也是异常烦琐的类。说简单,是因为每个类 的写法非常简单,只需要为每个表单域提供对应的 setter 和 getter 方法即可。说烦琐,是 因为必须大量书写这种简单的类。 好在 Struts 提供了动态 ActionForm ,通过使用动态 ActionForm ,可以完全不用书写 ActionForm ,只需在 struts-config.xml 文件中配置即可。 3工 1 配置动态 ActionForm 所有的动态 ActionForm 的实现类都必须是 org.apache.struts.action.Dyn aActionForm 类,或者是它的子类。 使用动态 ActionForm 与前面不同的是:因为系统不清楚动态 ActionForm 的属性, 所以必须在配置文件中配置对应的属性。可以使用 form-property 元素来配置动态 Actio nForm 的属性。 下面是使用动态 Actio nForm 的 struts-config.xml 文件的源代码: 143轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 从上面的配置文件可看出,动态ActionForm 的配置必须增加 form-property 元素, 每个属性必须对应一个form-property元素。 form-property元素包含两个属性。 • name: 属性的名字,必须与 JSP 页面的表单域的名字相同。 • type: 属性的类型。 3工2 使用动态 ActionForm 动态 ActionForm 同样用于封装用户请求参数,但该 ActionForm 没有各属性的 getter 及 setter 方法,因此无法调用对应的 getter 方法来解析请求参数。 幸好 DynaActionForm 提供了多个重载的 getter 方法用于获取请求参数,下面的 Action 使用 DynaA ctionForm ,该 Action 的源代码如下: IIAction 必须继承 Action 类 public class LoginAction extends Action { II 必须重写该核心方法,该方法 actionForm 将表单的请求参数封装成值对象 public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request , HttpServletResponse response) throws Exception II将 ActionForm强制类型转换为DynaActionForm DynaActionForm loginForm = (DynaActionForm)form; II 从 ActionForm中解析出请求参数: username String username = (String)loginForm.get("username"); II 从 ActionForm中解析出请求参数: password String pass = (String)loginForm.get("pass"); II 后面的处理与前一个示例的Action 相同。 使用动态 ActionForm 时,请求参数必须使用DynaActionForm的 getter 方法获取。 DynaActionForm 的 getter 方法主要有如下三个。 • Object get(java.lang.String name): 根据属性名返回对应的值。 144框架由「幽~ • Object get(java.lang.String name, int index): 对于有多个重名表单域的情况, Struts 将其当成数组处理,此处根据表面域名和索引获取对应值。 • Object get(java.lang.String name, java.lang.String key): 对于使用 Map 属性的情况, 根据属性名及对应 key. 获取对应的值。 使用动态 ActionForm 的目的是为了减少代码的书写量。但有些IDE 工具可以自动 生成 ActionForm. 则可以使用普通 ActionForm。 动态 ActionForm 与普通 ActionForm 并没有太大的区别。动态 ActionForm 避免了书 写 ActionForm,但配置变得更复杂了。而普通ActionForm 使解析请求参数变得更直观。 3.8 Struts 的标签库 Struts 提供了大量的标签库,用于完成表现层的输出。借助于 Struts 的标签库,可避 免在 JSP 中嵌入大量的 Java 脚本,从而提高代码的可读性。 Struts 主要提供了如下三个标签库。 • html: 用于生成 HTML 的基本标签。 • bean: 用于完成程序国际化,输出 Struts 的 ActionForm 的属性值等。 • logic: 用于完成循环、选择等流程控制。 3.8.1 使用 Struts 标签的基本配置 为了使用 Sturts 标签,必须将 struts-bean. tI d 文件复制到 WEB-INF 路径下,并在 web.xml 文件中增加 html 标签库的配置。 下面是增加了三个标签库配置的 web.xml 文件: <1 一 Web 配置文件的根元素一〉 <'一配置ActionServlet -• action org.apache.struts.action.ActionServlet 2 145轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 工 ndex.jsp <'一配置html 标签库的 URI--> /WEB 工 NF/struts-html.tld /WEB-INF/struts-bean.tld <'一 指定 bean 标签库的物理位置一〉 /WEB-INF/struts-bean.tld <'一配置 log工 c 标签库 > /WEB-INF/struts-logic.tld /WEB-INF/struts-logic.tld 因为每个页面都需要使用这三个标签,为避免在每个页面中重复使用taglib 标签, 可将三个标签的导入放在单独的文件中。 下面是 taglib.jsp 文件的源代码: 〈草@ taglib ur工 ="/WEB-INF/struts-bean.tld"prefix="bean" 在〉 〈革@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" 革〉 〈革@ taglib uri="/WEB-工NF/struts-logic.tld" prefix="logic" 毡〉 其他的 JSP 页面只需使用 incluede 包含该页面,即可导入三个标签库,避免了重复使 用 taglib 标签。 3.8.2 使用 html 标签库 Struts 为 htrnl 的大部分标签提供了对应的 htrnl 标签, htrnl 所支持的标签大致有如下。 • base: 表现成一个 HTML 的 标签。 • button: 表现成一个按钮,该按钮默认没有任何动作。 • cancel: 表现成一个取消按钮。 • checkbox: 表现成一个 Checkbox 的输入框。 • error: 用于输出数据校验的出错提示。 • file: 表现成一个文件浏览输入框。 • form: 表现成一个 form 域。 • frame: 表现成一个 HTML标签。 • hidde: 表现成一个隐藏域。 • htrnl: 表现成 HTML 的 标签。 • image: 表现成表单域的 image 域。 146V 架 Struts ~ • img: 表现成一个 HTML 的 img 标签。 • javascrit: 表现成 JavaScript 的校验代码,这些校验代码根据 ValidatorPlugIn 生成。 • link: 表现成 HTML 的超级链接。 • messages: 用于输出 Struts 的各种提示信息,包括校验提示。 • multibox: 表现成一个 Checkbox 输入框。 • option: 表现成选择框的一个选项。 • password: 表现成一个密码输入框。 • radio: 表现成一个单选输入框。 • reset: 表现成一个重设按钮。 • rewrite: 表现成一个 URL 。 • select: 表现成一个列表选择框。 • submit: 表现成一个提交按钮。 • text: 表现成一个单行文本输入框。 • textarea: 表现成一个多行文本框。 这些标签的使用类似于 htrn1的标签,因此比较简单。下面是对 login.jsp 页面的改写, 将原有的 htrn1标签改写成 Struts 的 htrn1标签。 当然,此处还存在没有使用 Struts 的验证框架。因此,还必须保留 JavaScript 验证。 〈毡@ page language="java" contentType="text/html;charset=gb23l2" errorPage=""毡〉 <~-- 下面采用两种方式输出sess bean 的 comment 属性一〉 ' <' 下面采用两种方式输出sess bean 的 version属性一〉
Property Name Correct Value Test Result
comment &口bsp;
domain &口bsp;
maxAge &口bsp;
path  
secure   149轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发
value  
version  
在上面的源文件中,将JSESSIONID 的 cookie 定义成一个名为 sess 的 bean,然后依 次输出该 bean 的属性,页面的运行效果如图3.12 所示。 圈 3.12 cookie 标签的运行效果 2. define 标签 define 标签主要用于定义Java 脚本可以访问的变量。定义该变量时,变量的赋值非 常灵活,可以直接为其指定特定值:也可以使用某个bean 的属性值定义变量:或者将某 个范围的 bean 转向另一个范围。例如,将pageContext下的 bean 转换到 Session 范围内。 看下面的页面代码: 150Struts 国 〈草@ page contentType="text/html;charset=gb2312" language="java" 毛〉 〈草@ taglib uri="/WEB 工NF/struts-bean.tld" prefix="bean" 毡〉 bean:define标签用来定义一个页面变量

1.下面是定义的时候直接赋值的方式
如同采用 String test1 = "测试属性" ; 一样
〈革 =test1革〉


2. 下面是把一个变量的值赋给另一个变量的值
如同 String test2 = test1;
〈革 =test2革 >

3 .下面将某个 bean 的属性定义成一个页面变量
如同 String test3 = bean1.getPassword(); 〈毡 =test3毡 >


4. 下面将来自某个范围的bean 转到另一个范围
如同 session.setAtrribute("bean2".bea口 1) ;

5.bean:write的作用:
=((LoginForm)session.getAttribute("bean2")) .getPassword()
在上面的页面文件中,出现了以下4 种情况。 (1)在使用 bean 标签定义 Java 脚本可以访问的变量时,可直接为变量赋值。用法 如下: 将 variableValue的值赋 给 varialeName的变量。 (2) 将一个变量的值直接赋给另一个变量,用法如下: : 将 variable2 的值赋给 variablelo (3)将某个 bean 的属性定义成页面可以访问的变量,用法如下: : 将 beanName 的 beanProperty属性的值,赋给variableName的变量。 (4) 将一个范围的bean 转换到另一个范围内,用法如下: : 将 scope1 中的 bea口1 转到 scope2 的 bea口2 。 在最后部分演示了bean:write 的作用一一-输出bean 的属性值。图 3.13 显示了该页面 的运行效果。 151轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 1.下面是定义的时候直接赋值的方式 如同采用 String test1 ;" 11赋属性"一样 精赋属性 2 下面是把一个变量的值赋给另一个变量的值 如同 String test2 = test1; 测试属性 3 下面将某个bean的属性定义成一个页面变量 生响 String test3 = bcmli.. getP 回回町 dO; tiger 4 下面将来自某个黯围的be'"借到另一个范围 如同 SCSS10肌 setAtrribute(Nbem'l2'二 beon!) ; 5. be 町盯 i 恒的作用 B =«Loginf 田-m )sessi on. getAtt 主讪 ute("be en2")). getP..sO tiser 图 3.13 bean:define标签的运行效果 3. header 标签 header 标签用于将特定的请求头信息包装成脚本可以访问的变量。header 的用法如下: , 将 headerName 的请求头定 义成 variableName 的变量。 看下面的 JSP 页面源代码: 〈毡@ page contentType="text/html;charset=gb2312"毛〉 〈屯@ taglib ur工 ="/WEB-INF/struts-bean.tld"prefix="bean" 革〉 bean:header标签测试<It 工 tIe> </head> <body> <div align="center"> <hl>bean:header标签测试</hl> </div>显示本次请求的http 文件头 <br I> <br I> 〈毡 II 获取所有的请求头 java.util.Enumeration names = ((HttpServletRequest) request} .getHeaderNames(); 革〉 <table border="l"> <tr> <th>Header Name</th> <th>Header Value</th> </tr><毡 II J6历请求头,获取请求头的名字,根据请求头名依次输出每个头对应的值 while (names.hasMoreElements(» { String name = (String) names.nextElement(); 毡〉 <bean:header id="head" name="<草= name 毡>" I> <tr> <td> 152~ Struts 国 〈宅= name 毡〉 <ltd> <td> 〈革= head 革〉 <ltd> </tr><革 } 毡〉 </table> </body> </html> 4. parameter标签 parameter 标签用于将请求参数封装成一个脚本可以访问的变量,用法如下: <bean:parameter id="var工 ableName" name="parameter"/>: 将请求 parameter 的请求 参数定义成variableName的变量。 看如下代码: bean:parameter Tag测试 图 3.14 bean:paramter的运行效果 将请求参数封襄成beon 〈鲁@ page contentType="text/html;charset=gb2312"草〉 〈在@ tag1ib uri="/WEB-INF/struts-bean.tld" prefix="bean" 革〉 <html> <head> <title>bean:parameter Tag 测试

bean:parameter Tag 测试

将请求参数封装成bean

页面的运行效果如图3.14 所示。 注意:在程序的运行效果围的地址栏中 包含了 b 的请求参数,使用 bean:parameter 定义变量时,请求参数中必须包含b 的请求 参数。 5. include 标签 include 标签用于将一个完整的JSP 页面定义成 beano 用法如下: : 将 uri 对应的 JSP 资源定义成beanName 的 bean" 看下面的 JSP 页面: 〈屯@ page contentType="text/html;charset=gb2312"革〉 〈毡@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" 毡〉 153轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 测试 bean:include 标签 bean:include主要用来寻|入另外一个页面
测试 bean:include标签

  下面的内容是通过=header 输出 
〈宅= head 革〉

下面的内容是 filter="false"的输出


下面的内容是 f 工 lter::::: ll true" 的输出
    

注意:当标签后的 filter 为 true 时,将输出被包含页面的源代码。 JSP 页面使用 bean:include将 bean-header.jsp资源定义成标准bean,然后采用了三种 方式输出该bean。程序的运行效果如图3.15 所示。 下面的内容是通过二head盯输出 ~n:he础r标签塾~ 显示本次请求的http文件头 .2 Header Value f , 图 3.15 bean:in c1ude 的运行效果 154架 Struts I国 图 3.16 中第一个红色标记的内容来自 bean-inc1ude.jsp 页面:而第二个红色标记的内 容则来自 bean-header.jsp 页面。 3.8.4 使用 logic 标签库 logic 标签库是使用最频繁,相对复杂的标签库。 logic 标签库主要用于完成基本的 流程控制,比如循环及选择等。 logic 标签库主要有如下标签。 • empty: 如果给定的变量为空或者为空字符串,则就计算并输出标签体的内容。 • equal: 如果给定变量与特定的值相等,则会计算并输出该标签体的内容。 • forward: 将某个页面的控制权 forward 确定的 ActionForward 项。 • greaterEqual: 如果给定变量大于或等于特定的值,则会计算并输出标签体的内容。 • greaterThan: 如果给定变量大于特定的值,则会计算井输出标签体的内容。 • iterate: 通过遍历给定集合的元素,对标签体的内容进行循环。 • lessEqual: 如果给定变量小于或等于特定的值,则会计算并输出标签体的内容。 • lessThan: 如果给定变量小于特定的值,则会计算并输出标签体的内容。 • match: 如果特定字符串是给定消息合适的子字符串,则会计算并输出标签体的内容。 • messagesNotPresent: 如果请求中不包含特定的消息内容,将计算并输出标签体的内容。 • messagesPresent: 如果请求中包含特定的消息内容,则计算并输出标签体的内容。 • notEmpty: 如果给定的变量既不为空,也不是空字符串,则计算并输出标签体的内容。 • notEqual: 如果给定变量不等于特定的值,则会计算并输出标签体的内容。 • notMatch: 如果特定宇符串不是给定消息合适的子字符串,则会计算并输出标签 体的内容。 • notPresent: 如果特定的值没有出现在请求中,则计算并输出标签体的内容。 • present: 如果特定的值出现在请求中,则计算并输出标签体的内容。 • redirect: 重定向页面。 下面依次讲解这些标签。 1. empty 和 notEmpty标签 这两个标签都支持标签体。它们用于判断给定变量是否为空,并判断是否计算和输 出标签体,具体用法如下: 标签体:如果指定范围 scope 里 bean 为空,则计算并输出标签体的内容。此处的scope 有 page, request, session 和 application 等四个范围。如果没有指定范围,将在这四个范围中搜索。 标签体 d logic:empty >:如果 bean 的 propertyName 的属性为空,则计算井输出标签体的内容。 标签体:如果 bean 不为 空,则计算并输出标签体的内容。 155轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 <1ogic:notErnpty narne="bean" property="propertyNarne" scope="scope"> 标签体 :如果 bean 的 propertyN arne 的属性不为空,则计算并输出标签体的内容。 看下面的 JSP 页面代码: 〈革@ page contentType="text/html;charset=gb2312"毡〉 〈幸自 taglib uri="/WEB-INF/struts-logic.tld" prefix="logic" 革〉 〈毡@ tagl工 b uri="/WEB-工NF/struts-bean.tld" prefix="bean" 在〉 logic:empty标签测试
logic:empty标签测试

logic:empty 用来判断该bean 对象,及其属性是否为空

下面判断 bean 是否为空 不为空
为空


下面判断属性是否为空
为空

为空
程序的运行效果如图3.16 所示。 logic:empty标签测试 logic:e晤'Y 用来判断谊beon对象,及其属性量否为空 下面'句HilT beon是否为空 下面判断属性量否为空 为空 图 3.16 1ogic:empty标签的运行效果 156VC"lI! Struts ~ 2. match/notMatch 标签 这两个标签都支持标签体。它们可用于判断给定变量是否包含给定的字符串:并判 断是否计算和输出标签体。具体用法如下: 标签体 判断 scope 范围内名为 name 的 bean 的 propertyName 的属性,是否包含subStri吨, 如果包含该字符串,则计算并输出标签体。 标签体 判断请求参数 paramterName 的值中是否包含 subString,如果包含,则计算并输出 标签体。 标签体 判断 scope 范围内名为 name 的 bean 的 propertyName 的属性,是否包含subString, 如果不包含该字符串,则计算并输出标签体。 标签体 判断请求参数 paramterNarne 的值中是否包含 subString,如果不包含,则计算并输 出标签体。 看下面的 JSP 页面: 〈奄@ page contentType="text/html;charset=gb2312"屯〉 〈屯@ taglib uri="/WEB 工 NF/struts-logic.tld" prefix="logic" 毡〉 〈革@ taglib uri="/WEB-工 NF/struts-bean.tld" prefix="bean" 毛〉 logic:match标签测试
logic:match标签测试
bean 的 name 中包含子字符串lltom"

请求参数 ddd 中包含子字符串"tom" 157轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 程序的执行效果如图3.17 所示。 beon的name申包含子字符串"tom" 请求参数d吐吐申不包含子字符串"tom" 图 3 .17 logic:match 的运行效果 注意:红色标记部分的请求参数ddd 的值为 sfsd ,没有包含tom 子串,因此有图 3.17 的运行效果。 3. present 和 notPresent 标签 present 和 notPresent 标签用于判断某个bean,或判断请求参数是否存在。用法如下: 标签体: 判断 scope 中 beanName 的 bean 是否存在,如果存在,则计算并输出标签体。此处的scope 指的是 page, request, session, application 等范围,如果没有指定 scope ,则依次搜索这些范围。 标签体 : 判断 bean 的 propertyName 属性是否存在。如果存在,则计算并输出标 签体。 scope 属性作用与前面的 scope 属性完全相同。 标签体: 判断名为 cookieName 的 cookie 是否存在,如果存在,则计算并输出标签体。 标签体: 判断名为 headerName 的请求头是否存在,如果存在,则计算并输出标签体。 标签体 :判断请求是否包含名为 name 的请求参数,如果包含该参数,则计算并输出标签体。 标签体: 判断 scope 中 beanName 的 bean 是否存在,如果不存在,则计算并输出标签体。此处的scope 指的是 page, request, session, application 等范围,如果没有指定 scope ,则依次搜索这些 范围。 标签体 :判断 bean 的 propertyName 属性是否存在。如果不存在,则计算并 输出标签体。 scope 属性的作用与前面的相似。 标签体 :判断名为 158经典 MVC 框架 Struts 国 cookieName 的 cookie 是否存在,如果不存在,则计算并输出标签体。 标签体 :判断名为 headerName 的请求头是否存在,如果不存在,则计算并输出标签体。 标签体: 判断请求是否包含名 为 name 的请求参数,如果没有包含该参数,则计算并输出标签体。 注意:如果某个 bean 为 null ,或者 bean 的属性为 null 时, logic:notPresent 标签会输 出标签体;当 bean 为 null ,或属性值为 null 时, present 标签当其不存在。 看下面的 JSP 页面代码: 〈毡@ page contentType="text/html;charset=gb2312"毡〉 <%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic" 毡〉 logic:present标签测试
logic:present标签测试
session范围的 bean 实例 存在与否 存在 不存在
名为 FOOBAR 的 bea口 存在与否 存在 不存在
bean实例 存在与否 存在 159160 轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 不存在
名为 JSESS工 ONID 的 Cookie 存在与否 存在 不存在
User-Agent的请求头 存在与否 存在
FOOBAR的请求头 存在与否 存在 不存在
paraml 的请求参数 存在与否 存在 不存在
FOOBAR的请求参数 存在与否 不存在
bean 的 age 属性MVC 如truts ~ 是否存在 存在 不存在
bean 的口arne 属性 存在与否 存在 不存在
页面依次测试了上面的5 种情况,页面的运行效果如图3.18 所示。 logic:present标签测试 问二f二油田的民£案例院在呈苔[':F存在: 1名气壳丽丽耐b品二一…阵Z芮杏不存在 如由实例 院在写否带在十 ~珩画豆豆in丽的 洛夫面翩翩iC;'~ki~---lfftf~~j':F#tt I F二 er-.也町的请求实……除注与番静在一 i F面刷刷某一 一陆军f~电流黯! 再函搞清精敲一一 陆丽丽请求辜被一一 阎丽再孤注一一一一中建蓄存在潜在一 i ;be刷白属性…… i存在瑞!不存在1 图 3.18 1ogic:present 标签的运行效果 图中标记的部分地址栏里包含了 paraml 的请求参数, logic:present 标签也输出了标 签体的内容。 logic:messagesPresent 和 logic:messagesNotPresent 标签与 logic:present 的用法非常相 似,此处不再赘述。 4. forward 和 redirect 标签 这两个标签都用于转向,但转向的机制不一样。 logic:redirect 标签用于重定向,原有 HITP 请求中包含的属性及参数全部丢失,用 法如下: : 利用相对地址来控制转发。在本模块的URL 中增 加 page 属性对应的U阳生成的重定向地址。 161轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 : 利用所有全局 Forward 对象完成重定向, 其中 forwardName 必须在全局 Forward 中定义。 logic:forward 标签用于转发,但不会丢失原有的 HTTP 请求中的属性及参数,用法 如下。 : 利用所有全局 Forward 对象完成转发,其中 forwardName 必须在全局 Forward 中定义。 下面是分别利用两个标签转发的 JSP 页面: 利用 logic:redirect 控制重定向的页面 〈毡@ page language="java" 毡〉 〈毡@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bea丑"毛〉 〈毡@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic" 在〉 利用 logic:forward控制转发的页面代码 <%@ page language="java" 毛〉 〈靠自 taglib uri=" /WEB-INF/ struts-bean.tid" prefix="bean" 革〉 〈革@ taglib uri="/WEB-工NF/struts-logic.tld" prefix="logic" 毡〉 两次转发的实际资源都在other.jsp,该页面的源代码如下= <%@ page contentType="text/html;charset=gb2312" errorPage="error.jsp"毡〉 <%@ taglib uri="/WEB-工NF/struts-bean.tld" prefix="bean" 毛〉 〈革@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic" 在〉 被转向的页面 被转向的页面
请求参数 paraml存在 标签体 遍历名为 collectionNarne 的集合里每个元素被命名为item,可用于标签体访问,其 中 offset 是遍历的起始点, length 用于控制遍历的个数。 标签体 遍历名为 beanl 的集合将从 scope 的范围里搜索,如果没有指定范围,将从 page , request, session, application 中依次搜索。 offset 和 length 属性的效果与前面的用法相似。 标签体 遍历名为 beanl 的集合将从 scope 的范围里搜索。如果没有指定范围,将从page, request, session, application 中依次搜索。 offset 和 length 属性的效果与前面的用法相似。 此处还指定了集合 beanl 里每个元素的类型,从而可以直接访问每个元素的属性。 看下面的 JSP 页面代码: 〈革@ page language="java" contentType="text/html;charset=gb2312"毡〉 〈屯@ taglib ur工=" IWEB-INFIstruts-bean. tld" pref工 x="bean" 宅〉 〈苍白 taglib uri="/WEB-INF/struts-logic.tld" prefix="log工 c" 屯〉 〈革@ page import = "lee.Person"茸〉 〈草@ page import = "java.uti l. ArrayList" 奄〉 〈毡 II 初始化集合 ArrayList list1 =口ew ArrayList(); list1. add ( "张三 II) i list1. add ("李四") ; list1.add ("王五 II) ; list1.add( "蔡六") ; 毡〉 1.输出的集合的元素,也就是对集合元素做遍历,遍历时指定了集合的起始点,也指定了遍历长度
"工ndex工 d=" 工 ndex" offset="l" length="2"> 〈毡 =index毡>.&口bsp;<毡 =item毛 >

2. 将集合放入某个生存周期内,然后输出集合元素 〈屯pageContext.setAttribute("bean1", list1 , PageContext.PAGE_SCOPE); 革〉
&口bsp; 

〈毡 II 再次初始化集合,集合里每个元素都是一个Person 实例 ArrayList users = new ArrayList(); users. add (new Person (" tomcat" , 12) ); users. add (new Person ("mysql" , 24) ); users.add(new Person("oracle" , 36)); pageContext.setAttr工 bute("stuff", users); 革〉 4 .输入集合里元素的属性
<革=工ndex草 >.
页面的执行效果如图3.21 所示。 I.输出的第舍的元素,也就是对集合元素做遗历 1 李四 2. 王五 2 将集合放入某个生存周期内,然后输出集合元素 O. 张三 l 李四 2 王五 3 蔡六 图 3.21 1ogic:iterator 的运行效果 3.9 Struts 的数据校验 数据校验也称为输入校验,指导对用户的输入进行基本过滤,包括必填宇段,宇段 必须为数字及两次输入的密码必须相匹配等。这些是每个 MVC 框架都应该完成的任务, 164如tru恼「国 Struts 提供了基本的数据校验,如果结合 c∞ommons胳s-v刊alid由atωor.扣r , Struts 则拥有强大的校 验框架,包括进行客户端的 JavaScript 校验等。 Struts 的数据校验大致有如下几种方式: • ActionForm 的代码校验。 • Action 里的代码校验。 ·结合 commons-validator.jar 的校验。 3.9.1 ActionForm 的代码校验 ActionForm 的代码校验是最基本的校验方式。这种校验方式是重写 ActionForm 的 validate 方法,在该方法内对所有的宇段进行基本校验。如果出现不符合要求的输出,则 将出错提示封装在 Actio nError 对象里,最后将多个 Actio nError 组合成 Actio nErrors 对象, 该对象里封装了全部的出错提示。 下面是重写了 validate 方法的 ActionForm 的代码: public class LoginForm extends ActionForm implements Serializable { prvate String username = null; private String password = null; private String vercode = null; II username 的 getter 方法 public String getUsername() { return username; Ilusername 的 setter 方法 public void setUsername(String username) { this. username = username; Ilusername 的 setter 方法 public String getPassword() { return password; II password的 setter 方法 public void setPassword(String password) { this.password = password; II vercode 的 getter 方法 public String getVercode() { return vercode; II vercode 的 setter 方法 public void setVercode(String vercode) { this.vercode = vercode; II 重写的 validate方法,完成数据校验 public ActionErrors validate(ActionMapping mapping, HttPServletRequest request) IIActionErrors用于包装所有的出错提示 ActionErrors errors = new ActionErrors(); II 如果用户名为空 165轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 if (username == null II username. equals ("") ) Ilerror.username 对于资源文件的 key. 用户名是对应资源文件的第一个参数 errors.add ( "user口arne II , new ActionError ( "error.username" , "用户名")) ; II 如果密码为空 if (password == null II password.equals("")) errors. add ("password" , new Act 工 onError( II error.password" I II 密码")) ; II 如果验证码为空 if (vercode null II vercode.equals("")) errors.add ( "vercode" ,丑ew ActionError ( "error.vercode" , "验证码")) ; II 返回包装了所有出错提示的 ActionErrors 对象 return errors; 注意:重写的是validate(ActionMappingmapping, HttpServletRequest request)方法, 第二个参数的类型是 HttpServletRequest,而不是 ServletRequesto 上面的代码非常简单,为每个属性提供了对应的 setter 和 getter 方法。最后重写了 validate 方法,在 valid 方法里对每个需要校验的域完成校验。如果没有通过校验,则将 出错提示包装成 ActionError 对象,最后将多个 ActionError 对象组合成 ActionErrors 后返 回。 注意:使用 ActionForm 的输入校验时,应为对应的 action 元素增加 input 属性,该 属性指定当校验失败后的返回页面。 JSP 页面使用 标签输出出错提示,但出错提示并没有采用硬编码的方 式直接定义,而是使用资源文件的 key ,这样可实现出错提示的国际化。 下面是登录所用的 JSP 页面的代码: 〈草@ page contentType="text/html;charset=gb2312"毡〉 〈屯@ tagl工 b uri="/tags/struts-bean" prefix="bean" 革〉 〈草@ taglib uri="/tags/struts-html" prefix="html" 毡〉 注册 mask ~[a-zA-Z]+$
    <1- 依次遍历每个错误 >

经过上面的 6 个步骤,就可将Struts 与 common-validator校验框架成功结合。以后 需要增加校验某个表单域时,只需简单修改validation.xml文件即可,在其中增加需要校 验的 form-bean,以及表单域应满足的规则等。 2. common-validator支持的校验规则 common-validator支持的校验规则非常丰富,特别是mask 和 validwhen 两个规则, 极大地丰富了该校验框架的功能。 常用的校验规则有如下几种。 • required: 必填。 • va1idwhen: 必须满足某个有效条件。 • minlength: 输入必须大于最小长度。 • maxlength: 输入必须小于最大长度。 173轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 • mask: 输入匹配正确的表达式。 • byte: 输入只能是一个 byte 类型变量。 • short: 输入只能是一个 short 类型变量。 • integer: 输入只能是一个 integer 变量。 • long: 输入只能是一个 long 变量。 • float: 输入只能是一个 float 变量。 • double: 输入只能是一个 double 变量。 • date: 输入必须是一个日期。 • intRange: 输入的数字必须在整数范围内。 • floatRange: 输入的数字必须在单精度浮点数范围内。 • doubleRange: 输入的数字必须在双精度浮点数范围内。 • email: 输入必须是有效的 E-mail 地址。 • uri: 输入必须是有效的 uri 地址。 由此可见,当输入校验无法通过时,系统将出现出错提示。上面的校验规则大多有 默认的出错提示 key ,因此在国际化资源文件中,下面这些标准的出错提示也必不可少: #违反必填规则时候的出错信息 errors.required={O} 必须填写。 #违反最小长度规则时候的出错信息 errors.minlength={O} 的长度不能少于 {l} 个字符。 #违反最大长度规则时候的出错信息 errors.maxlength={ 们的长度不能大于{l}个字符。 #违反 validwhen 规则时候的出错信息 errors.invalid={O} 无效。 #违反 byte 规则时候的出错信息 errors.byte={O} 必须是个 byte 类型的值。 #违反 short 规则时候的出错信息 errors.short={O} 必须是个 short 类型的值。 #违反 integer 规则时候的出错信息 errors.integer={O} 必须是个 integer 类型的值。 #违反 long 规则时候的出错信息 errors.long={O} 必须是个 long 类型的值。 #违反 float 规则时候的出错信息 errors.float={O} 必须是个 float 类型的值。 #违反 double 规则时候的出错信息 errors.double={O} 必须是个 double 类型的值。 #违反 date 规则时候的出错信息 errors.date={O} 必须是有效的日期。 #违反所有的 range 规则时候的出错信息 errors.range={O} 必须在{l}和 {2} 之间。 #违反 email 规则时候的出错信息 errors.email={O} 不是一个有效的 E-mail 地址。 这些出错提示都是默认的,如果输入校验无法通过,则将自动出现这些提示。 3. 使用 DynaValidatorForm 的校验 即使不书写 ActionForm ,也可以利用 cornmon-validator 校验框架。如同在 3.7 节中 使用动态 Form 一样,不编写 ActionForm ,也可直接在 struts-config.xm1文件中配置 174V 框架 Struts ~ ActionForm 。 此时使用的 ActionForm 的实现类,必须既是动态 Form ,也是验证 Form , DynaValidatorForm 就是满足这两个条件的Form。 下面是使用 DynaValidatorForm 的 struts-config.xrnl 文件的源代码: <1-- struts 配置文件的文件头,包含DTD 等信息--> <1 下面依次定义了囚个 form 属性-> 程序的其他地方与第一个示例没有丝毫区别,此处不再赘述。 4. 弹出客户端 JavaScript提示 如需要弹出客户端 JavaScript 校验非常简单,无须修改其他配置文件,只需修改登 录使用的 JSP 页面的两个地方。 (1)为 form 元素增加 onsubmit="retumvalidateXx.xForm(this);"属性,其中的Xx.xForm 就是需要校验的form 名,与 struts-config.xrnl中配置的 form-bean 的 name 属性一致,也 与 validation.xrnl文件中需要校验的form 的 name 属性一致。 175轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 (2) 增加 ,其中 xxxForm 是需要校验的 form 名。 下面是修改的 login.jsp 页面的代码:
 
<' 全局异常定义--> <' 定义 lee.exceptionExceptionTestA对应的处理 > <' 加载验证的资源文件 一〉 异常的定义用于确定当某个异常出现时,系统自动转向某个异常显示页面。在上面 的定义中,有两个全局异常处理定义: 该定义表明,当Action 的 execute 方法中抛出 lee.exception.ExceptionTestA时, Struts .将重定向到/error.jsp 页面。该异常定义是全局异常定义,因此当每个 Action 的 execute 179轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 方法抛出该异常时,都将可以自动重定向。 而局部异常定义只对本 Action 有效。(作为 action 元素的子元素定义的 exception 是局部异常) 如果在用户名的文本框中输入任意字符串(只要不是usemame) ,而在密码框中输入 password,然后提交表单。根据业务逻辑方法,将抛出未处理的l出.ex臼ption.ExceptionTI白白, 此时, Struts 的异常框架将开始工作,于是出现如图3.27 所示的运行效果。 .业务逻辑异常:异常B 至整盒隘 图 3.27 异常的处理效果 图 3.27 页面中出现的异常提示信息则存在资源文件里,资源文件代码如下: #定义 Exception test.exceptionA=数据库访问异常:{a} test.exceptionB=业务逻辑异常:{a} test.unknown=未知异常 借助于 Struts 的异常处理框架,将异常处理部分交给Struts 框架完成,可以避免使 用烦琐的 try...catch 块。 3.11 几种常用的 Action 除了基本的 Action 之外, Struts 还提供了几个其他类型的 Action ,这些 Action 大大 丰富了 Struts 的功能。下面介绍如下儿个常用的 Action 。 • DispatchAction: 能同时完成多个 Action 功能的 Action 。 • ForwardActon: 该类用来整合 Struts 和其他业务逻辑组件,通常只对请求作有效 性检查。 • IncludeAction: 用于引入其他的资源和页面。 • LookupDispatchAction: DispatchAction 的子类,根据按钮的 key ,控制转发给 action 的方法。 • MappingDispatchAction: DispatchAction 的子类,一个 action 可映射出多个 Action 地址。 • SwitchAction: 用于从一个模块转换至另一个模块,如果应用分成多个模块时, 180…~ 就可以使用 SwitchAction 完成模块之间的切换。 下面对常用的 Action 进行介绍。 3.11.1 DispatchAction 及其子类 DispatchAction 是仅次于 Action ,使用最频繁的 Action。用于同一个表单中有两个提 交按钮时,但提交需要的逻辑处理完全不同的情况。如图3.28 所示为登录页面。 图 3.28 两个提交按钮的表单页 该页面包含了两个提交按钮,但提交按钮需要执行的逻辑却不一样。最容易想到的 解决方法是,为每个按钮增加JavaScipt 脚本,提交两个按钮时候分别提交给不同的Action 处理。这是最容易想到,也最麻烦的方式。 Struts 提供了 DispatchAction,可支持多个逻辑处理。对于上面的示例,表单需要两 个逻辑处理:增加和修改。下面是示例所使用的Action 类的源代码: public class LoginAction extends DispatchAction { II 第一个处理逻辑 public ActionForward add(ActionMapp工 ng mapping, ActionForm form , HttpServletRequest request , HttpServletResponse response) throws Exception System.out.pr工丑tln( "增加") ; request. setAttribute ("method" , "增加") ; return mapping.findForward("success"); II 第二个处理逻辑 public ActionForward modify(Act工 onMapping mapping, ActionForm form, HttpServletRequest request , HttpServletResponse response) throws Exception System.out.println("修改 n) ; request. setAttribute ("method回"修改"); return mapping.findForward("success"}; 上面的 Action 非常简单,其两个逻辑处理也非常简单。该Action 并没有重写 execute 方法,而是书写了两个自定义的方法:add 和 modi句。这两个方法除了方法名与execute 181轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 方法不同之外,其他的参数列表及异常的处理完全相同。这两个方法正是 execute 方法 的替代,用于完成业务逻辑的处理。 问题的关键是: Struts 如何区别不同表单提交与方法之间的对应关系?因为当使用 DispatchAction 要求表单提交时,会额外多传递一个参数,该参数用于区分到底调用 Action 中的哪个方法。 这个参数名在 struts-config.xml 文件中指定。注意下面 action 的配置代码: 在该 action 的配置中,增加了parameter属性,该属性用于指定参数名,即Struts 将 根据该参数的值调用对应的方法。为了让请求增加method 的参数,对上面的JSP 页面代 码进行简单修改,可在JSP 页面中增加一个隐藏域,使该隐藏域的名字为method。下面 是 JSP 页面的表单代码:
从上面的代码中可以看到,页面中增加了method 的隐藏域,该隐藏域的默认值为 add,当单击页面中的【修改】按钮时,该隐藏域的值将变成modify,单击【添加】按 钮时,该隐藏域的值变成add。这个隐藏域就是额外传递的参数值,用于告诉Dispatch 调用哪个方法来处理请求。 如果 method 参数的值为 add,将调用 add 方法;如果 method 参数的值为 modify, 则调用 modify 方法。因此在单击不同按钮时, DispatchAction将可自动调用对应的方法 来完成处理。 1. 使用 MappingDispatchAction MappingDispatchAction可将同一个 Action 的不同方法映射成多个Action URI ,这种 182~Struts ~ Action 的写法与 DispatchAction 非常相似,同样不需要重写 execute 方法,而是将书写多 个自定义的方法。这些方法除了方法名与 execute 方法不同外,其他的参数列表及异常 处理完全一样。 下面是本示例所使用的 Action 的源代码: public class LoginAct工 on extends MappingDispatchAction II 第一个处理逻辑 public ActionForward add(ActionMapping mapping, ActionForm form, HttpServletRequest request , HttpServletResponse respo口 se) throws Exception System.out.println("增加") ; request.setAttribute( 咱ethod" , "增加") ; return mapping.f工口dForward("success"); } II 第二个处理逻辑 public ActionForward modify(ActionMapping mapping , ActionForm form, HttpServletRequest request , HttpServletResponse response) throws Exception System.out.println("修改") ; request.setAttribute( 叮nethod" "修改") ; return mapping.findForward("success"); 该 Action 与前面的 DispatchAction 没有太大的区别,仅仅改变它的父类: MappingDispatchAction,但变化在于该Action 的配置,看下面关于该Action 的配置代码: <1- 配置第一个 Action. 实现类是 lee.LoginAction , parameter 为 add--> 在这种情况下,两个action 使用的是同一个Action 处理类,只是调用的方法不同,同 样也可达到上面的效果。当然也需要为页面中的两个按钮增加相应的JavaScript 脚本,当 单击不同按钮时,表单可提交到不同的action,下面是 JSP 页面三个按钮的源代码: 其中,前面两个提交按钮都增加了onClick 方法,即单击该按钮时,会改变表单的提 交地址。 183轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 注意:使用 MappingDispatchAction 并没有带来太大的优势,系统完全可以书写两个 Action ,分别定义两个不同的 action 映射,而其他部分没有区别。 2. f吏用 LookupDispatchAction LookupDispatchAction也是 DispatchAction 的一种,但它的处理更加简单。该Action 也可包含多个处理方法,它可让处理方法与按钮直接关联,无须使用任何的JavaScript 脚本。 使用 LookupDispatchAction时,提交按钮必须使用 Struts 的 html 标签,下面是该示 例按钮部分的源代码: 代码中两个提交按钮分别增加了property 属性,该属性的值为method。而在 action 的配置中,也使用parameter 作为参数,看下面的action 配置代码: 这段配置代码表明:该action 也根据 method 参数来区分请求分别调用哪个方法,此 时无须使用 method 的隐藏域,而是将按钮的property 设为 method。通过这种方式可以 避免书写 JavaScript脚本。 因此可通过重写 getKeyMethodMap方法完成按钮与 Action 中方法的关联,下面是 该 Action 的源代码: public class LoginAction extends LookupDispatchAction { II 用于关联按钮和方法 protected Map getKeyMethodMap() Map map = new HashMap(); II 如果按钮标题的key 为 button.add. 则提交该按钮时对应add 方法 map .put ("button. add" , "add"); II 如果按钮标题的key 为 button.modify. 则提交该按钮时对应modify 方法 map.put ("button.modify" , "mod工 fy") ; return map; } II 第一个处理逻辑 public ActionForward add(ActionMapping mapping, ActionForm form, HttpServletRequest request , HttpServletResponse response) throws Exception 184框架 Struts ~ System.out.println(" 增加 II) i request.setAttribute("method" , "增加") ; return mapp工 ng.f工 ndForward(" success") ; } 第二个处理逻辑 public ActionForward modify(ActionMapping mapping, ActionForm form, HttpServletRequest request , HttpServletResponse response) throws Exception System.out.println("修改 II) ; request.setAttribute( 咱ethod II II 修改 II) ; return mapping.findForward("success"); LookupDispatchAction必须重写 getKeyMethodMap方法,该方法返回一个Map 对象, 并在该对象内保存了按钮标题与方法之间的对应。 3.11.2 使用 ForwardAction 如果需要从一个页面或资源转换到另一个资源时,直接使用页面或资源路径的超级 链接定位并不是好的做法,这使得控制器没有机会处理相关的请求事直。 使用 ForwardAction可以完成请求的转发,当控制器调用ForwardAction的 performO 方法时,它会使用属性parameter 所设定的路径进行 forward 的动作。下面是一个设定 ForwardAction的例子: 该 action 仅仅完成转发,并没有执行其他的额外动作。 页面控制转发的代码如下: 转入 当单击转入超级链接时,将可以转向ForwardAction中 parameter 指向的资源。 3.11.3 使用 IncludeAction IncludeAction 的用法与 ForwardAction的用法比较相似,区别在于ForwardAction将 跳转到 action 定义的资源,而IncludeAction用于引入该 action 对应的资源。 下面是 IncludeAction定义的源代码: 185轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 该 action 用于经 welcome.jsp 作为资源导入。 页面中负责加载该 action 所导入资源的代码如下:
上面的代码将会把welcome action 定义的资源导入该页面。 3.11.4 使用 SwitchAction SwitchAction 主要用于模块之间的切换。当一个应用之中存在多个模块时,使用 SwitchAction在不同模块之间的action 之间切换还是相当方便的。 在下面的 web.xml 中,力日载了 Struts 的两个配置文件,其中一个作为系统的一个模 块加载,该 web.xml 的配置代码如下: action org.apache.struts.action.ActionServlet <'一 指定 Struts 的第一个配置文件--> config /WEB-INF/struts-conf工 g.xml config/wawa /WEB-INF/struts-configl.xml 2 该应用包括了一个wawa 的模块,并在 struts-config1.xml 文件中配置一个action,该 action 的配置代码如下: 该 action 的定义非常简单,仅完成页面的转向。如果现在需要从应用的页面请求该 action,可以使用如下SwitchAction。 定义 SwitchAction也相当简单,只需要定义path、 type 属性即可。下面是SwitchAction 的定义代码: 在使用 SwitchAction 时,必须在请求中带两个参数:第一个是prefix,用来指定模 块宅称:另一个是page,用来指定相模块中的资源路径。下面是页面中超级链接对wawa 模块的 welcome action 请求,页面的超级链接代码如下: 186阳tru除国 转入另一个模块 上面的超级链接地址中,/wawa是模块名,而page对应wawa模块下的welcome的 actiono 3.12 Struts 的常见扩展方法 Struts 的强大吸引力还来自于它的可扩展性,其扩展性通常有如下三种方式。 ·实现 PlugIn: 如果需要在应用启动或关闭时完成某些操作,可以创建自己的 PlugIn 类。 ·继承 RequestProcessor: 如果想在请求被处理中的某个时刻做一些业务逻辑时,可 以考虑实现自己的 RequestProcessor 类。 ·继承 ActionServlet: 如果需要在每次开始处理请求之前,或者处理请求结束之后 完成某些操作,可以实现自己的 ActionServlet 来完成扩展。 下面分别从三个方面来介绍 Struts 的扩展。 3.12.1 实现 PlogIn 接口 Struts 已经演示了 PlugIn 的扩展方法:与 common- validation 的整合。后面还将介绍 Spring 与 Struts 的整合,也利用了 PlugIn 的扩展。 在下面的应用中,系统使用 Hibernate 作为持久层,在启动时创建 SessionFactory 实 例,并将该 SessionFactory 存入 application ,在应用关闭时销毁 SessionFactory 。只需如 下两步即可完成此功能。 (1)实现自己的 PlugIn 类。 实现 PlugIn 接口必须实现如下两个方法。 • void destroyO 。 • void init(ActionServlet se凹 let , ModuleConfig config) 。 应用启动时调用 init 方法,而应用关闭时则调用 destroy 方法。 下面是 SessionFactoryLoaderPlugIn 的实现类: public class SessionFactoryLoaderPlug工 n implements PlugIn { IIHibernate 的配置文件 private String configFile; II应用关闭时,销毁资源 public void destroy() System.out.println("系统销毁 SessionFactory"); } II 应用启动时,完成 SessionFactory 的初始化 public void init(ActionServlet actionServlet , ModuleConfig config) throws ServletException System.out.println("系统以" + getConfigFile() + "为配置文件初始化 SessionFactory") ; 187轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 II 获取 Plugln 配置文件的方法 public String getConfigFile() return configFile; II 负责加载 Plugln 配置属性的方法 public void setConfigFile(String configFile) this.configF工 le = conf 工 gFile; 在上面的 PlugIn 中,并没有真正初始化 SessionFactory ,仅在控制台打印出字符串 来标识创建动作。另外,还提供了 configFile 属性的 setter 和 getter 方法,这两个方法负 责访问 plugin 元素的 configFile 属性。 ( 2 )将 SessionFactory LoaderPlugIn 配置在 struts-config.xml 文件中。方法与 ValidatorPlugIn 的配置并没有区别,下面是配置 SessionFactoryLoaderPlugIn 的代码: 在配置 SessionFactoryLoaderPlugIn 时,配置了 configFile 属性,该属性用于确定 Hibernate 配置文件的文件名。 图 3.29 显示了应用启动时的提示。 L一 一山土耳| 图 3.29 使用 PlugIn后的启动提示 图 3.29 的红色标记部分表明PlugIn 随应用启动时完成了SessionFactory 的加载。 注意:本示例并未真正加载SessionFactory,因为这不是本示例的重点。 3.12.2 继承 RequestProcessor RequestProcessor 是 Struts 的核心类,而 Struts 的核心控制器是 ActionServlet 。但 ActionServlet 并未完成真正的处理,只是调用 RequestProcessor , RequestProcessor 才是 Struts 的核心类。 扩展 RequestProcessor 的实例在 Spring 中有个示范, Spring 提供的 Delegating RequestProcessor 是一个很好的示例。下面示例对 RequestProcessor 进行简单的扩展。 RequestProcessor 包含了如下常见的方法。 • ActionForm processActionForm: RequestProcessor 填充 ActionForm 时执行该方法。 188…~ • Action processActionCreate: RequestProcessor 调用 Action 时调用该方法。 • boolean processPreprocess: 预处理用户请求时执行该方法。 • boolean processValidate: 处理输入校验时调用该方法。 扩展 RequestProcessor 只需两步即可。 (1)实现自己的 RequestProcessor。自己的 RequestProcessor 通常都是 RequestProcessor 的子类,下面是 MyRequestProcessor 的源代码: publ 工 c class MyRequestProcessor extends RequestProcessor { Date tl; II 填充 ActionForm时调用的方法 protected ActionForm processActionForm(HttpServletRequest request , HttpServletResponse response , ActionMapp 工口 g mapping) tl = new Date(); System.out.println(tl + "===========ActionServlet 开始填充 ActionForm II 调用父类方法 return super.processActionForm(request, respo口 se , mapping); II 创建 Action 时调用的方法 protected Action processActionCreate(HttpServletRequest request , HttpServletResponse response , ActionMapping mapping) throws java.io. 工 OException tl = new Date(); System.out.println(tl + "===========Actio口 Servlet 开始实例化 Action II 调用父类方法 return super.processActionCreate(request, response, mapping); II 处理用户请求时执行的方法 protected boolean processPreprocess(HttpServletRequest request , HttpServletResponse response) $ystem.out.pr工ntln("一一一一一-一开始显示请求相关信息--------------"); System.out.println("请求获取的 URI = " + request.getRequestUR 工(») ; System. out.println ("请求访问的虚拟路= " + request.getContextPath(); Cookie cookies[] = request.getCookies(); if (cookies != null) System. out .println("Cookies: ") ; for (int i = 0; i < cookies.length; i++) System.out.println(cookies[i] .getName() + " = " + cookies[i]. getValue(); } Enumeration headerNames = request.getHeaderNames(); System.out.println("请求头: "); while (headerNames.hasMoreElements(») { String headerName = (String)headerNames.nextElement(); Enumeration headerValues = request.getHeaders(headerName); while (headerValues.hasMoreElements(») { String headerValue = (String)headerValues.nextElement(); 189轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 System.out.pr 工 nt ln ("" + headerName + " = " + headerValue) ; System.out.println(" 本地化语言环境= " + request.getLocale()); System.out.println("请求的方法= " + request.getMethod () ); System.out.println(" 请求协议= " + request.getProtocol()); System.out.println("请求的远程地址= " + request.getRemoteAddr()); System.out.println("请求的远程主机= " + request.getRemoteHost()); String address = request.getRemoteAddr(); System.out.println("请求的用户名= " + request.getRemoteUser()); 乌rstem.out.println ("请求的Session Id= 川 request.getRequestedSessionId()); System.out.println(" 请求配置= " + request.getScheme()); System.out.println("服务器名= " + request.getServerName()); System.out.prιin川1比tln丑1 ( System.out.println(" 请求的资源地址= " + request.getServletPath()); System.out.pr工 ntln( "是否加密= " + request.isSecure()); System.out.println("一 一---一-结束显示请求相关信息 一一 一一") ; int position = address.lastIndexOf("."); int last = Integer.parse 工 nt(address.substring(position + 1)); 工 f (address. substring(O , 9) .e~吐als("192.l68.6") && (last < 10 && last >= 1)) return true; } if (address.equals("127.0.0.l")) return true; } return false; 本示例继承了 RequestProcessor类,并重写该类的processPreprocess 方法。如果该方 法返回 false. Struts 将不再继续调用业务逻辑控制器action。本示例对用户的请求作出分 析,然后对局域网内的IP 地址进行过滤,如果IP 地址不在 192.168.1.1 和 192.168.1. 10 之间,将返回 false 。 (2)在 struts-config.xml 文件中配置 MyRequestProcessor。用户重写了 Request­ Processor,但 Struts 并不知道,必须在struts-config且nl 中配置才可以。 下面是配置 MyRequestProcessor 的代码: 该属性的配置应该放在action-mappings元素之后。 注意:重写 RequestProccessor的方法时,别忘了使用super 来调用父类的动作。如 果没有调用该方法,意味着开发者必须完成Struts 框架所完成的动作。这是不应该的, 因为程序员只是在框架中加入额外的处理,并不是要替代Struts。 3.12.3 继承 ActionServlet 如果需要在开始处理请求,或者处理结束之后加入自己的处理时,可对 ActionServlet 进行扩展。例如解决中文的编码问题。 1908t附国 ActionServlet 接收处理请求参数时,并不是按 GBK 的解码方式处理请求,因此容易 形成乱码。为了解决该问题,可以强制指定 ActionServlet 使用 GBK 的解码方式。 实现该功能只需要两个步骤。 (1)实现自己的 ActionServlet ,它是 ActionServlet 的子类: publ 工 c class MyActionServlet extends ActionServlet protected void process(HttpServletRequest request , HttpServletResponse response) throws java.io.工 OException, javax.servlet.ServletException II 设置解码方式 request.setCharacterEncoding("GBK"); II 调用父类的方法 super.process(request , response); 在本示例中,重写了 process 方法,该方法是 ActionServlet 处理用户请求的方法。 当然,该方法会调用RequestProcossor处理,首先在重写该方法的第一行设计解码方式, 然后调用父类的方法。 注意:重写 ActionServlet 的方法时,别忘了调用父类的同名方法。否则Struts 将完 全停止工作。 (2) 在 web.xml 文件中配置MyActionServlet。由于系统改变了ActionServlet,因此 必须使用 MyActionServlet来拦截所有的用户请求。 下面是 MyActionServlet的配置代码: action lee.MyActionServlet l 经过上面的配置, Struts 可以正确处理请求中的中文参数。 本章小结 本章全面介绍了经典的MVC 框架: Struts ,包括 Struts 的基本使用和基本配置,对 Struts 的流程进行深入剖析,井通过示例讲解了Struts 的程序国际化,动态表单。 另外重点讲解了 Struts 的验证框架和异常处理框架,使读者真正掌握Struts 的使用。 最后结合示例详细讲解了Struts 的常用标签,并对Struts 的常用 Action 也进行了详 细的讲解,最后给出了Struts 常用的扩展方法。 191本章要点 溢 ORr\11 的基本知识 被忖 ibernate 入门知识 滋 Hibernate 基本映射 3强 集合属性,引用属性,复合主键映射 到 关联关系映射 到 HOl 查询、条件查询 ¥ SOL 查询、过滤等 3曲 Hibernate 的事件机制 Hibernate 是目前最流行的开源对象关系映射 (ORM) 框架。 Hibernate 采用 低侵入式的设计,完全采用普通的 Java 对象 (POJO ),而不必继承 Hibernate 的 某个超类或实现Hibernate 的某个接口。因为Hibernate 是面向对象的程序设计语 言和关系数据库之间的桥梁,所以 Hibernate 允许程序开发者采用面向对象的方式 来操作关系数据库。阳 Hi… 4.1 DRM 简介 ORM 的全称是 Objec tlR elation Mapping ,对象/关系映射。 ORM 也可理解是一种规 范,具体的 ORM 框架可作为应用程序和数据库的桥梁。目前 ORM 的产品非常多,比 如 Apache 组织下的 OJB; Oracle 组织下的 Top Li nk 、 JDO 等。 4.1.1 什么是 ORM ORM 并不是一种具体的产品,而是一类框架的总称。它概述了这类框架的基本特 征:完成面向对象的程序设计语言与关系数据库的映射。基于 ORM 框架完成映射后, 既可利用面向对象程序设计语言的简单易用性,又可利用关系数据库的技术优势。 ORM 框架是面向对象程序设计语言与关系数据库发展不同步时的中间解决方案。 笔者认为,随着面向对象数据库的发展,其理论逐步完善,最终会取代关系数据库。只 是这个过程不可一蹦而就, ORM 框架在此期间内会蓬勃发展,但随着面向对象数据库 的出现, ORM 工具也会退出历史舞台 o 4.1.2 为什么需要 ORM 在上一节已经基本回答了这个问题,面向对象的程序设计语言代表了目前程序设计 语言的主流和趋势,其具备非常多的优势,比如: ·面向对象的建模与操作。 ·多态及继承。 ·槟弃难以理解的过程。 ·简单易用,易理解性。 但数据库的发展并未与程序设计语言同步,而且关系数据库系统的某些优势也是面 向对象的语言目前无法解决的。比如: ·大量数据操作查找与排序。 ·集合数据连接操作与映射。 .数据库访问的并发与事务。 .数据库的约束与隔离。 面对这种面向对象语言与关系数据库系统并存的局面,采用 ORM 就变成一种必然。 4.1.3 流行的 ORM 框架介绍 目前 ORM 框架的产品非常多,除了各大著名公司的产品外,甚至其他一些小团队 也都有推出自己的 ORM 框架。目前流行的 ORM 框架有如下产品。 193轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 ·大名鼎鼎的 Hibernate Hibernate 出自 Gavin King 的手笔,是目前最流行的开源 ORM 框架,其灵巧的设计, 优秀的性能,以及丰富的文档都是其迅速风靡全球的重要因素。 ·传统的 Entity EJB Entity EJB 实质上也是一种 ORM 技术,是一种备受争议的组件技术,很多人说它非 常优秀,也有人说它一钱不值。事实上, EJB 为J2EE 的蓬勃发展赢得了极高的声誉。 就笔者的实际开发经验而言, EJB 作为一种重量级,高花费的 ORM 技术,具有不可比 拟的优势。但由于其必须运行在 EJB 容器内,而且学习曲线陡峭,开发周期及成本相对 较高,因而限制了 EJB 的广泛使用。 • iBATIS Apache 软件基金组织的子项目,与其称它是一种 ORM 框架,不如称它是一种" Sql Mapping ,,框架。相对 Hibernate 的完全对象化封装, iB ATIS 更加灵活,但开发过程中开 发人员需要完成的代码量更大,而且需要直接编写SQL 语句。 • Oracle 的 TopLink 作为一个遵循 OTN 协议的商业产品, TopLink 在开发过程中可以自由下载和使用, 但作为商业产品使用后,则需要收取费用。可能正是这一点,导致了 TopLink 的市场占 有率低下。 ·OJB OJB 是 Apache 软件基金组织的子项目。开源的ORM 框架,但由于开发文档不多, 而且 OJB 的规范并不稳定,因此并未在开发者中赢得广泛的支持。 4.2 Hibernate 概述 Hibernate 是目前最流行的 ORM 框架,其采用非常优雅的方式将 SQL 操作完全包装 成对象化的操作。其作者 Gavin Ki ng 在持久层设计上极富经验,采用非常少的代码实现 了整个框架,同时完全开放源代码,即使偶尔遇到无法理解的情况,也可以参照源代码 来理解其在持久层上灵巧而智能的设计。 目前 Hibernate 在国内的开发人员相当多, Hibernate 的文档也非常丰富,这些都为 学习 Hiberante 铺平了道路,因而 Hibernate 的学习相对简单一些。下面通过对比来了解 Hibernate 和传统 JDBC 操作数据库持久层之间的差异。 4.2.1 Hibernate 的起源 当前的软件开发语言已经全面转向面向对象,而数据库系统仍停留在关系数据库阶 段。面对复杂的企业环境,同时使用面向对象语言和关系数据库是相当麻烦的,不但中 间的过渡难以理解,而且其开发周期也相当长。 Hibernate 是一个面向 Java 环境的对象/关系数据库映射工具。对象/关系数据库映射 194久化 E ( Object/Relational Mapping) 表示一种技术,用来把对象模型表示的对象映射到基于SQL 的关系模型数据结构中去。 Hibernate 的目标是:释放开发者通常的数据持久化相关的编程任务的95% 。对于以 数据为中心的程序而言,往往在数据库中使用存储过程来实现商业逻辑,Hibernate 可能 不是最好的解决方案。但对于那些基于Java 的中间件应用中,设计采用面向对象的业务 模型和商业逻辑时, Hibernate 是最有用的。不管怎样, Hibernate 能消除那些针对特定数 据库厂商的 SQL 代码,并且把结果集由表格式的形式转换成值对象的形式。 Hibernate 不仅管理 Java 类到数据库表的映射(包括Java 数据类型到 SQL 数据类型 的映射),还提供数据查询和获取数据的方法,可以大幅度地减少在开发时人工使用SQL 和 JDBC 处理数据的时间。 4.2.2 Hibernate 与其他 ORM 框架的对比 Hibernate 能在众多的 aRM 框架中脱颖而出,因为 Hibernate 与其他 aRM 框架对比 具有如下优势。 ·开源和免费的 License ,方便需要时研究源代码、改写源代码并进行功能定制。 ·轻量级封装,避免引入过多复杂的问题,调试容易,减轻程序员负担。 ·具有可扩展性, API 开放。功能不够用时,可以自己编码进行扩展。 ·开发者活跃,产品有稳定的发展保障。 4.3 Hibernate 的安装和使用 Hibernate 的学习难度不大,简单易用。正是这种易用性,征服了大量的开发者。下 面简要介绍 Hibernate 的安装和使用。 4.3.1 Hibernate 下载和安装 Hibernate 目前的最新版本是 3. 1. 2 ,本章所用的代码也是基于该版本测试通过的。安 装和使用 Hibernate 请按如下步骤进行: ·首先登录 http://www.hibernate.org 网站,下载 Hibernate 的二进制包 (windows 平 台下载 zip 包, Linux 平台下载 tar 包)。 ·解压缩下载的压缩包,在 hibernate-3.1 路径下有个 hibernate3.jar 的压缩文件,该文 件是 Hibernate 的核心类库文件。该路径下还有 lib 路径,该路径包含 Hibernate 编 译和运行的第三方类库。关于这些类库的使用请参看该路径下的 readme.txt 文件。 ·将必需的 Hibernate 类库添加到 CLASSPATH 里,或者使用 ANT 工具。总之,编译 和运行时可以找到这些类即可。在 Web 应用中,则应该将这些类库复制到 WEB-的Fllib 下。 195轻量级 J2EE 企业应用实战一-St阳ts+Spring+Hibernate 整合开发 4.3.2 传统 JDBC 的数据库操作 先看这样一个需求:向数据库里增加一条新闻,该新闻有新闻 Id 、新闻标题及新闻 内容三个属性。在传统的 JOBe 数据库访问里,实现此功能并不难。 我们可采用如下方法来实现(本程序采用 MySql 数据库): import java.sql.*; public class NewsDao { 1** * @param News 需要保存的新闻实例 *1 public void saveNews(News news) Connection conn = null; PreparedStatement pstmt = null; int newsld = news.getld(); String title = news.getTitle(); String content = news.getContent(); try II 注册驱动 Class. forName ("com.mysql. jdbc.Driver") ; 川 hibernate: 想连接的数据库 user: 连接数据库的用户名 pass: 连接数据库的密码 *1 String url="jdbc:mysql:lllocalhost/hibernate?user=root&password=pass"; /I 获取连接 conn= DriverManager.getconnection(url); II 创建预编译的 Statement pstmt=conn.prepareStatement("insert into news_table values(?,?,?)"); II 下面语句为预编译Statement传入参数 pstmt.setlnt(l, newsld); pstmt.setString(2 , title); pstmt.setString(3 , content); II 执行更新 pstmt.executeUpdate(); } catch (ClassNotFoundException cnf) cnf.printStackTrace(); } catch (SQLException se) { se.printStackTrace(); finally try { II 关闭预编译的 Statement if (pstmt != null)pstmt.close(); II 关闭连接 if (conn != null) co口口 .close(); } catch (SQLException se2) 196使用 Hibernate 完成持久化 EE se2.printStackTrace() ; 由此可见,这种操作方式丝毫没有面向对象的优雅和易用,而是一种纯粹的过程式 操作。在这种简单的数据库访问里,我们没有过多地感觉到这种方式的复杂与缺陷,相 比下面采用 Hibernate 的操作,但我们还是可以体会到 Hiberate 的灵巧。 4.3.3 Hibernate 的数据库操作 在使用 Hibernate 之前,首先了解一个概念: PO (Persistent Object) 持久化对象。 持久化对象的作用是完成持久化操作。简单地说,通过该对象可对数据执行增、删和改 的操作,以面向对象的方式操作数据库。 Hibernate 里的 PO 是非常简单的,前面已经说过Hibernate 是低侵入式的设计,完全 采用普通 Java 对象来作为持久化对象使用,看下面的POJO (普通 Java 对象)类: public class News { int id; String title; String content; publ工 c void setId(int id) { this.id = id; public int getId() { return (this.id); 此处笔者并未列出该类的title 和 content 属性的 setter 与 getter 方法,读者可自行增 加。这个类与常规的JavaBean 没有任何区别,是个非常标准的简单JavaBeano 这个普通的 JavaBean 目前还不具备持久化操作的能力,为了使其具备持久化操作的 能力, Hibernate 应采用 XML 映射文件,该映射文件也是非常简单。下面提供该XML 文件的全部代码: <'一上面四行对所有的hibernate映射文件都相同一〉 197轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 对这个文件作简单地解释:从1 到 4 行,是该 XML 文件的文件头部分,定义该文 件的 xml 版本和 DTD 声明,对于所有 Hibernate 3.x 的映射文件全部相同。因为 hibernate-mapping 元素是所有 hibernate 映射文件的根元素,这个根元素对所有的映射文 件都是相同的。 另外, hibernate-mapping 元素下有子元素 class ,每个 class 子元素映射一个 PO ,更 准确地说,应该是持久化类。可以看到: PO=POJO+ 映射文件。 现在就可以通过这个持久化类完成数据库的访问:插入一条新闻。 在插入一条新闻之前,还必须完成 Hibernate 管理数据库的配置一一连接数据库所需 的用户名、密码及数据库名等等基本信息。连接所需的基本信息配置可通过.properties 的属性文件,或 hibernate.cfg.xml 的方式来配置。本例采用 hibernate.cfg.xml 的配置方式, 下面是这个配置文件的详细代码: root pass 5 org.hibernate.dialect.MySQLDialect<1 property> create 该配置文件非常简单,前面四行都是XML 的基本定义和DTD 声明,所有的Hibernate 配置文件前面四行都完全相同。Hibernate 配置文件的根元素是hibernate-configuration, 根元素里有子元素session-factory,该元素依次有很多property 元素, property 元素依次 198拍完成机回 定义连接数据库的驱动、 URL 、用户名、密码及数据库连接池的大小等。另外,还定义 一个名为 dialect 的属性,该属性定义Hibernate 连接的数据库类型是 MySQL, Hibernate 会针对该数据库的特性在访问时进行优化。最后一行mapping 定义了持久化类的映射文 件,如果有多个持久化映射文件,可在此处罗列多个mapping 元素。 下面是完成插入新闻的代码: publ 工 c class NewsDaoH工 bernate { Configuration configurat工 on; SessionFactory sessionFactory; Session session; public void saveNews(News news) { II 实例化 Configuration conf 工 guration=new Configuration() .configure(); II 实例化 SessionFactory sessionFactory = configuration.buildSessionFactory(); II 实例化 Session sess J. on = sess 工 onFactory.ope口 Sess 工 on() ; II 开始事务 Transaction tx = session.beginTransaction(); II 增加新闻 session.save(news); II 提交事务 tx.commit(); II 关闭 Session session.close(); 此时的代码结构非常清晰,保存新闻仅仅只需要这个语句:session.save(news),而 且是完全对象化的操作方式,可以说是非常简单明了。 在代码显示执行session.save(News)之前,首先要获取Session对象。PO 只有在 Session 的管理下才可完成数据库访问,按PO 与 Session 的关系, PO 可有如下三个状态: ·瞬态 ·持久化 ·脱管 对 PO 的操作必须在Session 管理下才能与数据库同步。Session 由 SessionFactory厂 商提供, SessionFactory 是数据库编译后的内存镜像,通常一个应用对应一个 SessionFactory 对象,该对象由 Configuration 对象生成。 Configuration 对象用来加载 Hibernate 配置文件。 最后使用如下方法来完成对新闻的增加: public static void main(String[] args) { News n = new News(); n.setTitle("新闻标题") i n.setContent(" 新闻内容 II) ; NewsDaoHibernate ndh = new NewsDaoHibernate(); 口dh. saveNews (n) ; 199轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 这里仅仅提供一个大体的框架,读者可以将其补充完整,以完成新闻的添加。 4.4 Hibernate 的基本映射 在上面的例子里,可看到一个简单的 Hibernate 映射文件,每个 Hibernate 映射文件 的基本结构都是相同的。 4.4.1 映射文件结构 映射文件的根元素为 hibernate- mapping 元素,这个元素下可以拥有多个 class 子元 素,每个 class 子元素对应一个持久化类的映射。如下是一个映射文件的基本结构,在 hibernate-mapping 元素下可以有多个 class 子元素: 接下来看 class 元素,每个 class 元素对应一个持久化类。首先必须采用name 元素 来指定该持久化类映射的类名,此处的类名应该是全限定的类名。如果不使用全限定的 类名,则必须在hibernate-mapping元素里指定 package 元素,该元素指定持久化类所在 的包名。 如果需要采用继承映射,则class 元素下还会增加 subclass 元素、 joined-subclass或 union-subclass元素,这些元素分别用于定义子类。 另外,持久化类都需要有一个标识属性,该标识属性用来标识该久化类的实例,因 此标识属性通常被映射成数据表主键。标识属性通过id 元素来指定, id 元素的 name 属 性的值就是持久化类标识属性名。 标识属性通常应该指定主键生成策略,Hibernate 推荐数据表采用逻辑主键,而不采 用有物理含义的实体主键。逻辑主键没有实际意义,仅仅用来标识一行记录,通常由 Hibernate 负责生成。负责生成主键的工具称为主键生成器,应尽量为每个持久化类都设 置主键生成器。 4.4.2 主键生成器 主键生成器是负责生成数据表记录的主键,通常有如下几种常见的主键生成器。 increment: 对 long , short 或 int 的数据列生成自动增长主键。 identity: 对如 SQL server, MySQL 等支持自动增长列的数据库,如果数据列的类型 是 long , short 或 int ,可使用主键生成器生成自动增长主键。 200…化~ sequence: 对如 Oracle , DB2 等支持 Sequence 的数据库,如果数据列的类型是 long , short 或 int ,可使用该主键生成器生成自动增长主键。 uuid: 对字符串列的数据采用 128-位 uuid 算法生成唯一的字符串主键。 property 元素定义持久化类的普通属性,该持久化类有多少个普通属性,就需要有 多少个 property 元素, class 元素的结构如下所示: 在上面的映射文件中, element 元素用于映射集合属性里的每个元素。该元素有个 not-null 属性,该属性默认为false ,即该列默认可以为空。 对比List 和 Set 两种集合属性,可以发现List 集合里的元素有顺序,而Set 集合里 的元素没有顺序。当集合属性在另外的表中存储时,List 集合属性可以用关联持久化类 的外键和集合次序列作为联合主键。但Set 集合没有次序列,则以关联持久化类的外键 和元素列作为联合主键,前提是元素列不能为空。 注意:映射 Set 集合属性时,如果element 元素包括 not-null= "true" 属性,则集合 204棚 Hi… 属性表以关联持久化类的外键和元素列作为联合主键,否则该表没有主键。但Li st 集合 属性的表总是以外键列和元素次序列作为联合主键。 观察图 4.1 和图 4.2 的表结构,对比两个图的红色标识处,图 4.2 是当 Set 集合属性 的 element 不能为空时,该表才有联合主键。否则该表没有主键。 图 4.1 保存Li st 集合属性的表结构 图 4.2 保存 Set 集合属性的表结构 3. bag 元素映射 bag 元素既可以为Li st 集合属性映射,也可以为 Collection 集合属性映射。不管是哪 种集合属性,使用 bag 元素都将被映射成无序集合,而集合属性对应的表没有主键。 bag 元素只需要 key 元素来映射关联的外键列,而使用 element 元素来映射集合属性 的每个元素。 下面是持久化类使用 bag 元素的映射文件代码: < J -- Hibernate映射文件的文件头,包含DTD 等信息-→ <'一指定主键生成器策略…〉 <1- 映射 age 属性…〉 <1-- 映射集合属性--> 205轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 4. Map 集合属性 Map 集合属性不仅需要映射属性 value ,还需要映射属性 key 。映射 Map 集合属性 时,同样需要指定外键列,同时还必须指定 Map 的 key 列。显然,系统将以外键列和 key 列作为联合主键。 与所有集合属性类似的是,集合属性的声明只能使用接口,看下面的 POJO 类: public class Person implements Serializable II 标识属性 private int 工 d; Ilname 属性 prlvate String name; Ilage 属性 pr工 vate int age; IIMap 集合属性,成绩 private Map scores = new HashMap(); IIPerson类的默认构造器 Person() {} II 标识属性 id 的 setter 方法 public void set 工 d(int id) this.id = id; II 标识属性 id 的 setter 方法 public int getId() return 工 d; II 属性 name 的 setter 方法 public String getName() return name; /I属性 name 的 setter 方法 public void setName(String name) this.name = name; II 属性 age 的 setter 方法 public vo工 d setAge(int age) this.age = age; II 属性 age 的 setter 方法 public int getAge() return age; IIMap 集合属性 id 的 getter 方法 public Map getScores() 206loerna怡完成持久化~ return scores; //Map 集合属性 scores 的 setter 方法 public void setScores(Map scores) this.scores = scores; Map 集合属性应使用map 元素映射,该map 元素需要 key 和 map-key 两个子元素。 其中 key 子元素用于映射外键列,而map-key 子元素则用于映射Map 集合的 Key。该持 久化类的映射文件如下: <'一映射Map Key--> 程序运行结束后,保存集合属性scores 的表,并以 personid 和 xueke 作为联合主键。 注意: map-key 和 element 元素都必须确定type 属性。 5. 集合性能的对比 当系统从数据库中初始化某个持久化类时,集合属性是否随持久化类一起初始化 呢?如果集合属性里包含十万,甚至百万的记录,在初始化持久化类之时,要完成所有 集合属性的加载,势必将导致性能急剧下降。系统很有可能只需要使用持久化类的某个 属性中的部分记录,这样,没有必要一次加载所有的集合属性。 对于集合属性,通常推荐使用延迟加载策略。所谓延迟加载就是当系统需要使用集 207轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 合属性时才从数据库装载关联的数据。 Hibernate 对集合属性默认采用延迟加载,在某些特殊的情况下为 set , list, map 等元 素设置 lazy= "false" 属性来取消延迟加载。 根据前面的讲解,可将集合分成如下两类。 ·有序集合:集合里的元素可以根据 key 或 index 访问。 ·无序集合:集合里的元素只能遍历。 有序集合都拥有一个由 组成的联合主键,在这种情况下,集合属性 的更新是非常高效的一一主键已经被有效地索引。因此当 Hibernate 试图更新或删除某行 时,可以迅速找到该行数据。 而对无序集合而言,如果集合中元素是组合元素或者大量文本及二进制宇段,数据 库可能无法有效地对复杂的主键进行索引。即使可以建立索引,性能也非常差。例如 Set 的主键由 和其他元素宇段构成,或者根本没有主键。 显然,有序集合的属性在增加、删除及修改中拥有较好的性能表现。 在设计良好的 Hibernate Domain Object 中,集合属性通常都会增加 inverse="true"的 属性,此时集合端不再控制关联关系。因此,无须考虑其集合的更新性能。 4.4.4 映射引用属性 引用属性的意思是:持久化类的属性既不是基本数据类型,也不是 String 字符串, 而是某个引用变量,该引用属性的类型可以是自定义类。看下面 POJO 的源代码: public class Person implements Serial工 zable { II 标坝、属性 private int id; II 普通属性 age prlvate 工 nt agE!; II 引用属性 name private Name name; II 无参数的构造器 Person () {} II 标识属性 id 的 setter 方法 private void setld(int id) this.id=id; II 标识属性 id 的 getter 方法 public int getld() return id; Ilage 属性的 setter 方法 public void setAge(int age) this.age=age; I!age 属性的 getter 方法 public int getAge() 208久化~ return age; II 引用属性 name 的 setter 方法 public void setName(Name name) this.name = name; II 引用属性 name 的 getter 方法 public Name getName() return name; 此时 Person 的 name 属性既不是基本数据类型,也不是Stri吨,而是一个自定义类: Name。由于数据库的列无法存储Name 对象,因此无法直接使用property 映射 name 属性。 为了映射引用属性, Hibernate 提供了 component元素。每个 component元素映射一 个引用属性,引用属性必须指定该属性的类型。因此componet 元素要求具有class 属性, 该属性用于确定引用属性的类型。 注意:由于 Hibernate 使用 component 映射引用属性,因此很多地方将引用属性翻译 成组件属性。笔者认为,此处的组件与一般意义上的纽件毫不相干,故不采用此种翻译。 一个自定义类通常还包括其他属性,因此还应该为component 元素增加 property 的 子元素来映射引用属性的子属性。下面是持久化类的映射文件: <'一 Hibernate 映射文件的文件头,包含DTD 信息一〉 映射文件中的component 还有 unique= "true" 属性,这并不是必需的,而是与具体 209轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 的业务逻辑相关联的。 引用属性还有如下两种特殊的情况: ·集合属性的元素既不是基本数据类型,也不是 String 字符串,而是引用类型。 ·持久化类的主键是引用类型。 下面对这两种情况具体分析。 1. 集合引用属性映射 集合除了可以存放 String 字符串外,还可以存放引用类型。事实上,在更多情况下, 集合里存放的都是引用类型,看下面 POJO 的源代码: public class Person implements Ser工 alizable { 1/ 标识属性工 d private int id; II 普通属性 name private Str工口g name; II 普通属性 age pr工vate 工 nt age; II 存放引用类型的集合属性 private List schools = new ArrayList(); Person () {} II 标识属性 id 的 setter 和 getter 方法 public void setld(工 nt id) this.id = id; public int get工 d() { return id; Ilname 属性的 getter 和 setter 方法 public String getName() return name; } public void setName(String name) { this.name = name; Ilage 属性的 setter 和 getter 方法 public void setAge(int age) this.age = age; } public int getAge() { return age; IIList 集合属性的 setter 和 getter 方法 public List getSchools() return schools; public vo工 d setSchools(List schools) { 210… r叩肌~ this.schools = schools; 表面上看,该持久化类与前面的带集合属性的POJO 并没有太大的区别。区别仅在 主程序部分,前面Person 实例的 schools 属性里存放系列的字符串,而现在的schools 属 性里存放系列的School 实例。下面是School 的源代码: public class School implements Ser工 alizable IISchool 类的两个属性: name 和 address private String name; private String address; public School(){} II 带两个参数的构造器 public School(String sl , Str 工 ng s2) this.name = sl; th工 s.address = s2; } Ilname 属性的 setter 方法 public void setName(String name) { this.name = name; Ilname 属性的 setter 方法 public void setAddress(String address) { this.address = address; II口arne 属性的 setter 方法 public String getName() { return (this.name); Ilname 属性的 setter 方法 public String getAddress() { return (this.address); 对于有集合属性的POJO,都需要使用 set, list, bag 等集合元素来映射集合属性。如 果集合里的元素是普通宇符串,则使用 element 映射集合元素即可。如果集合元素是自 定义类,则须使用 composite-element 子元素来映射集合元素。 由于 composite-element 元素映射一个引用类型,因此需要增加 class 元素来确定集 合元素的类型,该元素还支持以 property 的子元素来定义引用类型的子属性。 下面是 Person 类的持久化映射文件: <'一 Hibernate 映射文件的根元素-> <'一 id 属性映射…〉 211轻量级 J2EE 企业应用实战一一Struts+Spring+Hibernate 整合开我 〈卜- List 有序集合,需要索引列一〉 2. 引用类型主键的映射 在数据库中建模时,尽量不要使用复杂的物理主键,应考虑为数据库增加一列,作 为逻辑主键。表面上看,增加逻辑主键增加了数据冗余,但如果从外键关联的角度看, 使用逻辑主键的主从表关联中,从表只需增加一个外键列。如果使用多列作为联合主键, 则需要在从表中增加多个外键列。如果有多个从表需要增加外键列,则数据冗余更大。 使用物理主键还会增加数据库维护的复杂度,因为主从表之间的约束关系隐讳难懂, 难于维护。 如果数据库采用简单的逻辑主键,则不会出现引用类型主键。但在一些特殊的情况 下,也会出现引用类型主键,看下面的持久化类: public class Person { II 用作持久化类 Person 的标识属性 private Name name; II 普通属性 age private int age; II 默认构造器 Person () {} II 口arne 属性的 setter 方法 public void setName(Name name) this.name = name; Ilname 属性的 getter 方法 public Name getName() return name; Ilage 属性的 setter 方法 212阳… public void setAge(int age) { this.age = age; Ilage 属性的 getter 方法 public int getAge() return age; Person 的标识属性不再是基本数据类型,也不是String 字符串,而是Name 类型, 该类型是用户自定义的类型。 如果持久化类需要使用引用类型作为表示属性时,则该类应该满足如下两个条件: .实现 java.io.Seria1izable 接口。 ·重写 equalsO和 hashCodeO方法,这两个方法的返回值都应该根据数据表中联合主 键的列来判断。 下面是 Name 的源代码: II 标识属性类,实现Serializable接口 public class Name implements Serializable private String firstName; private String lastName; public Name(){} public Name(String sl , String s2) { this.firstName = sl; this.lastName = s2; IlfirstName 的 setter 和 getter 方法 public void setFirstName(String fisrtName) this.firstName = firstName; } public String getFirstName() { return this.firstName; } I !l astName 的 setter 和 getter 方法 public void setLastName(String lastName) this.lastName = lastName; } public String getLastName() { return th工 s.lastName; II 重写 hashCode方法,该方法根据firstName和 last Name 的值计算得到 public int hashCode() return firstName.hashCode() + lastName.hashCode(); II 重写 equals 方法,同样也根据firstName和 lastName 两个属性来判断 public boolean equals(Object 0) 213轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 if (0 工 nstanceof Name) { Name p = (Name)o; 工 f (p.getF工 rstName() .equals(firstName) && p.getLastName() .equals(lastName)) { return true; else return false; else return false; 对于 Person 持久化类,其Name 是标识属性的类, Name 实例可以唯一标识Person 实例。根据业务需要,能唯一标识Person 实例的应该是firstName 和 lastName 两个属性。 因此 hashCode 和 equals 方法都应根据firstName 和 lastName 两个属性来判断。 引用类型主键的映射时,应使用composite-id元素,该元素需要class 属性来确定主 键的引用类型,并使用key-property元素来确定引用类型包含的基本属性。 下面是 POJO Person 持久化映射文件: 218a饱完机 E <' 用来映射关联的PO column 是 Address 在该表中的外键列名 > <' 下面持久化类映射Address --> <'一映射标识属性 > 2. 有连接表的 N -1 如果需要使用有连接表的 N -1,则需要使用 join 元素(通常, join 元素用于强制使 用连接表 )0 join 元素的 table 属性用于确定连接表的表名:使用 key 子元素来确定连接 表外键,并为 join 元素增加 many-to-one 子元素,该子元素用于映射关联属性。 该 many-to-one 子元素的定义与不使用连接表的 many-to-one 几乎相同:同样使用 name 属性确定关联属性的属性名, type 指定关联类的类型, column 属性指定列名。当 然,除了 name 属性必须指定外,其他都是可选的。 下面是使用有连接表的 N -1 映射文件代码: <'一映射 Person 持久化类-> 220…完成持久化 E 2. 有连接衰的单向 1 -1 虽然这种情况很少见,但Hibernate 同样允许采用连接表关联1 -1 。有连接表的 1 -1 同样只需要将连接表的N 一 l 的 many-to-one元素增加 unique= "true" 属性即可。 下面是有连接表的 1 一 1 映射文件代码: <'一映射持久化类Address--> <'一映射 Person 持久化类--> <1-- 指定主键生成器策略--> 3. 基于主键的单向 1 -1 1 一1 的关联可以基于主键关联,但基于主键关联的持久化类不能拥有自己的主键生 成器策略,它的主键由关联类负责生成。另外,增加one-to-one 元素来映射关联属性, 必须为 one-to-one 元素增加 constrained="true"属 a性,表明该类的主键由关联类生成。 下面是基于主键关联的单向1 -1 的映射文件代码: 221轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 address 4.5.3 单向 l-N 的关系映射 与单向 1 - N 关联的 POlO 不同,需要使用集合属性。因为 1 的一端需要访问 N 的 一端,而 N 的一端将以集合的形式表现。 通常,不推荐使用单向的 l-N 关联。对于 l-N 的父子关联,使用 1 的一端控制关 系的性能比使用 N 的一端控制关系的性能低。性能低的原因是使用 1 的一端控制关联关 系时,会额外多出 update 语句,并且一旦使用了 1 的一端来控制关系,因为插入数据时 无法同时插入外键列,因此外键列无法增加非空约束。 N 的一端是 Address 端,在单向关联中无须访问关联类,其源代码如下: public class Address implements Serial工 zable { II 标识属性 private int addressid; private String addressdetail; public Address(){ } public Address(String addressdetail) { this.addressdetail = addressdetail; public void setAddressid(int addressid) { this.addressid = addressid; public void setAddressdetail(String addressdetail) { this.addressdetail = addressdetail; } public int getAddressid() { return (this.addressid); public String getAddressdetail() { return (this.addressdetail); 222完 久化 E 下面是 Person 端,需要增加集合属性,并且集合属性里每个元素都是持久化类,下 面是 Person 的源代码: public class Person implements Serializable private int personid; private String name; pr~vate 工 nt age; II 集合属性,用于关联持久化类 private Set addresses = new HashSet(); II 标识属性的 setter 方法 public void setPersonid(int personid) { this.personid = perso口 id; Ilname 属性的 setter 方法 public void setName(String name) { this.name = name; Ilage 属性的 setter 方法 public void setAge(int age) { this.age = age; II标识属性 id 的 getter 方法 public int getPersonid() { return (this.person工 d) ; Ilname 属性的 getter 方法 public String getName() { return (this.name); Ilage 属性的 getter 方法 public int getAge() { return (this.age); II 集合属性的 getter 方法 public Set getAddresses(){ return addresses; II 集合属性的 getter 方法 public void setAddresses(Set addresses){ this.addresses = addresses; 对于 l-N 的单向关联而言,需要在1 的一端增加对应的集合映射元素,例如set, list, bag 等。与映射集合属性类似,必须为 set, list, bag 等集合元素增加 key 子元素,用以映 射关联外键列。 与集合属性不同的是:建立 l-N 关联时,集合中的元素使用 one-to-many 来映射, 而不是使用 elment 子元素。详细映射看下面部分。 1.无连接表的单向 1-N 使用对应的集合元素映射集合属性时,集合属性必须增加 key 子元素,该子元素用 223轻量级 J2EE 企业应用实战一-St阳 ts+Spring+Hibernate 整合开发 以确定关联的外键列,使用 one-to-many 映射关联属性。 下面是无连接表的单向 l-N 映射文件的代码: <'一 Hibernate 映射文件的根元素一〉 <'一 每个 class 元素映射一个持久化类一〉 <1 映射集合属性,关联到持久化类 > < set name="addresses"> 2. 有连接衰的单向1-N 有连接表的单向 l-N 非常类似于N-N 的映射,但集合元素中不使用one-to-many 元素来映射关联属性,而是使用many-to-many元素。但为了保证是1 的一端,因此增加 unique="true"属'性。下面是有连接表的单向l-N 关联的映射文件: 224久化国 <' 确定主键生成器策略一〉 4.5.4 单向 N-N 的关系映射 单向 N-N 的 POlO 与 l-N 的代码完全相同,其控制关系的一端访问的是集合,被 关联的持久化实例以集合的形式存在。 N-N 的关联只能使用连接表,与有连接表 l-N 的关联非常相似,只要去掉 many-to-many 元素的 unique= "true" 属性即可。 下面是单向 N-N 关联的映射文件: <'一 Hibernate映射文件的根元素一〉 <' 确定主键生成器策略 > 注意:在上面的配直文件中,两个持久化类的配直文件都需要指定外键列的列名, 此时不可以省略。因为不使用连接表的1- N 关联的外键,所以外键只保存在N 一端的 表中。如果两边指定的外键列名不同,将导致关联映射出错。如果不指定外键列的列名, 该列名由系统自动生成,而系统很难保证自动生成的两个列名相同。 2. 有连接表的双向1-N 关联 有连接表的 1- N 双向关联类似于N-N 关联。 1 的一端应使用集合元素映射,然后在 集合元素里增加 many-to-many 的子元素,该子元素映射到关联类。另外,应该为 many-to-many元素增加 unique= "tnIe" 属性。 N 的一端则使用join 元素来强制增加连接表。 228完 久化国 注意:两边确定连接表的 table 属性值应该相同,而且 table 属性不能省略。否则关 联映射将出错。 join 元素不仅使用 key 子元素来确定外键列,还需要增加 many-to-one 元素映射到关 联属性。 下面是双向 I-N 关联的配置文件: <'一映射标识属性--> 注意:在双向N-N 关联的两边都需指定连接表的表名及外键列的列名。两个集合 元素 set 的 table 元素的值必须指定,而且必须相同。set 元素的两个子元素: key 和 many-to-many都必须指定 column 属性,其中, key 和 many-to-many 分别指定本持久化 类和关联类在连接表中的外键列名,因此两边的key 与 many-to-many的 column 属性交 叉相同。也就是说,一边的set 元素的 key 的 colomn 值为 a , many-to-many 的 column 为 b; 则另一边的 set 元素的问的 column 值为 b , many-to-many的 column 值为 a o 4.5.7 双向 1-1 关联 前面介绍过,单向的1 一 1 关联有三种映射策略:基于主键、基于外键和使用连接表。 双向的 1 - 1 关联同样有这三种映射策略。 双向的 1 - 1 关联需要修改 POlO 类,让两边都增加对关联类的访问。看下面 Person 和 Address 的源代码。 在 Person 类中,包含对 Address 属性的 setter 和 getter 方法: public class Person { II 基本属性 private int personid; private String name; private int age; II 关联类属性 private Address address; II标识属性 personid的 setter 方法 232使用 Hibernate 完成持久化 public void setPersonid(int personid) { this.personid = personid; II口arne 属性的 setter 方法 public void setName(String name) { this.name = name; Ilage 属性的 setter 方法 public void setAge(int age) { this.age = age; II 标识属性 personid的 getter 方法 public int getPersonid() { return (this.personid); Ilname 属性的 getter 方法 public String getName() { return (this.name); Ilage 属性的 getter 方法 public int getAge() { return (this.age); II 关联属性 address 的 getter 方法 public Address getAddress(){ return address; II 关联属性 address 的 setter 方法 public void setAddress(Address address){ this.address = address; 在 Address 类中,同样也包括对关联类Person 的 setter 和 getter 方法: public class Address implements Serializable { II 标识属性 addressid private int addressid; private String addressdetail; pr工vate Person person; II无参数的构造器 public Address(){ } II 有参数的构造器 public Address(String addressdetail){ this.addressdetail = addressdetail; II标识属性 addressid的 setter 方法 public void setAddressid(int addressid) { this.addressid = addressid; Iladdressdestail属性的 setter 方法 public void setAddressdetail(String addressdetail) { this.addressdetail = addressdetail; 法方·'哑{由ZC-­tUSHd 出 g. 引 U 内配 d um 町 idhsdt臼出 M retdg(&t 咀 性 EU皿 属 c 四 忡吵 -lI 标 M /'u }/P 国 233轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 Iladdressdestail 属性的 getter 方法 public String getAddressdeta工 1 () ( return (this.addressdetail); II 关联类 Perso口的 getter 方法 public Person getPerson(){ return person; II 关联类 Person 的 setter 方法 public void setPerson(Person person){ this.person = perso口; 下面分别介绍这三种映射策略。 1.基于外键的双向 1 - 1 关联 对于基于外键的 1 - 1 关联,其外键可以存放在任意一边,在需要存放外键的一端, 增加 many-to-one 元素。正如前面介绍的,为 many-to-one 元素增加 unique="true" 属性来 表示为 1 一 1 的关联:井用 name 属性来指定关联属性的属性名。 若另一端需要使用 one-to-one 元素,则该元素需要使用 name 属性指定关联的属性 名。为了让系统懂得不再为本表增加一列,因此使用外键关联,用 property -ref 属性来引 用关联类的自身属性。 看下面的映射文件: <'一映射标识属性--> <' 指定主键生成器策略 > 234化 E <'一 指定主键生成器策略一〉 <'一映射标识属性一〉 <1-- 指定根据主键生成的关联属性--> person 235轻量级 J2EE 企业应用实战→一-Struts+Spring+Hibernate 整合开发 3. 有连接表的双向 1 -1 关联 采用连接表的双向 1 - 1 关联是相当罕见的情形,其映射相当复杂,数据模型也非 常烦琐。通常不推荐使用这种策略。 双向 1 - 1 关联的两边都需要使用 join 元素来显式指定连接表, join 元素的 table 属 性用于确定连接表的表名,因此两边的 join 元素的 table 属性值应该相同。除了在两边 都增加 key 元素映射连接表中的外键列外,还需增加 many-to-one 元素映射关联属性, 并为两个 many-to-one 元素增加 unique="true" 属'性表明为 1-1 关联。 下面是完整的映射文件: <'… Hibernate 映射文件的根元素一〉 <'一映射持久化类Person--> ? 该命名的 HQL 查询可以直接通过Session 访问,调用命名查询的示例代码如下: private void findByNamedQuery() throws Exception II 获得 Hibernate Session对象 Session sess = HibernateUtil.currentSession(); II 开始事务 Transaction tx = sess.beginTransaction(); System.out.println("执行命名查询"); II 调用命名查询 List pl = sess.getNamedQuery( 叮nyNamedQuery" ) II 为参数赋值 .set 工 nteger(O , 20) II 返回全部结果 .list (); 1I:ll!i历结果集 for (工 terator pit = pl.iterator() ; pit.hasNext(); ) Person p = ( Person )pit.next(); 246完 久化 E System.out.println(p.getName() ; /I 提交事务 tx.comm 工 t (); HibernateUtil.closeSess工 on() ; 4.6.2 条件查询 条件查询是更具面向对象特色的数据查询方式,通过如下三个类完成。 • Criteria: 代表一次查询。 • Criterion: 代表一个查询条件。 • Restrictions: 产生查询条件的工具类。 执行条件查询的步骤如下: (1)获得 Hibernate 的 Session 对象。 (2) 以 Session 对象创建 Criteria 对象。 (3)增加 Criterion 查询条件。 (4) 执行 Criteria 的 list 等方法返回结果集。 看下面的条件查询示例: private void test() { II 获取 Hibernate Session 对象 Session session = HibernateUtil.currentSession(); II 开始事务 Transaction tx = session.beginTransaction(); II 创建 Cr工 teria 和添加查询条件同步完成 II 最后调用 list 方法,返回查询到的结果集。 List 1 = session.createCr 工 teria(Student.class) II 此处增加的限制条件必须是 Student 己经存在的属性 .add( Restrictions.gt("studentNumber" , new Long(20050231) )) /I 如果要增加对 Student 关联类的属性限制,则必须重新创建 II 如果此关联属性是集合,则只要集合里任意一个对象的属性满足下面条件即可 .createCriteria("enrolments") .add( Restrictions.gt("semester" , new Short("2") )) .list (); Iterator it = l.iterator(); II 遍历查询到的记录 while (it.hasNext() Student s = (Student)it.next(); System.out.println(s.getName(); Set enrolments = s.getEnrolments(); Iterator iter = enrolments.iterator(); while(iter.hasNext(») { Enrolment e = (Enrolment) 工 ter.next(); System.out.println(e.getCourse() .getName(»); } tx.commit(); ibernateUt工 1.closeSess工 on() ; 247轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 在条件查询中, Criteria 接口代表一次查询,该查询本身不具备任何的数据筛选功能: Session 调用 createCriteria(Class clazz}方法对某个持久化类创建条件查询实例。 Criteria 包含如下两个方法。 • Criteria setFirstResult(int firstResult}: 设置查询返回的第一行记录。 • Criteria setMaxResults(int maxResults}: 设置查询返回的记录数。 这两个方法与 Query 的用法相似,都用于完成查询分页。 此外, Criteria 还包含如下常用方法。 • Criteria add(Criterion criterion}: 增加查询条件。 • Criteria addOrder(Order order}: 增加排序规则。 • List list(}: 返回结果集。 Criterion 接口代表一个查询条件,该查询条件由 Restrictions 负责产生,而 Restrictions 是专门用于产生查询条件的工具类,它的方法大部分都是静态方法,常有的方法有如下 几种。 • static Criterion allEq(Map propertyNameValues}: 判断指定属性(由 Map 参数的 key 指定)和指定值(由 Map 参数的 value 指定)是否完全相等。 • static Criterion between(String propertyName, 0时 ect 10, Object hi}: 判断属性值是否 在某个值范围之内。 • static Criterion ilike(String propertyName, Object value}: 判断属性值是否匹配某个 字符串。 • static Criterion ilike(String propertyName, String value, MatchMode matchMode}: 判 断属性值是否匹配某个字符串,并确定匹配模式。 • static Criterion in(String propertyName, Collection values}: 判断属性值是否在某个 集合内。 • static Criterion in(String propertyName, Object[] values}: 判断属性值是否是数组元 素的其中之一。 • static Criterion isEmpty(String propertyName}: 判断属性值是否为空。 • static Criterion isNotEmpty(String propertyName}: 判断属性值是否不为空。 • static Criterion isNotNull(String propertyName}: 判断属性值是否为空。 • static Criterion isNull(String propertyName}: 判断属性值是否不为空。 • static Criterion not(Criterion expression}: 对 Criterion 求否。 • static Criterion sizeEq(String propertyName, int size}: 判断某个属性的元素个数是 否与 size 相等。 • static Criterion sqlRestriction(String sql}: 直接使用 SQL 语句作为筛选条件。 • static Criterion sqlRestriction(String sql, Object[] values, Type[] types}: 直接使用带 参数占位符的 SQL 语句作为条件,并指定多个参数值。 • static Criterion sqlRestriction(String sql, 0时 ect value, Type type}: 直接使用带参数 占位符的 SQL 语句作为条件,并指定参数值。 248使用 H… Order 实例代表一个排序标准,井有如下构造器。 Order(String propertyName, boolean ascending): 根据 propertyName 排序,如果后一 个参数为 true ,则采用升序排序,否则采用降序排序。 如果需要使用关联类的属性来增加查询条件,则应该对属性再次使用createCriteria 方法。看如下示例: session.createCriteria(Person.class) . add (Restrictions .like ("name" "dd屯") ) .createCriteria("addresses") .add(Restrictions.like("addressdetail" , "上海革") ) .list (); 上面的代码表示建立Person 类的条件查询,第一个查询条件是直接过滤Person 的属 性,即选出 name 属性以 dd 开始的 Person 实例,第二个查询条件则过滤Person 的关联 实例的属性,其中addresses 是 Person 类的关联持久化类Address,而 addressdetail 则是 Address 类的属性。值得注意的是,查询并不是查询Address 持久化类,而是查询Person 持久化类。 注意:使用关联类的条件查询,依然是查询原有持久化类的实例,而不是查询被关 联类的实例O 4.6.3 SQL 查询 Hibernate 还支持使用 SQL 查询,使用 SQL 查询可以利用某些数据库的特性,或者 用于将原有的 JOBC 应用迁移到 Hibernate 应用上。因此,使用命名的 SQL 查询不仅可 以将 SQL 语句放在配置文件中配置,还可以用于调用存储过程,从而提高程序的解祸。 如果是一个新的应用,通常不要使用 SQL 查询。 SQL 查询是通过 SQLQuery 接口来表示的,由于 SQLQuery 接口是 Query 接口的子 接口,因此完全可以调用 Query 接口的方法,例如: • setFirstResultO: 设置返回结果集的起始点。 • setMaxResultsO: 设置查询获取的最大记录数。 • listO: 返回查询到的结果集。 但 SQLQuery 比 Query 多了两个重载的方法。 • addEntity: 将查询到的记录与特定的实体关联。 • addScalar: 将查询的记录关联成标量值。 执行 SQL 查询的步骤如下。 (})获取 Hibernate Session 对象。 (2) 编写 SQL 语句。 (3)以 SQL 语句作为参数,调用 Session 的 createSQLQuery 方法创建查询对象。 (4) 如果 SQL 语句包含参数,则调用 Query 的 setXxx 方法为参数赋值。 (5) 调用 SQLQuery 对象的 addEntity 或 addScal缸方法,将选出的结果与实体或标 量值关联。 249轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 (6) 调用 Query 的 list 方法返回查询的结果集。 看下面的 SQL 查询示例: private void test() { II 获取 Hibernate Session 对象 Session session = HibernateUtil.currentSession(); II 开始事务 Transaction tx = session.beginTra口 sact 工 on() ; II 编写 SQL 语句 String sqlStr工口9 = "select {s.*} from student s where s.name like' 马军'门 II 以 SQL 语句创建 SQLQuery对象, List 1 = session.createSQLQuery(sqlString) II 将查询到的记录与特定实体关联起来 .addEntity("s" , Student.class) II 返回全部的记录集 .list () ; II~历结果集 工 terator it = l.iterator(); while (工 t.hasNext()) { II 因为将查询的结果与 Student 类关联,因此返回是 Student 的集合 Student s = (Student)it.next(); Set enrolments = s.getEnrolments(); 工 terator iter = enrolments.iterator(); while(iter.hasNext()) Enrolment e = (Enrolment)iter.next(); System.out.println("========================================= System.out.println(e.getCourse() .getName()); System.out.pr工 ntln("========================================= II 提交事务 tx.commit(); II 关闭 Session HibernateUtil.closeSession(); 上面的示例显示了将查询记录关联成一个实体。事实上,SQL 查询也支持将查询结 果转换成标量值,转换成标量值时使用addScalar 方法。例如: Double max = (Double) session.createSQLQuery("select max(cat.weight) as maxWeight from cats cat") . addScalar ("maxWeight" , Hibernate. DOUBLE) ; .uniqueResult(); 在使用 SQL 查询时,如果需要将查询到的结果转换成特定实体,就要求为选出的宇 段另起别名。这别名不是随意命名的,而是以"实例名.属性名"的格式命名。例如: II 依次将多个选出的字段命名别名,命名别名时都以ss 作为前缀, ss 是关联实体的别名 String sqlStr = "select stu.studentld as {ss.studentNumber} + "stu.name as {ss.name} from + "student as stu where stu.name like' 杨海华 III i List 1 = session.createSQLQuery(sqlStr) 250使用 Hibernate 完成持久化 川国 II 将查询出的 ss 实例,关联到 Student 类 .addEnt 工 ty("ss" , Student.class) .list () ; 在第一个示例中,以 {s.* }代表该表的全部字段,且关联实例的别名也被指定为 s 。 注意:如果不使用 {so 叶的形式,就可让实体别名和表别名王不相同,关联实体的类 型时,被关联的类必须有对应的 setter 方法。 1.命名 Sal 查询 可以将 SQL 语句不放在程序中,而是放在配置文件中,这种方式以松藕合的方式配 置 SQLi吾句,可以提高程序解祸。 在 Hibernate 的映射文件中定义查询名,并确定查询所用的 SQL 语旬,然后就可以 直接调用该命名 SQL 查询。在这种情况下,无须调用 addEntityO方法,因为在配置命名 SQL 查询时,己经完成了查询结果与实体的关联。 下面是命名 SQL 查询的配置片段: <1-- 映射 N-N 关联属性--> 〈工 d name=" 工 d" column="product_id" > <1-- 指定主键生成器策略-> < !… 定义关联属性的key ,对应连接表中外键列--> jdbc:mysql://localhost/hibernate root 32147 S update 主程序部分如下: public class SpringTest { public static void main(String[] args) { II 实例化 Spring 容器 ApplicationContext ctx = new FileSysternXmlAppl工 cationContext "bean.xml"); II 定义 Person 接口的实例 Person p = null; II 通过 Spring 上下文获得 chinese 实例 p = (Person)ctx.getBean("chinese"); II执行 chinese 实例的方法 System.out.println(p.sayHello("wawa")); System. out .println (p. sayGoodBye ("wawa") ); II 通过 Spring 上下文获得 american 实例 270…E p = (Person)ctx.getBean("american"); II 执行 american实例的方法 System.out.println(p.sayHello("wawa")) ; System. out .println (p. sayGoodBye ("wawa") ); 使用 Spring 时:即使没有工厂类 PersonFactory ,程序一样可以使用工厂模式, Spring 完全可以提供所有工厂模式的功能。 下面对主程序部分进行简单的修改: public class SpringTest { public static void main(String[] args) { II 实例化 Spring 容器 ApplicationContext ctx = new FileSystemXmlAppl工 cationContext("bean.xml"); II 定义 Person 接口的实例 pl Person pl = null; II 通过 Spring 上下文获得 chinese 实例 pl = (Person)ctx.getBean("chinese"); II 定义 Person 接口的实例 pl Person p2 = null; p2 = (Person)ctx.getBean("chinese"); System.out.printl口 (pl -- p2); 程序执行的结果是: true 表明 Spring 对接受容器管理的全部bean,默认采用单态模式管理。笔者建议不要随 便更改 bean 的行为方式。因为在性能上,单态的bean 比非单态的 bean 更优秀。 仔细检查上面的代码,就会发现如下特点: ·除测试用的主程序部分外,代码并未出现Spring 特定的类和接口。 ·调用者代码,也就是测试用的主程序部分,仅仅面向Person 接口编程,而无须知 道实现类的具体名称。同时,可以通过修改配置文件来切换底层的具体实现类。 .由于厂无须多个实例,因此工厂应该采用单态模式设计。其中Spring 的上下文, 也就是 Spring 工厂,已被设计成单态的。 Spring 工厂模式,不仅提供了创建bean 的功能,还提供对bean 生命周期的管理。 最重要的是还可管理bean 与 bean 之间的依赖关系。 5.4 Spring 的依赖注入 依赖注入( Dependency I时 ection) 是时下的"流行语",也是目前最优秀的解藕方式。 使用依赖注入时, J2EE 应用中的各种组件不需要以硬编码方式糯合在一起,甚至无 271轻量级 J2EE 企业应用实战一一-Struts+Spring+Hibernate 整合开发 须使用工厂模式。当某个 Java 实例需要其他 Java 实例时,系统会自动提供需要的实例, 无须程序显式获取。 依赖注入,是 Spring 的核心机制,可以使 Spring 的 bean 以配置文件组织在一起, 而不是以硬编码的方式棉合在一起。 5.4.1 理解依赖注入 因为某些历史原因,依赖注入还有一种称呼:控制反转 (Inversion of Control)。 不管是依赖注入,还是控制反转,其含义完全相同。当某个Java 实例(调用者)需 要另一个 Java 实例(被调用者)时,在传统的程序设计过程中,通常由调用者来创建被 调用者的实例。而在依赖注入的模式下,创建被调用者的工作不再由调用者来完成,通 常由 Spring 容器来完成,然后注入调用者,因此称为控制反转,也称为依赖注入。 不管是依赖注入,还是控制反转,都说明Spring 采用动态及灵活的方式来管理各种 对象,使对象与对象之间的具体实现互相透明。 为了更好地理解依赖注入,笔者建议参考人类社会的发展,看如下问题在各种社会 形态里如何解决:一个人(Java 实例,调用者)需要一把斧子(Java 实例,被调用者)。 在"原始社会"里,几乎没有社会分工。需要斧子的人(调用者)只能自己去磨一 把斧子(被调用者)。对应的情形为:Java 程序里的调用者自己创建被调用者。 进入"工业社会"后,随着工厂的出现,斧子不再由普通人完成,而在工厂里被生 产出来。此时需要斧子的人(调用者)只需找到工厂,购买斧子,无须关心斧子的制造 过程。对应简单工厂设计模式:调用者只需要定位工厂,无须管理被调用者具体的实现。 进入"共产主义"社会后,需要斧子的人甚至无须定位工厂,"坐等"社会提供即可。 调用者无须关心被调用者的实现,无须理会工厂,等待Spring 依赖注入即可。 在第一种情况下,由Java 实例的调用者创建被调用的Java 实例,调用者直接使用 new 关键宇创建被调用者实例,其程序高度藕合,效率低下。在实际应用中极少使用这 种方式。 在第二种情况下,调用者无须关心被调用者的具体实现过程,只需要找到符合某种 标准(接口)的实例即可使用。此时调用的代码面向接口编程,可以让调用者和被调用 者解楠,这也是工厂模式被大量使用的原因。但调用者需要自己定位工厂,使调用者与 工厂糯合在一起。 第三种情况,是最理想的情况,程序完全无须理会被调用者的实现,也无须定位工 厂,是最好的解糯方式。实例之间的依赖关系由容器提供。 所谓依赖注入,是指在程序运行过程中,如果需要调用另一个对象协助时,无须在 代码中创建被调用者,而是依赖于外部的注入。Spring 的依赖注入对调用者和被调用者 几乎没有任何要求,完全支持对POJO 之间依赖关系的管理。 依赖注入通常有两种: ·设值注入 ·构造注入 272部 ri 吨介绍国 5.4.2 设值注人 设值注入是指通过 setter 方法传入被调用者的实例。这种注入方式简单、直观,因 而在 Spring 的依赖注入里大量使用。 Person 接口的代码如下: /I定义 Person 接口 public interface Person IIPerson接口里定义一个使用斧子的方法 public vo工 d useAxe(); Axe 接口的代码如下: II 定义 Axe 接口 publ工 c interface Axe IIAxe 接口里有个砍的方法 public void chop(); Person 的实现类代码如下: IICh工 nese 实现 Person 接口 public class Chinese 工mplements Person II 面向 Axe 接口编程,而不是具体的实现类 private Axe axe; II 默认的构造器 public Chinese() II 设值注入所需的 setter 方法 public void setAxe(Axe axe) this.axe = axe; II 实现 Person 接口的 useAxe 方法 public void useAxe() System.out.println(axe.chop()); Axe 的第一个实现类代码如下: I IAxe 的第一个实现类 StoneAxe public class StoneAxe implements Axe /I默认构造器 public StoneAxe() 273轻量级 J2EE 企业应用实战 Struts+Spring+Hibernate 整合开发 II 实现 Axe 接口的 chop 方法 public String chop() return II 石斧砍柴好慢"; 下面采用 Spring 的配置文件将 Person 实例和 Axe 实例组织在一起。配置文件如下 所示: 从配置文件中可以看到Spring 管理 bean 的灵巧性。 bean 与 bean 之间的依赖关系被 放在配置文件里组织,而不是写在代码里。通过配置文件的指定,Spring 能精确地为每 个 bean 注入属性。因此,配置文件里bean 的 class 元素不能是接口,而必须是真正的实 现类。 另外, Spring 会自动接管每个bean 定义里的 property 元素定义。 Spring 会在执行无 参数的构造器后,创建默认的bean 实例,并调用对应的setter 方法为程序注入属性值。 在这里, property 定义的属性值将不再由该bean 来主动创建和管理,而是接收Spring 的 注入。 每个 bean 的 id 属性是该 bean 的唯一标识,程序通过id 属性来访问 bean , bean 与 bean 的依赖关系也通过id 属性关联。 下面是主程序部分: public class BeanTest { II 主方法,程序的入口 public static void rnain(String[] args)throws Exception II 因为是独立的应用程序,显式了实例化Spring 的上下文。 ApplicationContext ctx = new FileSysternXrnlApplicationContext("bean.xrnl"); II 通过 Person bean 的 id 来获取 bean 实例,面向接口编程,因此 274句仰制阳川…r川巾问in问叫n咱9 II 此处强制类型转换为接口类型 Person p = (Person)ctx.getBean("chinese"); II 直接执行 Person 的 userAxe ()方法。 p.useAxe() ; 程序的执行结果如下: 石斧砍柴好慢 当主程序调用 Person 的 useAxeO 方法时,该方法的方法体内需要使用 Axe 的实例, 但程序里没有任何地方将特定的 Person 实例和 Axe 实例糯合在一起。或者说,程序里没 有为 Person 实例传入 Axe 的实例,而 Axe 实例由 Spring 在运行期间动态注入。 Person 实例既不需要了解Axe 实例的具体实现,也无须了解Axe 的创建过程。程序 在运行到需要 Axe 实例时,由 Spring 创建 Axe 实例,然后注入给需要 Axe 实例的调用 者。因此,当 Person 实例运行到需要 Axe 实例的地方时,自然就产生了 Axe 实例,用 来供 Person 实例使用。 下面也给出使用 Ant 编译和运行该应用的简单脚本: 由此可看出,执行效果与使用steelAxe 设值注入时的执行效果完全相同。区别在于 创建 Person 实例中 Axe 属性的时机不同一-设值注入是先创建一个默认的bean 实例, 然后调用对应的setter 方法注入依赖关系;而构造注入则在创建bean 实例时,已经完成 了依赖关系的注入。 5.4.4 两种注人方式的对比 Spring 同时支持两种依赖注入方式:设值注入和构造注入。这两种注入方式各有其 优、缺点。 1.设值注入的优点 ·设值注入与传统的 JavaB ean 的写法更相似,程序开发人员更容易了解,接受。通 过 setter 方法设定依赖关系显得更加直观、自然。 ·对于复杂的依赖关系,如果采用构造注入,会导致构造器过于雕肿,难以阅读。 因为 Spring 在创建 bean 实例时,需要同时实例化其依赖的全部实例,因而导致 277轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 性能下降。而使用设值注入,则能避免这些问题。 ·尤其是在某些属性可选的情况下,多参数的构造器更加笨重。 2. 构造注入的优点 ·可以在构造器中决定依赖关系的注入顺序。例如,组件中其他依赖关系的注入, 常常需要依赖于 Datasource 的注入。采用构造注入时,可以在代码中清晰地决定 注入顺序,优先依赖的优先注入。 ·对于依赖关系无须变化的 bean ,构造注入更有用处。因为没有 setter 方法,所有 的依赖关系全部在构造器内设定。因此,无须担心后续的代码对依赖关系产生破 坏。 ·依赖关系只能在构造器中设定,因为只有组件的创建者才能改变组件的依赖关 系。对组件的调用者而言,组件内部的依赖关系完全透明,更符合高内聚的原则。 建议采用以设值注入为主,构造注入为辅的注入策略。对于依赖关系无须变化的注 入,尽量采用构造注入:而其他的依赖关系的注入,则考虑采用设值注入。 5.5 bean 和 BeanFactory bean 是 Spring 管理的基本单位,在 Spring 的 J2EE 应用中,所有的组件都是 bean , bean 包括数据源、 Hibernate 的 SessionFactory 及事务管理器等。 Spring 里的 bean 是非常 广义的概念,任何的 Java 对象, Java 组件都可被当成 bean 处理。甚至这些组件并不是 标准的 JavaBean 。 整个应用中各层的对象都处于 Spring 的管理下,这些对象以 bean 的方式存在。 Spring 负责创建 bean 实例,并管理其生命周期。 bean 在 Spring 容器中运行时,无须感受 Spring 容器的存在,一样可以接受 Spring 的依赖注入,包括 bean 属性的注入,合作者的注入 及依赖关系的注入等。 Spring 的容器有两个接口: BeanFacto 可和 ApplicationContext ,这两个接口的实例也 被称为 Spring 上下文,它们都是产生 bean 的工厂, bean 是 Spring 工厂产生的实例。在 Spring 产生 bean 实例时,需要知道每个 bean 的实现类,而 bean 实例的使用者面向接口, 无须关心 bean 实例的实现类。因为 Spring 工厂负责维护 bean 实例的实例化,所以使用 者无须关心实例化。 bean 定义通常使用 XML 配置文件。正确定义的 bean 由 Spring 提供实例化,以及依 赖关系的注入。 bean 实例通过 BeanFactory 访问。对于大部分J2 EE 应用, bean 通过 AppliactionContext 提供访问,因为 AppliactionContext 是 BeanFactory 的子接口,提供比 BeanFactory 更多的功能。 5.5.1 Spring 容器 Spring 的容器最基本的接口就是: BeanFactory 0 BeanFactory 负责配置、创建及管理 278…E bean ,它有个子接口: ApplictionContext ,因此也被称为 Spring 上下文。另外, Spring 容器还负责管理 bean 与 bean 之间的依赖关系。 BeanFactory 接口包含如下的基本方法。 • public boolean containsBean(String name): 判断 Spring 容器是否包含 id 为 name 的 bean 定义。 • public Object getBean(String name): 返回容器 id 为 name 的 bean 。 • public Object getBean(String name, Class required可pe): 返回容器中 id 为 name , 并且类型为 required可pe 的 bean 。 • public Class getType(String name): 返回容器中 id 为 name 的 bean 的类型。 调用者只需使用 getBean 方法即可获得指定 bean 的引用,无须关心 bean 的实例化 过程。即 bean 实例的创建过程完全透明。 BeanFacto可有很多实现类,通常使用 org.springframework. beans.factory.xml.XmlBean Factory 类。但对大部分J2EE 应用而言,推荐使用 ApplicationContext ,因为其是 BeanFactory 的子接口,其常用的实现类是 org.springframework.context.support. FileSystem XmlApplicationContext。 创建 BeanFactory 的实例时,必须提供 Spring 容器管理 bean 的详细配置信息。Spring 的配置信息通常采用 XML 配置文件来设置。因此,在创建BeanFactory 实例时,应该提 供 XML 配置文件作为参数, XML 配置文件通常使用 Resource 对象传入。 Resource 接口:用于访问配置文件资源。 对于大部分J2EE 应用而言,可在启动Web 应用时自动加载 ApplicationContext实例, 接受 Sp由19 管理的 bean 无须知道ApplicationContext的存在,也一样可以利用ApplicationConte刻 的管理。对于独立的应用程序,也可通过如下方法来实例化BeanFactory: II 以指定路径下 bean.xml 配置文件为参数,创建文件输入流 工 nputStream is = new FileInputStream("beans.xml"); II 以指定的文件输入流is ,创建 Resource 对象 InputStreamResource isr = new 工 nputStreamResource(is); II 以 Resource 对象作为参数,创建 BeanFactory 的实例 XmlBeanFactory factory = new XmlBeanFactory(isr); 或者采用如下方法: II 搜索 CLASSPATH路径,以 CLASSPATH路径下的 beans.xml 文件创建 Resource对象 ClassPathResource res = new ClassPathResource("beans.xml"); II 以 Resource对象为参数,创建BeanFactory实例 XmlBeanFactory factory = new XmlBeanFactory(res); 如果应用里有多个属性配置文件,则应该采用BeanF邵阳可的子接口ApplicationContext 来创建 BeanFactory 的实例, ApplicationContext通常使用如下两个实现类。 • FileSystemXmlApplicationContext: 以指定路径的 XML 配置文件创建 ApplicationContext 。 • ClassPathXmlApplicationContext: 以 CLASSPATH 路径下的 XML 配置文件创建 ApplicationContext 。 279轻量级 J2EE 企业应用实战--Struts+Spring+Hibernate 整合开发 如果需要同时加载多个 XML 配置文件,可以采用如下方式: II 搜索 CLASSPATH 路径,以 CLASSPATH 路径下的 applicationContext.xml Ilservice.xml 文件创建 ApplicationContext ClassPathXmlApplicationContext appContext = new ClassPathXrnlApplicationContext( new String [] {" applicationContext.xml" , "service. 泪nl"}) ; II 事实上, ApplicationContext 是 BeanFactory 的子接口,支持强制类型转换 BeanFactory factory = (BeanFactory) appContext; 当然也可支持从指定路径来搜索特定文件加载: II 指定路径下的applicationContext.xml, service.xml文件创建ApplicationContext FileSystemXrnlApplicationContext appContext = new F 工 leSystemXrnlApplicationContext( new String[] {"applicationContext.xml" , "service.xml"}); II'事实上, ApplicationContext是 BeanFactory的子接口,支持强制类型转换 BeanFactory factory = (BeanFactory) appContext; 下面是 Spring 最简单的配置文件: 在 Spring 容器集中管理 bean 的实例化时, bean 实例可以通过 BeanFactory 的 getBean(String beanid)方法得到。此时, BeanFactory 将变成简单工厂模式里的工厂,程 序只需要获取 BeanFactory 引用,即可获得 Spring 容器管理全部实例的引用,从而使程 序不需要与具体实例的实现过程藕合。在大部分J2EE 应用中,当应用启动时,会自动 创建 Spring 容器实例,组件之间直接以依赖注入的方式藕合,甚至无须访问Spring 容器。 5.5.3 定义 Bean 的行为方式 在 Spring 1. 2 版本中, bean 在 Spring 的容器中有两种基本行为。 • singleton: 单态。 • non-singleton 或 prototype: 原型。 如果一个 bean 被设置成 non-singleton 行为,当程序每次请求该 id 的 bean 时, Spring 都会新建一个 bean 实例,然后返回给程序。在这种情况下, Spring 容器仅仅使用 new 关 键字创建 bean 实例,一旦创建成功,容器不再跟踪实例,也不会维护 bean 实例的状态。 通常要求将 Web 应用的控制器 bean 配置成 non-singleton 行为。因为,每次 HttpServletRequest 都需要系统启动一个新 Action 来处理用户请求。 如果一个 bean 被设置成 singleton 时,整个 Spring 容器里只有一个共享实例存在, 程序每次请求该 id 的 bean 时, Spring 都会返回该 bean 的共享实例。该容器负责跟踪单 态 bean 实例的状态,维护 bean 实例的生命周期。 如果不指定 bean 的基本行为, Spring 默认使用 singleton 行为。在创建 Java 实例时, 需要进行内存申请:销毁实例时,需要完成垃圾回收,这些工作都会导致系统开销的增 加。因此, non-singleton 行为的 bean 创建、销毁时代价比较大。而 singleton 行为的 bean 实例成功后,可以重复使用。因此,应尽量避免将 bean 设置成 non-singleton 行为。 关于自定义 bean 的生命周期行为,请参看 5.7.2 节。设置 bean 的基本行为,是通过 singleton 属性来指定, singleton 属性只接受 true 或 false 值。例如在下面配置文件中配置 singleton 和 non-singleton: root pass 主程序部分由 BeanFactory 来获取该 bean 的实例,获取实例时使用bean 的唯一标识 符: id 属性。该属性是bean 实例在容器中的访问点。 下面是主程序部分: public class BeanTest public static void main(String[] args)throws Exception II 实例化 Spring 容器。 Spring 容器负责实例化bean ApplicationContext ctx = new FileSystemXmlApplicationContext("bean.刀nl") ; /I 通过 bean id 获取 bean 实例,并强制类型转换为 DataSource DataSource ds = (DataSource)ctx.getBean("dataSource"); II 通过 DataSource来获取数据库连接 Connection conn = ds.getConnection(); /I 通过数据库连接获取Statement java.sql.Statement stmt = conn.createStatement(); II 使用 statement 执行 sql 语句 stmt.execute("insert into mytable values('wdda2')"); /I清理资源,回收数据库连接资源 if (stmt != null)stmt.close(); if (conn != null)conn.close(); 从该实例可以看出, Spring 的 bean 远远超出值对象的JavaBean 范畴,此时 bean 可 以代表应用中的任何组件及任何资源实例。 虽然 Spring 对 bean 没有特殊要求,但笔者还是建议在Spring 中的 bean 应满足如下 几个原则: 283轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 ·每个 bean 实现类都应提供无参数的构造器。 ·接受构造注入的 bean ,则应提供对应的构造函数。 ·接受设值注入的 bean ,则应提供对应的 setter 方法,并不强制要求提供对应的 getter 方法。 传统 lavaBean 和 Spring 中 bean 存在如下区别。 ·用处不同:传统 lavaBean 更多地作为值对象传递参数,而 Spring 中的 bean 用处 几乎无所不在,任何应用组件都可被称为 beano ·写法不同:传统 lavaBean 作为值对象,要求每个属性都提供 getter 和 setter 方法: 但 Spring 中的 bean 只需为接受设值注入的属性提供 setter 方法。 ·生命周期不同:传统 lavaBean 作为值对象传递,不接收任何容器管理其生命周期: Spring 中的 bean 由 Spring 管理其生命周期行为。 5.5.5 创建 bean 实例 大多数情况下, BeanFactory 可直接调用构造函数来创建一个 bean ,并以 class 属性 确定 bean 实例的实现类。因此, bean 元素的 class 属性通常是必需的,但这并不是创建 bean 的唯一方法。 创建 bean 通常有如下方法: ·调用构造器创建一个 bean 实例。 • BeanF actory 调用某个类的静态工厂方法创建 beano • BeanFactory 调用实例工厂方法创建 beano 1. 调用构造函数 "new" 一个 bean 实例 通过 "new" 关键字创建 bean 实例是最常见的情形,如果采用设值注入的方式,则 要求该类提供无参数的构造器。在这种情况下, class 元素是必需的(除非采用继承) , class 属性的值就是 bean 实例的实现类。 然后, BeanFactory 将调用该构造器来创建 bean 实例,该实例是个默认实例。所有 的属性执行默认初始化。 接下来, BeanFactory 会根据配置文件来决定依赖关系:首先实例化依赖的 bean; 然后为 bean 注入依赖关系:最后将一个完整的 bean 实例返回给程序。此时该 bean 实例 的所有属性,已经由 Spring 容器完成了初始化。下面是用调用构造函数" new" 一个 bean 的实例。 调用者 bean 的接口和实现类如下: II 定义 Person 接口 public interface Person IIPerson 接口里定义一个使用斧子的方法 public void useAxe(); 284…~ Person 的实现类 IIChinese 实现 Person 接口 public class Chinese implements Person II 面向Axe 接口编程,而不是具体的实现类 private Axe axe; II 默认的构造器 public Chinese() System.out.println("Spring实例化主调 bean: Chinese 实例... "); II 设值注入所需的setter 方法 public void setAxe(Axe axe) System.out.pr工 ntln (" Spring 执行依赖关系注入..."); this.axe = axe; II 实现 Person 接口的 useAxe 方法 public void useAxe() System.out.println(axe.chop(»; 下面给出 Person 接口依赖 bean 的接口和实现类: II 定义Axe 接口 public 工 nterface Axe IIAxe 接口里有个砍的方法 public void chop(); Axe 的实现类 SteelAxe: IISteelAxe类实现Axe 接口 public class SteelAxe implements Axe II 默认构造器 public SteelAxe() System.out.println("Spring实例化依赖bean: SteelAxe 实例... ") i II 实现Axe 接口的 chop 方法 public String chop() return "钢斧砍柴真快11 i Spring 的配置文件如下: 285轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 主程序如下: public class BeanTest publ工 c static void mai~(String[] args)throws Exception II;在当前路径下搜索bean.xml 文件,实例化文件输入流 InputStream is = new File工口 putStream("bean. xml") ; II 以指定的文件输入流 ~s ,创建 Resource 对象 InputStreamResource isr =口ew 工 nputStreamResource(is); II 以 Resource对象作为参数,创建BeanFactory的实例 XmlBeanFactory factory = new XmlBeanFactory(isr); System.out.println("程序已经实例化BeanFactory...") ; Person p = (Person)factory.getBean("chinese"); System.out.pr工 ntln( "程序中己经完成了chinese bean 的实例化.. .11); p.useAxe(); 执行结果如下: 程序已经实例化BeanFactory Spring 实例化主调 bean: Chinese 实例.. . Spring 实例化依赖 bean: SteelAxe 实例., . Spring 执行依赖关系注入.. . 程序中已经完成了 chinese bean 的实例化., . 钢斧砍柴真快 执行结果清楚地反映了执行过程: (1)创建 BeanF actory 实例。 (2)调用 Chinese 类的默认构造器创建默认实例。 (3)根据配置文件注入依赖关系:先实例化依赖 bean ,然后将依赖 bean 注入。 (4) 返回一个完整的 lavaBean 实例。 2. 使用静态工厂方法创建 bean 使用静态工厂方法创建 bean 实例时, class 属性也是必需的,但此时 class 属性并不 是该实例的实现类,而是静态工厂类。由于 Spring 需要知道由哪个静态工厂方法来创建 bean 实例,因此使用 factory- method 属性来确定静态工厂方法名,在之后的过程中, Spring 的处理步骤与采用其他方法的创建完全一样。 286句 ring 介绍国 下面通过 factory-method 指定的方法来创建 bean 。注意:这个 bean 定义并没有指定 返回对象的类型,只指定静态工厂类。该方法必须是静态的,如果静态工厂方法需要参 数,则使用 元素将其导入。 下面是 Being 接口: public interface Being { II 接口定义 testBeing 方法 publ 工 c void testBeing(); 下面是接口的两个实现类。 Dog 实现 Being 接口: public class Dog implements Being private Str工口 g msg; II 依赖注入时必需的setter 方法 public void setMsg(String msg) this.msg = msg; II 实现接口必须实现的testBeing方法 public void testBeing() System.out.println(msg + " 狗爱啃骨头") ; Cat 实现 Being 接口: public class Cat implements Being private String msg; II 依赖注入时必需的setter 方法 public void setMsg(String msg) this.msg = msg; II 实现接口必须实现的testBeing方法 public void testBeing() System.out.pr工 ntln(msg + • 猫喜欢吃老鼠") ; 下面的工厂包含静态方法,其静态方法可返回 Being 实例: public class BeingFactory 1** *获取 Being 实例的静态工厂方法 * param arg 静态工厂方法根据该参数决定返回Being 的哪个实例 *I public static Being getBeing(String arg) 287轻量级 J2EE 企业应用实战-一-Struts+Spring+Hibernate 整合开发 288 II 调用此静态方法的参数为 dog ,则返回 Dog 实例 if (arg.equalsIgnoreCase("dog")) return new Dog(); II 否则返回 Cat 实例 else return new Cat(); 下面是使用静态工厂方法创建bean 实例的配置文件: < !一指定 Spring 配置文件的 dtd> <1-- Spring 配置文件的根元素一〉 <'一定义 chinese bean --> <'一定义List 属性,使用 list 元素一〉 小学 中学 大学 87 89 卸伽制阳川…r川阳问i阳问叫n吨g 82 297轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 30 ll <1 以下是访问 bean 属性的方式,这样可以将 perso 且这个 bean 的 son 的 age 属性赋值给 son2 这个 bean 的 age 属性-→ 主程序如下: public class SpringTest { public static void main(String[] args) ApplicationContext ctx = new FileSysternXm lApplicationContext("bean.xml"); /I打印出 son2 的 age 属性值 System.out.println("son2的 age 属性值: " + ((Son)ctx.getBean("son2")) .getAge()); 执行结果如下: [java] 系统获取 son2 的 age 属性值: 11 其中, son2 实例的 age 属性,来自于person bean 的嵌套 bean 的 age 属性。 一个 bean 实例的属性值,不仅可以注入另一个bean,还可将 bean 实例的属性值直 接定义成 bean 实例,也是通过PropertyPathFactoryBean 完成的。对上面的配置文件增加 如下代码: person <'一 确定属性名,表明son1 bean 来自哪个目标bean 的属性一〉 son 298…~ 主程序部分增加如下的输出: System.out.println("系统获取的 son1: " + ctx.getBean("son1")); 主程序部分直接输出bean! ,此输出语旬的执行结果如下: [java] 系统获取的 son1: lee.Son@25d2b2 使用 PropertyPathFactory Bean 必须指定以下两个属性。 • targetB e anName: 用于指定目标 bean ,确定获取哪个 bean 的属性值。 • propertyPath: 用于指定属性,确定获取目标 bean 的哪个属性值,此处的属性可 直接使用属性表达式。例如,如想获取 person bean 的 son 属性的 age 属性,可采 用 son.age 。 也可将基本数据类型的属性值定义成 bean 实例。在配置文件中再增加如下代码: <~-- 将基本数据类型的属性值定义成 bean 实例--> person son.age 主程序部分增加如下输出: System.out.println("系统获取 theAge 的值: " + ctx.getBean("theAge")); 程序执行结果如下: [java] 系统获取 theAge 的值: 11 目标 bean 既可以是容器中己有的bean 实例,也可是嵌套的bean 实例。因此,下面 的定义也是有效的: 12 age 299轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 5.6.3 注人 field 值 通过 FieldRetrievingFactory Bean 类,可以完成 field 值的注入。 FieldRetrieving FactoryBean 用来获得目标 bean 的 field 值。获得的值可注入给其他 bean ,也可直接定义 成新的 bean 。看如下配置文件: java.lang.System getPropert工 es 该配置文件将 java.lang.System 类的静态方法 getProperties 的返回值,直接定义为 bean 实例。然后再增加如下代码: <1 一 targetObject确定目标 bean,确定调用哪个bean 一〉 getProperty java.version 该配置文件将实例方法返回值直接定义成bean。该实例是上面配置的sysProps bean, 实例方法是Property 类的 getProperty 方法。在主程序中增加如下输出: System.out.println(" 系统获取 Java 版本: " + ctx.getBean (" javaVersion川) ; 输出结果如下: [java] 系统获取 Java 版本: 1.4.2_04 因此,这种配置方式也可用于定义静态工厂方法来创建bean 实例,或用实例工厂方 303轻量级 J2EE 企业应用实战一-8t阳ts+8pring+Hibernate 整合开发 法来创建 bean 实例。配置示例如下: wangwang 上面的配置文件,要求Chinese 类中有如下方法: /** *依赖关系必需的setter 方法 *因为需要通过名字自动装配,因此setter 方法名必须是 set + bean 名。 bean 名首字母大写 * @ dog 设置的 dog 值 public void setGunDog(Dog dog) this.dog = dog; 2. byType 规则 byType 规则,指根据类型匹配来注入依赖关系。假如A 实例有 setB(B b)方法,而 Spring 配置文件中恰有一个类型B 的 bean 实例,当容器为 A 注入类型匹配的 bean 实例 时,如果容器中没有一个类型为B 的实例,或有多于一个的B 实例时,都将抛出异常。 看如下配置文件: 飞Nangwang 上面的配置文件要求Chinese 类中有如下方法: /** *依赖关系必需的setter 方法 *因为使用按类型自动装配,要求setter 方法的参数类型与容器的bean 的类型相同。 *程序中的 GunDog 实现 Dog 接口 * @ dog 设置的 dog 值 */ public void setDog(Dog dog) th工 s.dog = dog; 但如果出现如下配置文件: wangwang 也可采用第二种方法达到同样的效果,可让Chinese 类实现 DisposableBean接口, 该接口提供一个方法: void destroy() throws Exception; 实现该接口必须实现该方法,该方法就是依赖注入之后执行的方法。因此可将 Chinese 修改成如下形式: public class Chinese implements Person , DisposableBean { pri 飞rate Axe axe; public Chinese() { System.out.println( 叩pring 实例化主调 bean: Chinese 实例...") i II 依赖注入必需的 setter 方法 public void setAxe(Axe axe) System.out.println("Spring执行依赖关系注入..."); this.axe = axe; II 测试用销毁方法 public void destroy() throws Exception; System.out.println(町正在执行销毁前的资源回收方法..."); 实现 DisposableBean 接口,则无须使用 destroy-method 属性来指定销毁之前的方法。 由于在销毁 bean 实例之前, Spring 容器会自动调用 destroy 方法,因此执行效果与采用 312…E destroy-method 完全一样。但实现 DisposableBean 接口污染了代码,是侵入式设计,因 此不推荐采用。 注意:如果既采用 destroy-method 属性指定销毁之前的方法,又采用实现 DisposableBean 接口来指定销毁之前的方法。则容器就会先执行DisposableBean 接口中 定义的方法,然后执行 destroy-method 属性指定的方法。 5工3 协调不同步的 bean Spring 容器中的 bean 有如下两种基本行为: • singleton bean 。 • non-singleton bean 。 当两个 singleton bean 存在某种依赖时,或 non-singleton 依赖 singleton bean 时,仅通 过属性定义依赖就足够了。但对 singleton bean 依赖于 non-singleton bean 时, singleton bean 只有一次初始化机会,其依赖关系的设置也在初始化时进行。而其依赖的 non-singleton bean 每次都有新实例,这将导致 singleton bean 的依赖不能得到即时更新,使 singleton bean 的依赖一直是最开始的 bean ,即使 non-singleton bean 后来有了更多新的实例。 这样问题就产生了:当 singleton bean 依赖于 non-singleton bean 时,会产生不同步的 现象。解决该问题有以下两种思路。 ·部分放弃依赖注入:当 singleton bean 每次需要 non-singleton bean 时,主动向容 器请求新的 bean 实例,保证了每次产生的 bean 实例都是新的实例。 ·利用方法注入。 第一种方式显然不是一个好的做法,因为在代码中主动请求新的bean 实例时,必然 导致代码与 Spring API 糯合,造成严重代码污染。通常情形下,我们采用第二种做 法一一使用方法注入,通常使用 lookup 方法注入。 lookup 方法注入:指容器能够重写容器中 bean 的抽象或具体方法,并返回查找容器 中其他 bean 的结果。被查找的 bean 通常是一个 non-singleton bean (尽管也可以是一个 singleton 的 )0 Spring 通过使用 CGLIB 库修改客户端的二进制码,从而实现上述的要求。 看下面代码: public class SteelAxe implements Axe { Ilcount 是个状态值,每次执行 chop 方法该值增加 1 private int count = 0; public SteelAxe() System.out.println("Spring实例化依赖bean: SteelAxe 实例.. ."); II 测试用方法 public String chop() return "钢斧砍柴真快" + ++count; 313轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 上面的 SteelAxe 将被部署成 non-singleton bean ,并被一个 singleton bean 所依赖。 singleton bean 的代码如下: public abstract class Chinese implements Person { pr 工 vate Axe axe; public Chinese () { System.out.println("Spring 实例化主调 bean: Chinese 实例.. ."); II 方法注入所需要的方法,该方法由Spring 提供实现。 public abstract Axe createAxe(); II 依赖注入必需的 setter 方法 public void setAxe(Axe axe) System.out.println("Spring执行依赖关系注入..."); this.axe = axe; II用于返回此bean 的依赖 bean public Axe getAxe() return axe; 下面给出其配置文件: 从配置文件中可以看出,抽象bean 的定义与普通bean 的定义几乎没有区别,仅仅 增加 abstract 属性为 true ,但主程序执行结果却有显著的差别。下面的主程序采用 AppliactionContext 作为 Spring 容器, . AppliationContext 默认预初始化所有的 singleton bean 。其主程序部分如下: public class BeanTest { public static void main(Str工口g[] args)throws Exception Appl工 cationCo口 text ctx = new FileSysternXmlApplicationContext("bean.xml"); 主程序部分仅仅实例化了ApplicationContext,在实例化 ApplicationContext 时,默 认实例化 singleton bean。程序执行结果如下z [java] Spring 实例化依赖bean: SteelAxe 实例.. . 容器并没有实例化 chineseTemplate bean ,而忽略了所有声明为 abstract 的 beano 如 果取消 abstract 属性定义,则程序执行结果如下: [java] Spring 实例化依赖bean: SteelAxe 实~J... [java] Spring 实例化主调 bean: Chinese 实例.. . [java] Spring 执行依赖关系注入... 可以看出,抽象 bean 是→个 bean 模板,容器会忽略抽象 bean 定义,因而不会实例 化抽象 bean 。但抽象 bean 无须实例化,因此可以没有 class 属性。如下的配置文件也有 效: 316…E 注意:抽象 bean 不能实例化,既不能通过getBean 获得抽象 bean ,也不能让其他 bean 的 ref 属性值指向抽象bean ,因而只要企图实例化抽象bean ,都将导致错误。 5.8.2 定义子 bean 我们把指定了 parent 属性值的 bean 称为子 bean; parent 指向子 bean 的模板,称为 父 bean 。 子 bean 可以从父 bean 继承实现类、构造器参数及属性值,也可以增加新的值。如 果指定了 init-method , destroy-method 和 factory-method 的属性,则它们会覆盖父 bean 的定义。 子 bean 无法从父 bean 继承如下属性: depends-on, autowire, dependency-check, singleton, lazy-init 。这些属性将从子 bean 定义中获得,或采用默认值。 通过设置 p缸ent 属性来定义子 bean , p缸ent 属性值为父 bean id 。修改上面的配置文 件如下,增加了子 bean 定义: 程序执行结果如下: [java] Spring 实例化依赖bean: SteelAxe 实例.. . [java] spring 实例化依赖bean: StoneAxe 实例.. . [java] Spring 实例化主调 bean: Chinese 实例.. . [java] Spring 执行依赖关系注入... [java] 石斧砍柴好慢 此时,子 bean 的依赖不再是父 bean 定义的依赖了。 注意:上例中的子 bean 定义都没有 class 属性,因为父 bean 定义中已有 class 属性, 子 bean 的 class 属性可从父 bean 定义中继承;如果父 bean 定义中也没有指定 class 属性, 则子 bean 定义中必须指定 class 属性,否则会出错;如果父 bean 定义指定了 class 属性, 子 bean 定义也指定了 class 属性,则子 bean 将定义的 class 属性覆盖父 bean 定义的 class 属性。 5.8.3 Spring bean 的继承与 Java 中继承的区别 Spring 中的 bean 继承与 Java 中的继承截然不同。前者是实例与实例之间参数值的 延续,后者则是从一般到特殊的细化。前者是对象与对象之间的关系,后者是类与类之 间的关系。因此, Spring 中 bean 的继承和 Java 中 bean 的继承有如下区别: 318…E • Spring 中的子 bean 和父 bean 可以是不同类型,但在 Java 中的,子类是对父类的 加强,是一种特殊的父类。 • Spring 中 bean 的继承是实例之间的关系,主要表现为参数值的延续:而 Java 中 的继承是类与类之间的关系,主要表现为方法及属性的延续。 • Spring 中子 bean 不可作父 bean 使用,不具备多态'性:而 Java 中的子类实例完全 可当成父类实例使用。 5.9 bean 后处理器 Spring 提供了一种 bean ,这种 bean 并不对外提供服务,无需 id 属性,但它负责对 容器中的其他 bean 执行处理,例如为容器中的目标 bean 生成代理。这种 bean 可称为 bean 后处理器,它在 bean 实例创建成功后,对其进行进一步的加强处理。 bean 后处理器必须实现 BeanPos tProcessor 接口,该接口包含以下两个方法。 • Object postProcessBeforelnitialization(Object bean , String name)throws Beans Exception: 该方法第一个参数是系统即将初始化的 bean 实例:第二个参数是 bean 实例的名字。 • Object postProcessAfterInitialization(Object bean , String name)throws Beans Exception: 该方法的第一个参数是系统刚完成初始化的 bean 实例:第二个参数 是 bean 实例的名字。 实现该接口的 bean 必然会实现这两个方法,这两个方法在容器中的每个 bean 执行 初始化方法前后分别调用。 另外,这两个方法也可用于对系统完成的默认初始化进行加强。看如下代码: II 自定义 bean 后处理器,负责后处理容器中的所有 bean public class MyBeanPostProcessor implements BeanPostProcessor II 在初始化 bean 之前调用该方法 public Object postProcessBeforelnit工 alization(Object bean String beanName)throws BeansException II 仅仅打印一行字符串 System.out.println("系统正在准备对" + beanName + "进行初始化. .."); return bean: II 在初始化 bean 之后调用该方法 public Object postProcessAfter工 nitialization(Object bean , String beanName)throws BeansException System.out.println("系统己经完成对" + beanName + "的初始化") ; II 如果系统刚完成初始化的 bean 是 Chinese if (bean instance of Ch工nese) II 为 Chinese 的实例设置 name 属性。 Chinese c = (Chinese)bean; c.setName("wawa"); 319轻量级 J2EE 企业应用实战一-5t阳ts+5pring+Hibernate 整合开发 return bean; 下面是 Chinese 的源代码,该类实现了InitializingBean接口,另外还提供了一个初 始化方法,这都是由Spring 容器控制的生命周期行为。 public class Chinese implements Person,工nitializingBean { private Axe axe; pr工vate Str工 ng name; public Chinese() { System.out.println("Spring 实例化主调 bean: Chinese 实例... "); public void setAxe(Axe axe) { System.out.println("Spring 执行依赖关系注入... "); this.axe = axe; } public void setName(String name) { th 工 s.name = name; } public void useAxe() { System.out.println(name + axe.chop()); } public void init() System.out.println("正在执行初始化方法 init.. ."); public void afterPropertiesSet() throws Exception System.out.println("正在执行初始化方法 afterPropertiesSet…") ; 配置文件如下: <1 一指定 Spring 配置文件的 DTD> <'一配置bean 后处理器,没有id 属性一〉 本程序配置了两个初始化方法: • init-method 指定初始化方法。 320…~ ·实现 InitializingBean 接口。 Chinese 类实现了 BeanPostProcessor 接口,并实现了这两个方法,这两个方法分别 在初始化方法调用前、后得到回调。其主程序如下: public class BeanTest { public static void main(String[] args)throws Exception 工nputStream is = new File工 nputStream("bean. xml") ; /I以指定的文件输入流 lS. 创建 Resource 对象 工 nputStreamResource 工 sr = new 工 nputStreamResource(is); II 以 Resource 对象作为参数,创建 BeanFactory 的实例 Xm lBeanFactory factory = new XmlBeanFactory(isr); II 实例化 BeanPostProcessor Chinese c = new Chinese(); II 注册 BeanPostProcessor实例 factory.addBeanPostProcessor(c) ; System.out.pr 工 ntln( "程序已经实例化 BeanFactory...") ; Person p = (Person)factory.getBean( 町 chinese") ; System.out.println(" 程序中已经完成了 chinese bean 的实例化.. ."); p.useAxe(); 使用 BeanFactory 时必须手动注册BeanPostProcessor实例。通过 XmlBeanFactory的 addBeanPostProcessor 可以注册 BeanPostProcessor 实例。程序执行结果如下: [java] 系统正在准备对steelAxe进行初始化... [java] 系统已经完成对 steelAxe 的初始化 [java] Spring 实例化主调 bean: Chinese 实例.. . [java] Spring 执行依赖关系注入... [java] 系统正在准备对 chinese 进行初始化.. . [java] 正在执行初始化方法 afterPropertiesSet... [java] 正在执行初始化方法 init.. . [java] 系统己经完成对 chinese 的初始化 [java] wawa 钢斧砍柴真快 注意:在配置文件中配直Chinese 实例时,并未确定name 属性,但程序执行时,其 name 属性有了确定值。这是由bean 后处理器完成的,在bean 后处理器中判断bean 是 否为 Chinese 实例,并设置了它的name 属性。 容器中一旦注册了 bean 后处理器后, bean 后处理器就会自动启动,并在容器中每 个 bean 创建时自动工作。 如果实现了 BeanPostProcessor接口,就可对 bean 做任何操作,包括完全忽略这个 回调。BeanPostProcessor通常用来检查标记接口,或者将bean 包装成一个proxy 等。 Spring 的很多工具类,就是通过实现BeanPostProcessor 接口实现的。 从主程序中看到,采用 BeanFactory 作为 Spring 容器时,必须手动注册 BeanPostProcessor。而对于 ApplicationContext,则无须手动注册。因为ApplicationContext 可自动检测到容器中的bean 后处理器,并将其注册成BeanPostProcessor,它们会在 bean 实例创建时自动启动。其主程序采用如下代码可以达到相同的效果: 321轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 public class BeanTest { public static void main(String[] args)throws Exception { ApplicationContext ctx = new FileSystemXmlApplicationContext("bean.xml"); Person p = (Person)factory.getBean("chinese"); System.out.println("程序中已经完成了chinese bean 的实例化... ") i p.useAxe(); 使用 ApplicationContext作为容器时,无须手动注册BeanPostProcessor。因此如果需 要使用 bean 后处理器,建议使用ApplicationContext,而不使用 BeanFactory。 5.10 容器后处理器 与 bean 后处理器对应的是, Spring 还提供了容器后处理器。容器后处理器在容器实 例化结束后,对容器进行额外的处理。 容器后处理器必须实现BeanFactoryPostProcessor接口。实现该接口必须实现一个方 法,该方法的方法体是对BeanFacto可所做的定制: void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) 类似于 BeanPostProcessor, ApplicationContext 可自动检测到 BeanFactoryPost Processor,然后作为容器后处理器注册,但若使用BeanFactory 作为容器,则必须手动 注册。看如下bean,该 bean 实现了 BeanFactoryPostProcessor接口: public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor public void postProcessBeanFactory (ConfigurableListableBeanFactory beanFactory) throws BeansException II 仅打印了一行代码 System.out.println("程序对 Spring 所做的 BeanFactory的初始化没有意见..."); 将该 bean 作为普通 bean 部署在容器中,然后使用 ApplicationContext 作为容器, 此时,容器会自动调用 BeanFactory PostProcessor 来处理 BeanFactory 。程序执行结果 如下: [java] 程序对 Spring 所做的 BeanFactory 的初始化没奋意见.. . Spring 己提供很多容器后处理器,主要包括以下几种。 • PropertyPlaceholderConfigurer: 属性占位符配置器。 • PropertyOverrideConfigurer: 另一种属性占位符配置器。 322…E 5.10.1 属性占位符配置器 PropertyPlaceholderC onfigurer 是 BeanFactoryPostProcessor 的实现类,它可用于读取 Java Properties 文件中属性,然后插入BeanFactory 定义中。通过使用 PropertyPlaceholder Configurer,可以将 Spring 配置文件的某些属性放入属性文件中配置,从而可以修改属 性文件。而修改 Spring 配置时,无须修改BeanFactory 的主 XML 定义灭件(比如说数据 库的 urls 、用户名和密码)。 使用 BeanFactoryPostProcessor 在修改某个部分的属性时,可以无须打开Spring 配置 文件,从而保证不会将新的错误引入Spring 配置文件。 看下面的配置文件: <1-- Spr工 ng 配置文件的根元素一〉 dbconn.properties dbconn.properties <'…配置本地的 DBCP 数据源一〉 容器自动注册propertyOverriderbean,读取 dbconn.properties 文件中的属性,并用于 覆盖目标 bean 的属性。其配置文件中dataSourcebean 的属性完全是随意输入的,最终被 属性文件的配置覆盖,其属性文件如下: dataSource.driverClassName=com.mysql.jdbc.Driver dataSource.url=jdbc:mysql://wonder:3306/j2ee dataSource.username=root dataSource.password=32147 324句伽制川…『川巾问i阳问叫n咱g 注意属性文件的格式必须是: beanName.property=value 也就是说, dataSource 必须是容器中真实存在的bean 名,否则程序将出错。 注意:仅仅察看XML 定义文件,程序无法知道BeanFactory 定义是否被覆盖;当有 多个 PorpertyOverrideConfigurer对同一个 bean 属性定义了覆盖,则最后一个覆盖有效。 5.11 与容器交互 Spring 容器本质上是一个高级工厂,负责产生 bean 实例,并管理 singleton bean 的 生命周期。 bean 处于容器管理下,通常无须访问容器,一样可以接受容器的注入,依赖 关系通常由容器动态注入,而无须 bean 主动请求。容器通常有两种表现形式: • BeanFactory 。 • ApplicationContext 。 当然,程序中也可显式地获得容器本身,如需获得容器本身,可采用如下代码。 获得 BeanFactory 容器: InputStream is = new File工 nputStream("bean. xml" ); II 以指定的文件输入流 is ,创建 Resource 对象 工 nputStreamResource isr = new 工 nputStreamResource(is); II 以 Resource 对象作为参数,创建 Bea口Factory 的实例 BeanFactory factory = new XrnlBeanFactory(isr); 获得 ApplicationContext容器: ApplicationContext ctx = new FileSystemXrnlApplicationContext("bean.xml"); 5.11.1 工厂 bean 简介与配置 通常情况下, Spring 容器担任工厂角色, bean 无须自己实现工厂模式,仅需要提供 一个工厂方法,用来返回其他 bean 实例。但少数情况下,容器中的 bean 本身就是工厂, 其作用是产生其他 bean 实例。 工厂 bean 的配置和其他 bean 的配置区别在于其产品 bean 的配置。在如下的配置文 件中配置了一个工厂 bean: <'一指定 Spring 配置文件的 dtd> 对主程序部分进行简单测试,将直接实例化获得BeanFactory 与通过 Chinese bean 获得 BeanFactory 进行比较,其主程序如下: publ 工 c class SpringTest { public static void main(String[] args)throws Exception II 在当前路径下搜索bean.xml 文件,实例化文件输入流 InputStream 工 s = new FileInputStream("bean.xml"); /I以指定的文件输入流坷,创建Resource对象 InputStreamResource isr =口ew I 口putStreamResource(is); II 以 Resource对象作为参数,创建BeanFactory的实例 Xrn lBeanFactory factory = new Xrn lBeanFactory(isr); Chinese p = (Chinese)factory.getBean("ch 工 nese") ; /I打印出 Chinese 实例获得的 BeanFactory System.out.println(p.getFactory()); /I比较两种方法获得的BeanFactory System.out.println(factory -- p.getFactory()); 程序运行结果如下: [java] org.springframework.beans.factory.xml. XrnlBeanFactory defining beans [chinese]; root of BeanFactory hierarchy [java] true 实现 BeanFactoryAware 接口,可让 bean 拥有了访问容器的能力,但污染了代码, 导致代码与 Spring 接口糯合在一起。因此,建议不要直接访问容器。 注意:一般情况下, bean 无须访问容器本身, bean 的依赖关系由容器负责注入,在 运行过程中无须感受到容器的存在。 5.11.4 使用 BeanNameAware 回调本身 如果某个 bean 需要访问配置文件中本身的 id 属性,则可以使用 BeanNameAware 接 口,该接口提供了回调本身的能力。实现该接口的 bean ,能访问到本身的 id 属性。该接 330句 ring 介绍 E 口提供一个方法: void setBeanName(String name) 。 该方法的 name 参数就是 bean 的 id 。该方法在依赖关系设置之后,初始化回调 ( InitializingBean 的 afterPropertiesSet 方法,或者 init- method 指定的方法)之前被执行。 回调 setBeanName 方法可让 bean 获得自己的 id 。看如下代码: public class Chinese implements InitializingBean, BeanNameAware private String beanName; II 测试用初始化方法,该方法通过init-method属性确定为初始化方法 public void init() System.out.println("正在执行初始化方法 init...") ; II 实现 In 工 tial 工 zingBean 接口必须实现的方法,初始化方法的一种 public void afterPropertiesSet() throws Exception System.out.println("正在执行初始化方法 afterPropertiesSet. .."); 1** *实现 BeanNameAware接口必须实现的方法。 * @Param name bean 的 id. *1 public void setBeanName(String name) this.beanName = name; II 测试,打印出 bean id. System.out.println("回调 setBeanName方法 II + name).; 将该 bean 部署在容器中,与普通bean 的部署没有任何区别。在主程序中通过如下 代码测试: public class SpringTest { public static void rna工 n(String[] args)throws Exception ApplicationContext ctx = new FileSysternXmlApplicationContext("bean.xml"); Chinese p = (Chinese)ctx.getBean("chinese"); 执行结果如下: [java] 回调 setBeanName方法 chinese [java] 正在执行初始化方法 afterPropertiesSet. [java] 正在执行初始化方法 init.. . 5.12 ApplicationContext 介绍 ApplicationContext 是 BeanFactory 接口的子接口,它增强了 BeanFactory 的功能,处 于 context 包下。很多时候, ApplicationContext 允许以声明式方式操作容器,无须手动 331轻量级 J2EE 企业应用实战一一Struts+Spring+Hibernate 整合开发 创建。可利用如 ContextLoader 的支持类,在 Web 应用启动时自动创建 Application Context 。当然,也可以采用编程方式创建 ApplicationContexto context 的基础是 ApplicationContext 接口,它继承 BeanFactory 接口,并提供 BeanFactory 所有的功能。为了以一种更加面向框架的方式工作, context 包使用分层和 有继承关系的上下文类,包括以下几种: • MessageSource ,提供国际化支持。 ·资源访问,比如 URL 和文件。 ·事件传递。 ·载入多个配置文件。 ApplicationContext 包括 BeanFactory 的全部功能,因此建议优先使用 ApplicationContext 。 除非对于某些内存非常关键的应用,才考虑使用 BeanFactory 。 5.12.1 国际化支持 ApplicationContext 接口继承 MessageSource 接口,因此具备国际化功能。下面是 MessageSource 接口中定义的三个用于国际化的方法: • String getMessage (String code, 0均 ect[] args, Locale loc) 。 • String getMessage (String code, Object[] 缸gs , String default, Locale loc) 。 • String getMessage(MessageSourceResolvable resolvable, Locale locale) 。 ApplicationContext 也通过这三个方法,完成消息的国际化。在 ApplicationContext 加载时,自动查找在 context 中 MessageSource bean 。该 bean 的名字必须是 MessageSource。 一旦找到这个 bean ,上述三个方法的调用被委托给MessageSource。如果没有找到该 bean , ApplicationContext 会查找其父定义中的 MessageSource bean 0 如果找到,它将被作为 MessageSource使用。如果无法找到MessageSource bean ,则将会实例化空的 StaticMessage Source bean ,该 bean 能接受上述三个方法的调用。 Spring 的国际化通常采用 ResourceBundleMessage Source 类。看下面配置文件: <'一 Spring 配置文件的根元素一〉 message 然后给出如下两份资源文件: 第一份,英文的资源文件,文件名:message_en.properties。 hello=welcome, {O} now=now is :{l} 第二份,中文的资源文件,文件名:message_zh. properties 。 hello= 欢迎你, {O} now= 现在时间是: {1} 当然,应使用 native2ascii 工具将这份资源文件国际化,命令如下: native2ascii message_zh.properties message_zh_l.properties 删除 message_zh.properties 文件,将 message_zh_l.properties文件重命名为 message_ zh. properties 。此时,程序拥有了两份资源文件,可以自适应英语和汉语的环境。主程序 部分如下: public class SpringTest { public static void main(String[] args)throws Exception { II 实例化 ApplicationContext ApplicationContext ctx = new FileSystemXrnlApplicationContext("bean.xml"); II 创建参数数组 String[] a = {"读者" }; II 使用 getMessage 方法获取本地化消息。 Locale 的 getDefault 方法用来返回计算机 II 环境的 Locale String hello = ctx.getMessage("hello" , a , Locale.getDefault()); Object[] b = {口 ew Date()}; String now = ctx.getMessage("hello" , b , Locale.getDefault()); II 打印出两条本地化消息 System.out.pri口tln(hello) ; System.out.println(now); 程序的执行结果会随环境不同而改变,在简体中文的环境下,执行结果如下: [java] 欢迎你,读者 [java] 欢迎你, 06-5-8 下午 3:34 英文环境下,执行结果如下: [java] welcome,读者 [java] welcome, 5/8/06 3:53 PM 当然,即使在英文环境下,"读者"这个词都无法变成英文,因为"读者"是写在程 序代码中,而不是从资源文件中获得。 333轻量级 J2EE 企业应用实战 Struts+Spring+Hibernate 整合开发 5.12.2 事件处理 通过 ApplicationEvent 类和 ApplicationListener 接口,可实现 ApplicationContext 的 事件处理。如果容器中有一个 ApplicationListener bean ,每当 ApplicationContext 发布 ApplicationEvent 时, ApplicationListener bean 将自动响应,这是标准的观察者模式。 Spring 的事件框架有如下两个重要成员。 • ApplicationEvent: 容器事件,必须由 ApplicationContext 发布。 • ApplicationLi stener: 监听器,可由容器中的任何监昕器 bean 担任。 以下是个简单的容器事件类,该类继承 ApplicationEvent 。代码如下: public class EmailListEvent extends ApplicationEvent { public EmailListEvent(Object source) { super(source); } public EmailListEvent(Object source, String address , String text) { super(source); th工 s.address = address; this.text = text; 下面是监昕器类,监听器类必须实现ApplicationListener 接口。实现该接口必须实 现一个方法: • void onApplicationEvent(ApplicationEvent event) 每当容器内发生任何事件时,此方法都会被触发,监听器类的配置文件如下: II 监昕器类,实现 ApplicationListener 接口 public class EmailNotifier 工mplements ApplicationListener II 实现 ApplicationListener接口必须实现的方法,该方法会在容器发生事件时自动触发 public void onApplicationEvent(ApplicationEvent evt) II如果事件是程序触发的事件 if (evt instanceof EmailEvent) II 发送 E-mail 通知.. . EmailEvent emailEvent = (EmailEvent)evt; System.out.println("需要发送邮件的接收地址 + emailEvent.address); System.out.println("需要发送邮件的邮件正文 " + emailEvent.text); } else II 容器内置事件不作任何处理 System.out.println("容器本身的事件 " + evt); 334制 n9 介绍~ 将监昕器配置在容器中,配置文件如下= 其主程序部分使用ApplicationContext的 publishEvent来发布事件: public class Spr工口 gTest public static void main(String[] args) { ApplicationContext ctx = new FileSystemXrnlAppl工 cat 工 onContext ( "bean. xml "); EmailEvent ele = new EmailEvent("hello" , "kongyeeku@gmail.com" , "th 工 s isatest"); ctx.publishEvent(ele); 程序执行结果如下: [java] 容器本身的事件 。 rg.springframework.context.event.ContextRefreshed Event[source=org.springframework.context.support.FileSystemXrnlAppl工 cationC ontext: display name [org.springframework.context.support.FileSystemXrn lApplicationContext; hashCode=76l5385]; startup date [Mon May 08 16: 54: 39 CST 2006]; root of context hierarchy] [java] 需要发送邮件的接收地址 kongyeeku@gmail.com [java] 需要发送邮件的邮件正文 this is a test 此时监昕器不仅监昕到程序发布的事件,同时也监昕到容器内置的事件。 注意:如果 bean 想发布事件,则 bean 必须获得其容器的引用,应通过实现 BeanFactoryAware 接口达到此目的。 Spring 提供了以下三个内置事件: • ContextRefreshedEven: ApplicationContext 容器初始化或刷新触发该事件。 • ContextClosedEvent: ApplicationContext 容器关闭时触发该事件。 • RequestHandledEvent: Web 相关的事件,只能应用于使用 DispatcherServlet 的 Web 应用。 5.12.3 Web 应用中自动加载 ApplicationContext 对于 Web 应用,不必在代码中手动实例化 ApplicationContext 。可通过 ContextLoader 声明式地创建 ApplicationContext 0 ContextLoader 有以下两个实现类: 335轻量级 J2EE 企业应用实战一-5t阳 ts+5pring+Hibernate 整合开发 • ContextLoaderListener 。 • ContextLoaderServlet 。 这两类功能相同,只是 listener 不能在 Servlet 2.2 兼容的容器中使用。根据 Servlet 2.4 规范, listener 会随 Web 应用启动时自动初始化,很多 Se凹 let 2.3 兼容的容器也提供该 功能。 1. 使用 ContextLoader Li stener 使用 ContextLoader Li stener 注册 ApplicationContext 的配置文件如下,注意:下面的 配置文件不是在 Spring 的配置文件中增加,而是在 web.x m1文件中增加。 contextConfigLocation <1 一 此处可以列出多个Spr工 ng 的 XML 配置文件→ /WEB-INF/daoContext.xml /WEB-工NF/applicationContext.xml org.spr工 ngframework.web.context.ContextLoader L 工 stener 2. 使用 ContextLoaderServlet 使用 ContextLoaderServlet注册 ApplicationContext的配置文件如下。同样,下面的 配置文件也不是在Spring 的配置文件中增加,而是在web.xm1文件中增加。 context <'一定义数据源,该bean 的 ID 为 dataSource--> com.mysql.jdbc.Driver jdbc:mysql://wonder:3306/j2ee lee/MyTest.hbm.xml org.hibernate.d工 alect. MySQLDialect update SessionFactory 由 ApplicationContext管理,井随着应用启动时自动加载,可以被处 于 ApplicaionContext管理的任意一个bean 引用,比如 DAOo Hibernate 的数据库访问需 要在 Session 管理下,而 SessionFactory 是 Session 的工厂。 Spring 采用依赖注入为DAO 对象注入 SessionFactory 的引用。 Spring 也提供了 Hibernate 的简化访问方式,Spring采用模板设计模式,提供Hibernate 访问与其他持久层访问的一致性。如果需要使用容器管理的数据源,则无须提供数据驱 动等信息,只需要提供数据源的JNDI 即可。对上文的SessionFactory 只需将 dataSource 341轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 的配置替换成 JNDI 数据源,并将原有的 dataSource Bean 替换成如下所示: java:comp/env/jdbc/myds 6.3 Spring 对 Hibernate 的简化 Hibernate 的持久层访问必须按如下步骤进行: (1)创建 Configuration 实例。 (2) 创建 SessionFactory 实例。 (3)创建 Session 实例。 (4) 打开事务。 (5) 开始持久化访问。 (6) 提交事务。 (7) 如果遇到异常,回滚事务。 (8) 关闭 Session 。 虽然可以采用类似于 HibernateUtils 工具类封装了部分过程,但依然不够简沽,需要 通过代码显式地打开 Session ,开始事务,然后关闭事务,最后关闭 Session 。而 Spring 提供更简单的方式操作持久层,无须显式地打开 Session ,也无须在代码中执行任何的事 务操作语句。 Spring 提供了 HibernateTemplate ,用于持久层访问,该模板类无须显示打开 Session 及关闭 Session 。它只要获得 SessionFactory 的引用,将可以智能打开 Session ,并在持久 化访问结束后关闭 Session ,程序开发只需完成持久层逻辑,通用的操作则由 HibernateTemplate 完成。 事务的处理,当然也可以采用编程式事务。 Spring 提供了编程式事务的支持。通常, 推荐使用声明式事务,使用声明式事务有如下优点: ·代码中无须实现任何事务逻辑,程序开发者可以更专注于业务逻辑的实现。 ·声明式事务不与任何事务策略藕合,采用声明式事务可以方便地在全局事务和局 部事务之间切换。 Spring 的声明式事务以 Spring 的 AOP 为基础,开发者可以不需要对 AOP 深入了解, 只需按本章后面部分配置声明式事务代理即可。 Spring 对 Hibernate 的简化,还得益于 Spring 异常处理策略。 Spring 认为:底层数 据库异常几乎都不可恢复,强制处理底层数据库几乎没有任何意义,但传统 JDBC 数据 库访问的异常都是 checked 异常,必须使用 try... 、 catch 块处理。 另外, Spring 包装了 Hibernate 异常,并转换到 DataAccessException 继承树内,所 342… i …整合 E 有 DataAccessException 全部是 runtime 异常,但并不强制捕捉。归纳起来, Spring 对 Hibernate 的简化主要有如下几个方面。 ·基于依赖注入的 SessionFactory 管理机制, SessionFactory 是执行持久化操作的核 心组件。传统 Hibernate 应用中, SessionFactory 必须手动创建,通过依赖注入, 代码无须关心 SessionFactory ,而它的创建和维护由 BeanFactory 负责管理。 ·更优秀的 Session 管理机制。 Spring 提供"每事务一次 Session" 的机制,该机制 能大大提高了系统性能,而且 Spring 对 Session 的管理是透明的,无须在代码中 操作 Session 。 ·统一的事务管理。无论是编程式事务,还是声明式事务, Spring 都提供一致的编 程模型,无须烦琐的开始事务、显式提交及回滚。如果使用声明式事务管理,可 将事务管理逻辑与代码分离,使事务可在全局事务和局部事务之间切换。 ·统一的异常处理机制。不再强制开发者在持久层捕捉异常,通常持久层异常被包 装成 DataAccessException 异常的子类,将底层数据库异常包装成业务异常,开发 者可以自己决定在合适的层处理异常。 • HibernateTemplate 支持类。 HibernateTempate 能完成大量 Hibernate 持久层操作, 这些操作大多只需要一些简单的代码即可实现。 6.4 使用 HibernateTemplate HibernateTemplate 可将 Hibernate 的持久层访问模板化,使用 HibernateTemplate 非 常简单。创建Hi bernateTemplate 实例后,注入一个 SessionFactory 的引用,就可执行持 久化操作。 SessionFactoyr 对象可通过构造参数传入,或通过设值方式传入。例如: II 获取 Spring 上下文 ApplicationContext ctx = new FileSystemXmlApplicationContext("bean.xml"); II 通过上下文获得Sess工 onFactory SessionFactory sessionFactory = (SessionFactory) ctx.getBean("sessionFactory") , HibernateTemplate 提供如下三个构造函数: • HibernateTemplateO 。 • HibernateTemplate(org.hibernate.SessionFactory sessionFactory)。 • HibernateTemplate(org.hibernate.SessionFactory sessionFactory, boolean a1lowCreate) 。 第一个构造函数:构造一个默认的HibernateTemplate 实例,因此,使用 HibernateTemplate 实例之前,还必须使用方法 setSessionFactory(SessionFactory sessionFactory)来为Hibernate Template 传入 SessionFactory 的引用。 第二个构造函数:在构造时已经传入 SessionFactory 引用。 第三个构造函数:其 boolean 型参数表明,如果当前线程己经存在一个非事务性的 Session ,是否直接返回此非事务性的 Session 。 对于在 Web 应用中,通常启动时自动力日载 ApplicationContext , SessionFactory 和 DAD 343轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 对象都处在 Spring 上下文管理下。因此无须在代码中显式设置,可采用依赖注入解藕 SessionFactory 和 DAO ,其依赖关系可通过配置文件来设置,如下所示: jdbc:mysql:!!wonder:3306!j2ee update 在 DAO 实现类中,可采用更简单的方式来取得HibemateTemplate的实例。代码如下: public class PersonDaoImpl implements PersonDao { 344… i …整合国 II 执行持久化操作的 HibernateTemplate HibernateTemplate ht; private SessionFactory sessionFactory; II 依赖注入 Sess工 onFactory 的必需的 setter 方法 public void setSessionFactory(SessionFactory sessionFactory) this.sessionFactory = sessionFactory; /I该方法用于完成H 工 bernateTemplate的初始化 private void setHibernateTemplate() if (ht ==口ull) ht = new HibernateTemplate(sessionFactory); 1** 食加载 Person 实例 * @param id 需要加载 Person 实例的主键值 * @return 返回加载的 Person 实例 *1 public Person get(int id) setHibernateTemplate(); return (Person)ht.get(Person.class , new Integer(id)); 1** *保存 Person 实例 * @param person 需要保存的 Person 实例 *1 public void save(Person person) setHibernateTemplate() ; ht.save(person) ; 1** *修改 Person 实例 * @param person 需要修改的 Person 实例 *1 public void update(Person person) setHibernateTemplate(); ht.update(person); 1** *删除 Person 实例 * @param id 需要删除的 Person id *1 public void delete(int id) setHibernateTemplate(); ht.delete(ht.get(Person.class , new Integer(id))); 1** *删除 Person 实例 * @param perso口需要删除的 Person 实例 *1 public void delete(Person person) 345轻量级 J2EE 企业应用实战一-St阳 ts+Spring+Hibernate 整合开发 setHibernateTemplate(); ht.delete(perso口) ; 1** *根据用户名查找 Person * @param name 用户名 * @return 用户名对应的全部用户 *1 public List findByPerson(String name) setHibernateTemplate(); return h t. find("from Person p where p.name like ?" , name); 1** *返回全部的 Person 实例 * @return 全部的 Person 实例 *1 public L 工 st findAllPerson() setHibernateTemplate(); return ht.findl"from Person "); 6.4.1 HibernateTemplate 的常规用法 HibernateTemplate 提供了非常多的常用方法来完成基本的操作,比如增加、删除、 修改及查询等操作, Spring 2.0 更增加对命名 SQL 查询的支持,也增加对分页的支持。 大部分情况下,使用 Hibernate 的常规用法,就可完成大多数DAO 对象的 CRUD 操作。 下面是 HibernateTemplate 的常用方法。 • void delete(Object entity): 删除指定持久化实例。 • deleteAll(Collection entities): 删除集合内全部持久化类实例。 • find(String queηString): 根据 HQL 查询字符串来返回实例集合。 • findByNamedQuery(String queryName): 根据命名查询返回实例集合。 • get(Class entityClass, Serializable id): 根据主键加载特定持久化类的实例。 • save(Object entity): 保存新的实例。 • saveOrUpdate(Object entity): 根据实例状态,选择保存或者更新。 • update(Object entity): 更新实例的状态,要求 entity 是持久状态。 • setMaxResults(int maxResults): 设置分页的大小。 下面是一个完整 DAO 类的源代码: public class PersonDAOlmpl implements PersonDAO { II 采用 log4j 来完成调试时的日志功能 pr 工 vate static Log log = LogFactory.getLog(NewsDAOHibernate.class); II 以私有的成员变量来保存SessionFactory" private SessionFactory sessionFactory; II 以私有变量的方式保存旧bernateTemplate private HibernateTemplate hibernateTemplate = null; II 设值注入 SessionFactory必需的 setter 方法 346Spring 与 Hibernate 的整合 EE public void setSessionFactory(SessionFactory sess工 onFactory) this.sess工 onFactory = sessionFactory; II 初始化本 DAO 所需的 HibernateTemplate public HIbernateTemplate getHibernateTemplate() II 首先,检查原来的hibernateTemplate实例是否还存在 if ( hibernateTemplate ==口ull) II 如果不存在,新建一个HibernateTemplate实例 hibernateTemplate = new HibernateTemplate(sessionFactory); return h工 bernateTemplate; II 返回全部人的实例 publ工 c List getPersons() II 通过 HibernateTemplate的 find 方法返回 Person 的全部实例 return getHibernateTemplate() .find("from Person"); 1** *根据主键返回特定实例 * @ return 特定主键对应的 Person 实例 * @ param 主键值 public News getNews(int person id) return (Person)getHibernateTemplate() .get(Person.class , new Integer(person id)); 1** * @ person 需要保存的 Person 实例 *1 public void savePerson(Person person) { getHibernateTemplate() .saveOrUpdate(person); 1** * @ param personid 需要删除 Person 实例的主键 * I public void removePerson(int person id) II 先加载特定实例 Object p = getHibernateTemplate() .1oad(Person.class , new 工 nteger(person id)); II删除特定实例 getHibernateTemplate() .delete(p); 6.4.2 Hibernate 的复杂用法 HibernateCallback HibernateTemplate 还提供了一种更加灵活的方式来操作数据库,通过这种方式可以 完全使用 Hibernate 的操作方式。 HibernateTemplate 的灵活访问方式是通过如下两个方法 完成的 z 347轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 • Object execute(HibernateCallback action) 。 • List execute(HibernateCallback action) 。 这两个方法都需要一个 HibernateCallback 的实例,该实例可在任何有效的 Hibernate 数据访问中使用。程序开发者通过HibernateCallback,可以完全使用 Hibernate 灵活的方 式来访问数据库,解决了 Spring 封装 Hibernate 后灵活性不足的缺陷。 HibernateCallback 是一个接口,该接口只有一个方法 doInHibernate(org.hibernate.Session session) ,该方法只 有一个参数 Session 。 通常,程序中采用实现 HibernateCallback 的匿名内部类来获取 HibernateCallback 的 实例,方法 doInHibernate 的方法体就是 Spring 执行的持久化操作。具体代码如下: public class PersonDaolmpl implements PersonDao { II 私有实例变量保存 Sess 工 onFactory private Sess工 onFactory sessionFactory; II 依赖注入必需的 setter 方法 public void setSessionFactory(SessionFactory sessionFactory) { this.sessionFactory = sessionFactory; 1** *通过人名查找所有匹配该名的Person 实例 * @param name 匹配的人名 * @return 匹配该任命的全部 Person 集合 *1 public List findPersonsByName(final String name) II 创建 HibernateTemplate实例 HibernateTemplate hibernateTemplate = new HibernateTemplate(this.sessionFactory); II 返回 HibernateTemplate的 execute 的结果 return (List) hibernateTemplate.execute( II 创建匿名内部类 new HibernateCallback() public Object do工 nHibernate(Session session) throws Hibernate Exception { II 使用条件查询的方法返回 List result = session.createCriteria(Person.class) .add(Restrictions.like(HnameH, name+ H 在 H) .list (); return result; .,)} 注意:在方法 doInHibernate 内可以访问 Session ,该 Session 对象是绑定到该线程的 Session 实例,该方法内的持久层操作与不使用 Spring 时的持久层操作完全相同,这保证 了在对于复杂的持久层访问时,依然可以使用 Hibernate 的访问方式。 348s… 6.5 Hibernate 的 DAD 实现 DAO 是J2EE 应用的重要组件,它隐藏了底层的数据库访问细节。 DAO 层也是J2 EE 应用分层中的重要分层,该层向上提供通用的数据访问接口。 通过 DAO 组件,可实现业务逻辑和数据库访问的分离,避免业务逻辑与具体的数 据库访问实现藕合。对于J2 EE 应用而言,数据库是相对稳定的部分,其 DAO 组件依赖 于数据库系统,提供数据库访问的接口,只要数据库没有重构,则 DAO 层通常无须 改写。 DAO 层也分隔了数据库与业务逻辑层,使业务逻辑层更加专注于业务逻辑的实现, 而无须理会持久层访问实现。 6.5.1 DAO 模式简介 DAO 模式是J2 EE 设计模式中的一种核心设计模式。图 6.1 是 DAO 模式的类图,该 类图表示了 DAO 模式的各个参与者之间的关系。 BusinessObject DataAccessObject encapsulates DataSourc冠 uses 、 eMρUW 噜t m-w 11lthut- 土 AU-h b\ 阳 呻\归\币、3 、 ku 、 nv 、 图 6.1 DAO 模式的类图 图 6.2 是 DAO 模式的顺序图,该顺序图则显示模式的各个参与者是如何互动的。 DAO 模式可以提供更好的解糯,将业务逻辑层与持久层访问技术分离,使业务逻辑 层无须关注底层数据库访问的实现。使用 DAO 模式主要有如下优势。 • DAO 模式可抽象出数据访问方式,在 BO 访问数据源时,完全感觉不到数据源的 存在。软件工程里面有一条很重要的法则,就是一个对象对其他对象的了解越少 越好,了解越少就意味着依赖越少,可复用性越高。 • DAO 将数据访问集中在独立的一层。因为所有的数据访问都由 DAO 代理,这层 独立的 DAO 就将数据访问的实现与系统的其余部分剥离,将数据访问集中使得 系统更具可维护性。 • DAO 还降低了 BO 层的复杂程度。由 DAO 管理复杂的数据访问,从而简化了 BO 。 所有与数据访问实现有关的代码(如 SQL 语言等)都不用写在 BO 里,从而使 349轻量级 J2EE 企业应用实战→一Struts+Spring+Hibernate 整合开发 BO 可以集中精力处理业务逻辑,提高了代码的可读性和生产率。 • DAO 还有助于提升系统的可移植性。独立的 DAO 层使得系统能在不同的数据库 之间轻易切换,底层的数据库实现对于 BO 来说是不可见的。数据移植时影响的 仅仅是 DAO 层,切换不同的数据库并不会影响 BO ,因此提高了系统的可复用性。 DataSource厂τAOI I I 悔自 BusinessObiect 『一一 2 阳 D创!!....e悔。 主翌E主且单单l 2.2:Create 4国~ U2.3 :Return Object lY- w & T 5.1 :Get Property 5.2:Get Prooertv 5.3:Set Data VLV­M-n-严 -m-a u 一百-创 m-R-Dtie-e-eQJM-QU-CJU--;3-AZζJ 回 白 e..- DAO 模式的顺序图 Spring 对 Hibernate 的 DAO 实现提供了良好的支持,主要有如下两种方式: .继承 HibernateDaoSupport 的实现 DAO 。 ·基于 Hibernate 3.0 实现 DAO 。 不管采用哪一种实现,这种 DAO 对象都能极好地融合到 Spring 的 ApplicationContext 中,遵循依赖注入模式,从而提高解稿。 图 6.2 继承 HibernateDaoSupport 实现 DAO6.5.2 Spring 为 Hibernate 的 DAO 提供了工具类一-HibernateDaoSupport 。该类主要提供 如下两个方法来方便 DAO 的实现: • public final HibernateTemplate getHibernateTemplateO 。 • public final void setSessionFactory(SessionFactory sessionFactory) 。 其中, setSessionFactory 方法用来接收 Spring 的 ApplicationContext 依赖注入,可接 收配置在 Spring 的 SessionFactory 实例; getHibernateTemplate 方法则用来根据刚才的 350Spring 与 Hibernate 的整合 SessionFactory 产生 Session ,最后生成 HibemateTemplate 来完成数据库访问。 下面的代码是对利用 HibemateDaoSupport 的 DAO 实现: public class PersonDaoImpl implements PersonDao { 食加载 Person 实例 * @param id 需要加载的 Person 实例的主键值 * @return 返回加载的 Person 实例 public Person get(int id) setH工 bernateTemplate(); return (Person)getHibernateTemplate() .get(Person.class, new Integer(id)); *保存 Person 实例 * @param person 需要保存的 Person 实例 publ工 c va工 d save(Person perso口) setH工 bernateTemplate(); getHibernateTemplate() .save(person); /** *修改 Person 实例 * @param person 需要修改的 Person 实例 */ public void update(Person person) setHibernateTemplate(); getHibernateTemplate() .update(person); /** *删除 Person 实例 * @param 工 d 需要删除的 Person id */ publ工 c void delete(int id) setHibernateTemplate(); getHibernateTemplate() .delete (getHibernateTemplate () . get(Person.class , new 工 nteger (id) )); /** *删除 Person 实例 * @param perso且需要删除的 Person 实例 */ public void delete(Person person) setH工 bernateTemplate(); getHibernateTemplate() .delete(person); /** *根据用户名查找Person * @param name 用户名 * @return 用户名对应的全部用户 国 351轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 public List findByPerson(String name) { setH 工 bernateTemplate(); return getHibernateTemplate () . find ( "from Person p where p. name like?" , name) ; /** *返回全部的 Person 实例 * @return 全部的 Person 实例 */ public List findAllPerson() setHibernateTemplate(); return getHibernateTemplate() .find("from Person "); 与前面的 PersonDAOImpl 对比,会发现其代码大大减少。事实上,DAO 的实现依 然借助于 HibernateTemplate的模板访问方式。该DAO 的配置必须依赖于SessionFactory, 具体的配置如下: <1-- Spring 配置文件的根元素是beans--> <' 定义数据源,该bean 的工D 为 dataSource--> com.mysql.jdbc.Driver jdbc:mysql://wonder:3306/j2ee root pass <'一以下用来列出所有的PO 映射文件一〉 lee/Person.hbm.xml <'一指定Hibernate的连接方法--> org.hibernate.dialect. 352… i …整合~ MySQLDialect update corn.rnysql.jdbc.Driver jdbc:rnysql://wonder:3306/j2ee root lee/MyTest.hbm.xml org.hibernate.dialect. MySQLDialect update <'… HibernateTransactionManager bean 需要依赖注入一个 SessionFactory bean 的引用--> 下面是采用 TransactionTemplate和 HibemateTemplate的事务操作代码: public class TransactionTest { public static void main(String[] args) { /I因为并未在 web 应用中测试,故需要手动创建 Spring 的上下文 final ApplicationContext ctx = new FileSystemXrnlApplicationContext("bean.xml"); II 获得 Spring 上下文的事务管理器 PlatformTransactionManager transactionManager= (PlatformTransactionManager)ctx.getBean("transactionManager"); final SessionFactory sessionFactory = (SessionFactory)ctx.getBean("sessionFactory"); /I以事务管理器实例为参数,创建TransactionTemplate对象 TransactionTemplate tt = new TransactionTemplate(transactionManager); II 设置 TransactionTemplate的事务传播属性 tt.setPropagationBehavior (TransactionDef 工nition.PROPAGATION_REQUlRED); II执行 TransactionTemplate的 execute方法,该方法需要TransactionCallback实例 356……整合国 tt.execute(new TransactionCallbackW工 thoutResult() II 采用 TransactionCallbackWithoutResult匿名内部类的形式执行 protectedvoid dolnTransactionWithoutResult(TransactionStatus ts) try { 以 SessionFactory 实例为参数创建 HibernateTemplate HibernateTemplate hibernateTemplate = new HibernateTemplate(sessionFactory); MyTestpl =丑ew MyTest ("Jack"); II 保存第一个实例 hibernateTemplate.save(pl); II 让下面的数据库操作抛出异常即可看出事务效果。前面的操作也 II 不会生效 MyTestp2 = new MyTest ("Jack"); II 保存第二个实例,可将Person 的 name 属性设为标识属性,并 II 引起主键重复的异常,可看出前一条记录也不会加入数据库中 hibernateTemplate.save(p2); } catch (Exception e) { ts.setRollbackOnly(); .,)} 查看数据库的 mytable 表,该表中没有任何记录(如果没有采用事务,第一条记录 应该可以进去。而两次保存记录放在 doInTransactionWithoutResult 方法中执行) ,因为该 方法的方法体具有事务性,该方法的数据库操作要么全部生效,要么全部失效。由于第 二条记录违反了数据库的主键约束,因此,记录全部失效。 6.6.2 声明式事务管理 通常建议采用声明式事务管理。声明式事务管理的优势非常明显,代码中无须关注 事务逻辑,由 Spring 声明式事务管理负责事务逻辑:声明式事务管理无须与具体的事务 逻辑糯合,可以方便地在不同事务逻辑之间切换。 声明式事务管理的配置方式通常有如下四种。 ·使用 TransactionProxyFactoryBean 为目标 bean 生成事务代理的配置。此方式最传 统,但配置文件膝肿,难以阅读。 ·采用 bean 继承的事务代理配置方式比较简沽,但依然是增量式配置。 ·使用 BeanNameAutoProxyCreator ,根据 bean name 自动生成事务代理的方式,这 是直接利用 Spring 的 AOP 框架配置事务代理的方式,需要对 Spring 的 AOP 框架 有所理解,但这种方式避免了增量式配置,效果非常不错。 • DefaultAdvisorAutoProxyCreator: 这也是直接利用 Spring 的 AOP 框架配置事务代 理的方式,效果也非常不错,只是这种配置方式的可读性不如第三种方式。 357轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 1. 利用 Transaction ProxyFactoryBean 生成事务代理 采用这种方式的配置时,其配置文件的增加非常快,每个 bean 有需要两个 bean 配 置一个目标,另外还需要使用 TransactionProxyFactoryBean 配置一个代理 bean 。 这是一种最原始的配置方式,下面是使用 TransactionProxyFactoryBean 的配置文件: <'一 Spring 配置文件的文件头,包含DTD 等信息-> com.mysql.jdbc.Driver jdbc:mysql://localhost:3306/spr工 ng root <1-- 定义数据库密码一〉 <1-- 以下用来列出所有的PO 映射文件一〉 Person.hbm.xml org.hibernate.dialect. MySQLDialect update <' 设置事务属性 > <' 采用嵌套 bean 配置目标 bean--> 359轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 jdbc:mysql://localhost:3306/spring root <1-- 定义数据库密码一〉 32147 PROPAGATION_REQU工 RED , readOnly PROPAGATION_REQUIRED <'一定义数据源一〉 com.mysql.jdbc.Driver <1-- 定义数据库url--> jdbc:mysql://localhost:3306/spring root <1-- 定义数据库密码一〉 32147 <1-- 定义 SessionFactory必须注入 DataSource--> Person.hbm.xml org.hibernate.dialect. MySQLDialect update <1-- 定义事务管理器,使用适用于Hibernate 的事务管理器-→ personDao <11 工 st> transactionlnterceptor <'…此处可增加其他新的工nterceptor ←> <11 工 st> jdbc:mysql://localhost:3306/spring <1 一定义数据库用户名--> root 32147 <1 …以下用来列出所有的PO 映射文件一〉 Person.hbm.xml org.hibernate.dialect. MySQLDialect update PROPAGATION_REQUIRED 365轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 PROPAGAT工 ON_REQUIRED, readOnly PROPAGAT工 ON_REQUIRED <' 定义事务 Advisor--> org.spr工 ngframework.web.context.ContextLoader Listener 如果没有 contextConfigLocation 指定配置文件,则 Spring 自动查找 application Context. xrnl 配置文件。如果有contextConfigLocation,则利用该参数确定的配置文件。 该参数指定的一个字符串, Spring 的 ContextLoaderListener 负责将该字符串分解成多个 配置文件,逗号","、空格" "及分号";"都可作为字符串的分割符。 如果既没有 applicationContext. xrnl 文件,也没有使用 contextConfigLocation参数确 定配置文件,或者contextConfigLocation确定的配置文件不存在。都将导致Spring 无法 369轻量级 J2EE 企业应用实战 Struts+Spring+Hibernate 整合开发 加载配置文件或无法正常创建 ApplicationContext 实例。 Spring 根据 bean 定义创建 WebApplicationContext 对象,并将其保存在 Web 应用的 ServletContext 中。大部分情况下,应用中的 bean 无须感受到 ApplicationContext 的存在, 只要利用 ApplicationContext 的 JoC 即可。 如果需要在应用中获取 ApplicationContext 实例,则可以通过如下方法获取1 WebApplicat工 onContext ctx = WebApplicationContextUtils.getWebAppl 工 cationContext(servletContext); 下面是采用 Servlet 获取 ApplicationContext 的完整源代码: public class SpringTestServlet extends HttpServlet IIServlet 的响应方法。 public void serv工 ce(HttpServletRequest request, HttpServletResponse response) throws ServletExceptio口, java.io.IOException II 获取 Servlet 的 ServletContext对象 ServletContext sc = getServletContext(); II 使用 WebApplicationContextUtils类获得 Applicat工 onContext WebApplicationContext ctx = WebApplicationContextUtils. getWebApplicationContext(sc); II 获取 Servlet 的页面输出流 PrintWriter out = response.getWriter(); II 将 ApplicationContext对象输出 out.println(ctx); 可在程序里手动获取ApplicationContext对象,然后直接输出到Servlet 的响应。将 看到 ApplicationContext加载了 web.xml 文件中指定的两个配置文件。 该 Se凹let 执行的效果如图 7.1 所示。 。rg. springfrmoework. web. context. 5叩·port. Xml WebApplicationConte:r: t: display n8l'田 [Root WebApplicationContextJ; start叩 d.t c [S.t Aug 19 22:22:04 C町 2006]; root of context hierarchy; config locations [IWEB-INI'/也oContext. xm1.IWEB-INF/applicationContext. xml] 图 7.1 Contex tLo aderLi stener 创建 Spring 容器的测试 7.1.2 采用 load-on-startup Servlet 创建 AppiicationContext 如果容器不支持Li stener ,则只能使用 load-on-startup Servlet 创建 ApplicationContext 实例,下面的容器都不支持Li stener: • BEA WebLogic up to 8.1 S P2。 370…叫整合 E • IBM WebSphere S.x 。 • Oracle OC4J 9.0 .3。 Spring 提供了一个特殊的 Se凹 let 类: ContextLoaderServlet。该 Servlet 在启动时,会 自动查找 WEB-IN日下的 applicationContext. xml 文件。 当然,为了让 ContextLoaderServlet 随应用启动而启动,应将此 Servlet 配置成 load-on-startup 的 Servleto load-on-startup 的值小一点比较合适,因为要保证 Application Context 优先创建。如果只有一个配置文件,并且文件名为 applicationContext. xml ,则在 web.xml 文件中增加如下代码即可: context org.springframework.web.context.ContextLoaderServlet l 该如何let 用于提供"后台"服务,作为容器管理应用中的其他bean,不需要响应客 户请求,因此无须配置servlet-mapping。 如果有多个配置文件,同样可以使用元素来确定多个配置文件。事 实上,不管是ContextLoaderServlet ,还是 ContextLoaderListener,都依赖于 ContextLoader 创建 ApplicationContext实例。在 ContextLoader代码中有如下代码: String configLocatio口 2 日 ervletContext.getInitParameter(CONFIG_LOCAT工 ON一 PARAM) ; if (configLocation ! = null) { wac.setConfigLocations(StringUtils.tokenizeToStr 工 ngArray(configLocatio口, ConfigurableWebApplicationContext.CONF 工 G_LOCATION_DELIMITERS)); 其中 CONFIG_LOCATION_PARAM 是该类的常量,其值为 contextConfigLocation 。 可看出, ContextLoader 首先检查 servletContext 中是否有 contextConfigLocation 的参数, 如果有该参数,则加载该参数指定的配置文件。带多个配置文件的 web且nl 文件如下: <'一 确定多个配置文件一> contextConfigLocation <'一 struts 配置文件的根元素一〉 <' 定义 action 部分,所有的action 都放在 action-mapping元素里定义--> 修改后的 struts-config.xml文件中加载了国际化资源文件,其配置Struts 的 action 不 需要 class 属性,也可完成ApplicationContext的创建。 由于程序没有使用web.xml 文件加载 Spring 容器实例,因此无须增加ContextLoader 的配置。仅仅增加了 Struts 的标签库配置,主要用于程序国际化等方面。在Struts 标签 库配置中增加如下代码即可: <' 配置 bean 标签-→ /tags/struts-bean /WEB-INF/struts-bean.tld /tags/struts-html /WEB-INF/struts-html.tld /tags/struts-logic /WEB-INF/struts-logic.tld Struts 的 plug-in 配置部分明确指出,Spring 的配置文件有两个,applicationContext.xml 和 acton-servle t. xml 。其实完全可以使用一个配置文件,通常习惯将 action bean 单独配置 在表现层的 context 内。 acton-servle t. xml 用于配置表现层 context ,其详细配置信息如下: 由于每次请求时,都应该启动新的action 来处理用户请求,因此应将action bean 配 置成 non-singleton行为。 注意: ActionServlet转发请求时,是根据bean 的 name 属性,而不是id 属性。因此, 此处确定的 name 属性应与 Struts 的 action 属性相同。 377轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 applicationContext. xml 只有一个 bean 配置(配置了 vb bean) 。其详细配置如下: <1 一指定 Spring 配置文件的 dtd> <1- 定义 action 部分,所有的action 都放在 action-mapping元素里定义-> DelegatingActionProxy 接受 ActionServlet 转发过来的请求,然后转发给 ApplicationContext管理的 bean,这是典型的链式处理。 通过配置文件可看出,在struts-config.xml文件中配置了大量DelegatingActionProxy 实例, Spring 容器中也配置了同名的Action。即 Struts 的业务控制器分成了两个部分: 第→个部分是 Spring 的 DelegatingActionProxy,这个部分没有实际意义,仅仅完成转发: 第二个部分是用户的Action 实现类,负责实际的处理工作。 这种策略的性能比前一种的策略要差一些,因为需要多创建一个DelegatingActionProxy 实例。而且在J2EE 应用中的 Action 非常多,这将导致需要大量创建DelegatingActionProxy 实例,在使用一次之后,要等待垃圾回收机制回收,从而降低了其性能。 DelegatingActionProxy 的执行效果如图7.6 所示。 图 7.6 DelegatingActionProxy整合策略登录成功的效果 7.4 使用 ActionSupport 代替 Action 另外,还有一种方法可以用于 Spring 与 Struts 的整合:让 Action 在程序中手动获得 ApplicationContext 实例。在这种整合策略下, Struts 的 Action 不接受 IoC 容器管理,使 Action 的代码与 Spring API 部分稿合,但造成代码污染。 这种策略也有其好处:代码的可读性非常强,在 Action 的代码中显式调用业务逻辑 组件时,无须等待容器注入。 在 Action 中访问 ApplicationContext 有以下两种方法: ·利用 WebApplicationContextUtils 工具类。 ·利用 ActionSupport 支持类。 WebApplicationContextUtils 可以通过 ServletContext 获得 Spring 容器实例。而 ActionSupport 类则提供一个更简单的方法: getWebApplicationContextO ,该方法用于获 取 ApplicationContext 实例。 Spring 扩展了 Struts 的标准 Action 类,可在其 Struts 的 Action 后加上 Support , Spring 的 Action 有如下几种: • ActionSupport 。 • DispatchActionSupport 。 382…tr峭的整合国 • LookupDispatchActionSupporto • MappingDispatchActionSupport 0 下面分别给出利用 ActionSupport 的示例代码: 新的业务控制器,继承 Spring 的 ActionSupport 类 public class LoginAction extends ActionSupport II 依然将 ValidBean作为成员变量 pr工vate ValidBean vb; II 构造器,注意:不可在构造器中调用getWebApplicationContext()方法 public LoginAction() II完成 ValidBean的初始化 public ValidBean getVb() return (ValidBean)getWebAppl工 cationContext() . getBean ("vb") ; II 必须重写该核心方法,该方法 actionForm 将表单的请求参数封装成值对象 public ActionForward execute(ActionMapping mapping , ActionForm form, HttpServletRequest request , HttpServletResponse response) throws Exception Ilform 由 ActionServlet转发请求时创建,包装了所有的请求参数 LoginForm loginForm = (LoginForm)form; II 获取 username请求参数 String username = loginForm.getUsername(); II 获取 pass 请求参数 String pass = loginForm.getPass(); II 下面作服务器端的数据校验 String errMsg = II 判断用户名不能为空 if (username -- null II username.equals("")) errMsg :t-= "您的用户名丢失或没有输入,请重新输入 II; II 判断密码不能为空 else if (pass == null II pass.equals ("") ) errMsg += "您的密码丢失或没有输入,请重新输入" ; II 如果用户名和密码不为空,则调用业务组件 else IIvb 是业务逻辑组件,通过上面的初始化方法获得 if (getVb() .valid(username, pass)) return mapping.findForward("welcome"); else errMsg =什盔的用户名和密码不匹配II i II 判断是否生成了错误信息, if (errMsg != null && !errMsg.equals("")) 383轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 II 将错误信息保存在 request 里,则跳转到 input 对应的 forward 对象 request. setAttribute ("err" , errMsg); return mapping.f工 ndForward( "工 nput"); else II 如果没有错误信息,跳转到welcome 对应的 forward对象 return mapping. findForward ("welcome") ; 在这种整合策略下,表现层的控制器组件不再接受 Joe 容器管理,因此没有了控制 器 context 。应将原有的 action-serv le t. xml 文件删除,井修改 plug-in 元素,但不要加载该 文件。另外,还要修改其 action 配置,将 action 配置的 type 元素修改成实际的处理类。 这种整合策略也有个好处,提高了代码的可读性,对传统 Struts 应用开发的改变很小, 容易使用。 将该 Action 部署在 struts-config.xml 中,此时 Struts 将负责创建该 Action 。 struts-config.xml 文件的源代码如下: <' 定义了一个 formbean. 确定 formbean 名和实现类一〉 <'一 定义 action 部分,所有的 action 都放在 action-mapping元素里定义 > 该配置文件中的业务逻辑组件由Spring 容器负责实现,而ActionSupport 能够先定 位 Spring 容器,然后获得容器的业务逻辑组件。这种整合策略与前面两种整合策略的执 行效果完全相同。 从代码中我们可以看出,在这种整合策略下,业务控制器再次退回到Struts 起初的 设计,仅由 struts-config.xml 中 Action 充当,可以只创建实际的Action 实例,而避免了 如 DelegatingActionProxy整合策略的性能低下,但这种整合策略的代价是污染了代码。 7.5 实用的整合策略 下面介绍一种实用的整合策略,采用这种整合策略的 Action 不仅可以避免直接与 Spring API 稿合,也可以避免在配置文件中大量地重复配置,还可以避免创建多余的 DelegatingActionProxy 实例。 这种整合策略其实相当简单,其思路是将获取业务逻辑组件的方式放在父类中实现, 而其余的 Action 则从父类中获取。 下面是 BaseAction 的源代码: IIBaseAction 继承 ActionSupport 类 public class BaseAction extends ActionSupport { 1** *根据 beanName 获取容器中的bean 实例 * @param beanName 容器中的 bean 名 * @return 返回 Spring 容器中的 bean *1 public Object getBean(String beanName) return getWebApplicationContext() .getBean(beanName); 该父类仅仅将ActionSupport的方法再次包装,但产生的优势非常明显,至少可以在 实际的业务 Action 中避免调用 getWebApplicationContext方法。 下面是业务 Action 的源代码: II 新的业务控制器,继承BaseActiont类 public class LoginAction extends BaseAct工 on II 依然将 ValidBean作为成员变量 private ValidBean vb; 385386 轻量级 J2EE 企业应用实战-一-Struts+Spring+Hibernate 整合开发 II 构造器,注意:不可在构造器中调用 getWebApplicationContext() 方法 publ 工 c LoginAction () II 完成 ValidBean的初始化 public ValidBean getVb() return (ValidBean) getBean ("vb") ; II 必须重写该核心方法,该方法 actionForm 将表单的请求参数封装成值对象 public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletReql山 est request , HttpServletResponse respo口se) throws Exception Ilform 由 ActionServlet转发请求时创建,包装了所有的请求参数 LoginForm log工 nForm = (LoginForm)form; II 获取 username请求参数 String username = loginForm.getUsername(); II 获取 pass 请求参数 String pass = loginForm.getPass(); II 下面作服务器端的数据校验 String errMsg = II 判断用户名不能为空 工 f (username == null II username.equals("")) errMsg += "您的用户名丢失或没有输入,请重新输入11; II 判断密码不能为空 else if(pass -- null II pass.equals("")) errMsg += "您的密码丢失或没有输入,请重新输入II; II 如果用户名和密码不为空,则调用业务组件 else IIvb 是业务逻辑组件,通过上面的初始化方法获得 if (getVb() .valid(username, pass)) return mapping. findForward ("welcome") ; else errMsg = "您的用户名和密码不匹配 11; II 判断是否生成了错误信息, if (errMsg != null && lerrMsg.equals('''')) II将错误信息保存在request 里,则跳转到 input 对应的 forward 对象 request.setAttribute("err" , errMsg); return mapping.findForward("input"); else II 如果没有错误信息,跳转到welcome 对应的 forward 对象 return mapping. findForward ("welcome") ;9 ~ Stru怡整合 E 从代码中可看出,在实际的业务控制器 Action 中,完全从 Spring 的 API 中分离出 来,从而可以避免代码污染。 另外,还有一个最大的好处:实际的业务 Action 并没有与任何的整合策略糯合。假 如需要在不同的整合策略之间切换,其业务 Action 完全不需要改变。 假设需要将整合策略切换到利用 Spring Ioe 特'性,则只需将 BaseAction 改成如下形式: IIBaseAction 不再继承任何类 public class BaseAction II 业务逻辑组件 Object serviceObj; II 依赖注入必需的 setter 方法 public void setServiceObj(Object obj) this. serviceObj = obj 1** *根据 beanName 获取容器中的bean 实例 * @param beanName 容器中的 bean 名 * @return 返回 Spring 容器中的 bean *1 public Object getBean(String beanName) return serviceObj; 从代码中可以看出, BaseAction 等待容器依赖注入。当 BaseAction 的子类调用 getBean 方法时,传入的baseName 完全没有作用。因为使用了Spring 的依赖注入,Action 与业务逻辑组件之间的依赖关系可通过配置文件来设定。 下面的示例采用这种实用整合策略,而 Spring 管理 Action 则采用 DelegatingRequest Processor 策略。其 struts-config.xml 和 actionServlet 的配置文件如下: 从上面的策略中看出,采用这种整合策略,有如下优势: ·可在不同整合策略中自由切换。 ·避免重复创建DelegatingActionProxy 实例。 .使业务 Action 避免代码污染。 本章小结 本章首先从原理上介绍了Spring 与 MVC 框架整合,包括对Spring 与 MVC 框架整 合的通用配置,以及如何在Web 应用启动时自动创建ApplicationContext实例。 其次重点介绍了 Spring 与 Struts 整合的几种策略:包括利用 DelegatingRequest Processor, DelegatingActionProxy ,以及 ActionSupport 的整合策略,并详细分析了各种 整合策略的优缺点。 最后介绍了一种实用的整合策略,并分析了其实用整合策略的优势。 388本章要点 被 企业应用开发面临的挑战 M 如何应对开发的挑战 温 主在态模式的使用 被 代理模式的使用 边 Spring AOP 介绍 滋 贫血模型介绍 2边 Rich Domain Object 模型介绍 法 几种简化的架构模型 企业级应用的开发乎台相当多,如J2EE ,. .Ne t., RJ.lbyO~ Rails 等。这些平台 为企业级应用的开发提供了丰富的支持,都实现了企业应用底层的功能:缓冲池、 多钱程及持久层访问等。即使有如此多的选择,企业级应用的开发依然困难重重。 所有企业级应用的开发平台都提供了高级、抽象的 A凹,但仅依靠这些 API 构建企业级的应用远远不够。在这些高级 API 基础上,搭建一个良好的开发体系, 也是企业级应用开发必不可少的步骤。本章将具体讨论如何搭建一个良好、可维 护、可扩展、高稳定性且能够快速开发的应用架构。轻量级 J2EE 企业应用实战一一Struts+Spring+Hibernate 整合开发 8.1 企业应用开发面临的挑战 企业应用的开发是相当复杂的,这种复杂除了表现在技术方面外,还表现在行业本 身。 企业级应用的开发往往需要面对更多的问题:大量的并发访问,复杂的环境,网络 的不稳定,还有外部的 Crack 行为等。因此企业级应用必须提供更好的多线程支持,具 备良好的适应性及良好的安全性等。 由于各行业的应用往往差别非常大,因此企业级应用往往具有很强的行业规则,尤 其是优良的企业级应用往往更需要丰富的行业知识。企业应用的开发成功,也需要很多 人的共同协作。 下面对企业应用开发面临的挑战作具体分析。 8.1.1 可扩展性、可伸缩性 市场是瞬息万变的,企业也是随之而变的。而信息化系统是为企业服务的,随着企 业需求的变化,企业应用的变化也是必然的。 笔者在多年开发过程中,经常昕到软件开发者对于需求变更的抱怨。当开发进行到 中间时,大量的工作需要重新开始,确实给人极大的挫败感,难免软件开发者会抱怨。 不过,笔者认为,一个积极的软件开发者应该可以正确对待需求的变更。需求的变更, 表明有市场前景,只有有变化的产品才是有市场的产品。 优秀的企业级应用必须具备良好的可扩展性和可伸缩性。因为良好的可扩展性可允 许系统动态增加新功能,而不会影响原有的功能。 良好的可扩展性建立在高度的解藕之上。使用 Delphi 、 PowerBuilder 等工具的软件 开发人员对 ini 文件一定不会陌生。使用 ini 文件是一种基本的解糯方式,将运行所需资 源、模块的糯合等从代码中分离出来,放入配置文件管理。这是一种优秀的设计思路, 最理想的情况是允许使用可插拔式的模块(类似于 Eclipse 的插件方式)。 在J2 EE 应用里,大多采用 XML 文件作为配置文件。使用 XML 配置文件可以避免 修改代码,从而能极好地提高程序的解稿。 XML 文件常用于配置数据库连接信息,通过 使用 XML 文件的配置方式,可以让应用在不同的数据库平台上轻松切换。从而避免在 程序中使用硬编码的方式来定义数据库的连接,也避免了在更改数据库时,需要更改程 序代码,从而提供更好的适应性。 下面是使用 Spring 的 bean 定义数据源的代码: com.mysql.jdbc.Driver 390企……略 E < ! 指定数据库服务的 URL--> jdbc:mysql:lllocalhost:3306/j2ee 30 1000 true 60 上面的配置文件可用于建立数据库的连接,可等同于如下代码: II 创建数据源实例 BasicDataSource ds = new BasicDataSource(); II 设置连接数据库的驱动 ds.setDriverClassName("com.mysql.jdbc.Driver") ; II 设置数据库库服务的 URL ds. setUrl (" j dbc :mysql: I Ilocalhost : 3306/j2ee") ; II 设置数据库用户名 ds.setUsername("root"); II 设置数据密码 ds.setPassword("32147"); II 设置数据源的最大连接数 ds.setMaxActive(100); II 设置数据源的最大活动数 ds.setMaxldle(30); II 设置数据源的最大等待数 ds.setMaxWait(1000); II 设置数据源是否自动提交 ds.setDefaultAutoCommit(true); 391轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 II 设置空闲连接是否自动回收 ds.setRemoveAbandoned(true) ; II 设置回收连接超时的时长 ds. removeAbandonedTimeout(60); 可以看出,第一种方式明显比第二种方式更优秀。因为当系统的数据库发生变化时 (这是相当常见的情形),开发用的数据库与实际应用的数据不可能是同一个数据库,当 软件系统由客户使用时,其数据库系统也是需要改变的。采用第一种方式则无须修改系 统源代码,仅通过修改配置文件就可以让系统适应数据库的改变。 使用 XML 配置文件提高解稿的方式,是目前企业级应用最常用的解藕方式,而依 赖注入的方式则提供了更高层次的解藕。使用依赖注入可以将各模块之间的调用从代码 中分离出来,并通过配置文件来装配组件。此处的依赖注入并非特指Spring,事实上, 依赖注入容器很多,如HiveMind等。 8.1.2 快捷、可控的开发 如果没有时间限制,任何一个软件系统在理论上都是可实现的。但这样的条件不存 在,软件系统必须要及时投放市场。对于企业级应用,时间的限制则更加严格。正如前 文介绍的,企业的信息是瞬息万变的,与之对应的系统必须能与时俱进。因此快捷、可 控是企业信息化系统必须面对的挑战。 软件开发人员常常乐于尝试各种新的技术,总希望将各种新的技术带入项目的开发 中,因而难免有时会将整个项目陷入危险的境地。 当然,采用更优秀、更新颖的技术,通常可以保证软件系统的性能更加稳定。例如, 从早期的 CIS 架构向 B/S 架构的过渡,以及从 Modell 到 Model2 的过渡等。这些都提 高了软件系统的可扩展性及可伸缩性。 但采用新的技术所带来的风险也是不得不考虑的,开发架构必须重新论证,开发人 员必须重新培训,这都需要成本投入。如果整个团队缺乏精通该技术的领导者,项目的 开发难免会陷入技术难题,从而导致软件的开发过程变成不可控的一-这是非常危险的 事情。 成功的企业级应用,往往是保证其良好的可扩展性及可伸缩性,并建立在良好的可 控性的基础上。 8.1.3 稳定性、高效性 企业级应用还有个显著特点:并发访问量大,访问频繁。因此稳定性、高效性是企 业级信息化系统必须达到的要求。 企业级应用必须有优秀的性能,如采用缓冲池的技术。缓冲池专用于保存那些创建 开销大的对象,如果对象的创建开销大,花费时间长,该技术可将这些对象缓存,避免 了重复创建,从而提高系统性能。典型的应用是数据连接池。 提高企业级应用性能的另一个方法是一一数据缓存。但数据缓存有其缺点:数据缓 392企业…盹策略~ 存虽然在内存中,可极好地提高系统的访问速度:但缓存的数据占用了相当大的内存空 间,这将会导致系统的性能下降。因此,数据缓存必须根据实际硬件设施制定,最好使 用配置文件来动态管理缓存的大小。 8.1.4 花费最小化,利益最大化 这是个永恒的话题,任何一个商业组织都希望尽可能地降低开销。对开发者而言, 降低开销主要是如何使在开发上的投资更有保值效果。即开发的软件系统具有很好的复 用性,而不是每次面临系统开发任务,总是需要重复开发。 尽可能让软件可以有高层次的复用,这也是软件行业的发展趋势。早期软件多采用 结构化的程序设计语言,此时的软件复用多停留在代码复用的层次。面向对象的程序设 计语言的出现,使代码复用提高到了类的复用中。 在良好的J2EE 架构设计中,复用是一个永恒的追求目标。架构设计师希望系统中 大部分的组件可以复用,甚至能让系统的整个层可以复用。对于采用 DAO 模式的系统 架构,如果数据库不发生大的改变,整个 DAO 层都不需要变化。 8.2 如何面对挑战 除了在上文介绍的所面临的各种技术挑战之外,企业级应用还有更多的挑战。每个 行业都有各自复杂的规则,软件开发者往往缺乏对行业规则的了解。企业级应用的开发 通常需要软件开发者和行业专家齐心协作,但系统开发中沟通成本又相当地高,因为软 件开发者与行业专家之间的沟通往往存在不少障碍,这些都会影响系统的开发。 面对这些挑战,笔者有如下建议。 8.2.1 使用建模王具 此处的建模工具不一定是 ROSE 等,可以是简单的手画草图。当然,借助于专业的 建模工具可以更好地确定系统模型。 任何语言的描述都是很空洞,而且具有很大的歧义性。使用图形则更加直观,而且 意义更加明确。推荐使用建模工具主要出于如下两个方面的考虑。 ·用于软件开发者与行业专家之间沟通,正如前文所介绍的,行业专家与软件开发 者之间对系统的理解可能存在少许差异。使用图形来帮助交流是不错的主意,通 过建模工具绘制的各种图形,可使软件系统的模型更加清晰化。 ·用于软件开发者之间的沟通。即使在软件开发者内部,对于软件的模型往往也不 是非常统一。使用建模工具可以减少软件开发者对于系统的理解分歧,从而降低 沟通成本。 关于建模工具,推荐采用统一建模语言: UML。但 UML 的使用也需要掌握分寸, 393轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 在软件开发人员内部使用时,尽可能使用规范的 UML; 但用于与行业专家沟通时,则应 该尽量增加文字说明,而不要拘泥于 UML 图形的表现上,切忌仅将一个图形生硬摆出。 8.2.2 利用优秀的框架 使用框架可以大大提高系统的开发效率。除非开发一个非常小的系统,而且是开发 后无须修改的系统,才可以完全抛弃框架。 优秀的框架本身就是从实际开发中抽取的通用部分,使用框架就可以避免重复开发 通用部分。使用优秀的框架不仅可以直接使用框架中的基本组件和类库,还可以提高软 件开发人员对系统架构设计的把握。使用框架有如下几个优势。 1.提高生产效率 框架是在实际开发过程中提取出来的通用部分。使用框架可以避免开发重复的代码, 看下面的 JDBe 数据库访问代码: II 注册数据库驱动 Class. forName ("com.mysql.jdbc .Driver"); II 数据服务的 URL String url=" jdbc:mysql: I !localhost/j2ee" ; /I 数据库的用户名 String username="root"; /I 数据库密码 String password="32147"; /I 获取数据库连接 Connection co口口= DriverManager.getConnection(url , username , password); II 创建 Statement Statement stmt=conn.createStatement(); String sql=". II执行更新 stmt.executeUpdate(sql); 上面的代码是连接数据库执行数据更新的代码。而这个过程的大部分都是固定的, 包括连接数据库、创建Statement 及执行更新等,唯一需要变化的是SQL 语句。 在实际的开发过程中,不可能总是采用这种步骤进行数据库访问,为避免代码重复, 可在实际的开发中提取出如下方法: public Class DbBean { II 用于执行更新的方法 update(String sql) /I 创建 Statement 对象 Statement stmt= getConnection() .createStatement(); /I 更新所使用的 SQL 语句 String sql=". /I 执行更新 stmt.executeUpdate(sql); II 获取数据库连接 private Connection getConnection() 394的思与策略~ 工 f (conn == null) II注册数据库驱动 Class. forName (" com.mysql. jdbc.Driver") ; II 数据服务的 URL String url="jdbc:mysql:lllocalhost/j2ee"; II 数据库的用户名 Stri口g username= II root II ; II 数据库密码 String password="32147"; II 获取数据库连接 conn= Dr工 verManager.getConnection(url, username , password); 上面的代码可以大大减少代码的重复量,但依然需要开发者完成连接数据库,创建 Statement 等步骤。如果使用Spring 的 JOBC 抽象框架,上面的代码则可以简化为如下: JdbcTemplate jt =口ew JdbcTemplate(); II 为 JdbcTemplate指定 DataSource jt.setDataSource(ds); II 更新所使用的 SQL 语句 String sql=".. II 执行更新 j t.update (sql) ; 借助于 Spring 的 JOBC 抽象框架,数据库访问无须手动获取连接,无须创建 Statement 等对象。只需要传入一个 OataSource 对象,由 JdbcTemplate 完成 OataSource 获取数据库 连接:创建 Statement 对象:执行数据库更新的通用步骤。而软件开发者只需要提供简 单的 SQL 语句即可。 另外,使用框架可以缩短系统的开发时间,特别是对于大型项目的开发,使用框架 的优势将更加明显。根据 javaworld 社区的调查,使用框架和不使用框架的时间对比如 图 8.1 所示。 时间/精力 人天 3X 。 小规模应用开发J 采用框架 」 业务功能 大规模应用开发 图 8.1 使用框架和不使用框架的对比 395轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 2. 具有更稳定、更优秀的性能 如果不使用已有的框架,系统开发者将面临着需要自己完成所有的底层部分。除非 开发者丝毫不遵守软件复用的原则,总是重复书写相同代码。 系统开发者从系统开发中提取出的共同部分,也可成为框架。不可否认,完全由开 发者自己提取框架有自己的优势,开发人员更加熟悉框架的运行,无须投入成本学习新 的技术:但借助于已有框架的优势更加明显,己有的框架通常己被非常多的项目验证过, 框架的性能等通常更有保障,而开发者自己提取的框架则可能包含许多未知的隐患。 因此为了更好地了解框架底层的运行,建议使用开源框架。 3. 更好的保值性 采用框架开发的系统使模块组织更加一致,从而降低了软件开发者之间的沟通成本, 使系统具有更好的可读性,从而让软件系统具有更好的保值性。 后期的更新、维护也是企业级应用开发的重要组成部分。而使用框架的系统具有很 大的相似性,从而有利于后期的更新及维护。 8.2.3 选择性地扩展 软件的需求千变万化,任何框架不可能总是那么完美,难免需要扩展现有的框架。 在许多项目中,开发者往往喜欢实现自己的框架,认为一个固定的框架会限制其发 挥,事实上,他们没有意识到如何扩展框架。虽然开发自己的框架可以获得全部的控制 权,但是这也意味着需要很多资源来实现它。正如前文讨论过的,实现自己的框架将需 要开发者保证框架的稳定性及性能。 而对己有的框架进行扩展,则可最大限度地利用己有的框架,即使是扩展已有的框 架,笔者也不建议盲目扩展,因为新增的部分有时会引入新的风险。笔者建议应对己有 框架深入研究,尽量利用己有组件,除非无法使用已有框架时,才考虑选择性地扩展。 8.2.4 使用代码生成器 使用代码生成器可以自动生成部分程序,不但可以省去许多重复性的劳动,而且在 系统开发过程中可以大大节省时间。程序生成器的效率很高,在开发软件的许多环节都 有很好的作用,如数据持久化、界面及中间件等。 代码生成器还有个最大的作用:在原型开发期间可以大量重复利用代码生成器。原 型系统通常在需求不十分明确时非常有用,此时的需求尚未确定,而软件功能业务无须 十分完备,仅提供大致的软件功能,此时的代码生成器就非常有用。 396,也腊的思考与策略国 8.3 常用的设计模式及应用 关于设计模式的定义可能已经有成千上万,在这里笔者不再重复那些烦琐的设计模 式定义。事实上,设计模式既不是深奥的理论,也不是难以理解的概念,仅仅是一种习 惯行为,只是这种习惯行为通常比较有效。 设计模式可以理解成一种思维定势,当固定的问题出现时,程序开发者可采用固定 的解决方案(这种解决方案通常比较有效)。因此,学习设计模式,并不是学习如何实现 设计模式,而是学习一种思维定势一一对于固定的问题,通常采用何种方案解决。 8.3.1 单态模式的使用 单态模式的实现相当简单,是最常用的设计模式之一,以致于在相当多的地方都可 以见到单态模式的痕迹。 下面代码实现了简单的单态模式: public class Singleton { II 普通成员属性 private int value private static Singleton st; II 单态模式需要将构造器私有 private Singleton() II 静态方法,用于初始化该类的实例 public static Singleton instance() II 首先判断该实例是否为空,如果为空 if (st == null) II 创建新的实例 st = new Singleton(); return st; II 返回该实例的成员属性的getter 方法 public int getValue() return value; II 设置实例的成员属性的setter 方法 public void setValue() value = 9; II 主方法,程序的入口 public static void main(String[] args) II 创建该类的第一个实例 397轻量级 J2EE 企业应用实战一-8t阳 ts+8pring+Hibernate 整合开发 Singleton tl = Singleton.instance(); ,/ 1 创建该类的第二个实f7!J Singleton t2 = Singleton.instance(); II 比较两个实例是否相等 System.out.println(tl == t2); 正如前文所介绍的,学习设计模式绝不是学习这种代码实现。更多地是需要掌握在 何时使用这种模式。 在任何不需要重复生成Java 实例的场景中,都应该考虑使用单态模式。使用单态模 式可以保证系统无须生成多个Java 实例,从而减少内存占用率,也降低JVM (Java 虚 拟机)进行垃圾回收的开销。单态模式通常有如下两个使用场景: ·工厂模式中的工厂。 ·使用服务定位器模式时的服务定位器。 关于工厂模式,可以参考5.3.2 节。本节将具体介绍如何使用服务定位器。服务定 位器模式在经典的EJB 应用中非常常用,但这种模式决不局限在这种场景中使用,在任 何需要大量定位相似服务的场景中,都可以使用服务定位器模式。 下面介绍 EJB 定位器模式的使用,关于EJB 的开发,由于篇幅原因,本节不作详细 介绍。 为了使用 EJB 定位器模式,可对EJB 的 Home 接口进行扩展,让所有的EJB 的 Home 接口都继承下面接口,而不是只继承系统默认的接口。下面是扩展的Home 接口: II 扩展后的 Home 接口,该接口继承EJBHome 接口 public interface MyEJBHome extends EJBHome public EJBObject create() throws RemoteException, CreateException; 这个接口比原来的接口增加了一个create 方法,该方法用于返回EJB 的 Remote 接 口实例。后面开发的日B 其 Home 接口都从该接口派生出来。 下面是服务定位器的代码: public class RemoteFactory { II 将该工厂写成单态模式 private static RemoteFactory instance; II 用 HashMap 来缓存获得的 EJBRemote private Map remoteInterfaces; private Context context; II 单态模式,构造器私有 private RemoteFactory() throws NamingExcept工 0口 II 执行构造器时,完成缓存池的初始化 remote工 nterfaces = new HashMap(); II 创建 Context 实例 context = get 工 nitialContext(); II 同步方法,用于获取工厂实例 public static synchron工 zed RemoteFactory getInstance() throws NamingException 398、出…策略目 II 不是线程安全的,但已经足够 II 加上 synchron 工 zed 会变成线程安全,但性能会下降 if (instance -- null) instance = new RemoteFactory(); return 工 nstance; II 核心方法,通过该lookup 方法可以返回 EJB 的 Remote 接口 private EJBObject lookup(String jndiName, Class homelnterfaceClass) throws NamingExceptio口, CreateException, RemoteExcept工 on EJBObject remotelnterface = null; II 从缓存池中取出 EJB 的 Remote 接口实例 r凹otelnterface = (EJBObject)remotelnterfaces.get(home工 nterfaceClass) ; II 如果该接口实例不存在,则执行 JNDI 查找 if (remotelnterface == null) Object obj = context.lookup(jndiName); II将查找的对象强制类型转换为MyEJBHome类型,该类型是EJBHome 的子接口 MyEJBHome homelnterface = (MyEJBHome)PortableRemoteObject.narrow(obj , homelnterfaceClass); II 调用 home工 nterface 的 create 方法返回 Remote 接口 remotelnterface = homelnterface.create(); II 将新创建的对象存入缓存 remotelnterfaces.put(home工 nterfaceClass, remotelnterface); II 返回 EJB 的 remote 接口实例 return remotelnterface; II 工具方法,用于初始化Context private Context getlnitialContext() if (context -- null) try II 如果不是在J2EE 程序里,还需要属性文件才能正确创建InitialConext() context = new 工 n 工 t 工 alContext(); } catch (NamingException 口e) ne.printStackTrace(); return context; 从上面的代码可以看出,服务定位器实际上是工厂模式的一种应用。通过使用服务 定位器,可以将服务的查找及服务的调用简化成简单的方法调用。而在这个服务定位器 中,使用单态模式保证了服务定位器只有一个。 399轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 8.3.2 代理模式的使用 代理模式也是一种常用的设计模式。传统的代理模式主要用于采用简单对象来代替 复杂的对象,如果创建一个对象所需的时间比较长,且计算资源相当昂贵,可以采用一 个相对简单的对象来代替它。代理模式可将创建过程推迟到真正需要该对象时完成,一 旦整个对象创建成功,对代理的方法调用将变成对实际对象的方法调用。 J2 EE 里的代理模式通常是采用功能更强大的对象来代替目标对象。例如,对于普通 的业务逻辑组件,其方法都应该有事务性,但这种开始事务和结束事务都是通用步骤。 因此原始业务逻辑对象的方法可以无须事务操作,而是由系统生成动态代理来负责事务 操作,并调用实际的目标方法。 看如下代理模式的实现,下面是原始的业务逻辑对象,该业务逻辑对象包含两个方 法,这两个方法都没有事务操作。 Java 的动态代理通常是代理接口,而且,是面向接口 编程的。下面给出实现业务逻辑对象接口的源代码: /I 业务接口 public interface Service /I 两个方法声明 publ工 c void methodl(); public void method2(); 下面是该接口的实现类,实现类并没有真正的业务逻辑,仅仅在控制台打印出简单 的一行字符: /I 业务逻辑组件,实现Serv工 ce 接口 public class Service工mpl implements Service II 实现接口实现的两个方法 II 第一个方法,在控制台打印出执行业务方法l public void methodl() System.out.println("执行业务方法 1") ; /I 第二个方法,在控制台打印出执行业务方法 2 public void method2() System.out.println("执行业务方法 2") ; 业务逻辑组件通常由工厂负责产生,下面是业务逻辑组件的工厂,该工厂被写成单 态模式: II 负责产生业务逻辑组件的工厂 public class ServiceFactory II将工厂设计成单态模式 private static Serv工 ceFactory sf; 400业即…策略国 II 业务逻辑组件 private Service service; II 构造器私有,保证最多只能产生一个工厂 private ServiceFactory() II 实例化工厂的方法,用于创建工厂实例 public static ServiceFactory instance() II 如果工厂实例为空,则重新创建工厂实例 if (sf -- null) sf = new ServiceFactory(); II 返回工厂实-f9iJ return sf; II 获取业务逻辑组件的方法 public Service getService(String impl) II 工厂里负责产生业务逻辑组件 if (impl.equals("one")) 工 f (service == null ) ser飞rice = new ServiceImpl(); return service; return null; 此时,依然只是一个简单的工厂模式。当获得业务逻辑组件时,其方法不会有任何 的事务操作代码。为了让其方法具有事务性,可以增加事务操作所需的代理处理器。 下面是为增加事务操作而提供的代理处理器z II 代理处理器,实现InvocationHandler public class ProxyHandler implements InvocationHandler Iltarget 是被代理的目标对象 private Object target; Ilinvoke 方法是实现工nvocationHandler接口必须实现的方法,该方法会在目标对象方法 II 被调用时,自动被调用 public Object invoke(Object proxy , Method method, Object[] args) throws Exception Ilresult 是调用目标方法的返回值 Object result = null; II 如果调用的目标方法名为methodl if (method.getName() .equals("methodl")) II 开始事务 System.out.println("======开始事务:;::::::====="); II 执行目标方法 result =method. invoke (target , args); II提交事务 System.out.println("======提交事务:;:::;:::;:::;:::;::::;:::;::"); 401轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 II 否则 else II 直接调用目标方法 result =method.invoke(target , args); II 返回调用结果 return result; II 该方法用于设置目标对象 public void setTarget(Object 0) this.target = 0; 这是个通用的代理处理器,该处理器并不需要与任何特定的目标对象糯合。该处理 器对 method1 方法增加了事务处理代码。实际上,此处并未有真正的事务操作代码,程 序仅仅以控制台打印的两行语句来代表事务操作代码。 该代理处理器仅加强了method1 方法,而且加强的方法以硬编码的方式写在代码中。 这种方式显然存在一定的局限性:如果需要对其他方法增加事务处理,则需要修改其源 代码,重新编译。 为了该代理处理器有更好的适应性,系统可以将需要增加事务操作的方法以XML 文 件配置,而该代理处理器负责解析XML 文档,根据配置决定对哪个方法增加事务处理。 注意:在灵活的代理模式里,不仅仅目标方法可以提供XML 文件配直,甚至代理 处理器都可以通过依赖注入管理,这也是Spring AOP 的处理方式。 下面是负责生成动态代理的代理工厂: public class MyProxyFactory { 1** *实例 Service 对象 * @param serviceName String * @return Object *1 public static Object getProxy(Object object) II 代理处理类实例 ProxyHandler handler = new ProxyHandler(); II 把该 servlce 实~J 托付给代理操作 handler.setTarget(object); II 第一个参数是用来创建动态代理的ClassLoader对象,只要该对象能访问Service 接口即可 II 第二个参数表明该代理所实现的所有接口,第三个参数是代理的处理类 return Proxy.newProxyInstance(object.getClass() .getClassLoader() , object.getClass() .get工 nterfaces() , handler); 在上面的动态代理工厂里,可以动态生成Java 实例,该实例将实现目标对象所实现 402企业应用开发的思考与策略 国 的全部接口。此处的目标对象是 Servicelmpl 实例,该实例实现 Service 接口。 下面是测试动态代理的程序: public class TestService { publ 工 c static void main(String[] args) { IIService 对象 Service service = null; II 通过 Serv工 cFactory 生成 Service 实例,该 Service 实例是需要代理的目标对象 Service targetObject = ServiceFactory. 工 nstance() .getService("one"); II 根据目标对象,生成代理对象 Object proxy = MyProxyFactory.getProxy(targetObject); II 判断代理对象是否实现Service 接口 if (proxy instanceof Service) II将代理对象转换Service 实例 service = (Serv 工 ce)proxy; II 通过代理对象执行 methodl service.methodl(); II 通过代理对象执行method2 serv工 ce.method2(); 从中可以看出,代理处理器对method! 方法增加了事务操作代码。由代理工厂生成 的代理对象,并具有事务性。下面是执行测试代码的执行结果: --开始事务======= 执行业务方法 1 ------提交事务======= 执行业务方法2 在执行业务方法 1 的前后,分别增加了开始事务和提交事务的操作代码。虽然这些 代码本身没有事务性,但完全可以替换成事务操作代码,也可以替换成权限检查代码, 如果调用目标方法的角色没有足够权限,则直接拒绝调用。 8.3.3 Spring AOP 介绍 Spring 的 AOP 是上面代理模式的深入。使用Spring AOP 时,开发者无须实现业务 逻辑对象工厂及代理工厂,这两个工厂都由Spring 容器充当。 Spring AOP 允许使用 XML 文件配置目标方法,但ProxyHandler允许使用依赖注入管理,为Spring AOP 提供了更 多灵活的选择。 在下面 Spring AOP 的示例中, InvocationHandler采用动态配置,需要增加的方法也 采用动态配置,一个目标对象可以有多个拦截器(类似于代理模式中的代理处理器)。 下面是原始的目标对象: II 目标对象的接口 public interface Person 403轻量级 J2EE 企业应用实战一-8t阳ts+8pring+Hibernate 整合开发 II 该接口声明了两个方法 void info(); void run(); 下面是原始目标对象的实现类: II 目标对象的实现类,实现类实现Person 接口 public class Personlmpl implements Person II 两个成员属性 private String name; private int age; II口arne 属性的 setter 方法 public void setName(String name) this.name = name; Ilage 属性的 setter 方法 public void setAge(工 nt age) this.age = age; Ilinfo 方法,该方法仅仅在控制台打印一行字符串 public void 工 nfo () System.out.println("我的名字是: II + name + "今年年龄为: "+ age); Ilrun 方法,该方法也在控制台打印一行字符串 public void run() if (age < 45) System.out.println("我还年轻,奔跑迅速... n) ; else System.out.println("我年老体弱,只能慢跑..."); 该 Person 实例将由 Spring 容器负责产生和管理,其 name 属性和 age 属性也采用依 赖注入管理。 为了充分展示 Spring AOP 的功能,此处为 Person 对象创建三个拦截器。第一个拦 截器是调用方法前的拦截器,代码如下: II 调用目标方法前的拦截器,拦截器实现 MethodBeforeAdvice 接口 public class MyBeforeAdvice implements MethodBeforeAdvice II 实现 MethodBeforeAdvice接口,必须实现before 方法,该方法将在目标 II 方法调用之前,自动被调用 public void before (Method m, Object [] args , Object target) throws Throwable System.out.println("方法调用之前..."); System.out.println("下面是方法调用的信息: "); 404企业应用开发的思考与策略 国 System.out.println(" 所执行的方法是:" + m); System.out.println("调用方法的参数是:" + args); System. out .println ("目标对象是: " + target); 第二个拦截器是方法调用后的拦截器,该拦截器将在方法调用结束后自动被调用, 代码如下: II 调用目标方法后的拦截器,该拦截器实现AfterReturningAdvice接口 public class MyAfterAdv工 ce implements AfterReturningAdvice II 实现 AfterReturningAdvice接口必须实现afterReturn工 ng 方法,该方法将在目标方法 II 调用结束后,自动被调用 public void afterReturning(Object returnValue, Method m, Object[] args , Object target}throws Throwable System.out.println("方法调用结束..."); System.out.println("目标方法的返回值是: " + returnValue); System.out.println("目标方法是: " + m}; System.out.println("目标方法的参数是: " + args}; System.out.println("目标对象是: " + target); 第三个拦截器是 Around 拦截器,该拦截器既可以在目标方法之前调用,也可以在 目标方法调用之后被调用,代码如下: IIAround拦截器实现Method工 nterceptor接口 public class MyAround工nterceptor implements Method工nterceptor II 实现 Methodlnterceptor接口必须实现invoke 方法 public Object invoke(Method工 nvocation invocation) throws Throwable II 调用目标方法之前执行的动作 System.out.println("调用方法之前: invocation对象: [" +工nvocation + "]"); II 调用目标方法 Object rval = invocation.proceed(); II 调用目标方法之后执行的动作 System.out.println("调用结束..."); return rval; 利用 Spring AOP 框架,实现之前的代理模式相当简单,只需要实现对应的拦截器即 可,无须创建自己的代理工厂,采用Spring 容器作为代理工厂即可。下面在Spring 配置 文件中配置目标bean,以及拦截器。 Spring 配置文件的代码: Sl <1 工 st> .*run.* <' 代理对象所实现的接口一〉 lee.Person <1 工 st> runAdvisor myAdvice myAroundlnterceptor 该配置文件使用ProxyFactoryBean来生成代理对象,在配置ProxyFactoryBean工厂 bean 时,指定了 target 属性,该属性值为 personTarget,即指定代理的目标对象为 personTarget。通过 interceptorNames属性确定代理所需要的拦截器,拦截器可以是普通 的 Advice,对目标对象的所有方法起作用:拦截器也可以是Advisor (Advisor 是 Advice 406企业…思考与策略国 和切面的组合),用于确定目标对象的哪些方法需要增加处理,以及怎样的处理。在上面 的配置文件中,使用了三个拦截器,其中 myAdvice 、 myAroundInterceptor 都是普通 Advice ,它们将对目标对象的所有方法起作用。而 runAdvisor 则使用了正则表达式切面, 匹配 run 方法,即该拦截器只对目标对象的 run 方法起作用。 下面是测试代理的主程序: publ 工 c class BeanTest { publ 工 c static void main(String[] args)throws Exception II 创建 Spring 容器 ApplicationContext ctx = new FileSystern这mlApplicationContext("bean.刀口111) ; II 获取代理对象 Person p = (Person)ctx.getBean("person"); II 执行 info 方法 p. info () ; System.out.println("==========================================="); II执行 run 方法 p.run() ; 下面是程序的执行结果: 方法调用之前.. . 下面是方法调用的信息: 所执行的方法是 :public abstract void lee.Person.info() 调用方法的参数是: null 目标对象是: lee.Personlmpl@b23210 调用方法之前: invocation对象: [invocation: method 'info' , arguments []; target is of class [lee.Personlmpl]] 我的名字是: Wawa ,今年年龄为: 51 调用结束.. . 方法调用之前.. . 下面是方法调用的信息: 所执行的方法是 :public abstract void lee.Person.run() 调用方法的参数是: null 目标对象是: lee.Person工mpl@b23210 调用方法之前:工nvocation对象: [invocation: method 'run' , arguments [ ]; target is of class [lee.Person工mpl] ] 我年老体弱,只能慢跑.. . 调用结束.. . 方法调用结束.. . 目标方法的返回值是: null 目标方法是: public abstract void lee.Person.run() 目标方法的参数是: null 目标对象是: lee.Person工mpl@b23210 程序的执行结果中用一行"-"用于区分两次调用的方法。在调用info 方法时,只 有 myAdvice 和 myAroundInterceptor两个拦截器起作用:调用run 方法时,三个拦截器 都起了作用。 通过上面的介绍可看出Spring 的 AOP 框架是对代理模式的简化,并拓展了代理模式。 407轻量级 J2EE 企业应用实战一一-Struts+Spring+Hibernate 整合开发 Spring AOP 是 Spring 声明式事务的基础。了解 Spring AOP 对深入理解 Spring 的声 明式事务管理是非常有好处的。另外, Spring AOP 还可以完成很多功能,如基于 AOP 的权限检查,本书的第 10 章示范了基于 Spring AOP 的权限检查范例,此处不在赘述。 8.4 常见的架构设计策略 日前流行的轻量级J2EE 应用的架构比较一致,采用的技术也比较一致,通常使用 Spring 作为核心,向上整合MVC 框架,向下整合ORM 框架。使用 Spring 的 loC 容器 来管理各组件之间的依赖关系时, Spring 的声明事务将负责业务逻辑层对象方法的事务 管理。 但在固定的技术组合上,依然可能存在小的变化。F 面依次讨论可能存在的架构 策略。 8.4.1 贫血模式 贫血模式是最常用的设计架构,也是最容易理解的架构。为了让读者通过本书顺利 进入轻量级J2 EE 企业应用开发,本书的第 9 章及第 10 章的范例都将采用这种简单的架 构模式。 所谓贫血,指 Domain Object 只是单纯的数据类,不包含业务逻辑方法,即每个 Domain Object 类只包含基本的 setter 和 getter 方法。所有的业务逻辑都由业务逻辑组件 实现,这种 Domain Object 就是所谓的贫血的 Domain Object ,采用这种 Domain Object 的架构即所谓的贫血模式。 下面以第 9 章的消息发布系统的部分代码为例,介绍贫血模式。 在贫血模式里,所有的 DomainO时 ect 只是单纯的数据类,只包含每个属性的 setter 和 getter 方法,如下是两个持久化类。 第一个 Domain Obj民t 是消息,其代码如下: public class News extends BaseObject implements Ser工 alizable II 主键 private Long id; II 消息标题 private String title; II 消息内容 private String content; II 消息的发布时间 pr工vate Date postDate; II 消息的最后修改时间 private Date lastModifyDate; II 消息所属分类 private Category category; II 消息对应的消息回复 private Set newsReviews; II 无参数的构造器 408思 策略~ public News() { II 消息回复对应的 getter 方法 public Set getNewsReviews() { return newsRev工 ews; II 消息回复对应的 setter 方法 public void setNewsReviews(Set newsReviews) { this.newsReviews = newsReviews; II 消息分类对应的getter 方法 public Category getCategory() { return category; II 消息分类对应的 setter 方法 public vo工 d setCategory(Category category) { this.category = category; II 消息最后修改时间的getter 方法 public Date getLastModifyDate() { return lastMod工 fyDate; II 消息最后修改时间的setter 方法 public void setLastModifyDate(Date lastModifyDate) { this.lastModifyDate = lastModifyDate; II 消息发布时间的getter 方法 public Date getPostDate() { return postDate; II 消息发布时间的 setter 方法 public void setPostDate(Date postDate) { this.postDate = postDate; II 消息内容对应的getter 方法 public String getContent() { return content; II 消息发布者对应的setter 方法 public void setContent(String content) { this.content = content; II 消息主键对应的getter 方法 public Long getld() { return id; II 消息主键对应的setter 方法 public void set工 d(Long id) { this.id = id; II 消息标题对应的getter 方法 public String getTitle() { return title; II 消息标题对应的setter 方法 public void setTitle(String title) { this.title = title; IIDomain Object 重写 equals 方法 409轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 public boolean equals(Object object) { 工 f (! (obj ect instanceof News)) { return false; } News rhs = (News) object; return this.poster.equals(rhs.getPoster()) && this.postDate.equals(rhs.getPostDate()); IIDomain Object 重写的 hashCode方法 public int hashCode() { return this.poster.hashCode() + this.postDate.hashCode(); } IIDomain Object 重写 toString方法 public String toString() { return new ToStringBuilder(this) .append("id" , this.id) .append("title" , this.title) . append( "postDate" , this.postDate) . append( "content" , this.content) .append("lastModifyDate" , this.lastModifyDate) .append("poster" , this.poster) . append (" category" , this. category) . append ( "newsReviews" , this.newsReviews) .toString(); 410 第二个 Domain Object 是消息对应的回复,其代码如下: public class NewsReview extends BaseObject { II 消息回复的主键 private Long id; II 消息回复的内容 private String content; II 消息回复的回复时间 private Date postDate; II 回复的最后修改时间 private Date lastModifyDate; II 回复的对应的消息 private News news; II 消息回复的构造器 public NewsReview() { II 回复内容对应的 getter 方法 public String getContent() { return content; II 回复内容对应的setter 方法 public void setContent(String content) { this.content = content; II 回复主键对应的setter 方法 public Long getld() { return id; II 回复主键对应的 setter 方法 public void set工 d (Long id) { this.id = id; II 回复的最后修改时间对应的getter 方法 public Date getLastModifyDate() { return lastModifyDate;业…思考与策略~ II 回复的最后修改时间对应的 setter 方法 public void setLastModifyDate(Date lastModifyDate) { this.lastModifyDate = lastMod 工 fyDate; II 回复对应的消息的 getter 方法 public News getNews() { return news; II[回复对应的消息的setter 方法 public void setNews(News news) { this.news = news; } /I 回复发布时间的 getter 方法 public Date getPostDate() { return postDate; } /I 回复发布时间的 setter 方法 public void setPostDate(Date postDate) { this.postDate = postDate; } IIDomain Object 重写的 equals 方法 publ工 c boolean equals(Object object) { if (! (object instanceof NewsReview)) { return false; } NewsReview rhs = (NewsReview) object; return this.poster.equals(rhs.getposter()) && this.postDate.equals(rhs.getPostDate()); I*return new EqualsBuilder() .append(this.news, rhs.news) .append( this.content, rhs.content) .append(this.postDate, rhs.postDate) .append(this.lastModifyDate, rhs.lastModifyDate) .append( this.id, rhs.id) .append(this.poster, rhs.poster) .isEquals();*1 IIDomain Object 对应的 hashCode 方法 public int hashCode() { return this.poster.hashCode() + this.postDate.hashCode(); I*return new HashCodeBuilder(-1152635115, 884310249).append(this.news) .append(this.content) .append(this.postDate) .append( this.lastModifyDate) .append(this.id) .append(this.poster) .toHashCode() ;*1 } IIDomain Object 对应的 toString方法 public String toString() { return new ToStringBuilder(this) .append("id" , this.id) .append( "postDate" , this.postDate) .append("lastModifyDate" , this.lastModifyDate) .append("content" , this.content) .append( "poster" , this .poster) . append ("news" , this .news) . toString () ; 从上面贫血模式的 Domain Object 可看出,其类代码中只有 setter 和 getter 方法,这 种 Domain Object 只是单纯的数据体,类似于 C 语言的数据结构。虽然它的名字是 Domain Object ,却没有包含任何业务对象的相关方法。 Martin Fowler 认为,这是一种不健康的 建模方式, Domain Model 既然代表了业务对象,就应该包含相关的业务方法。从语言的 角度上来说, Domain Model 在这里被映射为 Java 对象(一般都是 aRM) , Java 对象应 411轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 该是数据与动作的集合,贫血模型相当于抛弃了 Java 面向对象的性质。 Rod Johnson 和 Martin Fowler 一致认为:贫血的 DomainO均 ect 实际上以数据结构代 替了对象。他们认为 Domain Object 应该是个完整的 Java 对象,既包含基本的数据,也 包含了操作数据相应的业务逻辑方法。 下面是 NewsDAOHibemate 的源代码,该 DAO 对象用于操作 News 对象: IINewsDAOHibernate 继承 HibernateDaoSupport,实现 NewsDAO 接口 public class NewsDAOHibernate extends HibernateDaoSupport implements NewsDAO { II 根据主键加载消息 public News getNews(Long id) News news = (News) getHibernateTemplate() .get(News.class, id); if (news -- null) ( throw new ObjectRetrievalFailureException(News.class , id); return news; II 保存新的消息 public vo工 d saveNews(News news) ( getHibernateTemplate() .saveOrUpdate(口ews) ; II 根据主键删除消息 public void removeNews(Long id) getHibernateTemplate() .delete(getNews(id)); II 查找全部的消息 public List f 工口dAll () getHibernateTemplate() .find("from News"); 既然 DAO 对象完成具体的持久化操作,因此基本的CRUD 操作都应该在 DAO 对 象中实现。但 DAO 对象应该包含多少个查询方法,并不是确定的。因此,根据业务逻 辑的不同需要,不同的DAO 对象可能有数量不等的查询方法。 对于现实中 News,应该包含一个业务方法(addNewsReviews方法)。在贫血模式下, News 类的代码并没有包含该业务方法,只是将该业务方法放到业务逻辑对象中实现, 下面是业务逻辑对象实现addNewsReviews的代码: public class FacadeManagerImpl implements FacadeManager { II 业务逻辑对象依赖的 DAO 对象 private CategoryDAO categoryDAO; pr工 vate NewsDAO newsDAO; private NewsReviewDAO newsReviewDAO; private UserDAO userDAO; I I. . .此处还应该增加依赖注入 DAO 对象必需的 setter 方法 I I. . .此处还应该增加其他业务逻辑方法 II 下面是增加新闻回复的业务方法 public NewsReview addNewsReview(Long newsId , String content) II根据新闻 id 加载新闻 412企业应用开发的思考与策略 JH News news = newsDao.getNews(newsld); /I以默认构造器创建新闻回复 NewsReview review =口ew NewsReview(); /I设置新闻与新闻回复之间的关联 review.setNews(news); II 设置新闻回复的内容 review.setContent(content); II 设置回复的回复时间 review.setPostDate(new Date()); II 设置新闻回复的最后修改时间 review.setLastModifyDate(new Date()); II 保存回复 newsReviewDAO.saveNewsReview(review) ; retur口 reVlew; 在贫血模式下,业务逻辑对象正面封装了全部的业务逻辑方法, Web 层仅与业务逻 辑组件交互即可,无须访问底层的 DAO 对象。 Spring 的声明式事务管理将负责业务逻 辑对象方法的事务性。 在贫血模式下,其分层非常清晰。 Domain 0时 ect 并不具备领域对象的业务逻辑功 能,仅仅是 ORM 框架持久化所需的 POJO ,仅是数据载体。贫血模型容易理解,开发便 捷,但严重背离了面向对象的设计思想,所有的 Domain Object 并不是完整的 Java 对象。 总结起来,贫血模式存在如下缺点: ·项目需要书写大量的贫血类,当然也可以借助某些工具自动生成。 • Domain Object 的业务逻辑得不到体现。由于业务逻辑对象的复杂度大大增加,许 多不应该由业务逻辑对象实现的业务逻辑方法,完全由业务逻辑对象实现,从而 使业务逻辑对象的实现类变得相当雕肿。 贫血模式的优点是:开发简单、分层清晰、架构明晰且不易混淆;所有的依赖都是 单向依赖,解藕优秀。适合于初学者及对架构把握不十分清晰的开发团队。 8.4.2 Rich Domain Object 模式 在这种模式下, Domain Object 不再是单纯的数据载体, Domain Object 包含了相关 的业务逻辑方法。例如, News 类包含了 ad dN ewsView 方法等。 下面是修改后的 News 类的源代码: public cl 号 ss News extends BaseObject II 此处省略了其他的属性 /I此处省略了属性对应的setter 和 getter 方法 II 增加新闻回复的业务逻辑方法 public NewsReview addNewsReview(String content) II 以默认构造器创建新闻回复实例 NewsReview review = new NewsReview(); II 设置回复内容 review.setContent(content); II 设置回复的发布日期 413轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 review.setPostDate(new Date()); 1/ 设置回复的最后修改日期 review.setLastModifyDate(new Date()); II 设置回复与消息的关联 review.setNews(this); return revJ. ew; II 此处省略了重写的hashCode. equals 等方法 在上面的 Domain a均ect 中,包含了相应的业务逻辑方法,这是一种更完备的建模 方法。 注意:不要在Domain Object 中对消息回复完成持久化,如需完成持久化,必须调用 DAO 纽件;一旦调用 DAO 纽件,将造成DAO 对象和 Domain Object 的双向依赖;另外, DomainO均ect 中的业务逻辑方法还需要在业务逻辑纽件中代理,才能真正实现持久化。 在上面的业务逻辑方法中,并没有进行持久化。如果抛开DAO 层,这种 Domain Object 也可以独立测试,只是没有进行持久化。 DAO 对象是变化最小的对象,它们都是进行基本的CRUD 操作,在两种模型下的 DAO 对象没有变化。 另外还需要对业务逻辑对象进行改写,虽然Domain Object 包含了基本业务逻辑方 法,但业务逻辑对象还需代理这些方法,修改后业务逻辑对象的代码如下: public class FacadeManagerImpl implements FacadeManager { II 业务逻辑对象依赖的 DAO 对象 pr 工 vate CategoryDAO categoryDAO; private NewsDAO newsDAO; private NewsRev工 ewDAO newsReviewDAO; private UserDAO userDAO; I I. . .此处还应该增加依赖注入 DAO 对象必需的 setter 方法 I I. . .此处还应该增加其他业务逻辑方法 1/ 下面是增加新闻回复的业务方法 public NewsReview addNewsReview(Long news工 d , Str 工 ng content) II 根据新闻 id 加载新闻 News news = newsDao.getNews(news 工 d) ; II 通过 News 的业务方法添加回复 NewsReview review = news.addNewsReview(content); II 此处必须显示持久化消息回复 newsReviewDAO.saveNewsReview(review); return review; 在 Rich Domain Object 的模型中, addNewsReview 方法将放在 News 类中实现,而 业务逻辑对象仅对该方法进行简单的代理,执行必要的持久化操作。 在这里存在一个问题:业务逻辑方法很多,哪些业务逻辑方法应该放在Domain Object 对象中实现,而哪些业务逻辑方法完全由业务逻辑对象实现呢? Rod Johnson 认为,可重 用度高,与 Domain Object 密切相关的业务方法应放在 Domain Object 对象中实现。 业务逻辑方法是否需要由 Domain Object 实现的标准,从一定程序上说明了采用 Rich 414策略~ Domain Object 模型的原因。由于某些业务方法只是专一地属于某个Domain Object,因 此将这些方法由 Domain 0均ect 实现,能提供更好的软件复用,能更好地体现面向对象 的封装性。 Rich Domain Object 模型的各组件之间关系大致如图8.2 所示(贫血模式的组件关系 图与此类似)。 Rich Domain Object 模式 、 、 、 、 、 、 、 ,,,,-v 图 8.2 Rich Domain Object 的组件关系图 这种 Rich Domain Object 模型主要的问题是业务逻辑对象比较复杂,由于业务逻辑对 象需要正面封装所有的DAO 对象,因而难免有大量的DAO 方法(基本的 CRUD) 需要 业务逻辑对象封装。业务逻辑对象封装DAO 方法主要基于如下考虑: • DAO 对象不应该暴露为 Web 层。 • DAO 对象的 DAO 方法必须增加事务控制代码,而事务控制则放在业务逻辑层完成。 为了简化业务逻辑对象的开发, Rich Domain Object 模型可以有如下两个方向的改变: .合并业务逻辑对象与DAO 对象。 ·合并业务逻辑对象和Domain Object。 1. 合井业务逻辅对象与DAO 对象 在这种模型下DAO 对象不仅包含了各种CRUD 方法,而且还包含各种业务逻辑方 法。此时的 DAO 对象,已经完成了业务逻辑对象所有任务,变成了DAO 对象和业务逻 辑对象混合体。此时,业务逻辑对象依赖Domain Object,既提供基本的CRUD 方法, 也提供相应的业务逻辑方法。 下面是这种模式的代码(Domain Object 的实现与前面的Rich Domain Object 模式一 样,此处不再给出): II NewsServ工 ceHibernate继承 HibernateDaoSupport,实现 Newsservice接口 public class NewsServiceHibernate extends HibernateDaoSupport implements NewsService II 此处添加 NewsService 对象依赖的 DAO 对象,以及对应的 setter 方法 II 根据主键加载消息 public News getNews(Long id) 415轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 News news = (News) getHibernateTemplate() .get(News.class , id); if (news == null) { throw new ObjectRetrievalFailureException(News.class , id); } return news; II 保存新的消息 public void saveNews(News news) { getHibernateTemplate() .saveOrUpdate(news); II 根据主键删除消息 public vo工 d removeNews(Long id) getHibernateTemplate() .delete(getNews(id)); II 查找全部的消息 publ工 c List f 工 ndAll () getHibernateTemplate() .find("from News"); II 下面是增加新闻回复的业务方法 public NewsReview addNewsReview(Lo口g newsId , String content) II 根据新闻工d 加载新闻 News news = newsDao.getNews( 口 ewsId) ; II 通过 News 的业务方法添加回复 NewsReview review = news.addNewsReview(content); II 此处必须显示持久化消息回复 newsReviewService.saveNewsReview(review); return review; 正如上面见到的, DAD 对象和业务逻辑对象之间容易形成交叉依赖(可能某个业务 逻辑方法的实现,必须依赖于原来的DAD 对象)。当 DAD 对象被取消后,业务逻辑对 象取代了 DAD 对象,因此变成了一个业务逻辑对象依赖多个业务逻辑对象。而每个业 务逻辑对象都可能包含需要多个DAD 对象协作的业务方法,从而导致业务逻辑对象之 间的交叉依赖。 业务逻辑对象和DAD 对象合并后的组件关系如图8.3 所示。 、 、 、 、 、 、 、 、 、 、 合并 DAO 对象 和业务逻辑对象,,,,,,,,,,,,,,,, ,,,,, v 、 、 、 、 图 8.3 合并 DAD 对象和业务逻辑对象 这种模型也导致了 DAD 方法和业务逻辑方法混合在一起,显得职责不够单一,软 416照 策略~ 件分层结构不够清晰。此外,使业务逻辑对象之间交叉依赖,容易产生混乱,未能做到 彻底的简化。 2. 合并业务逻辑对象和 Domain Object 在这种模型下,所有的业务逻辑都应该被放在 Domain Object 里面,而此时的业务 逻辑层不再是传统的业务逻辑层,它仅仅封装了事务和少量逻辑,完全无需业务逻辑 对象的支持。而 Domain Object 依赖于 DAO 对象执行持久化操作,此处 Domain Object 和 DAO 对象形成双向依赖,这种设计在某些地方也被称为充血模式,但有时会带来相 当大的危险。 在这种设计模式下,几乎不再需要业务逻辑层,而 Domain Object 则依赖 DAO 对象 完成持久化操作,下面是在这种模式下的 News 类代码: public class News extends BaseObject II 此处省略了其他的属性 II 此处省略了属性对应的setter 和 getter 方法 II 增加新闻回复的业务逻辑方法 publ工 c NewsRev工 ew addNewsReview(Str工 ng content) II 以默认构造器创建新闻回复实例 NewsReview review = new NewsReview(); II 设置回复内容 review.setContent(content); II 设置回复的发布日期 review.setPostDate(new Date()); II 设置回复的最后修改日期 review.setLastModifyDate(new Date()); II 设置回复与消息的关联 review.setNews(this); II 直接调用 newsReviewsDao完成消息回复的持久化 newsReviewsDao.save(review); return review; II 此处省略了重写的hashCode. equals 等方法 从上面代码中可以看到,由于Domain Object 必须使用 DAO 对象完成持久化,因此 Domain Object 必须接收 IoC 容器的注入,而 DomainO 均 ect 获取容器注入的 DAO 对象, 通过 DAO 对象完成持久化操作。 合并业务逻辑对象和 Domain Object 后各组件的关系如图 8 .4所示。 这种模型的优点是:业务逻辑对象非常简单,只提供简单的事务操作,业务逻辑对 象无须依赖于 DAO 对象。 但这种模型的缺点也是非常明显的: • DAO 对象和 Domain Object 形成了双向依赖,其复杂的双向依赖会导致很多潜在 的问题。 ·业务逻辑层和 Domain 层的逻辑混淆不清,在实际项目中,极容易导致架构混乱。 .由于使用业务逻辑对象提供事务封装特性,业务逻辑层必须对所有的 Domain Object 的逻辑提供相应的事务封装,因此业务逻辑对象必须重新定义 Domain 417轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 Object 实现的业务逻辑,其工作相当烦琐。 业务逻辑对象 和 Domain Object 合并 、 、 、 、 、 、 、 图 8 .4 合并业务逻辑组件和 DoaminO均 ect 8.4.3 抛弃业务逻辑层 在 Rich Domain Object 模型的各种变化中,虽然努力简化业务逻辑对象,但业务逻 辑对象依然存在,依然使用业务逻辑对象正面封装所有的业务请求。下面介绍更彻底的 简化,即彻底放弃业务逻辑层。 抛弃业务逻辑层也有两种形式: • Domain Object 彻底取代业务逻辑对象。 .由控制器直接调用 DAO 对象。 1. Domain Object 完全取代业务逻辑对象 这种设计模式是充血模式更加激进的演化。由于在充血模式中业务逻辑对象的作用 仅仅只提供事务封装,业务逻辑对象存在的必要性不是很大,因此考虑对Domain Object 的业务逻辑方法增加事务管理,而Web 层的控制器则直接依赖于 Domain Object 。 这种模型更加简化,使 Domain Object 与 DAO 对象形成双向依赖,而 Web 层的控 制器直接调用 Domain Object 的业务逻辑方法。这种模型在有些地方也被称为胀血模式。 这种模型的优点是:分层少,代码实现简单。 但这种模型的缺点也很明显: ·业务逻辑对象的所有业务逻辑都将在 Domain Object 中实现,势必引起 Domain Object 的混乱。 • Domain Object 必须向 Web 层直接暴露,可能导致意想不到的问题。 这种模型与充血模式的缺点相同: Domain 0时 ect 必须配置在 Spring 容器中,接受 Spring 容器的依赖注入。 在这种架构模型下, Domain Object 相当不稳定。如果业务逻辑需要改变, Domain O均 ect 也需要发生改变,而 DAO 对象与 Domain Object 形成双向依赖,这将导致从底层的 Domain Object 和 DAO 对象的修改,使这种架构模式的分层完全失去意义。各层之间以强 418企业…思考与策略~ 祸合方式组合在一起,各层对象互相依赖,牵一发而动全身,几乎是最差的一种策略。 2. 控制器完成业务逻瞌 在这种模型里,控制器直接调用 DAO 对象的 CRUD 方法,通过调用基本的 CRUD 方法,完成对应的业务逻辑方法。这种模型下,业务逻辑对象的功能由控制器完成。事 务则推迟到控制器中完成,因此对控制器的 execute 方法增加事务控制即可。 对于基本的 CRUD 操作,控制器可直接调用 DAO 对象的方法,省略了业务逻辑对 象的封装,这就是这种模型的最大优势。对于业务逻辑简单(当业务逻辑只是大量的 CRUD 操作时)的项目,使用这种模型也未尝不是一种好的选择。 但这种模型将导致控制变得雕肿,因为每个控制器除了包含原有的 execute 方法之 外,还必须包含所需要的业务逻辑方法的实现。极大地省略了业务逻辑层的开发,避免 了业务逻辑对象不得不大量封装基本的 CRUD 方法的弊端。 这种模型也有其缺点: ·因为没有业务逻辑层,对于那些需要多个 DAO 参与的复杂业务逻辑,在控制器 中必须重复实现,其效率低,也不利于软件重用。 • Web 层的功能不再清晰,人为复杂化。 Web 层不仅负责实现控制器逻辑,还需要 完成业务逻辑的实现,因此必须精确控制何时调用 DAO 方法控制持久化。 ·扩大了事务的影响范围。大部分情况下,只有业务逻辑方法需要增加事务控制, 而 execute 方法无须增加事务控制。但如果 execute 方法直接调用了 DAO 对象的 CRUD 方法,则会导致这些方法不在事务环境下执行。为了让数据库访问都在事 务环境下进行,因此不得不将事务范围扩大到整个 execute 方法。这是一种低性 能的做法。 本章小结 本章首先介绍了笔者在架构设计方面的一些经验,从企业应用开发面临的困难讲起, 讲解了面对这些困难时应该采用何种应对策略。 其次介绍了常用的代理模式的使用,并深入介绍了由此衍生出来的 Spring AOP 框架。 最后重点介绍了贫血模型、 Rich Domain Object 模型,以及儿种简化的模型,并分 别分析了几种模型各自的优缺点。 419本意要点 劲 架构设计的基本知识 ¥ 架构设计的原则 3通 Domain Object 的设计 3 设计 DAO 层 3业 设计业务逻辑层 ~ 设计 Web 层 该系统是一个消息发布系统,作为一个示毡,隘的应用,本身并没有特别复杂 的业务逻辑。 注册用户可以发布消息,回复消息,是一种优秀的J2EE 架构。 系统以 Spring 框架为核心,向下整合 Hibernate 进行持久层访问;向上整合 Struts 按清晰的 MVC 模式控制,可以清晰划分应用的层次,提高系统灵活性,提 高代码的可扩展、可维护及可复用性等。 本系统示范了一种非常优秀的 J2丑E 应用架构,并涉及到如下开源框架: Hibernate 、 Spring 及 Struts 等。系统的结构清晰、灵活,具有很高的伸缩性,完 全能面对复杂多变的业务需求。tl: …统「国 9.1 系统架构说明 本系统不仅严格按 MVC 模式设计,还按J2 EE 分层设计,将中间层严格分成业务逻 辑层、 DAO 层及数据持久层等。 MVC 层的控制器绝对禁止持久层访问,甚至不参与业 务逻辑的实现。 表现层采用传统 JSP 技术,但页面禁止使用 JSP 脚本,从而可以避免将 JSP 页面变 得凌乱。 JSP 技术结合 Struts 的标签库,让应用的表现层层次清晰,可读性极好。 9.1.1 系统架构说明 本系统采用的是典型的J2EE 三层结构,分为表现层、中间层(业务逻辑层)和数 据服务层。三层体系将业务规则、数据访问及合法性校验等工作放在中间层处理。客户 端不直接与数据库交互,而是通过组件与中间层建立连接,再由中间层与数据库交互。 该系统的表现层是传统的 JSP 技术, JSP 技术自 1999 年问世以来,经过多年的发展, 其广泛的应用和稳定的表现,为其作为表现层技术打下了坚实的基础。 中间层采用的是流行的 Spring+Hibernate ,为了将控制层与业务逻辑层分离,又细分 为以下几种。 Web 层,就是 MVC 模式里面的 交互,调用业务逻辑层,并将业务数据返回给表现层作组织表现,该系统的 MVC 框架 采用 Struts 。 Service 层(就是业务逻辑层),负责实现业务逻辑。业务逻辑层以 DAO 层为基础, 通过对 DAO 组件的正面模式包装,完成系统所要求的业务逻辑。 DAO 层,负责与持久化对象交互。该层封装了数据的增、删、查、改的操作。 PO ,持久化对象。通过实体关系映射工具将关系型数据库的数据映射成对象,很方 便地实现以面向对象方式操作数据库,该系统采用 Hibernate 作为 ORM 框架。 Spring 的作用贯穿了整个中间层,将 Web 层、 Service 层、 DAO 层及 PO 无缝整合, 其数据服务层用来存放数据。 9.1.2 采用架构的优势 不可否认,对于简单的应用,采用 ASP 或者 PHP 的开发效率比采用J2EE 框架的开 发效率要高。甚至有人会觉得:这种分层的结构,比一般采用 JSP + Servlet 的系统开发 效率还要低。 笔者从以下几个角度来阐述这个问题。 ·开发效率:软件工程是个特殊的行业,不同于传统的工业,如电器、建筑及汽车 等行业。这些行业的产品一旦开发出来,交付用户使用后将很少需要后续的维护。 421轻量级 J2EE 企业应用实战 Struts+Spring+Hibernate 整合开发 但软件行业不同,软件产品的后期运行维护是个巨大的工程,单纯从前期开发时 间上考虑其开发效率是不理智的,也是不公平的。众所周知,对于传统的 ASP 和 PHP 等脚本站点技术,将整个站点的业务逻辑和表现逻辑部混杂在 ASP 或 PHP 页面里,从而导致页面的可读性相当差,可维护性非常低。即使需要简单改变页 面的按钮,也不得不打开页面文件,冒着破坏系统的风险。但采用严格分层J2EE 架构,则可完全避免这个问题。对表现层的修改即使发生错误,也绝对不会将错 误扩展到业务逻辑层,更不会影响持久层。因此,采用J2EE 分层架构,即使前 期的开发效率稍微低一点,但也是值得的。 ·需求的变更:以笔者多年的开发经验来看,很少有软件产品的需求从-开始就完 全是固定的。客户对软件需求,是随着软件开发过程的深入,不断明晰起来的。 因此,常常遇到软件开发到一定程度时,由于客户对软件需求发生了变化,使得 软件的实现不得不随之改变。当软件实现需要改变时,是否可以尽可能多地保留 软件的部分,尽可能少地改变软件的实现,从而满足客户需求的变更?答案 是一一采用优秀的解藕架构。这种架构就是J2EE 的分层架构,在优秀的分层架 构里,控制层依赖于业务逻辑层,但绝不与任何具体的业务逻辑组件藕合,只与 接口楠合:同样,业务逻辑层依赖于 DAO 层,也不会与任何具体的 DAO 组件相 合,而是面向接口编程。采用这种方式的软件实现,即使软件的部分发生改变, 其他部分也尽可能不要改变。 注意:即使在传统的硬件行业,也有大量的接口规范。例如, PCl 接口、显卡或者 网卡,只要其遵守 PCl 的规范,就可以插入主板,与主板通信。至于这块卡内部的实现, 不是主板所关心的,这也正是面向接口编程的好处。假如需要提高电脑的性能,需要更 新显卡,只要更换另一块 PCl 接口的显卡,而不是特整台电脑抛弃。如果一台电脑不是 采用各种接口组合在一起,而是做成整块,那将意味着即使只需要更新网卡,也要放弃 整台电脑。同样,对于软件中的一个个组件,当一个组件需要重构时,尽量不要影响到 其他纽件。实际上,这是最理想的情况,即使采用目前最优秀的架构,也会有或多或少 的影响,这也是软件工程需要努力提高的地方。 ·技术的更新,系统重构:软件行业的技术更新很快,虽然软件行业的发展不快, 但小范围的技术更新特别快。一旦由于客观环境的变化,不得不更换技术时,如 何保证系统的改变最小呢?答案还是选择优秀的架构。 在传统的 Modell 的程序结构中,只要有一点小的需求发生改变,将意味着放弃整 个页面或者改写。虽然前期的开发速度快,除非可以保证以后永远不会改变应用的结构, 否则不要采用 Modell 的结构。 9.2 Hibernate 持久层 采用 Hibernate 作为持久层技术的最大的好处在于:可以完全以面向对象的方式进行 系统分析、系统设计。面向对象的分析和面向对象的设计才最接近于程序员的自然思维。 422i 息发系统 E Hibernate 的功能十分强大,对于 Hibernate 的介绍,在这里仅介绍与本节内容相关的技 术,如需了解 Hibernate 的详细情况,请参考 Hibernate 的官方文档和相关书籍。 9.2.1 编写 PO 类 下面介绍系统的类图,在开发过程中可以根据类图,生成关系型数据库表;或者先 把数据库的业务表设计好,再通过工具生成对象。有很多设计工具都可以实现以上功能, 如 PowerDesigner 等。但从面向对象分析与设计来讲,推荐使用第一种方法,因为更贴 近面向对象的思想。映射配置文件写好以后,我们也可以用 Hibernate 生成数据库的表。 图 9.1 显示了本系统 PO 的类图。 Serializablel BaseObject Setializable User +Category +equals:B∞lean +hashCode:int +toString:String id:Long name:String news:Set +NewsReview +equals:Boolean +hashCode:int +toString:Sting content:String id:Long lastmbdityDate:Dete news:News postdate:Date poster:User +News +equals:Boolean +hashCode:int +toString:String newsReviews:Set catego叩 Category lastMbdifyDate:Date postdate:Date poster:User cpmtemt:String id:Long lille:String 图 9.1 系统 PO 的类图 -seriaIVersionUID:lon且 +User +equals:Boolean +hashCodeing +toString:String Password:String Use町、a 町、 e:String 从类图可以看出,根据要实现的功能,系统的模型 Model 实现类有四个: Category, News, NewsReview 和 User ,它们均继承父类 BaseObject ,都是普通的 Jav aB ean ,下面是 423轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 四个基本的 Persisent Object 类。 • News: 封装了一条消息。包括标题、内容、发布时间及发布人等。 • Category: 封装了一个消息分类。 • User: 封装了一个用户的信息。 • NewsReview: 封装了一条消息评论。 其中, News 有一个 Category 类型的成员变量及 User 类型的成员变量; NewsReview 有一个 News 类型的成员变量和一个 User 类型的成员变量。为了能够使用双向关联 (Hibernete 的映射功能,稍后解释), Category 有一个集合型成员变量,用于存放与这个 Category 对象有关联的 News 对象:同样 News 也有一个集合型成员变量,用于存放与这 个 News 对象有关联的 NewsReview 对象。 实际上,持久化就是通过成员变量来映射关系数据库里的l-N 和 N-N 的关系。 下面是 PO 父类 BaseObject 的代码: II 将父类声明为 abstract 类 public abstract class BaseObject implements Serializable IIPO 推荐实现的 toString方法 public abstract String toString(); IIPO 推荐实现的 equals 方法 public abstract boolean equals(Object 0); IIPO 推荐实现的 hashCode 方法 publ工 c abstract int hashCode(); 父类 BaseObject 是一个抽象类,定义了三个抽象方法toStringO , equalsO 和 hashCodeO,这三个方法是Hibernate 推荐持久化对象时重写的。 关系数据库里的记录可以由主键来唯一标识,但是用什么标准来标记两个对象 "相等"呢?两个对象相等与否的判断结果很可能影响到数据的完整性。如果在 Hibernate 无法自行制定两个对象相等与否的标准时,则需要用户自行定义即重写 equalsO方法。 如果希望将持久化对象放进Set 里,或者重新接管己脱管(detached) 的持久化对象, 则必须重写 equalsO 和 hashCode。这两个方法,因为Java 语法要求如果两个对象相等, 那么它们的 hashCodeO 返回值必须相等,因此该方法也需要重写。另外,toStringO方法 用于给出该对象的描述信息,因此也推荐重写该方法(Hibernate 井没有硬性规定)。此 外, BaseObject还实现了 Serializable接口,该接口是持久化对象推荐实现的。 让其他类继承这个抽象类只是一个可选的写法,至少直接让其他类重写equalsO和 hashCodeO方法来实现 Serializable 接口也是可以的。 下面具体来看News 类。 1** * @hibernate.class table="news" * @struts.form include-all="false" extends="BaseForm" *1 public class News extends BaseObject 424完整…发布系统国 II 标识属性 private Long 工 d· II 消息标题 private String title; II 消息内容 pr 工 vate Str 工 ng content; II 用于关联发布人,对应另一个持久化类 pr工vate User poster; II 发布日期 pr 工飞rate Date postDate; II 最后一次回复日期 private Date lastModifyDate; II 消息分类,用于关联另一个持久化类 pr工 vate Category category; II 用于关联消息回复 pr 工vate Set newsRev工 ewsi II 无参数的构造器 public News () 1** *用于获取与此消息相关的全部回复 * @return 返回消息的全部回复 *1 public Set getNewsReviews() return newsReviews; 1** *设置消息关联的回复 * @param newsReview 消息关联的全部回复 public void setNewsReviews(Set newsReviews) this.newsReviews = newsReviews; 1* 食 *返回消息所属的种类 * @return 消息所属的种类 * @hibernate.many-to-o口 e column="id" not-null="true" *1 publ 工 c Category getCategory() return category; 1** *设置消息所在的种类 * @param 消息所属的种类 *1 public void setCategory(Category category) this.category = category; 1** *返回消息的最后评论日期 * @return 消息的最后评论日期 * @hibernate.property column="last_modifY_date"口ot-null="true" *1 public Date getLastModifyDate() 425h 426 轻量级 J2EE 企业应用实战一-Struts+Spring+Hibemate整合开发 return lastModifyDate; /** *设置消息的最后评论时间 * @param 消息的最后评论日期 */ public void setLastModifyDate(Date lastModifyDate) this.lastModifyDate = lastModifyDate; /** *返回消息的发布日期 * @return 消息的发布日期 * @hibernate.property column="post_date" not-null="true" */ public Date getPostDate() return postDate; /** *设置消息的发布日期 * @param 消息的发布日期 */ public void setPostDate(Date postDate) this.postDate = postDate; /** *返回消息的发布者 * @return 消息的发布者 * @hibernate.many-to-one colum且 =llusername" not-null="true" */ public User getPoster() return poster; /** *设置消息的发布者 * @param 消息的发布者 * @hibernate.many-to-one column="username" not-null="true" */ public void setPoster(User poster) this.poster = poster; /** *返回消息的内容 * @return 消息的内容 * @hibernate.property column="content" length="3000" not-null="true" */ public String getContent() return content; /** *设置消息的内容 * @param 消息的内容完时…统 E */ public void setContent(String content) this.content = content; /** *返回消息的 id * @return 消息的 id * @hibernate. id column=" id" generator-.class="increment" * unsaved-value="null" */ public Long getld() return id; /** *设置消息的 id 食自param 消息的 id */ public void setld(Long id) this.id = id; /** *返回消息的标题 * @return 消息的标题 * @hibernate.property column="title" length="50" 口ot-null="true" */ public String getTitle() return title; /** *设置消息的标题 * @return 消息的标题 */ public void setTitle(String title) this.title = title; /** * 实现抽象父类 BaseObject 的 equals 方法 * @see java. lang.Object#equals (Object) */ public boolean equals(Object object) if (! (object instanceof News)) return false; } News rhs = (News) object; return this.poster.equals(rhs.getPoster()) && this.postDate.equals(rhs.getPostDate()); /** * 实现抽象父类 BaseObject 的 hashCode 方法 * @see java.lang.Object#hashCode() */ 427轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 public int hashCode() { return this.poster.hashCode() + this.postDate.hashCode(); /** * 实现抽象父类 BaseObject 的 hashCode 的方法 * @see java.lang.Object#toString() */ public String toString() { return new ToStringBuilder(th工 s) . append (" id" , this. id) . append (" title" , this.title) .append("postDate" , this.postDate) .append("content" , this.content) .append("lastModifyDate" , this.lastModifyDate) .append("poster" , this.poster) . append (" category" , this. category) . append ("newsRev工 ews l • , this.newsReviews) .toStr工 ng() ; Hibernate 对 PO 几乎不作任何要求,一般的 POJO (Plain Old Java Object) 就可以充 当 POo 只要有一个默认的不带参数的构造器就可以。推荐实现Serializable 接口时,最 好重写 equalsO 和 hashCodeO 方法。 所谓的 POJO 就是只有属性,以及属性对应 getter 和 setter 方法的 Java 对象。 下面将解释各个属性的含义(后面的映射配置文件会有进一步探讨):每个News 对 象就相当于数据库表里的一条记录,其中 id 属性映射的是记录的主键: title 与 content 分别是消息的标题和内容; postDate 是发布时间; lastModifyDate 是最后评论时间: categ。可则是 News 关联(在数据库里通过外键关联)的 Categ。可对象; newsReviews 则是 News 对象关联的所有 NewsReview 对象。 细心的读者会发现,上面的代码重写 equalsO 和 hashCodeO 方法时,并不是采用 id 作为判断这两个对象相等的标准。这与 Hibernate 的机制有关,因为对象的标识属性值由 Hibernate 负责生成, Hibernate 仅对已经持久化的对象分配标识属性值,未被持久化的对 象是没有标识属性值的。如果在 Set 里面的一个新建对象未被持久化,此时持久化该对 象并分配主键。由于 equalsO 和 hashCodeO 方法都基于主键,因此, hashCode 得出的 结果会改变,在 Set 里面就可能起冲突。 因此建议使用"业务键"来作为 equalsO 和 hashCodeO 方法的基准。"业务键"指 的是能在真实世界里区分两个对象的属性。例如, News 就采用了发布人 (poster) 跟发 布时间 (postDate) 的组合作为业务键,因为同一用户不可能在同一时间发布多条消息。 关于如何选择业务键的更多信息请参照 Hibernate 官方文档。 注意:除了由用户自己生成 equalsO 和 hashCodeO 方法外,也有一些工具可以自动 生成这两个方法。例如, Eclipse 插件 cornmonclipse。有兴趣的读者可以尝试一下 O 9.2.2 编写 PO 的映射配置文件 仅有一个 POJO 是无法完成数据库的持久化操作的,还必须为 POJO 增加映射文件。 428}息发系统~ 当 POlO 增加映射文件后,可以完成 OIR Mapping ,从而在某个特定对象的管理下完成 数据库访问。 因此必须为这个 POlO 配上一个映射文件,通常将这个映射文件命名为:类名 .hbm. xml ,并与这个类放置在同一目录下。 News.hbm.xml 的具体内容如下: <'一 映射消息标题属性一〉 @hibernate.list lazy="true" 工 nverse="fals巴" cascade="口one" @hibernate.collection-key colum且 ="id ll @h工bernate.collection-one-to-manyclass="org.yeeku.model. NewsReview" <0口e-to-many class="org.yeeku.model.NewsReview" /> 429轻量级 J2EE 企业应用实战一一-St阳 ts+Spring+Hibernate 整合开发 映射文件根元素 hibernate-mapping r面可以有多个 class 子元素,即在一个映射文 件里可以配置多个映射对象,但为了清晰起见,建议为每个类单独写一个映射配置文件。 class 的 name 属'性就是要映射的对象类; table 是数据库里对应的表名; class 下面的 子元素就是这个类的属性并与数据库里的宇段相对应。 id 用于唯一标识该对象,称为标识属性。其中 name 属性是类里面的属性名; column 是数据库的对应宇段。另外, id 还有 generator 子元素,用于指定主键生成方式,这里用 的是自动增长生成(increment) 策略,其他方式请参照 Hibernate 文档。 这里重点介绍 l-N (set 是其中一种方式)和 N一 1 (many-to-one,通常就是外键关联) 的映射。首先来看 l-N 关系,一条消息对应多条评论。 name 属性是指类里面存放 "N" 的一方对象的属性。 meta 一栏这里只是用作为阅读者提供额外信息。 key 就是 "N" 的 一方存放 "1" 的一方外键的字段, one-to-many 的 class 是 "N" 的一方的映射对象。 注意: lazy 属性与 inverse 属性的设置。 lazy 是 Hibernate 的一种延迟加载数据策略, 比如说一个 News 对象与多个 NewsReview 对象关联,如果使用 lazy 策略,则只有当 NewsReview 对象被请求时(如调用 News 的 getNewsReviews 方法)才会从数据库里读 取相应记录,从而达到优化性能的目的。特别是系统如果非常复杂,关联很多时,不使 用延迟加载将会对性能有比较大的影响。当然使用延迟加载还有很多问题需要注意,这 里为了方便起见设为 false ,有兴趣的读者可以参考 Hibernate 官方文档。 inverse 属性标 明该元素是否拉制关联关系,对于 l-N 的关系,通常推荐由 "N" 的一端拉制关联关系, 因此 set 元素通常都应包含 inverse= "true" 属性。 N-l 的配置则比较简单,与普通属性的配置一样,依此类推将所有映射文件完成, 可以参照 src/org/yeeku/modellNewsReview.hbm.xm1, Category.hbm.xm1和 User.hbm.xm1。 完成后可以通过配置 Hibernate 的属性(数据库连接驱动,数据库方言,登录用户名与密 码等)自动生成数据库的表,这里不再赘述。 注意:除了手工编写以外,开发者还有一些工具可以帮助生成映射配直文件。例如, Xdoclet 就可以用来生成 Hibernate 和 Struts 等配直文件,只需在编写源代码时在适当的 地方加上注释,再用 Xdoclet 生成即可。 具体例子参看上面的 News 类源码,类定义上包含如下注释: @h工 bernate.class table="news" 标明了该类映射的数据库表是news。所有的 getter 方法上面的注释里都有类似的注 释,用来标明属性映射的宇段。例如: @hibernate.property column="content" length="3000" not-null="true" 有兴趣的读者可以对照News.hbm.xm1或者直接参考Xdoclet 的文档。 430完叫…~ 9.2.3 连接数据库 除了前面介绍的 POlO 和映射文件之外, Hibernate 不知道与哪个数据库连接,也不 知道连接数据库时需要哪些属性。因此, Hibernate 控制数据库连接提供了两种方式: ·采用 hibernate.properties 属性文件。 ·采用 hibernate.cfg.x m1配置文件。 这两种方式只是形式上的区别,实质的内容没有任何改变。都需要指定连接数据库 的 URL 、数据库的驱动、用户名及密码等基本信息。如果需要使用连接池,则还应确定 连接池配置信息。 1.直接 Hibernate 的配置连接 直接 Hibernate 的配置连接就是使用 hibernate.cfg.x m1文件,关于使用 hibernate. properties 的方式与此类似,不再赘述。 下面是使用。 PO 连接池的 hibernate.cfg.x m1配置文件的源代码: jdbc:mysql://localhost/j2ee <'一 配置连接数据库的用户名一〉 root 32147 com.mysql.jdbc.Driver <'一 配置连接数据库的方言一〉 org.hibernate.dialect.MYSQLDialect 500 2 5000 2 true 431轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 Hibernate 通过上面的配置文件,知道如何控制与数据库的连接,以及采用连接池的 方式。 2. 采用 Spring 管理 Hibernate 让 Spring 管理 Hibernate 时,必须将 Hibernate 的 SessionFactory配置在 Spring 容器 内,此时有两种做法: ·在 Spring 容器中配置 SessionFactory。 ·让 hibernate.cfg.xml文件控制 SessionFactory。 这两种做法的效果相似,都是利用Spring 的 LocalSessionFactoryBean,负责产生 Hibernate 的 SessionFactory,该 bean 作为其他持久化访问组件的属性注入。 下面是完成在Spring 容器配置 Hibernate 的 SessionFactory 的形式,该项目采用以下 方式: <'一配置数据源,使用DBCP 数据源 > 本系统使用的数据源是DBCP 的数据源,DBCP 数据源是来自 Apache 组织的一个优 秀的数据源。 432实… DBCP 数据源有四个属性值得注意。 • driverClassName: 数据库驱动,通常由第三方提供。 • url: 数据库 U虹,不同数据库的写法存在差异。 • username: 数据库的用户名。 • password: 数据库的密码。 配置不同的数据库时,这四个属性通常需要改动。 driverClassName 由数据库厂商提 供, url 的写法也由厂商决定,请参照数据库的文档。这里的数据源使用的是 MySQL , 连接的是 newsboard 数据库。 另外, Spring 提供了对 Hibernate 的 SessionFactory 的管理,只需在 applicationContext. xml 文件中增加如下配置: org.hibernate.dialect. MySQLDBDialect 其中就是我们刚才配置的数据源。 mappingResources 是我们编写好的映射配置文件。除此之外还需要在hibernateProperties 里配置 Hibernate 访问数据库使用的方法,到此为止,己经基本完成了 Spring 与 Hibernate 的整合。 如果需要让 hibernate.cfg.xml 文件自己负责Hibernate SessionFactory 的配置,可以采 用如下方式:同样使用 LocalSessionFactoryBean 生成 SessionFactory ,但此时无须确定数 据库用户名及密码等具体信息,只需在配置文件中确定hibernate.cfg.xml 文件的位置。 配置代码如下: 433轻量级 J2EE 企业应用实战一一-Struts+Spring+Hibernate 整合开发 classpath:h 工 bernate.cfg.xml 9.3 DAD 组件层 业务逻辑层组件依赖于持久层组件,而持久层组件则提供数据表的基本 CRUD 操作。通过持久层组件,使业务逻辑层组件的实现与特定数据库访问分离,从而提高 系统的解祸。 9.3.1 DAO 组件的结构 通过第 8 章关于系统架构的介绍可知, DAO 模式需要为每个 DAO 组件编写 DAO 接口,同时至少提供一个实现类,根据不同需要,可能有多个实现类。 为了让逻辑组件与具体的 DAO 组件分离,还必须有一个 DAO 工厂。 图 9.2 是本系统的 DAO 组件接口的类图。图 9.3 是本系统 DAO 组件实现类的类图。 此处的实现类都是基于 Hibernate 的实现。 interface DAO hibernate +UserDAOHibernate +getObjects:L +NewsReviewDAOHiber +getObject:Ob +NewsDAOHibernate +saveObject:v +BaseDAOHibernate +removeObject +CategoryDAOHiberna 7'} "v-\与芒 = interface interface interface interface CategoryDAO NewsDAO NewsReviewDAO UserDAO +getCategory:Ca +getNews:New! +getNewsReview:NewsR +getUser: Use +saveCategory: v +saveNews: vo +saveNewsReview:void +getUsers:Li +removeCategory +removeNews: 1 +removeNewsReview:vo +saveUser: vo +removeUser: categories:Lis 图 9.2 DAO 组件接口的类图 434息… E H 工 bernateDaoS DAO BaseDAOHibern #log:Log +saveObj ect: v' +getObject:Ob +getObjects:L +removeObject A UserD1'J 。 Category NewsDA NewsReviewr』叫 UserDAOHiber:d CategoryDAOHibe NewsDAOHibern NewsReviewDAOHibe +getUser: Usel +getCategory:Ca +getNews:News +getNewsReview:Newsl +getUsers: Lh +saveCategory:v +saveNews:vo工 +saveNewsReview:voi +saveUser:voj +removeCategory +removeNews:v +removeNewsReview:v +removeUser:飞 categories:Lis 图 9.3 DAO 组件实现类的类图 9.3.2 编写 DAO 接口 DAO 接口包类里有 5 个 DAO,其中 DAO 是一个具有一般性的DAO 接口,其他 DAO 接口都继承这个DAO。这里选择DAO 与 NewsDAO作为示例。 DAO 接口的代码如下: publ工 c interface DAO { II 返回全部的物品 public List getObjects(Class class); II 根据 id 返回某个持久化类 public Object getObject(Class class, Serializable id); II 保存持久化类 public void saveObject(Object 0); II删除某个持久化类 public void removeObject(Class class , Serializable id); 下面对各个方法进行简要说明。 • getObject 是获取所有该类型对象的方法。输入参数是某个类的类型,相当于取出 一个表所有行的 sql 语句。 • getObject 封装的是读取单条记录的操作。 • saveObject 封装的是插入或更新单条记录的操作。插入或更新取决于该对象是否 已被持久化过。 435轻量级 J2 巨 E 企业应用实战 Struts+Spring+Hibernate 整合开发 • removeObject 封装的是删除单条记录的操作。 由此可见, DAO 的实质就是对数据表的 CRUD 原子操作。 一般的 DAO 的实现要求用到 Java 的反射机制,因此对于初学者不容易实现。但实 现之后可以通过实现类对任何 PO 进行操作。通常,我们用每个类各自的 DAO 对该类进 行 CRUD 操作。 NewsDAO 接口的代码如下: public interface NewsDAO extends DAO II 根据 id 返回 News 持久化类 publ 工 c News getNews(Long news工 d) ; II 保存消息 public void saveNews(News news); II 删除消息 public void removeNews(Long news工 d) ; 可以看到 NewsDAO 里并没有封装取出所有 News 的操作,其实是否封装该操作取 决于业务需求。在这个系统里面没有取出所有 News 的需求,因此可以不封装。如果要 考虑扩展系统,也可以先封装,在扩展时使用。 注意:接口的使用在这里起了很重要的作用 O 接口就是定义与实现的分离,在一个 面向对象的系统中,其功能是由很多对象协作完成的。各个对象是如何实现的,对系统 设计人员来讲是透明的;而各个对象之间的协作关系则成为系统设计的关键。小到不同 类之间的通信,大到各模块之间的交互,在系统设计之初都要着重考虑,这也是系统设 计的主要工作内容。然而通过多态引入接口可以降低对象之间的依赖,解除辑合。很多 经典的设计模式都离不开接口,因此,更深层次理解接口是提高技术水平的必不可少的 步骤,有兴趣的读者可以参考相关设计模式方面的书籍。 下面依次是 UserDAO 、 CategoryDAO 及 NewsReviewDAO 的源代码: UserDAO 的源代码: public interface UserDAO extends DAO{ 1** *根据用户名获取User * @param username 需要访问的用户名 * @return 用户名对应的用户 *1 public User getUser(String username); 1** *保存用户 * @param 需要保存的用户 *1 public void saveUser(User user); 1** *根据用户名,删除用户, * @param username 需要删除用户的用户名 *1 public void removeUser(String username); 436发布系统~ CategoryDAO 的源代码: public interface CategoryDAO extends DAO { /** *根据 id 获取种类 * @parameter category工 d 需要加载的种类id * @return 种类 id 对应的种类 */ public Category getCategory(Lo口g categoryld); /** *保存消息分类 * @parameter category 需要保存的消息分类 */ public void saveCategory(Category category); /** *根据消息 id 删除消息分类 * @ categoryld 需要删除的消息分类 id */ public void removeCategory(Long category工 d) ; /** *获取全部消息分类 * @return 返回数据库中全部消息分类 */ public List getCategories(); NewsReviewDAO的源代码: public interface NewsReviewDAO extends DAO{ /** *根据回复工d 获取消息回复 * @return id 对应的消息回复 */ public NewsReview getNewsReview(Long newsReviewld); /** *保存特定的消息回复 * @parameter newsReview需要保存的消息回复。 */ public void saveNewsReview(NewsReview newsReview); /** *根据回复工d 删除消息回复 * @par皿eter newsReviewld 需删除的消息回复 id */ public void removeNewsReview(Long newsReviewld); 9.3.3 编写 DAO 的具体实现 编写 DAO 的具体实现在这里也很简单,下面是 NewsDAO 接口的 Hibernate 实现的 源代码: NewsDAOHibernate 继承 BaseDAOHibernate. 实现了 NewsDAO public class NewsDAOHibernate extends BaseDAOHibernate implements NewsDAO 437轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 II 根据主键查找 News 持久化对象 public News getNews(Long id) News news = (News) getHibernateTemplate() .get(News.class, id); II 如果不能力日载该持久化对象,抛出异常 if (news -- null) throw new ObjectRetrievalFailureException(News.class , id); return news; II 保存消息 public void saveNews(News news) getHibernateTemplate() .saveOrUpdate(口ews) ; II 根据主键删除消息 public void removeNews(Long id) getHibernateTemplate() .delete(getNews(id)); • NewsDAO 的 Hibernate 实现继承了 BaseDAOHibernate ,下面是 BaseDAOHibernate 的源代码: public class BaseDAOHibernate extends HibernateDaoSupport implements DAO II 提供日志功能 protected final Log log = LogFactory.getLog(getClass()); II 保存对象 public void saveObject(Object 0) getH工 bernateTemplate().saveOrUpdate(o); II 根据持久化类的类,主键获取对象 publ工 c Object getObject(Class clazz , Serial 工 zable id) Object 0 = getHibernateTemplate() .get(clazz, id); II 如果没有获得该对象 if (0 -- null) throw new ObjectRetrievalFa工 lureException(clazz, id); return 0; II 查找某个持久化类的全部实例,查找到数据库对应表的全部记录 public List getObjects(Class clazz) return getHibernateTemplate() .10adAll(clazz); II 根据主键,删除某个持久化类的特定实例,对应删除数据库的特定记录 public void removeObject(Class clazz , Serializable 工 d) getH 工 bernateTemplate() .delete(getObject(clazz, id)); 438完整实…布系统 E BaseDAOHibernate 就是前面所说的一般 DAO 接口的实现,里面附加了日志 Clog) 功能,增强了系统的可维护性和可管理性。这里的 DAO 实现都很简捷,只需要简单的 一行代码就分别实现 CRUD 操作。这得益于 Spring 的强大功能: Spring 提供了 HibernateDaoSupport 工具类以及 HibernateTemplate ,其中 HibernateTemplate 允许以模板 化方式的操作访问数据库。 注意:前面提到 DAO 需要封装数据源,而在这个 DAO 里面我们感觉不到数据源的 存在,这也得益于 Spring 和 Hibernate 的封装,对数据源的访问放在配直文件里管理。 下面依次是 UserDAO 、 CategoryDAO 和 NewsReviewDAO 实现类的源代码。这些实 现类都基于 Spring 的 Hibernate 支持,因此非常简单。 UserDAOHibernate 的源代码如下: public class UserDAOHibernate extends BaseDAOHibernate implements UserDAO /** *根据用户名获取User *由param username 需要访问的用户名 * @return 用户名对应的用户 */ public User getUser(String username) { User user = (User) getH工 bernateTemplate().get(User.class, username); if(user ==口ull) { log.warn("user '" + username + '" not found..."); return user; /** *保存用户 * @param 需要保存的用户 */ public void saveUser(final User user) { if (log.isDebugEnabled()) { log.debug("user's id: " + user.getUsername()); getHibernateTemplate() .saveOrUpdate(user); // necessary to throw a Data 工 ntegrityV工 olat 工 on and catch it 工口 UserManager getHibernateTemplate() .flush(); /** *根据用户名,删除用户, * @param username 需要删除用户的用户名 */ public void removeUser(String username) { getH 工 bernateTemplate() .delete(getUser(username)); CategoryDAOHibernate的源代码如下: public class CategoryDAOHibernate extends BaseDAOHibernate implements CategoryDAO { 439轻量级 J2EE 企业应用实战--Struts+Spring+Hibernate 整合开发 /** *根据 id 获取种类 * @parameter categoryId 需要加载的种类id * @return 种类 id 对应的种类 */ public Category getCategory(Long id) { Category category = (Category) getHibernateTemplate().get( Category.class , id); if (category == null) { throw new ObjectRetrievalFailureException(Category.class, id); return category; /** *保存消息分类 * @parameter category 需要保存的消息分类 */ public void saveCategory(Category category) { getHibernateTemplate() .saveOrUpdate(category); /** *根据消息 id 删除消息分类 * @ categoryId 需要删除的消息分类 id */ public void removeCategory(Long id) { getHibernateTemplate() .d~lete(getCategory(id)); /** *获取全部消息分类 * @return 返回数据库中全部消息分类 */ public List getCategories () { return getHibernateTemplate() .find("from Category"); 440 NewsReviewDAOHibernate 的源代码如下: public class NewsReviewDAOHibernate extends BaseDAOHibernate implements NewsReviewDAO /** *根据回复 id 获取消息回复 * @return id 对应的消息回复 */ public NewsReview getNewsReview(Long id) { NewsReview newsReview = (NewsRev 工 ew) getHibernateTemplate() .get( NewsReview.class, id); if (newsReview --口ull) { throw new ObjectRetrievalFailureException(NewsReview.class, id); return newsReview; *保存特定的消息回复 * @parameter newsReview需要保存的消息回复 */ public void saveNewsReview(NewsReview newsReview) {完整实例:消息发布系统 川国 getHibernateTemplate() .saveOrUpdate(口ewsReview) ; /** *根据回复 id 删除消息回复 * @parameter newsReviewId需删除的消息回复id */ public void removeNewsReview(Long id) { getHibernateTemplate() .delete(getNewsReview(工 d)) ; 9.3.4 用 Spring 容器代替 DAO 王厂 通常情况下,引入接口就不可避免需要引入工厂来负责 DAO 组件的生成。但根据 第 5 章的介绍可知, Spring 实现了两种基本模式:单态模式和工厂模式。而使用 Spring 可以完全避免使用工厂模式,因为 Spring 就是个功能非常强大的工厂。因此,完全可以 让 Spring 充当 DAO 工厂。 由 Spring 充当 DAO 工厂时,无须程序员自己实现工厂模式,只需要将 DAO 组件配 置在 Spring 容器中,由 ApplicationContext 负责管理 DAO 组件的创建即可。借助于 Spring 提供的依赖注入,其他组件甚至不用访问工厂,一样可以直接使用 DAO 实例。 正如在工厂模式里必须要提供接口的实现类一样,此处的配置必须提供实现 类,而不能仅提供接口,下面的配置代码是在 applicationContext.xml 里面配置了 DAO 组件。 在上面配置的 property 子元素里,引用了 Hibernate 的 SessionFactory。其中, SessionFactory 负责产生 Hibernate Session 0 Hibernate 的持久化操作必须在 Session 管理 下完成。 NewsDAOHibernate 实现类并没有提供 setSessionFactory 方法,该方法由其父 类 HibernateDaoSupport 提供,用于为 DAO 组件依赖注入 SessionFactory 。 依此类推,编写其他几个 PO 的 DAO 接口和 Hibernate 实现,将它们配置在 Spring 的容器中。 通常建议将 DAO 组件以单独的配置文件配置,下面是 daoContext.xml 文件的源代 码,该文件负责配置所有的 DAO 组件,并使用了继承简化 DAO 组件的配置: <'一配置普通的DAO 组件: NewsReviewDAO--> 至此,我们已经完成了Spring 与 Hibernate 的整合。 9.4 业务逻辑层 业务逻辑层建立在DAO 层之上,由业务逻辑组件对DAO 组件进行 Facade 封装。 为了分离业务逻辑层与DAO 层之间的稿合,业务逻辑层应面向接口编程,即业务逻辑 组件只调用 DAO 组件的接口,而不与具体的实现类精合,同时将业务逻辑放在接口中 定义。使 Web 层仅仅与业务逻辑组件的接口辑合,而无须理会业务逻辑组件的实现。 9.4.1 业务逻辑组件的结构 业务逻辑组件同样分为接口和实现类两个部分,接口用于定义业务逻辑组件,定义 业务逻辑组件必须实现的方法是整个系统运行的核心。 在应用中需要多少个业务逻辑组件,往往取决于系统的大小。通常按模块来设计业 务逻辑组件,每个模块设计一个业务逻辑组件,并且每个业务逻辑组件以多个 DAO 组 件作为基础,从而实现对外提供系统的业务逻辑服务。 图 9 .4显示了业务逻辑组件的接口类图。 9.4.2 业务逻辑组件的接口 增加业务逻辑组件的接口,也是为了提供更好的解藕。通过面向接口编程,控制器 无须与具体的业务逻辑组件藕合,而是面向接口编程。假如需要改变业务逻辑的实现时, 可以只提供新的实现类,而不需要改变其控制器代码。 442tl: 5肖… E 4呻 1 申 interface Manager +BaseManager +FacadeManagerJ Lb 「 HVt ·-orcs--tettc.Jccebee ,30 .3.Jbebbovooeottvmeeaeggsr++++ DAO:DAO 申 interface FacadeManager +getCategory:Categor +saveCategory:void +removeCategory:void +getNews :News +saveNews:void +removeNews:void +getNewsReview:NewsR +saveNewsReview:void +removeNewsReview:vo +getUser: User +getUsers:List +saveUser:void +removeUser: void +validateUser:boolea categoryDAO:Categor丁 categorles:L工 st newsDAO:NewsDAO newsReviewDAO:NewsR userDAO:UserDAO 图 9 .4 业务逻辑组件的接口类图 下面给出 FacadeManager接口的源代码,该接口是对PAO 的正面包装: public interface FacadeManager { II 增加一个种类 publ 工 c void saveCategory(Category category); II 根据 id 删除种类 public void removeCategory(String id); II 获取所有的种类 public List getCategories(); II 根据主键加载News 对象 public News getNews(String id); II 保存 News 对象 public void saveNews(News news); II 删除 News 对象 public void removeNews(String id); II 根据 id 加载消息回复对象 public NewsReview getNewsReview(String id); II 保存消息回复 public vo工 d saveNewsReview(NewsReview newsReview); II 删除消息回复 443轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 public void removeNewsReview(String id); II 根据用户名加载User 对象 publ工 c User getUser(String username); II 查找用户 public List getUsers(User user); II 保存用户 public void saveUser(User user) throws Exception; II 删除用户 public void removeUser(String username); II 验证用户是否有效 publ工 c boolean validateUser(User user); 9.4.3 业务逻辑组件的实现类 业务逻辑组件以 DAO 组件为基础,必须接收 Spring 容器注入的 DAO 组件,因此必 须为业务逻辑组件的实现类提供对应的 setter 方法。 业务逻辑组件的实现类将 DAO 组件接口实例作为属性(面向接口编程),下面就是 FacadeManagerImpl 类的源代码: public class FacadeManagerlmpl extends BaseManager implements CategoryManager II将 CategoryDAO作为成员属性 private CategoryDAO categoryDAO; II 将 NewsDAO作为成员属性 private NewsDAO newsDAO; II将 NewsReviewDAO作为成员属性 private NewsReviewDAO newsReviewDAO; II 将 UserDAO 作为成员属性 private UserDAO userDAO; II 提供依赖注入CategoryDAO所需的 setter 方法 publ工 c void setCategoryDAO(CategoryDAO categoryDAO) this.categoryDAO = categoryDAO; II 依赖注入 NewsDAO所需的 setter 方法 public void setNewsDAO(NewsDAO newsDAO) this.newsDAO = newsDAO; II 依赖注入 NewsReviewDAO所需的 setter 方法 public void setNewsReviewDAO(NewsReviewDAO newsReviewDAO) this.newsReviewDAO = newsReviewDAO; II 依赖注入 UserDAO 必需的 setter 方法 publ工 c void setUserDAO(UserDAO userDAO) this.userDAO = userDAO; II 根据 id 加载消息分类 public Category getCategory(String id) return categoryDAO.getCategory(Long.valueOf(工 d)) ; 444完时消…国 /I 增加消息分类 public void saveCategory(Category category) categoryDAO.saveCategory(category) ; II 删除消息分类 public void removeCategory(String id) categoryDAO.removeCategory(Long.valueOf(id)); II 查询全部的消息分类 public List getCategories() return categoryDAO.getCategories(); II 根据主键加载消息 public News getNews(String id) return newsDAO.getNews(Long.valueOf(id)); II 增加消息 public void saveNews(News news) newsDAO.saveNews(口ews) ; /I 删除消息 public vo工 d removeNews(String id) newsDAO.removeNews(Long.valueOf(id)); /I 获取消息回复 public NewsReview getNewsReview(String id) return newsReviewDAO.getNewsReview(Long.valueOf(id)); } /I 增加消息回复 public void saveNewsReview(NewsReview newsReview) { newsReviewDAO.saveNewsReview(newsReview); II 删除消息回复 public void removeNewsReview(String id) newsRev工 ewDAO.removeNewsReview(Long.valueOf(id)); II 根据用户名查找用户 public User getUser(String username) { return userDAO.getUser(username); II 获取用户列表 public List getUsers(User user) return userDAO.getUsers(user); II 增加用户 public void saveUser(User user) throws Exception try 445轻量级 J2EE 企业应用实战-一-Struts+Spring+Hibernate 整合开发 userDAO.saveUser(user); } catch (DatalntegrityV工 olationException e) throw new Exception("User '" + user.getUsername() + '" already exists!"); II 删除用户 public void removeUser(String username) userDAO.removeUser(username); II 验证用户的方法,用户名存在而且密码正确才会返回true public boolean validateUser(User aUser) User user = getUser(aUser.getUsername()); if (user != null && user.getPassword() .equals(aUser.getPassword())) return true; else return false; FacadeManagerImpl 继承了 BaseManager,以下是 BaseManager的源代码: public class BaseManager implements Manager { protected DAO dao = null; protected final Log log = LogFactory.getLog(getClass()); II 依赖注入 DAO 组件必需的 setter 方法 public void setDAO(DAO dao) th工 s.dao = dao; II 根据主键加载持久化对象 public Object getObject(Class clazz , Serializable id) return dao.getObject(clazz , id); ZZJHzc 臼 『D143cuusmhd 例 M 但 陀回民主目 -1J 呻咔 全由 MU M丑 thv 勤但阳对二 t 化 NU2 久 .LU 持 CH 出国 V-1-- ,Ea l--ι14·刀 b /U }/P{ II 根据主键删除特定对象 public void removeObject(Class clazz , Serializable id) dao.removeObject(clazz , id); II 保存对象 public void saveObject(Object 0) dao.saveObject(o); 这里的 BaseManager 与 BaseDAO 的作用类似,用于包装工具方法和完成一般的 446完整实川息发布系统 E CRUD 操作。 对于普通的 get 操作如 getNewsO等, Facade 仅仅是调用对应的 DAO 接口: publ 工 c News getNews(String id) { return newsDAO.getNews(Long.valueOf(id)); 而对于复杂的业务逻辑,可能需要访问多个对象的数据,那么只需在这个方法里调 用多个 DAO 接口,将具体实现委派给DAO 完成。 9.4.4 业务逻辑组件的配置 既然上面业务逻辑组件的 DAO 组件从未被初始化过,那么业务方法如何完成? DAO 组件初始化是由 Spring 的反向控制 (Inverse of Control, IoC),或者称为依赖注入 (Dependency Injection, DI)机制完成的。为此我们还需要在 applicationContex t. xm1里面配 置 FacadeManager 组件。 定义 FacadeManager 组件时必须为其配置所需要的 DAO 组件,配置的示例代码如下: 请按如下步骤配置业务逻辑层组件。 (1)在 applicationContext. xm1里加入如下内容: PROPAGATION_REQUIRED PROPAGAT工 ON_REQUIRED <' 其他方法的事务属性-> PROPAGATION_REQU工 RED , readOnly 在上面加入的内容中定义了一个事务代理模板,其中"key" 属性用来制定使用该模 447轻量级 J2EE 企业应用实战一一-Struts+Spring+Hibernate 整合开发 板的事务传播属性。例如, key="save*" ,表明该类以 save 开头的方法均采用 PROPAGATION_REQUIRED 的事务传播属性。 (2) 在配置文件中增加如下内容: <' 配置具体的业务逻辑层组件的事务代理一〉 <'一 生成业务代理之前,必须使用target 制定需要生成代理的目标bean 目标 bean 采用嵌套 bean 的方式定义一〉 上面的配置信息表示 BaseManager 继承刚才配置的事务代理模板。并且由容器给 BaseManager注入"dao" 的组件,即 BaseDAOHibemate。而 target 则是 TransactionProxy FactoryBean需要指定的属性, TransactionProxyFactoryBean负责为某个bean 实例生成代 理,而代理必须有个目标, target 属性则用于指定目标。 注意:此处使用嵌套bean 配直代理的目标,因为目标bean 没有事务属性,通过使 用嵌套 bean 可避免系统直接访问目标beano 当然也可以不使用事务代理模板及嵌套bean,而是为组件指定单独的事务代理属 性,让事务代理的目标引用容器中已经存在的bean。其源代码如下: PROPAGATION_REQUIRED PROPAGATION_REQUIRED PROPAGAT工 ON_REQUIRED, readOn1y 下面是 applicationContext. xml 文件的源代码,该文件配置了应用的数据源和 SessionFactory等 bean。而业务逻辑组件也被部署在该文件中: 448发布系统~ <'一指定数据库的密码一〉 <'一 指定数据库的最大等待数--> org/yeeku/model/User.hbm.xml org/yeeku/model/News.hbm.xml org/yeeku/model/NewsReview.hbm.xml org/yeeku/model/Category ..hbm. 双nl <' 为业务逻辑组件注入DAO 组件 > 在上面的配置文件中,采用继承业务逻辑组件的事务代理,将原有的业务逻辑组件 作为嵌套 bean 配置,避免了直接调用没有事务特性的业务逻辑组件。 至此,我们的系统已经实现了所有的后台业务逻辑,并且向外提供了统一的Facade 接口,前台 Web 层仅仅依赖这个Facade 接口。这样Web 层与后台业务层的藕合已经非常 松散,系统可以在不同的Web 框架中方便切换,即使将整个Web 层替换掉也非常容易。 9.5 Web 层设计 在架构综述里面提到,系统的 Web 层采用的是经典的J2 EE WebMVC 框架 Struts , 其表现层也大量使用 Struts 的标签库,关于 Struts 标签库的详细用法,请参考第 3 章的 介绍。 9.5.1 Action 的实现 Struts 的 Action 实现非常简单,通过继承 Struts 的 Action 基类重写 execute 方法,并 在该方法里调用业务逻辑组件的业务方法。在这里,可以发现所有的 Action 有个共同之 处一一一都需要调用业务逻辑组件。 在 Spring 与 Struts 的整合策略里介绍过,业务逻辑组件的实现也不是由 Action 自己 控制的,而是接受 Spring 容器的依赖注入。因此必须为 Action 提供对应的 setter 方法, 而且每个 Action 都必须为其注入业务逻辑组件,因此可写成一个 Action 基类,让所有的 450完叫… E Action 都从该基类派生。 下面是 Action 基类的源代码: IIBaseAction. 作为其他 Actio口的父类 public class BaseAction extends Action II FacadeManager属性,面向接口编程 protected FacadeManager mgr; II 依赖注入业务逻辑组件必需的setter 方法 public void setMgr(FacadeManager mgr) this.mgr = mgr; 注意:在 BaseAction 中,故意将 mgr 属性设置成 protected 的访问权限,目的是为 了让其子类可以直接访问该属性,从而提供更简单的访问方式。 而其他的 Action 则简单地从 BaseAction 派生出来,派生出来的Action 具有一个属 性: mgr,该属性就是业务逻辑组件的引用。 下面以几个 Action 作为代表,分别介绍Action 的实现。 下面的 AddReviewAction 用于拦截用户提交消息回复时的请求,其代码如下: II 业务控制器,以BaseAction作为基础 public class AddReviewAction extends BaseAction I I 必须重写该核心方法,该方法 actionForm 将表单的请求参数封装成值对象 publ 工 c ActionForward execute(Act工 onMapping mapping, Act 工 onForm form , HttpServletRequest request , HttpServletResponse response) throws Exception II 解析 ActionForm. 用于获取请求参数 DynaVal工 datorForm addForm = (DynaValidatorForm)form; II 获取请求参数 String content = (Str 工 ng)addForm.get("content") ; String newsld = (String)addForm.get("news 工 d") ; try II 调用 mgr 的业务方法加载News 对象 News news = mgr.getNews(newsld); II 获取 sess~on 中的 user 值 String username = (Str 工口g)request.getSession(true) . getAttribute (AppConstants.LOGIN_USER); II 调用 mgr 的业务方法加载User 对象 User poster = mgr.getUser(username); II创建消息回复实例 NewsReview newsRev工 ew = new NewsRev工 ew() ; II 设置消息回复的属性 newsRev 工 ew.setNews(news); newsReview.setPoster(poster); newsReview.setContent(content); newsReview.setPostDate(new Date()); newsReview.setLastModifyDate(口ew Date()); II 持久化消息回复 mgr.saveNewsReview(newsReview); 451轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 II 捕捉异常 catch (Exception e) II 出现异常就跳转到failure request. setAttribute ("newsld" , newsld); return mapping.findForward("failure"); II 执行成功,跳转到success request.setAttribute("news工 d" , newsld); return mapping.findForward("success"); 下面的 LoadReviewsByNews 用于拦截用户查看消息细节的Action,该 Action 根据 用户请求的 id 加载该消息的回复及消息本身: public class LoadReviewsByNews extends BaseActio口 { II 必须重写该核心方法,该方法 actionForm 将表单的请求参数封装成值对象 public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request , HttpServletResponse respo口 se) throws Exception II 希望加载的消息id String newsld = null; II 考虑到请求有可能从不同地方转发过来.request 的参数和属性 II 分别获取 news工 d 属性 if (request .getAttribute ("newsld") == null) news工 d = request.getParameter("newsld"); } else newsld = (String)request.getAttribute("newsld"); II 获取 News 实例 News news = mgr.getNews(newsld); II将 News 实例设置成 Request 属性后转发 request. setAttribute ("news" , news); II 同时将消息的回复也设置成Request 属性后转发 request.setAttribute("reviews",口ews.getNewsReviews()); return mapping.findForward("success"); 上面介绍了两种Action: 一种有 ActionForm 的 Action; 另一种无需 ActionForm 的 Action。这两种 Action 大同小异,都需要调用mgr 的业务逻辑方法,区别在于获取请求 参数的方式不同。 注意:在上面的 Action 中,多次重复访问 PO 对象,而 Action 中通常不允许访问 PO 对象,因为 PO 对象是持久层的组件,应该使用更普通的JavaBean 作为 VO (值对 象) ; VO 用于封装业务逻辑组件访问的值,并将这些值传递到 JSP 页面。因为本示例 是个较小的示例,所以在 Action 中可直接访问 PO 对象。在第 10 幸的示例中,读者将可 以看到更加严格的控制。 452叫…「国 9.5.2 Spring 容器管理 Action 正如前面介绍的,推荐使用 Spring 管理 Struts 的 Action 。因为这样可以充分利用 Spring 的 Ioe 功能,使 Action 无须关心业务逻辑组件的实现,而由 Spring 负责为 Action 注入业务逻辑组件引用,从而实现更好地解祸。 为了让 Struts 将请求转发到 Spring 容器内的 bean ,系统将采用 DelegatingRequest Processor 的整合策略。因为这种策略无需 Struts 创建 Action 实例,直接由 Spring 容器负 责创建 Action 实例,并为其注入依赖关系。使系统更早将请求转发给 Spring 容器控制。 采用这种整合策略,必须在 struts-config.xrnl 文件中配置 controller 元素,并通过指 定 processorClass 属性指定 DelegatingRequestProcessor 处理器。即在配置文件中增加如 下代码: 经过这个简单的配置,则无须为struts-config.xrnl中的 Action 配置 class 属性,因为 Struts 无须负责创建Action 实例,由 DelegatingRequestProcessor直接将请求转发到Spring 容器内。 下面是 struts-config.xrnl文件中 Action 的配置代码: 息发系统 E ' 这个配置文件与 Struts 基本的配置文件并没有太多的不同,区别在于该配置文件的 action 元素没有 class 属性,以及使用 DelegatingRequestProcessor 代替了系统默认的 RequestProcessor。 注意:虽然 action 元素没有确定class 属性,但也允许指定class 属性,只是不会有 任何作用。 Spring 对 Action 的配置采用单独的文件配置action-Servlet.xml,该文件中配置了所 有的 Action bean。 因为所有的 Action 都需要为其注入业务逻辑组件,所以此处采用继承简化了Action bean 的配置。具体的配置文件代码如下: 455轻量级 J2EE 企业应用实战-一-Struts+Spring+Hibernate 整合开发 <1-- 配置 Action 模板,用于被其他Action 继承 > <' 列出所有的消息分类二〉 <'一 根据种类列出所有消息--> <1- 添加消息评论 > 至此,已经基本完成了Struts 与 Spring 的整合。当 ActionServlet 拦截到用户请求时, 则调用 DelegatingRequestProcessor,该处理器将请求转发到Spring 容器中的 bean,由该 bean 负责调用业务逻辑组件处理用户请求,井将处理结果呈现给用户。 9.5.3 数据校验的选择 数据校验是表现层必须处理的基本问题。根据第 3 章的介绍,表现层的数据校验分 成客户端校验和服务器端校验。不管是客户端校验,还是服务器端校验, Struts 都有很 好的支持,完全可以弹出 JavaScript 校验。 此处要提醒读者的是, Struts 的客户端校验有一个弊端。 看如图 9.5 所示的简单登录页面。 图 9 .5 简单的登录页面 456制布系统~ 在这个简单的登录页面中,假设只需要对页面中三个表单域进行校验,如用户名、 密码及电子邮件这三项必填,并且电子邮件为有效的地址,可以采用如下的 JavaScript 代码校验: 这段 JavaScript代码结构清晰,相当简洁。 通过第 3 章的学习,我们也可以使用Struts 的验证框架来生成客户端校验,但Struts 生成的 JavaScript 的校验代码非常多,笔者在此处不能完全列出,但读者可以通过客户 端查看网页源代码看到这将近1000 行的 JavaScript代码。 仔细检查 Struts 生成的 JavaScript代码,可以发现这些JavaScript代码包含了最小长 度校验、最长长度校验及有效范围校验等,而这些与该页面的需求没有丝毫关系。 这正是手写 JavaScript 校验和 Struts 自动生成 JavaScript 校验的区别: Struts 生成的 校验会包含更多的代码,即使页面只需要校验一个简单的必填项,Struts 也会生成将近 1000 行的 JavaScript 代码。虽然这些代码不需要程序员手写,但这些代码必须要下载到 客户端执行,显然这些无用的代码将加重客户端网络带宽的负担。因此客户端校验依然 建议使用手写校验。 457轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 注意:笔者在这里介绍给读者一个技巧,仔细观察 Struts 生成的 JavaScript 客户端 技验代码,就可发现每个页面的 JavaScript 代码只有前面数行不同,其他部分则是完全 相同的。没错,这些部分是通用的,我们完全可以将这些通用的部分提取出来,作为通 用的 JavaScript 代码,并以单独的 JavaScript 文件保存,当每个页面需要进行校验时,只 需导入该通用的 JavaScript 文件即可;而页面则只需加入 Struts 为不同页面生成的不同 部分。关于客户端技验,还可以直接采用 ProtoType 校验。 在网络带宽受限的情况下,建议不要采用 Struts 的 JavaScript 校验,并不是意味着 可以不使用 Struts 的校验框架。因为服务器端校验依赖于 Struts 的校验要简单得多。 增加校验的详细步骤请参考第 3 章的内容。此处给出本应用的校验规则文件, validation.xml 文件的源代码如下: <'一 需要校验的第一个form: 登录用的 loginForm-->
mi口 Ie口gth 4 minlength 4
<1-- 需要校验的第二个form: 添加消息用的 addNewsForm 一〉
458完整实例:消息发布系统 IE <1-- 需要校验的 content 域,需满足必填规则→ Login Filter org.yeeku.webapp.filter.UserLoginFilter 〈工 nlt-param> ignore false forwardpath index.jsp 如果采用如下代码关联Parent 与 Child 的关系: Parent p = new Person; Child c = new Child(); p.getChildre口() .add(c); session.save(c); session.flush() ; Hibernate 会产生两条 SQL 语句: 一条 INSERT 语句,为 c 创建一条记录。 一条 UPDATE 语句,创建从 p 到 C 的关联。 采用这种方式不仅效率低,而且违反了列 parencid 非空的约束。我们可以通过在集 合类映射上指定 not-null="true" 来解决此问题: 产生这种现象的根本原因是:p 到 c 的关联不是Child 对象的一部分,因而在INSERT 语句中没有创建该外键值。解决的办法是将关联添加到Child 的映射中。 并改为由实体Child 在管理关联,为了使Collection不更新连接,设置inv刊erse="true旷" 如下所示: 480完…工作流系统~ 下面的代码是用来添加一个新的Child: Parent p = new Parent(); Ch工 ld c = new Child(); c. setParent (p) ; session.save(c) ; session.flush() ; 现在,只会有一条 INSERT 语句被执行。为了让 Person 端也可以"管理关联",可 以为 Parent 加一个 addChildO方法。 II 由 Parent 管理关联的方法 public void addChild(Child c) II 实际依然由 Child 一端控制关联 c.setParent(th工 s) ; children.add(c); 另外,也可以通过如下方式来增加它们之间的关联。 Parent p = new Person Child c = new Child(); p.addChild (c) ; session.save(c); session.flush(); 通过上面的讨论,对于}-N 的关联关系,通常推荐映射成双向关联,而且由N 的 一端控制关联关系。 注意:对所有 }-N 的关联关系,建议不要使用 "1" 的一端控制关系,因此建议为 set 元素增加 inverse= "true" 属性,让 "N" 的一端来控制关联关系。 除此之外,分析对象之间的继承层次也是非常重要的任务。在面向对象设计里,继 承体现了软件复用的概念。因此,在本应用中,Manager 需要继承 Employee类。 Hibernate 支持三种继承映射策略。 subclass 元素映射子类:用一张表存储整个继承树的全部实例。 joined-subclass元素映射子类:继承树的每层实例对应一张表,且基类的表中保存所 有子类的公有列,因此如需创建子类实例,总是需要查询基类的表数据,其子类所在深 度越深,查询涉及到的表就越多。 unioned-subclass元素映射子类:继承树的每层实例对应一张表,每层的实例完整地 保存在对应的表中,表中的每条记录即可对应一个实例。 关于继承策略的选择,建议使用unioned-subclass的继承映射策略,理由如下。 ·使用 subclass 的映射策略时,因为整个继承树的全部实例都保存在一张表内,因 481轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 此子类新增的数据列都不可增加非空约束,即使实际需要非空约束也不行,因为 父类实例对应的记录根本没有该列,这将导致数据库的约束降低,同时也增加数 据冗余,可以说这是最差的映射策略。 ·使用 joined-subclass 映射策略可以避免上面的问题,但使用这种映射策略时,如 需查询多层下的子类,可能要访问的表数据过多,影响性能。 而使用 unioned-subclass 的映射策略则可以避免上面的两个问题。 一旦映射了合适的继承策略, Hibernate 完全可以理解多态查询。即查询 Parent 类的 实例时,所有 Parent 子类的实例也可被查询到。 下面是本系统所使用的 6 个映射文件。 Employee 和 Manager 的映射文件 <'一映射标识属性--> <' 映射标识属性,该属性与父类的标示属性对应,无需主键生成器一〉 <1- 映射普通属性dept--> 482完整应用:简单工作流系统 Attend 的映射文件如下: 〈工 d name="id" type="integer" column="attend_id"> <'一 Hibernate 映射文件的根元素--> <' 映射标识属性一〉 <'一 指定主键生成器策略一〉 完整应用:简单工作流系统 Payment 的映射文件如下: <1 一映射持久化类Payment--> 正如前面所提示:所有的set 元素都被加上 inverse= "true" 声明,让 "N" 的一端 控制关联关系,而不要让"1" 的一端控制关联关系。 10.3 实现 DAD 层 在 Hibernate 持久层之上,可使用 DAO 组件再次封装数据库操作。通过 DAO 层, 可以让业务逻辑层与具体持久层技术分离,一旦需要更换持久层技术时,业务逻辑层组 件不需要任何改变。因此,使用 DAO 组件,即意味着引入 DAO 模式,使每个 DAO 组 件包含了数据库的访问逻辑:每个 DAO 组件可对一个数据库表完成基本的 CRUD 等操作。 DAO 模式的实现至少需要如下三个部分。 • DAO 工厂类。 • DAO 接口。 485轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 • DAO 接口的实现类。 DAO 模式是一种更符合软件工程的开发方式,使用 DAO 模式有如下理由。 • DAO 模式抽象出数据访问方式,业务逻辑组件无须理会底层的数据库访问,而只 专注于业务逻辑的实现。 • DAO 将数据访问集中在独立的一层,所有的数据访问都由 DAO 对象完成,这层 独立的 DAO 分离了数据访问的实现与其他业务逻辑,使得系统更具可维护性。 • DAO 还有助于提升系统的可移植性。独立的 DAO 层使得系统能在不同的数据库 之间轻易切换,底层的数据库实现对于业务逻辑组件是透明的。数据库移植时仅 仅影响 DAO 层,不同数据库的切换不会影响业务逻辑组件,因此提高了系统的 可复用性。 ·对于不同的持久层技术, Spring 的 DAO 提供一个 DAO 模板,将通用的操作放在 模板里完成,而对于特定的操作,则通过回调接口完成。 Spring 为 Hibernate 提供的 DAO 支持类是: HibernateDaoSupport 。 10.3.1 DAO 组件的定义 DAO 组件提供了各持久化对象的基本的 CRUD 操作。而在 DAO 接口里则对 DAO 组件包含的各种 CRUD 方法提供了声明,但有一些 IDE 工具也可以生成基本的 CRUD 方法。使用 DAO 接口的原因是:避免业务逻辑组件与特定的 DAO 组件藕合。 由于 DAO 组件中的方法不是开始就设计出来的,其中的很多方法可能会随着业务 逻辑的需求而增加,但以下几个方法是通用的。 • get: 根据主键加载持久化实例。 • save: 保存持久化实例。 • update: 更新持久化实例。 • delete: 删除持久化实例。 注意:在经典的 EJB 中, Entity EJB 是个功能非常强大的 DAO 纽件,其功能远远超 出这里实现的 DAO 纽件。 Entity EJB 同样包含了四个类似的方法。 DAO 接口无须给出任f可实现,仅仅是 DAO 组件包含的 CRUD 方法的定义,这些方 法定义的实现取决于底层的持久化技术, DAO 组件的实现既可以使用传统JDBC ,也可 以采用 Hibernate 持久化技术,以及iBATIS 等技术。下面是关于 DAO 接口的源代码。 ApplicationDao 的接口定义如下: public interface ApplicationDao { /** *根据工d 查找异动申请 * @pararn id 需要查找的异动申请id */ Application get(Integer id); /** *增加异动申请 486完…工作流系统 E *自param appl 工 cation 需要增加的异动申请 */ void save(Application application); /** *修改异动申请 * @param application 需要修改的异功申请 */ void update(Application applicatio口) ; /** *删除异动申请 * @param id 需要删除的异动申请id */ void delete(Integer id); /** *删除异动申请 * @param appl工 cation 需要删除的异动申请 */ void delete(Applicat工 on applicat工 on) ; /** *查询全部异功申请 * @return 全部异动申请 */ List findAll(); /** *根据员工查询未处理的异动申请 * @param emp 需要查询的员工 * @return 该员工对应的未处理的异动申请 */ List findByEmp(Employee emp); AttendDao 的接口定义如下: public interface AttendDao { /** *根据 id 查找打卡记录 * @param id 需要查找的打卡记录id */ Attend get(Integer id); /** *增加打卡记录 * @param attend 需要增加的打卡记录 */ void save(Attend attend); /** *修改打卡记录 * @param attend 需要修改的打卡记录 */ void update(Attend attend); /** *删除打卡记录 * @param id 需要删除的打卡记录工d */ void delete(工 nteger id); /** *删除打卡记录 * @param attend 需要删除的打卡记录 487轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 488 */ void delete(Attend attend); /** 食查询全部打卡记录 * @return 全部打卡记录 */ List findAll(}; /** *根据员工查询该员工的打卡记录 * @param emp 员工 食 @return 该员工的全部出勤记录 */ List findByEmp(Employee emp); /** *根据员工、日期查询该员工的打卡记录集合 * @param emp 员工 * @param dutyDay 日期 * @return 该员工某天的打卡记录集合 */ List findByErnpAndDutyDay(Employee emp , String dutyDay); /** *根据员工、日期、上下班查询该员工的打卡记录集合 * @param emp 员工 * @param dutyDay 日期 * @param isCome 是否上班 * @return 该员工某天上班或下班的打卡记录 */ Attend findByEmpAndDutyDayAndCome (Employee emp , String dutyDay , boolean isCome) ; /** *查看员工前三天的非正常打卡 * @param emp 员工 * @return 该员工前二天的非正常打卡 */ List findByEmpUnAttend(Employee emp , AttendType type); AttendTypeDao 的接口定义如下: public interface AttendTypeDao /** *根据 id 查找出勤种类 * @param id 需要查找的出勤种类id Attendτype get(Integer id}; /** *增加出勤种类 * @pararn type 需要增加的出勤种类 */ 飞roid save(AttendType type}; /** *修改出勤种类 * @param type 需要修改的出勤种类 */ void update(AttendType type); *删除出勤种类 r* @param id 需要删除的出勤种类id */ void delete(Integer id); /** *删除出勤种类 * @param type 需要删除的出勤种类 void delete(AttendType type); /** *查询全部出勤种类 * @return 全部出勤种类 List findAll(); CheckBackDao 的接口定义如下: public 工 nterface CheckBackDao { /** *根据工d 查找申请批复 * @param id 需要查找的申请批复工d CheckBack get(Integer id); /** *增加申请批复 * @param check 需要增加的申请批复 void save(CheckBack check); /*食 *修改申请批复 * @param check 需要修改的申请批复 */ void update(CheckBack check); /** *删除申请批复 * @param id 需要删除的申请批复id */ void delete(Integer id); /** *删除申请批复 * @param check 需要删除的申请批复 void delete(CheckBack check); /** *查询全部申请批复 * @return 全部申请批复 */ List findAll(); EmployeeDao的接口定义如下: public 工 nterface EmployeeDa。 /** *根据 id 查找员工 * @param id 需要查找的员工id 完整应用:简单工作流系统 489轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 490 */ Employee get(Integer id); /** *增加员工 * @param emp 需要增加的员工 */ void save(Employee emp); /** *修改员工 * @param emp 需要修改的员工 */ void update(Employee emp); /** *删除员工 * @param id 需要删除的员工 id */ void delete(Integer id); /** *删除员工 * @param emp 需要删除的员工 */ vo工 d delete(Employee emp); /** *查询全部员工 * @return 全部员工 */ List findAll(); /** *根据用户名和密码查询员工 * @param name 员工的用户名 * @param pass 员工的密码 * @return 符合用户名和密码的员工集合 */ List findByNameAndPass(String name , String pass); /** *根据用户名查询员工 * @param name 员工的用户名 * @return 符合用户名的员工 */ Employee findByName(String name); /** *根据经理查询员工 * @param mgr 经理 * @return 符合经理下的所有员工 */ List f 工 ndByMgr(Manager mgr); ManagerDao 的接口定义如下: public interface ManagerDao { /** *根据 id 查找员工 * @param id 需要查找的经理id */ Manager get( 工 nteger id); /**完整应用:简单工作流系统 食增加经理 * @param mgr 需要增加的经理 */ void save(Manager mgr); /** *修改经理 * @param mgr 需要修改的经理 */ vo 工 d update(Manager mgr); /** *删除经理 * @param id 需要删除的经理id */ void delete(Integer id); /** *删除经理 * @param mgr 需要删除的经理 */ void delete(Manager mgr); /** *查询全部经理 * @return 全部经理 */ List findAll(); /** *根据用户名和密码查询经理 * @param name 经理的用户名 * @param pass 经理的密码 * @return 符合用户名和密码的经理 */ List findByNameAndPass(String name , String pass); /** *根据用户名查找经理 * @param 经理的名字 * @return 名字对应的经理 */ Manager findByName(String name); PaymentDao 的接口定义如下: public interface PaymentDao { *根据 id 查找月结薪水 * @param id 需要查找的月结薪水id */ Payment get( 工 nteger id); *增加月结薪水 * @param payment 需要增加的月结薪水 */ void save(Payment payment); /** *修改月结薪水 * @param payment 需要修改的月结薪水 */ void update(Payment payment); 491轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 *删除月结薪水 * @param id 需要删除的月结薪水id */ void delete(Integer id); /** *删除月结薪水 * @param payment 需要删除的月结薪水 */ void delete(Payment payment); *查询全部月结薪水 * @return 全部月结薪水 */ List findAll(); /** *根据员工查询月结薪水 * @return 员工对应的月结薪水 List findByEmp(Employee emp); *根据员工和发薪水月查看月结薪水 * @payMonth 发薪月份 * @emp 领薪的员工 * @return 员工对应的月结薪水 Payment findByMonthAndEmp(String payMonth , Employee emp); 10.3.2 实现 DAO 组件 借助于 Spring 的 DAO 支持,可以很方便地实现 DAO 类。 Spring 为 Hibernate 的整合提供了很好的支持, Spring 的 DAO 支持类是: HiberanteDaoSupport ,该类只需要传入一个 SessionFactory 引用,即可得到一个 HibernateTemplate 实例,该实例功能非常强大,数据库的大部分操作也很容易实现。 所有的 DAO 类都继承HibernateDaoSupport,并实现相应的 DAO 接口。而业务逻辑 对象则面向接口编程,无须关心 DAO 的实现细节。通过这种方式,可以让应用在不同 的持久化技术之间切换。 如下是 ApplicationDao 接口的实现类 ApplicationDaoHibernate 的源代码: public class ApplicationDaoHibernate extends HibernateDaoSupport implements ApplicaticnDao /** *根据 id 查找异动申请 * @param id 需要查找的异动申请id */ public Application get( 工 nteger id) return (Application)getHibernateTemplate() .get(Application.class , id); /** *增加异动申请 492完整应用:简单工作流系统 * @param application 需要增加的异动申请 */ public void save(Application application) getHibernateTemplate() .save(application); /** *修改异动申请 * @param application 需要修改的异动申请 */ public void update(Application application) getHibernateTemplate() .saveOrUpdate(applicatio口) ; /** *删除异动申请 * @param id 需要删除的异动申请id */ public void delete(工 nteger id) getHibernateTemplate() .delete (getHibernateTemplate () . get (Application.class , id»; /** *删除异动申请 * @param application 需要删除的异动申请 */ public void delete(Application application) getHibernateTemplate() .delete(application); /** *查询全部异动申请 * @return 全部异动申请 */‘ public List findAll () return (List findByEmp(Employee emp) E List apps = (List result = new ArrayList(); for (Application app : apps ) { result.add(app); } return result; 493轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 494 如下是 AttendDao 接口的实现类 AttendDaoHibemate 的源代码: public class AttendDaoHibernate extends HibernateDaoSupport implements AttendDao /** *根据 id 查找打卡记录 * @param id 需要查找的打卡记录id */ public Attend get(Integer id) return (Attend)getHibernateTemplate() .get(Attend.class ,工 d) ; /** *增加打卡记录 * @param attend 需要增加的打卡记录 */ public void save(Attend attend) getHibernateTemplate() .save(attend); /** *修改打卡记录 * @param attend 需要修改的打卡记录 */ publ工 c void update(Attend attend) getHibernateTemplate() .saveOrUpdate(attend); /** *删除打卡记录 *自param id 需要删除的打卡记录id */ public void delete(Integer id) getH工 bernateTemplate().delete (getHibernateTemplate () . get(Attend. class , id)); /** *删除打卡记录 * @param attend 需要删除的打卡记录 */ public void delete(Attend attend) getHibernateTemplate() .delete(attend); /** *查询全部打卡记录 * @return 全部打卡记录 */ public List findAll() return (List f 工 ndByEmp(Employee emp) return (L 工 st findByEmpAndDutyDay(Employee emp , String dutyDay) Object[] args = {emp , dutyDay}; return (List attends = (L工 st findByEmpUnAttend(Employee emp , AttendType type) SimpleDateFormat sdf = new S 工 mpleDateFormat("yyyy-MM-dd"); Calendar c = Calendar.getlnstance(); String end = sdf.format(c.getTime()); c.add(Calendar.DAY_OF_MONTH , -3); 495轻量级 J2EE 企业应用实战一-Struts+Spring+Hibemate 整合开发 496 String start = sdf.format(c.getT 工 me()) ; Object[] args = {emp , type , start , end}; return (List findAll()π 用制工作流系统「国 return (List findAll() return (List findAll() return (List findByNameAndPass(String name , String pass) String[] args = {name , pass}; return (L工 st emps = (List= 1) return emps.get(O); return null; /** *根据经理查询员工 * @par 缸n mgr 经理 * @return 符合经理下的所有员工 */ public List findByMgr(Manager mgr) return (List findAll() return (List findByNameAndPass(String name , String pass) String[] args = {name, pass}; return (List ml = (List 0) return ml.get(O); return null;完整应用:简单工作流系统 国 如下是 PaymentDao 接口的实现类 PaymentDaoHibemate 的源代码: public class PaymentDaoHibernate extends HibernateDaoSupport implements PaymentDao /** *根据 id 查找月结薪水 * @param id 需要查找的月结薪水id */ public Payment get(Integer id) return (Payment)getHibernateTemplate() .get(Payment.class , id); /** *增加月结薪水 * @param payment 需要增加的月结薪水 */ public void save(Payment payment) getHibernateTemplate() .save(payment); /** *修改月结薪水 * @param payme口 t 需要修改的月结薪水 */ public void update(Payment payment) getHibernateTemplate() .saveOrUpdate(payment); /** *删除月结薪水 * @param id 需要删除的月结薪水id */ public void delete(工 nteger id) getHibernateTemplate() .delete (getHibernateTemplate () .get(Payment. class , id)); /** *删除月结薪水 * @param payment 需要删除的月结薪水 */ public void delete(Payment payment) getHibernateTemplate() .delete(payment); /** *查询全部月结薪水 * @return 全部月结薪水 */ public List findAll() return (List findByEmp( Employee emp) 501轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 return (List pays = (List com.mysql.jdbc.Driver jdbc:mysql://localhost/auction root l Employee.hbm.xml Attend.hbm.xml AttendType.hbm.泪nl Application.hbm.xml CheckBack.hbm.xml Payment.hbm.xml org.hibernate.dialect. MySQLDialect <'一 程序运行过程中,显示Hibernate转换的 SQL 语句一〉 503轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 true update 20 504完…工作流系统 E 注意:系统的 DAO 实现类中并未提供 setSessionFactory 方法,该方法由其父类 HibernateDaoSupport 提供,用于依赖注入 SessionFactory 引用 O 该配直文件中也并未配 直 SessionFactory bean ,其中 DataSource 和 SessionFactory 的配直在另一个文件中。 10.4 实现 Service 层 本系统只使用了两个业务逻辑组件,分别对应系统中包含的两个模块: Manager 和 Employee 模块。这两个模块分别使用不同的 Service 组件,每个组件 Facade 7 个 DAO 组件,系统使用这两个业务逻辑组件将这些 DAO 对象封装在一起。 10.4.1 Service 组件设计 Service 组件采用正面模式封装多个 DAO 组件, DAO 对象与 Service 组件之间的关 系如图 10.3 所示。 .Ql!l(lnld: I成鸣阳J' 何)plication ...,嘎酌匈苟地ε硝阳衔事lie.ion): 唰d .叩d幽(in 酬it曲n: ",pph:曲n): void • delebl:(lnld: Ir常町8"): void -""酬。 n ‘明M 阳、匈唰 it«ion): void."ndO IO -""'组yE m p( ln lfIlTI P: Em曲帽.J 、 、 、 、 、 、 、 .9Md,缸"咄"耐 .""刷刷nid: lrt毗阳): void .""幽 {Inattlnd: A脑时}: v喇 _"_~J .",曲恒 m p{in 酬 p: Em 曲"创 - "ndIlyEmpAndlu-说肌酬.'阳...,.时 n dut>d T"时 图 10 .3 EmpManager 与 DAO 组件接口的类图 505轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 在 EmpManager 接口里定义了大量的业务方法,这些方法的实现依赖于 DAO 组件。 由于每个业务方法要涉及到多个 DAO 操作,其 DAO 操作是单个的数据记录的操作,而 业务逻辑方法的访问,则需要设计多个 DAO 操作,因此每个业务逻辑方法可能需要涉 及多条记录的访问。 业务逻辑组件面向 DAO 接口编程,可让业务逻辑组件从 DAO 组件的实现中分离。 因此业务逻辑组件只关心业务逻辑的实现,无须关心数据访问逻辑的实现。 10.4.2 Service 组件的实现 Service 组件需要实现的业务方法主要取决于业务的需要,通常需要在业务组件中包 含对应的方法。 1.业务层组件的实现 业务层组件与具体的数据库访问技术分离,使所有的数据库访问依赖于 DAO 组件, 下面是 EmpManagerlmpl 的源代码。 public class EmpManager工mpl implements EmpManager 1/ 下面是本业务逻辑组件所依赖的7 个业务逻辑组件 private ApplicationDao appDao; private AttendDao attendDao; private AttendTypeDao typeDao; private CheckBackDao checkDao; private EmployeeDao empDao; private ManagerDao mgrDao; private PaymentDao payDao; 1/ 格式化日期所使用的DataFormat对象 private SimpleDateFormat sdf = new SimpleDateFormat ("yyyy-MM") ; II 下面是依赖注入 7 个业务逻辑组件的 setter 方法 public void setAppDao(ApplicationDao appDao) this.appDao = appDao; } public void setAttendDao(AttendDao attendDao) { this.attendDao = attendDao; } public void setTypeDao(AttendTypeDao typeDao) { this.typeDao = typeDao; } public void setCheckDao(CheckBackDao checkDao) { this.checkDao = checkDao; } public void setEmpDao(EmployeeDao empDao) { this.empDao = empDao; } public void setMgrDao(ManagerDao mgrDao) this.mgrDao = mgrDao; 506完…工作仰~ public void setPayDao(PaymentDao payDao) { th 工 s.payDao = payDao; 1** *验证登录 * @param user 登录用的用户名 * @param pass 登录用的密码 * @return 登录后的身份确认: 0 为登录失败, 1 为登录 emp 2 为登录 mgr *1 public int validLogin(String user , String pass) if (mgrDao.findByNameAndPass(user , pass) .size() >= 1) return LOGIN_MGR; else if (empDao.findByNameAndPass(user , pass) .size() >= 1) { return LOGIN_EMP; else return LOG工N_FAIL; 1** *自动打卡,周一到周五,早上7: 00 为每个员工打入旷工记录 public void autoPunch() System.out.println("自动插入旷工记录n) ; List emps = empDao.findA1l(); String dutyDay = new java.sql.Date(System.currentTimeMi1lis()). toString(); for (Employee e : emps) { Attendτype atype = typeDao.get(6); Attend a = new Attend(); a.setDutyDay(dutyDay); a.setType(atype); if (Calendar.get工 nstance() .HOUR_OF_DAY < 11) a.set工 sCome(true) ; } a.setIsCome(false); a.setEmployee(e) ; attendDao.save(a); 1** *自动结算工资,每月1 日凌晨 2 点将调用该作业 *1 public void autoPay() { System.out.print1n(" 自动插入工资结算 II) ; List emps = empDao.findAll(); II 获取上个月时间 Calendar c = Calendar.getI 口 sta口 ce() ; 507508 轻量级 J2EE 企业应用实战一 -Struts+Spring+Hibernate 整合开发 c.add(Calendar.DAY_OF_MONTH, -15); Str工 ng payMo口 th = sdf.format(c.getTime()); for (Employee e : emps) { Payment pay = new Payment(); List attends = attendDao.findByEmp(e); double amount = e.getSalary(); for ( Attend a : attends ) { amount += a.getType() .getAmerce(); } pay.setPayMonth(payMonth); pay.setEmployee(e); pay.setAmount(amount); payDao.save(pay); 1** *验证某个员工是否可打卡 * @param user 员工名 * @param dutyDay 日期 * @return 可打卡的类别 *1 public 工 nt validPunch(String user , String dutyDay) II 不能查找到对应用户,返回无法打卡 Employee emp = empDao.findByName(user); if (emp == null) return NO_PUNCH; List attends = attendDao.findByEmpAndDutyDay(emp , dutyDay); II 系统没有为用户在当天插入空打卡记录,无法打卡 if (attends == null) return NO_PUNCH; II 可以上班、下班打卡 if (attends.size() <= 0) return NO_PUNCH; } else if (attends.size() == 1 && attends.get(O) .get工 sCome( ) && attends.get(O) .getPunchTime() == null) return COME_PUNCH; } else if (attends.size() == 1 && attends.get(O) .getPunchTime() == null) return LEAVE_PUNCH; } else if (attends.size() == 2) { if (attends.get(O) .getPunchTime() == null && attends.get(1). getPunchTime() null) return BOTH_PUNCH; } else if (attends.get(1) .getPunchTime() == null)… E return LEAVE_PUNCH; } else return NO_PUNCH; } return NO_PUNCH; 1** *打卡 * @param user 员工名 * @param dutyDay 打卡日期 * @param isCome 是否是上班打卡 * @return 打卡结果 *1 public int punch(String user , String dutyDay , boolean isCome) Employee emp = empDao.findByName(user); if (emp == null) return PUNCH_FAIL; Attend attend = attendDao.findByEmpAndDutyDayAndCome(emp , dutyDay , iSCome) ; if (attend == null) return PUNCH_FAIL; if (attend.getPunchTime() != null) return PUNCHED; } int punchHour = Calendar.getlnstance() .HOUR_OF_DAY; attend.setPunchTime(new Date(»; /I 上班打卡 if (isCome) II 9 点之前算正常 if (punchHour < 9) attend.setType(typeDao.get(l»; II 9-11 点之间算迟到 else if (punchHour < 11) attend.setType(typeDao.get(4»); 1111 点之后算旷工 II 下班打卡 else /I 6 点之后算正常 工 f (punchHour > 6) attend.setType(typeDao.get(l); 509510 轻量级 J2EE 企业应用实战一-Struts+Spring+Hibernate 整合开发 II 4-6 点之间算早退 else if (punchHour < 4) attend.setType(typeDao.get(5)); attendDao.update(attend); return PUNCH_SUCC; 1** *员工浏览自己的工资 * @param empName 员工名 * @return 该员工的工资列表 *1 public List empSalary(String empName) Employee emp = empDao.findByName(empName); List pays = payDao.findByEmp(emp); List result = new ArrayList(); for (Payment p : pays ) result.add(new PaymentBean(p.getPayMonth() , p.getAmount())); return result; 1** *员工查看自己的最近三天非正常打卡 * @param empName 员工名 * @return 该员工最近三天的非正常打卡 *1 public List unAttend(String empName) { AttendType type = typeDao.get(l); Employee emp = empDao.findByName(empName); List attends = attendDao.findByEmpUnAttend(emp, type); List result = new ArrayList(); for (Attend att : attends ) { result.add(new AttendBean(att.getId() , att.getDutyDay() , att.getType() .getName() , att.getPunchTime())); } return result; 1** *返回全部的出勤类别 * @return 全部的出勤类别 *1 public List getAllType() return typeDao.findAll(); 1** *添加申请 * @param attId 申请的出勤 id * @param typeId 申请的类别 id * @param reason 申请的理由 * @return 添加的结果完整应用制工作流系统 E */ public boolean addApplication(int attld , int typeld , String reason) { try { Application app =口ew Application(); Attend attend = attendDao.get(att 工 d) ; AttendType type = typeDao.get(typeld); app.setAttend(attend) ; app.setType(type); if (reason != null) { app.setReason(reason); } appDao.save(app); return true; } catch (Exception e) { e.pr 工 ntStackTrace(); return false; 在上面的业务逻辑组件中,有autoPunch 和 autoPay 两个方法,这两个方法并不由客 户端直接调用,而是由任务调度来执行,负贡每天为员工完成自动考勤,以及每月1 日 为所有员工完成工资结算。 2. 事务管理 事务管理将推迟到Service 组件而不是DAO 组件,因为只有对业务逻辑方法添加事 务才有实际的意义,对于单个DAO 方法(基本的 CRUD 方法)增加事务操作是没有太 大实际意义的。 关于事务属性的配置,建议直接使用Spring 的 AOP 框架生成事务代理,而不要使 用 Spring 提供的 TransactionProxyFactoryBean配置事务代理。 采用 AOP 框架配置事务代理主要有两个类。 • BeanNameAutoProxyCreator。 • DefaultAdvisorAutoProxyCreator 。 这两个配置方式都可避免增量式的配置,不必为每个目标对象配置代理 bean; 避免 了目标对象被直接调用。 下面是 BeanNameAutoProxyCreator 配置事务代理的代码: <'一 下面是所有需要自动创建事务代理的bean--> MgrManager EmpManager 511轻量级 J2EE 企业应用实战一-5t阳ts+5pring+Hibernate 整合开发 <'一此处可增加其他新的工nterceptor 一〉 transactionInterceptor 3. 部暑业务层组件 单独配置系统的业务逻辑层,可避免因配置文件过大引起配置文件难以阅读。将配 置文件按层和模块分开配置,可以提高Spring 配置文件的可读性和可理解性。 在 applicationContext. xml 配置文件中配置数据源、事务管理器、业务逻辑组件和事 务管理器等 bean。具体的配置文件如下: Employee.hbm.xml Attend.hbm.xml AttendType.hbm.xml Application.hbm.xml CheckBack.hbm.xml Payment.hbm.xml <' 定义 empManager bea口,以 managerTemplate 作为父 bean--> <'一定义 BeanNameAutoProxyCreator--> <'一 指定对满足哪些beanname 的 bean 自动生成业务代理一〉 MgrManager EmpManager <'一此处可增加其他新的工nterceptor 一〉 transact工 on工 nterceptor org.yeeku.schedule.PunchJob