spring实战 第4版

cxiiiiiii

贡献于2018-03-27

字数:0 关键词: Spring JEE框架

目录目录 版权信息 作者简介 版权声明 内容提要 关于本书 路线图 代码规范与下载 作者在线 封面插图简介 前言 译者序 致谢 第1部分 Spring的核心 第1章 Spring之旅 1.1 简化Java开发 1.1.1 激发POJO的潜能 1.1.2 依赖注入 1.1.3 应用切面 1.1.4 使用模板消除样板式代码 1.2 容纳你的Bean 1.2.1 使用应用上下文 1.2.2 bean的生命周期 1.3 俯瞰Spring风景线 1.3.1 Spring模块 1.3.2 Spring Portfolio 1.4 Spring的新功能 1.4.1 Spring 3.1新特性 1.4.2 Spring 3.2新特性 1.4.3 Spring 4.0新特性 1.5 小结 第2章 装配Bean 2.1 Spring配置的可选方案 2.2 自动化装配bean 2.2.1 创建可被发现的bean 2.2.2 为组件扫描的bean命名 2.2.3 设置组件扫描的基础包 2.2.4 通过为bean添加注解实现自动装配 2.2.5 验证自动装配 2.3 通过Java代码装配bean 2.3.1 创建配置类 2.3.2 声明简单的bean 2.3.3 借助JavaConfig实现注入 2.4 通过XML装配bean 2.4.1 创建XML配置规范 2.4.2 声明一个简单的 2.4.3 借助构造器注入初始化bean 2.4.4 设置属性 2.5 导入和混合配置 2.5.1 在JavaConfig中引用XML配置 2.5.2 在XML配置中引用JavaConfig 2.6 小结 第3章 高级装配 3.1 环境与profile 3.1.1 配置profile bean 3.1.2 激活profile 3.2 条件化的bean 3.3 处理自动装配的歧义性 3.3.1 标示首选的bean 3.3.2 限定自动装配的bean 3.4 bean的作用域 3.4.1 使用会话和请求作用域 3.4.2 在XML中声明作用域代理 3.5 运行时值注入 3.5.1 注入外部的值 3.5.2 使用Spring表达式语言进行装配 3.6 小结 第4章 面向切面的Spring 4.1 什么是面向切面编程 4.1.1 定义AOP术语 4.1.2 Spring对AOP的支持 4.2 通过切点来选择连接点 4.2.1 编写切点 4.2.2 在切点中选择bean 4.3 使用注解创建切面 4.3.1 定义切面 4.3.2 创建环绕通知 4.3.3 处理通知中的参数 4.3.4 通过注解引入新功能 4.4 在XML中声明切面 4.4.1 声明前置和后置通知 4.4.2 声明环绕通知 4.4.3 为通知传递参数 4.4.4 通过切面引入新的功能 4.5 注入AspectJ切面 4.6 小结 第2部分 Web中的Spring 第5章 构建Spring Web应用程序 5.1 Spring MVC起步 5.1.1 跟踪Spring MVC的请求 5.1.2 搭建Spring MVC 5.1.3 Spittr应用简介 5.2 编写基本的控制器 5.2.1 测试控制器 5.2.2 定义类级别的请求处理 5.2.3 传递模型数据到视图中 5.3 接受请求的输入 5.3.1 处理查询参数 5.3.2 通过路径参数接受输入 5.4 处理表单 5.4.1 编写处理表单的控制器 5.4.2 校验表单 5.5 小结 第6章 渲染Web视图 6.1 理解视图解析 6.2 创建JSP视图 6.2.1 配置适用于JSP的视图解析器 6.2.2 使用Spring的JSP库 6.3 使用Apache Tiles视图定义布局 6.3.1 配置Tiles视图解析器 6.4 使用Thymeleaf 6.4.1 配置Thymeleaf视图解析器 6.4.2 定义Thymeleaf模板 6.5 小结 第7章 Spring MVC的高级技术 7.1 Spring MVC配置的替代方案 7.1.1 自定义DispatcherServlet配置 7.1.2 添加其他的Servlet和Filter 7.1.3 在web.xml中声明DispatcherServlet 7.2 处理multipart形式的数据 7.2.1 配置multipart解析器 7.2.2 处理multipart请求 7.3 处理异常 7.3.1 将异常映射为HTTP状态码 7.3.2 编写异常处理的方法 7.4 为控制器添加通知 7.5 跨重定向请求传递数据 7.5.1 通过URL模板进行重定向 7.5.2 使用flash属性 7.6 小结 第8章 使用Spring Web Flow 8.1 在Spring中配置Web Flow 8.1.1 装配流程执行器 8.1.2 配置流程注册表 8.1.3 处理流程请求 8.2 流程的组件 8.2.1 状态 8.2.2 转移 8.2.3 流程数据 8.3 组合起来:披萨流程 8.3.1 定义基本流程 8.3.2 收集顾客信息 8.3.3 构建订单 8.3.4 支付 8.4 保护Web流程 8.5 小结 第9章 保护Web应用 9.1 Spring Security简介 9.1 Spring Security简介 9.1.1 理解Spring Security的模块 9.1.2 过滤Web请求 9.1.3 编写简单的安全性配置 9.2 选择查询用户详细信息的服务 9.2.1 使用基于内存的用户存储 9.2.2 基于数据库表进行认证 9.2.3 基于LDAP进行认证 9.2.4 配置自定义的用户服务 9.3 拦截请求 9.3.1 使用Spring表达式进行安全保护 9.3.2 强制通道的安全性 9.3.3 防止跨站请求伪造 9.4 认证用户 9.4.1 添加自定义的登录页 9.4.2 启用HTTP Basic认证 9.4.3 启用Remember-me功能 9.4.4 退出 9.5 保护视图 9.5.1 使用Spring Security的JSP标签库 9.5.2 使用Thymeleaf的Spring Security方言 9.6 小结 第3部分 后端中的Spring 第10章 通过Spring和JDBC征服数据库 10.1 Spring的数据访问哲学 10.1.1 了解Spring的数据访问异常体系 10.1.2 数据访问模板化 10.2 配置数据源 10.2.1 使用JNDI数据源 10.2.2 使用数据源连接池 10.2.3 基于JDBC驱动的数据源 10.2.4 使用嵌入式的数据源 10.2.5 使用profile选择数据源 10.3 在Spring中使用JDBC 10.3.1 应对失控的JDBC代码 10.3.2 使用JDBC模板 10.4 小结 第11章 使用对象-关系映射持久化数据 11.1 在Spring中集成Hibernate 11.1.1 声明Hibernate的Session工厂 11.1.2 构建不依赖于Spring的Hibernate代码 11.2 Spring与Java持久化API 11.2.1 配置实体管理器工厂 11.2.2 编写基于JPA的Repository 11.3 借助Spring Data实现自动化的JPA Repository 11.3.1 定义查询方法 11.3.2 声明自定义查询 11.3.3 混合自定义的功能 11.4 小结 第12章 使用NoSQL数据库 12.1 使用MongoDB持久化文档数据 12.1.1 启用MongoDB 12.1.2 为模型添加注解,实现MongoDB持久化 12.1.3 使用MongoTemplate访问MongoDB 12.1.4 编写MongoDB Repository 12.2 使用Neo4j操作图数据 12.2.1 配置Spring Data Neo4j 12.2.2 使用注解标注图实体 12.2.3 使用Neo4jTemplate 12.2.4 创建自动化的Neo4j Repository 12.3 使用Redis操作key-value数据 12.3.1 连接到Redis 12.3.2 使用RedisTemplate 12.3.3 使用key和value的序列化器 12.4 小结 第13章 缓存数据 13.1 启用对缓存的支持 13.1.1 配置缓存管理器 13.2 为方法添加注解以支持缓存 13.2.1 填充缓存 13.2.2 移除缓存条目 13.3 使用XML声明缓存 13.4 小结 第14章 保护方法应用 14.1 使用注解保护方法 14.1.1 使用@Secured注解限制方法调用 14.1.2 在Spring Security中使用JSR-250的@RolesAllowed注解 14.2 使用表达式实现方法级别的安全性 14.2.1 表述方法访问规则 14.2.2 过滤方法的输入和输出 14.3 小结 第4部分 Spring集成 第15章 使用远程服务 15.1 Spring远程调用概览 15.2 使用RMI 15.2.1 导出RMI服务 15.2.2 装配RMI服务 15.3 使用Hessian和Burlap发布远程服务 15.3.1 使用Hessian和Burlap导出bean的功能 15.3.2 访问Hessian/Burlap服务 15.4 使用Spring的HttpInvoker 15.4.1 将bean导出为HTTP服务 15.4.2 通过HTTP访问服务 15.5 发布和使用Web服务 15.5.1 创建基于Spring的JAX-WS端点 15.5.2 在客户端代理JAX-WS服务 15.6 小结 第16章 使用Spring MVC创建REST API 16.1 了解REST 16.1.1 REST的基础知识 16.1.2 Spring是如何支持REST的 16.2 创建第一个REST端点 16.2.1 协商资源表述 16.2.2 使用HTTP信息转换器 16.3 提供资源之外的其他内容 16.3.1 发送错误信息到客户端 16.3.2 在响应中设置头部信息 16.4 编写REST客户端 16.4.1 了解RestTemplate的操作 16.4.2 GET资源 16.4.3 检索资源 16.4.4 抽取响应的元数据 16.4.5 PUT资源 16.4.6 DELETE资源 16.4.7 POST资源数据 16.4.8 在POST请求中获取响应对象 16.4.9 在POST请求后获取资源位置 16.4.10 交换资源 16.5 小结 第17章 Spring消息 17.1 异步消息简介 17.1.1 发送消息 17.1.2 评估异步消息的优点 17.2 使用JMS发送消息 17.2.1 在Spring中搭建消息代理 17.2.2 使用Spring的JMS模板 17.2.3 创建消息驱动的POJO 17.2.4 使用基于消息的RPC 17.3 使用AMQP实现消息功能 17.3.1 AMQP简介 17.3.2 配置Spring支持AMQP消息 17.3.3 使用RabbitTemplate发送消息 17.3.4 接收AMQP消息 17.4 小结 第18章 使用WebSocket和STOMP实现消息功能 18.1 使用Spring的低层级WebSocket API 18.2 应对不支持WebSocket的场景 18.3 使用STOMP消息 18.3.1 启用STOMP消息功能 18.3.2 处理来自客户端的STOMP消息 18.3.3 发送消息到客户端 18.4 为目标用户发送消息 18.4.1 在控制器中处理用户的消息 18.4.2 为指定用户发送消息 18.5 处理消息异常 18.6 小结 第19章 使用Spring发送Email 19.1 配置Spring发送邮件 19.1.1 配置邮件发送器 19.1.2 装配和使用邮件发送器 19.2 构建丰富内容的Email消息 19.2.1 添加附件 19.2.1 添加附件 19.2.2 发送富文本内容的Email 19.3 使用模板生成Email 19.3.1 使用Velocity构建Email消息 19.3.2 使用Thymeleaf构建Email消息 19.4 小结 第20章 使用JMX管理Spring Bean 20.1 将Spring bean导出为MBean 20.1.1 通过名称暴露方法 20.1.2 使用接口定义MBean的操作和属性 20.1.3 使用注解驱动的MBean 20.1.4 处理MBean冲突 20.2 远程MBean 20.2.1 暴露远程MBean 20.2.2 访问远程MBean 20.2.3 代理MBean 20.3 处理通知 20.3.1 监听通知 20.4 小结 第21章 借助Spring Boot简化Spring开发 21.1 Spring Boot简介 21.1.1 添加Starter依赖 21.1.2 自动配置 21.1.3 Spring Boot CLI 21.1.4 Actuator 21.2 使用Spring Boot构建应用 21.2.1 处理请求 21.2.2 创建视图 21.2.3 添加静态内容 21.2.4 持久化数据 21.2.5 尝试运行 21.3 组合使用Groovy与Spring Boot CLI 21.3.1 编写Groovy控制器 21.3.2 使用Groovy Repository实现数据持久化 21.3.3 运行Spring Boot CLI 21.4 通过Actuator获取了解应用内部状况 21.5 小结 看完了 版权信息版权信息 书名:Spring实战(第4版) ISBN:978-7-115-41730-5 本书由人民邮电出版社发行数字版。版权所有,侵权必究。本书由人民邮电出版社发行数字版。版权所有,侵权必究。 您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。 我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。 如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。 • 著     [美] Craig Walls   译     张卫滨   责任编辑 陈冀康 • 人民邮电出版社出版发行  北京市丰台区成寿寺路11号   邮编 100164  电子邮件 315@ptpress.com.cn   网址 http://www.ptpress.com.cn • 读者服务热线:(010)81055410  反盗版热线:(010)81055315 作者简介作者简介 Craig Walls是Pivotal的高级工程师,是Spring Social和Spring Sync的项目领导者,同时也是Manning出版社《Spring In Action》的作者,目前 这本书已经更新到了第四版。他非常热心于Spring框架的推广,经常在当地的用户组和会议上演讲并在博客上撰写Spring相关的内容。在不琢 磨代码的时候,Craig Walls会尽可能多地陪伴他的妻子、两个女儿、两只小鸟以及两只小狗。 本书特色本书特色 全球有超过100 000的开发者使用本书来学习Spring 中文版累计销售超10万册,畅销经典Spring 技术图书,针对Spring 4 全新升级 作者Craig Walls,SpringSource的软件开发人员,也是一位畅销书作者。 第3版译者继续翻译新版,品质保障! 版权声明版权声明 Original English language edition, entitled Spring in Action,4th Edition by Craig Walls Bibeault published by Manning Publications Co., 209 Bruce Park Avenue, Greenwich, CT 06830. Copyright ©2015 by Manning Publications Co. Simplified Chinese-language edition copyright ©2016 by Posts & Telecom Press. All rights reserved. 本书中文简体字版由本书中文简体字版由Manning Publications Co.授权人民邮电出版社独家出版。未经出版者书面许可,不得以任何方式复制本书内容。授权人民邮电出版社独家出版。未经出版者书面许可,不得以任何方式复制本书内容。 版权所有,侵权必究。版权所有,侵权必究。 内容提要内容提要 本书是经典的、畅销的Spring学习和实践指南。 第4版针对Spring 4进行了全面更新。全书分为4部分。第1部分介绍Spring框架的核心知识。第2部分在此基础上介绍了如何使用Spring构建 Web应用程序。第3部分告别前端,介绍了如何在应用程序的后端使用Spring。第4部分描述了如何使用Spring与其他的应用和服务进行集成。 本书适用于已具有一定Java 编程基础的读者,以及在Java 平台下进行各类软件开发的开发人员、测试人员,尤其适用于企业级Java 开发人 员。本书既可以被刚开始学习Spring 的读者当作学习指南,也可以被那些想深入了解Spring 某方面功能的资深用户作为参考用书。 关于本书关于本书 Spring框架是以简化Java EE应用程序的开发为目标而创建的。同样,本书是为了帮助读者更容易地使用Spring而编写的。我的目标不是为读 者详细地列出Spring API,而是希望通过现实中的实际示例代码来为Java EE开发人员展现Spring框架。因为Spring是一个模块化的框架,所以 这本书也是按照这种方式编写的。我们知道并不是所有的开发人员都有相同的需求,有些人想从头学习Spring,而有的可能只想排出几个主 题,然后按照自己的节奏来学习。所以,本书既可以被刚开始学习Spring的读者当作学习指南,也可以被那些想深入了解某方面功能的读者作 为参考。 本书适用于所有的Java开发人员,企业级Java开发人员将会发现更有帮助。我将会循序渐进地指导读者浏览本书中每章复杂的示例代码,但 Spring的真正强大之处在于它能够使企业级应用程序的开发更简单。因此,企业级应用程序的开发人员会更加欣赏本书的示例代码。因为 Spring的绝大部分内容都是提供企业级服务的,所以这里包含了许多Spring和EJB的比较。 路线图路线图 本书分为4部分。第1部分介绍Spring框架的核心知识。第2部分在此基础上介绍如何使用Spring构建Web应用程序。第3部分告别前端,介绍如 何在应用程序的后端使用Spring。第4部分描述如何使用Spring与其他的应用和服务进行集成。 在第1部分中,读者将会学习到Spring容器、依赖注入(dependency injection,DI)和面向切面编程(aspect-oriented programming,AOP),也就是Spring框架的核心。这能让读者很好地理解Spring的基础原理,而这些原理将会在本书各个章节都会用到。 第1章将会概要地介绍Spring,包括DI和AOP的一些基本样例。同时,读者还会了解到更大的Spring生态系统的整体情况。 第2章更为详细地介绍DI,展现应用程序中的各个组件(bean)如何装配在一起。这包括基于XML装配、基于Java装配以及自动装配。 在掌握了基本的bean装配后,第3章会介绍几种高级装配技术,读者可能并不会经常用到这些技术,但是如果用到的话,本章的内容将会 告诉读者如何发挥Spring容器最强大的威力。 第4章介绍如何使用Spring的AOP来为对象解耦那些对其提供服务的横切性关注点。这一章也为后面各章提供基础,在后面读者将会使用 AOP来提供声明式服务,如事务、安全和缓存。 在第2部分中,读者将会看到如何使用Spring来构建Web应用程序。 第5章介绍使用Spring MVC的基础知识,这是Spring中的基础Web框架。读者将会看到如何编写控制器来处理请求,并使用模型数据产 生响应。 当控制器的工作完成后,模型数据必须要使用一个视图来进行渲染。第6章将会探讨在Spring中可以使用的各种视图技术,包括JSP、 Apache Tiles以及Thymeleaf。 第7章的内容不再是Spring MVC的基础知识了,在本章中,读者将会学习到如何自定义Spring MVC配置、处理multipart类型的文件上 传、处理在控制器中可能会出现的异常并且会通过flash属性在请求之间传递数据。 第8章将会介绍Spring Web Flow,这是Spring MVC的一个扩展,能够开发会话式的Web应用程序。在本章中,读者将会学习到如何构建 引导用户完成特定流程的Web应用程序。 第9章读者将会学到如何使用Spring Security为自己的应用程序Web层实现安全性。 第3部分所关注的内容不再是应用程序的前端了,而是关注于如何处理和持久化数据。 第10章首先会介绍如何使用Spring对JDBC的抽象实现关系型数据库中的数据持久化。 第11章从另外一个角度介绍数据持久化,也就是使用Java持久化API(JPA)存储关系型数据库中的数据。 第12章将会介绍如何将Spring与非关系型数据库结合使用,如MongoDB和Neo4j。 不管数据存储在什么地方,缓存都有助于性能的提升,这是通过只有在必要的时候才去查询数据库实现的。第13章将会为读者介绍 Spring对声明式缓存的支持。 第14章重新回到Spring Security,将会介绍如何通过AOP将安全性应用到方法级别。 本书的最后一部分会介绍如何将Spring应用程序与其他系统进行集成。 第15章将会学习如何创建与使用远程服务,包括RMI、Hessian、Burlap以及基于SOAP的服务。 第16章将会再次回到Spring MVC,我们将会看到如何创建RESTful服务,在这个过程中所使用的编程模型与之前在第5章中所描述的是一 致的。 第17章将会探讨Spring对异步消息的支持,本章将会包括Java消息服务(Java Message Service,JMS)以及高级消息队列协议 (Advanced Message Queuing Protocol,AMQP)。 在第18章中,异步消息有了新的花样,在这一章中读者会看到如何将Spring与WebSocket和STOMP结合起来,实现服务端与客户端之间 的异步通信。 第19章将会介绍如何使用Spring发送E-mail。 第20章会关注于Spring对Java管理扩展(Java Management Extensions,JMX)功能的支持,借助这项功能可以对Spring应用程序进行 监控和修改运行时配置。 最后,在第21章,读者将会看到一个全新并且会改变游戏规则的Spring使用方式,名为Spring Boot。我们将会看到Spring Boot如何将 Spring应用中样板式的配置移除掉,这样就能让读者更加专注于业务功能。 代码规范与下载代码规范与下载 本书中有大量的示例代码。这些代码将会使用固定宽度的代码字体。本书正文中的类名、方法名或XML片段也都使用代码字体。 很多Spring类和包的名字很长(不过会有较强的表达性)。鉴于此,我们有时候会用到换行符(➥)。 本书中的示例代码并不都是完整的。为了关注某个主题,我有时候只会展示类的一个或两个方法。本书所构建的应用程序完整代码可以在出版 社站点上下载,地址是www.manning.com/SpringinActionFourthEdition。 作者在线作者在线 购买了本书,读者就可以免费访问Manning出版社提供的在线论坛,在这里读者可以给本书写评论,问一些技术问题并可以得到作者和其他用 户的帮助。要进入这个论坛或订阅它,读者可以在浏览器中访问www.manning.com/SpringinActionFourthEdition。这个页面会告诉读者注册后 怎样进入论坛,能够得到什么帮助以及论坛的规则。 Manning对读者的许诺是为读者提供一个交流平台,在这里读者之间以及读者和作者之间可以进行有意义的交流。对于作者来说,对论坛进行 多少次的访问不是强制的,他们对本书论坛的贡献是自愿和免费的。我们建议读者尽量向作者问一些有挑战性的问题,以保持他们的兴趣! 只要本书还在发售,读者就可以访问作者在线论坛以及以前讨论的归档信息。 封面插图简介封面插图简介 《Spring实战》第4版的封面人物是“Le Caraco”,也就是约旦西南部卡拉克(Karak)省的居民。该省的首府是Al-Karak,那里的山顶有座古城 堡,对死海和周边的平原有着极佳的视野。这幅图出自1796年出版的法国旅游图书,Encyclopédiedes Voyages,该书由J.G.St.Sauveur 编写。在那时,为了娱乐而去旅游还是相对新鲜的做法,而像这样的旅游指南是很流行的,它能够让旅行家和足不出户的人们了解法国其他地 区和国外的居民。 Encyclopédiedes Voyages中多种多样的图画生动描绘了200年前世界上各个城镇和地区的独特魅力。在那时,相隔数十千米的两个地区着装 就不相同,可以通过着装判断人们究竟属于哪个地区。这本旅行指南展现了那个时代和其他历史时代的隔离感和距离感,这与我们这个运动过 度的时代是截然不同的。 从那以后,服装风格发生了改变,富有地方特色的多样性开始淡化。现在,有时很难说一个洲的居民和其他洲的居民有什么不同。从积极的方 面来看,我们或许是用原来文化和视觉上的多样性换来了个人风格的多变性,或者可以说是更为多样化和有趣的知识科技生活。 这本旅行指南中的图片反映了两个世纪前各个地区生活的多样性,我们现在用图书封面的方式对其进行了再现。Manning出版社的员工都认为 这是计算机行业中一个很有意思的创意。 前言前言 百尺竿头更进一步。十几年前,Spring刚刚进入Java开发领域,其目标是简化企业级Java开发。它使用更为简单和轻量级的模型,该模型基于 简单老式的Java对象,以此挑战了当时重量级的开发模型。 现在,已经过去了很多年,Spring也发布了众多的版本,我们可以看到Spring在企业级应用开发领域已经有了巨大的影响力。对于无数的Java 项目来说,它就是事实上的标准,并且对于一些规范和它本来想取代的框架,Spring也对其演进产生了影响。毫无疑问,如果Spring不挑战之 前版本的企业级JavaBean(EJB)规范的话,现在的EJB规范肯定是完全不同的一个样子。 但是,Spring本身也在持续地演化和提升,它一直致力于将困难的开发任务进行简化,不断地为Java开发人员带来创新性的特性。在Spring最 初所挑战的领域,Spring已经突飞猛进,涉及的范围扩展到Java应用开发的各个方面。 因此,为了介绍Spring的现状,我们需要对这本书升级了。在本书上一版出版到现在的几年间,发生了太多的事情,想在这一版中将所有的变 化都涵盖进来是不可能的。不过,在第4版的《Spring实战》中,我依然会使其包含尽可能多的内容。下面列出了在这一版中新增的一些令人 兴奋的新内容: 强调基于Java的Spring配置,基于Java的配置方案几乎可以用在所有Spring开发领域之中; 条件化的配置以及profile特性能够让Spring在运行时确定该使用或忽略哪些Spring配置; Spring MVC的多项增强和改善,尤其是与创建REST服务相关的; 在Spring应用中使用Thymeleaf替代JSP; 使用基于Java的配置启用Spring Security; 使用Spring Data,在运行时自动为JPA、MongoDB和Neo4j生成Repository实现; Spring新提供的声明式缓存支持; 借助WebSocket和STOMP,实现异步的Web消息; Spring Boot,改变使用Spring游戏规则的新方法。 如果在Spring方面读者已经有相当多经验的话,那么将会发现这些新元素对于自己的Spring工具箱来说是非常有价值的补充。如果读者是要学 习Spring的新手,那么就赶上了学习Spring的一个好时代,这本书会帮助读者起步。 对于Spring的使用来说,这的确是一个令人兴奋的时代。在过去的12年里,在使用Spring进行开发以及编写与之相关的文章方面形成了一股浪 潮。我迫不及待地想看到Spring接下来会做些什么! 译者序译者序 3年前,有幸和耿渊同学合作翻译了《Spring实战(第3版)》。3年的时光过去了,技术在不断发展,这本书也推出了最新的第4版,顺利将这 本书翻译完成后,顿时感觉轻松了许多。译书是一件比较辛苦的工作,但是在这3年的时间内,每当看到有朋友选择本书来学习Spring,自己 觉得还是蛮有成就感的。所以,看到本书的第4版时,我迫不及待地联系编辑约定了本书的翻译事宜。 本书的作者Craig Walls先生,从10年前编写本书的第1版开始,持续把一件事情做好,紧跟技术的发展,不断地升级和更新这本书的内容,世 界范围内无数的Java开发者通过这本书学习和掌握了Spring技术。 本书的主题是Spring框架,从十多年前问世以来,它一直致力于简化JEE应用的开发。从最初的挑战者,到现在诸多标准的制定者;从传统的 JEE应用,到大数据、NoSQL、企业应用集成、批处理、移动开发等领域,Spring都在参与和发挥影响力。新版本的Spring提供了更加丰富的 功能,但更重要的是Spring在想尽办法简化开发人员的使用,包括自动配置、基于Java的配置,还有现在越来越受到欢迎的Spring Boot。 Spring Boot是对Spring本身的一种颠覆和革命,但是唯有这种颠覆,才会换来开发人员更多的喜爱和框架本身的发展。 这本书从第1版到第4版之所以长盛不衰,是因为它紧跟了技术的发展;Spring十多年来一直受到Java开发者的青睐,是因为它不断地进步和改 善,并且坚持最初的目标:简化企业级Java的开发。处于一个不断革新的领域,我们技术人员何尝不需要如此呢,只有不断地汲取新的知识, 学习新的技术,才能保证不被时代所淘汰。 本书涵盖了Spring框架的许多领域,既有核心框架,也有各种功能扩展,不少的同学曾经对我言及,感觉书中所讲述的内容深度不够,但是我 个人认为,对于开源框架的学习,我们会有不同的掌握深度,从最初始的使用、配置,到设计原理,再到源码分析,一本书很难面面俱到深入 介绍所有的内容,但是它却能够提供一个方向,让我们按图索骥深入学习更多的知识。 译书占用了大量的业余时间,因此感谢我的爱人,帮我承担了许多家务和带孩子的工作,还要感谢我的儿子,每天看到他的成长和进步,都让 我感觉如果懈怠的话,该被小朋友嘲笑了。 尽管在翻译的过程中,我力争达到准确和通畅,并与作者进行了很多的沟通和交流,但限于水平和时间,肯定还有许多的不足或纰漏之处,热 忱期待您提出意见,希望本书能够对您有用!您可以通过levinzhang1981@126.com联系到我。 张卫滨 2015年11月于大连 致谢致谢 在本书付印之前,在本书捆扎之前,在本书装箱之前,在本书交付运输之前,在本书到达你手里之前,在整个过程中,有很多双手都曾经接触 过它。即便你阅读电子版,省去了上面所述的流程,在你所下载的位和字节上依然凝结着很多双手的辛勤劳动——编辑、审阅、录入以及校 对。如果没有这么多人的付出,这本书也就不会存在了。 首先,我要感谢Manning辛苦工作的每个人,当这本书的进展速度没有达到预期时,他们给予了足够的耐心,并促使我完成这本书:Marjan Bace、Michael Stephens、Cynthia Kane、Andy Carroll、Benjamin Berg、Alyson Brener、Dottie Marisco、Mary Piergies、Janet Vail以及幕 后的其他很多人。 写书的时候,尽早和频繁的反馈是相当重要的,这一点与开发软件是一样的。当这本书还非常粗糙的时候,有些人审阅了初稿并提供反馈,帮 助本书最终成型。要感谢下面的人:Bob Casazza、Chaoho Hsieh、Christophe Martini、Gregor Zurowski、James Wright、Jeelani Basha、 Jens Richter、Jonathan Thoms、Josh Hart、Karen Christenson、Mario Arias、Michael Roberts、Paul Balogh、Ricardo da Silva Lima。尤其 要感谢John Ryan,在本书交付前,他对书稿进行了全面的技术审校。 当然,我要感谢美丽的妻子,感谢她容忍我开始了这个新的写作工程,感谢她整个过程中所给予我的鼓励。我深深地爱着你。 Maisy和Madi,世界上最可爱的小姑娘,感谢你们的拥抱、欢笑以及对本书内容别出心裁的见解。 对于Spring团队的同事,怎么说呢?你们太酷了!能够作为推动Spring前进的团队中的一员,我感到非常荣幸和感激。你们层出不穷的新创意 总是让我感到惊叹。 感谢我在用户组和No Fluff/Just Stuff会议上演讲时所遇到的每个人。 最后,感谢Phoenicians,你们(以及Epcot)太棒了![1] [1]  Phoenicians指的是远古时代的腓尼基人,他们被认为是字母系统的创建者,基于字母的所有现代语言都由此衍生而来。在迪斯尼世界的 Epcot,有名为Spaceship Earth的时光穿梭体验,我们可以了解到人类交流的历史,甚至能够回到腓尼基人的时代,在这段旅程的旁白中这样 说道:如果你觉得学习字母语言很容易的话,那感谢腓尼基人吧,是他们发明了它。这是作者的一种幽默说法。——译者注 第第1部分 部分 Spring的核心的核心 Spring可以做很多事情,它为企业级开发提供给了丰富的功能,但是这些功能的底层都依赖于它的两个核心特性,也就是依赖注入 (dependency injection,DI)和面向切面编程(aspect-oriented programming,AOP)。 作为本书的开始,在第1章“Spring之旅”中,我将快速介绍一下Spring框架,包括Spring DI和AOP的概况,以及它们是如何帮助读者解耦应用组 件的。 在第2章“装配Bean”中,我们将深入探讨如何将应用中的各个组件拼装在一起,读者将会看到Spring所提供的自动配置、基于Java的配置以及 XML配置。 在第3章“高级装配”中,将会告别基础的内容,为读者展现一些最大化Spring威力的技巧和技术,包括条件化装配、处理自动装配时的歧义性、 作用域以及Spring表达式语言。 在第4章“面向切面的Spring”中,展示如何使用Spring的AOP特性把系统级的服务(例如安全和审计)从它们所服务的对象中解耦出来。本章也 为后面的第9章、第13章和第14章做了铺垫,这几章将会分别介绍如何将Spring AOP用于声明式安全以及缓存。 第第1章 章 Spring之旅之旅 本章内容:本章内容: Spring的bean容器 介绍Spring的核心模块 更为强大的Spring生态系统 Spring的新功能 对于Java程序员来说,这是一个很好的时代。 在Java近20年的历史中,它经历过很好的时代,也经历过饱受诟病的时代。尽管有很多粗糙的地方,如applet、企业级JavaBean(Enterprise JavaBean,EJB)、Java数据对象(Java Data Object,JDO)以及无数的日志框架,但是作为一个平台,Java的历史是丰富多彩的,有很多 的企业级软件都是基于这个平台构建的。Spring是Java历史中很重要的组成部分。 在诞生之初,创建Spring的主要目的是用来替代更加重量级的企业级Java技术,尤其是EJB。相对于EJB来说,Spring提供了更加轻量级和简 单的编程模型。它增强了简单老式Java对象(Plain Old Java object,POJO)的功能,使其具备了之前只有EJB和其他企业级Java规范才具有 的功能。 随着时间的推移,EJB以及Java 2企业版(Java 2 Enterprise Edition,J2EE)在不断演化。EJB自身也提供了面向简单POJO的编程模型。现 在,EJB也采用了依赖注入(Dependency Injection,DI)和面向切面编程(Aspect-Oriented Programming,AOP)的理念,这毫无疑问是受 到Spring成功的启发。 尽管J2EE(现在称之为JEE)能够赶上Spring的步伐,但Spring也没有停止前进。Spring继续在其他领域发展,而JEE则刚刚开始涉及这些领 域,或者还完全没有开始在这些领域的创新。移动开发、社交API集成、NoSQL数据库、云计算以及大数据都是Spring正在涉足和创新的领 域。Spring的前景依然会很美好。 正如我之前所言,对于Java开发者来说,这是一个很好的时代。 本书会对Spring进行研究,在这一章中,我们将会在较为宏观的层面上介绍Spring,让你对Spring是什么有直观的体验。本章将让读者对 Spring所解决的各类问题有一个清晰的认识,同时为其他章奠定基础。 1.1 简化 简化Java开发开发 Spring是一个开源框架,最早由Rod Johnson创建,并在《Expert One-on-One:J2EE Design and Development》 (http://amzn.com/076454385)这本著作中进行了介绍。Spring是为了解决企业级应用开发的复杂性而创建的,使用Spring可以让简单的 JavaBean实现之前只有EJB才能完成的事情。但Spring不仅仅局限于服务器端开发,任何Java应用都能在简单性、可测试性和松耦合等方面从 Spring中获益。 bean的各种名称……虽然Spring用bean或者JavaBean来表示应用组件,但并不意味着Spring组件必须要遵循JavaBean规范。一个Spring组件 可以是任何形式的POJO。在本书中,我采用JavaBean的广泛定义,即POJO的同义词。 纵览全书,读者会发现Spring 可以做非常多的事情。但归根结底,支撑Spring的仅仅是少许的基本理念,所有的理念都可以追溯到Spring最根 本的使命上:简化Java开发。 这是一个郑重的承诺。许多框架都声称在某些方面做了简化,但Spring的目标是致力于全方位的简化Java开发。这势必引出更多的解 释,Spring是如何简化Java开发的? 为了降低Java开发的复杂性,Spring采取了以下4种关键策略: 基于POJO的轻量级和最小侵入性编程; 通过依赖注入和面向接口实现松耦合; 基于切面和惯例进行声明式编程; 通过切面和模板减少样板式代码。 几乎Spring所做的任何事情都可以追溯到上述的一条或多条策略。在本章的其他部分,我将通过具体的案例进一步阐述这些理念,以此来证明 Spring是如何完美兑现它的承诺的,也就是简化Java开发。让我们先从基于POJO的最小侵入性编程开始。 1.1.1 激发 激发POJO的潜能的潜能 如果你从事Java编程有一段时间了,那么你或许会发现(可能你也实际使用过)很多框架通过强迫应用继承它们的类或实现它们的接口从而导 致应用与框架绑死。一个典型的例子是EJB 2时代的无状态会话bean。早期的EJB是一个很容易想到的例子,不过这种侵入式的编程方式在早 期版本的Struts、WebWork、Tapestry以及无数其他的Java规范和框架中都能看到。 Spring竭力避免因自身的API而弄乱你的应用代码。Spring不会强迫你实现Spring规范的接口或继承Spring规范的类,相反,在基于Spring构建 的应用中,它的类通常没有任何痕迹表明你使用了Spring。最坏的场景是,一个类或许会使用Spring注解,但它依旧是POJO。 不妨举个例子,请参考下面的HelloWorldBean类: 程序清单程序清单1.1  Spring不会在不会在HelloWorldBean上有任何不合理的要求上有任何不合理的要求 可以看到,这是一个简单普通的Java类——POJO。没有任何地方表明它是一个Spring组件。Spring的非侵入编程模型意味着这个类在Spring 应用和非Spring应用中都可以发挥同样的作用。 尽管形式看起来很简单,但POJO一样可以具有魔力。Spring赋予POJO魔力的方式之一就是通过DI来装配它们。让我们看看DI是如何帮助应用 对象彼此之间保持松散耦合的。 1.1.2 依赖注入 依赖注入 依赖注入这个词让人望而生畏,现在已经演变成一项复杂的编程技巧或设计模式理念。但事实证明,依赖注入并不像它听上去那么复杂。在项 目中应用DI,你会发现你的代码会变得异常简单并且更容易理解和测试。 DI功能是如何实现的功能是如何实现的 任何一个有实际意义的应用(肯定比Hello World示例更复杂)都会由两个或者更多的类组成,这些类相互之间进行协作来完成特定的业务逻 辑。按照传统的做法,每个对象负责管理与自己相互协作的对象(即它所依赖的对象)的引用,这将会导致高度耦合和难以测试的代码。 举个例子,考虑下程序清单1.2所展现的Knight类。 程序清单程序清单1.2  DamselRescuingKnight只能执行只能执行RescueDamselQuest探险任务探险任务 可以看到,DamselRescuingKnight在它的构造函数中自行创建了Rescue DamselQuest。这使得DamselRescuingKnight紧密地 和RescueDamselQuest耦合到了一起,因此极大地限制了这个骑士执行探险的能力。如果一个少女需要救援,这个骑士能够召之即来。但 是如果一条恶龙需要杀掉,或者一个圆桌……额……需要滚起来,那么这个骑士就爱莫能助了。 更糟糕的是,为这个DamselRescuingKnight编写单元测试将出奇地困难。在这样的一个测试中,你必须保证当骑士 的embarkOnQuest()方法被调用的时候,探险的embark()方法也要被调用。但是没有一个简单明了的方式能够实现这一点。很遗 憾,DamselRescuingKnight将无法进行测试。 耦合具有两面性(two-headed beast)。一方面,紧密耦合的代码难以测试、难以复用、难以理解,并且典型地表现出“打地鼠”式的bug特性 (修复一个bug,将会出现一个或者更多新的bug)。另一方面,一定程度的耦合又是必须的——完全没有耦合的代码什么也做不了。为了完成 有实际意义的功能,不同的类必须以适当的方式进行交互。总而言之,耦合是必须的,但应当被小心谨慎地管理。 通过DI,对象的依赖关系将由系统中负责协调各对象的第三方组件在创建对象的时候进行设定。对象无需自行创建或管理它们的依赖关系,如 图1.1所示,依赖关系将被自动注入到需要它们的对象当中去。 图1.1 依赖注入会将所依赖的关系自动交给目标对象,而不是让对象自己去获取依赖 为了展示这一点,让我们看一看程序清单1.3中的BraveKnight,这个骑士不仅勇敢,而且能挑战任何形式的探险。 程序清单程序清单1.3  BraveKnight足够灵活可以接受任何赋予他的探险任务足够灵活可以接受任何赋予他的探险任务 我们可以看到,不同于之前的DamselRescuingKnight,BraveKnight没有自行创建探险任务,而是在构造的时候把探险任务作为构造 器参数传入。这是依赖注入的方式之一,即构造器注入(constructor injection)。 更重要的是,传入的探险类型是Quest,也就是所有探险任务都必须实现的一个接口。所以,BraveKnight能够响 应RescueDamselQuest、 SlayDragonQuest、 MakeRound TableRounderQuest等任意的Quest实现。 这里的要点是BraveKnight没有与任何特定的Quest实现发生耦合。对它来说,被要求挑战的探险任务只要实现了Quest接口,那么具体是 哪种类型的探险就无关紧要了。这就是DI所带来的最大收益——松耦合。如果一个对象只通过接口(而不是具体实现或初始化过程)来表明依 赖关系,那么这种依赖就能够在对象本身毫不知情的情况下,用不同的具体实现进行替换。 对依赖进行替换的一个最常用方法就是在测试的时候使用mock实现。我们无法充分地测试DamselRescuingKnight,因为它是紧耦合的; 但是可以轻松地测试BraveKnight,只需给它一个Quest的mock实现即可,如程序清单1.4所示。 程序清单程序清单1.4 为了测试 为了测试BraveKnight,需要注入一个,需要注入一个mock Quest 你可以使用mock框架Mockito去创建一个Quest接口的mock实现。通过这个mock对象,就可以创建一个新的BraveKnight实例,并通过构 造器注入这个mock Quest。当调用embarkOnQuest()方法时,你可以要求Mockito框架验证Quest的mock实现的embark()方法仅仅被调 用了一次。 将将Quest注入到注入到Knight中中 现在BraveKnight类可以接受你传递给它的任意一种Quest的实现,但该怎样把特定的Query实现传给它呢?假设,希望BraveKnight所 要进行探险任务是杀死一只怪龙,那么程序清单1.5中的SlayDragonQuest也许是挺合适的。 程序清单程序清单1.5  SlayDragonQuest是要注入到是要注入到BraveKnight中的中的Quest实现实现 我们可以看到,SlayDragonQuest实现了Quest接口,这样它就适合注入到BraveKnight中去了。与其他的Java入门样例有所不 同,SlayDragonQuest没有使用System.out.println(),而是在构造方法中请求一个更为通用的PrintStream。这里最大的问题在 于,我们该如何将SlayDragonQuest交给BraveKnight呢?又如何将PrintStream交给SlayDragonQuest呢? 创建应用组件之间协作的行为通常称为装配(wiring)。Spring有多种装配bean的方式,采用XML是很常见的一种装配方式。程序清单1.6展现 了一个简单的Spring配置文件:knights.xml,该配置文件将BraveKnight、SlayDragonQuest和PrintStream装配到了一起。 程序清单程序清单1.6 使用 使用Spring将将SlayDragonQuest注入到注入到BraveKnight中中 在这里,BraveKnight和SlayDragonQuest被声明为Spring中的bean。就BraveKnight bean来讲,它在构造时传入了 对SlayDragonQuest bean的引用,将其作为构造器参数。同时,SlayDragonQuest bean的声明使用了Spring表达式语言(Spring Expression Language),将System.out(这是一个PrintStream)传入到了SlayDragonQuest的构造器中。 如果XML配置不符合你的喜好的话,Spring还支持使用Java来描述配置。比如,程序清单1.7展现了基于Java的配置,它的功能与程序清单1.6 相同。 程序清单程序清单1.7  Spring提供了基于提供了基于Java的配置,可作为的配置,可作为XML的替代方案的替代方案 不管你使用的是基于XML的配置还是基于Java的配置,DI所带来的收益都是相同的。尽管BraveKnight依赖于Quest,但是它并不知道传递 给它的是什么类型的Quest,也不知道这个Quest来自哪里。与之类似,SlayDragonQuest依赖于PrintStream,但是在编码时它并不 需要知道这个PrintStream是什么样子的。只有Spring通过它的配置,能够了解这些组成部分是如何装配起来的。这样的话,就可以在不改 变所依赖的类的情况下,修改依赖关系。 这个样例展现了在Spring中装配bean的一种简单方法。谨记现在不要过多关注细节。第2章我们会深入讲解Spring的配置文件,同时还会了解 Spring装配bean的其他方式,甚至包括一种让Spring自动发现bean并在这些bean之间建立关联关系的方式。 现在已经声明了BraveKnight和Quest的关系,接下来我们只需要装载XML配置文件,并把应用启动起来。 观察它如何工作观察它如何工作 Spring通过应用上下文(Application Context)装载bean的定义并把它们组装起来。Spring应用上下文全权负责对象的创建和组装。Spring自带 了多种应用上下文的实现,它们之间主要的区别仅仅在于如何加载配置。 因为knights.xml中的bean是使用XML文件进行配置的,所以选择ClassPathXmlApplicationContext[1]作为应用上下文相对是比较合适 的。该类加载位于应用程序类路径下的一个或多个XML配置文件。程序清单1.8中的main()方法调 用ClassPathXmlApplicationContext加载knights.xml,并获得Knight对象的引用。 程序清单程序清单1.8  KnightMain.java加载包含加载包含Knight的的Spring上下文上下文 这里的main()方法基于knights.xml文件创建了Spring应用上下文。随后它调用该应用上下文获取一个ID为knight的bean。得到Knight对象的 引用后,只需简单调用embarkOnQuest()方法就可以执行所赋予的探险任务了。注意这个类完全不知道我们的英雄骑士接受哪种探险任 务,而且完全没有意识到这是由BraveKnight来执行的。只有knights.xml文件知道哪个骑士执行哪种探险任务。 通过示例我们对依赖注入进行了一个快速介绍。纵览全书,你将对依赖注入有更多的认识。如果你想了解更多关于依赖注入的信息,我推荐阅 读Dhanji R. Prasanna的《Dependency Injection》,该著作覆盖了依赖注入的所有内容。 现在让我们再关注Spring简化Java开发的下一个理念:基于切面进行声明式编程。 1.1.3 应用切面 应用切面 DI能够让相互协作的软件组件保持松散耦合,而面向切面编程(aspect-oriented programming,AOP)允许你把遍布应用各处的功能分离出来 形成可重用的组件。 面向切面编程往往被定义为促使软件系统实现关注点的分离一项技术。系统由许多不同的组件组成,每一个组件各负责一块特定功能。除了实 现自身核心的功能之外,这些组件还经常承担着额外的职责。诸如日志、事务管理和安全这样的系统服务经常融入到自身具有核心业务逻辑的 组件中去,这些系统服务通常被称为横切关注点,因为它们会跨越系统的多个组件。 如果将这些关注点分散到多个组件中去,你的代码将会带来双重的复杂性。 实现系统关注点功能的代码将会重复出现在多个组件中。这意味着如果你要改变这些关注点的逻辑,必须修改各个模块中的相关实现。 即使你把这些关注点抽象为一个独立的模块,其他模块只是调用它的方法,但方法的调用还是会重复出现在各个模块中。 组件会因为那些与自身核心业务无关的代码而变得混乱。一个向地址簿增加地址条目的方法应该只关注如何添加地址,而不应该关注它 是不是安全的或者是否需要支持事务。 图1.2展示了这种复杂性。左边的业务对象与系统级服务结合得过于紧密。每个对象不但要知道它需要记日志、进行安全控制和参与事务,还 要亲自执行这些服务。 图1.2 在整个系统内,关注点(例如日志和安全) 的调用经常散布到各个模块中,而这些关注点并不是模块的核心业务 AOP能够使这些服务模块化,并以声明的方式将它们应用到它们需要影响的组件中去。所造成的结果就是这些组件会具有更高的内聚性并且会 更加关注自身的业务,完全不需要了解涉及系统服务所带来复杂性。总之,AOP能够确保POJO的简单性。 如图1.3所示,我们可以把切面想象为覆盖在很多组件之上的一个外壳。应用是由那些实现各自业务功能的模块组成的。借助AOP,可以使用 各种功能层去包裹核心业务层。这些层以声明的方式灵活地应用到系统中,你的核心应用甚至根本不知道它们的存在。这是一个非常强大的理 念,可以将安全、事务和日志关注点与核心业务逻辑相分离。 图1.3 利用AOP,系统范围内的关注点覆盖在它们所影响组件之上 为了示范在Spring中如何应用切面,让我们重新回到骑士的例子,并为它添加一个切面。 AOP应用应用 每一个人都熟知骑士所做的任何事情,这是因为吟游诗人用诗歌记载了骑士的事迹并将其进行传唱。假设我们需要使用吟游诗人这个服务类来 记载骑士的所有事迹。程序清单1.9展示了我们会使用的Minstrel类。 程序清单程序清单1.9 吟游诗人是中世纪的音乐记录器 吟游诗人是中世纪的音乐记录器 正如你所看到的那样,Minstrel是只有两个方法的简单类。在骑士执行每一个探险任务之前,singBeforeQuest()方法会被调用;在骑 士完成探险任务之后,singAfterQuest()方法会被调用。在这两种情况下,Minstrel都会通过一个PrintStream类来歌颂骑士的事 迹,这个类是通过构造器注入进来的。 把Minstrel加入你的代码中并使其运行起来,这对你来说是小事一桩。我们适当做一下调整从而让BraveKnight可以使用Minstrel。程 序清单1.10展示了将BraveKnight和Minstrel组合起来的第一次尝试。 程序清单程序清单1.10  BraveKnight必须要调用必须要调用Minstrel的方法的方法 这应该可以达到预期效果。现在,你所需要做的就是回到Spring配置中,声明Minstrel bean并将其注入到BraveKnight的构造器之中。 但是,请稍等…… 我们似乎感觉有些东西不太对。管理他的吟游诗人真的是骑士职责范围内的工作吗?在我看来,吟游诗人应该做他份内的事,根本不需要骑士 命令他这么做。毕竟,用诗歌记载骑士的探险事迹,这是吟游诗人的职责。为什么骑士还需要提醒吟游诗人去做他份内的事情呢? 此外,因为骑士需要知道吟游诗人,所以就必须把吟游诗人注入到BarveKnight类中。这不仅使BraveKnight的代码复杂化了,而且还让 我疑惑是否还需要一个不需要吟游诗人的骑士呢?如果Minstrel为null会发生什么呢?我是否应该引入一个空值校验逻辑来覆盖该场景? 简单的BraveKnight类开始变得复杂,如果你还需要应对没有吟游诗人时的场景,那代码会变得更复杂。但利用AOP,你可以声明吟游诗人 必须歌颂骑士的探险事迹,而骑士本身并不用直接访问Minstrel的方法。 要将Minstrel抽象为一个切面,你所需要做的事情就是在一个Spring配置文件中声明它。程序清单1.11是更新后的knights.xml文 件,Minstrel被声明为一个切面。 程序清单程序清单1.11 将 将Minstrel声明为一个切面声明为一个切面 这里使用了Spring的aop配置命名空间把Minstrel bean声明为一个切面。首先,需要把Minstrel声明为一个bean,然后 在元素中引用该bean。为了进一步定义切面,声明(使用)在embarkOnQuest()方法执行前调 用Minstrel的singBeforeQuest()方法。这种方式被称为前置通知(before advice)。同时声明(使用) 在embarkOnQuest()方法执行后调用singAfter Quest()方法。这种方式被称为后置通知(after advice)。 在这两种方式中,pointcut-ref属性都引用了名字为embank的切入点。该切入点是在前边的元素中定义的,并配 置expression属性来选择所应用的通知。表达式的语法采用的是AspectJ的切点表达式语言。 现在,你无需担心不了解AspectJ或编写AspectJ切点表达式的细节,我们稍后会在第4章详细地探讨Spring AOP的内容。现在你已经知 道,Spring在骑士执行探险任务前后会调用Minstrel的singBeforeQuest()和singAfterQuest()方法,这就足够了。 这就是我们需要做的所有的事情!通过少量的XML配置,就可以把Minstrel声明为一个Spring切面。如果你现在还没有完全理解,不必担 心,在第4章你会看到更多的Spring AOP示例,那将会帮助你彻底弄清楚。现在我们可以从这个示例中获得两个重要的观点。 首先,Minstrel仍然是一个POJO,没有任何代码表明它要被作为一个切面使用。当我们按照上面那样进行配置后,在Spring的上下文 中,Minstrel实际上已经变成一个切面了。 其次,也是最重要的,Minstrel可以被应用到BraveKnight中,而BraveKnight不需要显式地调用它。实际上,BraveKnight完全不 知道Minstrel的存在。 必须还要指出的是,尽管我们使用Spring魔法把Minstrel转变为一个切面,但首先要把它声明为一个Spring bean。能够为其他Spring bean 做到的事情都可以同样应用到Spring切面中,例如为它们注入依赖。 应用切面来歌颂骑士可能只是有点好玩而已,但是Spring AOP可以做很多有实际意义的事情。在后续的各章中,你还会了解基于Spring AOP 实现声明式事务和安全(第9章和第14章)。 但现在,让我们再看看 Spring简化Java开发的其他方式。 1.1.4 使用模板消除样板式代码 使用模板消除样板式代码 你是否写过这样的代码,当编写的时候总会感觉以前曾经这么写过?我的朋友,这不是似曾相识。这是样板式的代码(boilerplate code)。通 常为了实现通用的和简单的任务,你不得不一遍遍地重复编写这样的代码。 遗憾的是,它们中的很多是因为使用Java API而导致的样板式代码。样板式代码的一个常见范例是使用JDBC访问数据库查询数据。举个例 子,如果你曾经用过JDBC,那么你或许会写出类似下面的代码。 程序清单程序清单1.12 许多 许多Java API,例如,例如JDBC,会涉及编写大量的样板式代码,会涉及编写大量的样板式代码 正如你所看到的,这段JDBC代码查询数据库获得员工姓名和薪水。我打赌你很难把上面的代码逐行看完,这是因为少量查询员工的代码淹没 在一堆JDBC的样板式代码中。首先你需要创建一个数据库连接,然后再创建一个语句对象,最后你才能进行查询。为了平息JDBC可能会出 现的怒火,你必须捕捉SQLException,这是一个检查型异常,即使它抛出后你也做不了太多事情。 最后,毕竟该说的也说了,该做的也做了,你不得不清理战场,关闭数据库连接、语句和结果集。同样为了平息JDBC可能会出现的怒火,你 依然要捕捉SQLException。 程序清单1.12中的代码和你实现其他JDBC操作时所写的代码几乎是相同的。只有少量的代码与查询员工逻辑有关系,其他的代码都是JDBC的 样板代码。 JDBC不是产生样板式代码的唯一场景。在许多编程场景中往往都会导致类似的样板式代码,JMS、JNDI和使用REST服务通常也涉及大量的 重复代码。 Spring旨在通过模板封装来消除样板式代码。Spring的JdbcTemplate使得执行数据库操作时,避免传统的JDBC样板代码成为了可能。 举个例子,使用Spring的JdbcTemplate(利用了 Java 5特性的JdbcTemplate实现)重写的getEmployeeById()方法仅仅关注于获取 员工数据的核心逻辑,而不需要迎合JDBC API的需求。程序清单1.13展示了修订后的getEmployeeById()方法。 程序清单程序清单1.13 模板能够让你的代码关注于自身的职责 模板能够让你的代码关注于自身的职责 正如你所看到的,新版本的getEmployeeById()简单多了,而且仅仅关注于从数据库中查询员工。模板的queryForObject()方法需要 一个SQL查询语句,一个RowMapper对象(把数据映射为一个域对象),零个或多个查询参数。GetEmp loyeeById()方法再也看不到以 前的JDBC样板式代码了,它们全部被封装到了模板中。 我已经向你展示了Spring通过面向POJO编程、DI、切面和模板技术来简化Java开发中的复杂性。在这个过程中,我展示了在基于XML的配置 文件中如何配置bean和切面,但这些文件是如何加载的呢?它们被加载到哪里去了?让我们再了解下Spring容器,这是应用中的所有bean所驻 留的地方。 1.2 容纳你的 容纳你的Bean 在基于Spring的应用中,你的应用对象生存于Spring容器(container)中。如图1.4所示,Spring容器负责创建对象,装配它们,配置它们并管 理它们的整个生命周期,从生存到死亡(在这里,可能就是new到finalize())。 图1.4 在Spring应用中,对象由Spring容器创建和装配,并存在容器之中 在下一章,你将了解如何配置Spring,从而让它知道该创建、配置和组装哪些对象。但首先,最重要的是了解容纳对象的容器。理解容器将有 助于理解对象是如何被管理的。 容器是Spring框架的核心。Spring容器使用DI管理构成应用的组件,它会创建相互协作的组件之间的关联。毫无疑问,这些对象更简单干净, 更易于理解,更易于重用并且更易于进行单元测试。 Spring容器并不是只有一个。Spring自带了多个容器实现,可以归为两种不同的类型。bean工厂(由org.springframework. beans. factory.eanFactory接口定义)是最简单的容器,提供基本的DI支持。应用上下文 (由org.springframework.context.ApplicationContext接口定义)基于BeanFactory构建,并提供应用框架级别的服务,例如从 属性文件解析文本信息以及发布应用事件给感兴趣的事件监听者。 虽然我们可以在bean工厂和应用上下文之间任选一种,但bean工厂对大多数应用来说往往太低级了,因此,应用上下文要比bean工厂更受欢 迎。我们会把精力集中在应用上下文的使用上,不再浪费时间讨论bean工厂。 1.2.1 使用应用上下文 使用应用上下文 Spring自带了多种类型的应用上下文。下面罗列的几个是你最有可能遇到的。 AnnotationConfigApplicationContext:从一个或多个基于Java的配置类中加载Spring应用上下文。 AnnotationConfigWebApplicationContext:从一个或多个基于Java的配置类中加载Spring Web应用上下文。 ClassPathXmlApplicationContext:从类路径下的一个或多个XML配置文件中加载上下文定义,把应用上下文的定义文件作为类 资源。 FileSystemXmlapplicationcontext:从文件系统下的一个或多个XML配置文件中加载上下文定义。 XmlWebApplicationContext:从Web应用下的一个或多个XML配置文件中加载上下文定义。 当在第8章讨论基于Web的Spring应用时,我们会对AnnotationConfigWeb- ApplicationContext和XmlWebApplicationContext进行更详细的讨论。现在我们先简单地使 用FileSystemXmlApplicationContext从文件系统中加载应用上下文或者使用ClassPathXmlApplicationContext从类路径中加 载应用上下文。 无论是从文件系统中装载应用上下文还是从类路径下装载应用上下文,将bean加载到bean工厂的过程都是相似的。例如,如下代码展示了如 何加载一个FileSystemXmlApplicationContext: 类似地,你可以使用ClassPathXmlApplicationContext从应用的类路径下加载应用上下文: 使用FileSystemXmlApplicationContext和使用ClassPathXmlApp-licationContext的区别在 于:FileSystemXmlApplicationContext在指定的文件系统路径下查找knight.xml文件; 而ClassPathXmlApplicationContext是在所有的类路径(包含JAR文件)下查找 knight.xml文件。 如果你想从Java配置中加载应用上下文,那么可以使用AnnotationConfig-ApplicationContext: 在这里没有指定加载Spring应用上下文所需的XML文件,AnnotationConfig-ApplicationContext通过一个配置类加载bean。 应用上下文准备就绪之后,我们就可以调用上下文的getBean()方法从Spring容器中获取bean。 现在你应该基本了解了如何创建Spring容器,让我们对容器中bean的生命周期做更进一步的探究。 1.2.2  bean的生命周期的生命周期 在传统的Java应用中,bean的生命周期很简单。使用Java关键字new进行bean实例化,然后该bean就可以使用了。一旦该bean不再被使用, 则由Java自动进行垃圾回收。 相比之下,Spring容器中的bean的生命周期就显得相对复杂多了。正确理解Spring bean的生命周期非常重要,因为你或许要利用Spring提供的 扩展点来自定义bean的创建过程。图1.5展示了bean装载到Spring应用上下文中的一个典型的生命周期过程。 图1.5 bean在Spring容器中从创建到销毁经历了 若干阶段,每一阶段都可以针对Spring如何管理bean进行个性化定制 正如你所见,在bean准备就绪之前,bean工厂执行了若干启动步骤。我们对图1.5进行详细描述: 1.Spring对bean进行实例化; 2.Spring将值和bean的引用注入到bean对应的属性中; 3.如果bean实现了BeanNameAware接口,Spring将bean的ID传递给setBean-Name()方法; 4.如果bean实现了BeanFactoryAware接口,Spring将调用setBeanFactory()方法,将BeanFactory容器实例传入; 5.如果bean实现了ApplicationContextAware接口,Spring将调用setApplicationContext()方法,将bean所在的应用上下文的 引用传入进来; 6.如果bean实现了BeanPostProcessor接口,Spring将调用它们的post-ProcessBeforeInitialization()方法; 7.如果bean实现了InitializingBean接口,Spring将调用它们的after-PropertiesSet()方法。类似地,如果bean使用init- method声明了初始化方法,该方法也会被调用; 8.如果bean实现了BeanPostProcessor接口,Spring将调用它们的post-ProcessAfterInitialization()方法; 9.此时,bean已经准备就绪,可以被应用程序使用了,它们将一直驻留在应用上下文中,直到该应用上下文被销毁; 10.如果bean实现了DisposableBean接口,Spring将调用它的destroy()接口方法。同样,如果bean使用destroy-method声明了销 毁方法,该方法也会被调用。 现在你已经了解了如何创建和加载一个Spring容器。但是一个空的容器并没有太大的价值,在你把东西放进去之前,它里面什么都没有。为了 从Spring的DI中受益,我们必须将应用对象装配进Spring容器中。我们将在第2章对bean装配进行更详细的探讨。 我们现在首先浏览一下Spring的体系结构,了解一下Spring框架的基本组成部分和最新版本的Spring所发布的新特性。 1.3 俯瞰 俯瞰Spring风景线风景线 正如你所看到的,Spring框架关注于通过DI、AOP和消除样板式代码来简化企业级Java开发。即使这是Spring所能做的全部事情,那Spring也 值得一用。但是,Spring实际上的功能超乎你的想象。 在Spring框架的范畴内,你会发现Spring简化Java开发的多种方式。但在Spring框架之外还存在一个构建在核心框架之上的庞大生态圈,它将 Spring扩展到不同的领域,例如Web服务、REST、移动开发以及NoSQL。 首先让我们拆开Spring框架的核心来看看它究竟为我们带来了什么,然后我们再浏览下Spring Portfolio中的其他成员。 1.3.1  Spring模块模块 当我们下载Spring发布版本并查看其lib目录时,会发现里面有多个JAR文件。在Spring 4.0中,Spring框架的发布版本包括了20个不同的模块, 每个模块会有3个JAR文件(二进制类库、源码的JAR文件以及JavaDoc的JAR文件)。完整的库JAR文件如图1.6所示。 图1.6 Spring框架由20个不同的模块组成 这些模块依据其所属的功能可以划分为6类不同的功能,如图1.7所示。 总体而言,这些模块为开发企业级应用提供了所需的一切。但是你也不必将应用建立在整个Spring框架之上,你可以自由地选择适合自身应用 需求的Spring模块;当Spring不能满足需求时,完全可以考虑其他选择。事实上,Spring甚至提供了与其他第三方框架和类库的集成点,这样 你就不需要自己编写这样的代码了。 图1.7 Spring框架由6个定义良好的模块分类组成 让我们逐一浏览Spring的模块,看看它们是如何构建起Spring整体蓝图的。 Spring核心容器核心容器 容器是Spring框架最核心的部分,它管理着Spring应用中bean的创建、配置和管理。在该模块中,包括了Spring bean工厂,它为Spring提供了 DI的功能。基于bean工厂,我们还会发现有多种Spring应用上下文的实现,每一种都提供了配置Spring的不同方式。 除了bean工厂和应用上下文,该模块也提供了许多企业服务,例如E-mail、JNDI访问、EJB集成和调度。 所有的Spring模块都构建于核心容器之上。当你配置应用时,其实你隐式地使用了这些类。贯穿本书,我们都会涉及到核心模块,在第2章中 我们将会深入探讨Spring的DI。 Spring的的AOP模块模块 在AOP模块中,Spring对面向切面编程提供了丰富的支持。这个模块是Spring应用系统中开发切面的基础。与DI一样,AOP可以帮助应用对象 解耦。借助于AOP,可以将遍布系统的关注点(例如事务和安全)从它们所应用的对象中解耦出来。 我们将在第4章深入探讨Spring对AOP支持。 数据访问与集成数据访问与集成 使用JDBC编写代码通常会导致大量的样板式代码,例如获得数据库连接、创建语句、处理结果集到最后关闭数据库连接。Spring的JDBC和 DAO(Data Access Object)模块抽象了这些样板式代码,使我们的数据库代码变得简单明了,还可以避免因为关闭数据库资源失败而引发的 问题。该模块在多种数据库服务的错误信息之上构建了一个语义丰富的异常层,以后我们再也不需要解释那些隐晦专有的SQL错误信息了! 对于那些更喜欢ORM(Object-Relational Mapping)工具而不愿意直接使用JDBC的开发者,Spring提供了ORM模块。Spring的ORM模块建立 在对DAO的支持之上,并为多个ORM框架提供了一种构建DAO的简便方式。Spring没有尝试去创建自己的ORM解决方案,而是对许多流行的 ORM框架进行了集成,包括Hibernate、Java Persisternce API、Java Data Object和iBATIS SQL Maps。Spring的事务管理支持所有的ORM框 架以及JDBC。 在第10章讨论Spring数据访问时,你会看到Spring基于模板的JDBC抽象层能够极大地简化JDBC代码。 本模块同样包含了在JMS(Java Message Service)之上构建的Spring抽象层,它会使用消息以异步的方式与其他应用集成。从Spring 3.0开 始,本模块还包含对象到XML映射的特性,它最初是Spring Web Service项目的一部分。 除此之外,本模块会使用Spring AOP模块为Spring应用中的对象提供事务管理服务。 Web与远程调用与远程调用 MVC(Model-View-Controller)模式是一种普遍被接受的构建Web应用的方法,它可以帮助用户将界面逻辑与应用逻辑分离。Java从来不缺少 MVC框架,Apache的Struts、JSF、WebWork和Tapestry都是可选的最流行的MVC框架。 虽然Spring能够与多种流行的MVC框架进行集成,但它的Web和远程调用模块自带了一个强大的MVC框架,有助于在Web层提升应用的松耦 合水平。在第5章到第7章中,我们将会学习Spring的MVC框架。 除了面向用户的Web应用,该模块还提供了多种构建与其他应用交互的远程调用方案。Spring远程调用功能集成了RMI(Remote Method Invocation)、Hessian、Burlap、JAX-WS,同时Spring还自带了一个远程调用框架:HTTP invoker。Spring还提供了暴露和使用REST API的 良好支持。 我们将会在第15章讨论Spring的远程调用功能。在第16章学习如何创建和使用REST API。 Instrumentation Spring的Instrumentation模块提供了为JVM添加代理(agent)的功能。具体来讲,它为Tomcat提供了一个织入代理,能够为Tomcat传递类文 件,就像这些文件是被类加载器加载的一样。 如果这听起来有点难以理解,不必对此过于担心。这个模块所提供的Instrumentation使用场景非常有限,在本书中,我们不会介绍该模块。 测试测试 鉴于开发者自测的重要性,Spring提供了测试模块以致力于Spring应用的测试。 通过该模块,你会发现Spring为使用JNDI、Servlet和Portlet编写单元测试提供了一系列的mock对象实现。对于集成测试,该模块为加载Spring 应用上下文中的bean集合以及与Spring上下文中的bean进行交互提供了支持。 在本书中,有很多的样例都是测试驱动的,将会使用到Spring所提供的测试功能。 1.3.2  Spring Portfolio 当谈论Spring时,其实它远远超出我们的想象。事实上,Spring远不是Spring框架所下载的那些。如果仅仅停留在核心的Spring框架层面,我 们将错过Spring Portfolio所提供的巨额财富。整个Spring Portfolio包括多个构建于核心Spring框架之上的框架和类库。概括地讲,整个Spring Portfolio几乎为每一个领域的Java开发都提供了Spring编程模型。 或许需要几卷书才能覆盖Spring Portfolio所提供的所有内容,这也远远超出了本书的范围。不过,我们会介绍Spring Portfolio中的一些项目, 同样,我们将体验一下核心框架之外的另一番风景。 Spring Web Flow Spring Web Flow建立于Spring MVC框架之上,它为基于流程的会话式Web应用(可以想一下购物车或者向导功能)提供了支持。我们将在第 8章讨论更多关于Spring Web Flow的内容,你还可以访问Spring Web Flow的主页(http://projects.spring.io/spring-webflow/)。 Spring Web Service 虽然核心的Spring框架提供了将Spring bean以声明的方式发布为Web Service的功能,但是这些服务是基于一个具有争议性的架构(拙劣的契 约后置模型)之上而构建的。这些服务的契约由bean的接口来决定。 Spring Web Service提供了契约优先的Web Service模型,服务的实现都 是为了满足服务的契约而编写的。 本书不会再探讨Spring Web Service,但是你可以浏览站点http://docs.spring.io/spring- ws/site/来了解更多关于Spring Web Service的信息。 Spring Security 安全对于许多应用都是一个非常关键的切面。利用Spring AOP,Spring Security为Spring应用提供了声明式的安全机制。你将会在第9章看到如 何为应用的Web层添加Spring Security功能。同时,我们还会在第14章重新回到Spring Security的话题,学习如何保护方法调用。你可以在主 页http://projects.spring.io/spring-security/上获得关于Spring Security的更多信息。 Spring Integration 许多企业级应用都需要与其他应用进行交互。Spring Integration提供了多种通用应用集成模式的Spring声明式风格实现。 我们不会在本书覆盖Spring Integration的内容,但是如果你想了解更多关于Spring Integration的信息,我推荐Mark Fisher、Jonas Partner、 Marius Bogoevici和Iwein Fuld编写的《Spring Integration in Action》(Manning,2012,www.manning.com/fisher/);或者你还可以访问Spring Integration的主页http://projects.spring.io/spring-integration/。 Spring Batch 当我们需要对数据进行大量操作时,没有任何技术可以比批处理更胜任这种场景。如果需要开发一个批处理应用,你可以通过Spring Batch, 使用Spring强大的面向POJO的编程模型。 Spring Batch超出了本书的范畴,但是你可以阅读Arnaud Cogoluegnes、Thierry Templier、Gary Gregory和Olivier Bazoud编写的《Spring Batch in Action》(Manning,2012,www.manning.com/templier/),或者访问Spring Batch的主页http://projects.spring.io/ spring-batch/。 Spring Data Spring Data使得在Spring中使用任何数据库都变得非常容易。尽管关系型数据库统治企业级应用多年,但是现代化的应用正在认识到并不是所 有的数据都适合放在一张表中的行和列中。一种新的数据库种类,通常被称之为NoSQL数据库[2],提供了使用数据的新方法,这些方法会比传 统的关系型数据库更为合适。 不管你使用文档数据库,如MongoDB,图数据库,如Neo4j,还是传统的关系型数据库,Spring Data都为持久化提供了一种简单的编程模型。 这包括为多种数据库类型提供了一种自动化的Repository机制,它负责为你创建Repository的实现。 我们将会在第11章看到如何使用Spring Data简化Java Persistence API(JPA)开发,然后在第12章,将相关的讨论拓展至几种NoSQL数据 库。 Spring Social 社交网络是互联网领域中新兴的一种潮流,越来越多的应用正在融入社交网络网站,例如Facebook或者Twitter。如果对此感兴趣,你可以了解 一下Spring Social,这是Spring的一个社交网络扩展模块。 不过,Spring Social并不仅仅是tweet和好友。尽管名字是这样,但Spring Social更多的是关注连接(connect),而不是社交(social)。它能 够帮助你通过REST API连接Spring应用,其中有些Spring应用可能原本并没有任何社交方面的功能目标。 限于篇幅,我们在本书中不会涉及Spring Social。但是,如果你对Spring如何帮助你连接Facebook或Twitter感兴趣的话,可以查看网 址https://spring.io/guides/gs/accessing- facebook/和https://spring.io/guides/gs/accessing-twitter/中的入门指南。 Spring Mobile 移动应用是另一个引人瞩目的软件开发领域。智能手机和平板设备已成为许多用户首选的客户端。Spring Mobile是Spring MVC新的扩展模 块,用于支持移动Web应用开发。 Spring for Android 与Spring Mobile相关的是Spring Android项目。这个新项目,旨在通过Spring框架为开发基于Android设备的本地应用提供某些简单的支持。最 初,这个项目提供了Spring RestTemplate的一个可以用于Android应用之中的版本。它还能与Spring Social协作,使得原生应用可以通过 REST API进行社交网络的连接。 本书中,我不会讨论Spring for Android,不过你可以通过http://projects.spring.io /spring-android/了解更多内容。 Spring Boot Spring极大地简化了众多的编程任务,减少甚至消除了很多样板式代码,如果没有Spring的话,在日常工作中你不得不编写这样的样板代码。 Spring Boot是一个崭新的令人兴奋的项目,它以Spring的视角,致力于简化Spring本身。 Spring Boot大量依赖于自动配置技术,它能够消除大部分(在很多场景中,甚至是全部)Spring配置。它还提供了多个Starter项目,不管你使 用Maven还是Gradle,这都能减少Spring工程构建文件的大小。 在本书即将结束的第21章,我们将会学习Spring Boot。 1.4  Spring的新功能的新功能 当本书的第3版交付印刷的时候,当时Spring的最新版本是3.0.5。那大约是在3年前,从那时到现在发生了很多的变化。Spring框架经历了3个 重要的发布版本——3.1、3.2以及现在的4.0——每个版本都带来了新的特性和增强,以简化应用程序的研发。Spring Portfolio中的一些成员项 目也经历了重要的变更。 本书也进行了更新,试图涵盖这些发布版本中众多最令人兴奋和有用的特性。但现在,我们先简要地了解一下Spring带来了哪些新功能。 1.4.1  Spring 3.1新特性新特性 Spring 3.1带来了多项有用的新特性和增强,其中有很多都是关于如何简化和改善配置的。除此之外,Spring 3.1还提供了声明式缓存的支持以 及众多针对Spring MVC的功能增强。下面的列表展现了Spring 3.1重要的功能升级: 为了解决各种环境下(如开发、测试和生产)选择不同配置的问题,Spring 3.1引入了环境profile功能。借助于profile,就能根据应用部 署在什么环境之中选择不同的数据源bean; 在Spring 3.0基于Java的配置之上,Spring 3.1添加了多个enable注解,这样就能使用这个注解启用Spring的特定功能; 添加了Spring对声明式缓存的支持,能够使用简单的注解声明缓存边界和规则,这与你以前声明事务边界很类似; 新添加的用于构造器注入的c命名空间,它类似于Spring 2.0所提供的面向属性的p命名空间,p命名空间用于属性注入,它们都是非常简 洁易用的; Spring开始支持Servlet 3.0,包括在基于Java的配置中声明Servlet和Filter,而不再借助于web.xml; 改善Spring对JPA的支持,使得它能够在Spring中完整地配置JPA,不必再使用persistence.xml文件。 Spring 3.1还包含了多项针对Spring MVC的功能增强: 自动绑定路径变量到模型属性中; 提供了@RequestMappingproduces和consumes属性,用于匹配请求中的Accept和Content-Type头部信息; 提供了@RequestPart注解,用于将multipart请求中的某些部分绑定到处理器的方法参数中; 支持Flash属性(在redirect请求之后依然能够存活的属性)以及用于在请求间存放flash属性的RedirectAttributes类型。 除了Spring 3.1所提供的新功能以外,同等重要的是要注意Spring 3.1不再支持的功能。具体来讲,为了支持原生的EntityManager,Spring 的JpaTemplate和JpaDaoSupport类被废弃掉了。尽管它们已经被废弃了,但直到Spring 3.2版本,它依然是可以使用的。但最好不要再 使用它们了,因为它们不会进行更新以支持JPA 2.0,并且已经在Spring 4中移除掉了。 现在,让我们看一下Spring 3.2提供了什么新功能。 1.4.2  Spring 3.2新特性新特性 Spring 3.1在很大程度上聚焦于配置改善以及其他的一些增强,包括Spring MVC的增强,而Spring 3.2是主要关注Spring MVC的一个发布版 本。Spring MVC 3.2带来了如下的功能提升: Spring 3.2的控制器(Controller)可以使用Servlet 3.0的异步请求,允许在一个独立的线程中处理请求,从而将Servlet线程解放出来处理 更多的请求; 尽管从Spring 2.5开始,Spring MVC控制器就能以POJO的形式进行很便利地测试,但是Spring 3.2引入了Spring MVC测试框架,用于为 控制器编写更为丰富的测试,断言它们作为控制器的行为行为是否正确,而且在使用的过程中并不需要Servlet容器; 除了提升控制器的测试功能,Spring 3.2还包含了基于RestTemplate的客户端的测试支持,在测试的过程中,不需要往真正的REST端 点上发送请求; @ControllerAdvice注解能够将通用的@ExceptionHandler、@ InitBinder和@ModelAttributes方法收集到一个类中,并 应用到所有控制器上; 在Spring 3.2之前,只能通过ContentNegotiatingViewResolver使用完整的内容协商(full content negotiation)功能。但是在 Spring 3.2中,完整的内容协商功能可以在整个Spring MVC中使用,即便是依赖于消息转换器(message converter)使用和产生内容的 控制器方法也能使用该功能; Spring MVC 3.2包含了一个新的@MatrixVariable注解,这个注解能够将请求中的矩阵变量(matrix variable)绑定到处理器的方法参 数中; 基础的抽象类AbstractDispatcherServletInitializer能够非常便利地配置DispatcherServlet,而不必再使用web.xml。 与之类似,当你希望通过基于Java的方式来配置Spring的时候,可以使用Abstract- Annotat ionConfigDispatcherServletInitializer的子类; 新增了ResponseEntityExceptionHandler,可以用来替代Default- HandlerException Resolver。ResponseEntityExceptionHandler方法会返回ResponseEntity,而不是ModelAndView; RestTemplate和@RequestBody的参数可以支持范型; RestTemplate和@RequestMapping可以支持HTTP PATCH方法; 在拦截器匹配时,支持使用URL模式将其排除在拦截器的处理功能之外。 虽然Spring MVC是Spring 3.2改善的核心内容,但是它依然还增加了多项非MVC的功能改善。下面列出了Spring 3.2中几项最为有意思的新特 性: @Autowired、@Value和@Bean注解能够作为元注解,用于创建自定义的注入和bean声明注解; @DateTimeFormat注解不再强依赖JodaTime。如果提供了JodaTime,就会使用它,否则的话,会使用SimpleDateFormat; Spring的声明式缓存提供了对JCache 0.5的支持; 支持定义全局的格式来解析和渲染日期与时间; 在集成测试中,能够配置和加载WebApplicationContext; 在集成测试中,能够针对request和session作用域的bean进行测试。 在本书的多个章节中,都能看到Spring 3.2的特性,尤其是在Web和REST相关的章节中。 1.4.3  Spring 4.0新特性新特性 当编写本书时,Spring 4.0是最新的发布版本。在Spring 4.0中包含了很多令人兴奋的新特性,包括: Spring提供了对WebSocket编程的支持,包括支持JSR-356——Java API for WebSocket; 鉴于WebSocket仅仅提供了一种低层次的API,急需高层次的抽象,因此Spring 4.0在WebSocket之上提供了一个高层次的面向消息的编 程模型,该模型基于SockJS,并且包含了对STOMP协议的支持; 新的消息(messaging)模块,很多的类型来源于Spring Integration项目。这个消息模块支持Spring的SockJS/STOMP功能,同时提供了 基于模板的方式发布消息; Spring是第一批(如果不说是第一个的话)支持Java 8特性的Java框架,比如它所支持的lambda表达式。别的暂且不说,这首先能够让 使用特定的回调接口(如RowMapper和JdbcTemplate)更加简洁,代码更加易读; 与Java 8同时得到支持的是JSR-310——Date与Time API,在处理日期和时间时,它为开发者提供了 比java.util.Date或java.util.Calendar更丰富的API; 为Groovy开发的应用程序提供了更加顺畅的编程体验,尤其是支持非常便利地完全采用Groovy开发Spring应用程序。随这些一起提供的 是来自于Grails的BeanBuilder,借助它能够通过Groovy配置Spring应用; 添加了条件化创建bean的功能,在这里只有开发人员定义的条件满足时,才会创建所声明的bean; Spring 4.0包含了Spring RestTemplate的一个新的异步实现,它会立即返回并且允许在操作完成后执行回调; 添加了对多项JEE规范的支持,包括JMS 2.0、JTA 1.2、JPA 2.1和Bean Validation 1.1。 可以看到,在Spring框架的最新发布版本中,包含了很多令人兴奋的新特性。在本书中,我们将会看到很多这样的新特性,同时也会学习 Spring中长期以来一直存在的特性。 1.5 小结 小结 现在,你应该对Spring的功能特性有了一个清晰的认识。Spring致力于简化企业级Java开发,促进代码的松散耦合。成功的关键在于依赖注入 和AOP。 在本章,我们先体验了Spring的DI。DI是组装应用对象的一种方式,借助这种方式对象无需知道依赖来自何处或者依赖的实现方式。不同于自 己获取依赖对象,对象会在运行期赋予它们所依赖的对象。依赖对象通常会通过接口了解所注入的对象,这样的话就能确保低耦合。 除了DI,我们还简单介绍了Spring对AOP的支持。AOP可以帮助应用将散落在各处的逻辑汇集于一处——切面。当Spring装配bean的时候,这 些切面能够在运行期编织起来,这样就能非常有效地赋予bean新的行为。 依赖注入和AOP是Spring框架最核心的部分,因此只有理解了如何应用Spring最关键的功能,你才有能力使用Spring框架的其他功能。在本 章,我们只是触及了Spring DI和AOP特性的皮毛。在以后的几章,我们将深入探讨DI和AOP。 闲言少叙,我们立即转到第2章学习如何在Spring中使用DI装配对象。 [1]对于基于Java的配置,Spring提供了AnnotationConfigApplicationContext。 [2]相对于NoSQL,我更喜欢非关系型(non-relational)或无模式(schema-less)这样的术语。将这些数据库称之为NoSQL,实际上将问题归 因于查询语言,而不是数据模型。 第第2章 装配章 装配Bean 本章内容:本章内容: 声明bean 构造器注入和Setter方法注入 装配bean 控制bean的创建和销毁 在看电影的时候,你曾经在电影结束后留在位置上继续观看片尾字幕吗?一部电影需要由这么多人齐心协力才能制作出来,这真是有点令人难 以置信!除了主要的参与人员——演员、编剧、导演和制片人,还有那些幕后人员——音乐师、特效制作人员和艺术指导,更不用说道具师、 录音师、服装师、化妆师、特技演员、广告师、第一助理摄影师、第二助理摄影师、布景师、灯光师和伙食管理员(或许是最重要的人员) 了。 现在想象一下,如果这些人彼此之间没有任何交流,你最喜爱的电影会变成什么样子?让我这么说吧,他们都出现在摄影棚中,开始各做各的 事情,彼此之间互不合作。如果导演保持沉默不喊“开机”,摄影师就不会开始拍摄。或许这并没什么大不了的,因为女主角还呆在她的保姆车 里,而且因为没有雇佣灯光师,一切处于黑暗之中。或许你曾经看过类似这样的电影。但是大多数电影(总之,都还是很优秀的)都是由成千 上万的人一起协作来完成的,他们有着共同的目标:制作一部广受欢迎的佳作。 在这方面,一个优秀的软件与之相比并没有太大区别。任何一个成功的应用都是由多个为了实现某一个业务目标而相互协作的组件构成的。这 些组件必须彼此了解,并且相互协作来完成工作。例如,在一个在线购物系统中,订单管理组件需要和产品管理组件以及信用卡认证组件协 作。这些组件或许还需要与数据访问组件协作,从数据库读取数据以及把数据写入数据库。 但是,正如我们在第1章中所看到的,创建应用对象之间关联关系的传统方法(通过构造器或者查找)通常会导致结构复杂的代码,这些代码 很难被复用也很难进行单元测试。如果情况不严重的话,这些对象所做的事情只是超出了它应该做的范围;而最坏的情况则是,这些对象彼此 之间高度耦合,难以复用和测试。 在Spring中,对象无需自己查找或创建与其所关联的其他对象。相反,容器负责把需要相互协作的对象引用赋予各个对象。例如,一个订单管 理组件需要信用卡认证组件,但它不需要自己创建信用卡认证组件。订单管理组件只需要表明自己两手空空,容器就会主动赋予它一个信用卡 认证组件。 创建应用对象之间协作关系的行为通常称为装配(wiring),这也是依赖注入(DI)的本质。在本章我们将介绍使用Spring装配 bean的基础知 识。因为DI是Spring的最基本要素,所以在开发基于Spring的应用时,你随时都在使用这些技术。 在Spring中装配bean有多种方式。作为本章的开始,我们先花一点时间来介绍一下配置Spring容器最常见的三种方法。 2.1  Spring配置的可选方案配置的可选方案 如第1章中所述,Spring容器负责创建应用程序中的bean并通过DI来协调这些对象之间的关系。但是,作为开发人员,你需要告诉Spring要创 建哪些bean并且如何将其装配在一起。当描述bean如何进行装配时,Spring具有非常大的灵活性,它提供了三种主要的装配机制: 在XML中进行显式配置。 在Java中进行显式配置。 隐式的bean发现机制和自动装配。 乍看上去,提供三种可选的配置方案会使Spring变得复杂。每种配置技术所提供的功能会有一些重叠,所以在特定的场景中,确定哪种技术最 为合适就会变得有些困难。但是,不必紧张——在很多场景下,选择哪种方案很大程度上就是个人喜好的问题,你尽可以选择自己最喜欢的方 式。 Spring有多种可选方案来配置bean,这是非常棒的,但有时候你必须要在其中做出选择。 这方面,并没有唯一的正确答案。你所做出的选择必须要适合你和你的项目。而且,谁说我们只能选择其中的一种方案呢?Spring的配置风格 是可以互相搭配的,所以你可以选择使用XML装配一些bean,使用Spring基于Java的配置(JavaConfig)来装配另一些bean,而将剩余的 bean让Spring去自动发现。 即便如此,我的建议是尽可能地使用自动配置的机制。显式配置越少越好。当你必须要显式配置bean的时候(比如,有些源码不是由你来维护 的,而当你需要为这些代码配置bean的时候),我推荐使用类型安全并且比XML更加强大的JavaConfig。最后,只有当你想要使用便利的XML 命名空间,并且在JavaConfig中没有同样的实现时,才应该使用XML。 在本章中,我们会详细介绍这三种技术并且在整本书中都会用到它们。现在,我们会尝试一下每种方法,对它们是什么样子的有一个直观的印 象。作为Spring配置的开始,我们先看一下Spring的自动化配置。 2.2 自动化装配 自动化装配bean 在本章稍后的内容中,你会看到如何借助Java和XML来进行Spring装配。尽管你会发现这些显式装配技术非常有用,但是在便利性方面,最强 大的还是Spring的自动化配置。如果Spring能够进行自动化装配的话,那何苦还要显式地将这些bean装配在一起呢? Spring从两个角度来实现自动化装配: 组件扫描(component scanning):Spring会自动发现应用上下文中所创建的bean。 自动装配(autowiring):Spring自动满足bean之间的依赖。 组件扫描和自动装配组合在一起就能发挥出强大的威力,它们能够将你的显式配置降低到最少。 为了阐述组件扫描和装配,我们需要创建几个bean,它们代表了一个音响系统中的组件。首先,要创建CompactDisc类,Spring会发现它并 将其创建为一个bean。然后,会创建一个CDPlayer类,让Spring发现它,并将CompactDiscbean注入进来。 2.2.1 创建可被发现的 创建可被发现的bean 在这个MP3和流式媒体音乐的时代,CD(compact disc)显得有点典雅甚至陈旧。它不像卡带机、八轨磁带、塑胶唱片那么普遍,随着以物 理载体进行音乐交付的方式越来越少,CD也变得越来越稀少了。 尽管如此,CD为我们阐述DI如何运行提供了一个很好的样例。如果你不将CD插入(注入)到CD播放器中,那么CD播放器其实是没有太大用 处的。所以,可以这样说,CD播放器依赖于CD才能完成它的使命。 为了在Spring中阐述这个例子,让我们首先在Java中建立CD的概念。程序清单2.1展现了CompactDisc,它是定义CD的一个接口: 程序清单程序清单2.1  CompactDisc接口在接口在Java中定义了中定义了CD的概念的概念 CompactDisc的具体内容并不重要,重要的是你将其定义为一个接口。作为接口,它定义了CD播放器对一盘CD所能进行的操作。它将CD 播放器的任意实现与CD本身的耦合降低到了最小的程度。 我们还需要一个CompactDisc的实现,实际上,我们可以有CompactDisc接口的多个实现。在本例中,我们首先会创建其中的一个实现, 也就是程序清单2.2所示的SgtPeppers类。 程序清单程序清单2.2 带有 带有@Component注解的注解的CompactDisc实现类实现类SgtPeppers 和CompactDisc接口一样,SgtPeppers的具体内容并不重要。你需要注意的就是SgtPeppers类上使用了@Component注解。这个简单 的注解表明该类会作为组件类,并告知Spring要为这个类创建bean。没有必要显式配置SgtPeppersbean,因为这个类使用 了@Component注解,所以Spring会为你把事情处理妥当。 不过,组件扫描默认是不启用的。我们还需要显式配置一下Spring,从而命令它去寻找带有@Component注解的类,并为其创建bean。程序清 单2.3的配置类展现了完成这项任务的最简洁配置。 程序清单程序清单2.3  @ComponentScan注解启用了组件扫描注解启用了组件扫描 类CDPlayerConfig通过Java代码定义了Spring的装配规则。在2.3节中,我们还会更为详细地介绍基于Java的Spring配置。不过,现在我们 只需观察一下CDPlayerConfig类并没有显式地声明任何bean,只不过它使用了@ComponentScan注解,这个注解能够在Spring中启用组 件扫描。 如果没有其他配置的话,@ComponentScan默认会扫描与配置类相同的包。因为CDPlayerConfig类位于soundsystem包中,因此Spring 将会扫描这个包以及这个包下的所有子包,查找带有@Component注解的类。这样的话,就能发现CompactDisc,并且会在Spring中自动为 其创建一个bean。 如果你更倾向于使用XML来启用组件扫描的话,那么可以使用Spring context命名空间的元素。程序清 单2.4展示了启用组件扫描的最简洁XML配置。 程序清单程序清单2.4 通过 通过XML启用组件扫描启用组件扫描 尽管我们可以通过XML的方案来启用组件扫描,但是在后面的讨论中,我更多的还是会使用基于Java的配置。如果你更喜欢XML的 话,元素会有与@ComponentScan注解相对应的属性和子元素。 可能有点让人难以置信,我们只创建了两个类,就能对功能进行一番尝试了。为了测试组件扫描的功能,我们创建一个简单的JUnit测试,它会 创建Spring上下文,并判断CompactDisc是不是真的创建出来了。程序清单2.5中的CDPlayerTest就是用来完成这项任务的。 程序清单程序清单2.5 测试组件扫描能够发现 测试组件扫描能够发现CompactDisc CDPlayerTest使用了Spring的SpringJUnit4ClassRunner,以便在测试开始的时候自动创建Spring的应用上下文。注 解@ContextConfiguration会告诉它需要在CDPlayerConfig中加载配置。因为CDPlayerConfig类中包含了@ComponentScan, 因此最终的应用上下文中应该包含CompactDiscbean。 为了证明这一点,在测试代码中有一个CompactDisc类型的属性,并且这个属性带有@Autowired注解,以便于将CompactDiscbean注入 到测试代码之中(稍后,我会讨论@Autowired)。最后,会有一个简单的测试方法断言cd属性不为null。如果它不为null的话,就意味着 Spring能够发现CompactDisc类,自动在Spring上下文中将其创建为bean并将其注入到测试代码之中。 这个代码应该能够通过测试,并以测试成功的颜色显示(在你的测试运行器中,或许会希望出现绿色)。你第一个简单的组件扫描练习就成功 了!尽管我们只用它创建了一个bean,但同样是这么少的配置能够用来发现和创建任意数量的bean。在soundsystem包及其子包中,所有带 有@Component注解的类都会创建为bean。只添加一行@ComponentScan注解就能自动创建无数个bean,这种权衡还是很划算的。 现在,我们会更加深入地探讨@ComponentScan和@Component,看一下使用组件扫描还能做些什么。 2.2.2 为组件扫描的 为组件扫描的bean命名命名 Spring应用上下文中所有的bean都会给定一个ID。在前面的例子中,尽管我们没有明确地为SgtPeppersbean设置ID,但Spring会根据类名为 其指定一个ID。具体来讲,这个bean所给定的ID为sgtPeppers,也就是将类名的第一个字母变为小写。 如果想为这个bean设置不同的ID,你所要做的就是将期望的ID作为值传递给@Component注解。比如说,如果想将这个bean标识 为lonelyHeartsClub,那么你需要将SgtPeppers类的@Component注解配置为如下所示: 还有另外一种为bean命名的方式,这种方式不使用@Component注解,而是使用Java依赖注入规范(Java Dependency Injection)中所提供 的@Named注解来为bean设置ID: Spring支持将@Named作为@Component注解的替代方案。两者之间有一些细微的差异,但是在大多数场景中,它们是可以互相替换的。 话虽如此,我更加强烈地喜欢@Component注解,而对于@Named……怎么说呢,我感觉它的名字起得很不好。它并没有像@Component那样 清楚地表明它是做什么的。因此在本书及其示例代码中,我不会再使用@Named。 2.2.3 设置组件扫描的基础包 设置组件扫描的基础包 到现在为止,我们没有为@ComponentScan设置任何属性。这意味着,按照默认规则,它会以配置类所在的包作为基础包(base package) 来扫描组件。但是,如果你想扫描不同的包,那该怎么办呢?或者,如果你想扫描多个基础包,那又该怎么办呢? 有一个原因会促使我们明确地设置基础包,那就是我们想要将配置类放在单独的包中,使其与其他的应用代码区分开来。如果是这样的话,那 默认的基础包就不能满足要求了。 要满足这样的需求其实也完全没有问题!为了指定不同的基础包,你所需要做的就是在@ComponentScan的value属性中指明包的名称: 如果你想更加清晰地表明你所设置的是基础包,那么你可以通过basePackages属性进行配置: 可能你已经注意到了basePackages属性使用的是复数形式。如果你揣测这是不是意味着可以设置多个基础包,那么恭喜你猜对了。如果想 要这么做的话,只需要将basePackages属性设置为要扫描包的一个数组即可: 在上面的例子中,所设置的基础包是以String类型表示的。我认为这是可以的,但这种方法是类型不安全(not type-safe)的。如果你重构代 码的话,那么所指定的基础包可能就会出现错误了。 除了将包设置为简单的String类型之外,@ComponentScan还提供了另外一种方法,那就是将其指定为包中所包含的类或接口: 可以看到,basePackages属性被替换成了basePackageClasses。同时,我们不是再使用String类型的名称来指定包, 为basePackageClasses属性所设置的数组中包含了类。这些类所在的包将会作为组件扫描的基础包。 尽管在样例中,我为basePackageClasses设置的是组件类,但是你可以考虑在包中创建一个用来进行扫描的空标记接口(marker interface)。通过标记接口的方式,你依然能够保持对重构友好的接口引用,但是可以避免引用任何实际的应用程序代码(在稍后重构中,这 些应用代码有可能会从想要扫描的包中移除掉)。 在你的应用程序中,如果所有的对象都是独立的,彼此之间没有任何依赖,就像SgtPeppersbean这样,那么你所需要的可能就是组件扫描 而已。但是,很多对象会依赖其他的对象才能完成任务。这样的话,我们就需要有一种方法能够将组件扫描得到的bean和它们的依赖装配在一 起。要完成这项任务,我们需要了解一下Spring自动化配置的另外一方面内容,那就是自动装配。 2.2.4 通过为 通过为bean添加注解实现自动装配添加注解实现自动装配 简单来说,自动装配就是让Spring自动满足bean依赖的一种方法,在满足依赖的过程中,会在Spring应用上下文中寻找匹配某个bean需求的其 他bean。为了声明要进行自动装配,我们可以借助Spring的@Autowired注解。 比方说,考虑程序清单2.6中的CDPlayer类。它的构造器上添加了@Autowired注解,这表明当Spring创建CDPlayerbean的时候,会通过 这个构造器来进行实例化并且会传入一个可设置给CompactDisc类型的bean。 程序清单程序清单2.6 通过自动装配,将一个 通过自动装配,将一个CompactDisc注入到注入到CDPlayer之中之中 @Autowired注解不仅能够用在构造器上,还能用在属性的Setter方法上。比如说,如果CDPlayer有一个setCompactDisc()方法,那么可 以采用如下的注解形式进行自动装配: 在Spring初始化bean之后,它会尽可能得去满足bean的依赖,在本例中,依赖是通过带有@Autowired注解的方法进行声明的,也就 是setCompactDisc()。 实际上,Setter方法并没有什么特殊之处。@Autowired注解可以用在类的任何方法上。假设CDPlayer类有一个insertDisc()方法,那 么@Autowired能够像在setCompactDisc()上那样,发挥完全相同的作用: 不管是构造器、Setter方法还是其他的方法,Spring都会尝试满足方法参数上所声明的依赖。假如有且只有一个bean匹配依赖需求的话,那么 这个bean将会被装配进来。 如果没有匹配的bean,那么在应用上下文创建的时候,Spring会抛出一个异常。为了避免异常的出现,你可以将@Autowired的required属 性设置为false: 将required属性设置为false时,Spring会尝试执行自动装配,但是如果没有匹配的bean的话,Spring将会让这个bean处于未装配的状态。 但是,把required属性设置为false时,你需要谨慎对待。如果在你的代码中没有进行null检查的话,这个处于未装配状态的属性有可能会 出现NullPointerException。 如果有多个bean都能满足依赖关系的话,Spring将会抛出一个异常,表明没有明确指定要选择哪个bean进行自动装配。在第3章中,我们会进 一步讨论自动装配中的歧义性。 @Autowired是Spring特有的注解。如果你不愿意在代码中到处使用Spring的特定注解来完成自动装配任务的话,那么你可以考虑将其替换 为@Inject: @Inject注解来源于Java依赖注入规范,该规范同时还为我们定义了@Named注解。在自动装配中,Spring同时支 持@Inject和@Autowired。尽管@Inject和@Autowired之间有着一些细微的差别,但是在大多数场景下,它们都是可以互相替换的。 在@Inject和@Autowired中,我没有特别强烈的偏向性。实际上,在有的项目中,我会发现我同时使用了这两个注解。不过在本书的样例 中,我会一直使用@Autowired,而你可以根据自己的情况,选择其中的任意一个。 2.2.5 验证自动装配 验证自动装配 现在,我们已经在CDPlayer的构造器中添加了@Autowired注解,Spring将把一个可分配给CompactDisc类型的bean自动注入进来。为了 验证这一点,让我们修改一下CDPlayerTest,使其能够借助CDPlayer bean播放CD: 现在,除了注入CompactDisc,我们还将CDPlayerbean注入到测试代码的player成员变量之中(它是更为通用的MediaPlayer类 型)。在play()测试方法中,我们可以调用CDPlayer的play()方法,并断言它的行为与你的预期一致。 在测试代码中使用System.out.println()是稍微有点棘手的事情。因此,该样例中使用了StandardOutputStreamLog,这是来源于 System Rules库(http://stefanbirkner.github.io/system-rules/index.html)的一个JUnit规则,该规则能够基于控制台的输出编写断言。在这里,我 们断言SgtPeppers.play()方法的输出被发送到了控制台上。 现在,你已经了解了组件扫描和自动装配的基础知识,在第3章中,当我们介绍如何处理自动装配的歧义性时,还会继续研究组件扫描。 但是现在,我们先将组件扫描和自动装配放在一边,看一下在Spring中如何显式地装配bean,首先从通过Java代码编写配置开始。 2.3 通过 通过Java代码装配代码装配bean 尽管在很多场景下通过组件扫描和自动装配实现Spring的自动化配置是更为推荐的方式,但有时候自动化配置的方案行不通,因此需要明确配 置Spring。比如说,你想要将第三方库中的组件装配到你的应用中,在这种情况下,是没有办法在它的类上添 加@Component和@Autowired注解的,因此就不能使用自动化装配的方案了。 在这种情况下,你必须要采用显式装配的方式。在进行显式配置的时候,有两种可选方案:Java和XML。在这节中,我们将会学习如何使用 Java配置,接下来的一节中将会继续学习Spring的XML配置。 就像我之前所说的,在进行显式配置时,JavaConfig是更好的方案,因为它更为强大、类型安全并且对重构友好。因为它就是Java代码,就像 应用程序中的其他Java代码一样。 同时,JavaConfig与其他的Java代码又有所区别,在概念上,它与应用程序中的业务逻辑和领域代码是不同的。尽管它与其他的组件一样都使 用相同的语言进行表述,但JavaConfig是配置代码。这意味着它不应该包含任何业务逻辑,JavaConfig也不应该侵入到业务逻辑代码之中。尽 管不是必须的,但通常会将JavaConfig放到单独的包中,使它与其他的应用程序逻辑分离开来,这样对于它的意图就不会产生困惑了。 接下来,让我们看一下如何通过JavaConfig显式配置Spring。 2.3.1 创建配置类 创建配置类 在本章前面的程序清单2.3中,我们第一次见识到JavaConfig。让我们重温一下那个样例中的CDPlayerConfig: 创建JavaConfig类的关键在于为其添加@Configuration注解,@Configuration注解表明这个类是一个配置类,该类应该包含在Spring应 用上下文中如何创建bean的细节。 到此为止,我们都是依赖组件扫描来发现Spring应该创建的bean。尽管我们可以同时使用组件扫描和显式配置,但是在本节中,我们更加关注 于显式配置,因此我将CDPlayerConfig的@ComponentScan注解移除掉了。 移除了@ComponentScan注解,此时的CDPlayerConfig类就没有任何作用了。如果你现在运行CDPlayerTest的话,测试会失败,并且 会出现BeanCreation- Exception异常。测试期望被注入CDPlayer和CompactDisc,但是这些bean根本就没有创建,因为组件扫描不 会发现它们。 为了再次让测试通过,你可以将@ComponentScan注解添加回去,但是我们这一节关注显式配置,因此让我们看一下如何使用JavaConfig装 配CDPlayer和CompactDisc。 2.3.2 声明简单的 声明简单的bean 要在JavaConfig中声明bean,我们需要编写一个方法,这个方法会创建所需类型的实例,然后给这个方法添加@Bean注解。比方说,下面的 代码声明了CompactDisc bean: @Bean注解会告诉Spring这个方法将会返回一个对象,该对象要注册为Spring应用上下文中的bean。方法体中包含了最终产生bean实例的逻 辑。 默认情况下,bean的ID与带有@Bean注解的方法名是一样的。在本例中,bean的名字将会是sgtPeppers。如果你想为其设置成一个不同的 名字的话,那么可以重命名该方法,也可以通过name属性指定一个不同的名字: 不管你采用什么方法来为bean命名,bean声明都是非常简单的。方法体返回了一个新的SgtPeppers实例。这里是使用Java来进行描述的, 因此我们可以发挥Java提供的所有功能,只要最终生成一个CompactDisc实例即可。 请稍微发挥一下你的想象力,我们可能希望做一点稍微疯狂的事情,比如说,在一组CD中随机选择一个CompactDisc来播放: 现在,你可以自己想象一下,借助@Bean注解方法的形式,我们该如何发挥出Java的全部威力来产生bean。当你想完之后,我们要回过头来 看一下在JavaConfig中,如何将CompactDisc注入到CDPlayer之中。 2.3.3 借助 借助JavaConfig实现注入实现注入 我们前面所声明的CompactDisc bean是非常简单的,它自身没有其他的依赖。但现在,我们需要声明CDPlayerbean,它依赖 于CompactDisc。在JavaConfig中,要如何将它们装配在一起呢? 在JavaConfig中装配bean的最简单方式就是引用创建bean的方法。例如,下面就是一种声明CDPlayer的可行方案: cdPlayer()方法像sgtPeppers()方法一样,同样使用了@Bean注解,这表明这个方法会创建一个bean实例并将其注册到Spring应用上下 文中。所创建的bean ID为cdPlayer,与方法的名字相同。 cdPlayer()的方法体与sgtPeppers()稍微有些区别。在这里并没有使用默认的构造器构建实例,而是调用了需要传入CompactDisc对 象的构造器来创建CDPlayer实例。 看起来,CompactDisc是通过调用sgtPeppers()得到的,但情况并非完全如此。因为sgtPeppers()方法上添加了@Bean注解,Spring 将会拦截所有对它的调用,并确保直接返回该方法所创建的bean,而不是每次都对其进行实际的调用。 比如说,假设你引入了一个其他的CDPlayerbean,它和之前的那个bean完全一样: 假如对sgtPeppers()的调用就像其他的Java方法调用一样的话,那么每个CDPlayer实例都会有一个自己特有的SgtPeppers实例。如果 我们讨论的是实际的CD播放器和CD光盘的话,这么做是有意义的。如果你有两台CD播放器,在物理上并没有办法将同一张CD光盘放到两个 CD播放器中。 但是,在软件领域中,我们完全可以将同一个SgtPeppers实例注入到任意数量的其他bean之中。默认情况下,Spring中的bean都是单例 的,我们并没有必要为第二个CDPlayer bean创建完全相同的SgtPeppers实例。所以,Spring会拦截对sgtPeppers()的调用并确保返回 的是Spring所创建的bean,也就是Spring本身在调用sgtPeppers()时所创建的CompactDiscbean。因此,两个CDPlayer bean会得到相 同的SgtPeppers实例。 可以看到,通过调用方法来引用bean的方式有点令人困惑。其实还有一种理解起来更为简单的方式: 在这里,cdPlayer()方法请求一个CompactDisc作为参数。当Spring调用cdPlayer()创建CDPlayerbean的时候,它会自动装配一 个CompactDisc到配置方法之中。然后,方法体就可以按照合适的方式来使用它。借助这种技术,cdPlayer()方法也能够 将CompactDisc注入到CDPlayer的构造器中,而且不用明确引用CompactDisc的@Bean方法。 通过这种方式引用其他的bean通常是最佳的选择,因为它不会要求将CompactDisc声明到同一个配置类之中。在这里甚至没有要 求CompactDisc必须要在JavaConfig中声明,实际上它可以通过组件扫描功能自动发现或者通过XML来进行配置。你可以将配置分散到多个 配置类、XML文件以及自动扫描和装配bean之中,只要功能完整健全即可。不管CompactDisc是采用什么方式创建出来的,Spring都会将其 传入到配置方法中,并用来创建CDPlayer bean。 另外,需要提醒的是,我们在这里使用CDPlayer的构造器实现了DI功能,但是我们完全可以采用其他风格的DI配置。比如说,如果你想通过 Setter方法注入CompactDisc的话,那么代码看起来应该是这样的: 再次强调一遍,带有@Bean注解的方法可以采用任何必要的Java功能来产生bean实例。构造器和Setter方法只是@Bean方法的两个简单样例。 这里所存在的可能性仅仅受到Java语言的限制。 2.4 通过 通过XML装配装配bean 到此为止,我们已经看到了如何让Spring自动发现和装配bean,还看到了如何进行手动干预,即通过JavaConfig显式地装配bean。但是,在装 配bean的时候,还有一种可选方案,尽管这种方案可能不太合乎大家的心意,但是它在Spring中已经有很长的历史了。 在Spring刚刚出现的时候,XML是描述配置的主要方式。在Spring的名义下,我们创建了无数行XML代码。在一定程度上,Spring成为了XML 配置的同义词。 尽管Spring长期以来确实与XML有着关联,但现在需要明确的是,XML不再是配置Spring的唯一可选方案。Spring现在有了强大的自动化配置 和基于Java的配置,XML不应该再是你的第一选择了。 不过,鉴于已经存在那么多基于XML的Spring配置,所以理解如何在Spring中使用XML还是很重要的。但是,我希望本节的内容只是用来帮助 你维护已有的XML配置,在完成新的Spring工作时,希望你会使用自动化配置和JavaConfig。 2.4.1 创建 创建XML配置规范配置规范 在使用XML为Spring装配bean之前,你需要创建一个新的配置规范。在使用JavaConfig的时候,这意味着要创建一个带 有@Configuration注解的类,而在XML配置中,这意味着要创建一个XML文件,并且要以元素为根。 最为简单的Spring XML配置如下所示: 很容易就能看出来,这个基本的XML配置已经比同等功能的JavaConfig类复杂得多了。作为起步,在JavaConfig中所需要的只 是@Configuration,但在使用XML时,需要在配置文件的顶部声明多个XML模式(XSD)文件,这些文件定义了配置Spring的XML元素。 借助借助Spring Tool Suite创建创建XML配置文件配置文件创建和管理Spring XML配置文件的一种简便方式是使用Spring Tool Suite(https://spring.io/tools/sts)。在Spring Tool Suite的菜单中,选择File>New>Spring Bean Configuration File,能够创建Spring XML配置文件,并且可以选择可用的配置命名空间。 用来装配bean的最基本的XML元素包含在spring-beans模式之中,在上面这个XML文件中,它被定义为根命名空间。是该模式中 的一个元素,它是所有Spring配置文件的根元素。 在XML中配置Spring时,还有一些其他的模式。尽管在本书中,我更加关注自动化以及基于Java的配置,但是在本书讲解的过程中,当出现其 他模式的时候,我至少会提醒你。 就这样,我们已经有了一个合法的Spring XML配置。不过,它也是一个没有任何用处的配置,因为它(还)没有声明任何bean。为了给予它 生命力,让我们重新创建一下CD样例,只不过我们这次使用XML配置,而不是使用JavaConfig和自动化装配。 2.4.2 声明一个简单的 声明一个简单的 要在基于XML的Spring配置中声明一个bean,我们要使用spring-beans模式中的另外一个元素:元素类似于JavaConfig 中的@Bean注解。我们可以按照如下的方式声明CompactDiscbean: 这里声明了一个很简单的bean,创建这个bean的类通过class属性来指定的,并且要使用全限定的类名。 因为没有明确给定ID,所以这个bean将会根据全限定类名来进行命名。在本例中,bean的ID将会是“soundsystem.SgtPeppers#0”。其 中,“#0”是一个计数的形式,用来区分相同类型的其他bean。如果你声明了另外一个SgtPeppers,并且没有明确进行标识,那么它自动得到 的ID将会是“soundsystem.SgtPeppers#1”。 尽管自动化的bean命名方式非常方便,但如果你要稍后引用它的话,那自动产生的名字就没有多大的用处了。因此,通常来讲更好的办法是借 助id属性,为每个bean设置一个你自己选择的名字: 稍后将这个bean装配到CDPlayer bean之中的时候,你会用到这个具体的名字。 减少繁琐减少繁琐为了减少XML中繁琐的配置,只对那些需要按名字引用的bean(比如,你需要将对它的引用注入到另外一个bean中)进 行明确地命名。 在进一步学习之前,让我们花点时间看一下这个简单bean声明的一些特征。 第一件需要注意的事情就是你不再需要直接负责创建SgtPeppers的实例,在基于JavaConfig的配置中,我们是需要这样做的。当Spring发现 这个元素时,它将会调用SgtPeppers的默认构造器来创建bean。在XML配置中,bean的创建显得更加被动,不过,它并没有 JavaConfig那样强大,在JavaConfig配置方式中,你可以通过任何可以想象到的方法来创建bean实例。 另外一个需要注意到的事情就是,在这个简单的声明中,我们将bean的类型以字符串的形式设置在了class属性中。谁能保证设置 给class属性的值是真正的类呢?Spring的XML配置并不能从编译期的类型检查中受益。即便它所引用的是实际的类型,如果你重命名了类, 会发生什么呢? 借助借助IDE检查检查XML的合法性的合法性使用能够感知Spring功能的IDE,如Spring Tool Suite,能够在很大程度上帮助你确保Spring XML配置 的合法性。 以上介绍的只是JavaConfig要优于XML配置的部分原因。我建议在为你的应用选择配置风格时,要记住XML配置的这些缺点。接下来,我们继 续Spring XML配置的学习进程,了解如何将SgtPeppersbean注入到CDPlayer之中。 2.4.3 借助构造器注入初始化 借助构造器注入初始化bean 在Spring XML配置中,只有一种声明bean的方式:使用元素并指定class属性。Spring会从这里获取必要的信息来创建bean。 但是,在XML中声明DI时,会有多种可选的配置方案和风格。具体到构造器注入,有两种基本的配置方案可供选择: 元素 使用Spring 3.0所引入的c-命名空间 两者的区别在很大程度就是是否冗长烦琐。可以看到,元素比使用c-命名空间会更加冗长,从而导致XML更加难以读 懂。另外,有些事情可以做到,但是使用c-命名空间却无法实现。 在介绍Spring XML的构造器注入时,我们将会分别介绍这两种可选方案。首先,看一下它们各自如何注入bean引用。 构造器注入构造器注入bean引用引用 按照现在的定义,CDPlayerbean有一个接受CompactDisc类型的构造器。这样,我们就有了一个很好的场景来学习如何注入bean的引用。 现在已经声明了SgtPeppers bean,并且SgtPeppers类实现了CompactDisc接口,所以实际上我们已经有了一个可以注入 到CDPlayerbean中的bean。我们所需要做的就是在XML中声明CDPlayer并通过ID引用SgtPeppers: 当Spring遇到这个元素时,它会创建一个CDPlayer实例。元素会告知Spring要将一个ID为compactDisc的 bean引用传递到CDPlayer的构造器中。 作为替代的方案,你也可以使用Spring的c-命名空间。c-命名空间是在Spring 3.0中引入的,它是在XML中更为简洁地描述构造器参数的方式。 要使用它的话,必须要在XML的顶部声明其模式,如下所示: 在c-命名空间和模式声明之后,我们就可以使用它来声明构造器参数了,如下所示: 在这里,我们使用了c-命名空间来声明构造器参数,它作为元素的一个属性,不过这个属性的名字有点诡异。图2.1描述了这个属性名 是如何组合而成的。 图2.1 通过Spring的c-命名空间将bean引用注入到构造器参数中 属性名以“c:”开头,也就是命名空间的前缀。接下来就是要装配的构造器参数名,在此之后是“-ref”,这是一个命名的约定,它会告诉 Spring,正在装配的是一个bean的引用,这个bean的名字是compactDisc,而不是字面量“compactDisc”。 很显然,使用c-命名空间属性要比使用元素简练得多。这是我很喜欢它的原因之一。除了更易读之外,当我在编写样 例代码时,c-命名空间属性能够更加有助于使代码的长度保持在书的边框之内。 在编写前面的样例时,关于c-命名空间,有一件让我感到困扰的事情就是它直接引用了构造器参数的名称。引用参数的名称看起来有些怪异, 因为这需要在编译代码的时候,将调试标志(debug symbol)保存在类代码中。如果你优化构建过程,将调试标志移除掉,那么这种方式可能 就无法正常执行了。 替代的方案是我们使用参数在整个参数列表中的位置信息: 这个c-命名空间属性看起来似乎比上一种方法更加怪异。我将参数的名称替换成了“0”,也就是参数的索引。因为在XML中不允许数字作为属性 的第一个字符,因此必须要添加一个下画线作为前缀。 使用索引来识别构造器参数感觉比使用名字更好一些。即便在构建的时候移除掉了调试标志,参数却会依然保持相同的顺序。如果有多个构造 器参数的话,这当然是很有用处的。在这里因为只有一个构造器参数,所以我们还有另外一个方案——根本不用去标示参数: 到目前为止,这是最为奇特的一个c-命名空间属性,这里没有参数索引或参数名。只有一个下画线,然后就是用“-ref”来表明正在装配的是一 个引用。 我们已经将引用装配到了其他的bean之中,接下来看一下如何将字面量值(literal value)装配到构造器之中。 将字面量注入到构造器中将字面量注入到构造器中 迄今为止,我们所做的DI通常指的都是类型的装配——也就是将对象的引用装配到依赖于它们的其他对象之中——而有时候,我们需要做的只 是用一个字面量值来配置对象。为了阐述这一点,假设你要创建CompactDisc的一个新实现,如下所示: 在SgtPeppers中,唱片名称和艺术家的名字都是硬编码的,但是这个CompactDisc实现与之不同,它更加灵活。像现实中的空磁盘一样, 它可以设置成任意你想要的艺术家和唱片名。现在,我们可以将已有的SgtPeppers替换为这个类: 我们再次使用元素进行构造器参数的注入。但是这一次我们没有使用“ref”属性来引用其他的bean,而是使用 了value属性,通过该属性表明给定的值要以字面量的形式注入到构造器之中。 如果要使用c-命名空间的话,这个例子又该是什么样子呢?第一种方案是引用构造器参数的名字: 可以看到,装配字面量与装配引用的区别在于属性名中去掉了“-ref”后缀。与之类似,我们也可以通过参数索引装配相同的字面量值,如下所 示: XML不允许某个元素的多个属性具有相同的名字。因此,如果有两个或更多的构造器参数的话,我们不能简单地使用下画线进行标示。但是如 果只有一个构造器参数的话,我们就可以这样做了。为了完整地展现该功能,假设BlankDisc只有一个构造器参数,这个参数接受唱片的名 称。在这种情况下,我们可以在Spring中这样声明它: 在装配bean引用和字面量值方面,和c-命名空间的功能是相同的。但是有一种情况是能够实 现,c-命名空间却无法做到的。接下来,让我们看一下如何将集合装配到构造器参数中。 装配集合装配集合 到现在为止,我们假设CompactDisc在定义时只包含了唱片名称和艺术家的名字。如果现实世界中的CD也是这样的话,那么在技术上就不 会任何的进展。CD之所以值得购买是因为它上面所承载的音乐。大多数的CD都会包含十多个磁道,每个磁道上包含一首歌。 如果使用CompactDisc为真正的CD建模,那么它也应该有磁道列表的概念。请考虑下面这个新的BlankDisc: 这个变更会对Spring如何配置bean产生影响,在声明bean的时候,我们必须要提供一个磁道列表。 最简单的办法是将列表设置为null。因为它是一个构造器参数,所以必须要声明它,不过你可以采用如下的方式传递null给它: 元素所做的事情与你的期望是一样的:将null传递给构造器。这并不是解决问题的好办法,但在注入期它能正常执行。当调 用play()方法时,你会遇到NullPointerException异常,因此这并不是理想的方案。 更好的解决方法是提供一个磁道名称的列表。要达到这一点,我们可以有多个可选方案。首先,可以使用元素将其声明为一个列表: 其中,元素是的子元素,这表明一个包含值的列表将会传递到构造器中。其中,元素用来指定列表 中的每个元素。 与之类似,我们也可以使用元素替代,实现bean引用列表的装配。例如,假设你有一个Discography类,它的构造器如下 所示: 那么,你可以采取如下的方式配置Discography bean: 当构造器参数的类型是java.util.List时,使用元素是合情合理的。尽管如此,我们也可以按照同样的方式使用元素: 元素的区别不大,其中最重要的不同在于当Spring创建要装配的集合时,所创建的是java.util.Set还 是java.util.List。如果是Set的话,所有重复的值都会被忽略掉,存放顺序也不会得以保证。不过无论在哪种情况 下,都可以用来装配List、Set甚至数组。 在装配集合方面,比c-命名空间的属性更有优势。目前,使用c-命名空间的属性无法实现装配集合的功能。 使用和c-命名空间实现构造器注入时,它们之间还有一些细微的差别。但是到目前为止,我们所涵盖的内容已经足够 了,尤其是像我之前所建议的那样,要首选基于Java的配置而不是XML。因此,与其不厌其烦地花费时间讲述如何使用XML进行构造器注入, 还不如看一下如何使用XML来装配属性。 2.4.4 设置属性 设置属性 到目前为止,CDPlayer和BlankDisc类完全是通过构造器注入的,没有使用属性的Setter方法。接下来,我们就看一下如何使用Spring XML实现属性注入。假设属性注入的CDPlayer如下所示: 该选择构造器注入还是属性注入呢?作为一个通用的规则,我倾向于对强依赖使用构造器注入,而对可选性的依赖使用属性注入。按照这个规 则,我们可以说对于BlankDisc来讲,唱片名称、艺术家以及磁道列表是强依赖,因此构造器注入是正确的方案。不过,对于CDPlayer来 讲,它对CompactDisc是强依赖还是可选性依赖可能会有些争议。虽然我不太认同,但你可能会觉得即便没有将CompactDisc装入进 去,CDPlayer依然还能具备一些有限的功能。 现在,CDPlayer没有任何的构造器(除了隐含的默认构造器),它也没有任何的强依赖。因此,你可以采用如下的方式将其声明为Spring bean: Spring在创建bean的时候不会有任何的问题,但是CDPlayerTest会因为出现NullPointerException而导致测试失败,因为我们并没有 注入CDPlayer的compactDisc属性。不过,按照如下的方式修改XML,就能解决该问题: 元素为属性的Setter方法所提供的功能与元素为构造器所提供的功能是一样的。在本例中,它引用了ID 为compactDisc的bean(通过ref属性),并将其注入到compactDisc属性中(通过setCompactDisc()方法)。如果你现在运行测试 的话,它应该就能通过了。 我们已经知道,Spring为元素提供了c-命名空间作为替代方案,与之类似,Spring提供了更加简洁的p-命名空间,作 为元素的替代方案。为了启用p-命名空间,必须要在XML文件中与其他的命名空间一起对其进行声明: 我们可以使用p-命名空间,按照以下的方式装配compactDisc属性: p-命名空间中属性所遵循的命名约定与c-命名空间中的属性类似。图2.2阐述了p-命名空间属性是如何组成的。 图2.2 借助Spring的p-命名空间,将bean引用注入到属性中 首先,属性的名字使用了“p:”前缀,表明我们所设置的是一个属性。接下来就是要注入的属性名。最后,属性的名称以“-ref”结尾,这会提示 Spring要进行装配的是引用,而不是字面量。 将字面量注入到属性中将字面量注入到属性中 属性也可以注入字面量,这与构造器参数非常类似。作为示例,我们重新看一下BlankDisc bean。不过,BlankDisc这次完全通过属性注 入进行配置,而不是构造器注入。新的BlankDisc类如下所示: 现在,它不再强制要求我们装配任何的属性。你可以按照如下的方式创建一个BlankDiscbean,它的所有属性全都是空的: 当然,如果在装配bean的时候不设置这些属性,那么在运行期CD播放器将不能正常播放内容。play()方法可能会遇到的输出内容是“Playing null by null”,随之会抛出NullPointerException异常,这是因为我们没有指定任何的磁道。所以,我们需要装配这些属性,可以借 助元素的value属性实现该功能: 在这里,除了使用元素的value属性来设置title和artist,我们还使用了内嵌的元素来设置tracks属性,这与之 前通过装配tracks是完全一样的。 另外一种可选方案就是使用p-命名空间的属性来完成该功能: 与c-命名空间一样,装配bean引用与装配字面量的唯一区别在于是否带有“-ref”后缀。如果没有“-ref”后缀的话,所装配的就是字面量。 但需要注意的是,我们不能使用p-命名空间来装配集合,没有便利的方式使用p-命名空间来指定一个值(或bean引用)的列表。但是,我们可 以使用Spring util-命名空间中的一些功能来简化BlankDiscbean。 首先,需要在XML中声明util-命名空间及其模式: util-命名空间所提供的功能之一就是元素,它会创建一个列表的bean。借助,我们可以将磁道列表转移 到BlankDisc bean之外,并将其声明到单独的bean之中,如下所示: 现在,我们能够像使用其他的bean那样,将磁道列表bean注入到BlankDisc bean的tracks属性中: 只是util-命名空间中的多个元素之一。表2.1列出了util-命名空间提供的所有元素。 在需要的时候,你可能会用到util-命名空间中的部分成员。但现在,在结束本章前,我们看一下如何将自动化配置、JavaConfig以及XML配 置混合并匹配在一起。 表表2.1  Spring util-命名空间中的元素命名空间中的元素 元元 素素 描描 述述 引用某个类型的public static域,并将其暴露为bean util:list 创建一个java.util.List类型的bean,其中包含值或引用 util:map 创建一个java.util.Map类型的bean,其中包含值或引用 util:properties 创建一个java.util.Properties类型的bean util:property-path 引用一个bean的属性(或内嵌属性),并将其暴露为bean util:set 创建一个java.util.Set类型的bean,其中包含值或引用 2.5 导入和混合配置 导入和混合配置 在典型的Spring应用中,我们可能会同时使用自动化和显式配置。即便你更喜欢通过JavaConfig实现显式配置,但有的时候XML却是最佳的方 案。 幸好在Spring中,这些配置方案都不是互斥的。你尽可以将JavaConfig的组件扫描和自动装配和/或XML配置混合在一起。实际上,就像在2.2.1 小节中所看到的,我们至少需要有一点显式配置来启用组件扫描和自动装配。 关于混合配置,第一件需要了解的事情就是在自动装配时,它并不在意要装配的bean来自哪里。自动装配的时候会考虑到Spring容器中所有的 bean,不管它是在JavaConfig或XML中声明的还是通过组件扫描获取到的。 你可能会想在显式配置时,比如在XML配置和Java配置中该如何引用bean呢。让我们先看一下如何在JavaConfig中引用XML配置的bean。 2.5.1 在 在JavaConfig中引用中引用XML配置配置 现在,我们临时假设CDPlayerConfig已经变得有些笨重,我们想要将其进行拆分。当然,它目前只定义了两个bean,远远称不上复杂的 Spring配置。不过,我们假设两个bean就已经太多了。 我们所能实现的一种方案就是将BlankDisc从CDPlayerConfig拆分出来,定义到它自己的CDConfig类中,如下所示: compactDisc()方法已经从CDPlayerConfig中移除掉了,我们需要有一种方式将这两个类组合在一起。一种方法就是 在CDPlayerConfig中使用@Import注解导入CDConfig: 或者采用一个更好的办法,也就是不在CDPlayerConfig中使用@Import,而是创建一个更高级别的SoundSystemConfig,在这个类中 使用@Import将两个配置类组合在一起: 不管采用哪种方式,我们都将CDPlayer的配置与BlankDisc的配置分开了。现在,我们假设(基于某些原因)希望通过XML来配 置BlankDisc,如下所示: 现在BlankDisc配置在了XML之中,我们该如何让Spring同时加载它和其他基于Java的配置呢? 答案是@ImportResource注解,假设BlankDisc定义在名为cd-config.xml的文件中,该文件位于根类路径下,那么可以修 改SoundSystemConfig,让它使用@ImportResource注解,如下所示: 两个bean——配置在JavaConfig中的CDPlayer以及配置在XML中BlankDisc——都会被加载到Spring容器之中。因为CDPlayer中带 有@Bean注解的方法接受一个CompactDisc作为参数,因此BlankDisc将会装配进来,此时与它是通过XML配置的没有任何关系。 让我们继续这个练习,但是这一次,我们需要在XML中引用JavaConfig声明的bean。 2.5.2 在 在XML配置中引用配置中引用JavaConfig 假设你正在使用Spring基于XML的配置并且你已经意识到XML逐渐变得无法控制。像前面一样,我们正在处理的是两个bean,但事情实际上会 变得更加糟糕。在被无数的尖括号淹没之前,我们决定将XML配置文件进行拆分。 在JavaConfig配置中,我们已经展现了如何使用@Import和@ImportResource来拆分JavaConfig类。在XML中,我们可以使用import元 素来拆分XML配置。 比如,假设希望将BlankDisc bean拆分到自己的配置文件中,该文件名为cd-config.xml,这与我们之前使用@ImportResource是一样的。 我们可以在XML配置文件中使用元素来引用该文件: 现在,我们假设不再将BlankDisc配置在XML之中,而是将其配置在JavaConfig中,CDPlayer则继续配置在XML中。基于XML的配置该 如何引用一个JavaConfig类呢? 事实上,答案并不那么直观。元素只能导入其他的XML配置文件,并没有XML元素能够导入JavaConfig类。 但是,有一个你已经熟知的元素能够用来将Java配置导入到XML配置中:元素。为了将JavaConfig类导入到XML配置中,我们可以这 样声明bean: 采用这样的方式,两种配置——其中一个使用XML描述,另一个使用Java描述——被组合在了一起。类似地,你可能还希望创建一个更高层次 的配置文件,这个文件不声明任何的bean,只是负责将两个或更多的配置组合起来。例如,你可以将CDConfig bean从之前的XML文件中移 除掉,而是使用第三个配置文件将这两个组合在一起: 不管使用JavaConfig还是使用XML进行装配,我通常都会创建一个根配置(root configuration),也就是这里展现的这样,这个配置会将两个 或更多的装配类和/或XML文件组合起来。我也会在根配置中启用组件扫描(通过或@ComponentScan)。 你会在本书的很多例子中看到这种技术。 2.6 小结 小结 Spring框架的核心是Spring容器。容器负责管理应用中组件的生命周期,它会创建这些组件并保证它们的依赖能够得到满足,这样的话,组件 才能完成预定的任务。 在本章中,我们看到了在Spring中装配bean的三种主要方式:自动化配置、基于Java的显式配置以及基于XML的显式配置。不管你采用什么方 式,这些技术都描述了Spring应用中的组件以及这些组件之间的关系。 我同时建议尽可能使用自动化配置,以避免显式配置所带来的维护成本。但是,如果你确实需要显式配置Spring的话,应该优先选择基于Java 的配置,它比基于XML的配置更加强大、类型安全并且易于重构。在本书中的例子中,当决定如何装配组件时,我都会遵循这样的指导意见。 因为依赖注入是Spring中非常重要的组成部分,所以本章中介绍的技术在本书中所有的地方都会用到。基于这些基础知识,下一章将会介绍一 些更为高级的bean装配技术,这些技术能够让你更加充分地发挥Spring容器的威力。 第第3章 高级装配章 高级装配 本章内容:本章内容: Spring profile 条件化的bean声明 自动装配与歧义性 bean的作用域 Spring表达式语言 在上一章中,我们看到了一些最为核心的bean装配技术。你可能会发现上一章学到的知识有很大的用处。但是,bean装配所涉及的领域并不 仅仅局限于上一章  所学习到的内容。Spring提供了多种技巧,借助它们可以实现更为高级的bean装配功能。 在本章中,我们将会深入介绍一些这样的高级技术。本章中所介绍的技术也许你不会天天都用到,但这并不意味着它们的价值会因此而降低。 3.1 环境与 环境与profile 在开发软件的时候,有一个很大的挑战就是将应用程序从一个环境迁移到另外一个环境。开发阶段中,某些环境相关做法可能并不适合迁移到 生产环境中,甚至即便迁移过去也无法正常工作。数据库配置、加密算法以及与外部系统的集成是跨环境部署时会发生变化的几个典型例子。 比如,考虑一下数据库配置。在开发环境中,我们可能会使用嵌入式数据库,并预先加载测试数据。例如,在Spring配置类中,我们可能会在 一个带有@Bean注解的方法上使用EmbeddedDatabaseBuilder: 这会创建一个类型为javax.sql.DataSource的bean,这个bean是如何创建出来的才是最有意思的。使 用EmbeddedDatabaseBuilder会搭建一个嵌入式的Hypersonic数据库,它的模式(schema)定义在schema.sql中,测试数据则是通过 test-data.sql加载的。 当你在开发环境中运行集成测试或者启动应用进行手动测试的时候,这个DataSource是很有用的。每次启动它的时候,都能让数据库处于一 个给定的状态。 尽管EmbeddedDatabaseBuilder创建的DataSource非常适于开发环境,但是对于生产环境来说,这会是一个糟糕的选择。在生产环境 的配置中,你可能会希望使用JNDI从容器中获取一个DataSource。在这样场景中,如下的@Bean方法会更加合适: 通过JNDI获取DataSource能够让容器决定该如何创建这个DataSource,甚至包括切换为容器管理的连接池。即便如此,JNDI管理 的DataSource更加适合于生产环境,对于简单的集成和开发测试环境来说,这会带来不必要的复杂性。 同时,在QA环境中,你可以选择完全不同的DataSource配置,可以配置为Commons DBCP连接池,如下所示: 显然,这里展现的三个版本的dataSource()方法互不相同。虽然它们都会生成一个类型为javax.sql.DataSource的bean,但它们的相 似点也仅限于此了。每个方法都使用了完全不同的策略来生成DataSource bean。 再次强调的是,这里的讨论并不是如何配置DataSource(我们将会在第10章更详细地讨论这个话题)。看起来很简单的DataSource实际 上并不是那么简单。这是一个很好的例子,它表现了在不同的环境中某个bean会有所不同。我们必须要有一种方法来配置DataSource,使 其在每种环境下都会选择最为合适的配置。 其中一种方式就是在单独的配置类(或XML文件)中配置每个bean,然后在构建阶段(可能会使用Maven的profiles)确定要将哪一个配置编译 到可部署的应用中。这种方式的问题在于要为每种环境重新构建应用。当从开发阶段迁移到QA阶段时,重新构建也许算不上什么大问题。但 是,从QA阶段迁移到生产阶段时,重新构建可能会引入bug并且会在QA团队的成员中带来不安的情绪。 值得庆幸的是,Spring所提供的解决方案并不需要重新构建。 3.1.1 配置 配置profile bean Spring为环境相关的bean所提供的解决方案其实与构建时的方案没有太大的差别。当然,在这个过程中需要根据环境决定该创建哪个bean和不 创建哪个bean。不过Spring并不是在构建的时候做出这样的决策,而是等到运行时再来确定。这样的结果就是同一个部署单元(可能会是 WAR文件)能够适用于所有的环境,没有必要进行重新构建。 在3.1版本中,Spring引入了bean profile的功能。要使用profile,你首先要将所有不同的bean定义整理到一个或多个profile之中,在将应用部署 到每个环境时,要确保对应的profile处于激活(active)的状态。 在Java配置中,可以使用@Profile注解指定某个bean属于哪一个profile。例如,在配置类中,嵌入式数据库的DataSource可能会配置成如 下所示: 我希望你能够注意的是@Profile注解应用在了类级别上。它会告诉Spring这个配置类中的bean只有在dev profile激活时才会创建。如果dev profile没有激活的话,那么带有@Bean注解的方法都会被忽略掉。 同时,你可能还需要有一个适用于生产环境的配置,如下所示: 在本例中,只有prod profile激活的时候,才会创建对应的bean。 在Spring 3.1中,只能在类级别上使用@Profile注解。不过,从Spring 3.2开始,你也可以在方法级别上使用@Profile注解,与@Bean注解 一同使用。这样的话,就能将这两个bean的声明放到同一个配置类之中,如下所示: 程序清单程序清单3.1  @Profile注解基于激活的注解基于激活的profile实现实现bean的装配的装配 这里有个问题需要注意,尽管每个DataSource bean都被声明在一个profile中,并且只有当规定的profile激活时,相应的bean才会被创建,但 是可能会有其他的bean并没有声明在一个给定的profile范围内。没有指定profile的bean始终都会被创建,与激活哪个profile没有关系。 在在XML中配置中配置profile 我们也可以通过元素的profile属性,在XML中配置profile bean。例如,为了在XML中定义适用于开发阶段的嵌入式数据 库DataSourcebean,我们可以创建如下所示的XML文件: 与之类似,我们也可以将profile设置为prod,创建适用于生产环境的从JNDI获取的DataSource bean。同样,可以创建基于连接池定义 的DataSource bean,将其放在另外一个XML文件中,并标注为qaprofile。所有的配置文件都会放到部署单元之中(如WAR文件),但是只 有profile属性与当前激活profile相匹配的配置文件才会被用到。 你还可以在根元素中嵌套定义元素,而不是为每个环境都创建一个profile XML文件。这能够将所有的profile bean定义放到 同一个XML文件中,如下所示: 程序清单程序清单3.2 重复使用元素来指定多个 重复使用元素来指定多个profile 除了所有的bean定义到了同一个XML文件之中,这种配置方式与定义在单独的XML文件中的实际效果是一样的。这里有三个bean,类型都 是javax.sql.DataSource,并且ID都是dataSource。但是在运行时,只会创建一个bean,这取决于处于激活状态的是哪个profile。 那么问题来了:我们该怎样激活某个profile呢? 3.1.2 激活 激活profile Spring在确定哪个profile处于激活状态时,需要依赖两个独立的属性:spring.profiles.active和spring.profiles.default。如 果设置了spring.profiles.active属性的话,那么它的值就会用来确定哪个profile是激活的。但如果没有设 置spring.profiles.active属性的话,那Spring将会查找spring.profiles.default的值。如 果spring.profiles.active和spring.profiles.default均没有设置的话,那就没有激活的profile,因此只会创建那些没有定义在 profile中的bean。 有多种方式来设置这两个属性: 作为DispatcherServlet的初始化参数; 作为Web应用的上下文参数; 作为JNDI条目; 作为环境变量; 作为JVM的系统属性; 在集成测试类上,使用@ActiveProfiles注解设置。 你尽可以选择spring.profiles.active和spring.profiles.default的最佳组合方式以满足需求,我将这样的自主权留给读者。 我所喜欢的一种方式是使用DispatcherServlet的参数将spring.profiles.default设置为开发环境的profile,我会在Servlet上下文 中进行设置(为了兼顾到ContextLoaderListener)。例如,在Web应用中,设置spring.profiles.default的web.xml文件会如下 所示: 程序清单程序清单3.3 在 在Web应用的应用的web.xml文件中设置默认的文件中设置默认的profile 按照这种方式设置spring.profiles.default,所有的开发人员都能从版本控制软件中获得应用程序源码,并使用开发环境的设置(如 嵌入式数据库)运行代码,而不需要任何额外的配置。 当应用程序部署到QA、生产或其他环境之中时,负责部署的人根据情况使用系统属性、环境变量或JNDI设置spring.profiles.active即 可。当设置spring.profiles.active以后,至于spring.profiles.default置成什么值就已经无所谓了;系统会优先使 用spring.profiles.active中所设置的profile。 你可能已经注意到了,在spring.profiles.active和spring.profiles.default中,profile使用的都是复数形式。这意味着你可以 同时激活多个profile,这可以通过列出多个profile名称,并以逗号分隔来实现。当然,同时启用dev和prod profile可能也没有太大的意义,不 过你可以同时设置多个彼此不相关的profile。 使用使用profile进行测试进行测试 当运行集成测试时,通常会希望采用与生产环境(或者是生产环境的部分子集)相同的配置进行测试。但是,如果配置中的bean定义在了 profile中,那么在运行测试时,我们就需要有一种方式来启用合适的profile。 Spring提供了@ActiveProfiles注解,我们可以使用它来指定运行测试时要激活哪个profile。在集成测试时,通常想要激活的是开发环境的 profile。例如,下面的测试类片段展现了使用@ActiveProfiles激活dev profile: 在条件化创建bean方面,Spring的profile机制是一种很棒的方法,这里的条件要基于哪个profile处于激活状态来判断。Spring 4.0中提供了一种 更为通用的机制来实现条件化的bean定义,在这种机制之中,条件完全由你来确定。让我们看一下如何使用Spring 4和@Conditional注解 定义条件化的bean。 3.2 条件化的 条件化的bean 假设你希望一个或多个bean只有在应用的类路径下包含特定的库时才创建。或者我们希望某个bean只有当另外某个特定的bean也声明了之后 才会创建。我们还可能要求只有某个特定的环境变量设置之后,才会创建某个bean。 在Spring 4之前,很难实现这种级别的条件化配置,但是Spring 4引入了一个新的@Conditional注解,它可以用到带有@Bean注解的方法 上。如果给定的条件计算结果为true,就会创建这个bean,否则的话,这个bean会被忽略。 例如,假设有一个名为MagicBean的类,我们希望只有设置了magic环境属性的时候,Spring才会实例化这个类。如果环境中没有这个属 性,那么MagicBean将会被忽略。在程序清单3.4所展现的配置中,使用@Conditional注解条件化地配置了MagicBean。 程序清单程序清单3.4 条件化地配置 条件化地配置bean 可以看到,@Conditional中给定了一个Class,它指明了条件——在本例中,也就是MagicExistsCondition。@Conditional将会 通过Condition接口进行条件对比: 设置给@Conditional的类可以是任意实现了Condition接口的类型。可以看出来,这个接口实现起来很简单直接,只需提 供matches()方法的实现即可。如果matches()方法返回true,那么就会创建带有@Conditional注解的bean。如果matches()方法返 回false,将不会创建这些bean。 在本例中,我们需要创建Condition的实现并根据环境中是否存在magic属性来做出决策。程序清单3.5展现 了MagicExistsCondition,这是完成该功能的Condition实现类: 程序清单程序清单3.5 在 在Condition中检查是否存在中检查是否存在magic属性属性 在上面的程序清单中,matches()方法很简单但功能强大。它通过给定的ConditionContext对象进而得到Environment对象,并使用 这个对象检查环境中是否存在名为magic的环境属性。在本例中,属性的值是什么无所谓,只要属性存在即可满足要求。如果满足这个条件的 话,matches()方法就会返回true。所带来的结果就是条件能够得到满足,所有@Conditional注解上引用MagicExistsCondition的 bean都会被创建。 话说回来,如果这个属性不存在的话,就无法满足条件,matches()方法会返回false,这些bean都不会被创建。 MagicExistsCondition中只是使用了ConditionContext得到的Environment,但Condition实现的考量因素可能会比这更 多。matches()方法会得到ConditionContext和AnnotatedTypeMetadata对象用来做出决策。 ConditionContext是一个接口,大致如下所示: 通过ConditionContext,我们可以做到如下几点: 借助getRegistry()返回的BeanDefinitionRegistry检查bean定义; 借助getBeanFactory()返回的ConfigurableListableBeanFactory检查bean是否存在,甚至探查bean的属性; 借助getEnvironment()返回的Environment检查环境变量是否存在以及它的值是什么; 读取并探查getResourceLoader()返回的ResourceLoader所加载的资源; 借助getClassLoader()返回的ClassLoader加载并检查类是否存在。 AnnotatedTypeMetadata则能够让我们检查带有@Bean注解的方法上还有什么其他的注解。像ConditionContext一 样,AnnotatedTypeMetadata也是一个接口。它如下所示: 借助isAnnotated()方法,我们能够判断带有@Bean注解的方法是不是还有其他特定的注解。借助其他的那些方法,我们能够检 查@Bean注解的方法上其他注解的属性。 非常有意思的是,从Spring 4开始,@Profile注解进行了重构,使其基于@Conditional和Condition实现。作为如何使 用@Conditional和Condition的例子,我们来看一下在Spring 4中,@Profile是如何实现的。 @Profile注解如下所示: 注意:注意: @Profile本身也使用了@Conditional注解,并且引用ProfileCondition作为Condition实现。如下所示,ProfileCondition实现了Condition接口,并且在做出决策的过程中,考虑到 了ConditionContext和AnnotatedTypeMetadata中的多个因素。 程序清单程序清单3.6  ProfileCondition检查某个检查某个bean profile是否可用是否可用 我们可以看到,ProfileCondition通过AnnotatedTypeMetadata得到了用于@Profile注解的所有属性。借助该信息,它会明确地检 查value属性,该属性包含了bean的profile名称。然后,它根据通过ConditionContext得到的Environment来检查[借 助acceptsProfiles()方法]该profile是否处于激活状态。 3.3 处理自动装配的歧义性 处理自动装配的歧义性 在第2章中,我们已经看到如何使用自动装配让Spring完全负责将bean引用注入到构造参数和属性中。自动装配能够提供很大的帮助,因为它 会减少装配应用程序组件时所需要的显式配置的数量。 不过,仅有一个bean匹配所需的结果时,自动装配才是有效的。如果不仅有一个bean能够匹配结果的话,这种歧义性会阻碍Spring自动装配属 性、构造器参数或方法参数。 为了阐述自动装配的歧义性,假设我们使用@Autowired注解标注了setDessert()方法: 在本例中,Dessert是一个接口,并且有三个类实现了这个接口,分别为Cake、Cookies和IceCream: 因为这三个实现均使用了@Component注解,在组件扫描的时候,能够发现它们并将其创建为Spring应用上下文里面的bean。然后,当Spring 试图自动装配setDessert()中的Dessert参数时,它并没有唯一、无歧义的可选值。在从多种甜点中做出选择时,尽管大多数人并不会有 什么困难,但是Spring却无法做出选择。Spring此时别无他法,只好宣告失败并抛出异常。更精确地讲,Spring会抛 出NoUniqueBeanDefinitionException: 当然,使用吃甜点的样例来阐述自动装配在遇到歧义性时所面临的问题多少有些牵强。在实际中,自动装配歧义性的问题其实比你想象中的更 为罕见。就算这种歧义性确实是个问题,但更常见的情况是给定的类型只有一个实现类,因此自动装配能够很好地运行。 但是,当确实发生歧义性的时候,Spring提供了多种可选方案来解决这样的问题。你可以将可选bean中的某一个设为首选(primary)的 bean,或者使用限定符(qualifier)来帮助Spring将可选的bean的范围缩小到只有一个bean。 3.3.1 标示首选的 标示首选的bean 如果你像我一样,喜欢所有类型的甜点,如蛋糕、饼干、冰激凌……它们都很美味。但如果只能在其中选择一种甜点的话,那你最喜欢的是哪 一种呢? 在声明bean的时候,通过将其中一个可选的bean设置为首选(primary)bean能够避免自动装配时的歧义性。当遇到歧义性的时候,Spring将 会使用首选的bean,而不是其他可选的bean。实际上,你所声明就是“最喜欢”的bean。 假设冰激凌就是你最喜欢的甜点。在Spring中,可以通过@Primary来表达最喜欢的方案。@Primary能够与@Component组合用在组件扫描 的bean上,也可以与@Bean组合用在Java配置的bean声明中。比如,下面的代码展现了如何将@Component注解的IceCream bean声明为首 选的bean: 或者,如果你通过Java配置显式地声明IceCream,那么@Bean方法应该如下所示: 如果你使用XML配置bean的话,同样可以实现这样的功能。元素有一个primary属性用来指定首选的bean: 不管你采用什么方式来标示首选bean,效果都是一样的,都是告诉Spring在遇到歧义性的时候要选择首选的bean。 但是,如果你标示了两个或更多的首选bean,那么它就无法正常工作了。比如,假设Cake类如下所示: 现在,有两个首选的Dessert bean:Cake和IceCream。这带来了新的歧义性问题。就像Spring无法从多个可选的bean中做出选择一样, 它也无法从多个首选的bean中做出选择。显然,如果不止一个bean被设置成了首选bean,那实际上也就是没有首选bean了。 就解决歧义性问题而言,限定符是一种更为强大的机制,下面就将对其进行介绍。 3.3.2 限定自动装配的 限定自动装配的bean 设置首选bean的局限性在于@Primary无法将可选方案的范围限定到唯一一个无歧义性的选项中。它只能标示一个优先的可选方案。当首选 bean的数量超过一个时,我们并没有其他的方法进一步缩小可选范围。 与之相反,Spring的限定符能够在所有可选的bean上进行缩小范围的操作,最终能够达到只有一个bean满足所规定的限制条件。如果将所有的 限定符都用上后依然存在歧义性,那么你可以继续使用更多的限定符来缩小选择范围。 @Qualifier注解是使用限定符的主要方式。它可以与@Autowired和@Inject协同使用,在注入的时候指定想要注入进去的是哪个bean。 例如,我们想要确保要将IceCream注入到setDessert()之中: 这是使用限定符的最简单的例子。为@Qualifier注解所设置的参数就是想要注入的bean的ID。所有使用@Component注解声明的类都会创 建为bean,并且bean的ID为首字母变为小写的类名。因此,@Qualifier("iceCream")指向的是组件扫描时所创建的bean,并且这个 bean是IceCream类的实例。 实际上,还有一点需要补充一下。更准确地讲,@Qualifier("iceCream")所引用的bean要具有String类型的“iceCream”作为限定符。如 果没有指定其他的限定符的话,所有的bean都会给定一个默认的限定符,这个限定符与bean的ID相同。因此,框架会将具有“iceCream”限定符 的bean注入到setDessert()方法中。这恰巧就是ID为iceCream的bean,它是IceCream类在组件扫描的时候创建的。 基于默认的bean ID作为限定符是非常简单的,但这有可能会引入一些问题。如果你重构了IceCream类,将其重命名为Gelato的话,那此时会 发生什么情况呢?如果这样的话,bean的ID和默认的限定符会变为gelato,这就无法匹配setDessert()方法中的限定符。自动装配会失 败。 这里的问题在于setDessert()方法上所指定的限定符与要注入的bean的名称是紧耦合的。对类名称的任意改动都会导致限定符失效。 创建自定义的限定符创建自定义的限定符 我们可以为bean设置自己的限定符,而不是依赖于将bean ID作为限定符。在这里所需要做的就是在bean声明上添加@Qualifier注解。例 如,它可以与@Component组合使用,如下所示: 在这种情况下,cold限定符分配给了IceCreambean。因为它没有耦合类名,因此你可以随意重构IceCream的类名,而不必担心会破坏自动 装配。在注入的地方,只要引用cold限定符就可以了: 值得一提的是,当通过Java配置显式定义bean的时候,@Qualifier也可以与@Bean注解一起使用: 当使用自定义的@Qualifier值时,最佳实践是为bean选择特征性或描述性的术语,而不是使用随意的名字。在本例中,我将IceCream bean描述为“cold”bean。在注入的时候,可以将这个需求理解为“给我一个凉的甜点”,这其实就是描述的IceCream。类似地,我可以 将Cake描述为“soft”,将Cookie描述为“crispy”。 使用自定义的限定符注解使用自定义的限定符注解 面向特性的限定符要比基于bean ID的限定符更好一些。但是,如果多个bean都具备相同特性的话,这种做法也会出现问题。例如,如果引入 了这个新的Dessert bean,会发生什么情况呢: 不会吧?!现在我们有了两个带有“cold”限定符的甜点。在自动装配Dessert bean的时候,我们再次遇到了歧义性的问题,需要使用更多的限定 符来将可选范围限定到只有一个bean。 可能想到的解决方案就是在注入点和bean定义的地方同时再添加另外一个@Qualifier注解。IceCream类大致就会如下所示: Popsicle类同样也可能再添加另外一个@Qualifier注解: 在注入点中,我们可能会使用这样的方式来将范围缩小到IceCream: 这里只有一个小问题:Java不允许在同一个条目上重复出现相同类型的多个注解。[1]如果你试图这样做的话,编译器会提示错误。在这里,使 用@Qualifier注解并没有办法(至少没有直接的办法)将自动装配的可选bean缩小范围至仅有一个可选的bean。 但是,我们可以创建自定义的限定符注解,借助这样的注解来表达bean所希望限定的特性。这里所需要做的就是创建一个注解,它本身要使 用@Qualifier注解来标注。这样我们将不再使用@Qualifier("cold"),而是使用自定义的@Cold注解,该注解的定义如下所示: 同样,你可以创建一个新的@Creamy注解来代替@Qualifier("creamy"): 当你不想用@Qualifier注解的时候,可以类似地创建@Soft、@Crispy和@Fruity。通过在定义时添加@Qualifier注解,它们就具有 了@Qualifier注解的特性。它们本身实际上就成为了限定符注解。 现在,我们可以重新看一下IceCream,并为其添加@Cold和@Creamy注解,如下所示: 类似地,Popsicle类可以添加@Cold和@Fruity注解: 最终,在注入点,我们使用必要的限定符注解进行任意组合,从而将可选范围缩小到只有一个bean满足需求。为了得到IceCream bean,setDessert()方法可以这样使用注解: 通过声明自定义的限定符注解,我们可以同时使用多个限定符,不会再有Java编译器的限制或错误。与此同时,相对于使用原始 的@Qualifier并借助String类型来指定限定符,自定义的注解也更为类型安全。 让我们近距离观察一下setDessert()方法以及它的注解,这里并没有在任何地方明确指定要将IceCream自动装配到该方法中。相反,我 们使用所需bean的特性来进行指定,即@Cold和@Creamy。因此,setDessert()方法依然能够与特定的Dessert实现保持解耦。任意满 足这些特征的bean都是可以的。在当前选择Dessert实现时,恰好如此,IceCream是唯一能够与之匹配的bean。 在本节和前面的节中,我们讨论了几种通过自定义注解扩展Spring的方式。为了创建自定义的条件化注解,我们创建一个新的注解并在这个注 解上添加了@Conditional。为了创建自定义的限定符注解,我们创建一个新的注解并在这个注解上添加了@Qualifier。这种技术可以用 到很多的Spring注解中,从而能够将它们组合在一起形成特定目标的自定义注解。 现在我们来看一下如何在不同的作用域中声明bean。 3.4  bean的作用域的作用域 在默认情况下,Spring应用上下文中所有bean都是作为以单例(singleton)的形式创建的。也就是说,不管给定的一个bean被注入到其他bean 多少次,每次所注入的都是同一个实例。 在大多数情况下,单例bean是很理想的方案。初始化和垃圾回收对象实例所带来的成本只留给一些小规模任务,在这些任务中,让对象保持无 状态并且在应用中反复重用这些对象可能并不合理。 有时候,可能会发现,你所使用的类是易变的(mutable),它们会保持一些状态,因此重用是不安全的。在这种情况下,将class声明为单例 的bean就不是什么好主意了,因为对象会被污染,稍后重用的时候会出现意想不到的问题。 Spring定义了多种作用域,可以基于这些作用域创建bean,包括: 单例(Singleton):在整个应用中,只创建bean的一个实例。 原型(Prototype):每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的bean实例。 会话(Session):在Web应用中,为每个会话创建一个bean实例。 请求(Rquest):在Web应用中,为每个请求创建一个bean实例。 单例是默认的作用域,但是正如之前所述,对于易变的类型,这并不合适。如果选择其他的作用域,要使用@Scope注解,它可以 与@Component或@Bean一起使用。 例如,如果你使用组件扫描来发现和声明bean,那么你可以在bean的类上使用@Scope注解,将其声明为原型bean: 这里,使用ConfigurableBeanFactory类的SCOPE_PROTOTYPE常量设置了原型作用域。你当然也可以使 用@Scope("prototype"),但是使用SCOPE_PROTOTYPE常量更加安全并且不易出错。 如果你想在Java配置中将Notepad声明为原型bean,那么可以组合使用@Scope和@Bean来指定所需的作用域: 同样,如果你使用XML来配置bean的话,可以使用元素的scope属性来设置作用域: 不管你使用哪种方式来声明原型作用域,每次注入或从Spring应用上下文中检索该bean的时候,都会创建新的实例。这样所导致的结果就是每 次操作都能得到自己的Notepad实例。 3.4.1 使用会话和请求作用域 使用会话和请求作用域 在Web应用中,如果能够实例化在会话和请求范围内共享的bean,那将是非常有价值的事情。例如,在典型的电子商务应用中,可能会有一个 bean代表用户的购物车。如果购物车是单例的话,那么将会导致所有的用户都会向同一个购物车中添加商品。另一方面,如果购物车是原型作 用域的,那么在应用中某一个地方往购物车中添加商品,在应用的另外一个地方可能就不可用了,因为在这里注入的是另外一个原型作用域的 购物车。 就购物车bean来说,会话作用域是最为合适的,因为它与给定的用户关联性最大。要指定会话作用域,我们可以使用@Scope注解,它的使用 方式与指定原型作用域是相同的: 这里,我们将value设置成了WebApplicationContext中的SCOPE_SESSION常量(它的值是session)。这会告诉Spring为Web应用 中的每个会话创建一个ShoppingCart。这会创建多个ShoppingCart bean的实例,但是对于给定的会话只会创建一个实例,在当前会话 相关的操作中,这个bean实际上相当于单例的。 要注意的是,@Scope同时还有一个proxyMode属性,它被设置成了ScopedProxyMode.INTERFACES。这个属性解决了将会话或请求作 用域的bean注入到单例bean中所遇到的问题。在描述proxyMode属性之前,我们先来看一下proxyMode所解决问题的场景。 假设我们要将ShoppingCart bean注入到单例StoreService bean的Setter方法中,如下所示: 因为StoreService是一个单例的bean,会在Spring应用上下文加载的时候创建。当它创建的时候,Spring会试图将ShoppingCart bean 注入到setShoppingCart()方法中。但是ShoppingCart bean是会话作用域的,此时并不存在。直到某个用户进入系统,创建了会话之 后,才会出现ShoppingCart实例。 另外,系统中将会有多个ShoppingCart实例:每个用户一个。我们并不想让Spring注入某个固定的ShoppingCart实例 到StoreService中。我们希望的是当StoreService处理购物车功能时,它所使用的ShoppingCart实例恰好是当前会话所对应的那一 个。 Spring并不会将实际的ShoppingCart bean注入到StoreService中,Spring会注入一个到ShoppingCart bean的代理,如图3.1所示。这个 代理会暴露与ShoppingCart相同的方法,所以StoreService会认为它就是一个购物车。但是,当StoreService调 用ShoppingCart的方法时,代理会对其进行懒解析并将调用委托给会话作用域内真正的ShoppingCart bean。 现在,我们带着对这个作用域的理解,讨论一下proxyMode属性。如配置所示,proxyMode属性被设置成 了ScopedProxyMode.INTERFACES,这表明这个代理要实现ShoppingCart接口,并将调用委托给实现bean。 如果ShoppingCart是接口而不是类的话,这是可以的(也是最为理想的代理模式)。但如果ShoppingCart是一个具体的类的话,Spring 就没有办法创建基于接口的代理了。此时,它必须使用CGLib来生成基于类的代理。所以,如果bean类型是具体类的话,我们必须要 将proxyMode属性设置为ScopedProxyMode.TARGET_CLASS,以此来表明要以生成目标类扩展的方式创建代理。 尽管我主要关注了会话作用域,但是请求作用域的bean会面临相同的装配问题。因此,请求作用域的bean应该也以作用域代理的方式进行注 入。 图3.1 作用域代理能够延迟注入请求和会话作用域的bean 3.4.2 在 在XML中声明作用域代理中声明作用域代理 如果你需要使用XML来声明会话或请求作用域的bean,那么就不能使用@Scope注解及其proxyMode属性了。元素的scope属性能够 设置bean的作用域,但是该怎样指定代理模式呢? 要设置代理模式,我们需要使用Spring aop命名空间的一个新元素: 是与@Scope注解的proxyMode属性功能相同的Spring XML配置元素。它会告诉Spring为bean创建一个作用域代 理。默认情况下,它会使用CGLib创建目标类的代理。但是我们也可以将proxy-target-class属性设置为false,进而要求它生成基于接 口的代理: 为了使用元素,我们必须在XML配置中声明Spring的aop命名空间: 在第4章中,当我们使用Spring和面向切面编程的时候,会讨论Spring aop命名空间的更多知识。不过,在结束本章的内容之前,我们来看一 下Spring高级配置的另外一个可选方案:Spring表达式语言(Spring Expression Language)。 3.5 运行时值注入 运行时值注入 当讨论依赖注入的时候,我们通常所讨论的是将一个bean引用注入到另一个bean的属性或构造器参数中。它通常来讲指的是将一个对象与另 一个对象进行关联。 但是bean装配的另外一个方面指的是将一个值注入到bean的属性或者构造器参数中。我们在第2章中已经进行了很多值装配,如将专辑的名字 装配到BlankDisc bean的构造器或title属性中。例如,我们可能按照这样的方式来组装BlankDisc: 尽管这实现了你的需求,也就是为BlankDisc bean设置title和artist,但它在实现的时候是将值硬编码在配置类中的。与之类似,如果使用 XML的话,那么值也会是硬编码的: 有时候硬编码是可以的,但有的时候,我们可能会希望避免硬编码值,而是想让这些值在运行时再确定。为了实现这些功能,Spring提供了两 种在运行时求值的方式: 属性占位符(Property placeholder)。 Spring表达式语言(SpEL)。 很快你就会发现这两种技术的用法是类似的,不过它们的目的和行为是有所差别的。让我们先看一下属性占位符,在这两者中它较为简单,然 后再看一下更为强大的SpEL。 3.5.1 注入外部的值 注入外部的值 在Spring中,处理外部值的最简单方式就是声明属性源并通过Spring的Environment来检索属性。例如,程序清单3.7展现了一个基本的 Spring配置类,它使用外部的属性来装配BlankDisc bean。 程序清单程序清单3.7 使用 使用@PropertySource注解和注解和Environment 在本例中,@PropertySource引用了类路径中一个名为app.properties的文件。它大致会如下所示: 这个属性文件会加载到Spring的Environment中,稍后可以从这里检索属性。同时,在disc()方法中,会创建一个新的BlankDisc,它的 构造器参数是从属性文件中获取的,而这是通过调用getProperty()实现的。 深入学习深入学习Spring的的Environment 当我们去了解Environment的时候会发现,程序清单3.7所示的getProperty()方法并不是获取属性值的唯一方法,getProperty()方 法有四个重载的变种形式: 前两种形式的getProperty()方法都会返回String类型的值。我们已经在程序清单3.7中看到了如何使用第一种getProperty()方法。但 是,你可以稍微对@Bean方法进行一下修改,这样在指定属性不存在的时候,会使用一个默认值: 剩下的两种getProperty()方法与前面的两种非常类似,但是它们不会将所有的值都视为String类型。例如,假设你想要获取的值所代表的 含义是连接池中所维持的连接数量。如果我们从属性文件中得到的是一个String类型的值,那么在使用之前还需要将其转换为Integer类型。但 是,如果使用重载形式的getProperty()的话,就能非常便利地解决这个问题: Environment还提供了几个与属性相关的方法,如果你在使用getProperty()方法的时候没有指定默认值,并且这个属性没有定义的话, 获取到的值是null。如果你希望这个属性必须要定义,那么可以使用getRequiredProperty()方法,如下所示: 在这里,如果disc.title或disc.artist属性没有定义的话,将会抛出IllegalStateException异常。 如果想检查一下某个属性是否存在的话,那么可以调用Environment的containsProperty()方法: 最后,如果想将属性解析为类的话,可以使用getPropertyAsClass()方法: 除了属性相关的功能以外,Environment还提供了一些方法来检查哪些profile处于激活状态: String[] getActiveProfiles():返回激活profile名称的数组; String[] getDefaultProfiles():返回默认profile名称的数组; boolean acceptsProfiles(String... profiles):如果environment支持给定profile的话,就返回true。 在程序清单3.6中,我们已经看到了如何使用acceptsProfiles()。在那个例子中,Environment是从ConditionContext中获取到 的,在bean创建之前,使用acceptsProfiles()方法来确保给定bean所需的profile处于激活状态。通常来讲,我们并不会频繁使 用Environment相关的方法,但是知道有这些方法还是有好处的。 直接从Environment中检索属性是非常方便的,尤其是在Java配置中装配bean的时候。但是,Spring也提供了通过占位符装配属性的方法, 这些占位符的值会来源于一个属性源。 解析属性占位符解析属性占位符 Spring一直支持将属性定义到外部的属性的文件中,并使用占位符值将其插入到Spring bean中。在Spring装配中,占位符的形式为使用“${ ... }”包装的属性名称。作为样例,我们可以在XML中按照如下的方式解析BlankDisc构造器参数: 可以看到,title构造器参数所给定的值是从一个属性中解析得到的,这个属性的名称为disc.title。artist参数装配的是名为 disc.artist的属性值。按照这种方式,XML配置没有使用任何硬编码的值,它的值是从配置文件以外的一个源中解析得到的。(我们稍后 会讨论这些属性是如何解析的。) 如果我们依赖于组件扫描和自动装配来创建和初始化应用组件的话,那么就没有指定占位符的配置文件或类了。在这种情况下,我们可以使 用@Value注解,它的使用方式与@Autowired注解非常相似。比如,在BlankDisc类中,构造器可以改成如下所示: 为了使用占位符,我们必须要配置一个PropertyPlaceholderConfigurer bean或PropertySourcesPlaceholderConfigurer bean。从Spring 3.1开始,推荐使用PropertySourcesPlaceholderConfigurer,因为它能够基于Spring Environment及其属性源来 解析占位符。 如下的@Bean方法在Java中配置了PropertySourcesPlaceholderConfigurer: 如果你想使用XML配置的话,Spring context命名空间中的元素将会为你生 成PropertySourcesPlaceholderConfigurer bean: 解析外部属性能够将值的处理推迟到运行时,但是它的关注点在于根据名称解析来自于Spring Environment和属性源的属性。而Spring表达 式语言提供了一种更通用的方式在运行时计算所要注入的值。 3.5.2 使用 使用Spring表达式语言进行装配表达式语言进行装配 Spring 3引入了Spring表达式语言(Spring Expression Language,SpEL),它能够以一种强大和简洁的方式将值装配到bean属性和构造器参 数中,在这个过程中所使用的表达式会在运行时计算得到值。使用SpEL,你可以实现超乎想象的装配效果,这是使用其他的装配技术难以做 到的(甚至是不可能的)。 SpEL拥有很多特性,包括: 使用bean的ID来引用bean; 调用方法和访问对象的属性; 对值进行算术、关系和逻辑运算; 正则表达式匹配; 集合操作。 在本书后面的内容中你可以看到,SpEL能够用在依赖注入以外的其他地方。例如,Spring Security支持使用SpEL表达式定义安全限制规则。 另外,如果你在Spring MVC应用中使用Thymeleaf模板作为视图的话,那么这些模板可以使用SpEL表达式引用模型数据。 作为起步,我们看几个SpEL表达式的样例,以及如何将其注入到bean中。然后我们会深入学习一些SpEL的基础表达式,它们能够组合起来形 成更为强大的表达式。 SpEL样例样例 SpEL是一种非常灵活的表达式语言,所以在本书中不可能面面俱到地介绍它的各种用法。但是我们可以展示几个基本的例子,这些例子会激 发你的灵感,有助于你编写自己的表达式。 需要了解的第一件事情就是SpEL表达式要放到“#{ ... }”之中,这与属性占位符有些类似,属性占位符需要放到“${ ... }”之中。下面所 展现的可能是最简单的SpEL表达式了: 除去“#{ ... }”标记之后,剩下的就是SpEL表达式体了,也就是一个数字常量。这个表达式的计算结果就是数字1,这恐怕并不会让你感到 丝毫惊讶。 当然,在实际的应用程序中,我们可能并不会使用这么简单的表达式。我们可能会使用更加有意思的表达式,如: 它的最终结果是计算表达式的那一刻当前时间的毫秒数。T()表达式会将java.lang.System视为Java中对应的类型,因此可以调用 其static修饰的currentTimeMillis()方法。 SpEL表达式也可以引用其他的bean或其他bean的属性。例如,如下的表达式会计算得到ID为sgtPeppers的bean的artist属性: 我们还可以通过systemProperties对象引用系统属性: 这只是SpEL的几个基础样例。在本章结束之前,你还会看到很多这样的表达式。但是,在此之前,让我们看一下在bean装配的时候如何使用 这些表达式。 如果通过组件扫描创建bean的话,在注入属性和构造器参数时,我们可以使用@Value注解,这与之前看到的属性占位符非常类似。不过,在 这里我们所使用的不是占位符表达式,而是SpEL表达式。例如,下面的样例展现了BlankDisc,它会从系统属性中获取专辑名称和艺术家的 名字: 在XML配置中,你可以将SpEL表达式传入的value属性中,或者将其作为p-命名空间或c-命名空间条 目的值。例如,在如下BlankDisc bean的XML声明中,构造器参数就是通过SpEL表达式设置的: 我们已经看过了几个简单的样例,也学习了如何将SpEL解析得到的值注入到bean中,那现在就来继续学习一下SpEL所支持的基础表达式吧。 表示字面值表示字面值 我们在前面已经看到了一个使用SpEL来表示整数字面量的样例。它实际上还可以用来表示浮点数、String值以及Boolean值。 下面的SpEL表达式样例所表示的就是浮点值: 数值还可以使用科学记数法的方式进行表示。如下面的表达式计算得到的值就是98,700: SpEL表达式也可以用来计算String类型的字面值,如: 最后,字面值true和false的计算结果就是它们对应的Boolean类型的值。例如: 在SpEL中使用字面值其实没有太大的意思,毕竟将整型属性设置为1,或者将Boolean属性设置为false时,我们并不需要使用SpEL。我承认 在SpEL表达式中,只包含字面值情况并没有太大的用处。但需要记住的一点是,更有意思的SpEL表达式是由更简单的表达式组成的,因此了 解在SpEL中如何使用字面量还是很有用处的。当组合更为复杂的表达式时,你迟早会用到它们。 引用引用bean、属性和方法、属性和方法 SpEL所能做的另外一件基础的事情就是通过ID引用其他的bean。例如,你可以使用SpEL将一个bean装配到另外一个bean的属性中,此时要 使用bean ID作为SpEL表达式(在本例中,也就是sgtPeppers): 现在,假设我们想在一个表达式中引用sgtPeppers的artist属性: 表达式主体的第一部分引用了一个ID为sgtPeppers的bean,分割符之后是对artist属性的引用。 除了引用bean的属性,我们还可以调用bean上的方法。例如,假设有另外一个bean,它的ID为artistSelector,我们可以在SpEL表达式 中按照如下的方式来调用bean的selectArtist()方法: 对于被调用方法的返回值来说,我们同样可以调用它的方法。例如,如果selectArtist()方法返回的是一个String,那么可以调 用toUpperCase()将整个艺术家的名字改为大写字母形式: 如果selectArtist()的返回值不是null的话,这没有什么问题。为了避免出现NullPointerException,我们可以使用类型安全的运 算符: 与之前只是使用点号(.)来访问toUpperCase()方法不同,现在我们使用了“?.”运算符。这个运算符能够在访问它右边的内容之前,确保它 所对应的元素不是null。所以,如果selectArtist()的返回值是null的话,那么SpEL将不会调用toUpperCase()方法。表达式的返 回值会是null。 在表达式中使用类型在表达式中使用类型 如果要在SpEL中访问类作用域的方法和常量的话,要依赖T()这个关键的运算符。例如,为了在SpEL中表达Java的Math类,需要按照如下 的方式使用T()运算符: 这里所示的T()运算符的结果会是一个Class对象,代表了java.lang.Math。如果需要的话,我们甚至可以将其装配到一个Class类型的 bean属性中。但是T()运算符的真正价值在于它能够访问目标类型的静态方法和常量。 例如,假如你需要将PI值装配到bean属性中。如下的SpEL就能完成该任务: 与之类似,我们可以调用T()运算符所得到类型的静态方法。我们已经看到了通过T()调用System.currentTimeMillis()。如下的这个 样例会计算得到一个0到1之间的随机数: SpEL运算符运算符 SpEL提供了多个运算符,这些运算符可以用在SpEL表达式的值上。表3.1概述了这些运算符。 表表3.1 用来操作表达式值的 用来操作表达式值的SpEL运算符运算符 运算符类型运算符类型 运 算 符运 算 符 算术运算 +、-、 * 、/、%、^ 比较运算 < 、 > 、 == 、 <= 、 >= 、 lt 、 gt 、 eq 、 le 、 ge 逻辑运算 and 、 or 、 not 、│ 条件运算 ?: (ternary) 、 ?: (Elvis) 正则表达式 matches 作为使用上述运算符的一个简单样例,我们看一下下面这个SpEL表达式: 这不仅是使用SpEL中乘法运算符(*)的绝佳样例,它也为你展现了如何将简单的表达式组合为更为复杂的表达式。在这里PI的值乘以2,然 后再乘以radius属性的值,这个属性来源于ID为circle的bean。实际上,它计算了circle bean中所定义圆的周长。 类似地,你还可以在表达式中使用乘方运算符(^)来计算圆的面积: “^”是用于乘方计算的运算符。在本例中,我们使用它来计算圆半径的平方。 当使用String类型的值时,“+”运算符执行的是连接操作,与在Java中是一样的: SpEL同时还提供了比较运算符,用来在表达式中对值进行对比。注意在表3.1中,比较运算符有两种形式:符号形式和文本形式。在大多数情 况下,符号运算符与对应的文本运算符作用是相同的,使用哪一种形式均可以。 例如,要比较两个数字是不是相等,可以使用双等号运算符(==): 或者,也可以使用文本型的eq运算符: 两种方式的结果都是一样的。表达式的计算结果是个Boolean值:如果counter.total等于100的话,为true,否则为false。 SpEL还提供了三元运算符(ternary),它与Java中的三元运算符非常类似。例如,如下的表达式会判断如果scoreboard.score>1000的 话,计算结果为String类型的“Winner!”,否则的话,结果为Loser: 三元运算符的一个常见场景就是检查null值,并用一个默认值来替代null。例如,如下的表达式会判断disc.title的值是不是null,如 果是null的话,那么表达式的计算结果就会是“Rattle and Hum”: 这种表达式通常称为Elvis运算符。这个奇怪名称的来历是,当使用符号来表示表情时,问号看起来很像是猫王(Elvis Presley)的头发。 计算正则表达式计算正则表达式 当处理文本时,有时检查文本是否匹配某种模式是非常有用的。SpEL通过matches运算符支持表达式中的模式匹配。matches运算符对 String类型的文本(作为左边参数)应用正则表达式(作为右边参数)。matches的运算结果会返回一个Boolean类型的值:如果与正则表达 式相匹配,则返回true;否则返回false。 为了进一步解释matches运算符,假设我们想判断一个字符串是否包含有效的邮件地址。在这个场景下,我们可以使用matches运算符,如 下所示: 探寻正则表达式语法的秘密超出了本书的范围,同时我们也应该意识到这里的正则表达式还不足够健壮来涵盖所有的场景。但对于演 示matches运算符的用法,这已经足够了。 计算集合计算集合 SpEL中最令人惊奇的一些技巧是与集合和数组相关的。最简单的事情可能就是引用列表中的一个元素了: 这个表达式会计算songs集合中第五个(基于零开始)元素的title属性,这个集合来源于ID为jukebox bean。 为了让这个表达式更丰富一些,假设我们要从jukebox中随机选择一首歌: “[]”运算符用来从集合或数组中按照索引获取元素,实际上,它还可以从String中获取一个字符。比如: 这个表达式引用了String中的第四个(基于零开始)字符,也就是“s”。[2] SpEL还提供了查询运算符(.?[]),它会用来对集合进行过滤,得到集合的一个子集。作为阐述的样例,假设你希望得到jukebox 中artist属性为Aerosmith的所有歌曲。如下的表达式就使用查询运算符得到了Aerosmith的所有歌曲: 可以看到,选择运算符在它的方括号中接受另一个表达式。当SpEL迭代歌曲列表的时候,会对歌曲集合中的每一个条目计算这个表达式。如 果表达式的计算结果为true的话,那么条目会放到新的集合中。否则的话,它就不会放到新集合中。在本例中,内部的表达式会检查歌曲 的artist属性是不是等于Aerosmith。 SpEL还提供了另外两个查询运算符:“.^[]”和“.$[]”,它们分别用来在集合中查询第一个匹配项和最后一个匹配项。例如,考虑下面的表达 式,它会查找列表中第一个artist属性为Aerosmith的歌曲: 最后,SpEL还提供了投影运算符(.![]),它会从集合的每个成员中选择特定的属性放到另外一个集合中。作为样例,假设我们不想要歌曲 对象的集合,而是所有歌曲名称的集合。如下的表达式会将title属性投影到一个新的String类型的集合中: 实际上,投影操作可以与其他任意的SpEL运算符一起使用。比如,我们可以使用如下的表达式获得Aerosmith所有歌曲的名称列表: 我们所介绍的只是SpEL功能的一个皮毛。在本书中还有更多的机会继续介绍SpEL,尤其是在定义安全规则的时候。 现在对SpEL的介绍要告一段落了,不过在此之前,我们有一个提示。在动态注入值到Spring bean时,SpEL是一种很便利和强大的方式。我们 有时会忍不住编写很复杂的表达式。但需要注意的是,不要让你的表达式太智能。你的表达式越智能,对它的测试就越重要。SpEL毕竟只是 String类型的值,可能测试起来很困难。鉴于这一点,我建议尽可能让表达式保持简洁,这样测试不会是什么大问题。 3.6 小结 小结 我们在本章介绍了许多背景知识,在第2章所介绍的基本bean装配基础之上,又学习了一些强大的高级装配技巧。 首先,我们学习了Spring profile,它解决了Spring bean要跨各种部署环境的通用问题。在运行时,通过将环境相关的bean与当前激活的profile 进行匹配,Spring能够让相同的部署单元跨多种环境运行,而不需要进行重新构建。 Profile bean是在运行时条件化创建bean的一种方式,但是Spring 4提供了一种更为通用的方式,通过这种方式能够声明某些bean的创建与否要 依赖于给定条件的输出结果。结合使用@Conditional注解和Spring Condition接口的实现,能够为开发人员提供一种强大和灵活的机制, 实现条件化地创建bean。 我们还看了两种解决自动装配歧义性的方法:首选bean以及限定符。尽管将某个bean设置为首选bean是很简单的,但这种方式也有其局限 性,所以我们讨论了如何将一组可选的自动装配bean,借助限定符将其范围缩小到只有一个符合条件的bean。除此之外,我们还看到了如何 创建自定义的限定符注解,这些限定符描述了bean的特性。 尽管大多数的Spring bean都是以单例的方式创建的,但有的时候其他的创建策略更为合适。Spring能够让bean以单例、原型、请求作用域或 会话作用域的方式来创建。在声明请求作用域或会话作用域的bean的时候,我们还学习了如何创建作用域代理,它分为基于类的代理和基于接 口的代理的两种方式。 最后,我们学习了Spring表达式语言,它能够在运行时计算要注入到bean属性中的值。 对于bean装配,我们已经掌握了扎实的基础知识,现在我们要将注意力转向面向切面编程(aspect-oriented programming ,AOP)了。依赖 注入能够将组件及其协作的其他组件解耦,与之类似,AOP有助于将应用组件与跨多个组件的任务进行解耦。在下一章,我们将会深入学习在 Spring中如何创建和使用切面。 [1]Java 8允许出现重复的注解,只要这个注解本身在定义的时候带有@Repeatable注解就可以。不过,Spring的@Qualifier注解并没有在 定义时添加@Repeatable注解。 [2]不要责怪我,我不太认同这个名字。但是我必须承认,它看起来确实有点像猫王的头发。 第第4章 面向切面的章 面向切面的Spring 本章内容:本章内容: 面向切面编程的基本原理 通过POJO创建切面 使用@AspectJ注解 为AspectJ切面注入依赖 在编写本章时,得克萨斯州(我所居住的地方)正值盛夏,这几天正在经历创历史记录的高温天气。这里真的非常热,在这种天气下,空调当 然是必不可少的。但是空调的缺点是它会耗电,而电需要钱。为了享受凉爽和舒适,我们没有什么办法可以避免这种开销。这是因为每家每户 都有一个电表来记录用电量,每个月都会有人来查电表,这样电力公司就知道应该收取多少费用了。 现在想象一下,如果没有电表,也没有人来查看用电量,假设现在由户主来联系电力公司并报告自己的用电量。虽然可能会有一些特别执着的 户主会详细记录使用电灯、电视和空调的情况,但大多数人肯定不会这么做。基于信用的电力收费对于消费者可能非常不错,但对于电力公司 来说结果可能就不那么美妙了。 监控用电量是一个很重要的功能,但并不是大多数家庭重点关注的问题。所有家庭实际上所关注的可能是修剪草坪、用吸尘器清理地毯、打扫 浴室等事项。从家庭的角度来看,监控房屋的用电量是一个被动事件(其实修剪草坪也是一个被动事件——特别是在炎热的天气下)。 软件系统中的一些功能就像我们家里的电表一样。这些功能需要用到应用程序的多个地方,但是我们又不想在每个点都明确调用它们。日志、 安全和事务管理的确都很重要,但它们是否为应用对象主动参与的行为呢?如果让应用对象只关注于自己所针对的业务领域问题,而其他方面 的问题由其他应用对象来处理,这会不会更好呢? 在软件开发中,散布于应用中多处的功能被称为横切关注点(cross-cutting concern)。通常来讲,这些横切关注点从概念上是与应用的业务逻 辑相分离的(但是往往会直接嵌入到应用的业务逻辑之中)。把这些横切关注点与业务逻辑相分离正是面向切面编程(AOP)所要解决的问 题。 在第2章,我们介绍了如何使用依赖注入(DI)管理和配置我们的应用对象。DI有助于应用对象之间的解耦,而AOP可以实现横切关注点与它 们所影响的对象之间的解耦。 日志是应用切面的常见范例,但它并不是切面适用的唯一场景。通览本书,我们还会看到切面所适用的多个场景,包括声明式事务、安全和缓 存。 本章展示了Spring对切面的支持,包括如何把普通类声明为一个切面和如何使用注解创建切面。除此之外,我们还会看到AspectJ——另一种 流行的AOP实现——如何补充Spring AOP框架的功能。但是,我们先不管事务、安全和缓存,先看一下Spring是如何实现切面的,就从AOP 的基础知识开始吧。 4.1 什么是面向切面编程 什么是面向切面编程 如前所述,切面能帮助我们模块化横切关注点。简而言之,横切关注点可以被描述为影响应用多处的功能。例如,安全就是一个横切关注点, 应用中的许多方法都会涉及到安全规则。图4.1直观呈现了横切关注点的概念。 图4.1 切面实现了横切关注点(跨多个应用对象的逻辑)的模块化 图4.1展现了一个被划分为模块的典型应用。每个模块的核心功能都是为特定业务领域提供服务,但是这些模块都需要类似的辅助功能,例如 安全和事务管理。 如果要重用通用功能的话,最常见的面向对象技术是继承(inheritance)或委托(delegation)。但是,如果在整个应用中都使用相同的基类, 继承往往会导致一个脆弱的对象体系;而使用委托可能需要对委托对象进行复杂的调用。 切面提供了取代继承和委托的另一种可选方案,而且在很多场景下更清晰简洁。在使用面向切面编程时,我们仍然在一个地方定义通用功能, 但是可以通过声明的方式定义这个功能要以何种方式在何处应用,而无需修改受影响的类。横切关注点可以被模块化为特殊的类,这些类被称 为切面(aspect)。这样做有两个好处:首先,现在每个关注点都集中于一个地方,而不是分散到多处代码中;其次,服务模块更简洁,因为 它们只包含主要关注点(或核心功能)的代码,而次要关注点的代码被转移到切面中了。 4.1.1 定义 定义AOP术语术语 与大多数技术一样,AOP已经形成了自己的术语。描述切面的常用术语有通知(advice)、切点(pointcut)和连接点(join point)。图4.2展 示了这些概念是如何关联在一起的。 图4.2 在一个或多个连接点上, 可以把切面的功能(通知)织入到程序的执行过程中 遗憾的是,大多数用于描述AOP功能的术语并不直观,尽管如此,它们现在已经是AOP行话的组成部分了,为了理解AOP,我们必须了解这 些术语。在我们进入某个领域之前,必须学会在这个领域该如何说话。 通知(通知(Advice)) 当抄表员出现在我们家门口时,他们要登记用电量并回去向电力公司报告。显然,他们必须有一份需要抄表的住户清单,他们所汇报的信息也 很重要,但记录用电量才是抄表员的主要工作。 类似地,切面也有目标——它必须要完成的工作。在AOP术语中,切面的工作被称为通知。 通知定义了切面是什么以及何时使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。它应该应用在某个方法被调用之 前?之后?之前和之后都调用?还是只在方法抛出异常时调用? Spring切面可以应用5种类型的通知: 前置通知(Before):在目标方法被调用之前调用通知功能; 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么; 返回通知(After-returning):在目标方法成功执行之后调用通知; 异常通知(After-throwing):在目标方法抛出异常后调用通知; 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。 连接点(连接点(Join point)) 电力公司为多个住户提供服务,甚至可能是整个城市。每家都有一个电表,这些电表上的数字都需要读取,因此每家都是抄表员的潜在目标。 抄表员也许能够读取各种类型的设备,但是为了完成他的工作,他的目标应该房屋内所安装的电表。 同样,我们的应用可能也有数以千计的时机应用通知。这些时机被称为连接点。连接点是在应用执行过程中能够插入切面的一个点。这个点可 以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。 切点(切点(Poincut)) 如果让一位抄表员访问电力公司所服务的所有住户,那肯定是不现实的。实际上,电力公司为每一个抄表员都分别指定某一块区域的住户。类 似地,一个切面并不需要通知应用的所有连接点。切点有助于缩小切面所通知的连接点的范围。 如果说通知定义了切面的“什么”和“何时”的话,那么切点就定义了“何处”。切点的定义会匹配通知所要织入的一个或多个连接点。我们通常使用 明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。有些AOP框架允许我们创建动态的切点,可以根据运 行时的决策(比如方法的参数值)来决定是否应用通知。 切面(切面(Aspect)) 当抄表员开始一天的工作时,他知道自己要做的事情(报告用电量)和从哪些房屋收集信息。因此,他知道要完成工作所需要的一切东西。 切面是通知和切点的结合。通知和切点共同定义了切面的全部内容——它是什么,在何时和何处完成其功能。 引入(引入(Introduction)) 引入允许我们向现有的类添加新方法或属性。例如,我们可以创建一个Auditable通知类,该类记录了对象最后一次修改时的状态。这很简 单,只需一个方法,setLastModified(Date),和一个实例变量来保存这个状态。然后,这个新方法和实例变量就可以被引入到现有的类 中,从而可以在无需修改这些现有的类的情况下,让它们具有新的行为和状态。 织入(织入(Weaving)) 织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期里有多个点可以 进行织入: 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的。 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增 强该目标类的字节码。AspectJ 5的加载时织入(load-time weaving,LTW)就支持以这种方式织入切面。 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。Spring AOP就是以这种方式织入切面的。 要掌握的新术语可真不少啊。再看一下图4.1,现在我们已经了解了如下的知识,通知包含了需要用于多个应用对象的横切行为;连接点是程 序执行过程中能够应用通知的所有点;切点定义了通知被应用的具体位置(在哪些连接点)。其中关键的概念是切点定义了哪些连接点会得到 通知。 我们已经了解了一些基础的AOP术语,现在让我们再看看这些AOP的核心概念是如何在Spring中实现的。 4.1.2  Spring对对AOP的支持的支持 并不是所有的AOP框架都是相同的,它们在连接点模型上可能有强弱之分。有些允许在字段修饰符级别应用通知,而另一些只支持与方法调用 相关的连接点。它们织入切面的方式和时机也有所不同。但是无论如何,创建切点来定义切面所织入的连接点是AOP框架的基本功能。 因为这是一本介绍Spring的图书,所以我们会关注Spring AOP。虽然如此,Spring和AspectJ项目之间有大量的协作,而且Spring对AOP的支 持在很多方面借鉴了AspectJ项目。 Spring提供了4种类型的AOP支持: 基于代理的经典Spring AOP; 纯POJO切面; @AspectJ注解驱动的切面; 注入式AspectJ切面(适用于Spring各版本)。 前三种都是Spring AOP实现的变体,Spring AOP构建在动态代理基础之上,因此,Spring对AOP的支持局限于方法拦截。 术语“经典”通常意味着是很好的东西。老爷车、经典高尔夫球赛、可口可乐精品都是好东西。但是Spring的经典AOP编程模型并不怎么样。当 然,曾经它的确非常棒。但是现在Spring提供了更简洁和干净的面向切面编程方式。引入了简单的声明式AOP和基于注解的AOP之后,Spring 经典的AOP看起来就显得非常笨重和过于复杂,直接使用 ProxyFactory Bean会让人感觉厌烦。所以在本书中我不会再介绍经典的Spring AOP。 借助Spring的aop命名空间,我们可以将纯POJO转换为切面。实际上,这些POJO只是提供了满足切点条件时所要调用的方法。遗憾的是,这 种技术需要XML配置,但这的确是声明式地将对象转换为切面的简便方式。 Spring借鉴了AspectJ的切面,以提供注解驱动的AOP。本质上,它依然是Spring基于代理的AOP,但是编程模型几乎与编写成熟的AspectJ注 解切面完全一致。这种AOP风格的好处在于能够不使用XML来完成功能。 如果你的AOP需求超过了简单的方法调用(如构造器或属性拦截),那么你需要考虑使用AspectJ来实现切面。在这种情况下,上文所示的第 四种类型能够帮助你将值注入到AspectJ驱动的切面中。 我们在将在本章展示更多的Spring AOP技术,但是在开始之前,我们必须要了解Spring AOP框架的一些关键知识。 Spring通知是通知是Java编写的编写的 Spring所创建的通知都是用标准的Java类编写的。这样的话,我们就可以使用与普通Java开发一样的集成开发环境(IDE)来开发切面。而 且,定义通知所应用的切点通常会使用注解或在Spring配置文件里采用XML来编写,这两种语法对于Java开发者来说都是相当熟悉的。 AspectJ与之相反。虽然AspectJ现在支持基于注解的切面,但AspectJ最初是以Java语言扩展的方式实现的。这种方式有优点也有缺点。通过 特有的AOP语言,我们可以获得更强大和细粒度的控制,以及更丰富的AOP工具集,但是我们需要额外学习新的工具和语法。 Spring在运行时通知对象在运行时通知对象 通过在代理类中包裹切面,Spring在运行期把切面织入到Spring管理的bean中。如图4.3所示,代理类封装了目标类,并拦截被通知方法的调 用,再把调用转发给真正的目标bean。当代理拦截到方法调用时,在调用目标bean方法之前,会执行切面逻辑。 图4.3 Spring的切面由包裹了目标对象的代理类实现。 代理类处理方法的调用,执行额外的切面逻辑,并调用目标方法 直到应用需要被代理的bean时,Spring才创建代理对象。如果使用的是ApplicationContext的话, 在ApplicationContext从BeanFactory中加载所有bean的时候,Spring才会创建被代理的对象。因为Spring运行时才创建代理对象,所 以我们不需要特殊的编译器来织入Spring AOP的切面。 Spring只支持方法级别的连接点只支持方法级别的连接点 正如前面所探讨过的,通过使用各种AOP方案可以支持多种连接点模型。因为Spring基于动态代理,所以Spring只支持方法连接点。这与一些 其他的AOP框架是不同的,例如AspectJ和JBoss,除了方法切点,它们还提供了字段和构造器接入点。Spring缺少对字段连接点的支持,无法 让我们创建细粒度的通知,例如拦截对象字段的修改。而且它不支持构造器连接点,我们就无法在bean创建时应用通知。 但是方法拦截可以满足绝大部分的需求。如果需要方法拦截之外的连接点拦截功能,那么我们可以利用Aspect来补充Spring AOP的功能。 对于什么是AOP以及Spring如何支持AOP的,我们现在已经有了一个大致的了解。现在是时候学习如何在Spring中创建切面了,让我们先从 Spring的声明式AOP模型开始。 4.2 通过切点来选择连接点 通过切点来选择连接点 正如之前所提过的,切点用于准确定位应该在什么地方应用切面的通知。通知和切点是切面的最基本元素。因此,了解如何编写切点非常重 要。 在Spring AOP中,要使用AspectJ的切点表达式语言来定义切点。如果你已经很熟悉AspectJ,那么在Spring中定义切点就感觉非常自然。但是 如果你一点都不了解AspectJ的话,本小节我们将快速介绍一下如何编写AspectJ风格的切点。如果你想进一步了解AspectJ和AspectJ切点表达 式语言,我强烈推荐Ramniva Laddad编写的《AspectJ in Action》第二版(Manning,2009,www.manning.com/laddad2/)。 关于Spring AOP的AspectJ切点,最重要的一点就是Spring仅支持AspectJ切点指示器(pointcut designator)的一个子集。让我们回顾 下,Spring是基于代理的,而某些切点表达式是与基于代理的AOP无关的。表4.1列出了Spring AOP所支持的AspectJ切点指示器。 表表4.1  Spring借助借助AspectJ的切点表达式语言来定义的切点表达式语言来定义Spring切面切面 AspectJ指示器指示器 描  述描  述 arg() 限制连接点匹配参数为指定类型的执行方法 @args() 限制连接点匹配参数由指定注解标注的执行方法 execution() 用于匹配是连接点的执行方法 this() 限制连接点匹配AOP代理的bean引用为指定类型的类 target 限制连接点匹配目标对象为指定类型的类 @target() 限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类型的注解 within() 限制连接点匹配指定的类型 @within() 限制连接点匹配指定注解所标注的类型(当使用Spring AOP时,方法定义在由指定的注解所标注的类里) @annotation 限定匹配带有指定注解的连接点 在Spring中尝试使用AspectJ其他指示器时,将会抛出IllegalArgument-Exception异常。 当我们查看如上所展示的这些Spring支持的指示器时,注意只有execution指示器是实际执行匹配的,而其他的指示器都是用来限制匹配 的。这说明execution指示器是我们在编写切点定义时最主要使用的指示器。在此基础上,我们使用其他指示器来限制所匹配的切点。 4.2.1 编写切点 编写切点 为了阐述Spring中的切面,我们需要有个主题来定义切面的切点。为此,我们定义一个Performance接口: Performance可以代表任何类型的现场表演,如舞台剧、电影或音乐会。假设我们想编写Performance的perform()方法触发的通知。 图4.4展现了一个切点表达式,这个表达式能够设置当perform()方法执行时触发通知的调用。 图4.4 使用AspectJ切点表达式来选择Performance的perform()方法 我们使用execution()指示器选择Performance的perform()方法。方法表达式以“*”号开始,表明了我们不关心方法返回值的类型。然 后,我们指定了全限定类名和方法名。对于方法参数列表,我们使用两个点号(..)表明切点要选择任意的perform()方法,无论该方法的 入参是什么。 现在假设我们需要配置的切点仅匹配concert包。在此场景下,可以使用within()指示器来限制匹配,如图4.5所示。 图4.5 使用within()指示器限制切点范围 请注意我们使用了“&&”操作符把execution()和within()指示器连接在一起形成与(and)关系(切点必须匹配所有的指示器)。类似 地,我们可以使用“||”操作符来标识或(or)关系,而使用“!”操作符来标识非(not)操作。 因为“&”在XML中有特殊含义,所以在Spring的XML配置里面描述切点时,我们可以使用and来代替“&&”。同样,or和not可以分别用来代 替“||”和“!”。 4.2.2 在切点中选择 在切点中选择bean 除了表4.1所列的指示器外,Spring还引入了一个新的bean()指示器,它允许我们在切点表达式中使用bean的ID来标识bean。bean()使用 bean ID或bean名称作为参数来限制切点只匹配特定的bean。 例如,考虑如下的切点: 在这里,我们希望在执行Performance的perform()方法时应用通知,但限定bean的ID为woodstock。 在某些场景下,限定切点为指定的bean或许很有意义,但我们还可以使用非操作为除了特定ID以外的其他bean应用通知: 在此场景下,切面的通知会被编织到所有ID不为woodstock的bean中。 现在,我们已经讲解了编写切点的基础知识,让我们再了解一下如何编写通知和使用这些切点声明切面。 4.3 使用注解创建切面 使用注解创建切面 使用注解来创建切面是AspectJ 5所引入的关键特性。AspectJ 5之前,编写AspectJ切面需要学习一种Java语言的扩展,但是AspectJ面向注解 的模型可以非常简便地通过少量注解把任意类转变为切面。 我们已经定义了Performance接口,它是切面中切点的目标对象。现在,让我们使用AspecJ注解来定义切面。 4.3.1 定义切面 定义切面 如果一场演出没有观众的话,那不能称之为演出。对不对?从演出的角度来看,观众是非常重要的,但是对演出本身的功能来讲,它并不是核 心,这是一个单独的关注点。因此,将观众定义为一个切面,并将其应用到演出上就是较为明智的做法。 程序清单4.1展现了Audience类,它定义了我们所需的一个切面。 程序清单程序清单4.1  Audience类:观看演出的切面类:观看演出的切面 Audience类使用@AspectJ注解进行了标注。该注解表明Audience不仅仅是一个POJO,还是一个切面。Audience类中的方法都使用注 解来定义切面的具体行为。 Audience有四个方法,定义了一个观众在观看演出时可能会做的事情。在演出之前,观众要就坐(takeSeats())并将手机调至静音状态 (silenceCellPhones())。如果演出很精彩的话,观众应该会鼓掌喝彩(applause())。不过,如果演出没有达到观众预期的话,观 众会要求退款(demandRefund())。 可以看到,这些方法都使用了通知注解来表明它们应该在什么时候调用。AspectJ提供了五个注解来定义通知,如表4.2所示。 表表4.2  Spring使用使用AspectJ注解来声明通知方法注解来声明通知方法 注  解注  解 通  知通  知 @After 通知方法会在目标方法返回或抛出异常后调用 @AfterReturning 通知方法会在目标方法返回后调用 @AfterThrowing 通知方法会在目标方法抛出异常后调用 @Around 通知方法会将目标方法封装起来 @Before 通知方法会在目标方法调用之前执行 Audience使用到了前面五个注解中的三个。takeSeats()和silence CellPhones()方法都用到了@Before注解,表明它们应该在演出 开始之前调用。applause()方法使用了@AfterReturning注解,它会在演出成功返回后调用。demandRefund()方法上添加 了@AfterThrowing注解,这表明它会在抛出异常以后执行。 你可能已经注意到了,所有的这些注解都给定了一个切点表达式作为它的值,同时,这四个方法的切点表达式都是相同的。其实,它们可以设 置成不同的切点表达式,但是在这里,这个切点表达式就能满足所有通知方法的需求。让我们近距离看一下这个设置给通知注解的切点表达 式,我们发现它会在Performance的perform()方法执行时触发。 相同的切点表达式我们重复了四遍,这可真不是什么光彩的事情。这样的重复让人感觉有些不对劲。如果我们只定义这个切点一次,然后每次 需要的时候引用它,那么这会是一个很好的方案。 幸好,我们完全可以这样做:@Pointcut注解能够在一个@AspectJ切面内定义可重用的切点。接下来的程序清单4.2展现了新 的Audience,现在它使用了@Pointcut。 程序清单程序清单4.2 通过 通过@Pointcut注解声明频繁使用的切点表达式注解声明频繁使用的切点表达式 在Audience中,performance()方法使用了@Pointcut注解。为@Pointcut注解设置的值是一个切点表达式,就像之前在通知注解上 所设置的那样。通过在performance()方法上添加@Pointcut注解,我们实际上扩展了切点表达式语言,这样就可以在任何的切点表达式 中使用performance()了,如果不这样做的话,你需要在这些地方使用那个更长的切点表达式。我们现在把所有通知注解中的长表达式都替 换成了performance()。 performance()方法的实际内容并不重要,在这里它实际上应该是空的。其实该方法本身只是一个标识,供@Pointcut注解依附。 需要注意的是,除了注解和没有实际操作的performance()方法,Audience类依然是一个POJO。我们能够像使用其他的Java类那样调用 它的方法,它的方法也能够独立地进行单元测试,这与其他的Java类并没有什么区别。Audience只是一个Java类,只不过它通过注解表明会 作为切面使用而已。 像其他的Java类一样,它可以装配为Spring中的bean: 如果你就此止步的话,Audience只会是Spring容器中的一个bean。即便使用了AspectJ注解,但它并不会被视为切面,这些注解不会解析, 也不会创建将其转换为切面的代理。 如果你使用JavaConfig的话,可以在配置类的类级别上通过使用EnableAspectJ-AutoProxy注解启用自动代理功能。程序清单4.3展现了 如何在JavaConfig中启用自动代理。 程序清单程序清单4.3 在 在JavaConfig中启用中启用AspectJ注解的自动代理注解的自动代理 假如你在Spring中要使用XML来装配bean的话,那么需要使用Spring aop命名空间中的元素。下面的XML配 置展现了如何完成该功能。 程序清单程序清单4.4 在 在XML中,通过中,通过Spring的的aop命名空间启用命名空间启用AspectJ自动代理自动代理 不管你是使用JavaConfig还是XML,AspectJ自动代理都会为使用@Aspect注解的bean创建一个代理,这个代理会围绕着所有该切面的切点所 匹配的bean。在这种情况下,将会为Concertbean创建一个代理,Audience类中的通知方法将会在perform()调用前后执行。 我们需要记住的是,Spring的AspectJ自动代理仅仅使用@AspectJ作为创建切面的指导,切面依然是基于代理的。在本质上,它依然是Spring 基于代理的切面。这一点非常重要,因为这意味着尽管使用的是@AspectJ注解,但我们仍然限于代理方法的调用。如果想利用AspectJ的所 有能力,我们必须在运行时使用AspectJ并且不依赖Spring来创建基于代理的切面。 到现在为止,我们的切面在定义时,使用了不同的通知方法来实现前置通知和后置通知。但是表4.2还提到了另外的一种通知:环绕通知 (around advice)。环绕通知与其他类型的通知有所不同,因此值得花点时间来介绍如何进行编写。 4.3.2 创建环绕通知 创建环绕通知 环绕通知是最为强大的通知类型。它能够让你所编写的逻辑将被通知的目标方法完全包装起来。实际上就像在一个通知方法中同时编写前置通 知和后置通知。 为了阐述环绕通知,我们重写Audience切面。这次,我们使用一个环绕通知来代替之前多个不同的前置通知和后置通知。 程序清单程序清单4.5 使用环绕通知重新实现 使用环绕通知重新实现Audience切面切面 在这里,@Around注解表明watchPerformance()方法会作为performance()切点的环绕通知。在这个通知中,观众在演出之前会将手 机调至静音并就坐,演出结束后会鼓掌喝彩。像前面一样,如果演出失败的话,观众会要求退款。 可以看到,这个通知所达到的效果与之前的前置通知和后置通知是一样的。但是,现在它们位于同一个方法中,不像之前那样分散在四个不同 的通知方法里面。 关于这个新的通知方法,你首先注意到的可能是它接受ProceedingJoinPoint作为参数。这个对象是必须要有的,因为你要在通知中通过 它来调用被通知的方法。通知方法中可以做任何的事情,当要将控制权交给被通知的方法时,它需要调 用ProceedingJoinPoint的proceed()方法。 需要注意的是,别忘记调用proceed()方法。如果不调这个方法的话,那么你的通知实际上会阻塞对被通知方法的调用。有可能这就是你想 要的效果,但更多的情况是你希望在某个点上执行被通知的方法。 有意思的是,你可以不调用proceed()方法,从而阻塞对被通知方法的访问,与之类似,你也可以在通知中对它进行多次调用。要这样做的 一个场景就是实现重试逻辑,也就是在被通知方法失败后,进行重复尝试。 4.3.3 处理通知中的参数 处理通知中的参数 到目前为止,我们的切面都很简单,没有任何参数。唯一的例外是我们为环绕通知所编写的watchPerformance()示例方法中使用 了ProceedingJoinPoint作为参数。除了环绕通知,我们编写的其他通知不需要关注传递给被通知方法的任意参数。这很正常,因为我们 所通知的perform()方法本身没有任何参数。 但是,如果切面所通知的方法确实有参数该怎么办呢?切面能访问和使用传递给被通知方法的参数吗? 为了阐述这个问题,让我们重新看一下2.4.4小节中的BlankDisc样例。play()方法会循环所有的磁道并调用playTrack()方法。但是,我们 也可以通过playTrack()方法直接播放某一个磁道中的歌曲。 假设你想记录每个磁道被播放的次数。一种方法就是修改playTrack()方法,直接在每次调用的时候记录这个数量。但是,记录磁道的播放 次数与播放本身是不同的关注点,因此不应该属于playTrack()方法。看起来,这应该是切面要完成的任务。 为了记录每个磁道所播放的次数,我们创建了TrackCounter类,它是通知playTrack()方法的一个切面。下面的程序清单展示了这个切 面。 程序清单程序清单4.6 使用参数化的通知来记录磁道播放的次数 使用参数化的通知来记录磁道播放的次数 像之前所创建的切面一样,这个切面使用@Pointcut注解定义命名的切点,并使用@Before将一个方法声明为前置通知。但是,这里的不同 点在于切点还声明了要提供给通知方法的参数。图4.6将切点表达式进行了分解,以展现参数是在什么地方指定的。 图4.6 在切点表达式中声明参数,这个参数传入到通知方法中 在图4.6中需要关注的是切点表达式中的args(trackNumber)限定符。它表明传递给playTrack()方法的int类型参数也会传递到通知中 去。参数的名称trackNumber也与切点方法签名中的参数相匹配。 这个参数会传递到通知方法中,这个通知方法是通过@Before注解和命名切点trackPlayed(trackNumber)定义的。切点定义中的参数 与切点方法中的参数名称是一样的,这样就完成了从命名切点到通知方法的参数转移。 现在,我们可以在Spring配置中将BlankDisc和TrackCounter定义为bean,并启用AspectJ自动代理,如程序清单4.7所示。 程序清单程序清单4.7 配置 配置TrackCount记录每个磁道播放的次数记录每个磁道播放的次数 最后,为了证明它能正常工作,你可以编写如下的简单测试。它会播放几个磁道并通过TrackCounter断言播放的数量。 程序清单程序清单4.8 测试 测试TrackCounter切面切面 到目前为止,在我们所使用的切面中,所包装的都是被通知对象的已有方法。但是,方法包装仅仅是切面所能实现的功能之一。让我们看一下 如何通过编写切面,为被通知的对象引入全新的功能。 4.3.4 通过注解引入新功能 通过注解引入新功能 一些编程语言,例如Ruby和Groovy,有开放类的理念。它们可以不用直接修改对象或类的定义就能够为对象或类增加新的方法。不过,Java并 不是动态语言。一旦类编译完成了,我们就很难再为该类添加新的功能了。 但是如果仔细想想,我们在本章中不是一直在使用切面这样做吗?当然,我们还没有为对象增加任何新的方法,但是已经为对象拥有的方法添 加了新功能。如果切面能够为现有的方法增加额外的功能,为什么不能为一个对象增加新的方法呢?实际上,利用被称为引入的AOP概念,切 面可以为Spring bean添加新方法。 回顾一下,在Spring中,切面只是实现了它们所包装bean相同接口的代理。如果除了实现这些接口,代理也能暴露新接口的话,会怎么样呢? 那样的话,切面所通知的bean看起来像是实现了新的接口,即便底层实现类并没有实现这些接口也无所谓。图4.7展示了它们是如何工作的。 图4.7 使用Spring AOP,我们可以为bean引入新的方法。 代理拦截调用并委托给实现该方法的其他对象 我们需要注意的是,当引入接口的方法被调用时,代理会把此调用委托给实现了新接口的某个其他对象。实际上,一个bean的实现被拆分到了 多个类中。 为了验证该主意能行得通,我们为示例中的所有的Performance实现引入下面的Encoreable接口: 暂且先不管Encoreable是不是一个真正存在的单词[1],我们需要有一种方式将这个接口应用到Performance实现中。我们现在假设你能够 访问Performance的所有实现,并对其进行修改,让它们都实现Encoreable接口。但是,从设计的角度来看,这并不是最好的做法,并不 是所有的Performance都是具有Encoreable特性的。另外一方面,有可能无法修改所有的Performance实现,当使用第三方实现并且没 有源码的时候更是如此。 值得庆幸的是,借助于AOP的引入功能,我们可以不必在设计上妥协或者侵入性地改变现有的实现。为了实现该功能,我们要创建一个新的切 面: 可以看到,EncoreableIntroducer是一个切面。但是,它与我们之前所创建的切面不同,它并没有提供前置、后置或环绕通知,而是通 过@DeclareParents注解,将Encoreable接口引入到Performance bean中。 @DeclareParents注解由三部分组成: value属性指定了哪种类型的bean要引入该接口。在本例中,也就是所有实现Performance的类型。(标记符后面的加号表示 是Performance的所有子类型,而不是Performance本身。) defaultImpl属性指定了为引入功能提供实现的类。在这里,我们指定的是DefaultEncoreable提供实现。 @DeclareParents注解所标注的静态属性指明了要引入了接口。在这里,我们所引入的是Encoreable接口。 和其他的切面一样,我们需要在Spring应用中将EncoreableIntroducer声明为一个bean: Spring的自动代理机制将会获取到它的声明,当Spring发现一个bean使用了@Aspect注解时,Spring就会创建一个代理,然后将调用委托给被 代理的bean或被引入的实现,这取决于调用的方法属于被代理的bean还是属于被引入的接口。 在Spring中,注解和自动代理提供了一种很便利的方式来创建切面。它非常简单,并且只涉及到最少的Spring配置。但是,面向注解的切面声 明有一个明显的劣势:你必须能够为通知类添加注解。为了做到这一点,必须要有源码。 如果你没有源码的话,或者不想将AspectJ注解放到你的代码之中,Spring为切面提供了另外一种可选方案。让我们看一下如何在Spring XML 配置文件中声明切面。 4.4 在 在XML中声明切面中声明切面 在本书前面的内容中,我曾经建立过这样一种原则,那就是基于注解的配置要优于基于Java的配置,基于Java的配置要优于基于XML的配置。 但是,如果你需要声明切面,但是又不能为通知类添加注解的时候,那么就必须转向XML配置了。 在Spring的aop命名空间中,提供了多个元素用来在XML中声明切面,如表4.3所示。 表表4.3  Spring的的AOP配置元素能够以非侵入性的方式声明切面配置元素能够以非侵入性的方式声明切面 AOP配置元素配置元素 用  途用  途 定义AOP通知器 定义AOP后置通知(不管被通知的方法是否执行成功) 定义AOP返回通知 定义AOP异常通知 定义AOP环绕通知 定义一个切面 启用@AspectJ注解驱动的切面 定义一个AOP前置通知 顶层的AOP配置元素。大多数的元素必须包含在元素内 以透明的方式为被通知的对象引入额外的接口 定义一个切点 我们已经看过了元素,它能够自动代理AspectJ注解的通知类。aop命名空间的其他元素能够让我们直接在 Spring配置中声明切面,而不需要使用注解。 例如,我们重新看一下Audience类,这一次我们将它所有的AspectJ注解全部移除掉: 正如你所看到的,Audience类并没有任何特别之处,它就是有几个方法的简单Java类。我们可以像其他类一样把它注册为Spring应用上下文 中的bean。 尽管看起来并没有什么差别,但Audience已经具备了成为AOP通知的所有条件。我们再稍微帮助它一把,它就能够成为预期的通知了。 4.4.1 声明前置和后置通知 声明前置和后置通知 你可以再把那些AspectJ注解加回来,但这并不是本节的目的。相反,我们会使用Spring aop命名空间中的一些元素,将没有注解 的Audience类转换为切面。下面的程序清单4.9展示了所需要的XML。 程序清单程序清单4.9 通过 通过XML将无注解的将无注解的Audience声明为切面声明为切面 关于Spring AOP配置元素,第一个需要注意的事项是大多数的AOP配置元素必须在元素的上下文内使用。这条规则有几种例 外场景,但是把bean声明为一个切面时,我们总是从元素开始配置的。 在元素内,我们可以声明一个或多个通知器、切面或者切点。在程序清单4.9中,我们使用元素声明了一个 简单的切面。ref元素引用了一个POJO bean,该bean实现了切面的功能——在这里就是audience。ref元素所引用的bean提供了在切面中 通知所调用的方法。 该切面应用了四个不同的通知。两个元素定义了匹配切点的方法执行之前调用前置通知方法—也就是Audience bean的takeSeats()和turnOffCellPhones()方法(由method属性所声明)。元素定义了一个返回 (after-returning)通知,在切点所匹配的方法调用之后再调用applaud()方法。同样,元素定义了异常 (after-throwing)通知,如果所匹配的方法执行时抛出任何的异常,都将会调用demandRefund()方法。图4.8展示了通知逻辑如何织 入到业务逻辑中。 图4.8 Audience切面包含四种通知,它们把通知逻辑织入进匹配切面切点的方法中 在所有的通知元素中,pointcut属性定义了通知所应用的切点,它的值是使用AspectJ切点表达式语法所定义的切点。 你或许注意到所有通知元素中的pointcut属性的值都是一样的,这是因为所有的通知都要应用到相同的切点上。 在基于AspectJ注解的通知中,当发现这种类型的重复时,我们使用@Pointcut注解消除了这些重复的内容。而在基于XML的切面声明中,我 们需要使用元素。如下的XML展现了如何将通用的切点表达式抽取到一个切点声明中,这样这个声明就能在所有的通知元 素中使用了。 程序清单程序清单4.10 使用 使用定义命名切点定义命名切点 现在切点是在一个地方定义的,并且被多个通知元素所引用。元素定义了一个id为performance的切点。同时修改所有 的通知元素,用pointcut-ref属性来引用这个命名切点。 正如程序清单4.10所展示的,元素所定义的切点可以被同一个元素之内的所有通知元素引用。如果想让定 义的切点能够在多个切面使用,我们可以把元素放在元素的范围内。 4.4.2 声明环绕通知 声明环绕通知 目前Audience的实现工作得非常棒,但是前置通知和后置通知有一些限制。具体来说,如果不使用成员变量存储信息的话,在前置通知和后 置通知之间共享信息非常麻烦。 例如,假设除了进场关闭手机和表演结束后鼓掌,我们还希望观众确保一直关注演出,并报告每个参赛者表演了多长时间。使用前置通知和后 置通知实现该功能的唯一方式是在前置通知中记录开始时间并在某个后置通知中报告表演耗费的时间。但这样的话我们必须在一个成员变量中 保存开始时间。因为Audience是单例的,如果像这样保存状态的话,将会存在线程安全问题。 相对于前置通知和后置通知,环绕通知在这点上有明显的优势。使用环绕通知,我们可以完成前置通知和后置通知所实现的相同功能,而且只 需要在一个方法中 实现。因为整个通知逻辑是在一个方法内实现的,所以不需要使用成员变量保存 状态。 例如,考虑程序清单4.11中新Audience类的watchPerformance()方法,它没有使用任何的注解。 程序清单程序清单4.11  watchPerformance()方法提供了方法提供了AOP环绕通知环绕通知 在观众切面中,watchPerformance()方法包含了之前四个通知方法的所有功能。不过,所有的功能都放在了这一个方法中,因此这个方法 还要负责自身的异常处理。 声明环绕通知与声明其他类型的通知并没有太大区别。我们所需要做的仅仅是使用元素。 程序清单程序清单4.12 在 在XML中使用中使用元素声明环绕通知元素声明环绕通知 像其他通知的XML元素一样,指定了一个切点和一个通知方法的名字。在这里,我们使用跟之前一样的切点,但是为该切点 所设置的method属性值为watchPerformance()方法。 4.4.3 为通知传递参数 为通知传递参数 在4.3.3小节中,我们使用@AspectJ注解创建了一个切面,这个切面能够记录CompactDisc上每个磁道播放的次数。现在,我们使用XML来 配置切面,那就看一下如何完成这一相同的任务。 首先,我们要移除掉TrackCounter上所有的@AspectJ注解。 程序清单程序清单4.13 无注解的 无注解的TrackCounter 去掉@AspectJ注解后,TrackCounter显得有些单薄了。现在,除非显式调用countTrack()方法,否则TrackCounter不会记录磁道 播放的数量。但是,借助一点Spring XML配置,我们能够让TrackCounter重新变为切面。 如下的程序清单展现了完整的Spring配置,在这个配置中声明了TrackCounter bean和BlankDisc bean,并将TrackCounter转化为切 面。 程序清单程序清单4.14 在 在XML中将中将TrackCounter配置为参数化的切面配置为参数化的切面 可以看到,我们使用了和前面相同的aop命名空间XML元素,它们会将POJO声明为切面。唯一明显的差别在于切点表达式中包含了一个参 数,这个参数会传递到通知方法中。如果你将这个表达式与程序清单4.6中的表达式进行对比会发现它们几乎是相同的。唯一的差别在于这里 使用and关键字而不是“&&”(因为在XML中,“&”符号会被解析为实体的开始)。 我们通过练习已经使用Spring的aop命名空间声明了几个基本的切面,那么现在让我们看一下如何使用aop命名空间声明引入切面。 4.4.4 通过切面引入新的功能 通过切面引入新的功能 在前面的4.3.4小节中,我向你展现了如何借助AspectJ的@DeclareParents注解为被通知的方法神奇地引入新的方法。但是AOP引入并不是 AspectJ特有的。使用Spring aop命名空间中的元素,我们可以实现相同的功能。 如下的XML代码片段与之前基于AspectJ的引入功能是相同: 顾名思义,声明了此切面所通知的bean要在它的对象层次结构中拥有新的父类型。具体到本例中,类型匹 配Performance接口(由types-matching属性指定)的那些bean在父类结构中会增加Encoreable接口(由implement- interface属性指定)。最后要解决的问题是Encoreable接口中的方法实现要来自于何处。 这里有两种方式标识所引入接口的实现。在本例中,我们使用default-impl属性用全限定类名来显式指定Encoreable的实现。或者,我 们还可以使用delegate-ref属性来标识。 delegate-ref属性引用了一个Spring bean作为引入的委托。这需要在Spring上下文中存在一个ID为encoreableDelegate的bean。 使用default-impl来直接标识委托和间接使用delegate-ref的区别在于后者是Spring bean,它本身可以被注入、通知或使用其他的 Spring配置。 4.5 注入 注入AspectJ切面切面 虽然Spring AOP能够满足许多应用的切面需求,但是与AspectJ相比,Spring AOP 是一个功能比较弱的AOP解决方案。AspectJ提供了Spring AOP所不能支持的许多类型的切点。 例如,当我们需要在创建对象时应用通知,构造器切点就非常方便。不像某些其他面向对象语言中的构造器,Java构造器不同于其他的正常方 法。这使得Spring基于代理的AOP无法把通知应用于对象的创建过程。 对于大部分功能来讲,AspectJ切面与Spring是相互独立的。虽然它们可以织入到任意的Java应用中,这也包括了Spring应用,但是在应用 AspectJ切面时几乎不会涉及到Spring。 但是精心设计且有意义的切面很可能依赖其他类来完成它们的工作。如果在执行通知时,切面依赖于一个或多个类,我们可以在切面内部实例 化这些协作的对象。但更好的方式是,我们可以借助Spring的依赖注入把bean装配进AspectJ切面中。 为了演示,我们为上面的演出创建一个新切面。具体来讲,我们以切面的方式创建一个评论员的角色,他会观看演出并且会在演出之后提供一 些批评意见。下面的CriticAspect就是一个这样的切面。 程序清单程序清单4.15 使用 使用AspectJ实现表演的评论员实现表演的评论员 CriticAspect的主要职责是在表演结束后为表演发表评论。程序清单4.15中的performance()切点匹配perform()方法。当它 与afterReturning()通知一起配合使用时,我们可以让该切面在表演结束时起作用。 程序清单4.15有趣的地方在于并不是评论员自己发表评论,实际上,CriticAspect与一个CriticismEngine对象相协作,在表演结束 时,调用该对象的getCriticism()方法来发表一个苛刻的评论。为了避免CriticAspect和CriticismEngine之间产生不必要的耦 合,我们通过Setter依赖注入为CriticAspect设置CriticismEngine。图4.9展示了此关系。 图4.9 切面也需要注入。像其他的bean一样, Spring可以为AspectJ切面注入依赖 CriticismEngine自身是声明了一个简单getCriticism()方法的接口。程序清单4.16为CriticismEngine的实现。 程序清单程序清单4.16 要注入到 要注入到CriticAspect中的中的CriticismEngine实现实现 CriticismEngineImpl实现了CriticismEngine接口,通过从注入的评论池中随机选择一个苛刻的评论。这个类可以使用如下的XML声 明为一个Spring bean。 到目前为止,一切顺利。我们现在有了一个要赋予CriticAspect的Criticism-Engine实现。剩下的就是为CriticAspect装 配CriticismEngineImple。 在展示如何实现注入之前,我们必须清楚AspectJ切面根本不需要Spring就可以织入到我们的应用中。如果想使用Spring的依赖注入为AspectJ 切面注入协作者,那我们就需要在Spring配置中把切面声明为一个Spring配置中的。如下的声明会 把criticismEnginebean注入到CriticAspect中: 很大程度上,的声明与我们在Spring中所看到的其他配置并没有太多的区别,但是最大的不同在于使用了factory- method属性。通常情况下,Spring bean由Spring容器初始化,但是AspectJ切面是由AspectJ在运行期创建的。等到Spring有机会 为CriticAspect注入CriticismEngine时,CriticAspect已经被实例化了。 因为Spring不能负责创建CriticAspect,那就不能在 Spring中简单地把CriticAspect声明为一个bean。相反,我们需要一种方式为 Spring获得已经由AspectJ创建的CriticAspect实例的句柄,从而可以注入CriticismEngine。幸好,所有的AspectJ切面都提供了一个 静态的aspectOf()方法,该方法返回切面的一个单例。所以为了获得切面的实例,我们必须使用factory-method来调用asepctOf()方 法而不是调用CriticAspect的构造器方法。 简而言之,Spring不能像之前那样使用声明来创建一个CriticAspect实例——它已经在运行时由AspectJ创建完成了。Spring需要 通过aspectOf()工厂方法获得切面的引用,然后像元素规定的那样在该对象上执行依赖注入。 4.6 小结 小结 AOP是面向对象编程的一个强大补充。通过AspectJ,我们现在可以把之前分散在应用各处的行为放入可重用的模块中。我们显示地声明在何 处如何应用该行为。这有效减少了代码冗余,并让我们的类关注自身的主要功能。 Spring提供了一个AOP框架,让我们把切面插入到方法执行的周围。现在我们已经学会如何把通知织入前置、后置和环绕方法的调用中,以及 为处理异常增加自定义的行为。 关于在Spring应用中如何使用切面,我们可以有多种选择。通过使用@AspectJ注解和简化的配置命名空间,在Spring中装配通知和切点变得 非常简单。 最后,当Spring AOP不能满足需求时,我们必须转向更为强大的AspectJ。对于这些场景,我们了解了如何使用Spring为AspectJ切面注入依 赖。 此时此刻,我们已经覆盖了Spring框架的基础知识,了解到如何配置Spring容器以及如何为Spring管理的对象应用切面。正如我们所看到的, 这些核心技术为创建松散耦合的应用奠定了坚实的基础。 现在,我们越过这些基础的内容,看一下如何使用Spring构建真实的应用。从下一章开始,首先看到的是如何使用Spring构建Web应用。 [1]对应的英文单词词根为encore,指的是演唱会演出结束后应观众要求进行返场表演。——译者注 第第2部分 部分 Web中的中的Spring Spring通常用来开发Web应用。因此,在第2部分中,将会看到如何使用Spring的MVC框架为应用程序添加Web前端。 在第5章“构建Spring Web应用”中,你将会学习到Spring MVC的基本用法,它是构建在Spring理念之上的一个Web框架。我们将会看到如何编 写处理Web请求的控制器以及如何透明地绑定请求参数和负载到业务对象上,同时它还提供了数据检验和错误处理的功能。 在第6章“渲染Web视图”中,将会基于第5章的内容继续讲解,展现了如何得到Spring MVC控制器所生成的模型数据,并将其渲染为用户浏览器 中的HTML。这一章的讨论包括JavaServer Pages(JSP)、Apache Tiles和Thymeleaf模板。 在第7章“Spring MVC的高级技术”中,将会学习到构建Web应用时的一些高级技术,包括自定义Spring MVC配置、处理multipart文件上传、处 理异常以及使用flash属性跨请求传递数据。 第8章,“使用Spring Web Flow”将会为你展示如何使用Spring Web Flow来构建会话式、基于流程的Web应用程序。 鉴于安全是很多应用程序的重要关注点,因此第9章“保护Web应用”将会为你介绍如何使用Spring Security来为Web应用程序提供安全性,保护 应用中的信息。 第第5章 构建章 构建Spring Web应用程序应用程序 本章内容:本章内容: 映射请求到Spring控制器 透明地绑定表单参数 校验表单提交 作为企业级Java开发者,你可能开发过一些基于Web的应用程序。对于很多Java开发人员来说,基于Web的应用程序是他们主要的关注点。如 果你有这方面经验的话,你会意识到这种系统所面临的挑战。具体来讲,状态管理、工作流以及验证都是需要解决的重要特性。HTTP协议的 无状态性决定了这些问题都不那么容易解决。 Spring的Web框架就是为了帮你解决这些关注点而设计的。Spring MVC基于模型-视图-控制器(Model-View-Controller,MVC)模式实现,它 能够帮你构建像Spring框架那样灵活和松耦合的Web应用程序。 在本章中,我们将会介绍Spring MVC Web框架,并使用新的Spring MVC注解来构建处理各种Web请求、参数和表单输入的控制器。在深入介 绍Spring MVC之前,让我们先总体上介绍一下Spring MVC,并建立起Spring MVC运行的基本 配置。 5.1  Spring MVC起步起步 你见到过孩子们的捕鼠器游戏吗?这真是一个疯狂的游戏,它的目标是发送一个小钢球,让它经过一系列稀奇古怪的装置,最后触发捕鼠器。 小钢球穿过各种复杂的配件,从一个斜坡上滚下来,被跷跷板弹起,绕过一个微型摩天轮,然后被橡胶靴从桶中踢出去。经过这些后,小钢球 会对那只可怜又无辜的橡胶老鼠进行捕获。 乍看上去,你会认为Spring MVC框架与捕鼠器有些类似。Spring将请求在调度Servlet、处理器映射(handler mapping)、控制器以及视图解 析器(view resolver)之间移动,而捕鼠器中的钢球则会在各种斜坡、跷跷板以及摩天轮之间滚动。但是,不要将Spring MVC与Rube Goldberg-esque捕鼠器游戏做过多比较。每一个Spring MVC中的组件都有特定的目的,并且它也没有那么复杂。 让我们看一下请求是如何从客户端发起,经过Spring MVC中的组件,最终再返回到客户端的。 5.1.1 跟踪 跟踪Spring MVC的请求的请求 每当用户在Web浏览器中点击链接或提交表单的时候,请求就开始工作了。对请求的工作描述就像是快递投送员。与邮局投递员或FedEx投送 员一样,请求会将信息从一个地方带到另一个地方。 请求是一个十分繁忙的家伙。从离开浏览器开始到获取响应返回,它会经历好多站,在每站都会留下一些信息同时也会带上其他信息。图5.1 展示了请求使用Spring MVC所经历的所有站点。 图5.1 一路上请求会将信息带到很多站点,并生产期望的结果 在请求离开浏览器时 ,会带有用户所请求内容的信息,至少会包含请求的URL。但是还可能带有其他的信息,例如用户提交的表单信息。 请求旅程的第一站是Spring的DispatcherServlet。与大多数基于Java的Web框架一样,Spring MVC所有的请求都会通过一个前端控制器 (front controller)Servlet。前端控制器是常用的Web应用程序模式,在这里一个单实例的Servlet将请求委托给应用程序的其他组件来执行实际 的处理。在Spring MVC中,DispatcherServlet就是前端控制器。 DispatcherServlet的任务是将请求发送给Spring MVC控制器(controller)。控制器是一个用于处理请求的Spring组件。在典型的应用程 序中可能会有多个控制器,DispatcherServlet需要知道应该将请求发送给哪个控制器。所以DispatcherServlet以会查询一个或多个 处理器映射(handler mapping) 来确定请求的下一站在哪里。处理器映射会根据请求所携带的URL信息来进行决策。 一旦选择了合适的控制器,DispatcherServlet会将请求发送给选中的控制器 。到了控制器,请求会卸下其负载(用户提交的信息)并 耐心等待控制器处理这些信息。(实际上,设计良好的控制器本身只处理很少甚至不处理工作,而是将业务逻辑委托给一个或多个服务对象进 行处理。) 控制器在完成逻辑处理后,通常会产生一些信息,这些信息需要返回给用户并在浏览器上显示。这些信息被称为模型(model)。不过仅仅给 用户返回原始的信息是不够的——这些信息需要以用户友好的方式进行格式化,一般会是HTML。所以,信息需要发送给一个视图(view), 通常会是JSP。 控制器所做的最后一件事就是将模型数据打包,并且标示出用于渲染输出的视图名。它接下来会将请求连同模型和视图名发送 回DispatcherServlet 。 这样,控制器就不会与特定的视图相耦合,传递给DispatcherServlet的视图名并不直接表示某个特定的JSP。实际上,它甚至并不能确 这样,控制器就不会与特定的视图相耦合,传递给DispatcherServlet的视图名并不直接表示某个特定的JSP。实际上,它甚至并不能确 定视图就是JSP。相反,它仅仅传递了一个逻辑名称,这个名字将会用来查找产生结果的真正视图。DispatcherServlet将会使用视图解 析器(view resolver) 来将逻辑视图名匹配为一个特定的视图实现,它可能是也可能不是JSP。 既然DispatcherServlet已经知道由哪个视图渲染结果,那请求的任务基本上也就完成了。它的最后一站是视图的实现(可能是JSP) , 在这里它交付模型数据。请求的任务就完成了。视图将使用模型数据渲染输出,这个输出会通过响应对象传递给客户端(不会像听上去那样硬 编码) 。 可以看到,请求要经过很多的步骤,最终才能形成返回给客户端的响应。大多数的步骤都是在Spring框架内部完成的,也就是图5.1所示的组件 中。尽管本章的主要内容都关注于如何编写控制器,但在此之前我们首先看一下如何搭建Spring MVC的基础组件。 5.1.2 搭建 搭建Spring MVC 基于图5.1,看上去我们需要配置很多的组成部分。幸好,借助于最近几个Spring新版本的功能增强,开始使用Spring MVC变得非常简单了。 现在,我们要使用最简单的方式来配置Spring MVC:所要实现的功能仅限于运行我们所创建的控制器。在第7章中,我们会看一些其他的配置 选项。 配置配置DispatcherServlet DispatcherServlet是Spring MVC的核心。在这里请求会第一次接触到框架,它要负责将请求路由到其他的组件之中。 按照传统的方式,像DispatcherServlet这样的Servlet会配置在web.xml文件中,这个文件会放到应用的WAR包里面。当然,这是配 置DispatcherServlet的方法之一。但是,借助于Servlet 3规范和Spring 3.1的功能增强,这种方式已经不是唯一的方案了,这也不是我们 本章所使用的配置方法。 我们会使用Java将DispatcherServlet配置在Servlet容器中,而不会再使用web.xml文件。如下的程序清单展示了所需的Java类。 程序清单程序清单5.1 配置 配置DispatcherServlet 在我们深入介绍程序清单5.1之前,你可能想知道spittr到底是什么意思。这个类的名字是SpittrWebAppInitializer,它位于名为 spittr.config的包中。我稍后会对其进行介绍(在5.1.3小节中),但现在,你只需要知道我们所要创建的应用名为Spittr。 要理解程序清单5.1是如何工作的,我们可能只需要知道扩展AbstractAnnotation-ConfigDispatcherServletInitializer的任 意类都会自动地配置Dispatcher-Servlet和Spring应用上下文,Spring的应用上下文会位于应用程序的Servlet上下文之中。 AbstractAnnotationConfigDispatcherServletInitializer剖析剖析 如果你坚持要了解更多细节的话,那就看这里吧。在Servlet 3.0环境中,容器会在类路径中查找实 现javax.servlet.ServletContainerInitializer接口的类,如果能发现的话,就会用它来配置Servlet容器。 Spring提供了这个接口的实现,名为SpringServletContainerInitializer,这个类反过来又会查找实 现WebApplicationInitializer的类并将配置的任务交给它们来完成。Spring 3.2引入了一个便利 的WebApplicationInitializer基础实现,也就是AbstractAnnotationConfigDispatcherServletInitializer。因为我们 的Spittr-WebAppInitializer扩展了AbstractAnnotationConfig DispatcherServlet-Initializer(同时也就实现 了WebApplicationInitializer),因此当部署到Servlet 3.0容器中的时候,容器会自动发现它,并用它来配置Servlet上下文。 尽管它的名字很长,但是AbstractAnnotationConfigDispatcherServlet-Initializer使用起来很简便。在程序清单5.1 中,SpittrWebAppInitializer重写了三个方法。 第一个方法是getServletMappings(),它会将一个或多个路径映射到DispatcherServlet上。在本例中,它映射的是“/”,这表示它会 是应用的默认Servlet。它会处理进入应用的所有请求。 为了理解其他的两个方法,我们首先要理解DispatcherServlet和一个Servlet监听器(也就是ContextLoaderListener)的关系。 两个应用上下文之间的故事两个应用上下文之间的故事 当DispatcherServlet启动的时候,它会创建Spring应用上下文,并加载配置文件或配置类中所声明的bean。在程序清单5.1 的getServletConfigClasses()方法中,我们要求DispatcherServlet加载应用上下文时,使用定义在WebConfig配置类(使用 Java配置)中的bean。 但是在Spring Web应用中,通常还会有另外一个应用上下文。另外的这个应用上下文是由ContextLoaderListener创建的。 我们希望DispatcherServlet加载包含Web组件的bean,如控制器、视图解析器以及处理器映射,而ContextLoaderListener要加载 应用中的其他bean。这些bean通常是驱动应用后端的中间层和数据层组件。 实际上,AbstractAnnotationConfigDispatcherServletInitializer会同时创 建DispatcherServlet和ContextLoaderListener。GetServlet-ConfigClasses()方法返回的带有@Configuration注解的 类将会用来定义DispatcherServlet应用上下文中的bean。getRootConfigClasses()方法返回的带有@Configuration注解的类将 会用来配置ContextLoaderListener创建的应用上下文中的bean。 在本例中,根配置定义在RootConfig中,DispatcherServlet的配置声明在WebConfig中。稍后我们将会看到这两个类的内容。 需要注意的是,通过AbstractAnnotationConfigDispatcherServlet-Initializer来配置DispatcherServlet是传统web.xml 方式的替代方案。如果你愿意的话,可以同时包含web.xml和AbstractAnnotationConfigDispatcher-ServletInitializer,但 这其实并没有必要。 如果按照这种方式配置DispatcherServlet,而不是使用web.xml的话,那唯一问题在于它只能部署到支持Servlet 3.0的服务器中才能正常 工作,如Tomcat 7或更高版本。Servlet 3.0规范在2009年12月份就发布了,因此很有可能你会将应用部署到支持Servlet 3.0的Servlet容器之 中。 如果你还没有使用支持Servlet 3.0的服务器,那么在AbstractAnnotation-ConfigDispatcherServletInitializer子类中配 置DispatcherServlet的方法就不适合你了。你别无选择,只能使用web.xml了。我们将会在第7章学习web.xml和其他配置选项。但现在, 我们先看一下程序清单5.1中所引用的WebConfig和RootConfig,了解一下如何启用Spring MVC。 启用启用Spring MVC 我们有多种方式来配置DispatcherServlet,与之类似,启用Spring MVC组件的方法也不仅一种。以前,Spring是使用XML进行配置的, 你可以使用启用注解驱动的Spring MVC。 我们会在第7章讨论Spring MVC配置可选项的时候,再讨论。不过,现在我们会让Spring MVC的搭建过程尽 可能简单并基于Java进行配置。 我们所能创建的最简单的Spring MVC配置就是一个带有@EnableWebMvc注解的类: 这可以运行起来,它的确能够启用Spring MVC,但还有不少问题要解决: 没有配置视图解析器。如果这样的话,Spring默认会使用BeanNameView-Resolver,这个视图解析器会查找ID与视图名称匹配的 bean,并且查找的bean要实现View接口,它以这样的方式来解析视图。 没有启用组件扫描。这样的结果就是,Spring只能找到显式声明在配置类中的控制器。 这样配置的话,DispatcherServlet会映射为应用的默认Servlet,所以它会处理所有的请求,包括对静态资源的请求,如图片和样式 表(在大多数情况下,这可能并不是你想要的效果)。 因此,我们需要在WebConfig这个最小的Spring MVC配置上再加一些内容,从而让它变得真正有用。如下程序清单中的WebConfig解决了 上面所述的问题。 程序清单程序清单5.2 最小但可用的 最小但可用的Spring MVC配置配置 在程序清单5.2中第一件需要注意的事情是WebConfig现在添加了@Component-Scan注解,因此将会扫描spitter.web包来查找组件。稍后你 就会看到,我们所编写的控制器将会带有@Controller注解,这会使其成为组件扫描时的候选bean。因此,我们不需要在配置类中显式声明 任何的控制器。 接下来,我们添加了一个ViewResolver bean。更具体来讲,是Internal-ResourceViewResolver。我们将会在第6章更为详细地讨 论视图解析器。我们只需要知道它会查找JSP文件,在查找的时候,它会在视图名称上加一个特定的前缀和后缀(例如,名为home的视图将 会解析为/WEB-INF/views/home.jsp)。 最后,新的WebConfig类还扩展了WebMvcConfigurerAdapter并重写了其configureDefaultServletHandling()方法。通过调 用DefaultServlet-HandlerConfigurer的enable()方法,我们要求DispatcherServlet将对静态资源的请求转发到Servlet容器 中默认的Servlet上,而不是使用DispatcherServlet本身来处理此类请求。 WebConfig已经就绪,那RootConfig呢?因为本章聚焦于Web开发,而Web相关的配置通过DispatcherServlet创建的应用上下文都 已经配置好了,因此现在的RootConfig相对很简单: 唯一需要注意的是RootConfig使用了@ComponentScan注解。这样的话,在本书中,我们就有很多机会用非Web的组件来充实完 善RootConfig。 现在,我们基本上已经可以开始使用Spring MVC构建Web应用了。此时,最大的问题在于,我们要构建的应用到底是什么。 5.1.3  Spittr应用简介应用简介 为了实现在线社交的功能,我们将要构建一个简单的微博(microblogging)应用。在很多方面,我们所构建的应用与最早的微博应用Twitter很 类似。在这个过程中,我们会添加一些小的变化。当然,我们要使用Spring技术来构建这个应用。 因为从Twitter借鉴了灵感并且通过Spring来进行实现,所以它就有了一个名字:Spitter。再进一步,应用网站命名中流行的模式,如Flickr,我 们去掉字母e,这样的话,我们就将这个应用称为Spittr。这个名称也有助于区分应用名称和领域类型,因为我们将会创建一个名为Spitter的 领域类。 Spittr应用有两个基本的领域概念:Spitter(应用的用户)和Spittle(用户发布的简短状态更新)。当我们在书中完善Spittr应用的功能时,将会 介绍这两个领域概念。在本章中,我们会构建应用的Web层,创建展现Spittle的控制器以及处理用户注册成为Spitter的表单。 舞台已经搭建完成了。我们已经配置了DispatcherServlet,启用了基本的Spring MVC组件并确定了目标应用。让我们进入本章的核心内 容:使用Spring MVC控制器处理Web请求。 5.2 编写基本的控制器 编写基本的控制器 在Spring MVC中,控制器只是方法上添加了@RequestMapping注解的类,这个注解声明了它们所要处理的请求。 开始的时候,我们尽可能简单,假设控制器类要处理对“/”的请求,并渲染应用的首页。程序清单5.3所示的HomeController可能是最简单的 Spring MVC控制器类了。 程序清单程序清单5.3  HomeController:超级简单的控制器:超级简单的控制器 你可能注意到的第一件事情就是HomeController带有@Controller注解。很显然这个注解是用来声明控制器的,但实际上这个注解对 Spring MVC本身的影响并不大。 HomeController是一个构造型(stereotype)的注解,它基于@Component注解。在这里,它的目的就是辅助实现组件扫描。因 为HomeController带有@Controller注解,因此组件扫描器会自动找到HomeController,并将其声明为Spring应用上下文中的一个 bean。 其实,你也可以让HomeController带有@Component注解,它所实现的效果是一样的,但是在表意性上可能会差一些,无法确 定HomeController是什么组件类型。 HomeController唯一的一个方法,也就是home()方法,带有@RequestMapping注解。它的value属性指定了这个方法所要处理的请求 路径,method属性细化了它所处理的HTTP方法。在本例中,当收到对“/”的HTTP GET请求时,就会调用home()方法。 你可以看到,home()方法其实并没有做太多的事情:它返回了一个String类型的“home”。这个String将会被Spring MVC解读为要渲染的 视图名称。DispatcherServlet会要求视图解析器将这个逻辑名称解析为实际的视图。 鉴于我们配置InternalResourceViewResolver的方式,视图名“home”将会解析为“/WEB-INF/views/home.jsp”路径的JSP。现在,我们会 让Spittr应用的首页相当简单,如下所示。 程序清单程序清单5.4  Spittr应用的首页,定义为一个简单的应用的首页,定义为一个简单的JSP 这个JSP并没有太多需要注意的地方。它只是欢迎应用的用户,并提供了两个链接:一个是查看Spittle列表,另一个是在应用中进行注册。 图5.2展现了此时的首页是什么样子的。 在本章完成之前,我们将会实现处理这些请求的控制器方法。但现在,让我们对这个控制器发起一些请求,看一下它是否能够正常工作。测试 控制器最直接的办法可能就是构建并部署应用,然后通过浏览器对其进行访问,但是自动化测试可能会给你更快的反馈和更一致的独立结果。 所以,让我们编写一个针对HomeController的测试。 图5.2 当前的Spittr首页 5.2.1 测试控制器 测试控制器 让我们再审视一下HomeController。如果你眼神不太好的话,你甚至可能注意不到这些注解,所看到的仅仅是一个简单的POJO。我们都 知道测试POJO是很容易的。因此,我们可以编写一个简单的类来测试HomeController,如下所示: 程序清单程序清单5.5  HomeControllerTest:测试:测试HomeController 程序清单5.5中的测试很简单,但它只测试了home()方法中会发生什么。在测试中会直接调用home()方法,并断言返回包含“home”值 的String。它完全没有站在Spring MVC控制器的视角进行测试。这个测试没有断言当接收到针对“/”的GET请求时会调用home()方法。因为 它返回的值就是“home”,所以也没有真正判断home是视图的名称。 不过从Spring 3.2开始,我们可以按照控制器的方式来测试Spring MVC中的控制器了,而不仅仅是作为POJO进行测试。Spring现在包含了一 种mock Spring MVC并针对控制器执行HTTP请求的机制。这样的话,在测试控制器的时候,就没有必要再启动Web服务器和Web浏览器了。 为了阐述如何测试Spring MVC的控制器,我们重写HomeControllerTest并使用Spring MVC中新的测试特性。程序清单5.6展现了新 的HomeControllerTest。 程序清单程序清单5.6 改进 改进HomeControllerTest 尽管新版本的测试只比之前版本多了几行代码,但是它更加完整地测试了HomeController。这次我们不是直接调用home()方法并测试它 的返回值,而是发起了对“/”的GET请求,并断言结果视图的名称为home。它首先传递一个HomeController实例 到MockMvcBuilders.standaloneSetup()并调用build()来构建MockMvc实例。然后它使用MockMvc实例来执行针对“/”的GET请求并 设置期望得到的视图名称。 5.2.2 定义类级别的请求处理 定义类级别的请求处理 现在,已经为HomeController编写了测试,那么我们可以做一些重构,并通过测试来保证不会对功能造成什么破坏。我们可以做的一件事 就是拆分@RequestMapping,并将其路径映射部分放到类级别上。程序清单5.7展示了这个过程。 程序清单程序清单5.7 拆分 拆分HomeController中的中的@RequestMapping 在这个新版本的HomeController中,路径现在被转移到类级别的@RequestMapping上,而HTTP方法依然映射在方法级别上。当控制器 在类级别上添加@RequestMapping注解时,这个注解会应用到控制器的所有处理器方法上。处理器方法上的@RequestMapping注解会对 类级别上的@RequestMapping的声明进行补充。 就HomeController而言,这里只有一个控制器方法。与类级别的@Request-Mapping合并之后,这个方法的@RequestMapping表 明home()将会处理对“/”路径的GET请求。 换言之,我们其实没有改变任何功能,只是将一些代码换了个地方,但是HomeController所做的事情和以前是一样的。因为我们现在有了 测试,所以可以确保在这个过程中,没有对原有的功能造成破坏。 当我们在修改@RequestMapping时,还可以对HomeController做另外一个变更。@RequestMapping的value属性能够接受一 个String类型的数组。到目前为止,我们给它设置的都是一个String类型的“/”。但是,我们还可以将它映射到对“/homepage”的请求,只需 将类级别的@RequestMapping改为如下所示: 现在,HomeController的home()方法能够映射到对“/”和“/homepage”的GET请求。 5.2.3 传递模型数据到视图中 传递模型数据到视图中 到现在为止,就编写超级简单的控制器来说,HomeController已经是一个不错的样例了。但是大多数的控制器并不是这么简单。在Spittr应 用中,我们需要有一个页面展现最近提交的Spittle列表。因此,我们需要一个新的方法来处理这个页面。 首先,需要定义一个数据访问的Repository。为了实现解耦以及避免陷入数据库访问的细节之中,我们将Repository定义为一个接口,并在稍 后实现它(第10章中)。此时,我们只需要一个能够获取Spittle列表的Repository,如下所示的SpittleRepository功能已经足够了: findSpittles()方法接受两个参数。其中max参数代表所返回的Spittle中,Spittle ID属性的最大值,而count参数表明要返回多少 个Spittle对象。为了获得最新的20个Spittle对象,我们可以这样调用findSpittles(): 现在,我们让Spittle类尽可能的简单,如下面的程序清单5.8所示。它的属性包括消息内容、时间戳以及Spittle发布时对应的经纬度。 程序清单程序清单5.8  Spittle类:包含消息内容、时间戳和位置信息类:包含消息内容、时间戳和位置信息 就大部分内容来看,Spittle就是一个基本的POJO数据对象——没有什么复杂的。唯一要注意的是,我们使用Apache Common Lang包来实 现equals()和hashCode()方法。这些方法除了常规的作用以外,当我们为控制器的处理器方法编写测试时,它们也是有用的。 既然我们说到了测试,那么我们继续讨论这个话题并为新的控制器方法编写测试。如下的程序清单使用Spring的MockMvc来断言新的处理器方 法中你所期望的行为。 程序清单程序清单5.9 测试 测试SpittleController处理针对处理针对“/spittles”的的GET请求请求 这个测试首先会创建SpittleRepository接口的mock实现,这个实现会从它的findSpittles()方法中返回20个Spittle对象。然后, 它将这个Repository注入到一个新的SpittleController实例中,然后创建MockMvc并使用这个控制器。 需要注意的是,与HomeController不同,这个测试在MockMvc构造器上调用了setSingleView()。这样的话,mock框架就不用解析控 制器中的视图名了。在很多场景中,其实没有必要这样做。但是对于这个控制器方法,视图名与请求路径是非常相似的,这样按照默认的视图 解析规则时,MockMvc就会发生失败,因为无法区分视图路径和控制器的路径。在这个测试中,构建InternalResourceView时所设置的 实际路径是无关紧要的,但我们将其设置为与InternalResourceViewResolver配置一致。 这个测试对“/spittles”发起GET请求,然后断言视图的名称为spittles并且模型中包含名为spittleList的属性,在spittleList中包含预期 的内容。 当然,如果此时运行测试的话,它将会失败。它不是运行失败,而是在编译的时候就会失败。这是因为我们还没有编 写SpittleController。现在,我们创建SpittleController,让它满足程序清单5.9的预期。如下的SpittleController实现将会 满足以上测试的要求。 程序清单程序清单5.10  SpittleController:在模型中放入最新的:在模型中放入最新的spittle列表列表 我们可以看到SpittleController有一个构造器,这个构造器使用了@Autowired注解,用来注入SpittleRepository。这 个SpittleRepository随后又用在spittles()方法中,用来获取最新的spittle列表。 需要注意的是,我们在spittles()方法中给定了一个Model作为参数。这样,spittles()方法就能将Repository中获取到 的Spittle列表填充到模型中。Model实际上就是一个Map(也就是key-value对的集合),它会传递给视图,这样数据就能渲染到客户端 了。当调用addAttribute()方法并且不指定key的时候,那么key会根据值的对象类型推断确定。在本例中,因为它是一 个List,因此,键将会推断为spittleList。 spittles()方法所做的最后一件事是返回spittles作为视图的名字,这个视图会渲染模型。 如果你希望显式声明模型的key的话,那也尽可以进行指定。例如,下面这个版本的spittles()方法与程序清单5.10中的方法作用是一样 的: 如果你希望使用非Spring类型的话,那么可以用java.util.Map来代替Model。下面这个版本的spittles()方法与之前的版本在功能上 是一样的: 既然我们现在提到了各种可替代的方案,那下面还有另外一种方式来编写spittles()方法: 这个版本与其他的版本有些差别。它并没有返回视图名称,也没有显式地设定模型,这个方法返回的是Spittle列表。当处理器方法像这样 返回对象或集合时,这个值会放到模型中,模型的key会根据其类型推断得出(在本例中,也就是spittleList)。 而逻辑视图的名称将会根据请求路径推断得出。因为这个方法处理针对“/spittles”的GET请求,因此视图的名称将会是spittles(去掉开头的 斜线)。 不管你选择哪种方式来编写spittles()方法,所达成的结果都是相同的。模型中会存储一个Spittle列表,key为spittleList,然后这 个列表会发送到名为spittles的视图中。按照我们配置InternalResourceViewResolver的方式,视图的JSP将会是“/WEB- INF/views/spittles.jsp”。 现在,数据已经放到了模型中,在JSP中该如何访问它呢?实际上,当视图是JSP的时候,模型数据会作为请求属性放到请求(request)之 中。因此,在spittles.jsp文件中可以使用JSTL(JavaServer Pages Standard Tag Library)的标签渲染spittle列表: 图5.3为显示效果,能够让你对它在Web浏览器中是什么样子有个可视化的印象。 尽管SpittleController很简单,但是它依然比HomeController更进一步了。不过,SpittleController和HomeController都 没有处理任何形式的输入。现在,让我们扩展SpittleController,让它从客户端接受一些输入。 图5.3 控制器中的Spittle模型数据将会作为请求参数,并在Web页面上渲染为列表的形式 5.3 接受请求的输入 接受请求的输入 有些Web应用是只读的。人们只能通过浏览器在站点上闲逛,阅读服务器发送到浏览器中的内容。 不过,这并不是一成不变的。众多的Web应用允许用户参与进去,将数据发送回服务器。如果没有这项能力的话,那Web将完全是另一番景 象。 Spring MVC允许以多种方式将客户端中的数据传送到控制器的处理器方法中,包括: 查询参数(Query Parameter)。 表单参数(Form Parameter)。 路径变量(Path Variable)。 你将会看到如何编写控制器处理这些不同机制的输入。作为开始,我们先看一下如何处理带有查询参数的请求,这也是客户端往服务器端发送 数据时,最简单和最直接的方式。 5.3.1 处理查询参数 处理查询参数 在Spittr应用中,我们可能需要处理的一件事就是展现分页的Spittle列表。在现在的SpittleController中,它只能展现最新的Spittle,并 没有办法向前翻页查看以前编写的Spittle历史记录。如果你想让用户每次都能查看某一页的Spittle历史,那么就需要提供一种方式让用户传递 参数进来,进而确定要展现哪些Spittle集合。 在确定该如何实现时,假设我们要查看某一页Spittle列表,这个列表会按照最新的Spittle在前的方式进行排序。因此,下一页中第一条的ID肯 定会早于当前页最后一条的ID。所以,为了显示下一页的Spittle,我们需要将一个Spittle的ID传入进来,这个ID要恰好小于当前页最后一条 Spittle的ID。另外,你还可以传入一个参数来确定要展现的Spittle数量。 为了实现这个分页的功能,我们所编写的处理器方法要接受如下的参数: before参数(表明结果中所有Spittle的ID均应该在这个值之前)。 count参数(表明在结果中要包含的Spittle数量)。 为了实现这个功能,我们将程序清单5.10中的spittles()方法替换为使用before和count参数的新spittles()方法。我们首先添加一个 测试,这个测试反映了新spittles()方法的功能。 程序清单程序清单5.11 用来测试分页 用来测试分页Spittle列表的新方法列表的新方法 这个测试方法与程序清单5.9中的测试方法关键区别在于它针对“/spittles”发送GET请求,同时还传入了max和count参数。它测试了这些参数存 在时的处理器方法,而另一个测试方法则测试了没有这些参数时的情景。这两个测试就绪后,我们就能确保不管控制器发生什么样的变化,它 都能够处理这两种类型的请求: SpittleController中的处理器方法要同时处理有参数和没有参数的场景,那我们需要对其进行修改,让它能接受参数,同时,如果这些 参数在请求中不存在的话,就使用默认值Long.MAX_VALUE和20。@RequestParam注解的defaultValue属性可以完成这项任务: 现在,如果max参数没有指定的话,它将会是Long类型的最大值。因为查询参数都是String类型的,因此defaultValue属性需 要String类型的值。因此,使用Long.MAX_VALUE是不行的。我们可以将Long.MAX_VALUE转换为名为MAX_LONG_-AS_STRING的 String类型常量: 尽管defaultValue属性给定的是String类型的值,但是当绑定到方法的max参数时,它会转换为Long类型。 如果请求中没有count参数的话,count参数的默认值将会设置为20。 请求中的查询参数是往控制器中传递信息的常用手段。另外一种方式也很流行,尤其是在构建面向资源的控制器时,这种方式就是将传递参数 作为请求路径的一部分。让我们看一下如何将路径变量作为请求路径的一部分,从而实现信息的输入。 5.3.2 通过路径参数接受输入 通过路径参数接受输入 假设我们的应用程序需要根据给定的ID来展现某一个Spittle记录。其中一种方案就是编写处理器方法,通过使用@RequestParam注解, 让它接受ID作为查询参数: 这个处理器方法将会处理形如“/spittles/show?spittle_id=12345”这样的请求。尽管这也可以正常工作,但是从面向资源的角度来看这并不理想。 在理想情况下,要识别的资源(Spittle)应该通过URL路径进行标示,而不是通过查询参数。对“/spittles/12345”发起GET请求要优于 对“/spittles/show?spittle_id=12345”发起请求。前者能够识别出要查询的资源,而后者描述的是带有参数的一个操作——本质上是通过HTTP发 起的RPC。 既然已经以面向资源的控制器作为目标,那我们将这个需求转换为一个测试。程序清单5.12展现了一个新的测试方法,它会断 言SpittleController中对面向资源 请求的处理。 程序清单程序清单5.12 测试对某个 测试对某个Spittle的请求,其中的请求,其中ID要在路径变量中指定要在路径变量中指定 可以看到,这个测试构建了一个mock Repository、一个控制器和MockMvc,这与本章中我们所编写的其他测试很类似。这个测试中最重要的 部分是最后几行,它对“/spittles/12345”发起GET请求,然后断言视图的名称是spittle,并且预期的Spittle对象放到了模型之中。因为我 们还没有为这种请求实现处理器方法,因此这个请求将会失败。但是,我们可以通过为SpittleController添加新的方法来修正这个失败 的测试。 到目前为止,在我们编写的控制器中,所有的方法都映射到了(通过@RequestMapping)静态定义好的路径上。但是,如果想让这个测试 通过的话,我们编写的@RequestMapping要包含变量部分,这部分代表了Spittle ID。 为了实现这种路径变量,Spring MVC允许我们在@RequestMapping路径中添加占位符。占位符的名称要用大括号(“{”和“}”)括起来。路径 中的其他部分要与所处理的请求完全匹配,但是占位符部分可以是任意的值。 下面的处理器方法使用了占位符,将Spittle ID作为路径的一部分: 例如,它就能够处理针对“/spittles/12345”的请求,也就是程序清单5.12中的路径 我们可以看到,spittle()方法的spittleId参数上添加了@PathVariable("spittleId")注解,这表明在请求路径中,不管占位符 部分的值是什么都会传递到处理器方法的spittleId参数中。如果对“/spittles/54321”发送GET请求,那么将会把“54321”传递进来,作 为spittleId的值。 需要注意的是:在样例中spittleId这个词出现了好几次:先是在@RequestMapping的路径中,然后作为@PathVariable属性的值,最 后又作为方法的参数名称。因为方法的参数名碰巧与占位符的名称相同,因此我们可以去掉@PathVariable中的value属性: 如果@PathVariable中没有value属性的话,它会假设占位符的名称与方法的参数名相同。这能够让代码稍微简洁一些,因为不必重复写占 位符的名称了。但需要注意的是,如果你想要重命名参数时,必须要同时修改占位符的名称,使其互相匹配。 spittle()方法会将参数传递到SpittleRepository的findOne()方法中,用来获取某个Spittle对象,然后将Spittle对象添加到 模型中。模型的key将会是spittle,这是根据传递到addAttribute()方法中的类型推断得到的。 这样Spittle对象中的数据就可以渲染到视图中了,此时需要引用请求中key为spittle的属性(与模型的key一致)。如下为渲 染Spittle的JSP视图片段: 这个视图并没有什么特别之处,它的屏幕截图如图5.4所示。 图5.4 在浏览器中展现一个spittle 如果传递请求中少量的数据,那查询参数和路径变量是很合适的。但通常我们还需要传递很多的数据(也许是表单提交的数据),那查询参数 显得有些笨拙和受限了。下面让我们来看一下如何编写控制器方法来处理表单提交。 5.4 处理表单 处理表单 Web应用的功能通常并不局限于为用户推送内容。大多数的应用允许用户填充表单并将数据提交回应用中,通过这种方式实现与用户的交互。 像提供内容一样,Spring MVC的控制器也为表单处理提供了良好的支持。 使用表单分为两个方面:展现表单以及处理用户通过表单提交的数据。在Spittr应用中,我们需要有个表单让新用户进行注 册。SpitterController是一个新的控制器,目前只有一个请求处理的方法来展现注册表单。 程序清单程序清单5.13  SpitterController:展现一个表单,允许用户注册该应用:展现一个表单,允许用户注册该应用 showRegistrationForm()方法的@RequestMapping注解以及类级别上的@RequestMapping注解组合起来,声明了这个方法要处理 的是针对“/spitter/register”的GET请求。这是一个简单的方法,没有任何输入并且只是返回名为registerForm的逻辑视图。按照我们配 置InternalResourceViewResolver的方式,这意味着将会使用“/WEB-INF/ views/registerForm.jsp”这个JSP来渲染注册表单。 尽管showRegistrationForm()方法非常简单,但测试依然需要覆盖到它。因为这个方法很简单,所以它的测试也比较简单。 程序清单程序清单5.14 测试展现表单的控制器方法 测试展现表单的控制器方法 这个测试方法与首页控制器的测试非常类似。它对“/spitter/register”发送GET请求,然后断言结果的视图名为registerForm。 现在,让我们回到视图上。因为视图的名称为registerForm,所以JSP的名称需要是registerForm.jsp。这个JSP必须要包含一个HTML
标签,在这个标签中用户输入注册应用的信息。如下就是我们现在所要使用的JSP。 程序清单程序清单5.15 渲染注册表单的 渲染注册表单的JSP 可以看到,这个JSP非常基础。它的HTML表单域中记录用户的名字、姓氏、用户名以及密码,然后还包含一个提交表单的按钮。在浏览器渲 染之后,它的样子大致如图5.5所示。 需要注意的是:这里的标签中并没有设置action属性。在这种情况下,当表单提交时,它会提交到与展现时相同的URL路径上。也 就是说,它会提交到“/spitter/register”上。 这就意味着需要在服务器端处理该HTTP POST请求。现在,我们在Spitter-Controller中再添加一个方法来处理这个表单提交。 图5.5 注册页提供了一个表单,这个表单会由SpitterController 进行处理,完成为应用添加新用户的功能 5.4.1 编写处理表单的控制器 编写处理表单的控制器 当处理注册表单的POST请求时,控制器需要接受表单数据并将表单数据保存为Spitter对象。最后,为了防止重复提交(用户点击浏览器的 刷新按钮有可能会发生这种情况),应该将浏览器重定向到新创建用户的基本信息页面。这些行为通过下面 的shouldProcessRegistration()进行了测试。 程序清单程序清单5.16 测试处理表单的控制器方法 测试处理表单的控制器方法 显然,这个测试比展现注册表单的测试复杂得多。在构建完SpitterRepository的mock实现以及所要执行的控制器和MockMvc之 后,shouldProcess-Registration()对“/spitter/ register”发起了一个POST请求。作为请求的一部分,用户信息以参数的形式放到 request中,从而模拟提交的表单。 在处理POST类型的请求时,在请求处理完成后,最好进行一下重定向,这样浏览器的刷新就不会重复提交表单了。在这个测试中,预期请求 会重定向到“/spitter/jbauer”,也就是新建用户的基本信息页面。 最后,测试会校验SpitterRepository的mock实现最终会真正用来保存表单上传入的数据。 现在,我们来实现处理表单提交的控制器方法。通过shouldProcess-Registration()方法,我们可能认为要满足这个需求需要做很多 的工作。但是,在如下的程序清单中,我们可以看到新的SpitterController并没有做太多的事情。 程序清单程序清单5.17 处理所提交的表单并注册新用户 处理所提交的表单并注册新用户 我们之前创建的showRegistrationForm()方法依然还在,不过请注意新创建的processRegistration()方法,它接受一 个Spitter对象作为参数。这个对象有firstName、lastName、username和password属性,这些属性将会使用请求中同名的参数进行 填充。 当使用Spitter对象调用processRegistration()方法时,它会进而调用SpitterRepository的save()方 法,SpitterRepository是在Spitter-Controller的构造器中注入进来的。 processRegistration()方法做的最后一件事就是返回一个String类型,用来指定视图。但是这个视图格式和以前我们所看到的视图有 所不同。这里不仅返回了视图的名称供视图解析器查找目标视图,而且返回的值还带有重定向的格式。 当InternalResourceViewResolver看到视图格式中的“redirect:”前缀时,它就知道要将其解析为重定向的规则,而不是视图的名称。在 本例中,它将会重定向到用户基本信息的页面。例如,如果Spitter.username属性的值为“jbauer”,那么视图将会重定向 到“/spitter/jbauer”。 需要注意的是,除了“redirect:”,InternalResourceViewResolver还能识别“forward:”前缀。当它发现视图格式中 以“forward:”作为前缀时,请求将会前往(forward)指定的URL路径,而不再是重定向。 万事俱备!现在,程序清单5.16中的测试应该能够通过了。但是,我们的任务还没有完成,因为我们重定向到了用户基本信息页面,那么我们 应该往SpitterController中添加一个处理器方法,用来处理对基本信息页面的请求。如下的showSpitterProfile()将会完成这项任 务: SpitterRepository通过用户名获取一个Spitter对象,showSpitter-Profile()得到这个对象并将其添加到模型中,然后返 回profile,也就是基本信息页面的逻辑视图名。像本章展现的其他视图一样,现在的基本信息视图非常简单: 图5.6展现了在Web浏览器中渲染的基本信息页面。 如果表单中没有发送username或password的话,会发生什么情况呢?或者说,如果firstName或lastName的值为空或太长的话,又会 怎么样呢?接下来,让我们看一下如何为表单提交添加校验,从而避免数据呈现的不一致性。 图5.6 Spittr的基本信息页展现了用户的情况,这些信息是 由SpitterController填充到模型中的 5.4.2 校验表单 校验表单 如果用户在提交表单的时候,username或password文本域为空的话,那么将会导致在新建Spitter对象中,username或password是 空的String。至少这是一种怪异的行为。如果这种现象不处理的话,这将会出现安全问题,因为不管是谁只要提交一个空的表单就能登录应 用。 同时,我们还应该阻止用户提交空的firstName和/或lastName,使应用仅在一定程度上保持匿名性。有个好的办法就是限制这些输入域值 的长度,保持它们的值在一个合理的长度范围,避免这些输入域的误用。 有种处理校验的方式非常初级,那就是在processRegistration()方法中添加代码来检查值的合法性,如果值不合法的话,就将注册表单 重新显示给用户。这是一个很简短的方法,因此,添加一些额外的if语句也不是什么大问题,对吧? 与其让校验逻辑弄乱我们的处理器方法,还不如使用Spring对Java校验API(Java Validation API,又称JSR-303)的支持。从Spring 3.0开 始,在Spring MVC中提供了对Java校验API的支持。在Spring MVC中要使用Java校验API的话,并不需要什么额外的配置。只要保证在类路径 下包含这个Java API的实现即可,比如Hibernate Validator。 Java校验API定义了多个注解,这些注解可以放到属性上,从而限制这些属性的值。所有的注解都位 于javax.validation.constraints包中。表5.1列出了这些校验注解。 表表5.1  Java校验校验API所提供的校验注解所提供的校验注解 注  解注  解 描  述描  述 @AssertFalse 所注解的元素必须是Boolean类型,并且值为false @AssertTrue 所注解的元素必须是Boolean类型,并且值为true @DecimalMax 所注解的元素必须是数字,并且它的值要小于或等于给定的BigDecimalString值 @DecimalMin 所注解的元素必须是数字,并且它的值要大于或等于给定的BigDecimalString值 @Digits 所注解的元素必须是数字,并且它的值必须有指定的位数 @Future 所注解的元素的值必须是一个将来的日期 @Max 所注解的元素必须是数字,并且它的值要小于或等于给定的值 @Min 所注解的元素必须是数字,并且它的值要大于或等于给定的值 @NotNull 所注解元素的值必须不能为null @Null 所注解元素的值必须为null @Past 所注解的元素的值必须是一个已过去的日期 @Pattern 所注解的元素的值必须匹配给定的正则表达式 @Size 所注解的元素的值必须是String、集合或数组,并且它的长度要符合给定的范围 除了表5.1中的注解,Java校验API的实现可能还会提供额外的校验注解。同时,也可以定义自己的限制条件。但就我们来讲,将会关注于上表 中的两个核心限制条件。 请考虑要添加到Spitter域上的限制条件,似乎需要使用@NotNull和@Size注解。我们所要做的事情就是将这些注解添加到Spitter的属 性上。如下的程序清单展现了Spitter类,它的属性已经添加了校验注解。 程序清单程序清单5.18  Spitter:包含了要提交到:包含了要提交到Spittle POST请求中的域请求中的域 现在,Spitter的所有属性都添加了@NotNull注解,以确保它们的值不为null。类似地,属性上也添加了@Size注解以限制它们的长度在 最大值和最小值之间。对Spittr应用来说,这意味着用户必须要填完注册表单,并且值的长度要在给定的范围内。 我们已经为Spitter添加了校验注解,接下来需要修改processRegistration()方法来应用校验功能。启用校验功能 的processRegistration()如下所示: 程序清单程序清单5.19  processRegistration():确保所提交的数据是合法的:确保所提交的数据是合法的 与程序清单5.17中最初的processRegistration()方法相比,这里有了很大的变化。Spitter参数添加了@Valid注解,这会告 知Spring,需要确保这个对象满足校验限制。 在Spitter属性上添加校验限制并不能阻止表单提交。即便用户没有填写某个域或者某个域所给定的值超出了最大长 度,processRegistration()方法依然会被调用。这样,我们就需要处理校验的错误,就像在processRegistration()方法中所看到 的那样。 如果有校验出现错误的话,那么这些错误可以通过Errors对象进行访问,现在这个对象已作为processRegistration()方法的参数。 (很重要一点需要注意,Errors参数要紧跟在带有@Valid注解的参数后面,@Valid注解所标注的就是要检验的参数。 )processRegistration()方法所做的第一件事就是调用Errors.hasErrors()来检查是否有错误。 如果有错误的话,Errors.hasErrors()将会返回到registerForm,也就是注册表单的视图。这能够让用户的浏览器重新回到注册表单 页面,所以他们能够修正错误,然后重新尝试提交。现在,会显示空的表单,但是在下一章中,我们将在表单中显示最初提交的值并将校验错 误反馈给用户。 如果没有错误的话,Spitter对象将会通过Repository进行保存,控制器会像之前那样重定向到基本信息页面。 5.5 小结 小结 在本章中,我们为编写应用程序的Web部分开了一个好头。可以看到,Spring有一个强大灵活的Web框架。借助于注解,Spring MVC提供了 近似于POJO的开发模式,这使得开发处理请求的控制器变得非常简单,同时也易于测试。 当编写控制器的处理器方法时,Spring MVC极其灵活。概括来讲,如果你的处理器方法需要内容的话,只需将对应的对象作为参数,而它不 需要的内容,则没有必要出现在参数列表中。这样,就为请求处理带来了无限的可能性,同时还能保持一种简单的编程模型。 尽管本章中的很多内容都是关于控制器的请求处理的,但是渲染响应同样也是很重要的。我们通过使用JSP的方式,简单了解了如何为控制器 编写视图。但是就Spring MVC的视图来说,它并不限于本章所看到的简单JSP。 在接下来的第6章中,我们将会更深入地学习Spring视图,包括如何在JSP中使用Spring标签库。我们还会学习如何借助Apache Tiles为视图添 加一致的布局结构。同时,还会了解Thymeleaf,这是一个很有意思的JSP替代方案,Spring为其提供了内置的支持。 第第6章 渲染章 渲染Web视图视图 本章内容:本章内容: 将模型数据渲染为HTML 使用JSP视图 通过tiles定义视图布局 使用Thymeleaf视图 上一章主要关注于如何编写处理Web请求的控制器。我们也创建了一些简单的视图,用来渲染控制器产生的模型数据,但我们并没有花太多时 间讨论视图,也没有讨论控制器完成请求到结果渲染到用户的浏览器中的这段时间内到底发生了什么,而这正是本章的主要内容。 6.1 理解视图解析 理解视图解析 在第5章中,我们所编写的控制器方法都没有直接产生浏览器中渲染所需的HTML。这些方法只是将一些数据填充到模型中,然后将模型传递给 一个用来渲染的视图。这些方法会返回一个String类型的值,这个值是视图的逻辑名称,不会直接引用具体的视图实现。尽管我们也编写了几 个简单的JavaServer Page(JSP)视图,但是控制器并不关心这些。 将控制器中请求处理的逻辑和视图中的渲染实现解耦是Spring MVC的一个重要特性。如果控制器中的方法直接负责产生HTML的话,就很难在 不影响请求处理逻辑的前提下,维护和更新视图。控制器方法和视图的实现会在模型内容上达成一致,这是两者的最大关联,除此之外,两者 应该保持足够的距离。 但是,如果控制器只通过逻辑视图名来了解视图的话,那Spring该如何确定使用哪一个视图实现来渲染模型呢?这就是Spring视图解析器的任 务了。 在第5章中,我们使用名为InternalResourceViewResolver的视图解析器。在它的配置中,为了得到视图的名字,会使用“/WEB- INF/views/”前缀和“.jsp”后缀,从而确定来渲染模型的JSP文件的物理位置。现在,我们回过头来看一下视图解析的基础知识以及Spring提供的 其他视图解析器。 Spring MVC定义了一个名为ViewResolver的接口,它大致如下所示: 当给resolveViewName()方法传入一个视图名和Locale对象时,它会返回一个View实例。View是另外一个接口,如下所示: View接口的任务就是接受模型以及Servlet的request和response对象,并将输出结果渲染到response中。 这看起来非常简单。我们所需要做的就是编写ViewResolver和View的实现,将要渲染的内容放到response中,进而展现到用户的浏览器 中。对吧? 实际上,我们并不需要这么麻烦。尽管我们可以编写ViewResolver和View的实现,在有些特定的场景下,这样做也是有必要的,但是一般 来讲,我们并不需要关心这些接口。我在这里提及这些接口只是为了让你对视图解析内部如何工作有所了解。Spring提供了多个内置的实现, 如表6.1所示,它们能够适应大多数的场景。 表表6.1  Spring自带了自带了13个视图解析器,能够将逻辑视图名转换为物理实现个视图解析器,能够将逻辑视图名转换为物理实现 视图解析器视图解析器 描  述描  述 BeanNameViewResolver 将视图解析为Spring应用上下文中的bean,其中bean的ID与视图的名字相同 ContentNegotiatingViewResolver 通过考虑客户端需要的内容类型来解析视图,委托给另外一个能够产生对应内容类型的视图解析器 FreeMarkerViewResolver 将视图解析为FreeMarker模板 InternalResourceViewResolver 将视图解析为Web应用的内部资源(一般为JSP) JasperReportsViewResolver 将视图解析为JasperReports定义 ResourceBundleViewResolver 将视图解析为资源bundle(一般为属性文件) TilesViewResolver 将视图解析为Apache Tile定义,其中tile ID与视图名称相同。注意有两个不同的TilesViewResolver实现,分别对应于Tiles 2.0和 Tiles 3.0 UrlBasedViewResolver 直接根据视图的名称解析视图,视图的名称会匹配一个物理视图的定义 VelocityLayoutViewResolver 将视图解析为Velocity布局,从不同的Velocity模板中组合页面 VelocityViewResolver 将视图解析为Velocity模板 XmlViewResolver 将视图解析为特定XML文件中的bean定义。类似于BeanName-ViewResolver XsltViewResolver 将视图解析为XSLT转换后的结果 Spring 4和Spring 3.2支持表6.1中的所有视图解析器。Spring 3.1支持除Tiles 3 TilesViewResolver之外的所有视图解析器。 我们没有足够的篇幅介绍Spring所提供的13种视图解析器。这其实也没什么,因为在大多数应用中,我们只会用到其中很少的一部分。 对于表6.1中的大部分视图解析器来讲,每一项都对应Java Web应用中特定的某种视图技术。InternalResourceViewResolver一般会用 于JSP,TilesViewResolver用于Apache Tiles视图,而FreeMarkerViewResolver和VelocityViewResolver分别对应 FreeMarker和Velocity模板视图。 在本章中,我们将会关注与大多数Java开发人员最息息相关的视图技术。因为大多数Java Web应用都会用到JSP,我们首先将会介 绍InternalResourceViewResolver,这个视图解析器一般会用来解析JSP视图。接下来,我们将会介绍TilesViewResolver,控制 JSP页面的布局。 在本章的最后,我们将会看一个没有列在表6.1中的视图解析器。Thymeleaf是一种用来替代JSP的新兴技术,Spring提供了与Thymeleaf的原生 模板(natural template)协作的视图解析器,这种模板之所以得到这样的称呼是因为它更像是最终产生的HTML,而不是驱动它们的Java代 码。Thymeleaf是一种非常令人兴奋的视图方案,所以你尽可以先往后翻几页,去6.4节看一下在Spring中是如何使用  它的。 如果你依然停留在本页的话,那么你可能知道JSP曾经是,而且现在依然还是Java领域占主导地位的视图技术。在以前的项目中,也许你使用 过JSP,将来有可能还会继续使用这项技术,所以接下来让我们看一下如何在Spring MVC中使用JSP  视图。 6.2 创建 创建JSP视图视图 不管你是否相信,JavaServer Pages作为Java Web应用程序的视图技术已经超过15年了。尽管开始的时候它很丑陋,只是类似模板技术(如 Microsoft的Active Server Pages)的Java版本,但JSP这些年在不断进化,包含了对表达式语言和自定义标签库的支持。 Spring提供了两种支持JSP视图的方式: InternalResourceViewResolver会将视图名解析为JSP文件。另外,如果在你的JSP页面中使用了JSP标准标签库(JavaServer Pages Standard Tag Library,JSTL)的话,InternalResourceViewResolver能够将视图名解析为JstlView形式的JSP文件,从而 将JSTL本地化和资源bundle变量暴露给JSTL的格式化(formatting)和信息(message)标签。 Spring提供了两个JSP标签库,一个用于表单到模型的绑定,另一个提供了通用的工具类特性。 不管你使用JSTL,还是准备使用Spring的JSP标签库,配置解析JSP的视图解析器都是非常重要的。尽管Spring还有其他的几个视图解析器都 能将视图名映射为JSP文件,但就这项任务来讲,InternalResourceViewResolver是最简单和最常用的视图解析器。我们在第5章已经 接触到了如何配置InternalResourceViewResolver。但是在那里,我们只是匆忙体验了一下,以便于查看控制器在浏览器中的效果。 接下来,我们将会更加仔细地了解InternalResourceViewResolver,看看如何让它完全听命于我们。 6.2.1 配置适用于 配置适用于JSP的视图解析器的视图解析器 有一些视图解析器,如ResourceBundleViewResolver会直接将逻辑视图名映射为特定的View接口实现, 而InternalResourceViewResolver所采取的方式并不那么直接。它遵循一种约定,会在视图名上添加前缀和后缀,进而确定一个Web 应用中视图资源的物理路径。 作为样例,考虑一个简单的场景,假设逻辑视图名为home。通用的实践是将JSP文件放到Web应用的WEB-INF目录下,防止对它的直接访 问。如果我们将所有的JSP文件都放在“/WEB-INF/views/”目录下,并且home页的JSP名为home.jsp,那么我们可以确定物理视图的路径就是逻 辑视图名home再加上“/WEB-INF/views/”前缀和“.jsp”后缀。如图6.1所示。 图6.1 nternalResourceViewResolver解析视图时, 会在视图名上添加前缀和后缀 当使用@Bean注解的时候,我们可以按照如下的方式配置Internal-ResourceView Resolver,使其在解析视图时,遵循上述的约定。 作为替代方案,如果你更喜欢使用基于XML的Spring配置,那么可以按照如下的方式配置InternalResourceViewResolver: InternalResourceViewResolver配置就绪之后,它就会将逻辑视图名解析为JSP文件,如下所示: home将会解析为“/WEB-INF/views/home.jsp” productList将会解析为“/WEB-INF/views/productList.jsp” books/detail将会解析为“/WEB-INF/views/books/detail.jsp” 让我们重点看一下最后一个样例。当逻辑视图名中包含斜线时,这个斜线也会带到资源的路径名中。因此,它会对应到prefix属性所引用目 录的子目录下的JSP文件。这样的话,我们就可以很方便地将视图模板组织为层级目录结构,而不是将它们都放到同一个目录之中。 解析解析JSTL视图视图 到目前为止,我们对InternalResourceViewResolver的配置都很基础和简单。它最终会将逻辑视图名解析 为InternalResourceView实例,这个实例会引用JSP文件。但是如果这些JSP使用JSTL标签来处理格式化和信息的话,那么我们会希 望InternalResourceViewResolver将视图解析为JstlView。 JSTL的格式化标签需要一个Locale对象,以便于恰当地格式化地域相关的值,如日期和货币。信息标签可以借助Spring的信息资源 和Locale,从而选择适当的信息渲染到HTML之中。通过解析JstlView,JSTL能够获得Locale对象以及Spring中配置的信息资源。 如果想让InternalResourceViewResolver将视图解析为JstlView,而不是InternalResourceView的话,那么我们只需设置它 的viewClass属性即可: 同样,我们也可以使用XML完成这一任务: 不管使用Java配置还是使用XML,都能确保JSTL的格式化和信息标签能够获得Locale对象以及Spring中配置的信息资源。 6.2.2 使用 使用Spring的的JSP库库 当为JSP添加功能时,标签库是一种很强大的方式,能够避免在脚本块中直接编写Java代码。Spring提供了两个JSP标签库,用来帮助定义 Spring MVC Web的视图。其中一个标签库会用来渲染HTML表单标签,这些标签可以绑定model中的某个属性。另外一个标签库包含了一些 工具类标签,我们随时都可以非常便利地使用它们。 在这两个标签库中,你可能会发现表单绑定的标签库更加有用。所以,我们就从这个标签库开始学习Spring的JSP标签。我们将会看到如何将 Spittr应用的注册表单绑定到模型上,这样表单就可以预先填充值,并且在表单提交失败后,能够展现校验错误。 将表单绑定到模型上将表单绑定到模型上 Spring的表单绑定JSP标签库包含了14个标签,它们中的大多数都用来渲染HTML中的表单标签。但是,它们与原生HTML标签的区别在于它们 会绑定模型中的一个对象,能够根据模型中对象的属性填充值。标签库中还包含了一个为用户展现错误的标签,它会将错误信息渲染到最终的 HTML之中。 为了使用表单绑定库,需要在JSP页面中对其进行声明: 需要注意,我将前缀指定为“sf”,但通常也可能使用“form”前缀。你可以选择任意喜欢的前缀,我之所以选择“sf”是因为它很简洁、易于输 入,并且还是Spring form的简写形式。在本书中,当使用表单绑定库的时候,我会一直使用“sf”前缀。 在声明完表单绑定标签库之后,你就可以使用14个相关的标签了。如表6.2所示。 表表6.2 借助 借助Spring表单绑定标签库中所包含的标签,我们能够将模型对象绑定到渲染后的表单绑定标签库中所包含的标签,我们能够将模型对象绑定到渲染后的HTML表单中表单中 JSP标签标签 描  述描  述 渲染成一个HTML 标签,其中type属性设置为checkbox 渲染成多个HTML 标签,其中type属性设置为checkbox 在一个HTML 中渲染输入域的错误 渲染成一个HTML 标签,并为其内部标签暴露绑定路径,用于数据绑定 渲染成一个HTML 标签,其中type属性设置为hidden 渲染成一个HTML 标签,其中type属性设置为text 渲染成一个HTML