Spring5-高级教程-全-
Spring5 高级教程(全)
原文:Pro Spring 5
一、Spring 简介
当我们想到 Java 开发人员社区时,我们会想起 19 世纪 40 年代后期成群结队的淘金者,他们疯狂地在北美的河流中淘金,寻找黄金碎片。作为 Java 开发人员,我们的河流中充满了开源项目,但是,像勘探者一样,找到一个有用的项目可能是耗时且费力的。
许多开源 Java 项目的一个常见问题是,它们仅仅是出于填补最新流行技术或模式实现中的空白的需要而设计的。话虽如此,许多高质量、可用的项目满足并解决了实际应用的实际需求,在本书的过程中,您将会遇到这些项目的子集。你会对其中的一个特别有所了解——Spring。Spring 的第一个版本发布于 2002 年 10 月,由一个小的核心和一个易于配置和使用的控制反转(IoC)容器组成。多年来,Spring 已经成为 Java 企业版(JEE)服务器的主要替代品,并且已经发展成为一项成熟的技术,由许多不同的项目组成,每个项目都有自己的目的,所以无论您想要构建微服务、应用还是传统的 ERP,Spring 都有一个项目可以满足您的需求。
纵观这本书,你会看到很多不同开源技术的应用,都统一在 Spring 框架下。当使用 Spring 时,应用开发人员可以使用各种各样的开源工具,而不需要编写大量代码,也不需要将应用与任何特定工具过于紧密地耦合在一起。
正如标题所示,在这一章中,我们将向您介绍 Spring 框架,而不是展示任何可靠的例子或解释。如果你已经熟悉 Spring,你可能想跳过这一章,直接进入第二章。
Spring 是什么?
也许解释 Spring 最困难的部分之一是准确地分类它是什么。通常,Spring 被描述为用于构建 Java 应用的轻量级框架,但是这种说法带来了两个有趣的问题。
首先,您可以使用 Spring 在 Java 中构建任何应用(例如,独立的、web 或 JEE 应用),这与许多其他框架不同(例如 Apache Struts,它仅限于 web 应用)。
其次,描述中的轻量级部分并不真正指类的数量或分布的大小,而是定义了 Spring 哲学的整体原则——也就是说,最小影响。Spring 是轻量级的,因为您只需对应用代码做很少的更改,就可以获得 Spring Core 的好处,如果您选择在任何时候停止使用 Spring,您会发现这样做非常简单。
请注意,我们将最后一个声明限定为仅指 Spring Core 许多额外的 Spring 组件,比如数据访问,需要与 Spring 框架更紧密的耦合。然而,这种耦合的好处是显而易见的,在整本书中,我们介绍了将这种耦合对应用的影响最小化的技术。
Spring 框架的演变
Spring 框架源自 Rod Johnson 所著的《专家一对一:J2EE 设计和开发》一书(Wrox,2002)。在过去的十年中,Spring 框架在核心功能、相关项目和社区支持方面有了显著的发展。随着 Spring Framework 新的主要版本的发布,有必要快速回顾一下 Spring 的每个里程碑版本带来的重要特性,直到 Spring Framework 5.0。
-
Spring 0.9:这是该框架的第一个公开发布,基于《专家一对一:J2EE 设计与开发》一书,提供了 bean 配置基础、AOP 支持、JDBC 抽象框架、抽象事务支持等等。这个版本没有官方的参考文档,但是你可以在 SourceForge 上找到现有的资源和文档。 1
-
Spring 1.x : This is the first version released with official reference documentation. It is composed of the seven modules shown in Figure 1-1.
- Spring Core : Bean 容器和支持工具
- Spring 上下文:
ApplicationContext、UI、验证、JNDI、企业 JavaBean(EJB)、远程处理和邮件支持 - Spring DAO:事务基础设施、Java 数据库连接(JDBC)和数据访问对象(DAO)支持
- Spring ORM: Hibernate、iBATIS 和 Java 数据对象(JDO)支持
- Spring AOP:一个符合 AOP 联盟的面向方面编程(AOP)实现
- Spring Web:基本的集成特性,比如多部分功能、通过 servlet 监听器的上下文初始化以及面向 Web 的应用上下文
- Spring Web MVC:基于 Web 的模型-视图-控制器(MVC)框架
![A315511_5_En_1_Fig1_HTML.jpg]()
图 1-1。
Overview of the Spring Framework, version 1.x
-
Spring 2.x : This is composed of the six modules shown in Figure 1-2. The Spring Context module is now included in Spring Core, and all Spring web components have been represented here by a single item.
- 通过使用新的基于 XML 模式的配置而不是 DTD 格式,简化了 XML 配置。值得注意的改进领域包括 bean 定义、AOP 和声明性事务。
- web 和门户使用的新 bean 作用域(请求、会话和全局会话)。
- 对 AOP 开发的注释支持。
- Java 持久性 API (JPA)抽象层。
- 完全支持异步 JMS 消息驱动的 POJOs(对于普通的旧 Java 对象)。
- 使用 Java 5+时的 JDBC 简化包括
SimpleJdbcTemplate。 - JDBC 命名参数支持(
NamedParameterJdbcTemplate)。 - Spring MVC 的表单标签库。
- Portlet MVC 框架介绍。
- 动态语言支持。Beans 可以用 JRuby、Groovy 和 BeanShell 编写。
- JMX 的通知支持和可控 MBean 注册。
- 为调度任务而引入的抽象。
- Java 5 注释支持,专门针对
@Transactional、@Required和@AspectJ。
![A315511_5_En_1_Fig2_HTML.jpg]()
图 1-2。
Overview of the Spring Framework, version 2.x
-
Spring 2.5.x:这个版本有以下特性:
- 名为
@Autowired的新配置注释,并支持 JSR-250 注释(@Resource、@PostConstruct、@PreDestroy) - 新八股注解:
@Component、@Repository、@Service、@Controller - 自动类路径扫描支持检测和连接用原型注释标注的类
- AOP 更新,包括新的 bean 切入点元素和 AspectJ 加载时编织
- 完整的 WebSphere 事务管理支持
- 除了 Spring MVC
@Controller注释之外,还添加了@RequestMapping、@RequestParam和@ModelAttribute注释,通过注释配置来支持请求处理 - 瓷砖 2 支架
- JSF 1.2 支持
- JAX-WS 2.0/2.1 支持
- 引入 Spring TestContext 框架,提供注释驱动和集成测试支持,不知道所使用的测试框架
- 将 Spring 应用上下文部署为 JCA 适配器的能力
- 名为
-
Spring 3.0.x : This is the first version of Spring based on Java 5 and is designed to take full advantage of Java 5 features such as generics, varargs, and other language improvements. This version introduces the Java-based
@Configurationmodel. The framework modules have been revised to be managed separately with one source tree per module JAR. This is abstractly depicted in Figure 1-3.- 支持 Java 5 的特性,比如泛型、varargs 和其他改进
- 对可调用、期货、
ExecutorService适配器和ThreadFactory集成的一流支持 - 框架模块现在可以单独管理,每个模块 JAR 有一个源代码树
- Spring 表达式语言(SpEL)简介
- 核心 Java 配置特性和注释的集成
- 通用类型转换系统和字段格式化系统
- 综合休息支持
- 新的 MVC XML 名称空间和额外的注释,如 Spring MVC 的
@CookieValue和@RequestHeaders - 验证增强和 JSR-303 (Bean 验证)支持
- 对 Java EE 6 的早期支持,包括
@Async/@Asynchronous注释、JSR-303、JSF 2.0、JPA 2.0 等等 - 支持嵌入式数据库,如 HSQL、H2 和 Derby
![A315511_5_En_1_Fig3_HTML.jpg]()
图 1-3。
Overview of the Spring Framework, version 3.0.x
-
Spring 3.1.x:这个版本有以下特性:
- 新缓存抽象
- Bean 定义概要可以用 XML 定义,并支持
@Profile注释 - 用于统一属性管理的环境抽象
- 常见 Spring XML 名称空间元素的注释等价物,例如
@ComponentScan、@EnableTransactionManagement、@EnableCaching、@EnableWebMvc、@EnableScheduling、@EnableAsync、@EnableAspectJAutoProxy、@EnableLoadTimeWeaving和@EnableSpringConfigured - 支持 Hibernate 4
- Spring TestContext 框架对
@Configuration类和 bean 定义概要的支持 - c:简化构造函数注入的命名空间
- 支持 Servlet 容器的基于 Servlet 3 代码的配置
- 无需
persistence.xml即可引导 JPAEntityManagerFactory - 添加到 Spring MVC 中的
Flash和RedirectAttributes,允许属性通过使用 HTTP 会话在重定向中存活 - URI 模板变量增强
- 能够用
@Valid注释 Spring MVC@RequestBody控制器方法参数 - 能够用
@RequestPart注释来注释 Spring MVC 控制器方法参数
-
Spring 3.2.x:这个版本有以下特性:
- 支持基于 Servlet 3 的异步请求处理。
- 新的 Spring MVC 测试框架。
- 新的 Spring MVC 注释
@ControllerAdvice和@MatrixVariable。 - 在
RestTemplate和@RequestBody参数中支持泛型类型。 - 杰克逊 JSON 2 支持。
- 支持瓷砖 3。
@RequestBody或@RequestPart参数现在可以跟一个Errors参数,这使得处理验证错误成为可能。- 能够通过使用 MVC 名称空间和 Java Config 配置选项来排除 URL 模式。
- 支持
@DateTimeFormat无 Joda 时间。 - 全局日期和时间格式。
- 跨框架的并发优化,最大限度地减少锁,并总体上改善了作用域/原型化 beans 的并发创建
- 新的基于梯度的构建系统。
- 迁移到 GitHub (
https://github.com/SpringSource/spring-framework)。 - 框架和第三方依赖项中改进的 Java SE 7/OpenJDK 7 支持。CGLIB 和 ASM 现在作为 Spring 的一部分被包含进来。除了 1.6 还支持 AspectJ 1.7。
-
Spring 4.0.x : This is a major Spring release and the first to fully support Java 8. Older versions of Java can be used, but the minimum requirement has been raised to Java SE6. Deprecated classes and methods were removed, and the module organization is pretty much the same, as depicted in Figure 1-4.
- 通过新的
www.spring.io/guides网站上的一系列入门指南改善了入门体验 - 从以前的 Spring 3 版本中删除了不推荐使用的包和方法
- Java 8 支持,将最低 Java 版本提高到 6 update 18
- Java EE 6 和更高版本现在被认为是 Spring Framework 4.0 的基准
- Groovy bean 定义 DSL,允许通过 Groovy 语法配置 bean 定义
- 核心容器、测试和一般 web 改进
- WebSocket、SockJS 和 STOMP 消息传递
![A315511_5_En_1_Fig4_HTML.jpg]()
图 1-4。
Overview of the Spring Framework, version 4.0.x
- 通过新的
-
Spring 4.2.x:这个版本有以下特性:
- 核心改进(例如,引入
@AliasFor并修改现有注释以利用它) - 完全支持 Hibernate ORM 5.0
- JMS 和 web 改进
- WebSocket 消息传递改进
- 测试改进,最显著的是引入了
@Commit来代替@Rollback(false),以及引入了AopTestUtils实用程序类,该类允许访问隐藏在 Spring 代理后面的底层对象
- 核心改进(例如,引入
-
Spring 4.3.x:这个版本有以下特性:
- 编程模型已经过优化。
- 核心容器(包含 ASM 5.1、
spring-core.jar中的 CGLIB 3.2.4 和 Objenesis 2.4)和 MVC 的显著改进。 - 添加了合成注释。
- Spring TestContext 框架需要 JUnit 4.12 或更高版本。
- 支持新的库,包括 Hibernate ORM 5.2、Hibernate Validator 5.3、Tomcat 8.5 和 9.0、Jackson 2.8 等等
-
Spring 5.0.x:这是一个主要版本。整个框架代码库基于 Java 8,截至 2016 年 7 月,完全兼容 Java 9。 2
- 不再支持 Portlet、Velocity、JasperReports、XMLBeans、JDO、番石榴、Tiles2 和 Hibernate3。
- XML 配置命名空间现在被流式传输到未版本化的架构;特定于版本的声明仍然受支持,但根据最新的 XSD 模式进行验证。
- 通过利用 Java 8 特性的全部功能,引入了全面的改进。
Resource抽象为防御性getFile访问提供了isFile指示器。- Spring 提供的
Filter实现中完整的 Servlet 3.1 签名支持。 - 支持 Protobuf 3.0。
- 支持 JMS 2.0+,JPA 2.1+。
- 引入 Spring Web Flow,这是一个建立在反应基础上的项目,是
Spring MVC的替代方案,这意味着它是完全异步和非阻塞的,旨在用于事件循环执行模型,而不是传统的每个请求一个线程的大型线程池执行模型(建立在 Project Reactor 3 之上)。 - 网络和核心模块适应了反应式编程模型。 4
- 在 Spring 测试模块中有很多改进。现在支持 JUnit 5,并且引入了新的注释来支持 Jupiter 编程和扩展模型,例如
@SpringJUnitConfig、@SpringJUnitWebConfig、@EnabledIf、@DisabledIf。 - 在 Spring TestContext 框架中支持并行测试执行。
反转控制还是注入依赖?
Spring 框架的核心是基于控制反转的原理。IoC 是一种外部化组件依赖关系的创建和管理的技术。考虑一个例子,其中类Foo依赖于类Bar的实例来执行某种处理。传统上,Foo通过使用new操作符创建一个Bar的实例,或者从某种工厂类中获得一个。使用 IoC 方法,某个外部进程在运行时向Foo提供一个Bar(或一个子类)的实例。这种行为,即运行时的依赖注入,导致马丁·福勒将 IoC 重新命名为更具描述性的依赖注入(DI)。第三章讨论了由 DI 管理的依赖关系的精确本质。
正如你将在第三章中看到的,当提到控制反转时,使用术语依赖注入总是正确的。在 Spring 的上下文中,您可以互换使用这两个术语,不会失去任何意义。
Spring 的 DI 实现基于两个核心 Java 概念:JavaBeans 和接口。当您使用 Spring 作为 DI 提供者时,您可以灵活地在应用中以不同的方式定义依赖配置(例如,XML 文件、Java 配置类、代码中的注释或新的 Groovy bean 定义方法)。JavaBean s(POJO)提供了创建 Java 资源的标准机制,这些资源可以通过多种方式进行配置,比如构造函数和 setter 方法。在第三章,你会看到 Spring 是如何使用 JavaBean 规范来构成其 DI 配置模型的核心的;事实上,任何 Spring 管理的资源都被称为 bean。如果你对 JavaBeans 不熟悉,请参考我们在第三章开头给出的快速入门。
接口和 DI 是互利的技术。显然,将应用设计和编码为接口有助于实现灵活的应用,但是将使用接口设计的应用连接在一起的复杂性非常高,并且给开发人员带来了额外的编码负担。通过使用 DI,您将在应用中使用基于接口的设计所需的代码量减少到几乎为零。同样,通过使用接口,您可以充分利用 DI,因为您的 beans 可以利用任何接口实现来满足它们的依赖性。接口的使用还允许 Spring 利用 JDK 动态代理(代理模式)为横切关注点提供强大的概念,比如 AOP。
在 DI 环境中,Spring 更像是一个容器而不是一个框架——为应用类的实例提供它们需要的所有依赖关系——但是它是以一种更少干扰的方式实现的。将 Spring 用于 DI 仅仅依赖于遵循类中的 JavaBeans 命名约定——没有特殊的类可以继承,也没有专有的命名模式可以遵循。如果有的话,您在使用 DI 的应用中所做的唯一改变是在 JavaBeans 上公开更多的属性,从而允许在运行时注入更多的依赖项。
依赖注入的发展
在过去的几年中,由于 Spring 和其他 DI 框架的流行,DI 在 Java 开发人员社区中得到了广泛的接受。同时,开发人员确信使用 DI 是应用开发中的最佳实践,并且使用 DI 的好处也很好理解。
当 Java 社区进程(JCP)在 2009 年采用 JSR-330(Java 的依赖注入)时,DI 的流行得到了认可。JSR-330 已经成为一个正式的 Java 规范请求,正如你所料,规范的领导者之一是 Rod Johnson——Spring 框架的创始人。在《JEE 6》中,JSR-330 成为整个技术堆栈中包含的规格之一。与此同时,EJB 架构(从 3.0 版本开始)也进行了巨大的改进;为了简化各种企业 JavaBeans 应用的开发,它采用了 DI 模型。
尽管我们将 DI 的完整讨论留到第三章进行,但还是有必要看看使用 DI 而不是更传统的方法的好处。
- 减少粘合代码:DI 最大的优点之一是它能够显著减少将应用组件粘合在一起所需编写的代码量。这些代码通常很简单,所以创建依赖关系只需要创建一个对象的新实例。然而,当您需要在 JNDI 存储库中查找依赖关系,或者当依赖关系不能被直接调用时,就像远程资源的情况一样,粘合代码会变得非常复杂。在这些情况下,通过提供自动 JNDI 查找和远程资源的自动代理,DI 可以真正简化粘合代码。
- 简化的应用配置:通过采用 DI,您可以大大简化应用的配置过程。您可以使用各种选项来配置那些可注入到其他类的类。您可以使用相同的技术向“注入器”表达依赖性要求,以便注入适当的 bean 实例或属性。此外,DI 使得将一个依赖项的实现替换为另一个实现变得更加简单。假设您有一个对 PostgreSQL 数据库执行数据操作的 DAO 组件,并且您想升级到 Oracle。使用 DI,您可以简单地重新配置业务对象上的适当依赖项,以使用 Oracle 实现而不是 PostgreSQL 实现。
- 在单个存储库中管理公共依赖关系的能力:使用公共服务(例如,数据源连接、事务和远程服务)的传统依赖关系管理方法,您可以在需要的地方(在依赖类中)创建依赖关系的实例(或从一些工厂类中查找)。这将导致依赖关系分散到应用中的各个类,改变它们可能会有问题。当您使用 DI 时,关于这些公共依赖项的所有信息都包含在一个存储库中,这使得依赖项的管理更加简单,并且不容易出错。
- 改进的可测试性:当你为 DI 设计你的类时,你可以很容易地替换依赖关系。这在测试应用时尤其方便。考虑一个执行一些复杂处理的业务对象;其中一部分,它使用 DAO 来访问存储在关系数据库中的数据。对于您的测试,您对测试 DAO 不感兴趣;您只是想用不同的数据集测试业务对象。在传统的方法中,业务对象负责获取 DAO 本身的实例,您很难测试这一点,因为您无法轻松地用返回测试数据集的模拟实现替换 DAO 实现。相反,您需要确保您的测试数据库包含正确的数据,并为您的测试使用完整的 DAO 实现。使用 DI,您可以创建一个返回测试数据集的 DAO 对象的模拟实现,然后您可以将它传递给业务对象进行测试。这种机制可以扩展到测试应用的任何一层,对于测试 web 组件尤其有用,在 web 组件中可以创建
HttpServletRequest和HttpServletResponse的模拟实现。 - 培养良好的应用设计:一般来说,为 DI 设计意味着针对接口进行设计。一个典型的面向注入的应用被设计成所有主要组件都被定义为接口,然后这些接口的具体实现被创建并使用 DI 容器连接在一起。在 DI 和基于 DI 的容器(如 Spring)出现之前,这种设计在 Java 中是可能的,但是通过使用 Spring,您可以免费获得大量 DI 特性,并且您可以专注于构建您的应用逻辑,而不是支持它的框架。
正如您从这个列表中看到的,DI 为您的应用提供了很多好处,但它也不是没有缺点。特别是,对于不太熟悉代码的人来说,DI 很难看出特定依赖项的什么实现被挂接到了哪些对象上。通常,只有当开发人员对 DI 缺乏经验时,这才是一个问题;在变得更有经验并遵循良好的 DI 编码实践(例如,将每个应用层中的所有可注入类放入同一个包中)之后,开发人员将能够很容易地发现全貌。在大多数情况下,巨大的好处远远超过这个小缺点,但是在规划应用时应该考虑到这一点。
超越依赖注入
Spring Core 本身,凭借其高级的 DI 功能,是一个有价值的工具,但是 Spring 真正擅长的地方在于它无数的附加特性,所有这些特性都是使用 DI 原则优雅地设计和构建的。Spring 为应用的所有层提供了特性,从帮助应用编程接口(API)到高级 MVC 功能。Spring 中这些特性的伟大之处在于,尽管 Spring 经常提供自己的方法,但您可以轻松地将它们与 Spring 中的其他工具集成,使这些工具成为 Spring 家族的一流成员。
支持 Java 9
Java 8 带来了许多 Spring Framework 5 支持的令人兴奋的特性,最显著的是 lambda 表达式和带有 Spring 回调接口的方法引用。Spring 5 的发布计划与 JDK 9 的最初发布计划一致,尽管 JDK 9 的发布截止日期已经推迟,但 Spring 5 仍按计划发布。估计 Spring 5.1 会全面拥抱 JDK 9。Spring 5 将利用 JDK 9 的特性,比如压缩字符串、ALPN 堆栈和新的 HTTP 客户端实现。虽然 Spring Framework 4.0 支持 Java 8,但兼容性仍然保持到 JDK 6 update 18。对于新的开发项目,建议使用更新的 Java 版本,如 7 或 8。Spring 5.0 需要 Java 8+,因为 Spring 开发团队已经将 Java 8 语言级别应用于整个框架代码库,但是 Spring 5 也构建在 JDK 9 之上,甚至从一开始就为 JDK 9 的广告功能提供全面的支持。
用 Spring 进行面向方面编程
AOP 提供了在一个地方实现横切逻辑——也就是说,应用于应用许多部分的逻辑——并让该逻辑自动应用于整个应用的能力。Spring 的 AOP 方法是创建目标对象的动态代理,并用配置好的通知编织对象来执行横切逻辑。根据 JDK 动态代理的本质,目标对象必须实现一个接口来声明应用 AOP 建议的方法。另一个流行的 AOP 库是 Eclipse AspectJ 项目, 5 ,它提供了更强大的特性,包括对象构造、类加载和更强的横切能力。然而,对于 Spring 和 AOP 开发人员来说,好消息是从 2.0 版本开始,Spring 提供了与 AspectJ 更紧密的集成。以下是一些亮点:
- 支持 AspectJ 风格的切入点表达式
- 支持
@AspectJ注释风格,同时仍然使用 Spring AOP 进行编织 - 支持在 AspectJ 中为 DI 实现的方面
- 支持 Spring 内的加载时编织
ApplicationContext
从 Spring Framework 版开始,@AspectJ可以通过 Java 配置启用注释支持。
两种 AOP 都有自己的位置,在大多数情况下,Spring AOP 足以满足应用的横切需求。但是,对于更复杂的需求,可以使用 AspectJ,Spring AOP 和 AspectJ 可以混合在同一个 Spring 驱动的应用中。
AOP 有很多应用。许多传统 AOP 示例中给出的一个典型例子是执行某种日志记录,但是 AOP 已经发现了远远超出普通日志记录应用的用途。事实上,在 Spring 框架本身中,AOP 被用于许多目的,尤其是在事务管理中。Spring AOP 在第五章中有完整的详细介绍,我们将向您展示 AOP 在 Spring 框架和您自己的应用中的典型用法,以及 AOP 的性能和传统技术比 AOP 更适合的领域。
Spring 表达式语言
表达式语言(EL)是一种允许应用在运行时操作 Java 对象的技术。然而,EL 的问题是不同的技术提供了它们自己的 EL 实现和语法。例如,Java Server Pages (JSP)和 Java Server Faces (JSF)都有自己的 EL,它们的语法是不同的。为了解决这个问题,统一表达式语言(EL)应运而生。
因为 Spring 框架发展如此之快,所以需要一种标准的表达式语言,可以在所有 Spring 框架模块以及其他 Spring 项目之间共享。因此,从 3.0 版本开始,Spring 引入了 Spring 表达式语言。SpEL 为在运行时计算表达式和访问 Java 对象和 Spring beans 提供了强大的功能。结果可以在应用中使用或者注入到其他 JavaBeans 中。
Spring 验证
验证是任何应用中的另一个大主题。理想的场景是,包含业务数据的 JavaBeans 中的属性验证规则可以以一致的方式应用,不管数据操作请求是从前端、批处理作业还是远程发起的(例如,通过 web 服务、RESTful web 服务或远程过程调用[RPC])。
为了解决这些问题,Spring 通过Validator接口提供了一个内置的验证 API。这个接口提供了一个简单而简洁的机制,允许您将验证逻辑封装到一个负责验证目标对象的类中。除了目标对象之外,validate 方法还采用了一个Errors对象,用于收集任何可能发生的验证错误。
Spring 还提供了一个方便的实用程序类ValidationUtils,它提供了调用其他验证器、检查常见问题(如空字符串)以及向所提供的Errors对象报告错误的便利方法。
受需求驱动,JCP 还开发了 JSR-303 (Bean Validation ),它提供了定义 Bean 验证规则的标准方法。例如,当将@NotNull注释应用于 bean 的属性时,它要求属性在能够持久存储到数据库之前不应该包含null值。
从 3.0 版本开始,Spring 为 JSR-303 提供了开箱即用的支持。要使用 API,只需声明一个LocalValidatorFactoryBean并将Validator接口注入到任何 Spring 管理的 beans 中。Spring 将为您解析底层实现。默认情况下,Spring 将首先寻找 Hibernate 验证器(hibernate.org/subprojects/validator),这是一个流行的 JSR 303 实现。很多前端技术(比如 JSF 2 和 Google Web Toolkit),包括 Spring MVC,也支持在用户界面中应用 JSR-303 验证。开发人员需要在用户界面和后端层编写相同验证逻辑的时代已经一去不复返了。第十章讨论细节。
从 Spring Framework 版开始,支持 1.1 版的 JSR-349 (Bean 验证)。
在 Spring 中访问数据
数据访问和持久性似乎是 Java 世界中讨论最多的话题。Spring 提供了与这些数据访问工具的完美集成。此外,Spring 使普通的 JDBC 成为许多项目的可行选择,其简化的包装 API 围绕着标准 API。Spring 的数据访问模块为 JDBC、Hibernate、JDO 和 JPA 提供了开箱即用的支持。
从 Spring Framework 版开始,iBATIS 支持被移除。MyBatis-Spring 项目提供了与 Spring 的集成,你可以在 http://mybatis.github.io/spring/ 找到更多信息。
然而,在过去的几年中,由于互联网和云计算的爆炸式增长,除了关系数据库之外,还开发了许多其他“专用”数据库。示例包括基于键值对来处理海量数据的数据库(通常称为 NoSQL)、图形数据库和文档数据库。为了帮助开发人员支持这些数据库,并且不使 Spring Data 访问模块复杂化,创建了一个名为 Spring Data 6 的独立项目。该项目被进一步划分为不同的类别,以支持更具体的数据库访问需求。
本书不包括 Spring 对非关系数据库的支持。如果您对这个主题感兴趣,前面提到的 Spring Data 项目是一个很好的地方。项目页面详细描述了它所支持的非关系数据库,以及这些数据库主页的链接。
Spring 中的 JDBC 支持使得在 JDBC 上构建应用成为现实,甚至对于更复杂的应用也是如此。对 Hibernate、JDO 和 JPA 的支持使得已经很简单的 API 变得更加简单,从而减轻了开发人员的负担。当使用 Spring APIs 通过任何工具访问数据时,您可以利用 Spring 出色的事务支持。你会在第九章中找到对此的全面讨论。
Spring 最好的特性之一是能够在应用中轻松混合和匹配数据访问技术。例如,您可能正在使用 Oracle 运行一个应用,使用 Hibernate 来处理大部分数据访问逻辑。然而,如果您想利用一些 Oracle 特有的特性,那么通过使用 Spring 的 JDBC API 来实现数据访问层的这一部分是很简单的。
Spring 中的对象/XML 映射
大多数应用需要集成其他应用或为其提供服务。一个常见的需求是定期或实时地与其他系统交换数据。就数据格式而言,XML 是最常用的。因此,您经常需要将 JavaBean 转换成 XML 格式,反之亦然。Spring 支持许多常见的 Java 到 XML 映射框架,并且像往常一样,不需要直接耦合到任何特定的实现。Spring 为 DI 到任何 Spring beans 的编组(将 JavaBeans 转换成 XML)和解组(将 XML 转换成 Java 对象)提供了通用接口。支持 Java Architecture for XML Binding(JAXB)、Castor、XStream、JiBX 和 XMLBeans 等常见库。在第十二章中,当我们讨论为 XML 格式的业务数据远程访问 Spring 应用时,您将看到如何在您的应用中使用 Spring 的对象/XML 映射(OXM)支持。
管理交易
Spring 为事务管理提供了一个优秀的抽象层,允许编程和声明性的事务控制。通过为事务使用 Spring 抽象层,您可以简化底层事务协议和资源管理器的更改。您可以从简单的、本地的、特定于资源的事务开始,然后转移到全局的、多源的事务,而不必更改您的代码。第九章详细介绍了交易。
简化和整合 JEE
随着像 Spring 这样的 DI 框架被越来越多的人所接受,许多开发人员选择使用 DI 框架来构建应用,以支持 JEE 的 EJB 方法。因此,JCP 社区也意识到 EJB 的复杂性。从 EJB 规范的 3.0 版本开始,API 被简化了,所以它现在包含了 DI 的许多概念。
然而,对于那些构建在 EJB 上的应用,或者需要在 JEE 容器中部署基于 Spring 的应用并利用应用服务器的企业服务(例如,Java Transaction API 的事务管理器、数据源连接池和 JMS 连接工厂)的应用,Spring 也为这些技术提供了简化的支持。对于 EJB,Spring 提供了一个简单的声明来执行 JNDI 查找并注入到 Spring beans 中。另一方面,Spring 也为将 Spring beans 注入 EJB 提供了简单的注释。
对于存储在 JNDI 可访问位置的任何资源,Spring 允许您去除复杂的查找代码,并在运行时将 JNDI 管理的资源作为依赖项注入到其他对象中。这样做的一个副作用是,您的应用变得与 JNDI 相分离,允许您在将来有更多的代码重用空间。
Web 层中的 MVC
尽管 Spring 几乎可以在任何环境中使用,从桌面到 web,但是它提供了丰富的类来支持基于 Web 的应用的创建。使用 Spring,当您选择如何实现 web 前端时,您拥有最大的灵活性。对于开发 web 应用,MVC 模式是最流行的实践。在最近的版本中,Spring 已经逐渐从一个简单的 web 框架发展成为一个成熟的 MVC 实现。首先,Spring MVC 中的视图支持是广泛的。除了对 JSP 和 Java 标准标记库(JSTL)的标准支持(由 Spring 标记库大大支持)之外,您还可以利用对 Apache Velocity、FreeMarker、Apache Tiles、Thymeleaf 和 XSLT 的完全集成的支持。此外,您将发现一组基本视图类,它们使得向您的应用添加 Microsoft Excel、PDF 和 JasperReports 输出变得简单。
在许多情况下,您会发现 Spring MVC 足以满足您的 web 应用开发需求。然而,Spring 也可以集成其他流行的 web 框架,比如 Struts、JSF、Atmosphere、Google Web Toolkit (GWT)等等。
在过去的几年中,web 框架技术发展迅速。用户需要更具响应性和交互性的体验,这导致了 Ajax 的兴起,成为开发富互联网应用(RIA)时广泛采用的技术。另一方面,用户还希望能够从任何设备访问他们的应用,包括智能手机和平板电脑。这就产生了对支持 HTML5、JavaScript 和 CSS3 的 web 框架的需求。在第十六章中,我们将讨论使用 Spring MVC 开发 web 应用。
WebSocket 支持
从 Spring Framework 4.0 开始,可以支持 JSR-356 (Java API for WebSocket)。WebSocket 定义了一个 API,用于在客户端和服务器之间创建持久连接,通常在 web 浏览器和服务器中实现。WebSocket 风格的开发为高效的全双工通信打开了大门,为高响应应用实现实时消息交换。WebSocket 支持的使用将在第十七章中详细介绍。
远程支持
在 Java 中访问或公开远程组件从来都不是最简单的工作。使用 Spring,您可以利用对各种远程技术的广泛支持来快速公开和访问远程服务。Spring 支持多种远程访问机制,包括 Java 远程方法调用(RMI)、JAX-WS、Caucho Hessian 和 Burlap、JMS、高级消息队列协议(AMQP)和 REST。除了这些远程协议之外,Spring 还提供了自己的基于 HTTP 的 invoker,它是基于标准 Java 序列化的。通过应用 Spring 的动态代理功能,您可以将一个远程资源的代理作为一个依赖注入到您的一个类中,这样就不需要将您的应用耦合到一个特定的远程实现,也减少了您需要为您的应用编写的代码量。我们将在第十二章讨论 Spring 中的远程支持。
邮件支持
发送电子邮件是许多应用的典型需求,在 Spring 框架中被给予了头等待遇。Spring 为发送电子邮件消息提供了一个简化的 API,非常适合 Spring DI 功能。Spring 支持标准的 JavaMail API。Spring 提供了在 DI 容器中创建原型消息的能力,并以此作为应用发送的所有消息的基础。这允许容易地定制邮件参数,例如主题和发件人地址。另外,为了定制消息体,Spring Integration 了模板引擎,比如 Apache Velocity 这允许邮件内容从 Java 代码中具体化。
作业调度支持
大多数重要的应用都需要某种调度能力。无论是向客户发送更新还是执行日常任务,调度代码在预先定义的时间运行的能力对于开发人员来说都是一个非常有价值的工具。Spring 提供了调度支持,可以满足大多数常见的场景。可以按照固定的时间间隔或通过使用 Unix cron 表达式来调度任务。另一方面,对于任务执行和调度,Spring 也集成了其他调度库。例如,在应用服务器环境中,Spring 可以将执行委托给许多应用服务器使用的 CommonJ 库。对于作业调度,Spring 还支持包括 JDK 定时器 API 和 Quartz 在内的库,Quartz 是一个常用的开源调度库。第十一章中详细介绍了 Spring 中的调度支持。
动态脚本支持
从 JDK 6 开始,Java 引入了动态语言支持,可以在 JVM 环境中执行用其他语言编写的脚本。例子包括 Groovy、JRuby 和 JavaScript。Spring 还支持在 Spring 驱动的应用中执行动态脚本,或者您可以定义一个用动态脚本语言编写的 Spring bean,并注入到其他 JavaBeans 中。Spring 支持的动态脚本语言包括 Groovy、JRuby 和 BeanShell。在第十四章,我们详细讨论了 Spring 对动态脚本的支持。
简化的异常处理
Spring 真正有助于减少您需要编写的重复性样板代码的一个领域是异常处理。在这方面,Spring 哲学的核心是检查异常在 Java 中被过度使用,框架不应该强迫你捕捉任何你不可能恢复的异常——这是我们完全同意的观点。实际上,许多框架的设计都是为了减少必须编写代码来处理检查异常的影响。然而,这些框架中的许多采用了坚持检查异常的方法,但是人为地降低了异常类层次结构的粒度。使用 Spring 您会注意到一件事,由于使用未检查的异常给开发人员带来了方便,异常层次结构非常细粒度。在整本书中,您将看到一些例子,在这些例子中,Spring 异常处理机制可以减少您必须编写的代码量,同时提高您在应用中识别、分类和诊断错误的能力。
Spring 项目
Spring 项目最吸引人的一点是社区中的活跃程度,以及 Spring 和其他项目(如 CGLIB、Apache Geronimo 和 AspectJ)之间的交叉影响。开源最受吹捧的好处之一是,如果项目明天就结束了,你将只剩下代码;但是让我们面对现实吧——你不希望留下一个 Spring 大小的代码库来支持和改进。出于这个原因,令人欣慰的是 Spring 社区是如此的完善和活跃。
Spring 的起源
正如本章前面提到的,Spring 的起源可以追溯到专家一对一:J2EE 设计和开发。在这本书里,Rod Johnson 展示了他自己的框架,叫做 Interface 21 Framework,他开发这个框架是为了在自己的应用中使用。这个框架被发布到开源世界,形成了我们今天所知的 Spring 框架的基础。Spring 很快通过了早期的测试和候选发布阶段,第一个正式的 1.0 发布版于 2004 年 3 月发布。从那以后,Spring 经历了巨大的发展,在撰写本文时,Spring Framework 的最新主要版本是 5.0。
Spring 社区
Spring 社区是我们遇到的所有开源项目中最好的之一。邮件列表和论坛总是很活跃,新功能的进展通常很快。开发团队真正致力于使 Spring 成为所有 Java 应用框架中最成功的,这从复制的代码质量中可以看出。正如我们已经提到的,Spring 还受益于与其他开源项目的良好关系,当您考虑到完整的 Spring 发行版所具有的大量依赖性时,这一事实是非常有益的。从用户的角度来看,Spring 最好的特性之一可能是发行版附带的优秀文档和测试套件。Spring 的几乎所有特性都提供了文档,使得新用户很容易掌握这个框架。Spring 提供的测试套件非常全面——开发团队为所有东西编写测试。如果他们发现了一个 bug,他们通过首先编写一个测试来突出这个 bug,然后让测试通过来修复这个 bug。修复 bug 和创建新特性不仅仅局限于开发团队!你可以通过官方的 GitHub 库( http://github.com/spring-projects )针对任何 Spring 项目组合通过 pull 请求贡献代码。此外,可以通过官方的 Spring JIRA ( https://jira.spring.io/secure/Dashboard.jspa )来创建和跟踪问题。这一切对你来说意味着什么?简而言之,这意味着您可以对 Spring 框架的质量充满信心,并且相信在可预见的未来,Spring 开发团队将继续改进已经非常优秀的框架。
Spring 工具套件
为了简化 Eclipse 中基于 Spring 的应用的开发,Spring 创建了 Spring IDE 项目。此后不久,Rod Johnson 创建的 Spring 背后的公司 SpringSource 创建了一个名为 Spring Tool Suite (STS)的集成工具,可以从 https://spring.io/tools 下载。虽然它曾经是一个付费产品,但现在该工具可以免费获得。该工具将 Eclipse IDE、Spring IDE、my Lyn(Eclipse 中基于任务的开发环境)、Maven for Eclipse、AspectJ 开发工具和许多其他有用的 Eclipse 插件集成到一个包中。在每个新版本中,都添加了更多的功能,例如 Groovy 脚本语言支持、图形化的 Spring 配置编辑器、用于 Spring Batch 和 Spring Integration 等项目的可视化开发工具,以及对 Pivotal tc Server 应用服务器的支持。
SpringSource 被 VMware 收购,并入 Pivotal Software。
除了基于 Java 的套件,还有一个 Groovy/Grails 工具套件,具有类似的功能,但目标是 Groovy 和 Grails 开发( http://spring.io/tools )。
Spring Security 项目
Spring Security 项目( http://projects.spring.io/spring-security ),以前称为 Spring 的 Acegi 安全系统,是 Spring 组合中的另一个重要项目。Spring Security 为 web 应用和方法级安全性提供了全面的支持。它与 Spring 框架和其他常用的身份验证机制紧密集成,如 HTTP 基本身份验证、基于表单的登录、X.509 证书和单点登录(SSO)产品(例如 CA SiteMinder)。它为应用资源提供基于角色的访问控制,并且在具有更复杂的安全需求(例如,数据隔离)的应用中,支持使用访问控制列表(ACL)。然而,Spring Security 主要用于保护 web 应用,我们将在第十六章中详细讨论。
Spring Boot
建立应用的基础是一项繁琐的工作。必须创建项目的配置文件,并且必须安装和配置附加工具(如应用服务器)。Spring Boot ( http://projects.spring.io/spring-boot/ )是一个 Spring 项目,它使得创建可以运行的独立的、生产级的基于 Spring 的应用变得很容易。Spring Boot 为不同类型的 Spring 应用提供了开箱即用的配置,这些配置打包在初始包中。例如,web-starter包包含一个预配置的、可轻松定制的 web 应用上下文,支持 Tomcat 7+、Jetty 8+和 Undertow 1.3 嵌入式 servlet 容器。
考虑到版本之间的兼容性,Spring Boot 还包装了 Spring 应用需要的所有依赖项。在撰写本文时,Spring Boot 的当前版本是 2.0.0.RELEASE
Spring Boot 将在第四章中介绍,作为 Spring 项目配置的一个替代方案,分配到后面章节的大多数项目将使用 Spring Boot 运行,因为它使开发和测试更加实用和快速。
Spring 批处理和集成
不用说,批处理作业执行和集成是应用中常见的用例。为了满足这种需求,也为了让这些领域的开发人员更加容易,Spring 创建了 Spring Batch 和 Spring Integration 项目。Spring Batch 为批处理作业实现提供了一个公共框架和各种策略,减少了大量样板代码。通过实现企业集成模式(EIP),Spring Integration 可以使 Spring 应用与外部系统的集成变得容易。我们将在第二十章中讨论细节。
许多其他项目
我们已经介绍了 Spring 的核心模块和 Spring 组合中的一些主要项目,但是还有许多其他项目是由社区的不同需求驱动的。一些例子包括 Spring Boot、Spring XD、Spring for Android、Spring Mobile、Spring Social 和 Spring AMQP。其中一些项目将在第 20 章中进一步讨论。更多细节,可以参考 Spring by Pivotal 网站( www.spring.io/projects )。
Spring 的替代品
回到我们之前关于开源项目数量的评论,你不应该惊讶于 Spring 不是唯一一个为构建应用提供依赖注入特性或完整端到端解决方案的框架。事实上,有太多的项目需要提及。本着开放的精神,我们在这里包括了对其中几个框架的简短讨论,但是我们相信这些平台中没有一个能提供像 Spring 中那样全面的解决方案。
JBoss Seam 框架
由 Gavin King(Hibernate ORM 库的创建者)创立的 Seam 框架( http://seamframework.org )是另一个成熟的基于 DI 的框架。它支持 web 应用前端开发(JSF)、业务逻辑层(EJB 3)和用于持久性的 JPA。如您所见,Seam 和 Spring 的主要区别在于 Seam 框架完全是基于 JEE 标准构建的。JBoss 还将 Seam 框架中的思想贡献给了 JCP,并成为了 JSR-299(Java EE 平台的上下文和依赖注入)。
谷歌指南
另一个流行的 DI 框架是 Google Guice ( http://code.google.com/p/google-guice )。由搜索引擎巨头 Google 牵头,Guice 是一个轻量级框架,专注于为应用配置管理提供 DI。它也是 JSR-330(Java 依赖注入)的参考实现。
微微容器
PicoContainer ( http://picocontainer.com )是一个非常小的 DI 容器,它允许您在应用中使用 DI,而不引入除 PicoContainer 之外的任何依赖。因为 PicoContainer 只不过是阿迪容器,您可能会发现随着应用的增长,您需要引入另一个框架,比如 Spring,在这种情况下,您最好从一开始就使用 Spring。然而,如果您需要的只是一个小的 DI 容器,那么 PicoContainer 是一个不错的选择,但是由于 Spring 将 DI 容器与框架的其他部分分开打包,您可以轻松地使用它,并为将来保留灵活性。
JEE 7 号集装箱 7 号
如前所述,DI 的概念被广泛采用,JCP 也认识到了这一点。当您为符合 JEE 7 (JSR-342)的应用服务器开发应用时,您可以在所有层使用标准的 DI 技术。
摘要
在这一章中,我们给了你一个 Spring 框架的高级视图,完成了对所有主要特性的讨论,并且我们引导你到书中详细讨论这些特性的相关章节。看完这一章,你应该明白 Spring 能为你做什么;剩下的就是看它怎么做了。在下一章中,我们将讨论启动和运行一个基本的 Spring 应用所需的所有信息。我们向您展示如何获得 Spring 框架,并讨论打包选项、测试套件和文档。此外,第二章介绍了一些基本的 Spring 代码,包括一个历史悠久的 Hello World 示例。
Footnotes 1
你可以从 SourceForge 网站: https://sourceforge.net/projects/springframework/files/springframework/ 下载包括 0.9 在内的旧版 Spring。
2
请记住,根据甲骨文在 http://openjdk.java.net/projects/jdk9/ 发布的时间表,Java 9 将于 2017 年 9 月正式向公众发布。
3
Project Reactor 实现了反应流 API 规范; https://projectreactor.io/见。
4
反应式编程是一种微架构风格,涉及智能路由和事件消费。这应该会导致非阻塞的应用,它们是异步的和事件驱动的,并且需要少量的线程在 JVM 中垂直伸缩,而不是通过集群水平伸缩。
5
6
http://projects.spring.io/spring-data
7
JEE8 发布日期已推迟至 2017 年底; https://jcp.org/en/jsr/detail?id=366见。
二、入门指南
通常,学习使用任何新的开发工具最困难的部分是弄清楚从哪里开始。通常情况下,当工具提供像 Spring 一样多的选择时,这个问题会更严重。幸运的是,如果您知道首先从哪里开始,那么开始使用 Spring 并不困难。在这一章中,我们将向你介绍所有你需要的基本知识,以便有一个良好的开端。具体来说,您将看到以下内容:
- 获取 Spring:第一个逻辑步骤是获取或构建 Spring JAR 文件。如果您想快速启动并运行,只需在您的构建系统中使用依赖管理片段,例如
http://projects.spring.io/spring-framework中提供的例子。然而,如果你想走在 Spring 开发的最前沿,可以从 Spring 的 GitHub 库中查看最新版本的源代码。 1 - Spring 包装选项:Spring 包装是模块化的;它允许您挑选要在应用中使用的组件,并在分发应用时只包含这些组件。Spring 有许多模块,但是根据应用的需要,您只需要这些模块的一个子集。每个模块在一个 JAR 文件中有其编译的二进制代码,以及相应的 Javadoc 和源 JAR。
- Spring 指南:新的 Spring 网站包括一个位于
http://spring.io/guides的指南部分。这些指南旨在为使用 Spring 构建任何开发任务的 Hello World 版本提供快速、实用的指导。这些指南还反映了最新的 Spring 项目发布和技术,为您提供了最新的可用示例。 - 测试套件和文档:Spring 社区成员最引以为豪的事情之一是他们全面的测试套件和文档集。测试是团队工作的一大部分。标准发行版提供的文档集也非常优秀。
- 抛开所有不好的双关语,我们认为开始使用任何新的编程工具的最好方式是直接进入并编写一些代码。我们给出一个简单的例子,这是一个所有人都喜欢的 Hello World 应用的完全基于 DI 的实现。如果你不能马上理解所有的代码,不要惊慌;本书后面会有完整的讨论。
如果你已经熟悉了 Spring 框架的基础,可以直接进入第三章,深入了解 Spring 中的 IoC 和 DI。然而,即使你熟悉 Spring 的基础知识,你也会发现本章的一些讨论很有趣,尤其是那些关于打包和依赖的讨论。
获得 Spring 框架
在开始任何 Spring 开发之前,您需要获得 Spring 库。您有几个选择来检索库:您可以使用您的构建系统来引入您想要使用的模块,或者您可以从 Spring GitHub 存储库中检出并构建代码。使用 Maven 或 Gradle 这样的依赖管理工具通常是最直接的方法;您所需要做的就是在配置文件中声明依赖关系,并让工具为您获取所需的库。
如果你有一个互联网连接,并结合使用一个智能 IDE(如 Eclipse 或 IntelliJ IDEA)使用一个构建工具(如 Maven 或 Gradle ),你可以自动下载 Javadoc 和库,这样你就可以在开发过程中访问它们。当您在构建项目时升级构建配置文件中的版本时,库和 Javadoc 也将被更新。
快速入门
访问 Spring Framework 项目页面 2 以获取您的构建系统的依赖管理片段,从而在您的项目中包含 Spring 的最新发布版本。您还可以为即将发布的版本或之前的版本使用里程碑/夜间快照。
当使用 Spring Boot 时,不需要指定您想要使用的 Spring 版本,因为 Spring Boot 提供了固执己见的“starter”项目对象模型(POM)文件来简化您的 Maven 配置和默认的 Gradle starter 配置。请记住,2.0.0.RELEASE 之前的 Spring Boot 版本使用 Spring 4.x 版本。
检查 GitHub 中的 Spring
如果您想在新特性进入快照之前了解它们,您可以直接从 Pivotal 的 GitHub 资源库中查看源代码。要查看 Spring 代码的最新版本,首先安装 Git,可以从 http://git-scm.com 下载。然后打开终端外壳并运行以下命令:
git clone git://github.com/spring-projects/spring-framework.git
查看项目根目录中的README.md文件,了解如何从源代码构建的完整细节和要求。
使用正确的 JDK
Spring 框架是用 Java 构建的,这意味着您需要能够在您的计算机上执行 Java 应用才能使用它。为此你需要安装 Java。当人们谈论 Java 应用开发时,有三个广泛使用的 Java 首字母缩写词。
- Java 虚拟机(JVM)是一种抽象机器。它是一个提供运行时环境的规范,Java 字节码可以在这个环境中执行。
- Java 运行时环境(JRE)用于提供运行时环境。它是物理上存在的 JVM 的实现。它包含一组 JVM 在运行时使用的库和其他文件。甲骨文 2010 年收购了太阳微系统公司;从那以后,新的版本和补丁被积极地提供。其他公司,比如 IBM,提供了他们自己的 JVM 实现。
- Java 开发工具包(JDK)包含 JRE、文档和 Java 工具。这是 Java 开发人员安装在他们机器上的东西。像 IntelliJ IDEA 或 Eclipse 这样的智能编辑器会要求您提供 JDK 的位置,以便可以在开发过程中加载和使用类和文档。
如果你使用的是像 Maven 或 Gradle 这样的构建工具(本书附带的源代码组织在一个 Gradle multimodule 项目中),它也需要一个 JVMMaven 和 Gradle 本身都是基于 Java 的项目。
最新稳定的 Java 版本是 Java 8,Java 9 计划于 2017 年 9 月 21 日发布。你可以从 https://www.oracle.com/ 下载 JDK。默认情况下,它将安装在您计算机上的某个默认位置,这取决于您的操作系统。如果您想从命令行使用 Maven 或 Gradle,您需要为 JDK 和 Maven/Gradle 定义环境变量,并将它们的可执行文件的路径添加到系统路径中。你可以在每个产品的官方网站和本书的附录中找到如何操作的说明。
第一章介绍了 Spring 版本和所需 JDK 版本的列表。书中介绍的 Spring 版本是 5.0.x。书中介绍的源代码是使用 Java 8 语法编写的,因此您至少需要 JDK 版本 8 才能编译和运行这些示例。
了解 Spring 包装
Spring 模块是简单的 JAR 文件,它封装了该模块所需的代码。了解每个模块的用途后,您可以选择项目中所需的模块,并将它们包含在代码中。从 Spring 版本 5.0.0.RELEASE 开始,Spring 附带了 21 个模块,打包成 21 个 JAR 文件。表 2-1 描述了这些 JAR 文件及其对应的模块。例如,实际的 JAR 文件名是spring-aop-5.0.0.RELEASE.jar,尽管为了简单起见,我们只包含了特定的模块部分(例如在aop中)。
表 2-1。
Spring modules
| 组件 | 描述 | | --- | --- | | `aop` | 这个模块包含了在应用中使用 Spring 的 AOP 特性所需的所有类。如果您计划在 Spring 中使用其他使用 AOP 的特性,比如声明式事务管理,那么您也需要在您的应用中包含这个 JAR。此外,支持与 AspectJ 集成的类被打包在这个模块中。 | | `aspects` | 这个模块包含了与 AspectJ AOP 库高级集成的所有类。例如,如果您在 Spring 配置中使用 Java 类,并且需要 AspectJ 风格的注释驱动的事务管理,那么您将需要这个模块。 | | `beans` | 这个模块包含了所有支持 Spring 操作 Spring beans 的类。这里的大多数类都支持 Spring 的 bean 工厂实现。例如,处理 Spring XML 配置文件和 Java 注释所需的类被打包到这个模块中。 | | `beans-groovy` | 该模块包含 Groovy 类,用于支持 Spring 对 Spring beans 的操作。 | | `context` | 这个模块包含了为 Spring Core 提供许多扩展的类。你会发现所有的类都需要使用 Spring 的`ApplicationContext`特性(在第五章中有所涉及),以及用于企业 JavaBeans (EJB)、Java 命名和目录接口(JNDI)和 Java 管理扩展(JMX)集成的类。该模块中还包含 Spring remoting 类,用于集成动态脚本语言(例如,JRuby、Groovy 和 BeanShell)、JSR-303 (Bean 验证)、调度和任务执行等的类。 | | `context` `-indexer` | 该模块包含一个索引器实现,提供对`META-INF/spring.components`中定义的候选对象的访问。核心类`CandidateComponentsIndex`不打算在外部使用。 | | `context-support` | 该模块包含对`spring-context`模块的进一步扩展。在用户界面方面,有一些支持邮件和集成模板引擎的类,比如 Velocity、FreeMarker 和 JasperReports。此外,这里还集成了各种任务执行和调度库,包括 CommonJ 和 Quartz。 | | `core` | 这是每个 Spring 应用都需要的主要模块。在这个 JAR 文件中,您将找到所有其他 Spring 模块共享的所有类(例如,用于访问配置文件的类)。此外,在这个 JAR 中,您将发现在 Spring 代码库中使用的非常有用的实用程序类的选择,并且您可以在自己的应用中使用它们。 | | `expression` | 这个模块包含了 Spring 表达式语言(SpEL)的所有支持类。 | | `instrument` | 这个模块包括 Spring 的用于 JVM 引导的工具代理。在 Spring 应用中使用 AspectJ 的加载时编织需要这个 JAR 文件。 | | `dbc` | 本模块包括 JDBC 支持的所有课程。所有需要数据库访问的应用都需要这个模块。用于支持数据源、JDBC 数据类型、JDBC 模板、本地 JDBC 连接等的类都打包在这个模块中。 | | `jms` | 该模块包括 JMS 支持的所有类。 | | `messaging` | 该模块包含来自 Spring Integration 项目的关键抽象,作为基于消息的应用的基础,并增加了对 STOMP 消息的支持。 | | `orm` | 该模块扩展了 Spring 的标准 JDBC 特性集,支持流行的 ORM 工具,包括 Hibernate、JDO、JPA 和数据映射器 iBATIS。这个 JAR 中的许多类都依赖于包含在`spring-jdbc` JAR 文件中的类,所以您肯定需要在您的应用中包含这些类。 | | `oxm` | 该模块提供了对对象/XML 映射(OXM)的支持。这个模块中包含了抽象 XML 编组和解组的类,以及对 Castor、JAXB、XMLBeans 和 XStream 等流行工具的支持。 | | `test` | Spring 提供了一组模拟类来帮助测试您的应用,并且这些模拟类中的许多都在 Spring 测试套件中使用,因此它们经过了很好的测试,并且使测试您的应用变得更加简单。当然,我们已经发现模拟`HttpServletRequest`和`HttpServletResponse`类在我们的 web 应用的单元测试中非常有用。另一方面,Spring 提供了与 JUnit 单元测试框架的紧密集成,在这个模块中提供了许多支持 JUnit 测试用例开发的类;例如,`SpringJUnit4ClassRunner`提供了一种在单元测试环境中引导 Spring`ApplicationContext`的简单方法。 | | `tx` | 该模块提供了支持 Spring 事务基础设施的所有类。您会发现事务抽象层中的类支持 Java 事务 API (JTA)以及与主要供应商的应用服务器的集成。 | | `web` | 这个模块包含了在 web 应用中使用 Spring 的核心类,包括自动加载一个`ApplicationContext`特性的类、文件上传支持类和一些用于执行重复任务的有用类,比如从查询字符串中解析整数值。 | | `web-reactive` | 该模块包含 Spring Web 反应模型的核心接口和类。 | | `web-` `mvc` | 这个模块包含了 Spring 自己的 MVC 框架的所有类。如果您为您的应用使用一个单独的 MVC 框架,您将不需要这个 JAR 文件中的任何类。Spring MVC 在第十六章中有更详细的介绍。 | | `websocket` | 这个模块提供了对 JSR-356 (Java API for WebSocket)的支持。 |为您的应用选择模块
如果没有 Maven 或 Gradle 这样的依赖管理工具,选择在应用中使用哪些模块可能会有点棘手。例如,如果您只需要 Spring 的 bean factory 和 DI 支持,您还需要几个模块,包括spring-core、spring-beans、spring-context和spring-aop。如果你需要 Spring 的 web 应用支持,那么你需要进一步添加spring-web等等。由于 Maven 的传递依赖支持等构建工具特性,所有必需的第三方库都将自动包含在内。
访问 Maven 仓库上的 Spring 模块
由 Apache Software Foundation 创建的 Maven 3 已经成为管理 Java 应用依赖性的最流行的工具之一,从开源到企业环境。Maven 是一个强大的应用构建、打包和依赖管理工具。它管理应用的整个构建周期,从资源处理和编译到测试和打包。还存在大量用于各种任务的 Maven 插件,例如更新数据库和将打包的应用部署到特定的服务器(例如,Tomcat、JBoss 或 WebSphere)。截至本文撰写之时,当前的 Maven 版本是 3.3.9。
几乎所有开源项目都支持通过 Maven 资源库分发库。最流行的是托管在 Apache 上的 Maven Central repository,您可以在 Maven Central web 站点上访问和搜索工件的存在和相关信息。 4 如果您将 Maven 下载并安装到您的开发机器中,您将自动获得对 Maven 中央存储库的访问权。其他一些开源社区(例如 JBoss 和 Pivotal 的 Spring)也为他们的用户提供了自己的 Maven 资源库。但是,为了能够访问这些存储库,您需要将存储库添加到 Maven 的设置文件或项目的 POM 文件中。
关于 Maven 的详细讨论不在本书的讨论范围之内,您可以随时参考在线文档或书籍,它们为您提供了关于 Maven 的详细参考。然而,由于 Maven 被广泛采用,因此值得一提 Maven 存储库上项目打包的典型结构。
组 ID、工件 ID、打包类型和版本标识每个 Maven 工件。例如,对于 log4j,组 ID 是log4j,工件 ID 是log4j,打包类型是jar。在此之下,定义了不同的版本。例如,对于版本 1.2.12,工件的文件名变成组 ID、工件 ID 和版本文件夹下的log4j-1.2.17.jar。Maven 配置文件是用 XML 编写的,必须遵守由 http://maven.apache.org/maven-v4_0_0.xsd 模式定义的 Maven 标准语法。项目的 Maven 配置文件的默认名称是om.xml,这里显示了一个示例文件:
<project
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.apress.prospring5.ch02</groupId>
<artifactId>hello-world</artifactId>
<packaging>jar</packaging>
<version>5.0-SNAPSHOT</version>
<name>hello-world</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.version>5.0.0.RELEASE</spring.version>
</properties>
<dependencies>
<!
-- https://mvnrepository.com/artifact/log4j/log4j -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
...
</plugin>
</plugins>
</build>
</project>
Maven 还定义了一个典型的标准项目结构,如图 2-1 所示。

图 2-1。
Typical Maven project structure
main目录包含应用的类(java目录)和配置文件(resources目录)。
test目录包含用于测试来自main目录的应用的类(java目录)和配置文件(resources目录)。
使用 Gradle 访问 Spring 模块
Maven 项目的标准结构以及工件的分类和组织非常重要,因为 Gradle 遵守相同的规则,甚至使用 Maven 中央存储库来检索工件。Gradle 是一个强大的构建工具,它放弃了臃肿的 XML 配置,转而使用 Groovy 的简单性和灵活性。在撰写本文时,Gradle 的当前版本是 4.0。 5 从 4.x 版本开始,Spring 团队已经改用 Gradle 进行每一款 Spring 产品的配置。这就是为什么这本书的源代码也可以使用 Gradle 来构建和执行。项目的 Gradle 配置文件的默认名称是build.gradle。这里显示了前面描述的pom.xml文件的等效物(嗯,它的一个版本):
group 'com.apress.prospring5.ch02'
version '5.0-SNAPSHOT'
apply plugin: 'java'
repositories {
mavenCentral()
}
ext{
springVersion = '5.0.0.RELEASE'
}
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
dependencies {
compile group: 'log4j', name: 'log4j', version: '1.2.17'
...
}
这样可读性更强,对吧?正如您所观察到的,工件是使用 Maven 前面介绍的组、工件和版本来标识的,但是属性名称不同。由于 Gradle 也不在本书的讨论范围之内,所以对它的讨论必须到此为止。
使用 Spring 文档
Spring 使其成为开发真正应用的开发人员的有用框架的一个方面是它丰富的编写良好的、准确的文档。在每个版本中,Spring Framework 的文档团队都努力确保所有的文档都由开发团队完成和润色。这意味着 Spring 的每个特性不仅在 Javadoc 中有完整的文档,而且在每个发行版中包含的 Spring 参考手册中也有涉及。如果您还没有熟悉 Spring Javadoc 和参考手册,现在就开始吧。这本书不是这些资源的替代品;相反,它是一个补充参考,演示了如何从头开始构建一个基于 Spring 的应用。
给 Hello World 注入 Spring
我们希望在本书的这一点上,您已经意识到 Spring 是一个可靠的、得到良好支持的项目,它具备了应用开发的所有优秀工具的素质。然而,还缺少一点——我们还没有向您展示任何代码。我们确信您渴望看到 Spring 的运行,因为我们不能再继续下去而不进入代码,让我们就这么做吧。如果您没有完全理解本节中的所有代码,请不要担心;随着全书的深入,我们会对所有主题进行更详细的讨论。
构建 Hello World 示例应用
现在,我们确信您熟悉传统的 Hello World 示例,但是如果您在过去 30 年中一直生活在月球上,以下代码片段展示了 Java 版本的辉煌:
package com.apress.prospring5.ch2;
public class HelloWorld {
public static void main(String... args) {
System.out.println("Hello World!");
}
}
就示例而言,这个非常简单——它完成了工作,但是不太具有可扩展性。如果我们想改变信息呢?如果我们希望以不同的方式输出消息,可能是标准错误而不是标准输出,或者是包含在 HTML 标记中而不是作为纯文本,该怎么办?我们将重新定义示例应用的需求,并说它必须支持简单、灵活的消息更改机制,并且必须易于更改呈现行为。在基本的 Hello World 示例中,只需适当地更改代码,就可以快速、轻松地完成这两项更改。然而,在更大的应用中,重新编译需要时间,并且需要再次对应用进行全面测试。更好的解决方案是将消息内容外部化,并在运行时读取它,可能是从下面的代码片段中显示的命令行参数读取:
package com.apress.prospring5.ch2;
public class HelloWorldWithCommandLine {
public static void main(String... args) {
if (args.length > 0) {
System.out.println(args[0]);
} else {
System.out.println("Hello World!");
}
}
}
这个例子实现了我们想要的——我们现在可以在不改变代码的情况下改变消息。但是,这个应用仍然有一个问题:负责呈现消息的组件也负责获取消息。更改获取消息的方式意味着更改呈现器中的代码。此外,我们仍然不能轻易改变渲染器;这样做意味着更改启动应用的类。
如果我们让这个应用更进一步(远离 Hello World 的基础),更好的解决方案是将呈现和消息检索逻辑重构到单独的组件中。另外,如果我们真的想让您的应用灵活,我们应该让这些组件实现接口,并使用这些接口定义组件和启动器之间的依赖关系。通过重构消息检索逻辑,我们可以用一个方法getMessage()定义一个简单的MessageProvider接口,如下面的代码片段所示:
package com.apress.prospring5.ch2.decoupled;
public interface MessageProvider {
String getMessage();
}
所有可以呈现消息的组件都实现了MessageRenderer接口,下面的代码片段描述了这样一个组件:
package com.apress.prospring5.ch2.decoupled;
public interface MessageRenderer {
void render();
void setMessageProvider(MessageProvider provider);
MessageProvider getMessageProvider();
}
如您所见,MessageRenderer接口声明了一个方法render(),以及一个 JavaBean 样式的方法setMessageProvider()。任何MessageRenderer实现都与消息检索分离,并将该职责委托给提供它们的MessageProvider实例。这里,MessageProvider是MessageRenderer的依赖。创建这些接口的简单实现很容易,如下面的代码片段所示:
package com.apress.prospring5.ch2.decoupled;
public class HelloWorldMessageProvider implements MessageProvider {
@Override
public String getMessage() {
return "Hello World!";
}
}
您可以看到,我们已经创建了一个简单的MessageProvider,它总是返回“Hello World!”作为信息。接下来显示的StandardOutMessageRenderer类同样简单:
package com.apress.prospring5.ch2.decoupled;
public class StandardOutMessageRenderer implements MessageRenderer {
private MessageProvider messageProvider;
@Override
public void render() {
if (messageProvider == null) {
throw new RuntimeException(
"You must set the property messageProvider of class:"
+ StandardOutMessageRenderer.class.getName());
}
System.out.println(messageProvider.getMessage());
}
@Override
public void setMessageProvider(MessageProvider provider) {
this.messageProvider = provider;
}
@Override
public MessageProvider getMessageProvider() {
return this.messageProvider;
}
}
现在剩下的就是重写入口类的main()方法。
package com.apress.prospring5.ch2.decoupled;
public class HelloWorldDecoupled {
public static void main(String... args) {
MessageRenderer mr = new StandardOutMessageRenderer();
MessageProvider mp = new HelloWorldMessageProvider();
mr.setMessageProvider(mp);
mr.render();
}
}
图 2-2 描绘了到目前为止构建的应用的抽象模式。

图 2-2。
A little more decoupled Hello World application
这里的代码相当简单。我们实例化了HelloWorldMessageProvider和StandardOutMessageRenderer的实例,尽管声明的类型分别是MessageProvider和MessageRenderer。这是因为我们只需要与编程逻辑中的接口提供的方法进行交互,而HelloWorldMessageProvider和StandardOutMessageRenderer已经分别实现了那些接口。然后,我们将MessageProvider传递给MessageRenderer并调用MessageRenderer.render()。如果我们编译并运行这个程序,我们会得到预期的“Hello World!”输出。现在,这个例子更像是我们正在寻找的,但是有一个小问题。改变MessageRenderer或MessageProvider接口的实现意味着代码的改变。为了解决这个问题,我们可以创建一个简单的工厂类,它从属性文件中读取实现类名,并代表应用实例化它们,如下所示:
package com.apress.prospring5.ch2.decoupled;
import java.util.Properties;
public class MessageSupportFactory {
private static MessageSupportFactory instance;
private Properties props;
private MessageRenderer renderer;
private MessageProvider provider;
private MessageSupportFactory() {
props = new Properties();
try {
props.load(this.getClass().getResourceAsStream("/msf.properties"));
String rendererClass = props.getProperty("renderer.class");
String providerClass = props.getProperty("provider.class");
renderer = (MessageRenderer) Class.forName(rendererClass).newInstance();
provider = (MessageProvider) Class.forName(providerClass).newInstance();
} catch (Exception ex) {
ex.printStackTrace();
}
}
static {
instance = new MessageSupportFactory();
}
public static MessageSupportFactory getInstance() {
return instance;
}
public MessageRenderer getMessageRenderer() {
return renderer;
}
public MessageProvider getMessageProvider() {
return provider;
}
}
这里的实现很简单,错误处理很简单,配置文件的名称是硬编码的,但是我们已经有了大量的代码。这个类的配置文件非常简单。
renderer.class=
com.apress.prospring5.ch2.decoupled.StandardOutMessageRenderer
provider.class=
com.apress.prospring5.ch2.decoupled.HelloWorldMessageProvider
要使用前面的实现,您必须再次修改 main 方法。
package com.apress.prospring5.ch2.decoupled;
public class HelloWorldDecoupledWithFactory {
public static void main(String... args) {
MessageRenderer mr =
MessageSupportFactory.getInstance().getMessageRenderer();
MessageProvider mp =
MessageSupportFactory.getInstance().getMessageProvider();
mr.setMessageProvider(mp);
mr.render();
}
}
在我们继续讨论如何将 Spring 引入这个应用之前,让我们快速回顾一下我们已经完成的工作。从简单的 Hello World 应用开始,我们定义了应用必须满足的两个附加要求。第一是改变消息应该简单,第二是改变呈现机制也应该简单。为了满足这些需求,我们使用了两个接口:MessageProvider和MessageRenderer。MessageRenderer接口依赖于MessageProvider接口的实现来检索要呈现的消息。最后,我们添加了一个简单的工厂类来检索实现类的名称,并根据需要实例化它们。
用 Spring 重构
前面展示的最后一个示例达到了示例应用的目标,但是仍然存在一些问题。第一个问题是,我们必须编写大量粘合代码来将应用拼凑在一起,同时保持组件的松散耦合。第二个问题是,我们仍然必须手动为MessageRenderer的实现提供一个MessageProvider的实例。我们可以通过使用 Spring 来解决这两个问题。要解决胶水代码太多的问题,我们可以把MessageSupportFactory类从应用中完全去掉,换成一个 Spring 接口,ApplicationContext。不用太担心这个接口;现在,只要知道 Spring 使用这个接口来存储关于 Spring 管理的应用的所有环境信息就足够了。这个接口扩展了另一个接口ListableBeanFactory,它充当任何 Spring 管理的 bean 实例的提供者。
package com.apress.prospring5.ch2;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class HelloWorldSpringDI {
public static void main(String args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext
("spring/app-context.xml");
MessageRenderer mr = ctx.getBean("renderer", MessageRenderer.class);
mr.render();
}
}
在前面的代码片段中,可以看到main()方法获得了一个ClassPathXmlApplicationContext的实例(应用配置信息从项目类路径中的文件spring/app-context.xml加载),类型为ApplicationContext,并由此通过使用ApplicationContext.getBean()方法获得了MessageRenderer实例。现在不要太担心getBean()方法;只要知道这个方法读取应用配置(本例中是一个 XML 文件),初始化 Spring 的ApplicationContext环境,然后返回配置好的 bean 6 实例。这个 XML 文件(app-context.xml)的用途与用于MessageSupportFactory的文件相同。
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="provider"
class="com.apress.prospring5.ch2.decoupled.HelloWorldMessageProvider"/>
<bean id="renderer"
class="com.apress.prospring5.ch2.decoupled.StandardOutMessageRenderer"
p:messageProvider-ref="provider"/>
</beans>
前一个文件显示了一个典型的 SpringApplicationContext配置。首先声明 Spring 的名称空间,默认名称空间是beans。beans名称空间用于声明需要由 Spring 管理的 bean,并声明它们的依赖需求(对于前面的例子,渲染器 bean 的messageProvider属性引用了提供者 bean)。Spring 将解析并注入这些依赖。
之后,我们声明 ID 为provider的 bean 和相应的实现类。当 Spring 在ApplicationContext初始化期间看到这个 bean 定义时,它将实例化这个类,并用指定的 ID 存储它。
然后用相应的实现类声明renderer bean。记住,这个 bean 依赖于MessageProvider接口来获取要呈现的消息。为了通知 Spring 关于 DI 的要求,我们使用了p名称空间属性。标签属性p:messageProvider-ref="provider"告诉 Spring,bean 的属性messageProvider应该被注入另一个 bean。要注入到属性中的 bean 应该引用 ID 为provider的 bean。当 Spring 看到这个定义时,它将实例化这个类,查找名为messageProvider的 bean 属性,并用 ID 为provider的 bean 实例注入它。
可以看到,在 Spring 的ApplicationContext初始化时,main()方法现在只是通过使用其类型安全的getBean()方法(传入 ID 和预期返回类型,这是MessageRenderer接口)获取MessageRenderer bean 并调用render();Spring 创建了MessageProvider实现,并将其注入到MessageRenderer实现中。注意,我们不需要对使用 Spring 连接在一起的类做任何修改。事实上,这些类没有引用 Spring,完全不知道它的存在。然而,情况并非总是如此。您的类可以实现 Spring 指定的接口,以多种方式与 DI 容器交互。
使用您的新 Spring 配置和修改过的main()方法,让我们看看它的运行情况。使用 Gradle,在终端中输入以下命令来构建项目和源代码的根目录:
gradle clean build copyDependencies
唯一需要在配置文件中声明的 Spring 模块是spring-context。Gradle 将自动引入该模块所需的任何可传递的依赖关系。在图 2-3 中,你可以看到spring-context.jar的传递依赖关系。

图 2-3。
spring-context and its transitive dependencies depicted in IntelliJ IDEA
前面的命令将从头开始构建项目,删除先前生成的文件,并将所有需要的依赖项复制到放置结果工件的相同位置,在build/libs下。当构建 JAR 时,这个路径值也将被用作添加到MANIFEST.MF的库文件的附加前缀。如果您不熟悉 Gradle JAR 构建配置和过程,请参阅章节 2 源代码(可在 Apress 网站上获得),特别是 Gradle hellor-world/build.properties文件,了解更多信息。最后,要运行 Spring DI 示例,请输入以下命令:
cd build/libs; java -jar hello-world-5.0-SNAPSHOT.jar
此时,您应该会看到 Spring 容器的启动过程生成的一些日志语句,后面是预期的 Hello World 输出。
使用注释的 Spring 配置
从 Spring 3.0 开始,开发 Spring 应用时不再需要 XML 配置文件。它们可以被注释和配置类代替。配置类是用@Configuration标注的 Java 类,包含 bean 定义(用@Bean标注的方法),或者通过用@ComponentScanning标注来配置它们自己以识别应用中的 bean 定义。这里显示了前面给出的app-context.xml文件的等效文件:
package com.apress.prospring5.ch2.annotated;
import com.apress.prospring5.ch2.decoupled.HelloWorldMessageProvider;
import com.apress.prospring5.ch2.decoupled.MessageProvider;
import com.apress.prospring5.ch2.decoupled.MessageRenderer;
import com.apress.prospring5.ch2.decoupled.StandardOutMessageRenderer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class HelloWorldConfiguration {
// equivalent to <bean id="provider" class=".."/>
@Bean
public MessageProvider provider() {
return new HelloWorldMessageProvider();
}
// equivalent to <bean id="renderer" class=".."/>
@Bean
public MessageRenderer renderer(){
MessageRenderer renderer = new StandardOutMessageRenderer();
renderer.setMessageProvider(provider());
return renderer;
}
}
必须修改main()方法,用另一个知道如何从配置类中读取 bean 定义的ApplicationContext实现来替换ClassPathXmlApplicationContext。那个班是AnnotationConfigApplicationContext。
package com.apress.prospring5.ch2.annotated;
import com.apress.prospring5.ch2.decoupled.MessageRenderer;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class HelloWorldSpringAnnotated {
public static void main(String... args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext
(HelloWorldConfiguration.class);
MessageRenderer mr = ctx.getBean("renderer", MessageRenderer.class);
mr.render();
}
}
这只是使用注释和配置类的配置的一个版本。如果没有 XML,Spring 配置就会变得非常灵活。在本书的后面部分,您将会学到更多,但是配置的重点是 Java 配置和注释。
Hello World 示例中定义的一些接口和类可能会在后面的章节中用到。尽管我们在本示例中展示了完整的源代码,但在以后的章节中,我们可能会展示更简洁的代码,尤其是在增量代码修改的情况下。代码已经被组织了一点,所有可以在 Spring future 示例中使用的类都被放在了com.apress.prospring5.ch2.decoupled和com.apress.prospring5.ch2.annotated包下,但是请记住,在一个真实的应用中,您会希望对代码进行适当的分层。
摘要
在这一章中,我们向您介绍了使用 Spring 所需的所有背景信息。我们向您展示了如何通过依赖管理系统和直接来自 GitHub 的当前开发版本开始使用 Spring。我们描述了 Spring 是如何打包的,以及 Spring 的每个特性所需要的依赖关系。使用这些信息,您可以做出明智的决定,您的应用需要哪些 Spring JAR 文件,以及您需要将哪些依赖项与您的应用一起分发。Spring 的文档、指南和测试套件为 Spring 用户提供了一个理想的基础来开始他们的 Spring 开发,所以我们花了一些时间来研究 Spring 提供了什么。最后,我们给出了一个例子,展示了如何使用 Spring DI 将传统的 Hello World 变成一个松散耦合、可扩展的消息呈现应用。要意识到的重要的一点是,在这一章中,我们只触及了 Spring DI 的表面,我们几乎没有对 Spring 整体做出任何改动。在下一章,我们来看看 Spring 的 IoC 和 DI。
Footnotes 1
在 http://github.com/spring-projects/spring-framework 找到 Spring 的 GitHub 库。
2
http://projects.spring.io/spring-framework
3
4
5
在官方项目网站上,你可以找到关于如何下载、安装和配置 Gradle for development 的详细说明: https://gradle.org/install 。
6
bean 是一个类的实例在 Spring 中的叫法。
三、Spring IoC 和 DI 简介
在第二章中,我们介绍了控制反转的基本原理。实际上,依赖注入是 IoC 的一种特殊形式,尽管你会经常发现这两个术语可以互换使用。在这一章中,我们将更详细地介绍 IoC 和 DI,形式化这两个概念之间的关系,并详细介绍 Spring 是如何融入其中的。
在定义了这两者并查看了 Spring 与它们的关系之后,我们将探索对 Spring 实现 DI 至关重要的概念。本章仅涵盖 Spring 的 DI 实现的基础知识;我们将在第四章讨论更高级的 DI 特性。更具体地说,本章涵盖以下主题:
- 控制概念的反转:在这一节中,我们将讨论各种 IoC,包括依赖注入和依赖查找。本节介绍了各种国际奥委会方法之间的差异以及每种方法的优缺点。
- Spring 中的控制反转:这一节着眼于 Spring 中可用的 IoC 功能以及它们是如何实现的。特别是,您将看到 Spring 提供的依赖注入服务,包括 setter、constructor 和方法注入。
- Spring 中的依赖注入:这一节涵盖了 Spring 对 IoC 容器的实现。对于 bean 定义和 DI 需求,
BeanFactory是应用与之交互的主要接口。然而,除了前几个,本章提供的样本代码的其余部分集中在使用 Spring 的ApplicationContext接口,它是BeanFactory的扩展,提供了更强大的功能。我们将在后面的章节中介绍BeanFactory和ApplicationContext的区别。 - 配置 Spring 应用上下文:这一章的最后一部分集中在使用 XML 和注释方法进行
ApplicationContext配置。Groovy 和 Java 配置将在第四章中进一步讨论。本节首先讨论 DI 配置,然后介绍由BeanFactory提供的附加服务,比如 bean 继承、生命周期管理和自动连接。
控制反转和依赖注入
其核心是 IoC,因此也是 DI,旨在提供一种更简单的机制,用于提供组件依赖关系(通常称为对象的协作者),并在整个生命周期中管理这些依赖关系。需要某些依赖关系的组件通常被称为依赖对象,或者在 IoC 的情况下,被称为目标。一般来说,IoC 可以分解为两个子类型:依赖注入和依赖查找。这些子类型被进一步分解成 IoC 服务的具体实现。从这个定义中,你可以清楚地看到,当我们在谈论 DI 时,我们总是在谈论 IoC,但当我们在谈论 IoC 时,我们并不总是在谈论 DI(例如,依赖查找也是 IoC 的一种形式)。
控制反转的类型
您可能想知道为什么有两种类型的 IoC,以及为什么这些类型被进一步分成不同的实现。这个问题似乎没有明确的答案;当然,不同的类型提供了一定程度的灵活性,但对我们来说,国际奥委会似乎更多的是新旧思想的混合。两种国际奥委会代表了这一点。依赖关系查找是一种更传统的方法,乍一看,Java 程序员似乎更熟悉它。依赖注入,虽然一开始看起来违反直觉,但实际上比依赖查找更灵活、更有用。使用依赖关系查找样式的 IoC,组件必须获取对依赖关系的引用,而使用依赖关系注入,依赖关系由 IoC 容器注入到组件中。依赖性查找有两种类型:依赖性拉取和上下文依赖性查找(CDL)。依赖注入也有两种常见的风格:构造器和设置器依赖注入。
对于本节的讨论,我们并不关心虚构的 IoC 容器是如何知道所有不同的依赖关系的,只是在某个时候,它会执行为每个机制描述的动作。
依赖拉动
对于 Java 开发人员来说,依赖拉是最熟悉的 IoC 类型。在依赖项提取中,依赖项是根据需要从注册表中提取的。任何曾经编写过代码来访问 EJB (2.1 或更早版本)的人都使用过依赖拉(即通过 JNDI API 来查找 EJB 组件)。图 3-1 显示了通过查找机制进行依赖拉动的场景。

图 3-1。
Dependency pull via JNDI lookup
Spring 还提供了依赖拉取机制来检索框架管理的组件;你可以在第二章中看到这一点。以下代码示例显示了基于 Spring 的应用中的典型依赖项拉查找:
package com.apress.prospring5.ch3;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class DependencyPull {
public static void main(String... args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext
("spring/app-context.xml");
MessageRenderer mr = ctx.getBean("renderer", MessageRenderer.class);
mr.render();
}
}
这种 IoC 不仅在基于 JEE 的应用(使用 EJB 2.1 或更早的版本)中很流行,这些应用大量使用 JNDI 查找来从注册中心获得依赖关系,而且对于在许多环境中使用 Spring 也很重要。
上下文依赖查找
在某些方面,上下文依赖查找(CDL)类似于依赖拉取,但是在 CDL 中,查找是针对管理资源的容器执行的,而不是从某个中央注册中心执行的,并且通常在某个设定点执行。图 3-2 为 CDL 机构。

图 3-2。
Contextualized dependency lookup
CDL 的工作原理是让组件实现一个类似于下面代码片段中的接口:
package com.apress.prospring5.ch3;
public interface ManagedComponent {
void performLookup(Container container);
}
通过实现这个接口,组件向容器发出信号,表明它想要获得一个依赖项。容器通常由底层应用服务器或框架(例如 Tomcat 或 JBoss)或框架(例如 Spring)提供。下面的代码片段展示了一个简单的Container接口,它提供了一个依赖查找服务:
package com.apress.prospring5.ch3;
public interface Container {
Object getDependency(String key);
}
当容器准备好将依赖关系传递给一个组件时,它依次调用每个组件上的performLookup()。然后,组件可以通过使用Container接口来查找它的依赖项,如下面的代码片段所示:
package com.apress.prospring5.ch3;
public class ContextualizedDependencyLookup
implements ManagedComponent {
private Dependency dependency;
@Override
public void performLookup(Container container) {
this.dependency = (Dependency) container.getDependency("myDependency");
}
@Override
public String toString() {
return dependency.toString();
}
}
构造函数依赖注入
当一个组件的依赖项在它的构造函数中被提供给它时,就会发生构造函数依赖注入。组件声明一个构造函数或一组构造函数,将其依赖项作为参数,IoC 容器在实例化发生时将依赖项传递给组件,如下面的代码片段所示:
package com.apress.prospring5.ch3;
public class ConstructorInjection {
private Dependency dependency;
public ConstructorInjection(Dependency dependency) {
this.dependency = dependency;
}
@Override
public String toString() {
return dependency.toString();
}
}
使用构造函数注入的一个显而易见的结果是,如果没有依赖关系,就无法创建对象;因此,它们是强制性的。
Setter 依赖注入
在 setter 依赖注入中,IoC 容器通过 JavaBean 样式的 setter 方法注入组件的依赖。组件的设置器公开了 IoC 容器可以管理的依赖关系。以下代码示例显示了一个典型的基于 setter 依赖注入的组件:
package com.apress.prospring5.ch3;
public class SetterInjection {
private Dependency dependency;
public void setDependency(Dependency dependency) {
this.dependency = dependency;
}
@Override
public String toString() {
return dependency.toString();
}
}
使用 setter 注入的一个显而易见的结果是,可以在没有依赖项的情况下创建对象,并且可以在以后通过调用 setter 来提供它们。
在容器中,setDependency()方法公开的依赖需求由 JavaBeans 风格的名称dependency引用。在实践中,setter 注入是最广泛使用的注入机制,也是实现起来最简单的 IoC 机制之一。
Spring 支持另一种类型的注入,称为 field injection,但这将在本章稍后介绍,届时您将学习使用@Autowire注释进行自动连接。
注入与查找
选择使用哪种类型的 IoC 注入或查找——通常不是一个困难的决定。在许多情况下,您使用的 IoC 类型是由您正在使用的容器决定的。例如,如果您正在使用 EJB 2.1 或更早的版本,您必须使用查找样式的 IoC(通过 JNDI)从 JEE 容器获得 EJB。在 Spring 中,除了最初的 bean 查找之外,您的组件及其依赖项总是使用注入式 IoC 连接在一起。
当您使用 Spring 时,您可以访问 EJB 资源,而不需要执行显式查找。Spring 可以充当查找和注入式 IoC 系统之间的适配器,从而允许您通过使用注入来管理所有资源。
真正的问题是:如果可以选择,您应该使用哪种方法,注入还是查找?答案肯定是注射。如果您查看前面代码示例中的代码,您可以清楚地看到,使用注入对组件代码没有任何影响。另一方面,依赖项拉代码必须主动获取对注册表的引用并与之交互以获取依赖项,使用 CDL 需要您的类实现特定的接口并手动查找所有依赖项。当你使用注入时,你的类所要做的最多就是允许依赖项通过使用构造函数或者设置函数被注入。
使用注入,您可以自由地使用与 IoC 容器完全分离的类,IoC 容器手动提供依赖对象及其合作者,而使用查找,您的类总是依赖于容器定义的类和接口。lookup 的另一个缺点是很难在容器之外测试你的类。使用注入,测试您的组件是很简单的,因为您可以通过使用适当的构造函数或设置器自己简单地提供依赖关系。
关于使用依赖注入和 Spring 进行测试的更完整的讨论,请参考第十三章。
基于查找的解决方案必然比基于注入的解决方案更复杂。虽然复杂性没什么可怕的,但我们质疑向依赖管理这样对应用至关重要的过程添加不必要的复杂性的有效性。
除了所有这些原因,选择注入而不是查找的最大原因是它使您的生活更容易。当您使用注入时,您编写的代码要少得多,而且您编写的代码很简单,并且通常可以由一个好的 IDE 自动执行。您会注意到,注入示例中的所有代码都是被动的,因为它不会主动尝试完成任务。您在注入代码中看到的最令人兴奋的事情是对象只存储在一个字段中;从任何注册表或容器中提取依赖项不涉及任何其他代码。因此,代码更简单,更不容易出错。被动代码比主动代码更容易维护,因为很少会出错。考虑以下取自 CDL 示例的代码:
public void performLookup(Container container) {
this.dependency = (Dependency) container.getDependency("myDependency");
}
在这段代码中,可能会出现很多错误:依赖项键可能会改变,容器实例可能是null,或者返回的依赖项可能是不正确的类型。我们称这个代码为有许多移动的部分,因为许多东西可能会损坏。使用依赖关系查找可能会分离应用的组件,但它会增加将这些组件重新耦合在一起以执行任何有用任务所需的额外代码的复杂性。
Setter 注入与构造函数注入
现在我们已经确定了 IoC 的哪种方法更好,您仍然需要选择是使用 setter 注入还是构造函数注入。当您在使用组件之前必须拥有依赖类的实例时,构造函数注入特别有用。包括 Spring 在内的许多容器都提供了一种机制,确保在使用 setter 注入时定义了所有的依赖项,但是通过使用构造函数注入,您以一种与容器无关的方式断言了对依赖项的需求。构造函数注入也有助于实现不可变对象的使用。
Setter 注入在各种情况下都很有用。如果组件向容器公开其依赖关系,但又乐于提供自己的默认值,setter 注入通常是实现这一点的最佳方式。setter 注入的另一个好处是,它允许在一个接口上声明依赖关系,尽管这并不像您最初认为的那样有用。考虑一个带有一个业务方法的典型业务接口,defineMeaningOfLife()。如果除了这个方法之外,您还为注入定义了一个 setter,比如setEncylopedia(),那么您就要求所有的实现都必须使用或者至少意识到百科全书的依赖性。但是,您不需要在业务接口中定义setEncylopedia()。相反,您可以在实现业务接口的类中定义方法。以这种方式编程时,所有最新的 IoC 容器,包括 Spring,都可以在业务接口方面与组件一起工作,但仍然提供实现类的依赖性。这方面的一个例子可以稍微澄清这个问题。考虑下面代码片段中的业务接口:
package com.apress.prospring5.ch3;
public interface Oracle {
String defineMeaningOfLife();
}
注意,业务接口没有为依赖注入定义任何设置器。该接口可以如下面的代码片段所示实现:
package com.apress.prospring5.ch3;
public class BookwormOracle implements Oracle {
private Encyclopedia encyclopedia;
public void setEncyclopedia(Encyclopedia encyclopedia) {
this.encyclopedia = encyclopedia;
}
@Override
public String defineMeaningOfLife() {
return "Encyclopedias are a waste of money - go see the world instead";
}
}
如您所见,BookwormOracle类不仅实现了 Oracle 接口,还定义了依赖注入的 setter。Spring 非常适合处理这样的结构。完全没有必要定义业务接口上的依赖关系。使用接口来定义依赖关系的能力是 setter 注入的一个经常被吹捧的好处,但是实际上,您应该努力保持 setter 只用于接口之外的注入。除非您完全确定特定业务接口的所有实现都需要特定的依赖关系,否则让每个实现类定义自己的依赖关系,并为业务方法保留业务接口。
尽管您不应该总是在业务接口中放置依赖项的 setter,但是在业务接口中放置配置参数的 setter 和 getter 是一个好主意,这使得 setter 注入成为一个有价值的工具。我们认为配置参数是依赖关系的特例。当然,您的组件依赖于配置数据,但是配置数据与您到目前为止看到的依赖类型有很大的不同。我们将很快讨论这些差异,但是现在,考虑下面代码片段中显示的业务接口:
package com.apress.prospring5.ch3;
public interface NewsletterSender {
void setSmtpServer(String smtpServer);
String getSmtpServer();
void setFromAddress(String fromAddress);
String getFromAddress();
void send();
}
通过电子邮件发送一组时事通讯的类实现了NewsletterSender接口。send()方法是唯一的业务方法,但是请注意,我们已经在接口上定义了两个 JavaBean 属性。当我们刚刚说不应该在业务接口中定义依赖关系时,我们为什么要这样做呢?原因是这些值,SMTP 服务器地址和发送电子邮件的地址,在实际意义上并不依赖;相反,它们是影响所有NewsletterSender接口功能实现的配置细节。这里的问题是:配置参数和任何其他类型的依赖之间有什么区别?在大多数情况下,您可以清楚地看到是否应该将依赖项归类为配置参数,但是如果您不确定,请查找指向配置参数的以下三个特征:
- 配置参数是被动的。在前面代码片段中描述的
NewsletterSender示例中,SMTP 服务器参数是被动依赖的一个示例。被动依赖不直接用于执行动作;相反,它们在内部使用或被另一个依赖项用来执行它们的操作。在第二章的MessageRenderer例子中,MessageProvider依赖不是被动的;它执行了MessageRenderer完成任务所必需的功能。 - 配置参数通常是信息,而不是其他组件。这意味着配置参数通常是组件完成其工作所需的一些信息。显然,SMTP 服务器是
NewsletterSender所需的一条信息,但是MessageProvider实际上是MessageRenderer正常运行所需的另一个组件。 - 配置参数通常是简单值或简单值的集合。这确实是前两点的副产品,但配置参数通常是简单的值。在 Java 中,这意味着它们是一个原语(或相应的包装类)或一个
String或这些值的集合。简单值一般是被动的。这意味着除了操作它所代表的数据,你不能对一个String做太多事情;您几乎总是将这些值用于信息目的,例如,int值表示网络套接字应该监听的端口号,或者String值表示电子邮件程序应该通过其发送消息的 SMTP 服务器。
当考虑是否在业务接口中定义配置选项时,还要考虑配置参数是适用于业务接口的所有实现还是仅适用于一个实现。例如,在实现NewsletterSender的情况下,很明显所有的实现都需要知道发送电子邮件时使用哪个 SMTP 服务器。但是,我们可能会选择保留标记是否从业务接口发送安全电子邮件的配置选项,因为不是所有的电子邮件 API 都支持这一点,并且可以正确地假设许多实现根本不会考虑安全性。
回想一下,在第二章中,选择它来定义业务目的中的依赖关系。这只是为了举例说明,无论如何都不应被视为最佳实践。
Setter 注入还允许您动态交换不同实现的依赖关系,而无需创建父组件的新实例。Spring 的 JMX 支持使这成为可能。setter 注入的最大好处可能是它是注入机制中侵入性最小的。
一般来说,您应该根据您的用例来选择注入类型。基于 Setter 的注入允许在不创建新对象的情况下交换依赖项,还允许您的类选择适当的默认值,而无需显式注入对象。当您希望确保依赖关系被传递给组件时,以及为不可变对象进行设计时,构造函数注入是一个很好的选择。请记住,虽然构造函数注入可以确保向组件提供所有依赖关系,但大多数容器也提供了一种机制来确保这一点,但可能会导致将代码耦合到框架的成本。
Spring 控制反转
如前所述,控制反转是 Spring 的主要功能。Spring 实现的核心是基于依赖注入,尽管也提供了依赖查找特性。当 Spring 自动向依赖对象提供协作者时,它使用依赖注入来实现。在基于 Spring 的应用中,最好使用依赖注入将合作者传递给依赖对象,而不是让依赖对象通过查找获得合作者。图 3-3 为 Spring 的依赖注入机构。尽管依赖注入是将协作者和依赖对象连接在一起的首选机制,但是您需要依赖查找来访问依赖对象。在许多环境中,Spring 不能通过使用依赖注入来自动连接所有的应用组件,您必须使用依赖查找来访问组件的初始集合。例如,在独立的 Java 应用中,您需要在main()方法中引导 Spring 的容器,并获得依赖关系(通过ApplicationContext接口)以便以编程方式进行处理。然而,当您使用 Spring 的 MVC 支持构建 web 应用时,Spring 可以通过自动将整个应用粘合在一起来避免这种情况。只要有可能在 Spring 中使用依赖注入,就应该这样做;否则,您可以依靠依赖关系查找功能。在本章的过程中,你将会看到两者都在起作用的例子,当它们第一次出现时,我们将会指出它们。

图 3-3。
Spring’s dependency injection mechanism
Spring 的 IoC 容器的一个有趣特性是,它能够充当自己的依赖注入容器和外部依赖查找容器之间的适配器。我们将在本章后面讨论这个特性。
Spring 支持构造器和设置器注入,并通过大量有用的附加功能支持标准的 IoC 特性集,使您的生活更加轻松。
本章的其余部分介绍了 Spring 的 DI 容器的基础知识,并提供了大量的例子。
Spring 中的依赖注入
Spring 对依赖注入的支持是全面的,正如你将在第四章中看到的,超越了我们到目前为止讨论过的标准 IoC 特性集。本章的其余部分讲述了 Spring 的依赖注入容器的基础知识,包括 setter、constructor 和 Method Injection,以及如何在 Spring 中配置依赖注入的细节。
大豆和大豆工厂
Spring 的依赖注入容器的核心是BeanFactory接口。BeanFactory负责管理组件,包括它们的依赖关系以及它们的生命周期。在 Spring 中,术语 bean 用来指由容器管理的任何组件。通常,您的 bean 在某种程度上遵循 JavaBeans 规范,但这不是必需的,尤其是如果您计划使用构造函数注入来将 bean 连接在一起。
如果您的应用只需要 DI 支持,您可以通过BeanFactory接口与 Spring DI 容器进行交互。在这种情况下,您的应用必须创建一个实现BeanFactory接口的类的实例,并用 bean 和依赖信息对其进行配置。完成之后,您的应用可以通过BeanFactory访问 beans 并继续处理。
在某些情况下,所有这些设置都是自动处理的(例如,在一个 web 应用中,Spring 的ApplicationContext将在应用启动时通过 Spring 提供的在web.xml描述符文件中声明的ContextLoaderListener类由 web 容器引导)。但是在许多情况下,您需要自己编写设置代码。本章中的所有例子都需要手动设置BeanFactory实现。
尽管可以通过编程来配置BeanFactory,但更常见的是使用某种配置文件在外部配置它。在内部,bean 配置由实现BeanDefinition接口的类的实例来表示。bean 配置不仅存储关于 bean 本身的信息,还存储关于它所依赖的 bean 的信息。对于任何也实现了BeanDefinitionReader接口的BeanFactory实现类,您可以从配置文件中读取BeanDefinition数据,使用PropertiesBeanDefinitionReader或XmlBeanDefinitionReader. PropertiesBeanDefinitionReader从属性文件中读取 bean 定义,而XmlBeanDefinitionReader从 XML 文件中读取。
因此,您可以在BeanFactory内识别您的 beans 可以为每个 bean 分配一个 ID 和/或名称。也可以实例化没有任何 ID 或名称的 bean(称为匿名 bean ),或者作为另一个 bean 中的内部 bean。每个 bean 至少有一个名称,但是可以有任意数量的名称(其他名称用逗号分隔)。第一个名称之后的任何名称都被视为同一个 bean 的别名。您使用 bean IDs 或名称从BeanFactory中检索 bean,并建立依赖关系(即 bean X 依赖于 bean Y)。
beanfactory 实现
对BeanFactory接口的描述可能看起来过于复杂,但实际上并非如此。看一个简单的例子。假设您有一个模仿神谕的实现,它可以告诉您生命的意义。
//interface
package com.apress.prospring5.ch3;
public interface Oracle {
String defineMeaningOfLife();
}
//implementation
package com.apress.prospring5.ch3;
public class BookwormOracle implements Oracle {
private Encyclopedia encyclopedia;
public void setEncyclopedia(Encyclopedia encyclopedia) {
this.encyclopedia = encyclopedia;
}
@Override
public String defineMeaningOfLife() {
return "Encyclopedias are a waste of money - go see the world instead";
}
}
现在让我们看看,在一个独立的 Java 程序中,如何初始化 Spring 的BeanFactory并获取oracle bean 进行处理。代码如下:
package com.apress.prospring5.ch3;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.core.io.ClassPathResource;
public class XmlConfigWithBeanFactory {
public static void main(String... args) {
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader rdr = new XmlBeanDefinitionReader(factory);
rdr.loadBeanDefinitions(new
ClassPathResource("spring/xml-bean-factory-config.xml"));
Oracle oracle = (Oracle) factory.getBean("oracle");
System.out.println(oracle.defineMeaningOfLife());
}
}
在前面的代码示例中,您可以看到我们正在使用DefaultListableBeanFactory,它是 Spring 提供的两个主要BeanFactory实现之一,并且我们正在通过使用XmlBeanDefinitionReader从 XML 文件中读取BeanDefinition信息。一旦创建并配置了BeanFactory实现,我们就通过使用名称oracle来检索oracle bean,这个名称是在 XML 配置文件中配置的。
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="oracle"
name="wiseworm"
class="com.apress.prospring5.ch3.BookwormOracle"/>
</beans>
在声明 Spring XSD 位置时,最好不要包含版本号。Spring 已经为您处理了这个解决方案,因为版本化的 XSD 文件是通过spring.schemas文件中的指针配置的。该文件驻留在项目中定义为依赖项的spring-beans模块中。这也避免了您在升级到 Spring 的新版本时必须修改所有的 bean 文件。
前面的文件声明了一个 Spring bean,给它一个 IDoracle和一个名称wiseworm,并告诉 Spring 底层实现类是com.apress.prospring4.ch3.BookwormOracle。暂时不用太担心配置;我们将在后面的章节中讨论细节。
定义好配置后,运行前面代码示例中显示的程序;您将在控制台输出中看到由defineMeaningOfLife()方法返回的短语。
除了XmlBeanDefinitionReader,Spring 还提供了PropertiesBeanDefinitionReader,它允许您通过使用属性而不是 XML 来管理您的 bean 配置。虽然属性对于小而简单的应用来说是理想的,但是当您处理大量的 beans 时,它们会很快变得很麻烦。因此,除了最普通的应用之外,最好对所有应用都使用 XML 配置格式。
当然,您可以自由定义自己的BeanFactory实现,尽管要知道这样做相当复杂;您需要实现更多的接口,而不仅仅是BeanFactory来获得与所提供的BeanFactory实现相同的功能。如果您想要做的只是定义一个新的配置机制,那么通过开发一个扩展了DefaultListableBeanFactory类的类来创建您的定义阅读器,该类实现了BeanFactory接口。
应用上下文
在 Spring 中,ApplicationContext接口是对BeanFactory的扩展。除了 DI 服务,ApplicationContext还提供其他服务,例如事务和 AOP 服务、国际化消息源(i18n)和应用事件处理等等。在开发基于 Spring 的应用时,建议您通过ApplicationContext接口与 Spring 交互。Spring 支持通过手动编码(手动实例化并加载适当的配置)或通过ContextLoaderListener在 web 容器环境中引导ApplicationContext。从这一点开始,本书中的所有示例代码都使用了ApplicationContext及其实现。
配置应用上下文
讨论了 IoC 和 DI 的基本概念,并通过一个使用 Spring 的BeanFactory接口的简单例子,让我们深入了解如何配置 Spring 应用的细节。在接下来的小节中,我们将介绍配置 Spring 应用的各个方面。具体来说,我们应该把注意力集中在ApplicationContext界面上,它比传统的BeanFactory界面提供了更多的配置选项。
设置 Spring 配置选项
在我们深入配置 Spring 的ApplicationContext的细节之前,让我们看一下在 Spring 中定义应用配置的可用选项。最初,Spring 支持通过属性或 XML 文件定义 beans。自从 JDK 5 的发布和 Spring 对 Java 注释的支持,Spring(从 Spring 2.5 开始)在配置ApplicationContext时也支持使用 Java 注释。那么,XML 和注释哪个更好呢?关于这个话题有很多争论,你可以在网上找到很多讨论。 1 没有确定的答案,每种方法都有利弊。使用 XML 文件可以将所有配置从 Java 代码中具体化,而注释允许开发人员从代码中定义和查看 DI 设置。Spring 还支持在单个ApplicationContext中混合使用这两种方法。一种常见的方法是在 XML 文件中定义应用基础设施(例如,数据源、事务管理器、JMS 连接工厂或 JMX ),同时在注释中定义 DI 配置(可注入 bean 和 bean 的依赖项)。然而,无论你选择哪一个选项,坚持下去,并在整个开发团队中清楚地传递信息。就使用的风格达成一致,并在整个应用中保持一致,将使正在进行的开发和维护活动变得更加容易。
为了便于您理解 XML 和注释配置,我们在适当的时候提供了 XML 和注释并列的示例代码,但是本书的重点将放在注释和 Java 配置上,因为 XML 已经在本书以前的版本中讨论过了。
基本配置概述
对于 XML 配置,您需要声明应用所需的 Spring 提供的名称空间库。下面的配置示例显示了最基本的示例,它只声明了 bean 的名称空间,供您定义 Spring beans。对于整个示例中的 XML 风格的配置,我们将这个配置文件称为app-context-xml.xml。
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
</beans>
除了 beans 之外,Spring 还为不同的目的提供了大量其他名称空间。一些例子包括ApplicationContext配置的上下文、aopAOP 支持的上下文和tx事务支持的上下文。名称空间包含在相应的章节中。
要在应用中使用 Spring 的注释支持,需要在 XML 配置中声明下一个配置示例中显示的标记。我们将这个配置文件称为app-context-annotation.xml,用于在整个示例中支持注释的 XML 配置。
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan
base-package="com.apress.prospring5.ch3.annotation"/>
</beans>
<context:component-scan>标签告诉 Spring 扫描带有@Component、@Controller、@Repository和@Service注释的可注入 beans 的代码,并支持指定包(及其所有子包)下的@Autowired、@Inject和@Resource注释。在<context:component-scan>标签中,可以使用逗号、分号或空格作为分隔符来定义多个包。此外,该标签支持包含和排除组件扫描,以实现更细粒度的控制。例如,考虑以下配置示例:
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan
base-package="com.apress.prospring5.ch3.annotation">
<context:exclude-filter type="assignable"
expression="com.example.NotAService"/>
</context:component-scan>
</beans>
前一个标签告诉 Spring 按照指定的方式扫描包,但是忽略了可分配给表达式中指定的类型的类(可以是类,也可以是接口)。除了排除过滤器,您还可以使用包含过滤器。对于类型,您可以使用 annotation、regex、assignable、AspectJ 或 custom(使用您自己的实现org.springframework.core.type.filter.TypeFilter的过滤器类)作为过滤标准。表达式格式取决于您指定的类型。
声明 Spring 组件
在开发了某种服务类并希望在基于 Spring 的应用中使用它之后,您需要告诉 Spring 这些 bean 可以注入到其他 bean 中,并让 Spring 为您管理它们。考虑第二章中的例子,其中MessageRender输出消息,并依赖MessageProvider提供要呈现的消息。以下代码示例描述了这两个服务的接口和实现:
package com.apress.prospring5.ch2.decoupled;
//renderer interface
public interface MessageRenderer {
void render();
void setMessageProvider(MessageProvider provider);
MessageProvider getMessageProvider();
}
// rendered implementation
public class StandardOutMessageRenderer
implements MessageRenderer {
private MessageProvider messageProvider;
@Override
public void render() {
if (messageProvider == null) {
throw new RuntimeException(
"You must set the property messageProvider of class:"
+ StandardOutMessageRenderer.class.getName());
}
System.out.println(messageProvider.getMessage());
}
@Override
public void setMessageProvider(MessageProvider provider) {
this.messageProvider = provider;
}
@Override
public MessageProvider getMessageProvider() {
return this.messageProvider;
}
}
//provider interface
public interface MessageProvider {
String getMessage();
}
//provider implementation
public class HelloWorldMessageProvider implements MessageProvider {
@Override
public String getMessage() {
return "Hello World!";
}
}
前面显示的类是com.apress.prospring5.ch2.decoupled包的一部分。它们也在本章的项目中使用,因为在真实的生产应用中,开发人员试图重用代码而不是复制代码。这就是为什么,正如你将在获得源代码时看到的,第二章的项目被定义为第三章的一些项目的依赖项。
为了在 XML 文件中声明 bean 定义,使用了<bean ../>标记,生成的app-context-xml.xml文件现在看起来像这样:
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="provider"
class="com.apress.prospring5.ch2.decoupled.HelloWorldMessageProvider"/>
<bean id="renderer"
class="com.apress.prospring5.ch2.decoupled.StandardOutMessageRenderer"
p:messageProvider-ref="provider"/>
</beans>
前面的标签声明了两个 beans,一个 ID 为provider,实现为HelloWorldMessageProvider,另一个 ID 为renderer,实现为StandardOutMessageRenderer。
从这个例子开始,名称空间将不再被添加到配置样本中,除非引入新的名称空间,因为这将使 bean 定义更加可见。
要使用注释创建 bean 定义,bean 类必须使用适当的原型注释进行注释, 2 ,方法或构造函数必须使用@Autowired进行注释,以告诉 Spring IoC 容器查找该类型的 bean,并在调用该方法时将其用作参数。在下面的代码片段中,用于创建 bean 定义的注释带有下划线。原型注释可以将结果 bean 的名称作为一个参数。
package com.apress.prospring5.ch3.annotation;
import com.apress.prospring5.ch2.decoupled.MessageProvider;
import org.springframework.stereotype.Component;
//simple bean
@Component("provider")
public class HelloWorldMessageProvider implements MessageProvider {
@Override
public String getMessage() {
return "Hello World!";
}
}
import com.apress.prospring5.ch2.decoupled.MessageProvider;
import com.apress.prospring5.ch2.decoupled.MessageRenderer;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
//complex, service bean
@Service("renderer")
public class StandardOutMessageRenderer
implements MessageRenderer {
private MessageProvider messageProvider;
@Override
public void render() {
if (messageProvider == null) {
throw new RuntimeException(
"You must set the property messageProvider of class:"
+ StandardOutMessageRenderer.class.getName());
}
System.out.println(messageProvider.getMessage());
}
@Override
@Autowired
public void setMessageProvider(MessageProvider provider) {
this.messageProvider = provider;
}
@Override
public MessageProvider getMessageProvider() {
return this.messageProvider;
}
}
当用这里描述的 XML 配置引导 Spring 的ApplicationContext时,在文件app-context-annotation.xml中,Spring 将找出那些组件,并用指定的名称实例化 beans:
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan
base-package="com.apress.prospring5.ch3.annotation"/>
</beans>
使用任何一种方法都不会影响您从ApplicationContext获取 beans 的方式。
package com.apress.prospring5.ch3;
import com.apress.prospring5.ch2.decoupled.MessageRenderer;
import
org.springframework.context.support.GenericXmlApplicationContext;
public class DeclareSpringComponents {
public static void main(String... args) {
GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-xml.xml");
ctx.refresh();
MessageRenderer messageRenderer = ctx.getBean("renderer",
MessageRenderer.class);
messageRenderer.render();
ctx.close();
}
}
代替DefaultListableBeanFactory,实例化GenericXmlApplicationContext的实例。GenericXmlApplicationContext类实现了ApplicationContext接口,并且能够从 XML 文件中定义的配置中引导 Spring 的ApplicationContext。
您可以将本章提供的源代码中的app-context-xml.xml文件与app-context-annotation.xml交换,您会发现两种情况产生的结果是一样的:“Hello World!”已打印。唯一的区别是在交换之后,提供功能的 beans 是用com.apress.prospring5.ch3.annotation包中的注释定义的。
使用 Java 配置
在第一章中,我们提到过app-context-xml.xml可以用一个配置类来代替,而不需要修改代表被创建的 bean 类型的类。当应用需要的 bean 类型是不能修改的第三方库的一部分时,这很有用。这样的配置类用@Configuration注释,并且包含用@Bean注释的方法,这些方法被 Spring IoC 容器直接调用来实例化 beans。bean 名称将与用于创建它的方法的名称相同。下面的代码示例中显示了该类,方法名带有下划线,以使生成的 beans 的命名更加明显:
package com.apress.prospring5.ch2.annotated;
import
com.apress.prospring5.ch2.decoupled.HelloWorldMessageProvider;
import com.apress.prospring5.ch2.decoupled.MessageProvider;
import com.apress.prospring5.ch2.decoupled.MessageRenderer;
import com.apress.prospring5.ch2.decoupled.StandardOutMessageRenderer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class HelloWorldConfiguration {
@Bean
public MessageProvider provider() {
return new HelloWorldMessageProvider();
}
@Bean
public MessageRenderer renderer(){
MessageRenderer renderer = new StandardOutMessageRenderer();
renderer.setMessageProvider(provider());
return renderer;
}
}
为了从这个类中读取配置,需要一个不同的ApplicationContext实现。
package com.apress.prospring5.ch2.annotated;
import com.apress.prospring5.ch2.decoupled.MessageRenderer;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class HelloWorldSpringAnnotated {
public static void main(String... args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext
(HelloWorldConfiguration.class);
MessageRenderer mr = ctx.getBean("renderer", MessageRenderer.class);
mr.render();
}
}
代替DefaultListableBeanFactory,实例化AnnotationConfigApplicationContext的实例。AnnotationConfigApplicationContext类实现了ApplicationContext接口,并且能够从HelloWorldConfiguration类定义的配置中引导 Spring 的ApplicationContext。
配置类也可以用来读取带注释的 beans 定义。在这种情况下,因为 bean 的定义配置是 bean 类的一部分,该类将不再需要任何@Bean带注释的方法。但是,为了能够在 Java 类中查找 bean 定义,必须启用组件扫描。这是通过用一个相当于<context:component-scanning ../>元素的注释来注释配置类来完成的。这个注释是@ComponentScanning,具有与 XML analogous 元素相同的参数。
package com.apress.prospring5.ch3.annotation;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@ComponentScan(basePackages = {"com.apress.prospring5.ch3.annotation"})
@Configuration
public class HelloWorldConfiguration {
}
使用AnnotationConfigApplicationContext引导 Spring 环境的代码也将适用于这个类,没有额外的修改。
在现实生活中的生产应用中,可能存在使用 Spring 的旧版本开发的遗留代码,或者需求可能具有需要 XML 和配置类的性质。幸运的是,XML 和 Java 配置可以以多种方式混合使用。例如,一个配置类可以使用@ImportResource从一个 XML 文件(或更多)中导入 bean 定义,使用AnnotationConfigApplicationContext的相同引导在这种情况下也可以工作。
package com.apress.prospring5.ch3.mixed;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
@ImportResource(locations = {"classpath:spring/app-context-xml.xml"})
@Configuration
public class HelloWorldConfiguration {
}
所以,Spring 允许你在定义你的 beans 的时候有真正的创造力;你将在第四章中了解更多,这一章只关注 Spring 应用的配置。
使用 Setter 注入
要使用 XML 配置来配置 setter 注入,您需要在<bean>标签下为您想要注入依赖项的每个<property>指定<property>标签。例如,要将消息提供者 bean 分配给messageRenderer bean 的messageProvider属性,只需更改renderer bean 的<bean>标记,如下面的代码片段所示:
<beans ...>
<bean id="renderer"
class="com.apress.prospring5.ch2.decoupled.StandardOutMessageRenderer">
<property name="messageProvider" ref="provider"/>
</bean>
<bean id="provider"
class="com.apress.prospring5.ch2.decoupled.HelloWorldMessageProvider"/>
</beans>
从这段代码中,我们可以看到provider bean 被分配给了messageProvider属性。您可以使用ref属性将 bean 引用分配给一个属性(稍后将详细讨论)。
如果您使用的是 Spring 2.5 或更新版本,并且在 XML 配置文件中声明了p namespace,那么您可以声明注入,如下面的代码片段所示:
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="renderer"
class="com.apress.prospring5.ch2.decoupled.StandardOutMessageRenderer"
p:messageProvider-ref="provider"/>
<bean id="provider"
class="com.apress.prospring5.ch2.decoupled.HelloWorldMessageProvider"/>
</beans>

p namespace没有在 XSD 文件中定义,只存在于 Spring core 中;因此,在schemaLocation属性中没有声明 XSD。
有了注解,就更简单了。您只需要向 setter 方法添加一个@Autowired注释,如下面的代码片段所示:
package com.apress.prospring5.ch3.annotation;
...
import org.springframework.beans.factory.annotation.Autowired;
@Service("renderer")
public class StandardOutMessageRenderer implements MessageRenderer {
...
@Override
@Autowired
public void setMessageProvider(MessageProvider provider) {
this.messageProvider = provider;
}
}
因为我们在 XML 配置文件中声明了<context:component-scan>标签,所以在 Spring 的ApplicationContext初始化期间,Spring 会发现那些@Autowired注释,并根据需要注入依赖项。
代替@Autowired,可以用@Resource(name="messageProvider")达到同样的效果。@Resource是 JSR-250 标准中的注释之一,它定义了一组在 JSE 和 JEE 平台上使用的通用 Java 注释。与@Autowired不同的是,@Resource注释支持name参数,以满足更细粒度的 DI 需求。此外,Spring 支持使用作为 JSR-299(Java EE 平台的上下文和依赖注入)的一部分引入的@Inject注释。@Inject在行为上等同于 Spring 的@Autowired注释。
为了验证结果,您可以使用前面提到的DeclareSpringComponents。如前一节所述,您可以将本章提供的源代码中的app-context-xml.xml文件与app-context-annotation.xml文件交换,您会发现两种情况都会产生相同的结果:“Hello World!”已打印。
使用构造函数注入
在前面的例子中,MessageProvider实现HelloWorldMessageProvider为getMessage()方法的每次调用返回相同的硬编码消息。在 Spring 配置文件中,您可以轻松地创建一个可配置的MessageProvider,它允许在外部定义消息,如下面的代码片段所示:
package com.apress.prospring5.ch3.xml;
import com.apress.prospring5.ch2.decoupled.MessageProvider;
public class ConfigurableMessageProvider
implements MessageProvider {
private String message;
public ConfigurableMessageProvider(String message) {
this.message = message;
}
@Override
public String getMessage() {
return message;
}
}
如您所见,如果不为消息提供一个值,就不可能创建一个ConfigurableMessageProvider的实例(除非您提供了null)。这正是我们想要的,这个类非常适合用于构造函数注入。下面的代码片段显示了如何重新定义provider bean 定义来创建ConfigurableMessageProvider的实例,通过使用构造函数注入来注入消息:
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="messageProvider"
class="com.apress.prospring5.ch3.xml.ConfigurableMessageProvider">
<constructor-arg value="I hope that someone gets my message in a bottle"/>
</bean>
</beans>
在这段代码中,我们没有使用<property>标签,而是使用了<constructor-arg>标签。因为我们这次没有传入另一个 bean,只是一个String文字,所以我们使用value属性而不是ref来指定构造函数参数的值。当您有不止一个构造函数参数或者您的类有不止一个构造函数时,您需要给每个<constructor-arg>标签一个 index 属性来指定参数在构造函数签名中的索引,从 0 开始。每当处理具有多个参数的构造函数时,最好使用 index 属性,以避免参数之间的混淆,并确保 Spring 选择正确的构造函数。
除了p名称空间,从 Spring 3.1 开始,您还可以使用c名称空间,如下所示:
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="provider"
class="com.apress.prospring5.ch3.xml.ConfigurableMessageProvider"
c:message="I hope that someone gets my message in a bottle"/>
</beans>

c名称空间也没有在 XSD 文件中定义,只存在于 Spring Core 中;因此,在schemaLocation属性中没有声明 XSD。
为了将注释用于构造函数注入,我们还在目标 bean 的构造函数方法中使用了@Autowired注释,这是使用 setter 注入的一个替代选项,如下面的代码片段所示:
package com.apress.prospring5.ch3.annotated;
import com.apress.prospring5.ch2.decoupled.MessageProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service("provider")
public class ConfigurableMessageProvider implements MessageProvider {
private String message;
@Autowired
public ConfigurableMessageProvider(
(@Value("Configurable message") String message) {
this.message = message;
}
@Override
public String getMessage() {
return this.message;
}
}
从前面的代码中,我们可以看到,我们使用了另一个注释@Value,来定义要注入到构造函数中的值。这是我们在 Spring 中向 bean 注入值的方式。除了简单的字符串之外,我们还可以使用强大的 SpEL 进行动态值注入(本章后面会详细介绍)。
但是,将值硬编码在代码中并不是一个好主意;要改变它,我们需要重新编译程序。即使您选择注释风格的 DI,一个好的实践是将那些值外部化以进行注入。为了将消息具体化,让我们在注释配置文件中将消息定义为一个 Spring bean,如下面的代码片段所示:
<beans ...>
<context:component-scan
base-package="com.apress.prospring5.ch3.annotated"/>
<bean id="message" class="java.lang.String"
c:_0="I hope that someone gets my message in a bottle"/>
</beans>
这里我们定义一个 ID 为message类型为java.lang.String的 bean。注意,我们还为构造函数注入使用了c名称空间来设置字符串值,并且_0表示构造函数参数的索引。声明了 bean 之后,我们可以从目标 bean 中去掉@Value注释,如下面的代码片段所示:
package com.apress.prospring5.ch3.annotated;
import com.apress.prospring5.ch2.decoupled.MessageProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service("provider")
public class ConfigurableMessageProvider implements MessageProvider {
private String message;
@Autowired
public ConfigurableMessageProvider(String message) {
this.message = message;
}
@Override
public String getMessage() {
return this.message;
}
}
因为我们声明消息 bean 及其 ID 与构造函数中指定的参数名称相同,所以 Spring 将检测注释并将值注入构造函数方法。现在对 XML ( app-context.xml.xml)和注释配置(app-context-annotation.xml)使用以下代码运行测试,配置的消息将在两种情况下显示:
package com.apress.prospring5.ch3;
import com.apress.prospring5.ch2.decoupled.MessageProvider;
import org.springframework.context.support.GenericXmlApplicationContext;
public class DeclareSpringComponents {
public static void main(String... args) {
GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-annotation.xml");
ctx.refresh();
MessageProvider messageProvider = ctx.getBean("provider",
MessageProvider.class);
System.out.println(messageProvider.getMessage());
}
}
在某些情况下,Spring 发现不可能告诉我们希望它使用哪个构造函数来进行构造函数注入。当我们有两个具有相同数量参数的构造函数,并且参数中使用的类型以相同的方式表示时,通常会出现这种情况。考虑以下代码:
package com.apress.prospring5.ch3.xml;
import org.springframework.context.support.GenericXmlApplicationContext;
public class ConstructorConfusion {
private String someValue;
public ConstructorConfusion(String someValue) {
System.out.println("ConstructorConfusion(String) called");
this.someValue = someValue;
}
public ConstructorConfusion(int someValue) {
System.out.println("ConstructorConfusion(int) called");
this.someValue = "Number: " + Integer.toString(someValue);
}
public String toString() {
return someValue;
}
public static void main(String... args) {
GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-xml.xml");
ctx.refresh();
ConstructorConfusion cc = (ConstructorConfusion)
ctx.getBean("constructorConfusion");
System.out.println(cc);
ctx.close
}
}
这只是从ApplicationContext中检索类型为ConstructorConfusion的 bean,并将值写入控制台输出。现在看看下面的配置代码:
<beans ...>
<bean id="provider"
class="com.apress.prospring5.ch3.xml.ConfigurableMessageProvider"
c:message="I hope that someone gets my message in a bottle"/>
<bean id="constructorConfusion"
class="com.apress.prospring5.ch3.xml.ConstructorConfusion">
<constructor-arg>
<value>90</value>
</constructor-arg>
</bean>
</beans>
在这种情况下,调用哪个构造函数?运行该示例会产生以下输出:
ConstructorConfusion(String) called
这表明调用了带有String参数的构造函数。这不是我们想要的效果,因为我们想用Number:作为构造函数注入传入的任何整数值的前缀,如int构造函数所示。为了解决这个问题,我们需要对配置做一个小的修改,如下面的代码片段所示:
<beans ...>
<bean id="provider"
class="com.apress.prospring5.ch3.xml.ConfigurableMessageProvider"
c:message="I hope that someone gets my message in a bottle"/>
<bean id="constructorConfusion"
class="com.apress.prospring5.ch3.xml.ConstructorConfusion">
<constructor-arg type="int">
<value>90</value>
</constructor-arg>
</bean>
</beans>
请注意,<constructor-arg>标签有一个额外的属性type,它指定了 Spring 应该寻找的参数类型。使用正确的配置再次运行示例会产生正确的输出。
ConstructorConfusion(int) called
Number: 90
对于注释样式的构造注入,可以通过将注释直接应用到目标构造函数方法来避免混淆,如下面的代码片段所示:
package com.apress.prospring5.ch3.annotated;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.support.GenericXmlApplicationContext;
import org.springframework.stereotype.Service;
@Service("constructorConfusion")
public class ConstructorConfusion {
private String someValue;
public ConstructorConfusion(String someValue) {
System.out.println("ConstructorConfusion(String) called");
this.someValue = someValue;
}
@Autowired
public ConstructorConfusion(@Value("90") int someValue) {
System.out.println("ConstructorConfusion(int) called");
this.someValue = "Number: " + Integer.toString(someValue);
}
public String toString() {
return someValue;
}
public static void main(String... args) {
GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-annotation.xml");
ctx.refresh();
ConstructorConfusion cc = (ConstructorConfusion)
ctx.getBean("constructorConfusion");
System.out.println(cc);
ctx.close();
}
}
通过将@Autowired注释应用于所需的构造函数方法,Spring 将使用该方法实例化 bean 并注入指定的值。和以前一样,我们应该从配置中将值具体化。

@Autowired注释只能应用于一个构造函数方法。如果我们将注释应用于多个构造函数方法,Spring 将在引导ApplicationContext时报错。
使用现场注射
Spring 支持第三种类型的依赖注入,称为字段注入。顾名思义,依赖项直接注入到字段中,不需要任何构造函数或设置器。这是通过用Autowired注释对类成员进行注释来完成的。这看起来似乎很实际,因为当依赖项不在它所属的对象之外时,开发人员就不必编写一些在最初创建 bean 之后就不再使用的代码了。在下面的代码片段中,类型为Singer的 bean 有一个类型为Inspiration的字段:
package com.apress.prospring5.ch3.annotated;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service("singer")
public class Singer {
@Autowired
private Inspiration inspirationBean;
public void sing() {
System.out.println("... " + inspirationBean.getLyric());
}
}
该字段是私有的,但是 Spring IoC 容器并不关心这个问题;它使用反射来填充所需的依赖项。这里显示的是Inspiration类代码;它是一个简单的 bean,有一个String成员:
package com.apress.prospring5.ch3.annotated;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class Inspiration {
private String lyric =
"I can keep the door cracked open, to let light through";
public Inspiration(
@Value("For all my running, I can understand") String lyric) {
this.lyric = lyric;
}
public String getLyric() {
return lyric;
}
public void setLyric(String lyric) {
this.lyric = lyric;
}
}
以下配置使用组件扫描来发现将由 Spring IoC 容器创建的 bean 定义:
<beans ...>
<context:component-scan
base-package="com.apress.prospring5.ch3.annotated"/>
</beans>
找到一个类型为Inspiration的 bean,Spring IoC 容器会将该 bean 注入到singer bean 的inspirationBean成员中。这就是为什么在运行下一个代码片段中描述的示例时,控制台中将显示“对于我的所有运行,我可以理解”。
package com.apress.prospring5.ch3.annotated;
import org.springframework.context.support.GenericXmlApplicationContext;
public class FieldInjection {
public static void main(String... args) {
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context.xml");
ctx.refresh();
Singer singerBean = ctx.getBean(Singer.class);
singerBean.sing();
ctx.close();
}
}
但是也有缺点,这就是为什么通常避免使用场注入的原因。
- 虽然这样添加依赖关系很容易,但是我们必须小心不要违反单一责任原则。拥有更多的依赖意味着一个类要承担更多的责任,这可能会导致在重构时难以分离关注点。当使用构造函数或 setters 设置依赖关系时,类变得臃肿的情况更容易看到,但当使用字段注入时,这种情况就隐藏得很好了。
- 在 Spring 中,注入依赖项的责任被传递给容器,但是该类应该使用公共接口通过方法或构造函数清楚地传达所需的依赖项类型。使用字段注入,会变得不清楚什么类型的依赖是真正需要的,以及依赖是否是强制的。
- 字段注入引入了对 Spring 容器的依赖,因为
@Autowired注释是一个 Spring 组件;因此,该 bean 不再是 POJO,不能独立实例化。 - 字段注入不能用于最终字段。这种类型的字段只能使用构造函数注入来初始化。
- 当编写测试时,字段注入会带来困难,因为依赖项必须手动注入。
使用注射参数
在前面的三个例子中,您看到了如何通过使用 setter 注入和 constructor 注入将其他组件和值注入到 bean 中。Spring 支持无数的注入参数选项,不仅允许您注入其他组件和简单值,还允许您注入 Java 集合、外部定义的属性,甚至是另一个工厂中的 beans。通过分别使用<property>和<constructor-args>标签下的相应标签,可以将所有这些注入参数类型用于 setter 注入和 constructor 注入。
注入简单的价值观
向 beans 中注入简单的值很容易。要做到这一点,只需在配置标签中指定值,并封装在一个<value>标签中。默认情况下,<value>标签不仅可以读取String值,还可以将这些值转换成任何原始或原始包装类。下面的代码片段显示了一个简单的 bean,它具有各种为注入而公开的属性:
package com.apress.prospring5.ch3.xml;
import org.springframework.context.support.GenericXmlApplicationContext;
public class InjectSimple {
private String name;
private int age;
private float height;
private boolean programmer;
private Long ageInSeconds;
public static void main(String... args) {
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-xml.xml");
ctx.refresh();
InjectSimple simple = (InjectSimple) ctx.getBean("injectSimple");
System.out.println(simple);
ctx.close();
}
public void setAgeInSeconds(Long ageInSeconds) {
this.ageInSeconds = ageInSeconds;
}
public void setProgrammer(boolean programmer) {
this.programmer = programmer;
}
public void setAge(int age) {
this.age = age;
}
public void setHeight(float height) {
this.height = height;
}
public void setName(String name) {
this.name = name;
}
public String toString() {
return "Name: " + name + "\n"
+ "Age: " + age + "\n"
+ "Age in Seconds: " + ageInSeconds + "\n"
+ "Height: " + height + "\n"
+ "Is Programmer?: " + programmer;
}
}
除了属性之外,InjectSimple类还定义了main()方法,该方法创建一个Application Context,然后从 Spring 检索一个InjectSimple bean。然后,该 bean 的属性值被写入控制台输出。下面的代码片段描述了这个 bean 的app-context-xml.xml中包含的配置:
<beans ...>
<bean id="injectSimpleConfig"
class="com.apress.prospring5.ch3.xml.InjectSimpleConfig"/>
<bean id="injectSimpleSpel"
class="com.apress.prospring5.ch3.xml.InjectSimpleSpel"
p:name="John Mayer"
p:age="39"
p:height="1.92"
p:programmer="false"
p:ageInSeconds="1241401112"/>
</beans>
从前面的两个代码片段中可以看出,可以在 bean 上定义属性,这些属性接受String值、原始值或原始包装器值,然后通过使用<value>标记为这些属性注入值。以下是按预期运行此示例所创建的输出:
Name: John Mayer
Age: 39
Age in Seconds: 1241401112
Height: 1.92
Is Programmer?: false
对于注释风格的简单值注入,我们可以将@Value注释应用于 bean 属性。这一次,我们没有使用 setter 方法,而是将注释应用于属性声明语句,如下面的代码片段所示(Spring 支持 setter 方法或属性中的注释):
package com.apress.prospring5.ch3.annotated;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.support.GenericXmlApplicationContext;
import org.springframework.stereotype.Service;
@Service("injectSimple")
public class InjectSimple {
@Value("John Mayer")
private String name;
@Value("39")
private int age;
@Value("1.92")
private float height;
@Value("false")
private boolean programmer;
@Value("1241401112")
private Long ageInSeconds;
public static void main(String... args) {
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-annotation.xml");
ctx.refresh();
InjectSimple simple = (InjectSimple) ctx.getBean("injectSimple");
System.out.println(simple);
ctx.close();
}
public String toString() {
return "Name: " + name + "\n"
+ "Age: " + age + "\n"
+ "Age in Seconds: " + ageInSeconds + "\n"
+ "Height: " + height + "\n"
+ "Is Programmer?: " + programmer;
}
}
这实现了与 XML 配置相同的结果。
使用 SpEL 注入值
Spring 3 中引入的一个强大特性是 Spring 表达式语言(SpEL)。SpEL 使您能够动态地评估表达式,然后在 Spring 的ApplicationContext中使用它。您可以将结果注入到 Spring beans 中。在这一节中,我们通过使用上一节中的示例,来看看如何使用 SpEL 从其他 beans 中注入属性。
假设现在我们想要在一个配置类中外部化要注入到 Spring bean 中的值,如下面的代码片段所示:
package com.apress.prospring5.ch3.annotated;
import org.springframework.stereotype.Component;
@Component("injectSimpleConfig")
public class InjectSimpleConfig {
private String name = "John Mayer";
private int age = 39;
private float height = 1.92f;
private boolean programmer = false;
private Long ageInSeconds = 1_241_401_112L;
public String getName() {
return name;
}
public int getAge() {
return age;
}
public float getHeight() {
return height;
}
public boolean isProgrammer() {
return programmer;
}
public Long getAgeInSeconds() {
return ageInSeconds;
}
}
然后,我们可以在 XML 配置中定义 bean,并使用 SpEL 将 bean 的属性注入到依赖 bean 中,如下面的配置片段所示:
<beans ...>
<bean id="injectSimpleConfig"
class="com.apress.prospring5.ch3.xml.InjectSimpleConfig"/>
<bean id="injectSimpleSpel"
class="com.apress.prospring5.ch3.xml.InjectSimpleSpel"
p:name="#{injectSimpleConfig.name}"
p:age="#{injectSimpleConfig.age + 1}"
p:height="#{injectSimpleConfig.height}"
p:programmer="#{injectSimpleConfig.programmer}"
p:ageInSeconds="#{injectSimpleConfig.ageInSeconds}"/>
</beans>
注意,我们使用 SpEL #{injectSimpleConfig.name}来引用另一个 bean 的属性。对于 age,我们在 bean 的值上加 1,表示我们可以使用 SpEL 来操作我们认为合适的属性,并将其注入到依赖的 bean 中。现在,我们可以使用以下代码片段中显示的程序来测试配置:
package com.apress.prospring5.ch3.xml;
import org.springframework.context.support.GenericXmlApplicationContext;
public class InjectSimpleSpel {
private String name;
private int age;
private float height;
private boolean programmer;
private Long ageInSeconds;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return this.age;
}
public void setAge(int age) {
this.age = age;
}
public float getHeight() {
return this.height;
}
public void setHeight(float height) {
this.height = height;
}
public boolean isProgrammer() {
return this.programmer;
}
public void setProgrammer(boolean programmer) {
this.programmer = programmer;
}
public Long getAgeInSeconds() {
return this.ageInSeconds;
}
public void setAgeInSeconds(Long ageInSeconds) {
this.ageInSeconds = ageInSeconds;
}
public String toString() {
return "Name: " + name + "\n"
+ "Age: " + age + "\n"
+ "Age in Seconds: " + ageInSeconds + "\n"
+ "Height: " + height + "\n"
+ "Is Programmer?: " + programmer;
}
public static void main(String... args) {
GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-xml.xml");
ctx.refresh();
InjectSimpleSpel simple = (InjectSimpleSpel)ctx.getBean("injectSimpleSpel");
System.out.println(simple);
ctx.close();
}
}
以下是该程序的输出:
Name: John Mayer
Age: 40
Age in Seconds: 1241401112
Height: 1.92
Is Programmer?: false
当使用注释样式的值注入时,我们只需要用 SpEL 表达式替换值注释(参见下面的代码片段):
package com.apress.prospring5.ch3.annotated;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.support.GenericXmlApplicationContext;
import org.springframework.stereotype.Service;
@Service("injectSimpleSpel")
public class InjectSimpleSpel {
@Value("#{injectSimpleConfig.name}")
private String name;
@Value("#{injectSimpleConfig.age + 1}")
private int age;
@Value("#{injectSimpleConfig.height}")
private float height;
@Value("#{injectSimpleConfig.programmer}")
private boolean programmer;
@Value("#{injectSimpleConfig.ageInSeconds}")
private Long ageInSeconds;
public String toString() {
return "Name: " + name + "\n"
+ "Age: " + age + "\n"
+ "Age in Seconds: " + ageInSeconds + "\n"
+ "Height: " + height + "\n"
+ "Is Programmer?: " + programmer;
}
public static void main(String... args) {
GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-annotation.xml");
ctx.refresh();
InjectSimpleSpel simple = (InjectSimpleSpel)ctx.getBean("injectSimpleSpel");
System.out.println(simple);
ctx.close();
}
}
这里显示的是InjectSimpleConfig的版本:
package com.apress.prospring5.ch3.annotated;
import org.springframework.stereotype.Component;
@Component("injectSimpleConfig")
public class InjectSimpleConfig {
private String name = "John Mayer";
private int age = 39;
private float height = 1.92f;
private boolean programmer = false;
private Long ageInSeconds = 1_241_401_112L;
// getters here ...
}
在前面的代码片段中,没有使用@Service annotation,而是使用了@Component。基本上使用@Component和@Service的效果是一样的。这两个注释都在指示 Spring,被注释的类是使用基于注释的配置和类路径扫描进行自动检测的候选者。然而,由于InjectSimpleConfig类存储的是应用配置,而不是提供业务服务,所以使用@Component更有意义。实际上,@Service是@Component的专门化,这表明被注释的类正在向应用中的其他层提供业务服务。
测试程序会产生相同的结果。使用 SpEL,您可以访问任何 Spring 管理的 beans 和属性,并通过 Spring 对复杂语言特性和语法的支持来操纵它们供应用使用。
在同一个 XML 单元中注入 Beans
正如您已经看到的,可以通过使用ref标签将一个 bean 注入到另一个 bean 中。下一个代码片段显示了一个类,该类公开了一个 setter 以允许注入 bean:
package com.apress.prospring5.ch3.xml;
import org.springframework.context.support.GenericXmlApplicationContext;
import com.apress.prospring5.ch3.Oracle;
public class InjectRef {
private Oracle oracle;
public void setOracle(Oracle oracle) {
this.oracle = oracle;
}
public static void main(String... args) {
GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-xml.xml");
ctx.refresh();
InjectRef injectRef = (InjectRef) ctx.getBean("injectRef");
System.out.println(injectRef);
ctx.close();
}
public String toString() {
return oracle.defineMeaningOfLife();
}
}
要配置 Spring 将一个 bean 注入到另一个 bean 中,首先需要配置两个 bean:一个被注入,一个作为注入的目标。一旦完成,您只需在目标 bean 上使用<ref>标签来配置注入。下面的代码片段显示了这种配置的一个例子(文件app-context-xml.xml):
<beans ...>
<bean id="oracle" name="wiseworm"
class="com.apress.prospring5.ch3.BookwormOracle"/>
<bean id="injectRef"
class="com.apress.prospring5.ch3.xml.InjectRef">
<property name="oracle">
<ref bean="oracle"/>
</property>
</bean>
</beans>
运行InjectRef类会产生以下输出:
Encyclopedias are a waste of money - go see the world instead
需要注意的重要一点是,被注入的类型不必与目标上定义的类型完全相同;类型只需要兼容。Compatible 意味着如果目标上声明的类型是接口,则注入的类型必须实现该接口。如果声明的类型是类,则注入的类型必须是相同的类型或子类型。在这个例子中,InjectRef类定义了setOracle()方法来接收Oracle的一个实例,这个实例是一个接口,注入的类型是BookwormOracle,这个类实现了Oracle。这是一个让一些开发人员感到困惑的点,但它真的很简单。注入遵循与任何 Java 代码相同的类型规则,因此只要熟悉 Java 类型的工作方式,理解注入中的类型就很容易。
在前面的示例中,要注入的 bean 的 ID 是通过使用<ref>标记的 local 属性指定的。正如您将在后面看到的,在“理解 bean 命名”一节中,您可以给 Bean 一个以上的名称,以便您可以使用各种别名来引用它。当您使用 local 属性时,这意味着<ref>标记只查看 bean 的 ID,从不查看它的任何别名。而且,bean 定义应该存在于同一个 XML 配置文件中。要注入任何名称的 bean 或从其他 XML 配置文件导入 bean,请使用<ref>标签的 bean 属性,而不是本地属性。下面的代码片段显示了前一个示例的替代配置,为注入的 bean 使用了替代名称:
<beans ...>
<bean id="oracle" name="wiseworm"
class="com.apress.prospring5.ch3.BookwormOracle"/>
<bean id="injectRef"
class="com.apress.prospring5.ch3.xml.InjectRef">
<property name="oracle">
<ref bean="wiseworm"/>
</property>
</bean>
</beans>
在这个例子中,oracle bean 通过使用name属性被赋予一个别名,然后通过使用这个别名和<ref>标记的 bean 属性被注入到injectRef bean 中。在这一点上,不要太担心命名语义。我们将在本章后面更详细地讨论这一点。再次运行InjectRef类会产生与上一个例子相同的结果。
注入和应用上下文嵌套
到目前为止,我们一直在注入的 bean 都位于与它们被注入的 bean 相同的ApplicationContext(因此也是相同的BeanFactory)中。然而,Spring 支持ApplicationContext的层次结构,因此一个上下文(以及关联的BeanFactory)被认为是另一个上下文的父上下文。通过允许嵌套ApplicationContexts, Spring 允许你将你的配置分成不同的文件,这对于有很多 beans 的大型项目来说是天赐之物。
当嵌套ApplicationContext实例时,Spring 允许被认为是子上下文中的 bean 引用父上下文中的 bean。使用GenericXmlApplicationContext嵌套很容易理解。要将一个GenericXmlApplicationContext嵌套在另一个GenericXmlApplicationContext中,只需调用子ApplicationContext中的setParent()方法,如下面的代码示例所示:
package com.apress.prospring5.ch3;
import org.springframework.context.support.GenericXmlApplicationContext;
public class HierarchicalAppContextUsage {
public static void main(String... args) {
GenericXmlApplicationContext parent = new GenericXmlApplicationContext();
parent.load("classpath:spring/parent-context.xml");
parent.refresh();
GenericXmlApplicationContext child = new GenericXmlApplicationContext();
child.load("classpath:spring/child-context.xml");
child.setParent(parent);
child.refresh();
Song song1 = (Song) child.getBean("song1");
Song song2 = (Song) child.getBean("song2");
Song song3 = (Song) child.getBean("song3");
System.out.println("from parent ctx: " + song1.getTitle());
System.out.println("from child ctx: " + song2.getTitle());
System.out.println("from parent ctx: " + song3.getTitle());
child.close();
parent.close();
}
}
Song类非常简单,如下所示:
package com.apress.prospring5.ch3;
public class Song {
private String title;
public void setTitle(String title) {
this.title = title;
}
public String getTitle() {
return title;
}
}
在子节点ApplicationContext的配置文件中,引用父节点ApplicationContext中的 bean 就像引用子节点ApplicationContext中的 bean 一样,除非子节点ApplicationContext中有一个同名的 bean。在这种情况下,只需用parent替换ref元素的 bean 属性,就可以了。下面的配置片段描述了名为parent-context.xml的父BeanFactory的配置文件的内容:
<beans ...>
<bean id="childTitle" class="java.lang.String" c:_0="Daughters"/>
<bean id="parentTitle" class="java.lang.String" c:_0="Gravity"/>
</beans>
如您所见,这个配置简单地定义了两个 bean:childTitle和parentTitle。两者都是值为Daughters和Gravity的String对象。下面的配置片段描述了包含在child-context.xml中的子节点ApplicationContext的配置:
<beans ...>
<bean id="song1" class="com.apress.prospring5.ch3.Song"
p:title-ref="parentTitle"/>
<bean id="song2" class="com.apress.prospring5.ch3.Song"
p:title-ref="childTitle"/>
<bean id="song3" class="com.apress.prospring5.ch3.Song">
<property name="title">
<ref parent="childTitle"/>
</property>
</bean>
<bean id="childTitle" class="java.lang.String" c:_0="No Such Thing"/>
</beans>
注意,我们在这里定义了四个 beans。此代码中的childTitle与父代码中的childTitle相似,只是它所代表的String具有不同的值,表明它位于子代码ApplicationContext中。
song1 bean 使用 bean ref属性来引用名为parentTitle的 bean。因为这个 bean 只存在于父 beanBeanFactory中,song1收到了对这个 bean 的引用。这里有两个有趣的地方。首先,您可以使用 bean 属性来引用子节点和父节点ApplicationContext中的 bean,这使得透明地引用 bean 变得容易,允许您随着应用的增长在配置文件之间移动 bean。第二个有趣的地方是,不能使用 local 属性来引用父ApplicationContext中的 beans。XML 解析器检查本地属性的值是否作为有效元素存在于同一个文件中,防止它被用来引用父上下文中的 beans。
song2 bean 使用 bean ref属性来引用childTitle。因为这个 bean 在两个ApplicationContext中都有定义,所以song2 bean 在它自己的ApplicationContext中接收到一个对childTitle的引用。
song3 bean 使用<ref>标签直接在父ApplicationContext中引用childTitle。因为song3正在使用<ref>标签的父属性,所以在子ApplicationContext中声明的childTitle实例被完全忽略。
您可能已经注意到,与song1和song2不同,song3 bean 没有使用p名称空间。虽然p名称空间提供了方便的快捷方式,但是它没有提供使用属性标签时的所有功能,比如引用父 bean。虽然我们将它作为一个例子来展示,但是最好选择p名称空间或属性标签来定义您的 beans,而不是混合使用不同的样式(除非绝对必要)。
下面是运行HierarchicalAppContextUsage类的输出:
from parent ctx: Gravity
from child ctx: No Such Thing
from parent ctx: Daughters
正如所料,song1和song3bean 都获得了对父ApplicationContext中 bean 的引用,而song2 bean 获得了对子ApplicationContext中 bean 的引用。
注入集合
通常,您的 bean 需要访问对象集合,而不仅仅是单个 bean 或值。因此,Spring 允许您将一组对象注入到一个 beans 中,这并不奇怪。使用集合很简单:您可以选择<list>、<map>、<set>或<props>来表示一个List、Map、Set或Properties实例,然后像对待任何其他注入一样传递各个项目。<props>标签只允许将String s 作为值传入,因为Properties类只允许属性值为String s,当使用<list>、<map>或<set>时,您可以在注入属性时使用任何想要的标签,甚至是另一个集合标签。这允许你传入一个Map s 的List,一个Set s 的Map,甚至一个List s 的Set s 的List!下面的代码片段显示了一个可以将所有四种集合类型注入其中的类:
package com.apress.prospring5.ch3.xml;
import org.springframework.context.support.GenericXmlApplicationContext;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
public class CollectionInjection {
private Map<String, Object> map;
private Properties props;
private Set set;
private List list;
public static void main(String... args) {
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-xml.xml");
ctx.refresh();
CollectionInjection instance =
(CollectionInjection) ctx.getBean("injectCollection");
instance.displayInfo();
ctx.close();
}
public void displayInfo() {
System.out.println("Map contents:\n");
map.entrySet().stream().forEach(e -> System.out.println(
"Key: " + e.getKey() + " - Value: " + e.getValue()));
System.out.println("\nProperties contents:\n");
props.entrySet().stream().forEach(e -> System.out.println(
"Key: " + e.getKey() + " - Value: " + e.getValue()));
System.out.println("\nSet contents:\n");
set.forEach(obj -> System.out.println("Value: " + obj));
System.out.println("\nList contents:\n");
list.forEach(obj -> System.out.println("Value: " + obj));
}
public void setList(List list) {
this.list = list;
}
public void setSet(Set set) {
this.set = set;
}
public void setMap(Map<String, Object> map) {
this.map = map;
}
public void setProps(Properties props) {
this.props = props;
}
}
这是相当多的代码,但它实际上做得很少。main()方法从 Spring 中检索一个CollectionInjection bean,然后调用displayInfo()方法。该方法只输出将从 Spring 注入的Map、Properties、Set和List实例的内容。接下来描述了为CollectionInjection类中的每个属性注入值所需的配置,配置文件被命名为app-context-xml.xml。
另外,请注意对Map<String,Object>属性的声明。对于比 5 更新的 JDK 版本,Spring 也支持强类型的Collection声明,并将执行从 XML 配置到相应指定类型的转换。
<beans ...>
<bean id="lyricHolder"
lass="com.apress.prospring5.ch3.xml.LyricHolder"/>
<bean id="injectCollection"
class="com.apress.prospring5.ch3.xml.CollectionInjection">
<property name="map">
<map>
<entry key="someValue">
<value>It's a Friday, we finally made it</value>
</entry>
<entry key="someBean">
<ref bean="lyricHolder"/>
</entry>
</map>
</property>
<property name="props">
<props>
<prop key="firstName">John</prop>
<prop key="secondName">Mayer</prop>
</props>
</property>
<property name="set">
<set>
<value>I can't believe I get to see your face</value>
<ref bean="lyricHolder"/>
</set>
</property>
<property name="list">
<list>
<value>You've been working and I've been waiting</value>
<ref bean="lyricHolder"/>
</list>
</property>
</bean>
</beans>
在这段代码中,您可以看到我们已经将值注入到在CollectionInjection类上公开的所有四个 setters 中。对于map属性,我们通过使用<map>标签注入了一个Map实例。注意,每个条目都是用一个<entry>标签指定的,每个条目都有一个String键和一个条目值。该条目值可以是您可以单独注入到属性中的任何值;这个例子展示了如何使用<value>和<ref>标签来添加一个String值和一个对Map的 bean 引用。这里描述了LyricHolder类,它是在前面的配置中注入到映射中的lyricHolder bean 的类型:
package com.apress.prospring5.ch3.xml;
import com.apress.prospring5.ch3.ContentHolder;
public class LyricHolder implements ContentHolder{
private String value = "'You be the DJ, I'll be the driver'";
@Override public String toString() {
return "LyricHolder: { " + value + "}";
}
}
对于props属性,我们使用<props>标签创建一个java.util.Properties的实例,并使用<prop>标签的键控方式与<entry>标签类似,但是我们只能为进入Properties实例的每个属性指定String值。
此外,对于<map>元素,有一个更紧凑的替代配置,使用value和value-ref属性,而不是<value>和<ref>元素。这里声明的map与之前配置中的等效:
<property name="map">
<map>
<entry key="someValue" value="It's a Friday, we finally made it"/>
<entry key="someBean" value-ref="lyricHolder"/>
</map>
</property>
<list>和<set>标签的工作方式是一样的:通过使用任何一个单独的值标签来指定每个元素,例如<value>和<ref>,这些标签用于将单个值注入到属性中。在前面的配置中,您可以看到我们已经为List和Set实例添加了一个String值和一个 bean 引用。
下面是由类CollectionInjection中的main()方法生成的输出。正如所料,它只是在配置文件中列出了添加到集合中的元素。
Map contents:
Key: someValue - Value: It's a Friday, we finally made it
Key: someBean - Value: LyricHolder: { 'You be the DJ, I'll be the driver'}
Properties contents:
Key: secondName - Value: Mayer
Key: firstName - Value: John
Set contents:
Value: I can't believe I get to see your face
Value: LyricHolder: { 'You be the DJ, I'll be the driver'}
List contents:
Value: You've been working and I've been waiting
Value: LyricHolder: { 'You be the DJ, I'll be the driver'}
记住,使用<list>、<map>和<set>元素,您可以使用任何用于设置非集合属性值的标签来指定集合中某个条目的值。这是一个非常强大的概念,因为你不仅仅局限于注入原始值的集合;还可以注入 beans 集合或其他集合。
使用该功能,可以更容易地模块化应用,并为应用逻辑的关键部分提供不同的、用户可选的实现。考虑一个允许公司职员在线创建、校对和订购他们的个性化商务信纸的系统。在该系统中,每份订单的成品在准备生产时会被发送到相应的打印机。唯一复杂的是,一些印刷商希望通过电子邮件接收作品,一些通过 FTP,还有一些使用安全复制协议(SCP)。使用 Spring 的集合注入,您可以为此功能创建一个标准接口,如下面的代码片段所示:
package com.apress.prospring5.ch3;
public interface ArtworkSender {
void sendArtwork(String artworkPath, Recipient recipient);
String getFriendlyName();
String getShortName();
}
在前面的例子中,Recipient类是一个空类。从这个接口,您可以创建多个实现,每个实现都能够向人描述自己,如下所示:
package com.apress.prospring5.ch3;
public class FtpArtworkSender
implements ArtworkSender {
@Override
public void sendArtwork(String artworkPath, Recipient recipient) {
// ftp logic here...
}
@Override
public String getFriendlyName() {
return "File Transfer Protocol";
}
@Override
public String getShortName() {
return "ftp";
}
}
假设您开发了一个支持所有可用的Artwork-Sender接口实现的ArtworkManager类。实现就绪后,您只需将一个List传递给您的ArtworkManager类,就万事大吉了。使用getFriendlyName()方法,您可以显示一个交付选项列表,供系统管理员在配置每个信纸模板时选择。此外,如果您只对ArtworkSender接口编码,您的应用可以保持与单个实现完全解耦。我们将把ArtworkManager类的实现留给您作为练习。
除了 XML 配置,您还可以使用注释进行集合注入。但是,您还想将集合的值外部化到配置文件中,以便于维护。下面的代码片段是四个不同的 Spring beans 的配置,它们模拟了上一个示例(配置文件app-context-annotation.xml)的相同集合属性:
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util.xsd">
<context:component-scan
base-package="com.apress.prospring5.ch3.annotated"/>
<util:map id="map" map-class="java.util.HashMap">
<entry key="someValue" value="It's a Friday, we finally made it"/>
<entry key="someBean" value-ref="lyricHolder"/>
</util:map>
<util:properties id="props">
<prop key="firstName">John</prop>
<prop key="secondName">Mayer</prop>
</util:properties>
<util:set id="set" set-class="java.util.HashSet">
<value>I can't believe I get to see your face</value>
<ref bean="lyricHolder"/>
</util:set>
<util:list id="list" list-class="java.util.ArrayList">
<value>You've been working and I've been waiting</value>
<ref bean="lyricHolder"/>
</util:list>
</beans>
让我们也开发一个LyricHolder类的注释版本。此处描述了课程内容:
package com.apress.prospring5.ch3.annotated;
import com.apress.prospring5.ch3.ContentHolder;
import org.springframework.stereotype.Service;
@Service("lyricHolder")
public class LyricHolder implements ContentHolder{
private String value = "'You be the DJ, I'll be the driver'";
@Override public String toString() {
return "LyricHolder: { " + value + "}";
}
}
在前面描述的配置中,我们利用 Spring 提供的util名称空间来声明用于存储集合属性的 beans】名称空间。与以前版本的 Spring 相比,它极大地简化了配置。在我们用来测试您的配置的类中,我们注入了以前的 bean,并使用 JSR-250 @Resource注释(其名称被指定为参数)来正确识别 bean。displayInfo()方法与之前相同,因此这里不再显示。
@Service("injectCollection")
public class CollectionInjection {
@Resource(name="map")
private Map<String, Object> map;
@Resource(name="props")
private Properties props;
@Resource(name="set")
private Set set;
@Resource(name="list")
private List list;
public static void main(String... args) {
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-annotation.xml");
ctx.refresh();
CollectionInjection instance = (CollectionInjection)
ctx.getBean("injectCollection");
instance.displayInfo();
ctx.close();
}
...
}
运行测试程序,您将获得与使用 XML 配置的示例相同的结果。
你可能会奇怪为什么用注释@Resource而不用@Autowired。这是因为@Autowired注释在语义上是这样定义的,它总是将数组、集合和映射视为一组对应的 bean,目标 bean 类型从声明的集合值类型派生而来。因此,例如,如果一个类有一个类型为List<ContentHolder>的属性并定义了@Autowired注释,Spring 将尝试将当前ApplicationContext中所有类型为ContentHolder的 bean 注入到该属性中(而不是在配置文件中声明的<util:list>),这将导致注入意外的依赖项,或者如果没有定义类型为ContentHolder的 bean,Spring 抛出一个异常。因此,对于集合类型注入,我们必须通过指定 bean 名称来明确指示 Spring 执行注入,这是@Resource注释所支持的。
可以使用@Autowired和@Qualifier的组合来达到同样的目的,但是最好使用一个注释而不是两个。在下面的代码片段中,您可以看到通过使用@Autowired和@Qualifier使用 bean 名称注入集合的等效配置。
package com.apress.prospring5.ch3.annotated;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
@Service("injectCollection")
public class CollectionInjection {
@Autowired
@Qualifier("map")
private Map<String, Object> map;
...
}
使用方法注入
除了构造函数和设置器注入,Spring 提供的另一个不常用的 DI 特性是方法注入。Spring 的方法注入功能有两种松散关联的形式,查找方法注入和方法替换。查找方法注入提供了另一种机制,通过这种机制,bean 可以获得它的一个依赖项。方法替换允许您任意替换 bean 上任何方法的实现,而不必更改原始源代码。为了提供这两个特性,Spring 使用了 CGLIB 的动态字节码增强功能。 3
查找方法注入
Spring 1.1 版本中添加了查找方法注入,以克服 bean 依赖于另一个具有不同生命周期的 bean 时遇到的问题,特别是当 singleton 依赖于非 singleton 时。在这种情况下,setter 和 constructor 注入都会导致 singleton 维护一个应该是非 singleton bean 的实例。在某些情况下,您可能希望 singleton bean 在每次需要相关 bean 时都获得 nonsingleton 的新实例。
考虑一个场景,其中一个LockOpener类提供打开任何储物柜的服务。LockOpener类依赖于一个KeyHelper类来打开储物柜,这个类被注入到LockOpener中。然而,KeyHelper类的设计涉及到一些内部状态,使得它不适合重用。每次调用openLock()方法,都需要一个新的KeyHelper实例。在这种情况下,LockOpener将是单例的。然而,如果我们使用普通机制注入KeyHelper类,那么KeyHelper类的同一个实例(在 Spring 第一次执行注入时被实例化)将被重用。为了确保每次调用KeyHelper实例时都有一个新的实例传递给openLock()方法,我们需要使用查找方法注入。
通常,您可以通过让单例 bean 实现ApplicationContextAware接口来实现这一点(我们将在下一章讨论这个接口)。然后,使用ApplicationContext实例,singleton bean 可以在每次需要时查找 nonsingleton 依赖项的新实例。Lookup Method Injection 允许 singleton bean 声明它需要一个非 singleton 依赖项,并且它将在每次需要与之交互时接收一个非 singleton bean 的新实例,而不需要实现任何特定于 Spring 的接口。
查找方法注入的工作方式是让您的 singleton 声明一个方法,即 Lookup 方法,该方法返回 nonsingleton bean 的一个实例。当您在应用中获得对 singleton 的引用时,您实际上是在接收对一个动态创建的子类的引用,Spring 已经在该子类上实现了 lookup 方法。典型的实现包括将查找方法定义为抽象的,从而将 bean 类定义为抽象的。当您忘记配置方法注入,并且直接使用空方法实现而不是 Spring 增强的子类来处理 bean 类时,这可以防止任何奇怪的错误出现。这个主题相当复杂,最好用例子来说明。
在本例中,我们创建了一个非 singleton bean 和两个 singleton bean,它们都实现了相同的接口。其中一个单体通过使用“传统的”setter 注入获得了一个非单体 bean 的实例;另一种使用方法注入。以下代码示例描述了Singer类,在本例中,它是非 singleton bean 的类型:
package com.apress.prospring5.ch3;
public class Singer {
private String lyric = "I played a quick game of chess with the salt
and pepper shaker";
public void sing() {
//commented because it pollutes the output
//System.out.println(lyric);
}
}
这个类毫无疑问是无趣的,但是它完美地服务于这个例子的目的。接下来,您可以看到DemoBean接口,它由两个单独的 bean 类实现。
package com.apress.prospring5.ch3;
public interface DemoBean {
Singer getMySinger();
void doSomething();
}
这个 bean 有两个方法:getMySinger()和doSomething()。示例应用使用getMySinger()方法获取对Singer实例的引用,并且在方法查找 bean 的情况下,执行实际的方法查找。doSomething()方法是一个简单的方法,它依赖于Singer类来完成处理。下面的代码片段显示了StandardLookupDemoBean类,它使用 setter 注入来获得Singer类的一个实例:
package com.apress.prospring5.ch3;
public class StandardLookupDemoBean
implements DemoBean {
private Singer mySinger;
public void setMySinger(Singer mySinger) {
this.mySinger = mySinger;
}
@Override
public Singer getMySinger() {
return this.mySinger;
}
@Override
public void doSomething() {
mySinger.sing();
}
}
这段代码看起来应该很熟悉,但是请注意,doSomething()方法使用存储的Singer实例来完成它的处理。在下面的代码片段中,您可以看到AbstractLookupDemoBean类,它使用方法注入来获得Singer类的一个实例。
package com.apress.prospring5.ch3;
public abstract class AbstractLookupDemoBean
implements DemoBean {
public abstract Singer getMySinger();
@Override
public void doSomething() {
getMySinger().sing();
}
}
注意,getMySinger()方法被声明为抽象的,并且这个方法被doSomething()方法调用以获得一个Singer实例。本例的 Spring XML 配置包含在名为app-context-xml.xml的文件中,如下所示:
<beans ...>
<bean id="singer" class="com.apress.prospring5.ch3.Singer"
scope="prototype"/>
<bean id="abstractLookupBean"
class="com.apress.prospring5.ch3.AbstractLookupDemoBean">
<lookup-method name="getMySinger" bean="singer"/>
</bean>
<bean id="standardLookupBean"
class="com.apress.prospring5.ch3.StandardLookupDemoBean">
<property name="mySinger" ref="singer"/>
</bean>
</beans>
到现在为止,singer和standardLookupBeanbean 的配置看起来应该很熟悉了。对于abstract-LookupBean,您需要使用<lookup-method>标签来配置查找方法。<lookup-method>标签的name属性告诉 Spring 它应该覆盖的 bean 上的方法的名称。该方法不能接受任何参数,并且返回类型应该是要从该方法返回的 bean 的类型。在这种情况下,该方法应该返回一个类型为Singer的类,或者它的子类。bean 属性告诉 Spring 查找方法应该返回哪个 bean。下面的代码片段显示了这个例子的最后一段代码,它是包含用于运行这个例子的main()方法的类:
package com.apress.prospring5.ch3;
import org.springframework.context.support.GenericXmlApplicationContext;
import org.springframework.util.StopWatch;
public class LookupDemo {
public static void main(String... args) {
GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-xml.xml");
ctx.refresh();
DemoBean abstractBean = ctx.getBean("abstractLookupBean",
DemoBean.class);
DemoBean standardBean = ctx.getBean("standardLookupBean",
DemoBean.class);
displayInfo("abstractLookupBean", abstractBean);
displayInfo("standardLookupBean", standardBean);
ctx.close();
}
public static void displayInfo(String beanName, DemoBean bean) {
Singer singer1 = bean.getMySinger();
Singer singer2 = bean.getMySinger();
System.out.println("" + beanName + ": Singer Instances the Same? "
+ (singer1 == singer2));
StopWatch stopWatch = new StopWatch();
stopWatch.start("lookupDemo");
for (int x = 0; x < 100000; x++) {
Singer singer = bean.getMySinger();
singer.sing();
}
stopWatch.stop();
System.out.println("100000 gets took "
+ stopWatch.getTotalTimeMillis() + " ms");
}
}
在这段代码中,您可以看到来自GenericXmlApplicationContext的abstractLookupBean和standardLookupBean被检索,并且每个引用被传递给displayInfo()方法。只有在使用查找方法注入时才支持抽象类的实例化,在查找方法注入中,Spring 将使用 CGLIB 来生成 AbstractLookupDemoBean 类的子类,该子类动态覆盖该方法。displayInfo()方法的第一部分创建了两个Singer类型的局部变量,并通过调用传递给它们的 bean 上的getMySinger()给它们赋值。使用这两个变量,它向控制台写入一条消息,指示这两个引用是否指向同一个对象。
对于abstractLookupBean bean,每次调用getMySinger()都应该检索一个新的Singer实例,所以引用不应该相同。
对于standardLookupBean,setter 注入将Singer的单个实例传递给 bean,并且每次调用getMySinger()时都会存储和返回这个实例,所以这两个引用应该是相同的。
上一个例子中使用的StopWatch类是 Spring 提供的一个实用程序类。当您需要执行简单的性能测试和测试您的应用时,您会发现StopWatch非常有用。
displayInfo()方法的最后一部分运行一个简单的性能测试,看看哪个 bean 更快。显然,standardLookupBean应该更快,因为它每次都返回相同的实例,但是看到不同之处是很有趣的。我们现在可以运行LookupDemo类进行测试。下面是我们从这个例子中得到的输出:
[abstractLookupBean]: Singer Instances the Same? false
100000 gets took 431 ms
[standardLookupBean]: Singer Instances the Same? true
100000 gets took 1 ms
如您所见,正如所料,当我们使用standardLookupBean时,Singer实例是相同的,而当我们使用abstractLookupBean时是不同的。使用standardLookupBean时会有明显的性能差异,但这是意料之中的。
当然,有一种等效的方法可以使用注释来配置前面介绍的 beans。singer bean 必须有一个额外的注释来指定prototype的范围。
package com.apress.prospring5.ch3.annotated;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component("singer)
@Scope("prototype")
public class Singer {
private String lyric = "I played a quick game of chess
with the salt and pepper shaker";
public void sing() {
// commented to avoid console pollution
//System.out.println(lyric);
}
}
AbstractLookupDemoBean类不再是一个抽象类,方法getMySinger()有一个空体,并且用@Lookup进行了注释,该方法接收Singer bean 的名称作为参数。在动态生成的子类中,方法体将被覆盖。
package com.apress.prospring5.ch3.annotated;
import org.springframework.beans.factory.annotation.Lookup;
import org.springframework.stereotype.Component;
@Component("abstractLookupBean")
public class AbstractLookupDemoBean implements DemoBean {
@Lookup("singer")
public Singer getMySinger() {
return null; // overriden dynamically
}
@Override
public void doSomething() {
getMySinger().sing();
}
}
只有StandardLookupDemoBean类必须用@Component注释,setMySinger必须用@Autowired和@Qualifier注释,以注入singer bean。
package com.apress.prospring5.ch3.annotated;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
@Component("standardLookupBean")
public class StandardLookupDemoBean implements DemoBean {
private Singer mySinger;
@Autowired
@Qualifier("singer")
public void setMySinger(Singer mySinger) {
this.mySinger = mySinger;
}
@Override
public Singer getMySinger() {
return this.mySinger;
}
@Override
public void doSomething() {
mySinger.sing();
}
}
名为app-context-annotated. xml的配置文件必须只为包含注释类的包启用组件扫描。
<beans ...>
<context:component-scan
base-package="com.apress.prospring5.ch3.annotated"/>
</beans>
用于执行代码的类与类LookupDemo相同;唯一的区别是 XML 文件被用作创建GenericXmlApplicationContext对象的参数。
如果我们想完全摆脱 XML 文件,可以使用一个配置类来启用对com.apress.prospring5.ch3.annotated包的组件扫描。并且这个类可以在你需要的地方被声明,这意味着在这个例子中在类内部被运行来测试 beans,如下所示:
package com.apress.prospring5.ch3.config;
import com.apress.prospring5.ch3.annotated.DemoBean;
import com.apress.prospring5.ch3.annotated.Singer;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.util.StopWatch;
import java.util.Arrays;
public class LookupConfigDemo {
@Configuration
@ComponentScan(basePackages = {"com.apress.prospring5.ch3.annotated"})
public static class LookupConfig {}
public static void main(String... args) {
GenericApplicationContext ctx =
new
AnnotationConfigApplicationContext(LookupConfig.class);
DemoBean abstractBean = ctx.getBean("abstractLookupBean",
DemoBean.class);
DemoBean standardBean = ctx.getBean("standardLookupBean",
DemoBean.class);
displayInfo("abstractLookupBean", abstractBean);
displayInfo("standardLookupBean", standardBean);
ctx.close();
}
public static void displayInfo(String beanName, DemoBean bean) {
// same implementation as before
...
}
}
使用注释和 Java 配置的替代配置在第四章中有更详细的介绍。
查找方法注入的注意事项
查找方法注入适用于当您想要使用两个不同生命周期的 beans 时。当 beans 共享相同的生命周期时,避免使用查找方法注入的诱惑,尤其是当它们是单例的时候。运行前一个示例的输出显示了使用方法注入获取依赖项的新实例与使用标准 DI 获取依赖项的单个实例之间的显著性能差异。此外,确保不要不必要地使用查找方法注入,即使您有不同生命周期的 beans。
考虑这样一种情况,其中有三个单体共享一个共同的依赖项。您希望每个 singleton 都有自己的依赖实例,所以您创建了一个非 singleton 的依赖,但是您对每个 singleton 在其整个生命周期中使用 collaborator 的同一个实例感到满意。在这种情况下,setter 注入是理想的解决方案;查找方法注入只是增加了不必要的开销。
当您使用查找方法注入时,在构建您的类时,有几个设计准则您应该记住。在前面的例子中,我们在接口中声明了 lookup 方法。我们这样做的唯一原因是我们不必为两种不同的 bean 类型重复两次displayInfo()方法。如前所述,通常您不需要用不必要的定义来污染业务接口,这些定义仅用于 IoC 目的。另一点是,虽然您不必使您的查找方法抽象,但这样做可以防止您忘记配置查找方法,然后意外地使用空白实现。当然,这只适用于 XML 配置。基于批注的配置强制该方法的空实现;否则,您的 bean 将不会被创建。
方法替换
尽管 Spring 文档将方法替换归类为一种注入形式,但它与您目前所看到的不同。到目前为止,我们使用 injection 纯粹是为了给他们的合作者提供 beans。使用方法替换,您可以任意替换任何 bean 上的任何方法的实现,而不必更改正在修改的 bean 的源代码。例如,您有一个在 Spring 应用中使用的第三方库,您需要更改某个方法的逻辑。但是,您不能更改源代码,因为它是由第三方提供的,所以一种解决方案是使用方法替换,用您自己的实现来替换该方法的逻辑。
在内部,您可以通过动态创建 bean 类的子类来实现这一点。您使用 CGLIB 并将对您想要替换的方法的调用重定向到实现MethodReplacer接口的另一个 bean。在下面的代码示例中,您可以看到一个简单的 bean,它声明了formatMessage()方法的两个重载:
package com.apress.prospring5.ch3;
public class ReplacementTarget {
public String formatMessage(String msg) {
return "<h1>" + msg + "</h1>";
}
public String formatMessage(Object msg) {
return "<h1>" + msg + "</h1>";
}
}
通过使用 Spring 的方法替换功能,可以替换ReplacementTarget类中的任何方法。在这个例子中,我们向您展示了如何替换formatMessage(String)方法,并且我们还比较了替换后的方法与原始方法的性能。
要替换一个方法,首先需要创建一个MethodReplacer接口的实现;下面的代码示例显示了这一点:
package com.apress.prospring5.ch3;
import org.springframework.beans.factory.support.MethodReplacer;
import java.lang.reflect.Method;
public class FormatMessageReplacer
implements MethodReplacer {
@Override
public Object reimplement(Object arg0, Method method, Object... args)
throws Throwable {
if (isFormatMessageMethod(method)) {
String msg = (String) args0;
return "<h2>" + msg + "</h2>";
} else {
throw new IllegalArgumentException("Unable to reimplement method "
+ method.getName());
}
}
private boolean isFormatMessageMethod(Method method) {
if (method.getParameterTypes().length != 1) {
return false;
}
if (!("formatMessage".equals(method.getName()))) {
return false;
}
if (method.getReturnType() != String.class) {
return false;
}
if (method.getParameterTypes()[0] != String.class) {
return false;
}
return true;
}
}
MethodReplacer接口只有一个方法reimplement(),您必须实现它。向reimplement()传递三个参数:调用原始方法的 bean、表示被覆盖方法的Method实例,以及传递给该方法的参数数组。reimplement()方法应该返回你重新实现的逻辑的结果,并且,显然,返回值的类型应该与你替换的方法的返回类型兼容。在前面的代码示例中,FormatMessageReplacer首先检查被覆盖的方法是否是formatMessage(String)方法;如果是,它执行替换逻辑(在本例中,用<h2>和</h2>包围消息)并将格式化的消息返回给调用者。没有必要检查消息是否正确,但是如果您使用几个具有相似参数的MethodReplacer时,这可能是有用的。使用检查有助于防止意外使用具有兼容参数和返回类型的不同MethodReplacer的情况。
在下面列出的配置示例中,您可以看到一个定义了两个类型为ReplacementTarget的 beans 的ApplicationContext实例;一个替换了formatMessage(String)方法,另一个没有(文件名为app-context-xml.xml):
<beans ...>
<bean id="methodReplacer"
class="com.apress.prospring5.ch3.FormatMessageReplacer"/>
<bean id="replacementTarget"
class="com.apress.prospring5.ch3.ReplacementTarget">
<replaced-method name="formatMessage" replacer="methodReplacer">
<arg-type>String</arg-type>
</replaced-method>
</bean>
<bean id="standardTarget"
class="com.apress.prospring5.ch3.ReplacementTarget"/>
</beans>
如您所见,MethodReplacer实现在ApplicationContext中被声明为 bean。然后使用<replaced-method>标签替换replacementTargetBean上的formatMessage(String)方法。<replaced-method>标签的 name 属性指定要替换的方法的名称,replacer 属性用于指定我们想要替换方法实现的MethodReplacer bean 的名称。在有重载方法的情况下,比如在ReplacementTarget类中,您可以使用<arg-type>标签来指定要匹配的方法签名。<arg-type>标签支持模式匹配,所以String与java.lang.String匹配,也与java.lang.StringBuffer匹配。
下面的代码片段展示了一个简单的演示应用,它从ApplicationContext中检索standardTarget和replacement-Targetbean,执行它们的formatMessage(String)方法,然后运行一个简单的性能测试,看看哪个更快。
package com.apress.prospring5.ch3;
import org.springframework.context.support.GenericXmlApplicationContext;
import org.springframework.util.StopWatch;
public class MethodReplacementDemo {
public static void main(String... args) {
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-xml.xml");
ctx.refresh();
ReplacementTarget replacementTarget = (ReplacementTarget) ctx
.getBean("replacementTarget");
ReplacementTarget standardTarget = (ReplacementTarget) ctx
.getBean("standardTarget");
displayInfo(replacementTarget);
displayInfo(standardTarget);
ctx.close();
}
private static void displayInfo(ReplacementTarget target) {
System.out.println(target.formatMessage("Thanks for playing, try again!"));
StopWatch stopWatch = new StopWatch();
stopWatch.start("perfTest");
for (int x = 0; x < 1000000; x++) {
String out = target.formatMessage("No filter in my head");
//commented to not pollute the console
//System.out.println(out);
}
stopWatch.stop();
System.out.println("1000000 invocations took: "
+ stopWatch.getTotalTimeMillis() + " ms");
}
}
您现在应该对这段代码很熟悉了,所以我们就不赘述了。在我们的机器上,运行此示例会产生以下输出:
<h2>Thanks for playing, try again!</h2>
1000000 invocations took: 188 ms
<h1>Thanks for playing, try again!</h1>
1000000 invocations took: 24 ms
正如所料,replacementTarget bean 的输出反映了Method-Replacer提供的被覆盖的实现。有趣的是,动态替换的方法比静态定义的方法慢很多倍。在MethodReplacer中取消对有效方法的检查对许多执行来说没有什么影响,所以我们可以得出结论,大部分开销都在 CGLIB 子类中。
何时使用方法替换
方法替换在各种情况下都非常有用,特别是当您只想为单个 bean 而不是同一类型的所有 bean 重写一个特定的方法时。也就是说,我们仍然喜欢使用标准的 Java 机制来覆盖方法,而不是依赖运行时字节码的增强。
如果您打算将方法替换作为应用的一部分,我们建议您为每个方法或重载方法组使用一个Method-Replacer。避免对许多不相关的方法使用一个MethodReplacer的诱惑;这将导致额外的不必要的字符串比较,而您的代码会决定应该重新实现哪个方法。我们发现执行简单的检查来确保MethodReplacer使用正确的方法是有用的,并且不会给你的代码增加太多的开销。如果您真的关心性能,您可以简单地向您的MethodReplacer添加一个布尔属性,它允许您使用依赖注入来打开和关闭检查。
理解 Bean 命名
Spring 支持相当复杂的 bean 命名结构,允许您灵活地处理许多情况。每个 bean 必须至少有一个名称,该名称在包含的ApplicationContext中是唯一的。Spring 遵循一个简单的解析过程来确定 bean 的名称。如果您给<bean>标签一个id属性,该属性的值将被用作名称。如果没有指定id属性,Spring 将查找一个name属性,如果定义了一个属性,它将使用在name属性中定义的名字。(我们说名字是因为可以在name属性中定义多个名字;稍后将对此进行更详细的介绍。)如果既没有指定id也没有指定name属性,Spring 使用 bean 的类名作为名称,当然,前提是没有其他 bean 使用相同的类名。如果声明了多个没有 ID 或名称的相同类型的 beans,Spring 将在ApplicationContext初始化期间抛出一个异常(类型org.springframework.beans.factory.NoSuchBeanDefinitionException)。以下配置示例描述了所有三种命名方案(app-context-01.xml):
<beans ...>
<bean id="string1" class="java.lang.String"/>
<bean name="string2" class="java.lang.String"/>
<bean class="java.lang.String"/>
</beans>
从技术角度来看,这两种方法都同样有效,但是哪一种是您的应用的最佳选择呢?首先,避免使用自动按类命名的行为。这不允许您灵活地定义多个相同类型的 beans,定义自己的名称要好得多。这样,如果 Spring 将来改变了默认行为,您的应用将继续工作。如果您想看看 Spring 是如何命名 beans 的,使用前面的配置,运行下面的例子:
package com.apress.prospring5.ch3.xml;
import org.springframework.context.support.GenericXmlApplicationContext;
public class BeanNamingTest {
public static void main(String... args) {
GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-01.xml");
ctx.refresh();
Map<String,String> beans = ctx.getBeansOfType(String.class);
beans.entrySet().stream().forEach(b -> System.out.println(b.getKey()));
ctx.close();
}
}
ctx.getBeansOfType(String.class)用于获得一个映射,其中包含所有类型为String的 beans 以及它们在ApplicationContext中的 id。映射的键是 bean IDs,它是使用前面代码中的 lambda 表达式打印的。使用上述配置,输出如下:
string1
string2
java.lang.String#0
前面的输出示例中的最后一行是 Spring 给类型为String的 bean 的 ID,该 bean 在配置中没有明确命名。如果修改配置以添加另一个String未命名的 bean,它将如下所示:
<beans ...>
<bean id="string1" class="java.lang.String"/>
<bean name="string2" class="java.lang.String"/>
<bean class="java.lang.String"/>
<bean class="java.lang.String"/>
</beans>
输出将更改如下:
string1
string2
java.lang.String#0
java.lang.String#1
在 Spring 3.1 之前,id属性与 XML 标识(即xsd:ID)相同,这限制了您可以使用的字符。从 Spring 3.1 开始,Spring 使用xsd:String作为id属性,所以以前对可以使用的字符的限制已经没有了。然而,Spring 将继续确保id在整个ApplicationContext中是独一无二的。通常,您应该使用id属性为 bean 命名,然后使用名称别名将 bean 与其他名称关联起来,这将在下一节中讨论。
Bean 名称别名
Spring 允许一个 bean 有多个名字。您可以通过在 bean 的<bean>标记的name属性中指定以空格、逗号或分号分隔的名称列表来实现这一点。您可以代替id属性或者与该属性结合使用。除了使用name属性,您还可以使用<alias>标签来定义 Spring bean 名称的别名。下面的配置示例展示了一个简单的<bean>配置,它为单个 bean ( app-context-02.xml)定义了多个名称:
<beans ...>
<bean id="john" name="john johnny,jonathan;jim" class="java.lang.String"/>
<alias name="john" alias="ion"/>
</beans>
正如您所看到的,我们已经定义了六个名称:一个使用了id属性,另外四个作为列表使用了name属性中所有允许的 bean 名称分隔符(这只是为了演示,不建议在实际开发中使用)。在实际开发中,建议您标准化用于在应用中分隔 bean 名称声明的分隔符。使用<alias>标签又定义了一个别名。下面的代码示例描述了一个 Java 例程,它使用不同的名称从ApplicationContext实例中获取同一个 bean 六次,并验证它们是同一个 bean。此外,它利用前面介绍的ctx.getBeansOfType(..)方法来确保上下文中只有一个String bean。
package com.apress.prospring5.ch3.xml;
import org.springframework.context.support.GenericXmlApplicationContext;
import java.util.Map;
public class BeanNameAliasing {
public static void main(String... args) {
GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-02.xml");
ctx.refresh();
String s1 = (String) ctx.getBean("john");
String s2 = (String) ctx.getBean("jon");
String s3 = (String) ctx.getBean("johnny");
String s4 = (String) ctx.getBean("jonathan");
String s5 = (String) ctx.getBean("jim");
String s6 = (String) ctx.getBean("ion");
System.out.println((s1 == s2));
System.out.println((s2 == s3));
System.out.println((s3 == s4));
System.out.println((s4 == s5));
System.out.println((s5 == s6));
Map<String,String> beans = ctx.getBeansOfType(String.class);
if(beans.size() == 1) {
System.out.println("There is only one String bean.");
}
ctx.close();
}
}
执行前面的代码将打印五次true和“只有一个字符串 bean”文本,验证使用不同名称访问的 bean 实际上是同一个 bean。
您可以通过调用ApplicationContext.getAliases(String)并传入任何 bean 的名称或 id 来检索 bean 别名的列表。除了您指定的别名之外,别名列表将作为一个String数组返回。
前面提到过,在 Spring 3.1 之前,id属性与 XML 标识(即xsd:ID)相同,这意味着 bean IDs 不能包含空格、逗号或分号等特殊字符。从 Spring 3.1 开始,xsd:String被用于id属性,因此以前对您可以使用的字符的限制消失了。但是,这并不意味着您可以使用以下内容:
<bean name="jon johnny,jonathan;jim" class="java.lang.String"/>
而不是这个:
<bean id="jon johnny,jonathan;jim" class="java.lang.String"/>
Spring IoC 对属性name和id的处理是不同的。您可以通过调用ApplicationContext.getAliases(String)并传入任何一个 bean 的名称或 id 来检索 bean 别名的列表。除了您指定的别名之外,别名列表将作为一个String数组返回。这意味着,在第一种情况下,jon将成为 id,其余的值将成为别名。
在第二种情况下,当相同的字符串用作id属性的值时,完整的字符串成为 bean 的唯一标识符。这可以很容易地用如图所示的配置进行测试(在文件app-context-03.xml中找到):
<beans ...>
<bean name="jon johnny,jonathan;jim" class="java.lang.String"/>
<bean id="jon johnny,jonathan;jim" class="java.lang.String"/>
</beans>
和一个主类,如下面的代码示例所示:
package com.apress.prospring5.ch3.xml;
import org.springframework.context.support.GenericXmlApplicationContext;
import java.util.Arrays;
import java.util.Map;
public class BeanCrazyNaming {
public static void main(String... args) {
GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-03.xml");
ctx.refresh();
Map<String,String> beans = ctx.getBeansOfType(String.class);
beans.entrySet().stream().forEach(b ->
{
System.out.println("id: " + b.getKey() +
"\n aliases: " + Arrays.toString(ctx.getAliases(b.getKey())) +"\n");
});
ctx.close();
}
}
运行时,将产生以下输出:
id: jon
aliases: jonathan, jim, johnny
id: jon johnny,jonathan;jim
aliases:
如您所见,带有Stringbean 的映射包含两个 bean,一个带有jon惟一标识符和三个别名,另一个带有jon johnny,jonathan;jim惟一标识符,没有别名。
Bean 名称别名是一种奇怪的东西,因为在构建新的应用时,您并不倾向于使用它。如果您要让许多其他 bean 注入另一个 bean,它们也可以使用相同的名称来访问该 bean。然而,随着您的应用进入生产环境,维护工作得到执行,修改被进行,等等,bean 名称别名变得更加有用。
考虑以下场景:您有一个应用,其中使用 Spring 配置的 50 个 beans 都需要一个Foo接口的实现。其中 25 个 bean 使用 bean 名称为standardFoo的StandardFoo实现,另外 25 个 bean 使用 bean 名称为superFoo的SuperFoo实现。在您将应用投入生产的六个月后,您决定将前 25 个 beans 转移到SuperFoo实现中。为此,您有三种选择。
- 首先是将
standardFoobean 的实现类改为SuperFoo。这种方法的缺点是,当您实际上只需要一个时,却有两个SuperFoo类的实例。此外,当配置发生变化时,您现在有两个 beans 可以进行更改。 - 第二个选项是更新正在变化的 25 个 bean 的注入配置,这将 bean 的名称从
standardFoo更改为superFoo。这种方法并不是最优雅的处理方式。您可以执行查找和替换,但是当管理层不满意时回滚您的更改意味着从您的版本控制系统中检索您的配置的旧版本。 - 第三种也是最理想的方法是删除(或注释掉)对
standardFoobean 的定义,并使standardFoo成为superFoo的别名。这种改变只需最少的努力,将系统恢复到以前的配置也同样简单。
带有注释配置的 Bean 命名
当使用注释声明 bean 定义时,bean 命名与 XML 略有不同,您可以做更多有趣的事情。不过,让我们从基础开始:使用原型注释(@Component及其所有专门化,如Service、Repository和Controller)声明 bean 定义。
考虑下面的Singer类:
package com.apress.prospring5.ch3.annotated;
import org.springframework.stereotype.Component;
@Component
public class Singer {
private String lyric = "We found a message in a bottle we were drinking";
public void sing() {
System.out.println(lyric);
}
}
该类包含使用@Component注释编写的类型为Singer的单例 bean 的声明。@Component注释没有任何参数,所以 Spring IoC 容器为 bean 决定了一个惟一的标识符。在这种情况下,遵循的惯例是将 bean 命名为类本身,但首字母要小写。这意味着该 bean 将被命名为singer。这个约定也受到其他原型注释的尊重。为了测试这一点,可以使用下面的类:
package com.apress.prospring5.ch3.annotated;
import org.springframework.context.support.GenericXmlApplicationContext;
import java.util.Arrays;
import java.util.Map;
public class AnnotatedBeanNaming {
public static void main(String... args) {
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-annotated.xml");
ctx.refresh();
Map<String,Singer> beans =
ctx.getBeansOfType(Singer.class);
beans.entrySet().stream().forEach(b ->
System.out.println("id: " + b.getKey()));
ctx.close();
}
}
app-context-annotated.xml配置文件仅包含com.apress.prospring5.ch3.annotated的组件扫描声明,因此不会再次显示。运行前面的类时,控制台中会输出以下内容:
id: singer
因此,使用@Component("singer")相当于用@Component来注释Singer类。如果您想用不同的方式命名 bean,那么@Component注释必须接收 bean 名称作为参数。
package com.apress.prospring5.ch3.annotated;
import org.springframework.stereotype.Component;
@Component("johnMayer")
public class Singer {
private String lyric = "Down there below us, under the clouds";
public void sing() {
System.out.println(lyric);
}
}
正如所料,如果运行AnnotatedBeanNaming,将产生以下输出:
id: johnMayer
但是,别名呢?由于@Component注释的参数变成了 bean 的唯一标识符,所以在以这种方式声明 bean 时,bean 别名是不可能的。这就是 Java 配置的用武之地。让我们考虑下面的类,它包含一个在其中定义的静态配置类(是的,Spring 允许这样做,我们在这里很实际,将所有的逻辑保存在同一个文件中):
package com.apress.prospring5.ch3.config;
import com.apress.prospring5.ch3.annotated.Singer;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;
import java.util.Arrays;
import java.util.Map;
public class AliasConfigDemo {
@Configuration
public static class AliasBeanConfig {
@Bean
public Singer singer(){
return new Singer();
}
}
public static void main(String... args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(AliasBeanConfig.class);
Map<String,Singer> beans = ctx.getBeansOfType(Singer.class);
beans.entrySet().stream().forEach(b ->
System.out.println("id: " + b.getKey()
+ "\n aliases: "
+ Arrays.toString(ctx.getAliases(b.getKey())) + "\n")
);
ctx.close();
}
}
这个类包含一个类型为Singer的 bean 的 bean 定义,它是通过用@Bean注释对singer()方法进行注释而声明的。当没有为这个注释提供参数时,bean 的唯一标识符,它的id,成为方法名。因此,当前面的类运行时,我们得到以下输出:
id: singer
aliases:
为了声明别名,我们使用了@Bean注释的name属性。该属性是该注释的默认属性,这意味着在这种情况下,通过用@Bean、@Bean("singer")或@Bean(name="singer")注释singer()方法来声明 bean 会导致相同的结果。Spring IoC 容器将创建一个类型为SingerID 为singer的 bean。
如果该属性的值是包含别名特定分隔符(空格、逗号、分号)的字符串,则该字符串将成为 bean 的 ID。但是,如果它的值是一个字符串数组,第一个就变成了id,其他的变成了别名。如下所示修改 bean 配置:
@Configuration
public static class AliasBeanConfig {
@Bean(name={"johnMayer","john","jonathan","johnny"})
public Singer singer(){
return new Singer();
}
}
当运行AliasConfigDemo类时,输出将变为如下:
id: johnMayer
aliases: jonathan, johnny, john
谈到别名,Spring 4.2 中引入了@AliasFor注释。该注释用于声明注释属性的别名,大多数 Spring 注释都使用它。例如,@Bean注释有两个属性,name和value,它们被声明为彼此的别名。使用此注释,它们是显式别名。下面的代码片段是@Bean注释代码的快照,取自官方的 Spring GitHub 库。跳过目前不相关的代码和文档: 4
package org.springframework.context.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
...
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Bean {
@AliasFor("name")
String value() default {};
@AliasFor("value")
String name() default {};
...
}
这里有一个例子。当然,声明一个名为@Award的注释,它可以用在Singer实例上。
package com.apress.prospring5.ch3.annotated;
import org.springframework.core.annotation.AliasFor;
public @interface Award {
@AliasFor("prize")
String value() default {};
@AliasFor("value")
String prize() default {};
}
使用这个注释,您可以像这样修改Singer类:
package com.apress.prospring5.ch3.annotated;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
@Component("johnMayer")
@Award(prize = {"grammy", "platinum disk"})
public class Singer {
private String lyric = "We found a message in a bottle we were drinking";
public void sing() {
System.out.println(lyric);
}
}
前面的注释相当于@Award(value={"grammy", "platinum disk"})和@Award({"grammy", "platinum disk"})。
但是使用@AliasFor注释可以做一些更有趣的事情:可以声明元注释属性的别名。在下面的代码片段中,我们为@Award注释声明了一个专门化,它声明了一个名为name的属性,这是@Award注释的value属性的别名。我们这样做是因为我们想清楚地表明参数是唯一的 bean 标识符。
package com.apress.prospring5.ch3.annotated;
import org.springframework.core.annotation.AliasFor;
@Award
public @interface Trophy {
@AliasFor(annotation = Award.class, attribute = "value")
String name() default {};
}
因此,不要像这样编写Singer类:
package com.apress.prospring5.ch3.annotated;
import org.springframework.stereotype.Component;
@Component("johnMayer")
@Award(value={"grammy", "platinum disk"})
public class Singer {
private String lyric = "We found a message in a bottle we were drinking";
public void sing() {
System.out.println(lyric);
}
}
我们可以这样写:
package com.apress.prospring5.ch3.annotated;
@Component("johnMayer")
@Trophy(name={"grammy", "platinum disk"})
public class Singer {
private String lyric = "We found a message in a bottle we were drinking";
public void sing() {
System.out.println(lyric);
}
}
使用另一个注释@AliasFor为注释的属性创建别名确实有局限性。@AliasFor不能用在任何原型注释上(@Component及其专门化)。原因是对这些value属性的特殊处理在@AliasFor发明之前就已经存在了。因此,由于向后兼容性的问题,不可能使用带有这种值属性的@AliasFor。当编写代码这样做时(在原型注释中别名化value属性),不会向您显示任何编译错误,代码甚至可能运行,但是为别名提供的任何参数都将被忽略。这同样适用于@Qualifier注释。
了解 Bean 实例化模式
默认情况下,Spring 中的所有 beans 都是单例的。这意味着 Spring 维护 bean 的单个实例,所有依赖对象使用同一个实例,所有对ApplicationContext.getBean()的调用返回同一个实例。我们在上一节中演示了这一点,我们能够使用身份比较(==)而不是equals()来检查 beans 是否相同。
术语 singleton 在 Java 中可以互换使用,指两个不同的概念:在应用中有单个实例的对象和 Singleton 设计模式。我们将第一个概念称为单例,将单例模式称为单例。单体设计模式在 Erich Gamma 等人的开创性的设计模式:可重用面向对象软件的元素(Addison-Wesley,1994)中流行开来。当人们混淆了对单例实例的需求和应用单例模式的需求时,问题就出现了。下面的代码片段显示了 Java 中单例模式的典型实现:
package com.apress.prospring5.ch3;
public class Singleton {
private static Singleton instance;
static {
instance = new Singleton();
}
public static Singleton getInstance() {
return instance;
}
}
这种模式实现了允许您在整个应用中维护和访问类的单个实例的目标,但这是以增加耦合为代价的。为了获得实例,您的应用代码必须始终明确了解 Singleton 类——完全消除了编写接口代码的能力。
实际上,单例模式实际上是两种模式合二为一。第一种模式,也是我们想要的模式,涉及到对象的单个实例的维护。第二种,也是不太理想的,是一种对象查找模式,它完全排除了使用接口的可能性。使用单例模式也使得随意交换实现变得困难,因为大多数需要单例实例的对象都直接访问单例对象。当您试图对您的应用进行单元测试时,这会导致各种各样的麻烦,因为您无法用 mock 替换 Singleton 来进行测试。
幸运的是,使用 Spring,您可以利用单例实例化模型,而不必绕过单例设计模式。默认情况下,Spring 中的所有 bean 都被创建为单例实例,并且 Spring 使用相同的实例来完成对该 bean 的所有请求。当然,Spring 不仅仅局限于使用 Singleton 实例;它仍然可以创建一个新的 bean 实例来满足每个依赖项和对getBean()的每个调用。它完成所有这些,对您的应用代码没有任何影响,因此,我们喜欢称 Spring 为实例化模式不可知的。这是一个强大的概念。如果您从一个单一对象开始,但后来发现它并不真正适合多线程访问,那么您可以将它更改为非单一对象(原型),而不会影响任何应用代码。
虽然改变 bean 的实例化模式不会影响你的应用代码,但是如果你依赖 Spring 的生命周期接口,这确实会引起一些问题。我们将在第四章中详细介绍这一点。
将实例化模式从 singleton 更改为 nonsingleton 很简单。以下配置片段展示了如何在 XML 中并使用注释来实现这一点:
<!-- app-context-xml.xml -->
<beans ...>
<bean id="nonSingleton" class="com.apress.prospring5.ch3.annotated.Singer"
scope="prototype" c:_0="John Mayer"/>
</beans>
\\Singer.java
package com.apress.prospring5.ch3.annotated;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component("nonSingleton")
@Scope("prototype")
public class Singer {
private String name = "unknown";
public Singer(@Value("John Mayer") String name) {
this.name = name;
}
}
在 XML 配置中,Singer类可以用作 XML 中声明的 bean 的类型。如果没有启用组件扫描,那么类中的注释将被忽略。
正如您所看到的,这个 bean 声明与您到目前为止看到的任何声明之间的唯一区别是我们添加了scope属性并将值设置为prototype。Spring 将范围默认为值singleton。prototype 作用域指示 Spring 在每次应用请求 bean 实例时实例化一个新的 bean 实例。以下代码片段显示了此设置对您的应用的影响:
package com.apress.prospring5.ch3;
import com.apress.prospring5.ch3.annotated.Singer;
import org.springframework.context.support.GenericXmlApplicationContext;
public class NonSingletonDemo {
public static void main(String... args) {
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-xml.xml");
ctx.refresh();
Singer singer1 = ctx.getBean("nonSingleton", Singer.class);
Singer singer2 = ctx.getBean("nonSingleton", Singer.class);
System.out.println("Identity Equal?: " + (singer1 ==singer2));
System.out.println("Value Equal:? " + singer1.equals(singer2));
System.out.println(singer1);
System.out.println(singer2);
ctx.close();
}
}
运行此示例会得到以下输出:
Identity Equal?: false
Value Equal:? false
John Mayer
John Mayer
从这里可以看出,尽管两个String对象的值明显相等,但它们的标识并不相等,即使两个实例都是使用相同的 bean 名称检索的。
选择实例化模式
在大多数场景中,很容易看出哪种实例化模式是合适的。通常,您会发现 singleton 是 beans 的默认模式。一般来说,单件应该在下列情况下使用:
- 没有状态的共享对象:您有一个没有维护状态的对象,并且有许多依赖对象。因为在没有状态的情况下不需要同步,所以不需要在每次依赖对象需要使用它进行某些处理时创建新的 bean 实例。
- 具有只读状态的共享对象:这与上一点类似,但是您有一些只读状态。在这种情况下,您仍然不需要同步,因此创建一个实例来满足 bean 的每个请求只是增加了开销。
- 具有共享状态的共享对象:如果您的 bean 具有必须共享的状态,那么 singleton 是理想的选择。在这种情况下,请确保状态写入的同步尽可能精确。
- 具有可写状态的高吞吐量对象:如果您有一个在应用中经常使用的 bean,您可能会发现保持一个 singleton 并同步对 bean 状态的所有写访问比不断创建数百个 bean 实例有更好的性能。使用这种方法时,在不牺牲一致性的情况下,尽量保持同步的粒度。您会发现,当您的应用长时间创建大量实例时,当您的共享对象只有少量可写状态时,或者当新实例的实例化代价很高时,这种方法特别有用。
您应该考虑在下列情况下使用非 nonsingletons:
- 具有可写状态的对象:如果您有一个具有许多可写状态的 bean,您可能会发现同步的成本大于创建一个新实例来处理来自依赖对象的每个请求的成本。
- 具有私有状态的对象:一些依赖对象需要一个具有私有状态的 bean,这样它们就可以独立于依赖该 bean 的其他对象进行处理。在这种情况下,singleton 显然不合适,应该使用 nonsingleton。
您从 Spring 的实例化管理中获得的主要好处是,您的应用可以立即受益于与单例相关的较低内存使用,而您只需付出很少的努力。然后,如果您发现单例模式不能满足您的应用的需求,那么修改您的配置以使用非单例模式是一件很简单的事情。
实现 Bean 范围
除了 singleton 和 prototype 作用域之外,在为更具体的目的定义 Spring bean 时,还存在其他作用域。也可以实现自己的自定义作用域,在 Spring 的ApplicationContext中注册。从版本 4 开始,支持以下 bean 范围:
- Singleton:默认的 singleton 范围。每个 Spring IoC 容器只能创建一个对象。
- 原型:当应用请求时,Spring 将创建一个新的实例。
- 请求:供 web 应用使用。将 Spring MVC 用于 web 应用时,具有请求范围的 beans 将为每个 HTTP 请求实例化,然后在请求完成时销毁。
- 会话:供 web 应用使用。将 Spring MVC 用于 web 应用时,具有会话范围的 beans 将为每个 HTTP 会话实例化,然后在会话结束时销毁。
- 全局会话:用于基于 portlet 的 web 应用。全局会话范围 beans 可以在同一个 Spring MVC 驱动的门户应用中的所有 portlets 之间共享。
- Thread:当新线程请求时,Spring 将创建一个新的 bean 实例,而对于同一个线程,将返回同一个 bean 实例。请注意,默认情况下,此范围没有注册。
- 自定义:自定义 bean 作用域,可以通过实现接口
org.springframework.beans.factory.config.Scope并在 Spring 的配置中注册自定义作用域来创建(对于 XML,使用类org.springframework.beans.factory.config.CustomScopeConfigurer)。
解决依赖关系
在正常操作中,Spring 能够通过简单地查看配置文件或类中的注释来解决依赖性。通过这种方式,Spring 可以确保每个 bean 都以正确的顺序配置,这样每个 bean 都可以正确地配置其依赖项。如果 Spring 不执行这个操作,只是创建 bean 并以任何顺序配置它们,那么 bean 可以在依赖项之前创建和配置。这显然不是您想要的,并且会在您的应用中引起各种各样的问题。
不幸的是,Spring 不知道在配置中没有指定的代码中 beans 之间存在的任何依赖关系。例如,以一个名为johnMayer、类型为Singer的 bean 为例,它使用ctx.getBean()获得另一个名为gopher、类型为Guitar的 bean 的实例,并在调用johnMayer.sing()方法时使用它。在这个方法中,您通过调用ctx.getBean("gopher")获得类型Guitar的实例,而不需要 Spring 为您注入依赖关系。在这种情况下,Spring 不知道johnMayer依赖于gopher,因此,它可能会在gopher之前实例化johnMayer。您可以使用<bean>标记的depends-on属性为 Spring 提供关于 bean 依赖关系的附加信息。以下配置片段(包含在名为app-context-01.xml的文件中)显示了如何配置johnMayer和gopher的场景:
<beans ...">
<bean id="johnMayer" class="com.apress.prospring5.ch3.xml.Singer"
depends-on="gopher"/>
<bean id="gopher" class="com.apress.prospring5.ch3.xml.Guitar"/>
</beans>
在这个配置中,我们断言 bean johnMayer依赖于 bean gopher。Spring 应该在实例化 beans 时考虑到这一点,并确保在johnMayer之前创建gopher。然而,要做到这一点,johnMayer需要访问ApplicationContext。因此,我们还必须告诉 Spring 注入这个引用,这样当调用johnMayer.sing()方法时,就可以用它来获取gopher bean。这是通过让Singer bean 实现ApplicationContextAware接口来实现的。这是一个特定于 Spring 的接口,强制实现一个ApplicationContext对象的 setter。它被 Spring IoC 容器自动检测到,创建 bean 的ApplicationContext被注入其中。这是在 bean 的构造函数被调用之后完成的,所以显然在构造函数中使用ApplicationContext会导致一个NullPointerException。你可以在这里看到Singer类的代码:
package com.apress.prospring5.ch3.xml;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
public class Singer implements ApplicationContextAware {
ApplicationContext ctx;
@Override
public void setApplicationContext(
ApplicationContext applicationContext) throws BeansException {
this.ctx = applicationContext;
}
private Guitar guitar;
public Singer(){
}
public void sing() {
guitar = ctx.getBean("gopher", Guitar.class);
guitar.sing();
}
}
Guitar类相当简单;它只包含sing方法,如下所示:
package com.apress.prospring5.ch3.xml;
public class Guitar {
public void sing(){
System.out.println("Cm Eb Fm Ab Bb");
}
}
要测试这个示例,可以使用下面的类:
package com.apress.prospring5.ch3.xml;
import org.springframework.context.support.GenericXmlApplicationContext;
public class DependsOnDemo {
public static void main(String... args) {
GenericXmlApplicationContext
ctx = new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-01.xml");
ctx.refresh();
Singer johnMayer = ctx.getBean("johnMayer", Singer.class);
johnMayer.sing();
ctx.close();
}
}
当然,有一个注释配置相当于前面的 XML 配置。Singer和Guitar必须使用一个原型注释声明为 beans(在这种情况下将使用@Component)。这里的新奇之处在于@DependsOn注释,它被放置在Singer类上。这相当于 XML 配置中的depends-on属性。
package com.apress.prospring5.ch3.annotated;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.DependsOn;
import org.springframework.stereotype.Component;
@Component("johnMayer")
@DependsOn("gopher")
public class Singer implements ApplicationContextAware{
ApplicationContext applicationContext;
@Override public void setApplicationContext(ApplicationContext
applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
private Guitar guitar;
public Singer(){
}
public void sing() {
guitar = applicationContext.getBean("gopher", Guitar.class);
guitar.sing();
}
}
你现在要做的就是启用组件扫描,然后在DependsOnDemo类中使用application- context-02.xml来创建ApplicationContext。
<!-- application-context-02.xml -->
<beans...>
<context:component-scan
base-package="com.apress.prospring5.ch3.annotated"/>
</beans>
该示例将运行,输出将是“Cm Eb Fm Ab Bb”
在开发应用时,避免将它们设计为使用此功能;相反,通过 setter 和 constructor 注入契约来定义依赖关系。然而,如果您将 Spring 与遗留代码集成,您可能会发现代码中定义的依赖项要求您向 Spring 框架提供额外的信息。
自动连接您的 Bean
Spring 支持五种自动布线模式。
byName:当使用byName自动连接时,Spring 试图将每个属性连接到一个同名的 bean。因此,如果目标 bean 有一个名为foo的属性,并且在ApplicationContext中定义了一个foobean,那么foobean 被分配给目标的foo属性。byType:当使用byType自动连接时,Spring 试图通过自动使用ApplicationContext中相同类型的 bean 来连接目标 bean 上的每个属性。constructor:这个功能就像byType连线一样,除了它使用构造函数而不是 setters 来执行注入。Spring 试图在构造函数中匹配尽可能多的参数。因此,如果您的 bean 有两个构造函数,一个接受一个String,另一个接受一个String和一个Integer,并且您的ApplicationContext中有一个String和一个Integerbean,Spring 使用两个参数的构造函数。default:Spring 会自动在constructor和byType模式之间选择。如果您的 bean 有一个默认的(无参数)构造函数,Spring 使用byType;否则,它使用constructor。no:这是默认设置。
因此,如果您在目标 bean 上有一个类型为String的属性,并且在ApplicationContext中有一个类型为String的 bean,那么 Spring 会将String bean 连接到目标 bean 的String属性。如果在同一个ApplicationContext实例中有不止一个相同类型的 bean,在本例中为String,那么 Spring 无法决定使用哪一个进行自动连接,并抛出一个异常(类型org.springframework.beans.factory.NoSuchBeanDefinitionException)。
下面的配置片段显示了一个简单的配置,它通过使用每种模式(app-context-03.xml)自动连接三个相同类型的 beans:
<beans ...>
<bean id="fooOne" class="com.apress.prospring5.ch3.xml.Foo"/>
<bean id="barOne" class="com.apress.prospring5.ch3.xml.Bar"/>
<bean id="targetByName" autowire="byName"
class="com.apress.prospring5.ch3.xml.Target" lazy-init="true"/>
<bean id="targetByType" autowire="byType"
class="com.apress.prospring5.ch3.xml.Target" lazy-init="true"/>
<bean id="targetConstructor" autowire="constructor"
class="com.apress.prospring5.ch3.xml.Target" lazy-init="true"/>
</beans>
您现在应该对这个配置很熟悉了。Foo和Bar是空类。注意,每个Targetbean 的autowire属性都有不同的值。此外,lazy-init属性被设置为true以通知 Spring 仅在第一次请求时实例化 bean,而不是在启动时,这样我们就可以在测试程序的正确位置输出结果。下面的代码示例展示了一个简单的 Java 应用,它从ApplicationContext中检索每个Targetbean:
package com.apress.prospring5.ch3.xml;
import org.springframework.context.support.GenericXmlApplicationContext;
public class Target {
private Foo fooOne;
private Foo fooTwo;
private Bar bar;
public Target() {
}
public Target(Foo foo) {
System.out.println("Target(Foo) called");
}
public Target(Foo foo, Bar bar) {
System.out.println("Target(Foo, Bar) called");
}
public void setFooOne(Foo fooOne) {
this.fooOne = fooOne;
System.out.println("Property fooOne set");
}
public void setFooTwo(Foo foo) {
this.fooTwo = foo;
System.out.println("Property fooTwo set");
}
public void setBar(Bar bar) {
this.bar = bar;
System.out.println("Property bar set");
}
public static void main(String... args) {
GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-03.xml");
ctx.refresh();
Target t = null;
System.out.println("Using byName:\n");
t = (Target) ctx.getBean("targetByName");
System.out.println("\nUsing byType:\n");
t = (Target) ctx.getBean("targetByType");
System.out.println("\nUsing constructor:\n");
t = (Target) ctx.getBean("targetConstructor");
ctx.close();
}
}
在这段代码中,可以看到Target类有三个构造函数:一个无参数构造函数,一个接受Foo实例的构造函数,一个接受Foo和Bar实例的构造函数。除了这些构造函数,Target bean 还有三个属性:两个类型为Foo的属性和一个类型为Bar的属性。这些属性和构造函数中的每一个在被调用时都会向控制台输出中写入一条消息。main()方法只是检索在ApplicationContext中声明的每个Targetbean,触发自动连线过程。以下是运行此示例的输出:
Using byName:
Property fooOne set
Using byType:
Property bar set
Property fooOne set
Property fooTwo set
Using constructor:
Target(Foo, Bar) called
从输出中可以看到,当 Spring 使用byName时,唯一设置的属性是foo,因为这是配置文件中唯一具有相应 bean 条目的属性。当使用byType时,Spring 设置所有三个属性的值。fooOne和fooTwo属性由fooOne bean 设置,bar属性由barOne bean 设置。当使用构造函数时,Spring 使用双参数构造函数,因为 Spring 可以为这两个参数提供 beans,并且不需要回退到另一个构造函数。
当按类型自动连接时,当 bean 类型相关时,事情变得复杂,并且当您有更多实现相同接口的类并且需要自动连接的属性将接口指定为类型时,会抛出异常,因为 Spring 不知道要注入哪个 bean。为了创建这样一个场景,我们将把Foo转换成一个接口,并声明实现它的两个 bean 类型,每个 bean 类型都有自己的 bean 声明。让我们保持默认配置,没有额外的命名。
package com.apress.prospring5.ch3.xml.complicated;
public interface Foo {
// empty interface, used as a marker interface
}
public class FooImplOne implements Foo {
}
public class FooImplOne implements Foo {
}
如果我们要添加一个名为app-context-04.xml的新配置文件,它将包含以下配置:
<beans ...>
<bean id="fooOne"
class="com.apress.prospring5.ch3.xml.complicated.FooImplOne"/>
<bean id="fooTwo"
class="com.apress.prospring5.ch3.xml.complicated.FooImplOne"/>
<bean id="bar" class="com.apress.prospring5.ch3.xml.Bar"/>
<bean id="targetByType" autowire="byType"
class="com.apress.prospring5.ch3.xml.complicated.CTarget"
lazy-init="true"/>
</beans>
对于这个更简单的例子,我们还引入了CTarget类。这与最近引进的Target级相同;只有main()方法不同。代码片段如下所示:
package com.apress.prospring5.ch3.xml.complicated;
import com.apress.prospring5.ch3.xml.*;
import org.springframework.context.support.GenericXmlApplicationContext;
public class CTarget {
...
public static void main(String... args) {
GenericXmlApplicationContext
ctx = new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-04.xml");
ctx.refresh();
System.out.println("\nUsing byType:\n");
CTarget t = (CTarget) ctx.getBean("targetByType");
ctx.close();
}
运行前面的类会产生以下输出:
Using byType:
Exception in thread "main"
org.springframework.beans.factory.UnsatisfiedDependencyException:
Error creating bean with name 'targetByType' defined in class path
resource spring/app-context-04.xml:
Unsatisfied dependency expressed through bean property 'foo';
nested exception is
org.springframework.beans.factory.NoUniqueBeanDefinitionException:
No qualifying bean of type
'com.apress.prospring5.ch3.xml.complicated.Foo' available:
expected single matching bean but found 2: fooOne,fooTwo
...
控制台输出要大得多,但是前面输出中的第一行以一种非常易读的方式揭示了问题。当 Spring 不知道自动绑定哪个 bean 时,它抛出一个带有明确消息的UnsatisfiedDependencyException。它告诉您找到了哪些 beanss,但是它不能选择在哪里使用哪些 bean。有两种方法可以解决这个问题。第一种方法是使用 bean 定义中的primary属性,您希望 Spring 首先考虑自动绑定,并将true设置为它的值。
<beans ...>
<bean id="fooOne"
class="com.apress.prospring5.ch3.xml.complicated.FooImpl1"
primary="true"/>
<bean id="fooTwo"
class="com.apress.prospring5.ch3.xml.complicated.FooImpl2"/>
<bean id="bar" class="com.apress.prospring5.ch3.xml.Bar"/>
<bean id="targetByType" autowire="byType"
class="com.apress.prospring5.ch3.xml.complicated.CTarget"
lazy-init="true"/>
</beans>
因此,如果如前所述修改了配置,那么在运行该示例时,将会打印以下输出:
Using byType:
Property bar set
Property fooOne set
Property fooTwo set
所以,一切都恢复正常了。但是,只有当只有两个与 bean 相关的类型时,primary属性才是一个解决方案。如果多了,用了也摆脱不了UnsatisfiedDependencyException。第二种方法将完成这项工作,它将让您完全控制哪个 bean 在哪里被注入,这是通过 XML 命名您的 bean 并配置它们在哪里被注入。前面的例子是一个非常复杂和肮脏的实现,它只是为了证明如何在 XML 中配置每种自动连接类型。当切换到注释时,事情有些变化。有一个相当于lazy-init属性的注释;@Lazy注释在类级别用于声明 beanss,这些 bean 将在第一次被访问时被实例化。使用原型注释,我们可以为一个 bean 只创建一个配置,因此 bean 的名称并不重要,因为每种类型只有一个 bean,这看起来很合理。因此,通过注释使用配置时,默认的自动布线是byType。当存在与 bean 相关的类型时,能够指定应该通过名称进行自动连接是很有用的。这是通过使用@Qualifier注释和@Autowired注释,并提供被注入的 bean 的名称作为参数来完成的。
考虑以下代码:
package com.apress.prospring5.ch3.sandbox;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.support.GenericXmlApplicationContext;
import org.springframework.stereotype.Component;
@Component
@Lazy
public class TrickyTarget {
Foo fooOne;
Foo fooTwo;
Bar bar;
public TrickyTarget() {
System.out.println("Target.constructor()");
}
public TrickyTarget(Foo fooOne) {
System.out.println("Target(Foo) called");
}
public TrickyTarget(Foo fooOne, Bar bar) {
System.out.println("Target(Foo, Bar) called");
}
@Autowired
public void setFooOne(Foo fooOne) {
this.fooOne = fooOne;
System.out.println("Property fooOne set");
}
@Autowired
public void setFooTwo(Foo foo) {
this.fooTwo = foo;
System.out.println("Property fooTwo set");
}
@Autowired
public void setBar(Bar bar) {
this.bar = bar;
System.out.println("Property bar set");
}
public static void main(String... args) {
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-04.xml");
ctx.refresh();
TrickyTarget t = ctx.getBean(TrickyTarget.class);
ctx.close();
}
}
如果Foo是这里描述的类:
package com.apress.prospring5.ch3.sandbox;
@Component
public class Foo {
}
然后,当运行TrickyTarget类时,会产生以下输出:
Property fooOne set
Property fooTwo set
Property bar set
Bar类也一样简单。
package com.apress.prospring5.ch3.sandbox;
import org.springframework.stereotype.Component;
@Component
public class Bar {
}
如果我们要修改TrickyTarget类并给 bean 命名,如下所示:
@Component("gigi")
@Lazy
public class TrickyTarget {
...
}
然后当运行该类时,将产生相同的输出,因为只有一个类型为Target的 bean,并且当使用ctx.getBean(TrickyTarget.class)从上下文请求时,上下文返回该类型的唯一 bean,而不管其名称。此外,如果我们要为类型为Bar的 bean 提供一个名称:
package com.apress.prospring5.ch3.sandbox;
import org.springframework.stereotype.Component;
@Component("kitchen")
public class Bar {
}
然后,当再次运行该示例时,我们将看到相同的输出。这意味着默认的自动布线类型是byType。
如前所述,当 bean 类型相关时,事情会变得复杂。让我们将Foo转换成一个接口,并声明实现它的两个 bean 类型,每个类型都有自己的 bean 声明。让我们保持默认配置,没有额外的命名。
package com.apress.prospring5.ch3.sandbox;
//Foo.java
public interface Foo {
// empty interface, used as a marker interface
}
//FooImplOne.java
@Component
public class FooImplOne implements Foo {
}
//FooImplTwo.java
@Component
public class FooImplTwo implements Foo{
}
TrickyTarget类保持不变,当它运行时,我们会看到输出可能会变成类似这样的内容:
Property bar set
Exception in thread "main"
org.springframework.beans.factory.UnsatisfiedDependencyException:
Error creating bean with name 'gigi':
Unsatisfied dependency expressed through method 'setFoo' parameter 0;
nested exception is
org.springframework.beans.factory.NoUniqueBeanDefinitionException:
No qualifying bean of type 'com.apress.prospring5.ch3.sandbox.Foo' available:
expected single matching bean but found 2: fooImplOne,
fooImplTwo
...
还有很多输出,但这些是第一行,如你所见,Spring 非常明确。它告诉您它不知道要通过方法setFoo自动连接哪个 bean,它还告诉您它选择了哪个 bean。beans 的名称是由 Spring 根据类名决定的,将类名的第一个字母小写。利用这些信息,TrickyTarget可以被修复。有两种方法可以做到这一点。第一种方法是在定义 bean 的类上使用@Primary注释(相当于前面介绍的primary属性),这将告诉 Spring 在按类型自动连接时优先考虑这个 bean。我们将注释FooImplOne。
package com.apress.prospring5.ch3.sandbox;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
@Component
@Primary
public class FooImplOne implements Foo {
}
@Primary标注是一个标记接口;它没有属性。当需要使用byType自动连接这种类型的 bean 时,它在 bean 配置中的存在将该 bean 标记为具有优先级。如果您运行TrickyTarget类,预期的输出将被再次打印。
Property fooOne set
Property fooTwo set
Property bar set
与primary属性的情况一样,@Primary注释只有在恰好有两个相关的 bean 类型时才有用。对于处理更多相关的 bean 类型,Qualifier注释更合适。这个放在不明确的设定器上的@Autowired旁边:setFooOne()和setFooTwo()。(保持不变的代码不再显示。)
@Component("gigi")
@Lazy
public class TrickyTarget {
...
@Autowired
@Qualifier("fooImplOne")
public void setFoo(Foo foo) {
this.foo = foo;
System.out.println("Property fooOne set");
}
@Autowired
@Qualifier("fooImplTwo")
public void setFooTwo(Foo fooTwo) {
this.fooTwo = fooTwo;
System.out.println("Property fooTeo set");
}
...
}
现在,如果运行该示例,将再次打印预期的输出。
Property fooOne set
Property fooTwo set
Property bar set
当使用 Java 配置时,唯一改变的是 beans 的定义方式。因为@Bean注释将用于配置类中的 bean 声明方法,而不是 bean 类上的@Component。此处显示了这样一个示例:
package com.apress.prospring5.ch3.config;
import com.apress.prospring5.ch3.sandbox.*;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.GenericApplicationContext;
public class TargetDemo {
@Configuration
static class TargetConfig {
@Bean
public Foo fooImplOne() {
return new FooImplOne();
}
@Bean
public Foo fooImplTwo() {
return new FooImplTwo();
}
@Bean
public Bar bar() {
return new Bar();
}
@Bean
public TrickyTarget trickyTarget() {
return new TrickyTarget();
}
}
public static void main(String args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(TargetConfig.class);
TrickyTarget t = ctx.getBean(TrickyTarget.class);
ctx.close();
}
}
这里还重用了来自com.apress.prospring5.ch3.sandbox包的现有类,以避免代码重复,因为组件扫描没有启用,任何使用原型注释的 bean 声明都将被忽略。如果您运行前面的类,您会注意到输出与前面的示例相同。如果您还记得,如前所述,使用带有@Bean的 bean 声明时的惯例是方法的名称成为 bean 的名称,因此用@Qualifier注释配置的TrickyTarget仍将按预期工作。
何时使用自动布线
在大多数情况下,是否应该使用自动布线这个问题的答案肯定是否定的!在小型应用中,自动连接可以节省您的时间,但在许多情况下,它会导致不好的做法,并且在大型应用中不灵活。使用byName似乎是一个好主意,但是它可能会导致您给自己的类取人工属性名,这样您就可以利用自动连接功能。Spring 背后的整个思想是,你可以随心所欲地创建你的类,让 Spring 为你工作,而不是反过来。您可能很想使用byType,直到您意识到在您的ApplicationContext中每种类型只能有一个 bean——当您需要维护同一类型不同配置的 bean 时,这种限制是有问题的。同样的论点也适用于构造函数自动连接的使用。
在某些情况下,自动连接可以节省您的时间,但是显式定义您的连接并不需要太多额外的工作,而且您可以从显式语义和在属性命名以及管理多少相同类型的实例方面的充分灵活性中受益。对于任何重要的应用,要不惜一切代价避开自动布线。
设置 Bean 继承
在某些情况下,您可能需要相同类型或实现共享接口的 beans 的多个定义。如果您希望这些 beans 共享一些配置设置而不共享其他设置,这可能会有问题。保持共享配置设置同步的过程很容易出错,而且在大型项目中,这样做可能相当耗时。为了解决这个问题,Spring 允许您提供一个从同一个ApplicationContext实例中的另一个 bean 继承其属性设置的<bean>定义。您可以根据需要覆盖子 bean 上的任何属性值,这允许您拥有完全控制权,但是父 bean 可以为您的每个 bean 提供基本配置。下面的代码示例展示了一个简单的配置,它有两个 beans,其中一个是另一个的子级(app-context-xml.xml):
<beans ...>
<bean id="parent" class="com.apress.prospring5.ch3.xml.Singer"
p:name="John Mayer" p:age="39"/>
<bean id="child" class="com.apress.prospring5.ch3.xml.Singer"
parent="parent" p:age="0"/>
</beans>
在这段代码中,您可以看到child bean 的<bean>标签有一个额外的属性parent,这表明 Spring 应该将parent bean 视为 bean 的父项,并继承它的配置。如果不希望从ApplicationContext中查找父 bean 定义,可以在声明父 bean 时在<bean>标记中添加属性abstract="true"。因为child bean 有自己的age属性值,所以 Spring 将这个值传递给 bean。然而,child对于name属性没有值,所以 Spring 使用赋予inheritParent bean 的值。
这个Singer bean 很简单。
package com.apress.prospring5.ch3.xml;
public class Singer {
private String name;
private int age;
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public String toString() {
return "\tName: " + name + "\n\t" + "Age: " + age;
}
}
为了测试它,您可以编写一个简单的类,如下所示:
package com.apress.prospring5.ch3.xml;
import org.springframework.context.support.GenericXmlApplicationContext;
public class InheritanceDemo {
public static void main(String... args) {
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-xml.xml");
ctx.refresh();
Singer parent = (Singer) ctx.getBean("parent");
Singer child = (Singer) ctx.getBean("child");
System.out.println("Parent:\n" + parent);
System.out.println("Child:\n" + child);
}
}
如您所见,Singer类的main()方法从ApplicationContext获取了child和parentbean,并将它们属性的内容写入stdout。以下是该示例的输出:
Parent:
Name: John Mayer
Age: 39
Child:
Name: John Mayer
Age: 0
正如所料,inheritChild bean 从inheritParent bean 继承了其name属性的值,但是能够为age属性提供自己的值。
子 bean 从父 bean 继承构造函数参数和属性值,因此可以在 bean 继承中使用这两种注入方式。这种级别的灵活性使得 bean 继承成为一个强大的工具,可以用许多 bean 定义来构建应用。如果您正在声明许多具有共享属性值的相同值的 beans,请避免使用复制和粘贴来共享值的诱惑;相反,应该在配置中设置继承层次结构。
当您使用继承时,请记住 bean 继承不必与 Java 继承层次结构相匹配。在相同类型的五个 bean 上使用 bean 继承是完全可以接受的。将 bean 继承看作更像是一个模板特性,而不是一个继承特性。但是,请注意,如果您正在更改子 bean 的类型,该类型必须扩展父 bean 类型。
摘要
在这一章中,我们概括地介绍了 Spring Core 和 IoC。我们向您展示了 IoC 类型的示例,并展示了在您的应用中使用每种机制的优缺点。我们研究了 Spring 提供了哪些 IoC 机制,以及何时在应用中使用每种机制。在探索 IoC 时,我们介绍了 Spring BeanFactory,它是 Spring IoC 功能的核心组件,然后介绍了ApplicationContext,它扩展了BeanFactory,并提供了额外的功能。对于ApplicationContext,我们关注的是GenericXmlApplicationContext,它允许使用 XML 对 Spring 进行外部配置。还讨论了声明ApplicationContext的 DI 需求的另一种方法,即使用 Java 注释。还包括了一些关于AnnotationConfigApplicationContext和 Java 配置的例子,只是为了慢慢介绍这种配置 beans 的方式。
本章还向您介绍了 Spring 的 IoC 特性集的基础,包括 setter 注入、构造函数注入、方法注入、自动连接和 bean 继承。在关于配置的讨论中,我们展示了如何使用 XML 和注释类型配置以及GenericXmlApplicationContext用各种各样的值来配置 bean 属性,包括其他 bean。
这一章仅仅触及了 Spring 和 Spring 的 IoC 容器的表面。在下一章中,您将看到一些特定于 Spring 的与 IoC 相关的特性,并且您将更详细地了解 Spring Core 中的其他可用功能。
Footnotes 1
例如,试试 http://forum.spring.io 的 Spring 社区论坛。
2
这些注释被称为原型,因为它们是名为org.springframework.stereotype的包的一部分。这个包将所有用于定义 beans 的注释组合在一起。这些注释也与 bean 的角色相关。例如,@Service用于定义一个服务 bean,它是一个更复杂的功能 bean,提供其他 bean 可能需要的服务,@Repository用于定义一个 bean,用于从/向数据库检索/保存数据,等等。
3
cglib是一个强大、高性能、高质量的代码生成库。它可以在运行时扩展 Java 类和实现接口。它是开源的,你可以在 https://github.com/cglib 找到官方的资源库。
4
四、Spring 详细配置和 Spring Boot
在前一章中,我们详细介绍了控制反转(IoC)的概念以及它是如何融入 Spring 框架的。然而,我们实际上只是触及了 Spring Core 的皮毛。Spring 提供了大量的服务来补充和扩展其基本的 IoC 功能。在这一章中,你将详细探讨这些。具体来说,您将看到以下内容:
- 管理 bean 生命周期:到目前为止,您看到的所有 bean 都相当简单,并且与 Spring 容器完全解耦。在这一章中,我们将介绍一些策略,您可以使用这些策略来使您的 beans 在其生命周期的不同阶段接收来自 Spring 容器的通知。你可以通过实现 Spring 设计的特定接口,指定 Spring 可以通过反射调用的方法,或者使用 JSR-250 JavaBeans 生命周期注释来实现。
- 让您的 bean“感知 Spring”:在某些情况下,您希望 bean 能够与配置它的
ApplicationContext实例进行交互。出于这个原因,Spring 提供了两个接口,BeanNameAware和ApplicationContextAware(在第三章的最后介绍),分别允许 bean 获得它的指定名称和引用它的ApplicationContext。本章的这一节介绍了这些接口的实现,并给出了在应用中使用它们的一些实际注意事项。 - 使用 FactoryBeans:顾名思义,
FactoryBean接口意味着可以由充当其他 bean 工厂的任何 bean 来实现。FactoryBean接口提供了一种机制,通过这种机制,您可以轻松地将自己的工厂与 SpringBeanFactory接口集成在一起。 - 使用 JavaBean s property editors:
PropertyEditor接口是在java.beans包中提供的标准接口。PropertyEditor用于在属性值和String表示之间进行转换。Spring 广泛使用PropertyEditors,主要是读取BeanFactory配置中指定的值,并将它们转换成正确的类型。在本章中,我们将讨论 Spring 提供的一组PropertyEditor以及如何在应用中使用它们。我们还看一下如何实现定制的PropertyEditor。 - 了解关于 Spring ApplicationContext 的更多信息:正如我们所知,
ApplicationContext是BeanFactory的扩展,旨在用于完整的应用。ApplicationContext接口提供了一组有用的附加功能,包括国际化消息支持、资源加载和事件发布。在这一章中,我们将详细介绍除 IoC 之外ApplicationContext提供的特性。我们还提前一点向您展示了ApplicationContext如何在您构建 web 应用时简化 Spring 的使用。 - 使用 Java 类进行配置:在 3.0 之前,Spring 只支持带有 beans 注释的 XML 基本配置和依赖配置。从 3.0 开始,Spring 为开发人员提供了另一种选择,使用 Java 类配置 Spring
ApplicationContext接口。我们来看看 Spring 应用配置中的这个新选项。 - 使用 Spring Boot:通过使用 Spring Boot,Spring 应用配置变得更加实用。这个 Spring 项目使得创建独立的、生产级的、基于 Spring 的应用变得容易,您可以“直接运行”
- 使用配置增强:我们展示了使应用配置更容易的特性,比如概要文件管理、环境和属性源抽象等等。在本节中,我们将介绍这些特性,并展示如何使用它们来满足特定的配置需求。
- 使用 Groovy 进行配置:Spring 4.0 的新增功能是用 Groovy 语言配置 bean 定义,这可以作为现有 XML 和 Java 配置方法的替代或补充。
Spring 对应用可移植性的影响
本章中讨论的大多数特性都是 Spring 特有的,在许多情况下,其他 IoC 容器中没有这些特性。尽管许多 IoC 容器提供了生命周期管理功能,但它们可能是通过一组不同于 Spring 的接口来实现的。如果应用在不同 IoC 容器之间的可移植性非常重要,那么您可能希望避免使用一些将应用耦合到 Spring 的特性。
但是,请记住,通过设置约束——意味着您的应用可以在 IoC 容器之间移植——您就失去了 Spring 提供的丰富功能。因为您可能会做出使用 Spring 的战略选择,所以尽最大能力使用它是有意义的。
注意不要凭空制造出可移植性的需求。在许多情况下,应用的最终用户并不关心应用是否可以在三个不同的 IoC 容器上运行;他们只是想让它跑起来。根据我们的经验,试图在您选择的技术中可用的最小公分母特性上构建应用通常是一个错误。这样做通常会使您的应用从一开始就处于不利地位。但是,如果您的应用需要 IoC 容器的可移植性,不要把这看作是一个缺点——这是一个真实的需求,因此,您的应用应该满足这个需求。在《专家一对一:没有 EJB 的 J2EE 开发》( Wrox,2004)中,Rod Johnson 和 jürgen h ller 将这些类型的需求描述为幻影需求,并对它们以及它们如何影响您的项目进行了更详细的讨论。
尽管使用这些特性可能会将您的应用耦合到 Spring 框架,但实际上您是在更大范围内增加应用的可移植性。假设您使用的是一个免费的开源框架,没有特定的供应商关系。使用 Spring 的 IoC 容器构建的应用可以在 Java 运行的任何地方运行。对于 Java 企业应用,Spring 为可移植性开辟了新的可能性。Spring 提供了许多与 JEE 相同的功能,还提供了抽象和简化 JEE 其他方面的类。在许多情况下,可以使用 Spring 构建一个 web 应用,该应用运行在一个简单的 servlet 容器中,但是具有与面向成熟的 JEE 应用服务器的应用相同的复杂程度。通过耦合到 Spring,您可以用 Spring 中的等效特性替换许多特定于供应商或依赖于特定于供应商的配置的特性,从而提高应用的可移植性。
Bean 生命周期管理
任何 IoC 容器(包括 Spring)的一个重要部分是,beans 可以以这样一种方式构造,即它们可以在生命周期的某些点接收通知。这使您的 beans 能够在其生命周期的某些点上执行相关的处理。通常,有两个生命周期事件与 bean 特别相关:初始化后和销毁前。
在 Spring 的上下文中,一旦 Spring 完成了对 bean 的所有属性值的设置,并完成了您配置它执行的任何依赖检查,就会引发后初始化事件。销毁前事件在 Spring 销毁 bean 实例之前触发。然而,对于具有原型范围的 beans,预销毁事件不会被 Spring 触发。Spring 的设计是,初始化生命周期回调方法将在对象上被调用,而不管 bean 的作用域,而对于具有原型作用域的 bean,销毁生命周期回调方法将不会被调用。Spring 提供了三种机制,bean 可以使用这三种机制来挂钩这些事件并执行一些额外的处理:基于接口的、基于方法的和基于注释的机制。
使用基于接口的机制,您的 bean 实现了一个特定于它想要接收的通知类型的接口,Spring 通过接口中定义的回调方法通知 bean。对于基于方法的机制,Spring 允许您在ApplicationContext配置中指定 bean 初始化时要调用的方法的名称,以及 bean 销毁时要调用的方法的名称。对于注释机制,您可以使用 JSR-250 注释来指定 Spring 在构造之后或销毁之前应该调用的方法。
在这两种情况下,机制实现了完全相同的目标。接口机制在 Spring 中被广泛使用,因此每次使用 Spring 的一个组件时,您不必记得指定初始化或销毁。但是,在您自己的 bean 中,使用基于方法的机制或注释机制可能会更好,因为您的 bean 不需要实现任何特定于 Spring 的接口。尽管我们说过,可移植性并不像许多书让你相信的那样重要,但这并不意味着当存在一个非常好的替代方案时,你应该牺牲可移植性。也就是说,如果您以其他方式将您的应用耦合到 Spring,使用接口方法允许您指定一次回调,然后忘记它。如果您正在定义许多需要利用生命周期通知的相同类型的 bean,那么使用接口机制可以避免为 XML 配置文件中的每个 bean 指定生命周期回调方法的需要。使用 JSR-250 注释也是另一个可行的选择,因为它是由 JCP 定义的标准,并且您也没有耦合到 Spring 的特定注释。只需确保运行应用的 IoC 容器支持 JSR-250 标准。
总的来说,选择哪种机制来接收生命周期通知取决于您的应用需求。如果您关心可移植性,或者您只是定义一个或两个需要回调的特定类型的 beans,请使用基于方法的机制。如果您使用注释类型的配置,并且确定您使用的是支持 JSR-250 的 IoC 容器,请使用注释机制。如果您不太关心可移植性,或者您正在定义许多需要生命周期通知的相同类型的 bean,那么使用基于接口的机制是确保您的 bean 总是收到它们所期望的通知的最佳方式。如果您计划在许多不同的 Spring 项目中使用一个 bean,您几乎肯定希望该 bean 的功能尽可能独立,因此您肯定应该使用基于接口的机制。
图 4-1 显示了 Spring 如何管理其容器中 beans 的生命周期的高级概述。

图 4-1。
Spring beans life cycle
挂钩到 Bean 创建
通过知道何时被初始化,bean 可以检查它是否满足了所有需要的依赖关系。尽管 Spring 可以为您检查依赖关系,但这几乎是一种要么全有要么全无的方法,它没有提供任何机会将额外的逻辑应用于依赖关系解析过程。考虑一个 bean,它有四个被声明为 setters 的依赖项,其中两个是必需的,另一个在没有提供依赖项的情况下有合适的默认值。使用初始化回调,您的 bean 可以检查它需要的依赖项,根据需要抛出异常或提供默认值。
bean 不能在其构造函数中执行这些检查,因为此时,Spring 还没有机会为它可以满足的依赖项提供值。Spring 中的初始化回调是在 Spring 完成提供它所能提供的依赖项并执行您要求的任何依赖项检查之后调用的。
您不仅限于使用初始化回调来检查依赖关系;您可以在回调中做任何您想做的事情,但是它对于我们描述的目的是最有用的。在许多情况下,初始化回调也是触发 bean 响应其配置时必须自动采取的任何操作的地方。例如,如果您构建一个 bean 来运行调度任务,初始化回调提供了启动调度程序的理想位置——毕竟,配置数据是在 bean 上设置的。
您将不必编写 bean 来运行调度任务,因为这是 Spring 可以通过其内置的调度特性或通过与 Quartz 调度程序的集成自动完成的。我们将在第十一章中对此进行更详细的介绍。
创建 Bean 时执行方法
正如我们前面提到的,接收初始化回调的一种方法是在 bean 上指定一个方法作为初始化方法,并告诉 Spring 使用这个方法作为初始化方法。如前所述,当您只有几个相同类型的 beans 时,或者当您想让您的应用与 Spring 分离时,这种回调机制非常有用。使用这种机制的另一个原因是使您的 Spring 应用能够与以前构建的或由第三方供应商提供的 beans 一起工作。指定回调方法就是在 bean 的<bean>标签的init-method属性中指定名称。以下代码示例显示了一个具有两个依赖项的基本 bean:
package com.apress.prospring5.ch4;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;
public class Singer {
private static final String DEFAULT_NAME = "Eric Clapton";
private String name;
private int age = Integer.MIN_VALUE;
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public void init() {
System.out.println("Initializing bean");
if (name == null) {
System.out.println("Using default name");
name = DEFAULT_NAME;
}
if (age == Integer.MIN_VALUE) {
throw new IllegalArgumentException(
"You must set the age property of any beans of type " + Singer.class);
}
}
public String toString() {
return "\tName: " + name + "\n\tAge: " + age;
}
public static void main(String... args) {
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-xml.xml");
ctx.refresh();
getBean("singerOne", ctx);
getBean("singerTwo", ctx);
getBean("singerThree", ctx);
ctx.close();
}
public static Singer getBean(String beanName,
ApplicationContext ctx) {
try {
Singer bean = (Singer) ctx.getBean(beanName);
System.out.println(bean);
return bean;
} catch (BeanCreationException ex) {
System.out.println("An error occured in bean configuration: "
+ ex.getMessage());
return null;
}
}
}
注意,我们已经定义了一个方法init(),作为初始化回调。init()方法检查 name 属性是否已经设置,如果没有,它使用保存在DEFAULT_NAME常量中的默认值。The init()方法还检查是否设置了age属性,如果没有,抛出IllegalArgumentException。
SimpleBean类的main()方法试图使用自己的getBean()方法从GenericXmlApplicationContext获得三个Singer类型的 beans。注意,在getBean()方法中,如果成功获得了 bean,其详细信息将被写入控制台输出。如果在init()方法中抛出异常(如果没有设置age属性,就会出现这种情况),那么 Spring 会在BeanCreationException. The getBean()方法中包装该异常,捕捉这些异常,并向控制台输出写入一条消息,通知我们该错误,并返回一个null值。
下面的配置片段显示了一个ApplicationContext配置,它定义了在前面的代码片段(app-context-xml.xml)中使用的 beans:
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd"
default-lazy-init="true">
<bean id="singerOne"
class="com.apress.prospring5.ch4.Singer"
init-method="init" p:name="John Mayer" p:age="39"/>
<bean id="singerTwo"
class="com.apress.prospring5.ch4.Singer"
init-method="init" p:age="72"/>
<bean id="singerThree"
class="com.apress.prospring5.ch4.Singer"
init-method="init" p:name="John Butler"/>
</beans>
正如您所看到的,三个 bean 中每一个的<bean>标签都有一个init-method属性,它告诉 Spring 一旦完成 bean 的配置就应该调用init()方法。singerOne bean 的name和age属性都有值,所以它通过init()方法时完全没有变化。singerTwo bean 的name属性没有值,这意味着在init()方法中,name属性被赋予默认值。最后,singerThree bean 没有age属性的值。在init()方法中定义的逻辑将此视为错误,因此抛出IllegalArgumentException。还要注意,在<beans>标签中,我们添加了属性default-lazy-init="true"来指示 Spring 仅在应用请求 bean 时实例化配置文件中定义的 bean。如果我们不指定它,Spring 会在ApplicationContext的引导过程中尝试初始化所有的 beans,在singerThree的初始化过程中会失败。
当配置文件中的所有 beans 都具有相同的init-method配置时,可以通过在<beans>元素上设置default-init-method属性来简化文件。豆子可以是不同的类型;它们的唯一条件是拥有一个名为default-init-method属性值的方法。因此,前面的配置也可以写成这样:
<beans ...
default-lazy-init="true" default-init-method="init">
<bean id="singerOne"
class="com.apress.prospring5.ch4.Singer"
p:name="John Mayer" p:age="39"/>
<bean id="singerTwo"
class="com.apress.prospring5.ch4.Singer"
p:age="72"/>
<bean id="singerThree"
class="com.apress.prospring5.ch4.Singer"
p:name="John Butler"/>
</beans>
运行前面的示例会产生以下输出:
Initializing bean
Name: John Mayer
Age: 39
Initializing bean
Using default name
Name: Eric Clapton
Age: 72
Initializing bean
An error occured in bean configuration: Error creating bean
with name 'singerThree' defined in class path
resource spring/app-context-xml.xml: Invocation of init method failed;
nested exception is java.lang.IllegalArgumentException:
You must set the age property of any beans of type class
com.apress.prospring5.ch4.Singer
从这个输出中,您可以看到singerOne已经用我们在配置文件中指定的值进行了正确配置。对于singerTwo,使用了name属性的默认值,因为在配置中没有指定任何值。最后,对于singerThree,没有创建 bean 实例,因为init()方法由于缺少age属性的值而引发了一个错误。
如您所见,使用初始化方法是确保正确配置 beans 的理想方法。通过使用这种机制,您可以充分利用 IoC 的优势,而不会失去手动定义依赖关系所获得的任何控制权。对初始化方法的唯一约束是它不能接受任何参数。您可以定义任何返回类型,尽管 Spring 会忽略它,您甚至可以使用静态方法,但是该方法必须不接受任何参数。
当使用静态初始化方法时,这种机制的好处被否定了,因为您不能访问 bean 的任何状态来验证它。如果您的 bean 使用静态状态作为节省内存的机制,并且您使用静态初始化方法来验证该状态,那么您应该考虑将静态状态转移到实例状态,并使用非静态初始化方法。如果您使用 Spring 的单例管理功能,最终效果是相同的,但是您有了一个测试更简单的 bean,并且您还可以在必要时创建具有自己状态的 bean 的多个实例。当然,在某些情况下,您需要使用跨 bean 的多个实例共享的静态状态,在这种情况下,您总是可以使用静态初始化方法。
实现 InitializingBean 接口
Spring 中定义的InitializingBean接口允许您在 bean 代码中定义希望 bean 接收 Spring 已完成配置的通知。与使用初始化方法时一样,这使您有机会检查 bean 配置以确保它是有效的,同时提供任何默认值。InitializingBean接口定义了一个方法afterPropertiesSet(),它与上一节中介绍的init()方法的作用相同。下面的代码片段显示了使用InitializingBean接口代替初始化方法重新实现前面的示例:
package com.apress.prospring5.ch4;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;
public class SingerWithInterface implements InitializingBean {
private static final String DEFAULT_NAME = "Eric Clapton";
private String name;
private int age = Integer.MIN_VALUE;
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public void afterPropertiesSet() throws Exception {
System.out.println("Initializing bean");
if (name == null) {
System.out.println("Using default name");
name = DEFAULT_NAME;
}
if (age == Integer.MIN_VALUE) {
throw new IllegalArgumentException(
"You must set the age property of any beans of type "
+ SingerWithInterface.class);
}
}
public String toString() {
return "\tName: " + name + "\n\tAge: " + age;
}
public static void main(String... args) {
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-xml.xml");
ctx.refresh();
getBean("singerOne", ctx);
getBean("singerTwo", ctx);
getBean("singerThree", ctx);
ctx.close();
}
private static SingerWithInterface getBean(String beanName,
ApplicationContext ctx) {
try {
SingerWithInterface bean =
(SingerWithInterface) ctx.getBean(beanName);
System.out.println(bean);
return bean;
} catch (BeanCreationException ex) {
System.out.println("An error occured in bean configuration: "
+ ex.getMessage());
return null;
}
}
}
如您所见,本例中没有太多变化。除了明显的类名变化,唯一的区别是这个类实现了InitializingBean并且初始化逻辑已经移到了afterPropertiesSet()方法中。在下面的代码片段中,您可以看到这个示例(app-context- xml.xml)的配置:
<beans ... default-lazy-init="true">
<bean id="singerOne"
class="com.apress.prospring5.ch4.SingerWithInterface"
p:name="John Mayer" p:age="39"/>
<bean id="singerTwo"
class="com.apress.prospring5.ch4.SingerWithInterface"
p:age="72"/>
<bean id="singerThree"
class="com.apress.prospring5.ch4.SingerWithInterface"
p:name="John Butler"/>
</beans>
同样,这里介绍的配置代码和上一节中的配置代码没有太大区别。值得注意的区别是省略了init-method属性。因为SimpleBeanWithInterface类实现了InitializingBean接口,Spring 知道调用哪个方法作为初始化回调,因此不需要任何额外的配置。此示例的输出如下所示:
Initializing bean
Name: John Mayer
Age: 39
Initializing bean
Using default name
Name: Eric Clapton
Age: 72
Initializing bean
An error occured in bean configuration: Error creating bean with name 'singerThree'
defined in class path resource spring/app-context-xml.xml: Invocation of
init method failed; nested exception is java.lang.IllegalArgumentException:
You must set the age property of any beans of type class
com.apress.prospring5.ch4.SingerWithInterface
使用 JSR-250 @PostConstruct 注释
另一个可以达到同样目的的方法是使用 JSR-250 生命周期注释。从 Spring 2.5 开始,还支持 JSR-250 注释来指定 Spring 应该调用的方法,如果类中存在与 bean 的生命周期相关的相应注释的话。以下代码示例显示了应用了@PostConstruct注释的程序:
package com.apress.prospring5.ch4;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;
public class SingerWithJSR250 {
private static final String DEFAULT_NAME = "Eric Clapton";
private String name;
private int age = Integer.MIN_VALUE;
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
@PostConstruct
public void init() throws Exception {
System.out.println("Initializing bean");
if (name == null) {
System.out.println("Using default name");
name = DEFAULT_NAME;
}
if (age == Integer.MIN_VALUE) {
throw new IllegalArgumentException(
"You must set the age property of any beans of type " +
SingerWithJSR250.class);
}
}
public String toString() {
return "\tName: " + name + "\n\tAge: " + age;
}
public static void main(String... args) {
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-annotation.xml");
ctx.refresh();
getBean("singerOne", ctx);
getBean("singerTwo", ctx);
getBean("singerThree", ctx);
ctx.close();
}
public static SingerWithJSR250 getBean(String beanName,
ApplicationContext ctx) {
try {
SingerWithJSR250 bean =
(SingerWithJSR250) ctx.getBean(beanName);
System.out.println(bean);
return bean;
} catch (BeanCreationException ex) {
System.out.println("An error occured in bean configuration: "
+ ex.getMessage());
return null;
}
}
}
该程序与使用init-method方法相同;只需在init()方法之前应用@PostConstruct注释。请注意,您可以为该方法指定任何名称。在配置方面,由于我们使用了注释,我们需要将上下文名称空间中的<context:annotation-driven>标记添加到配置文件中。
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd"
default-lazy-init="true">
<context:annotation-config/>
<bean id="singerOne"
class="com.apress.prospring5.ch4.SingerWithJSR250"
p:name="John Mayer" p:age="39"/>
<bean id="singerTwo"
class="com.apress.prospring5.ch4.SingerWithJSR250"
p:age="72"/>
<bean id="singerThree"
class="com.apress.prospring5.ch4.SingerWithJSR250"
p:name="John Butler"/>
</beans>
运行该程序,您将看到与其他机制相同的输出。
Initializing bean
Name: John Mayer
Age: 39
Initializing bean
Using default name
Name: Eric Clapton
Age: 72
Initializing bean
An error occurred in bean configuration: Error creating bean with name 'singerThree':
Invocation of init method failed; nested exception is
java.lang.IllegalArgumentException: You must set the age property of any beans
of type class com.apress.prospring5.ch4.SingerWithJSR250
这三种方法各有利弊。使用初始化方法的好处是保持应用与 Spring 的解耦,但是您必须记住为每个需要它的 bean 配置初始化方法。使用InitializingBean接口,您可以为 bean 类的所有实例指定一次初始化回调,但是您必须耦合您的应用才能做到这一点。使用注释,您需要将注释应用到方法中,并确保 IoC 容器支持 JSR-250。最后,您应该让应用的需求来决定使用哪种方法。如果可移植性是一个问题,使用初始化或注释方法;否则,使用InitializingBean接口来减少您的应用所需的配置数量,以及由于错误配置而导致的错误蔓延到您的应用的可能性。
用init-method或@PostConstruct配置初始化时,用不同的访问权限声明初始化方法是有好处的。Spring IoC 应该只在 bean 创建时调用一次初始化方法。后续调用将导致意外结果甚至失败。通过初始化方法private可以禁止外部附加调用。Spring IoC 将能够通过反射调用它,但是不允许在代码中进行任何额外的调用。
使用@Bean 声明初始化方法
声明 bean 初始化方法的另一种方式是为@Bean注释指定initMethod属性,并将初始化方法名设置为其值。该注释用于在 Java 配置类中声明 beans。尽管 Java 配置将在本章的稍后部分讨论,但是 bean 初始化部分属于这里。对于这个例子,使用初始的Singer类,因为配置是外部的,就像使用init-method属性一样。我们将只写一个配置类和一个新的main()方法来测试它。另外,default-lazy-init="true"将被每个 bean 声明上的@Lazy注释所取代。
package com.apress.prospring5.ch4.config;
import com.apress.prospring5.ch4.Singer;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.support.GenericApplicationContext;
import static com.apress.prospring5.ch4.Singer.getBean;
public class SingerConfigDemo {
@Configuration
static class SingerConfig{
@Lazy
@Bean(initMethod = "init")
Singer singerOne() {
Singer singerOne = new Singer();
singerOne.setName("John Mayer");
singerOne.setAge(39);
return singerOne;
}
@Lazy
@Bean(initMethod = "init")
Singer singerTwo() {
Singer singerTwo = new Singer();
singerTwo.setAge(72);
return singerTwo;
}
@Lazy
@Bean(initMethod = "init")
Singer singerThree() {
Singer singerThree = new Singer();
singerThree.setName("John Butler");
return singerThree;
}
}
public static void main(String args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(SingerConfig.class);
getBean("singerOne", ctx);
getBean("singerTwo", ctx);
getBean("singerThree", ctx);
ctx.close();
}
}
运行这段代码将会产生到目前为止观察到的相同结果,如下所示:
Initializing bean
Name: John Mayer
Age: 39
Initializing bean
Using default name
Name: Eric Clapton
Age: 72
Initializing bean
An error occurred in bean configuration: Error creating bean with name 'singerThree'
defined in com.apress.prospring5.ch4.config.SingerConfigDemo$SingerConfig:
Invocation of init method failed; nested exception is
java.lang.IllegalArgumentException: You must set the age property of any beans
of type class com.apress.prospring5.ch4.Singer
了解解决方案的顺序
所有初始化机制都可以在同一个 bean 实例上使用。在这种情况下,Spring 首先调用用@PostConstruct注释的方法,然后调用afterPropertiesSet(),接着调用配置文件中指定的初始化方法。这种顺序是有技术原因的,通过遵循图 4-1 中的路径,我们可以注意到 bean 创建过程中的以下步骤:
- 首先调用构造函数来创建 bean。
- 依赖项被注入(调用 setters)。
- 既然 bean 已经存在并且提供了依赖关系,那么就要咨询预初始化的
BeanPostProcessor基础设施 bean,看它们是否想从这个 bean 中调用任何东西。这些是特定于 Spring 的基础设施 bean,它们在创建后执行 bean 修改。@PostConstruct注释是由CommonAnnotationBeanPostProcessor注册的,所以这个 bean 将调用用@PostConstruct注释的方法。这个方法在 bean 构造完成之后,类投入使用之前执行,在 bean 实际初始化之前(在afterPropertiesSet和init-method之前)执行 1 。 InitializingBean的afterPropertiesSet在依赖项注入后立即执行。在设置了所有提供的 bean 属性并满足了BeanFactoryAware和ApplicationContextAware之后,afterPropertiesSet()方法被BeanFactory调用。- 最后执行
init-method属性,因为这是 bean 的实际初始化方法。
如果您有一个以特定方法执行一些初始化的现有 bean,但是您需要在使用 Spring 时添加更多的初始化代码,那么理解不同类型的 bean 初始化的顺序会很有用。
钩住豆破坏
当使用一个包装了DefaultListableBeanFactory接口的ApplicationContext实现时(比如通过getDefaultListableBeanFactory()方法的GenericXmlApplicationContext,你可以通过调用ConfigurableBeanFactory.destroySingletons()向BeanFactory发出信号,告诉他你想要销毁所有的单例实例。通常,您在应用关闭时这样做,它允许您清理 beans 可能保持打开的任何资源,从而允许您的应用正常关闭。这个回调还提供了一个完美的地方,可以将内存中存储的任何数据刷新到持久存储中,并允许 beans 结束它们可能已经启动的任何长时间运行的进程。
为了让您的 bean 接收到已经调用了destroySingletons()的通知,您有三种选择,都类似于接收初始化回调的可用机制。销毁回调通常与初始化回调一起使用。在许多情况下,在初始化回调中创建和配置资源,然后在销毁回调中释放资源。
销毁 Bean 时执行方法
要指定在 bean 被销毁时调用的方法,只需在 bean 的<bean>标记的destroy-method属性中指定方法的名称。Spring 在销毁 bean 的单例实例之前调用它(Spring 不会为那些具有 prototype 作用域的 bean 调用这个方法)。下面的代码片段提供了一个使用destroy-method回调的例子:
package com.apress.prospring5.ch4;
import java.io.File;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.support.GenericXmlApplicationContext;
public class DestructiveBean implements InitializingBean {
private File file;
private String filePath;
public void afterPropertiesSet() throws Exception {
System.out.println("Initializing Bean");
if (filePath == null) {
throw new IllegalArgumentException(
"You must specify the filePath property of"
+ DestructiveBean.class);
}
this.file = new File(filePath);
this.file.createNewFile();
System.out.println("File exists: " + file.exists());
}
public void destroy() {
System.out.println("Destroying Bean");
if(!file.delete()) {
System.err.println("ERROR: failed to delete file.");
}
System.out.println("File exists: " + file.exists());
}
public void setFilePath(String filePath) {
this.filePath = filePath;
}
public static void main(String... args) throws Exception {
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-xml.xml");
ctx.refresh();
DestructiveBean bean = (DestructiveBean) ctx.getBean("destructiveBean");
System.out.println("Calling destroy()");
ctx.destroy();
System.out.println("Called destroy()");
}
}
这段代码定义了一个destroy()方法,在这个方法中,创建的文件被删除。main()方法从GenericXmlApplicationContext中检索一个DestructiveBean类型的 bean,然后调用它的destroy()方法(它将依次调用被ApplicationContext包装的ConfigurableBeanFactory.destroySingletons(),指示 Spring 销毁它管理的所有单例。初始化和销毁回调都向控制台输出写入一条消息,通知我们它们已被调用。在下面的代码片段中,您可以看到destructiveBean bean ( app-context-xml.xml)的配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="destructiveBean"
class="com.apress.prospring5.ch4.DestructiveBean"
destroy-method="destroy"
p:filePath=
"#{systemProperties'java.io.tmpdir'}#{systemProperties'file.separator'}test.txt"/>
</beans>
注意,我们已经通过使用destroy-method属性将destroy()方法指定为销毁回调。filePath 属性值是通过使用 SpEL 表达式构建的,在文件名test.txt前连接系统属性java.io.tmpdir和file.separator,以确保跨平台兼容性。运行此示例会产生以下输出:
Initializing Bean
File exists: true
Calling destroy()
Destroying Bean
File exists: false
Called destroy()
可以看到,Spring 首先调用初始化回调,DestructiveBean实例创建File实例并存储。接下来,在调用destroy()的过程中,Spring 遍历它所管理的单例集,在本例中只有一个,并调用任何指定的销毁回调。在这里,DestructiveBean实例删除创建的文件,并将消息记录到屏幕上,表明它不再存在。
实现 DisposableBean 接口
与初始化回调一样,Spring 提供了一个接口,在本例中为DisposableBean,它可以由 beans 作为接收销毁回调的机制来实现。DisposableBean接口定义了一个方法destroy(),这个方法在 bean 被销毁之前被调用。使用这种机制与使用InitializingBean接口接收初始化回调是正交的。下面的代码片段显示了实现DisposableBean接口的DestructiveBean类的修改实现:
package com.apress.prospring5.ch4;
import java.io.File;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.support.GenericXmlApplicationContext;
public class DestructiveBeanWithInterface implements InitializingBean, DisposableBean {
private File file;
private String filePath;
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("Initializing Bean");
if (filePath == null) {
throw new IllegalArgumentException(
"You must specify the filePath property of " +
DestructiveBeanWithInterface.class);
}
this.file = new File(filePath);
this.file.createNewFile();
System.out.println("File exists: " + file.exists());
}
@Override
public void destroy() {
System.out.println("Destroying Bean");
if(!file.delete()) {
System.err.println("ERROR: failed to delete file.");
}
System.out.println("File exists: " + file.exists());
}
public void setFilePath(String filePath) {
this.filePath = filePath;
}
public static void main(String... args) throws Exception {
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-xml.xml");
ctx.refresh();
DestructiveBeanWithInterface bean =
(DestructiveBeanWithInterface) ctx.getBean("destructiveBean");
System.out.println("Calling destroy()");
ctx.destroy();
System.out.println("Called destroy()");
}
}
使用回调方法机制的代码和使用回调接口机制的代码没有太大区别。在这种情况下,我们甚至使用了相同的方法名。此示例的配置如下所示(app-context-xml.xml):
<beans ...>
<bean id="destructiveBean"
class="com.apress.prospring5.ch4.DestructiveBeanWithInterface"
p:filePath=
"#{systemProperties'java.io.tmpdir'}#{systemProperties'file.separator'}test.txt"/>
</beans>
除了不同的类名,唯一的区别是省略了destroy-method属性。运行此示例会产生以下输出:
Initializing Bean
File exists: true
Calling destroy()
Destroying Bean
File exists: false
Called destroy()
使用 JSR-250 @PreDestroy 注释
定义在销毁 bean 之前调用的方法的第三种方式是使用 JSR-250 生命周期@PreDestroy注释,这是@PostConstruct注释的逆。下面的代码片段是DestructiveBean的一个版本,它在同一个类中同时使用了@PostConstruct和@PreDestroy来执行程序初始化和销毁操作:
package com.apress.prospring5.ch4;
import java.io.File;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import org.springframework.context.support.GenericXmlApplicationContext;
public class DestructiveBeanWithJSR250 {
private File file;
private String filePath;
@PostConstruct
public void afterPropertiesSet() throws Exception {
System.out.println("Initializing Bean");
if (filePath == null) {
throw new IllegalArgumentException(
"You must specify the filePath property of " +
DestructiveBeanWithJSR250.class);
}
this.file = new File(filePath);
this.file.createNewFile();
System.out.println("File exists: " + file.exists());
}
@
PreDestroy
public void destroy() {
System.out.println("Destroying Bean");
if(!file.delete()) {
System.err.println("ERROR: failed to delete file.");
}
System.out.println("File exists: " + file.exists());
}
public void setFilePath(String filePath) {
this.filePath = filePath;
}
public static void main(String... args) throws Exception {
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-annotation.xml");
ctx.refresh();
DestructiveBeanWithJSR250 bean =
(DestructiveBeanWithJSR250) ctx.getBean("destructiveBean");
System.out.println("Calling destroy()");
ctx.destroy();
System.out.println("Called destroy()");
}
}
在下面的代码片段中,您可以看到这个 bean 的配置文件,它使用了<context:annotation-config>标记(app-context-annotation.xml)。
<beans ...>
<context:annotation-config/>
<bean id="destructiveBean"
class="com.apress.prospring5.ch4.DestructiveBeanWithJSR250"
p:filePath=
"#{systemProperties'java.io.tmpdir'}#{systemProperties'file.separator'}test.txt"/>
</beans>
使用@Bean 声明销毁方法
为 bean 声明 destroy 方法的另一种方式是为@Bean注释指定destroyMethod属性,并将 destroy 方法名设置为其值。该注释用于在 Java 配置类中声明 beans。尽管 Java 配置将在本章的稍后部分讨论,但是 bean 销毁部分属于这里。对于这个例子,使用初始的DestructiveBeanWithJSR250类,因为配置是外部的,就像使用destroy-method属性一样。我们将只写一个配置类和一个新的main()方法来测试它。另外,default-lazy-init="true"将被每个 bean 声明上的@Lazy注释所取代。
package com.apress.prospring5.ch4.config;
import com.apress.prospring5.ch4.DestructiveBeanWithJSR250;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.support.GenericApplicationContext;
/**
* Created by iuliana.cosmina on 2/27/17.
*/
public class DestructiveBeanConfigDemo {
@Configuration
static class DestructiveBeanConfig {
@Lazy
@Bean(initMethod = "afterPropertiesSet", destroyMethod = "destroy")
DestructiveBeanWithJSR250 destructiveBean() {
DestructiveBeanWithJSR250 destructiveBean =
new DestructiveBeanWithJSR250();
destructiveBean.setFilePath(System.getProperty("java.io.tmpdir") +
System.getProperty("file.separator") + "test.txt");
return destructiveBean;
}
}
public static void main(String... args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(DestructiveBeanConfig.class);
ctx.getBean(DestructiveBeanWithJSR250.class);
System.out.println("Calling destroy()");
ctx.destroy();
System.out.println("Called destroy()");
}
}
在 bean 配置中也使用了@PostConstruct注释;因此,运行这段代码将产生到目前为止观察到的相同结果。
Initializing Bean
File exists: true
Calling destroy()
Destroying Bean
File exists: false
Called destroy()
销毁回调是一种理想的机制,可以确保您的应用正常关闭,并且不会让资源处于打开或不一致的状态。然而,您仍然必须决定是使用析构方法回调、DisposableBean接口、@PreDestroy注释、XML destroy-attribute属性还是destroyMethod。同样,让您的应用的需求驱动您在这方面的决策;在可移植性成问题的地方使用方法回调,并使用DisposableBean接口或 JSR-250 注释来减少所需的配置量。
了解解决方案的顺序
与创建 bean 的情况一样,您可以在同一个 bean 实例上使用所有机制来销毁 bean。在这种情况下,Spring 首先调用用@PreDestroy注释的方法,然后调用DisposableBean.destroy(),接着调用在 XML 定义中配置的 destroy 方法。
使用关闭挂钩
Spring 中销毁回调的唯一缺点是它们不会自动触发;您需要记住在应用关闭之前调用AbstractApplicationContext.destroy()。当您的应用作为 servlet 运行时,您可以简单地调用 servlet 的destroy()方法中的destroy()。然而,在一个独立的应用中,事情并不那么简单,尤其是当您的应用有多个出口点时。幸运的是,有一个解决方案。Java 允许您创建一个关闭挂钩,这是一个在应用关闭之前执行的线程。这是调用您的AbstractApplicationContext的destroy()方法的完美方式(它被所有具体的ApplicationContext实现扩展)。利用这种机制最简单的方法是使用AbstractApplicationContext的registerShutdownHook()方法。该方法自动指示 Spring 注册底层 JVM 运行时的关闭挂钩。bean 声明和配置与以前一样;唯一改变的是 main 方法:添加了对ctx.registerShutdownHook的调用,对ctx.destroy()或close()的调用将被移除。
...
public class DestructiveBeanWithHook
{
public static void main(String... args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(
DestructiveBeanConfig.class);
ctx.getBean(DestructiveBeanWithJSR250.class);
ctx.registerShutdownHook();
}
}
运行这段代码将会产生到目前为止观察到的相同结果。
Initializing Bean
File exists: true
Destroying Bean
File exists: false
如您所见,调用了destroy()方法,尽管我们没有编写任何代码在应用关闭时显式调用它。
让你的豆子“感知 Spring”
作为一种实现控制反转的机制,依赖注入相对于依赖查找的最大卖点之一是,您的 beans 不需要知道管理它们的容器的实现。对于使用构造函数或 setter 注入的 bean,Spring 容器与 Google Guice 或 PicoContainer 提供的容器是一样的。但是,在某些情况下,您可能需要一个使用依赖注入来获取其依赖项的 bean,这样它就可以出于其他原因与容器进行交互。这方面的一个例子可能是一个 bean,它自动为您配置一个关闭挂钩,因此它需要访问ApplicationContext。在其他情况下,一个 bean 可能想知道它的名称是什么(也就是在当前的ApplicationContext中分配的 bean 名称),这样它就可以基于这个名称执行一些额外的处理。
也就是说,这个特性实际上是供内部 Spring 使用的。赋予 bean 名称某种业务含义通常不是一个好主意,并且会导致配置问题,因为必须人为地操纵 bean 名称来支持它们的业务含义。然而,我们发现让 bean 在运行时找到它的名字对于日志记录非常有用。假设您有许多相同类型的 beans 在不同的配置下运行。bean 名称可以包含在日志消息中,以帮助您区分产生错误的 bean 和出错时工作正常的 bean。
使用 BeanNameAware 接口
想获得自己名字的 bean 可以实现的BeanNameAware接口只有一个方法:setBeanName(String)。Spring 在配置完 bean 之后,调用任何生命周期回调(初始化或销毁)之前调用setBeanName()方法(参见图 4-1 )。在大多数情况下,setBeanName()接口的实现只是一行代码,它将容器传入的值存储在一个字段中,供以后使用。下面的代码片段显示了一个简单的 bean,它通过使用BeanNameAware获得自己的名称,然后使用这个 bean 名称打印到控制台:
package com.apress.prospring5.ch4;
import org.springframework.beans.factory.BeanNameAware;
public class NamedSinger implements BeanNameAware {
private String name;
/** @Implements {@link BeanNameAware#setBeanName(String)} */
public void setBeanName(String beanName) {
this.name = beanName;
}
public void sing() {
System.out.println("Singer " + name + " - sing()");
}
}
这个实现相当简单。请记住,在通过调用ApplicationContext.getBean()将 bean 的第一个实例返回到您的应用之前,会调用BeanNameAware.setBeanName(),因此不需要检查 bean 名称在sing()方法中是否可用。在这里,您可以看到本例中使用的app-context-xml.xml文件中包含的配置:
<beans ...>
<bean id="johnMayer"
class="com.apress.prospring5.ch4.NamedSinger"/>
</beans>
如您所见,利用BeanNameAware接口不需要特殊的配置。在下面的代码片段中,您可以看到一个简单的示例应用,它从ApplicationContext中检索Singer实例,然后调用sing()方法:
package com.apress.prospring5.ch4;
import org.springframework.context.support.GenericXmlApplicationContext;
public class NamedSingerDemo {
public static void main(String... args) {
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-xml.xml");
ctx.refresh();
NamedSinger bean = (NamedSinger) ctx.getBean("johnMayer");
bean.sing();
ctx.close();
}
}
此示例生成以下日志输出;注意调用sing()的日志消息中包含了 bean 名称:
Singer johnMayer - sing()
使用BeanNameAware接口真的很简单,当您提高日志消息的质量时,它会派上用场。避免仅仅因为您可以访问 bean 名称就试图赋予它们业务意义;通过这样做,您将您的类耦合到 Spring 来获得一个可以忽略不计的好处。如果您的 bean 需要某种内部名称,让它们用方法setName()实现一个接口,比如Nameable(特定于您的应用),然后使用依赖注入给每个 bean 一个名称。这样,您可以保持用于配置的名称简洁,并且您不需要不必要地操纵您的配置来为您的 beans 赋予具有业务意义的名称。
使用 ApplicationContextAware 接口
在第三章的结尾引入了ApplicationContextAware,以展示如何使用 Spring 来处理需要其他 bean 来运行的 bean,这些 bean 不是使用配置中的构造函数或设置函数注入的(例如depends-on)。
使用ApplicationContextAware接口,您的 beans 可以获得对配置它们的ApplicationContext实例的引用。创建该接口的主要原因是允许 bean 访问应用中 Spring 的ApplicationContext,例如,使用getBean()以编程方式获取其他 Spring beans。但是,您应该避免这种做法,并使用依赖注入来为您的 beans 提供它们的协作者。如果在可以使用依赖注入的情况下,使用基于查找的getBean()方法来获取依赖,那么就会给 beans 增加不必要的复杂性,并且毫无理由地将它们耦合到 Spring 框架。
当然,ApplicationContext不仅仅是用来查豆子的;它执行许多其他任务。正如您之前看到的,这些任务之一是销毁所有的单例,在这样做之前依次通知它们。在上一节中,您看到了如何创建一个关闭挂钩来确保在应用关闭之前指示ApplicationContext销毁所有的单例。通过使用ApplicationContextAware接口,您可以构建一个可以在ApplicationContext中配置的 bean,以自动创建和配置关机钩子 bean。以下配置显示了此 bean 的代码:
package com.apress.prospring5.ch4;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.support.GenericApplicationContext;
public class ShutdownHookBean implements ApplicationContextAware {
private ApplicationContext ctx;
/** @Implements {@link ApplicationContextAware#s
etApplicationContext(ApplicationContext)} }*/
public void setApplicationContext(ApplicationContext ctx)
throws BeansException {
if (ctx instanceof GenericApplicationContext) {
((GenericApplicationContext) ctx).registerShutdownHook();
}
}
}
现在,您应该对这些代码的大部分已经很熟悉了。ApplicationContextAware接口定义了一个单独的方法setApplicationContext(ApplicationContext),Spring 调用该方法向 bean 传递对其ApplicationContext的引用。在前面的代码片段中,ShutdownHookBean类检查ApplicationContext是否属于GenericApplicationContext类型,这意味着它支持registerShutdownHook()方法;如果是这样,它将向ApplicationContext注册一个关机挂钩。下面的配置片段显示了如何配置这个 bean 来与DestructiveBeanWithInterface bean ( app-context-annotation.xml)一起工作:
<beans ...">
<context:annotation-config/>
<bean id="destructiveBean"
class="com.apress.prospring5.ch4.DestructiveBeanWithInterface"
p:filePath=
"#{systemProperties'java.io.tmpdir'}#{systemProperties'file.separator'}test.txt"/>
<bean id="shutdownHook"
class="com.apress.prospring5.ch4.ShutdownHookBean"/>
</beans>
请注意,不需要特殊的配置。下面的代码片段显示了一个简单的示例应用,它使用ShutdownHookBean来管理单例 beans 的销毁:
package com.apress.prospring5.ch4;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.io.File;
import org.springframework.context.support.GenericXmlApplicationContext;
public class DestructiveBeanWithInterface {
private File file;
private String filePath;
@PostConstruct
public void afterPropertiesSet() throws Exception {
System.out.println("Initializing Bean");
if (filePath == null) {
throw new IllegalArgumentException(
"You must specify the filePath property of " +
DestructiveBeanWithInterface.class);
}
this.file = new File(filePath);
this.file.createNewFile();
System.out.println("File exists: " + file.exists());
}
@PreDestroy
public void destroy() {
System.out.println("Destroying Bean");
if(!file.delete()) {
System.err.println("ERROR: failed to delete file.");
}
System.out.println("File exists: " + file.exists());
}
public void setFilePath(String filePath) {
this.filePath = filePath;
}
public static void main(String... args) throws Exception {
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-annotation.xml");
ctx.registerShutdownHook();
ctx.refresh();
ctx.getBean("destructiveBean",
DestructiveBeanWithInterface.class);
}
}
您应该对这段代码非常熟悉。当在配置中定义了 Spring bootstraps ApplicationContext和destructiveBean时,Spring 将ApplicationContext的引用传递给shutdownHook bean,以注册关机挂钩。运行此示例会产生预期的以下输出:
Initializing Bean
File exists: true
Destroying Bean
File exists: false
正如你所看到的,即使在主应用中没有对destroy()的调用,ShutdownHookBean也被注册为一个关闭钩子,它在应用关闭之前调用destroy()。
工厂设备的使用
使用 Spring 时,您将面临的一个问题是如何创建并注入依赖项,这些依赖项不能简单地通过使用new操作符来创建。为了克服这个问题,Spring 提供了FactoryBean接口,作为不能使用标准 Spring 语义创建和管理的对象的适配器。通常,您使用FactoryBean来创建您不能使用new操作符创建的 beanss,比如那些您通过静态工厂方法访问的 bean,尽管情况并不总是如此。简单地说,FactoryBean是一种充当其他豆子工厂的豆子。像任何普通 bean 一样在您的ApplicationContext中配置FactoryBean s,但是当 Spring 使用FactoryBean接口来满足一个依赖或查找请求时,它不会返回FactoryBean;相反,它调用FactoryBean.getObject()方法并返回调用的结果。
s 在 Spring 发挥了巨大的作用;最显著的用途是创建事务代理,我们将在第九章中介绍,以及从 JNDI 上下文中自动检索资源。然而,FactoryBean不仅对构建 Spring 的内部有用;当您构建自己的应用时,您会发现它们非常有用,因为它们允许您通过使用 IoC 来管理比其他方式更多的资源。
FactoryBean 示例:MessageDigestFactoryBean
通常我们工作的项目需要某种密码处理;通常,这包括生成要存储在数据库中的用户密码的消息摘要或散列。在 Java 中,MessageDigest类提供了创建任意数据摘要的功能。MessageDigest本身是抽象的,通过调用MessageDigest.getInstance()并传入您想要使用的摘要算法的名称,您可以获得具体的实现。例如,如果我们想使用 MD5 算法创建一个摘要,我们使用下面的代码来创建MessageDigest实例:
MessageDigest md5 = MessageDigest.getInstance("MD5");
如果我们想使用 Spring 来管理MessageDigest对象的创建,那么在没有FactoryBean的情况下,我们能做的最好的事情就是在 bean 上有一个属性algorithmName,然后使用一个初始化回调来调用MessageDigest.getInstance()。使用一个FactoryBean,我们可以将这个逻辑封装在一个 bean 中。然后,任何需要一个MessageDigest实例的 beans 都可以简单地声明一个属性messageDigest,并使用FactoryBean来获取实例。下面的代码片段显示了FactoryBean的一个实现,它就是这样做的:
package com.apress.prospring5.ch4;
import java.security.MessageDigest;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
public class MessageDigestFactoryBean implements
FactoryBean<MessageDigest>, InitializingBean {
private String algorithmName = "MD5";
private MessageDigest messageDigest = null;
public MessageDigest getObject() throws Exception {
return messageDigest;
}
public Class<MessageDigest> getObjectType() {
return MessageDigest.class;
}
public boolean isSingleton() {
return true;
}
public void afterPropertiesSet() throws Exception {
messageDigest = MessageDigest.getInstance(algorithmName);
}
public void setAlgorithmName(String algorithmName) {
this.algorithmName = algorithmName;
}
}
Spring 调用getObject()方法来检索由FactoryBean创建的对象。这是传递给使用FactoryBean作为合作者的其他 beans 的实际对象。在代码片段中,您可以看到MessageDigestFactoryBean传递了在InitializingBean.afterPropertiesSet()回调中创建的存储的MessageDigest实例的克隆。
getObjectType()方法允许你告诉 Spring 你的FactoryBean将返回什么类型的对象。如果事先不知道返回类型,这可以是null(例如,FactoryBean根据配置创建不同类型的对象,这只有在FactoryBean初始化后才能确定),但是如果你指定一个类型,Spring 可以使用它进行自动连接。我们返回MessageDigest作为我们的类型(在本例中,是一个类,但是尝试返回一个接口类型,并让FactoryBean实例化具体的实现类,除非有必要)。原因是我们不知道将返回什么具体类型(这并不重要,因为所有 beans 都将通过使用MessageDigest来定义它们的依赖关系)。
isSingleton()属性允许您通知 SpringFactoryBean是否正在管理单例实例。记住,通过设置FactoryBean的<bean>标签的 singleton 属性,您告诉 Spring 关于FactoryBean本身的 singleton 状态,而不是它返回的对象。现在让我们看看FactoryBean是如何在应用中使用的。在下面的代码片段中,您可以看到一个简单的 bean,它维护两个MessageDigest实例,然后显示传递给其digest()方法的消息摘要:
package com.apress.prospring5.ch4;
import java.security.MessageDigest;
public class MessageDigester {
private MessageDigest digest1;
private MessageDigest digest2;
public void setDigest1(MessageDigest digest1) {
this.digest1 = digest1;
}
public void setDigest2(MessageDigest digest2) {
this.digest2 = digest2;
}
public void digest(String msg) {
System.out.println("Using digest1");
digest(msg, digest1);
System.out.println("Using digest2");
digest(msg, digest2);
}
private void digest(String msg, MessageDigest digest) {
System.out.println("Using alogrithm: " + digest.getAlgorithm());
digest.reset();
byte[] bytes = msg.getBytes();
byte[] out = digest.digest(bytes);
System.out.println(out);
}
}
以下配置片段显示了两个MessageDigestFactoryBean类的示例配置,一个用于 SHA1 算法,另一个使用默认(MD5)算法(app-context-xml.xml):
<beans ...>
<bean id="shaDigest"
class="com.apress.prospring5.ch4.MessageDigestFactoryBean"
p:algorithmName="SHA1"/>
<bean id="defaultDigest"
class="com.apress.prospring5.ch4.MessageDigestFactoryBean"/>
<bean id="digester"
class="com.apress.prospring5.ch4.MessageDigester"
p:digest1-ref="shaDigest"
p:digest2-ref="defaultDigest"/>
</beans>
如您所见,我们不仅配置了两个MessageDigestFactoryBean类,还配置了一个MessageDigester,使用两个MessageDigestFactoryBean类来为digest1和digest2属性提供值。对于defaultDigest bean,因为没有指定algorithmName属性,所以不会发生注入,将使用类中编码的默认算法(MD5)。在下面的代码示例中,您可以看到一个基本的示例类,它从BeanFactory中检索MessageDigester bean,并创建一个简单消息的摘要:
package com.apress.prospring5.ch4;
import org.springframework.context.support.GenericXmlApplicationContext;
public class MessageDigestDemo {
public static void main(String... args) {
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-xml.xml");
ctx.refresh();
MessageDigester digester = ctx.getBean("digester",
MessageDigester.class);
digester.digest("Hello World!");
ctx.close();
}
}
运行此示例会产生以下输出:
Using digest1
Using alogrithm: SHA1
B@130f889
Using digest2
Using alogrithm: MD5
[B@1188e820
如您所见,MessageDigest bean 提供了两个MessageDigest实现,SHA1和MD5,尽管在BeanFactory中没有配置MessageDigestbean。这就是FactoryBean在起作用。
当你正在处理不能用new操作符创建的类时,这是一个完美的解决方案。如果您使用通过工厂方法创建的对象,并且希望在 Spring 应用中使用这些类,那么创建一个FactoryBean作为适配器,允许您的类充分利用 Spring 的 IoC 功能。
当使用通过 Java 配置的配置时,使用FactoryBean s 是不同的,因为在这种情况下,编译器限制使用正确的类型设置属性;因此,必须显式调用getObject()方法。在下面的代码片段中,您可以看到一个配置与上一个示例相同的 beans 的示例,但是使用了 Java 配置:
package com.apress.prospring5.ch4.config;
import com.apress.prospring5.ch4.MessageDigestFactoryBean;
import com.apress.prospring5.ch4.MessageDigester;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.GenericApplicationContext;
public class MessageDigesterConfigDemo {
@Configuration
static class MessageDigesterConfig {
@Bean
public MessageDigestFactoryBean shaDigest() {
MessageDigestFactoryBean factoryOne =
new MessageDigestFactoryBean();
factoryOne.setAlgorithmName("SHA1");
return factoryOne;
}
@Bean
public MessageDigestFactoryBean defaultDigest() {
return new MessageDigestFactoryBean();
}
@Bean
MessageDigester digester() throws Exception {
MessageDigester messageDigester = new MessageDigester();
messageDigester.setDigest1(shaDigest().getObject());
messageDigester.setDigest2(defaultDigest().getObject());
return messageDigester;
}
}
public static void main(String... args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(MessageDigesterConfig.class);
MessageDigester digester = (MessageDigester) ctx.getBean("digester");
digester.digest("Hello World!");
ctx.close();
}
}
如果运行这个类,将会打印出与之前相同的输出。
直接访问工厂 Bean
假设 Spring 自动满足由某个FactoryBean产生的对象对该FactoryBean的任何引用,您可能想知道是否可以直接访问该FactoryBean。答案是肯定的。
访问FactoryBean很简单:在对getBean()的调用中,在 bean 名称前面加上一个&符号,如下面的代码示例所示:
package com.apress.prospring5.ch4;
import org.springframework.context.support.GenericXmlApplicationContext;
import java.security.MessageDigest;
public class AccessingFactoryBeans {
public static void main(String... args) {
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-xml.xml");
ctx.refresh();
ctx.getBean("shaDigest", MessageDigest.class);
MessageDigestFactoryBean factoryBean =
(MessageDigestFactoryBean) ctx.getBean("&shaDigest");
try {
MessageDigest shaDigest = factoryBean.getObject();
System.out.println(shaDigest.digest("Hello world".getBytes()));
} catch (Exception ex) {
ex.printStackTrace();
}
ctx.close();
}
}
运行该程序会生成以下输出:
[B@130f889
Spring 代码中有几个地方使用了这个特性,但是您的应用应该没有理由使用它。FactoryBean的目的是作为一个支持基础设施,允许你在 IoC 环境中使用更多的应用类。避免直接访问FactoryBean并手动调用其getObject(),让 Spring 替你做;如果您手动地这样做,您正在为自己做额外的工作,并且不必要地将您的应用耦合到将来很容易改变的特定实现细节。
使用工厂 bean 和工厂方法属性
有时,您需要实例化由非 Spring 驱动的第三方应用提供的 JavaBeans。您不知道如何实例化该类,但您知道第三方应用提供了一个类,可用于获取您的 Spring 应用需要的 JavaBean 的实例。在这种情况下,可以使用 Spring bean 的<bean>标签中的factory-bean和factory-method属性。
为了了解它是如何工作的,下面的代码片段展示了另一个版本的MessageDigestFactory,它提供了一个返回MessageDigest bean 的方法:
package com.apress.prospring5.ch4;
import java.security.MessageDigest;
public class MessageDigestFactory {
private String algorithmName = "MD5";
public MessageDigest createInstance() throws Exception {
return MessageDigest.getInstance(algorithmName);
}
public void setAlgorithmName(String algorithmName) {
this.algorithmName = algorithmName;
}
}
下面的配置片段显示了如何配置工厂方法来获取相应的MessageDigest bean 实例(app-context-xml.xml):
<beans...>
<bean id="shaDigestFactory"
class="com.apress.prospring5.ch4.MessageDigestFactory"
p:algorithmName="SHA1"/>
<bean id="defaultDigestFactory"
class="com.apress.prospring5.ch4.MessageDigestFactory"/>
<bean id="shaDigest"
factory-bean="shaDigestFactory"
factory-method="createInstance">
</bean>
<bean id="defaultDigest"
factory-bean="defaultDigestFactory"
factory-method="createInstance"/>
<bean id="digester"
class="com.apress.prospring5.ch4.MessageDigester"
p:digest1-ref="shaDigest"
p:digest2-ref="defaultDigest"/>
</beans>
注意,定义了两个摘要工厂 beans,一个使用 SHA1,另一个使用默认算法。然后对于 beanshaDigest和defaultDigest,我们通过factory-bean属性指示 Spring 使用相应的消息摘要工厂 bean 来实例化 bean,并且我们通过factory-method属性指定了用于获得 bean 实例的方法。下面的代码片段描述了测试类:
package com.apress.prospring5.ch4;
import org.springframework.context.support.GenericXmlApplicationContext;
public class MessageDigestFactoryDemo {
public static void main(String... args) {
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-xml.xml");
ctx.refresh();
MessageDigester digester = ctx.getBean("digester",
MessageDigester.class);
digester.digest("Hello World!");
ctx.close();
}
}
运行该程序会生成以下输出:
Using digest1
Using alogrithm: SHA1 [B@77a57272
Using digest2
Using alogrithm: MD5 [B@7181ae3f
JavaBeans 属性编辑器
如果您不完全熟悉 JavaBeans 的概念,PropertyEditor是一个接口,它将属性值转换为本机类型表示,或者从本机类型表示转换为String。最初,这被认为是一种允许属性值作为String值输入到编辑器中并将其转换为正确类型的方法。然而,因为PropertyEditor本质上是轻量级的类,所以它们在许多环境中都有应用,包括 Spring。
因为基于 Spring 的应用中很大一部分属性值都是在BeanFactory配置文件中开始的,所以它们本质上是String s。然而,设置这些值的属性可能不是String类型的。因此,为了让您不必人工创建大量的String类型的属性,Spring 允许您定义PropertyEditor来管理基于String的属性值到正确类型的转换。图 [4-2 显示了作为spring-beans包一部分的PropertyEditor的完整列表;您可以在任何智能 Java 编辑器中看到这个列表。

图 4-2。
Spring PropertyEditors
它们都扩展了java.beans.PropertyEditorSupport,并且可以用于将String文字隐式转换为要注入 beans 的属性值;因此,他们在BeanFactory预先注册。
使用内置的属性编辑器
下面的代码片段显示了一个简单的 bean,它声明了 14 个属性,内置PropertyEditor实现支持的每种类型都有一个属性:
package com.apress.prospring5.ch4;
import java.io.File;
import java.io.InputStream;
import java.net.URL; import java.util.Date; import java.util.List; import java.util.Locale;
import java.util.Properties; import java.util.regex.Pattern; import java.text.SimpleDateFormat;
import org.springframework.beans.PropertyEditorRegistrar;
import org.springframework.beans.PropertyEditorRegistry;
import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.beans.propertyeditors.StringTrimmerEditor;
import org.springframework.context.support.GenericXmlApplicationContext;
public class PropertyEditorBean {
)
private byte[] bytes; // ByteArrayPropertyEditor
)
private Character character; //CharacterEditor
private Class cls; // ClassEditor
private Boolean trueOrFalse; // CustomBooleanEditor
private List<String> stringList; // CustomCollectionEditor
private Date date; // CustomDateEditor
private Float floatValue; // CustomNumberEditor
private File file; // FileEditor
private InputStream stream; // InputStreamEditor
private Locale locale; // LocaleEditor
private Pattern pattern; // PatternEditor
private Properties properties; // PropertiesEditor
private String trimString; // StringTrimmerEditor
private URL url; // URLEditor
public void setCharacter(Character character) {
System.out.println("Setting character: " + character);
this.character = character;
}
public void setCls(Class cls) {
System.out.println("Setting class: " + cls.getName());
this.cls = cls;
}
public void setFile(File file) {
System.out.println("Setting file: " + file.getName());
this.file = file;
}
public void setLocale(Locale locale) {
System.out.println("Setting locale: " + locale.getDisplayName());
this.locale = locale;
}
public void setProperties(Properties properties) {
System.out.println("Loaded " + properties.size() + " properties");
this.properties = properties;
}
public void setUrl(URL url) {
System.out.println("Setting URL: " + url.toExternalForm());
this.url = url;
}
public void setBytes(byte... bytes) {
System.out.println("Setting bytes: " + Arrays.toString(bytes));
this.bytes = bytes;
}
public void setTrueOrFalse(Boolean trueOrFalse) {
System.out.println("Setting Boolean: " + trueOrFalse);
this.trueOrFalse = trueOrFalse;
}
public void setStringList(List<String> stringList) {
System.out.println("Setting string list with size: "
+ stringList.size());
this.stringList = stringList;
for (String string: stringList) {
System.out.println("String member: " + string);
}
}
public void setDate(Date date) {
System.out.println("Setting date: " + date);
this.date = date;
}
public void setFloatValue(Float floatValue) {
System.out.println("Setting float value: " + floatValue);
this.floatValue = floatValue;
}
public void setStream(InputStream stream) {
System.out.println("Setting stream: " + stream);
this.stream = stream;
}
public void setPattern(Pattern pattern) {
System.out.println("Setting pattern: " + pattern);
this.pattern = pattern;
}
public void setTrimString(String trimString) {
System.out.println("Setting trim string: " + trimString);
this.trimString = trimString;
}
public static class CustomPropertyEditorRegistrar
implements PropertyEditorRegistrar {
@Override
public void registerCustomEditors(PropertyEditorRegistry registry) {
SimpleDateFormat dateFormatter = new SimpleDateFormat("MM/dd/yyyy");
registry.registerCustomEditor(Date.class,
new CustomDateEditor(dateFormatter, true));
registry.registerCustomEditor(String.class, new StringTrimmerEditor(true));
}
}
public static void main(String... args) throws Exception {
File file = File.createTempFile("test", "txt");
file.deleteOnExit();
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-01.xml");
ctx.refresh();
PropertyEditorBean bean =
(PropertyEditorBean) ctx.getBean("builtInSample");
ctx.close();
}
}
在下面的配置示例中,您可以看到用于声明类型为PropertyEditorBean的 bean 的配置,其中为所有先前的属性(app-config-01.xml)指定了值:
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util.xsd">
<bean id="customEditorConfigurer"
class="org.springframework.beans.factory.config.CustomEditorConfigurer"
p:propertyEditorRegistrars-ref="propertyEditorRegistrarsList"/>
<util:list id="propertyEditorRegistrarsList">
<bean class="com.apress.prospring5.ch4.PropertyEditorBean$
CustomPropertyEditorRegistrar"/>
</util:list>
<bean id="builtInSample"
class="com.apress.prospring5.ch4.PropertyEditorBean"
p:character="A"
p:bytes="John Mayer"
p:cls="java.lang.String"
p:trueOrFalse="true"
p:stringList-ref="stringList"
p:stream="test.txt"
p:floatValue="123.45678"
p:date="05/03/13"
p:file="#{systemProperties'java.io.tmpdir'}
#{systemProperties'file.separator'}test.txt"
p:locale="en_US"
p:pattern="a*b"
p:properties="name=Chris age=32"
p:trimString=" String need trimming "
p:url="https://spring.io/"
/>
<util:list id="stringList">
<value>String member 1</value>
<value>String member 2</value>
</util:list>
</beans>
正如您所看到的,虽然PropertyEditorBean上的所有属性都不是String s,但是属性的值被指定为简单的String s。还要注意,我们注册了CustomDateEditor和StringTrimmerEditor,因为这两个编辑器在 Spring 中不是默认注册的。运行此示例会产生以下输出:
Setting bytes: [74, 111, 104, 110, 32, 77, 97, 121, 101, 114]
Setting character: A
Setting class: java.lang.String
Setting date: Wed May 03 00:00:00 EET 13
Setting file: test.txt
Setting float value: 123.45678
Setting locale: English (United States)
Setting pattern: a*b
Loaded 1 properties
Setting stream: java.io.BufferedInputStream@42e25b0b
Setting string list with size: 2
String member: String member 1
String member: String member 2
Setting trim string: String need trimming
Setting Boolean: true
Setting URL: https://spring.io/
如您所见,Spring 已经使用内置的PropertyEditor将各种属性的String表示转换为正确的类型。表 4-1 列出了 Spring 可用的最重要的内置PropertyEditor。
表 4-1。
Spring PropertyEditors
| 使用 | 描述 | | --- | --- | | `ByteArrayPropertyEditor` | 将`String`值转换成相应的字节表示。 | | `CharacterEditor` | 从`String`值填充`Character`或`char`类型的属性。 | | `ClassEditor` | 从完全限定的类名转换成一个`Class`实例。当使用这个`PropertyEditor`时,注意不要在使用`GenericXmlApplicationContext`时类名的两边包含任何多余的空格,因为这会导致一个`ClassNotFoundException`。 | | `CustomBooleanEditor` | 将字符串转换为 Java 布尔类型。 | | `CustomCollectionEditor` | 将源集合(例如,由`Spring`中的`util`名称空间表示)转换成目标`Collection`类型。 | | `CustomDateEditor` | 将日期的字符串表示转换为`java.util.Date`值。您需要用期望的日期格式在 Spring 的`ApplicationContext`中注册`CustomDateEditor`实现。 | | `FileEditor` | 将一个`String`文件路径转换成一个`File`实例。Spring 不检查文件是否存在。 | | `InputStreamEditor` | 将资源的字符串表示形式(例如,使用`file:D:/temp/test.txt or classpath:test.txt`的文件资源)转换为输入流属性。 | | `LocaleEditor` | 将一个地区的`String`表示,比如`en-GB`,转换成一个`java.util.Locale`实例。 | | `PatternEditor` | 将一个字符串转换成 JDK `Pattern`对象或者反过来。 | | `PropertiesEditor` | 将格式为`key1=value1 key2=value2 keyn=valuen`的`String`转换为配置了相应属性的`java.util.Properties`的实例。 | | `StringTrimmerEditor` | 在注入之前对字符串值执行修整。您需要显式注册这个编辑器。 | | `URLEditor` | 将 URL 的`String`表示转换成`java.net.URL`的实例。 |这组PropertyEditor为使用 Spring 提供了一个很好的基础,并使得用文件和 URL 等通用组件配置应用变得更加简单。
创建自定义属性编辑器
尽管内置的PropertyEditor涵盖了属性类型转换的一些标准情况,但有时您可能需要创建自己的PropertyEditor来支持您在应用中使用的一个类或一组类。Spring 完全支持注册自定义PropertyEditors;唯一的缺点是java.beans.PropertyEditor接口有很多方法,其中许多与手头的任务无关,即转换属性类型。谢天谢地,JDK 5 或更新版本提供了PropertyEditorSupport类,您自己的PropertyEditor可以扩展这个类,让您只实现一个方法:setAsText()。让我们考虑一个简单的例子,看看如何实现自定义属性编辑器。假设我们有一个只有两个属性firstName和lastName的FullName类,定义如下:
package com.apress.prospring5.ch4.custom;
public class FullName {
private String firstName;
private String lastName;
public FullName(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String toString() {
return "First name: " + firstName + " - Last name: " + lastName;
}
}
为了简化应用配置,让我们开发一个定制编辑器,将带有空格分隔符的字符串分别转换为FullName类的名字和姓氏。下面的代码片段描述了自定义属性编辑器的实现:
package com.apress.prospring5.ch4.custom;
import java.beans.PropertyEditorSupport;
public class NamePropertyEditor extends PropertyEditorSupport {
@Override
public void setAsText(String text) throws IllegalArgumentException {
String[] name = text.split("\\s");
setValue(new FullName(name[0], name[1]));
}
}
编辑器很简单。它扩展了 JDK 的PropertyEditorSupport类并实现了setAsText()方法。在该方法中,我们简单地将String分割成一个字符串数组,以空格作为分隔符。之后,FullName类的一个实例被实例化,在空格字符前传入String作为名字,在空格字符后传入String作为姓氏。最后,通过调用带有结果的setValue()方法返回转换后的值。为了在您的应用中使用NamePropertyEditor,我们需要在 Spring 的ApplicationContext中注册编辑器。以下配置示例显示了一个CustomEditorConfigurer和NamePropertyEditor ( app-context-02.xml)的ApplicationContext配置:
<beans ...>
<bean name="customEditorConfigurer"
class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="customEditors">
<map>
<entry key="com.apress.prospring5.ch4.custom.FullName"
value="com.apress.prospring5.ch4.custom.NamePropertyEditor"/>
</map>
</property>
</bean>
<bean id="exampleBean"
class="com.apress.prospring5.ch4.custom.CustomEditorExample"
p:name="John Mayer"/>
</beans>
在这个配置中,您应该注意到两件事。首先,通过使用Map类型的customEditors属性,自定义的PropertyEditor被注入到CustomEditorConfigurer类中。其次,Map中的每个条目代表一个单独的PropertyEditor,条目的关键字是使用PropertyEditor的类的名称。如您所见,NamePropertyEditor的键是com.apress.prospring4.ch4.FullName,这表示这是应该使用编辑器的类。下面的代码片段显示了在前面的配置中注册为 bean 的CustomEditorExample类的代码:
package com.apress.prospring5.ch4.custom;
import org.springframework.context.support.GenericXmlApplicationContext;
public class CustomEditorExample {
private FullName name;
public FullName getName() {
return name;
}
public void setName(FullName name) {
this.name = name;
}
public static void main(String... args) {
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-02.xml");
ctx.refresh();
CustomEditorExample bean =
(CustomEditorExample) ctx.getBean("exampleBean");
System.out.println(bean.getName());
ctx.close();
}
}
前面的代码没什么特别的。运行该示例,您将看到以下输出:
First name: John - Last name: Mayer
这是我们在FullName类中实现的toString()方法的输出,您可以看到通过使用配置的NamePropertyEditor,Spring 正确地填充了FullName对象的名字和姓氏。从版本 3 开始,Spring 引入了类型转换 API 和字段格式化服务提供者接口(SPI ),它们提供了一个更简单且结构良好的 API 来执行类型转换和字段格式化。这对于 web 应用开发尤其有用。类型转换 API 和字段格式化 SPI 将在第十章中详细讨论。
更多 Spring 应用上下文配置
到目前为止,虽然我们正在讨论 Spring 的ApplicationContext,但是我们已经讨论过的大多数特性主要围绕着由ApplicationContext包装的BeanFactory接口。在 Spring 中,BeanFactory接口的各种实现负责 bean 的实例化,为 Spring 管理的 bean 提供依赖注入和生命周期支持。然而,如前所述,作为BeanFactory接口的扩展,ApplicationContext也提供了其他有用的功能。ApplicationContext的主要功能是提供一个更加丰富的框架来构建你的应用。ApplicationContext更了解您在其中配置的 beans(与BeanFactory相比),对于许多 Spring 基础设施类和接口,比如BeanFactoryPostProcessor,它代表您与它们进行交互,减少了使用 Spring 所需编写的代码量。
使用ApplicationContext的最大好处是它允许你以完全声明的方式配置和管理 Spring 和 Spring 管理的资源。这意味着,只要有可能,Spring 都会提供支持类来将ApplicationContext自动加载到您的应用中,这样您就不需要编写任何代码来访问ApplicationContext。实际上,这个特性目前只有在您使用 Spring 构建 web 应用时才可用,这允许您在 web 应用部署描述符中初始化 Spring 的ApplicationContext。在使用单机应用时,也可以通过简单的编码来初始化 Spring 的ApplicationContext。
除了提供更侧重于声明性配置的模型之外,ApplicationContext还支持以下特性:
- 国际化
- 事件发布
- 资源管理和访问
- 附加生命周期界面
- 改进了基础架构组件的自动配置
在接下来的章节中,我们将讨论除了 DI 之外的一些最重要的特性。
消息源的国际化
Spring 真正擅长的一个领域是对国际化(i18n)的支持。使用MessageSource接口,您的应用可以访问用各种语言存储的String资源,称为消息。对于您希望在应用中支持的每种语言,您需要维护一个消息列表,这些消息对应于其他语言的消息。例如,如果你想用英语和捷克语显示“敏捷的棕色狐狸跳过了懒惰的狗”,你可以创建两个消息,都键入msg;英语的读法是“一只敏捷的棕色狐狸跳过了一只懒惰的狗”,德语的读法是“一只棕色的狐狸跳过了一只懒惰的狗”。
虽然您不需要使用ApplicationContext来使用MessageSource,但是ApplicationContext接口扩展了MessageSource,并为加载消息和使它们在您的环境中可用提供了特殊的支持。消息的自动加载在任何环境中都是可用的,但是自动访问只在某些 Spring 管理的场景中提供,比如当您使用 Spring 的 MVC 框架构建 web 应用时。尽管任何类都可以实现ApplicationContextAware,从而访问自动加载的消息,但我们在本章后面的“在独立应用中使用 MessageSource”一节中建议了一个更好的解决方案
在继续之前,如果您不熟悉 Java 中的 i18n 支持,我们建议您至少查看一下 Javadocs ( http://download.java.net/jdk8/docs/api/index.html )。
消息源的国际化
除了ApplicationContext,Spring 还提供了三个MessageSource实现。
ResourceBundleMessageSourceReloadableResourceBundleMessageSourceStaticMessageSource
不应该在生产应用中使用StaticMessageSource实现,因为您不能在外部配置它,这通常是您向应用添加 i18n 功能时的主要需求之一。
ResourceBundleMessageSource使用 Java ResourceBundle加载消息。ReloadableResourceBundleMessageSource本质上是相同的,除了它支持底层源文件的计划重载。
所有三个 MessageSource 实现还实现了另一个名为HierarchicalMessageSource的接口,该接口允许嵌套许多MessageSource实例。这是ApplicationContext使用MessageSource实例的关键。
为了利用ApplicationContext对MessageSource的支持,您必须在您的配置中定义一个类型为MessageSource的 bean,并使用名称messageSource. ApplicationContext将这个MessageSource嵌套在其自身中,允许您通过使用ApplicationContext来访问消息。这可能很难想象,所以看看下面的例子。以下代码示例显示了一个简单的应用,该应用访问英语和德语区域设置的一组消息:
package com.apress.prospring5.ch4;
import java.util.Locale;
import org.springframework.context.support.GenericXmlApplicationContext;
public class MessageSourceDemo {
public static void main(String... args) {
GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-xml.xml");
ctx.refresh();
Locale english = Locale.ENGLISH;
Locale german = new Locale("de", "DE");
System.out.println(ctx.getMessage("msg", null, english));
System.out.println(ctx.getMessage("msg", null, german));
System.out.println(ctx.getMessage("nameMsg", new Object[]
{ "John", "Mayer" }, english));
System.out.println(ctx.getMessage("nameMsg", new Object[]
{ "John", "Mayer" }, german));
ctx.close();
}
}
现在还不要担心对getMessage()的调用;我们将很快回到这些问题上。现在,只需知道它们为指定的地区检索一个键控消息。在下面的配置片段中,您可以看到这个应用使用的配置(app-context-xml.xml):
<beans ...>
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource"
p:basenames-ref="basenames"/>
<util:list id="basenames">
<value>buttons</value>
<value>labels</value>
</util:list>
</beans>
这里,我们根据需要定义一个名为messageSource的ResourceBundleMessageSource bean,并用一组名称对其进行配置,以形成其文件集的基础。一个由ResourceBundleMessageSource使用的 Java ResourceBundle,在一组由基本名称标识的属性文件上工作。当查找特定Locale的消息时,ResourceBundle会查找由基本名称和地区名称组合而成的文件。例如,如果基本名称是foo,并且我们在en-GB(英国英语)语言环境中查找消息,ResourceBundle将查找名为foo_en_GB.properties的文件。
在前面的示例中,英语(labels_en.properties)和德语(labels_de_DE.properties)的属性文件的内容如下所示:
#labels_en.properties
msg=My stupid mouth has got me in trouble
nameMsg=My name is {0} {1}
#labels_de_DE.properties
msg=Mein dummer Mund hat mich in Schwierigkeiten gebracht
nameMsg=Mein Name ist {0} {1}
这个例子引发了更多的问题。那些打给getMessage()的电话是什么意思?为什么我们使用ApplicationContext.getMessage()而不是直接访问ResourceBundleMessageSource bean?我们将依次回答这些问题。
使用 getMessage()方法
MessageSource接口为getMessage()方法定义了三个重载。这些在表 4-2 中描述。
表 4-2。
Overloads for MessageSource.getMessage()
| 方法签名 | 描述 | | --- | --- | | `getMessage (String, Object[], Locale)` | 这是标准的`getMessage()`方法。`String`参数是对应于属性文件中的键的消息的键。在前面的代码示例中,对`getMessage()`的第一次调用使用了`msg`作为键,这对应于`en`地区的属性文件中的以下条目:`msg=The quick brown fox jumped over the lazy dog`。`Object[]`数组参数用于替换消息中的内容。在对`getMessage()`的第三次调用中,我们传入了一个由两个`String`组成的数组。大括号中的数字是占位符,每个数字都被替换为参数数组中相应的条目。最后一个参数`Locale`告诉`ResourceBundleMessageSource`要查看哪个属性文件。尽管示例中对`getMessage()`的第一次和第二次调用使用了相同的键,但是它们返回了不同的消息,这些消息对应于传递给`getMessage()`的`Locale`设置。 | | `getMessage (String, Object[], String, Locale)` | 这个重载的工作方式与`getMessage(String, Object[], Locale)`相同,除了第二个`String`参数,它允许我们在所提供的键的消息对于所提供的`Locale`不可用的情况下传入一个默认值。 | | `getMessage (MessageSourceResolvable, Locale)` | 这个重载是一个特例。我们将在“MessageSourceResolvable 接口”一节中详细讨论它。 |为什么使用 ApplicationContext 作为消息源?
要回答这个问题,我们需要稍微超前一点,看看 Spring 中的 web 应用支持。总的来说,答案是您不应该使用ApplicationContext作为MessageSource,因为这样做会不必要地将您的 bean 耦合到ApplicationContext(这将在下一节详细讨论)。当你使用 Spring 的 MVC 框架构建一个 web 应用时,你应该使用ApplicationContext。
Spring MVC 中的核心接口是Controller。不像 Struts 这样的框架要求你通过从一个具体的类继承来实现你的控制器,Spring 只要求你实现Controller接口(或者用@Controller注释来注释你的控制器类)。话虽如此,Spring 提供了一组有用的基类,您可以用它们来实现自己的控制器。这些基类中的每一个都是ApplicationObjectSupport类的子类(直接或间接),对于任何想要知道ApplicationContext的应用对象来说,这是一个方便的超类。请记住,在 web 应用设置中,ApplicationContext是自动加载的。
ApplicationObjectSupport访问这个ApplicationContext,将其包装在一个MessageSourceAccessor对象中,并通过受保护的getMessageSourceAccessor()方法使其对您的控制器可用。MessageSourceAccessor 提供了大量使用MessageSource实例的便捷方法。为使用MessageSource实例提供了一系列方便的方法。这种形式的自动注射非常有益;它消除了所有控制器暴露一个MessageSource属性的需要。
然而,这并不是在 web 应用中使用ApplicationContext作为MessageSource的最好理由。使用ApplicationContext而不是手动定义的MessageSource bean 的主要原因是 Spring 尽可能将ApplicationContext作为MessageSource暴露给视图层。这意味着当你使用 Spring 的 JSP 标签库时,<spring:message>标签会自动从ApplicationContext读取消息,当你使用 JSTL 时,<fmt:message>标签也会这么做。
所有这些好处意味着,在构建 web 应用时,最好使用ApplicationContext中的MessageSource支持,而不是单独管理MessageSource的一个实例。当您考虑到您需要做的就是配置一个名为messageSource的MessageSource bean 来利用这个特性时,这一点尤其正确。
在独立应用中使用 MessageSource
当您在独立应用中使用MessageSource时,Spring 除了在ApplicationContext中自动嵌套MessageSource bean 之外不提供额外的支持,最好通过使用依赖注入来使MessageSource可用。您可以选择让您的 bean 成为ApplicationContextAware,但是这样做会妨碍它在BeanFactory上下文中的使用。除此之外,您使测试变得复杂,没有任何明显的好处,很明显,您应该坚持在独立设置中使用依赖注入来访问MessageSource对象。
MessageSourceResolvable 接口
当您查找来自MessageSource的消息时,您可以使用实现MessageSourceResolvable的对象来代替一个键和一组参数。这个接口在 Spring 验证库中被广泛使用,用来将Error对象链接到它们的国际化错误消息。
应用事件
另一个BeanFactory中没有的ApplicationContext特性是通过使用ApplicationContext作为代理来发布和接收事件的能力。在本节中,您将了解它的用法。
使用应用事件
事件是从ApplicationEvent派生的类,它本身从java.util.EventObject派生。任何 bean 都可以通过实现ApplicationListener<T>接口来监听事件;ApplicationContext在配置时,自动将实现该接口的任何 bean 注册为监听器。事件是使用ApplicationEventPublisher.publishEvent()方法发布的,因此发布类必须了解ApplicationContext(它扩展了ApplicationEventPublisher接口)。在 web 应用中,这很简单,因为您的许多类都是从 Spring Framework 类派生的,这些类允许通过受保护的方法访问ApplicationContext。在独立应用中,您可以让发布 bean 实现ApplicationContextAware来发布事件。
以下代码示例显示了一个基本事件类的示例:
package com.apress.prospring5.ch4;
import org.springframework.context.ApplicationEvent;
public class MessageEvent extends ApplicationEvent {
private String msg;
public MessageEvent(Object source, String msg) {
super(source);
this.msg = msg;
}
public String getMessage() {
return msg;
}
}
这段代码非常简单;唯一值得注意的一点是,ApplicationEvent有一个单一的构造函数,它接受对事件源的引用。这反映在MessageEvent的构造函数中。在这里,您可以看到监听器的代码:
package com.apress.prospring5.ch4;
import org.springframework.context.ApplicationListener;
public class MessageEventListener
implements ApplicationListener<MessageEvent> {
@Override
public void onApplicationEvent(MessageEvent event) {
MessageEvent msgEvt = (MessageEvent) event;
System.out.println("Received: " + msgEvt.getMessage());
}
}
ApplicationListener接口定义了一个方法onApplicationEvent,当事件发生时,Spring 会调用这个方法。通过实现强类型的ApplicationListener接口,MessageEventListener只对类型MessageEvent(或其子类)的事件感兴趣。如果接收到MessageEvent,它将消息写入stdout。发布事件很简单;这只是创建一个事件类的实例并将其传递给ApplicationEventPublisher.publishEvent()方法,如下所示:
package com.apress.prospring5.ch4
;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Publisher implements ApplicationContextAware {
private ApplicationContext ctx;
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
this.ctx = applicationContext;
}
public void publish(String message) {
ctx.publishEvent(new MessageEvent(this, message));
}
public static void main(String... args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext(
"classpath:spring/app-context-xml.xml");
Publisher pub = (Publisher) ctx.getBean("publisher");
pub.publish("I send an SOS to the world... ");
pub.publish("... I hope that someone gets my...");
pub.publish("... Message in a bottle");
}
}
这里您可以看到,Publisher类从ApplicationContext中检索自己的一个实例,然后使用publish()方法,向ApplicationContext发布两个MessageEvent实例。Publisher bean 实例通过实现ApplicationContextAware来访问ApplicationContext实例。以下是本例的配置(app-context-xml.xml):
<beans ...>
<bean id="publisher"
class="com.apress.prospring5.ch4.Publisher"/>
<bean id="messageEventListener"
class="com.apress.prospring5.ch4.MessageEventListener"/>
</beans>
注意你不需要特殊的配置来注册MessageEventListener到ApplicationContext;它被 Spring 自动拾起。运行此示例会产生以下输出:
Received: I send an SOS to the world...
Received: ... I hope that someone gets my...
Received: ... Message in a bottle
事件使用的注意事项
在应用的许多情况下,某些组件需要被通知某些事件。通常,您可以通过编写代码来显式通知每个组件,或者通过使用 JMS 之类的消息传递技术来实现这一点。编写代码依次通知每个组件的缺点是,您将这些组件耦合到了发布者,在许多情况下这是不必要的。
考虑这样一种情况,您在应用中缓存产品详细信息,以避免访问数据库。另一个组件允许修改产品细节并保存到数据库中。为了避免使缓存无效,更新组件显式地通知缓存用户详细信息已经改变。在这个例子中,更新组件被耦合到一个实际上与其业务职责无关的组件。更好的解决方案是让更新组件在每次修改产品细节时发布一个事件,然后让感兴趣的组件(如缓存)监听该事件。这样做的好处是保持组件的解耦性,这使得在需要的时候移除缓存或者添加另一个监听器变得很简单,这个监听器有兴趣知道产品细节的变化。
在这种情况下使用 JMS 可能有些矫枉过正,因为使产品在缓存中的条目无效的过程很快,并且不是业务关键的。Spring 事件基础设施的使用给应用增加了很少的开销。
通常,我们将事件用于快速执行的反应性逻辑,而不是主应用逻辑的一部分。在前面的例子中,产品在缓存中的失效发生在对产品细节更新的反应中,它执行得很快(或者应该执行),并且它不是应用主要功能的一部分。对于长期运行并构成主要业务逻辑一部分的流程,建议使用 JMS 或类似的消息传递系统,如 RabbitMQ。使用 JMS 的主要好处是它更适合长时间运行的流程,并且随着系统的增长,如果有必要,您可以将 JMS 驱动的包含业务信息的消息处理放在单独的机器上。
访问资源
通常,应用需要以不同的形式访问各种资源。您可能需要访问存储在文件系统的某个文件中的一些配置数据、存储在类路径上的 JAR 文件中的一些图像数据,或者其他地方的服务器上的一些数据。Spring 提供了一种以独立于协议的方式访问资源的统一机制。这意味着您的应用可以以相同的方式访问文件资源,无论它是存储在文件系统中、类路径中还是远程服务器上。
Spring 资源支持的核心是org.springframework.core.io.Resource接口。Resource接口定义了十种自解释方法:contentLength()、exists()、getDescription()、getFile()、getFileName()、getURI()、getURL()、isOpen()、isReadable()、lastModified()。除了这十个方法之外,还有一个不那么自明的:createRelative()。createRelative()方法通过使用一个相对于调用它的实例的路径来创建一个新的Resource实例。您可以提供自己的Resource实现,尽管这超出了本章的范围,但是在大多数情况下,您使用一个内置实现来访问文件(FileSystemResource类)、类路径(ClassPathResource类)或 URL 资源(UrlResource类)。在内部,Spring 使用另一个接口ResourceLoader和默认实现DefaultResourceLoader来定位和创建Resource实例。然而,你通常不会与DefaultResourceLoader交互,而是使用另一个ResourceLoader实现,叫做ApplicationContext。下面是一个示例应用,它使用ApplicationContext访问三个资源:
package com.apress.prospring5.ch4;
import java.io.File;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.core.io.Resource;
public class ResourceDemo {
public static void main(String... args) throws Exception{
ApplicationContext ctx = new ClassPathXmlApplicationContext();
File file = File.createTempFile("test", "txt");
file.deleteOnExit();
Resource res1 = ctx.getResource("file://" + file.getPath());
displayInfo(res1);
Resource res2 = ctx.getResource("classpath:test.txt");
displayInfo(res2);
Resource res3 = ctx.getResource("http://www.google.com");
displayInfo(res3);
}
private static void displayInfo(Resource res) throws Exception{
System.out.println(res.getClass());
System.out.println(res.getURL().getContent());
System.out.println("");
}
}
注意,在对getResource()的每次调用中,我们为每个资源传入一个 URI。你会认出我们为res1和res3传递的通用file:和http:协议。我们为res2使用的classpath:协议是特定于 Spring 的,它指示ResourceLoader应该在类路径中查找资源。运行此示例会产生以下输出:
class org.springframework.core.io.UrlResource
java.io.BufferedInputStream@3567135c
class org.springframework.core.io.ClassPathResource
sun.net.www.content.text.PlainTextInputStream@90f6bfd
class org.springframework.core.io.UrlResource
sun.net.www.protocol.http.HttpURLConnection$HttpInputStream@735b5592
注意,对于file:和http:协议,Spring 都返回了一个UrlResource实例。Spring 确实包含了一个FileSystemResource类,但是DefaultResourceLoader根本没有使用这个类。这是因为 Spring 的默认资源加载策略将 URL 和文件视为具有不同协议(file:和http:)的相同类型的资源。如果需要FileSystemResource的实例,使用FileSystemResourceLoader。一旦获得了一个Resource实例,您就可以使用getFile()、getInputStream()或getURL()随意访问内容。在某些情况下,例如当您使用http:协议时,对getFile()的调用会导致FileNotFoundException。因此,我们建议您使用getInputStream()来访问资源内容,因为它可能适用于所有可能的资源类型。
使用 Java 类进行配置
除了 XML 和属性文件配置,您还可以使用 Java 类来配置 Spring 的ApplicationContext。到目前为止,到处都介绍了代码示例,以使您熟悉注释风格的配置。Spring JavaConfig 曾经是一个独立的项目,但是从 Spring 3.0 开始,它使用 Java 类进行配置的主要特性被合并到了核心的 Spring 框架中。在这一节中,我们将展示在使用 XML 配置时,如何使用 Java 类来配置 Spring 的ApplicationContext和它的等价物。
Java 中的应用上下文配置
我们来看看 Spring 的ApplicationContext如何使用 Java 类进行配置;我们将引用在第二章和第三章中给出的消息提供者和呈现者的相同例子。下面的代码概括了消息提供者接口和一个可配置的消息提供者: 2
//chapter02/hello-world/src/main/java/com/apress/prospring5/
// ch2/decoupled/MessageProvider.java
package com.apress.prospring5.ch2.decoupled;
public interface MessageProvider {
String getMessage();
}
//chapter03/constructor-injection/src/main/java/com/apress/prospring5/
// ch3/xml/ConfigurableMessageProvider.java
package com.apress.prospring5.ch3.xml;
import com.apress.prospring5.ch2.decoupled.MessageProvider;
public class ConfigurableMessageProvider implements MessageProvider {
private String message = "Default message";
public ConfigurableMessageProvider() {
}
public ConfigurableMessageProvider(String message) {
this.message = message;
}
public void setMessage(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
下面的代码片段显示了MessageRenderer接口和StandardOutMessageRenderer实现:
//chapter02/hello-world/src/main/java/com/apress/prospring5/
// ch2/decoupled/MessageRenderer.java
package com.apress.prospring5.ch2.decoupled;
public interface MessageRenderer {
void render();
void setMessageProvider(MessageProvider provider);
MessageProvider getMessageProvider();
}
//chapter02/hello-world/src/main/java/com/apress/prospring5/
// ch2/decoupled/StandardOutMessageRenderer.java
package com.apress.prospring5.ch2.decoupled;
public class StandardOutMessageRenderer
implements MessageRenderer {
private MessageProvider messageProvider;
public StandardOutMessageRenderer(){
System.out.println(" -->
StandardOutMessageRenderer: constructor called");
}
@Override
public void render() {
if (messageProvider == null) {
throw new RuntimeException(
"You must set the property messageProvider of class:"
+ StandardOutMessageRenderer.class.getName());
} System.out.println(messageProvider.getMessage());
}
@Override
public void setMessageProvider(MessageProvider provider) {
System.out.println(" -->
StandardOutMessageRenderer: setting the provider");
this.messageProvider = provider;
}
@Override
public MessageProvider getMessageProvider() {
return this.messageProvider;
}
}
下面的配置片段描述了 XML 配置(app-context-xml.xml):
<beans ...>
<bean id="messageRenderer"
class="com.apress.prospring5.ch2.decoupled.StandardOutMessageRenderer"
p:messageProvider-ref="messageProvider"/>
<bean id="messageProvider"
class="com.apress.prospring5.ch3.xml.ConfigurableMessageProvider"
c:message="This is a configurable message"/>
</beans>
测试这个的类看起来也很熟悉,如下所示:
package com.apress.prospring5.ch4;
import com.apress.prospring5.ch2.decoupled.MessageRenderer;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class JavaConfigXMLExample {
public static void main(String... args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("
classpath:spring/app-context-xml.xml");
MessageRenderer renderer =
ctx.getBean("messageRenderer", MessageRenderer.class);
renderer.render();
}
}
运行该程序会产生以下输出:
--> StandardOutMessageRenderer: constructor called
--> StandardOutMessageRenderer: setting the provider
This is a configurable message
为了去掉 XML 配置,app-context-xml.xml文件必须由一个特殊的类代替,这个特殊的类称为配置类,它将用@Configuration进行注释。@Configuration注释用于通知 Spring 这是一个基于 Java 的配置文件。这个类将包含用代表 bean 声明的@Bean定义注释的方法。@Bean注释用于声明一个 Spring bean 和 DI 需求。@Bean标注相当于<bean>标签,方法名相当于<bean>标签内的id属性,实例化MessageRender bean 时,通过调用相应的方法获取消息提供者来实现 setter 注入,这与在 XML 配置中使用<ref>属性是一样的。这些注释和这种类型的配置在前面的章节中已经介绍过了,目的是让您熟悉它们,但是直到现在才详细介绍它们。下面的代码片段描述了与前面介绍的 XML 配置等效的AppConfig内容:
package com.apress.prospring5.ch4;
import com.apress.prospring5.ch2.decoupled.MessageProvider;
import com.apress.prospring5.ch2.decoupled.MessageRenderer;
import com.apress.prospring5.ch2.decoupled.StandardOutMessageRenderer;
import com.apress.prospring5.ch3.xml.ConfigurableMessageProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean
public MessageProvider messageProvider() {
return new ConfigurableMessageProvider();
}
@Bean
public MessageRenderer messageRenderer() {
MessageRenderer renderer = new StandardOutMessageRenderer();
renderer.setMessageProvider(messageProvider());
return renderer;
}
}
下面的代码片段显示了如何从 Java 配置文件初始化ApplicationContext实例:
package com.apress.prospring5.ch4;
import com.apress.prospring5.ch2.decoupled.MessageRenderer;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class JavaConfigExampleOne {
public static void main(String... args) {
ApplicationContext ctx = new
AnnotationConfigApplicationContext(AppConfig.class);
MessageRenderer renderer =
ctx.getBean("messageRenderer", MessageRenderer.class);
renderer.render();
}
}
在前面的清单中,我们使用了AnnotationConfigApplicationContext类,将配置类作为构造函数参数传入(您可以通过 JDK varargs 特性将多个配置类传递给它)。之后,您可以照常使用返回的ApplicationContext。有时出于测试目的,可以将配置类声明为静态内部类,如下所示:
package com.apress.prospring5.ch4;
import com.apress.prospring5.ch2.decoupled.MessageProvider;
import com.apress.prospring5.ch2.decoupled.MessageRenderer;
import com.apress.prospring5.ch2.decoupled.StandardOutMessageRenderer;
import com.apress.prospring5.ch3.xml.ConfigurableMessageProvider;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
public class JavaConfigSimpleExample {
@Configuration
static class AppConfigOne {
@Bean
public MessageProvider messageProvider() {
return new ConfigurableMessageProvider();
}
@Bean
public MessageRenderer messageRenderer() {
MessageRenderer renderer = new StandardOutMessageRenderer();
renderer.setMessageProvider(messageProvider());
return renderer;
}
}
public static void main(String... args) {
ApplicationContext ctx = new
AnnotationConfigApplicationContext(AppConfig.class);
MessageRenderer renderer =
ctx.getBean("messageRenderer", MessageRenderer.class);
renderer.render();
}
}
返回的ApplicationContext实例可以照常使用,输出将与 XML 配置的应用的情况相同。
--> StandardOutMessageRenderer: constructor called
--> StandardOutMessageRenderer: setting the provider
Default message
已经了解了 Java 配置类的基本用法,让我们继续了解更多的配置选项。对于消息提供者,假设我们想要将消息外部化到一个属性文件(message.properties)中,然后通过使用构造函数注入将其注入到ConfigurableMessageProvider中。message.properties的内容如下:
message=Only hope can keep me together
让我们看看修改后的测试程序,它通过使用@PropertySource注释加载属性文件,然后将它们注入到消息提供者实现中。
package com.apress.prospring5.ch4;
import com.apress.prospring5.ch2.decoupled.MessageProvider;
import com.apress.prospring5.ch2.decoupled.MessageRenderer;
import com.apress.prospring5.ch2.decoupled.StandardOutMessageRenderer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
@Configuration
@PropertySource(value = "classpath:message.properties")
public class AppConfigOne {
@Autowired
Environment env;
@Bean
public MessageProvider messageProvider() {
return new ConfigurableMessageProvider(env.getProperty("message"));
}
@Bean(name = "messageRenderer")
public MessageRenderer messageRenderer() {
MessageRenderer renderer = new StandardOutMessageRenderer();
renderer.setMessageProvider(messageProvider());
return renderer;
}
}
在第三章中介绍了配置类,以显示 XML 元素和属性的等价物。可以用与 Bean 作用域、加载类型和依赖关系相关的其他注释来注释 bean 声明。在下面的代码片段中,AppConfigOne配置类增加了 bean 声明的注释:
package com.apress.prospring5.ch4;
import com.apress.prospring5.ch2.decoupled.MessageProvider;
import com.apress.prospring5.ch2.decoupled.MessageRenderer;
import com.apress.prospring5.ch2.decoupled.StandardOutMessageRenderer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.*;
import org.springframework.core.env.Environment;
@Configuration
@PropertySource(value = "classpath:message.properties")
public class AppConfig {
@Autowired
Environment env;
@Bean
@Lazy
public MessageProvider messageProvider() {
return new ConfigurableMessageProvider(env.getProperty("message"));
}
@Bean(name = "messageRenderer")
@Scope(value="prototype")
@DependsOn(value="messageProvider")
public MessageRenderer messageRenderer() {
MessageRenderer renderer = new StandardOutMessageRenderer();
renderer.setMessageProvider(messageProvider());
return renderer;
}
}
在之前的代码示例中,引入了一些注释,如表 4-3 中所述。通过启用组件扫描并在需要的地方自动连接,使用类似@ Component、@Service等原型注释定义的 Beans 可以在 Java 配置类中使用。在下面的例子中,我们将ConfigurableMessageProvider声明为服务 bean。
表 4-3。
Java Configuration Annotations Table
| 注释 | 描述 | | --- | --- | | `@PropertySource` | 该注释用于将属性文件加载到 Spring 的`ApplicationContext`中,它接受位置作为参数(可以提供多个位置)。对于 XML 来说,`package com.apress.prospring5.ch4.annotated;
import com.apress.prospring5.ch2.decoupled.MessageProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@Service("provider")
public class ConfigurableMessageProvider implements MessageProvider {
private String message;
public ConfigurableMessageProvider(
@Value("Love on the weekend")String message) {
this.message = message;
}
@Override
public String getMessage() {
return this.message;
}
}
这里您可以看到配置类
package com.apress.prospring5.ch4;
import com.apress.prospring5.ch2.decoupled.MessageProvider;
import com.apress.prospring5.ch2.decoupled.MessageRenderer;
import com.apress.prospring5.ch2.decoupled.StandardOutMessageRenderer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.*;
@Configuration
@ComponentScan(basePackages={"com.apress.prospring5.ch4.annotated"})
public class AppConfigTwo {
@Autowired
MessageProvider provider;
@Bean(name = "messageRenderer")
public MessageRenderer messageRenderer() {
MessageRenderer renderer =
new StandardOutMessageRenderer();
renderer.setMessageProvider(provider);
return renderer;
}
}
@ComponentScan定义了 Spring 应该扫描哪些包来查找 bean 定义的注释。它与 XML 配置中的<context:component-scan>标签相同。执行以下示例中的代码:
package com.apress.prospring5.ch4;
import com.apress.prospring5.ch2.decoupled.MessageRenderer;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class JavaConfigExampleTwo {
public static void main(String... args) {
ApplicationContext ctx = new
AnnotationConfigApplicationContext(AppConfigTwo.class);
MessageRenderer renderer =
ctx.getBean("messageRenderer", MessageRenderer.class);
renderer.render();
}
}
您将获得以下结果:
--> StandardOutMessageRenderer: constructor called
--> StandardOutMessageRenderer: setting the provider
Love on the weekend
一个应用还可以有多个配置类,这些配置类可以用于解耦配置和按用途组织 bean(例如,一个类可以专用于 DAO beans 声明,一个用于服务 bean 声明,等等)。让我们使用另一个名为AppConfigFour的配置类来定义provider bean。通过导入该类定义的 bean,可以从另一个配置类访问该 bean。这是通过用@Import注释目标配置类AppConfigThree来实现的。
//AppConfigFour.java
package com.apress.prospring5.ch4.multiple;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan(basePackages={"com.apress.prospring5.ch4.annotated"})
public class AppConfigFour { }
package com.apress.prospring5.ch4.multiple;
import com.apress.prospring5.ch2.decoupled.MessageProvider;
import com.apress.prospring5.ch2.decoupled.MessageRenderer;
import com.apress.prospring5.ch2.decoupled.StandardOutMessageRenderer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import(AppConfigFour.class)
public class AppConfigThree {
@Autowired
MessageProvider provider;
@Bean(name = "messageRenderer")
public MessageRenderer messageRenderer() {
MessageRenderer renderer = new StandardOutMessageRenderer();
renderer.setMessageProvider(provider);
return renderer;
}
}
如果在类JavaConfigExampleTwo的 main 方法中,您用AppConfigThree替换了类AppConfigTwo,那么当运行该示例时,会打印出相同的输出。
Spring 混合配置
但是 Spring 能做的远不止这些。Spring 允许混合 XML 和 Java 配置类。当应用带有由于某种原因不能更改的遗留代码时,这是很有用的。要从 XML 文件导入 bean 声明,可以使用@ImportResource注释。在下面的配置片段中,您可以看到在名为app-context-xml-01.xml的 XML 文件中声明的provider bean:
<beans ...>
<bean id="provider"
class="com.apress.prospring5.ch4.ConfigurableMessageProvider"
p:message="Love on the weekend" />
</beans>
下一个代码示例描述了导入 XML 文件中声明的 beans 的类AppConfigFive。如果在classJavaConfigExampleTwo的 main 方法中,我们用AppConfigFive替换了类AppConfigTwo,那么当这个例子运行时,同样的输出被打印出来。
package com.apress.prospring5.ch4.mixed;
import com.apress.prospring5.ch2.decoupled.MessageProvider;
import com.apress.prospring5.ch2.decoupled.MessageRenderer;
import com.apress.prospring5.ch2.decoupled.StandardOutMessageRenderer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
@Configuration
@ImportResource(value="classpath:spring/app-context-xml-01.xml")
public class AppConfigFive {
@Autowired
MessageProvider provider;
@Bean(name = "messageRenderer")
public MessageRenderer messageRenderer() {
MessageRenderer renderer = new StandardOutMessageRenderer();
renderer.setMessageProvider(provider);
return renderer;
}
}
同样,反过来也可以:在 Java 配置类中定义的 beans 可以导入到 XML 配置文件中。在下一个例子中,messageRenderer bean 是在 XML 文件中定义的,它的依赖项,provider bean 是在配置类AppConfigSix中定义的。XML 配置文件app-context-xml-02.xml的内容描述如下:
<beans ...>
<context:annotation-config/>
<bean class="com.apress.prospring5.ch4.mixed.AppConfigSix"/>
<bean id="messageRenderer"
class="com.apress.prospring5.ch2.decoupled.StandardOutMessageRenderer"
p:messageProvider-ref="provider"/>
</beans>
必须声明配置类类型的 bean,并且必须使用<context:annotation-config/>启用对带注释方法的支持。这使得类中声明的 bean 可以配置为 XML 文件中声明的 bean 的依赖项。配置类AppConfigSix非常简单。
package com.apress.prospring5.ch4.mixed
;
import com.apress.prospring5.ch2.decoupled.MessageProvider;
import com.apress.prospring5.ch4.annotated.ConfigurableMessageProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfigSix {
@Bean
public MessageProvider provider() {
return new ConfigurableMessageProvider("Love on the weekend");
}
}
创建一个ApplicationContext实例是使用ClassPathXmlApplicationContext来完成的,到目前为止它已经被大量使用。
package com.apress.prospring5.ch4;
import com.apress.prospring5.ch2.decoupled.MessageRenderer;
import com.apress.prospring5.ch4.mixed.AppConfigFive;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class JavaConfigExampleThree {
public static void main(String... args) {
ApplicationContext ctx =
new ClassPathXmlApplicationContext
("classpath:spring/app-context-xml-02.xml");
MessageRenderer renderer =
ctx.getBean("messageRenderer", MessageRenderer.class);
renderer.render();
}
}
运行前面的代码会产生与前面描述的相同的输出。
应用基础设施服务也可以在 Java 配置类中定义。例如,@EnableTransactionManagement定义我们将使用 Spring 的事务管理特性,这将在第九章中进一步讨论,而@EnableWebSecurity和@EnableGlobalMethodSecurity用于启用 Spring Security 上下文,这将在第十六章中详细讨论。
Java 还是 XML 配置?
正如您已经看到的,使用 Java 类可以实现与 XML 相同级别的ApplicationContext配置。那么,你应该用哪一个呢?这种考虑很像是在 DI 配置中使用 XML 还是 Java 注释。每种方法都有自己的优点和缺点。但是,建议是一样的;也就是说,当您和您的团队决定使用哪种方法时,坚持使用它并保持配置风格的持久性,而不是分散在 Java 类和 XML 文件之间。使用一种方法会使维护工作容易得多。
轮廓
Spring 提供的另一个有趣的特性是配置文件的概念。基本上,概要文件指示 Spring 只配置在指定概要文件激活时定义的ApplicationContext实例。在这一节中,我们将演示如何在一个简单的程序中使用概要文件。
使用 Spring 轮廓特征的示例
假设有一个叫FoodProviderService的服务,负责给学校提供食物,包括幼儿园和高中。FoodProviderService接口只有一个名为provideLunchSet()的方法,它为调用学校的每个学生生成午餐套餐。午餐集是一个由Food对象组成的列表,它是一个简单的类,只有一个name属性。下面的代码片段显示了Food类:
package com.apress.prospring5.ch4;
public class Food {
private String name;
public Food() {
}
public Food(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
而这里是FoodProviderService界面:
package com.apress.prospring5.ch4;
import java.util.List;
public interface FoodProviderService {
List<Food> provideLunchSet();
}
现在假设午餐套餐有两个提供者,一个是幼儿园的,一个是高中的。他们生产的午餐套餐是不一样的,虽然他们提供的服务是一样的,就是给学生提供午餐。所以,现在让我们创建FoodProviderService的两个实现,使用相同的名称,但是将它们放入不同的包中以标识它们的目标学校。这里显示了两个类:
//chapter04/profiles/src/main/java/com/apress/prospring5/ch4/
highschool/FoodProviderServiceImpl.java
package com.apress.prospring5.ch4.highschool;
import java.util.ArrayList;
import java.util.List;
import com.apress.prospring5.ch4.Food;
import com.apress.prospring5.ch4.FoodProviderService;
public class FoodProviderServiceImpl implements FoodProviderService {
@Override
public List<Food> provideLunchSet() {
List<Food> lunchSet = new ArrayList<>();
lunchSet.add(new Food("Coke"));
lunchSet.add(new Food("Hamburger"));
lunchSet.add(new Food("French Fries"));
return lunchSet;
}
}
//chapter04/profiles/src/main/java/com/apress/prospring5/ch4/
kindergarten/FoodProviderServiceImpl.java
package com.apress.prospring5.ch4.kindergarten;
import java.util.ArrayList;
import java.util.List;
import com.apress.prospring5.ch4.Food;
import com.apress.prospring5.ch4.FoodProviderService;
public class FoodProviderServiceImpl implements FoodProviderService {
@Override
public List<Food> provideLunchSet() {
List<Food> lunchSet = new ArrayList<>();
lunchSet.add(new Food("Milk"));
lunchSet.add(new Food("Biscuits"));
return lunchSet;
}
}
从前面的清单中,您可以看到这两个实现提供了相同的FoodProviderService接口,但是在午餐集中产生了不同的食物组合。因此,现在假设一所幼儿园希望供应商为他们的学生提供午餐套餐;让我们看看如何使用 Spring 的概要文件配置来实现这一点。我们将首先浏览 XML 配置。我们将创建两个 XML 配置文件,一个用于幼儿园配置文件,另一个用于高中配置文件。下面的配置片段描述了两个概要文件配置:
<!-- highschool-config.xml -->
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd"
profile="highschool">
<bean id="foodProviderService"
class="com.apress.prospring5.ch4.highschool.FoodProviderServiceImpl"/>
</beans>
<!-- kindergarten-config.xml -->
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd"
profile="kindergarten">
<bean id="foodProviderService"
class="com.apress.prospring5.ch4.kindergarten.FoodProviderServiceImpl"/>
</beans>
在前两个配置中,注意在<beans>标签中分别使用了profile="kindergarten"和profile="highschool"。它实际上告诉 Spring,只有当指定的概要文件处于活动状态时,文件中的那些 beans 才应该被实例化。现在让我们看看在独立应用中使用 Spring 的ApplicationContext时如何激活正确的配置文件。以下代码片段显示了测试程序:
package com.apress.prospring5.ch4;
import java.util.List;
import org.springframework.context.support.GenericXmlApplicationContext;
public class ProfileXmlConfigExample {
public static void main(String... args) {
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext();
ctx.load("classpath:spring/*-config.xml");
ctx.refresh();
FoodProviderService foodProviderService =
ctx.getBean("foodProviderService", FoodProviderService.class);
List<Food> lunchSet = foodProviderService.provideLunchSet();
for (Food food: lunchSet) {
System.out.println("Food: " + food.getName());
}
ctx.close();
}
}
ctx.load()方法将加载kindergarten-config.xml和highschool-config.xml,因为我们将通配符作为前缀传递给该方法。在这个例子中,只有文件kindergarten-config.xml中的 beans 会根据profile属性被 Spring 实例化,这个属性是通过传递 JVM 参数-Dspring.profiles.active="kindergarten"激活的。使用这个 JVM 参数运行程序会产生以下输出:
Food: Milk
Food: Biscuits
这正是幼儿园提供者的实现将为午餐集产生的内容。现在,将前面清单中的配置文件参数更改为 high school ( -Dspring.profiles.active="highschool"),输出将更改如下:
Food: Coke
Food: Hamburger
Food: French Fries
您还可以通过调用ctx.getEnvironment().setActiveProfiles("kindergarten")以编程方式设置要在代码中使用的配置文件。此外,您可以通过向您的类添加@Profile注释,使用 Java Config 注册由概要文件启用的类。
使用 Java 配置的 Spring 概要文件
当然,有一种方法可以使用 Java configuration 来配置 Spring profiles,因为不喜欢 XML 配置的开发人员也应该感到满意。前一节中声明的 XML 文件必须替换为等效的 Java 配置类。下面是kindergarten概要文件的配置类:
package com.apress.prospring5.ch4.config;
import com.apress.prospring5.ch4.FoodProviderService;
import com.apress.prospring5.ch4.kindergarten.FoodProviderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
@Configuration
@Profile("kindergarten")
public class KindergartenConfig {
@Bean
public FoodProviderService foodProviderService(){
return new FoodProviderServiceImpl();
}
}
如您所见,foodProviderService bean 是使用@Bean注释定义的。使用@Profile注释将该类标记为特定于kindergarten概要文件。显然,除了 bean 类型、配置文件名和类名之外,特定于highschool配置文件的类是相同的。
package com.apress.prospring5.ch4.config;
import com.apress.prospring5.ch4.FoodProviderService;
import com.apress.prospring5.ch4.highschool.FoodProviderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
@Configuration
@Profile("highschool")
public class HighschoolConfig {
@Bean
public FoodProviderService foodProviderService(){
return new FoodProviderServiceImpl();
}
}
这些类的使用方式与 XML 文件相同。一个上下文声明使用它们两个,实际上只有其中一个被用来创建ApplicationContext实例,这取决于-Dspring.profiles.active JVM 选项的值。
package com.apress.prospring5.ch4.config;
import com.apress.prospring5.ch4.Food;
import com.apress.prospring5.ch4.FoodProviderService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import java.util.List;
public class ProfileJavaConfigExample {
public static void main(String... args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(
KindergartenConfig.class,
HighschoolConfig.class);
FoodProviderService foodProviderService =
ctx.getBean("foodProviderService",
FoodProviderService.class);
List<Food> lunchSet = foodProviderService.provideLunchSet();
for (Food food : lunchSet) {
System.out.println("Food: " + food.getName());
}
ctx.close();
}
}
通过使用kindergarten作为 JVM 选项-Dspring.profiles.active的值运行前面的示例,可以打印出预期的输出。
Food: Milk
Food: Biscuits
还有一个用于配置已用概要文件的注释,它取代了-Dspring.profiles.active JVM 选项,但是这只能用于测试类。由于测试 Spring 应用已经在第十三章中介绍过了,这里就不详细介绍了。但是包含了一些示例代码。
package com.apress.prospring5.ch4.config;
import com.apress.prospring5.ch4.FoodProviderService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes={KindergartenConfig.class,
HighschoolConfig.class})
@ActiveProfiles("kindergarten")
public class ProfilesJavaConfigTest {
@Autowired FoodProviderService foodProviderService;
@Test
public void testProvider(){
assertTrue(foodProviderService.provideLunchSet() != null);
assertFalse(foodProviderService.provideLunchSet().isEmpty());
assertEquals(2, foodProviderService.provideLunchSet().size());
}
}
正如您自己可能已经发现的,指定使用哪个概要文件来运行这个测试的注释是@ActiveProfiles("kindergarten")。在复杂的应用中,通常会有不止一个概要文件,而且更多的概要文件可以用来组成测试的上下文配置。这个类可以在任何 Java 智能编辑器中运行,并且在执行gradle clean build时自动运行。
使用配置文件的注意事项
Spring 中的 profiles 特性为开发人员创建了另一种方式来管理应用的运行配置,这在以前是在构建工具中完成的(例如,Maven 的 profile 支持)。构建工具依靠传递到工具中的参数将正确的配置/属性文件打包到 Java 归档文件(JAR 或 WAR,取决于应用类型)中,然后部署到目标环境中。Spring 的 profile 特性允许我们作为应用开发人员自己定义配置文件,并通过编程或传入 JVM 参数来激活它们。通过使用 Spring 的概要文件支持,您现在可以使用相同的应用归档文件,并通过在 JVM 启动期间将正确的概要文件作为参数传入来部署到所有环境中。例如,您可以拥有不同概要文件的应用,比如(dev, hibernate)、(prd, jdbc)等等,每种组合代表运行环境(开发或生产)和要使用的数据访问库(Hibernate 或 JDBC)。它将应用概要管理引入到编程中。
但是这种方法也有缺点。例如,有些人可能认为,如果不小心处理,将不同环境的所有配置放入应用配置文件或 Java 类并将它们捆绑在一起将容易出错(例如,管理员可能忘记在应用服务器环境中设置正确的 JVM 参数)。将所有概要文件打包在一起也会使包比平常大一点。同样,让应用和配置需求驱动您选择最适合您的项目的方法。
环境和属性资源抽象
要设置激活的配置文件,我们需要访问Environment界面。Environment接口是一个抽象层,用于封装正在运行的 Spring 应用的环境。
除了概要文件之外,Environment接口封装的其他关键信息是属性。属性用于存储应用的基础环境配置,如应用文件夹的位置、数据库连接信息等。
Spring 中的Environment和PropertySource抽象特性帮助开发人员从运行平台访问各种配置信息。在抽象下,所有系统属性、环境变量和应用属性都由Environment接口提供服务,Spring 在引导ApplicationContext时会填充这个接口。以下代码片段显示了一个简单的示例:
package com.apress.prospring5.ch4;
import java.util.HashMap;
import java.util.Map;
import org.springframework.context.support.GenericXmlApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;
public class EnvironmentSample {
public static void main(String... args) {
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext();
ctx.refresh();
ConfigurableEnvironment env = ctx.getEnvironment();
MutablePropertySources propertySources = env.getPropertySources();
Map<String,Object> appMap = new HashMap<>();
appMap.put("user.home", "application_home");
propertySources.addLast(new MapPropertySource("prospring5_MAP", appMap));
System.out.println("user.home: " + System.getProperty("user.home"));
System.out.println("JAVA_HOME: " + System.getenv("JAVA_HOME"));
System.out.println("user.home: " + env.getProperty("user.home"));
System.out.println("JAVA_HOME: " + env.getProperty("JAVA_HOME"));
ctx.close();
}
}
在前面的代码片段中,在ApplicationContext初始化之后,我们获得了对ConfigurableEnvironment接口的引用。通过该接口,可以获得一个对MutablePropertySources(PropertySources接口的默认实现,它允许对包含的属性源进行操作)的句柄。之后,我们构造一个映射,将应用属性放入映射中,然后用映射构造一个MapPropertySource类(一个从Map实例中读取键和值的PropertySource子类)。最后,通过addLast()方法将MapPropertySource类添加到MutablePropertySources中。运行程序,打印出以下内容:
user.home: /home/jules
JAVA_HOME: /home/jules/bin/java
user.home: /home/jules
JAVA_HOME: /home/jules/bin/java
application.home: application_home
对于前两行,检索 JVM 系统属性user.home和环境变量JAVA_HOME,和以前一样(通过使用 JVM 的System类)。然而,对于最后三行,您可以看到所有的系统属性、环境变量和应用属性都可以通过Environment接口访问。您可以看到Environment抽象如何帮助我们管理和访问应用运行环境中的各种属性。
对于PropertySource抽象,Spring 将按照以下默认顺序访问属性:
- 正在运行的 JVM 的系统属性
- 环境变量
- 应用定义的属性
例如,假设我们定义了同一个应用属性user.home,并通过MutablePropertySources类将其添加到Environment接口。如果您运行该程序,您仍然会看到user.home是从 JVM 属性中检索的,而不是您的。然而,Spring 允许您控制Environment检索属性的顺序。以下代码片段显示了修订后的版本:
package com.apress.prospring5.ch4;
import java.util.HashMap;
import java.util.Map;
import org.springframework.context.support.GenericXmlApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;
public class EnvironmentSample {
public static void main(String... args) {
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext();
ctx.refresh();
ConfigurableEnvironment env = ctx.getEnvironment();
MutablePropertySources propertySources = env.getPropertySources();
Map<String,Object> appMap = new HashMap<>();
appMap.put("application.home", "application_home");
propertySources.addFirst(new MapPropertySource("prospring5_MAP", appMap));
System.out.println("user.home: " + System.getProperty("user.home"));
System.out.println("JAVA_HOME: " + System.getenv("JAVA_HOME"));
System.out.println("user.home: " + env.getProperty("user.home"));
System.out.println("JAVA_HOME: " + env.getProperty("JAVA_HOME"));
ctx.close();
}
}
在前面的代码示例中,我们定义了一个应用属性,也称为user.home,并通过MutablePropertySources类的addFirst()方法将其添加为第一个要搜索的属性。当您运行该程序时,您将看到以下输出:
user.home: /home/jules
JAVA_HOME: /home/jules/bin/java
user.home: application_home
JAVA_HOME: /home/jules/bin/java
前两行保持不变,因为我们仍然使用 JVM System类的getProperty()和getenv()方法来检索它们。然而,当使用Environment接口时,您会看到我们定义的user.home属性优先,因为我们将其定义为第一个搜索属性值的属性。
在现实生活中,您很少需要直接与Environment接口交互,而是会使用一个以${}形式的属性占位符(例如${application.home})并将解析后的值注入到 Spring beans 中。让我们来看看实际情况。假设我们有一个类来存储从属性文件加载的所有应用属性。下面显示了AppProperty类:
package com.apress.prospring5.ch4;
public class AppProperty {
private String applicationHome;
private String userHome;
public String getApplicationHome() {
return applicationHome;
}
public void setApplicationHome(String applicationHome) {
this.applicationHome = applicationHome;
}
public String getUserHome() {
return userHome;
}
public void setUserHome(String userHome) {
this.userHome = userHome;
}
}
在这里你可以看到application.properties文件的内容:
application.home=application_home
user.home=/home/jules-new
注意,属性文件还声明了user.home property。让我们来看看 Spring XML 配置;参见下面的代码(app-context-xml.xml):
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:property-placeholder
location="classpath:application.properties"/>
<bean id="appProperty" class="com.apress.prospring5.ch4.AppProperty"
p:applicationHome="${application.home}"
p:userHome="${user.home}"/>
</beans>
我们使用<context:property-placeholder>标签将属性加载到 Spring 的Environment中,后者被包装到ApplicationContext接口中。我们还使用 SpEL 占位符将值注入到AppProperty bean 中。以下代码片段显示了测试程序:
package com.apress.prospring5.ch4;
import org.springframework.context.support.GenericXmlApplicationContext;
public class PlaceHolderDemo {
public static void main(String... args) {
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-xml.xml");
ctx.refresh();
AppProperty appProperty = ctx.getBean("appProperty",
AppProperty.class);
System.out.println("application.home: " +
appProperty.getApplicationHome());
System.out.println("user.home: " +
appProperty.getUserHome());
ctx.close();
}
}
让我们运行程序,您将看到以下输出:
application.home: application_home
user.home: /Users/jules
您将看到application.home占位符被正确解析,而user.home属性仍然从 JVM 属性中检索,这是正确的,因为它是PropertySource抽象的默认行为。为了指示 Spring 优先考虑application.properties文件中的值,我们将属性local-override="true"添加到<context:property-placeholder>标签中。
<context:property-placeholder local-override="true"
location="classpath:env/application.properties"/>
属性指示 Spring 用这个占位符中定义的属性覆盖现有的属性。运行该程序,您将会看到现在已经从application.properties文件中检索到了user.home属性。
application.home: application_home
user.home: /home/jules-new
使用 JSR-330 注释的配置
正如我们在第一章中所讨论的,JEE 6 提供了对 JSR-330(Java 依赖注入)的支持,这是一个注释集合,用于在 JEE 容器或其他兼容的 IoC 框架中表达应用的 DI 配置。Spring 也支持和识别这些注释,所以尽管您可能没有在 JEE 6 容器中运行您的应用,您仍然可以在 Spring 中使用 JSR-330 注释。使用 JSR-330 注释可以帮助您轻松地从 Spring 迁移到 JEE 6 容器或其他兼容的 IoC 容器(例如 Google Guice)。
同样,让我们以消息呈现器和消息提供器为例,使用 JSR-330 注释来实现它。为了支持 JSR-330 注释,您需要向项目添加javax.inject 3 依赖项。
下面的代码片段显示了MessageProvider和ConfigurableMessageProvider的实现:
//chapter04/jsr330/src/main/java/com/apress/prospring5/
ch4/MessageProvider.java
package com.apress.prospring5.ch4;
public interface MessageProvider {
String getMessage();
}
//chapter04/jsr330/src/main/java/com/apress/prospring5/
ch4/ConfigurableMessageProvider.java
package com.apress.prospring5.ch4;
import javax.inject.Inject;
import javax.inject.Named;
@Named("messageProvider")
public class ConfigurableMessageProvider
implements MessageProvider {
private String message = "Default message";
public ConfigurableMessageProvider() {
}
@Inject
@Named("message")
public ConfigurableMessageProvider(String message) {
this.message = message;
}
public void setMessage(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
你会注意到所有的注释都属于javax.inject package,这是 JSR-330 标准。这个类在两个地方使用了@Named。首先,它可以用来声明一个可注入的 bean(与 Spring 中的@Component注释或其他原型注释相同)。在清单中,@Named("messageProvider")注释指定ConfigurableMessageProvider是一个可注入的 bean,并将其命名为messageProvider,这与 Spring 的<bean>标签中的name属性相同。其次,我们通过在接受字符串值的构造函数前使用@Inject注释来使用构造函数注入。然后,我们使用@Named来指定我们想要注入被赋予名称message的值。让我们继续看一下MessageRenderer接口和StandardOutMessageRenderer实现。
//chapter04/jsr330/src/main/java/com/apress/prospring5/
ch4/MessageRenderer.java
package com.apress.prospring5.ch4;
public interface MessageRenderer {
void render();
void setMessageProvider(MessageProvider provider);
MessageProvider getMessageProvider();
}
//chapter04/jsr330/src/main/java/com/apress/prospring5/
ch4/StandardOutMessageRenderer.java
package com.apress.prospring5.ch4;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
@Named("messageRenderer")
@Singleton
public class StandardOutMessageRenderer
implements MessageRenderer {
@Inject
@Named("messageProvider")
private MessageProvider messageProvider = null;
public void render() {
if (messageProvider == null) {
throw new RuntimeException(
"You must set the property messageProvider of class:"
+ StandardOutMessageRenderer.class.getName());
}
System.out.println(messageProvider.getMessage());
}
public void setMessageProvider(MessageProvider provider) {
this.messageProvider = provider;
}
public MessageProvider getMessageProvider() {
return this.messageProvider;
}
}
在前面的代码片段中,我们使用了@Named来定义它是一个可注入的 bean。请注意@Singleton注释。值得注意的是,在 JSR-330 标准中,bean 的默认作用域是非 singleton,这类似于 Spring 的prototype作用域。因此,在 JSR-330 环境中,如果您希望您的 bean 是单例的,您需要使用@Singleton注释。然而,在 Spring 中使用这个注释实际上没有任何效果,因为 Spring 的 bean 实例化的默认范围已经是 singleton 了。我们把它放在这里只是为了演示,值得注意的是 Spring 和其他 JSR-330 兼容容器之间的区别。
对于messageProvider属性,我们这次使用@Inject进行 setter 注入,并指定名为messageProvider的 bean 应该用于注入。下面的配置片段为应用定义了一个简单的 Spring XML 配置(app-context-annotation.xml):
<beans ...>
<context:component-scan
base-package="com.apress.prospring5.ch4"/>
<bean id="message" class="java.lang.String">
<constructor-arg value="Gravity is working against me"/>
</bean>
</beans>
使用 JSR-330 不需要任何特殊标签;就像普通的 Spring 应用一样配置您的应用。我们使用<context:component-scan>来指示 Spring 扫描 DI 相关的注释,Spring 将识别这些 JSR-330 注释。我们还声明了一个名为message的 Spring bean,用于将构造函数注入到ConfigurableMessageProvider类中。以下代码片段显示了测试程序:
package com.apress.prospring5.ch4;
import org.springframework.context.support.GenericXmlApplicationContext;
public class Jsr330Demo {
public static void main(String... args) {
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-annotation.xml");
ctx.refresh();
MessageRenderer renderer = ctx.getBean("messageRenderer",
MessageRenderer.class);
renderer.render();
ctx.close();
}
}
运行该程序会产生以下输出:
Gravity is working against me
通过使用 JSR-330,您可以轻松地迁移到其他 JSR-330 兼容的 IoC 容器(例如,JEE 6 兼容的应用服务器或其他 DI 容器,如 Google Guice)。然而,Spring 的注释比 JSR-330 注释更加丰富和灵活。这里强调了一些主要差异:
- 使用 Spring 的
@Autowired批注时,可以指定一个required属性来表示必须满足 DI(也可以使用 Spring 的@Required批注来声明这个要求),但是对于 JSR-330 的@Inject批注就没有这样的等价物了。此外,Spring 提供了@Qualifier注释,允许对 Spring 进行更细粒度的控制,以基于限定符名称执行依赖关系的自动连接。 - JSR-330 只支持 singleton 和非 singleton bean 作用域,而 Spring 支持更多的作用域,这对 web 应用很有用。
- 在 Spring 中,您可以使用
@Lazy注释来指示 Spring 仅在应用请求时实例化 bean。在 JSR 没有这样的对等物-330。
您还可以在同一个应用中混合搭配 Spring 和 JSR-330 注释。但是,建议您选择其中一种来保持应用的一致风格。一种可能的方法是尽可能多地使用 JSR-330 注释,并在需要时使用 Spring 注释。然而,这给你带来的好处很少,因为在迁移到另一个 DI 容器时,你仍然需要做相当多的工作。总之,推荐使用 Spring 的注释方法,而不是 JSR-330 注释,因为 Spring 的注释更强大,除非你的应用需要独立于 IoC 容器。
使用 Groovy 进行配置
Spring Framework 4.0 的新特性是能够使用 Groovy 语言配置 bean 定义和ApplicationContext。这为开发人员提供了另一种配置选择,可以替换或补充 XML 和/或基于注释的 bean 配置。Spring ApplicationContext可以直接在 Groovy 脚本中创建,也可以从 Java 加载,都是通过GenericGroovyApplicationContext类的方式。首先,让我们通过展示如何从外部 Groovy 脚本创建 bean 定义并从 Java 加载它们来深入了解细节。在前面的章节中,我们介绍了各种 bean 类,为了提高代码的可重用性,我们将在这个例子中使用在第三章中介绍的Singer类。以下代码片段显示了该类的内容:
package com.apress.prospring5.ch3.xml;
public class Singer {
private String name;
private int age;
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public String toString() {
return "\tName: " + name + "\n\t" + "Age: " + age;
}
}
正如您所看到的,这只是一个 Java 类,带有几个描述歌手的属性。我们在这里使用这个简单的 Java 类来说明,仅仅因为您在 Groovy 中配置了 bean,并不意味着您的整个代码库都需要用 Groovy 重写。不仅如此,Java 类还可以从依赖项中导入,并在 Groovy 脚本中使用。现在,让我们创建 Groovy 脚本(beans.groovy),它将用于创建 bean 定义,如前面的代码片段所示:
package com.apress.prospring5.ch4
import com.apress.prospring5.ch3.xml.Singer
beans {
singer(Singer, name: 'John Mayer', age: 39)
}
这个 Groovy 脚本从名为beans的顶级闭包开始,它向 Spring 提供 bean 定义。首先,我们指定 bean 名称(singer),然后作为参数,我们提供类类型(Singer),后跟我们想要设置的属性名称和值。接下来,让我们用 Java 创建一个简单的测试驱动程序,从 Groovy 脚本加载 bean 定义,如下所示:
package com.apress.prospring5.ch4;
import com.apress.prospring5.ch3.xml.Singer;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericGroovyApplicationContext;
public class GroovyBeansFromJava {
public static void main(String... args) {
ApplicationContext context =
new GenericGroovyApplicationContext("classpath:beans.groovy");
Singer singer = context.getBean("singer", Singer.class);
System.out.println(singer);
}
}
如您所见,ApplicationContext的创建是以典型的方式进行的,但是它是通过使用GenericGroovyApplicationContext类并提供构建 bean 定义的 Groovy 脚本来完成的。
在运行本节中的示例之前,您需要向这个项目添加一个依赖项:groovy-all库。这里显示了项目groovy-config-java的build.gradle配置文件内容:
apply plugin: 'groovy'
dependencies {
compile misc.groovy
compile project(':chapter03:bean-inheritance')
}
compile project(':chapter03:bean-inheritance')行指定章节 3项目必须被编译并作为该项目的依赖项使用。这是包含Singer类的项目。
Gradle 配置文件使用 Groovy 语法,misc.groovy引用父项目build.gradle文件中定义的misc数组的groovy属性。这里显示了该文件内容的一个片段(与 Groovy 相关的配置):
ext {
springVersion = '5.0.0.M4'
groovyVersion = '2.4.5'
...
misc = [
...
groovy: "org.codehaus.groovy:groovy-all:$groovyVersion"
]
...
}
运行GroovyBeansFromJava类会产生以下输出:
Name: John Mayer
Age: 39
既然您已经看到了如何通过外部 Groovy 脚本从 Java 加载 bean 定义,那么我们如何仅从 Groovy 脚本创建ApplicationContext和 bean 定义呢?让我们看看这里列出的 Groovy 代码(GroovyConfig.groovy):
package com.apress.prospring5.ch4
import com.apress.prospring5.ch3.xml.Singer
import org.springframework.context.support.GenericApplicationContext
import org.springframework.beans.factory.groovy.GroovyBeanDefinitionReader
def ctx = new GenericApplicationContext()
def reader = new GroovyBeanDefinitionReader(ctx)
reader.beans {
singer(Singer, name: 'John Mayer', age: 39)
}
ctx.refresh()
println ctx.getBean("singer")
当我们运行这个示例时,我们得到和以前一样的输出。这一次我们创建了一个典型的GenericApplicationContext的实例,但是使用了GroovyBeanDefinitionReader,它将用于向传递 bean 定义。然后,和前面的例子一样,我们从简单的 POJO 创建一个 bean,刷新ApplicationContext,并打印Singer bean 的字符串表示。没有比这更简单的了!
正如您可能已经知道的,我们只是触及了 Spring 中 Groovy 支持的皮毛。因为您拥有 Groovy 语言的全部功能,所以在创建 bean 定义时,您可以做各种有趣的事情。因为您拥有对ApplicationContext的完全访问权,所以您不仅可以配置 beans,还可以使用概要文件支持、属性文件等等。请记住,权力越大,责任越大。
Spring Boot
到目前为止,您已经学习了多种配置 Spring 应用的方法。无论是 XML、注释、Java 配置类、Groovy 脚本,还是所有这些的混合,现在您应该对如何实现有了一个基本的概念。但是如果我们告诉你有比这更酷的东西呢?
Spring Boot 项目旨在简化使用 Spring 构建应用的入门体验。Spring Boot 消除了手动收集依赖关系的猜测,并提供了大多数应用所需的一些最常见的功能,如指标和健康检查。
Spring Boot 采取了一种“固执己见”的方法,通过为已经包含适当依赖项和版本的各种类型的应用提供起始项目来实现开发人员简化的目标,这意味着花费更少的时间来开始。对于那些希望完全摆脱 XML 的人来说,Spring Boot 不要求任何配置都用 XML 编写。
在这个例子中,我们将创建一个传统的 Hello World web 应用。与典型的 Java web 应用设置相比,您可能会惊讶地发现这样做只需要很少的代码。通常,我们通过定义需要添加到项目中的依赖项来开始示例。Spring Boot 的简化模型的一部分是为你准备所有的依赖项,例如,当使用 Maven 时,你作为开发者利用一个父 POM 来获得这个功能。当使用 Gradle 时,事情变得更加简单。除了 Gradle 插件和 starter 依赖项之外,不需要任何父插件。在下面的例子中,我们将创建一个 Spring 应用,列出上下文中的所有 bean,然后访问helloWorld bean。这里描述了boot-simple项目的梯度配置:
buildscript {
repositories {
mavenLocal()
mavenCentral()
maven { url "http://repo.spring.io/release" }
maven { url "http://repo.spring.io/milestone" }
maven { url "http://repo.spring.io/snapshot" }
maven { url "https://repo.spring.io/libs-snapshot" }
}
dependencies {
classpath boot.springBootPlugin
}
}
apply plugin: 'org.springframework.boot'
dependencies {
compile boot.starter
}
boot.springBootPlugin行引用了父项目的build.gradle文件中定义的boot数组的springBootPlugin属性。此处描述了该文件内容的一个片段(仅与 Spring Boot 相关的配置):
ext {
bootVersion = '2.0.0.BUILD-SNAPSHOT'
...
boot = [
springBootPlugin:
"org.springframework.boot:spring-boot-gradle-plugin:$bootVersion", starter :
"org.springframework.boot:spring-boot-starter:$bootVersion", starterWeb :
"org.springframework.boot:spring-boot-starter-web:$bootVersion"
]
...
}
在撰写本文时,Spring Boot 版本 2.0.0 还没有发布。这就是为什么版本是2.0.0.BUILD-SNAPSHOT,我们需要在配置中添加 Spring 快照库 https://repo.spring.io/libs-snapshot 。很有可能在这本书之后,会发布一个官方版本。
Spring Boot 的每个版本都提供了它所支持的依赖项的精选列表。选择必要库的版本,使 API 完全匹配,这是由 Spring Boot 处理的。因此,不需要手动配置依赖项版本。升级 Spring Boot 将确保这些依赖项也得到升级。在前面的配置中,一组依赖项将被添加到项目中,每个依赖项都有适当的版本,这样它们的 API 将是兼容的。在像 IntelliJ IDEA 这样的智能编辑器中,有一个 Gradle Projects 视图,您可以在其中展开每个模块并检查可用的任务和依赖项,如图 4-3 所示。

图 4-3。
Gradle Projects view of the boot-simple project
现在设置已经就绪,让我们创建类。这里显示了HelloWorld类:
package com.apress.prospring5.ch4;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Component
public class HelloWorld {
private static Logger logger =
LoggerFactory.getLogger(HelloWorld.class);
public void sayHi() {
logger.info("Hello World!");
}
}
这没有什么特别或复杂的;它只是一个带有方法和 bean 声明注释的类。让我们看看如何使用 Spring Boot 构建一个 Spring 应用,并创建一个包含这个 bean 的ApplicationContext:
package com.apress.prospring5.ch4;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import java.util.Arrays;
@SpringBootApplication
public class Application {
private static Logger logger = LoggerFactory.getLogger(Application.class);
public static void main(String args) throws Exception {
ConfigurableApplicationContext ctx =
SpringApplication.run(Application.class, args);
assert (ctx != null);
logger.info("The beans you were looking for:");
// listing all bean definition names
Arrays.stream(ctx.getBeanDefinitionNames()).forEach(logger::info);
HelloWorld hw = ctx.getBean(HelloWorld.class);
hw.sayHi();
System.in.read();
ctx.close();
}
}
仅此而已。真的。这门课本来可以更小,但是我们想向你们展示如何做一些额外的事情。让我们把它们都包括进去。
- 检查我们有一个上下文:
assert语句用于测试你的假设ctx不是null。 - 设置日志记录:Spring Boot 附带了一组日志记录库,所以我们只需将我们想要使用的配置放在
resources目录下。在我们的例子中,我们选择了logback。 - 列出上下文中的所有 bean 定义:使用 Java 8 lambda 表达式,可以在一行中列出上下文中的所有 bean 定义。因此,我们添加了这一行,这样您就可以看到 Spring Boot 为您自动配置了哪些 beans。在列表中,您也可以找到
helloWorldbean。 - 确认退出:不使用
System.in.read();方法,应用打印 beans 名称,打印HelloWorld,然后退出。我们添加了这个调用,所以应用将等待开发人员按下一个键,然后退出。
这里的新奇之处在于@SpringBootApplication注释。这个注释是一个顶级注释,只设计用于类级别。这是一个方便的注释,相当于声明以下三个:
@Configuration:将该类标记为可以用@Bean声明 beans 的配置类。@EnableAutoConfiguration:这是来自包org.springframework.boot.autoconfigure的一个特定的 Spring Boot 注释,它可以启用 SpringApplicationContext,试图根据指定的依赖关系猜测和配置您可能需要的 beans。@EnableAutoConfiguration与 Spring 提供的启动器依赖项配合得很好,但它并不直接与它们绑定,因此可以使用启动器之外的其他依赖项。例如,如果类路径上有一个特定的嵌入式服务器,就会使用它,除非项目中有另一个EmbeddedServletContainerFactory配置。- 我们可以声明用原型注释标注的类,它们将成为某种类型的 beans。用于列出与
@SpringBootApplication一起使用的要扫描的包的属性是basePackages。在 1.3.0 版本中,组件扫描增加了另一个属性:basePackageClasses。这个属性提供了一个类型安全的选择来代替basePackages来指定要扫描带注释组件的包。将扫描每个指定类别的包。
如果@SpringBootApplication注释没有定义组件扫描属性,它将只扫描用它注释的类所在的包。这就是为什么在这里给出的例子中,找到了helloWorld bean 定义,并创建了 bean。
以前的 Spring Boot 应用是一个简单的控制台应用,带有一个开发人员定义的 bean 和一个成熟的开箱即用的环境。但是 Spring Boot 也为 web 应用提供了启动依赖。要使用的依赖项是spring-boot-starter-web,在图 4-4 中你可以看到这个引导启动库的可传递依赖项。在boot-web项目中,HelloWorld类是一个 Spring 控制器,这是一个用于创建 Spring web bean 的特殊类型的类。这是一个典型的 Spring MVC 控制器类,你将在第十六章中了解到。

图 4-4。
Gradle Projects view of the boot-web project
package com.apress.prospring5.ch4.ctrl;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloWorld {
@RequestMapping("/")
public String sayHi() {
return "Hello World!";
}
}
用于声明 Spring web beans 的注释是对@Component的专门化:即@Controller注释。这些类型的类包含用@RequestMapping注释的方法,这些方法被映射到某个请求 URL。您在示例@RestController中看到的注释是出于实际原因而使用的。它是用于 REST 服务的@Controller注释。将helloWorld bean 公开为 REST 服务在这里很有用,因为您不必创建一个带有用户界面和其他 web 组件的成熟的 web 应用,这将污染本节的基本思想。这里介绍的所有 web 组件都包含在第十六章中。
接下来,我们使用一个简单的main()方法创建您的引导类,如下所示:
package com.apress.prospring5.ch4;
import com.apress.prospring5.ch4.ctrl.HelloWorld;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import java.util.Arrays;
@SpringBootApplication(scanBasePackageClasses = HelloWorld.class)
public class WebApplication {
private static Logger logger =
LoggerFactory.getLogger(WebApplication.class);
public static void main(String args) throws Exception {
ConfigurableApplicationContext ctx =
SpringApplication.run(WebApplication.class, args);
assert (ctx != null);
logger.info("Application started...");
System.in.read();
ctx.close();
}
}
因为HelloWorld控制器是在与WebApplication类不同的包中声明的,所以我们创造了一个机会来描述如何使用scanBasePackageClasses属性。
此时,您可能会问自己:web.xml配置文件和我必须为基本 web 应用创建的所有其他组件在哪里?您已经在前面的清单中定义了所有需要的东西!不信?编译项目并运行Application类,直到您看到表明应用已经启动的日志消息。如果您查看生成的日志文件,您会看到这么少的代码发生了很多事情。最值得注意的是,看起来 Tomcat 正在运行,并且已经为您定义了各种端点,比如健康检查、环境输出信息和指标。首先导航到http://localhost:8080,你会看到 Hello World 网页如预期显示。接下来看看一些预配置的端点(例如,http://localhost:8080/health,它返回应用状态的 JSON 表示)。更进一步,加载http://localhost:8080/metrics以更好地理解正在收集的各种度量,比如堆大小、垃圾收集等等。
仅仅从这个例子中你就可以看出,Spring Boot 从根本上简化了你创建任何类型的应用的方式。必须配置大量文件才能运行一个简单的 web 应用的日子已经一去不复返了,有了准备好为您的 web 应用服务的嵌入式 servlet 容器,一切都“正常工作”
虽然我们已经向您展示了一个简单的示例,但是请记住,Spring Boot 并不限制您使用它选择的内容;它只是采取“固执己见”的方法,为您选择默认值。如果您不想使用嵌入式 Tomcat,而是使用 Jetty,只需修改配置文件,从spring-boot-starter-web dependency中排除 Tomcat starter 模块。利用 Gradle Projects 视图是一种帮助您可视化项目中引入了哪些依赖项的方法。Spring Boot 还为其他类型的应用提供了许多其他 starter 依赖项,我们鼓励您通读文档以了解更多详细信息。
更多关于 Spring Boot 的信息,请查看其项目页面 http://projects.spring.io/spring-boot/ 。
摘要
在本章中,您看到了大量 Spring 特有的特性,它们补充了核心 IoC 功能。您看到了如何挂钩到一个 bean 的生命周期,并让它知道 Spring 环境。我们引入了FactoryBean s 作为 IoC 的解决方案,支持更广泛的类集合。我们还展示了如何使用PropertyEditor来简化应用配置,并消除对人工String类型属性的需求。我们向您展示了使用 XML、注释和 Java 配置定义 beans 的多种方法。此外,我们还深入了解了由ApplicationContext提供的一些附加特性,包括 i18n、事件发布和资源访问。
我们还讨论了一些特性,比如使用 Java 类和新的 Groovy 语法代替 XML 配置、概要文件支持以及环境和属性源抽象层。最后,我们讨论了在 Spring 中使用 JSR-330 标准注释。
锦上添花的是如何使用 Spring Boot 来配置 beans 并尽快轻松启动您的应用。
到目前为止,我们已经介绍了 Spring 框架的主要概念及其作为阿迪容器的特性,以及核心 Spring 框架提供的其他服务。在下一章及以后,我们将讨论在特定领域使用 Spring,比如 AOP、数据访问、事务支持和 web 应用支持。
Footnotes 1
看看这段来自 JEE 官方 Javadoc 的片段: http://docs.oracle.com/javaee/7/api/javax/annotation/PostConstruct.html 。
2
这些类不会在com.apress.prospring5.ch4包中再次创建,但是定义它们的项目被用作java-config项目的依赖项。
3
您可以在 Maven 公共存储库中找到依赖特性,比如组 id 和最新版本。例如,这是java.inject : https://mvnrepository.com/artifact/javax.inject/javax.inject 的专用页面。
五、Spring AOP 简介
除了依赖注入(DI),Spring 框架提供的另一个核心特性是支持面向方面编程(AOP)。AOP 通常被称为实现横切关注点的工具。术语横切关注点指的是应用中不能从应用的其余部分分解的逻辑,它可能导致代码重复和紧密耦合。通过使用 AOP 将逻辑的各个部分模块化,称为关注点,您可以将它们应用到应用的许多部分,而无需复制代码或创建硬依赖。日志和安全性是许多应用中都存在的横切关注点的典型例子。考虑一个为调试目的记录每个方法的开始和结束的应用。您可能会将日志记录代码重构为一个特殊的类,但是为了执行日志记录,您仍然需要对应用中的每个方法调用该类上的方法两次。使用 AOP,您可以简单地指定您希望在应用中的每个方法调用之前和之后调用日志记录类上的方法。
重要的是要理解 AOP 是对面向对象编程(OOP)的补充,而不是与之竞争。OOP 非常擅长解决我们作为程序员遇到的各种各样的问题。然而,如果您再次查看日志记录示例,就可以明显看出在大规模实现横切逻辑时,OOP 的不足之处。考虑到 AOP 是在 OOP 之上运行的,单独使用 AOP 来开发一个完整的应用实际上是不可能的。同样,尽管使用 OOP 来开发整个应用肯定是可能的,但是通过使用 AOP 来解决某些涉及横切逻辑的问题,您可以更聪明地工作。
本章涵盖以下主题:
- AOP 基础知识:在讨论 Spring 的 AOP 实现之前,我们先介绍 AOP 作为一种技术的基础知识。“AOP 概念”一节中涉及的大多数概念并不特定于 Spring,可以在任何 AOP 实现中找到。如果您已经熟悉了另一个 AOP 实现,可以跳过“AOP 概念”部分。
- AOP 的类型:有两种不同类型的 AOP:静态和动态。在静态 AOP 中,就像 AspectJ 的 1 编译时编织机制所提供的那样,横切逻辑在编译时应用于你的代码,如果不修改代码并重新编译,你就不能改变它。对于动态 AOP,比如 Spring AOP,横切逻辑是在运行时动态应用的。这允许您对 AOP 配置进行更改,而不必重新编译应用。这些类型的 AOP 是互补的,当一起使用时,它们形成了一个强大的组合,可以在您的应用中使用。
- Spring AOP 架构:Spring AOP 只是其他实现(如 AspectJ)中完整 AOP 特性集的一个子集。在这一章中,我们将从较高的层次来看 Spring 中有哪些特性,它们是如何实现的,以及为什么有些特性被排除在 Spring 实现之外。
- Spring AOP 中的代理:代理是 Spring AOP 工作方式的重要组成部分,您必须理解它们才能充分利用 Spring AOP。在这一章中,我们来看两种代理:JDK 动态代理和 CGLIB 代理。具体来说,我们看一下 Spring 使用每种代理的不同场景,两种代理类型的性能,以及在应用中要从 Spring AOP 中获得最大收益需要遵循的一些简单准则。
- 使用 Spring AOP:在这一章中,我们给出了一些 AOP 使用的实例。我们从一个简单的 Hello World 示例开始,让您更容易理解 Spring 的 AOP 代码,然后我们继续用示例详细描述 Spring 中可用的 AOP 特性。
- 切入点的高级使用:我们探索了在应用中使用切入点时应该使用的
ComposablePointcut和ControlFlowPointcut类、介绍和适当的技术。 - AOP 框架服务:Spring 框架完全支持透明地和声明性地配置 AOP。我们看三种方式(
ProxyFactoryBean类、aop命名空间和@AspectJ-样式注释)将声明定义的 AOP 代理作为协作者注入到您的应用对象中,从而使您的应用完全不知道它正在与被通知的对象一起工作。 - 集成 AspectJ: AspectJ 是一个全功能的 AOP 实现。AspectJ 和 Spring AOP 的主要区别在于 AspectJ 通过编织(编译时或加载时编织)将通知应用于目标对象,而 Spring AOP 是基于代理的。AspectJ 的特性集比 Spring AOP 大很多,但是用起来比 Spring 复杂很多。当你发现 Spring AOP 缺少一个你需要的特性时,AspectJ 是一个很好的解决方案。
AOP 概念
和大多数技术一样,AOP 有自己的一套特定的概念和术语,理解它们的含义很重要。以下是 AOP 的核心概念:
- 连接点:连接点是应用执行过程中定义明确的点。连接点的典型例子包括方法调用、方法调用本身、类初始化和对象实例化。连接点是 AOP 的核心概念,它定义了应用中可以使用 AOP 插入额外逻辑的点。
- 建议:在特定连接点执行的代码是建议,由类中的方法定义。有许多类型的通知,比如 before,它在连接点之前执行,after,它在连接点之后执行。
- 切入点:切入点是连接点的集合,用于定义何时应该执行通知。通过创建切入点,您可以更好地控制如何将建议应用到应用的组件中。如前所述,典型的连接点是方法调用,或者特定类中所有方法调用的集合。通常,您可以在复杂的关系中组合切入点,以进一步限制何时执行建议。
- 方面:方面是封装在类中的通知和切入点的组合。这种组合定义了应该包含在应用中的逻辑以及应该在哪里执行。
- 编织:这是在适当的时候将方面插入到应用代码中的过程。对于编译时 AOP 解决方案,这种编织通常在构建时完成。同样,对于运行时 AOP 解决方案,编织过程是在运行时动态执行的。AspectJ 支持另一种称为加载时编织(load-time weaving,LTW)的编织机制,它拦截底层 JVM 类加载器,并在类加载器加载字节码时为字节码提供编织。
- 目标:其执行流被 AOP 过程修改的对象被称为目标对象。您经常会看到目标对象被称为建议对象。
- 简介:这是一个过程,通过这个过程,您可以通过向对象中引入额外的方法或字段来修改对象的结构。您可以使用 introduction AOP 使任何对象实现特定的接口,而不需要该对象的类显式地实现该接口。
如果你发现这些概念令人困惑,不要担心;当你看到一些例子时,这一切就会变得清楚了。此外,请注意,在 Spring AOP 中,许多这些概念都是屏蔽的,而且由于 Spring 对实现的选择,有些概念是不相关的。在本章中,我们将在 Spring 的上下文中讨论这些特性。
AOP 的类型
正如我们前面提到的,有两种不同类型的 AOP:静态和动态。它们之间的区别实际上是编织过程发生的点以及这个过程是如何实现的。
使用静态 AOP
在静态 AOP 中,编织过程构成了应用构建过程中的另一个步骤。用 Java 术语来说,通过修改应用的实际字节码,根据需要更改和扩展应用代码,可以在静态 AOP 实现中实现编织过程。这是实现编织过程的一种很好的方式,因为最终结果只是 Java 字节码,并且在运行时不需要执行任何特殊的技巧来决定何时应该执行通知。这种机制的缺点是,对方面的任何修改,即使只是想添加另一个连接点,也需要重新编译整个应用。AspectJ 的编译时编织是静态 AOP 实现的一个很好的例子。
使用动态 AOP
动态 AOP 实现(如 Spring AOP)与静态 AOP 实现的不同之处在于编织过程是在运行时动态执行的。这是如何实现的取决于实现,但是正如您将看到的,Spring 的方法是为所有被通知的对象创建代理,允许在需要时调用通知。动态 AOP 的缺点是,通常情况下,它的性能不如静态 AOP,但是性能在稳步提高。动态 AOP 实现的主要好处是可以轻松地修改应用的整个方面集,而不需要重新编译主应用代码。
选择 AOP 类型
选择使用静态还是动态 AOP 是一个非常困难的决定。两者都有各自的好处,你不局限于只使用一种类型。总的来说,静态 AOP 实现已经存在了很长时间,并且倾向于拥有更多功能丰富的实现,有更多可用的连接点。通常,如果性能是绝对重要的,或者你需要一个没有在 Spring 中实现的 AOP 特性,你会希望使用 AspectJ。在大多数其他情况下,Spring AOP 是理想的。请记住,Spring 已经为您提供了许多基于 AOP 的解决方案,比如事务管理,所以在推出您自己的解决方案之前,请检查一下框架的功能!和往常一样,让您的应用的需求来驱动您对 AOP 实现的选择,如果技术的组合更适合您的应用,不要将自己局限于单一的实现。一般来说,Spring AOP 没有 AspectJ 复杂,所以它往往是理想的首选。
Spring 的 AOP
Spring 的 AOP 实现可以被视为两个逻辑部分。第一部分是 AOP 核心,它提供完全解耦的、纯编程的 AOP 功能(也称为 Spring AOP API)。AOP 实现的第二部分是一组框架服务,它们使 AOP 更容易在您的应用中使用。在此基础上,Spring 的其他组件,比如事务管理器和 EJB 助手类,提供了基于 AOP 的服务来简化应用的开发。
AOP 联盟
AOP 联盟( http://aopalliance.sourceforge.net/ ))是许多开源 AOP 项目的代表共同努力的结果,它为 AOP 实现定义了一套标准的接口。只要适用,Spring 就使用 AOP 联盟接口,而不是定义自己的接口。这允许您在支持 AOP 联盟接口的多个 AOP 实现之间重用某些建议。
AOP 中的 Hello World
在我们开始详细讨论 Spring AOP 实现之前,让我们看一个例子。我们将看到如何改变典型的 Hello World 示例,我们将从电影中寻找灵感。我们将编写一个名为Agent的类,我们将实现它来打印Bond。当使用 AOP 时,这个类的实例将在运行时被转换以打印James Bond!。下面的代码描述了Agent类:
package com.apress.prospring5.ch5;
public class Agent {
public void speak() {
System.out.print("Bond");
}
}
实现了名称打印方法后,让我们建议——在 AOP 术语中意味着添加建议——这个方法,以便speak()打印James Bond!。
为此,我们需要在现有主体执行之前执行代码(编写James),并且我们需要在方法主体执行之后执行代码(编写!)。用 AOP 术语来说,我们需要的是围绕着建议,建议围绕着连接点执行。在这种情况下,连接点是对speak()方法的调用。下面的代码片段显示了AgentDecorator类的代码,它充当 around-advice 实现:
package com.apress.prospring5.ch5;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
public class AgentDecorator implements MethodInterceptor {
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.print("James ");
Object retVal = invocation.proceed();
System.out.println("!");
return retVal;
}
}
MethodInterceptor接口是一个标准的 AOP 联盟接口,用于实现方法调用连接点的 around advice。MethodInvocation对象表示被建议的方法调用,使用这个对象,我们可以控制何时允许方法调用继续进行。因为这是关于建议的,所以我们能够在方法被调用之前以及在方法被调用之后但在它返回之前执行动作。所以在前面的代码片段中,我们简单地将James写入控制台输出,通过调用invocation.proceed()来调用该方法,然后将!写入控制台输出。
该示例的最后一步是将AgentDecorator建议(更具体地说,是invoke()方法)编织到代码中。为此,我们创建一个目标Agent的实例,然后创建这个实例的代理,指示代理工厂织入AgentDecorator通知。如下所示:
package com.apress.prospring5.ch5;
import org.springframework.aop.framework.ProxyFactory;
public class AgentAOPDemo {
public static void main(String... args) {
Agent target = new Agent();
ProxyFactory pf = new ProxyFactory();
pf.addAdvice(new AgentDecorator());
pf.setTarget(target);
Agent proxy = (Agent) pf.getProxy();
target.speak();
System.out.println("");
proxy.speak();
}
}
这里重要的部分是我们使用ProxyFactory类来创建目标对象的代理,同时织入通知。我们通过调用addAdvice()将AgentDecorator建议传递给ProxyFactory,并通过调用setTarget()指定编织的目标。一旦目标被设定,一些建议被添加到ProxyFactory,我们就通过调用getProxy()来生成代理。最后,我们在原始目标对象和代理对象上调用speak()。运行前面的代码会产生以下输出:
Bond
James Bond!
如您所见,在未触及的目标对象上调用speak()会导致标准的方法调用,并且没有额外的内容被写入控制台输出。然而,代理的调用导致执行AgentDecorator中的代码,创建期望的James Bond!输出。从这个例子中,您可以看到 advised 类不依赖于 Spring 或 AOP Alliance 接口;Spring AOP 的美妙之处,实际上也是 AOP 的美妙之处,在于你可以建议几乎任何类,即使这个类是在没有考虑 AOP 的情况下创建的。唯一的限制是,至少在 Spring AOP 中,不能通知 final 类,因为它们不能被覆盖,因此不能被代理。
Spring AOP 架构
Spring AOP 的核心架构是基于代理的。当您想要创建一个类的通知实例时,您必须使用ProxyFactory来创建该类的代理实例,首先向ProxyFactory提供您想要编织到代理中的所有方面。使用ProxyFactory是一种创建 AOP 代理的纯编程方法。在大多数情况下,您不需要在您的应用中使用它;相反,您可以依靠 Spring 提供的声明性 AOP 配置机制(ProxyFactoryBean类、aop名称空间和@AspectJ-样式注释)来利用声明性代理创建。然而,理解代理创建是如何工作的很重要,所以我们将首先展示代理创建的编程方法,然后深入研究 Spring 的声明性 AOP 配置。
在运行时,Spring 分析为ApplicationContext中的 bean 定义的横切关注点,并动态生成代理 bean(包装底层目标 bean)。调用者不是直接调用目标 bean,而是被注入代理 bean。然后,代理 bean 分析运行条件(即连接点、切入点或通知),并相应地织入适当的通知。图 5-1 展示了一个 Spring AOP 代理的高级视图。在内部,Spring 有两个代理实现:JDK 动态代理和 CGLIB 代理。默认情况下,当被通知的目标对象实现一个接口时,Spring 将使用 JDK 动态代理来创建目标的代理实例。然而,当被通知的目标对象没有实现接口时(例如,它是一个具体的类),CGLIB 将用于代理实例的创建。一个主要原因是 JDK 动态代理只支持接口代理。我们将在“理解代理”一节中详细讨论代理

图 5-1。
Spring AOP proxy in action
Spring 的连接点
Spring AOP 中一个比较明显的简化是它只支持一种连接点类型:方法调用。乍一看,如果您熟悉其他 AOP 实现,比如 AspectJ,它支持更多的连接点,这似乎是一个严重的限制,但实际上这使得 Spring 更容易访问。
方法调用连接点是迄今为止最有用的连接点,使用它,您可以完成许多使 AOP 在日常编程中有用的任务。请记住,如果您需要在连接点而不是在方法调用上建议一些代码,您总是可以一起使用 Spring 和 AspectJ。
Spring 的景象
在 Spring AOP 中,一个方面由一个实现了Advisor接口的类的实例来表示。Spring 提供了方便的Advisor实现,您可以在您的应用中重用这些实现,这样您就不需要创建定制的Advisor实现了。Advisor有两个子接口:PointcutAdvisor和IntroductionAdvisor。
所有使用切入点来控制应用于连接点的通知的Advisor实现都实现了PointcutAdvisor接口。在 Spring 中,介绍被视为特殊类型的建议,通过使用IntroductionAdvisor接口,您可以控制介绍适用于哪些类。
我们将在下一节“Spring 中的顾问和切入点”中详细讨论PointcutAdvisor实现
关于 ProxyFactory 类
ProxyFactory类控制 Spring AOP 中的编织和代理创建过程。在创建代理之前,必须指定建议对象或目标对象。正如您之前看到的,您可以通过使用setTarget()方法来实现这一点。在内部,ProxyFactory将代理创建过程委托给DefaultAopProxyFactory的一个实例,后者又委托给Cglib2AopProxy或JdkDynamicAopProxy,这取决于应用的设置。我们将在本章后面更详细地讨论代理创建。
ProxyFactory类提供了您在前面的代码示例中看到的addAdvice()方法,用于您希望建议应用于类中所有方法的调用,而不仅仅是选择的情况。在内部,addAdvice()将您传递的通知包装在一个DefaultPointcutAdvisor的实例中,这是PointcutAdvisor的标准实现,并且用一个默认包含所有方法的切入点来配置它。当您想要对创建的Advisor进行更多的控制,或者想要向代理添加介绍时,您可以自己创建Advisor并使用ProxyFactory的addAdvisor()方法。
您可以使用同一个ProxyFactory实例来创建许多代理,每个代理都有不同的方面。为了帮助做到这一点,ProxyFactory有removeAdvice()和removeAdvisor()方法,允许您从之前传递给它的ProxyFactory中删除任何建议或顾问。要检查一个ProxyFactory是否附加了特定的建议,调用adviceIncluded(),传入您想要检查的建议对象。
在 Spring 创造建议
Spring 支持六种风格的建议,如表 5-1 所述。
表 5-1。
Advice Types in Spring
| 建议名称 | 连接 | 描述 | | --- | --- | --- | | `Before` | `org.springframework.aop.MethodBeforeAdvice` | 使用 before advice,您可以在连接点执行之前执行自定义处理。因为 Spring 中的连接点总是一个方法调用,这实质上允许您在方法执行之前执行预处理。Before advice 可以完全访问方法调用的目标以及传递给方法的参数,但是它不能控制方法本身的执行。如果 before 通知抛出异常,拦截器链(以及目标方法)的进一步执行将被中止,并且异常将向上传播回拦截器链。 | | `After-Returning` | `org.springframework.aop.AfterReturningAdvice` | 在连接点的方法调用完成执行并返回一个值之后,执行返回后通知。返回后通知可以访问方法调用的目标、传递给方法的参数和返回值。因为在调用返回后通知时已经执行了该方法,所以它对方法调用没有任何控制。如果目标方法抛出异常,返回后通知将不会运行,异常将照常向上传播到调用堆栈。 | | `After(finally)` | `org.springframework.aop.AfterAdvice` | 仅当建议的方法正常完成时,才执行返回后建议。但是,无论建议方法的结果如何,都将执行 after (finally)建议。即使建议的方法失败并抛出异常,建议也会执行。 | | `Around` | `org.aopalliance.intercep` t `.MethodInterceptor` | 在 Spring 中,around advice 使用 AOP 联盟标准的方法拦截器进行建模。您的建议允许在方法调用前后执行,并且您可以控制方法调用允许进行的时间点。如果愿意,您可以选择完全绕过该方法,提供您自己的逻辑实现。 | | `Throws` | `org.springframework.aop.ThrowsAdvice` | Throws advice 在方法调用返回后执行,但前提是该调用抛出了异常。throws 通知可以只捕捉特定的异常,如果您选择这样做,您可以访问抛出异常的方法、传递给调用的参数以及调用的目标。 | | `Introduction` | `org.springframework.aop.IntroductionInterceptor` | Spring 将引入建模为特殊类型的拦截器。使用引入拦截器,您可以指定由通知引入的方法的实现。 |建议界面
根据我们之前对ProxyFactory类的讨论,回想一下,通知可以通过使用addAdvice()方法直接添加到代理中,也可以通过使用带有addAdvisor()方法的 Advisor 实现间接添加到代理中。advice 和 advisor 之间的主要区别在于,advisor 携带带有关联切入点的 advice,这提供了对 advice 将拦截哪些连接点的更细粒度的控制。关于建议,Spring 为建议接口创建了一个定义良好的层次结构。这个层次结构基于 AOP 联盟接口,如图 5-2 所示。

图 5-2。
Interfaces for Spring advice types as depicted in IntelliJ IDEA
这种层次结构的好处是不仅是合理的 OO 设计,而且使您能够处理一般的通知类型,例如通过在ProxyFactory上使用单个addAdvice()方法,并且您可以容易地添加新的通知类型,而不必修改ProxyFactory类。
建议前创建
Before advice 是 Spring 中最有用的建议类型之一。该建议可以修改传递给方法的参数,并可以通过引发异常来阻止方法执行。在本节中,我们将向您展示使用 before advice 的两个简单示例:一个是在方法执行之前将包含方法名称的消息写入控制台输出,另一个是您可以用来限制对对象上的方法的访问。在下面的代码片段中,您可以看到SimpleBeforeAdvice类的代码:
package com.apress.prospring5.ch5;
import java.lang.reflect.Method;
import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.aop.framework.ProxyFactory;
public class SimpleBeforeAdvice implements MethodBeforeAdvice {
public static void main(String... args) {
Guitarist johnMayer = new Guitarist();
ProxyFactory pf = new ProxyFactory();
pf.addAdvice(new SimpleBeforeAdvice());
pf.setTarget(johnMayer)
;
Guitarist proxy = (Guitarist) pf.getProxy();
proxy.sing();
}
@Override
public void before(Method method, Object[] args, Object target)
throws Throwable {
System.out.println("Before '" + method.getName() + "', tune guitar.");
}
}
类Guitarist很简单,只有一个方法sing(),它在控制台中打印出一段歌词。它扩展了Singer接口,该接口将在整本书中使用。
package com.apress.prospring5.ch5;
import com.apress.prospring5.ch2.common.Singer;
public class Guitarist implements Singer {
private String lyric="You're gonna live forever in me";
@Override
public void sing(){
System.out.println(lyric);
}
}
基本上,这个建议可以确保豆子在唱歌前先给吉他调音。在这段代码中,你可以看到我们已经用SimpleBeforeAdvice类的一个实例通知了我们之前创建的Guitarist类的一个实例。由SimpleBeforeAdvice实现的MethodBeforeAdvice接口定义了一个方法before(),在连接点的方法被调用之前,AOP 框架会调用这个方法。记住,现在,我们使用由addAdvice()方法提供的默认切入点,它匹配一个类中的所有方法。向before()方法传递三个参数:要调用的方法、将传递给该方法的参数以及作为调用目标的Object。SimpleBeforeAdvice类使用before()方法的Method参数将消息写入控制台输出,其中包含要调用的方法的名称。运行此示例会得到以下输出:
Before 'sing', tune guitar.
You're gonna live forever in me
正如您所看到的,显示了调用sing()的输出,但是在它之前,您可以看到由SimpleBeforeAdvice生成的输出。
通过使用 Before Advice 保护方法访问
在本节中,我们将实现 before advice,它在允许方法调用继续进行之前检查用户凭证。如果用户凭据无效,通知将引发异常,从而阻止方法执行。本节中的示例非常简单。它允许用户使用任何密码进行身份验证,并且只允许单个硬编码用户访问受保护的方法。然而,它确实说明了使用 AOP 来实现安全性这样的横切关注点是多么容易。
这只是一个演示如何使用 before 建议的例子。为了保护 Spring beans 的方法执行,Spring Security 项目已经提供了全面的支持;您不需要自己实现这些特性。
下面的代码片段展示了SecureBean类。这是我们将使用 AOP 保护的类。
package com.apress.prospring5.ch5;
public class SecureBean {
public void writeSecureMessage() {
System.out.println("Every time I learn something new, "
+ "it pushes some old stuff out of my brain");
}
}
这门课传授了荷马·辛普森的智慧之珠,这是我们不想让每个人都看到的智慧。因为这个例子要求用户进行身份验证,所以我们需要存储他们的详细信息。下面的代码片段显示了我们可以用来存储用户凭证的UserInfo类:
package com.apress.prospring5.ch5;
public class UserInfo {
private String userName;
private String password;
public UserInfo(String userName, String password) {
this.userName = userName;
this.password = password;
}
public String getPassword() {
return password;
}
public String getUserName() {
return userName;
}
}
这个类只是保存关于用户的数据,这样我们就可以用它做一些有用的事情。下面的代码片段显示了SecurityManager类,它负责对用户进行身份验证,并存储他们的凭据以供以后检索:
package com.apress.prospring5.ch5;
public class SecurityManager {
private static ThreadLocal<UserInfo>
threadLocal = new ThreadLocal<>();
public void login(String userName, String password) {
threadLocal.set(new UserInfo(userName, password));
}
public void logout() {
threadLocal.set(null);
}
public UserInfo getLoggedOnUser() {
return threadLocal.get();
}
}
应用使用SecurityManager类对用户进行身份验证,然后检索当前已通过身份验证的用户的详细信息。应用通过使用login()方法来验证用户。这只是一个模拟实现。在实际的应用中,login()方法可能会根据数据库或 LDAP 目录检查提供的凭证,但是这里我们可以假设所有用户都被允许进行身份验证。login()方法为用户创建一个UserInfo对象,并使用ThreadLocal将其存储在当前线程中。logout()方法设置可能存储在ThreadLocal到null中的任何值。最后,getLoggedOnUser()方法返回当前认证用户的UserInfo对象。如果没有用户通过身份验证,该方法返回null。
要检查用户是否通过身份验证,如果是,是否允许用户访问Secure-Bean上的方法,我们需要创建在方法之前执行的通知,并根据允许用户的凭证集检查由Security-Manager.getLoggedOnUser()返回的UserInfo对象。此建议的代码SecurityAdvice如下所示:
package com.apress.prospring5.ch5;
import java.lang.reflect.Method;
import org.springframework.aop.MethodBeforeAdvice;
public class SecurityAdvice implements MethodBeforeAdvice {
private SecurityManager securityManager;
public SecurityAdvice() {
this.securityManager = new SecurityManager();
}
@Override
public void before(Method method, Object[] args, Object target)
throws Throwable {
UserInfo user = securityManager.getLoggedOnUser();
if (user == null) {
System.out.println("No user authenticated");
throw new SecurityException(
"You must login before attempting to invoke the method: "
+ method.getName());
} else if ("John".equals(user.getUserName())) {
System.out.println("Logged in user is John - OKAY!");
} else {
System.out.println("Logged in user is " + user.getUserName()
+ " NOT GOOD :(");
throw new SecurityException("User " + user.getUserName()
+ " is not allowed access to method " + method.getName());
}
}
}
SecurityAdvice类在其构造函数中创建一个SecurityManager的实例,然后将这个实例存储在一个字段中。你应该注意到应用和SecurityAdvice不需要共享同一个SecurityManager实例,因为所有的数据都通过使用ThreadLocal存储在当前线程中。在before()方法中,我们执行一个简单的检查来查看被认证用户的用户名是否是John。如果是,我们允许用户访问;否则,将引发异常。还要注意,我们检查了一个null UserInfo对象,这表明当前用户没有经过身份验证。
在下面的代码片段中,您可以看到一个使用SecurityAdvice类来保护SecureBean类的示例应用:
package com.apress.prospring5.ch5;
import org.springframework.aop.framework.ProxyFactory;
public class SecurityDemo {
public static void main(String... args) {
SecurityManager mgr = new SecurityManager();
SecureBean bean = getSecureBean();
mgr.login("John", "pwd");
bean.writeSecureMessage();
mgr.logout();
try {
mgr.login("invalid user", "pwd");
bean.writeSecureMessage();
} catch(SecurityException ex) {
System.out.println("Exception Caught: " + ex.getMessage());
} finally {
mgr.logout();
}
try {
bean.writeSecureMessage();
} catch(SecurityException ex) {
System.out.println("Exception Caught: " + ex.getMessage());
}
}
private static SecureBean getSecureBean() {
SecureBean target = new SecureBean();
SecurityAdvice advice = new SecurityAdvice();
ProxyFactory factory = new ProxyFactory();
factory.setTarget(target);
factory.addAdvice(advice);
SecureBean proxy = (SecureBean)factory.getProxy();
return proxy;
}
}
在getSecureBean()方法中,我们创建了一个SecureBean类的代理,使用SecurityAdvice的一个实例来通知它。这个代理被返回给调用者。当调用者调用这个代理上的任何方法时,调用首先被路由到SecurityAdvice的实例进行安全检查。在main()方法中,我们测试了三个场景,使用两组用户凭证调用SecureBean.writeSecureMessage()方法,然后完全不使用用户凭证。因为只有当当前认证的用户是John时SecurityAdvice才允许方法调用继续,所以我们可以预期前面代码中唯一成功的场景是第一个。运行此示例会产生以下输出:
Logged in user is John - OKAY!
Every time I learn something new, it pushes some old stuff out of my brain
Logged in user is invalid user NOT GOOD :(
Exception Caught: User invalid user is not allowed access to method
writeSecureMessage
No user authenticated
Exception Caught: You must login before attempting to invoke the method:
writeSecureMessage
如您所见,只有第一次调用SecureBean.writeSecureMessage()才被允许进行。剩余的调用被SecurityAdvice抛出的SecurityException异常阻止。这个例子很简单,但是它突出了 before advice 的用处。安全性是 before advice 的一个典型例子,但是我们也发现当一个场景需要修改传递给方法的参数时,它很有用。
创建退货后通知
返回后通知在连接点的方法调用返回后执行。假设方法已经执行,你不能改变传递给它的参数。虽然可以读取这些参数,但是不能改变执行路径,也不能阻止方法执行。这些限制是意料之中的;然而,不期望的是,您不能在返回后的通知中修改返回值。使用退货后通知时,您只能添加处理。尽管返回后通知不能修改方法调用的返回值,但它可以抛出一个异常,该异常可以代替返回值被发送到堆栈上。
在这一节中,我们来看两个在应用中使用退货后通知的例子。第一个示例只是在调用方法后将一条消息写入控制台输出。第二个例子展示了如何使用返回后通知向方法中添加错误检查。考虑一个为加密目的生成密钥的类KeyGenerator。许多密码算法都存在少量密钥被认为是脆弱的问题。弱密钥是其特征使得在不知道密钥的情况下导出原始消息明显更容易的任何密钥。对于 DES 算法,总共有 256 个可能的密钥。在这个密钥空间中,4 个密钥被认为是弱的,另外 12 个被认为是半弱的。尽管随机生成这些密钥的几率很小(252 分之一),但是测试这些密钥非常简单,值得一试。在本节的第二个例子中,我们构建了返回后通知,检查由KeyGenerator生成的弱键,如果发现一个,就抛出一个异常。
要了解更多关于弱密钥和加密技术的信息,我们推荐您访问 William Stallings 的网站 http://williamstallings.com/Cryptography/ 。
在下面的代码片段中,您可以看到SimpleAfterReturningAdvice类,它通过在方法返回后向控制台输出写入一条消息来演示返回后通知的使用。前面介绍的类Guitarist在这里被重用。
package com.apress.prospring5.ch5;
import java.lang.reflect.Method;
import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.framework.ProxyFactory;
public class SimpleAfterReturningAdvice implements
AfterReturningAdvice {
public static void main(String... args) {
Guitarist target = new Guitarist();
ProxyFactory pf = new ProxyFactory();
pf.addAdvice(new SimpleAfterReturningAdvice());
pf.setTarget(target);
Guitarist proxy = (Guitarist) pf.getProxy();
proxy.sing();
}
@Override
public void afterReturning(Object returnValue, Method method,
Object[] args, Object target) throws Throwable {
System.out.println("After '" + method.getName()+ "' put down guitar.");
}
}
注意,AfterReturningAdvice接口声明了一个方法afterReturning(),它被传递了方法调用的返回值、对被调用方法的引用、传递给方法的参数以及调用的目标。运行此示例会产生以下输出:
You're gonna live forever in me
After 'sing' put down guitar.
输出类似于通知之前的例子,除了如预期的那样,由通知编写的消息出现在由writeMessage()方法编写的消息之后。当一个方法有可能返回一个无效值时,使用返回后通知的一个好方法是执行一些额外的错误检查。
在我们前面描述的场景中,加密密钥生成器有可能生成一个对于特定算法来说被认为是脆弱的密钥。理想情况下,密钥生成器会检查这些弱密钥,但是由于这些密钥出现的机会通常非常小,所以许多生成器不会检查。通过使用返回后通知,我们可以通知生成密钥和执行这个附加检查的方法。下面是一个极其原始的密钥生成器:
package com.apress.prospring5.ch5;
import java.util.Random;
public class KeyGenerator {
protected static final long WEAK_KEY = 0xFFFFFFF0000000L;
protected static final long STRONG_KEY = 0xACDF03F590AE56L;
private Random rand = new Random();
public long getKey() {
int x = rand.nextInt(3);
if (x == 1) {
return WEAK_KEY;
}
return STRONG_KEY;
}
}
这个密钥生成器不应该被视为安全的。对于这个例子来说,它是故意简单的,并且有三分之一的机会产生一个弱密钥。在下面的代码片段中,您可以看到WeakKeyCheckAdvice,它检查getKey()方法的结果是否是一个弱键:
package com.apress.prospring5.ch5;
import java.lang.reflect.Method;
import org.springframework.aop.AfterReturningAdvice;
import static com.apress.prospring5.ch5.KeyGenerator.WEAK_KEY;
public class WeakKeyCheckAdvice implements AfterReturningAdvice {
@Override
public void afterReturning(Object returnValue, Method method,
Object args,Object target) throws Throwable {
if ((target instanceof KeyGenerator)
&& ("getKey".equals(method.getName()))) {
long key = ((Long) returnValue).longValue();
if (key == WEAK_KEY) {
throw new SecurityException(
"Key Generator generated a weak key. Try again");
}
}
}
}
在afterReturning()方法中,我们首先检查在连接点执行的方法是否是getKey()方法。如果是,那么我们检查结果值,看它是否是弱键。如果我们发现getKey()方法的结果是一个弱键,那么我们抛出一个SecurityException来通知调用代码这一点。下面的代码片段显示了一个简单的应用,演示了该建议的用法:
package com.apress.prospring5.ch5;
import org.springframework.aop.framework.ProxyFactory;
public class AfterAdviceDemo {
private static KeyGenerator getKeyGenerator() {
KeyGenerator target = new KeyGenerator();
ProxyFactory factory = new ProxyFactory();
factory.setTarget(target);
factory.addAdvice(new WeakKeyCheckAdvice());
return (KeyGenerator)factory.getProxy();
}
public static void main(String... args) {
KeyGenerator keyGen = getKeyGenerator();
for(int x = 0; x < 10; x++) {
try {
long key = keyGen.getKey();
System.out.println("Key: " + key);
} catch(SecurityException ex) {
System.out.println("Weak Key Generated!");
}
}
}
}
在创建了KeyGenerator目标的建议代理后,AfterAdviceDemo类试图生成十个密钥。如果在单个生成过程中抛出了一个SecurityException,一条消息将被写入控制台,通知用户生成了一个弱密钥;否则,将显示生成的密钥。在我们的机器上运行一次会生成以下输出:
Key: 48658904092028502
Weak Key Generated!
Key: 48658904092028502
Weak Key Generated!
Weak Key Generated!
Weak Key Generated!
Key: 48658904092028502
Key: 48658904092028502
Key: 48658904092028502
Key: 48658904092028502
如您所见,KeyGenerator类有时会生成弱键,正如所料,WeakKeyCheckAdvice确保每当遇到弱键时都会引发SecurityException。
围绕建议创建
Around advice 的功能类似于 before 和 after advice 的组合,有一个很大的区别:您可以修改返回值。不仅如此,您还可以阻止该方法的执行。这意味着通过使用 around advice,您可以用新代码替换方法的整个实现。Spring 中的 Around advice 被建模为一个使用MethodInterceptor接口的拦截器。around advice 有许多用途,您会发现 Spring 的许多特性都是通过使用方法拦截器创建的,比如远程代理支持和事务管理特性。方法拦截也是一种很好的分析应用执行的机制,它构成了本节中示例的基础。
我们不会看到如何为方法拦截构建一个简单的例子;相反,我们参考第一个使用Agent类的例子,它展示了如何使用一个基本的方法拦截器在方法调用的任何一端编写消息。从前面的例子中可以注意到,MethodInterceptor接口的invoke()方法没有提供与MethodBeforeAdvice和AfterReturningAdvice相同的参数集。不会将调用的目标、被调用的方法或使用的参数传递给该方法。但是,您可以通过使用传递给invoke()的MethodInvocation对象来访问这些数据。您将在下面的示例中看到这方面的演示。
对于这个例子,我们想要实现一些方法来通知一个类,这样我们就可以获得关于它的方法的运行时性能的基本信息。具体来说,我们想知道该方法执行了多长时间。为了实现这一点,我们可以使用 Spring 中包含的StopWatch类,我们显然需要一个MethodInterceptor,因为我们需要在方法调用之前启动StopWatch,然后立即停止。
下面的代码片段显示了我们将通过使用StopWatch类和 around advice 来分析的WorkerBean类:
package com.apress.prospring5.ch5;
public class WorkerBean {
public void doSomeWork(int noOfTimes) {
for(int x = 0; x < noOfTimes; x++) {
work();
}
}
private void work() {
System.out.print("");
}
}
这是一个简单的类。doSomeWork()方法接受一个参数noOfTimes,并精确地调用work()方法指定的次数。work()方法只是有一个对System.out.print()的虚拟调用,它传入一个空的String。这阻止了编译器优化work()方法以及对work()的调用。
在下面的代码片段中,您可以看到使用StopWatch类来分析方法调用时间的ProfilingInterceptor类。我们使用这个拦截器来分析前面显示的WorkerBean类。
package com.apress.prospring5.ch5;
import java.lang.reflect.Method;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.util.StopWatch;
public class ProfilingInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
StopWatch sw = new StopWatch();
sw.start(invocation.getMethod().getName());
Object returnValue = invocation.proceed();
sw.stop();
dumpInfo(invocation, sw.getTotalTimeMillis());
return returnValue;
}
private void dumpInfo(MethodInvocation invocation, long ms) {
Method m = invocation.getMethod();
Object target = invocation.getThis();
Object args = invocation.getArguments();
System.out.println("Executed method: " + m.getName());
System.out.println("On object of type: " +
target.getClass().getName());
System.out.println("With arguments:");
for (int x = 0; x < args.length; x++) {
System.out.print(" > " + argsx);
}
System.out.print("\n");
System.out.println
("Took: " + ms + " ms");
}
}
在invoke()方法中,这是MethodInterceptor接口中唯一的方法,我们创建了一个StopWatch的实例,然后立即开始运行,允许方法调用继续调用MethodInvocation.proceed()。一旦方法调用结束并且返回值被捕获,我们就停止StopWatch并将花费的总毫秒数连同MethodInvocation对象一起传递给dumpInfo()方法。最后,我们返回由MethodInvocation.proceed()返回的Object,以便调用者获得正确的返回值。在这种情况下,我们不想以任何方式中断调用堆栈;我们只是充当方法调用的窃听者。如果我们愿意,我们可以完全改变调用堆栈,将方法调用重定向到另一个对象或远程服务,或者我们可以简单地在拦截器中重新实现方法逻辑并返回不同的返回值。
dumpInfo()方法只是将一些关于方法调用的信息写入控制台输出,以及方法执行所用的时间。在dumpInfo()的前三行中,您可以看到如何使用MethodInvocation对象来确定被调用的方法、调用的原始目标和使用的参数。
下面的代码示例显示了ProfilingDemo类,它首先用ProfilingInterceptor通知WorkerBean的一个实例,然后分析doSomeWork()方法。
package com.apress.prospring5.ch5;
import org.springframework.aop.framework.ProxyFactory;
public class ProfilingDemo {
public static void main(String... args) {
WorkerBean bean = getWorkerBean();
bean.doSomeWork(10000000);
}
private static WorkerBean getWorkerBean() {
WorkerBean target = new WorkerBean();
ProxyFactory factory = new ProxyFactory();
factory.setTarget(target);
factory.addAdvice(new ProfilingInterceptor());
return (WorkerBean)factory.getProxy();
}
}
在我们的机器上运行这个示例会产生以下输出:
Executed method: doSomeWork
On object of type: com.apress.prospring5.ch5.WorkerBean
With arguments:
> 10000000
Took: 1139 ms
从这个输出中,您可以看到执行了哪个方法,目标的类是什么,传入了什么参数,以及调用花费了多长时间。
创建抛出建议
Throws advice 类似于 after-return advice,因为它在 joinpoint 之后执行,这总是一个方法调用,但是 throws advice 仅在方法抛出异常时执行。抛出建议也类似于返回后建议,因为它对程序执行几乎没有控制。如果您使用 throws 建议,您不能选择忽略引发的异常,而是为方法返回值。您可以对程序流进行的唯一修改是更改引发的异常的类型。这是一个非常强大的概念,可以使应用开发更加简单。考虑这样一种情况,您有一个 API,它抛出了一系列定义不当的异常。使用 throws 建议,您可以建议该 API 中的所有类,并将异常层次结构重新分类,使之更易于管理和描述。当然,您也可以使用 throws 建议在应用中提供集中的错误日志记录,从而减少应用中错误日志记录代码的数量。
如图 5-2 所示,抛出建议是由ThrowsAdvice接口实现的。与你目前看到的接口不同,ThrowsAdvice没有定义任何方法;相反,它只是 Spring 使用的一个标记接口。这样做的原因是 Spring 允许类型化的 throws 建议,这允许您准确地定义您的 throws 建议应该捕捉哪些Exception类型。Spring 通过使用反射检测带有特定签名的方法来实现这一点。Spring 寻找两个不同的方法签名。一个简单的例子很好地说明了这一点。下面的代码片段显示了一个简单的 bean,它有两个方法,这两个方法都抛出不同类型的异常:
package com.apress.prospring5.ch5;
public class ErrorBean {
public void errorProneMethod() throws Exception {
throw new Exception("Generic Exception");
}
public void otherErrorProneMethod() throws IllegalArgumentException {
throw new IllegalArgumentException("IllegalArgument Exception");
}
}
在这里,您可以看到SimpleThrowsAdvice类演示了 Spring 在 throws 建议中寻找的两个方法签名:
package com.apress.prospring5.ch5;
import java.lang.reflect.Method;
import org.springframework.aop.ThrowsAdvice;
import org.springframework.aop.framework.ProxyFactory;
public class SimpleThrowsAdvice implements ThrowsAdvice {
public static void main(String... args) throws Exception {
ErrorBean errorBean = new ErrorBean();
ProxyFactory pf = new ProxyFactory();
pf.setTarget(errorBean);
pf.addAdvice(new SimpleThrowsAdvice());
ErrorBean proxy = (ErrorBean) pf.getProxy();
try {
proxy.errorProneMethod();
} catch (Exception ignored) {
}
try {
proxy.otherErrorProneMethod();
} catch (Exception ignored) {
}
}
public void afterThrowing(Exception ex) throws Throwable {
System.out.println("***");
System.out.println("Generic Exception Capture");
System.out.println("Caught: " + ex.getClass().getName());
System.out.println("***\n");
}
public void afterThrowing(Method method, Object args, Object target,
IllegalArgumentException ex) throws Throwable {
System.out.println("***");
System.out.println("IllegalArgumentException Capture");
System.out.println("Caught: " + ex.getClass().getName());
System.out.println("Method: " + method.getName());
System.out.println("***\n");
}
}
Spring 在 throws advice 中首先寻找的是一个或多个名为afterThrowing()的公共方法。方法的返回类型并不重要,尽管我们发现最好坚持使用void,因为这个方法不能返回任何有意义的值。SimpleThrowsAdvice类中的第一个afterThrowing()方法有一个类型为Exception的参数。您可以指定任何类型的Exception作为参数,当您不关心抛出异常的方法或传递给它的参数时,这个方法是理想的。注意,这个方法捕捉Exception和Exception的任何子类型,除非所讨论的类型有自己的afterThrowing()方法。
在第二个afterThrowing()方法中,我们声明了四个参数来捕捉抛出异常的方法、传递给该方法的参数以及方法调用的目标。此方法中参数的顺序很重要,您必须指定所有四个参数。注意,第二个afterThrowing()方法捕获类型IllegalArgumentException(或其子类型)的异常。运行此示例会产生以下输出:
***
Generic Exception Capture
Caught: java.lang.Exception
***
***
IllegalArgumentException Capture
Caught: java.lang.IllegalArgumentException
Method: otherErrorProneMethod
***
如您所见,当抛出一个普通的旧Exception时,第一个afterThrowing()方法被调用,但是当抛出一个IllegalArgumentException时,第二个afterThrowing()方法被调用。Spring 只为每个Exception调用一个afterThrowing()方法,正如您在SimpleThrowsAdvice类的例子中看到的,Spring 使用其签名包含与Exception类型最佳匹配的方法。如果您的抛出后通知有两个afterThrowing()方法,都用相同的Exception类型声明,但一个只有一个参数,另一个有四个参数,Spring 调用四参数afterThrowing()方法。
抛后建议在各种情况下都很有用;它允许您重新分类整个Exception层次结构,并为您的应用构建集中式Exception日志记录。我们发现,当我们调试一个正在运行的应用时,抛出后建议特别有用,因为它允许我们添加额外的日志记录代码,而不需要修改应用的代码。
选择建议类型
一般来说,选择建议类型是由应用的需求决定的,但是您应该根据自己的需要选择最具体的建议类型。也就是说,当 before advice 可以使用时,不要使用 around advice。在大多数情况下,around advice 可以完成其他三种类型的建议所能完成的一切,但是对于你想要达到的目标来说,它可能是多余的。通过使用最具体的建议类型,您可以使代码的意图更加清晰,同时也减少了出错的可能性。考虑计算方法调用的建议。当您使用 before advice 时,您需要编写的只是计数器,但是对于 around advice,您需要记住调用方法并将值返回给调用者。这些小事可能会让虚假的错误潜入您的应用。通过使建议类型尽可能集中,您减少了错误的范围。
Spring 中的顾问和切入点
到目前为止,你看到的所有例子都使用了ProxyFactory类。这个类提供了一种简单的方法来获取和配置自定义用户代码中的 AOP 代理实例。ProxyFactory.addAdvice()方法是为代理配置建议。这个方法在幕后委托给addAdvisor(),创建一个DefaultPointcutAdvisor的实例,并用一个指向所有方法的切入点来配置它。这样,通知被认为适用于目标上的所有方法。在某些情况下,比如当您使用 AOP 进行日志记录时,这可能是可取的,但是在其他情况下,您可能希望限制建议适用的方法。
当然,您可以简单地在通知本身中执行检查,确认被通知的方法是正确的,但是这种方法有几个缺点。首先,将可接受的方法列表硬编码到建议中会降低建议的可重用性。通过使用切入点,您可以配置通知适用的方法,而不需要将这些代码放在通知中;这显然增加了建议的重用价值。将方法列表硬编码到通知中的其他缺点与性能有关。要检查通知中被通知的方法,您需要在每次调用目标上的任何方法时执行检查。这显然会降低应用的性能。当您使用切入点时,会对每个方法执行一次检查,并将结果缓存起来供以后使用。不使用切入点来限制 list-advised 方法的另一个与性能相关的缺点是,Spring 可以在创建代理时对非高级方法进行优化,这导致了对非高级方法的更快调用。当我们在本章后面讨论代理时,会更详细地介绍这些优化。
我们强烈建议您避免将方法检查硬编码到通知中,而是尽可能使用切入点来控制通知对目标方法的适用性。也就是说,在某些情况下,有必要将检查硬编码到您的建议中。考虑早先的返回后通知的例子,它被设计来捕捉由KeyGenerator类生成的弱键。这种通知与它所通知的类紧密相关,明智的做法是检查通知内部以确保它被应用于正确的类型。我们将建议和目标之间的这种耦合称为目标关联性。一般来说,当您的建议很少或没有目标关联性时,您应该使用切入点。也就是说,它可以应用于任何类型或广泛的类型。当你的建议有很强的目标亲和力时,试着检查建议本身是否被正确使用;当建议被误用时,这有助于减少令人头疼的错误。我们还建议您避免不必要的建议方法。正如您将看到的,这会导致调用速度明显下降,这对应用的整体性能有很大影响。
切入点接口
Spring 中的切入点是通过实现Pointcut接口创建的,如下所示:
package org.springframework.aop;
public interface Pointcut {
ClassFilter getClassFilter ();
MethodMatcher getMethodMatcher();
}
从这段代码中可以看出,Pointcut接口定义了两个方法,getClassFilter()和getMethodMatcher(),分别返回ClassFilter和MethodMatcher的实例。显然,如果选择实现Pointcut接口,就需要实现这些方法。幸运的是,正如您将在下一节中看到的,这通常是不必要的,因为 Spring 提供了一系列的Pointcut实现,涵盖了您的大部分(如果不是全部)用例。
当确定一个Pointcut是否适用于一个特定的方法时,Spring 首先通过使用由Pointcut.getClassFilter()返回的ClassFilter实例来检查Pointcut接口是否适用于该方法的类。这里是ClassFilter界面:
org.springframework.aop;
public interface ClassFilter {
boolean matches(Class<?> clazz);
}
如您所见,ClassFilter接口定义了一个方法matches(),它被传递了一个代表要检查的类的Class实例。正如您已经确定的,如果切入点适用于类,那么matches()方法返回true,否则返回false。
MethodMatcher接口比ClassFilter接口更复杂,如下所示:
package org.springframework.aop;
public interface MethodMatcher {
boolean matches(Method m, Class<?> targetClass);
boolean isRuntime();
boolean matches(Method m, Class<?> targetClass, Object[] args);
}
Spring 支持两种类型的MethodMatcher,静态和动态,由isRuntime()的返回值决定。在使用MethodMatcher之前,Spring 调用isRuntime()来确定MethodMatcher是静态的,由返回值false表示,还是动态的,由返回值true表示。
对于静态切入点,Spring 为目标上的每个方法调用一次MethodMatcher的matches(Method, Class<T>)方法,缓存这些方法的后续调用的返回值。通过这种方式,对每个方法只执行一次方法适用性检查,并且方法的后续调用不会导致matches()的调用。
使用动态切入点,Spring 仍然通过在第一次调用方法时使用matches(Method, Class<T>)来执行静态检查,以确定方法的整体适用性。然而,除此之外,假设静态检查返回了true,Spring 通过使用matches(Method, Class<T>, Object[])方法对方法的每次调用执行进一步的检查。这样,动态的MethodMatcher可以根据方法的特定调用,而不仅仅是方法本身,来决定切入点是否应该应用。例如,只有当参数是值大于 100 的Integer时,才需要应用切入点。在这种情况下,可以对matches(Method, Class<T>, Object[])方法进行编码,以便对每次调用的参数进行进一步检查。
显然,静态切入点的性能比动态切入点好得多,因为它们避免了每次调用都需要额外的检查。动态切入点为决定是否应用建议提供了更大的灵活性。一般来说,我们建议您尽可能使用静态切入点。然而,如果您的建议增加了大量的开销,通过使用动态切入点来避免任何不必要的建议调用可能是明智的。
一般来说,您很少从头开始创建自己的Pointcut实现,因为 Spring 为静态和动态切入点都提供了抽象基类。在接下来的几节中,我们将看看这些基类,以及其他的Pointcut实现。
可用的切入点实现
从 4.0 版本开始,Spring 提供了八个Pointcut接口的实现:两个抽象类,旨在作为创建静态和动态切入点的便利类,以及六个具体类,分别用于以下每一个:
- 将多个切入点组合在一起
- 处理控制流切入点
- 执行简单的基于名称的匹配
- 使用正则表达式定义切入点
- 使用 AspectJ 表达式定义切入点
- 定义在类或方法级别寻找特定注释的切入点
表 5-2 总结了八个Pointcut接口实现。
表 5-2。
Summary of Spring Pointcut Implementations
| 实现类 | 描述 | | --- | --- | | `org.springframework.aop.support.annotation.AnnotationMatchingPointcut` | 该实现在类或方法上寻找特定的 Java 注释。此类要求 JDK 5 或更高。 | | `org.springframework.aop.aspectj.AspectJExpressionPointcut` | 这个实现使用 AspectJ weaver 来评估 AspectJ 语法中的切入点表达式。 | | `org.springframework.aop.support.ComposablePointcut` | `ComposablePointcut`类用于通过`union()`和`intersection()`等操作组合两个或多个切入点。 | | `org.springframework.aop.support.ControlFlowPointcut` | `ControlFlowPointcut`是一个特例切入点,它匹配另一个方法的控制流中的所有方法,也就是说,作为另一个方法被调用的结果而被直接或间接调用的任何方法。 | | `org.springframework.aop.support.DynamicMethodMatcherPointcut` | 该实现旨在作为构建动态切入点的基类。 | | `org.springframework.aop.support.JdkRegexpMethodPointcut` | 这个实现允许您使用 JDK 1.4 正则表达式支持来定义切入点。此类要求 JDK 1.4 或更高版本。 | | `org.springframework.aop.support.NameMatchMethodPointcut` | 使用`NameMatchMethodPointcut`,您可以创建一个切入点,根据方法名列表执行简单的匹配。 | | `org.springframework.aop.support.StaticMethodMatcherPointcut` | `StaticMethodMatcherPointcut`类旨在作为构建静态切入点的基础。 |图 5-3 显示了Pointcut实现类的统一建模语言(UML) 2 图。

图 5-3。
Pointcut implementation classes represented as an UML diagram in Intellij IDEA
使用 DefaultPointcutAdvisor
在使用任何Pointcut实现之前,您必须首先创建一个Advisor接口的实例,或者更具体地说是一个PointcutAdvisor接口。在我们之前的讨论中,请记住Advisor是 Spring 的一个方面的表示(参见上一节“Spring 中的方面”),它是建议和切入点的结合,决定了应该建议哪些方法以及如何建议。Spring 提供了许多PointcutAdvisor的实现,但是现在我们只关注一个DefaultPointcutAdvisor。这是一个简单的PointcutAdvisor,用于将单个Pointcut与单个Advice相关联。
使用 StaticMethodMatcherPointcut 创建静态切入点
在这一节中,我们将通过扩展抽象的StaticMethodMatcherPointcut类来创建一个简单的静态切入点。因为StaticMethodMatcherPointcut类扩展了StaticMethodMatcher类(也是一个抽象类),后者实现了MethodMatcher接口,所以您需要实现方法matches(Method, Class<?>)。其余的Pointcut实现是自动处理的。尽管这是您需要实现的唯一方法(当扩展StaticMethodMatcherPointcut类时),您可能想要覆盖getClassFilter()方法,如本例所示,以确保只通知正确类型的方法。
对于这个例子,我们有两个类,GoodGuitarist和GreatGuitarist,它们都定义了相同的方法,是接口Singer中method的实现。
package com.apress.prospring5.ch5;
import com.apress.prospring5.ch2.common.Singer;
public class GoodGuitarist implements Singer {
@Override public void sing() {
System.out.println("Who says I can't be free \n" +
"From all of the things that I used to be");
}
}
public class GreatGuitarist implements Singer {
@Override public void sing() {
System.out.println("I shot the sheriff, \n" +
"But I did not shoot the deputy");
}
}
在这个例子中,我们希望能够通过使用相同的DefaultPointcutAdvisor来创建两个类的代理,但是让建议只应用于GoodGuitarist类的sing()方法。为此,我们创建了如下所示的SimpleStaticPointcut类:
package com.apress.prospring5.ch5;
import java.lang.reflect.Method;
import org.springframework.aop.ClassFilter;
import org.springframework.aop.support.StaticMethodMatcherPointcut;
public class SimpleStaticPointcut extends StaticMethodMatcherPointcut {
@Override
public boolean matches(Method method, Class<?> cls) {
return ("sing".equals(method.getName()));
}
@Override
public ClassFilter getClassFilter() {
return cls -> (cls == GoodGuitarist.class);
}
}
这里你可以看到我们按照StaticMethodMatcher抽象类的要求实现了matches(Method, Class<?>)方法。如果方法的名称是sing,实现简单地返回true;否则,它返回false。使用 lambda 表达式,在前面的代码示例中隐藏了在getClassFilter()方法中实现ClassFilter的匿名类的创建。扩展的 lambda 表达式如下所示:
public ClassFilter getClassFilter() {
return new ClassFilter() {
public boolean matches(Class<?> cls) {
return (cls == GoodGuitarist.class);
}
};
}
注意,我们还覆盖了getClassFilter()方法来返回一个ClassFilter实例,该实例的matches()方法只为GoodGuitarist类返回true。对于这个静态切入点,我们说只有GoodGuitarist类的方法会被匹配,而且,只有那个类的sing()方法会被匹配。
下面的代码片段显示了SimpleAdvice类,它只是在方法调用的任何一端写出一条消息:
package com.apress.prospring5.ch5;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
public class SimpleAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println(">> Invoking " + invocation.getMethod().getName());
Object retVal = invocation.proceed();
System.out.println(">> Done\n");
return retVal
;
}
}
在下面的代码片段中,您可以看到一个简单的驱动程序应用,它通过使用SimpleAdvice和SimpleStaticPointcut类创建了一个DefaultPointcutAdvisor的实例。此外,因为两个类实现了相同的接口,所以您可以看到代理可以基于接口而不是具体的类来创建。
package com.apress.prospring5.ch5;
import com.apress.prospring5.ch2.common.Singer;
import org.aopalliance.aop.Advice; import org.springframework.aop.Advisor;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
public class StaticPointcutDemo {
public static void main(String... args) {
GoodGuitarist johnMayer = new GoodGuitarist();
GreatGuitarist ericClapton = new GreatGuitarist();
Singer proxyOne;
Singer proxyTwo;
Pointcut pc = new SimpleStaticPointcut();
Advice advice = new SimpleAdvice();
Advisor advisor = new DefaultPointcutAdvisor(pc, advice);
ProxyFactory pf = new ProxyFactory();
pf.addAdvisor(advisor);
pf.setTarget(johnMayer);
proxyOne = (Singer)pf.getProxy();
pf = new ProxyFactory();
pf.addAdvisor(advisor);
pf.setTarget(ericClapton);
proxyTwo = (Singer)pf.getProxy();
proxyOne.sing();
proxyTwo.sing();
}
}
注意,DefaultPointcutAdvisor实例随后被用来创建两个代理:一个用于GoodGuitarist的实例,另一个用于EricClapton的实例。最后,在两个代理上调用sing()方法。运行此示例会产生以下输出:
>> Invoking sing
Who says I can't be free
From all of the things that I used to be
>> Done
I shot the sheriff,
But I did not shoot the deputy
正如你所看到的,实际上唯一被调用的SimpleAdvice方法是GoodGuitarist类的sing()方法,正如预期的那样。限制建议应用的方法非常简单,而且,正如您将在我们讨论代理选项时看到的,这是让您的应用获得最佳性能的关键。
使用 DyanmicMethodMatcherPointcut 创建动态切入点
创建一个动态切入点和创建一个静态切入点没有太大的不同,所以对于这个例子,我们将为下面显示的类创建一个动态切入点:
package com.apress.prospring5.ch5;
public class SampleBean {
public void foo(int x) {
System.out.println("Invoked foo() with: " + x);
}
public void bar() {
System.out.println("Invoked bar()");
}
}
对于这个例子,我们只想通知foo()方法,但是与前面的例子不同,我们只想在传递给它的int参数大于或小于 100 时通知这个方法。
与静态切入点一样,Spring 为创建动态切入点提供了一个方便的基类:DynamicMethodMatcherPointcut。DynamicMethodMatcherPointcut类有一个您必须实现的抽象方法matches(Method, Class<?>, Object[])(通过它实现的MethodMatcher接口),但是正如您将看到的,实现matches(Method, Class<?>)方法来控制静态检查的行为也是谨慎的。下面的代码片段显示了SimpleDynamicPointcut类:
package com.apress.prospring5.ch5;
import java.lang.reflect.Method;
import org.springframework.aop.ClassFilter;
import org.springframework.aop.support.DynamicMethodMatcherPointcut;
public class SimpleDynamicPointcut
extends DynamicMethodMatcherPointcut {
@Override
public boolean matches(Method method, Class<?> cls) {
System.out.println("Static check for " + method.getName());
return ("foo".equals(method.getName()));
}
@Override
public boolean matches(Method method, Class<?> cls, Object args) {
System.out.println("Dynamic check for " + method.getName());
int x = ((Integer) args0).intValue();
return (x != 100);
}
@Override
public ClassFilter getClassFilter() {
return cls -> (cls == SampleBean.class);
}
}
正如您在前面的代码示例中看到的,我们以与上一节类似的方式覆盖了getClassFilter()方法。这消除了在方法匹配方法中检查类的需要,而这对于动态检查是特别重要的。虽然只要求您实现动态检查,但是我们也实现静态检查。这样做的原因是你知道bar()方法永远不会被推荐。通过使用静态检查来表明这一点,Spring 永远不必为此方法执行动态检查。这是因为当实现静态检查方法时,Spring 将首先检查它,如果检查结果不匹配,Spring 将停止任何进一步的动态检查。此外,静态检查的结果将被缓存以获得更好的性能。但是如果我们忽略了静态检查,Spring 会在每次调用bar()方法时执行一次动态检查。推荐的做法是,在getClassFilter()方法中执行类检查,在matches(Method, Class<?>)方法中执行方法检查,在matches(Method, Class<?>, Object[])方法中执行参数检查。这将使您的切入点更容易理解和维护,性能也会更好。
在matches(Method, Class<?>, Object[])方法中,你可以看到如果传递给foo()方法的int参数的值不等于 100,我们就返回false;否则,我们返回true。注意,在动态检查中,我们知道我们正在处理foo()方法,因为没有其他方法通过静态检查。在下面的代码片段中,您可以看到这个切入点的一个实例:
package com.apress.prospring5.ch5;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
public class DynamicPointcutDemo {
public static void main(String... args) {
SampleBean target = new SampleBean();
Advisor advisor = new DefaultPointcutAdvisor(
new SimpleDynamicPointcut(), new SimpleAdvice());
ProxyFactory pf = new ProxyFactory();
pf.setTarget(target);
pf.addAdvisor(advisor);
SampleBean proxy = (SampleBean)pf.getProxy();
proxy.foo(1);
proxy.foo(10);
proxy.foo(100);
proxy.bar();
proxy.bar();
proxy.bar();
}
}
请注意,我们使用了与静态切入点示例中相同的通知类。然而,在这个例子中,应该只通知对foo()的前两次调用。动态检查防止第三次调用foo()被告知,静态检查防止bar()方法被告知。运行此示例会产生以下输出:
Static check for bar
Static check for foo
Static check for toString
Static check for clone
Static check for foo
Dynamic check for foo
>> Invoking foo
Invoked foo() with: 1
>> Done
Dynamic check for foo
>> Invoking foo
Invoked foo() with: 10
>> Done
Dynamic check for foo
Invoked foo() with: 100
Static check for bar
Invoked bar()
Invoked bar()
Invoked bar()
正如我们所料,只建议了前两次调用foo()方法。注意,由于对bar()的静态检查,没有一个bar()调用受到动态检查。这里值得注意的一点是,foo()方法受到两次静态检查:一次是在初始阶段检查所有方法,另一次是在第一次调用时。
正如您所看到的,动态切入点比静态切入点提供了更大程度的灵活性,但是由于它们需要额外的运行时开销,您应该只在绝对必要的时候使用动态切入点。
使用简单的名称匹配
通常当创建一个切入点时,我们希望仅仅基于方法的名字来匹配,忽略方法签名和返回类型。在这种情况下,您可以避免创建StaticMethodMatcherPointcut的子类,而是使用NameMatchMethodPointcut(它是StaticMethodMatcherPointcut的子类)来匹配方法名列表。当你使用NameMatchMethodPointcut时,不考虑方法的签名,所以如果你有方法sing()和sing(guitar),它们都匹配名字foo。
在下面的代码片段中,您可以看到GrammyGuitarist类,它是Singer的另一个实现,因为这位格莱美奖歌手用他的声音唱歌,用吉他,并且作为人类,偶尔会说话和休息。
package com.apress.prospring5.ch5;
import com.apress.prospring5.ch2.common.Guitar;
import com.apress.prospring5.ch2.common.Singer;
public class GrammyGuitarist implements Singer {
@Override public void sing() {
System.out.println("sing: Gravity is working against me\n" +
"And gravity wants to bring me down");
}
public void sing(Guitar guitar) {
System.out.println("play: " + guitar.play());
}
public void rest(){
System.out.println("zzz");
}
public void talk(){
System.out.println("talk");
}
}
//chapter02/hello-world/src/main/java/com/apress/prospring5/ch2/common/Guitar.java
package com.apress.prospring5.ch2.common;
public class Guitar {
public String play(){
return "G C G C Am D7";
}
}
对于这个例子,我们想通过使用NameMatchMethodPointcut;来匹配sing()、sing(Guitar)和rest()方法。这转化为匹配名字foo和bar。这显示在下面的代码片段中:
package com.apress.prospring5.ch5;
import com.apress.prospring5.ch2.common.Guitar;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.NameMatchMethodPointcut;
public class NamePointcutDemo {
public static void main(String... args) {
GrammyGuitarist johnMayer = new GrammyGuitarist();
NameMatchMethodPointcut pc = new NameMatchMethodPointcut();
pc.addMethodName("sing");
pc.addMethodName("rest");
Advisor advisor = new DefaultPointcutAdvisor(pc, new SimpleAdvice());
ProxyFactory pf = new ProxyFactory();
pf.setTarget(johnMayer);
pf.addAdvisor(advisor);
GrammyGuitarist proxy = (GrammyGuitarist) pf.getProxy();
proxy.sing();
proxy.sing(new Guitar());
proxy.rest();
proxy.talk();
}
}
没有必要为切入点创建一个类;您可以简单地创建一个NameMatchMethodPointcut的实例,然后您就上路了。注意,我们已经使用addMethodName()方法向切入点添加了两个方法名,sing和rest。运行此示例会产生以下输出:
>> Invoking sing
sing: Gravity is working against me
And gravity wants to bring me down
>> Done
>> Invoking sing
play: G C G C Am D7
>> Done
>> Invoking rest
zzz
>> Done
talk
正如预期的那样,由于切入点的原因,sing、sing(Guitar)和rest方法被推荐,但是talk()方法没有被推荐。
用正则表达式创建切入点
在上一节中,我们讨论了如何根据预定义的方法列表执行简单的匹配。但是,如果您事先不知道所有方法的名称,而是知道名称遵循的模式,该怎么办呢?例如,如果您想匹配名称以get开头的所有方法,该怎么办?在这种情况下,您可以使用正则表达式切入点JdkRegexpMethodPointcut来匹配基于正则表达式的方法名。这里您可以看到另一个Guitarist类,它包含三个方法:
package com.apress.prospring5.ch5;
import com.apress.prospring5.ch2.common.Singer;
public class Guitarist implements Singer {
@Override public void sing() {
System.out.println("Just keep me where the light is");
}
public void sing2() {
System.out.println("Just keep me where the light is");
}
public void rest() {
System.out.println("zzz");
}
}
使用基于正则表达式的切入点,我们可以匹配这个类中名称以string开头的所有方法。如下所示:
package com.apress.prospring5.ch5;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.JdkRegexpMethodPointcut;
public class RegexpPointcutDemo {
public static void main(String... args) {
Guitarist johnMayer = new Guitarist();
JdkRegexpMethodPointcut pc = new JdkRegexpMethodPointcut();
pc.setPattern(".*sing.*");
Advisor advisor = new DefaultPointcutAdvisor(pc, new SimpleAdvice());
ProxyFactory pf = new ProxyFactory();
pf.setTarget(johnMayer);
pf.addAdvisor(advisor);
Guitarist proxy = (Guitarist) pf.getProxy();
proxy.sing();
proxy.sing2();
proxy.rest();
}
}
注意,我们不需要为切入点创建一个类;相反,我们只需创建一个JdkRegexpMethodPointcut的实例,并指定要匹配的模式,我们就完成了。要注意的有趣的事情是模式。当匹配方法名时,Spring 匹配方法的完全限定名,所以对于sing1(),Spring 匹配com.apress.prospring5.ch5.Guitarist.sing1,这就是为什么模式中有前导.*。这是一个强大的概念,因为它允许您匹配给定包中的所有方法,而不需要确切地知道包中有哪些类以及方法的名称是什么。运行此示例会产生以下输出:
>> Invoking sing
Just keep me where the light is
>> Done
>> Invoking sing2
Oh gravity, stay the hell away from me
>> Done
zzz
如您所料,只建议使用sing()和sing2()方法,因为rest()方法与正则表达式模式不匹配。
用 AspectJ 切入点表达式创建切入点
除了 JDK 正则表达式,还可以使用 AspectJ 的切入点表达式语言进行切入点声明。在本章的后面,你会看到当我们使用aop名称空间在 XML 配置中声明切入点时,Spring 默认使用 AspectJ 的切入点语言。而且,在使用 Spring 的@AspectJ注释式 AOP 支持时,需要使用 AspectJ 的切入点语言。所以当使用表达式语言声明切入点时,使用 AspectJ 切入点表达式是最好的方法。Spring 提供了通过 AspectJ 的表达式语言定义切入点的类AspectJExpressionPointcut。要在 Spring 中使用 AspectJ 切入点表达式,需要在项目的类路径中包含两个 AspectJ 库文件,aspectjrt.jar和aspectjweaver.jar。依赖项及其版本在主build.gradle配置文件中配置(并配置为chapter05项目所有模块的依赖项)。
ext {
aspectjVersion = '1.9.0.BETA-5'
...
misc = [
...
Aspectjweaver : "org.aspectj:aspectjweaver:$aspectjVersion",
Aspectjrt : "org.aspectj:aspectjrt:$aspectjVersion"
]
...
考虑到前面的Guitarist类的实现,用 JDK 正则表达式实现的相同功能可以用 AspectJ 表达式来实现。下面是代码:
package com.apress.prospring5.ch5;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
public class AspectjexpPointcutDemo {
public static void main(String... args) {
Guitarist johnMayer = new Guitarist();
AspectJExpressionPointcut pc = new AspectJExpressionPointcut();
pc.setExpression("execution(* sing*(..))");
Advisor advisor = new DefaultPointcutAdvisor(pc, new SimpleAdvice());
ProxyFactory pf = new ProxyFactory();
pf.setTarget(johnMayer);
pf.addAdvisor(advisor);
Guitarist proxy = (Guitarist) pf.getProxy();
proxy.sing();
proxy.sing2();
proxy.rest();
}
}
注意,我们使用AspectJExpressionPointcut的setExpression()方法来设置匹配标准。表达式execution(* sing*(..))意味着通知应该应用于任何以sing开始、有任何参数并返回任何类型的方法的执行。运行该程序将得到与前面使用 JDK 正则表达式的例子相同的结果。
创建匹配切入点的注释
如果您的应用是基于注释的,您可能希望使用您自己指定的注释来定义切入点,也就是说,将通知逻辑应用到所有具有特定注释的方法或类型。Spring 提供了使用注释定义切入点的类AnnotationMatchingPointcut。同样,让我们重用前面的例子,看看在使用注释作为切入点时如何做。
首先我们定义一个名为AdviceRequired的注释,这是一个我们将用来声明切入点的注释。以下代码片段显示了批注类:
package com.apress.prospring5.ch5;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface AdviceRequired {
}
在前面的代码示例中,您可以看到我们通过使用@interface作为类型将接口声明为注释,并且@Target注释定义了注释可以应用于类型或方法级别。下面的代码片段显示了另一个Guitarist类的实现,其中一个方法带有您的注释:
package com.apress.prospring5.ch5;
import com.apress.prospring5.ch2.common.Guitar;
import com.apress.prospring5.ch2.common.Singer;
public class Guitarist implements Singer {
@Override public void sing() {
System.out.println("Dream of ways to throw it all away");
}
@AdviceRequired
public void sing(Guitar guitar) {
System.out.println("play: " + guitar.play());
}
public void rest(){
System.out.println("zzz");
}
}
以下代码片段显示了测试程序:
package com.apress.prospring5.ch5;
import com.apress.prospring5.ch2.common.Guitar;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
public class AnnotationPointcutDemo {
public static void main(String... args) {
Guitarist johnMayer = new Guitarist();
AnnotationMatchingPointcut pc = AnnotationMatchingPointcut
.forMethodAnnotation(AdviceRequired.class);
Advisor advisor = new DefaultPointcutAdvisor(pc, new SimpleAdvice())
;
ProxyFactory pf = new ProxyFactory();
pf.setTarget(johnMayer);
pf.addAdvisor(advisor);
Guitarist proxy = (Guitarist) pf.getProxy();
proxy.sing(new Guitar());
proxy.rest();
}
}
在前面的清单中,AnnotationMatchingPointcut的一个实例是通过调用它的静态方法forMethodAnnotation()并传入注释类型获得的。这表明我们希望将建议应用到所有用给定注释注释的方法中。也可以通过调用forClassAnnotation()方法来指定在类型级别应用的注释。下面显示了程序运行时的输出:
>> Invoking sing
play: G C G C Am D7
>> Done
zzz
如您所见,因为我们注释了sing()方法,所以只建议使用该方法。
便利顾问实现
对于许多Pointcut实现,Spring 还提供了一个方便的Advisor实现作为切入点。例如,在前面的例子中,我们没有使用NameMatchMethodPointcut和DefaultPointcutAdvisor,而是简单地使用了NameMatchMethodPointcutAdvisor,如下面的代码片段所示:
package com.apress.prospring5.ch5;
...
import org.springframework.aop.support.NameMatchMethodPointcutAdvisor;
public class NamePointcutUsingAdvisor {
public static void main(String... args) {
GrammyGuitarist johnMayer = new GrammyGuitarist();
NameMatchMethodPointcut pc = new NameMatchMethodPointcut();
pc.addMethodName("sing");
pc.addMethodName("rest");
Advisor advisor =
new NameMatchMethodPointcutAdvisor(new SimpleAdvice());
ProxyFactory pf = new ProxyFactory();
pf.setTarget(johnMayer);
pf.addAdvisor(advisor);
GrammyGuitarist proxy = (GrammyGuitarist) pf.getProxy();
proxy.sing();
proxy.sing(new Guitar());
proxy.rest();
proxy.talk();
}
}
注意,我们没有创建NameMatchMethodPointcut的实例,而是在NameMatchMethodPointcutAdvisor的实例上配置了切入点细节。这样,NameMatchMethodPointcutAdvisor既是顾问又是切入点。
通过研究org.springframework.aop.support包的 Javadoc,您可以找到不同Advisor实现的全部细节。这两种方法之间没有明显的性能差异,除了第二个示例中的代码稍微少一些之外,实际的编码方法几乎没有什么不同。我们更喜欢坚持第一种方法,因为我们觉得代码中的意图稍微清晰一些。归根结底,你选择的风格取决于个人喜好。
了解代理
到目前为止,我们只是粗略地看了一下由ProxyFactory生成的代理。我们提到过 Spring 中有两种类型的代理:使用 JDK Proxy类创建的 JDK 代理和使用 CGLIB Enhancer类创建的基于 CGLIB 的代理。您可能想知道这两种代理之间到底有什么区别,为什么 Spring 需要两种类型的代理。在本节中,我们将详细了解代理之间的差异。
代理的核心目标是拦截方法调用,并在必要时执行适用于特定方法的通知链。通知的管理和调用在很大程度上是独立于代理的,由 Spring AOP 框架管理。然而,代理负责拦截对所有方法的调用,并在必要时将它们传递给 AOP 框架,以便应用建议。
除了这个核心功能之外,代理还必须支持一组附加功能。可以将代理配置为通过AopContext类(一个抽象类)公开自己,这样就可以从目标对象中检索代理并调用代理上的建议方法。代理负责确保当通过ProxyFactory.setExposeProxy()启用该选项时,代理类被适当地公开。此外,默认情况下,所有代理类都实现了Advised接口,这允许在代理创建后修改通知链。代理还必须确保任何返回这个(即返回被代理的目标)的方法确实返回代理而不是目标。
如您所见,一个典型的代理有相当多的工作要做,所有这些逻辑都在 JDK 和 CGLIB 代理中实现。
使用 JDK 动态代理
JDK 代理是 Spring 中最基本的代理类型。与 CGLIB 代理不同,JDK 代理只能生成接口的代理,而不能生成类的代理。这样,您想要代理的任何对象都必须实现至少一个接口,并且生成的代理将是实现该接口的对象。图 5-4 显示了这种代理的抽象模式。

图 5-4。
JDK proxy abstract schema
一般来说,为类使用接口是一个好的设计,但这并不总是可行的,尤其是当您使用第三方或遗留代码时。在这种情况下,您必须使用 CGLIB 代理。当您使用 JDK 代理时,所有的方法调用都被 JVM 截获,并被路由到代理的invoke()方法。然后,该方法确定所讨论的方法是否被建议(由切入点定义的规则),如果是,它调用建议链,然后通过使用反射调用方法本身。除此之外,invoke()方法执行上一节讨论的所有逻辑。
JDK 代理在进入invoke()方法之前,不会在建议方法和未建议方法之间做出决定。这意味着对于代理上未修改的方法,仍然调用invoke()方法,仍然执行所有检查,并且仍然通过使用反射调用该方法。显然,每次调用方法时,这都会导致运行时开销,即使代理除了通过反射调用未经修改的方法之外,通常不执行任何额外的处理。
您可以通过使用setInterfaces()(在ProxyFactory类间接扩展的AdvisedSupport类中)指定要代理的接口列表来指示ProxyFactory使用 JDK 代理。
使用 CGLIB 代理
使用 JDK 代理,每次调用方法时,所有关于如何处理特定方法调用的决定都在运行时处理。当您使用 CGLIB 时,CGLIB 会为每个代理动态生成新类的字节码,尽可能重用已经生成的类。在这种情况下,产生的代理类型将是目标对象类的子类。图 5-5 显示了这种代理的抽象模式。

图 5-5。
CGLIB proxy abstract schema
当第一次创建 CGLIB 代理时,CGLIB 询问 Spring 它希望如何处理每个方法。这意味着在 JDK 代理上每次调用invoke()时执行的许多决策对于 CGLIB 代理只执行一次。因为 CGLIB 生成实际的字节码,所以处理方法的方式也更加灵活。例如,CGLIB 代理生成适当的字节码来直接调用任何未修改的方法,减少了代理带来的开销。此外,CGLIB 代理确定一个方法是否有可能返回这个;如果没有,它允许直接调用方法调用,再次减少了运行时开销。
CGLIB 代理处理固定通知链的方式也不同于 JDK 代理。固定通知链是在代理生成后保证不会改变的链。默认情况下,即使在代理创建之后,您也可以更改代理上的顾问和建议,尽管这很少是必需的。CGLIB 代理以一种特殊的方式处理固定通知链,减少了执行通知链的运行时开销。
比较代理性能
到目前为止,我们所做的只是泛泛地讨论了代理类型之间在实现上的差异。在本节中,我们将运行一个简单的测试来比较 CGLIB 代理和 JDK 代理的性能。
让我们创建一个名为DefaultSimpleBean的类,我们将使用它作为代理的目标对象。下面是SimpleBean接口和DefaultSimpleBean类:
ppackage com.apress.prospring5.ch5;
public interface SimpleBean {
void advised();
void unadvised();
}
public class DefaultSimpleBean implements SimpleBean {
private long dummy = 0;
@Override
public void advised() {
dummy = System.currentTimeMillis();
}
@Override
public void unadvised() {
dummy = System.currentTimeMillis();
}
}
在下面的例子中,TestPointcut类提供了对建议方法的静态检查:
package com.apress.prospring5.ch5;
import java.lang.reflect.Method;
import org.springframework.aop.support.StaticMethodMatcherPointcut;
public class TestPointcut extends StaticMethodMatcherPointcut {
@Override
public boolean matches(Method method, Class cls) {
return ("advise".equals(method.getName()));
}
}
下一个代码片段描述了NoOpBeforeAdvice类,它只是在没有任何操作的通知之前很简单:
package com.apress.prospring5.ch5;
import java.lang.reflect.Method;
import org.springframework.aop.MethodBeforeAdvice;
public class NoOpBeforeAdvice implements MethodBeforeAdvice {
@Override
public void before(Method method, Object args, Object target)
throws Throwable {
// no-op
}
}
在下面的代码片段中,您可以看到用于测试不同类型代理的代码:
package com.apress.prospring5.ch5;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
public class ProxyPerfTest {
public static void main(String... args) {
SimpleBean target = new DefaultSimpleBean();
Advisor advisor = new DefaultPointcutAdvisor(new TestPointcut(),
new NoOpBeforeAdvice());
runCglibTests(advisor, target);
runCglibFrozenTests(advisor, target);
runJdkTests(advisor, target);
}
private static void runCglibTests(Advisor advisor, SimpleBean target) {
ProxyFactory pf = new ProxyFactory();
pf.setProxyTargetClass(true);
pf.setTarget(target);
pf.addAdvisor(advisor);
SimpleBean proxy = (SimpleBean)pf.getProxy();
System.out.println("Running CGLIB (Standard) Tests");
test(proxy);
}
private static void runCglibFrozenTests(Advisor advisor, SimpleBean target) {
ProxyFactory pf = new ProxyFactory();
pf.setProxyTargetClass(true);
pf.setTarget(target);
pf.addAdvisor(advisor);
pf.setFrozen(true);
SimpleBean proxy = (SimpleBean) pf.getProxy();
System.out.println("Running CGLIB (Frozen) Tests");
test(proxy);
}
private static void runJdkTests(Advisor advisor, SimpleBean target) {
ProxyFactory pf = new ProxyFactory();
pf.setTarget(target);
pf.addAdvisor(advisor);
pf.setInterfaces(new Class{SimpleBean.class});
SimpleBean proxy = (SimpleBean)pf.getProxy();
System.out.println("Running JDK Tests");
test(proxy);
}
private static void test(SimpleBean bean) {
long before = 0;
long after = 0;
System.out.println("Testing Advised Method");
before = System.currentTimeMillis();
for(int x = 0; x < 500000; x++) {
bean.advised();
}
after = System.currentTimeMillis();
System.out.println("Took " + (after - before) + " ms");
System.out.println("Testing Unadvised Method");
before = System.currentTimeMillis();
for(int x = 0; x < 500000; x++) {
bean.unadvised();
}
after = System.currentTimeMillis();
System.out.println("Took " + (after - before) + " ms");
System.out.println("Testing equals() Method");
before = System.currentTimeMillis();
for(int x = 0; x < 500000; x++) {
bean.equals(bean);
}
after = System.currentTimeMillis();
System.out.println("Took " + (after - before) + " ms");
System.out.println("Testing hashCode() Method");
before = System.currentTimeMillis();
for(int x = 0; x < 500000; x++) {
bean.hashCode();
}
after = System.currentTimeMillis();
System.out.println("Took " + (after - before) + " ms");
Advised advised = (Advised)bean;
System.out.println("Testing Advised.getProxyTargetClass() Method");
before = System.currentTimeMillis();
for(int x = 0; x < 500000; x++) {
advised.getTargetClass();
}
after = System.currentTimeMillis();
System.out.println("Took " + (after - before) + " ms");
System.out.println(">>>\n");
}
}
在这段代码中,您可以看到您正在测试三种代理:
- 标准的 CGLIB 代理
- 一个冻结通知链的 CGLIB 代理(即当一个代理通过调用
ProxyFactory间接扩展的ProxyConfig类中的setFrozen()方法被冻结时,CGLIB 会进行进一步优化;但是,不允许进一步更改建议) - JDK 代理人
对于每种代理类型,您运行以下五个测试用例:
- 建议的方法(测试 1):这是一种建议的方法。测试中使用的通知类型在不执行任何处理的通知之前,因此它减少了通知对性能测试的影响。
- 未修改的方法(测试 2):这是代理上未修改的方法。通常你的代理有很多不被推荐的方法。这个测试着眼于未修改的方法在不同代理上的表现。
- equals()方法(测试 3):这个测试查看调用
equals()方法的开销。当您使用代理作为HashMap或类似集合中的键时,这尤其重要。 - hashCode()方法(测试 4):与
equals()方法一样,当您使用HashMap或类似的集合时,hashCode()方法很重要。 - 在 Advised 接口上执行方法(测试 5):正如我们前面提到的,默认情况下,代理实现了
Advised接口,允许您在创建后修改代理并查询关于代理的信息。这个测试着眼于使用不同的代理类型访问Advised接口上的方法有多快。
表 5-3 显示了这些测试的结果。
表 5-3。
Proxy Performance Test Results (in Milliseconds)
| | CGLIB(标准) | CGLIB(冻结) | java 开发工具包 | | --- | --- | --- | --- | | 建议的方法 | Two hundred and forty-five | One hundred and thirty-five | Two hundred and twenty-four | | 非修正方法 | Ninety-two | forty-two | seventy-eight | | `equals()` | nine | six | Seventy-seven | | `hashCode()` | Twenty-nine | Thirteen | Twenty-three | | `Advised.getProxyTargetClass()` | nine | six | Fifteen |正如你所看到的,标准 CGLIB 和 JDK 动态代理对于建议的和未建议的方法的性能差别不大。与往常一样,这些数字会因硬件和所用的 JDK 而异。
然而,当您使用带有冻结通知链的 CGLIB 代理时,有一个明显的区别。类似的数字也适用于equals()和hashCode()方法,当您使用 CGLIB 代理时,这两个方法明显更快。对于Advised接口上的方法,你会注意到它们在 CGLIB 冻结代理上也更快。原因是Advised方法在intercept()方法的早期被处理,因此它们避免了其他方法所需的许多逻辑。
选择要使用的代理
决定使用哪个代理通常很容易。CGLIB 代理可以代理类和接口,而 JDK 代理只能代理接口。在性能方面,JDK 和 CGLIB 标准模式之间没有明显的区别(至少在运行建议的和未建议的方法时),除非您在冻结模式下使用 CGLIB,在这种情况下,建议链不能更改,CGLIB 在冻结模式下执行进一步的优化。代理类时,CGLIB 代理是默认选择,因为它是唯一能够生成类代理的代理。要在代理接口时使用 CGLIB 代理,您必须通过使用setOptimize()方法将ProxyFactory中的optimize标志的值设置为true。
切入点的高级使用
在本章的前面,我们看了 Spring 提供的六个基本的Pointcut实现;在很大程度上,我们发现这些满足了我们应用的需求。然而,在定义切入点时,有时您可能需要更多的灵活性。Spring 提供了两个额外的Pointcut实现,ComposablePointcut和ControlFlowPointcut,它们提供了您所需要的灵活性。
使用控制流切入点
由ControlFlowPointcut类实现的 Spring 控制流切入点类似于许多其他 AOP 实现中可用的cflow构造,尽管它们没有那么强大。本质上,Spring 中的控制流切入点适用于一个给定方法下或一个类中所有方法下的所有方法调用。这很难想象,最好用一个例子来解释。
下面的代码片段显示了一个SimpleBeforeAdvice类,它写出一条描述它所建议的方法的消息:
package com.apress.prospring5.ch5;
import java.lang.reflect.Method;
import org.springframework.aop.MethodBeforeAdvice;
public class SimpleBeforeAdvice implements MethodBeforeAdvice {
@Override
public void before(Method method, Object args, Object target)
throws Throwable {
System.out.println("Before method: " + method);
}
}
这个通知类允许我们查看ControlFlowPointcut适用于哪些方法。在这里,您可以看到简单的TestBean类:
package com.apress.prospring5.ch5;
public class TestBean {
public void foo() {
System.out.println("foo()");
}
}
可以看到我们要建议的简单foo()方法。然而,我们有一个特殊的要求:只有当从另一个特定的方法调用这个方法时,我们才希望通知这个方法。以下代码片段显示了该示例的简单驱动程序:
package com.apress.prospring5.ch5;
import org.springframework.aop.Advisor;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.ControlFlowPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
public class ControlFlowDemo {
public static void main(String... args) {
ControlFlowDemo ex = new ControlFlowDemo();
ex.run();
}
public void run() {
TestBean target = new TestBean();
Pointcut pc = new ControlFlowPointcut(ControlFlowDemo.class,
"test");
Advisor advisor = new DefaultPointcutAdvisor(pc,
new SimpleBeforeAdvice());
ProxyFactory pf = new ProxyFactory();
pf.setTarget(target);
pf.addAdvisor(advisor);
TestBean proxy = (TestBean) pf.getProxy();
System.out.println("\tTrying normal invoke");
proxy.foo();
System.out.println("\n\tTrying under ControlFlowDemo.test()");
test(proxy);
}
private void test(TestBean bean) {
bean.foo();
}
}
在前面的代码片段中,advised 代理与ControlFlowPointcut组装在一起,然后foo()方法被调用两次,一次直接从main()方法调用,一次从test()方法调用。下面是特别有趣的一行:
Pointcut pc = new ControlFlowPointcut(ControlFlowDemo.class, "test");
在这一行中,我们为ControlFlowDemo类的test()方法创建了一个ControlFlowPointcut实例。本质上,这意味着,“切入点是从ControlFlowExample.test()方法调用的所有方法。”注意,尽管我们说“切入点所有方法”,实际上这实际上意味着“切入点代理对象上的所有方法,该代理对象被建议使用对应于这个ControlFlowPointcut实例的Advisor运行前面的示例会在控制台中产生以下结果:
Trying normal invoke
foo()
Trying under ControlFlowDemo.test()
Before method: public void com.apress.prospring5.ch5.TestBean.foo()
foo()
如您所见,当第一次在test()方法的控制流之外调用sing()方法时,它是未修改的。当它第二次执行时,这一次是在test()方法的控制流中,ControlFlowPointcut指示它的相关通知适用于该方法,因此该方法被通知。注意,如果我们从test()方法中调用了另一个方法,一个不在被通知的代理上的方法,它就不会被通知。
控制流切入点非常有用,允许您仅在一个对象在另一个对象的上下文中执行时选择性地通知该对象。但是,请注意,与其他切入点相比,使用控制流切入点会对性能造成很大影响。
让我们考虑一个例子。假设我们有一个事务处理系统,它包含一个TransactionService接口和一个AccountService接口。我们希望在通知后应用,以便当TransactionService.reverseTransaction()调用AccountService.updateBalance()方法时,在帐户余额更新后,向客户发送电子邮件通知。但是,在任何其他情况下都不会发送电子邮件。在这种情况下,控制流切入点将会很有用。图 5-6 显示了这个场景的 UML 序列图。

图 5-6。
UML sequence diagram for a control flow pointcut
使用可组合的切入点
在前面的切入点例子中,我们只为每个Advisor使用了一个切入点。在大多数情况下,这通常就足够了,但是在某些情况下,您可能需要将两个或更多的切入点组合在一起,以实现期望的目标。假设您想要横切 bean 上的所有 getter 和 setter 方法。您有一个 getters 的切入点和一个 setters 的切入点,但是您没有两个切入点。当然,您可以用新的逻辑创建另一个切入点,但是更好的方法是通过使用ComposablePointcut将两个切入点合并成一个切入点。
ComposablePointcut支持两种方式:union()和intersection()。默认情况下,ComposablePointcut是用匹配所有类的ClassFilter和匹配所有方法的MethodMatcher创建的,尽管您可以在构建期间提供自己的初始ClassFilter和MethodMatcher。union()和intersection()方法都被重载以接受ClassFilter和MethodMatcher参数。
可以通过传入ClassFilter、MethodMatcher或Pointcut接口的实例来调用ComposablePointcut.union()方法。联合操作的结果是ComposablePointcut将在它的调用链中添加一个“或”条件来匹配连接点。对于ComposablePointcut.intersection()方法也是如此,但是这次将添加一个“and”条件,这意味着ComposablePointcut中的所有ClassFilter、MethodMatcher和Pointcut定义都应该匹配以应用一个建议。您可以将它想象成 SQL 查询中的WHERE子句,其中的union()方法类似于“or”操作符,而intersection()方法类似于“and”操作符。
与控制流切入点一样,这很难可视化,但通过一个例子就更容易理解了。以下示例显示了前一示例中使用的GrammyGuitarist类及其四个方法:
package com.apress.prospring5.ch5;
import com.apress.prospring5.ch2.common.Guitar;
import com.apress.prospring5.ch2.common.Singer;
public class GrammyGuitarist implements Singer {
@Override public void sing() {
System.out.println("sing: Gravity is working against me\n" +
"And gravity wants to bring me down");
}
public void sing(Guitar guitar) {
System.out.println("play: " + guitar.play());
}
public void rest(){
System.out.println("zzz");
}
public void talk(){
System.out.println("talk");
}
}
在这个例子中,我们将使用同一个ComposablePointcut实例生成三个代理,但是每次,我们都将使用union()或intersection()方法来修改ComposablePointcut。接下来,我们将调用target bean 代理上的所有三个方法,并查看哪些方法已经被通知。以下代码示例对此进行了描述:
package com.apress.prospring5.ch5;
import java.lang.reflect.Method;
import org.springframework.aop.Advisor;
import org.springframework.aop.ClassFilter;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.ComposablePointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.StaticMethodMatcher;
public class ComposablePointcutExample {
public static void main(String... args) {
GrammyGuitarist johnMayer = new GrammyGuitarist();
ComposablePointcut pc = new ComposablePointcut(ClassFilter.TRUE,
new SingMethodMatcher());
System.out.println("Test 1 >> ");
GrammyGuitarist proxy = getProxy(pc, johnMayer);
testInvoke(proxy);
System.out.println();
System.out.println("Test 2 >> ");
pc.union(new TalkMethodMatcher());
proxy = getProxy(pc, johnMayer);
testInvoke(proxy);
System.out.println();
System.out.println("Test 3 >> ");
pc.intersection(new RestMethodMatcher());
proxy = getProxy(pc, johnMayer);
testInvoke(proxy);
}
private static GrammyGuitarist getProxy(ComposablePointcut pc,
GrammyGuitarist target) {
Advisor advisor = new DefaultPointcutAdvisor(pc,
new SimpleBeforeAdvice());
ProxyFactory pf = new ProxyFactory();
pf.setTarget(target);
pf.addAdvisor(advisor);
return (GrammyGuitarist) pf.getProxy();
}
private static void testInvoke(GrammyGuitarist proxy) {
proxy.sing();
proxy.sing(new Guitar());
proxy.talk();
proxy.rest();
}
private static class SingMethodMatcher extends StaticMethodMatcher {
@Override
public boolean matches(Method method, Class<?> cls) {
return (method.getName().startsWith("si"));
}
}
private static class TalkMethodMatcher extends StaticMethodMatcher {
@Override
public boolean matches(Method method, Class<?> cls) {
return "talk".equals(method.getName());
}
}
private static class RestMethodMatcher extends StaticMethodMatcher {
@Override
public boolean matches(Method method, Class<?> cls) {
return (method.getName().endsWith("st"));
}
}
}
在这个例子中首先要注意的是三个私有的MethodMatcher实现。SingMethodMatcher匹配所有以get开头的方法。这是我们用来组装ComposablePointcut的默认MethodMatcher。因此,我们预计对GrammyGuitarist方法的第一轮调用将导致只建议sing()方法。
TalkMethodMatcher匹配所有名为talk的方法,并通过使用union()进行第二轮调用来与ComposablePointcut结合。此时,我们有了两个MethodMatcher的并集——一个匹配所有以si开头的方法,另一个匹配所有名为talk的方法。我们现在期望第二轮中的所有调用都将被通知。TalkMethodMatcher非常具体,只匹配talk()方法。通过使用第三轮调用的intersection()将此MethodMatcher与ComposablePointcut结合。
因为RestMethodMatcher是通过使用intersection()来组合的,所以我们预计在第三轮中不会推荐任何方法,因为没有方法匹配所有组合的MethodMatcher。
运行此示例会产生以下输出:
Test 1 >>
Before method: public void
com.apress.prospring5.ch5.GrammyGuitarist.sing()
sing: Gravity is working against me
And gravity wants to bring me down
Before method: public void com.apress.prospring5.ch5.
GrammyGuitarist.sing(com.apress.prospring5.ch2.common.Guitar)
play: G C G C Am D7
talk
zzz
Test 2 >>
Before method: public void
com.apress.prospring5.ch5.GrammyGuitarist.sing()
sing: Gravity is working against me
And gravity wants to bring me down
Before method: public void
com.apress.prospring5.ch5.GrammyGuitarist.talk()
Before method: public void com.apress.prospring5.ch5.
GrammyGuitarist.sing(com.apress.prospring5.ch2.common.Guitar)
play: G C G C Am D7
talk
zzz
Test 3 >>
sing: Gravity is working against me
And gravity wants to bring me down
talk
zzz
虽然这个例子只演示了在合成过程中使用MethodMatcher s,但是在构建切入点时使用ClassFilter也同样简单。事实上,在构建复合切入点时,您可以使用MethodMatcher和ClassFilter的组合。
组合和切入点接口
在上一节中,您看到了如何通过使用多个MethodMatchers和ClassFilter来创建复合切入点。您还可以通过使用实现了Pointcut接口的其他对象来创建复合切入点。
构建复合切入点的另一种方法是使用org.springframework.aop.support.Pointcuts类。该类提供了三个静态方法。intersection()和union()方法都以两个切入点作为参数来构造一个复合切入点。另一方面,提供了一个matches(Pointcut, Method, Class, Object[])方法来快速检查切入点是否与提供的方法、类和方法参数匹配。
Pointcuts类只支持两个切入点的操作。所以,如果你需要将MethodMatcher和ClassFilter与Pointcut结合起来,你需要使用ComposablePointcut类。然而,当您只需要组合两个切入点时,Pointcuts类会更方便。
切入点摘要
Spring 提供了一组强大的Pointcut实现,可以满足应用的大部分需求。请记住,如果您找不到适合您需求的切入点,您可以通过实现Pointcut、MethodMatcher和ClassFilter从头开始创建您自己的实现。
您可以使用两种模式来组合切入点和顾问。第一种模式,也是我们到目前为止使用的模式,涉及到将切入点实现从顾问中分离出来。在我们到目前为止看到的代码中,我们创建了Pointcut实现的实例,然后使用DefaultPointcutAdvisor实现将通知和Pointcut一起添加到代理中。
第二种选择是将Pointcut封装在您自己的Advisor实现中,Spring 文档中的许多示例都采用了这种选择。这样,你就有了一个同时实现了Pointcut和PointcutAdvisor的类,而PointcutAdvisor.getPointcut()方法只是简单地返回这个。这是许多班级,比如StaticMethodMatcherPointcutAdvisor,在 Spring 中使用的方法。我们发现第一种方法是最灵活的,允许您使用不同的Pointcut实现和不同的Advisor实现。然而,第二种方法在您将在应用的不同部分或者跨许多应用使用相同的Pointcut和Advisor组合的情况下是有用的。
当每个Advisor必须有一个单独的Pointcut实例时,第二种方法是有用的;通过让Advisor负责创建Pointcut,您可以确保这一点。如果您还记得上一章中关于代理性能的讨论,您会记得未经修改的方法比被建议的方法性能好得多。出于这个原因,你应该确保,通过使用Pointcuts,你只建议绝对必要的方法。这样,您可以通过使用 AOP 来减少应用中不必要的开销。
开始介绍
介绍是 Spring 中可用的 AOP 特性集的重要部分。通过使用引入,您可以动态地向现有对象引入新功能。在 Spring 中,您可以向现有对象引入任何接口的实现。您很可能想知道这到底为什么有用。当您可以在开发时简单地添加功能时,为什么要在运行时动态添加功能呢?这个问题的答案很简单。当功能是横切的,并且使用传统的建议不容易实现时,您可以动态地添加功能。
简介基础
Spring 将介绍视为一种特殊类型的建议,更确切地说,是一种特殊类型的迂回建议。因为介绍只适用于类级别,所以不能在介绍中使用切入点;语义上,两者不匹配。简介向类中添加新的接口实现,切入点定义通知应用于哪些方法。您通过实现IntroductionInterceptor接口来创建一个介绍,该接口扩展了MethodInterceptor和DynamicIntroductionAdvice接口。图 5-7 显示了这个结构以及两个接口的方法,正如 IntelliJ IDEA UML 插件所描述的。如您所见,MethodInterceptor接口定义了一个invoke()方法。使用此方法,您可以为正在引入的接口提供实现,并根据需要对任何其他方法执行拦截。在一个方法中实现一个接口的所有方法可能会很麻烦,而且很可能会产生大量代码,您不得不费力地去决定调用哪个方法。幸运的是,Spring 提供了一个默认的IntroductionInterceptor实现,称为DelegatingIntroductionInterceptor,这使得创建介绍更加简单。要使用DelegatingIntroductionInterceptor构建一个介绍,您需要创建一个类,它继承了DelegatingIntroductionInterceptor并实现了您想要介绍的接口。然后,DelegatingIntroductionInterceptor实现简单地将所有对引入方法的调用委托给自身的相应方法。如果这看起来有点不清楚,不要担心;您将在下一节看到它的一个例子。

图 5-7。
Interface structure for introductions
正如您在处理切入点建议时需要使用PointcutAdvisor一样,您需要使用IntroductionAdvisor向代理添加介绍。IntroductionAdvisor的默认实现是DefaultIntroductionAdvisor,这应该满足您的大部分(如果不是全部)介绍需求。你应该知道使用ProxyFactory.addAdvice()添加介绍是不允许的,会导致AopConfigException被抛出。相反,您应该使用addAdvisor()方法并传递一个IntroductionAdvisor接口的实例。
当使用标准建议时——也就是说,不是介绍——同一个建议实例有可能用于许多对象。Spring 文档称之为每个类的生命周期,尽管您可以为许多类使用一个 advice 实例。对于介绍,介绍通知构成了被通知对象状态的一部分,因此,对于每个被通知对象,您必须有一个不同的通知实例。这称为每个实例的生命周期。因为您必须确保每个被通知的对象都有一个不同的介绍实例,所以创建一个负责创建介绍通知的子类DefaultIntroductionAdvisor通常更好。这样,您只需要确保为每个对象创建一个 advisor 类的新实例,因为它会自动创建一个新的介绍实例。例如,假设您想在Contact类的所有实例上对setFirstName()方法应用 before advice。图 5-8 显示了适用于所有Contact类型对象的相同建议。现在让我们假设您想要将一个介绍混合到Contact类的所有实例中,并且该介绍将携带每个Contact实例的信息(例如,一个属性isModified指示特定实例是否被修改)。

图 5-8。
Per-class life cycle of advice
在这种情况下,将为Contact的每个实例创建一个介绍,并绑定到该特定实例,如图 5-9 所示。这涵盖了创建简介的基础。我们现在将讨论如何使用介绍来解决对象修改检测的问题。

图 5-9。
Per-instance introduction
带有介绍的对象修改检测
由于许多原因,对象修改检测是一种有用的技术。通常,在保存对象数据时,应用修改检测来防止不必要的数据库访问。如果一个对象被传递给一个方法进行修改,但是它没有被修改就返回,那么向数据库发出 update 语句就没有什么意义了。以这种方式使用修改检查确实可以增加应用的吞吐量,特别是当数据库已经处于相当大的负载之下或者位于远程网络上,使得通信成为一种昂贵的操作时。
不幸的是,这种功能很难手工实现,因为它需要您添加到每个可以修改对象状态的方法中,以检查对象状态是否真的被修改了。当您考虑所有必须进行的null检查以及查看值是否真正改变的检查时,您会看到每个方法大约有八行代码。您可以将其重构为一个方法,但是每次需要执行检查时,您仍然需要调用这个方法。如果将这种情况扩散到一个有许多需要修改检查的类的典型应用中,灾难就要发生了。
这显然是一个介绍会有所帮助的地方。我们不希望每个需要修改检查的类都从某个基实现继承,从而失去继承的唯一机会,我们也不希望为每个状态改变方法添加检查代码。使用简介,我们可以为修改检测问题提供灵活的解决方案,而不必编写一堆重复的、容易出错的代码。
在这个例子中,我们将使用简介构建一个完整的修改检查框架。修改检查逻辑被封装在IsModified接口中,该接口的实现将被引入到适当的对象中,与拦截逻辑一起自动执行修改检查。出于这个例子的目的,我们使用 JavaBeans 约定,因为我们认为修改是对 setter 方法的任何调用。当然,我们不只是把对 setter 方法的所有调用都视为修改;我们检查传递给 setter 的值是否不同于当前存储在对象中的值。这种解决方案的唯一缺陷是,如果对象上的任何一个值发生了变化,将对象设置回其原始状态仍然会反映出修改。例如,您有一个带有firstName属性的Contact对象。假设在处理过程中,firstName属性从Peter更改为John。因此,该对象被标记为已修改。然而,它仍将被标记为已修改,即使该值在随后的处理中从John变回其原始值Peter。跟踪此类变更的一种方法是存储对象整个生命周期中的全部变更历史。然而,这里的实现并不简单,可以满足大多数需求。实现更完整的解决方案会导致一个过于复杂的例子。
使用 IsModified 接口
修改检查解决方案的核心是IsModified接口,虚拟应用使用它来做出关于对象持久性的智能决策。我们不讨论应用如何使用IsModified;相反,我们将把重点放在介绍的实现上。下面的代码片段显示了IsModified界面:
package com.apress.prospring5.ch5.introduction;
public interface IsModified {
boolean isModified();
}
这里没有什么特别的—只有一个方法,isModified(),指示对象是否被修改。
创建 Mixin
下一步是创建实现IsModified并被引入到对象中的代码;这被称为 mixin。正如我们前面提到的,通过子类化DelegatingIntroductionInterceptor来创建 mixinss 比通过直接实现IntroductionInterceptor接口来创建 mixin 要简单得多。mixin 类IsModifiedMixin是DelegatingIntroductionInterceptor的子类,也实现了IsModified接口。如下所示:
package com.apress.prospring5.ch5.introduction;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.support.DelegatingIntroductionInterceptor;
public class IsModifiedMixin extends DelegatingIntroductionInterceptor
implements IsModified {
private boolean isModified = false;
private Map<Method, Method> methodCache = new HashMap<>();
@Override
public boolean isModified() {
return isModified;
}
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
if (!isModified) {
if ((invocation.getMethod().getName().startsWith("set"))
&& (invocation.getArguments().length == 1)) {
Method getter = getGetter(invocation.getMethod());
if (getter != null) {
Object newVal = invocation.getArguments()0;
Object oldVal = getter.invoke(invocation.getThis(), null);
if((newVal == null) && (oldVal == null)) {
isModified = false;
} else if((newVal == null) && (oldVal != null)) {
isModified = true;
} else if((newVal != null) && (oldVal == null)) {
isModified = true;
} else {
isModified = !newVal.equals(oldVal);
}
}
}
}
return super.invoke(invocation);
}
private Method getGetter(Method setter) {
Method getter = methodCache.get(setter);
if (getter != null) {
return getter;
}
String getterName = setter.getName().replaceFirst("set", "get");
try {
getter = setter.getDeclaringClass().getMethod(getterName, null);
synchronized (methodCache) {
methodCache.put(setter, getter);
}
return getter;
} catch (NoSuchMethodException ex) {
return null;
}
}
}
这里首先要注意的是IsModified的实现,它由私有的 modified 字段和isModified()方法组成。这个例子强调了为什么每个被通知的对象必须有一个 mixin 实例——mixin 不仅向对象引入了方法,还引入了状态。如果您在许多对象之间共享这个 mixin 的单个实例,那么您也在共享状态,这意味着当单个对象第一次被修改时,所有对象都显示为已修改。
您实际上不必为 mixin 实现invoke()方法,但是在这种情况下,这样做允许我们在修改发生时自动检测。我们首先只在对象仍未被修改的情况下执行检查;一旦我们知道对象已经被修改,我们就不需要检查修改。接下来,我们检查该方法是否是 setter,如果是,我们检索相应的 getter 方法。注意,我们缓存了 getter/setter 对,以便将来更快地检索。最后,我们将 getter 返回的值与传递给 setter 的值进行比较,以确定是否发生了修改。请注意,我们检查了null的不同可能组合,并适当地设置了修改。重要的是要记住,当您使用DelegatingIntroductionInterceptor时,您必须在覆盖invoke()时调用super.invoke(),因为是DelegatingIntroductionInterceptor将调用分派到正确的位置,要么是被通知的对象,要么是 mixin 本身。
您可以在 mixin 中实现任意多的接口,每个接口都会被自动引入到 advised 对象中。
创建顾问
下一步是创建一个Advisor来包装 mixin 类的创建。这一步是可选的,但是它确实有助于确保 mixin 的一个新实例被用于每个被通知的对象。下面的代码片段显示了IsModifiedAdvisor类:
package com.apress.prospring5.ch5.introduction;
import org.springframework.aop.support.DefaultIntroductionAdvisor;
public class IsModifiedAdvisor extends DefaultIntroductionAdvisor {
public IsModifiedAdvisor() {
super(new IsModifiedMixin());
}
}
注意,我们已经扩展了DefaultIntroductionAdvisor来创建您的IsModifiedAdvisor。这个 advisor 的实现是简单明了的。
把所有的放在一起
现在我们有了一个 mixin 类和一个Advisor类,我们可以测试修改检查框架了。我们将要使用的类是前面提到的Contact类,它是common包的一部分。出于可重用性的原因,这个类经常被用作本书中项目的依赖项。此类的内容如下所示:
package com.apress.prospring5.ch2.common;
public class Contact {
private String name;
private String phoneNumber;
private String email;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
// getters and setter for other fields
...
}
这个 bean 有一组属性,但是只有用于测试修改检查 mixin 的name属性。以下代码片段显示了如何组装 advised 代理,然后测试修改检查代码:
package com.apress.prospring5.ch5;
import com.apress.prospring5.ch2.common.Contact;
import com.apress.prospring5.ch5.introduction.IsModified;
import com.apress.prospring5.ch5.introduction.IsModifiedAdvisor;
import org.springframework.aop.IntroductionAdvisor;
import org.springframework.aop.framework.ProxyFactory;
public class IntroductionDemo {
public static void main(String... args) {
Contact target = new Contact();
target.setName("John Mayer");
IntroductionAdvisor advisor = new IsModifiedAdvisor();
ProxyFactory pf = new ProxyFactory();
pf.setTarget(target);
pf.addAdvisor(advisor);
pf.setOptimize(true
);
Contact proxy = (Contact) pf.getProxy();
IsModified proxyInterface = (IsModified)proxy;
System.out.println("Is Contact?: " + (proxy instanceof Contact));
System.out.println("Is IsModified?: " + (proxy instanceof IsModified));
System.out.println("Has been modified?: " +
proxyInterface.isModified());
proxy.setName("John Mayer");
System.out.println("Has been modified?: " +
proxyInterface.isModified());
proxy.setName("Eric Clapton");
System.out.println("Has been modified?: " +
proxyInterface.isModified());
}
}
注意,当我们创建代理时,我们将optimize标志设置为true来强制使用 CGLIB 代理。这样做的原因是,当你使用 JDK 代理引入一个 mixin 时,产生的代理将不会是 object 类的一个实例(在这个例子中是Contact);代理只实现 mixin 接口,不实现原始类。使用 CGLIB 代理,原始类由代理和 mixin 接口一起实现。
注意,在代码中,我们首先测试代理是否是Contact的实例,然后测试它是否是IsModified的实例。当您使用 CGLIB 代理时,两个测试都返回true,但是对于 JDK 代理,只有IsModified测试返回true。最后,我们测试修改检查代码,首先将name属性设置为其当前值,然后设置为新值,每次检查isModified标志的值。运行此示例会产生以下输出:
Is Contact?: true
Is IsModified?: true
Has been modified?: false
Has been modified?: false
Has been modified?: true
正如所料,两个instanceof测试都返回true。注意,在任何修改发生之前,对isModified()的第一次调用返回false。在我们将name的值设置为相同的值之后,下一个调用也返回false。然而,对于最后一次调用,在我们将 name 的值设置为一个新值之后,isModified()方法返回true,表明该对象实际上已经被修改。
简介摘要
介绍是 Spring AOP 最强大的特性之一;它们不仅允许您扩展现有方法的功能,还允许您动态扩展一组接口和对象实现。使用引入是实现横切逻辑的最佳方式,应用通过定义良好的接口与横切逻辑进行交互。通常,这是您希望以声明方式而不是以编程方式应用的那种逻辑。通过使用本例中定义的IsModifiedMixin和下一节讨论的框架服务,我们可以声明性地定义哪些对象能够进行修改检查,而不需要修改这些对象的实现。
显然,因为介绍是通过代理进行的,所以会增加一定的开销。代理上的所有方法都被认为是建议的,因为切入点不能与引入一起使用。但是,对于许多可以通过使用对象修改检查之类的介绍来实现的服务来说,这种性能开销对于实现服务所需代码的减少以及完全集中服务逻辑带来的稳定性和可维护性的提高来说是很小的代价。
面向 AOP 的框架服务
到目前为止,我们不得不编写大量代码来通知对象并为它们生成代理。尽管这本身并不是一个大问题,但它确实意味着所有的通知配置都被硬编码到您的应用中,从而消除了能够透明地通知方法实现的一些好处。幸运的是,Spring 提供了额外的框架服务,允许您在应用配置中创建一个建议代理,然后将这个代理注入到目标 bean 中,就像任何其他依赖项一样。
使用 AOP 配置的声明性方法比手工编程机制更好。当您使用声明性机制时,您不仅外部化了通知的配置,还减少了编码错误的机会。您还可以利用 DI 和 AOP 的结合来启用 AOP,以便可以在完全透明的环境中使用它。
以声明方式配置 AOP
当使用 Spring AOP 的声明性配置时,有三种选择。
- 使用 ProxyFactoryBean:在 Spring AOP 中,
ProxyFactoryBean提供了一种声明性的方式来配置 Spring 的ApplicationContext(以及底层的BeanFactory)当基于定义的 Spring beans 创建 AOP 代理时。 - 使用 Spring aop 名称空间:在 Spring 2.0 中引入,
aop名称空间提供了一种简化的方式(与ProxyFactoryBean相比)来定义 Spring 应用中的方面及其 DI 需求。然而,aop名称空间也在幕后使用ProxyFactoryBean。 - 使用@AspectJ 样式的注释:除了基于 XML 的
aop名称空间,还可以在类中使用@AspectJ-样式的注释来配置 Spring AOP。虽然它使用的语法是基于 AspectJ 的,并且在使用这个选项时需要包含一些 AspectJ 库,但是 Spring 在引导ApplicationContext时仍然使用代理机制(也就是为目标创建代理对象)。
使用 ProxyFactoryBean
ProxyFactoryBean类是FactoryBean的一个实现,它允许您指定一个 bean 作为目标,并且它为这个 bean 提供了一组建议和顾问,这些建议和顾问最终被合并到一个 AOP 代理中。ProxyFactoryBean用于将拦截器逻辑应用于现有的目标 bean,当调用该 bean 上的方法时,拦截器在该方法调用之前和之后执行。因为可以通过ProxyFactoryBean使用 advisor 和 advice,所以不仅可以声明性地配置 advice,还可以配置切入点。
ProxyFactoryBean与ProxyFactory共享一个公共接口(org.springframework.aop.framework.Advised接口)(两个类都间接扩展了org.springframework.aop.framework.AdvisedSupport类,后者实现了Advised接口),因此,它公开了许多相同的标志,如frozen、optimize和exposeProxy。这些标志的值直接传递给底层的ProxyFactory,这也允许您以声明方式配置工厂。
ProxyFactoryBean 运行中
使用ProxyFactoryBean很简单。您定义一个将成为目标 bean 的 bean,然后使用ProxyFactoryBean,您定义您的应用将访问的 bean,使用目标 bean 作为代理目标。在可能的情况下,将目标 bean 定义为代理 bean 声明中的匿名 bean。这可以防止您的应用意外访问未经修改的 bean。然而,在某些情况下,比如我们将要向您展示的示例,您可能想要为同一个 bean 创建多个代理,因此对于这种情况,您应该使用一个普通的顶级 bean。
对于下面的例子,想象一下这个场景:你有一个歌手和一个纪录片制作人一起制作一个巡演的纪录片。在这种情况下,Documentarist依赖于Singer的实现。这里我们将使用的Singer实现是之前介绍的GrammyGuitarist。这里再次显示了内容:
package com.apress.prospring5.ch5;
import com.apress.prospring5.ch2.common.Guitar;
import com.apress.prospring5.ch2.common.Singer;
public class GrammyGuitarist implements Singer {
@Override public void sing() {
System.out.println("sing: Gravity is working against me\n" +
"And gravity wants to bring me down");
}
public void sing(Guitar guitar) {
System.out.println("play: " + guitar.play());
}
public void rest(){
System.out.println("zzz");
}
public void talk(){
System.out.println("talk");
}
}
这个Documentarist类基本上会告诉歌手在拍摄纪录片时该做什么,如下所示:
package com.apress.prospring5.ch5;
public class Documentarist {
private GrammyGuitarist guitarist;
public void execute() {
guitarist.sing();
guitarist.talk();
}
public void setDep(GrammyGuitarist guitarist) {
this.guitarist = guitarist;
}
}
对于这个例子,我们将为一个单独的GrammySinger实例创建两个代理,两者都使用这里显示的相同的基本建议:
package com.apress.prospring5.ch5;
import org.aspectj.lang.JoinPoint;
public class AuditAdvice {
public void simpleBeforeAdvice(JoinPoint joinPoint) {
System.out.println("Executing: " +
joinPoint.getSignature().getDeclaringTypeName() + " "
+ joinPoint.getSignature().getName());
}
}
第一代理将通过直接使用建议来建议目标;因此,将建议所有方法。对于第二个代理,我们将配置AspectJExpressionPointcut和DefaultPointcutAdvisor,以便只建议使用GrammySinger类的sing()方法。为了测试这个建议,我们将创建两个类型为Documentarist的 bean 定义,每个定义将被注入一个不同的代理。然后,我们将调用每个 beans 上的execute()方法,并观察调用依赖项上的建议方法时会发生什么。图 5-10 显示了该示例的配置(app-context-xml.xml)。我们使用了一个图像来描述这个配置,因为它可能看起来有点混乱,我们希望确保很容易看到每个 bean 被注入的位置。在本例中,我们只是简单地设置了使用 Spring 的 DI 功能在代码中设置的属性。唯一有趣的是,我们使用匿名 bean 作为切入点,并且使用了ProxyFactoryBean类。当切入点不被共享时,我们更喜欢使用匿名 bean 作为切入点,因为这使得可以直接访问的 bean 集尽可能小,并且尽可能与应用相关。当您使用ProxyFactoryBean时,要认识到的重要一点是ProxyFactoryBean声明是向您的应用公开的声明,也是当您实现依赖时要使用的声明。不建议使用底层的目标 bean 声明,所以只有当您想要绕过 AOP 框架时,才应该使用这个 bean,尽管一般来说,您的应用不应该知道 AOP 框架,因此也不应该想要绕过它。因此,您应该尽可能使用匿名 beans 来避免应用的意外访问。

图 5-10。
Declarative AOP configuration pict this configuration because it might look a little confusing and we wanted to make sure it is easy to see where each bean is injected. In the example, we are simply setting the properties that we set in code using Spring’s DI capabilities. The only points of interest are that we use an anonymous bean for the pointcut, and we use the
下面的代码片段显示了一个简单的类,它从ApplicationContext获得两个Documentarist实例,然后为每个实例运行execute()方法:
package com.apress.prospring5.ch5;
import org.springframework.context.support.GenericXmlApplicationContext;
public class ProxyFactoryBeanDemo {
public static void main(String... args) {
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext();
ctx.load("spring/app-context-xml.xml");
ctx.refresh();
Documentarist documentaristOne =
ctx.getBean("documentaristOne", Documentarist.class);
Documentarist documentaristTwo =
ctx.getBean("documentaristTwo", Documentarist.class)
;
System.out.println("Documentarist One >>");
documentaristOne.execute();
System.out.println("\nDocumentarist Two >> ");
documentaristTwo.execute();
}
}
运行此示例会产生以下输出:
Documentarist One >>
Executing: public void com.apress.prospring5.ch5.GrammyGuitarist.sing()
sing: Gravity is working against me
And gravity wants to bring me down
Executing: public void com.apress.prospring5.ch5.GrammyGuitarist.talk()
talk
Documentarist Two >>
Executing: public void com.apress.prospring5.ch5.GrammyGuitarist.sing()
sing: Gravity is working against me
And gravity wants to bring me down
talk
正如所料,第一个代理中的sing()和talk()方法都被建议,因为在其配置中没有使用切入点。然而,对于第二个代理,由于配置中使用的切入点,只建议使用sing()方法。
使用 ProxyFactoryBean 进行介绍
使用ProxyFactoryBean类不仅可以通知一个对象,还可以向对象引入混合。记住前面关于介绍的讨论,您必须使用IntroductionAdvisor来添加介绍;您不能直接添加简介。当你在介绍中使用ProxyFactoryBean时,同样的规则也适用。当你使用ProxyFactoryBean时,如果你为你的 mixin 创建了一个定制的Advisor,配置你的代理会变得容易得多。以下配置片段显示了本章前面的IsModifiedMixin简介(app-context-xml.xml)的示例配置:
<beans ...>
<bean id="guitarist"
class="com.apress.prospring5.ch2.common.Contact"
p:name="John Mayer"/>
<bean id="advisor"
class="com.apress.prospring5.ch5.introduction.IsModifiedAdvisor"/>
<util:list id="interceptorAdvisorNames">
<value>advisor</value>
</util:list>
<bean id="bean"
class="org.springframework.aop.framework.ProxyFactoryBean"
p:target-ref="guitarist"
p:interceptorNames-ref="interceptorAdvisorNames"
p:proxyTargetClass="true">
</bean>
</beans>
从配置中可以看到,我们使用IsModifiedAdvisor类作为ProxyFactoryBean的顾问,因为我们不需要创建同一个目标对象的另一个代理,所以我们对目标 bean 使用匿名声明。下面的代码片段显示了从ApplicationContext获得代理的前面的简介示例的修改:
package com.apress.prospring5.ch5;
import com.apress.prospring5.ch2.common.Contact;
import com.apress.prospring5.ch5.introduction.IsModified;
import org.springframework.context.support.GenericXmlApplicationContext;
public class IntroductionConfigDemo {
public static void main(String... args) {
GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-xml.xml");
ctx.refresh()
;
Contact bean = (Contact) ctx.getBean("bean");
IsModified mod = (IsModified) bean;
System.out.println("Is Contact?: " + (bean instanceof Contact));
System.out.println("Is IsModified?: " + (bean instanceof IsModified));
System.out.println("Has been modified?: " + mod.isModified());
bean.setName("John Mayer");
System.out.println("Has been modified?: " + mod.isModified());
bean.setName("Eric Clapton");
System.out.println("Has been modified?: " + mod.isModified());
}
}
运行这个示例会产生与前面的介绍示例完全相同的输出,但是这次代理是从ApplicationContext获得的,并且应用代码中没有配置。
因为我们已经介绍了 Java 配置,所以前面描述的 XML 配置可以替换为如下所示的配置类:
package com.apress.prospring5.ch5.config;
import com.apress.prospring5.ch2.common.Contact;
import com.apress.prospring5.ch5.introduction.IsModifiedAdvisor;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.ProxyFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig
{
@Bean
public Contact guitarist() {
Contact guitarist = new Contact();
guitarist.setName("John Mayer");
return guitarist;
}
@Bean
public Advisor advisor() {
return new IsModifiedAdvisor();
}
@Bean ProxyFactoryBean bean() {
ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
proxyFactoryBean.setTarget(guitarist());
proxyFactoryBean.setProxyTargetClass(true);
proxyFactoryBean.addAdvisor(advisor());
return proxyFactoryBean;
}
}
为了测试前面的类是否真的工作,在类IntroductionConfigDemo的main()方法中,用下面的代码替换初始化上下文的代码行:
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
配置类的不同之处在于,不需要通过名称引用advisor bean 或将其添加到列表中作为参数提供给ProxyFactoryBean,因为可以直接调用addAdvisor(..),并且可以作为参数提供advisor bean。这显然简化了配置。
ProxyFactoryBean 摘要
当您使用ProxyFactoryBean时,您可以配置 AOP 代理,提供编程方法的所有灵活性,而不需要将您的应用耦合到 AOP 配置。除非您需要在运行时决定如何创建代理,否则最好使用代理配置的声明性方法,而不是编程性方法。让我们继续,这样您就可以看到声明性 Spring AOP 的另外两个选项,这两个选项都是基于 Spring 2.0 或更新版本以及 JDK 5 或更新版本的应用的首选选项。
使用 aop 名称空间
aop名称空间为声明性 Spring AOP 配置提供了一个大大简化的语法。为了向您展示它是如何工作的,让我们重用前面的ProxyFactoryBean例子,稍微修改一下版本,以便演示它的用法。上一个例子中的GrammyGuitarist类仍然被使用,但是Documentarist将被扩展来调用带有Guitar参数的sing()方法。
package com.apress.prospring5.ch5;
import com.apress.prospring5.ch2.common.Guitar;
public class NewDocumentarist extends Documentarist {
@Override
public void execute() {
guitarist.sing();
guitarist.sing(new Guitar());
guitarist.talk();
}
}
建议类更改如下:
package com.apress.prospring5.ch5;
import org.aspectj.lang.JoinPoint;
public class SimpleAdvice {
public void simpleBeforeAdvice(JoinPoint joinPoint) {
System.out.println("Executing: " +
joinPoint.getSignature().getDeclaringTypeName() + " "
+ joinPoint.getSignature().getName());
}
}
您将看到 advice 类不再需要实现MethodBeforeAdvice接口。此外,before 通知接受连接点作为参数,但不接受方法、对象和参数。实际上,对于 advice 类,这个参数是可选的,所以您可以让这个方法没有参数。但是,如果在通知中您需要访问被通知的连接点的信息(在这种情况下,我们希望转储调用类型和方法名的信息),那么我们需要定义参数的接受。当为方法定义参数时,Spring 会自动将连接点传递到方法中进行处理。下面是来自app-context-xml-01.xml文件的带有aop名称空间的 Spring XML 配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<aop:config>
<aop:pointcut id="singExecution"
expression="execution(
* com.apress.prospring5.ch5..sing*(com.apress.prospring5.ch2.common.Guitar)
)"/>
<aop:aspect ref="advice">
<aop:before pointcut-ref="singExecution"
method="simpleBeforeAdvice"/>
</aop:aspect>
</aop:config>
<bean id="advice"
class="com.apress.prospring5.ch5.SimpleAdvice"/>
<bean id="johnMayer"
class="com.apress.prospring5.ch5.GrammyGuitarist"/>
<bean id="documentarist"
class="com.apress.prospring5.ch5.NewDocumentarist"
p:guitarist-ref="johnMayer"/>
</beans>
首先,我们需要在<beans>标签中声明aop名称空间。第二,所有的 Spring AOP 配置都放在标签<aop:config>下。在<aop:config>下,您可以定义切入点、方面、顾问等等,并像往常一样引用其他 Spring beans。
在前面的配置中,我们定义了一个 ID 为singExecution的切入点。表情
"execution(*
com.apress.prospring5.ch5..sing*(com.apress.prospring5.ch2.common.Guitar)
)"
意味着我们要通知所有带有前缀sing的方法,并且类是在包com.apress.prospring5.ch5下定义的(包括所有的子包)。另外,sing()方法应该接收一个类型为Guitar的参数。随后,通过使用<aop:aspect>标签声明了方面,advice 类引用了 ID 为advice的 Spring bean,它是SimpleAdvice类。pointcut-ref属性引用 ID 为singExecution的已定义切入点,而 before advice(使用<aop:before>标签声明)是 advice bean 中的方法simpleBeforeAdvice ()。要测试前面的配置,可以使用下面的类:
package com.apress.prospring5.ch5;
import org.springframework.context.support.GenericXmlApplicationContext;
public class AopNamespaceDemo {
public static void main(String... args) {
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-xml-01.xml");
ctx.refresh();
NewDocumentarist documentarist =
ctx.getBean("documentarist", NewDocumentarist.class);
documentarist.execute();
ctx.close();
}
}
在这个例子中,我们简单地像往常一样初始化ApplicationContext,检索 bean,并调用它的execute()方法。运行该程序将产生以下输出:
sing: Gravity is working against me
And gravity wants to bring me down
Executing: com.apress.prospring5.ch5.GrammyGuitarist sing
play: G C G C Am D7
talk
如您所见,只建议调用带有Guitar参数的sing(..)方法;没有参数的sing()方法和talk()方法则没有。这完全符合预期,您可以看到与ProxyFactoryBean配置相比,配置大大简化了。
让我们进一步把前面的例子修改成稍微复杂一点的情况。假设现在我们只想建议那些带有 Spring beans 的方法,这些方法的 ID 以john开始,参数类型为Guitar,参数的brand属性设置为Gibson。
为此,首先必须更改Guitar类以添加brand属性。我们将使它成为非强制性的,并用默认值填充它,只是为了保持前面的例子正常工作。
package com.apress.prospring5.ch2.common;
public class Guitar {
private String brand =" Martin";
public String play(){
return "G C G C Am D7";
}
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
}
然后我们需要用一个特殊牌子的吉他让NewDocumentarist调用sing()方法。
package com.apress.prospring5.ch5;
import com.apress.prospring5.ch2.common.Guitar;
public class NewDocumentarist extends Documentarist {
@Override
public void execute() {
guitarist.sing();
Guitar guitar = new Guitar();
guitar.setBrand("Gibson");
guitarist.sing(guitar);
guitarist.talk();
}
}
现在我们需要一种新的更复杂的建议。参数guitar被添加到 before 建议的签名中。第二,在通知中,我们只在参数的brand属性等于Gibson时检查和执行逻辑。
package com.apress.prospring5.ch5;
import com.apress.prospring5.ch2.common.Guitar;
import org.aspectj.lang.JoinPoint;
public class ComplexAdvice {
public void simpleBeforeAdvice(JoinPoint joinPoint, Guitar value) {
if(value.getBrand().equals("Gibson")) {
System.out.println("Executing: " +
joinPoint.getSignature().getDeclaringTypeName() + " "
+ joinPoint.getSignature().getName());
}
}
}
此外,XML 配置需要修改,因为我们需要使用新类型的通知并更新切入点表达式。(你可以在app-context-xml-02.xml中找到完整的配置,除了下面显示的几行,其他的都和app-context-xml-01.xml的内容一样,这里不再赘述。)
<beans ..>
...
<bean id="advice"
class="com.apress.prospring5.ch5.ComplexAdvice"/>
<aop:config>
<aop:pointcut id="singExecution"
expression="execution(* sing*(com.apress.prospring5.ch2.common.Guitar))
and args(value) and bean(john*)"/>
</beans>
切入点表达式中又增加了两条指令。首先,args(value)指示 Spring 也将名为value的参数传递到 before 建议中。其次,bean(john*)指令指示 Spring 只通知 ID 前缀为john的 beans。这是一个强大的功能;如果您有一个定义良好的 Spring beans 命名结构,您可以很容易地建议您想要的对象。例如,您可以使用bean(*DAO*)获得适用于所有 DAO beans 的建议,或者使用bean(*Service*)获得适用于所有服务层 bean 的建议,而不是使用完全限定的类名进行匹配。使用新的配置文件app-context-xml02.xml运行相同的测试程序会产生以下输出:
sing: Gravity is working against me
And gravity wants to bring me down
Executing: com.apress.prospring5.ch5.GrammyGuitarist sing
play: G C G C Am D7
talk
您可以看到,只建议了带有Guitar参数并且brand等于Gibson的sing()方法。
让我们再看一个将aop名称空间用于 around 通知的例子。我们可以简单地给ComplexAdvice类添加一个新方法,而不是创建另一个类来实现MethodInterceptor接口。以下代码示例显示了修订后的ComplexAdvice类中名为simpleAroundAdvice()的新方法:
//ComplexAdvice.java
public Object simpleAroundAdvice(ProceedingJoinPoint pjp,
Guitar value) throws Throwable {
System.out.println("Before execution: " +
pjp.getSignature().getDeclaringTypeName() + " "
+ pjp.getSignature().getName()
+ " argument: " + value.getBrand());
Object retVal = pjp.proceed();
System.out.println("After execution: " +
pjp.getSignature().getDeclaringTypeName() + " "
+ pjp.getSignature().getName()
+ " argument: " + value.getBrand());
return retVal;
}
新添加的simpleAroundAdvice()方法需要接受至少一个类型为ProceedingJoinPoint的参数,这样它才能继续调用目标对象。我们还添加了value参数来显示建议中的值。必须修改<aop:aspect>的 XML 配置来添加新的建议。(您可以在app-context-xml-03.xml中找到完整的配置,除了下面显示的几行之外,其他内容都与app-context-xml-02.xml中的内容相同,因此这里不再赘述。)
<beans ..>
...
<aop:config>
<aop:pointcut id="singExecution"
expression="execution(
* sing*(com.apress.prospring5.ch2.common.Guitar))
and args(value) and bean(john*)"
/>
<aop:aspect ref="advice">
<aop:before pointcut-ref="singExecution"
method="simpleBeforeAdvice"/>
<aop:around pointcut-ref="singExecution"
method="simpleAroundAdvice"/>
</aop:aspect>
</aop:config>
</beans>
我们只是添加了新的标签<aop:around>来声明 around 通知并引用相同的切入点。让我们再次修改NewDocumentarist.execute()方法,加入一个带有默认Guitar的sing()调用,以获得我们想要分析的行为。
package com.apress.prospring5.ch5;
import com.apress.prospring5.ch2.common.Guitar;
public class NewDocumentarist extends Documentarist {
@Override
public void execute() {
guitarist.sing();
Guitar guitar = new Guitar();
guitar.setBrand("Gibson");
guitarist.sing(guitar);
guitarist.sing(new Guitar());
guitarist.talk();
}
}
再次运行测试程序,您将得到以下输出:
sing: Gravity is working against me
And gravity wants to bring me down
Executing: com.apress.prospring5.ch5.GrammyGuitarist sing
Before execution: com.apress.prospring5.ch5.GrammyGuitarist sing argument: Gibson
play: G C G C Am D7
After execution: com.apress.prospring5.ch5.GrammyGuitarist sing argument: Gibson
Before execution: com.apress.prospring5.ch5.GrammyGuitarist sing argument: Martin
play: G C G C Am D7
After execution: com.apress.prospring5.ch5.GrammyGuitarist sing argument: Martin
talk
这里有两个有趣的点。首先,您会看到 around 建议被应用于带有一个Guitar参数的sing(..)方法的两次调用,因为它不检查参数。第二,对于将"Gibson" Guitar作为参数的sing()方法,before 和 around 建议都被执行,默认情况下 before 建议优先。
当使用aop名称空间或@AspectJ样式时,有两种类型的 after 建议。只有当目标方法正常完成时,after-returning建议(使用<aop:after-returning>标签)才适用。另一个是 after advice(使用<aop:after>标签),无论方法是正常完成还是遇到错误并抛出异常都会发生。如果您需要不管目标方法的执行结果而执行的通知,您应该使用 after advice。
使用@AspectJ 样式的注释
在 JDK 5 或更新版本中使用 Spring AOP 时,您也可以使用@AspectJ-样式的注释来声明您的建议。然而,如前所述,Spring 仍然使用自己的代理机制来通知目标方法,而不是 AspectJ 的编织机制。
在这一节中,我们将介绍如何通过使用@AspectJ-样式注释来实现与aop名称空间中相同的方面。AspectJ 是一个通用的面向方面的 Java 扩展,是为了解决传统编程方法不能很好地捕捉的问题或关注点,换句话说,横切关注点。对于本节中的例子,我们也将对其他 Spring beans 使用注释,并且我们将使用 Java 配置类。
以下示例描述了使用注释声明 bean 的GrammyGuitarist类:
package com.apress.prospring5.ch5;
import com.apress.prospring5.ch2.common.Guitar;
import com.apress.prospring5.ch2.common.Singer;
import org.springframework.stereotype.Component;
@Component("johnMayer")
public class GrammyGuitarist implements Singer {
@Override public void sing() {
System.out.println("sing: Gravity is working against me\n" +
"And gravity wants to bring me down");
}
public void sing(Guitar guitar) {
System.out.println("play: " + guitar.play());
}
public void rest(){
System.out.println("zzz");
}
public void talk(){
System.out.println("talk");
}
}
这个职业也需要适应。
package com.apress.prospring5.ch5;
import com.apress.prospring5.ch2.common.Guitar;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
@Component("documentarist")
public class NewDocumentarist {
protected GrammyGuitarist guitarist;
public void execute() {
guitarist.sing();
Guitar guitar = new Guitar();
guitar.setBrand("Gibson");
guitarist.sing(guitar);
guitarist.talk();
}
@Autowired
@Qualifier("johnMayer")
public void setGuitarist(GrammyGuitarist guitarist) {
this.guitarist = guitarist;
}
}
我们用@Component注释来注释这两个类,并给它们分配相应的名称。在GrammyGuitarist类中,属性guitarist的 setter 方法被注释为@Autowired,用于 Spring 的自动注入。
现在让我们看看使用@AspectJ-样式注释的AnnotationAdvice类。我们将一次性实现通知前和通知周围的切入点。下面的代码片段显示了AnnotationAdvice类:
package com.apress.prospring5.ch5;
import com.apress.prospring5.ch2.common.Guitar;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class AnnotatedAdvice {
@Pointcut("execution(*
com.apress.prospring5.ch5..sing*(com.apress.prospring5.ch2.common.Guitar))
&& args(value)")
public void singExecution(Guitar value) {
}
@Pointcut("bean(john*)")
public void isJohn() {
}
@Before("singExecution(value) && isJohn()")
public void simpleBeforeAdvice(JoinPoint joinPoint, Guitar value) {
if(value.getBrand().equals("Gibson")) {
System.out.println("Executing: " +
joinPoint.getSignature().getDeclaringTypeName() + " "
+ joinPoint.getSignature().getName() + " argument: " + value.getBrand());
}
}
@Around("singExecution(value) && isJohn()")
public Object simpleAroundAdvice(ProceedingJoinPoint pjp,
Guitar value) throws Throwable {
System.out.println("Before execution: " +
pjp.getSignature().getDeclaringTypeName() + " "
+ pjp.getSignature().getName()
+ " argument: " + value.getBrand());
Object retVal = pjp.proceed();
System.out.println("After execution: " +
pjp.getSignature().getDeclaringTypeName() + " "
+ pjp.getSignature().getName()
+ " argument: " + value.getBrand());
return retVal;
}
}
您会注意到代码结构与我们在aop名称空间中使用的非常相似,只是在这种情况下我们使用了注释。不过,还是有几点值得注意。
- 我们使用了
@Component和@Aspect来注释AnnotatedAdvice类。@Aspect注释用于声明它是一个方面类。当我们在 XML 配置中使用<context:component-scan>标签时,为了允许 Spring 扫描组件,您还需要用@Component注释这个类。 - 切入点被定义为返回
void的方法。在类中,我们定义了两个切入点;两者都标注了@Pointcut。我们有意将aop名称空间示例中的切入点表达式一分为二。第一个(由方法singExecution(Guitar value))指示)用一个guitar参数定义了包com.apress.prospring4.ch5下所有类的sing*()方法执行的切入点,参数(value)也将被传递到通知中。另一个(由方法isJohn()指示)是定义另一个切入点,该切入点定义所有方法执行,并在 Spring beans 的名称前加上前缀john。还要注意,我们需要使用&&来定义切入点表达式中的“and”条件,而对于aop名称空间,我们需要使用and操作符。 - before-advice 方法用
@Before注释,而 around advice 用@Around注释。对于这两种通知类型,我们传递使用类中定义的两个切入点的值。值singExecution(value) && isJohn()意味着两个切入点的条件应该匹配以应用建议,这与ComposablePointcut中的交集操作相同。 - 通知前逻辑和绕过通知逻辑与
aop名称空间示例中的相同。
有了所有的注释,XML 配置就变得简单了。
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<aop:aspectj-autoproxy/>
<context:component-scan
base-package="com.apress.prospring5.ch5"/>
</beans>
只声明了两个标记。<aop:aspect-autoproxy>标签通知 Spring 扫描@AspectJ-样式的注释,而<context:component-scan>标签仍然是 Spring 扫描通知所在的包中的 Spring beans 所必需的。我们还需要用@Component注释 advice 类,以表明它是一个 Spring 组件。
下面的代码片段描述了测试此配置的类:
package com.apress.prospring5.ch5;
import org.springframework.context.support.GenericXmlApplicationContext;
public class AspectJAnnotationDemo {
public static void main(String... args) {
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-xml.xml");
ctx.refresh();
NewDocumentarist documentarist =
ctx.getBean("documentarist", NewDocumentarist.class);
documentarist.execute();
}
}
如果您按原样运行该示例,您会感到有点惊讶,因为您将在控制台中看到以下内容:
Exception in thread "main"
org.springframework.beans.factory.UnsatisfiedDependencyException:
Error creating bean with name 'documentarist': Unsatisfied dependency
expressed through method 'setGuitarist' parameter 0; nested exception is
org.springframework.beans.factory.BeanNotOfRequiredTypeException:
Bean named 'johnMayer' is expected to be of type
'com.apress.prospring5.ch5.GrammyGuitarist' but was actually of
type 'com.sun.proxy.$Proxy18'
...
这是怎么回事?嗯,GrammyGuitarist实现了Singer接口,默认情况下,基于接口的 JDK 动态代理被创建。但是NewDocumentarist严格要求依赖关系必须是类型Grammy-Guitarist或者是它的扩展。因此,会引发上一个异常。我们如何解决它?有两种方法:一种是修改NewDocumentarist来接受Singer依赖,但是这并不适合我们的例子,因为我们想要访问GrammyGuitarist类中的方法,这些方法不是在Singer接口中定义的方法的实现。第二种方法是请求 Spring 生成 CGLIB,基于类的代理。在 XML 中,这可以通过修改<aop:aspectj-autoproxy/>标签的配置并将proxy-target-class属性值设置为true来实现。
Java 配置类甚至比这更简单:
@Configuration
@ComponentScan(basePackages = {"com.apress.prospring5.ch5"})
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AppConfig {
}
注意@EnableAspectJAutoProxy注释。它相当于<aop:aspectj-autoproxy/>,也有一个类似于proxy-target-class属性的属性proxyTargetClass。该注释支持处理用 AspectJ 的@Aspect注释标记的组件,并且被设计用于用@Configuration注释的类。
下面是测试程序。它被设计成一个 JUnit 测试用例,以便 XML 和 Java 配置示例可以放在同一个类中。像 IntelliJ IDEA 这样的智能编辑器提供了独立执行每个测试方法的可能性。
package com.apress.prospring5.ch5;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;
public class AspectJAnnotationTest {
@Test
public void xmlTest() {
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-xml.xml");
ctx.refresh();
NewDocumentarist documentarist =
ctx.getBean("documentarist", NewDocumentarist.class);
documentarist.execute();
ctx.close();
}
@Test
public void configTest() {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
NewDocumentarist documentarist =
ctx.getBean("documentarist", NewDocumentarist.class);
documentarist.execute();
ctx.close();
}
}
运行这些测试方法中的任何一种,如果通过,都会产生以下输出:
sing: Gravity is working against me
And gravity wants to bring me down
Before execution: com.apress.prospring5.ch5.GrammyGuitarist sing argument: Gibson
Executing: com.apress.prospring5.ch5.GrammyGuitarist sing argument: Gibson
play: G C G C Am D7
After execution: com.apress.prospring5.ch5.GrammyGuitarist sing argument: Gibson
talk
Spring Boot 提供了一个特殊的 AOP 启动库,消除了一些配置的麻烦。该库通常在pro-spring-15/build.properties文件中配置,并作为依赖项添加到子项目配置文件aspectj-boot/build.gradle中。
//pro-spring-15/build.properties
ext {
bootVersion = '2.0.0.BUILD-SNAPSHOT'
...
boot = [
springBootPlugin:
"org.springframework.boot:spring-boot-gradle-plugin:$bootVersion",
...
starterAop:
"org.springframework.boot:spring-boot-starter-aop:$bootVersion"
]
}
//aspectj-boot/build.gradle
buildscript {
...
dependencies {
classpath boot.springBootPlugin
}
}
apply plugin: 'org.springframework.boot'
dependencies {
compile boot.starterAop
}
在图 5-11 中,你可以看到作为依赖项添加到 Spring Boot 项目中的一组库。通过添加这个库

图 5-11。
Spring Boot AOP starter tranzitive dependencies as depicted in IntelliJ IDEA
作为对应用的依赖,不再需要@EnableAspectJAutoProxy(proxyTargetClass = true)注释,因为 AOP Spring 支持已经默认启用。该属性也不必在任何地方设置,因为 Spring Boot 会自动检测您需要什么类型的代理。考虑到前面的例子,您可以删除AppConfig类,并用一个典型的 Spring Boot 应用类替换它。
package com.apress.prospring5.ch5;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
@SpringBootApplication
public class Application {
private static Logger logger = LoggerFactory.getLogger(Application.class);
public static void main(String args) throws Exception {
ConfigurableApplicationContext ctx =
SpringApplication.run(Application.class, args);
assert (ctx != null);
NewDocumentarist documentarist =
ctx.getBean("documentarist", NewDocumentarist.class);
documentarist.execute();
System.in.read();
ctx.close();
}
}
声明性 Spring AOP 配置的注意事项
到目前为止,我们已经讨论了声明 Spring AOP 配置的三种方式,包括ProxyFactoryBean、aop名称空间和@AspectJ-样式注释。我们相信你会同意aop名称空间比ProxyFactoryBean简单得多。所以,总的问题是,您使用的是aop名称空间还是@AspectJ-样式注释?
如果您的 Spring 应用是基于 XML 配置的,那么使用aop名称空间方法是一个自然的选择,因为它保持了 AOP 和 DI 配置风格的一致性。另一方面,如果您的应用主要基于注释,请使用@AspectJ注释。同样,让应用的需求驱动配置方法,并尽最大努力保持一致性。
此外,aop名称空间和@AspectJ注释方法之间还有一些其他的区别。
- 切入点表达式语法有一些细微的区别(例如,在前面的讨论中,我们需要在
aop名称空间中使用and,但是在@AspectJ注释中使用&&)。 aop名称空间方法只支持“单例”方面实例化模型。- 在
aop名称空间中,您不能“组合”多个切入点表达式。在使用@AspectJ的例子中,我们可以在 before 和 around 通知中组合两个切入点定义(即singExecution(value) && isJohn())。当使用aop名称空间并且需要创建一个新的组合匹配条件的切入点表达式时,需要使用ComposablePointcut类。
AspectJ 集成
AOP 为基于 OOP 的应用出现的许多常见问题提供了一个强大的解决方案。当使用 Spring AOP 时,您可以利用 AOP 功能的精选子集,在大多数情况下,这些功能可以帮助您解决应用中遇到的问题。然而,在某些情况下,您可能想要使用一些超出 Spring AOP 范围的 AOP 特性。
从连接点的角度来看,Spring AOP 只支持执行公共非静态方法时的切入点匹配。但是,在某些情况下,在对象构造或字段访问等过程中,您可能需要将建议应用于受保护的/私有的方法。
在这些情况下,您需要查看具有更全面特性集的 AOP 实现。在这种情况下,我们的首选是使用 AspectJ,因为现在可以使用 Spring 配置 AspectJ 方面,AspectJ 形成了 Spring AOP 的完美补充。
关于 AspectJ
AspectJ 是一个全功能的 AOP 实现,它使用编织过程(编译时或加载时编织)将方面引入到代码中。在 AspectJ 中,方面和切入点是使用类似 Java 的语法构建的,这减少了 Java 开发人员的学习曲线。我们不打算花太多时间研究 AspectJ 及其工作原理,因为这超出了本书的范围。相反,我们给出一些简单的 AspectJ 示例,并向您展示如何使用 Spring 配置它们。要了解更多关于 AspectJ 的信息,你一定要阅读 Ramnivas Laddad 的《AspectJ 在行动中:带有 Spring 应用的企业 AOP》(Manning,2009)。
我们不打算讨论如何将 AspectJ 方面编织到您的应用中。参考 AspectJ 文档获取详细信息,或者看看第 5 的aspectj-aspects项目中提供的 Gradle build。
使用单例方面
默认情况下,AspectJ 方面是单例的,这意味着每个类装入器只有一个实例。Spring 面对任何 AspectJ 方面的问题是它不能创建方面实例,因为这已经由 AspectJ 自己处理了。然而,每个方面都公开了一个名为org.aspectj.lang.Aspects.aspectOf()的方法,可以用来访问方面实例。使用aspectOf()方法和 Spring 配置的一个特殊特性,您可以让 Spring 为您配置方面。有了这种支持,您就可以充分利用 AspectJ 强大的 AOP 特性集,而不会失去 Spring 出色的 DI 和配置能力。这也意味着您的应用不需要两种单独的配置方法;您可以对所有 Spring 管理的 beans 和 AspectJ 方面使用相同的 Spring ApplicationContext方法。
为了支持 Spring 应用中的方面,需要在配置中添加一个 Gradle 插件。你可以在这里找到源代码和如何在 Gradle 应用中使用它的说明: https://github.com/eveoh/gradle-aspectj 。在这里你可以看到chapter05/aspectj-aspects/build.gradle的内容:
buildscript {
repositories {
mavenLocal()
mavenCentral()
maven { url "http://repo.spring.io/release" }
maven { url "http://repo.spring.io/milestone" }
maven { url "http://repo.spring.io/snapshot" }
maven { url "https://repo.spring.io/libs-snapshot" }
maven { url "https://maven.eveoh.nl/content/repositories/releases" }
}
dependencies {
classpath "nl.eveoh:gradle-aspectj:1.6"
}
}
apply plugin: 'aspectj'
jar {
manifest {
attributes(
'Main-Class': 'com.apress.prospring5.ch5.AspectJDemo',
"Class-Path": configurations.compile.collect { it.getName() }.join(' '))
}
}
在下面的代码片段中,您可以看到一个基本类MessageWriter,我们将使用 AspectJ 来建议它:
package com.apress.prospring5.ch5;
public class MessageWriter {
public void writeMessage() {
System.out.println("foobar!");
}
public void foo() {
System.out.println("foo");
}
}
对于这个例子,我们将使用 AspectJ 来通知writeMessage()方法,并在方法调用前后写出一条消息。这些消息可以使用 Spring 进行配置。下面的代码示例展示了MessageWrapper方面(文件名是MessageWrapper.aj,它是一个 AspectJ 文件,而不是一个标准的 Java 类):
package com.apress.prospring5.ch5;
public aspect MessageWrapper {
private String prefix;
private String suffix;
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public String getPrefix() {
return this.prefix;
}
public void setSuffix(String suffix) {
this.suffix = suffix;
}
public String getSuffix() {
return this.suffix;
}
pointcut doWriting() :
execution(*
com.apress.prospring5.ch5.MessageWriter.writeMessage());
before() : doWriting() {
System.out.println(prefix);
}
after() : doWriting() {
System.out.println(suffix);
}
}
本质上,我们创建了一个名为MessageWrapper的方面,就像普通的 Java 类一样,我们给了方面两个属性suffix和prefix,我们将在通知writeMessage()方法时使用它们。接下来,我们为单个连接点定义一个命名的切入点doWriting(),在本例中,是writeMessage()方法的执行。AspectJ 有大量的连接点,但是这些不在本例的讨论范围之内。最后,我们定义了两条建议:一条在doWriting()切入点之前执行,另一条在它之后执行。下面的配置片段展示了如何在 Spring ( app-config-xml.xml)中配置这个方面:
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="aspect" class="com.apress.prospring5.ch5.MessageWrapper"
factory-method="aspectOf" p:prefix="The Prefix" p:suffix="The Suffix"/>
</beans>
如您所见,方面 bean 的大部分配置类似于标准 bean 配置。唯一的区别是使用了<bean>标签的factory-method属性。factory-method属性旨在允许遵循传统工厂模式的类无缝集成到 Spring 中。例如,如果您有一个带有私有构造函数和静态工厂方法getInstance()的类Foo,使用factory-method属性允许 Spring 管理这个类的 bean。每个 AspectJ 方面公开的aspectOf()方法允许您访问方面的实例,从而允许 Spring 设置方面的属性。在这里,您可以看到一个简单的驱动程序应用:
package com.apress.prospring5.ch5;
import org.springframework.context.support.GenericXmlApplicationContext;
public class AspectJDemo {
public static void main(String... args) {
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context-xml.xml");
ctx.refresh();
MessageWriter writer = new MessageWriter();
writer.writeMessage();
writer.foo();
}
}
注意,首先我们加载ApplicationContext来允许 Spring 配置方面。接下来我们创建一个MessageWriter的实例,然后调用writeMessage()和foo()方法。该示例的输出如下:
The Prefix
foobar!
The Suffix
foo
如您所见,MessageWrapper方面中的建议被应用到了writeMessage()方法中,并且在ApplicationContext配置中指定的前缀和后缀值被建议在写出之前和之后的消息时使用。
摘要
在这一章中,我们讨论了大量的 AOP 核心概念,并研究了这些概念如何转化为 Spring AOP 实现。我们讨论了 Spring AOP 中实现的(和没有实现的)特性,并且指出 AspectJ 是 Spring 没有实现的那些特性的 AOP 解决方案。我们花了一些时间解释 Spring 中可用的建议类型的细节,并且您看到了四种类型的实例。我们还看了如何通过使用切入点来限制建议适用的方法。特别是,我们研究了 Spring 提供的六个基本切入点实现。我们还详细介绍了 AOP 代理是如何构造的,不同的选项,以及它们的不同之处。我们比较了三种代理类型的性能,并强调了在选择 JDK 代理和 CGLIB 代理之间的一些主要差异和限制。我们讨论了切入点的高级选项,以及如何使用简介来扩展由对象实现的接口集。我们还介绍了 Spring Framework 服务以声明方式配置 AOP,从而避免了将 AOP 代理构造逻辑硬编码到代码中的需要。我们花了一些时间来研究 Spring 和 AspectJ 是如何集成的,以允许您使用 AspectJ 的额外功能,而不会失去 Spring 的任何灵活性。那当然是大量的 AOP!
在下一章,我们将转移到一个完全不同的话题——我们如何使用 Spring 的 JDBC 支持来从根本上简化基于 JDBC 的数据访问代码的创建。
Footnotes 1
2
UML 在开发中非常重要,因为它是一种简化应用逻辑并使其可视化的方法,因此在编写代码之前就很容易发现问题。当向团队介绍新成员时,它也可以用作文档,使他们尽快提高工作效率。你可以在 www.uml.org/ 找到更多信息。
六、Spring JDBC 支持
到目前为止,您已经看到了构建一个完全由 Spring 管理的应用是多么容易。您对 bean 配置和面向方面编程(AOP)有很深的理解。然而,这个难题缺少了一部分:如何获得驱动应用的数据?
除了简单的一次性命令行实用程序,几乎每个应用都需要将数据保存到某种数据存储中。最常见和最方便的数据存储是关系数据库。
以下是 2017 年七大关系型企业数据库:
- 甲骨文数据库
- 搜寻配置不当的
- IBM DB2
- SAP Sybase ASE
- 一种数据库系统
- MariaDB 企业
- 关系型数据库
如果你不在一家能负担前四种许可证的大公司工作,你可能会使用前面列表中的后三种之一。使用最多的开源关系数据库可能是 MySQL ( http://mysql.com )和 PostgreSQL ( postgresql.org)。MySQL 通常更广泛地用于 web 应用开发,尤其是在 Linux 平台上。 1 另一方面,PostgreSQL 对 Oracle 开发者更友好,因为它的过程语言 PLpgSQL 非常接近 Oracle 的 PL/SQL 语言。
即使您选择了最快和最可靠的数据库,您也不能因为使用设计和实现不佳的数据访问层而失去它的速度和灵活性。应用倾向于非常频繁地使用数据访问层;因此,数据访问代码中任何不必要的瓶颈都会影响整个应用,不管它设计得有多好。
在本章中,我们将向您展示如何使用 Spring 来简化使用 JDBC 的数据访问代码的实现。我们首先看一下在没有 Spring 的情况下通常需要编写的大量重复代码,然后将其与使用 Spring 的数据访问类实现的类进行比较。结果确实令人惊讶,因为 Spring 允许您使用人工调整的 SQL 查询的全部功能,同时最小化您需要实现的支持代码的数量。具体来说,我们讨论以下内容:
- 比较传统的 JDBC 代码和 Spring JDBC 支持:我们探索 Spring 如何简化旧式的 JDBC 代码,同时保持相同的功能。您还将看到 Spring 如何访问低级 JDBC API,以及这个低级 API 如何映射到方便的类,如
JdbcTemplate。 - 连接到数据库:尽管我们没有深入到数据库连接管理的每一个细节,但我们确实向您展示了简单的
Connection和DataSource之间的根本区别。自然,我们会讨论 Spring 如何管理数据源,以及您可以在应用中使用哪些数据源。 - 检索数据并将数据映射到 Java 对象:我们向您展示如何检索数据,然后如何有效地将所选数据映射到 Java 对象。您还了解了 Spring JDBC 是对象关系映射(ORM)工具的一个可行的替代品。
- 插入、更新和删除数据:最后,我们讨论如何通过使用 Spring 执行这些类型的查询来实现插入、更新和删除操作。
Lambda 表达式简介
Java 版本 8 的发布带来了 lambda 表达式支持,以及许多其他特性。Lambda 表达式是匿名内部类用法的绝佳替代品,也是使用 Spring 的 JDBC 支持的理想选择。lambda 表达式的使用需要使用 Java 8。这本书是在 Java 8 的预发行版本和第一个通用版本期间写的,所以我们知道不是每个人都会使用 Java 8。鉴于此,本章代码示例和源代码下载展示了这两种风格。Lambda 表达式适用于 Spring API 中使用模板或回调的大多数地方,而不仅仅局限于 JDBC。本章不涉及 lambda 表达式本身,因为它们是 Java 语言的一个特性,你应该熟悉 lambda 的概念和语法。更多信息请参考 http://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html 的 Lambda 表达式教程。
示例代码的示例数据模型
在继续讨论之前,我们先介绍一个简单的数据模型,该模型在本章以及后面几章讨论其他数据访问技术时用于示例(我们将相应地扩展该模型,以满足每个主题的需要)。
这个模型是一个简单的音乐数据库,有两个表。第一个是SINGER表,存储一个歌手的信息,另一个表是ALBUM,存储那个歌手发行的专辑。每个歌手可以有零张或多张专辑;换句话说,SINGER和ALBUM之间是一对多的关系。歌手的信息包括他们的名和姓以及出生日期。图 6-1 显示了数据库的实体关系(ER)图。

图 6-1。
Simple data model for the example code
正如您所看到的,两个表都有一个 ID 列,在插入过程中,数据库会自动分配这个 ID 列。对于ALBUM表,有一个到SINGER表的外键关系,它通过列SINGER_ID与SINGER表的主键(即 ID 列)相链接。
在本章中,我们使用开源数据库 MySQL 在一些例子中展示与真实数据库的交互。这将要求您有一个 MySQL 实例可供使用。我们不讨论如何安装 MySQL。您可以使用自己选择的另一个数据库,但是可能需要修改模式和函数定义。我们还将介绍嵌入式数据库的使用,它不需要 MySQL 数据库。
万一你想使用 MySQL,在官方网站上你可以找到非常好的安装和配置 MySQL 的教程。下载完 MySQL 2 并安装后,就可以使用root账号访问了。通常,当您开发一个应用时,您需要一个新的模式和用户。对于本章中的代码示例,模式名为MUSICDB,访问它的用户名为prospring5。接下来描述了创建它们所执行的 SQL 代码,您可以在plain-jdbc项目的resources目录下的ddl.sql文件中找到它们。包括对 MySQL 社区服务器版本 5.17.18 中的一个错误的修复,这是本书撰写时的当前版本。
CREATE USER 'prospring5'@'localhost' IDENTIFIED BY 'prospring5';
CREATE SCHEMA MUSICDB;
GRANT ALL PRIVILEGES ON MUSICDB . * TO 'prospring5'@'localhost';
FLUSH PRIVILEGES;
/*in case of java.sql.SQLException: The server timezone value 'UTC'
is unrecognized or represents more than one timezone. */
SET GLOBAL time_zone = '+3:00';
下面的代码片段描述了创建前面提到的两个表所需的 SQL 代码。这个代码在schema.sql的plain-jdbc项目的resources目录下。
CREATE TABLE SINGER (
ID INT NOT NULL AUTO_INCREMENT
, FIRST_NAME VARCHAR(60) NOT NULL
, LAST_NAME VARCHAR(40) NOT NULL
, BIRTH_DATE DATE
, UNIQUE UQ_SINGER_1 (FIRST_NAME, LAST_NAME)
, PRIMARY KEY (ID)
);
CREATE TABLE ALBUM (
ID INT NOT NULL AUTO_INCREMENT
, SINGER_ID INT NOT NULL
, TITLE VARCHAR(100) NOT NULL
, RELEASE_DATE DATE
, UNIQUE UQ_SINGER_ALBUM_1 (SINGER_ID, TITLE)
, PRIMARY KEY (ID)
, CONSTRAINT FK_ALBUM FOREIGN KEY (SINGER_ID)
REFERENCES SINGER (ID)
);
如果您使用像 IntelliJ IDEA 这样的智能编辑器,您可以使用数据库视图来检查您的模式和表。在图 6-2 中,你可以看到 IntelliJ IDEA 中描述的MUSICDB模式的内容。

图 6-2。
Contents of the MUSICDB schema
因为我们需要数据来测试 JDBC 的使用,所以还提供了一个名为test-data.sql的文件,其中包含一组用于填充这两个表的INSERT语句。
insert into singer (first_name, last_name, birth_date)
values ('John', 'Mayer', '1977-10-16');
insert into singer (first_name, last_name, birth_date)
values ('Eric', 'Clapton', '1945-03-30');
insert into singer (first_name, last_name, birth_date)
values ('John', 'Butler', '1975-04-01');
insert into album (singer_id, title, release_date)
values (1, 'The Search For Everything', '2017-01-20');
insert into album (singer_id, title, release_date)
values (1, 'Battle Studies', '2009-11-17');
insert into album (singer_id, title, release_date)
values (2, ' From The Cradle ', '1994-09-13');
在本章的后面几节中,您将看到通过 JDBC 从数据库中检索数据并将结果集直接映射到 Java 对象(即 POJOs)的例子。这些映射到表中记录的类也称为entities。对于SINGER表,将创建一个Singer类,该类将被实例化以创建映射到歌手记录的 Java 对象。
package com.apress.prospring5.ch6.entities;
import java.io.Serializable;
import java.sql.Date;
import java.util.List;
public class Singer implements Serializable {
private Long id;
private String firstName;
private String lastName;
private Date birthDate;
private List<Album> albums;
public void setId(Long id) {
this.id = id;
}
public Long getId() {
return this.id;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getFirstName() {
return this.firstName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getLastName() {
return this.lastName;
}
public boolean addAbum(Album album) {
if (albums == null) {
albums = new ArrayList<>();
albums.add(album);
return true;
} else {
if (albums.contains(album)) {
return false;
}
}
albums.add(album);
return true;
}
public void setAlbums(List<Album> albums) {
this.albums = albums;
}
public List<Album> getAlbums() {
return albums;
}
public void setBirthDate(Date birthDate) {
this.birthDate = birthDate;
}
public Date getBirthDate() {
return birthDate;
}
public String toString() {
return "Singer - Id: " + id + ", First name: " + firstName
+ ", Last name: " + lastName + ", Birthday: " + birthDate;
}
}
以类似的方式,还创建了一个Album类。
package com.apress.prospring5.ch6.entities;
import java.io.Serializable;
import java.sql.Date;
public class Album implements Serializable {
private Long id;
private Long singerId;
private String title;
private Date releaseDate;
public void setId(Long id) {
this.id = id;
}
public Long getId() {
return this.id;
}
public void setSingerId(Long singerId) {
this.singerId = singerId;
}
public Long getSingerId() {
return this.singerId;
}
public void setTitle(String title) {
this.title = title;
}
public String getTitle() {
return this.title;
}
public void setReleaseDate(Date releaseDate) {
this.releaseDate = releaseDate;
}
public Date getReleaseDate() {
return this.releaseDate;
}
@Override
public String toString() {
return "Album - Id: " + id + ", Singer id: " + singerId
+ ", Title: " + title + ", Release Date: " + releaseDate;
}
}
让我们从一个简单的SingerDao接口开始,它封装了歌手信息的所有数据访问服务。代码如下:
package com.apress.prospring5.ch6.dao;
import com.apress.prospring5.ch6.entities.Singer;
import java.util.List;
public interface SingerDao {
List<Singer> findAll();
List<Singer> findByFirstName(String firstName);
String findLastNameById(Long id);
String findFirstNameById(Long id);
void insert(Singer singer);
void update(Singer singer);
void delete(Long singerId);
List<Singer> findAllWithDetail();
void insertWithDetail(Singer singer);
}
在前面的接口中,我们分别定义了两个 finder 方法以及 insert、update 和 delete 方法。它们对应于 CRUD 术语(创建、读取、更新、删除)。
最后,为了便于测试,让我们修改一下logback.xml配置文件,将所有类的日志级别改为DEBUG。在DEBUG级别,Spring JDBC 模块将把所有底层 SQL 语句输出到数据库,这样你就知道到底发生了什么;这对于排除 SQL 语句语法错误特别有用。下面的配置示例描述了打开了DEBUG级别的logback.xml文件(位于src/main/resources下,带有章节 6 项目的源代码文件)的内容。
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">
<resetJUL>true</resetJUL>
</contextListener>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{5} - %msg%n</pattern>
</encoder>
</appender>
<logger name="com.apress.prospring5.ch5" level="debug"/>
<logger name="org.springframework" level="off"/>
<root level="debug">
<appender-ref ref="console" />
</root>
</configuration>
探索 JDBC 的基础设施
JDBC 为 Java 应用访问存储在数据库中的数据提供了一种标准方法。JDBC 基础设施的核心是特定于每个数据库的驱动程序;正是这个驱动程序允许 Java 代码访问数据库。
一旦驱动程序被加载,它就向一个java.sql.DriverManager类注册自己。该类管理驱动程序列表,并提供建立数据库连接的静态方法。DriverManager的getConnection()方法返回一个驱动实现的java.sql.Connection接口。这个接口允许您对数据库运行 SQL 语句。
JDBC 框架相当复杂,而且久经考验;然而,随着这种复杂性而来的是开发上的困难。第一层复杂性在于确保您的代码管理到数据库的连接。连接是一种稀缺资源,建立起来非常昂贵。通常,数据库会为每个连接创建一个线程或一个子进程。此外,并发连接数通常是有限的,过多的打开连接数会降低数据库的速度。
我们将向您展示 Spring 如何帮助管理这种复杂性,但是在我们继续之前,我们需要向您展示如何在纯 JDBC 中选择、删除和更新数据。
本章涵盖的所有项目都需要特殊的数据库库作为依赖项:mysql-connector、spring-jdbc、dbcp等等。只需查看每个项目的build.gradle配置文件和pro-spring-15/build.gradle中使用的版本和库。
让我们创建一个简单形式的SingerDao接口实现,用于通过纯 JDBC 与数据库交互。记住我们已经知道的关于数据库连接的知识,我们将采取谨慎且昂贵的(就性能而言)方法为每个语句创建一个连接。这大大降低了 Java 的性能,并给数据库增加了额外的压力,因为必须为每个查询建立一个连接。但是,如果我们保持连接打开,我们可以使数据库服务器停止运行。下面的代码片段以 MySQL 为例,展示了管理 JDBC 连接所需的代码:
package com.apress.prospring5.ch6.dao;
...
public class PlainSingerDao implements SingerDao {
private static Logger logger =
LoggerFactory.getLogger(PlainSingerDao.class);
static {
try {
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException ex) {
logger.error("Prblem loadng DB dDiver!", ex);
}
}
private Connection getConnection() throws SQLException {
return DriverManager.getConnection(
"jdbc:mysql://localhost:3306/musicdb?useSSL=true",
"prospring5", "prospring5");
}
private void closeConnection(Connection connection) {
if (connection == null) {
return;
}
try {
connection.close();
} catch (SQLException ex) {
logger.error("Problem closing connection to the database!",ex);
}
}
...
这段代码还远未完成,但它让您了解了管理 JDBC 连接所需的步骤。这段代码甚至没有处理连接池,连接池是一种更有效地管理数据库连接的常用技术。我们现在不讨论连接池(连接池将在本章后面的“数据库连接和数据源”一节中讨论);相反,下面的代码片段显示了使用普通 JDBC 实现SingerDao接口的findAll()、insert()和delete()方法:
package com.apress.prospring5.ch6.dao;
...
public class PlainSingerDao implements SingerDao {
@Override
public List<Singer> findAll() {
List<Singer> result = new ArrayList<>();
Connection connection = null;
try {
connection = getConnection();
PreparedStatement statement =
connection.prepareStatement("select * from singer");
ResultSet resultSet = statement.executeQuery();
while (resultSet.next()) {
Singer singer = new Singer();
singer.setId(resultSet.getLong("id"));
singer.setFirstName(resultSet.getString("first_name"));
singer.setLastName(resultSet.getString("last_name"));
singer.setBirthDate(resultSet.getDate("birth_date"));
result.add(singer);
}
statement.close();
} catch (SQLException ex) {
logger.error("Problem when executing SELECT!",ex);
} finally {
closeConnection(connection);
}
return result;
}
@Override
public void insert(Singer singer) {
Connection connection = null;
try {
connection = getConnection();
PreparedStatement statement = connection.prepareStatement(
"insert into Singer (first_name, last_name, birth_date)
values (?, ?, ?)"
, Statement.RETURN_GENERATED_KEYS);
statement.setString(1, singer.getFirstName());
statement.setString(2, singer.getLastName());
statement.setDate(3, singer.getBirthDate());
statement.execute();
ResultSet generatedKeys = statement.getGeneratedKeys();
if (generatedKeys.next()) {
singer.setId(generatedKeys.getLong(1));
}
statement.close();
} catch (SQLException ex) {
logger.error("Prblem executing INSERT", ex);
} finally {
closeConnection(connection);
}
}
@Override
public void delete(Long singerId) {
Connection connection = null;
try {
connection = getConnection();
PreparedStatement statement = connection.prepareStatement
("delete from singer where id=?");
statement.setLong(1, singerId);
statement.execute();
statement.close();
} catch (SQLException ex) {
logger.error("Prblem executing DELETE", ex);
} finally {
closeConnection(connection);
}
}
...
}
要测试PlainSingerDao类,可以使用下面的类:
package com.apress.prospring5.ch6;
import com.apress.prospring5.ch6.dao.PlainSingerDao;
import com.apress.prospring5.ch6.dao.SingerDao;
import com.apress.prospring5.ch6.entities.Singer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Date;
import java.util.GregorianCalendar;
import java.util.List;
public class PlainJdbcDemo {
private static SingerDao singerDao = new PlainSingerDao();
private static Logger logger = LoggerFactory.getLogger(PlainJdbcDemo.class);
public static void main(String... args) {
logger.info("Listing initial singer data:");
listAllSingers();
logger.info("-------------");
logger.info("Insert a new singer");
Singer singer = new Singer();
singer.setFirstName("Ed");
singer.setLastName("Sheeran");
singer.setBirthDate(new Date
((new GregorianCalendar(1991, 2, 1991)).getTime().getTime()));
singerDao.insert(singer)
;
logger.info("Listing singer data after new singer created:");
listAllSingers();
logger.info("-------------");
logger.info("Deleting the previous created singer");
singerDao.delete(singer.getId());
logger.info("Listing singer data after new singer deleted:");
listAllSingers();
}
private static void listAllSingers() {
List<Singer> singers = singerDao.findAll();
for (Singer singer: singers) {
logger.info(singer.toString());
}
}
}
正如您所注意到的,我们现在将使用一个记录器来打印控制台中的消息。运行前面的程序会产生以下结果(假设您有一个本地安装的名为MUSICDB的 MySQL 数据库,它的用户名和密码设置为prospring5,并且样本数据已经加载):
INFO c.a.p.c.PlainJdbcDemo - Listing initial singer data:
INFO c.a.p.c.PlainJdbcDemo - Singer - Id: 1, First name: John, Last name: Mayer,
Birthday: 1977-10-15
INFO c.a.p.c.PlainJdbcDemo - Singer - Id: 2, First name: Eric, Last name: Clapton,
Birthday: 1945-03-29
INFO c.a.p.c.PlainJdbcDemo - Singer - Id: 3, First name: John, Last name: Butler,
Birthday: 1975-03-31
INFO c.a.p.c.PlainJdbcDemo - -------------
INFO c.a.p.c.PlainJdbcDemo - Insert a new singer
INFO c.a.p.c.PlainJdbcDemo - Listing singer data after new singer created:
INFO c.a.p.c.PlainJdbcDemo - Singer - Id: 1, First name: John, Last name: Mayer,
Birthday: 1977-10-15
INFO c.a.p.c.PlainJdbcDemo - Singer - Id: 2, First name: Eric, Last name: Clapton,
Birthday: 1945-03-29
INFO c.a.p.c.PlainJdbcDemo - Singer - Id: 3, First name: John, Last name: Butler,
Birthday: 1975-03-31
INFO c.a.p.c.PlainJdbcDemo - Singer - Id: 5, First name: Ed, Last name: Sheeran,
Birthday: 1996-08-10
INFO c.a.p.c.PlainJdbcDemo - -------------
INFO c.a.p.c.PlainJdbcDemo - Deleting the previous created singer
INFO c.a.p.c.PlainJdbcDemo - Listing singer data after new singer deleted:
INFO c.a.p.c.PlainJdbcDemo - Singer - Id: 1, First name: John, Last name: Mayer,
Birthday: 1977-10-15
INFO c.a.p.c.PlainJdbcDemo - Singer - Id: 2, First name: Eric, Last name: Clapton,
Birthday: 1945-03-29
INFO c.a.p.c.PlainJdbcDemo - Singer - Id: 3, First name: John, Last name: Butler,
Birthday: 1975-03-31
如输出所示,第一行显示了初始数据。第二行显示添加了新记录。最后一行显示新创建的歌手(艾德·希兰)被删除。
正如您在前面的代码示例中所看到的,许多代码需要移动到一个助手类中,或者更糟的是,在每个 DAO 类中复制。从应用员的角度来看,这是 JDBC 的主要缺点;您没有时间在每个 DAO 类中编写重复的代码。相反,您希望专注于编写代码,实际完成您需要 DAO 类做的事情:选择、更新和删除数据。您需要编写的助手代码越多,需要处理的检查异常就越多,您可能会在代码中引入更多的错误。
这就是 DAO 框架和 Spring 的用武之地。框架消除了实际上不执行任何定制逻辑的代码,并允许您忘记所有需要执行的内务处理。此外,Spring 广泛的 JDBC 支持会让您的生活轻松许多。
Spring JDBC 基础设施
我们在本章第一部分讨论的代码不是很复杂,但是很繁琐,因为要写的内容太多,所以编码错误的可能性很高。是时候看看 Spring 是如何让事情变得更简单、更优雅了。
概述和使用的包
JDBC Spring 支架分为五个包,详见表 6-1;每一个都处理 JDBC 访问的不同方面。
表 6-1。
Spring JDBC Packages
| 包裹 | 描述 | | --- | --- | | `org.springframework.jdbc.core` | 这包含了 Spring JDBC 课程的基础。它包括核心 JDBC 类`JdbcTemplate`,该类简化了用 JDBC 编程数据库操作。几个子包提供了对具有更特定目的的 JDBC 数据访问的支持(例如,一个支持命名参数的`JdbcTemplate`类)以及相关的支持类。 | | `org.springframework.jdbc.datasource` | 它包含助手类和`DataSource`实现,您可以使用它们在 JEE 容器之外运行 JDBC 代码。几个子包提供了对嵌入式数据库、数据库初始化和各种数据源查找机制的支持。 | | `org.springframework.jdbc.object` | 这包含帮助将从数据库返回的数据转换成对象或对象列表的类。这些对象和列表是普通的 Java 对象,因此与数据库断开连接。 | | `org.springframework.jdbc.support` | 这个包中最重要的类是`SQLException`翻译支持。这允许 Spring 识别数据库使用的错误代码,并将它们映射到更高级别的异常。 | | `org.springframework.` `jdbc.config` | 这包含了在 Spring 的`ApplicationContext`中支持 JDBC 配置的类。例如,它包含一个用于`jdbc`名称空间的处理程序类(例如,`让我们从最底层的功能开始讨论 Spring JDBC 支持。在运行 SQL 查询之前,需要做的第一件事是建立到数据库的连接。
数据库连接和数据源
通过提供一个实现javax.sql.DataSource的 bean,您可以使用 Spring 来管理数据库连接。一个DataSource和一个Connection的区别在于一个DataSource提供和管理Connection
DriverManagerDataSource(在包org.springframework.jdbc.datasource下)是一个DataSource最简单的实现。通过查看类名,您可以猜测它只是简单地调用DriverManager来获得连接。事实上DriverManagerDataSource不支持数据库连接池,这使得这个类除了测试之外不适合做任何事情。DriverManagerDataSource的配置相当简单,如以下代码片段所示;您只需要提供驱动程序类名、连接 URL、用户名和密码(drivermanager-cfg-01.xml)。
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<bean id="dataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource"
p:driverClassName="${jdbc.driverClassName}"
p:url="${jdbc.url}" p:username="${jdbc.username}"
p:password="${jdbc.password}"/>
<context:property-placeholder location="classpath:db/jdbc.properties"/>
</beans>
您很可能认识清单中的属性。它们表示您通常传递给 JDBC 以获得一个Connection接口的值。数据库连接信息通常存储在属性文件中,以便在不同的部署环境中进行维护和替换。下面的代码片段显示了一个示例jdbc.properties,Spring 的属性占位符将从该示例中加载连接信息:
jdbc.driverClassName=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/musicdb?useSSL=true
jdbc.username=prospring5
jdbc.password=prospring5
通过在 mix 中添加util名称空间,同样的配置也可以写成这样(drivermanager-cfg-02.xml):
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util.xsd">
<bean id="dataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource"
p:driverClassName="#{jdbc.driverClassName}"
p:url="#{jdbc.url}"
p:username="#{jdbc.username}"
p:password="#{jdbc.password}"/>
<util:properties id="jdbc" location="classpath:db/jdbc2.properties"/>
</beans>
这里需要做一个改变。这些属性被加载到一个名为jdbc的java.util.Properties bean 中,因此您需要在属性文件中更改属性名的名称,以便能够使用jdbc前缀来访问它们。下面是jdbc2.properties文件内容:
driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/musicdb?useSSL=true
username=prospring5
password=prospring5
因为这本书更关注 Java Configuration类,所以这里也有一个配置类:
package com.apress.prospring5.ch6.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.jdbc.datasource.SimpleDriverDataSource;
import org.springframework.jdbc.datasource.init.DatabasePopulatorUtils;
import javax.sql.DataSource;
import java.sql.Driver;
@Configuration
@PropertySource("classpath:db/jdbc2.properties")
public class DbConfig {
@Value("${driverClassName}")
private String driverClassName;
@Value("${url}")
private String url;
@Value("${username}")
private String username;
@Value("${password}")
private String password;
@Bean
public static PropertySourcesPlaceholderConfigurer
propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
@Lazy
@Bean
public DataSource dataSource() {
try {
SimpleDriverDataSource dataSource =
new SimpleDriverDataSource();
Class<? extends Driver> driver =
(Class<? extends Driver>) Class.forName(driverClassName);
dataSource.setDriverClass(driver);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
} catch (Exception e) {
return null;
}
}
}
要测试这些类中的任何一个,可以使用下面的测试类:
package com.apress.prospring5.ch6;
import com.apress.prospring5.ch6.config.DbConfig;
import org.junit.Test;
...
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class DbConfigTest {
private static Logger logger = LoggerFactory.getLogger(DbConfigTest.class);
@Test
public void testOne() throws SQLException {
GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
ctx.load("classpath:spring/drivermanager-cfg-01.xml");
ctx.refresh();
DataSource dataSource = ctx.getBean("dataSource", DataSource.class);
assertNotNull(dataSource);
testDataSource(dataSource);
ctx.close();
}
@Test
public void testTwo() throws SQLException {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(DbConfig.class);
DataSource dataSource = ctx.getBean("dataSource", DataSource.class);
assertNotNull(dataSource);
testDataSource(dataSource);
ctx.close();
}
private void testDataSource(DataSource dataSource) throws SQLException{
Connection connection = null;
try {
connection = dataSource.getConnection();
PreparedStatement statement =
connection.prepareStatement("SELECT 1");
ResultSet resultSet = statement.executeQuery();
while (resultSet.next()) {
int mockVal = resultSet.getInt("1");
assertTrue(mockVal== 1);
}
statement.close();
} catch (Exception e) {
logger.debug("Something unexpected happened.", e);
} finally {
if (connection != null) {
connection.close();
}
}
}
}
同样,我们使用了一个测试类,因为重用一些代码更实际,还可以教您使用 JUnit 为您编写的任何代码快速编写测试。第一个方法testOne()用于测试 XML 配置,第二个方法用于测试DbConfig配置类。从任何配置获得dataSource bean 后,模拟查询SELECT 1用于测试到 MySQL 数据库的连接。
在现实世界的应用中,您可以使用 Apache CommonsBasicDataSource3或由 JEE 应用服务器(例如 JBoss、WebSphere、WebLogic 或 GlassFish)实现的DataSource,这可能会进一步提高应用的性能。你可以在普通的 JDBC 代码中使用一个DataSource,并获得同样的统筹福利;然而,在大多数情况下,您仍然会错过一个配置DataSource的中心位置。另一方面,Spring 允许您声明一个dataSource bean,并在ApplicationContext定义文件中设置连接属性。请参见以下配置示例;文件名为datasource-dbcp.xml:
<beans ...>
<bean id="dataSource"
class="org.apache.commons.dbcp2.BasicDataSource"
destroy-method="close"
p:driverClassName="#{jdbc.driverClassName}"
p:url="#{jdbc.url}"
p:username="#{jdbc.username}"
p:password="#{jdbc.password}"/>
<util:properties id="jdbc" location="classpath:db/jdbc2.properties"/>
</beans>
这个特殊的 Spring 管理的DataSource在org.apache.commons.dbcp.BasicDataSource中实现。最重要的一点是,dataSource bean 实现了javax.sql.DataSource,您可以立即在您的数据访问类中使用它。
配置dataSource bean 的另一种方法是使用 JNDI。如果您正在开发的应用将在 JEE 容器中运行,我们可以利用容器管理的连接池。要使用基于 JNDI 的数据源,您需要更改dataSource bean 声明,如下例所示(datasource-jndi.xml):
<beans ...>
<bean id="dataSource"
class="org.springframework.jndi.JndiObjectFactoryBean"
p:jndiName="java:comp/env/jdbc/musicdb"/>
</beans>
在前面的例子中,我们使用 Spring 的JndiObjectFactoryBean通过 JNDI 查找获得数据源。从 2.5 版本开始,Spring 提供了jee名称空间,这进一步简化了配置。在这里,您可以看到使用jee名称空间(datasource-jee.xml)的相同 JNDI 数据源配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/jee
http://www.springframework.org/schema/jee/spring-jee.xsd">
<jee:jndi-lookup jndi-name="java:comp/env/jdbc/prospring5ch6"/>
</beans>
在前面的配置片段中,我们在<beans>标签中声明了jee名称空间,然后在<jee:jndi-lookup>标签中声明了数据源。如果采用 JNDI 方法,一定不要忘记在应用描述符文件中添加一个资源引用(resourceref)。请参见以下代码片段:
<root-node>
<resource-ref>
<res-ref-name>jdbc/musicdb</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>Container</res-auth>
</resource-ref>
</root-node>
<root-node>是占位符值;您需要根据您的模块如何打包来更改它。例如,如果应用是一个 web 模块,它在 web 部署描述符(WEB-INF/web.xml)中就变成了<web-app>。最有可能的是,您还需要在特定于应用服务器的描述符文件中配置resource-ref。但是,请注意,resource-ref元素配置了jdbc/musicdb引用名,并且dataSource bean 的jndiName被设置为java:comp/env/jdbc/musicdb。
正如您所看到的,Spring 允许您以几乎任何您喜欢的方式配置DataSource,并且它对应用的其余代码隐藏了数据源的实际实现或位置。换句话说,您的道类不知道也不需要知道DataSource指向哪里。
连接管理也被委托给dataSource bean,它自己执行管理或者使用 JEE 容器来完成所有的工作。
嵌入式数据库支持
从 3.0 版本开始,Spring 还提供了嵌入式数据库支持,它自动启动一个嵌入式数据库,并将其作为应用的DataSource公开。下面的配置片段显示了嵌入式数据库(embedded-h2-cfg.xml)的配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/jdbc
http://www.springframework.org/schema/jdbc/spring-jdbc.xsd">
<jdbc:embedded-database id="dataSource" type="H2">
<jdbc:script location="classpath:db/h2/schema.sql"/>
<jdbc:script location="classpath:db/h2/test-data.sql"/>
</jdbc:embedded-database>
</beans>
在前面的清单中,我们首先在<beans>标记中声明了jdbc名称空间。之后,我们使用<jdbc:embedded-database>来声明嵌入式数据库,并给它分配一个 IDdataSource。在标记中,我们还指示 Spring 执行指定的脚本来创建数据库模式,并相应地填充测试数据。请注意,脚本的顺序很重要,包含数据定义语言(DDL)的文件应该总是首先出现,然后是包含数据操作语言(DML)的文件。对于type属性,我们指定要使用的嵌入式数据库的类型。从 4.0 版本开始,Spring 支持 HSQL(默认)、H2 和 DERBY。
嵌入式数据库也可以使用 Java Configuration类来配置。要使用的类是EmbeddedDatabaseBuilder,它使用数据库创建和加载数据脚本作为参数来创建实现DataSource的EmbeddedDatabase实例。
package com.apress.prospring5.ch6.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import javax.sql.DataSource;
@Configuration
public class EmbeddedJdbcConfig {
private static Logger logger =
LoggerFactory.getLogger(EmbeddedJdbcConfig.class);
@Bean
public DataSource dataSource() {
try {
EmbeddedDatabaseBuilder dbBuilder =
new EmbeddedDatabaseBuilder();
return dbBuilder.setType(EmbeddedDatabaseType.H2)
.addScripts("classpath:db/h2/schema.sql",
"classpath:db/h2/test-data.sql").build();
} catch (Exception e) {
logger.error("Embedded DataSource bean cannot be created!", e);
return null;
}
}
...
}
嵌入式数据库支持对于本地开发或单元测试非常有用。在本章的其余部分,我们使用嵌入式数据库来运行示例代码,因此您的机器不需要安装数据库来运行示例。
您不仅可以通过 JDBC 命名空间利用嵌入式数据库支持,还可以初始化在其他地方运行的数据库实例,如 MySQL、Oracle 等。不要指定type和embedded-database,只需使用initialize-database,您的脚本将针对预期的dataSource执行,就像它们针对嵌入式数据库一样。
在 DAO 类中使用数据源
数据访问对象(DAO)模式用于将低级数据访问 API 或操作与高级业务服务分开。数据访问对象模式需要下列组件:
- DAO 接口:它定义了要在一个模型对象(或多个对象)上执行的标准操作。
- DAO 实现:这个类提供了 DAO 接口的具体实现。通常,这使用 JDBC 连接或数据源来处理模型对象。
- 模型对象也称为数据对象或实体:这是一个简单的到表记录的 POJO 映射。
让我们为示例创建一个要实现的SingerDao接口,如下所示:
package com.apress.prospring5.ch6.dao;
public interface SingerDao {
String findNameById(Long id);
}
对于简单的实现,首先我们将向JdbcSingerDao实现类添加一个dataSource属性。我们想要将dataSource属性添加到实现类而不是接口的原因应该是很明显的:接口不需要知道数据将如何被检索和更新。通过向接口添加DataSource mutator 方法,在最好的情况下,这迫使实现声明 getter 和 setter 存根。显然,这不是一个很好的设计实践。看看这里显示的简单的JdbcSingerDao类:
import com.apress.prospring5.ch6.dao.SingerDao;
import com.apress.prospring5.ch6.entities.Singer;
import org.apache.commons.lang3.NotImplementedException;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
import java.util.List;
public class JdbcSingerDao implements SingerDao, InitializingBean {
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
public void afterPropertiesSet() throws Exception {
if (dataSource == null) {
throw new BeanCreationException(
"Must set dataSource on SingerDao");
}
}
...
}
我们现在可以指示 Spring 通过使用JdbcSingerDao实现来配置我们的singerDao bean,并设置dataSource属性,如下面的EmbeddedJdbcConfig配置类所示:
package com.apress.prospring5.ch6.config;
...
@Configuration
public class EmbeddedJdbcConfig {
private static Logger logger =
LoggerFactory.getLogger(EmbeddedJdbcConfig.class);
@Bean
public DataSource dataSource() {
try {
EmbeddedDatabaseBuilder dbBuilder =
new EmbeddedDatabaseBuilder();
return dbBuilder.setType(EmbeddedDatabaseType.H2)
.addScripts("classpath:db/h2/schema.sql",
"classpath:db/h2/test-data.sql").build();
} catch (Exception e) {
logger.error("Embedded DataSource bean cannot be created!", e);
return null;
}
}
@Bean
public SingerDao singerDao(){
JdbcSingerDao dao = new JdbcSingerDao();
dao.setDataSource(dataSource());
return dao;
}
}
Spring 支持相当多的嵌入式数据库,但是它们必须作为依赖项添加到项目中。这里您可以看到一些特定于数据库的库被配置用于pro-spring-15\gradle.build:
ext {
derbyVersion = '10.13.1.1'
dbcpVersion = '2.1'
mysqlVersion = '6.0.6'
h2Version = '1.4.194'
...
db = [
mysql: "mysql:mysql-connector-java:$mysqlVersion",
derby: "org.apache.derby:derby:$derbyVersion",
dbcp : "org.apache.commons:commons-dbcp2:$dbcpVersion",
h2 : "com.h2database:h2:$h2Version"
]
}
Spring 现在通过实例化JdbcSingerDao类来创建singerDao bean,并将dataSource属性设置为dataSource bean。确保已经设置了 bean 上所有必需的属性是一种很好的做法。最简单的方法是实现InitializingBean接口,并为afterPropertiesSet()方法提供一个实现。这样,我们确保所有必需的属性都已经在我们的JdbcSingerDao上设置好了。关于 bean 初始化的进一步讨论,请参考第四章。
到目前为止,我们看到的代码使用 Spring 来管理数据源,并引入了SingerDao接口及其 JDBC 实现。我们还在 Spring ApplicationContext文件中的JdbcSingerDao类上设置了dataSource属性。现在,我们通过向接口和实现添加实际的 DAO 操作来扩展代码。
异常处理
因为 Spring 提倡使用运行时异常而不是检查异常,所以您需要一种机制来将检查的SQLException转换成运行时 Spring JDBC 异常。因为 Spring 的 SQL 异常是运行时异常,所以它们可以比检查异常更细粒度。根据定义,这不是运行时异常的特性,但是不得不在throws子句中声明一长串检查过的异常是不方便的;因此,被检查的异常往往比它们的运行时等价物更粗粒度。
Spring 提供了SQLExceptionTranslator接口的默认实现,它负责将通用 SQL 错误代码转换成 Spring JDBC 异常。在大多数情况下,这个实现已经足够了,但是您可以扩展 Spring 的默认实现,并将您的新的SQLExceptionTranslator实现设置为在JdbcTemplate中使用,如下面的代码示例所示:
package com.apress.prospring5.ch6;
import java.sql.SQLException;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DeadlockLoserDataAccessException;
import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator;
public class MySQLErrorCodesTranslator extends
SQLErrorCodeSQLExceptionTranslator {
@Override
protected DataAccessException customTranslate(String task,
String sql, SQLException sqlex) {
if (sqlex.getErrorCode() == -12345) {
return new DeadlockLoserDataAccessException(task, sqlex);
}
return null;
}
}
同时,我们需要将对spring-jdbc的依赖添加到项目中。在这里你可以看到spring-jdbc被配置用于pro-spring-15/gradle.build:
ext {
springVersion = '5.0.0.M4'
...
spring = [
jdbc : "org.springframework:spring-jdbc:$springVersion",
...
]
}
这个库作为一个依赖项被添加到所有用作本章示例的 JDBC 项目中。
//chapter06/spring-jdbc-embedded/build.gradle
dependencies {
compile spring.jdbc
compile db.h2, db.derby
}
要使用自定义翻译器,我们需要将它传递到 DAO 类中的JdbcTemplate中。下面的代码片段代表了一段增强的JdbcSingerDao.setDataSource()方法来说明它的用法:
...
public class JdbcSingerDao implements SingerDao, InitializingBean {
private DataSource dataSource;
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource);
MySQLErrorCodesTranslator errorTranslator =
new MySQLErrorCodesTranslator();
errorTranslator.setDataSource(dataSource);
jdbcTemplate.setExceptionTranslator(errorTranslator);
this.jdbcTemplate = jdbcTemplate;
}
...
}
有了定制的 SQL 异常翻译器,Spring 将在对数据库执行 SQL 语句时检测到 SQL 异常时调用它,当错误代码为-12345时,将进行定制的异常翻译。对于其他错误,Spring 将退回到其默认的异常翻译机制。显然,没有什么可以阻止您将SQLExceptionTranslator创建为 Spring 管理的 bean,并在您的 DAO 类中使用JdbcTemplate bean。不记得看过JdbcTemplate类的也不用担心;我们将更详细地讨论它。
JdbcTemplate 类
这个类代表了 Spring 的 JDBC 支持的核心。它可以执行所有类型的 SQL 语句。在最简单的视图中,您可以对数据定义和数据操作语句进行分类。数据定义语句包括创建各种数据库对象(表、视图、存储过程等等)。数据操作语句操作数据,可分为 select 和 update 语句。select 语句通常返回一组行;每一行都有相同的一组列。update 语句修改数据库中的数据,但不返回任何结果。
JdbcTemplate类允许您向数据库发出任何类型的 SQL 语句,并返回任何类型的结果。
在这一节中,我们将通过JdbcTemplate类来浏览 Spring 中 JDBC 编程的几个常见用例。
在 DAO 类中初始化 JdbcTemplate
在讨论如何使用JdbcTemplate之前,我们先来看看如何准备JdbcTemplate在 DAO 类中使用。很直白;大多数时候,您只需要通过传入数据源对象(应该由 Spring 注入到 DAO 类中)来构造类。下面的代码片段显示了将初始化JdbcTemplate对象的代码片段:
public class JdbcSingerDao implements SingerDao, InitializingBean {
private DataSource dataSource;
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource);
}
...
}
通常的做法是在setDataSource方法中初始化JdbcTemplate,这样一旦数据源被 Spring 注入,JdbcTemplate也将被初始化并准备好使用。
一旦配置好,JdbcTemplate就是线程安全的。这意味着您也可以选择在 Spring 的配置中初始化单个的JdbcTemplate实例,并将其注入到所有的 DAO beans 中。类似这样的配置如下所示:
package com.apress.prospring5.ch6.config;
...
@Configuration
public class EmbeddedJdbcConfig {
private static Logger logger =
LoggerFactory.getLogger(EmbeddedJdbcConfig.class);
@Bean
public DataSource dataSource() {
try {
EmbeddedDatabaseBuilder dbBuilder =
new EmbeddedDatabaseBuilder();
return dbBuilder.setType(EmbeddedDatabaseType.H2)
.addScripts("classpath:db/h2/schema.sql",
"classpath:db/h2/test-data.sql").build();
} catch (Exception e) {
logger.error("Embedded DataSource bean cannot be created!", e);
return null;
}
}
@Bean public JdbcTemplate jdbcTemplate(){
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource());
return jdbcTemplate;
}
@Bean
public SingerDao singerDao() {
JdbcSingerDao dao = new JdbcSingerDao();
dao.setJdbcTemplate(jdbcTemplate());
return dao;
}
}
使用 JdbcTemplate 检索单值
让我们从一个返回单个值的简单查询开始。例如,我们希望能够通过 ID 检索歌手的名字。使用 jdbcTemplate,我们可以轻松地检索值。以下代码片段显示了 JdbcSingerDao 类中 findNameById()方法的实现。对于其他方法,创建了空的实现。
...
public class JdbcSingerDao implements SingerDao, InitializingBean {
private JdbcTemplate jdbcTemplate;
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override public String findNameById(Long id) {
return jdbcTemplate.queryForObject(
"select first_name || ' ' || last_name from singer where id = ?",
new Object{id}, String.class);
}
@Override public void insert(Singer singer) {
throw new NotImplementedException("insert");
}
...
}
在前面的清单中,我们使用JdbcTemplate的queryForObject()方法来检索名字的值。第一个参数是 SQL 字符串,第二个参数由以对象数组格式传递给 SQL 进行参数绑定的参数组成。最后一个参数是要返回的类型,在本例中是String。除了Object,还可以查询其他类型,如Long、Integer。让我们来看看结果。下面的代码片段显示了测试程序。同样,将使用 JUnit 测试类,因为这允许我们单独运行测试方法,并且由于测试是在执行glade build时运行的,我们也确保了我们的构建保持稳定。
package com.apress.prospring5.ch6;
...
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class JdbcCfgTest {
@Test
public void testH2DB() {
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext();
ctx.load("classpath:spring/embedded-h2-cfg.xml");
ctx.refresh();
testDao(ctx.getBean(SingerDao.class));
ctx.close();
}
private void testDao(SingerDao singerDao) {
assertNotNull(singerDao);
String singerName = singerDao.findNameById(1l);
assertTrue("John Mayer".equals(singerName));
}
}
当执行测试方法testH2DB()时,我们期望通过singerDao.findNameById(1l)调用返回John Mayer字符串,并且我们使用asertTrue方法测试这个假设。如果在初始化数据库时出现任何问题,该测试将会失败。
对 NamedParameterJdbcTemplate 使用命名参数
在前面的例子中,我们使用普通的占位符(?字符)作为查询参数,我们需要将参数值作为一个Object数组传递。使用普通占位符时,顺序很重要,将参数放入数组的顺序应该与参数在查询中的顺序相同。
一些开发人员更喜欢使用命名参数来确保每个参数都完全按照预期进行绑定。在 Spring 中,JdbcTemplate类的一个扩展叫做NamedParameterJdbcTemplate(在org.springframework.jdbc.core.namedparam包下),提供了这种支持。
NamedParameterJdbcTemplate的初始化与JdbcTemplate相同,所以我们只需要声明一个NamedParameterJdbcTemplate类型的 bean,并将其注入到Dao类中。在下面的代码中,你可以看到新的和改进的JdbcSingerDao:
package com.apress.prospring5.ch6;
public class JdbcSingerDao implements
SingerDao, InitializingBean {
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
@Override
public String findNameById(Long id) {
String sql = "SELECT first_name ||' '|| last_name
FROM singer WHERE id = :singerId";
Map<String, Object> namedParameters = new HashMap<>();
namedParameters.put("singerId", id);
return namedParameterJdbcTemplate.queryForObject(sql,
namedParameters, String.class);
}
public void setNamedParameterJdbcTemplate
(NamedParameterJdbcTemplate namedParameterJdbcTemplate) {
this.namedParameterJdbcTemplate = namedParameterJdbcTemplate;
}
@Override
public void afterPropertiesSet() throws Exception {
if (namedParameterJdbcTemplate == null) {
throw new BeanCreationException
("Null NamedParameterJdbcTemplate on SingerDao");
}
}
...
}
您将看到使用了命名参数(以分号为前缀)而不是?占位符::singerId。下面的代码片段可以用来测试新的JdbcSingerDao:
public class NamedJdbcCfgTest {
@Test
public void testCfg(){
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(NamedJdbcCfg.class);
SingerDao singerDao = ctx.getBean(SingerDao.class);
assertNotNull(singerDao);
String singerName = singerDao.findNameById(1l);
assertTrue("John Mayer".equals(singerName));
ctx.close();
}
}
当执行测试方法testCfg()时,我们期望通过singerDao.findNameById(1l)调用返回John Mayer字符串,并且我们使用asertTrue方法测试这个假设。如果在初始化数据库时出现任何问题,该测试将会失败。
使用行映射器检索域对象
大多数情况下,您会希望查询一行或多行,然后将每一行转换成相应的域对象或实体,而不是检索单个值。Spring 的RowMapper<T>接口(在包org.springframework.jdbc.core下)提供了一种简单的方法来执行从 JDBC 结果集到 POJOs 的映射。让我们通过使用RowMapper<T>接口实现SingerDao的findAll()方法来看看它是如何工作的。在下面的代码片段中,您可以看到findAll()方法的实现:
package com.apress.prospring5.ch6;
...
public class JdbcSingerDao implements
SingerDao, InitializingBean {
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
public void setNamedParameterJdbcTemplate(
NamedParameterJdbcTemplate namedParameterJdbcTemplate) {
this.namedParameterJdbcTemplate = namedParameterJdbcTemplate;
}
@Override
public List<Singer> findAll() {
String sql = "select id, first_name, last_name, birth_date from singer";
return namedParameterJdbcTemplate.query(sql, new SingerMapper());
}
private static final class SingerMapper
implements RowMapper<Singer> {
@Override
public Singer mapRow(ResultSet rs, int rowNum)
throws SQLException {
Singer singer = new Singer();
singer.setId(rs.getLong("id"));
singer.setFirstName(rs.getString("first_name"));
singer.setLastName(rs.getString("last_name"));
singer.setBirthDate(rs.getDate("birth_date"));
return singer;
}
}
@Override
public void afterPropertiesSet() throws Exception {
if (namedParameterJdbcTemplate == null) {
throw new BeanCreationException(
"Null NamedParameterJdbcTemplate on SingerDao");
}
}
}
在前面的代码片段中,我们定义了一个名为SingerMapper的静态内部类,它实现了RowMapper<Singer>接口。该类需要提供mapRow()实现,它将ResultSet的特定记录中的值转换成您想要的域对象。使它成为一个静态内部类允许你在多个查找器方法中共享RowMapper<Singer>。
使用 Java 8 lambda 表达式可以完全跳过类SingerMapper的显式实现;因此,findAll()方法可以这样重构:
public List<Singer> findAll() {
String sql = "select id, first_name, last_name, birth_date from singer";
return namedParameterJdbcTemplate.query(sql, (rs, rowNum) -> {
Singer singer = new Singer();
singer.setId(rs.getLong("id"));
singer.setFirstName(rs.getString("first_name"));
singer.setLastName(rs.getString("last_name"));
singer.setBirthDate(rs.getDate("birth_date"));
return singer;
});
}
之后,findAll()方法只需要调用查询方法,并传入查询字符串和行映射器。如果查询需要参数,query()方法提供了一个接受查询参数的重载方法。下面的测试类包含了一个用于findAll()方法的测试方法:
public class RowMapperTest {
@Test
public void testRowMapper() {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(NamedJdbcCfg.class);
SingerDao singerDao = ctx.getBean(SingerDao.class);
assertNotNull(singerDao);
List<Singer> singers = singerDao.findAll();
assertTrue(singers.size() == 3);
singers.forEach(singer -> {
System.out.println(singer);
if (singer.getAlbums() != null) {
for (Album album :
singer.getAlbums()) {
System.out.println("---" + album);
}
}
});
ctx.close();
}
}
如果运行testRowMapper方法,测试必须通过,并且必须生成以下内容:
Singer - Id: 1, First name: John, Last name: Mayer, Birthday: 1977-10-16
Singer - Id: 2, First name: Eric, Last name: Clapton, Birthday: 1945-03-30
Singer - Id: 3, First name: John, Last name: Butler, Birthday: 1975-04-01
相册没有被打印出来,因为RowMapper<Singer>实现并没有在返回的Singer实例上设置它们。
使用 ResultSetExtractor 检索嵌套的域对象
让我们来看一个稍微复杂一些的例子,在这个例子中,我们需要通过一个连接从父表(SINGER)和子表(ALBUM)中检索数据,然后相应地将数据转换回嵌套对象(Singer中的Album)。
前面提到的RowMapper<T>只适用于单个域对象的行映射。对于更复杂的对象结构,我们需要使用ResultSetExtractor接口。为了演示它的用法,让我们在SingerDao接口中再添加一个方法findAllWithAlbums()。该方法应该用歌手的专辑详细信息填充歌手列表。
下面的代码片段显示了将findAllWithAlbums()方法添加到接口以及使用ResultSetExtractor实现该方法:
package com.apress.prospring5.ch6;
...
public class JdbcSingerDao implements SingerDao, InitializingBean {
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
public void setNamedParameterJdbcTemplate(
NamedParameterJdbcTemplate namedParameterJdbcTemplate) {
this.namedParameterJdbcTemplate = namedParameterJdbcTemplate;
}
@Override
public List<Singer> findAllWithAlbums() {
String sql = "select s.id, s.first_name, s.last_name, s.birth_date" +
", a.id as a.album_id, a.title, a.release_date from singer s " +
"left join album a on s.id = a.singer_id";
return namedParameterJdbcTemplate.query(sql, new SingerWithDetailExtractor());
}
private static final class SingerWithDetailExtractor implements
ResultSetExtractor<List<Singer>> {
@Override
public List<Singer> extractData(ResultSet rs) throws SQLException,
DataAccessException {
Map<Long, Singer> map = new HashMap<>();
Singer singer;
while (rs.next()) {
Long id = rs.getLong("id");
singer = map.get(id);
if (singer == null) {
singer = new Singer();
singer.setId(id);
singer.setFirstName(rs.getString("first_name"));
singer.setLastName(rs.getString("last_name"));
singer.setBirthDate(rs.getDate("birth_date"));
singer.setAlbums(new ArrayList<>());
map.put(id, singer);
}
Long albumId = rs.getLong("singer_tel_id");
if (albumId > 0) {
Album album = new Album();
album.setId(albumId);
album.setSingerId(id);
album.setTitle(rs.getString("title"));
album.setReleaseDate(rs.getDate("release_date"));
singer.addAbum(album);
}
}
return new ArrayList<>(map.values());
}
}
@Override
public void afterPropertiesSet() throws Exception {
if (namedParameterJdbcTemplate == null) {
throw new BeanCreationException(
"Null NamedParameterJdbcTemplate on SingerDao");
}
}
}
代码看起来很像RowMapper示例,但是这次我们声明了一个实现ResultSetExtractor的内部类。然后,我们实现extractData()方法,将结果集相应地转换成一列Singer对象。对于findAllWithDetail()方法,查询使用左连接来连接两个表,这样没有专辑的歌手也将被检索。结果是两个表的笛卡尔积。最后,我们使用JdbcTemplate.query()方法,传入查询字符串和结果集提取器。
当然,SingerWithDetailExtractor内部类实际上并不是必需的,因为使用了 lambda 表达式。这里您可以看到使用 Java 8 lambda 表达式的findAllWithAlbums()版本:
public List<Singer> findAllWithAlbums() {
String sql = "select s.id, s.first_name, s.last_name, s.birth_date" +
", a.id as a.album_id, a.title, a.release_date from singer s " +
"left join album a on s.id = a.singer_id";
return namedParameterJdbcTemplate.query(sql, rs -> {
Map<Long, Singer> map = new HashMap<>();
Singer singer;
while (rs.next()) {
Long id = rs.getLong("id");
singer = map.get(id);
if (singer == null) {
singer = new Singer();
singer.setId(id);
singer.setFirstName(rs.getString("first_name"));
singer.setLastName(rs.getString("last_name"));
singer.setBirthDate(rs.getDate("birth_date"));
singer.setAlbums(new ArrayList<>());
map.put(id, singer);
}
Long albumId = rs.getLong("singer_tel_id");
if (albumId > 0) {
Album album = new Album();
album.setId(albumId);
album.setSingerId(id);
album.setTitle(rs.getString("title"));
album.setReleaseDate(rs.getDate("release_date"));
singer.addAbum(album);
}
}
return new ArrayList<>(map.values());
});
}
下面的测试类包含了一个用于findAllWithAlbums()方法的测试方法:
public class ResultSetExtractorTest {
@Test
public void testResultSetExtractor() {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(NamedJdbcCfg.class);
SingerDao singerDao = ctx.getBean(SingerDao.class);
assertNotNull(singerDao);
List<Singer> singers = findAllWithAlbums();
assertTrue(singers.size() == 3);
singers.forEach(singer -> {
System.out.println(singer);
if (singer.getAlbums() != null) {
for (Album album :
singer.getAlbums()) {
System.out.println("\t--> " + album);
}
}
});
ctx.close();
}
}
如果运行testResultSetExtractor()方法,测试必须通过,并且必须生成以下内容:
Singer - Id: 1, First name: John, Last name: Mayer, Birthday: 1977-10-16
--> Album - Id: 2, Singer id: 1, Title: Battle Studies,
Release Date: 2009-11-17
--> Album - Id: 1, Singer id: 1, Title: The Search For Everything,
Release Date: 2017-01-20
Singer - Id: 2, First name: Eric, Last name: Clapton, Birthday: 1945-03-30
--> Album - Id: 3, Singer id: 2, Title: From The Cradle,
Release Date: 1994-09-13
Singer - Id: 3, First name: John, Last name: Butler, Birthday: 1975-04-01
你可以看到歌手和他们的专辑详细信息相应地列出。数据基于数据填充脚本,您可以在resources/db/test-data.sql中找到每个 JDBC 样本项目的数据填充脚本。到目前为止,您已经看到了如何使用JdbcTemplate来执行一些常见的查询操作。JdbcTemplate(以及NamedParameterJdbcTemplate类)提供了许多重载update()方法,支持数据更新操作,包括插入、更新、删除等等。然而,update()方法是不言自明的,所以我们把它作为一个练习留给你去探索。另一方面,正如您将在后面的章节中看到的,我们将使用 Spring 提供的SqlUpdate类来执行数据更新操作。
模拟 JDBC 操作的 Spring 类
在上一节中,您看到了JdbcTemplate和相关的数据映射器实用程序类如何在使用 JDBC 开发数据访问逻辑时极大地简化了编程模型。建立在JdbcTemplate之上,Spring 还提供了许多有用的类,这些类模拟 JDBC 数据操作,并让开发人员以更加面向对象的方式维护从ResultSet到域对象的查询和转换逻辑。具体来说,本节介绍了以下类别:
MappingSqlQuery<T>:MappingSqlQuery<T>类允许你将查询字符串和mapRow()方法打包成一个类。SqlUpdate:SqlUpdate类允许您将任何 SQL update 语句包装到其中。它还为您提供了许多有用的函数来绑定 SQL 参数,在插入新记录后检索 RDBMS 生成的键,等等。BatchSqlUpdate:顾名思义,这个类允许你进行批量更新操作。例如,您可以循环遍历一个 JavaList对象,让BatchSqlUpdate将记录排队,并批量提交更新语句。您可以随时设置批处理大小和刷新操作。SqlFunction<T>:SqlFunction<T>类允许你用参数和返回类型调用数据库中的存储函数。另一个类StoredProcedure也存在,它帮助您调用存储过程。- 利用注释设置 JDBC 道
首先让我们看看如何使用注释来设置 DAO 实现类。下面的示例代码逐个方法地实现了SingerDao接口,直到我们有了完整的SingerDao实现。下面的代码片段显示了SingerDao接口类及其提供的数据访问服务的完整列表:
package com.apress.prospring5.ch6.dao;
import com.apress.prospring5.ch6.entities.Singer;
import java.util.List;
public interface SingerDao {
List<Singer> findAll();
List<Singer> findByFirstName(String firstName);
String findNameById(Long id);
String findLastNameById(Long id);
String findFirstNameById(Long id);
List<Singer> findAllWithAlbums();
void insert(Singer singer);
void update(Singer singer);
void delete(Long singerId);
void insertWithAlbum(Singer singer);
}
在本书的开始,引入了原型注释,并且引入了作为@Component注释的专门化的@Repository,它被设计用于 beans 处理数据库操作。 4 下面的代码片段显示了使用 JSR-250 注释将数据源属性初始声明和注入到一个@Repository带注释的 DAO 类中:
package com.apress.prospring5.ch6.dao;
...
@Repository("singerDao")
public class JdbcSingerDao implements SingerDao {
private static final Log logger =
LogFactory.getLog(JdbcSingerDao.class);
private DataSource dataSource;
@Resource(name = "dataSource")
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
public DataSource getDataSource() {
return dataSource;
}
...
}
在前面的清单中,我们使用@Repository来声明名为singerDao的 Spring bean,由于该类包含数据访问代码,@Repository还指示 Spring 对 Spring 中更加应用友好的DataAccessException层次结构执行特定于数据库的 SQL 异常。
我们还通过使用 SL4J 日志记录来声明 log 变量,以记录程序中的消息。我们使用 JSR-250 的@Resource作为dataSource属性,让 Spring 注入名为dataSource的数据源。
在下面的代码示例中,您可以看到使用注释声明 DAO 处理 beans 时的 Java 配置类:
package com.apress.prospring5.ch6.config;
import org.apache.commons.dbcp2.BasicDataSource;
...
@Configuration
@PropertySource("classpath:db/jdbc2.properties")
@ComponentScan(basePackages = "com.apress.prospring5.ch6")
public class AppConfig {
private static Logger logger =
LoggerFactory.getLogger(AppConfig.class);
@Value("${driverClassName}")
private String driverClassName;
@Value("${url}")
private String url;
@Value("${username}")
private String username;
@Value("${password}")
private String password;
@Bean
public static PropertySourcesPlaceholderConfigurer
propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
@Bean(destroyMethod = "close")
public DataSource dataSource() {
try {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName(driverClassName);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
} catch (Exception e) {
logger.error("DBCP DataSource bean cannot be created!", e);
return null;
}
}
}
在这个配置中,我们声明了一个 MySQL 数据库,使用一个可池化的BasicDataSource来访问它,并使用组件扫描来自动发现 Spring bean。在本章的开始,你被告知如何安装和设置一个 MySQL 数据库并创建musicdb模式。基础设施就绪后,我们现在可以着手实施 JDBC 行动。
使用 MappingSqlQuery 查询数据
Spring 为建模查询操作提供了MappingSqlQuery<T>类。基本上,我们通过使用数据源和查询字符串来构造一个MappingSqlQuery<T>类。然后我们实现mapRow()方法,将每个ResultSet记录映射到相应的域对象。
让我们首先创建扩展了抽象类MappingSqlQuery<T>的SelectAllSingers类(它表示选择所有歌手的查询操作)。这里显示了SelectAllSingers类:
package com.apress.prospring5.ch6;
import java.sql.ResultSet;
import java.sql.SQLException;
import javax.sql.DataSource;
import com.apress.prospring5.ch6.entities.Singer;
import org.springframework.jdbc.object.MappingSqlQuery;
public class SelectAllSingers extends MappingSqlQuery<Singer> {
private static final String SQL_SELECT_ALL_SINGER =
"select id, first_name, last_name, birth_date from singer";
public SelectAllSingers(DataSource dataSource) {
super(dataSource, SQL_SELECT_ALL_SINGER);
}
protected Singer mapRow(ResultSet rs, int rowNum)
throws SQLException {
Singer singer = new Singer();
singer.setId(rs.getLong("id"));
singer.setFirstName(rs.getString("first_name"));
singer.setLastName(rs.getString("last_name"));
singer.setBirthDate(rs.getDate("birth_date"));
return singer;
}
}
在SelectAllSingers类中,声明了选择所有歌手的 SQL。在类构造函数中,调用super()方法来构造类,使用DataSource和 SQL 语句。此外,实现了MappingSqlQuery<T>.mapRow()方法来提供结果集到Singer域对象的映射。
有了SelectAllSingers类,我们可以在JdbcSingerDao类中实现findAll()方法。下面的代码片段描述了JdbcSingerDao类的一部分:
package com.apress.prospring5.ch6;
@Repository("singerDao")
public class JdbcSingerDao implements SingerDao {
private DataSource dataSource;
private SelectAllSingers selectAllSingers;
@Resource(name = "dataSource")
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
this.selectAllSingers = new SelectAllSingers(dataSource);
}
@Override
public List<Singer> findAll() {
return selectAllSingers.execute();
}
...
}
在setDataSource()方法中,在注入DataSource后,构建了一个SelectAllSingers类的实例。在findAll()方法中,我们简单地调用了execute()方法,该方法间接继承自SqlQuery<T>抽象类。这就是我们需要做的。以下代码片段显示了用于测试以这种方式实现的findAll()方法的方法:
com.apress.prospring5.ch6;
public class AnnotationJdbcTest {
private GenericApplicationContext ctx;
private SingerDao singerDao;
@Before
public void setUp() {
ctx = new AnnotationConfigApplicationContext(AppConfig.class);
singerDao = ctx.getBean(SingerDao.class);
assertNotNull(singerDao);
}
@Test
public void testFindAll() {
List<Singer> singers = singerDao.findAll();
assertTrue(singers.size() == 3);
singers.forEach(singer -> {
System.out.println(singer);
if (singer.getAlbums() != null) {
for (Album album : singer.getAlbums()) {
System.out.println("\t--> " + album);
}
}
});
ctx.close();
}
@After
public void tearDown() {
ctx.close();
}
}
如果测试通过,运行测试方法会产生以下输出:
Singer - Id: 1, First name: John, Last name: Mayer, Birthday: 1977-10-15
Singer - Id: 2, First name: Eric, Last name: Clapton, Birthday: 1945-03-29
Singer - Id: 3, First name: John, Last name: Butler, Birthday: 1975-03-31
如果通过编辑logback-test.xml配置文件并添加以下元素为org.springframework.jdbc包启用了DEBUG日志记录:
<logger name="org.springframework.jdbc" level="debug"/>
然后在控制台中,您还会看到 Spring 提交的查询,如下所示:
DEBUG o.s.j.c.JdbcTemplate - Executing prepared SQL query
DEBUG o.s.j.c.JdbcTemplate - Executing prepared SQL statement
[select id, first_name, last_name, birth_date from singer]
让我们继续实现findByFirstName()方法,它接受一个命名参数。与前面的示例一样,我们为操作创建了类SelectSingerByFirstName,如下所示:
package com.apress.prospring5.ch6;
...
import org.springframework.jdbc.core.SqlParameter;
public class SelectSingerByFirstName extends MappingSqlQuery<Singer> {
private static final String SQL_FIND_BY_FIRST_NAME =
"select id, first_name, last_name, birth_date from
singer where first_name = :first_name";
public SelectSingerByFirstName(DataSource dataSource) {
super(dataSource, SQL_FIND_BY_FIRST_NAME);
super.declareParameter(new SqlParameter("first_name", Types.VARCHAR));
}
protected Singer mapRow(ResultSet rs, int rowNum) throws SQLException {
Singer singer = new Singer();
singer.setId(rs.getLong("id"));
singer.setFirstName(rs.getString("first_name"));
singer.setLastName(rs.getString("last_name"));
singer.setBirthDate(rs.getDate("birth_date"));
return singer;
}
}
SelectSingerByFirstName类类似于SelectAllSingers类。首先,SQL 语句是不同的,它带有一个名为first_name的命名参数。在构造函数方法中,调用了declareParameter()方法(间接从org.springframework.jdbc.object.RdbmsOperation抽象类继承而来)。让我们继续在JdbcSingerDao类中实现findByFirstName()方法。在这里您可以看到更新后的代码:
package com.apress.prospring5.ch6.dao;
...
@Repository("singerDao")
public class JdbcSingerDao implements SingerDao {
private static Logger logger = LoggerFactory.getLogger(JdbcSingerDao.class);
private DataSource dataSource;
private SelectSingerByFirstName selectSingerByFirstName;
@Resource(name = "dataSource")
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
this.selectSingerByFirstName =
new SelectSingerByFirstName(dataSource);
}
@Override
public List<Singer> findByFirstName(String firstName) {
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("first_name", firstName);
return selectSingerByFirstName.executeByNamedParam(paramMap);
}
...
}
在数据源注入时,构造一个SelectSingerByFirstName的实例。然后,在findByFirstName()方法中,用指定的参数和值构造一个HashMap。最后,调用executeByNamedParam()方法(间接从SqlQuery<T>抽象类继承而来)。让我们通过执行这里所示的testFindByFirstName()测试方法来测试这个方法:
package com.apress.prospring5.ch6;
...
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class AnnotationJdbcTest {
private GenericApplicationContext ctx;
private SingerDao singerDao;
@Before
public void setUp() {
ctx = new AnnotationConfigApplicationContext(AppConfig.class);
singerDao = ctx.getBean(SingerDao.class);
assertNotNull(singerDao);
}
@Test
public void testFindByFirstName() {
List<Singer> singers = singerDao.findByFirstName("John");
assertTrue(singers.size() == 1);
listSingers(singers);
ctx.close();
}
private void listSingers(List<Singer> singers){
singers.forEach(singer -> {
System.out.println(singer);
if (singer.getAlbums() != null) {
for (Album album : singer.getAlbums()) {
System.out.println("\t--> " + album);
}
}
});
}
@After
public void tearDown() {
ctx.close();
}
}
如果测试通过,运行测试方法会产生以下输出:
Singer - Id: 1, First name: John, Last name: Mayer, Birthday: 1977-10-15
这里值得注意的一点是,MappingSqlQuery<T>只适合将单个行映射到一个域对象。对于嵌套对象,您仍然需要将JdbcTemplate与ResultSetExtractor一起使用,就像在JdbcTemplate类一节中介绍的示例方法findAllWithAlbums()一样。
使用 SqlUpdate 更新数据
为了更新数据,Spring 提供了SqlUpdate类。下面的代码片段显示了为更新操作扩展了SqlUpdate类的UpdateSinger类:
package com.apress.prospring5.ch6;
import java.sql.Types;
import javax.sql.DataSource;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.object.SqlUpdate;
public class UpdateSinger extends SqlUpdate {
private static final String SQL_UPDATE_SINGER =
"update singer set first_name=:first_name, last_name=:last_name,
birth_date=:birth_date where id=:id";
public UpdateSinger(DataSource dataSource) {
super(dataSource, SQL_UPDATE_SINGER);
super.declareParameter(new SqlParameter("first_name", Types.VARCHAR));
super.declareParameter(new SqlParameter("last_name", Types.VARCHAR));
super.declareParameter(new SqlParameter("birth_date", Types.DATE));
super.declareParameter(new SqlParameter("id", Types.INTEGER));
}
}
您现在应该对前面的清单很熟悉了。用查询构造了一个SqlUpdate类的实例,并声明了命名参数。下面的代码片段展示了在JdbcSingerDao类中update()方法的实现:
package com.apress.prospring5.ch6.dao;
...
@Repository("singerDao")
public class JdbcSingerDao implements SingerDao {
private static Logger logger =
LoggerFactory.getLogger(JdbcSingerDao.class);
private DataSource dataSource;
private UpdateSinger updateSinger;
@Override
public void update(Singer singer) {
Map<String, Object> paramMap = new HashMap<String, Object>();
paramMap.put("first_name", singer.getFirstName());
paramMap.put("last_name", singer.getLastName());
paramMap.put("birth_date", singer.getBirthDate());
paramMap.put("id", singer.getId());
updateSinger.updateByNamedParam(paramMap);
logger.info("Existing singer updated with id: " + singer.getId());
}
@Resource(name = "dataSource")
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
this.updateSinger = new UpdateSinger(dataSource);
}
...
}
在数据源注入时,构造一个UpdateSinger的实例。在update()方法中,从传入的Singer对象构造一个命名参数的HashMap,然后调用updateByNamedParam()更新联系人记录。为了测试操作,让我们给AnnotationJdbcTest添加一个新方法。
package com.apress.prospring5.ch6;
...
public class AnnotationJdbcTest {
private GenericApplicationContext ctx;
private SingerDao singerDao;
@Before
public void setUp() {
ctx = new AnnotationConfigApplicationContext(AppConfig.class);
singerDao = ctx.getBean(SingerDao.class);
assertNotNull(singerDao);
}
@Test
public void testSingerUpdate() {
Singer singer = new Singer();
singer.setId(1L);
singer.setFirstName("John Clayton");
singer.setLastName("Mayer");
singer.setBirthDate(new Date(
(new GregorianCalendar(1977, 9, 16)).getTime().getTime()));
singerDao.update(singer);
List<Singer> singers = singerDao.findAll();
listSingers(singers);
}
private void listSingers(List<Singer> singers){
singers.forEach(singer -> {
System.out.println(singer);
if (singer.getAlbums() != null) {
for (Album album : singer.getAlbums()) {
System.out.println("\t--> " + album);
}
}
});
}
@After
public void tearDown() {
ctx.close();
}
}
这里我们简单地构造一个Singer对象,然后调用update()方法。运行该程序会从最后一个listSingers()方法产生以下输出。如果测试通过,运行测试方法会产生以下输出:
Singer - Id: 1, First name: John Clayton, Last name: Mayer, Birthday: 1977-10-16
Singer - Id: 2, First name: Eric, Last name: Clapton, Birthday: 1945-03-29
Singer - Id: 3, First name: Jimi, Last name: Hendrix, Birthday: 1942-11-26
插入数据并检索生成的密钥
对于插入数据,我们也可以使用SqlUpdate类。有趣的一点是主键是如何生成的(通常是id列)。该值仅在 insert 语句完成后可用;这是因为 RDBMS 在插入时为记录生成标识值。列 ID 用属性AUTO_INCREMENT声明,并且是主键,该值将在插入操作期间由 RDBMS 分配。如果您使用 Oracle,您可能会首先从 Oracle 序列中获得一个惟一的 ID,然后使用查询结果执行 insert 语句。
在 JDBC 的旧版本中,这种方法有点棘手。例如,如果我们使用 MySQL,您需要为 Microsoft SQL Server 执行 SQL select last_insert_id()和 select @@IDENTITY语句。
幸运的是,从 JDBC 版本 3.0 开始,增加了一个新特性,允许以统一的方式检索 RDBMS 生成的键。下面的代码片段显示了insert()方法的实现,该方法还检索为插入的联系人记录生成的键。它可以在大多数数据库中工作(如果不是全部的话);请确保您使用的是与 JDBC 3.0 或更新版本兼容的 JDBC 驱动程序。
我们首先为插入操作创建InsertSinger类,它扩展了SqlUpdate类。
package com.apress.prospring5.ch6;
import java.sql.Types;
import javax.sql.DataSource;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.object.SqlUpdate;
public class InsertSinger extends SqlUpdate {
private static final String SQL_INSERT_SINGER =
"insert into singer (first_name, last_name, birth_date)
values (:first_name, :last_name, :birth_date)";
public InsertSinger(DataSource dataSource) {
super(dataSource, SQL_INSERT_SINGER);
super.declareParameter(new SqlParameter("first_name", Types.VARCHAR));
super.declareParameter(new SqlParameter("last_name", Types.VARCHAR));
super.declareParameter(new SqlParameter("birth_date", Types.DATE));
super.setGeneratedKeysColumnNames(new String {"id"});
super.setReturnGeneratedKeys(true);
}
}
InsertSinger级和UpdateSinger级差不多;我们还需要做两件事。当构造InsertSinger类时,我们调用方法SqlUpdate.setGeneratedKeysColumnNames()来声明 ID 列的名称。方法SqlUpdate.setReturnGeneratedKeys()然后指示底层的 JDBC 驱动程序检索生成的密钥。在下面的代码中,您可以看到JdbcSingerDao类中insert()方法的实现:
package com.apress.prospring5.ch6.dao;
...
@Repository("singerDao")
public class JdbcSingerDao implements SingerDao {
private static Logger logger =
LoggerFactory.getLogger(JdbcSingerDao.class);
private DataSource dataSource;
private InsertSinger insertSinger;
@Override
public void insert(Singer singer) {
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("first_name", singer.getFirstName());
paramMap.put("last_name", singer.getLastName());
paramMap.put("birth_date", singer.getBirthDate());
KeyHolder keyHolder = new GeneratedKeyHolder();
insertSinger.updateByNamedParam(paramMap, keyHolder);
singer.setId(keyHolder.getKey().longValue());
logger.info("New singer inserted with id: " + singer.getId());
}
@Resource(name = "dataSource")
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
this.insertSinger = new InsertSinger(dataSource);
}
...
}
在数据源注入时,构造一个InsertSinger的实例。在insert()方法中,我们也使用了SqlUpdate.updateByNamedParam()方法。此外,我们将KeyHolder的一个实例传递给该方法,该方法将把生成的 ID 存储在其中。插入数据后,我们可以从KeyHolder中检索生成的密钥。看看测试方法怎么找insert()。
package com.apress.prospring5.ch6;
...
public class AnnotationJdbcTest {
private GenericApplicationContext ctx;
private SingerDao singerDao;
@Before
public void setUp() {
ctx = new AnnotationConfigApplicationContext(AppConfig.class);
singerDao = ctx.getBean(SingerDao.class);
assertNotNull(singerDao);
}
@Test
public void testSingerInsert(){
Singer singer = new Singer();
singer.setFirstName("Ed");
singer.setLastName("Sheeran");
singer.setBirthDate(new Date(
(new GregorianCalendar(1991, 1, 17)).getTime().getTime()));
singerDao.insert(singer);
List<Singer> singers = singerDao.findAll();
listSingers(singers);
}
private void listSingers(List<Singer> singers){
singers.forEach(singer -> {
System.out.println(singer);
if (singer.getAlbums() != null) {
for (Album album : singer.getAlbums()) {
System.out.println("\t--> " + album);
}
}
});
}
@After
public void tearDown() {
ctx.close();
}
}
如果测试通过,运行测试方法会产生以下输出:
Singer - Id: 1, First name: John Clayton, Last name: Mayer, Birthday: 1977-10-16
Singer - Id: 2, First name: Eric, Last name: Clapton, Birthday: 1945-03-29
Singer - Id: 3, First name: Jimi, Last name: Hendrix, Birthday: 1942-11-26
Singer - Id: 6, First name: Ed, Last name: Sheeran, Birthday: 1991-02-17
使用 BatchSqlUpdate 进行批处理操作
对于批处理操作,我们使用BatchSqlUpdate类。新的insertWithAlbum()方法会将歌手及其发行的专辑插入数据库。为了能够插入专辑唱片,我们需要创建InsertSingerAlbum类,如下所示:
package com.apress.prospring5.ch6;
import java.sql.Types;
import javax.sql.DataSource;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.object.BatchSqlUpdate;
public class InsertSingerAlbum extends BatchSqlUpdate {
private static final String SQL_INSERT_SINGER_ALBUM =
"insert into album (singer_id, title, release_date)
values (:singer_id, :title, :release_date)";
private static final int BATCH_SIZE = 10;
public InsertSingerAlbum(DataSource dataSource) {
super(dataSource, SQL_INSERT_SINGER_ALBUM);
declareParameter(new SqlParameter("singer_id", Types.INTEGER));
declareParameter(new SqlParameter("title", Types.VARCHAR));
declareParameter(new SqlParameter("release_date", Types.DATE));
setBatchSize(BATCH_SIZE);
}
}
注意,在构造函数中,我们调用了BatchSqlUpdate.setBatchSize()方法来设置 JDBC 插入操作的批处理大小。这里您可以看到在JdbcSingerDao类中insertWithAlbum()方法的实现:
package com.apress.prospring5.ch6.dao;
...
@Repository("singerDao")
public class JdbcSingerDao implements SingerDao {
private static Logger logger =
LoggerFactory.getLogger(JdbcSingerDao.class);
private DataSource dataSource;
private InsertSingerAlbum insertSingerAlbum;
@Override
public void insertWithAlbum(Singer singer) {
insertSingerAlbum = new InsertSingerAlbum(dataSource);
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("first_name", singer.getFirstName());
paramMap.put("last_name", singer.getLastName());
paramMap.put("birth_date", singer.getBirthDate());
KeyHolder keyHolder = new GeneratedKeyHolder();
insertSinger.updateByNamedParam(paramMap, keyHolder);
singer.setId(keyHolder.getKey().longValue());
logger.info("New singer inserted with id: " + singer.getId());
List<Album> albums = singer.getAlbums();
if (albums != null) {
for (Album album : albums) {
paramMap = new HashMap<>();
paramMap.put("singer_id", singer.getId());
paramMap.put("title", album.getTitle());
paramMap.put("release_date", album.getReleaseDate());
insertSingerAlbum.updateByNamedParam(paramMap);
}
}
insertSingerAlbum.flush();
}
@Resource(name = "dataSource")
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public List<Singer> findAllWithAlbums() {
JdbcTemplate jdbcTemplate = new JdbcTemplate(getDataSource());
String sql = "SELECT s.id, s.first_name, s.last_name, s.birth_date" +
", a.id AS album_id, a.title, a.release_date FROM singer s " +
"LEFT JOIN album a ON s.id = a.singer_id";
return jdbcTemplate.query(sql, new SingerWithAlbumExtractor());
}
private static final class SingerWithAlbumExtractor
implements ResultSetExtractor<List<Singer>> {
public List<Singer> extractData(ResultSet rs) throws
SQLException, DataAccessException {
Map<Long, Singer> map = new HashMap<>();
Singer singer;
while (rs.next()) {
Long id = rs.getLong("id");
singer = map.get(id);
if (singer == null) {
singer = new Singer();
singer.setId(id);
singer.setFirstName(rs.getString("first_name"));
singer.setLastName(rs.getString("last_name"));
singer.setBirthDate(rs.getDate("birth_date"));
singer.setAlbums(new ArrayList<>());
map.put(id, singer);
}
Long albumId = rs.getLong("album_id");
if (albumId > 0) {
Album album = new Album();
album.setId(albumId);
album.setSingerId(id);
album.setTitle(rs.getString("title"));
album.setReleaseDate(rs.getDate("release_date"));
singer.getAlbums().add(album);
}
}
return new ArrayList<>(map.values());
}
}
...
}
每次调用insertWithAlbum()方法,都会构造一个新的InsertSingerAlbum实例,因为BatchSqlUpdate类不是线程安全的。然后我们就像SqlUpdate一样使用。主要的区别是BatchSqlUpdate类将对插入操作进行排队,并批量提交给数据库。每当记录的数量等于批量大小时,Spring 将对数据库执行一个批量插入操作,以获得未决的记录。另一方面,在完成时,我们调用BatchSqlUpdate.flush()方法来指示 Spring 刷新所有挂起的操作(也就是说,正在排队但还没有达到批处理大小的插入操作)。最后,我们遍历Singer对象中的Album对象列表,并调用BatchSqlUpdate.updateByNamedParam()方法。为了便于测试,还实现了insertWithAlbum()方法。由于这个实现相当大,可以通过使用 Java 8 lambda 表达式来减少它。
public List<Singer> findAllWithAlbums() {
JdbcTemplate jdbcTemplate = new JdbcTemplate(getDataSource());
String sql = "SELECT s.id, s.first_name, s.last_name, s.birth_date" +
", a.id AS album_id, a.title, a.release_date FROM singer s " +
"LEFT JOIN album a ON s.id = a.singer_id";
return jdbcTemplate.query(sql, rs -> {
Map<Long, Singer> map = new HashMap<>();
Singer singer;
while (rs.next()) {
Long id = rs.getLong("id");
singer = map.get(id);
if (singer == null) {
singer = new Singer();
singer.setId(id);
singer.setFirstName(rs.getString("first_name"));
singer.setLastName(rs.getString("last_name"));
singer.setBirthDate(rs.getDate("birth_date"));
singer.setAlbums(new ArrayList<>());
map.put(id, singer);
}
Long albumId = rs.getLong("album_id");
if (albumId > 0) {
Album album = new Album();
album.setId(albumId);
album.setSingerId(id);
album.setTitle(rs.getString("title"));
album.setReleaseDate(rs.getDate("release_date"));
singer.getAlbums().add(album);
}
}
return new ArrayList<>(map.values());
});
}
我们来看看测试方法怎么找insertWithAlbum()。
package com.apress.prospring5.ch6;
...
public class AnnotationJdbcTest {
private GenericApplicationContext ctx;
private SingerDao singerDao;
@Before
public void setUp() {
ctx = new AnnotationConfigApplicationContext(AppConfig.class);
singerDao = ctx.getBean(SingerDao.class);
assertNotNull(singerDao);
}
@Test
public void testSingerInsertWithAlbum(){
Singer singer = new Singer();
singer.setFirstName("BB");
singer.setLastName("King");
singer.setBirthDate(new Date(
(new GregorianCalendar(1940, 8, 16)).getTime().getTime()));
Album album = new Album();
album.setTitle("My Kind of Blues");
album.setReleaseDate(new Date(
(new GregorianCalendar(1961, 7, 18)).getTime().getTime()));
singer.addAbum(album);
album = new Album();
album.setTitle("A Heart Full of Blues");
album.setReleaseDate(new Date(
(new GregorianCalendar(1962, 3, 20)).getTime().getTime()));
singer.addAbum(album);
singerDao.insertWithAlbum(singer);
List<Singer> singers = singerDao.findAllWithAlbums();
listSingers(singers);
}
private void listSingers(List<Singer> singers){
singers.forEach(singer -> {
System.out.println(singer);
if (singer.getAlbums() != null) {
for (Album album : singer.getAlbums()) {
System.out.println("\t--> " + album);
}
}
});
}
@After
public void tearDown() {
ctx.close();
}
}
如果测试通过,运行测试方法会产生以下输出:
Singer - Id: 1, First name: John, Last name: Mayer, Birthday: 1977-10-15
--> Album - Id: 1, Singer id: 1, Title: The Search For Everything,
Release Date: 2017-01-19
--> Album - Id: 2, Singer id: 1, Title: Paradise Valley,
Release Date: 2013-08-19
--> Album - Id: 3, Singer id: 1, Title: Born and Raised,
Release Date: 2012-05-22
--> Album - Id: 4, Singer id: 1, Title: Battle Studies,
Release Date: 2009-11-16
Singer - Id: 2, First name: Eric, Last name: Clapton, Birthday: 1945-03-29
--> Album - Id: 5, Singer id: 2, Title: From The Cradle,
Release Date: 1994-09-13
Singer - Id: 3, First name: Jimi, Last name: Hendrix, Birthday: 1942-11-26
Singer - Id: 4, First name: BB, Last name: King, Birthday: 1940-09-15
--> Album - Id: 6, Singer id: 4, Title: My Kind of Blues,
Release Date: 1961-08-17
--> Album - Id: 7, Singer id: 4, Title: A Heart Full of Blues,
Release Date: 1962-04-19
使用 SqlFunction 调用存储函数
Spring 还提供了一些类来使用 JDBC 简化存储过程/函数的执行。在这一节中,我们将向您展示如何使用SqlFunction类来执行一个简单的函数。我们展示了如何将 MySQL 用于数据库,创建一个存储函数,并通过使用SqlFunction<T>类调用它。
我们假设您有一个 MySQL 数据库,其模式名为musicdb,用户名和密码都等于prospring5(与“探索 JDBC 基础设施”一节中的示例相同)。让我们创建一个名为getFirstNameById()的存储函数,它接受联系人的 ID 并返回联系人的名字。下面的代码展示了在 MySQL 中创建存储函数的脚本(stored-function.sql)。对 MySQL 数据库运行脚本。
DELIMITER //
CREATE FUNCTION getFirstNameById(in_id INT)
RETURNS VARCHAR(60)
BEGIN
RETURN (SELECT first_name FROM singer WHERE id = in_id);
END //
DELIMITER ;
存储函数只接受 ID,并返回带有 ID 的歌手唱片的名字。接下来我们创建一个StoredFunctionFirstNameById类来表示存储函数操作,它扩展了SqlFunction<T>类。您可以在下面的代码片段中看到该类的内容:
package com.apress.prospring5.ch6;
import java.sql.Types;
import javax.sql.DataSource;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.object.SqlFunction;
public class StoredFunctionFirstNameById extends SqlFunction<String> {
private static final String SQL = "select getfirstnamebyid(?)";
public StoredFunctionFirstNameById (DataSource dataSource) {
super(dataSource, SQL);
declareParameter(new SqlParameter(Types.INTEGER));
compile();
}
}
这里,该类扩展了SqlFunction<T>并传入了类型String,这表明了函数的返回类型。然后我们声明 SQL 来调用 MySQL 中的存储函数。之后,在构造函数中,声明参数,然后我们编译操作。该类现在可以在实现类中使用了。下面的代码片段显示了使用存储函数的更新后的JdbcSingerDao类:
package com.apress.prospring5.ch6.dao;
...
@Repository("singerDao")
public class JdbcSingerDao implements SingerDao {
private static Logger logger = LoggerFactory.getLogger(JdbcSingerDao.class);
private DataSource dataSource;
private StoredFunctionFirstNameById storedFunctionFirstNameById;
@Override
public String findFirstNameById(Long id) {
List<String> result = storedFunctionFirstNameById.execute(id);
return result.get(0);
}
...
}
在数据源注入时,构造一个StoredFunctionFirstNameById的实例。然后在findFirstNameById()方法中,调用它的execute()方法,传入联系人 ID。该方法将返回一个String列表,我们只需要第一个,因为结果集中应该只返回一条记录。测试这个功能非常简单。
package com.apress.prospring5.ch6;
...
public class AnnotationJdbcTest {
private GenericApplicationContext ctx;
private SingerDao singerDao;
@Before
public void setUp() {
ctx = new AnnotationConfigApplicationContext(AppConfig.class);
singerDao = ctx.getBean(SingerDao.class);
assertNotNull(singerDao);
}
@Test
public void testFindFirstNameById(){
String firstName = singerDao.findFirstNameById(2L);
assertEquals("Eric", firstName);
System.out.println("Retrieved value: " + firstName);
}
@After
public void tearDown() {
ctx.close();
}
}
在程序中,我们将 ID 2 传递给存储函数。这将返回Eric,如果对 MySQL 数据库运行test-data.sql,这是 ID 等于 2 的记录的第一个名称。运行该程序会产生以下输出:
o.s.j.c.JdbcTemplate - Executing prepared SQL query
o.s.j.c.JdbcTemplate - Executing prepared SQL statement
[select getfirstnamebyid?]
o.s.j.d.DataSourceUtils - Fetching JDBC Connection from DataSource
o.s.j.d.DataSourceUtils - Returning JDBC Connection to DataSource
Retrieved value: Eric
您可以看到名字被正确检索。这里展示的只是一个简单的示例,用来演示 Spring JDBC 模块的功能。Spring 还为您提供了其他类(例如,StoredProcedure)来调用返回复杂数据类型的复杂存储过程。如果您需要使用 JDBC 访问存储过程,我们建议您参考 Spring 的参考手册。
Spring Data 项目:JDBC 扩展
近年来,数据库技术发展如此之快,出现了如此多的专用数据库,如今 RDBMS 已不是应用后端数据库的唯一选择。为了响应这种数据库技术的发展和开发人员社区的需求,Spring 创建了 Spring Data 项目( http://springsource.org/spring-data )。该项目的主要目标是在 Spring 的核心数据访问功能之上提供有用的扩展,以便与传统 RDBMSs 之外的数据库进行交互。
Spring Data 项目附带了各种扩展。这里我们想提一下 JDBC 扩展( http://springsource.org/spring-data/jdbc-extensions )。顾名思义,该扩展提供了一些高级特性,便于使用 Spring 开发 JDBC 应用。下面列出了 JDBC 扩展提供的主要功能:
- QueryDSL 支持:QueryDSL (
http://querydsl.com)是一种特定于领域的语言,它为开发类型安全查询提供了一个框架。Spring Data 的 JDBC 扩展提供了QueryDslJdbcTemplate来促进使用 QueryDSL 而不是 SQL 语句开发 JDBC 应用。 - 对 Oracle 数据库的高级支持:该扩展为 Oracle 数据库用户提供了高级特性。在数据库连接方面,它支持 Oracle 特定的会话设置,以及在使用 Oracle RAC 时的快速连接故障转移技术。此外,还提供了与 Oracle 高级排队集成的类。在数据类型方面,提供了对 Oracle 的 XML 类型、
STRUCT和ARRAY等的原生支持。
如果您正在使用 Spring 和 Oracle 数据库开发 JDBC 应用,JDBC 扩展确实值得一看。
使用 JDBC 的注意事项
有了这个丰富的特性集,您可以看到在使用 JDBC 与底层 RDBMS 交互时,Spring 是如何让您的生活变得更加轻松的。然而,仍然有相当多的代码需要开发,尤其是在将结果集转换成相应的域对象时。
在 JDBC 之上,已经开发了许多开源库来帮助缩小关系数据结构和 Java 的 OO 模型之间的差距。例如,iBATIS 是一个流行的DataMapper框架,它也基于 SQL 映射。iBATIS 允许您将带有存储过程或查询的对象映射到 XML 描述符文件。像 Spring 一样,iBATIS 提供了查询对象映射的声明式方法,大大节省了维护分散在各种 DAO 类中的 SQL 查询的时间。
还有许多其他的 ORM 框架关注的是对象模型,而不是查询。流行的包括 Hibernate、EclipseLink(也称为 TopLink)和 OpenJPA。它们都符合 JCP 的 JPA 规范。
近年来,这些 ORM 工具和映射框架已经变得更加成熟,以至于大多数开发人员会选择其中的一个,而不是直接使用 JDBC。然而,在出于性能目的需要对提交给数据库的查询进行绝对控制的情况下(例如,在 Oracle 中使用分层查询),Spring JDBC 确实是一个可行的选择。当使用 Spring 时,一个很大的优势是您可以混合和匹配不同的数据访问技术。例如,您可以使用 Hibernate 作为主要的 ORM,然后使用 JDBC 作为一些复杂查询逻辑或批处理操作的补充;您可以在单个业务操作中混合和匹配它们,然后将它们包装在同一个数据库事务中。Spring 将帮助您轻松处理这些情况。
Spring Boot·JDBC
因为我们已经介绍了用于 web 和简单控制台应用的 Spring Boot,所以在本书中介绍用于 JDBC 的 Spring Boot 入门库是合乎逻辑的。它帮助您删除样板配置并直接进入实现。
当spring-boot-starter-jdbc作为一个依赖项被添加到一个项目中时,一组库被添加到该项目中。没有添加的是数据库驱动程序。这个决定必须由开发商做出。本节将涉及的项目是spring-boot-jdbc,它是chapter06项目的一个子模块。其分级依赖关系和版本在以下父build.gradle文件中指定:
ext {
h2Version = '1.4.194'
bootVersion = '2.0.0.BUILD-SNAPSHOT'
boot = [
springBootPlugin:
"org.springframework.boot:spring-boot-gradle-plugin:$bootVersion",
Starter :
"org.springframework.boot:spring-boot-starter:$bootVersion",
starterWeb :
"org.springframework.boot:spring-boot-starter-web:$bootVersion",
Actuator :
"org.springframework.boot:spring-boot-starter-actuator:$bootVersion",
starterTest :
"org.springframework.boot:spring-boot-starter-test:$bootVersion",
starterAop :
"org.springframework.boot:spring-boot-starter-aop:$bootVersion",
starterJdbc :
"org.springframework.boot:spring-boot-starter-jdbc:$bootVersion"
]
db = [
h2 : "com.h2database:h2:$h2Version",
..
]
...
}
它们在spring-boot-jdbc\build.gradle配置文件中被声明为依赖项,只是通过使用它们的属性名。
buildscript {
repositories {
mavenLocal() mavenCentral()
maven { url "http://repo.spring.io/release" }
maven { url "http://repo.spring.io/milestone" }
maven { url "http://repo.spring.io/snapshot" }
maven { url "https://repo.spring.io/libs-snapshot" }
}
dependencies {
classpath boot.springBootPlugin
}
}
apply plugin: 'org.springframework.boot'
dependencies {
compile project(':chapter06:plain-jdbc')
compile boot.starterJdbc, db.h2
}
在 IntelliJ IDEA Gradle 项目视图中,自动配置的库如图 6-3 所示。spring-boot-starter-jdbc库使用tomcat-jdbc来配置DataSource bean。因此,如果没有显式配置的DataSource bean,并且类路径中有一个嵌入式数据库驱动程序,Spring Boot 将使用内存中的数据库设置自动注册DataSource bean。Spring Boot 还会自动注册以下 beans:

图 6-3。
Spring Boot JDBC starter dependencies
- 一颗豆子
- 一颗豆子
- 一颗豆子
以下是其他一些有趣的事情,它们可以减少环境设置的工作量:
-
Spring Boot 在
src/main/resources下寻找嵌入式数据库初始化文件。它期望找到包含 SQL DDL 语句(例如,CREATE TABLE语句)的名为schema.sql的文件和包含 DML 语句(例如,INSERT语句)的名为data.sql的文件。它使用这个文件在启动时初始化数据库。 -
这些文件的位置和名称可以在
application.properties文件中配置,该文件也位于src/main/resources下。允许 Spring Boot 应用使用 SQL 文件的示例配置文件如下所示:spring.datasource.schema=db/schema.sql spring.datasource.data=db/test-data.sql -
默认情况下,Spring Boot 在引导时初始化数据库,但这也可以通过将属性
spring.datasource.initialize=false添加到application.properties文件来改变。
除了上面提到的,Spring Boot 剩下要做的就是提供一些域类或实体和一个 DAO bean。Singer bean 与本章中到处使用的 bean 相同,它的实现驻留在chapter06/plain-jdbc项目中,该项目在任何地方都作为依赖项添加。要使用的JdbcSingerDao类如下所示:
package com.apress.prospring5.ch6;
import com.apress.prospring5.ch6.dao.SingerDao;
import com.apress.prospring5.ch6.entities.Singer;
import org.apache.commons.lang3.NotImplementedException;
import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class JdbcSingerDao implements SingerDao, InitializingBean {
private JdbcTemplate jdbcTemplate;
@Autowired
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override public String findNameById(Long id) {
return jdbcTemplate.queryForObject(
"SELECT first_name || ' ' || last_name FROM singer WHERE id = ?",
new Object{id}, String.class);
}
...
}
用@SpringBootApplication标注的 Spring Boot 入口点类如下面的代码示例所示。请注意这是多么简单。
package com.apress.prospring5.ch6;
import com.apress.prospring5.ch6.dao.SingerDao;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
@SpringBootApplication
public class Application {
private static Logger logger =
LoggerFactory.getLogger(Application.class);
public static void main(String... args) throws Exception {
ConfigurableApplicationContext ctx =
SpringApplication.run(Application.class, args);
assert (ctx != null);
SingerDao singerDao = ctx.getBean(SingerDao.class);
String singerName = singerDao.findNameById(1L);
logger.info("Retrieved singer: " + singerName);
System.in.read();
ctx.close();
}
}
摘要
本章向您展示了如何使用 Spring 来简化 JDBC 编程。您学习了如何连接到数据库并执行选择、更新、删除和插入,以及调用数据库存储函数。使用核心 Spring JDBC 类,JdbcTemplate,详细讨论。我们讨论了构建在JdbcTemplate之上的其他 Spring 类,它们帮助您对各种 JDBC 操作建模。我们还展示了如何在适当的地方使用 Java 8 中新的 lambda 表达式。此外,Spring Boot·JDBC 也被涵盖在内,因为任何有助于你更多地关注应用的业务逻辑的实现而不是配置的东西都是一个很好的工具。在接下来的几章中,我们将讨论在开发数据访问逻辑时,如何使用 Spring 和流行的 ORM 技术。
Footnotes 1
WordPress 是一个广泛使用的博客平台,它使用 MySQL 或 MariaDB 来存储数据。
2
从 https://dev.mysql.com/downloads/ 下载 MySQL 社区服务器。
3
这里是官方网站: http://commons.apache.org/dbcp 。
4
这表明带注释的类是一个存储库,最初由领域驱动设计(Evans,2003)定义为“一种封装存储、检索和搜索行为的机制,它模拟一组对象。”
七、Spring 和 Hibernate
在前一章中,你看到了如何在 Spring 应用中使用 JDBC。然而,即使 Spring 在简化 JDBC 开发方面走了很长的路,您仍然有许多代码要写。在本章中,我们将介绍一个叫做 Hibernate 的对象关系映射(ORM)库。
如果您有使用 EJB 实体 beans(在 EJB 3.0 之前)开发数据访问应用的经验,您可能还记得这个痛苦的过程。在开发企业 Java 应用时,繁琐的映射配置、事务划分以及每个 bean 中管理其生命周期的大量样板代码极大地降低了生产率。
就像开发 Spring 是为了采用基于 POJO 的开发和声明性配置管理,而不是 EJB 笨重笨拙的设置一样,开发人员社区意识到一个更简单、轻量级和基于 POJO 的框架可以简化数据访问逻辑的开发。此后,出现了许多图书馆;它们通常被称为 ORM 库。ORM 库的主要目标是缩小关系数据库管理系统(RDBMS)中的关系数据结构和 Java 中的面向对象(OO)模型之间的差距,以便开发人员可以专注于使用对象模型进行编程,同时轻松执行与持久性相关的操作。
在开源社区提供的 ORM 库中,Hibernate 是最成功的一个。它的特性,比如基于 POJO 的方法、易于开发和支持复杂的关系定义,赢得了主流 Java 开发人员社区的青睐。
Hibernate 的流行也影响了 JCP,它开发了 Java 数据对象(JDO)规范,作为 Java EE 中的标准 ORM 技术之一。从 EJB 3.0 开始,EJB 实体 bean 甚至被 Java 持久性 API (JPA)所取代。JPA 的很多概念都受到了流行的 ORM 库的影响,比如 Hibernate、TopLink 和 JDO。Hibernate 和 JPA 的关系也很密切。Hibernate 的创始人 Gavin King 代表 JBoss 作为 JCP 专家组成员之一定义了 JPA 规范。从 3.2 版本开始,Hibernate 提供了 JPA 的实现。这意味着当您使用 Hibernate 开发应用时,您可以选择使用 Hibernate 自己的 API 或者使用 Hibernate 作为持久性服务提供者的 JPA API。
在简要介绍了 Hibernate 的历史之后,本章将介绍在开发数据访问逻辑时如何使用 Spring 和 Hibernate。Hibernate 是一个如此庞大的 ORM 库,以至于在一章中涵盖所有方面是不可能的,有许多书籍专门讨论 Hibernate。
本章涵盖了 Spring 中 Hibernate 的基本思想和主要用例。特别是,我们讨论以下主题:
- 配置 Hibernate session factory:Hibernate 的核心概念围绕着由
SessionFactory管理的Session接口。我们将向您展示如何配置 Hibernate 的会话工厂,以便在 Spring 应用中工作。 - 使用 Hibernate 的 ORM 的主要概念:我们讨论了如何使用 Hibernate 将 POJO 映射到底层关系数据库结构的主要概念。还讨论了一些常用的关系,包括一对多和多对多。
- 数据操作:我们展示了如何在 Spring 环境中使用 Hibernate 执行数据操作(查询、插入、更新、删除)的例子。使用 Hibernate 时,它的
Session接口是您将与之交互的主要接口。
在定义对象到关系的映射时,Hibernate 支持两种配置风格。一种是在 XML 文件中配置映射信息,另一种是在实体类中使用 Java 注释(在 ORM 或 JPA 世界中,映射到底层关系数据库结构的 Java 类称为实体类)。这一章着重于使用对象关系映射的注释方法。对于映射注释,我们使用 JPA 标准(例如,在javax.persistence包下),因为它们可以与 Hibernate 自己的注释互换,并将帮助您将来迁移到 JPA 环境。
示例代码的示例数据模型
图 7-1 显示了本章使用的数据模型。

图 7-1。
Sample data model
如该数据模型所示,添加了两个新表,即INSTRUMENT和SINGER_INSTRUMENT(连接表)。SINGER_INSTRUMENT对SINGER和INSTRUMENT表之间的多对多关系进行建模。在SINGER和ALBUM表中添加了一个VERSION列,用于乐观锁定,这将在后面详细讨论。在本章的示例中,我们将使用嵌入式 H2 数据库,因此数据库名称不是必需的。以下是创建本章示例所需表格的脚本:
CREATE TABLE SINGER (
ID INT NOT NULL AUTO_INCREMENT
, FIRST_NAME VARCHAR(60) NOT NULL
, LAST_NAME VARCHAR(40) NOT NULL
, BIRTH_DATE DATE
, VERSION INT NOT NULL DEFAULT 0
, UNIQUE UQ_SINGER_1 (FIRST_NAME, LAST_NAME)
, PRIMARY KEY (ID)
);
CREATE TABLE ALBUM (
ID INT NOT NULL AUTO_INCREMENT
, SINGER_ID INT NOT NULL
, TITLE VARCHAR(100) NOT NULL
, RELEASE_DATE DATE
, VERSION INT NOT NULL DEFAULT 0
, UNIQUE UQ_SINGER_ALBUM_1 (SINGER_ID, TITLE)
, PRIMARY KEY (ID)
, CONSTRAINT FK_ALBUM_SINGER FOREIGN KEY (SINGER_ID)
REFERENCES SINGER (ID)
);
CREATE TABLE INSTRUMENT (
INSTRUMENT_ID VARCHAR(20) NOT NULL
, PRIMARY KEY (INSTRUMENT_ID)
);
CREATE TABLE SINGER_INSTRUMENT (
SINGER_ID INT NOT NULL
, INSTRUMENT_ID VARCHAR(20) NOT NULL
, PRIMARY KEY (SINGER_ID, INSTRUMENT_ID)
, CONSTRAINT FK_SINGER_INSTRUMENT_1 FOREIGN KEY (SINGER_ID)
REFERENCES SINGER (ID) ON DELETE CASCADE
, CONSTRAINT FK_SINGER_INSTRUMENT_2 FOREIGN KEY (INSTRUMENT_ID)
REFERENCES INSTRUMENT (INSTRUMENT_ID)
);
以下 SQL 是数据填充的脚本:
insert into singer (first_name, last_name, birth_date)
values ('John', 'Mayer', '1977-10-16');
insert into singer (first_name, last_name, birth_date)
values ('Eric', 'Clapton', '1945-03-30');
insert into singer (first_name, last_name, birth_date)
values ('John', 'Butler', '1975-04-01');
insert into album (singer_id, title, release_date)
values (1, 'The Search For Everything', '2017-01-20');
insert into album (singer_id, title, release_date)
values (1, 'Battle Studies', '2009-11-17');
insert into album (singer_id, title, release_date)
values (2, 'From The Cradle ', '1994-09-13');
insert into instrument (instrument_id) values ('Guitar');
insert into instrument (instrument_id) values ('Piano');
insert into instrument (instrument_id) values ('Voice');
insert into instrument (instrument_id) values ('Drums');
insert into instrument (instrument_id) values ('Synthesizer');
insert into singer_instrument(singer_id, instrument_id) values (1, 'Guitar');
insert into singer_instrument(singer_id, instrument_id) values (1, 'Piano');
insert into singer_instrument(singer_id, instrument_id) values (2, 'Guitar');
配置 Hibernate 的会话工厂
正如本章前面提到的,Hibernate 的核心概念是基于从SessionFactory获得的Session接口。Spring 提供了一些类来支持将 Hibernate 的会话工厂配置为具有所需属性的 Spring bean。要使用 Hibernate,必须将 Hibernate 依赖项作为依赖项添加到项目中。以下是本章中项目使用的梯度配置:
//pro-spring-15/build.gradle
ext {
hibernateVersion = '5.2.10.Final'
...
hibernate = [
validator: "org.hibernate:hibernate-validator:5.1.3.Final",
ehcache : "org.hibernate:hibernate-ehcache:$hibernateVersion",
[ em] : "org.hibernate:hibernate-entitymanager:$hibernateVersion"
]
...
}
//chapter07.gradle
dependencies {
//we specify these dependencies for all submodules,
except the boot module, that defines its own
if !project.name.contains"boot" {
compile spring.contextSupport, spring.orm,
misc.slf4jJcl, misc.logback, db.h2, misc.lang3, [hibernate.em]
}
testCompile testing.junit
}
在下面的配置中,您可以看到为本章配置应用示例所需的 XML 元素:
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="
http://www.springframework.org/schema/jdbc
http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<jdbc:embedded-database id="dataSource" type="H2">
<jdbc:script location="classpath:sql/schema.sql"/>
<jdbc:script location="classpath:sql/test-data.sql"/>
</jdbc:embedded-database>
<bean id="transactionManager"
class="org.springframework.orm.hibernate5.HibernateTransactionManager"
p:sessionFactory-ref="sessionFactory"/>
<tx:annotation-driven/>
<context:component-scan base-package=
"com.apress.prospring5.ch7"/>
<bean id="sessionFactory"
class="org.springframework.orm.hibernate5.LocalSessionFactoryBean"
p:dataSource-ref="dataSource"
p:packagesToScan="com.apress.prospring5.ch7.entities"
p:hibernateProperties-ref="hibernateProperties"/>
<util:properties id="hibernateProperties">
<prop key="hibernate.dialect">org.hibernate.dialect.H2Dialect</prop>
<prop key="hibernate.max_fetch_depth">3</prop>
<prop key="hibernate.jdbc.fetch_size">50</prop>
<prop key="hibernate.jdbc.batch_size">10</prop>
<prop key="hibernate.hbm2ddl.auto">create-drop</prop>
<prop key="hibernate.format_sql">true</prop>
<prop key="hibernate.use_sql_comments">true</prop>
</util:properties>
</beans>
接下来描述了等效的 Java 配置类,这两种配置的组件在代码片段之后并行解释:
package com.apress.prospring5.ch7.config;
import com.apress.prospring5.ch6.CleanUp;
import org.hibernate.SessionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.hibernate5.HibernateTransactionManager;
import org.springframework.orm.hibernate5.LocalSessionFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
import java.io.IOException;
import java.util.Properties;
@Configuration
@ComponentScan(basePackages =
"com.apress.prospring5.ch7")
@EnableTransactionManagement
public class AppConfig {
private static Logger logger =
LoggerFactory.getLogger(AppConfig.class);
@Bean
public DataSource dataSource() {
try {
EmbeddedDatabaseBuilder dbBuilder =
new EmbeddedDatabaseBuilder();
return dbBuilder.setType(EmbeddedDatabaseType.H2)
.addScripts("classpath:sql/schema.sql",
"classpath:sql/test-data.sql").build();
} catch (Exception e) {
logger.error("Embedded DataSource bean cannot be created!", e);
return null;
}
}
private Properties hibernateProperties() {
Properties hibernateProp = new Properties();
hibernateProp.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
hibernateProp.put("hibernate.format_sql", true);
hibernateProp.put("hibernate.use_sql_comments", true);
hibernateProp.put("hibernate.show_sql", true);
hibernateProp.put("hibernate.max_fetch_depth", 3);
hibernateProp.put("hibernate.jdbc.batch_size", 10);
hibernateProp.put("hibernate.jdbc.fetch_size", 50);
return hibernateProp;
}
@Bean public SessionFactory sessionFactory()
throws IOException {
LocalSessionFactoryBean sessionFactoryBean = new LocalSessionFactoryBean();
sessionFactoryBean.setDataSource(dataSource());
sessionFactoryBean.setPackagesToScan("com.apress.prospring5.ch7.entities");
sessionFactoryBean.setHibernateProperties(hibernateProperties());
sessionFactoryBean.afterPropertiesSet();
return sessionFactoryBean.getObject();
}
@Bean public PlatformTransactionManager transactionManager()
throws IOException {
return new HibernateTransactionManager(sessionFactory());
}
}
在前面的配置中,声明了几个 beans 能够支持 Hibernate 的会话工厂。这里列出了主要配置:
-
数据源 bean:使用的数据库是一个 H2 嵌入式数据库,如前面在第六章中解释的那样声明。
-
transaction manager bean:Hibernate 会话工厂需要一个事务管理器来进行事务数据访问。Spring 专门为包
org.springframework.orm.hibernate5.HibernateTransactionManager中声明的 Hibernate 5 提供了一个事务管理器。bean 是用分配的 IDtransactionManager声明的。默认情况下,当使用 XML 配置时,只要需要事务管理,Spring 就会在其ApplicationContext中查找名为transactionManager的 bean。当通过 bean 的类型而不是名称来搜索 bean 时,Java 配置会更灵活一些。我们将在第九章中详细讨论事务。此外,我们声明标签<tx:annotation-driven>,以支持使用注释声明事务划分需求。Java 配置的对等物是@EnableTransactionManagement注释。 -
组件扫描:这个标签和
@ComponentScan注释对您来说应该很熟悉。我们指示 Spring 扫描包com.apress.prospring5.ch7下的组件,以检测标注有@Repository的 beans。 -
Hibernate SessionFactory bean: The
sessionFactorybean is the most important part. Within the bean, several properties are provided. First, we need to inject thedataSourcebean into the session factory. Second, we instruct Hibernate to scan for the domain objects under the packagecom.apress.prospring5.ch.entities. Finally, thehibernatePropertiesproperty provides configuration details for Hibernate. There are many configuration parameters, and we define only a few important properties that should be provided for every application. Table 7-1 lists the main configuration parameters for the Hibernate session factory.表 7-1。
Hibernate Properties
| 财产 | 描述 | | --- | --- | | `hibernate.dialect` | 为 Hibernate 应该使用的查询指定数据库方言。Hibernate 支持许多数据库的 SQL 方言。那些方言是`org.hibernate.dialect.Dialect`的子类。主要方言有`H2Dialect`、`Oracle10gDialect`、`PostgreSQLDialect`、`MySQLDialect`、`SQLServerDialect`等。 | | `hibernate.max_fetch_depth` | 当映射对象与其他映射对象关联时,声明外部连接的“深度”。这个设置可以防止 Hibernate 获取太多嵌套关联的数据。一个常用的值是 3。 | | `hibernate.jdbc.fetch_size` | 指定 Hibernate 每次从数据库获取记录时应该使用的底层 JDBC `ResultSet`中的记录数。例如,向数据库提交了一个查询,而`ResultSet`包含 500 条记录。如果读取大小为 50,Hibernate 将需要读取 10 次才能获得所有数据。 | | `ibernate.jdbc.batch_` `size` | 指示 Hibernate 应该分组到一个批处理中的更新操作的数量。这对于在 Hibernate 中执行批处理作业非常有用。显然,当我们在做一个更新成千上万条记录的批处理作业时,我们希望 Hibernate 将查询分组,而不是一个接一个地提交更新。 | | `hibernate.show_sql` | 指示 Hibernate 是否应该将 SQL 查询输出到日志文件或控制台。您应该在开发环境中打开它,这在测试和故障排除过程中非常有帮助。 | | `hibernate.format_sql` | 指示日志或控制台中的 SQL 输出是否应该格式化。 | | `hibernate..use_sql_comments` | 如果设置为`true`,Hibernate 会在 SQL 内部生成注释,以便于调试。 |
关于 Hibernate 支持的属性的完整列表,请参考 Hibernate 的 ORM 用户指南,特别是第二十三部分,在 https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html 。
使用 Hibernate 注释的 ORM 映射
配置就绪后,下一步是对 Java POJO 实体类及其到底层关系数据结构的映射进行建模。
有两种映射方法。第一个是设计对象模型,然后基于对象模型生成数据库脚本。例如,对于会话工厂配置,您可以传入 Hibernate 属性hibernate.hbm2ddl.auto,让 Hibernate 自动将模式 DDL 导出到数据库。第二种方法是从数据模型开始,然后用期望的映射对 POJOs 建模。我们更喜欢后一种方法,因为我们可以对数据模型有更多的控制,这对于优化数据访问的性能很有用。但是第一个问题将在本章后面讨论,以描述用 Hibernate 配置 Spring 应用的另一种方式。基于数据模型,图 7-2 用类图展示了相应的 OO 模型。

图 7-2。
Class diagram for the sample data model
您可以看到Singer和Album之间是一对多的关系,而Singer和Instrument对象之间是多对多的关系。
简单映射
首先让我们从映射类的简单属性开始。下面的代码片段显示了带有映射注释的Singer类:
package com.apress.prospring5.ch7.entities;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import static javax.persistence.GenerationType.IDENTITY;
@Entity
@Table(name = "singer")
public class Singer implements Serializable {
private Long id;
private String firstName;
private String lastName;
private Date birthDate;
private int version;
public void setId(Long id) {
this.id = id;
}
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "ID")
public Long getId() {
return this.id;
}
@Version
@Column(name = "VERSION")
public int getVersion() {
return version;
}
@Column(name = "FIRST_NAME")
public String getFirstName() {
return this.firstName;
}
@Column(name = "LAST_NAME")
public String getLastName() {
return this.lastName;
}
@Temporal(TemporalType.DATE)
@Column(name = "BIRTH_DATE")
public Date getBirthDate() {
return birthDate;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public void setBirthDate(Date birthDate) {
this.birthDate = birthDate;
}
public void setVersion(int version) {
this.version = version;
}
public String toString() {
return "Singer - Id: " + id + ", First name: " + firstName
+ ", Last name: " + lastName + ", Birthday: " + birthDate;
}
}
首先我们用@ Entity标注类型,这意味着这是一个映射的实体类。@Table注释定义了这个实体映射到的数据库中的表名。对于每个映射的属性,您可以用@Column注释对其进行注释,并提供列名。
如果类型和属性名称与表和列名称相同,则可以跳过表和列名称。
关于映射,我们想强调几点。
- 对于
birthDate属性,我们使用TemporalType.DATE值作为参数,用@Temporal对其进行注释。这意味着我们希望将数据类型从 Java 日期类型(java.util.Date)映射到 SQL 日期类型(java.sql.Date)。这允许我们像往常一样在应用中使用java.util.Date来访问Singer对象中的属性birthDate。 - 对于
id属性,我们用@Id对其进行注释。这意味着它是对象的主键。Hibernate 在管理其会话中的联系人实体实例时,将使用它作为唯一标识符。此外,@GeneratedValue注释告诉 Hibernate】值是如何生成的。IDENTITY策略意味着id值是在插入期间由后端生成的。 - 对于
version属性,我们用@Version对其进行注释。这指示 Hibernate 我们想要使用乐观锁定机制,使用version属性作为控制。每次 Hibernate 更新记录时,它都会将实体实例的版本与数据库中记录的版本进行比较。如果两个版本相同,说明之前没有人更新过数据,Hibernate 会更新数据并递增版本列。但如果版本不一样,说明之前有人更新过记录,Hibernate 会抛出StaleObjectStateException异常,Spring 会翻译成HibernateOptimisticLockingFailureException。我们使用整数进行版本控制的例子。除了整数,Hibernate 还支持使用时间戳。但是,建议使用整数进行版本控制,因为 Hibernate 在每次更新后都会将版本号加 1。当使用时间戳时,Hibernate 会在每次更新后更新最新的时间戳。时间戳稍微不太安全,因为两个并发事务可能在同一毫秒内加载和更新同一项。
另一个映射的对象是Album,如下图所示:
package com.apress.prospring5.ch7.entities;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
import static javax.persistence.GenerationType.IDENTITY;
@Entity
@Table(name = "album")
public class Album implements Serializable {
private Long id;
private String title;
private Date releaseDate;
private int version;
public void setId(Long id) {
this.id = id;
}
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "ID")
public Long getId() {
return this.id;
}
@Version
@Column(name = "VERSION")
public int getVersion() {
return version;
}
@Column
public String getTitle() {
return this.title;
}
@Temporal(TemporalType.DATE)
@Column(name = "RELEASE_DATE")
public Date getReleaseDate() {
return this.releaseDate;
}
public void setTitle(String title) {
this.title = title;
}
public void setReleaseDate(Date releaseDate) {
this.releaseDate = releaseDate;
}
public void setVersion(int version) {
this.version = version;
}
@Override
public String toString() {
return "Album - Id: " + id + ", Title: " +
title + ", Release Date: " + releaseDate;
}
}
以下是本章示例中使用的第三个实体类:
package com.apress.prospring5.ch7.entities;
import javax.persistence.*;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name = "instrument")
public class Instrument implements Serializable {
private String instrumentId;
@Id
@Column(name = "INSTRUMENT_ID")
public String getInstrumentId() {
return this.instrumentId;
}
public void setInstrumentId(String instrumentId) {
this.instrumentId = instrumentId;
}
@Override
public String toString() {
return "Instrument :" + getInstrumentId();
}
}
一对多映射
Hibernate 能够对多种关联进行建模。最常见的关联是一对多和多对多。每个Singer将有零个或多个相册,所以这是一个一对多的关联(在 ORM 术语中,一对多关联用于建模数据结构中的零对多和一对多关系)。下面的代码片段描述了定义Singer和Album实体之间的一对多关系所需的属性和方法:
package com.apress.prospring5.ch7.entities;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import static javax.persistence.GenerationType.IDENTITY;
@Entity
@Table(name = "singer")
public class Singer implements Serializable {
private Long id;
private String firstName;
private String lastName;
private Date birthDate;
private int version;
private Set<Album> albums = new HashSet<>();
...
@OneToMany(mappedBy = "singer", cascade=CascadeType.ALL,
orphanRemoval=true)
public Set<Album> getAlbums() {
return albums;
}
public boolean addAbum(Album album) {
album.setSinger(this);
return getAlbums().add(album);
}
public void removeAlbum(Album album) {
getAlbums().remove(album);
}
public void setAlbums(Set<Album> albums) {
this.albums = albums;
}
...
}
属性contactTelDetails的 getter 方法用@OneToMany注释,这表明与Album类的一对多关系。几个属性被传递给注释。mappedBy属性表示Album类中提供关联的属性(即通过FK_ALBUM_SINGER表中的外键定义链接起来)。属性意味着更新操作应该“级联”到子节点。orphanRemoval属性意味着在相册更新后,那些不再存在于相册集中的条目应该从数据库中删除。以下代码片段显示了关联映射的Album类中的更新代码:
package com.apress.prospring5.ch7.entities;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
import static javax.persistence.GenerationType.IDENTITY;
@Entity
@Table(name = "album")
public class Album implements Serializable {
private Long id;
private String title;
private Date releaseDate;
private int version;
private Singer singer;
@ManyToOne
@JoinColumn(name = "SINGER_ID")
public Singer getSinger() {
return this.singer;
}
public void setSinger(Singer singer) {
this.singer = singer;
}
...
}
我们用@ManyToOne注释singer属性的 getter 方法,这表明它是来自Singer的关联的另一方。我们还为底层外键列名指定了@JoinColumn注释。最后,在后面的示例代码中,toString()方法被覆盖,通过将其输出打印到控制台来方便测试。
多对多映射
每个歌手可以演奏零个或多个乐器,每个乐器也与零个或多个歌手相关联,这意味着这是一个多对多的映射。多对多映射需要一个连接表,就是SINGER_INSTRUMENT。以下代码示例显示了需要添加到Singer类中以实现这种关系的代码:
package com.apress.prospring5.ch7.entities;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import static javax.persistence.GenerationType.IDENTITY;
@Entity
@Table(name = "singer")
public class Singer implements Serializable {
private Long id;
private String firstName;
private String lastName;
private Date birthDate;
private int version;
private Set<Instrument> instruments = new HashSet<>();
@ManyToMany
@JoinTable(name = "singer_instrument",
joinColumns = @JoinColumn(name = "SINGER_ID"),
inverseJoinColumns = @JoinColumn(name = "INSTRUMENT_ID"))
public Set<Instrument> getInstruments() {
return instruments;
}
public void setInstruments(Set<Instrument> instruments) {
this.instruments = instruments;
}
...
}
Singer类中属性instruments的 getter 方法用@ManyToMany注释。我们还提供了@JoinTable来指示 Hibernate 应该寻找的底层连接表。该名称是连接表的名称,joinColumns定义了作为SINGER表的外键的列,inverseJoinColumns定义了作为关联另一端(即INSTRUMENT表)的外键的列。下面是添加了实现这种关系的另一方的代码的Instrument类:
package com.apress.prospring5.ch7.entities;
import javax.persistence.*;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name = "instrument")
public class Instrument implements Serializable {
private String instrumentId;
private Set<Singer> singers = new HashSet<>();
@Id
@Column(name = "INSTRUMENT_ID")
public String getInstrumentId() {
return this.instrumentId;
}
@ManyToMany
@JoinTable(name = "singer_instrument",
joinColumns = @JoinColumn(name = "INSTRUMENT_ID"),
inverseJoinColumns = @JoinColumn(name = "SINGER_ID"))
public Set<Singer> getSingers() {
return this.singers;
}
public void setSingers(Set<Singer> singers) {
this.singers = singers;
}
public void setInstrumentId(String instrumentId) {
this.instrumentId = instrumentId;
}
@Override
public String toString() {
return "Instrument :" + getInstrumentId();
}
}
该映射与Singer的映射大致相同,但是joinColumns和inverseJoinColumns属性被颠倒以反映关联。
Hibernate 会话接口
在 Hibernate 中,与数据库交互时,你需要处理的主要接口是Session接口,这个接口是从SessionFactory中获取的。
以下代码片段显示了本章示例中使用的SingerDaoImpl类,并将配置好的 Hibernate SessionFactory注入到该类中:
package com.apress.prospring5.ch7.dao;
import com.apress.prospring5.ch7.entities.Singer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.SessionFactory;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.List;
@Transactional
@Repository("singerDao")
public class SingerDaoImpl implements SingerDao {
private SessionFactory sessionFactory;
public SessionFactory getSessionFactory() {
return sessionFactory;
}
@Resource(name = "sessionFactory")
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
...
}
像往常一样,我们通过使用@Repository注释将 DAO 类声明为一个 Spring bean。注释定义了我们将在第九章中进一步讨论的事务需求。通过使用@Resource注释来注入sessionFactory属性。
package com.apress.prospring5.ch7.dao;
import com.apress.prospring5.ch7.entities.Singer;
import java.util.List;
public interface SingerDao {
List<Singer> findAll();
List<Singer> findAllWithAlbum();
Singer findById(Long id);
Singer save(Singer contact);
void delete(Singer contact);
}
界面简单;它只有三个查找方法,一个保存方法和一个删除方法。save()方法将执行插入和更新操作。
使用 Hibernate 查询语言查询数据
Hibernate 和其他 ORM 工具,如 JDO 和 JPA,都是围绕对象模型设计的。因此,在定义了映射之后,我们不需要构造 SQL 来与数据库交互。相反,对于 Hibernate,我们使用 Hibernate 查询语言(HQL)来定义我们的查询。当与数据库交互时,Hibernate 会代表我们将查询翻译成 SQL 语句。
当编码 HQL 查询时,语法很像 SQL。但是,您需要从对象的角度而不是数据库的角度来考虑问题。在接下来的几节中,我们将带您看几个例子。
带延迟抓取的简单查询
让我们从实现findAll()方法开始,该方法简单地从数据库中检索所有联系人。以下代码示例显示了此功能的更新代码:
package com.apress.prospring5.ch7.dao;
import com.apress.prospring5.ch7.entities.Singer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.SessionFactory;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.List;
@Transactional
@Repository("singerDao")
public class SingerDaoImpl implements SingerDao {
private static final Log logger = LogFactory.getLog(SingerDaoImpl.class);
private SessionFactory sessionFactory;
@Transactional(readOnly = true)
public List<Singer> findAll() {
return sessionFactory.getCurrentSession()
.createQuery("from Singer s").list();
}
...
}
方法SessionFactory.getCurrentSession()获得 Hibernate 的Session接口。然后,调用Session.createQuery()方法,传入 HQL 语句。语句from Singer s简单地从数据库中检索所有联系人。该语句的另一种语法是select s from Singer s。@Transactional(readOnly=true)注释意味着我们希望将事务设置为只读。为只读方法设置该属性将获得更好的性能。
下面的代码片段显示了一个简单的SingerDaoImpl测试程序:
package com.apress.prospring5.ch7;
import com.apress.prospring5.ch7.config.AppConfig;
import com.apress.prospring5.ch7.dao.SingerDao;
import com.apress.prospring5.ch7.entities.Singer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import java.util.List;
public class SpringHibernateDemo {
private static Logger logger =
LoggerFactory.getLogger(SpringHibernateDemo.class);
public static void main(String... args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
SingerDao singerDao = ctx.getBean(SingerDao.class);
singerDao.delete(singer);
listSingers(singerDao.findAll());
ctx.close();
}
private static void listSingers(List<Singer> singers) {
logger.info(" ---- Listing singers:");
for (Singer singer : singers) {
logger.info(singer.toString());
}
}
}
运行前面的类会产生以下输出:
---- Listing singers:
Singer - Id: 1, First name: John, Last name: Mayer, Birthday: 1977-10-16
Singer - Id: 3, First name: John, Last name: Butler, Birthday: 1975-04-01
Singer - Id: 2, First name: Eric, Last name: Clapton, Birthday: 1945-03-30
虽然找回了歌手唱片,但是专辑和乐器呢?让我们修改测试类来打印详细信息。在下面的代码片段中,您可以看到方法listSingers()被替换为listSingersWithAlbum():
package com.apress.prospring5.ch7;
import com.apress.prospring5.ch7.config.AppConfig;
import com.apress.prospring5.ch7.dao.SingerDao;
import com.apress.prospring5.ch7.entities.Album;
import com.apress.prospring5.ch7.entities.Instrument;
import com.apress.prospring5.ch7.entities.Singer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import java.util.List;
public class SpringHibernateDemo {
private static Logger logger =
LoggerFactory.getLogger(SpringHibernateDemo.class);
public static void main(String... args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
SingerDao singerDao = ctx.getBean(SingerDao.class);
Singer singer = singerDao.findById(2l);
singerDao.delete(singer);
listSingersWithAlbum(singerDao.findAllWithAlbum());
ctx.close();
}
private static void listSingersWithAlbum(List<Singer> singers) {
logger.info(" ---- Listing singers with instruments:");
for (Singer singer : singers) {
logger.info(singer.toString());
if (singer.getAlbums() != null) {
for (Album album :
singer.getAlbums()) {
logger.info("\t" + album.toString());
}
}
if (singer.getInstruments() != null) {
for (Instrument instrument : singer.getInstruments()) {
logger.info("\t" + instrument.getInstrumentId());
}
}
}
}
}
如果您再次运行该程序,您将看到以下异常:
---- Listing singers with instruments:
Singer - Id: 1, First name: John, Last name: Mayer, Birthday: 1977-10-16
org.hibernate.LazyInitializationException: failed to lazily initialize a
collection of role: com.apress.prospring5.ch7.entities.Singer.albums,
could not initialize proxy - no Session
当您试图访问关联时,您会看到 Hibernate 抛出了LazyInitializationException。
这是因为,默认情况下,Hibernate 会延迟获取关联,这意味着 Hibernate 不会连接记录的关联表(即ALBUM)。这背后的基本原理是为了性能;可以想象,如果一个查询要检索成千上万条记录,并且所有的关联都被检索到,那么大量的数据传输将会降低性能。
使用关联提取进行查询
要让 Hibernate 从关联中获取数据,有两个选项。首先可以定义与取模式EAGER的关联,例如@ManyToMany(fetch=FetchType.EAGER)。这告诉 Hibernate 在每个查询中获取相关的记录。然而,如前所述,这将影响数据检索性能。
另一种选择是在需要时强制 Hibernate 在查询中获取相关记录。如果使用Criteria查询,可以调用函数Criteria.setFetchMode()来指示 Hibernate 快速获取关联。使用NamedQuery时,可以使用fetch操作符指示 Hibernate 急切地获取关联。
让我们来看看findAllWithAlbum()方法的实现,它将检索所有联系信息以及他们的电话详细信息和爱好。这个例子将使用NamedQuery方法。NamedQuery可以外化到一个 XML 文件中,或者使用实体类上的注释来声明。在这里,您可以看到修改后的Singer域对象,带有使用注释定义的命名查询:
package com.apress.prospring5.ch7.entities;
import javax.persistence.Entity;
import javax.persistence.Table;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
...
@Entity
@Table(name = "singer")
@NamedQueries({
@NamedQuery(name="Singer.findAllWithAlbum",
query="select distinct s from Singer s " +
"left join fetch s.albums a " +
"left join fetch s.instruments i")
})
public class Singer implements Serializable {
...
}
首先定义一个名为Singer.findAllWithAlbum的NamedQuery实例。然后我们在 HQL 定义查询。注意left join fetch子句,它指示 Hibernate 急切地获取关联。还需要使用select distinct;否则,Hibernate 将返回重复的对象(如果一个歌手有两张相关联的专辑,将返回两个歌手对象)。
下面是findAllWithAlbum()方法的实现:
package com.apress.prospring5.ch7.dao;
...
@Transactional
@Repository("singerDao")
public class SingerDaoImpl implements SingerDao {
@Transactional(readOnly = true)
public List<Singer> findAllWithAlbum() {
return sessionFactory.getCurrentSession().
getNamedQuery("Singer.findAllWithAlbum").list();
}
}
这次我们使用Session.getNamedQuery()方法,传入NamedQuery实例的名称。修改测试程序(SpringHibernateDemo)来调用singerDao.findAllWithAlbum()将产生以下输出:
---- Listing singers with instruments:
Singer - Id: 1, First name: John, Last name: Mayer, Birthday: 1977-10-16
Album - Id: 2, Singer id: 1, Title: Battle Studies, Release Date: 2009-11-17
Album - Id: 1, Singer id: 1, Title: The Search For Everything,
Release Date: 2017-01-20
Instrument: Guitar
Instrument: Piano
Singer - Id: 3, First name: John, Last name: Butler, Birthday: 1975-04-01
Singer - Id: 2, First name: Eric, Last name: Clapton, Birthday: 1945-03-30
Album - Id: 3, Singer id: 2, Title: From The Cradle, Release Date: 1994-09-13
Instrument: Guitar
现在,所有带有详细信息的歌手都被正确检索到了。让我们看另一个带有参数的NamedQuery的例子。这一次,我们将实现findById()方法,并且也想获取关联。下面的代码片段显示了添加了新命名查询的Singer类:
package com.apress.prospring5.ch7.entities;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import static javax.persistence.GenerationType.IDENTITY;
@Entity
@Table(name = "singer")
@NamedQueries({
@NamedQuery(name="Singer.findById",
query="select distinct s from Singer s " +
"left join fetch s.albums a " +
"left join fetch s.instruments i " +
"where s.id = :id"),
@NamedQuery(name="Singer.findAllWithAlbum",
query="select distinct s from Singer s " +
"left join fetch s.albums a " +
"left join fetch s.instruments i")
})
public class Singer implements Serializable {
...
}
从名为Singer.findById的命名查询中,我们声明一个命名参数:id。这里你可以看到SingerDaoImpl中findById()方法的实现:
package com.apress.prospring5.ch7.dao;
import com.apress.prospring5.ch7.entities.Singer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.SessionFactory;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.List;
@Transactional
@Repository("singerDao")
public class SingerDaoImpl implements SingerDao {
private static final Log logger = LogFactory.getLog(SingerDaoImpl.class);
private SessionFactory sessionFactory;
@Transactional(readOnly = true)
public Singer findById(Long id) {
return (Singer) sessionFactory.getCurrentSession().
getNamedQuery("Singer.findById").
setParameter("id", id).uniqueResult();
}
...
}
在这个清单中,我们使用相同的Session.getNameQuery()方法。但是我们也调用了setParameter()方法,传入命名参数及其值。对于多个参数,可以使用Query接口的setParameterList()或setParameters()方法。
还有一些更高级的查询方法,比如原生查询和条件查询,我们将在下一章讨论 JPA 时讨论。为了测试这个方法,必须相应地修改SpringHibernateDemo类。
package com.apress.prospring5.ch7;
...
public class SpringHibernateDemo {
private static Logger logger =
LoggerFactory.getLogger(SpringHibernateDemo.class);
public static void main(String... args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
SingerDao singerDao = ctx.getBean(SingerDao.class);
Singer singer = singerDao.findById(2l);
logger.info(singer.toString());
ctx.close();
}
}
运行该程序会产生以下输出:
Singer - Id: 1, First name: John, Last name: Mayer, Birthday: 1977-10-16
插入数据
用 Hibernate 插入数据很简单。另一件有趣的事情是检索数据库生成的主键。在前面关于 JDBC 的章节中,我们需要显式声明我们想要检索生成的密钥,传入KeyHolder实例,并在执行 insert 语句后从中取回密钥。使用 Hibernate,所有这些操作都不是必需的。Hibernate 将检索生成的键,并在插入操作后填充域对象。下面的代码片段显示了save()方法的实现:
package com.apress.prospring5.ch7.dao;
import com.apress.prospring5.ch7.entities.Singer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.SessionFactory;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.List;
@Transactional
@Repository("singerDao")
public class SingerDaoImpl implements SingerDao {
private static final Log logger = LogFactory.getLog(SingerDaoImpl.class);
private SessionFactory sessionFactory;
public Singer save(Singer singer) {
sessionFactory.getCurrentSession().saveOrUpdate(singer);
logger.info("Singer saved with id: " + singer.getId());
return singer;
}
...
}
我们只需要调用Session.saveOrUpdate()方法,它可以用于插入和更新操作。我们还记录了保存的 singer 对象的 ID,该对象将在持久化后由 Hibernate 填充。下面的代码片段显示了在SINGER表中插入一个新的歌手记录,在ALBUM表中插入两个子记录,并测试插入是否成功的代码。此外,因为现在我们正在修改表的内容,所以 JUnit 类更适合单独测试每个操作。
package com.apress.prospring5.ch7;
import com.apress.prospring5.ch7.config.AppConfig;
import com.apress.prospring5.ch7.dao.SingerDao;
import com.apress.prospring5.ch7.entities.Album;
import com.apress.prospring5.ch7.entities.Instrument;
import com.apress.prospring5.ch7.entities.Singer;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
public class SingerDaoTest {
private static Logger logger =
LoggerFactory.getLogger(SingerDaoTest.class);
private GenericApplicationContext ctx;
private SingerDao singerDao;
@Before
public void setUp(){
ctx = new AnnotationConfigApplicationContext(AppConfig.class);
singerDao = ctx.getBean(SingerDao.class);
assertNotNull(singerDao);
}
@Test
public void testInsert(){
Singer singer = new Singer();
singer.setFirstName("BB");
singer.setLastName("King");
singer.setBirthDate(new Date(
(new GregorianCalendar(1940, 8, 16)).getTime().getTime()));
Album album = new Album();
album.setTitle("My Kind of Blues");
album.setReleaseDate(new java.sql.Date(
(new GregorianCalendar(1961, 7, 18)).getTime().getTime()));
singer.addAbum(album);
album = new Album();
album.setTitle("A Heart Full of Blues");
album.setReleaseDate(new java.sql.Date(
(new GregorianCalendar(1962, 3, 20)).getTime().getTime()));
singer.addAbum(album);
singerDao.save(singer);
assertNotNull(singer.getId());
List<Singer> singers = singerDao.findAllWithAlbum();
assertEquals(4, singers.size());
listSingersWithAlbum(singers);
}
@Test
public void testFindAll(){
List<Singer> singers = singerDao.findAll();
assertEquals(3, singers.size());
listSingers(singers);
}
@Test
public void testFindAllWithAlbum(){
List<Singer> singers = singerDao.findAllWithAlbum();
assertEquals(3, singers.size());
listSingersWithAlbum(singers);
}
@Test
public void testFindByID(){
Singer singer = singerDao.findById(1L);
assertNotNull(singer);
logger.info(singer.toString());
}
private static void listSingers(List<Singer> singers) {
logger.info(" ---- Listing singers:");
for (Singer singer : singers) {
logger.info(singer.toString());
}
}
private static void listSingersWithAlbum(List<Singer> singers) {
logger.info(" ---- Listing singers with instruments:");
for (Singer singer : singers) {
logger.info(singer.toString());
if (singer.getAlbums() != null) {
for (Album album :
singer.getAlbums()) {
logger.info("\t" + album.toString());
}
}
if (singer.getInstruments() != null) {
for (Instrument instrument : singer.getInstruments()) {
logger.info("\tInstrument: " + instrument.getInstrumentId());
}
}
}
}
@After
public void tearDown(){
ctx.close();
}
}
如前面的代码所示,在testInsert()方法中,我们添加了两个相册,并保存了对象。之后,我们通过调用listSingersWithAlbum再次列出所有歌手。运行testInsert()方法会产生以下输出:
...
INFO o.h.d.Dialect - HHH000400:
Using dialect: org.hibernate.dialect.H2Dialect
INFO o.h.h.i.QueryTranslatorFactoryInitiator - HHH000397:
Using ASTQueryTranslatorFactory
Hibernate:
/* insert com.apress.prospring5.ch7.entities.Singer
*/ insert
into
singer
(ID, BIRTH_DATE, FIRST_NAME, LAST_NAME, VERSION)
values
(null, ?, ?, ?, ?)
Hibernate:
/* insert com.apress.prospring5.ch7.entities.Album
*/ insert
into
album
(ID, RELEASE_DATE, SINGER_ID, title, VERSION)
values
(null, ?, ?, ?, ?)
Hibernate:
/* insert com.apress.prospring5.ch7.entities.Album
*/ insert
into
album
(ID, RELEASE_DATE, SINGER_ID, title, VERSION)
values
(null, ?, ?, ?, ?)
INFO c.a.p.c.d.SingerDaoImpl - Singer saved with id: 4
...
INFO - ---- Listing singers with instruments:
INFO - Singer - Id: 4, First name: BB, Last name: King,
Birthday: 1940-09-16
INFO - Album - Id: 5, Singer id: 4, Title: A Heart Full of Blues,
Release Date: 1962-04-20
INFO - Album - Id: 4, Singer id: 4, Title: My Kind of Blues,
Release Date: 1961-08-18
INFO - Singer - Id: 1, First name: John, Last name: Mayer,
Birthday: 1977-10-16
INFO - Album - Id: 2, Singer id: 1, Title: Battle Studies,
Release Date: 2009-11-17
INFO - Album - Id: 1, Singer id: 1, Title: The Search For Everything,
Release Date: 2017-01-20
INFO - Instrument: Piano
INFO - Instrument: Guitar
INFO - Singer - Id: 3, First name: John, Last name: Butler,
Birthday: 1975-04-01
INFO - Singer - Id: 2, First name: Eric, Last name: Clapton,
Birthday: 1945-03-30
INFO - Album - Id: 3, Singer id: 2, Title: From The Cradle,
Release Date: 1994-09-13
INFO - Instrument: Guitar
日志记录配置已修改,以便打印更详细的休眠信息。从INFO日志记录中,我们可以看到新保存的联系人的 ID 被正确填充。Hibernate 还会显示针对数据库执行的所有 SQL 语句,这样您就知道幕后发生了什么。
更新数据
更新记录就像插入数据一样简单。假设对于 ID 为 1 的歌手,我们希望更新其名字并删除一张专辑。为了测试更新操作,下面的代码片段显示了testUpdate()方法:
package com.apress.prospring5.ch7;
...
public class SingerDaoTest {
private GenericApplicationContext ctx;
private SingerDao singerDao;
...
@Test
public void testUpdate(){
Singer singer = singerDao.findById(1L);
//making sure such singer exists
assertNotNull(singer);
//making sure we got expected singer
assertEquals("Mayer", singer.getLastName());
//retrieve the album
Album album = singer.getAlbums().stream().filter(
a -> a.getTitle().equals("Battle Studies")).findFirst().get();
singer.setFirstName("John Clayton");
singer.removeAlbum(album);
singerDao.save(singer);
// test the update
listSingersWithAlbum(singerDao.findAllWithAlbum());
}
...
}
如前面的代码示例所示,我们首先检索 ID 为 1 的记录。后来,名字就改了。然后,我们遍历相册对象,检索标题为 Battle Studies 的相册,并将其从歌手的albums属性中删除。最后,我们再次调用singerDao.save()方法。当您运行该程序时,您将看到以下输出:
INFO o.h.h.i.QueryTranslatorFactoryInitiator - HHH000397:
Using ASTQueryTranslatorFactory
...
INFO - Singer saved with id: 1
Hibernate:
/* update
com.apress.prospring5.ch7.entities.Album */ update
album
set
RELEASE_DATE=?,
SINGER_ID=?,
title=?,
VERSION=?
where
ID=?
and VERSION=?
Hibernate:
/* delete com.apress.prospring5.ch7.entities.Album */ delete
from
album
where
ID=?
and VERSION=?
INFO ----- Listing singers with instruments:
INFO - Singer - Id: 1, First name: John Clayton, Last name: Mayer,
Birthday: 1977-10-16
INFO - Album - Id: 1, Singer id: 1, Title: The Search For Everything,
Release Date: 2017-01-20
INFO - Instrument: Guitar
INFO - Instrument: Piano
INFO - Singer - Id: 2, First name: Eric, Last name: Clapton,
Birthday: 1945-03-30
INFO - Album - Id: 3, Singer id: 2, Title: From The Cradle,
Release Date: 1994-09-13
INFO - Instrument: Guitar
INFO - Singer - Id: 3, First name: John, Last name: Butler,
Birthday: 1975-04-01
你会看到名字被更新,而战史相册被移除。相册可以被删除是因为我们传递给一对多关联的orphanRemoval=true属性,该属性指示 Hibernate 删除所有存在于数据库中但在持久化时在对象中不再存在的孤立记录。
删除数据
删除数据也很简单。只需调用Session.delete()方法,传入联系对象。以下代码片段显示了要删除的代码:
package com.apress.prospring5.ch7.dao;
...
@Transactional
@Repository("singerDao")
public class SingerDaoImpl implements SingerDao {
private static final Log logger =
LogFactory.getLog(SingerDaoImpl.class);
private SessionFactory sessionFactory;
public void delete(Singer singer) {
sessionFactory.getCurrentSession().delete(singer);
logger.info("Singer deleted with id: " + singer.getId());
}
...
}
删除操作将删除歌手记录及其所有相关信息,包括专辑和乐器,正如我们在映射中定义的cascade=CascadeType.ALL。下面的代码片段显示了用于测试删除方法testDelete()的代码:
package com.apress.prospring5.ch7;
...
public class SingerDaoTest {
private static Logger logger =
LoggerFactory.getLogger(SingerDaoTest.class);
private GenericApplicationContext ctx;
private SingerDao singerDao;
@Test
public void testDelete(){
Singer singer = singerDao.findById(2l);
//making sure such singer exists
assertNotNull(singer);
singerDao.delete(singer);
listSingersWithAlbum(singerDao.findAllWithAlbum());
}
}
前面的清单检索 ID 为 2 的歌手,然后调用 delete 方法删除歌手信息。运行该程序将产生以下输出:
INFO o.h.h.i.QueryTranslatorFactoryInitiator - HHH000397:
Using ASTQueryTranslatorFactory
...
INFO c.a.p.c.d.SingerDaoImpl - Singer deleted with id: 2
Hibernate:
/* delete collection com.apress.prospring5.ch7.entities.
Singer.instruments */ delete
from
singer_instrument
where
SINGER_ID=?
Hibernate:
/* delete com.apress.prospring5.ch7.entities.Album */ delete
from
album
where
ID=?
and VERSION=?
Hibernate:
/* delete com.apress.prospring5.ch7.entities.Singer */ delete
from
singer
where
ID=?
and VERSION=?
INFO - ---- Listing singers with instruments:
INFO - Singer - Id: 1, First name: John, Last name: Mayer,
Birthday: 1977-10-16
INFO - Album - Id: 1, Singer id: 1, Title: The Search For Everything,
Release Date: 2017-01-20
INFO - Album - Id: 2, Singer id: 1, Title: Battle Studies,
Release Date: 2009-11-17
INFO - Instrument: Piano
INFO - Instrument: Guitar
INFO - Singer - Id: 3, First name: John, Last name: Butler,
Birthday: 1975-04-01
您可以看到 ID 为 2 的歌手连同其在ALBUM和SINGER_INSTRUMENT表中的子记录一起被删除。
配置 Hibernate 从实体生成表
在使用 Hibernate 的启动应用中,常见的行为是首先编写实体类,然后根据它们的内容生成数据库表。这是通过使用hibernate.hbm2ddl.auto Hibernate 属性来完成的。当应用第一次启动时,该属性值被设置为create;这将使 Hibernate 扫描实体,并根据使用 JPA 和 Hibernate 注释定义的关系生成表和键(主、外来、唯一)。
如果实体配置正确,并且产生的数据库对象完全符合预期,那么属性的值应该更改为update。这将告诉 Hibernate 用以后在实体上执行的任何更改来更新现有的数据库,并保留原始数据库和插入其中的任何数据。
在生产应用中,编写在伪数据库上运行的单元和集成测试是可行的,该伪数据库在所有测试用例执行后被丢弃。通常测试数据库是内存数据库,Hibernate 被告知创建数据库,并在测试执行后通过将hibernate.hbm2ddl.auto值设置为create-drop来丢弃它。
您可以在 Hibernate 官方文档中找到hibernate.hbm2ddl.auto属性值的完整列表。1
下面的代码片段展示了 Java 配置AdvancedConfig类。正如您所看到的,引入了hibernate.hbm2ddl.auto,使用的数据源是一个 DBCP 池数据源。
package com.apress.prospring5.ch7.config;
import com.apress.prospring5.ch6.CleanUp;
import org.apache.commons.dbcp2.BasicDataSource;
import org.hibernate.SessionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.orm.hibernate5.HibernateTransactionManager;
import org.springframework.orm.hibernate5.LocalSessionFactoryBuilder;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
import java.io.IOException;
import java.util.Properties;
@Configuration
@ComponentScan(basePackages = "com.apress.prospring5.ch7")
@EnableTransactionManagement
@PropertySource("classpath:db/jdbc.properties")
public class AdvancedConfig {
private static Logger logger =
LoggerFactory.getLogger(AdvancedConfig.class);
@Value("${driverClassName}")
private String driverClassName;
@Value("${url}")
private String url;
@Value("${username}")
private String username;
@Value("${password}")
private String password;
@Bean
public static PropertySourcesPlaceholderConfigurer
propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
@Bean(destroyMethod = "close")
public DataSource dataSource() {
try {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName(driverClassName);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
} catch (Exception e) {
logger.error("DBCP DataSource bean cannot be created!", e);
return null;
}
}
private Properties hibernateProperties() {
Properties hibernateProp = new Properties();
hibernateProp.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
hibernateProp.put("hibernate.hbm2ddl.auto", "create-drop");
hibernateProp.put("hibernate.format_sql", true);
hibernateProp.put("hibernate.use_sql_comments", true);
hibernateProp.put("hibernate.show_sql", true);
hibernateProp.put("hibernate.max_fetch_depth", 3);
hibernateProp.put("hibernate.jdbc.batch_size", 10);
hibernateProp.put("hibernate.jdbc.fetch_size", 50);
return hibernateProp;
}
@Bean
public SessionFactory sessionFactory() {
return new LocalSessionFactoryBuilder(dataSource())
.scanPackages("com.apress.prospring5.ch7.entities")
.addProperties(hibernateProperties())
.buildSessionFactory();
}
@Bean public PlatformTransactionManager transactionManager()
throws IOException {
return new HibernateTransactionManager(sessionFactory());
}
}
jdbc.properties文件包含访问内存数据库所需的属性。
driverClassName=org.h2.Driver
url=jdbc:h2:musicdb
username=prospring5
password=prospring5
但是在这种情况下,我们最初如何填充数据呢?您可以使用一个DatabasePopulator实例,一个类似 DbUnit、 2 的库,或者一个类似于DbIntializer bean 的自定义 populator bean,如下所示:
package com.apress.prospring5.ch7.config;
import com.apress.prospring5.ch7.dao.InstrumentDao;
import com.apress.prospring5.ch7.dao.SingerDao;
import com.apress.prospring5.ch7.entities.Album;
import com.apress.prospring5.ch7.entities.Instrument;
import com.apress.prospring5.ch7.entities.Singer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.Date;
import java.util.GregorianCalendar;
@Service
public class DBInitializer {
private Logger logger =
LoggerFactory.getLogger(DBInitializer.class);
@Autowired SingerDao singerDao;
@Autowired InstrumentDao instrumentDao;
@PostConstruct
public void initDB(){
logger.info("Starting database initialization...");
Instrument guitar = new Instrument();
guitar.setInstrumentId("Guitar");
instrumentDao.save(guitar);
...
Singer singer = new Singer();
singer.setFirstName("John");
singer.setLastName("Mayer");
singer.setBirthDate(new Date(
(new GregorianCalendar(1977, 9, 16)).getTime().getTime()));
singer.addInstrument(guitar);
singer.addInstrument(piano);
Album album1 = new Album();
album1.setTitle("The Search For Everything");
album1.setReleaseDate(new java.sql.Date(
(new GregorianCalendar(2017, 0, 20)).getTime().getTime()));
singer.addAbum(album1);
Album album2 = new Album();
album2.setTitle("Battle Studies");
album2.setReleaseDate(new java.sql.Date(
(new GregorianCalendar(2009, 10, 17)).getTime().getTime()));
singer.addAbum(album2);
singerDao.save(singer);
...
logger.info("Database initialization finished.");
}
}
DbIntializer只是一个简单的 bean,其中存储库作为依赖项被注入,并且有一个由注释@PostConstruct定义的初始化方法,在该方法中对象被创建并保存到数据库中。该 bean 用@Service注释进行了注释,以将其标记为提供初始化数据库内容服务的 bean。这个 bean 将在创建ApplicationContext时创建,初始化方法将被执行,这将确保在使用上下文之前填充数据库。
使用AdvancedConfig配置类,之前运行的相同测试集将通过。
注释方法或字段?
在前面的例子中,实体的 getters 上有 JPA 注释。但是 JPA 注释可以直接在字段上使用,这有几个优点。
- 实体配置更清晰,位于 fields 部分,而不是分散在整个类内容中。显然,只有当代码是按照干净代码的建议编写的,将一个类中的所有字段声明放在同一个连续的部分中时,这一点才是正确的。
- 注释实体字段不会强制提供 setter/getter。这对于
@Version带注释的字段很有用,它不应该被手动修改;只有 Hibernate 可以访问它。 - 注释字段允许在 setters 中进行额外的处理(例如,在从数据库加载值之后对其进行加密/计算)。属性访问的问题是,当对象被加载时,setters 也被调用。
网上有很多关于哪个更好的讨论。从性能的角度来看,没有任何区别。这个决定最终取决于开发人员,因为在一些有效的情况下,注释访问器可能更有意义。但是请记住,在数据库中,对象的状态是实际保存的,对象的状态是由其字段的值定义的,而不是由访问器返回的值。这也意味着可以从数据库中准确地重新创建一个对象,就像它被持久化时一样。因此,在某种程度上,在 getters 上设置注释可以被视为破坏封装。
在这里,您可以看到Singer实体类被重写为具有带注释的字段,并扩展了抽象类AbstractEntity,它包含应用中所有 Hibernate 实体类共有的两个字段:
// AbstractEntity.java
package com.apress.prospring5.ch7.entities;
import javax.persistence.*;
import java.io.Serializable;
@MappedSuperclass
public abstract class AbstractEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(updatable = false)
protected Long id;
@Version
@Column(name = "VERSION")
private int version;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}
//Singer.java
@Entity
@Table(name = "singer")
@NamedQueries({
@NamedQuery(name=Singer.FIND_SINGER_BY_ID,
query="select distinct s from Singer s " +
"left join fetch s.albums a " +
"left join fetch s.instruments i " +
"where s.id = :id"),
@NamedQuery(name=Singer.FIND_ALL_WITH_ALBUM,
query="select distinct s from Singer s " +
"left join fetch s.albums a " +
"left join fetch s.instruments i")
})
public class Singer extends AbstractEntity {
public static final String FIND_SINGER_BY_ID = "Singer.findById";
public static final String FIND_ALL_WITH_ALBUM = "Singer.findAllWithAlbum";
@Column(name = "FIRST_NAME")
private String firstName;
@Column(name = "LAST_NAME")
private String lastName;
@Temporal(TemporalType.DATE)
@Column(name = "BIRTH_DATE")
private Date birthDate;
@OneToMany(mappedBy = "singer", cascade=CascadeType.ALL,
orphanRemoval=true)
private Set<Album> albums = new HashSet<>();
@ManyToMany
@JoinTable(name = "singer_instrument",
joinColumns = @JoinColumn(name = "SINGER_ID"),
inverseJoinColumns = @JoinColumn(name = "INSTRUMENT_ID"))
private Set<Instrument> instruments = new HashSet<>();
...
}
使用 Hibernate 时的注意事项
如本章示例所示,一旦正确定义了所有对象到关系的映射、关联和查询,Hibernate 就可以为您提供一个环境,让您专注于使用对象模型编程,而不是为每个操作编写 SQL 语句。在过去的几年里,Hibernate 一直在快速发展,并被 Java 开发人员广泛采用为数据访问层库,无论是在开源社区还是在企业中。
然而,有几点你需要记住。首先,因为您无法控制生成的 SQL,所以在定义映射时应该小心,尤其是关联和它们的获取策略。其次,您应该观察 Hibernate 生成的 SQL 语句,以验证所有语句都按照您的预期执行。
理解 Hibernate 如何管理其会话的内部机制也很重要,尤其是在批处理作业操作中。Hibernate 将保持被管理对象在会话中,并定期刷新和清除它们。设计不良的数据访问逻辑可能会导致 Hibernate 过于频繁地刷新会话,从而严重影响性能。如果你想绝对控制查询,你可以使用本地查询,我们将在下一章讨论。
最后,设置(批处理大小、获取大小等等)在调优 Hibernate 的性能方面起着重要的作用。您应该在会话工厂中定义它们,并在对应用进行负载测试时调整它们,以确定最佳值。
毕竟,Hibernate 以及我们将在下一章讨论的它出色的 JPA 支持,对于寻找一种面向对象方法来实现数据访问逻辑的 Java 开发人员来说是一个自然的选择。
摘要
在本章中,我们讨论了 Hibernate 的基本概念以及如何在 Spring 应用中配置它。然后我们讲述了定义 ORM 映射的常用技术,讲述了关联以及如何使用HibernateTemplate类来执行各种数据库操作。关于 Hibernate,我们只讨论了它的一小部分功能和特性。对于那些对使用 Hibernate 和 Spring 感兴趣的人,我们强烈建议您学习 Hibernate 的标准文档。此外,许多书籍详细讨论了 Hibernate。我们推荐约瑟夫·奥廷格、杰夫·林伍德和戴夫·明特(Apress,2016 年)的《开始冬眠:为了 Hibernate 5》, 3 以及迈克·基思和梅里克·辛卡里奥尔的《Pro JPA 2》(Apress,2013 年)。 4 下一章,你就来看看 JPA,以及在使用 Spring 时如何使用。Hibernate 为 JPA 提供了出色的支持,对于下一章中的例子,我们将继续使用 Hibernate 作为持久性提供者。对于查询和更新操作,JPA act 喜欢 Hibernate。在下一章,我们将讨论一些高级主题,包括本地查询和标准查询,以及如何使用 Hibernate 和它的 JPA 支持。
Footnotes 1
https://docs.jboss.org/hibernate/orm/5.0/manual/en-US/html/ch03.html 见表 3.7。
2
我们将在 http://dbunit.sourceforge.net/ 找到官方的 DbUnit 站点。
3
从阿普瑞斯官方网站下载电子书: http://apress.com/us/book/9781484223185 。
4
从阿普瑞斯官方网站下载电子书: http://apress.com/us/book/9781430249269 。
八、使用 JPA2 在 Spring 中访问数据
在前一章中,我们讨论了当用 ORM 方法实现数据访问逻辑时,如何使用 Hibernate 和 Spring。我们演示了如何在 Spring 的配置中配置 Hibernate 的SessionFactory,以及如何使用Session接口进行各种数据访问操作。然而,这只是 Hibernate 的一种使用方式。在 Spring 应用中采用 Hibernate 的另一种方式是使用 Hibernate 作为标准 Java 持久性 API (JPA)的持久性提供者。
Hibernate 的 POJO 映射和它强大的查询语言(HQL)取得了巨大的成功,也影响了 Java 世界中数据访问技术标准的发展。在 Hibernate 之后,JCP 开发了 Java 数据对象(JDO)标准和 JPA。
在撰写本文时,JPA 已经到了 2.1 版,并提供了标准化的概念,如PersistenceContext、EntityManager和 Java 持久性查询语言(JPQL)。这些标准化为开发人员提供了一种在 Hibernate、EclipseLink、Oracle TopLink 和 Apache OpenJPA 等 JPA 持久性提供者之间切换的方式。因此,大多数新的 JEE 应用都采用 JPA 作为数据访问层。
Spring 也为 JPA 提供了出色的支持。例如,提供了许多EntityManagerFactoryBean实现来引导 JPA 实体管理器,支持前面提到的所有 JPA 提供者。Spring Data 项目还提供了一个名为 Spring Data JPA 的子项目,它为在 Spring 应用中使用 JPA 提供了高级支持。Spring Data JPA 项目的主要特性包括存储库和规范的概念,以及对查询领域特定语言(QueryDSL)的支持。
本章介绍了如何将 JPA 2.1 与 Spring 一起使用,使用 Hibernate 作为底层的持久性提供者。您将学习如何使用 JPA 的EntityManager接口和 JPQL 实现各种数据库操作。然后您将看到 Spring Data JPA 如何进一步帮助简化 JPA 开发。最后,我们将介绍与 ORM 相关的高级主题,包括本地查询和标准查询。
具体来说,我们讨论以下主题:
- Java 持久性 API (JPA)的核心概念:我们涵盖了 JPA 的一些主要概念。
- 配置 JPA 实体管理器:我们讨论 Spring 支持的
EntityManagerFactory类型,以及如何在 Spring 的 XML 配置中配置最常用的LocalContainerEntityManagerFactoryBean。 - 数据操作:我们展示了如何在 JPA 中实现基本的数据库操作,这非常类似于单独使用 Hibernate 时的概念。
- 高级查询操作:我们将讨论如何在 JPA 中使用原生查询,以及在 JPA 中使用强类型标准 API 来实现更灵活的查询操作。
- 介绍 Spring Data Java Persistence API(JPA):我们讨论 Spring Data JPA 项目,并演示它如何帮助简化数据访问逻辑的开发。
- 跟踪实体变更和审计:在数据库更新操作中,跟踪实体的创建日期或上次更新日期以及谁进行了变更是一个常见的需求。此外,对于像客户这样的关键信息,通常需要一个存储实体每个版本的历史表。我们将讨论 Spring DataJPA 和 Hibernate Envers (Hibernate 实体版本管理系统)如何帮助简化这种逻辑的开发。
与 Hibernate 一样,JPA 支持 XML 或 Java 注释中的映射定义。本章关注映射的注释类型,因为它的使用比 XML 风格更受欢迎。
JPA 2.1 简介
像其他 Java 规范请求(JSR)一样,JPA 2.1 规范(JSR-338)的目标是标准化 JSE 和 JEE 环境中的 ORM 编程模型。它定义了一组公共的概念、注释、接口和 JPA 持久性提供者应该实现的其他服务。当按照 JPA 标准编程时,开发人员可以选择随意切换底层提供者,就像为基于 JEE 标准开发的应用切换到另一个符合 JEE 标准的应用服务器一样。
在 JPA 中,核心概念是来自类型为EntityManagerFactory的工厂的EntityManager接口。EntityManager的主要工作是维护一个持久化上下文,在这个上下文中存储了它所管理的所有实体实例。EntityManager的配置定义为一个持久化单元,一个应用中可以有多个持久化单元。如果你正在使用 Hibernate,你可以把持久化上下文想成和Session接口一样的方式,而EntityManagerFactory和SessionFactory是一样的。在 Hibernate 中,受管实体存储在会话中,您可以通过 Hibernate 的SessionFactory或Session接口直接与之交互。然而,在 JPA 中,您不能直接与持久性上下文交互。相反,你需要依靠EntityManager来为你完成工作。
JPQL 类似于 HQL,所以如果你以前用过 HQL,JPQL 应该很容易上手。然而,在 JPA 2 中,引入了强类型标准 API,它依赖于映射实体的元数据来构造查询。鉴于此,任何错误都将在编译时而不是运行时被发现。
关于 JPA 2 的详细讨论,我们推荐 Mike Keith 和 Merrick Schincariol 的书 Pro JPA 2(a press,2013)。 1 在这一节中,我们讨论 JPA 的基本概念,本章将要用到的样本数据模型,以及如何配置 Spring 的ApplicationContext来支持 JPA。
示例代码的示例数据模型
在本章中,我们使用与第七章相同的数据模型。然而,当我们讨论如何实现审计特性时,我们将添加几个列和一个历史表进行演示。首先,我们将从上一章中使用的相同的数据库创建脚本开始。如果你跳过了第七章,看看那一章的“示例代码的示例数据模型”一节中给出的数据模型,这可以帮助你理解本章中的示例代码。
配置 JPA 的 EntityManagerFactory
正如本章前面提到的,要在 Spring 中使用 JPA,我们需要配置EntityManagerFactory,就像在 Hibernate 中使用的SessionFactory一样。Spring 支持三种类型的EntityManagerFactory配置。
第一个使用了LocalEntityManagerFactoryBean类。这是最简单的一种,只需要持久性单元名。然而,由于它不支持DataSource的注入,因此不能参与全局事务,它只适合于简单的开发目的。
第二种选择是在符合 JEE 标准的容器中使用,其中应用服务器根据部署描述符中的信息引导 JPA 持久性单元。这允许 Spring 通过 JNDI 查找来查找实体管理器。下面的配置片段描述了通过 JNDI 查找实体管理器所需的元素:
<beans ...>
<jee:jndi-lookup id="prospring5Emf"
jndi-name="persistence/prospring5PersistenceUnit"/>
</beans>
在 JPA 规范中,应该在配置文件META-INF/persistence.xml中定义一个持久性单元。但是,从 Spring 3.1 开始,增加了一个新特性,消除了这种需要;我们将在本章的后面向您展示如何使用它。
第三个选项是最常见的,也是本章使用的,是支持DataSource注入的LocalContainerEntityManagerFactoryBean类,它可以参与本地和全局事务。下面的配置片段显示了相应的 XML 配置文件(app-context-annotation.xml):
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/jdbc
http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<jdbc:embedded-database id="dataSource" type="H2">
<jdbc:script location="classpath:sql/schema.sql"/>
<jdbc:script location="classpath:sql/test-data.sql"/>
</jdbc:embedded-database>
<bean id="transactionManager" class=
"org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="emf"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager" />
<bean id="emf" class=
"org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="jpaVendorAdapter">
<bean class=
"org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
</property>
<property name="packagesToScan" value="com.apress.prospring5.ch8.entities"/>
<property name="jpaProperties">
<props>
<prop key="hibernate.dialect">
org.hibernate.dialect.H2Dialect
</prop>
<prop key="hibernate.max_fetch_depth">3</prop>
<prop key="hibernate.jdbc.fetch_size">50</prop>
<prop key="hibernate.jdbc.batch_size">10</prop>
<prop key="hibernate.show_sql">true</prop>
</props>
</property>
</bean>
<context:component-scan base-package="com.apress.prospring5.ch8" />
</beans>
您可能期望有一个使用 Java 配置类的等效配置。有,如下图所示:
package com.apress.prospring5.ch8.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.util.Properties;
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = {"com.apress.prospring5.ch8.service"})
public class JpaConfig {
private static Logger logger = LoggerFactory.getLogger(JpaConfig.class);
@Bean
public DataSource dataSource() {
try {
EmbeddedDatabaseBuilder dbBuilder =
new EmbeddedDatabaseBuilder();
return dbBuilder.setType(EmbeddedDatabaseType.H2)
.addScripts("classpath:db/schema.sql", "classpath:db/test-data.sql").build();
} catch (Exception e) {
logger.error("Embedded DataSource bean cannot be created!", e);
return null;
}
}
@Bean
public PlatformTransactionManager transactionManager() {
return new JpaTransactionManager(entityManagerFactory());
}
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
return new HibernateJpaVendorAdapter();
}
@Bean
public Properties hibernateProperties() {
Properties hibernateProp = new Properties();
hibernateProp.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
hibernateProp.put("hibernate.format_sql", true);
hibernateProp.put("hibernate.use_sql_comments", true);
hibernateProp.put("hibernate.show_sql", true);
hibernateProp.put("hibernate.max_fetch_depth", 3);
hibernateProp.put("hibernate.jdbc.batch_size", 10);
hibernateProp.put("hibernate.jdbc.fetch_size", 50);
return hibernateProp;
}
@Bean
public EntityManagerFactory entityManagerFactory() {
LocalContainerEntityManagerFactoryBean factoryBean =
new LocalContainerEntityManagerFactoryBean();
factoryBean.setPackagesToScan("com.apress.prospring5.ch8.entities");
factoryBean.setDataSource(dataSource());
factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
factoryBean.setJpaProperties(hibernateProperties());
factoryBean.setJpaVendorAdapter(jpaVendorAdapter());
factoryBean.afterPropertiesSet();
return factoryBean.getNativeEntityManagerFactory();
}
}
在前面的配置中,声明了几个 beans,以便能够使用 Hibernate 作为持久性提供者来支持LocalContainerEntityManagerFactoryBean的配置。主要配置如下:
- dataSource bean:我们使用 H2 声明了带有嵌入式数据库的数据源。因为它是一个嵌入式数据库,所以不需要数据库名称。
- transactionManager bean :
EntityManagerFactory需要一个事务管理器来进行事务数据访问。Spring 提供了专门针对 JPA 的事务管理器(org.springframework.orm.jpa.JpaTransactionManager)。该 bean 是用分配的 IDtransactionManager声明的。我们将在第九章中详细讨论事务。我们声明标签<tx:annotation-driven>来支持使用注释声明事务界定需求。它的对等注解是@EnableTransactionManagement,必须放在用@Configuration注解的类上。 - 组件扫描:标签应该是你熟悉的。我们指示 Spring 扫描包
com.apress.prospring5.ch8下的组件。 - JPA entitymanager factory bean:
emfbean 是最重要的部分。首先,我们声明 bean 使用LocalContainerEntityManagerFactoryBean。在 bean 中,提供了几个属性。首先,如您所料,我们需要注入DataSource豆。其次,我们用类HibernateJpaVendorAdapter配置属性jpaVendorAdapter,因为我们使用的是 Hibernate。第三,我们指示实体工厂在包com.apress.prospring5.ch8(由<property name="packagesToScan">标签指定)下扫描带有 ORM 注释的域对象。注意,这个特性是从 Spring 3.1 开始才有的,在域类扫描的支持下,你可以跳过META-INF/persistence.xml文件中持久性单元的定义。最后,jpaProperties属性提供了持久性提供者 Hibernate 的配置细节。您将看到配置选项与我们在第七章中使用的选项相同,因此我们可以跳过这里的解释。
使用 JPA 注释进行 ORM 映射
Hibernate 在很多方面影响了 JPA 的设计。对于映射注释,它们是如此的接近,以至于我们在第七章中使用的用于将域对象映射到数据库的注释在 JPA 中是相同的。如果你看一下第七章中域类的源代码,你会看到所有的映射注释都在包javax.persistence下,这意味着那些注释已经是 JPA 兼容的了。
一旦EntityManagerFactory被正确配置,将其注入到您的类中就很简单了。下面的代码片段显示了SingerServiceImpl类的代码,我们将用它作为使用 JPA 执行数据库操作的示例:
package com.apress.prospring5.ch8.service;
import com.apress.prospring5.ch8.entities.Singer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import org.apache.commons.lang3.NotImplementedException;
import java.util.List;
import javax.persistence.PersistenceContext;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@Service("jpaSingerService")
@Repository
@Transactional
public class SingerServiceImpl implements SingerService {
final static String ALL_SINGER_NATIVE_QUERY =
"select id, first_name, last_name, birth_date, version from singer";
private static Logger logger =
LoggerFactory.getLogger(SingerServiceImpl.class);
@PersistenceContext
private EntityManager em;
@Transactional(readOnly=true)
@Override
public List<Singer> findAll() {
throw new NotImplementedException("findAll");
}
@Transactional(readOnly=true)
@Override
public List<Singer> findAllWithAlbum() {
throw new NotImplementedException("findAllWithAlbum");
}
@Transactional(readOnly=true)
@Override
public Singer findById(Long id) {
throw new NotImplementedException("findById");
}
@Override
public Singer save(Singer singer) {
throw new NotImplementedException("save");
}
@Override
public void delete(Singer singer) {
throw new NotImplementedException("delete");
}
@Transactional(readOnly=true)
@Override
public List<Singer> findAllByNativeQuery() {
throw new NotImplementedException("findAllByNativeQuery");
}
}
对该类应用了几个注释。@Service注释用于将类标识为向另一层提供业务服务的 Spring 组件,并将 Spring bean 命名为jpaSingerService。@Repository注释表明该类包含数据访问逻辑,并指示 Spring 将特定于供应商的异常转换为 Spring 的DataAccessException层次结构。正如您已经熟悉的,@Transactional注释用于定义事务需求。
为了注入EntityManager,我们使用了@PersistenceContext注释,这是实体管理器注入的标准 JPA 注释。关于我们为什么使用名称@PersistenceContext来注入实体管理器,这可能是有问题的,但是如果你考虑到持久化上下文本身是由EntityManager管理的,那么注释命名是完全有意义的。如果您的应用中有多个持久性单元,您还可以将unitName属性添加到注释中,以指定您想要注入哪个持久性单元。通常,一个持久性单元代表一个单独的后端DataSource。
用 JPA 执行数据库操作
本节介绍如何在 JPA 中执行数据库操作。下面的代码片段显示了SingerService接口,它表示我们将要提供的歌手信息服务:
package com.apress.prospring5.ch8.service;
import com.apress.prospring5.ch8.entities.Singer;
import java.util.List;
public interface SingerService {
List<Singer> findAll();
List<Singer> findAllWithAlbum();
Singer findById(Long id);
Singer save(Singer singer);
void delete(Singer singer);
List<Singer> findAllByNativeQuery();
}
界面非常简单;它只有三个查找方法,一个保存方法和一个删除方法。save 方法将同时服务于插入和更新操作。
使用 Java 持久性查询语言查询数据
JPQL 和 HQL 的语法是相似的,事实上,我们在第七章中使用的所有 HQL 查询都可以重用,以在SingerService接口中实现三个 finder 方法。要使用 JPA 和 Hibernate,您需要向项目添加以下依赖项:
//pro-spring-15/build.gradle
ext {
hibernateVersion = '5.2.10.Final'
hibernateJpaVersion = '1.0.0.Final'
..
hibernate = [
em :
"org.hibernate:hibernate-entitymanager:$hibernateVersion",
jpaApi :
"org.hibernate.javax.persistence:hibernate-jpa-2.1-api:$hibernateJpaVersion"
]
}
//chapter08.gradle
dependencies {
//we specify these dependencies for all submodules,
//except the boot module, that defines its own
if !project.name.contains"boot" {
compile spring.contextSupport, spring.orm, spring.context,
misc.slf4jJcl, misc.logback, db.h2, misc.lang3,
hibernate.em, hibernate.jpaApi
}
testCompile testing.junit
}
下面的代码片段概括了第七章中Singer域对象模型类的代码:
//Singer.java
package com.apress.prospring5.ch8.entities;
import static javax.persistence.GenerationType.IDENTITY;
import java.io.Serializable;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.Entity;
import javax.persistence.Table;
import javax.persistence.Id;
import javax.persistence.GeneratedValue;
import javax.persistence.Column;
import javax.persistence.Version;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.OneToMany;
import javax.persistence.ManyToMany;
import javax.persistence.JoinTable;
import javax.persistence.JoinColumn;
import javax.persistence.CascadeType;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.SqlResultSetMapping;
import javax.persistence.EntityResult;
@Entity
@Table(name = "singer")
@NamedQueries({
@NamedQuery(name=Singer.FIND_ALL, query="select s from Singer s"),
@NamedQuery(name=Singer.FIND_SINGER_BY_ID,
query="select distinct s from Singer s " +
"left join fetch s.albums a " +
"left join fetch s.instruments i " +
"where s.id = :id"),
@NamedQuery(name=Singer.FIND_ALL_WITH_ALBUM,
query="select distinct s from Singer s " +
"left join fetch s.albums a " +
"left join fetch s.instruments i")
})
@SqlResultSetMapping(
name="singerResult",
entities=@EntityResult(entityClass=Singer.class)
)
public class Singer implements Serializable {
public static final String FIND_ALL = "Singer.findAll";
public static final String FIND_SINGER_BY_ID = "Singer.findById";
public static final String FIND_ALL_WITH_ALBUM = "Singer.findAllWithAlbum";
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "ID")
private Long id;
@Version
@Column(name = "VERSION")
private int version;
@Column(name = "FIRST_NAME")
private String firstName;
@Column(name = "LAST_NAME")
private String lastName;
@Temporal(TemporalType.DATE)
@Column(name = "BIRTH_DATE")
private Date birthDate;
@OneToMany(mappedBy = "singer", cascade=CascadeType.ALL,
orphanRemoval=true)
private Set<Album> albums = new HashSet<>();
@ManyToMany
@JoinTable(name = "singer_instrument",
joinColumns = @JoinColumn(name = "SINGER_ID"),
inverseJoinColumns = @JoinColumn(name = "INSTRUMENT_ID"))
private Set<Instrument> instruments = new HashSet<>();
//setters and getters
@Override
public String toString() {
return "Singer - Id: " + id + ", First name: " + firstName
+ ", Last name: " + lastName + ", Birthday: " + birthDate;
}
}
// Album.java
package com.apress.prospring5.ch8.entities;
import static javax.persistence.GenerationType.IDENTITY;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
import javax.persistence.*;
@Entity
@Table(name = "album")
public class Album implements Serializable {
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "ID")
private Long id;
@Version
@Column(name = "VERSION")
private int version;
@Column
private String title;
@Temporal(TemporalType.DATE)
@Column(name = "RELEASE_DATE")
private Date releaseDate;
@ManyToOne
@JoinColumn(name = "SINGER_ID")
private Singer singer;
public Album() {
//needed byJPA
}
public Album(String title, Date releaseDate) {
this.title = title;
this.releaseDate = releaseDate;
}
//setters and getters
}
//Instrument.java
package com.apress.prospring5.ch8.entities;
import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.Table;
import javax.persistence.Column;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.JoinTable;
import javax.persistence.JoinColumn;
import java.util.Set;
import java.util.HashSet;
@Entity
@Table(name = "instrument")
public class Instrument implements Serializable {
@Id
@Column(name = "INSTRUMENT_ID")
private String instrumentId;
@ManyToMany
@JoinTable(name = "singer_instrument",
joinColumns = @JoinColumn(name = "INSTRUMENT_ID"),
inverseJoinColumns = @JoinColumn(name = "SINGER_ID"))
private Set<Singer> singers = new HashSet<>();
//setters and getters
}
如果您分析使用@NamedQuery定义的查询,您会发现 HQL 和 JPQL 之间似乎没有区别。让我们从findAll()方法开始,它简单地从数据库中检索所有歌手。
package com.apress.prospring5.ch8.service;
...
@Service("jpaSingerService")
@Repository
@Transactional
public class SingerServiceImpl implements SingerService {
final static String ALL_SINGER_NATIVE_QUERY =
"select id, first_name, last_name, birth_date, version from singer";
private static Logger logger =
LoggerFactory.getLogger(SingerServiceImpl.class);
@PersistenceContext
private EntityManager em;
@Transactional(readOnly=true)
@Override
public List<Singer> findAll() {
return em.createNamedQuery(Singer.FIND_ALL, Singer.class)
.getResultList();
}
...
}
如清单所示,我们使用EntityManager.createNamedQuery()方法,传入查询名称和预期的返回类型。在这种情况下,EntityManager将返回一个TypedQuery<X>接口。然后调用方法TypedQuery.getResultList()来检索歌手。为了测试该方法的实现,我们将使用一个测试类,该类将包含每个将要实现的 JPA 方法的测试方法。
package com.apress.prospring5.ch8;
import com.apress.prospring5.ch8.config.JpaConfig;
import com.apress.prospring5.ch8.entities.Singer;
import com.apress.prospring5.ch8.service.SingerService;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
public class SingerJPATest {
private static Logger logger = LoggerFactory.getLogger(SingerJPATest.class);
private GenericApplicationContext ctx;
private SingerService singerService;
@Before
public void setUp(){
ctx = new AnnotationConfigApplicationContext(JpaConfig.class);
singerService = ctx.getBean(SingerService.class);
assertNotNull(singerService);
}
@Test
public void testFindAll(){
List<Singer> singers = singerService.findAll();
assertEquals(3, singers.size());
listSingers(singers);
}
private static void listSingers(List<Singer> singers) {
logger.info(" ---- Listing singers:");
for (Singer singer : singers) {
logger.info(singer.toString());
}
}
@After
public void tearDown(){
ctx.close();
}
}
如果assertEquals没有抛出异常(测试失败),运行testFindAll()测试方法将产生以下输出:
---- Listing singers:
Singer - Id: 1, First name: John, Last name: Mayer, Birthday: 1977-10-16
Singer - Id: 2, First name: Eric, Last name: Clapton, Birthday: 1945-03-30
Singer - Id: 3, First name: John, Last name: Butler, Birthday: 1975-04-01
对于关联,JPA 规范规定,默认情况下,持久性提供者必须急切地获取关联。但是,对于 Hibernate 的 JPA 实现,默认的抓取策略仍然是 lazy。因此,当使用 Hibernate 的 JPA 实现时,您不需要显式地将关联定义为惰性抓取。Hibernate 的默认获取策略不同于 JPA 规范。
现在让我们实现findAllWithAlbum()方法,它将获取所有相关的专辑和乐器。实现如下所示:
package com.apress.prospring5.ch8.service;
...
@Service("jpaSingerService")
@Repository
@Transactional
public class SingerServiceImpl implements SingerService {
final static String ALL_SINGER_NATIVE_QUERY =
"select id, first_name, last_name, birth_date, version from singer";
private static Logger logger =
LoggerFactory.getLogger(SingerServiceImpl.class);
@PersistenceContext
private EntityManager em;
@Transactional(readOnly=true)
@Override
public List<Singer> findAllWithAlbum() {
List<Singer> singers = em.createNamedQuery
(Singer.FIND_ALL_WITH_ALBUM, Singer.class).getResultList();
return singers;
}
...
}
findAllWithAlbum()与findAll()方法相同,但是它使用不同的命名查询,并启用了left join fetch。用于测试和打印条目的方法如下所示:
package com.apress.prospring5.ch8;
...
public class SingerJPATest {
private static Logger logger =
LoggerFactory.getLogger(SingerJPATest.class);
private GenericApplicationContext ctx;
private SingerService singerService;
@Before
public void setUp(){
ctx = new AnnotationConfigApplicationContext(JpaConfig.class);
singerService = ctx.getBean(SingerService.class);
assertNotNull(singerService);
}
@Test
public void testFindAllWithAlbum(){
List<Singer> singers = singerService.findAllWithAlbum();
assertEquals(3, singers.size());
listSingersWithAlbum(singers);
}
private static void listSingersWithAlbum(List<Singer> singers) {
logger.info(" ---- Listing singers with instruments:");
for (Singer singer : singers) {
logger.info(singer.toString());
if (singer.getAlbums() != null) {
for (Album album :
singer.getAlbums()) {
logger.info("\t" + album.toString());
}
}
if (singer.getInstruments() != null) {
for (Instrument instrument : singer.getInstruments()) {
logger.info("\tInstrument: " + instrument.getInstrumentId());
}
}
}
}
@After
public void tearDown(){
ctx.close();
}
}
如果assertEquals没有抛出异常(测试失败),运行testFindAllWithAlbum()测试方法将产生以下输出:
INFO o.h.h.i.QueryTranslatorFactoryInitiator - HHH000397:
Using ASTQueryTranslatorFactory
Hibernate:
/* Singer.findAllWithAlbum */ select
distinct singer0_.ID as ID1_2_0_,
albums1_.ID as ID1_0_1_,
instrument3_.INSTRUMENT_ID as INSTRUME1_1_2_,
singer0_.BIRTH_DATE as BIRTH_DA2_2_0_,
singer0_.FIRST_NAME as FIRST_NA3_2_0_,
singer0_.LAST_NAME as LAST_NAM4_2_0_,
singer0_.VERSION as VERSION5_2_0_,
albums1_.RELEASE_DATE as RELEASE_2_0_1_,
albums1_.SINGER_ID as SINGER_I5_0_1_,
albums1_.title as title3_0_1_,
albums1_.VERSION as VERSION4_0_1_,
albums1_.SINGER_ID as SINGER_I5_0_0__,
albums1_.ID as ID1_0_0 ,
instrument2_.SINGER_ID as SINGER_I1_3_1__,
instrument2_.INSTRUMENT_ID as INSTRUME2_3_1__
from
singer singer0_
left outer join
album albums1_
on singer0_.ID=albums1_.SINGER_ID
left outer join
singer_instrument instrument2_
on singer0_.ID=instrument2_.SINGER_ID
left outer join
instrument instrument3_
on instrument2_.INSTRUMENT_ID=instrument3_.INSTRUMENT_ID
INFO ----- Listing singers with instruments:
INFO - Singer - Id: 1, First name: John, Last name: Mayer, Birthday: 1977-10-16
INFO - Album - id: 2, Singer id: 1, Title: Battle Studies,
Release Date: 2009-11-17
INFO - Album - id: 1, Singer id: 1, Title: The Search For Everything,
Release Date: 2017-01-20
INFO - Instrument: Guitar
INFO - Instrument: Piano
INFO - Singer - Id: 3, First name: John, Last name: Butler, Birthday: 1975-04-01
INFO - Singer - Id: 2, First name: Eric, Last name: Clapton, Birthday: 1945-03-30
INFO - Album - id: 3, Singer id: 2, Title: From The Cradle,
Release Date: 1994-09-13
INFO - Instrument: Guitar
如果为 Hibernate 启用了日志记录,您还可以看到为从数据库中提取所有数据而生成的本地查询。
现在让我们看看findById()方法,它演示了如何在 JPA 中使用带有命名参数的命名查询。关联也将被提取。以下代码片段显示了实现:
package com.apress.prospring5.ch8.service;
...
@Service("jpaSingerService")
@Repository
@Transactional
public class SingerServiceImpl implements SingerService {
final static String ALL_SINGER_NATIVE_QUERY =
"select id, first_name, last_name, birth_date, version from singer";
private static Logger logger =
LoggerFactory.getLogger(SingerServiceImpl.class);
@PersistenceContext
private EntityManager em;
@Transactional(readOnly=true)
@Override
public Singer findById(Long id) {
TypedQuery<Singer> query = em.createNamedQuery
(Singer.FIND_SINGER_BY_ID, Singer.class);
query.setParameter("id", id);
return query.getSingleResult();
}
...
}
调用了EntityManager.createNamedQuery(java.lang.String name, java.lang.Class<T> resultClass)来获取TypedQuery<T>接口的实例,这确保了查询结果必须是Singer类型的。然后用TypedQuery<T>.setParameter()方法设置查询中指定参数的值,并调用getSingleResult()方法,因为结果应该只包含一个具有指定 ID 的Singer对象。我们将把方法的测试作为一个练习留给您。
package com.apress.prospring5.ch8.service;
...
@Service("jpaSingerService")
@Repository
@Transactional
public class SingerServiceImpl implements SingerService {
final static String ALL_SINGER_NATIVE_QUERY =
"select id, first_name, last_name, birth_date, version from singer";
private static Logger logger =
LoggerFactory.getLogger(SingerServiceImpl.class);
@PersistenceContext
private EntityManager em;
@Transactional(readOnly=true)
@Override
public Singer findById(Long id) {
TypedQuery<Singer> query = em.createNamedQuery
(Singer.FIND_SINGER_BY_ID, Singer.class);
query.setParameter("id", id);
return query.getSingleResult();
} ...
}
使用非类型化结果进行查询
在许多情况下,您希望向数据库提交一个查询并随意操作结果,而不是将它们存储在映射的实体类中。一个典型的例子是一个基于 web 的报表,它只列出多个表中一定数量的列。例如,假设您有一个显示歌手信息和他最近发行的专辑名称的网页。摘要信息包含歌手的全名和他最近发行的专辑名称。没有专辑的歌手不会被列出。在这种情况下,我们可以用一个查询实现这个用例,然后手动操作ResultSet对象。
让我们创建一个名为SingerSummaryUntypeImpl的新类,并将方法命名为displayAllSingerSummary()。下面的代码片段显示了方法的典型实现:
package com.apress.prospring5.ch8.service;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.Iterator;
import java.util.List;
@Service("singerSummaryUntype")
@Repository
@Transactional
public class SingerSummaryUntypeImpl {
@PersistenceContext
private EntityManager em;
@Transactional(readOnly = true)
public void displayAllSingerSummary() {
List result = em.createQuery(
"select s.firstName, s.lastName, a.title from Singer s "
+ "left join s.albums a "
+ "where a.releaseDate=(select max(a2.releaseDate) "
+ "from Album a2 where a2.singer.id = s.id)")
.getResultList();
int count = 0;
for (Iterator i = result.iterator(); i.hasNext(); ) {
Object[] values = (Object[]) i.next();
System.out.println(++count + ": " + values[0] + ", "
+ values[1] + ", " + values[2]);
}
}
}
如前面的代码示例所示,我们使用EntityManager.createQuery()方法创建Query,传入 JPQL 语句,然后得到结果列表。
当我们在 JPQL 中显式指定要选择的列时,JPA 将返回一个迭代器,迭代器中的每一项都是一个对象数组。我们循环遍历迭代器,对于对象数组中的每个元素,都会显示值。每个对象数组对应于ResultSet对象中的一条记录。以下代码片段显示了测试程序:
package com.apress.prospring5.ch8;
import com.apress.prospring5.ch8.config.JpaConfig;
import com.apress.prospring5.ch8.service.SingerSummaryUntypeImpl;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
public class SingerSummaryJPATest {
private static Logger logger =
LoggerFactory.getLogger(SingerSummaryJPATest.class);
private GenericApplicationContext ctx;
private SingerSummaryUntypeImpl singerSummaryUntype;
@Before
public void setUp() {
ctx = new AnnotationConfigApplicationContext(JpaConfig.class);
singerSummaryUntype = ctx.getBean(SingerSummaryUntypeImpl.class);
assertNotNull(singerSummaryUntype);
}
@Test
public void testFindAllUntype() {
singerSummaryUntype.displayAllSingerSummary();
}
@After
public void tearDown() {
ctx.close();
}
}
运行测试程序会产生以下输出:
1: John, Mayer, The Search For Everything
2: Eric, Clapton, From The Cradle
在 JPA 中,有一个更优雅的解决方案,而不是摆弄查询返回的对象数组,这将在下一节讨论。
使用构造函数表达式查询自定义结果类型
在 JPA 中,当查询像上一节中那样的定制结果时,您可以指示 JPA 从每个记录中直接构造一个 POJO。这个 POJO 也称为视图,因为它包含来自多个表的数据。对于上一节中的示例,让我们创建一个名为SingerSummary的 POJO,它存储歌手摘要的查询结果。下面的代码片段显示了该类:
package com.apress.prospring5.ch8.view;
import java.io.Serializable;
public class SingerSummary implements Serializable {
private String firstName;
private String lastName;
private String latestAlbum;
public SingerSummary(String firstName, String lastName,
String latestAlbum) {
this.firstName = firstName;
this.lastName = lastName;
this.latestAlbum = latestAlbum;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public String getLatestAlbum() {
return latestAlbum;
}
public String toString() {
return "First name: " + firstName + ", Last Name: " + lastName
+ ", Most Recent Album: " + latestAlbum;
}
}
前面的SingerSummary类有每个 singer 摘要的属性,有一个接受所有属性的构造函数方法。有了SingerSummary类,我们可以修改findAll()方法,并在查询中使用构造函数表达式来指示 JPA 提供者将ResultSet映射到SingerSummary类。让我们首先为SingerSummary服务创建一个接口。以下代码片段显示了该界面:
package com.apress.prospring5.ch8.service;
import com.apress.prospring5.ch8.view.SingerSummary;
import java.util.List;
public interface SingerSummaryService {
List<SingerSummary> findAll();
}
在这里,您可以看到SingerSummaryImpl.findAll()方法的实现,使用了用于ResultSet映射的构造函数表达式:
package com.apress.prospring5.ch8.service;
import com.apress.prospring5.ch8.view.SingerSummary;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.List;
@Service("singerSummaryService")
@Repository
@Transactional
public class SingerSummaryServiceImpl implements SingerSummaryService {
@PersistenceContext
private EntityManager em;
@Transactional(readOnly = true)
@Override
public List<SingerSummary> findAll() {
List<SingerSummary> result = em.createQuery(
"select new com.apress.prospring5.ch8.view.SingerSummary("
+ "s.firstName, s.lastName, a.title) from Singer s "
+ "left join s.albums a "
+ "where a.releaseDate=(select max(a2.releaseDate):
+ "from Album a2 where a2.singer.id = s.id)",
SingerSummary.class).getResultList();
return result;
}
}
在 JPQL 语句中,指定了new关键字,以及 POJO 类的完全限定名,该类将存储结果并传入所选属性作为每个SingerSummary类的构造函数参数。最后,SingerSummary类被传入到createQuery()方法中以指示结果类型。以下代码片段显示了测试程序:
package com.apress.prospring5.ch8;
import com.apress.prospring5.ch8.config.JpaConfig;
import com.apress.prospring5.ch8.service.SingerSummaryService;
import com.apress.prospring5.ch8.view.SingerSummary;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
public class SingerSummaryJPATest {
private static Logger logger =
LoggerFactory.getLogger(SingerSummaryJPATest.class);
private GenericApplicationContext ctx;
private SingerSummaryService singerSummaryService;
@Before
public void setUp() {
ctx = new AnnotationConfigApplicationContext(JpaConfig.class);
singerSummaryService = ctx.getBean(SingerSummaryService.class);
assertNotNull(singerSummaryService);
}
@Test
public void testFindAll() {
List<SingerSummary> singers = singerSummaryService.findAll();
listSingerSummary(singers);
assertEquals(2, singers.size());
}
private static void listSingerSummary(List<SingerSummary> singers) {
logger.info(" ---- Listing singers summary:");
for (SingerSummary singer : singers) {
logger.info(singer.toString());
}
}
@After
public void tearDown() {
ctx.close();
}
}
再次执行testFindAll方法类产生列表中每个SingerSummary对象的输出,如下所示(其他输出被省略):
INFO ---- Listing singers summary:
INFO - First name: John, Last Name: Mayer, Most Recent Album: The Search For Everything
INFO - First name: Eric, Last Name: Clapton, Most Recent Album: From The Cradle
正如您所看到的,构造函数表达式对于将定制查询的结果映射到 POJOs 以供进一步的应用处理非常有用。
插入数据
使用 JPA 插入数据很简单。和 Hibernate 一样,JPA 也支持检索数据库生成的主键。下面的代码片段显示了save()方法:
package com.apress.prospring5.ch8.service;
...
@Service("jpaSingerService")
@Repository
@Transactional
public class SingerServiceImpl implements SingerService {
final static String ALL_SINGER_NATIVE_QUERY =
"select id, first_name, last_name, birth_date, version from singer";
private static Logger logger =
LoggerFactory.getLogger(SingerServiceImpl.class);
@PersistenceContext
private EntityManager em;
@Override
public Singer save(Singer singer) {
if (singer.getId() == null) {
logger.info("Inserting new singer");
em.persist(singer);
} else {
em.merge(singer);
logger.info("Updating existing singer");
}
logger.info("Singer saved with id: " + singer.getId());
return singer;
}
...
}
如此处所示,save()方法首先通过检查id值来检查对象是否是新的实体实例。如果id是null(即尚未赋值),则该对象是一个新的实体实例,将调用EntityManager.persist()方法。当调用persist()方法时,EntityManager持久化实体并使其成为当前持久化上下文中的托管实例。如果id值存在,那么我们正在执行更新,而EntityManager.merge()方法将被调用。当调用merge()方法时,EntityManager将实体的状态合并到当前的持久化上下文中。
下面的代码片段显示了插入新歌手记录的代码。这都是在测试方法中完成的,因为我们想要测试插入是否成功。
package com.apress.prospring5.ch8;
...
public class SingerJPATest {
private static Logger logger =
LoggerFactory.getLogger(SingerJPATest.class);
private GenericApplicationContext ctx;
private SingerService singerService;
@Before
public void setUp(){
ctx = new AnnotationConfigApplicationContext(JpaConfig.class);
singerService = ctx.getBean(SingerService.class);
assertNotNull(singerService);
}
@Test
public void testInsert(){
Singer singer = new Singer();
singer.setFirstName("BB");
singer.setLastName("King");
singer.setBirthDate(new Date(
(new GregorianCalendar(1940, 8, 16)).getTime().getTime()));
Album album = new Album();
album.setTitle("My Kind of Blues");
album.setReleaseDate(new java.sql.Date(
(new GregorianCalendar(1961, 7, 18)).getTime().getTime()));
singer.addAbum(album);
album = new Album();
album.setTitle("A Heart Full of Blues");
album.setReleaseDate(new java.sql.Date(
(new GregorianCalendar(1962, 3, 20)).getTime().getTime()));
singer.addAbum(album);
singerService.save(singer);
assertNotNull(singer.getId());
List<Singer> singers = singerService.findAllWithAlbum();
assertEquals(4, singers.size());
listSingersWithAlbum(singers);
}
...
@After
public void tearDown(){
ctx.close();
}
}
如此处所示,我们创建了一个新歌手,添加了两张专辑,并保存了对象。然后,我们再次列出所有的歌手,在我们测试了表中记录的正确数量之后。运行该程序会产生以下输出:
INFO - ---- Listing singers with instruments:
INFO - Singer - Id: 4, First name: BB, Last name: King, Birthday: 1940-09-16
INFO - Album - id: 5, Singer id: 4, Title: A Heart Full of Blues,
Release Date: 1962-04-20
INFO - Album - id: 4, Singer id: 4, Title: My Kind of Blues,
Release Date: 1961-08-18
INFO - Singer - Id: 1, First name: John, Last name: Mayer, Birthday: 1977-10-16
INFO - Album - id: 1, Singer id: 1, Title: The Search For Everything,
Release Date: 2017-01-20
INFO - Album - id: 2, Singer id: 1, Title: Battle Studies,
Release Date: 2009-11-17
INFO - Instrument: Piano
INFO - Instrument: Guitar
INFO - Singer - Id: 3, First name: John, Last name: Butler, Birthday: 1975-04-01
INFO - Singer - Id: 2, First name: Eric, Last name: Clapton, Birthday: 1945-03-30
INFO - Album - id: 3, Singer id: 2, Title: From The Cradle,
Release Date: 1994-09-13
INFO - Instrument: Guitar
从INFO日志记录中,可以看到新保存的歌手的id被正确填充。Hibernate 还会显示所有被发送到数据库的 SQL 语句。
更新数据
更新数据就像插入数据一样简单。我们来看一个例子。假设对于一个 ID 为 1 的歌手,我们希望更新其名字并删除一张专辑。为了测试更新操作,下面的代码片段显示了testUpdate()方法:
package com.apress.prospring5.ch8;
...
public class SingerJPATest {
private static Logger logger =
LoggerFactory.getLogger(SingerJPATest.class);
private GenericApplicationContext ctx;
private SingerService singerService;
@Before
public void setUp(){
ctx = new AnnotationConfigApplicationContext(JpaConfig.class);
singerService = ctx.getBean(SingerService.class);
assertNotNull(singerService);
}
@Test
public void testUpdate(){
Singer singer = singerService.findById(1L);
//making sure such singer exists assertNotNull(singer);
//making sure we got expected record assertEquals("Mayer", singer.getLastName());
//retrieve the album
Album album = singer.getAlbums().stream()
.filter(a -> a.getTitle().equals("Battle Studies")).findFirst().get();
singer.setFirstName("John Clayton");
singer.removeAlbum(album);
singerService.save(singer);
listSingersWithAlbum(singerService.findAllWithAlbum());
}
...
@After
public void tearDown(){
ctx.close();
}
}
我们首先检索 ID 为 1 的记录,然后更改名字。然后,我们遍历专辑对象,检索带有标题战研究的对象,并将其从歌手的albums属性中删除。最后,我们再次调用SingerService.save()方法。当您运行该程序时,您将看到以下输出(其他输出被省略):
---- Listing singers with instruments:
Singer - Id: 1, First name: John Clayton, Last name: Mayer, Birthday: 1977-10-16
Album - id: 1, Singer id: 1, Title: The Search For Everything,
Release Date: 2017-01-20
Instrument: Piano
Instrument: Guitar
Singer - Id: 2, First name: Eric, Last name: Clapton, Birthday: 1945-03-30
Album - id: 3, Singer id: 2, Title: From The Cradle,
Release Date: 1994-09-13
Instrument: Guitar
Singer - Id: 3, First name: John, Last name: Butler, Birthday: 1975-04-01
您将看到名字被更新,相册被移除。相册可以被删除,因为在一对多关联中定义了rphanRemoval=true属性,该属性指示 JPA provider (Hibernate)删除所有存在于数据库中但在持久化时在对象中不再找到的孤立记录。
@OneToMany(mappedBy = "singer", cascade=CascadeType.ALL, orphanRemoval=true)
删除数据
删除数据也一样简单。只需调用EntityManager.remove()方法并传入 singer 对象。以下代码片段显示了删除歌手的更新代码:
package com.apress.prospring5.ch8.service;
...
@Service("jpaSingerService")
@Repository
@Transactional
public class SingerServiceImpl implements SingerService {
final static String ALL_SINGER_NATIVE_QUERY =
"select id, first_name, last_name, birth_date, version from singer";
private static Logger logger =
LoggerFactory.getLogger(SingerServiceImpl.class);
@PersistenceContext
private EntityManager em;
@Override
public void delete(Singer singer) {
Singer mergedSinger = em.merge(singer);
em.remove(mergedSinger);
logger.info("Singer with id: " + singer.getId() + " deleted successfully");
}
...
}
首先调用EntityManager.merge()方法将实体的状态合并到当前的持久化上下文中。merge()方法返回托管实体实例。然后调用EntityManager.remove(),传入托管 singer 实体实例。remove 操作删除歌手记录及其所有相关信息,包括专辑和乐器,正如我们在映射中定义的cascade=CascadeType.ALL。为了测试删除操作,可以使用testDelete()方法,如下面的代码片段所示:
package com.apress.prospring5.ch8;
...
public class SingerJPATest {
private static Logger logger =
LoggerFactory.getLogger(SingerJPATest.class);
private GenericApplicationContext ctx;
private SingerService singerService;
@Before
public void setUp(){
ctx = new AnnotationConfigApplicationContext(JpaConfig.class);
singerService = ctx.getBean(SingerService.class);
assertNotNull(singerService);
}
@Test
public void testDelete(){
Singer singer = singerService.findById(2l);
//making sure such singer exists
assertNotNull(singer);
singerService.delete(singer);
listSingersWithAlbum(singerService.findAllWithAlbum());
}
...
@After
public void tearDown(){
ctx.close();
}
}
前面的清单检索 ID 为2的歌手,然后调用delete()方法删除歌手信息。运行该程序会产生以下输出:
---- Listing singers with instruments:
Singer - Id: 1, First name: John, Last name: Mayer, Birthday: 1977-10-16
Album - id: 1, Singer id: 1, Title: The Search For Everything,
Release Date: 2017-01-20
Album - id: 2, Singer id: 1, Title: Battle Studies,
Release Date: 2009-11-17
Instrument: Piano
Instrument: Guitar
Singer - Id: 3, First name: John, Last name: Butler, Birthday: 1975-04-01
可以看到 ID 为1的歌手被删除了。
使用本机查询
讨论了使用 JPA 执行简单的数据库操作之后,现在让我们继续讨论一些更高级的主题。有时,您可能希望对提交给数据库的查询拥有绝对控制权。一个例子是在 Oracle 数据库中使用分层查询。这种查询是特定于数据库的,称为本机查询。
JPA 支持本地查询的执行;EntityManager将按原样向数据库提交查询,不执行任何映射或转换。使用 JPA 原生查询的一个主要好处是将ResultSet映射回 ORM 映射的实体类。接下来的两节讨论了如何使用本地查询来检索所有歌手,并将ResultSet直接映射回Singer对象。
使用简单的本地查询
为了演示如何使用原生查询,让我们实现一个新方法来从数据库中检索所有歌手。下面的代码片段显示了必须添加到SingerServiceImpl中的新方法:
package com.apress.prospring5.ch8.service;
...
@Service("jpaSingerService")
@Repository
@Transactional
public class SingerServiceImpl implements SingerService {
final static String ALL_SINGER_NATIVE_QUERY =
"select id, first_name, last_name, birth_date, version from singer";
private static Logger logger =
LoggerFactory.getLogger(SingerServiceImpl.class);
@Transactional(readOnly=true)
@Override
public List<Singer> findAllByNativeQuery() {
return em.createNativeQuery(ALL_SINGER_NATIVE_QUERY,
Singer.class).getResultList();
}
...
}
您可以看到,本地查询只是一个简单的 SQL 语句,用于从SINGER表中检索所有列。为了创建和执行查询,首先调用了EntityManager.createNativeQuery(),传递了查询字符串和结果类型。结果类型应该是一个映射的实体类(在本例中是Singer类)。createNativeQuery()方法返回一个Query接口,该接口提供getResultList()操作来获取结果列表。JPA 提供者将执行查询,并根据实体类中定义的 JPA 映射,将ResultSet对象转换成实体实例。执行前面的方法会产生与findAll()方法相同的结果。
使用 SQL 结果集映射的本机查询
除了映射的域对象之外,还可以传入一个字符串,该字符串表示 SQL ResultSet映射的名称。通过使用@SqlResultSetMapping注释,在实体类级别定义了 SQL ResultSet映射。一个 SQL ResultSet映射可以有一个或多个实体和列映射。
package com.apress.prospring5.ch8.entities;
import javax.persistence.Entity;
import javax.persistence.Table;
import javax.persistence.SqlResultSetMapping;
import javax.persistence.EntityResult;
...
@Entity
@Table(name = "singer")
@SqlResultSetMapping(
name="singerResult",
entities=@EntityResult(entityClass=Singer.class)
)
public class Singer implements Serializable {
...
}
为实体类定义了一个名为singerResult的 SQL ResultSet映射,在Singer类本身中有entityClass属性。JPA 支持多个实体的更复杂的映射,并支持向下映射到列级映射。
在定义了 SQL ResultSet映射之后,可以使用ResultSet映射的名称来调用findAllByNativeQuery()方法。下面的代码片段显示了更新后的findAllByNativeQuery()方法:
package com.apress.prospring5.ch8.service;
...
@Service("jpaSingerService")
@Repository
@Transactional
public class SingerServiceImpl implements SingerService {
final static String ALL_SINGER_NATIVE_QUERY =
"select id, first_name, last_name, birth_date, version from singer";
private static Logger logger =
LoggerFactory.getLogger(SingerServiceImpl.class);
@Transactional(readOnly=true)
@Override
public List<Singer> findAllByNativeQuery() {
return em.createNativeQuery(ALL_SINGER_NATIVE_QUERY,
"singerResult").getResultList();
}
...
}
如您所见,JPA 还为执行原生查询提供了强大的支持,提供了灵活的 SQL ResultSet映射工具。
使用 JPA 2 标准 API 进行标准查询
大多数应用都为用户提供了搜索信息的前端。最有可能的情况是,显示了大量可搜索的字段,用户只在其中的一些字段中输入信息并进行搜索。很难准备大量的查询,包括用户可能选择输入的每个可能的参数组合。在这种情况下,criteria API 查询特性可以帮上忙。
在 JPA 2 中,引入的一个主要新特性是强类型标准 API 查询。在这个新的 Criteria API 中,传递到查询中的标准基于映射的实体类的元模型。因此,指定的每个标准都是强类型的,错误将在编译时发现,而不是在运行时发现。
在 JPA Criteria API 中,实体类元模型由带有下划线后缀(_)的实体类名表示。例如,Singer实体类的元模型类是Singer_。下面的代码片段显示了Singer_类:
package com.apress.prospring5.ch8;
import java.util.Date;
import javax.annotation.Generated;
import javax.persistence.metamodel.SetAttribute;
import javax.persistence.metamodel.SingularAttribute;
import javax.persistence.metamodel.StaticMetamodel;
@Generated(value = "org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor")
@StaticMetamodel(Singer.class)
public abstract class Singer_ {
public static volatile SingularAttribute<Singer, String> firstName;
public static volatile SingularAttribute<Singer, String> lastName;
public static volatile SetAttribute<Singer, Album> albums;
public static volatile SetAttribute<Singer, Instrument> instruments;
public static volatile SingularAttribute<Singer, Long> id;
public static volatile SingularAttribute<Singer, Integer> version;
public static volatile SingularAttribute<Singer, Date> birthDate;
}
元模型类用@StaticMetamodel标注,属性是映射的实体类。在类中是每个属性及其相关类型的声明。
编码和维护那些元模型类将是乏味的。然而,工具可以帮助基于实体类中的 JPA 映射自动生成那些元模型类。Hibernate 提供的那个叫做 Hibernate 元模型生成器( www.hibernate.org/subprojects/jpamodelgen.html )。
您生成元模型类的方式取决于您使用什么工具来开发和构建您的项目。我们建议阅读文档的“用法”部分( http://docs.jboss.org/hibernate/jpamodelgen/1.3/reference/en-US/html_single/#chapter-usage )了解具体细节。作为本书一部分的示例代码使用 Gradle 来生成元类。元模型类生成所需的依赖项是hibernate-jpamodelgen库。这个依赖项是用它在pro-spring-15/build.gradle文件中的版本配置的。
ext {
...
//persistency libraries
hibernateVersion = '5.2.10.Final'
hibernateJpaVersion = '1.0.0.Final'
hibernate = [
...
jpaModelGen: "org.hibernate:hibernate-jpamodelgen:$hibernateVersion",
jpaApi : "org.hibernate.javax.persistence:hibernate-jpa-2.1-api:
$hibernateJpaVersion",
querydslapt: "com.mysema.querydsl:querydsl-apt:2.7.1"
]
...
这是生成元模型类的主要库。在编译模块之前,generateQueryDSL Gradle 任务在chapter08/jpa-criteria/build.gradle中使用它来生成元模型类。这里显示了chapter08/jpa-criteria/build.gradle的配置:
sourceSets {
generated
}
sourceSets.generated.java.srcDirs = ['src/main/generated']
configurations {
querydslapt
}
dependencies {
compile hibernate.querydslapt, hibernate.jpaModelGen
}
task generateQueryDSL(type: JavaCompile, group: 'build',
description: 'Generates the QueryDSL query types') {
source = sourceSets.main.java
classpath = configurations.compile + configurations.querydslapt
options.compilerArgs = [
"-proc:only",
"-processor", "org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor"
]
destinationDir = sourceSets.generated.java.srcDirs.iterator.next
}
compileJava.dependsOn generateQueryDSL
设置好类生成策略后,让我们定义一个查询,该查询接受名字和姓氏来搜索歌手。下面的代码片段显示了在SingerService接口中新方法findByCriteriaQuery()的定义:
package com.apress.prospring5.ch8;
import java.util.List;
public interface SingerService {
List<Singer> findAll();
List<Singer> findAllWithAlbum();
Singer findById(Long id);
Singer save(Singer singer);
void delete(Singer singer);
List<Singer> findAllByNativeQuery();
List<Singer> findByCriteriaQuery(String firstName, String lastName);
}
下一个代码片段显示了使用 JPA 2 criteria API 查询的findByCriteriaQuery()方法的实现:
package com.apress.prospring5.ch8;
import org.springframework.stereotype.Service;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import javax.persistence.PersistenceContext;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;
import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.Predicate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@Service("jpaSingerService")
@Repository
@Transactional
public class SingerServiceImpl implements SingerService {
final static String ALL_SINGER_NATIVE_QUERY =
"select id, first_name, last_name, birth_date, version from singer";
private Log log =
LogFactory.getLog(SingerServiceImpl.class);
@PersistenceContext
private EntityManager em;
...
@Transactional(readOnly=true)
@Override
public List<Singer> findByCriteriaQuery(String firstName, String lastName) {
log.info("Finding singer for firstName: " + firstName
+ " and lastName: " + lastName);
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Singer> criteriaQuery = cb.createQuery(Singer.class);
Root<Singer> singerRoot = criteriaQuery.from(Singer.class);
singerRoot.fetch(Singer_.albums, JoinType.LEFT);
singerRoot.fetch(Singer_.instruments, JoinType.LEFT);
criteriaQuery.select(singerRoot).distinct(true);
Predicate criteria = cb.conjunction();
if (firstName != null) {
Predicate p = cb.equal(singerRoot.get(Singer_.firstName),
firstName);
criteria = cb.and(criteria, p);
}
if (lastName != null) {
Predicate p = cb.equal(singerRoot.get(Singer_.lastName),
lastName);
criteria = cb.and(criteria, p);
}
criteriaQuery.where(criteria);
return em.createQuery(criteriaQuery).getResultList();
}
}
让我们来分解 API 使用的标准。
- 调用
EntityManager.getCriteriaBuilder()来检索CriteriaBuilder的实例。 - 使用
CriteriaBuilder.createQuery()创建类型化查询,将Singer作为结果类型传入。 - 调用
CriteriaQuery.from()方法,传入实体类。结果是一个对应于指定实体的查询根对象(Root<Singer>接口)。查询根对象构成了查询中路径表达式的基础。 - 两个
Root.fetch()方法调用强制执行与专辑和乐器相关的关联的快速获取。JoinType.LEFT参数指定了一个外部连接。用JoinType.LEFT作为第二个参数调用Root.fetch()方法相当于在 JPQL 中指定 left join fetch join 操作。 - 调用
CriteriaQuery.select()方法,并将根查询对象作为结果类型传递。带有 true 的distinct()方法意味着应该消除重复的记录。 - 通过调用
CriteriaBuilder.conjunction()方法获得一个Predicate实例,这意味着一个或多个限制的合取。一个Predicate可以是一个简单的或者复合的谓词,一个谓词是一个限制,表示由一个表达式定义的选择标准。 - 检查名字和姓氏参数。对于每个非
null参数,将使用CriteriaBuilder()方法(即CriteriaBuilder.and()方法)构造一个新的Predicate。方法equal()是指定一个相等的限制,在这个限制中Root.get()被调用,传递限制所应用的实体类元模型的相应属性。然后,通过调用CriteriaBuilder.and()方法,构造的谓词与现有的谓词(由变量 criteria 存储)进行“合取”。 - 通过调用
CriteriaQuery.where()方法,Predicate由所有标准和限制构成,并作为where子句传递给查询。 - 最后,
CriteriaQuery被传递给EntityManager. EntityManager,然后基于传入的CriteriaQuery值构造查询,执行查询,并返回结果。
为了测试条件查询操作,下面的代码片段显示了更新后的SingerJPATest类:
package com.apress.prospring5.ch8;
import com.apress.prospring5.ch8.config.JpaConfig;
import com.apress.prospring5.ch8.Album;
import com.apress.prospring5.ch8.Instrument;
import com.apress.prospring5.ch8.Singer;
import com.apress.prospring5.ch8.SingerService;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
public class SingerJPATest {
private static Logger logger =
LoggerFactory.getLogger(SingerJPATest.class);
private GenericApplicationContext ctx;
private SingerService singerService;
@Before
public void setUp(){
ctx = new AnnotationConfigApplicationContext(JpaConfig.class);
singerService = ctx.getBean("jpaSingerService", SingerService.class);
assertNotNull(singerService);
}
@Test
public void tesFindByCriteriaQuery(){
List<Singer> singers = singerService.findByCriteriaQuery("John", "Mayer");
assertEquals(1, singers.size());
listSingersWithAlbum(singers);
}
private static void listSingersWithAlbum(List<Singer> singers) {
logger.info(" ---- Listing singers with instruments:");
singers.forEach(s -> {
logger.info(s.toString());
if (s.getAlbums() != null) {
s.getAlbums().forEach(a -> logger.info("\t" + a.toString()));
}
if (s.getInstruments() != null) {
s.getInstruments().forEach(i -> logger.info
("\tInstrument: " + i.getInstrumentId()));
}
});
}
@After
public void tearDown(){
ctx.close();
}
}
运行该程序会产生以下输出(省略了其他输出,但保留了生成的查询):
INFO o.h.h.i.QueryTranslatorFactoryInitiator -
HHH000397: Using ASTQueryTranslatorFactory
INFO c.a.p.c.SingerServiceImpl -
Finding singer for firstName: John and lastName: Mayer
Hibernate:
select
distinct singer0_.ID as ID1_2_0_,
albums1_.ID as ID1_0_1_,
instrument3_.INSTRUMENT_ID as INSTRUME1_1_2_,
singer0_.BIRTH_DATE as BIRTH_DA2_2_0_,
singer0_.FIRST_NAME as FIRST_NA3_2_0_,
singer0_.LAST_NAME as LAST_NAM4_2_0_,
singer0_.VERSION as VERSION5_2_0_,
albums1_.RELEASE_DATE as RELEASE_2_0_1_,
albums1_.SINGER_ID as SINGER_I5_0_1_,
albums1_.title as title3_0_1_,
albums1_.VERSION as VERSION4_0_1_,
albums1_.SINGER_ID as SINGER_I5_0_0__,
albums1_.ID as ID1_0_0__,
instrument2_.SINGER_ID as SINGER_I1_3_1__,
instrument2_.INSTRUMENT_ID as INSTRUME2_3_1__
from
singer singer0_
left outer join
album albums1_
on singer0_.ID=albums1_.SINGER_ID
left outer join
singer_instrument instrument2_
on singer0_.ID=instrument2_.SINGER_ID
left outer join
instrument instrument3_
on instrument2_.INSTRUMENT_ID=instrument3_.INSTRUMENT_ID
where
1=1
and singer0_.FIRST_NAME=?
and singer0_.LAST_NAME=?
INFO c.a.p.c.SingerJPATest - ---- Listing singers with instruments:
INFO c.a.p.c.SingerJPATest - Singer - Id: 1, First name: John, Last name: Mayer,
Birthday: 1977-10-16
INFO c.a.p.c.SingerJPATest - Album - id: 2, Singer id: 1,
Title: Battle Studies, Release Date: 2009-11-17
INFO c.a.p.c.SingerJPATest - Album - id: 1, Singer id: 1,
Title: The Search For Everything, Release Date: 2017-01-20
INFO c.a.p.c.SingerJPATest - Instrument: Guitar
INFO c.a.p.c.SingerJPATest - Instrument: Piano
您可以尝试不同的组合,或者向其中一个参数传递一个null值来观察输出。
Spring Data JPA 简介
Spring Data JPA 项目是 Spring Data 保护伞项目下的一个子项目。该项目的主要目标是提供额外的特性来简化 JPA 的应用开发。
Spring Data JPA 提供了几个主要特性。在本节中,我们讨论两个。第一个是Repository抽象,另一个是实体监听器,用于跟踪实体类的基本审计信息。
添加 Spring Data JPA 库依赖项
要使用 Spring Data JPA,我们需要向项目添加依赖项。在这里,您可以看到使用 Spring Data JPA 所需的 Gradle 配置:
//pro-spring-15/build.gradle
ext {
//spring libs
springVersion = '5.0.0.M5'
bootVersion = '2.0.0.BUILD-SNAPSHOT'
springDataVersion = '2.0.0.M2'
...
spring = [
data : "org.springframework.data:spring-data-jpa:$springDataVersion",
...
]
...
}
//chapter08/spring-data-jpa/build.gradle
dependencies {
compile spring.aop, spring.data, misc.guava
}
使用 Spring Data JPA 存储库抽象进行数据库操作
Spring Data 及其所有子项目的一个主要概念是Repository抽象,它属于 Spring Data Commons 项目( https://github.com/spring-projects/spring-data-commons )。在撰写本文时,它的版本是 2.0.0 M2。在 Spring Data JPA 中,存储库抽象包装了底层 JPA EntityManager,并为基于 JPA 的数据访问提供了一个更简单的接口。Spring Data 中的中心接口是org.springframework.data.repository.Repository<T,ID extends Serializable>接口,这是一个属于 Spring Data Commons 发行版的标记接口。Spring Data 提供了Repository接口的各种扩展;其中之一是org.springframework.data.repository.CrudRepository接口(也属于 Spring Data 共享项目),我们将在本节中讨论。
CrudRepository接口提供了许多常用的方法。下面的代码片段显示了接口声明,它是从 Spring Data Commons 项目源代码中提取的:
package org.springframework.data.repository;
import java.io.Serializable;
@NoRepositoryBean
public interface CrudRepository<T, ID extends Serializable>
extends Repository<T, ID> {
long count();
void delete(ID id);
void delete(Iterable<? extends T> entities);
void delete(T entity);
void deleteAll();
boolean exists(ID id);
Iterable<T> findAll();
T findOne(ID id);
Iterable<T> save(Iterable<? extends T> entities);
T save(T entity);
}
尽管方法命名是不言自明的,但最好通过一个简单的例子来展示Repository抽象是如何工作的。让我们稍微修改一下SingerService接口,只剩下三个 finder 方法。下面的代码片段显示了修改后的SingerService界面:
package com.apress.prospring5.ch8;
import com.apress.prospring5.ch8.entities.Singer;
import java.util.List;
public interface SingerService {
List<Singer> findAll();
List<Singer> findByFirstName(String firstName);
List<Singer> findByFirstNameAndLastName(String firstName, String lastName);
}
下一步是准备SingerRepository接口,它扩展了CrudRepository接口。下面的代码片段显示了SingerRepository界面:
package com.apress.prospring5.ch8;
import java.util.List;
import com.apress.prospring5.ch8.entities.Singer;
import org.springframework.data.repository.CrudRepository;
public interface SingerRepository extends CrudRepository<Singer, Long> {
List<Singer> findByFirstName(String firstName);
List<Singer> findByFirstNameAndLastName(String firstName, String lastName);
}
我们只需要在这个接口中声明两个方法,因为CrudRepository.findAll()方法已经提供了findAll()方法。如前面的清单所示,SingerRepository接口扩展了CrudRepository接口,传入了实体类(Singer)和 ID 类型(Long)。Spring Data 的Repository抽象的一个有趣的方面是,当您使用findByFirstName和findByFirstNameAndLastName的通用命名约定时,您不需要为 Spring Data JPA 提供命名查询。相反,Spring Data JPA 将根据方法名为您“推断”和构造查询。例如,对于findByFirstName()方法,Spring Data JPA 会自动为您准备查询select s from Singer s where s.firstName = :firstName,并根据实参设置命名参数firstName。
要使用Repository抽象,您必须在 Spring 的配置中定义它。下面的代码片段显示了配置文件(app-context-annotation.xml):
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:jpa="http://www.springframework.org/schema/data/jpa"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/jdbc
http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/data/jpa
http://www.springframework.org/schema/data/jpa/spring-jpa.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<jdbc:embedded-database id="dataSource" type="H2">
<jdbc:script location="classpath:db/schema.sql"/>
<jdbc:script location="classpath:db/test-data.sql"/>
</jdbc:embedded-database>
<bean id="transactionManager"
class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="emf"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager" />
<bean id="emf"
class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
</property>
<property name="packagesToScan"
value="com.apress.prospring5.ch8.entities"/>
<property name="jpaProperties">
<props>
<prop key="hibernate.dialect">
org.hibernate.dialect.H2Dialect
</prop>
<prop key="hibernate.max_fetch_depth">3</prop>
<prop key="hibernate.jdbc.fetch_size">50</prop>
<prop key="hibernate.jdbc.batch_size">10</prop>
<prop key="hibernate.show_sql">true</prop>
</props>
</property>
</bean>
<context:annotation-config/>
<context:component-scan base-package="com.apress.prospring5.ch8" >
<context:exclude-filter type="annotation"
expression="org.springframework.context.annotation.Configuration" />
</context:component-scan>
<jpa:repositories base-package="com.apress.prospring5.ch8"
entity-manager-factory-ref="emf"
transaction-manager-ref="transactionManager"/>
</beans>
首先,我们需要在配置文件中添加jpa名称空间。然后,<jpa:repositories>标签用于配置 Spring Data JPA 的Repository抽象。我们指示 Spring 扫描包com.apress.prospring5.ch8中的存储库接口,并分别传入EntityManagerFactory和事务管理器。
如果您还没有注意到的话,在<context:component-scan>定义中有一个<context:exclude-filter>指定了用@Configuration注释的类。引入该元素是为了排除对 Java 配置类的扫描,Java 配置类可以用来代替前面描述的 XML 配置。该类如下所示:
package com.apress.prospring5.ch8.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.util.Properties;
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = {"com.apress.prospring5.ch8"})
@EnableJpaRepositories(basePackages = {"com.apress.prospring5.ch8"})
public class DataJpaConfig {
private static Logger logger = LoggerFactory.getLogger(DataJpaConfig.class);
@Bean
public DataSource dataSource() {
try {
EmbeddedDatabaseBuilder dbBuilder = new EmbeddedDatabaseBuilder();
return dbBuilder.setType(EmbeddedDatabaseType.H2)
.addScripts("classpath:db/schema.sql",
"classpath:db/test-data.sql").build();
} catch (Exception e) {
logger.error("Embedded DataSource bean cannot be created!", e);
return null;
}
}
@Bean
public PlatformTransactionManager transactionManager() {
return new JpaTransactionManager(entityManagerFactory());
}
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
return new HibernateJpaVendorAdapter();
}
@Bean
public Properties hibernateProperties() {
Properties hibernateProp = new Properties();
hibernateProp.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
hibernateProp.put("hibernate.format_sql", true);
hibernateProp.put("hibernate.use_sql_comments", true);
hibernateProp.put("hibernate.show_sql", true);
hibernateProp.put("hibernate.max_fetch_depth", 3);
hibernateProp.put("hibernate.jdbc.batch_size", 10);
hibernateProp.put("hibernate.jdbc.fetch_size", 50);
return hibernateProp;
}
@Bean
public EntityManagerFactory entityManagerFactory() {
LocalContainerEntityManagerFactoryBean factoryBean =
new LocalContainerEntityManagerFactoryBean();
factoryBean.setPackagesToScan("com.apress.prospring5.ch8.entities");
factoryBean.setDataSource(dataSource());
factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
factoryBean.setJpaProperties(hibernateProperties());
factoryBean.setJpaVendorAdapter(jpaVendorAdapter());
factoryBean.afterPropertiesSet();
return factoryBean.getNativeEntityManagerFactory();
}
}
这里用来支持 Spring Data JPA 存储库的唯一配置元素是@ EnableJpaRepositories注释。使用basePackages属性,扫描定制的Repository扩展并创建存储库 beans 的包。其余的依赖项(emf和transactionManagerbean)由 Spring 容器自动注入。
下面的代码片段展示了SingerService接口的三个 finder 方法的实现:
package com.apress.prospring5.ch8;
import com.apress.prospring5.ch8.entities.Singer;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
import com.google.common.collect.Lists;
@Service("springJpaSingerService")
@Transactional
public class SingerServiceImpl implements SingerService {
@Autowired
private SingerRepository singerRepository;
@Transactional(readOnly=true)
public List<Singer> findAll() {
return Lists.newArrayList(singerRepository.findAll());
}
@Transactional(readOnly=true)
public List<Singer> findByFirstName(String firstName) {
return singerRepository.findByFirstName(firstName);
}
@Transactional(readOnly=true)
public List<Singer> findByFirstNameAndLastName(
String firstName, String lastName) {
return singerRepository.findByFirstNameAndLastName(
firstName, lastName);
}
}
你可以看到,我们只需要将 Spring 基于SingerRepository接口生成的singerRepository bean 注入到服务类中,而不是EntityManager,Spring Data JPA 将为我们完成所有底层工作。在下面的代码片段中,您可以看到一个测试类,现在您应该已经熟悉了它的内容:
package com.apress.prospring5.ch8;
import com.apress.prospring5.ch8.config.DataJpaConfig;
import com.apress.prospring5.ch8.entities.Singer;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class SingerDataJPATest {
private static Logger logger =
LoggerFactory.getLogger(SingerDataJPATest.class);
private GenericApplicationContext ctx;
private SingerService singerService;
@Before
public void setUp(){
ctx = new AnnotationConfigApplicationContext(DataJpaConfig.class);
singerService = ctx.getBean(SingerService.class);
assertNotNull(singerService);
}
@Test
public void testFindAll(){
List<Singer> singers = singerService.findAll();
assertTrue(singers.size() > 0);
listSingers(singers);
}
@Test
public void testFindByFirstName(){
List<Singer> singers = singerService.findByFirstName("John");
assertTrue(singers.size() > 0);
assertEquals(2, singers.size());
listSingers(singers);
}
@Test
public void testFindByFirstNameAndLastName(){
List<Singer> singers =
singerService.findByFirstNameAndLastName("John", "Mayer");
assertTrue(singers.size() > 0);
assertEquals(1, singers.size());
listSingers(singers);
}
private static void listSingers(List<Singer> singers) {
logger.info(" ---- Listing singers:");
for (Singer singer : singers) {
logger.info(singer.toString());
}
}
@After
public void tearDown() {
ctx.close();
}
}
运行测试类,所有的测试都应该通过,预期的数据将被打印在控制台中。
使用 JpaRepository
除了CrudRepository之外,还有一个更高级的 Spring 接口,可以更容易地创建定制库;它被称为JpaRepository接口,提供批处理、分页和排序操作。图 8-1 显示了JpaRepository与CrudRepository接口的关系。根据应用的复杂程度,可以选择使用CrudRepository或JpaRepository。从图 8-1 中可以看出JpaRepository延伸CrudRepository;因此,它提供了这个接口的所有功能。

图 8-1。
Spring Data JPA Repository interfaces hierarchy
带有自定义查询的 Spring Data JPA
在复杂的应用中,您可能需要 Spring 无法“推断”的定制查询。在这种情况下,必须使用@Query注释显式定义查询。让我们使用这个注释来搜索标题中包含的所有音乐专辑。下面的代码片段描述了AlbumRepository接口:
package com.apress.prospring5.ch8.repos;
import com.apress.prospring5.ch8.entities.Album;
import com.apress.prospring5.ch8.entities.Singer;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface AlbumRepository extends JpaRepository<Album, Long> {
List<Album> findBySinger(Singer singer);
@Query("select a from Album a where a.title like %:title%")
List<Album> findByTitle(@Param("title") String t);
}
前面的查询有一个名为title的命名参数。当命名参数的名称与用@Query标注的方法中的参数名称相同时,不需要@Param标注。但是如果方法参数有不同的名称,就需要使用@Param注释来告诉 Spring 这个参数的值将被注入到查询中的命名参数中。
AlbumServiceImpl非常简单,只使用了albumRepository bean 来调用它的方法。
//AlbumService.java
package com.apress.prospring5.ch8.services;
import com.apress.prospring5.ch8.entities.Album;
import com.apress.prospring5.ch8.entities.Singer;
import java.util.List;
public interface AlbumService {
List<Album> findBySinger(Singer singer);
List<Album> findByTitle(String title);
}
//AlbumServiceImpl.java
package com.apress.prospring5.ch8.services;
import com.apress.prospring5.ch8.entities.Album;
import com.apress.prospring5.ch8.entities.Singer;
import com.apress.prospring5.ch8.repos.AlbumRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service("springJpaAlbumService")
@Transactional
public class AlbumServiceImpl implements AlbumService {
@Autowired
private AlbumRepository albumRepository;
@Transactional(readOnly=true)
@Override public List<Album> findBySinger(Singer singer) {
return albumRepository.findBySinger(singer);
}
@Override public List<Album> findByTitle(String title) {
return albumRepository.findByTitle(title);
}
}
要测试findByTitle(..)方法,您可以使用下面的测试类:
package com.apress.prospring5.ch8;
import com.apress.prospring5.ch8.config.DataJpaConfig;
import com.apress.prospring5.ch8.entities.Album;
import com.apress.prospring5.ch8.services.AlbumService;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class SingerDataJPATest {
private static Logger logger =
LoggerFactory.getLogger(SingerDataJPATest.class);
private GenericApplicationContext ctx;
private AlbumService albumService;
@Before
public void setUp(){
ctx = new AnnotationConfigApplicationContext(DataJpaConfig.class);
albumService = ctx.getBean(AlbumService.class);
assertNotNull(albumService);
}
@Test
public void testFindByTitle(){
List<Album> albums = albumService.findByTitle("The");
assertTrue(albums.size() > 0);
assertEquals(2, albums.size());
albums.forEach(a -> logger.info(a.toString() + ", Singer: "
+ a.getSinger().getFirstName() + " "
+ a.getSinger().getLastName()));
}
@After
public void tearDown() {
ctx.close();
}
}
如果您运行前面的测试类,testFindByTitle通过,并且两个相册细节被打印在控制台中。
INFO c.a.p.c.SingerDataJPATest - Album - id: 1, Singer id: 1,
Title: The Search For Everything, Release Date: 2017-01-20, Singer: John Mayer
INFO c.a.p.c.SingerDataJPATest - Album - id: 3, Singer id: 2,
Title: From The Cradle, Release Date: 1994-09-13, Singer: Eric Clapton
跟踪实体类的变化
在大多数应用中,我们需要跟踪用户维护的业务数据的基本审计活动。审计信息通常包括创建数据的用户、数据的创建日期、数据的最后修改日期以及最后修改数据的用户。
Spring Data JPA 项目以 JPA 实体监听器的形式提供了这个功能,它可以帮助您自动跟踪审计信息。要使用该特性,在 Spring 4 之前,实体类需要实现Auditable<U, ID extends Serializable, T extends TemporalAccessor> extends Persistable<ID>接口(属于 Spring Data Commons)或扩展任何实现该接口的类。下面的代码片段显示了从 Spring Data 的参考文档中提取的Auditable接口:
package org.springframework.data.domain;
import java.io.Serializable;
import java.time.temporal.TemporalAccessor;
import java.util.Optional;
public interface Auditable<U, ID extends Serializable,
T extends TemporalAccessor> extends Persistable<ID> {
Optional<U> getCreatedBy();
void setCreatedBy(U createdBy);
Optional<T> getCreatedDate();
void setCreatedDate(T creationDate);
Optional<U> getLastModifiedBy();
void setLastModifiedBy(U lastModifiedBy);
Optional<T> getLastModifiedDate();
void setLastModifiedDate(T lastModifiedDate);
}
为了展示它是如何工作的,让我们在数据库模式中创建一个名为SINGER_AUDIT的新表,它基于SINGER表,添加了四个与审计相关的列。下面的代码片段显示了表创建脚本(schema.sql):
CREATE TABLE SINGER_AUDIT (
ID INT NOT NULL AUTO_INCREMENT
, FIRST_NAME VARCHAR(60) NOT NULL
, LAST_NAME VARCHAR(40) NOT NULL
, BIRTH_DATE DATE
, VERSION INT NOT NULL DEFAULT 0
, CREATED_BY VARCHAR(20)
, CREATED_DATE TIMESTAMP
, LAST_MODIFIED_BY VARCHAR(20)
, LAST_MODIFIED_DATE TIMESTAMP
, UNIQUE UQ_SINGER_AUDIT_1 (FIRST_NAME, LAST_NAME)
, PRIMARY KEY (ID)
);
从前面显示的Auditable接口的定义中可以看出,日期类型列被限制为扩展java.time.temporal.TemporalAccessor的类型。
从 Spring 5 开始,实现Auditable<U,ID extends Serializable>不再是必要的,因为一切都可以被注释代替。四个带下划线的列表示与审计相关的列。请注意@CreatedBy、@CreatedDate、@LastModifiedBy和@LastModifiedDate注释。使用这些批注,日期列的类型限制不再适用。下一步是创建名为SingerAudit的实体类。下面的代码片段显示了SingerAudit类:
package com.apress.prospring5.ch8.entities;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
import java.util.Optional;
import static javax.persistence.GenerationType.IDENTITY;
@Entity
@EntityListeners(AuditingEntityListener.class)
@Table(name = "singer_audit")
public class SingerAudit implements Serializable {
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "ID")
private Long id;
@Version
@Column(name = "VERSION")
private int version;
@Column(name = "FIRST_NAME")
private String firstName;
@Column(name = "LAST_NAME")
private String lastName;
@Temporal(TemporalType.DATE)
@Column(name = "BIRTH_DATE")
private Date birthDate;
@CreatedDate
@Column(name = "CREATED_DATE")
@Temporal(TemporalType.TIMESTAMP)
private Date createdDate;
@CreatedBy
@Column(name = "CREATED_BY")
private String createdBy;
@LastModifiedBy
@Column(name = "LAST_MODIFIED_BY")
private String lastModifiedBy;
@LastModifiedDate
@Column(name = "LAST_MODIFIED_DATE")
@Temporal(TemporalType.TIMESTAMP)
private Date lastModifiedDate;
public Long getId() {
return this.id;
}
public String getFirstName() {
return this.firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return this.lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public Date getBirthDate() {
return this.birthDate;
}
public void setBirthDate(Date birthDate) {
this.birthDate = birthDate;
}
public Optional<String> getCreatedBy() {
return Optional.of(createdBy);
}
public void setCreatedBy(String createdBy) {
this.createdBy = createdBy;
}
public Optional<Date> getCreatedDate() {
return Optional.of(createdDate);
}
public void setCreatedDate(Date createdDate) {
this.createdDate = createdDate;
}
public Optional<String> getLastModifiedBy() {
return Optional.of(lastModifiedBy);
}
public void setLastModifiedBy(String lastModifiedBy) {
this.lastModifiedBy = lastModifiedBy;
}
public Optional<Date> getLastModifiedDate() {
return Optional.of(lastModifiedDate);
}
public void setLastModifiedDate(Date lastModifiedDate) {
this.lastModifiedDate = lastModifiedDate;
}
public String toString() {
return "Singer - Id: " + id + ", First name: " + firstName
+ ", Last name: " + lastName + ", Birthday: " + birthDate
+ ", Created by: " + createdBy + ", Create date: " + createdDate
+ ", Modified by: " + lastModifiedBy + ", Modified date: "
+ lastModifiedDate;
}
}
@Column注释应用于审计字段,以映射到表中的实际列。@EntityListeners(AuditingEntityListener.class)注释注册了AuditingEntityListener,仅用于持久化上下文中的这个实体。在更复杂的例子中,当需要不止一个实体类时,审计功能被隔离到一个用@MappedSuperclass标注的抽象类中,这个抽象类也用@EntityListeners(AuditingEntityListener.class)标注。如果SingerAudit是这样的层次结构的一部分,它将必须扩展下面的类:
package com.apress.prospring5.ch8.entities;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
import java.util.Optional;
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class AuditableEntity<U> implements Serializable {
@CreatedDate
@Column(name = "CREATED_DATE")
@Temporal(TemporalType.TIMESTAMP)
protected Date createdDate;
@CreatedBy
@Column(name = "CREATED_BY")
protected String createdBy;
@LastModifiedBy
@Column(name = "LAST_MODIFIED_BY")
protected String lastModifiedBy;
@LastModifiedDate
@Column(name = "LAST_MODIFIED_DATE")
@Temporal(TemporalType.TIMESTAMP)
protected Date lastModifiedDate;
public Optional<String> getCreatedBy() {
return Optional.of(createdBy);
}
public void setCreatedBy(String createdBy) {
this.createdBy = createdBy;
}
public Optional<Date> getCreatedDate() {
return Optional.of(createdDate);
}
public void setCreatedDate(Date createdDate) {
this.createdDate = createdDate;
}
public Optional<String> getLastModifiedBy() {
return Optional.of(lastModifiedBy);
}
public void setLastModifiedBy(String lastModifiedBy) {
this.lastModifiedBy = lastModifiedBy;
}
public Optional<Date> getLastModifiedDate() {
return Optional.of(lastModifiedDate);
}
public void setLastModifiedDate(Date lastModifiedDate) {
this.lastModifiedDate = lastModifiedDate;
}
}
这将大大减小SingerAudit的大小,变成这样:
package com.apress.prospring5.ch8.entities;
import javax.persistence.*;
import java.util.Date;
import static javax.persistence.GenerationType.IDENTITY;
@Entity
@Table(name = "singer_audit")
public class SingerAudit extends AuditableEntity<SingerAudit> {
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "ID")
private Long id;
@Version
@Column(name = "VERSION")
private int version;
@Column(name = "FIRST_NAME")
private String firstName;
@Column(name = "LAST_NAME")
private String lastName;
@Temporal(TemporalType.DATE)
@Column(name = "BIRTH_DATE")
private Date birthDate;
public Long getId() {
return this.id;
}
public String getFirstName() {
return this.firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return this.lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public Date getBirthDate() {
return this.birthDate;
}
public void setBirthDate(Date birthDate) {
this.birthDate = birthDate;
}
public String toString() {
return "Singer - Id: " + id + ", First name: " + firstName
+ ", Last name: " + lastName + ", Birthday: " + birthDate
+ ", Created by: " + createdBy + ", Create date: " + createdDate
+ ", Modified by: " + lastModifiedBy + ", Modified date: "
+ lastModifiedDate;
}
}
下面的代码片段显示了SingerAuditService接口,在这里我们只定义了几个方法来演示审计特性:
package com.apress.prospring5.ch8.services;
import com.apress.prospring5.ch8.entities.SingerAudit;
import java.util.List;
public interface SingerAuditService {
List<SingerAudit> findAll();
SingerAudit findById(Long id);
SingerAudit save(SingerAudit singer);
}
SingerAuditRepository接口只是扩展了CrudRepository,它已经实现了我们将要为SingerAuditService使用的所有方法。findById()方法由CrudRepository.findOne()方法实现。下面的代码片段显示了服务实现类SingerAuditServiceImpl:
package com.apress.prospring5.ch8.services;
import com.apress.prospring5.ch8.entities.SingerAudit;
import com.apress.prospring5.ch8.repos.SingerAuditRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
import com.google.common.collect.Lists;
@Service("singerAuditService")
@Transactional
public class SingerAuditServiceImpl implements SingerAuditService {
@Autowired
private SingerAuditRepository singerAuditRepository;
@Transactional(readOnly=true)
public List<SingerAudit> findAll() {
return Lists.newArrayList(singerAuditRepository.findAll());
}
public SingerAudit findById(Long id) {
return singerAuditRepository.findOne(id).get();
}
public SingerAudit save(SingerAudit singer) {
return singerAuditRepository.save(singer);
}
}
我们还需要做一些配置工作。使用 XML 配置,需要将提供审计服务的AuditingEntityListener<T> JPA 实体监听器声明到项目根文件夹下的一个名为/src/main/resources/META-INF/orm.xml的文件中(必须使用这个文件名,这由 JPA 规范指示),并声明监听器,如下所示:
<?xml version="1.0" encoding="UTF-8" ?>
<entity-mappings
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm
http://java.sun.com/xml/ns/persistence/orm_2_0.xsd"
version="2.0">
<description>JPA</description>
<persistence-unit-metadata>
<persistence-unit-defaults>
<entity-listeners>
<entity-listener class="org.springframework.data.jpa.domain.support.AuditingEntityListener" />
</entity-listeners>
</persistence-unit-defaults>
</persistence-unit-metadata>
</entity-mappings>
使用注释配置,该文件被替换为@EntityListeners (AuditingEntityListener.class)注释。JPA 提供者将在审计字段处理的持久性操作(保存和更新事件)期间发现这个监听器。Spring 配置的其余部分几乎与您到目前为止看到的一样,只有一个例外:当然,是一个支持审计的新配置注释。
package com.apress.prospring5.ch8.com.apress.prospring5.ch8.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.util.Properties;
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = {"com.apress.prospring5.ch8"})
@EnableJpaRepositories(basePackages = {"com.apress.prospring5.ch8.repos"})
@EnableJpaAuditing(auditorAwareRef = "auditorAwareBean")
public class AuditConfig {
// same content as the configuration in the previous section
...
}
@EnableJpaAuditing(auditorAwareRef = "auditorAwareBean")声明相当于使用 XML 配置来启用 JPA 审计特性的<jpa:auditing auditor-aware-ref="auditorAwareBean"/>元素。auditorAwareBean bean 提供了用户信息。下面的代码片段显示了AuditorAwareBean类:
package com.apress.prospring5.ch8;
import org.springframework.data.domain.AuditorAware;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Component
public class AuditorAwareBean implements AuditorAware<String> {
public Optional<String> getCurrentAuditor() {
return Optional.of("prospring5");
}
}
AuditorAwareBean实现AuditorAware<T>接口,传入类型String。在实际情况下,这应该是用户信息的一个实例,例如一个User类,它表示正在执行数据更新操作的登录用户。为了简单起见,我们在这里使用String。在AuditorAwareBean类中,方法getCurrentAuditor()被实现,值被硬编码为prospring5。在实际情况下,应该从底层安全基础结构中获取用户。例如,在 Spring Security 中,可以从SecurityContextHolder类中检索用户信息。
现在所有的实现工作都完成了,下面的代码片段显示了SpringAuditJPADemo测试程序:
package com.apress.prospring5.ch8;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Date;
import com.apress.prospring5.ch8.config.AuditConfig;
import com.apress.prospring5.ch8.entities.SingerAudit;
import com.apress.prospring5.ch8.services.SingerAuditService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
public class SpringAuditJPADemo {
public static void main(String... args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(AuditConfig.class);
SingerAuditService singerAuditService = ctx.getBean(SingerAuditService.class);
List<SingerAudit> singers = singerAuditService.findAll();
listSingers(singers);
System.out.println("Add new singer");
SingerAudit singer = new SingerAudit();
singer.setFirstName("BB");
singer.setLastName("King");
singer.setBirthDate(new Date(
(new GregorianCalendar(1940, 8, 16)).getTime().getTime()));
singerAuditService.save(singer);
singers = singerAuditService.findAll();
listSingers(singers);
singer = singerAuditService.findById(4l);
System.out.println("");
System.out.println("Singer with id 4:" + singer);
System.out.println("");
System.out.println("Update singer");
singer.setFirstName("John Clayton");
singerAuditService.save(singer);
singers = singerAuditService.findAll();
listSingers(singers);
ctx.close();
}
private static void listSingers(List<SingerAudit> singerAudits) {
System.out.println("");
System.out.println("Listing singers without details:");
for (SingerAudit audit: singerAudits) {
System.out.println(audit);
System.out.println();
}
}
}
在main()方法中,我们列出了新歌手插入后和更新后的歌手审计信息。运行该程序会产生以下输出:
Add new singer
Listing singers without details:
// other singers ...
Singer - Id: 4, First name: BB, Last name: King, Birthday: 1940-09-16,
Created by: prospring5, Create date: 2017-05-07 14:19:02.96,
Modified by: prospring5, Modified date: 2017-05-07 14:19:02.96
Update singer
Listing singers without details:
// other singers ...
Singer - Id: 4, First name: Riley B., Last name: King, Birthday: 1940-09-16,
Created by: prospring5, Create date: 2017-05-07 14:33:15.645,
Modified by: prospring5, Modified date: 2017-05-07 14:33:15.663
在前面的输出中,您可以看到在新歌手创建之后,创建日期和最后修改日期是相同的。但是,在更新之后,会更新最后修改日期。审计是 Spring Data JPA 提供的另一个便利特性,因此您不需要自己实现逻辑。
通过使用 Hibernate 环境保持实体版本
在企业应用中,对于业务关键型数据,总是需要保留每个实体的版本。例如,在客户关系管理(CRM)系统中,每次插入、更新或删除客户记录时,以前的版本都应该保存在历史记录或审计表中,以满足公司的审计或其他合规性要求。
要实现这一点,有两种常见的选择。第一个是创建数据库触发器,它将在任何更新操作之前将更新前的记录克隆到历史表中。第二是开发数据访问层中的逻辑(例如,通过使用 AOP)。然而,这两种选择都有缺点。触发器方法依赖于数据库平台,而手动实现逻辑相当笨拙且容易出错。
Hibernate Envers(是“实体版本控制系统”的缩写)是一个 Hibernate 模块,专门用于自动控制实体的版本。在本节中,我们将讨论如何使用 Envers 来实现SingerAudit实体的版本控制。
Hibernate Envers 不是 JPA 的一个特性。我们之所以在这里提到它,是因为我们认为在我们讨论了一些可以与 Spring Data JPA 一起使用的基本审计特性之后,讨论这个问题更合适。
Envers 支持两种审计策略,如表 8-1 所示。
表 8-1。
Envers Auditing Strategies
| 审计策略 | 描述 | | --- | --- | | 默认 | Envers 保留了一个记录修订栏。每次插入或更新记录时,都会在历史记录表中插入一条新记录,并从数据库序列或表中检索修订号。 | | 有效性审计 | 该策略存储每个历史记录的开始和结束修订。每次插入或更新记录时,都会在历史记录表中插入一条新记录,其起始修订号为。同时,以前的记录将被更新为最终版本号。还可以配置 Envers 来记录时间戳,在该时间戳处,最终版本被更新到先前的历史记录中。 |在本节中,我们将演示有效性审计策略。尽管这将触发更多的数据库更新,但检索历史记录会变得更快。因为最终修订时间戳也被写入历史记录,所以在查询数据时更容易识别特定时间点的记录快照。
为实体版本控制添加表
为了支持实体版本控制,我们需要添加一些表。首先,对于实体(在本例中是SingerAudit实体类)将要进行版本控制的每个表,我们需要创建相应的历史表。为了对SINGER_AUDIT表中的记录进行版本控制,让我们创建一个名为SINGER_AUDIT_H的历史表。下面的代码片段显示了表创建脚本(schema.sql):
CREATE TABLE SINGER_AUDIT_H (
ID INT NOT NULL AUTO_INCREMENT
, FIRST_NAME VARCHAR(60) NOT NULL
, LAST_NAME VARCHAR(40) NOT NULL
, BIRTH_DATE DATE
, VERSION INT NOT NULL DEFAULT 0
, CREATED_BY VARCHAR(20)
, CREATED_DATE TIMESTAMP
, LAST_MODIFIED_BY VARCHAR(20)
, LAST_MODIFIED_DATE TIMESTAMP
, AUDIT_REVISION INT NOT NUL
, ACTION_TYPE INT
, AUDIT_REVISION_END INT
, AUDIT_REVISION_END_TS TIMESTAMP
, UNIQUE UQ_SINGER_AUDIT_H_1 (FIRST_NAME, LAST_NAME)
, PRIMARY KEY (ID, AUDIT_REVISION)
);
为了支持有效性审计策略,我们需要为每个历史表添加四列,在前面的脚本片段中显示为带下划线。表 8-2 显示了各列及其用途。Hibernate Envers 需要另一个表来跟踪修订号和每个修订创建时的时间戳。表名应该是REVINFO。
表 8-2。
Envers Auditing Strategies
| 审计策略 | 数据类型 | 描述 | | --- | --- | --- | | `AUDIT_REVISION` | `INT` | 历史记录的开始修订。 | | `ACTION_TYPE` | `INT` | 动作类型,可能值如下:0 表示添加,1 表示修改,2 表示删除 | | `AUDIT_REVISION_END` | `INT` | 历史记录的最终版本 | | `AUDIT_REVISION_END_TS` | `TIMESTAMP` | 更新结束修订的时间戳 |下面的代码片段显示了表创建脚本(schema.sql):
CREATE TABLE REVINFO (
REVTSTMP BIGINT NOT NULL
, REV INT NOT NULL AUTO_INCREMENT
, PRIMARY KEY (REVTSTMP, REV)
);
REV栏用于存储每个修订号,当创建新的历史记录时,修订号将自动增加。REVTSTMP列存储创建修订时的时间戳(以数字格式)。
为实体版本控制配置 EntityManagerFactory
Hibernate Envers 是以 EJB 监听器的形式实现的。我们可以在LocalContainerEntityManagerFactory bean 中配置这些监听器。这里你可以看到 Java 配置类。展示 XML 配置是没有意义的,因为这一节的唯一区别是我们有许多额外的特定于 Hibernate 的属性。
package com.apress.prospring5.ch8.config;
import org.hibernate.envers.boot.internal.EnversServiceImpl;
import org.hibernate.envers.event.spi.EnversPostUpdateEventListenerImpl;
import org.hibernate.event.spi.PostUpdateEventListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.util.Properties;
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = {"com.apress.prospring5.ch8"})
@EnableJpaRepositories(basePackages = {"com.apress.prospring5.ch8.repos"})
@EnableJpaAuditing(auditorAwareRef = "auditorAwareBean")
public class EnversConfig {
private static Logger logger = LoggerFactory.getLogger(EnversConfig.class);
@Bean
public DataSource dataSource() {
try {
EmbeddedDatabaseBuilder dbBuilder = new EmbeddedDatabaseBuilder();
return dbBuilder.setType(EmbeddedDatabaseType.H2)
.addScripts("classpath:db/schema.sql", "classpath:db/test-data.sql").build();
} catch (Exception e) {
logger.error("Embedded DataSource bean cannot be created!", e);
return null;
}
}
@Bean
public PlatformTransactionManager transactionManager() {
return new JpaTransactionManager(entityManagerFactory());
}
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
return new HibernateJpaVendorAdapter();
}
@Bean
public Properties hibernateProperties() {
Properties hibernateProp = new Properties();
hibernateProp.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
hibernateProp.put("hibernate.format_sql", true);
hibernateProp.put("hibernate.show_sql", true);
hibernateProp.put("hibernate.max_fetch_depth", 3);
hibernateProp.put("hibernate.jdbc.batch_size", 10);
hibernateProp.put("hibernate.jdbc.fetch_size", 50);
//Properties for Hibernate Envers
hibernateProp.put("org.hibernate.envers.audit_table_suffix", "_H");
hibernateProp.put("org.hibernate.envers.revision_field_name",
"AUDIT_REVISION");
hibernateProp.put("org.hibernate.envers.revision_type_field_name",
"ACTION_TYPE");
hibernateProp.put("org.hibernate.envers.audit_strategy",
"org.hibernate.envers.strategy.ValidityAuditStrategy");
hibernateProp.put(
"org.hibernate.envers.audit_strategy_validity_end_rev_field_name",
"AUDIT_REVISION_END");
hibernateProp.put(
"org.hibernate.envers.audit_strategy_validity_store_revend_timestamp",
"True");
hibernateProp.put(
"org.hibernate.envers.audit_strategy_validity_revend_timestamp_field_name",
"AUDIT_REVISION_END_TS");
return hibernateProp;
}
@Bean
public EntityManagerFactory entityManagerFactory() {
LocalContainerEntityManagerFactoryBean factoryBean =
new LocalContainerEntityManagerFactoryBean();
factoryBean.setPackagesToScan("com.apress.prospring5.ch8.entities");
factoryBean.setDataSource(dataSource());
factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
factoryBean.setJpaProperties(hibernateProperties());
factoryBean.setJpaVendorAdapter(jpaVendorAdapter());
factoryBean.afterPropertiesSet();
return factoryBean.getObject();
}
}
Envers 审计事件监听器org.hibernate.envers.event.AuditEventListener附加到各种持久性事件。侦听器拦截插入后、更新后或删除后事件,并将实体类的更新前快照克隆到历史表中。侦听器还附加到那些关联更新事件(预收集-更新、预收集-删除和预收集-重新创建),用于处理实体类关联的更新操作。Envers 能够保存关联中实体的历史记录(例如一对多或多对多)。还为 Hibernate Envers 定义了一些属性,这些属性汇总在表 8-3 中(为了清楚起见,省略了属性的前缀org.hibernate.envers)。22
表 8-3。
Hibernate Envers Properties Table
| 方法 | 描述 | | --- | --- | | `audit_table_suffix` | 版本化实体的表名后缀。例如,对于映射到表`SINGER_AUDIT`的实体类`SingerAudit`,Envers 将在表`SINGER_AUDIT_H`中保存历史,因为我们为属性定义了值`_H`。 | | `revision_field_name` | 历史表中用于存储每个历史记录的修订号的列。 | | `revision_type_field_name` | 用于存储更新操作类型的历史表列。 | | `audit_strategy` | 用于实体版本控制的审计策略。 | | `audit_strategy_validity_end_rev_field_name` | 历史表中用于存储每个历史记录的最终修订号的列。仅在使用有效性审计策略时需要。 | | `audit_strategy_validity_store_revend_timestamp` | 更新每个历史记录的结束修订号时是否存储时间戳。仅在使用有效性审计策略时需要。 | | `audit_strategy_validity_revend_timestamp_field_name` | 当每个历史记录的结束修订号被更新时,历史表中用于存储时间戳的列。仅在使用有效性审计策略且 previous 属性设置为`true`时需要。 |启用实体版本控制和历史检索
要启用实体的版本控制,只需用@Audited注释实体类。可以在类级别使用该注释,然后审计所有字段上的更改。如果您想逃避某些字段的审计,可以在这些字段上使用@NotAudited。这里您可以看到应用了注释的SingerAudit实体类的一个片段:
package com.apress.prospring5.ch8.entities;
import org.hibernate.envers.Audited;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
...
@Entity
@Table(name = "singer_audit")
@Audited
@EntityListeners(AuditingEntityListener.class)
public class SingerAudit implements Serializable {
...
}
entity 类用@ Audited注释,Envers 监听器将检查并执行更新实体的版本控制。默认情况下,Envers 还会尝试保存关联的历史记录;如果想避免这种情况,应该使用前面提到的@NotAudited注释。
为了检索历史记录,Envers 提供了org.hibernate.envers.AuditReader接口,该接口可以从AuditReaderFactory类中获得。让我们在SingerAuditService接口中添加一个名为findAuditByRevision()的新方法,用于通过修订号检索SingerAudit历史记录。下面的代码片段显示了SingerAuditService接口:
package com.apress.prospring5.ch8.services;
import com.apress.prospring5.ch8.entities.SingerAudit;
import java.util.List;
public interface SingerAuditService {
List<SingerAudit> findAll();
SingerAudit findById(Long id);
SingerAudit save(SingerAudit singerAudit);
SingerAudit findAuditByRevision(Long id, int revision);
}
要检索历史记录,一种方法是传入实体的 ID 和修订号。以下代码片段显示了提取修订的方法的实现:
package com.apress.prospring5.ch8.services;
import com.apress.prospring5.ch8.entities.SingerAudit;
import com.apress.prospring5.ch8.repos.SingerAuditRepository;
import com.google.common.collect.Lists;
import org.hibernate.envers.AuditReader;
import org.hibernate.envers.AuditReaderFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.List;
@Service("singerAuditService")
@Transactional
public class SingerAuditServiceImpl implements SingerAuditService {
@Autowired
private SingerAuditRepository singerAuditRepository;
@PersistenceContext
private EntityManager entityManager;
@Transactional(readOnly = true)
public List<SingerAudit> findAll() {
return Lists.newArrayList(singerAuditRepository.findAll());
}
public SingerAudit findById(Long id) {
return singerAuditRepository.findOne(id).get();
}
public SingerAudit save(SingerAudit singer) {
return singerAuditRepository.save(singer);
}
@Transactional(readOnly = true)
@Override
public SingerAudit findAuditByRevision(Long id, int revision) {
AuditReader auditReader = AuditReaderFactory.get(entityManager);
return auditReader.find(SingerAudit.class, id, revision);
}
}
EntityManager被注入到类中,该类被传递给AuditReaderFactory以检索AuditReader的一个实例。然后我们可以调用AuditReader.find()方法来检索特定版本的SingerAudit实体的实例。
测试实体版本
让我们看看实体版本控制是如何工作的。以下代码片段显示了测试代码片段;引导ApplicationContext和listSingers()函数的代码与SpringJpaDemo类中的代码相同。
package com.apress.prospring5.ch8;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Date;
import com.apress.prospring5.ch8.config.EnversConfig;
import com.apress.prospring5.ch8.entities.SingerAudit;
import com.apress.prospring5.ch8.services.SingerAuditService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;
public class SpringEnversJPADemo {
public static void main(String... args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicati onContext(EnversConfig.class);
SingerAuditService singerAuditService =
ctx.getBean(SingerAuditService.class);
System.out.println("Add new singer");
SingerAudit singer = new SingerAudit();
singer.setFirstName("BB");
singer.setLastName("King");
singer.setBirthDate(new Date(
(new GregorianCalendar(1940, 8, 16)).getTime().getTime()));
singerAuditService.save(singer);
listSingers(singerAuditService.findAll());
System.out.println("Update singer");
singer.setFirstName("Riley B.");
singerAuditService.save(singer);
listSingers(singerAuditService.findAll());
SingerAudit oldSinger = singerAuditService.findAuditByRevision(4l, 1);
System.out.println("");
System.out.println("Old Singer with id 1 and rev 1:" + oldSinger);
System.out.println("");
oldSinger = singerAuditService.findAuditByRevision(4l, 2);
System.out.println("");
System.out.println("Old Singer with id 1 and rev 2:" + oldSinger);
System.out.println("");
ctx.close();
}
private static void listSingers(List<SingerAudit> singers) {
System.out.println("");
System.out.println("Listing singers:");
for (SingerAudit singer: singers) {
System.out.println(singer);
System.out.println();
}
}
}
我们首先创建一个新的歌手,然后更新它。然后我们分别检索修订版为 1 和 2 的SingerAudit实体。运行代码会产生以下输出:
Listing singers:
...//
Singer - Id: 4, First name: BB, Last name: King, Birthday: 1940-09-16,
Created by: prospring5, Create date: 2017-05-11 23:50:14.778,
Modified by: prospring5, Modified date: 2017-05-11 23:50:14.778
Old Singer with id 1 and rev 1:Singer - Id: 4,
First name: BB, Last name: King, Birthday: 1940-09-16,
Created by: prospring5, Create date: 2017-05-11 23:50:14.778,
Modified by: prospring5, Modified date: 2017-05-11 23:50:14.778
Old Singer with id 1 and rev 2:Singer - Id: 4,
First name: Riley B., Last name: King, Birthday: 1940-09-16,
Created by: prospring5, Create date: 2017-05-11 23:50:14.778,
Modified by: prospring5, Modified date: 2017-05-11 23:50:15.0
从前面的输出中,您可以看到在更新操作之后,SingerAudit的名字被更改为Riley B.。然而,当查看历史时,在修订版 1 中,第一个名字是BB。在修订版 2 中,名字变成了Riley B.。另请注意,修订版 2 的最后修改日期正确反映了更新的日期和时间。
Spring Boot JPA
到目前为止,我们已经配置了所有的东西,包括实体、数据库、存储库和服务。正如您现在可能期望的那样,应该有一个 Spring Boot 启动构件来帮助您更快地开发项目并最小化配置工作。一个 Spring Boot JPA 启动程序依赖于 Spring Boot JPA,所以它带有预配置的嵌入式数据库;所需要的只是依赖关系在类路径上。Hibernate 也提供了对持久层的抽象。SpringRepository接口被自动检测。因此,留给开发人员去做的就是提供实体、存储库扩展和一个Application类来一起使用它们。最后,您还可以开发一个类来填充数据库。所有这些都将在本节中完成和解释。
首先,我们需要添加 Spring Boot starter JPA 作为依赖项。这是像之前所有其他库一样完成的。在根build.gradle文件中添加版本、组 ID 和工件 ID,并在chapter08/boot-jpa/build.gradle文件中定义依赖关系。这里显示了两个配置片段:
//pro-spring-15/build.gradle
ext {
//spring libs
springVersion = '5.0.0.M5'
bootVersion = '2.0.0.BUILD-SNAPSHOT'
springDataVersion = '2.0.0.M2'
...
boot = [
...
starterJdbc :
"org.springframework.boot:spring-boot-starter-jdbc:$bootVersion",
starterJpa :
"org.springframework.boot:spring-boot-starter-data-jpa:$bootVersion"
]
testing = [
junit: "junit:junit:$junitVersion"
]
db = [
...
h2 : "com.h2database:h2:$h2Version"
]
}
//chapter08/boot-jpa/build.gradle
buildscript {
repositories {
mavenLocal
mavenCentral
maven { url "http://repo.spring.io/release" }
maven { url "http://repo.spring.io/milestone" }
maven { url "http://repo.spring.io/snapshot" }
maven { url "https://repo.spring.io/libs-snapshot" }
}
dependencies {
classpath boot.springBootPlugin
}
}
apply plugin: 'org.springframework.boot'
dependencies {
compile boot.starterJpa, db.h2
}
}
添加完这些依赖项并刷新项目后,整个spring-boot-starter-data-jpa依赖项树应该在 IntelliJ IDEA Gradle 项目视图中可见,如图 8-2 所示。

图 8-2。
Dependency tree for the Spring Boot Starter artifact
这些实体将与迄今为止使用的相同(Singer、Album和Instrument),因此没有必要再次描述它们。初始化它们的 bean 的类型是DBInitializer,它是一个使用SingerRepository和InstrumentRepositorybean 的服务类,由 Spring Boot 提供,用于将一组对象保存到数据库中。其内容如下所示:
package com.apress.prospring5.ch8.config;
import com.apress.prospring5.ch8.InstrumentRepository;
import com.apress.prospring5.ch8.SingerRepository;
import com.apress.prospring5.ch8.entities.Album;
import com.apress.prospring5.ch8.entities.Instrument;
import com.apress.prospring5.ch8.entities.Singer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.Date;
import java.util.GregorianCalendar;
@Service
public class DBInitializer {
private Logger logger = LoggerFactory.getLogger(DBInitializer.class);
@Autowired
SingerRepository singerRepository;
@Autowired
InstrumentRepository instrumentRepository;
@PostConstruct
public void initDB(){
logger.info("Starting database initialization...");
Instrument guitar = new Instrument();
guitar.setInstrumentId("Guitar");
instrumentRepository.save(guitar);
Instrument piano = new Instrument();
piano.setInstrumentId("Piano");
instrumentRepository.save(piano);
Instrument voice = new Instrument();
voice.setInstrumentId("Voice");
instrumentRepository.save(voice);
Singer singer = new Singer();
singer.setFirstName("John");
singer.setLastName("Mayer");
singer.setBirthDate(new Date(
(new GregorianCalendar(1977, 9, 16)).getTime().getTime()));
singer.addInstrument(guitar);
singer.addInstrument(piano);
Album album1 = new Album();
album1.setTitle("The Search For Everything");
album1.setReleaseDate(new java.sql.Date(
(new GregorianCalendar(2017, 0, 20)).getTime().getTime()));
singer.addAbum(album1);
Album album2 = new Album();
album2.setTitle("Battle Studies");
album2.setReleaseDate(new java.sql.Date(
(new GregorianCalendar(2009, 10, 17)).getTime().getTime()));
singer.addAbum(album2);
singerRepository.save(singer);
singer = new Singer();
singer.setFirstName("Eric");
singer.setLastName("Clapton");
singer.setBirthDate(new Date(
(new GregorianCalendar(1945, 2, 30)).getTime().getTime()));
singer.addInstrument(guitar);
Album album = new Album();
album.setTitle("From The Cradle");
album.setReleaseDate(new java.sql.Date(
(new GregorianCalendar(1994, 8, 13)).getTime().getTime()));
singer.addAbum(album);
singerRepository.save(singer);
singer = new Singer();
singer.setFirstName("John");
singer.setLastName("Butler");
singer.setBirthDate(new Date(
(new GregorianCalendar(1975, 3, 1)).getTime().getTime()));
singer.addInstrument(guitar);
singerRepository.save(singer);
logger.info("Database initialization finished.");
}
}
在这个例子中,SingerRepository和InstrumentRepository接口非常简单,如下所示:
//InstrumentRepository.java
package com.apress.prospring5.ch8;
import com.apress.prospring5.ch8.entities.Instrument;
import org.springframework.data.repository.CrudRepository;
public interface InstrumentRepository
extends CrudRepository<Instrument, Long> {
}
//SingerRepository.java
package com.apress.prospring5.ch8;
import com.apress.prospring5.ch8.entities.Singer;
import org.springframework.data.repository.CrudRepository;
import java.util.List;
public interface SingerRepository
extends CrudRepository<Singer, Long> {
List<Singer> findByFirstName(String firstName);
List<Singer> findByFirstNameAndLastName(String firstName, String lastName);
}
SingerRepository将被直接注入到 Spring Boot 注释的Application类中,用于从数据库中检索所有歌手记录及其子记录,并记录到控制台中。这里显示了Application类:
package com.apress.prospring5.ch8;
import com.apress.prospring5.ch8.entities.Singer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@SpringBootApplication(scanBasePackages = "com.apress.prospring5.ch8.config")
public class Application implements CommandLineRunner {
private static Logger logger = LoggerFactory.getLogger(Application.class);
@Autowired
SingerRepository singerRepository;
public static void main(String... args) throws Exception {
ConfigurableApplicationContext ctx =
SpringApplication.run(Application.class, args);
System.in.read();
ctx.close();
}
@Transactional(readOnly = true)
@Override public void run(String... args) throws Exception {
List<Singer> singers = singerRepository.findByFirstName("John");
listSingersWithAlbum(singers);
}
private static void listSingersWithAlbum(List<Singer> singers) {
logger.info(" ---- Listing singers with instruments:");
singers.forEach(singer -> {
logger.info(singer.toString());
if (singer.getAlbums() != null) {
singer.getAlbums().forEach(
album -> logger.info("\t" + album.toString()));
}
if (singer.getInstruments() != null) {
singer.getInstruments().forEach(
instrument -> logger.info("\t" + instrument.getInstrumentId()));
}
});
}
}
这个Application类引入了一些新的东西;它实现了CommandLineRunner接口。这个接口用来告诉 Spring Boot,当这个 bean 包含在一个 Spring 应用中时,应该执行run()方法。
scanBasePackages = "com.apress.prospring5.ch8.config"属性用于为作为参数指定的包(或多个包)启用组件扫描,以便创建其中包含的 beans 并将其添加到应用上下文中。当 beans 在不同于Application类的包中定义时,这是需要的。
您应该注意的另一件事是,不需要其他配置类;您不需要任何 SQL 脚本来初始化数据库,也不需要任何其他关于Application类的注释。显然,如果你想专注于应用的逻辑,Spring Boot 和它的启动依赖是非常方便的。
如果您运行前面的类,您将获得预期的结果。(请注意,应用在退出前会等待按键被按下)。
INFO c.a.p.c.c.DBInitializer - Starting database initialization...
INFO c.a.p.c.c.DBInitializer - Database initialization finished.
INFO c.a.p.c.Application - ---- Listing singers with instruments:
INFO c.a.p.c.Application - Singer - Id: 1, First name: John, Last name: Mayer,
Birthday: 1977-10-16
INFO c.a.p.c.Application - Album - id: 1, Singer id: 1, Title: Battle Studies,
Release Date: 2009-11-17
INFO c.a.p.c.Application - Album - id: 2, Singer id: 1,
Title: The Search For Everything, Release Date: 2017-01-20
INFO c.a.p.c.Application - Piano
INFO c.a.p.c.Application - Guitar
INFO c.a.p.c.Application - Singer - Id: 3, First name: John, Last name: Butler,
Birthday: 1975-04-01
INFO c.a.p.c.Application - Guitar
INFO c.a.p.c.Application - Started Application in 3.464 seconds (JVM running for 4.0)
使用 JPA 时的注意事项
尽管我们讨论了相当多的内容,但本章只讨论了 JPA 的一小部分。例如,使用 JPA 调用数据库存储过程就不在讨论范围之内。JPA 是一个完整而强大的 ORM 数据访问标准,借助 Spring Data JPA 和 Hibernate Envers 等第三方库,可以相对容易地实现各种横切关注点。
JPA 是一个 JEE 标准,受到大多数主要开源社区以及商业供应商(JBoss、GlassFish、WebSphere、WebLogic 等)的支持。因此,采用 JPA 作为数据访问标准是一个令人信服的选择。如果需要对查询进行绝对控制,可以使用 JPA 的原生查询支持,而不是直接使用 JDBC。
总之,为了用 Spring 开发 JEE 应用,我们建议使用 JPA 来实现数据访问层。如果需要,您仍然可以在 JDBC 混合一些特殊的数据访问需求。永远记住,Spring 允许您轻松地混合和匹配数据访问技术,事务管理是透明地为您处理的。如果您想进一步简化开发,Spring Boot 可以通过其预配置的 beans 和定制配置来实现。
摘要
在这一章中,我们介绍了 JPA 的基本概念,以及如何在 Spring 中使用 Hibernate 作为持久性服务提供者来配置 JPA 的EntityManagerFactory。然后我们讨论了使用 JPA 来执行基本的数据库操作。高级主题包括本地查询和强类型 JPA 标准 API。
此外,我们展示了 Spring Data JPA 的Repository抽象如何帮助简化 JPA 应用开发,以及如何使用其实体监听器来跟踪实体类的基本审计信息。对于实体类的完整版本,还讨论了使用 Hibernate Envers 来满足需求。
还讨论了 JPA 应用的 Spring Boot,因为它极大地简化了配置,并将重点放在开发所需的功能上。
在下一章,我们将讨论 Spring 中的事务管理。
Footnotes 1
从 www.apress.com/us/book/9781430249269 在线获取。
2
你可以在 https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#envers-configuration 的 Hibernate 官方文档中找到 Hibernate 属性的完整列表。
九、事务管理
事务是构建可靠的企业应用的最关键部分之一。最常见的事务类型是数据库操作。在典型的数据库更新操作中,数据库事务开始,数据被更新,然后事务被提交或回滚,这取决于数据库操作的结果。然而,在许多情况下,根据应用的需求和应用需要与之交互的后端资源(如 RDBMS、面向消息的中间件、ERP 系统等等),事务管理可能要复杂得多。
在 Java 应用开发的早期(在 JDBC 创建之后,但在 JEE 标准或像 Spring 这样的应用框架可用之前),开发人员以编程方式控制和管理应用代码中的事务。当 JEE,更具体地说,EJB 标准可用时,开发人员能够使用容器管理的事务(CMTs)以声明的方式管理事务。但是 EJB 部署描述符中复杂的事务声明很难维护,并且给事务处理带来了不必要的复杂性。一些开发人员喜欢对事务有更多的控制,并选择 bean 管理的事务(BMTs)来以编程的方式管理事务。然而,用 Java 事务 API (JTA)编程的复杂性也阻碍了开发人员的生产力。
正如在第五章中所讨论的,事务管理是一个横切关注点,不应该编码在业务逻辑中。实现事务管理的最合适的方式是允许开发人员以声明的方式定义事务需求,并让 Spring、JEE 或 AOP 等框架代表我们编织事务处理逻辑。在本章中,我们将讨论 Spring 如何帮助简化事务处理逻辑的实现。Spring 同时支持声明式和编程式事务管理。
Spring 为声明性事务提供了出色的支持,这意味着您不需要用事务管理代码来混淆您的业务逻辑。您所要做的就是声明那些必须参与事务的方法(在类或层中),以及事务配置的细节,Spring 将负责处理事务管理。更具体地说,本章包括以下内容:
- Spring 事务抽象层:我们讨论 Spring 事务抽象类的基本组件,并解释如何使用这些类来控制事务的属性。
- 声明式事务管理:我们向您展示了如何使用 Spring 和普通 Java 对象来实现声明式事务管理。我们提供了使用 XML 配置文件和 Java 注释进行声明式事务管理的例子。
- 编程式事务管理:尽管编程式事务管理并不经常使用,但我们解释了如何使用 Spring 提供的
TransactionTemplate类,它让您可以完全控制事务管理代码。 - 使用 JTA 的全局事务:对于需要跨越多个后端资源的全局事务,我们展示了如何使用 JTA 在 Spring 中配置和实现全局事务。
探索 Spring 事务抽象层
在开发应用时,无论您是否选择使用 Spring,您都必须在使用事务时做出一个基本的选择,即是使用全局事务还是本地事务。本地事务特定于单个事务性资源(例如,JDBC 连接),而全局事务由容器管理,可以跨多个事务性资源。
交易类型
本地事务易于管理,如果应用中的所有操作只需要与一个事务性资源交互(如 JDBC 事务),使用本地事务就足够了。但是,如果您没有使用 Spring 之类的应用框架,那么您需要编写大量的事务管理代码,并且如果将来需要跨多个事务资源扩展事务的范围,那么您必须放弃本地事务管理代码,重新编写它以使用全局事务。
在 Java 世界中,全局事务是通过 JTA 实现的。在这个场景中,一个 JTA 兼容的事务管理器通过各自的资源管理器连接到多个事务资源,这些资源管理器能够通过 XA 协议(一个定义分布式事务的开放标准)与事务管理器通信,两阶段提交(2PC)机制用于确保所有后端数据源都被更新或一起回滚。如果任何一个后端资源失败,整个事务将回滚,因此对其他资源的更新也将回滚。
图 9-1 显示了与 JTA 的全球交易的高级视图。如图 9-1 所示,一个全局事务(通常也称为分布式事务)有四个主要参与方。第一方是后端资源,比如 RDBMS、消息中间件、企业资源规划(ERP)系统等等。

图 9-1。
Overview of global transactions with JTA
第二方是资源管理器,一般由后端资源厂商提供,负责与后端资源交互。例如,当连接到 MySQL 数据库时,我们需要与 MySQL 的 Java 连接器提供的MysqlXADataSource类进行交互。其他后端资源(例如 MQ、ERP 等等)也提供了它们的资源管理器。
第三方是 JTA 事务管理器,它负责管理、协调事务状态,并与参与事务的所有资源管理器同步事务状态。使用 XA 协议,这是一种广泛用于分布式事务处理的开放标准。JTA 事务管理器还支持 2PC,因此所有的更改将一起提交,如果任何资源更新失败,整个事务将回滚,导致没有任何资源被更新。整个机制由 Java 事务服务(JTS)规范指定。
最后一个组件是应用。应用本身或者运行应用的底层容器或 Spring 框架管理事务(开始、提交、回滚事务,等等)。同时,应用通过 JEE 定义的各种标准与底层后端资源进行交互。如图 9-1 所示,应用通过 JDBC 连接到 RDBMS,通过 JMS 连接到 MQ,通过 Java EE 连接器架构(JCA)连接到 ERP 系统。
所有成熟的符合 JEE 标准的应用服务器(例如,JBoss、WebSphere、WebLogic 和 GlassFish)都支持 JTA,在这些服务器中,可以通过 JNDI 查找来处理事务。至于独立的应用或 web 容器(例如,Tomcat 和 Jetty),也存在开放源代码和商业解决方案,在这些环境中提供对 JTA/XA 的支持(例如,Atomikos、Java Open Transaction Manager[JOTM]和 Bitronix)。
PlatformTransactionManager 的实现
在 Spring 中,PlatformTransactionManager接口使用TransactionDefinition和TransactionStatus接口来创建和管理事务。这些接口的实际实现必须详细了解事务管理器。
图 9-2 显示了PlatformTransactionManager在 Spring 中的实现。

图 9-2。
PlatformTransactionManager implementations as of Spring
Spring 为PlatformTransactionManager接口提供了丰富的实现。CciLocalTransactionManager类支持 JEE、JCA 和公共客户端接口(CCI)。DataSourceTransactionManager类用于一般的 JDBC 连接。对于 ORM 端,有很多实现,包括 JPA(JpaTransactionManager类) 1 和 Hibernate 5 ( HibernateTransactionManager)。 2 对于 JMS,实现通过JmsTransactionManager类支持 JMS 2.0。 3 对于 JTA,通用的实现类是JtaTransactionManager。Spring 还提供了几个特定于特定应用服务器的 JTA 事务管理器类。这些类为 WebSphere(WebSphereUowTransactionManager类)、WebLogic(WebLogicJtaTransactionManager类)和 Oracle OC4J(OC4JJtaTransactionManager类)提供了本地支持。
分析事务属性
在这一节中,我们将讨论 Spring 支持的事务属性,重点是作为后端资源与 RDBMS 进行交互。
事务有四个众所周知的 ACID 属性(原子性、一致性、隔离性和持久性),事务资源负责维护事务的这些方面。您无法控制事务的原子性、一致性和持久性。但是,您可以控制事务传播和超时,以及配置事务是否应该是只读的和指定隔离级别。
Spring 将所有这些设置封装在一个TransactionDefinition接口中。该接口用于 Spring 中事务支持的核心接口,即PlatformTransactionManager接口,其实现在特定平台上执行事务管理,如 JDBC 或 JTA。核心方法PlatformTransactionManager.getTransaction()将一个TransactionDefinition接口作为参数,并返回一个TransactionStatus接口。TransactionStatus接口用于控制交易执行,更具体地说是设置交易结果,检查交易是否完成或是否是新交易。
事务定义接口
正如我们前面提到的,TransactionDefinition接口控制事务的属性。让我们更详细地看看这里显示的TransactionDefinition接口,并描述它的方法:
package org.springframework.transaction;
import java.sql.Connection;
public interface TransactionDefinition {
// Variable declaration statements omitted
...
int getPropagationBehavior();
int getIsolationLevel();
int getTimeout();
boolean isReadOnly();
String getName();
}
这个接口的简单而明显的方法是getTimeout(),它返回事务必须完成的时间(以秒为单位)和isReadOnly(),它指示事务是否是只读的。事务管理器实现可以使用这个值来优化执行,并进行检查以确保事务只执行读操作。getName()方法返回事务的名称。
另外两种方法getPropagationBehavior()和getIsolationLevel(),需要更详细的讨论。我们从getIsolationLevel()开始,它控制其他事务看到的数据变化。表 9-1 列出了您可以使用的事务隔离级别,并解释了在当前事务中其他事务可以访问的更改。隔离级别表示为在TransactionDefinition接口中定义的静态值。
表 9-1。
Transaction Isolation Levels
| 隔离级别 | 描述 | | --- | --- | | `ISOLATION_DEFAULT` | 基础数据存储区的默认隔离级别。 | | `ISOLATION_READ_UNCOMMITTED` | 最低水平的隔离;它几乎不是一个事务,因为它允许这个事务看到被其他未提交的事务修改的数据。 | | `ISOLATION_READ_COMMITTED` | 大多数数据库中的默认级别;它确保其他事务不能读取其他事务未提交的数据。但是,由一个事务读取的数据可以由其他事务更新。 | | `ISOLATION_REPEATABLE_READ` | 比`ISOLATION_READ_COMMITTED`更严格;它确保一旦选择了数据,您至少可以再次选择相同的集合。但是,如果其他事务插入了新数据,您仍然可以选择新插入的数据。 | | `ISOLATION_SERIALIZABLE` | 最昂贵、最可靠的隔离级别;所有事务都被视为是一个接一个执行的。 |选择适当的隔离级别对于数据的一致性很重要,但是做出这些选择会对性能产生很大的影响。最高隔离级别ISOLATION_SERIALIZABLE的维护成本特别高。
getPropagationBehavior()方法根据是否有活动的事务来指定事务调用会发生什么。表 9-2 描述了该方法的数值。传播类型被表示为在TransactionDefinition接口中定义的静态值。
表 9-2。
Transaction Isolation Levels
| 传播类型 | 描述 | | --- | --- | | `PROPAGATION_REQUIRED` | 支持已经存在的事务。如果没有事务,它将开始一个新的事务。 | | `PROPAGATION_SUPPORTS` | 支持已经存在的事务。如果没有事务,它将以非事务方式执行。 | | `PROPAGATION_MANDATORY` | 支持已经存在的事务。如果没有活动事务,将引发异常。 | | `PROPAGATION_REQUIRES_NEW` | 总是开始一个新的事务。如果活动事务已经存在,它将被挂起。 | | `PROPAGATION_NOT_SUPPORTED` | 不支持活动事务的执行。总是以非事务方式执行,并挂起任何现有的事务。 | | `PROPAGATION_NEVER` | 即使存在活动事务,也总是以非事务方式执行。如果存在活动事务,将引发异常。 | | `PROPAGATION_NESTED` | 如果活动事务存在,则在嵌套事务中运行。如果没有活动的事务,则如同设置了`PROPAGATION_REQUIRED`一样执行。 |TransactionStatus 接口
接下来显示的TransactionStatus接口允许事务管理器控制事务的执行。该代码可以检查事务是新事务还是只读事务,并且可以启动回滚。
package org.springframework.transaction;
public interface TransactionStatus extends SavepointManager {
boolean isNewTransaction();
boolean hasSavepoint();
void setRollbackOnly();
boolean isRollbackOnly();
void flush();
boolean isCompleted();
}
TransactionStatus接口的方法是不言自明的;最值得注意的是setRollbackOnly(),它会导致回滚并结束活动事务。
hasSavePoint()方法返回事务内部是否带有保存点(也就是说,事务是作为基于保存点的嵌套事务创建的)。如果适用的话,flush()方法是到数据存储的底层会话(例如,当使用 Hibernate 时)。isCompleted()方法返回事务是否已经结束(即提交或回滚)。
示例代码的示例数据模型和基础结构
本节概述了我们的事务管理示例中使用的数据模型和基础设施。我们使用 JPA 和 Hibernate 作为实现数据访问逻辑的持久层。此外,Spring Data JPA 及其存储库抽象用于简化基本数据库操作的开发。
创建一个带有依赖项的简单 Spring JPA 项目
让我们从创建项目开始。因为我们使用的是 JPA,所以我们还需要为本章中的例子添加所需的依赖项。
//pro-spring-15/build.gradle
ext {
//spring libs
springVersion = '5.0.0.RC1'
bootVersion = '2.0.0.BUILD-SNAPSHOT'
springDataVersion = '2.0.0.M3'
//logging libs
slf4jVersion = '1.7.25'
logbackVersion = '1.2.3'
guavaVersion = '21.0'
junitVersion = '4.12'
aspectjVersion = '1.9.0.BETA-5'
//database library
h2Version = '1.4.194'
//persistency libraries
hibernateVersion = '5.2.10.Final'
hibernateJpaVersion = '1.0.0.Final'
atomikosVersion = '4.0.0M4'
spring = [
context : "org.springframework:spring-context:$springVersion",
aop : "org.springframework:spring-aop:$springVersion",
aspects : "org.springframework:spring-aspects:$springVersion",
tx : "org.springframework:spring-tx:$springVersion",
jdbc : "org.springframework:spring-jdbc:$springVersion",
contextSupport: "org.springframework:spring-context-support:$springVersion",
orm : "org.springframework:spring-orm:$springVersion",
data : "org.springframework.data:spring-data-jpa:$springDataVersion",
test : "org.springframework:spring-test:$springVersion"
]
hibernate = [
...
em : "org.hibernate:hibernate-entitymanager:$hibernateVersion",
tx : "com.atomikos:transactions-hibernate4:$atomikosVersion"
]
boot = [
...
springBootPlugin:
"org.springframework.boot:spring-boot-gradle-plugin:$bootVersion",
starterJpa :
"org.springframework.boot:spring-boot-starter-data-jpa:$bootVersion"
]
testing = [
junit: "junit:junit:$junitVersion"
]
misc = [
...
slf4jJcl : "org.slf4j:jcl-over-slf4j:$slf4jVersion",
logback : "ch.qos.logback:logback-classic:$logbackVersion",
aspectjweaver: "org.aspectj:aspectjweaver:$aspectjVersion",
lang3 : "org.apache.commons:commons-lang3:3.5",
guava : "com.google.guava:guava:$guavaVersion"
]
db = [
...
h2 : "com.h2database:h2:$h2Version"
]
}
//chapter09/build.gradle
dependencies {
//we specify these dependencies for all submodules, except
// the boot module, that defines its own
if !project.name.contains"boot" {
//we exclude transitive dependencies, because spring-data
//will take care of these
compile spring.contextSupport {
exclude module: 'spring-context'
exclude module: 'spring-beans'
exclude module: 'spring-core'
}
//we exclude the 'hibernate' transitive dependency
//to have control over the version used
compile hibernate.tx {
exclude group: 'org.hibernate', module: 'hibernate'
}
compile spring.orm, spring.context, misc.slf4jJcl,
misc.logback, db.h2, misc.lang3,
hibernate.em
}
testCompile testing.junit
}
为了在我们修改事务属性时观察示例代码的详细行为,让我们也在logback中打开DEBUG级别的日志记录。下面的代码片段显示了logback.xml文件:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">
<resetJUL>true</resetJUL>
</contextListener>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} %thread %-5level %logger{5} - %msg%n</pattern>
</encoder>
</appender>
<logger name="com.apress.prospring5.ch8" level="debug"/>
<logger name="org.springframework.transaction" level="info"/>
<logger name="org.hibernate.SQL" level="debug"/>
<root level="info">
<appender-ref ref="console" />
</root>
</configuration>
样本数据模型和公共类
为了简单起见,我们将只使用两个表,即我们在关于数据访问的章节中使用的SINGER和ALBUM表。不需要 SQL 脚本来创建表格,因为您可以使用 Hibernate 属性hibernate.hbm2ddl.auto并将其设置为create-drop,这样每次测试时我们都会有一个干净的运行。表格将基于Singer和Album实体生成。下面描述了带有注释字段的片段:
//Singer.java
package com.apress.prospring5.ch9.entities;
...
@Entity
@Table(name = "singer")
@NamedQueries({
@NamedQuery(name=Singer.FIND_ALL, query="select s from Singer s"),
@NamedQuery(name=Singer.COUNT_ALL, query="select count(s) from Singer s")
})
public class Singer implements Serializable {
public static final String FIND_ALL = "Singer.findAll";
public static final String COUNT_ALL = "Singer.countAll";
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "ID")
private Long id;
@Version
@Column(name = "VERSION")
private int version;
@Column(name = "FIRST_NAME")
private String firstName;
@Column(name = "LAST_NAME")
private String lastName;
@Temporal(TemporalType.DATE)
@Column(name = "BIRTH_DATE")
private Date birthDate;
@OneToMany(mappedBy = "singer", cascade=CascadeType.ALL, orphanRemoval=true)
private Set<Album> albums = new HashSet<>();
...
}
/Album.java
package com.apress.prospring5.ch9.entities;
...
@Entity
@Table(name = "album")
public class Album implements Serializable {
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "ID")
private Long id;
@Version
@Column(name = "VERSION")
private int version;
@Column
private String title;
@Temporal(TemporalType.DATE)
@Column(name = "RELEASE_DATE")
private Date releaseDate;
@ManyToOne
@JoinColumn(name = "SINGER_ID")
private Singer singer;
...
}
这两个类将被隔离在一个名为base-dao的项目中,该项目将成为所有事务项目的依赖项。除了实体之外,这个项目中还定义了存储库接口。我们将在稍后的课程中对它们进行描述。还需要一个配置类来定义DataSource bean。这里显示了配置类,出于实用和教育目的,将直接使用数据库凭证、驱动程序和 URL,而不是从外部文件读取。(不过,在生产中你永远不会遇到这种情况。我们希望。)
package com.apress.prospring5.ch9.config;
...
@Configuration
@EnableJpaRepositories(basePackages = {"com.apress.prospring5.ch9.repos"})
public class DataJpaConfig {
private static Logger logger =
LoggerFactory.getLogger(DataJpaConfig.class);
@SuppressWarnings("unchecked")
@Bean
public DataSource dataSource() {
try {
SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
Class<? extends Driver> driver =
(Class<? extends Driver>) Class.forName("org.h2.Driver");
dataSource.setDriverClass(driver);
dataSource.setUrl("jdbc:h2:musicdb");
dataSource.setUsername("prospring5");
dataSource.setPassword("prospring5");
return dataSource;
} catch (Exception e) {
logger.error("Populator DataSource bean cannot be created!", e);
return null;
}
}
@Bean
public Properties hibernateProperties() {
Properties hibernateProp = new Properties();
hibernateProp.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
hibernateProp.put("hibernate.hbm2ddl.auto", "create-drop");
//hibernateProp.put("hibernate.format_sql", true);
hibernateProp.put("hibernate.show_sql", true);
hibernateProp.put("hibernate.max_fetch_depth", 3);
hibernateProp.put("hibernate.jdbc.batch_size", 10);
hibernateProp.put("hibernate.jdbc.fetch_size", 50);
return hibernateProp;
}
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
return new HibernateJpaVendorAdapter();
}
@Bean
public EntityManagerFactory entityManagerFactory() {
LocalContainerEntityManagerFactoryBean factoryBean =
new LocalContainerEntityManagerFactoryBean();
factoryBean.setPackagesToScan("com.apress.prospring5.ch9.entities");
factoryBean.setDataSource(dataSource());
factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
factoryBean.setJpaProperties(hibernateProperties());
factoryBean.setJpaVendorAdapter(jpaVendorAdapter());
factoryBean.afterPropertiesSet();
return factoryBean.getNativeEntityManagerFactory();
}
}
定义了嵌入式 H2 数据库。凭证直接在代码中设置,DataSource实现是SimpleDriverDataSource,它被设计成只用于简单的、测试的或教育的应用。
目前,使用注释是 Spring 中定义事务需求最常见的方式。主要的好处是,事务需求和详细的事务属性(超时、隔离级别、传播行为等等)都是在代码本身中定义的,这使得应用更容易跟踪和维护。配置也是使用注释和 Java 配置类来完成的。为了使用 XML 配置在 Spring 中启用对事务管理的注释支持,我们需要在 XML 配置文件中添加<tx:annotation-driven>标记。在下面的配置片段中,您可以看到事务性配置和事务性匹配名称空间的片段。如果您感兴趣,可以在项目中找到完整的配置。
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
...
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd ...">
<bean id="transactionManager"
class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="emf"/>
</bean>
<tx:annotation-driven />
<bean id="emf"
class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
...
</bean>
<context:component-scan
base-package="com.apress.prospring5.ch9" />
<jpa:repositories base-package="com.apress.prospring5.ch9.repos"
entity-manager-factory-ref="emf"
transaction-manager-ref="transactionManager"/>
</beans>
因为我们使用 JPA,所以您定义了JpaTransactionManager bean。<tx:annotation-driven>标签指定我们使用注释进行事务管理。这个简单的定义指示 Spring 寻找一个名为transactionManager的类型为PlatformTransactionManager的 bean。如果事务 bean 的名称不同,比如说customTransactionManager,元素定义必须用属性transaction-manager声明,该属性必须接收事务管理 bean 的名称作为值。
<tx:annotation-driven transaction-manager="customTransactionManager"/>
然后定义了EntityManagerFactory bean,后面跟着<context:component-scan>标记来扫描服务层类。最后,<jpa:repositories>标签用于启用 Spring Data JPA 的存储库抽象。这个元素在DataJpaConfiguration类中被替换为@EnableJpaRepositories注释。
在专业环境中,将持久性配置(DAO)与事务性配置(服务)分开是一种常见的做法。这就是为什么前面介绍的 XML 内容在 Java 配置中被分成两个配置类。前面介绍的DataJpaConfig只包含数据访问 bean,接下来描述的ServicesConfig只包含与事务管理相关的 bean:
package com.apress.prospring5.ch9.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.persistence.EntityManagerFactory;
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = "com.apress.prospring5.ch9")
public class ServicesConfig {
@Autowired EntityManagerFactory entityManagerFactory;
@Bean
public PlatformTransactionManager transactionManager() {
return new JpaTransactionManager(entityManagerFactory);
}
}
对于SingerService接口的实现,我们首先用SingerService接口中所有方法的空实现来创建这个类。让我们首先实现SingerService.findAll()方法。下面的代码片段显示了实现了findAll()方法的SingerServiceImpl类:
package com.apress.prospring5.ch9.services;
import com.apress.prospring5.ch9.entities.Singer;
import com.apress.prospring5.ch9.repos.SingerRepository;
import com.google.common.collect.Lists;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service("singerService")
@Transactional
public class SingerServiceImpl implements SingerService {
private SingerRepository singerRepository;
@Autowired
public void setSingerRepository(SingerRepository singerRepository) {
this.singerRepository = singerRepository;
}
@Override
@Transactional(readOnly = true)
public List<Singer> findAll() {
return Lists.newArrayList(singerRepository.findAll());
}
}
当使用基于注释的事务管理时,我们需要处理的唯一注释是@Transactional。在前面的代码片段中,@Transactional注释应用于类级别,这意味着默认情况下,Spring 将确保在执行类中的每个方法之前存在一个事务。@Transactional注释支持许多属性,您可以提供这些属性来覆盖默认行为。表 9-3 显示了可用的属性,以及可能值和默认值。
表 9-3。
Attributes for the @Transactional Annotation
| 属性名 | 缺省值 | 可能的值 | | --- | --- | --- | | `propagation` | `Propagation.REQUIRED` | `Propagation.REQUIRED``Propagation.SUPPORTS``Propagation.MANDATORY``Propagation.REQUIRES_NEW``Propagation.NOT_SUPPORTED``Propagation.NEVER` | | `isolation` | `Isolation.DEFAULT`(底层资源的默认隔离级别) | `Isolation.DEFAULT``Isolation.READ_UNCOMMITTED``Isolation.READ_COMMITTED``Isolation.REPEATABLE_READ` | | `timeout` | `TransactionDefinition.TIMEOUT_DEFAULT`(底层资源的默认事务超时,以秒为单位) | 大于零的整数值;指示超时的秒数 | | `readOnly` | 错误的 | { `true`,`false` } | | `rollbackFor` | 将回滚事务的异常类 | 不适用的 | | `rollbackForClassName` | 将回滚事务的异常类名 | 不适用的 | | `noRollbackFor` | 不会回滚事务的异常类 | 不适用的 | | `noRollbackForClassName` | 不会回滚事务的异常类名 | 不适用的 | | `value` | `""`(指定交易的限定符值) | 不适用的 |因此,基于表 9-3 ,没有任何属性的@Transactional注释意味着需要事务传播,隔离是默认的,超时是默认的,模式是读写。对于之前介绍的findAll()方法,该方法用@Transactional(readOnly=true)标注。这将覆盖在类级别应用的默认注释,所有其他属性不变,但是事务被设置为只读。下面的代码片段显示了findAll()方法的测试程序:
package com.apress.prospring5.ch9;
import java.util.List;
import com.apress.prospring5.ch9.config.DataJpaConfig;
import com.apress.prospring5.ch9.config.ServicesConfig;
import com.apress.prospring5.ch9.entities.Singer;
import com.apress.prospring5.ch9.services.SingerService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
public class TxAnnotationDemo {
public static void main(String... args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(ServicesConfig.class,
DataJpaConfig.class);
SingerService singerService = ctx.getBean(SingerService.class);
List<Singer> singers = singerService.findAll();
singers.forEach(s -> System.out.println(s));
ctx.close();
}
}
启用适当的日志记录,换句话说,<logger name="org.springframework.orm.jpa" level="debug"/>,您将能够在日志中看到与事务处理相关的消息。运行该程序会产生以下缩减的输出(有关完整的详细信息,请参见控制台中的调试日志):
DEBUG o.s.o.j.JpaTransactionManager - Creating new transaction with name
[com.apress.prospring5.ch9.services.SingerServiceImpl.findAll]:
PROPAGATION_REQUIRED,ISOLATION_DEFAULT,readOnly; ''
DEBUG o.s.o.j.JpaTransactionManager - Participating in existing transaction
Hibernate: select singer0_.ID as ID1_1_, singer0_.BIRTH_DATE as BIRTH_DA2_1_,
singer0_.FIRST_NAME as FIRST_NA3_1_, singer0_.LAST_NAME as LAST_NAM4_1_,
singer0_.VERSION as VERSION5_1_ from singer singer0_
DEBUG o.s.o.j.JpaTransactionManager - Closing JPA EntityManager
[...] after transaction
DEBUG o.s.o.j.JpaTransactionManager - Initiating transaction commit
Singer - Id: 1, First name: John, Last name: Mayer, Birthday: 1977-10-16
Singer - Id: 2, First name: Eric, Last name: Clapton, Birthday: 1945-03-30
Singer - Id: 3, First name: John, Last name: Butler, Birthday: 1975-04-01
如前面的输出所示,为了清楚起见,删除了不相关的输出语句。首先,在运行findAll()方法之前,Spring 的JpaTransactionManager创建了一个具有默认属性的新事务(名称等于带有方法名称的完全限定类名),但是事务被设置为只读,正如在方法级@Transactional注释中定义的那样。然后,提交查询,在完成且没有任何错误的情况下,提交事务。JpaTransactionManager处理事务的创建和提交操作。
让我们继续执行更新操作。我们需要在SingerServiceImpl接口中实现findById()和save()方法。以下代码片段显示了实现:
package com.apress.prospring5.ch9.services;
...
@Service("singerService")
@Transactional
public class SingerServiceImpl implements SingerService {
private SingerRepository singerRepository;
@Autowired
public void setSingerRepository(SingerRepository singerRepository) {
this.singerRepository = singerRepository;
}
@Override
@Transactional(readOnly = true)
public List<Singer> findAll() {
return Lists.newArrayList(singerRepository.findAll());
}
@Override
@Transactional(readOnly = true)
public Singer findById(Long id) {
return singerRepository.findById(id).get();
}
@Override
public Singer save(Singer singer) {
return singerRepository.save(singer);
}
}
findById()方法也用@Transactional(readOnly=true)进行了注释。一般来说,readOnly=true属性应该应用于所有的查找器方法。主要原因是大多数持久性提供者会对只读事务执行一定程度的优化。例如,Hibernate 不会维护从打开只读的数据库中检索的托管实例的快照。
对于save()方法,我们简单地调用CrudRepository.save()方法,并且不提供任何注释。这意味着将使用类级别的注释,这是一个读写事务。让我们修改用于测试save()方法的TxAnnotationDemo类,如下面的代码所示:
package com.apress.prospring5.ch9;
...
public class TxAnnotationDemo {
public static void main(String... args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(ServicesConfig.class,
DataJpaConfig.class);
SingerService singerService = ctx.getBean(SingerService.class);
List<Singer> singers = singerService.findAll();
singers.forEach(s -> System.out.println(s));
Singer singer = singerService.findById(1L);
singer.setFirstName("John Clayton");
singer.setLastName("Mayer");
singerService.save(singer);
System.out.println("Singer saved successfully: " + singer);
ctx.close();
}
}
检索 ID 为 1 的Singer对象,然后更新名字并保存到数据库中。运行代码会产生以下相关输出:
Singer saved successfully: Singer - Id: 1, First name: John Clayton,
Last name: Mayer, Birthday: 1977-10-16
save()方法获取从类级@Transactional注释继承的默认属性。更新操作完成后,Spring 的JpaTransactionManager会触发一个事务提交,这会导致 Hibernate 刷新持久性上下文,并将底层的 JDBC 连接提交给数据库。最后,我们来看看countAll()的方法。我们将研究这种方法的两种事务配置。虽然CrudRepository.count()方法可以达到目的,但我们不会使用那种方法。相反,出于演示的目的,我们将实现另一个方法,主要是因为 Spring Data 中由CrudRepository接口定义的方法已经用适当的事务属性进行了标记。
下面的代码片段显示了在SingerRepository接口中定义的新方法countAllSingers():
package com.apress.prospring5.ch9.repos;
import com.apress.prospring5.ch9.entities.Singer;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
public interface SingerRepository extends
CrudRepository<Singer, Long> {
@Query("select count(s) from Singer s")
Long countAllSingers();
}
对于新的countAllSingers()方法,应用了@Query注释,其值等于计算联系人数量的 JPQL 语句。下面的代码片段显示了SingerServiceImpl类中countAll()方法的实现:
package com.apress.prospring5.ch9.services;
...
@Service("singerService")
@Transactional
public class SingerServiceImpl implements SingerService {
private SingerRepository singerRepository;
@Autowired
public void setSingerRepository(SingerRepository singerRepository) {
this.singerRepository = singerRepository;
}
@Override
@Transactional(readOnly=true)
public long countAll() {
return singerRepository.countAllSingers();
}
}
注释与其他 finder 方法相同。要测试这个方法,只需在TxAnnotationDemo类的main()方法中添加System.out.println("Singer count: " + contactService.countAll());,并观察控制台。如果您看到类似Singer count: 3的消息,则该方法执行正确。
在这个输出中,您可以看到countAll()的事务是用只读等于 true 创建的,正如所预期的那样。但是对于countAll()函数,我们根本不想让它加入到事务中。我们不需要由底层 JPA EntityManager来管理结果。相反,我们只想得到计数并忘记它。在这种情况下,我们可以将事务传播行为覆盖到Propagation.NEVER。下面的方法显示了修改后的countAll()方法:
package com.apress.prospring5.ch9.services;
...
@Service("singerService")
@Transactional
public class SingerServiceImpl implements SingerService {
...
@Override
@Transactional(propagation = Propagation.NEVER)
public long countAll() {
return singerRepository.countAllSingers();
}
}
再次运行测试代码,您会发现调试输出中不会为countAll()方法创建事务。
本节介绍了您在日常事务处理中需要处理的一些主要配置。对于特殊情况,您可能需要为特定异常定义超时、隔离级别、回滚(或不回滚)等等。
Spring 的JpaTransactionManager不支持自定义隔离级别。相反,它总是使用基础数据存储区的默认隔离级别。如果您使用 Hibernate 作为 JPA 服务提供者,您可以使用一种变通方法:扩展HibernateJpaDialect类以支持定制的隔离级别。
使用 AOP 配置进行事务管理
另一种常见的声明式事务管理方法是使用 Spring 的 AOP 支持。在 Spring 版本 2 之前,我们需要使用TransactionProxyFactoryBean类来定义 Spring beans 的事务需求。然而,从版本 2 开始,Spring 通过引入aop名称空间和使用通用 AOP 配置技术来定义事务需求,提供了一种更简单的方法。当然,在引入注释之后,这种配置事务管理的方式也受到了反对。但是知道它的存在是有用的,以防万一你可能需要包装一个不属于你的项目的事务代码,并且你不能编辑它来添加@Transaction注释。
在下面的配置片段中,上一节中的示例是使用 XML 配置的,并使用了aop名称空间:
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean name="dataJpaConfig"
class="com.apress.prospring5.ch9.config.DataJpaConfig" />
<aop:config>
<aop:pointcut id="serviceOperation" expression=
"execution(* com.apress.prospring5.ch9.*ServiceImpl.*(..))"/>
<aop:advisor pointcut-ref="serviceOperation" advice-ref="txAdvice"/>
</aop:config>
<tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="find*" read-only="true"/>
<tx:method name="count*" propagation="NEVER"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager
<property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
<context:component-scan
base-package="com.apress.prospring5.ch9.services" />
</beans>
该配置与本节开始时介绍的 XML 配置非常相似。基本上,<tx:annotation-driven>标记被移除,而<context:component-scan>标记被修改为我们用于声明性事务管理的包名。最重要的标签是<aop:config>和<tx:advice>。
在<aop:config>标签下,为服务层内的所有操作定义了一个切入点(即com.apress.prospring5.ch9.services包下的所有实现类)。该通知引用了 ID 为txAdvice的 bean,它是由<tx:advice>标记定义的。在<tx:advice>标签中,我们为想要参与事务的各种方法配置了事务属性。如标签所示,您指定所有的 finder 方法(带有前缀find的方法)将是只读的,我们指定 count 方法(带有前缀count的方法)将不参与事务。对于其余的方法,将应用默认的事务行为。该配置与注释示例中的配置相同。
因为事务管理是通过aop显式完成的,所以SingerServiceImpl类或其中的方法不再需要@Transactional注释。
要测试前面的配置,可以使用下面的类:
package com.apress.prospring5.ch9;
import java.util.List;
import com.apress.prospring5.ch9.entities.Singer;
import com.apress.prospring5.ch9.services.SingerService;
import org.springframework.context.support.GenericXmlApplicationContext;
public class TxDeclarativeDemo {
public static void main(String... args) {
GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
ctx.load("classpath:spring/tx-declarative-app-context.xml");
ctx.refresh();
SingerService singerService = ctx.getBean(SingerService.class);
// Testing findAll()
List<Singer> singers = singerService.findAll();
singers.forEach(s -> System.out.println(s));
// Testing save()
Singer singer = singerService.findById(1L);
singer.setFirstName("John Clayton");
singerService.save(singer);
System.out.println("Singer saved successfully: " + singer);
// Testing countAll()
System.out.println("Singer count: " + singerService.countAll());
ctx.close();
}
}
我们将让您测试程序,并观察 Spring 和 Hibernate 执行的与事务相关的操作的输出。基本上,它们与注释示例相同。
使用程序化事务
第三种选择是以编程方式控制事务行为。在这种情况下,我们有两个选择。第一种是在 bean 中注入一个PlatformTransactionManager实例,直接与事务管理器交互。另一个选择是使用 Spring 提供的TransactionTemplate类,这大大简化了您的工作。在本节中,我们将演示如何使用TransactionTemplate class。为了简单起见,我们集中于实现SingerServiceImpl.countAll()方法。下面的代码片段描述了为使用编程事务而修改的ServiceConfig类:
package com.apress.prospring5.ch9.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;
import javax.persistence.EntityManagerFactory;
@Configuration
@ComponentScan(basePackages = "com.apress.prospring5.ch9")
public class ServicesConfig {
@Autowired EntityManagerFactory entityManagerFactory;
@Bean
public TransactionTemplate transactionTemplate() {
TransactionTemplate tt = new TransactionTemplate();
tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NEVER);
tt.setTimeout(30);
tt.setTransactionManager(transactionManager());
return tt;
}
@Bean
public PlatformTransactionManager transactionManager() {
return new JpaTransactionManager(entityManagerFactory);
}
}
这里删除了 AOP 事务通知。此外,使用org.springframework.transaction.support.TransactionTemplate类定义了一个带有一些事务属性的transactionTemplate bean。此外,@EnableTransactionManagement也被删除了,因为事务管理现在不是显式完成的。让我们看看countAll()方法的实现,如下所示:
package com.apress.prospring5.ch9.services;
import com.apress.prospring5.ch9.entities.Singer;
import com.apress.prospring5.ch9.repos.SingerRepository;
import com.google.common.collect.Lists;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.List;
@Service("singerService")
@Repository
public class SingerServiceImpl implements SingerService {
@Autowired
private SingerRepository singerRepository;
@Autowired
private TransactionTemplate transactionTemplate;
@PersistenceContext
private EntityManager em;
@Override
public long countAll() {
return transactionTemplate.execute(
transactionStatus -> em.createNamedQuery(Singer.COUNT_ALL,
Long.class).getSingleResult());
}
}
这里的TransactionTemplate类是从 Spring 注入的。然后在countAll()方法中,调用TransactionTemplate.execute()方法,传入实现TransactionCallback<T>接口的内部类的声明。然后doInTransaction()被期望的逻辑覆盖。逻辑将在由transactionTemplate bean 定义的属性中运行。您没有清楚地看到前面代码片段中所有内容的原因是因为使用了 Java 8 lambda 表达式。以下代码是前面方法的扩展版本(因为它是在 lambda 表达式引入之前编写的):
public long countAll() {
return transactionTemplate.execute(new TransactionCallback<Long>() {
public Long doInTransaction(TransactionStatus transactionStatus) {
return em.createNamedQuery(Singer.COUNT_ALL,
Long.class).getSingleResult();
}
});
}
以下代码片段显示了测试程序:
package com.apress.prospring5.ch9;
import com.apress.prospring5.ch9.config.DataJpaConfig;
import com.apress.prospring5.ch9.config.ServicesConfig;
import com.apress.prospring5.ch9.services.SingerService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
public class TxProgrammaticDemo {
public static void main(String... args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(ServicesConfig.class,
DataJpaConfig.class);
SingerService singerService = ctx.getBean(SingerService.class);
System.out.println("Singer count: " + singerService.countAll());
ctx.close();
}
}
我们将让您来运行程序并观察结果。尝试调整事务属性,看看在countAll()方法的事务处理中会发生什么。
关于交易管理的思考
那么,在讨论了实现事务管理的各种方法之后,您应该使用哪一种呢?在所有情况下都推荐使用声明性方法,并且应该尽可能避免在代码中实现事务管理。大多数情况下,当您发现有必要在应用中编写事务控制逻辑时,这是由于糟糕的设计,在这种情况下,您应该考虑将您的逻辑重构为可管理的部分,并在这些部分上以声明方式定义事务需求。
对于声明性方法,使用 XML 和使用注释各有利弊。一些开发人员不喜欢在代码中声明事务需求,而另一些开发人员则喜欢使用注释以便于维护,因为您可以在代码中看到所有的事务需求声明。同样,让应用需求驱动您的决策,一旦您的团队或公司已经标准化了方法,就要保持与配置风格的一致。
Spring 的全球事务
许多企业 Java 应用需要访问多个后端资源。例如,从外部业务伙伴收到的一条客户信息可能需要更新多个系统(CRM、ERP 等)的数据库。有些人甚至需要为公司内对客户信息感兴趣的所有其他应用生成一条消息,并通过 JMS 将其发送到 MQ 服务器。跨越多个后端资源的事务被称为全局(或分布式)事务。
全局事务的一个主要特征是保证原子性,这意味着所涉及的资源都被更新,或者都不被更新。这包括应该由事务管理器处理的复杂的协调和同步逻辑。在 Java 世界中,JTA 是实现全局事务的事实上的标准。
Spring 支持 JTA 事务和本地事务,并在业务代码中隐藏了这种逻辑。在这一节中,我们将演示如何通过在 Spring 中使用 JTA 来实现全局事务。
实施 JTA 样本的基础设施
我们使用的表格与本章前面的示例中的表格相同。然而,嵌入式 H2 数据库并不完全支持 XA(至少在编写本文时是这样),所以在本例中,我们使用 MySQL 作为后端数据库。
我们还想展示如何在独立应用或 web 容器环境中实现与 JTA 的全局事务。因此,在这个例子中,我们使用 Atomikos ( www.atomikos.com/Main/TransactionsEssentials ),这是一个广泛用于非 JEE 环境的开源 JTA 事务管理器。
为了展示全局事务是如何工作的,我们至少需要两个后端资源。为了简单起见,我们将使用一个 MySQL 数据库和两个 JPA 实体管理器来模拟用例。效果是一样的,因为不同的后端数据库有多个 JPA 持久性单元。
在 MySQL 数据库中,我们创建了两个模式和相应的用户,如以下 DDL 脚本所示:
CREATE USER 'prospring5_a'@'localhost' IDENTIFIED BY 'prospring5_a';
CREATE SCHEMA MUSICDB_A;
GRANT ALL PRIVILEGES ON MUSICDB_A . * TO 'prospring5_a'@'localhost';
PRIVILEGES;
CREATE USER 'prospring5_b'@'localhost' IDENTIFIED BY 'prospring5_b';
CREATE SCHEMA MUSICDB_B;
GRANT ALL PRIVILEGES ON MUSICDB_B . * TO 'prospring5_b'@'localhost';
PRIVILEGES;
设置完成后,我们可以继续进行 Spring 配置和实现。
实施与 JTA 的全球交易
首先我们来看看 Spring 的配置。下面的代码片段描述了声明访问两个数据库所需的 beans 的XAJpaConfig配置类:
package com.apress.prospring5.ch9.config;
import com.atomikos.jdbc.AtomikosDataSourceBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.SimpleDriverDataSource;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.sql.Driver;
import java.util.Properties;
@Configuration
@EnableJpaRepositories
public class XAJpaConfig {
private static Logger logger = LoggerFactory.getLogger(XAJpaConfig.class);
@SuppressWarnings("unchecked")
@Bean(initMethod = "init", destroyMethod = "close")
public DataSource dataSourceA() {
try {
AtomikosDataSourceBean dataSource = new AtomikosDataSourceBean();
dataSource.setUniqueResourceName("XADBMSA");
dataSource.setXaDataSourceClassName(
"com.mysql.cj.jdbc.MysqlXADataSource");
dataSource.setXaProperties(xaAProperties());
dataSource.setPoolSize(1);
return dataSource;
} catch (Exception e) {
logger.error("Populator DataSource bean cannot be created!", e);
return null;
}
}
@Bean
public Properties xaAProperties() {
Properties xaProp = new Properties();
xaProp.put("databaseName", "musicdb_a");
xaProp.put("user", "prospring5_a");
xaProp.put("password", "prospring5_a");
return xaProp;
}
@SuppressWarnings("unchecked")
@Bean(initMethod = "init", destroyMethod = "close")
public DataSource dataSourceB() {
try {
AtomikosDataSourceBean dataSource = new AtomikosDataSourceBean();
dataSource.setUniqueResourceName("XADBMSB");
dataSource.setXaDataSourceClassName(
"com.mysql.cj.jdbc.MysqlXADataSource");
dataSource.setXaProperties(xaBProperties());
dataSource.setPoolSize(1);
return dataSource;
} catch (Exception e) {
logger.error("Populator DataSource bean cannot be created!", e);
return null;
}
}
@Bean
public Properties xaBProperties() {
Properties xaProp = new Properties();
xaProp.put("databaseName", "musicdb_b");
xaProp.put("user", "prospring5_b");
xaProp.put("password", "prospring5_b");
return xaProp;
}
@Bean
public Properties hibernateProperties() {
Properties hibernateProp = new Properties();
hibernateProp.put("hibernate.transaction.factory_class",
"org.hibernate.transaction.JTATransactionFactory");
hibernateProp.put("hibernate.transaction.jta.platform",
"com.atomikos.icatch.jta.hibernate4.AtomikosPlatform");
// required by Hibernate 5
hibernateProp.put("hibernate.transaction.coordinator_class", "jta");
hibernateProp.put("hibernate.dialect",
"org.hibernate.dialect.MySQL5Dialect");
// this will work only if users/schemas are created first,
// use ddl.sql script for this
hibernateProp.put("hibernate.hbm2ddl.auto", "create-drop");
hibernateProp.put("hibernate.show_sql", true);
hibernateProp.put("hibernate.max_fetch_depth", 3);
hibernateProp.put("hibernate.jdbc.batch_size", 10);
hibernateProp.put("hibernate.jdbc.fetch_size", 50);
return hibernateProp;
}
@Bean
public EntityManagerFactory emfA() {
LocalContainerEntityManagerFactoryBean factoryBean =
new LocalContainerEntityManagerFactoryBean();
factoryBean.setPackagesToScan("com.apress.prospring5.ch9.entities");
factoryBean.setDataSource(dataSourceA());
factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
factoryBean.setJpaProperties(hibernateProperties());
factoryBean.setPersistenceUnitName("emfA");
factoryBean.afterPropertiesSet();
return factoryBean.getObject();
}
@Bean
public EntityManagerFactory emfB() {
LocalContainerEntityManagerFactoryBean factoryBean =
new LocalContainerEntityManagerFactoryBean();
factoryBean.setPackagesToScan("com.apress.prospring5.ch9.entities");
factoryBean.setDataSource(dataSourceB());
factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
factoryBean.setJpaProperties(hibernateProperties());
factoryBean.setPersistenceUnitName("emfB");
factoryBean.afterPropertiesSet();
return factoryBean.getObject();
}
}
配置长但不太复杂。首先,定义两个DataSourcebean 来表示两个数据库资源。bean 名称是dataSourceA和dataSourceB,它们分别连接到模式musicdb_a和musicdb_a。两个DataSourcebean 都使用类com.atomikos.jdbc.AtomikosDataSourceBean,该类支持 XA 兼容的DataSource,在两个 bean 的定义中,定义了 MySQL 的 XA DataSource实现类:com.mysql.cj.jdbc.MysqlXADataSource,它是 MySQL 的资源管理器。然后,提供数据库连接信息。注意,poolSize属性定义了 Atomikos 需要维护的连接池中的连接数。这不是强制性的。但是,如果没有提供该属性,Atomikos 将使用默认值 1。
然后,定义两个EntityManagerFactorybean,命名为emfA和emfB。常见的 JPA 属性一起包装在hibernateProperties bean 中。两个 beans 唯一的区别就是被注入了相应的数据源(即dataSourceA注入了emfA,而dataSourceB注入了emfB)。因此,emfA将通过dataSourceA bean 连接到 MySQL 的prospring5_a模式,而emfB将通过dataSourceB bean 连接到prospring5_b模式。看看emfBase bean 中的属性hibernate.transaction.factory_class和hibernate.transaction.jta.platform。这两个属性非常重要,因为 Hibernate 使用它们来查找底层的UserTransaction和TransactionManagerbean,以参与它管理的全局事务的持久性上下文。同样重要的是让 Hibernate 4 的 Atomikos 类与 Hibernate 5 一起工作所需的hibernate.transaction.coordinator_class。 4
下面的代码片段描述了ServicesConfig,它声明了用于实现全局事务管理的 beans:
package com.apress.prospring5.ch9.config;
import com.atomikos.icatch.config.UserTransactionService;
import com.atomikos.icatch.config.UserTransactionServiceImp;
import com.atomikos.icatch.jta.UserTransactionImp;
import com.atomikos.icatch.jta.UserTransactionManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.jta.JtaTransactionManager;
import javax.transaction.SystemException;
import javax.transaction.UserTransaction;
import java.util.Properties;
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = "com.apress.prospring5.ch9.services")
public class ServicesConfig {
private Logger logger = LoggerFactory.getLogger(ServicesConfig.class);
@Bean(initMethod = "init", destroyMethod = "shutdownForce")
public UserTransactionService userTransactionService(){
Properties atProps = new Properties();
atProps.put("com.atomikos.icatch.service",
"com.atomikos.icatch.standalone.UserTransactionServiceFactory");
return new UserTransactionServiceImp(atProps);
}
@Bean (initMethod = "init", destroyMethod = "close")
@DependsOn("userTransactionService")
public UserTransactionManager atomikosTransactionManager(){
UserTransactionManager utm = new UserTransactionManager();
utm.setStartupTransactionService(false);
utm.setForceShutdown(true);
return utm;
}
@Bean
@DependsOn("userTransactionService")
public UserTransaction userTransaction(){
UserTransactionImp ut = new UserTransactionImp();
try {
ut.setTransactionTimeout(300);
} catch (SystemException se) {
logger.error("Configuration exception.", se);
return null;
}
return ut;
}
@Bean
public PlatformTransactionManager transactionManager(){
JtaTransactionManager ptm = new JtaTransactionManager();
ptm.setTransactionManager(atomikosTransactionManager());
ptm.setUserTransaction(userTransaction());
return ptm;
}
}
对于 Atomikos 部分,定义了两个 bean,即atomikosTransactionManager和atomikosUserTransactionbean。实现类由 Atomikos 提供,它分别实现了标准的 Spring org.springframework.transaction.PlatformTransactionManager和javax.transaction.UserTransaction接口。这些 beans 提供 JTA 所需的事务协调和同步服务,并通过支持 2PC 的 XA 协议与资源管理器通信。然后,定义 Spring 的transactionManager bean(用org.springframework.transaction.jta.JtaTransactionManager作为实现类),注入 Atomikos 提供的两个事务 bean。这指示 Spring 使用 Atomikos JTA 进行事务管理。另外,请注意用于配置 Atomikos 事务服务来管理未决事务的UserTransactionService bean。 5
下面的代码片段显示了 JTA 的SingerServiceImpl类。注意,为了简单起见,只实现了save()方法。
package com.apress.prospring5.ch9.services;
import com.apress.prospring5.ch9.entities.Singer;
import org.apache.commons.lang3.NotImplementedException;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceException;
import java.util.List;
@Service("singerService")
@Repository
@Transactional
public class SingerServiceImpl implements SingerService {
@PersistenceContext(unitName = "emfA")
private EntityManager emA;
@PersistenceContext(unitName = "emfB")
private EntityManager emB;
@Override
@Transactional(readOnly = true)
public List<Singer> findAll() {
throw new NotImplementedException("findAll");
}
@Override
@Transactional(readOnly = true)
public Singer findById(Long id) {
throw new NotImplementedException("findById");
}
@Override
public Singer save(Singer singer) {
Singer singerB = new Singer();
singerB.setFirstName(singer.getFirstName());
singerB.setLastName(singer.getLastName());
if (singer.getId() == null) {
emA.persist(singer);
emB.persist(singerB);
//throw new JpaSystemException(new PersistenceException());
} else {
emA.merge(singer);
emB.merge(singer);
}
return singer;
}
@Override
public long countAll() {
return 0;
}
}
定义的两个实体管理器被注入到SingerServiceImpl类中。在save()方法中,我们将联系对象分别持久化到两个模式中。此刻忽略抛出异常语句;稍后我们将使用它来验证当保存到模式prospring5_b失败时,事务被回滚。以下代码片段显示了测试程序:
package com.apress.prospring5.ch9;
import com.apress.prospring5.ch9.config.ServicesConfig;
import com.apress.prospring5.ch9.config.XAJpaConfig;
import com.apress.prospring5.ch9.entities.Singer;
import com.apress.prospring5.ch9.services.SingerService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import java.util.Date;
import java.util.GregorianCalendar;
public class TxJtaDemo {
private static Logger logger = LoggerFactory.getLogger(TxJtaDemo.class);
public static void main(String... args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(ServicesConfig.class,
XAJpaConfig.class);
SingerService singerService = ctx.getBean(SingerService.class);
Singer singer = new Singer();
singer.setFirstName("John");
singer.setLastName("Mayer");
singer.setBirthDate(new Date(
(new GregorianCalendar(1977, 9, 16)).getTime().getTime()));
singerService.save(singer);
if (singer.getId() != null) {
logger.info("--> Singer saved successfully");
} else {
logger.info("--> Singer was not saved, check the configuration!!");
}
ctx.close();
}
}
程序创建一个新的 contact 对象并调用SingerService.save()方法。该实现将尝试将同一个对象保存到两个数据库中。假设一切顺利,运行程序会产生以下输出(另一个输出被省略):
--> Singer saved successfully
Atomikos 创建一个复合事务,与 XA DataSource(这里是 MySQL)通信,执行同步,提交事务,等等。从数据库中,您将看到新的联系人被分别保存到数据库的两个模式中。但是如果您想检查代码中的保存,您可以为findAll()方法提供一个实现来完成这项工作。
package com.apress.prospring5.ch9.services;
...
@Service("singerService")
@Repository
@Transactional
public class SingerServiceImpl implements SingerService {
private static final String FIND_ALL= "select s from Singer s";
@PersistenceContext(unitName = "emfA")
private EntityManager emA;
@PersistenceContext(unitName = "emfB")
private EntityManager emB;
@Override
@Transactional(readOnly = true)
public List<Singer> findAll()
{
List<Singer> singersFromA = findAllInA();
List<Singer> singersFromB = findAllInB();
if (singersFromA.size()!= singersFromB.size()){
throw new AsyncXAResourcesException("
XA resources do not contain the same expected data.");
}
Singer sA = singersFromA.get(0);
Singer sB = singersFromB.get(0);
if (!sA.getFirstName().equals(sB.getFirstName())) {
throw new AsyncXAResourcesException("
XA resources do not contain the same expected data.");
}
List<Singer> singersFromBoth = new ArrayList<>();
singersFromBoth.add(sA);
singersFromBoth.add(sB);
return singersFromBoth;
}
private List<Singer> findAllInA(){
return emA.createQuery(FIND_ALL).getResultList();
}
private List<Singer> findAllInB(){
return emB.createQuery(FIND_ALL).getResultList();
}
...
}
因此,测试保存在两个数据库中的歌手的代码可以修改如下:
package com.apress.prospring5.ch9;
...
public class TxJtaDemo {
private static Logger logger = LoggerFactory.getLogger(TxJtaDemo.class);
public static void main(String... args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(ServicesConfig.class,
XAJpaConfig.class);
SingerService singerService = ctx.getBean(SingerService.class);
Singer singer = new Singer();
singer.setFirstName("John");
singer.setLastName("Mayer");
singer.setBirthDate(new Date(
(new GregorianCalendar(1977, 9, 16)).getTime().getTime()));
singerService.save(singer);
if (singer.getId() != null) {
logger.info("--> Singer saved successfully");
} else {
logger.error("--> Singer was not saved, check the configuration!!");
}
// check saving in both databases
List<Singer> singers = singerService.findAll();
if (singers.size()!= 2) {
logger.error("--> Something went wrong.");
} else {
logger.info("--> Singers form both DBs: " + singers);
}
ctx.close();
}
}
现在让我们看看回滚是如何工作的。如下面的代码片段所示,我们没有调用emB.persist(),而是抛出一个异常来模拟出了问题,数据无法保存在第二个数据库中。
package com.apress.prospring5.ch9.services;
...
@Service("singerService")
@Repository
@Transactional
public class SingerServiceImpl implements SingerService {
private static final String FIND_ALL= "select s from Singer s";
@PersistenceContext(unitName = "emfA")
private EntityManager emA;
@PersistenceContext(unitName = "emfB")
private EntityManager emB;
...
@Override
public Singer save(Singer singer) {
Singer singerB = new Singer();
singerB.setFirstName(singer.getFirstName());
singerB.setLastName(singer.getLastName());
if (singer.getId() == null) {
emA.persist(singer);
if(true) {
throw new JpaSystemException(new PersistenceException(
"Simulation of something going wrong."));
}
emB.persist(singerB);
} else {
emA.merge(singer);
emB.merge(singer);
}
return singer;
}
@Override
public long countAll() {
return 0;
}
}
再次运行该程序会产生以下结果:
...
INFO o.h.h.i.QueryTranslatorFactoryInitiator - HHH000397:
Using ASTQueryTranslatorFactory
INFO o.s.o.j.LocalContainerEntityManagerFactoryBean - Initialized JPA
EntityManagerFactory for persistence unit 'emfA'
INFO o.s.o.j.LocalContainerEntityManagerFactoryBean - Initialized JPA
EntityManagerFactory for persistence unit 'emfB'
INFO o.s.t.j.JtaTransactionManager - Using JTA UserTransaction:
com.atomikos.icatch.jta.UserTransactionImp@6da9dc6
INFO o.s.t.j.JtaTransactionManager - Using JTA TransactionManager:
com.atomikos.icatch.jta.UserTransactionManager@2216effc
DEBUG o.s.t.j.JtaTransactionManager - Creating new transaction with name
[com.apress.prospring5.ch9.services.SingerServiceImpl.save]:
PROPAGATION_REQUIRED,ISOLATION_DEFAULT; ''
DEBUG o.s.o.j.EntityManagerFactoryUtils - Opening JPA EntityManager
DEBUG o.s.o.j.EntityManagerFactoryUtils - Registering transaction synchronization
for JPA EntityManager
Hibernate: insert into singer (BIRTH_DATE, FIRST_NAME, LAST_NAME, VERSION)
values (?, ?, ?, ?)
DEBUG o.s.o.j.EntityManagerFactoryUtils - Closing JPA EntityManager
DEBUG o.s.t.j.JtaTransactionManager - Initiating transaction rollback
WARN c.a.j.AbstractConnectionProxy - Forcing close of pending statement:
com.mysql.cj.jdbc.PreparedStatementWrapper@3f685162
Exception in thread "main" org.springframework.orm.jpa.JpaSystemException:
Simulation of something going wrong.;
...
Caused by: javax.persistence.PersistenceException:
Simulation of something going wrong.
如前面的输出所示,第一个歌手被持久化(注意insert语句)。但是,当保存到第二个DataSource时,因为抛出了异常,Atomikos 将回滚整个事务。你可以看一下模式musicdb_a来检查新歌手没有被保存。
春船 JTA
JTA Spring Boot 入门版开箱即用,带有一组预配置的 beans,旨在帮助您专注于代码的业务功能,而不是环境设置。再说一遍,这是所有 Spring Boot 入门库都做的事情,不管是什么组件,所以前面的句子可能看起来有点多余。JTA 版的 Spring Boot 包含一个使用 Atomikos 的库,它会提取适当的库并为您配置 Atomikos 组件。将前面的例子迁移到 Spring Boot 意味着将DataSource和事务管理器配置导入到 Spring Boot 应用中。但是由于本节的目的是展示 Spring Boot 如何通过所提供的预配置 beans 来帮助加速涉及全局事务管理的应用的开发,因此有必要提供一个不同的示例。我们将假设我们想要向消息队列传输一条消息,表明创建了一个新的Singer实例。显然,如果将Singer记录保存到数据库失败,我们希望回滚事务并阻止消息被发送。为了总结这个例子,我们需要做以下事情:
-
Configure the Spring Boot Gradle project for JTA and JMS usage. The configuration is as follows:
//build.gradle ext { ... bootVersion = '2.0.0.M1' atomikosVersion = '4.0.4' boot = [ ... starterJpa : "org.springframework.boot:spring-boot-starter-data-jpa:$bootVersion", starterJta : "org.springframework.boot:spring-boot-starter-jta-atomikos:$bootVersion", starterJms : "org.springframework.boot:spring-boot-starter-artemis:$bootVersion" ] misc = [ ... artemis : "org.apache.activemq:artemis-jms-server:2.1.0" ] db = [ ... h2 : "com.h2database:h2:$h2Version" ] } //chapter09/boot-jta/build.gradle buildscript { repositories { ... } dependencies { classpath boot.springBootPlugin } } apply plugin: 'org.springframework.boot' dependencies { compile boot.starterJpa, boot.starterJta, boot.starterJms, db.h2 compilemisc.artemis { exclude group: 'org.apache.geronimo.specs', module: 'geronimo-jms_2.0_spec' } }In Figure 9-3 you can see the Spring Boot starter libraries declared earlier as dependencies for the project, and you can see the dependencies they add to the project.
![A315511_5_En_9_Fig3_HTML.jpg]()
图 9-3。
Spring Boot Starter libraries and their dependencies
-
定义
Singer实体类和处理它的存储库。Singer实体类的结构与前面提到的相同,但是没有任何相关的实体。并且SingerRepository将被留空,因为只有CrudRepository已经提供的方法将在本例中使用:save(..)和count()。 -
定义一个将保存
Singer记录并发送确认消息的服务类。package com.apress.prospring5.ch9.services; import com.apress.prospring5.ch9.entities.Singer; import com.apress.prospring5.ch9.ex.AsyncXAResourcesException; import com.apress.prospring5.ch9.repos.SingerRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jms.core.JmsTemplate; import org.springframework.stereotype.Repository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; @Service("singerService") @Transactional public class SingerServiceImpl implements SingerService { private SingerRepository singerRepository; private JmsTemplate jmsTemplate; public SingerServiceImpl(SingerRepository singerRepository, JmsTemplate jmsTemplate) { this.singerRepository = singerRepository; this.jmsTemplate = jmsTemplate; } @Override public Singer save(Singer singer) { jmsTemplate.convertAndSend("singers", "Just saved:" + singer); if(singer == null) { throw new AsyncXAResourcesException( "Simulation of something going wrong."); } singerRepository.save(singer); return singer; } @Override public long count() { return singerRepository.count(); } }不需要
@Autowired注释来注入存储库 bean,对于JmsTemplate也是如此。Spring Boot 就是这么神奇,它注入所需的豆子,如果只有它们被声明的话。 -
一个 bean,它将监听传递到
singers队列的消息并打印它们。package com.apress.prospring5.ch9; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.jms.annotation.JmsListener; import org.springframework.stereotype.Component; @Component public class Messages { private static Logger logger = LoggerFactory.getLogger(Messages.class); @JmsListener(destination="singers") public void onMessage(String content){ logger.info("--> Received content: " + content); } } -
配置 Artemis JMS 服务器,创建一个名为
singers的嵌入式队列。这是通过在application.properties文件中用值singers设置spring.artemis.embedded.queues属性来完成的,该文件可用于配置 Spring Boot 应用。spring.artemis.embedded.queues=singers spring.jta.log-dir=out前面的配置片段描述了
application.properties文件的内容。除了spring.artemis.embedded.queues属性之外,spring.jta.log-dir用于设置 Atomikos 应该在哪里写入 JTA 日志,在本例中设置了out目录。 -
下面是一个应用类,它将所有这些打包在一起并进行测试:
package com.apress.prospring5.ch9; import com.apress.prospring5.ch9.entities.Singer; import com.apress.prospring5.ch9.services.SingerService; import com.atomikos.jdbc.AtomikosDataSourceBean; import org.h2.jdbcx.JdbcDataSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; import javax.persistence.EntityManagerFactory; import javax.sql.DataSource; import java.util.Date; import java.util.GregorianCalendar; import java.util.Properties; import static org.hibernate.cfg.AvailableSettings.*; import static org.hibernate.cfg.AvailableSettings.STATEMENT_FETCH_SIZE; @SpringBootApplication(scanBasePackages = "com.apress.prospring5.ch9.services") public class Application implements CommandLineRunner { private static Logger logger = LoggerFactory.getLogger(Application.class); public static void main(String... args) throws Exception { ConfigurableApplicationContext ctx = SpringApplication.run(Application.class, args); System.in.read(); ctx.close(); } @Autowired SingerService singerService; @Override public void run(String... args) throws Exception { Singer singer = new Singer(); singer.setFirstName("John"); singer.setLastName("Mayer"); singer.setBirthDate(new Date( (new GregorianCalendar(1977, 9, 16)).getTime().getTime())); singerService.save(singer); long count = singerService.count(); if (count == 1) { logger.info("--> Singer saved successfully"); } else { logger.error("--> Singer was not saved, check the configuration!!"); } try { singerService.save(null); } catch (Exception ex) { logger.error(ex.getMessage() + "Final count:" + singerService.count()); } } }
如果您运行Application,您将看到类似如下的输出:
...
INFO c.a.j.AtomikosConnectionFactoryBean - AtomikosConnectionFactoryBean
'jmsConnectionFactory': init...
INFO o.s.t.j.JtaTransactionManager - Using JTA UserTransaction:
com.atomikos.icatch.jta.UserTransactionManager@408a247c
INFO c.a.j.AtomikosJmsXaSessionProxy - atomikos xa session proxy for resource
jmsConnectionFactory: calling createQueue on JMS driver session...
INFO c.a.j.AtomikosJmsXaSessionProxy - atomikos xa session proxy for resource
jmsConnectionFactory: calling getTransacted on JMS driver session...
DEBUG o.s.t.j.JtaTransactionManager - Participating in existing transaction
DEBUG o.s.t.j.JtaTransactionManager - Initiating transaction commit
INFO c.a.d.x.XAResourceTransaction - XAResource.start ...
INFO c.a.d.x.XAResourceTransaction - XAResource.end ...
DEBUG o.s.t.j.JtaTransactionManager - Initiating transaction commit
DEBUG o.s.t.j.JtaTransactionManager - Creating new transaction with name
[com.apress.prospring5.ch9.services.SingerServiceImpl.save]:
PROPAGATION_REQUIRED,ISOLATION_DEFAULT; ''
INFO c.a.i.i.BaseTransactionManager - createCompositeTransaction ( 10000 ):
created new ROOT transaction with id 127.0.0.1.tm0000200001
DEBUG o.s.t.j.JtaTransactionManager - Participating in existing transaction
INFO c.a.p.c.Application - --> Singer saved successfully
...//etc
从日志中,您可以清楚地看到为每个操作创建和重用的全局事务。如果您通过按任意键正常退出应用,然后回车,请耐心等待,因为应用需要一段时间才能正常关闭。
以下是关于使用 Spring Boot 创建 JTA 应用的一些结论:虽然看起来很容易,但是当处理多个数据源时,配置环境是您无法回避的事情。此外,如果 JTA 提供者是由 JEE 服务器提供的,事情会变得相当复杂。但是对于用于教育目的和测试的示例应用,它非常实用。
关于使用 JTA 事务管理器的思考
是否使用 JTA 进行全球交易管理正在激烈辩论中。例如,Spring 开发团队通常不推荐使用 JTA 进行全局事务。
作为一般原则,当您的应用被部署到一个成熟的 JEE 应用服务器时,不使用 JTA 是没有意义的,因为流行的 JEE 应用服务器的所有供应商都已经为他们的平台优化了他们的 JTA 实现。这是你花钱购买的一个主要功能。
对于独立或 web 容器部署,让应用需求驱动您的决策。尽可能早地执行负载测试,以验证使用 JTA 不会影响性能。
一个好消息是,Spring 可以与大多数主流 web 和 JEE 容器中的本地和全局事务无缝协作,所以当您从一种事务管理策略切换到另一种时,通常不需要修改代码。如果您决定在应用中使用 JTA,请确保使用 Spring 的JtaTransactionManager。
摘要
在几乎任何类型的应用中,事务管理都是确保数据完整性的关键部分。在这一章中,我们讨论了如何使用 Spring 来管理事务,而几乎不影响您的源代码。您还学习了如何使用本地和全局事务。
我们提供了各种事务实现的例子,包括使用 XML 配置和注释的声明性方法,以及编程方法。
本地事务在 JEE 应用服务器内部/外部都得到支持,只需要简单的配置就可以在 Spring 中启用本地事务支持。然而,设置一个全局事务环境需要做更多的工作,并且很大程度上取决于您的应用需要与哪个 JTA 提供者和相应的后端资源进行交互。
Footnotes 1
JDO 的支持在 Spring 5 月被放弃;因此,JdoTransactionManager从类图中消失了。
2
Spring 5 只能和 Hibernate 5 一起用;Hibernate 3 和 Hibernate 4 的实现已经删除。
3
对 JMS 1.1 的支持在 Spring 5 中被删除。
4
这个配置是在 Atomikos 官方文档的帮助下创建的,用于在 https://www.atomikos.com/Documentation/SpringIntegration 和 https://stackoverflow.com/questions/33127854/hibernate-5-with-spring-jta 的堆栈溢出社区的帮助下与 Spring Integration。
5
该配置是 XML 配置的注释配置改编,在 https://www.atomikos.com/Documentation/SpringIntegration#The_Advanced_Case_40As_of_3.3_41 的 Atomikos 文档中作为示例给出。
十、带有类型转换和格式的验证
在企业应用中,验证是至关重要的。验证的目的是验证正在处理的数据满足所有预定义的业务需求,并确保数据完整性和在应用的其他层的有用性。
在应用开发中,数据验证总是与转换和格式化一起被提及。原因是数据源的格式很可能与应用中使用的格式不同。例如,在 web 应用中,用户在 web 浏览器前端输入信息。当用户保存该数据时,它被发送到服务器(在本地验证完成之后)。在服务器端,执行数据绑定过程,在该过程中,来自 HTTP 请求的数据被提取、转换并绑定到相应的域对象(例如,用户在 HTML 表单中输入歌手信息,然后绑定到服务器中的Singer对象),这是基于为每个属性定义的格式规则(例如,日期格式模式是yyyy-MM-dd)。当数据绑定完成时,验证规则被应用到域对象,以检查任何约束违反。如果一切运行正常,数据将被持久化,并向用户显示一条成功消息。否则,验证错误消息将被填充并显示给用户。
在本章的第一部分,您将了解 Spring 如何为类型转换、字段格式化和验证提供复杂的支持。具体来说,本章涵盖以下主题:
- Spring 类型转换系统和格式化程序服务提供者接口(SPI):我们介绍泛型类型转换系统和格式化程序 SPI。我们将介绍如何使用新服务来取代以前的
PropertyEditor支持,以及它们如何在任何 Java 类型之间转换。 - Spring 中的验证:我们讨论 Spring 如何支持域对象验证。首先,我们简单介绍一下 Spring 自己的
Validator接口。然后,我们关注 JSR-349 (Bean 验证)支持。
属国
与前几章一样,本章中的示例需要一些依赖项,这些依赖项在下面的配置片段中有所描述。您可能会注意到的一个依赖项是joda-time。如果你运行的是 Java 8,Spring 5 也支持 JSR-310,也就是javax.time API。
//pro-spring-15/build.gradle
ext {
//spring libs
springVersion = '5.0.0.RC1'
jodaVersion = '2.9.9'
javaxValidationVersion = '2.0.0.Beta2' //1.1.0.Final
javaElVersion = '3.0.1-b04' // 3.0.0
glasshfishELVersion = '2.2.1-b05' // 2.2
hibernateValidatorVersion = '6.0.0.Beta2' //5.4.1.Final
spring = [...]
hibernate = [
validator :
"org.hibernate:hibernate-validator:$hibernateValidatorVersion",
...
]
misc = [
validation :
"javax.validation:validation-api:$javaxValidationVersion",
joda : "joda-time:joda-time:$jodaVersion",
...
]
...
}
//chapter10/build.gradle
dependencies {
compile spring.contextSupport, misc.slf4jJcl, misc.logback,
db.h2, misc.lang3, hibernate.em, hibernate.validator,
misc.joda, misc.validation
testCompile testing.junit
}
Spring 式转换系统
在 Spring 3 中,引入了一个新的类型转换系统,提供了一种在 Spring 支持的应用中在任何 Java 类型之间进行转换的强大方法。本节展示了这个新服务如何执行先前的PropertyEditor支持所提供的相同功能,以及它如何支持任何 Java 类型之间的转换。我们还演示了如何使用转换器 SPI 实现自定义类型的转换器。
使用 PropertyEditors 从字符串转换
第三章讲述了 Spring 如何通过支持PropertyEditor s 来处理从属性文件中的String到 POJOs 属性的转换。让我们在这里做一个快速回顾,然后讲述 Spring 的转换器 SPI(从 3.0 开始可用)如何提供一个更强大的选择。
考虑这个简单版本的Singer类:
package com.apress.prospring5.ch10;
import java.net.URL;
import java.text.SimpleDateFormat;
import org.joda.time.DateTime;
public class Singer {
private String firstName;
private String lastName;
private DateTime birthDate;
private URL personalSite;
//getters and setters
...
public String toString() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
return String.format("{First name: %s, Last name: %s,
Birthday: %s, Site: %s}",
firstName, lastName, sdf.format(birthDate.toDate()), personalSite);
}
}
对于birthDate属性,我们使用 JodaTime 的DateTime类。此外,如果适用的话,还有一个URL类型的字段,指示歌手的个人网站。现在假设我们想在 Spring 的ApplicationContext中构造Singer对象,其值存储在 Spring 的配置文件或属性文件中。下面的配置片段显示了 Spring XML 配置文件(prop-editor-app-context.xml):
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util.xsd">
<context:annotation-config/>
<context:property-placeholder location="classpath:application.properties"/>
<bean id="customEditorConfigurer"
class="org.springframework.beans.factory.config.CustomEditorConfigurer"
p:propertyEditorRegistrars-ref="propertyEditorRegistrarsList"/>
<util:list id="propertyEditorRegistrarsList">
<bean class="com.apress.prospring5.ch10.DateTimeEditorRegistrar">
<constructor-arg value="${date.format.pattern}"/>
</bean>
</util:list>
<bean id="eric" class="com.apress.prospring5.ch10.Singer"
p:firstName="Eric"
p:lastName="Clapton"
p:birthDate="1945-03-30"
p:personalSite="http://www.ericclapton.com"/>
<bean id="countrySinger" class="com.apress.prospring5.ch10.Singer"
p:firstName="${countrySinger.firstName}"
p:lastName="${countrySinger.lastName}"
p:birthDate="${countrySinger.birthDate}"
p:personalSite="${countrySinger.personalSite}"/>
</beans>
这里我们构造了两个不同的Singer类的 beans。eric bean 是用配置文件中提供的值构造的,而对于countrysinger bean,属性被外化到一个属性文件中。此外,还定义了一个自定义编辑器,用于从String到 JodaTime 的DateTime类型的转换,日期时间格式模式也在属性文件中具体化了。下面的代码片段显示了属性文件(application.properties):
date.format.pattern=yyyy-MM-dd
countrySinger.firstName=John
countrySinger.lastName=Mayer
countrySinger.birthDate=1977-10-16
countrySinger.personalSite=http://johnmayer.com/
以下代码片段显示了用于将String值转换为 JodaTime DateTime类型的自定义编辑器:
package com.apress.prospring5.ch10;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.springframework.beans.PropertyEditorRegistrar;
import org.springframework.beans.PropertyEditorRegistry;
import java.beans.PropertyEditorSupport;
public class DateTimeEditorRegistrar implements PropertyEditorRegistrar {
private DateTimeFormatter dateTimeFormatter;
public DateTimeEditorRegistrar(String dateFormatPattern) {
dateTimeFormatter = DateTimeFormat.forPattern(dateFormatPattern);
}
@Override
public void registerCustomEditors(PropertyEditorRegistry registry) {
registry.registerCustomEditor(DateTime.class,
new DateTimeEditor(dateTimeFormatter));
}
private static class DateTimeEditor extends PropertyEditorSupport {
private DateTimeFormatter dateTimeFormatter;
public DateTimeEditor(DateTimeFormatter dateTimeFormatter) {
this.dateTimeFormatter = dateTimeFormatter;
}
@Override
public void setAsText(String text) throws IllegalArgumentException {
setValue(DateTime.parse(text, dateTimeFormatter));
}
}
}
DateTimeEditorRegistrar实现PropertyEditorRegister接口来注册我们的自定义PropertyEditor。然后我们创建一个名为DateTimeEditor的内部类,处理从String到DateTime的转换。我们在这个例子中使用了一个内部类,因为它只被PropertyEditorRegistrar实现访问。现在我们来测试一下。下一个代码片段显示了测试程序:
package com.apress.prospring5.ch10;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.support.GenericXmlApplicationContext;
public class PropEditorDemo {
private static Logger logger =
LoggerFactory.getLogger(PropEditorDemo.class);
public static void main(String... args) {
GenericXmlApplicationContext ctx =
new GenericXmlApplicationContext();
ctx.load("classpath:spring/prop-editor-app-context.xml");
ctx.refresh();
Singer eric = ctx.getBean("eric", Singer.class);
logger.info("Eric info: " + eric);
Singer countrySinger = ctx.getBean("countrySinger", Singer.class);
logger.info("John info: " + countrySinger);
ctx.close();
}
}
如您所见,这两个Singerbean 是从ApplicationContext中检索并打印出来的。运行该程序会产生以下输出:
[main] INFO c.a.p.c.PropEditorDemo - Eric info: {First name: Eric,
Last name: Clapton, Birthday: 1945-03-30, Site: http://www.ericclapton.com}
[main] INFO c.a.p.c.PropEditorDemo - John info: {First name: John,
Last name: Mayer, Birthday: 1977-10-16, Site: http://johnmayer.com/}
如输出所示,属性被转换并应用于Singerbean。这里使用 XML 而不是 Java 配置类的原因是,要注入的值被声明为文本值,Spring 在后台透明地进行转换。
引入 Spring 类型转换
在 Spring 3.0 中,引入了一个通用类型转换系统,它位于包org.springframework.core.convert下。除了提供对PropertyEditor支持的替代,类型转换系统可以被配置成在任何 Java 类型和 POJOs 之间进行转换(而PropertyEditor则专注于将属性文件中的String表示转换成 Java 类型)。
实现自定义转换器
要查看类型转换系统的运行情况,让我们重新看看前面的例子,使用同一个Singer类。假设这次我们想使用类型转换系统将String格式的日期转换成Singer的birthDate属性,该属性属于 JodaTime 的DateTime类型。为了支持转换,我们通过实现org.springframework.core.convert.converter.Converter<S,T>接口来创建一个定制的转换器,而不是创建一个定制的PropertyEditor。以下代码片段显示了自定义转换器:
package com.apress.prospring5.ch10;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.springframework.core.convert.converter.Converter;
import javax.annotation.PostConstruct;
public class StringToDateTimeConverter implements Converter<String, DateTime> {
private static final String DEFAULT_DATE_PATTERN = "yyyy-MM-dd";
private DateTimeFormatter dateFormat;
private String datePattern = DEFAULT_DATE_PATTERN;
public String getDatePattern() {
return datePattern;
}
public void setDatePattern(String datePattern) {
this.datePattern = datePattern;
}
@PostConstruct
public void init() {
dateFormat = DateTimeFormat.forPattern(datePattern);
}
@Override
public DateTime convert(String dateString) {
return dateFormat.parseDateTime(dateString);
}
}
我们实现了接口Converter<String, DateTime>,这意味着转换器负责将一个String(源类型S)转换成一个DateTime(目标类型T)。日期时间模式的注入是可选的,可以通过调用 setter setDatePattern来完成。如果没有注入,则使用默认模式yyyy-MM-dd。然后,在初始化方法(用@PostConstruct注释的init()方法)中,构造一个 JodaTime 的DateTimeFormat类的实例,它将根据指定的模式执行转换。最后,实现convert()方法来提供转换逻辑。
配置 ConversionService
为了使用转换服务而不是PropertyEditor,我们需要在 Spring 的ApplicationContext中配置一个org.springframework.core.convert.ConversionService接口的实例。以下代码片段显示了 Java 配置类:
package com.apress.prospring5.ch10.config;
import com.apress.prospring5.ch10.Singer;
import com.apress.prospring5.ch10.StringToDateTimeConverter;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.support.ConversionServiceFactoryBean;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.convert.converter.Converter;
import java.net.URL;
import java.util.HashSet;
import java.util.Set;
@PropertySource("classpath:application.properties")
@Configuration
@ComponentScan(basePackages = "com.apress.prospring5.ch10")
public class AppConfig {
@Value("${date.format.pattern}")
private String dateFormatPattern;
@Bean
public static PropertySourcesPlaceholderConfigurer
propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
@Bean
public Singer john(@Value("${countrySinger.firstName}") String firstName,
@Value("${countrySinger.lastName}") String lastName,
@Value("${countrySinger.personalSite}") URL personalSite,
@Value("${countrySinger.birthDate}") DateTime birthDate)
throws Exception {
Singer singer = new Singer();
singer.setFirstName(firstName);
singer.setLastName(lastName);
singer.setPersonalSite(personalSite);
singer.setBirthDate(birthDate);
return singer;
}
@Bean
public ConversionServiceFactoryBean conversionService() {
ConversionServiceFactoryBean conversionServiceFactoryBean =
new ConversionServiceFactoryBean();
Set<Converter> convs = new HashSet<>();
convs.add(converter());
conversionServiceFactoryBean.setConverters(convs);
conversionServiceFactoryBean.afterPropertiesSet();
return conversionServiceFactoryBean;
}
@Bean
StringToDateTimeConverter converter(){
StringToDateTimeConverter conv = new StringToDateTimeConverter();
conv.setDatePattern(dateFormatPattern);
conv.init();
return conv;
}
}
这些值从一个属性文件中读取,该文件的内容与上一节中介绍的文件相同,并使用@Value注释注入到创建的 bean 中。
这里我们通过用类ConversionServiceFactoryBean声明一个conversionService bean 来指示 Spring 使用类型转换系统。如果没有定义转换服务 bean,Spring 将使用基于PropertyEditor的系统。
默认情况下,类型转换服务支持常见类型之间的转换,包括字符串、数字、枚举、集合、映射等。此外,在基于PropertyEditor的系统中,支持从String到 Java 类型的转换。
对于conversionService bean,配置了一个自定义转换器,用于从String到DateTime的转换。测试程序如下所示:
package com.apress.prospring5.ch10;
import com.apress.prospring5.ch10.config.AppConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
public class ConvServDemo {
private static Logger logger = LoggerFactory.getLogger(ConvServDemo.class);
public static void main(String... args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
Singer john = ctx.getBean("john", Singer.class);
logger.info("Singer info: " + john);
ctx.close();
}
}
运行测试程序会产生以下输出:
15:41:09.960 main INFO c.a.p.c.ConvServDemo - Singer info: {First name: John,
Last name: Mayer, Birthday: 1977-10-16, Site: http://johnmayer.com/}
如您所见,john bean 的属性转换结果与我们使用PropertyEditor s 时的结果相同。
在任意类型之间转换
类型转换系统的真正优势是能够在任意类型之间进行转换。为了查看它的运行情况,假设我们有另一个名为AnotherSinger的类,它与Singer类相同。代码如下所示:
package com.apress.prospring5.ch10;
import java.net.URL;
import java.text.SimpleDateFormat;
import org.joda.time.DateTime;
public class AnotherSinger {
private String firstName;
private String lastName;
private DateTime birthDate;
private URL personalSite;
//seters and getters
...
public String toString() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
return String.format("{First name: %s, Last name: %s,
Birthday: %s, Site: %s}", firstName, lastName,
sdf.format(birthDate.toDate()), personalSite);
}
}
我们希望能够将Singer类的任何实例转换成AnotherSinger类。转换后,Singer的firstName和lastName值将分别变成AnotherSinger的lastName和firstName。让我们实现另一个自定义转换器来执行转换。以下代码片段显示了自定义转换器:
package com.apress.prospring5.ch10;
import org.springframework.core.convert.converter.Converter;
public class SingerToAnotherSingerConverter
implements Converter<Singer, AnotherSinger> {
@Override
public AnotherSinger convert(Singer singer) {
AnotherSinger anotherSinger = new AnotherSinger();
anotherSinger.setFirstName(singer.getLastName());
anotherSinger.setLastName(singer.getFirstName());
anotherSinger.setBirthDate(singer.getBirthDate());
anotherSinger.setPersonalSite(singer.getPersonalSite());
return anotherSinger;
}
}
类是简单的;只需在Singer和AnotherSinger类之间交换firstName和lastName属性值。要将自定义转换器注册到ApplicationContext中,请用以下代码片段中的代码片段替换AppConfig类中的conversionService bean 的定义:
package com.apress.prospring5.ch10.config;
import com.apress.prospring5.ch10.Singer;
import com.apress.prospring5.ch10.SingerToAnotherSingerConverter;
import com.apress.prospring5.ch10.StringToDateTimeConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ConversionServiceFactoryBean;
import org.springframework.core.convert.converter.Converter;
import java.net.URL;
import java.util.HashSet;
import java.util.Set;
@Configuration
@ComponentScan(basePackages = "com.apress.prospring5.ch10")
public class AppConfig {
@Bean
public Singer john() throws Exception {
Singer singer = new Singer();
singer.setFirstName("John");
singer.setLastName("Mayer");
singer.setPersonalSite(new URL("http://johnmayer.com/"));
singer.setBirthDate(converter().convert("1977-10-16"));
return singer;
}
@Bean
public ConversionServiceFactoryBean conversionService() {
ConversionServiceFactoryBean conversionServiceFactoryBean =
new ConversionServiceFactoryBean();
Set<Converter> convs = new HashSet<>();
convs.add(converter());
convs.add(singerConverter());
conversionServiceFactoryBean.setConverters(convs);
conversionServiceFactoryBean.afterPropertiesSet();
return conversionServiceFactoryBean;
}
@Bean
StringToDateTimeConverter converter() {
return new StringToDateTimeConverter();
}
@Bean
SingerToAnotherSingerConverter singerConverter() {
return new SingerToAnotherSingerConverter();
}
}
converter 属性中 beans 的顺序并不重要。为了测试转换,我们使用下面的测试程序,它是这里显示的MultipleConvServDemo类:
package com.apress.prospring5.ch10;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.convert.ConversionService;
import com.apress.prospring5.ch10.config.AppConfig;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class MultipleConvServDemo {
private static Logger logger =
LoggerFactory.getLogger(MultipleConvServDemo.class);
public static void main(String... args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
Singer john = ctx.getBean("john", Singer.class);
logger.info("Singer info: " + john);
ConversionService conversionService =
ctx.getBean(ConversionService.class);
AnotherSinger anotherSinger =
conversionService.convert(john, AnotherSinger.class);
logger.info("Another singer info: " + anotherSinger);
String[] stringArray = conversionService.convert("a,b,c",
String[].class);
logger.info("String array: " + stringArray[0]
+ stringArray[1] + stringArray[2]);
List<String> listString = new ArrayList<>();
listString.add("a");
listString.add("b");
listString.add("c");
Set<String> setString =
conversionService.convert(listString, HashSet.class);
for (String string: setString)
System.out.println("Set: " + string);
}
}
从ApplicationContext获得ConversionService接口的句柄。因为我们已经用自定义转换器在ApplicationContext中注册了ConversionService,我们可以用它来转换Singer对象,以及在转换服务已经支持的其他类型之间进行转换。如清单所示,出于演示目的,还添加了从String(由逗号分隔)转换为Array以及从List转换为Set的示例。运行该程序会产生以下输出:
[main] INFO c.a.p.c.MultipleConvServDemo - Singer info:
{First name: John, Last name: Mayer, Birthday: 1977-10-16,
Site: http://johnmayer.com/}
[main] INFO c.a.p.c.MultipleConvServDemo - Another singer info:
{First name: Mayer, Last name: John, Birthday: 1977-10-16,
Site: http://johnmayer.com/}
[main] INFO c.a.p.c.MultipleConvServDemo - String array: abc
Set: a
Set: b
Set: c
在输出中,您将看到Singer和AnotherSinger被正确转换,以及String到Array和List到Set。使用 Spring 的类型转换服务,您可以轻松地创建自定义转换器,并在应用中的任何层执行转换。一个可能的用例是,您有两个系统,需要更新相同的歌手信息。但是,数据库结构不同(例如,系统 A 中的姓表示系统 B 中的名,以此类推)。在保存到每个单独的系统之前,您可以使用类型转换系统来转换对象。
从 Spring 3.0 开始,Spring MVC 大量使用转换服务(以及下一节讨论的格式化程序 SPI)。在 web 应用上下文配置中,标记<mvc:annotation-driven/>的声明,或者在 Java 配置类中使用 Spring 3.1 中引入的@EnableWebMvc,将自动注册所有默认转换器(例如,StringToArrayConverter、StringToBooleanConverter和StringToLocaleConverter,它们都位于org.springframework.core.convert.support包下)和格式化程序(例如,CurrencyFormatter、DateFormatter和NumberFormatter,它们都位于org.springframework.format包内的各个子包下)。当我们在 Spring 中讨论 web 应用开发时,会在第十六章中涉及更多内容。
Spring 中的字段格式
除了类型转换系统,Spring 带给开发者的另一个很棒的特性是格式化程序 SPI。如您所料,这个 SPI 可以帮助配置字段格式。
在格式化程序 SPI 中,实现格式化程序的主要接口是org.springframework.format.Formatter<T>接口。Spring 提供了一些常用类型的实现,包括CurrencyFormatter、DateFormatter、NumberFormatter和PercentFormatter。
实现自定义格式化程序
实现自定义格式化程序也很容易。我们将使用相同的Singer类,并实现一个自定义格式化程序,用于将birthDate属性的DateTime类型与String类型相互转换。
然而,这一次我们将采取不同的方法;我们将扩展 Spring 的org.springframework.format.support.FormattingConversionServiceFactoryBean类,并提供我们的自定义格式化程序。FormattingConversionServiceFactoryBean类是一个工厂类,它提供了对底层FormattingConversionService类的方便访问,后者支持类型转换系统,以及根据为每个字段类型定义的格式化规则进行字段格式化。
下面的代码片段显示了一个扩展了FormattingConversionServiceFactoryBean类的自定义类,其中定义了一个自定义格式化程序来格式化 JodaTime 的DateTime类型。
package com.apress.prospring5.ch10;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.Formatter;
import org.springframework.format.support.FormattingConversionServiceFactoryBean;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.text.ParseException;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
@Component("conversionService")
public class ApplicationConversionServiceFactoryBean extends
FormattingConversionServiceFactoryBean {
private static Logger logger =
LoggerFactory.getLogger(ApplicationConversionServiceFactoryBean.class);
private static final String DEFAULT_DATE_PATTERN = "yyyy-MM-dd";
private DateTimeFormatter dateFormat;
private String datePattern = DEFAULT_DATE_PATTERN;
private Set<Formatter<?>> formatters = new HashSet<>();
public String getDatePattern() {
return datePattern;
}
@Autowired(required = false)
public void setDatePattern(String datePattern) {
this.datePattern = datePattern;
}
@PostConstruct
public void init() {
dateFormat = DateTimeFormat.forPattern(datePattern);
formatters.add(getDateTimeFormatter());
setFormatters(formatters);
}
public Formatter<DateTime> getDateTimeFormatter() {
return new Formatter<DateTime>() {
@Override
public DateTime parse(String dateTimeString, Locale locale)
throws ParseException {
logger.info("Parsing date string: " + dateTimeString);
return dateFormat.parseDateTime(dateTimeString);
}
@Override
public String print(DateTime dateTime, Locale locale) {
logger.info("Formatting datetime: " + dateTime);
return dateFormat.print(dateTime);
}
};
}
}
在前面的清单中,自定义格式化程序带有下划线。它实现了Formatter<DateTime>接口,并实现了该接口定义的两个方法。parse()方法将String格式解析为DateTime类型(为了支持本地化,还传递了区域设置),而logger.info()方法将DateTime实例格式化为String。日期模式可以注入到 bean 中(或者默认为yyyy-MM-dd)。同样,在init()方法中,自定义格式化程序是通过调用setFormatters()方法注册的。您可以根据需要添加任意数量的格式化程序。
配置 ConversionServiceFactoryBean
声明一个类型为FormattingConversionServiceFactoryBean的 bean 大大减小了AppConfig配置类的大小。
package com.apress.prospring5.ch10.config;
import com.apress.prospring5.ch10.ApplicationConversionServiceFactoryBean;
import com.apress.prospring5.ch10.Singer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import java.net.URL;
import java.util.Locale;
@Configuration
@ComponentScan(basePackages = "com.apress.prospring5.ch10")
public class AppConfig {
@Autowired
ApplicationConversionServiceFactoryBean conversionService;
@Bean
public Singer john() throws Exception {
Singer singer = new Singer();
singer.setFirstName("John");
singer.setLastName("Mayer");
singer.setPersonalSite(new URL("http://johnmayer.com/"));
singer.setBirthDate(conversionService.
getDateTimeFormatter().parse("1977-10-16", Locale.ENGLISH));
return singer;
}
}
测试程序如下所示:
package com.apress.prospring5.ch10;
import com.apress.prospring5.ch10.config.AppConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.convert.ConversionService;
public class ConvFormatServDemo {
private static Logger logger =
LoggerFactory.getLogger(ConvFormatServDemo.class);
public static void main(String... args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
Singer john = ctx.getBean("john", Singer.class);
logger.info("Singer info: " + john);
ConversionService conversionService =
ctx.getBean("conversionService", ConversionService.class);
logger.info("Birthdate of singer is : " +
conversionService.convert(john.getBirthDate(), String.class));
ctx.close();
}
}
运行该程序会产生以下输出:
Parsing date string: 1977-10-16
[main] INFO c.a.p.c.ConvFormatServDemo - Singer info: {
First name: John, Last name: Mayer, Birthday: 1977-10-16,
Site: http://johnmayer.com/}
Formatting datetime: 1977-10-16T00:00:00.000+03:00
[main] INFO c.a.p.c.ConvFormatServDemo -
Birthdate of singer is : 1977-10-16
在输出中,您可以看到 Spring 使用我们的自定义格式化程序的parse()方法将属性从String转换为birthDate属性的DateTime类型。当我们调用ConversionService.convert()方法并传入birthDate属性时,Spring 将调用logger的info方法来格式化输出。
Spring 验证
验证是任何应用的关键部分。应用于域对象的验证规则确保所有的业务数据都是结构良好的,并且满足所有的业务定义。理想的情况是,所有的验证规则都在一个集中的位置维护,相同的规则集应用于相同类型的数据,而不管数据来自哪个源(例如,通过 web 应用的用户输入、通过 web 服务的远程应用、JMS 消息或文件)。
当谈到验证时,转换和格式化也很重要,因为在验证一段数据之前,应该根据为每种类型定义的格式化规则将它转换成所需的 POJO。例如,用户通过浏览器中的 web 应用输入一些歌手信息,然后将数据提交给服务器。在服务器端,如果 web 应用是在 Spring MVC 中开发的,Spring 将从 HTTP 请求中提取数据,并根据格式规则执行从String到所需类型的转换(例如,表示日期的String将被转换为Date字段,格式规则为yyyy-MM-dd)。这个过程称为数据绑定。当数据绑定完成并且构造了域对象后,将对该对象进行验证,并将所有错误返回并显示给用户。如果验证成功,对象将被保存到数据库中。
Spring 支持两种主要类型的验证。第一个是由 Spring 提供的,在其中可以通过实现org.springframework.validation.Validator接口来创建自定义验证器。另一个是通过 Spring 对 JSR-349 的支持(Bean 验证)。我们将在接下来的章节中介绍这两种方法。
使用 Spring 验证器接口
使用 Spring 的Validator接口,我们可以通过创建一个实现接口的类来开发一些验证逻辑。让我们看看它是如何工作的。对于我们到目前为止使用的Singer类,假设名字不能为空。为了根据这个规则验证Singer对象,我们可以创建一个定制的验证器。以下代码片段显示了验证器类:
package com.apress.prospring5.ch10;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
@Component("singerValidator")
public class SingerValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return Singer.class.equals(clazz);
}
@Override
public void validate(Object obj, Errors e) {
ValidationUtils.rejectIfEmpty(e, "firstName",
"firstName.empty");
}
}
validator 类实现了Validator接口并实现了两个方法。supports()方法指示验证器是否支持对传入的类类型的验证。validate()方法对传入的对象进行验证。结果将存储在org.springframework.validation.Errors接口的一个实例中。在validate()方法中,我们只对firstName属性执行检查,并使用方便的ValidationUtils.rejectIfEmpty()方法来确保歌手的名字不为空。最后一个参数是错误代码,可用于从资源包中查找验证消息,以显示本地化的错误消息。
以下代码片段描述了配置类:
package com.apress.prospring5.ch10.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan(basePackages = "com.apress.prospring5.ch10")
public class AppConfig {
}
以下代码片段包含测试程序:
package com.apress.prospring5.ch10;
import com.apress.prospring5.ch10.config.AppConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
import java.util.List;
public class SpringValidatorDemo {
private static Logger logger =
LoggerFactory.getLogger(SpringValidatorDemo.class);
public static void main(String... args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
Singer singer = new Singer();
singer.setFirstName(null);
singer.setLastName("Mayer");
Validator singerValidator = ctx.getBean("singerValidator",
Validator.class);
BeanPropertyBindingResult result =
new BeanPropertyBindingResult(singer, "John");
ValidationUtils.invokeValidator(singerValidator, singer, result);
List<ObjectError> errors = result.getAllErrors();
logger.info("No of validation errors: " + errors.size());
errors.forEach(e -> logger.info(e.getCode()));
ctx.close();
}
}
用设置为null的名字来构造一个Singer对象。然后,从ApplicationContext中检索验证器。为了存储验证结果,构建了一个BeanPropertyBindingResult类的实例。为了执行验证,调用了ValidationUtils.invokeValidator()方法。然后我们检查验证错误。运行该程序会产生以下输出:
[main] INFO c.a.p.c.SpringValidatorDemo - No of validation errors: 1
[main] INFO c.a.p.c.SpringValidatorDemo - firstName.empty
验证产生一个错误,错误代码显示正确。
使用 JSR-349 Bean 验证
从 Spring 4 开始,已经实现了对 JSR-349 (Bean 验证)的完全支持。Bean Validation API 在包javax.validation.constraints下定义了一组 Java 注释形式的约束(例如,@NotNull),这些约束可以应用于域对象。此外,可以通过使用注释来开发和应用定制验证器(例如,类级别的验证器)。
使用 Bean 验证 API 使您不必耦合到特定的验证服务提供者。通过使用 Bean 验证 API,您可以使用标准注释和 API 来实现域对象的验证逻辑,而无需了解底层的验证服务提供者。例如,Hibernate Validator 版本 5 ( http://hibernate.org/subprojects/validator )就是 JSR-349 参考实现。
Spring 为 Bean 验证 API 提供了无缝支持。主要特性包括支持定义验证约束的 JSR-349 标准注释、定制验证器,以及在 Spring 的ApplicationContext中配置 JSR-349 验证。让我们在接下来的章节中一个接一个地讨论它们。当在类路径中使用 Hibernate Validator 版本 4 和 1.0 版本的验证 API 时,Spring 仍然无缝地提供了对 JSR-303 的支持。
定义对象属性的验证约束
让我们从对域对象属性应用验证约束开始。下面的代码片段显示了一个更高级的Singer类,其中验证约束应用于firstName和genre属性:
package com.apress.prospring5.ch10.obj;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
public class Singer {
@NotNull
@Size(min=2, max=60)
private String firstName;
private String lastName;
@NotNull
private Genre genre;
private Gender gender;
//setters and getters
...
}
这里应用的验证约束显示为下划线。对于firstName属性,应用了两个约束。第一个由@NotNull注释控制,这表明该值不应该是null。此外,@Size注释决定了firstName属性的长度。@NotNull约束也适用于genre属性。
以下代码示例分别显示了Genre和Gender枚举类:
//Genre.java
package com.apress.prospring5.ch10.obj;
public enum Genre {
POP("P"),
JAZZ("J"),
BLUES("B"),
COUNTRY("C");
private String code;
private Genre(String code) {
this.code = code;
}
public String toString() {
return this.code;
}
}
//Genfer.java
package com.apress.prospring5.ch10.obj;
public enum Gender {
MALE("M"), FEMALE("F");
private String code;
Gender(String code) {
this.code = code;
}
@Override
public String toString() {
return this.code;
}
}
流派表示歌手所属的音乐流派,而性别与音乐生涯并没有太大关系,所以可能是null。
在 Spring 中配置 Bean 验证支持
为了在 Spring 的ApplicationContext中配置 bean 验证 API 的支持,我们在 Spring 的配置中定义了一个类型为org.springframework.validation.beanvalidation.LocalValidatorFactoryBean的 Bean。以下代码片段描述了配置类:
package com.apress.prospring5.ch10.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
@Configuration
@ComponentScan(basePackages = "com.apress.prospring5.ch10")
public class AppConfig {
@Bean LocalValidatorFactoryBean validator() {
return new LocalValidatorFactoryBean();
}
}
只需要声明一个类型为LocalValidatorFactoryBean的 bean。默认情况下,Spring 会搜索类路径中是否存在 Hibernate 验证器库。现在,让我们创建一个为Singer类提供验证服务的服务类。验证器类如下所示:
package com.apress.prospring5.ch10;
import com.apress.prospring5.ch10.obj.Singer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import java.util.Set;
@Service("singerValidationService")
public class SingerValidationService {
@Autowired
private Validator validator;
public Set<ConstraintViolation<Singer>>
validateSinger(Singer singer) {
return validator.validate(singer);
}
}
注入了javax.validation.Validator的一个实例(注意与 Spring 提供的Validator接口的不同,后者是org.springframework.validation.Validator)。一旦定义了LocalValidatorFactoryBean,您就可以在应用的任何地方创建一个Validator接口的句柄。为了对 POJO 执行验证,调用了Validator.validate()方法。验证结果将作为ConstraintViolation<T>接口的List返回。
测试程序如下所示:
package com.apress.prospring5.ch10;
import com.apress.prospring5.ch10.config.AppConfig;
import com.apress.prospring5.ch10.obj.Singer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import javax.validation.ConstraintViolation;
import java.util.Set;
public class Jsr349Demo {
private static Logger logger =
LoggerFactory.getLogger(Jsr349Demo.class);
public static void main(String... args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
SingerValidationService singerBeanValidationService =
ctx.getBean( SingerValidationService.class);
Singer singer = new Singer();
singer.setFirstName("J");
singer.setLastName("Mayer");
singer.setGenre(null);
singer.setGender(null);
validateSinger(singer, singerBeanValidationService);
ctx.close();
}
private static void validateSinger(Singer singer,
SingerValidationService singerValidationService) {
Set<ConstraintViolation<Singer>> violations =
singerValidationService.validateSinger(singer);
listViolations(violations);
}
private static void listViolations(
Set<ConstraintViolation<Singer>> violations) {
logger.info("No. of violations: " + violations.size());
for (ConstraintViolation<Singer> violation : violations) {
logger.info("Validation error for property: " +
violation.getPropertyPath()
+ " with value: " + violation.getInvalidValue()
+ " with error message: " + violation.getMessage());
}
}
}
如清单所示,Singer对象由违反约束的firstName和genre构成。在validateSinger()方法中,调用了SingerValidationService.validateSinger()方法,这又将调用 JSR-349 (Bean 验证)。运行该程序会产生以下输出:
[main] INFO o.h.v.i.u.Version - HV000001:
Hibernate Validator 6.0.0.Beta2
[main] INFO c.a.p.c.Jsr349Demo - No. of violations: 2
[main] INFO c.a.p.c.Jsr349Demo - Validation error for property:
firstName with value: J with error message: size must be between 2 and 60
[main] INFO c.a.p.c.Jsr349Demo - Validation error for property:
genre with value: null with error message: may not be null
如您所见,有两个违规,并且显示了消息。在输出中,您还会看到 Hibernate Validator 已经基于注释构造了默认的验证错误消息。您还可以提供自己的验证错误消息,我们将在下一节中演示。
创建自定义验证程序
除了属性级验证,我们还可以应用类级验证。例如,对于Singer类,对于乡村歌手,我们希望确保lastName和gender属性不是null,并不是说这真的很重要,只是出于教育目的。在这种情况下,我们可以开发一个定制的验证器来执行检查。在 Bean 验证 API 中,开发自定义验证器是一个两步过程。首先为验证器创建一个Annotation类型,如下面的代码片段所示。第二步是开发实现验证逻辑的类。
package com.apress.prospring5.ch10;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Constraint(validatedBy=CountrySingerValidator.class)
@Documented
public @interface CheckCountrySinger {
String message() default "Country Singer should
have gender and last name defined";
Class<?> groups() default {};
Class<? extends Payload> payload() default {};
}
@Target(ElementType.TYPE)注释意味着注释应该只应用于类级别。@Constraint注释表明它是一个验证器,而validatedBy属性指定了提供验证逻辑的类。在主体中,定义了三个属性(以方法的形式),如下所示:
message属性定义了违反约束时返回的消息(或错误代码)。还可以在注释中提供默认消息。- 如果适用的话,
groups属性指定验证组。可以将验证器分配给不同的组,并在特定的组上执行验证。 - 属性指定了额外的有效负载对象(实现接口的类的)。它允许您向约束附加附加信息(例如,有效负载对象可以指示违反约束的严重性)。
下面的代码片段显示了提供验证逻辑的CountrySingerValidator类:
package com.apress.prospring5.ch10;
import com.apress.prospring5.ch10.obj.Singer;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class CountrySingerValidator implements
ConstraintValidator<CheckCountrySinger, Singer> {
@Override
public void initialize(CheckCountrySinger constraintAnnotation) {
}
@Override
public boolean isValid(Singer singer,
ConstraintValidatorContext context) {
boolean result = true;
if (singer.getGenre() != null && (singer.isCountrySinger() &&
(singer.getLastName() == null || singer.getGender() == null))) {
result = false;
}
return result;
}
}
CountrySingerValidator实现了ConstraintValidator<CheckCountrySinger, Singer>接口,这意味着验证器检查Singer类上的CheckCountrySinger注释。实现了isValid()方法,底层的验证服务提供者(例如,Hibernate Validator)将验证下的实例传递给方法。在方法中,我们验证如果歌手是乡村音乐歌手,那么lastName和gender属性不应该是null。结果是一个指示验证结果的Boolean值。
要启用验证,请将@CheckCountrySinger注释应用到Singer类,如下所示:
package com.apress.prospring5.ch10.obj;
import com.apress.prospring5.ch10.CheckCountrySinger;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
@CheckCountrySinger
public class Singer {
@NotNull
@Size(min = 2, max = 60)
private String firstName;
private String lastName;
@NotNull
private Genre genre;
private Gender gender;
//getters and setter
...
public boolean isCountrySinger() {
return genre == Genre.COUNTRY;
}
}
为了测试定制验证,将在测试类Jsr349CustomDemo中创建下面的Singer实例:
public class Jsr349CustomDemo {
...
Singer singer = new Singer();
singer.setFirstName("John");
singer.setLastName("Mayer");
singer.setGenre(Genre.COUNTRY);
singer.setGender(null);
validateSinger(singer, singerValidationService);
...
}
运行该程序会产生以下输出(另一个输出被省略):
[main] INFO o.h.v.i.u.Version - HV000001: Hibernate Validator 6.0.0.Beta2
[main] INFO c.a.p.c.Jsr349CustomDemo - No. of violations: 1
[main] INFO c.a.p.c.Jsr349CustomDemo - Validation error for property:
with value: com.apress.prospring5.ch10.obj.Singer@3116c353
with error message: Country Singer should have gender and last name defined
在输出中,您可以看到被检查的值(即Singer实例)违反了乡村歌手的验证规则,因为gender属性是null。还要注意,在输出中,属性路径是空的,因为这是一个类级别的验证错误。
使用 AssertTrue 进行自定义验证
除了实现自定义验证器,在 Bean 验证 API 中应用自定义验证的另一种方式是使用@AssertTrue注释。让我们看看它是如何工作的。对于Singer类,移除了@CheckCountrySinge r 注释,并将isCountrySinger()方法修改如下:
public class Singer {
@NotNull
@Size(min = 2, max = 60)
private String firstName;
private String lastName;
@NotNull
private Genre genre;
private Gender gender;
...
@AssertTrue(message="ERROR! Individual customer should have
gender and last name defined")
public boolean isCountrySinger() {
boolean result = true;
if (genre!= null &&
(genre.equals(Genre.COUNTRY) &&
(gender == null || lastName == null))) {
result = false;
}
return result;
}
}
正如您可能推断的那样,@CheckCountrySinger注释和CountrySingerValidator类不再是必需的。
将isCountrySinger()方法添加到Singer类中,并用@AssertTrue进行注释(在包javax.validation.constraints下)。当调用验证时,提供者将调用检查并确保结果为真。JSR-349 还提供了@AssertFalse注释来检查一些应该为假的条件。现在运行测试程序Jsr349AssertTrueDemo,您将获得与定制验证程序相同的输出。
自定义验证的注意事项
那么,对于 JSR-349 中的自定义验证,您应该使用哪种方法:自定义验证器还是@AssertTrue注释?一般来说,@AssertTrue方法实现起来更简单,您可以在域对象的代码中看到验证规则。然而,对于具有更复杂逻辑的验证器(例如,假设您需要注入一个服务类,访问一个数据库,并检查有效值),那么实现一个定制的验证器是可行的,因为您永远不希望将服务层对象注入到您的域对象中。此外,自定义验证器可以在相似的域对象中重用。
决定使用哪个验证 API
讨论了 Spring 自己的Validator接口和 Bean 验证 API 之后,您应该在您的应用中使用哪一个?JSR-349 是绝对的出路。以下是主要原因:
- JSR-349 是一个 JEE 标准,被许多前端/后端框架广泛支持(例如,Spring、JPA 2、Spring MVC 和 GWT)。
- JSR-349 提供了一个标准的验证 API,它隐藏了底层的提供者,所以你不会被绑定到一个特定的提供者。
- 从版本 4 开始,Spring 与 JSR-349 紧密集成。例如,在 Spring MVC web 控制器中,您可以用
@Valid注释对方法中的参数进行注释(在包javax.validation下),Spring 将在数据绑定过程中自动调用 JSR-349 验证。而且,在一个 Spring MVC web 应用上下文配置中,一个简单的名为<mvc:annotation-driven/>的标签将配置 Spring 自动启用 Spring 类型转换系统和字段格式化,以及对 JSR-349 (Bean Validation)的支持。 - 如果您使用 JPA 2,提供者将在持久化之前自动对实体执行 JSR-349 验证,提供另一层保护。
关于使用 JSR-349 (Bean Validation)配合 Hibernate Validator 作为实现提供者的详细信息,请参考 Hibernate Validator 的文档页: http://docs.jboss.org/hibernate/validator/ 5.1/reference/en-US/html。
摘要
在本章中,我们介绍了 Spring 类型转换系统以及现场格式化程序 SPI。除了对PropertyEditors的支持之外,您还看到了新的类型转换系统是如何用于任意类型转换的。
我们还介绍了 Spring 中的验证支持,Spring 的Validator接口,以及 Spring 中推荐的 JSR-349 (Bean 验证)支持。
十一、任务调度
任务调度是企业应用中的一个常见功能。任务调度主要由三部分组成:任务(需要在特定时间或定期运行的业务逻辑)、触发器(指定任务执行的条件)和调度器(根据触发器的信息执行任务)。具体来说,本章涵盖以下主题:
- Spring 中的任务调度:我们讨论 Spring 如何支持任务调度,重点是 Spring 3 中引入的
TaskScheduler抽象。我们还涵盖了调度场景,比如固定间隔调度和cron表达式。 - 异步任务执行:我们展示了如何在 Spring 中使用
@Async注释来异步执行任务。 - Spring 中的任务执行:我们简单讨论一下 Spring 的
TaskExecutor接口以及任务是如何执行的。
任务计划示例的相关性
您可以在下面的 Gradle 配置片段中看到本章所需的依赖项:
//pro-spring-15/build.gradle
ext {
springDataVersion = '2.0.0.M3'
//logging libs
slf4jVersion = '1.7.25'
logbackVersion = '1.2.3'
guavaVersion = '21.0'
jodaVersion = '2.9.9'
utVersion = '6.0.1.GA'
junitVersion = '4.12'
spring = [
data :
"org.springframework.data:spring-data-jpa:$springDataVersion",
...
]
testing = [
junit: "junit:junit:$junitVersion"
]
misc = [
slf4jJcl : "org.slf4j:jcl-over-slf4j:$slf4jVersion",
logback : "ch.qos.logback:logback-classic:$logbackVersion",
guava : "com.google.guava:guava:$guavaVersion",
joda : "joda-time:joda-time:$jodaVersion",
usertype : "org.jadira.usertype:usertype.core:$utVersion",
...
]
...
}
...
//chapter11/build.gradle
dependencies {
compile (spring.contextSupport) {
exclude module: 'spring-context'
exclude module: 'spring-beans'
exclude module: 'spring-core'
}
compile misc.slf4jJcl, misc.logback, misc.lang3, spring.data,
misc.guava, misc.joda, misc.usertype, db.h2
testCompile testing.junit
}
Spring 任务调度
企业应用经常需要调度任务。在许多应用中,各种任务(例如向客户发送电子邮件通知、运行日终作业、进行数据整理和批量更新数据)需要定期运行,或者以固定的时间间隔(例如每小时)运行,或者按照特定的时间表运行(例如从周一到周五每天晚上 8 点运行)。如前所述,任务调度由三部分组成:调度定义(触发器)、任务执行(调度器)和任务本身。
在 Spring 应用中,有许多方法可以触发任务的执行。一种方法是从应用部署环境中已经存在的调度系统外部触发作业。例如,许多企业使用商业系统,如 Control-M 或 CA AutoSys 来调度任务。如果应用运行在 Linux/Unix 平台上,可以使用crontab调度程序。可以通过向 Spring 应用发送 RESTful-WS 请求并让 Spring 的 MVC 控制器触发任务来完成任务触发。
另一种方法是使用 Spring 中的任务调度支持。Spring 在任务调度方面提供了三个选项。
- 支持 JDK 定时器:Spring 支持 JDK 的
Timer对象进行任务调度。 - 与 Quartz 集成:Quartz 调度器 1 是一个流行的开源调度库。
- Spring 自己的 Spring TaskScheduler 抽象:Spring 3 引入了
TaskScheduler抽象,它提供了一种简单的任务调度方式,并支持大多数典型的需求。
本节重点介绍如何使用 Spring 的TaskScheduler抽象来进行任务调度。
介绍 Spring TaskScheduler 抽象
Spring 的TaskScheduler抽象主要有三个参与者。
- 触发接口:
org.springframework.scheduling.Trigger接口支持定义触发机制。Spring 提供了两个Trigger实现。CronTrigger类支持基于cron表达式的触发,而PeriodicTrigger类支持基于初始延迟和固定间隔的触发。 - 任务:任务是需要调度的业务逻辑的一部分。在 Spring 中,任务可以被指定为任何 Spring bean 中的一个方法。
- TaskScheduler 接口:
org.springframework.scheduling.TaskScheduler接口提供了对任务调度的支持。Spring 提供了三个TaskScheduler接口的实现类。TimerManagerTaskScheduler类(在包org.springframework. scheduling.commonj中)包装了 CommonJ 的commonj.timers.TimerManager接口,该接口通常用于商业 JEE 应用服务器,如 WebSphere 和 WebLogic。ConcurrentTaskScheduler和ThreadPoolTaskScheduler类(都在包org.springframework.scheduling.concurrent下)包装java.util.concurrent.ScheduledThreadPoolExecutor类。这两个类都支持从共享线程池执行任务。
图 11-1 显示了Trigger接口、TaskScheduler接口以及实现java.lang.Runnable接口的任务实现之间的关系。要使用 Spring 的TaskScheduler抽象来调度任务,您有两种选择。一种是在 Spring 的 XML 配置中使用task-namespace,另一种是使用注释。让我们逐一查看。

图 11-1。
Relationship between trigger, task, and scheduler
探索示例任务
为了演示 Spring 中的任务调度,让我们首先实现一个简单的作业,即维护汽车信息数据库的应用。下面的代码片段显示了作为 JPA 实体类实现的Car类:
package com.apress.prospring5.ch11;
import static javax.persistence.GenerationType.IDENTITY;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Version;
import org.hibernate.annotations.Type;
import org.joda.time.DateTime;
@Entity
@Table(name="car")
public class Car {
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "ID")
private Long id;
@Column(name="LICENSE_PLATE")
private String licensePlate;
@Column(name="MANUFACTURER")
private String manufacturer;
@Column(name="MANUFACTURE_DATE")
@Type(type="org.jadira.usertype.dateandtime.joda.PersistentDateTime")
private DateTime manufactureDate;
@Column(name="AGE")
private int age;
@Version
private int version;
//getters and setters
...
@Override
public String toString() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
return String.format("{License: %s, Manufacturer: %s,
Manufacture Date: %s, Age: %d}",
licensePlate, manufacturer, sdf.format(manufactureDate.toDate()), age);
}
}
这个实体类用作 Hibernate 生成的CAR表的模型。数据访问和服务层的配置,在本章中全部合并为一个,由下面代码片段中描述的DataServiceConfig类提供:
package com.apress.prospring5.ch11.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.util.Properties;
@Configuration
@EnableJpaRepositories(basePackages = {"com.apress.prospring5.ch11.repos"})
@ComponentScan(basePackages = {"com.apress.prospring5.ch11"} )
public class DataServiceConfig {
private static Logger logger =
LoggerFactory.getLogger(DataServiceConfig.class);
@Bean
public DataSource dataSource() {
try {
EmbeddedDatabaseBuilder dbBuilder = new EmbeddedDatabaseBuilder();
return dbBuilder.setType(EmbeddedDatabaseType.H2).build();
} catch (Exception e) {
logger.error("Embedded DataSource bean cannot be created!", e);
return null;
}
}
@Bean
public Properties hibernateProperties() {
Properties hibernateProp = new Properties();
hibernateProp.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
hibernateProp.put("hibernate.hbm2ddl.auto", "create-drop");
//hibernateProp.put("hibernate.format_sql", true);
hibernateProp.put("hibernate.show_sql", true);
hibernateProp.put("hibernate.max_fetch_depth", 3);
hibernateProp.put("hibernate.jdbc.batch_size", 10);
hibernateProp.put("hibernate.jdbc.fetch_size", 50);
return hibernateProp;
}
@Bean
public PlatformTransactionManager transactionManager() {
return new JpaTransactionManager(entityManagerFactory());
}
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
return new HibernateJpaVendorAdapter();
}
@Bean
public EntityManagerFactory entityManagerFactory() {
LocalContainerEntityManagerFactoryBean factoryBean =
new LocalContainerEntityManagerFactoryBean();
factoryBean.setPackagesToScan("com.apress.prospring5.ch11.entities");
factoryBean.setDataSource(dataSource());
factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
factoryBean.setJpaProperties(hibernateProperties());
factoryBean.setJpaVendorAdapter(jpaVendorAdapter());
factoryBean.afterPropertiesSet();
return factoryBean.getNativeEntityManagerFactory();
}
}
名为DBInitializer的类负责填充CAR表。
package com.apress.prospring5.ch11.config;
import com.apress.prospring5.ch11.entities.Car;
import com.apress.prospring5.ch11.repos.CarRepository;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
@Service
public class DBInitializer {
private Logger logger = LoggerFactory.getLogger(DBInitializer.class);
@Autowired CarRepository carRepository;
@PostConstruct
public void initDB() {
logger.info("Starting database initialization...");
DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd");
Car car = new Car();
car.setLicensePlate("GRAVITY-0405");
car.setManufacturer("Ford");
car.setManufactureDate(DateTime.parse("2006-09-12", formatter));
carRepository.save(car);
car = new Car();
car.setLicensePlate("CLARITY-0432");
car.setManufacturer("Toyota");
car.setManufactureDate(DateTime.parse("2003-09-09", formatter));
carRepository.save(car);
car = new Car();
car.setLicensePlate("ROSIE-0402");
car.setManufacturer("Toyota");
car.setManufactureDate(DateTime.parse("2017-04-16", formatter));
carRepository.save(car);
logger.info("Database initialization finished.");
}
}
让我们为Car实体定义一个 DAO 层。我们将使用 Spring Data 的 JPA 及其存储库抽象支持。这里你可以看到CarRepository接口,它是CrusRepository的简单扩展,因为我们对任何特殊的 DAO 操作都不感兴趣。
package com.apress.prospring5.ch11.repos;
import com.apress.prospring5.ch11.entities.Car;
import org.springframework.data.repository.CrudRepository;
public interface CarRepository extends CrudRepository<Car, Long> {
}
服务层由CarService接口及其实现CarServiceImpl表示。
package com.apress.prospring5.ch11.services;
//CarService.jar
import com.apress.prospring5.ch11.entities.Car;
import java.util.List;
public interface CarService {
List<Car> findAll();
Car save(Car car);
void updateCarAgeJob();
boolean isDone();
}
//CarServiceImpl.jar
...
@Service("carService")
@Repository
@Transactional
public class CarServiceImpl implements CarService {
public boolean done;
final Logger logger = LoggerFactory.getLogger(CarServiceImpl.class);
@Autowired
CarRepository carRepository;
@Override
@Transactional(readOnly=true)
public List<Car> findAll() {
return Lists.newArrayList(carRepository.findAll());
}
@Override
public Car save(Car car) {
return carRepository.save(car);
}
@Override
public void updateCarAgeJob() {
List<Car> cars = findAll();
DateTime currentDate = DateTime.now();
logger.info("Car age update job started");
cars.forEach(car -> {
int age = Years.yearsBetween(car.getManufactureDate(),
currentDate).getYears();
car.setAge(age);
save(car);
logger.info("Car age update --> " + car);
});
logger.info("Car age update job completed successfully");
done = true;
}
@Override
public boolean isDone() {
return done;
}
}
提供了四种方法,如下所示:
- 检索所有汽车的信息:
List<Car> findAll()。 - 一个持久化更新的
Car对象:Car save(Car car)。 - 第三种方法,
void updateCarAgeJob(),是需要定期运行的作业,根据汽车的制造日期和当前日期来更新车龄。 - 第四个方法是
boolean isDone(),这是一个实用方法,用来知道作业何时结束,这样应用就可以正常关闭。
像 Spring 中对其他名称空间的支持一样,task-namespace通过使用 Spring 的TaskScheduler抽象为调度任务提供了一个简化的配置。下面的 XML 配置片段显示了task-namespace-app-context.xml文件的内容,并显示了包含预定任务的 Spring 应用的配置。使用task-namespace进行任务调度非常简单。
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:task="http://www.springframework.org/schema/task"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/task
http://www.springframework.org/schema/task/spring-task.xsd">
<task:scheduler id="carScheduler" pool-size="10"/>
<task:scheduled-tasks scheduler="carScheduler">
<task:scheduled ref="carService"
method="updateCarAgeJob" fixed-delay="10000"/>
</task:scheduled-tasks>
</beans>
当遇到<task:scheduler>标签时,Spring 实例化一个ThreadPoolTaskScheduler类的实例,而属性pool-size指定调度程序可以使用的线程池的大小。在<task:scheduled-tasks>标记中,可以调度一个或多个任务。在<task:scheduled>标签中,一个任务可以引用一个 Spring bean(在本例中是carService bean)和 bean 中的一个特定方法(在本例中是updateCarAgeJob()方法)。属性fixed-delay指示 Spring 将PeriodicTrigger实例化为TaskScheduler的Trigger实现。
通过声明一个新的配置类并使用@Import导入两个配置(对于配置类使用@Import,对于 XML 配置使用@ImportResource),任务调度配置与数据访问配置相结合。
package com.apress.prospring5.ch11.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportResource;
@Configuration
@Import({ DataServiceConfig.class })
@ImportResource("classpath:spring/task-namespace-app-context.xml")
public class AppConfig {}
这个配置类AppConfig用于创建一个 Spring ApplicationContext来测试 Spring 调度功能:
package com.apress.prospring5.ch11;
import com.apress.prospring5.ch11.config.AppConfig;
import com.apress.prospring5.ch11.services.CarService;
import com.apress.prospring5.ch11.services.CarServiceImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
public class ScheduleTaskDemo {
final static Logger logger = LoggerFactory.getLogger(CarServiceImpl.class);
public static void main(String... args) throws Exception{
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
CarService carService = ctx.getBean("carService", CarService.class);
while (!carService.isDone()) {
logger.info("Waiting for scheduled job to end ...");
Thread.sleep(250);
}
ctx.close();
}
}
运行该程序会产生以下批处理作业输出:
[main] INFO c.a.p.c.s.CarServiceImpl - Waiting for scheduled job to end ...
[carScheduler-1] INFO c.a.p.c.s.CarServiceImpl - Car age update job started
[carScheduler-1] INFO c.a.p.c.s.CarServiceImpl - Car age update --> {License:
GRAVITY-0405, Manufacturer: Ford, Manufacture Date: 2006-09-12, Age: 10}
[carScheduler-1] INFO c.a.p.c.s.CarServiceImpl - Car age update --> {License:
CLARITY-0432, Manufacturer: Toyota, Manufacture Date: 2003-09-09, Age: 13}
[carScheduler-1] INFO c.a.p.c.s.CarServiceImpl - Car age update --> {License:
ROSIE-0402, Manufacturer: Toyota, Manufacture Date: 2017-04-16, Age: 0}
[carScheduler-1] INFO c.a.p.c.s.CarServiceImpl -
Car age update job completed successfully
在前面的示例中,应用仅在计划任务运行一次后停止。正如我们已经声明的,我们希望任务每 10 秒运行一次,通过设置fixed-delay="10000"属性;我们应该通过让应用运行直到用户按下一个键来允许任务的重复运行。修改ScheduleTaskDemo如下:
package com.apress.prospring5.ch11;
import com.apress.prospring5.ch11.config.AppConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
public class ScheduleTaskDemo {
public static void main(String... args) throws Exception {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
System.in.read();
ctx.close();
}
}
从输出中,您可以看到汽车的age属性得到了更新。除了固定的时间间隔,更灵活的调度机制是使用一个cron表达式。在 XML 配置文件中,更改以下代码行:
<task:scheduled ref="carService" method="updateCarAgeJob" fixed-delay="10000"/>
致以下内容:
<task:scheduled ref="carService" method="updateCarAgeJob" cron="0 * * * * *"/>
更改后,再次运行ScheduleTaskDemo类,让应用运行一分多钟。您将看到该作业每分钟都会运行。
使用注释进行任务调度
使用 Spring 的TaskScheduler抽象来调度任务的另一个选择是使用注释。Spring 为此提供了@Scheduled注释。为了启用任务调度的注释支持,我们需要在 Spring 的 XML 配置中提供<task:annotation-driven>标签。或者,如果使用了配置类,必须用@EnableScheduling进行注释。让我们采用这种方法,完全去掉 XML。
package com.apress.prospring5.ch11.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.scheduling.annotation.EnableScheduling;
@Configuration
@Import({DataServiceConfig.class})
@EnableScheduling
public class AppConfig {
}
是的,这就是所需要的。您甚至不再需要自己声明调度程序,因为 Spring 会处理它。在@Configuration类上使用的@EnableScheduling注释支持检测容器中任何 Spring 管理的 bean 或它们的方法上的@Scheduled注释。有趣的是,用@Scheduled注释的方法甚至可以直接在@Configuration类中声明。这个注释告诉 Spring 寻找一个相关的调度器定义:或者是上下文中唯一的TaskScheduler bean,或者是名为taskScheduler的TaskScheduler bean,或者是ScheduledExecutorService bean。如果没有找到,将在注册器中创建并使用一个本地单线程默认调度程序。
要在 Spring bean 中调度一个特定的方法,该方法必须用@Scheduled进行注释,并传递调度要求。在下面的代码片段中,CarServiceImpl类被扩展并用于声明一个带有预定方法的新 bean,该方法覆盖了父类中的updateCarAgeJob()方法以利用@Scheduled注释:
package com.apress.prospring5.ch11.services;
import com.apress.prospring5.ch11.entities.Car;
import org.joda.time.DateTime;
import org.joda.time.Years;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service("scheduledCarService")
@Repository
@Transactional
public class ScheduledCarServiceImpl extends CarServiceImpl{
@Override
@Scheduled(fixedDelay=10000)
public void updateCarAgeJob() {
List<Car> cars = findAll();
DateTime currentDate = DateTime.now();
logger.info("Car age update job started");
cars.forEach(car -> {
int age = Years.yearsBetween(
car.getManufactureDate(), currentDate).getYears();
car.setAge(age);
save(car);
logger.info("Car age update --> " + car);
});
logger.info("Car age update job completed successfully");
}
}
测试程序如下所示:
package com.apress.prospring5.ch11;
import com.apress.prospring5.ch11.config.AppConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
public class ScheduleTaskAnnotationDemo {
public static void main(String... args) throws Exception {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
System.in.read();
ctx.close();
}
}
运行该程序产生的输出与使用task-namespace几乎相同。您可以通过改变@Scheduled注释中的属性来尝试不同的触发机制(即fixedDelay、fixedRate、cron)。你可以自己测试。
[main] DEBUG o.s.s.a.ScheduledAnnotationBeanPostProcessor - Could not find default
TaskScheduler bean
org.springframework.beans.factory.NoSuchBeanDefinitionException:
No qualifying bean of type 'org.springframework.scheduling.TaskScheduler' available
... // more stacktrace here
[main] DEBUG o.s.s.a.ScheduledAnnotationBeanPostProcessor - Could not find default
ScheduledExecutorService bean
org.springframework.beans.factory.NoSuchBeanDefinitionException:
No qualifying bean of type 'java.util.concurrent.ScheduledExecutorService' available
... // more stacktrace here
[pool-1-thread-1] INFO c.a.p.c.s.CarServiceImpl - Car age update job started
[pool-1-thread-1] INFO c.a.p.c.s.CarServiceImpl - Car age update --> {License:
GRAVITY-0405, Manufacturer: Ford, Manufacture Date: 2006-09-12, Age: 10}
[pool-1-thread-1] INFO c.a.p.c.s.CarServiceImpl - Car age update --> {License:
CLARITY-0432, Manufacturer: Toyota, Manufacture Date: 2003-09-09, Age: 13}
[pool-1-thread-1] INFO c.a.p.c.s.CarServiceImpl - Car age update --> {License:
ROSIE-0402, Manufacturer: Toyota, Manufacture Date: 2017-04-16, Age: 0}
[pool-1-thread-1] INFO c.a.p.c.s.CarServiceImpl - Car age update job
completed successfully
此外,如果愿意,您可以定义自己的TaskScheduler bean。下面的例子声明了一个ThreadPoolTaskScheduler bean,它等同于上一节 XML 配置中声明的 bean:
package com.apress.prospring5.ch11.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.concurrent.DefaultManagedTaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
@Configuration
@Import({DataServiceConfig.class})
@EnableScheduling
public class AppConfig {
@Bean TaskScheduler carScheduler() {
ThreadPoolTaskScheduler carScheduler =
new ThreadPoolTaskScheduler();
carScheduler.setPoolSize(10);
return carScheduler;
}
}
如果您现在运行测试示例,您将会看到日志中不再打印异常,并且执行该方法的调度程序的名称已经更改,因为TaskScheduler bean 被命名为carScheduler。
[carScheduler-1] INFO c.a.p.c.s.CarServiceImpl - Car age update job started
[carScheduler-1] INFO c.a.p.c.s.CarServiceImpl - Car age update --> {License:
GRAVITY-0405, Manufacturer: Ford, Manufacture Date: 2006-09-12, Age: 10}
[carScheduler-1] INFO c.a.p.c.s.CarServiceImpl - Car age update --> {License:
CLARITY-0432, Manufacturer: Toyota, Manufacture Date: 2003-09-09, Age: 13}
[carScheduler-1] INFO c.a.p.c.s.CarServiceImpl - Car age update --> {License:
ROSIE-0402, Manufacturer: Toyota, Manufacture Date: 2017-04-16, Age: 0}
[carScheduler-1] INFO c.a.p.c.s.CarServiceImpl - Car age update job
completed successfully
Spring 中的异步任务执行
从 3.0 版本开始,Spring 也支持使用注释来异步执行任务。要做到这一点,你只需要用@Async对方法进行注释。
package com.apress.prospring5.ch11;
import java.util.concurrent.Future;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;
@Service("asyncService")
public class AsyncServiceImpl implements AsyncService {
final Logger logger = LoggerFactory.getLogger(AsyncServiceImpl.class);
@Async
@Override
public void asyncTask() {
logger.info("Start execution of async. task");
try {
Thread.sleep(10000);
} catch (Exception ex) {
logger.error("Task Interruption", ex);
}
logger.info("Complete execution of async. task");
}
@Async
@Override
public Future<String> asyncWithReturn(String name) {
logger.info("Start execution of async. task with return for "+ name);
try {
Thread.sleep(5000);
} catch (Exception ex) {
logger.error("Task Interruption", ex);
}
logger.info("Complete execution of async. task with return for " + name);
return new AsyncResult<>("Hello: " + name);
}
}
AsyncService定义了两种方法。asyncTask()方法是一个简单的任务,它将信息记录到记录器中。方法asyncWithReturn()接受一个String参数并返回一个java.util.concurrent.Future<V>接口的实例。在完成asyncWithReturn()之后,结果被存储在org.springframework.scheduling.annotation.AsyncResult<V>类的一个实例中,该类实现了Future<V>接口,调用者可以使用它在以后检索执行的结果。通过启用 Spring 的异步方法执行功能来获得@Async注释,这是通过用@EnableAsync注释 Java 配置类来完成的。 2
package com.apress.prospring5.ch11.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportResource;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
@Configuration
@EnableAsync
@ComponentScan(basePackages = {"com.apress.prospring5.ch11"} )
public class AppConfig {
}
测试程序如下所示:
package com.apress.prospring5.ch11;
import java.util.concurrent.Future;
import com.apress.prospring5.ch11.config.AppConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
public class AsyncTaskDemo {
private static Logger logger =
LoggerFactory.getLogger(AsyncTaskDemo.class);
public static void main(String... args) throws Exception{
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
AsyncService asyncService = ctx.getBean("asyncService",
AsyncService.class);
for (int i = 0; i < 5; i++) {
asyncService.asyncTask();
}
Future<String> result1 = asyncService.asyncWithReturn("John Mayer");
Future<String> result2 = asyncService.asyncWithReturn("Eric Clapton");
Future<String> result3 = asyncService.asyncWithReturn("BB King");
Thread.sleep(6000);
logger.info("Result1: " + result1.get());
logger.info("Result2: " + result2.get());
logger.info("Result3: " + result3.get());
System.in.read();
ctx.close();
}
}
我们用不同的参数调用asyncTask()方法五次,然后调用asyncWithReturn()三次,然后在休眠六秒后检索结果。运行该程序会产生以下输出:
...
17:55:31.851 [SimpleAsyncTaskExecutor-1] INFO c.a.p.c.AsyncServiceImpl -
Start execution of async. task
17:55:31.851 [SimpleAsyncTaskExecutor-2] INFO c.a.p.c.AsyncServiceImpl -
Start execution of async. task
17:55:31.851 [SimpleAsyncTaskExecutor-3] INFO c.a.p.c.AsyncServiceImpl -
Start execution of async. task
17:55:31.851 [SimpleAsyncTaskExecutor-4] INFO c.a.p.c.AsyncServiceImpl -
Start execution of async. task
17:55:31.852 [SimpleAsyncTaskExecutor-5] INFO c.a.p.c.AsyncServiceImpl -
Start execution of async. task
17:55:31.852 [SimpleAsyncTaskExecutor-6] INFO c.a.p.c.AsyncServiceImpl -
Start execution of async. task with return for John Mayer
17:55:31.852 [SimpleAsyncTaskExecutor-7] INFO c.a.p.c.AsyncServiceImpl -
Start execution of async. task with return for Eric Clapton
17:55:31.852 [SimpleAsyncTaskExecutor-8] INFO c.a.p.c.AsyncServiceImpl -
Start execution of async. task with return for BB King
17:55:36.856 [SimpleAsyncTaskExecutor-8] INFO c.a.p.c.AsyncServiceImpl -
Complete execution of async. task with return for BB King
17:55:36.856 [SimpleAsyncTaskExecutor-6] INFO c.a.p.c.AsyncServiceImpl -
Complete execution of async. task with return for John Mayer
17:55:36.856 [SimpleAsyncTaskExecutor-7] INFO c.a.p.c.AsyncServiceImpl -
Complete execution of async. task with return for Eric Clapton
17:55:37.852 [main] INFO c.a.p.c.AsyncTaskDemo - Result1: Hello: John Mayer
17:55:37.853 [main] INFO c.a.p.c.AsyncTaskDemo - Result2: Hello: Eric Clapton
17:55:37.853 [main] INFO c.a.p.c.AsyncTaskDemo - Result3: Hello: BB King
17:55:41.852 [SimpleAsyncTaskExecutor-1] INFO c.a.p.c.AsyncServiceImpl -
Complete execution of async. task
17:55:41.852 [SimpleAsyncTaskExecutor-4] INFO c.a.p.c.AsyncServiceImpl -
Complete execution of async. task
17:55:41.852 [SimpleAsyncTaskExecutor-3] INFO c.a.p.c.AsyncServiceImpl -
Complete execution of async. task
17:55:41.852 [SimpleAsyncTaskExecutor-5] INFO c.a.p.c.AsyncServiceImpl -
Complete execution of async. task
17:55:41.852 [SimpleAsyncTaskExecutor-2] INFO c.a.p.c.AsyncServiceImpl -
Complete execution of async. task
从输出中,您可以看到所有的调用都是同时开始的。这三个带有返回值的调用首先完成,并显示在控制台输出中。最后,调用的五个asyncTask()方法也完成了。
Spring 任务执行
从 Spring 2.0 开始,框架通过TaskExecutor接口提供了执行任务的抽象。一个TaskExecutor做的和它听起来一样:它执行一个由 Java Runnable实现代表的任务。开箱即用,Spring 提供了许多适合不同需求的TaskExecutor实现。您可以在 http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/core/task/TaskExecutor.html 找到 TaskExecutor 实现的完整列表。
下面列出了一些常用的TaskExecutor实现:
SimpleAsyncTaskExecutor:每次调用时创建新线程;不重用现有线程SyncTaskExecutor:不异步执行;调用发生在调用线程中SimpleThreadPoolTaskExecutor:石英SimpleThreadPool的子类;当需要石英和非石英组件共享一个线程池时使用ThreadPoolTaskExecutor:TaskExecutor实现提供了通过 bean 属性配置ThreadPoolExecutor并将其公开为 SpringTaskExecutor的能力
每个TaskExecutor实现都有自己的用途,调用约定也是一样的。唯一的变化是在配置中,当定义您想要使用哪个TaskExecutor实现及其属性时,如果有的话。让我们看一个简单的例子,它打印出许多消息。我们将使用的TaskExecutor实现是SimpleAsyncTaskExecutor。首先,让我们创建一个包含任务执行逻辑的 bean 类,如下所示:
package com.apress.prospring5.ch11;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.task.TaskExecutor;
import org.springframework.stereotype.Component;
@Component
public class TaskToExecute {
private final Logger logger =
LoggerFactory.getLogger(TaskToExecute.class);
@Autowired
private TaskExecutor taskExecutor;
public void executeTask() {
for(int i=0; i < 10; ++ i) {
taskExecutor.execute(() ->
logger.info("Hello from thread: " +
Thread.currentThread().getName()));
}
}
}
这个类只是一个普通的 bean,需要将TaskExecutor作为依赖项注入,并定义一个方法executeTask()。executeTask()方法通过创建一个新的Runnable实例来调用所提供的TaskExecutor的execute方法,该实例包含我们想要为该任务执行的逻辑。这在这里可能不明显,因为 lambda 表达式用于创建Runnable实例。配置相当简单;它类似于上一节中描述的配置。这里我们唯一要考虑的是我们需要为一个TaxExecutor bean 提供一个声明,它需要被注入到TaskToExecute bean 中。
package com.apress.prospring5.ch11.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
@Configuration
@EnableAsync
@ComponentScan(basePackages = {"com.apress.prospring5.ch11"} )
public class AppConfig {
@Bean TaskExecutor taskExecutor() {
return new SimpleAsyncTaskExecutor();
}
}
在前面的配置中声明了一个简单的名为taskExecutor的类型为SimpleAsyncTaskExecutor的 bean。Spring IoC 容器将这个 bean 注入到TaskToExecute bean 中。要测试执行情况,您可以使用以下程序:
package com.apress.prospring5.ch11;
import com.apress.prospring5.ch11.config.AppConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
public class TaskExecutorDemo {
public static void main(String... args) throws Exception {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
TaskToExecute taskToExecute = ctx.getBean(TaskToExecute.class);
taskToExecute.executeTask();
System.in.read();
ctx.close();
}
}
当该示例运行时,它应该打印类似于以下内容的输出:
Hello from thread: SimpleAsyncTaskExecutor-1
Hello from thread: SimpleAsyncTaskExecutor-5
Hello from thread: SimpleAsyncTaskExecutor-3
Hello from thread: SimpleAsyncTaskExecutor-10
Hello from thread: SimpleAsyncTaskExecutor-8
Hello from thread: SimpleAsyncTaskExecutor-6
Hello from thread: SimpleAsyncTaskExecutor-2
Hello from thread: SimpleAsyncTaskExecutor-4
Hello from thread: SimpleAsyncTaskExecutor-9
Hello from thread: SimpleAsyncTaskExecutor-7
从输出中可以看到,每个任务(我们正在打印的消息)在执行时都会显示出来。我们打印出消息加上线程名(默认情况下是类名SimpleAsyncTaskExecutor)和线程号。
摘要
在这一章中,我们介绍了 Spring 对任务调度的支持。我们重点介绍了 Spring 内置的TaskScheduler抽象,并通过一个示例批处理数据更新作业演示了如何使用它来满足任务调度需求。我们还介绍了 Spring 如何支持异步执行任务的注释。此外,我们简要介绍了 Spring 的TaskExecutor和常见实现。
不需要 Spring Boot 部分,因为任务注释的调度和异步执行是spring-context库的一部分,它们也必须与 Spring Boot 配置一起使用。另外,配置调度和异步任务已经和使用 Spring 一样简单了;在这个问题上,Spring Boot 能做的改进不多。3
Footnotes 1
你可以在 www.quartz-scheduler.org 找到官方页面。
2
在 XML 中,只需声明<task:annotation-driven />就可以了。
3
但如果你是古玩,想把提供的项目转换成 Spring Boot,可以在这里找到一个小教程: https://spring.io/guides/gs/scheduling-tasks/
十二、使用 Spring 远程处理
企业应用通常需要与其他应用通信。举个例子,一个卖产品的公司;当客户下订单时,订单处理系统处理该订单并生成交易。在订单处理过程中,会对库存系统进行查询,以检查产品是否有货。订单确认后,通知会发送到履行系统,以便将产品交付给客户。最后,信息被发送到会计系统,生成发票,并处理付款。
大多数情况下,这个业务流程不是由单个应用完成的,而是由许多应用协同工作完成的。一些应用可能是内部开发的,其他的可能是从外部供应商那里购买的。此外,应用可以在不同位置的不同机器上运行,并且用不同的技术和编程语言(例如,Java。NET 或 C++)。在设计和实现应用时,执行应用之间的握手以构建高效的业务流程始终是一项关键任务。因此,应用要很好地参与到企业环境中,就需要通过各种协议和技术提供远程支持。
在 Java 世界中,自从 Java 第一次被创建以来,远程支持就已经存在了。在早期(Java 1.x ),大多数远程需求是通过使用传统的 TCP 套接字或 Java 远程方法调用(RMI)来实现的。在 J2EE 出现之后,EJB 和 JMS 成为应用间服务器通信的常见选择。XML 和互联网的快速发展带来了通过 HTTP 使用 XML 的远程支持,包括基于 XML 的 RPC 的 Java API(JAX RPC)、XML Web 服务的 Java API(JAX WS)和基于 HTTP 的技术(例如,Hessian 和 Burlap)。Spring 还提供了自己的基于 HTTP 的远程支持,称为 Spring HTTP invoker。近年来,为了应对互联网的爆炸式增长和更具响应性的 web 应用需求(例如,通过 Ajax),对应用更轻量级和高效的远程支持已经成为企业成功的关键。因此,用于 RESTful Web 服务的 Java API(JAX-RS)应运而生并迅速流行起来。其他协议,如 Comet 和 HTML5 WebSocket,也吸引了很多开发者。不用说,远程技术一直在快速发展。
就远程处理而言,如前所述,Spring 提供了自己的支持(通过 Spring HTTP invoker),以及支持前面提到的许多技术(例如,RMI、EJB、JMS、Hessian、Burlap、JAX-RPC、JAX- WS 和 JAX-RS)。这一章不可能涵盖所有的内容。因此,这里我们重点关注那些最常用的。具体来说,本章涵盖以下主题:
- Spring HTTP invoker:如果需要通信的两个应用都是基于 Spring 的,那么 Spring HTTP invoker 提供了一种简单有效的方法来调用其他应用公开的服务。我们将向您展示如何使用 Spring HTTP invoker 在服务层中公开服务,以及调用远程应用提供的服务。
- 在 Spring 中使用 JMS:Java 消息服务(JMS)提供了另一种在应用之间交换消息的异步和松耦合方式。我们将向您展示 Spring 如何使用 JMS 简化应用开发。
- 在 Spring 中使用 RESTful web 服务:RESTful web 服务专门围绕 HTTP 设计,是为应用提供远程支持以及使用 Ajax 支持高度交互式 web 应用前端的最常用技术。我们展示了 Spring MVC 如何使用 JAX-RS 为公开服务提供全面的支持,以及如何使用
RestTemplate类调用服务。我们还讨论了如何保护服务免受未经授权的访问。 - 在 Spring 中使用 AMQP:Spring Advanced Message Queuing Protocol(AMQP)的姐妹项目围绕 AMQP 提供了一个典型的类似 Spring 的抽象以及一个 RabbitMQ 实现。这个项目提供了丰富的功能,但是在这一章中,我们通过 RPC 支持的项目来关注它的远程功能。
对样本使用数据模型
在本章的示例中,我们将使用一个简单的数据模型,它只包含用于存储信息的SINGER表。该表由 Hibernate 基于下面显示的Singer类生成。该类及其属性是用标准 JPA 注释修饰的。
package com.apress.prospring5.ch12.entities;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
import static javax.persistence.GenerationType.IDENTITY;
@Entity
@Table(name = "singer")
public class Singer implements Serializable {
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "ID")
private Long id;
@Version
@Column(name = "VERSION")
private int version;
@Column(name = "FIRST_NAME")
private String firstName;
@Column(name = "LAST_NAME")
private String lastName;
@Temporal(TemporalType.DATE)
@Column(name = "BIRTH_DATE")
private Date birthDate;
//setters and getters
...
}
要填充这个表,您需要使用一个初始化器 bean。该类如下所示:
package com.apress.prospring5.ch12.services;
import com.apress.prospring5.ch12.entities.Singer;
import com.apress.prospring5.ch12.repos.SingerRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.Date;
import java.util.GregorianCalendar;
@Service
public class DBInitializer {
private Logger logger = LoggerFactory.getLogger(DBInitializer.class);
@Autowired
SingerRepository singerRepository;
@PostConstruct
public void initDB() {
logger.info("Starting database initialization...");
Singer singer = new Singer();
singer.setFirstName("John");
singer.setLastName("Mayer");
singer.setBirthDate(new Date(
(new GregorianCalendar(1977, 9, 16)).getTime().getTime()));
singerRepository.save(singer);
singer = new Singer();
singer.setFirstName("Eric");
singer.setLastName("Clapton");
singer.setBirthDate(new Date(
(new GregorianCalendar(1945, 2, 30)).getTime().getTime()));
singerRepository.save(singer);
singer = new Singer();
singer.setFirstName("John");
singer.setLastName("Butler");
singer.setBirthDate(new Date(
(new GregorianCalendar(1975, 3, 1)).getTime().getTime()));
singerRepository.save(singer);
logger.info("Database initialization finished.");
}
}
为 JPA 后端添加所需的依赖项
我们需要将所需的依赖项添加到项目中。下面的配置片段显示了使用 JPA 2 和 Hibernate 作为持久性提供者来实现服务层所需的依赖关系。此外,将使用 Spring Data JPA。
//pro-spring-15/build.gradle
ext {
//spring libs
springVersion = '5.0.0.RC1'
springDataVersion = '2.0.0.M3'
//logging libs
slf4jVersion = '1.7.25'
logbackVersion = '1.2.3'
junitVersion = '4.12'
//database library
h2Version = '1.4.194'
//persistency libraries
hibernateVersion = '5.2.10.Final'
hibernateJpaVersion = '1.0.0.Final'
atomikosVersion = '4.0.0M4'
spring = [
context : "org.springframework:spring-context:$springVersion",
aop : "org.springframework:spring-aop:$springVersion",
aspects : "org.springframework:spring-aspects:$springVersion",
tx : "org.springframework:spring-tx:$springVersion",
jdbc : "org.springframework:spring-jdbc:$springVersion",
contextSupport: "org.springframework:spring-context-support:$springVersion",
orm : "org.springframework:spring-orm:$springVersion",
data : "org.springframework.data:spring-data-jpa:$springDataVersion",
test : "org.springframework:spring-test:$springVersion"
]
hibernate = [
...
em : "org.hibernate:hibernate-entitymanager:$hibernateVersion",
tx : "com.atomikos:transactions-hibernate4:$atomikosVersion"
]
testing = [
junit: "junit:junit:$junitVersion"
]
misc = [
...
slf4jJcl : "org.slf4j:jcl-over-slf4j:$slf4jVersion",
logback : "ch.qos.logback:logback-classic:$logbackVersion",
lang3 : "org.apache.commons:commons-lang3:3.5",
guava : "com.google.guava:guava:$guavaVersion"
]
db = [
...
h2 : "com.h2database:h2:$h2Version"
]
}
//chapter12/spring-invoker/build.gradle
dependencies {
//we specify these dependencies for all submodules, except
// the boot module, that defines its own
if (!project.name.contains("boot")) {
//we exclude transitive dependencies, because spring-data
//will take care of these
compile (spring.contextSupport) {
exclude module: 'spring-context'
exclude module: 'spring-beans'
exclude module: 'spring-core'
}
//we exclude the 'hibernate' transitive dependency
//to have control over the version used
compile (hibernate.tx) {
exclude group: 'org.hibernate', module: 'hibernate'
}
compile misc.slf4jJcl, misc.logback, misc.lang3,
hibernate.em, misc.guava
}
testCompile testing.junit
}
实现和配置 SingerService
概述完依赖关系后,我们开始展示如何为本章中的示例实现和配置服务层。在接下来的小节中,我们将讨论使用 JPA 2、Spring Data JPA 和 Hibernate 作为持久性服务提供者来实现SingerService。然后,我们将介绍如何在 Spring 项目中配置服务层。
实现单一服务
在示例中,我们展示了如何向远程客户端公开针对歌手信息的各种操作的服务;这里显示的是SingerService界面:
package com.apress.prospring5.ch12.services;
import com.apress.prospring5.ch12.entities.Singer;
import java.util.List;
public interface SingerService {
List<Singer> findAll();
List<Singer> findByFirstName(String firstName);
Singer findById(Long id);
Singer save(Singer singer);
void delete(Singer singer);
}
这些方法应该是不言自明的。因为我们将使用 Spring Data JPA 的存储库支持,所以我们实现了SingerRepository接口,如下所示:
package com.apress.prospring5.ch12.repos;
import com.apress.prospring5.ch12.entities.Singer;
import org.springframework.data.repository.CrudRepository;
import java.util.List;
public interface SingerRepository extends CrudRepository<Singer, Long> {
List<Singer> findByFirstName(String firstName);
}
通过扩展CrudRepository<T,ID extends Serializable>接口,对于SingerService中的方法,我们只需要显式声明findByFirstName()方法。
下一个代码片段显示了SingerService接口的实现类:
package com.apress.prospring5.ch12.services;
import com.apress.prospring5.ch12.entities.Singer;
import com.apress.prospring5.ch12.repos.SingerRepository;
import com.google.common.collect.Lists;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service("singerService")
@Transactional
public class SingerServiceImpl implements SingerService {
@Autowired
private SingerRepository singerRepository;
@Override
@Transactional(readOnly = true)
public List<Singer> findAll() {
return Lists.newArrayList(singerRepository.findAll());
}
@Override
@Transactional(readOnly = true)
public List<Singer> findByFirstName(String firstName) {
return singerRepository.findByFirstName(firstName);
}
@Override
@Transactional(readOnly = true)
public Singer findById(Long id) {
return singerRepository.findById(id).get();
}
@Override
public Singer save(Singer singer) {
return singerRepository.save(singer);
}
@Override
public void delete(Singer singer) {
singerRepository.delete(singer);
}
}
实现基本完成,下一步是在 web 项目内配置 Spring 的ApplicationContext中的服务,这将在下一节讨论。
正在配置 SingerService
对于数据访问和事务,您可以使用一个简单的 Java 配置类,如前面介绍的和这里显示的:
package com.apress.prospring5.ch12.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.util.Properties;
@Configuration
@EnableJpaRepositories(basePackages = {"com.apress.prospring5.ch12.repos"})
@ComponentScan(basePackages = {"com.apress.prospring5.ch12"} )
public class DataServiceConfig {
private static Logger logger =
LoggerFactory.getLogger(DataServiceConfig.class);
@Bean
public DataSource dataSource() {
try {
EmbeddedDatabaseBuilder dbBuilder =
new EmbeddedDatabaseBuilder();
return dbBuilder.setType(EmbeddedDatabaseType.H2).build();
} catch (Exception e) {
logger.error("Embedded DataSource bean cannot be created!", e);
return null;
}
}
@Bean
public Properties hibernateProperties() {
Properties hibernateProp = new Properties();
hibernateProp.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
hibernateProp.put("hibernate.hbm2ddl.auto", "create-drop");
hibernateProp.put("hibernate.show_sql", true);
hibernateProp.put("hibernate.max_fetch_depth", 3);
hibernateProp.put("hibernate.jdbc.batch_size", 10);
hibernateProp.put("hibernate.jdbc.fetch_size", 50);
return hibernateProp;
}
@Bean
public PlatformTransactionManager transactionManager() {
return new JpaTransactionManager(entityManagerFactory());
}
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
return new HibernateJpaVendorAdapter();
}
@Bean
public EntityManagerFactory entityManagerFactory() {
LocalContainerEntityManagerFactoryBean factoryBean =
new LocalContainerEntityManagerFactoryBean();
factoryBean.setPackagesToScan("com.apress.prospring5.ch12.entities");
factoryBean.setDataSource(dataSource());
factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
factoryBean.setJpaProperties(hibernateProperties());
factoryBean.setJpaVendorAdapter(jpaVendorAdapter());
factoryBean.afterPropertiesSet();
return factoryBean.getNativeEntityManagerFactory();
}
}
因为我们通过 Spring MVC 公开 HTTP 调用程序,所以我们需要配置 web 应用。要在不使用任何 XML 的情况下配置 Spring Web MVC 应用,需要两个配置类,如下所示:
-
一个配置类实现了
WebMvcConfigurer接口。这个接口是在 Spring 3.1 中引入的,它定义了回调方法来为使用@EnableWebMvc启用的 Spring MVC 定制基于 Java 的配置。因为我们只需要公开一个 HTTP 服务(不需要 web 接口),所以在这种情况下一个空的实现就足够了。用@EnableWebMvc注释的这个接口的实现使用mvc名称空间代替了 Spring XML 配置。更复杂的示例配置将在第十六章中详细介绍。package com.apress.prospring5.ch12.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration @EnableWebMvc public class WebConfig implements WebMvcConfigurer { } -
另一个配置类实现了
org.springframework.web.WebApplicationInitializer或者扩展了一个现成的 Spring 实现。这个接口需要在 Spring 3.0+环境中实现,以编程方式配置ServletContext。这消除了提供一个web.xml文件来配置 web 应用的必要性。该类导入数据访问和事务的配置,并基于它创建根应用上下文。web 应用上下文是使用WebConfig类和配置类创建的,配置类定义了 HTTP invoker 服务的配置。这些类也可以合并成一个类,但是使用 Spring 的良好实践是将定制服务和基础设施 beans 保存在不同的类中。package com.apress.prospring5.ch12.config; import org.springframework.web.servlet.support. AbstractAnnotationConfigDispatcherServletInitializer; public class WebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { @Override protected Class<?>[] getRootConfigClasses() { return new Class<?>[]{ DataServiceConfig.class }; } @Override protected Class<?>[] getServletConfigClasses() { return new Class<?>[]{ HttpInvokerConfig.class, WebConfig.class }; } @Override protected String[] getServletMappings() { return new String[]{"/invoker/*"}; } }
因为这是一个 Spring MVC web 应用,所以需要创建一个 WAR 文件并将其部署到 servlet 容器中。有多种方法可以做到这一点,例如,独立的容器(如 Tomcat)、IDE 启动的 Tomcat 实例,或者与构建工具(如 Maven)一起运行的嵌入式 Tomcat 实例。您选择哪个选项取决于您的需求,但是对于本地开发环境,建议从您的构建工具或直接从您的 IDE 启动嵌入式实例。在本书的代码中,我们使用 Tomcat Server version 9.x,并在 Intellij IDEA 中设置了一个启动器来启动 web 应用。更多细节请参见本书源代码。此时,您应该构建 web 应用,并通过您选择的方法进行部署。如果您尝试在浏览器中加载http://localhost:8080/ URL,您会看到以下消息:
Spring Remoting: Simplifying Development of Distributed Applications
RMI services over HTTP should be correctly exposed when this page is visible.
这意味着 web 应用已经被正确部署,现在应该可以在http://localhost:8080/invoker/httpInvoker/singerService URL 访问到singerService bean 了。
公开服务
如果您要与之通信的应用也是 Spring 支持的,那么使用 Spring HTTP invoker 是一个不错的选择。它提供了一种非常简单的方法,将 Spring WebApplicationContext中的服务公开给远程客户端,远程客户端也使用 Spring HTTP invoker 来调用服务。公开和访问服务的过程将在下面的章节中详细介绍。HttpInvokerConfig类包含一个用于公开 HTTP invoker 服务的 bean。
package com.apress.prospring5.ch12.config;
import com.apress.prospring5.ch12.services.SingerService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter;
@Configuration
public class HttpInvokerConfig {
@Autowired
SingerService singerService;
@Bean(name = "/httpInvoker/singerService")
public HttpInvokerServiceExporter httpInvokerServiceExporter() {
HttpInvokerServiceExporter invokerService =
new HttpInvokerServiceExporter();
invokerService.setService(singerService);
invokerService.setServiceInterface(SingerService.class);
return invokerService;
}
}
用HttpInvokerServiceExporter类定义了一个httpInvokerServiceExporter bean,用于通过 HTTP invoker 将任何 Spring bean 作为服务导出。在 bean 中,定义了两个属性。第一个是service属性,表示提供服务的 bean。对于这个属性,注入了singerService bean。第二个属性是要公开的接口类型,即com.apress.prospring5.ch12.serviced.SingerService接口。
现在,服务层已经完成,可以公开给远程客户端使用了。
调用服务
通过 Spring HTTP invoker 调用服务很简单。首先我们配置一个 SpringApplicationContext,如下面的配置类所示:
package com.apress.prospring5.ch12.config;
import com.apress.prospring5.ch12.services.SingerService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean;
@Configuration
public class RmiClientConfig {
@Bean
public SingerService singerService() {
HttpInvokerProxyFactoryBean factoryBean =
new HttpInvokerProxyFactoryBean();
factoryBean.setServiceInterface(SingerService.class);
factoryBean.setServiceUrl(
"http://localhost:8080/invoker/httpInvoker/singerService");
factoryBean.afterPropertiesSet();
return (SingerService) factoryBean.getObject();
}
}
如前面客户端所示,声明了一个类型为HttpInvokerProxyFactoryBean的 bean。设置了两个属性。serviceUrl指定远程服务的位置,即http://localhost:8080/invoker/httpInvoker/singerService。第二个属性是服务的接口(即SingerService)。如果您正在为客户端开发另一个项目,您需要在客户端应用的类路径中拥有SingerService接口和Singer实体类。
下面的代码片段显示了一个调用远程服务的测试类。我们正在使用一个测试类,它使用RmiClientConfig类来创建一个测试上下文。SpringRunner类是在 Spring 上下文中运行 Junit 测试所需的SpringJUnit4ClassRunner的别名。你会在第十三章中了解到更多。
package com.apress.prospring5.ch12;
import com.apress.prospring5.ch12.config.RmiClientConfig;
import com.apress.prospring5.ch12.entities.Singer;
import com.apress.prospring5.ch12.services.DBInitializer;
import com.apress.prospring5.ch12.services.SingerService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.List;
import static org.junit.Assert.assertEquals;
@ContextConfiguration(classes = RmiClientConfig.class)
@RunWith(SpringRunner.class)
public class RmiTests {
private Logger logger = LoggerFactory.getLogger(RmiTests.class);
@Autowired
private SingerService singerService;
@Test
public void testRmiAll() {
List<Singer> singers = singerService.findAll();
assertEquals(3, singers.size());
listSingers(singers);
}
@Test
public void testRmiJohn() {
List<Singer> singers = singerService.findByFirstName("John");
assertEquals(2, singers.size());
listSingers(singers);
}
private void listSingers(List<Singer> singers){
singers.forEach(s -> logger.info(s.toString()));
}
}
测试类应该在部署 web 应用之后执行。测试应该通过,并列出由singerService bean 返回的Singer实例。预期输出如下所示:
//testRmiAll
INFO c.a.p.c.RmiTests - Singer - Id: 1, First name: John, Last name: Mayer,
Birthday: 1977-10-16
INFO c.a.p.c.RmiTests - Singer - Id: 2, First name: Eric, Last name: Clapton,
Birthday: 1945-03-30
INFO c.a.p.c.RmiTests - Singer - Id: 3, First name: John, Last name: Butler,
Birthday: 1975-04-01
//testRmiJohn - all singers with firstName='John'
INFO c.a.p.c.RmiTests - Singer - Id: 1, First name: John, Last name: Mayer,
Birthday: 1977-10-16
INFO c.a.p.c.RmiTests - Singer - Id: 3, First name: John, Last name: Butler,
Birthday: 1975-04-01
在 Spring 中使用 JMS
使用面向消息的中间件(通常称为 MQ 服务器)是支持应用间通信的另一种流行方式。消息队列(MQ)服务器的主要优点是它为应用集成提供了一种异步和松散耦合的方式。在 Java 世界中,JMS 是连接到 MQ 服务器发送或接收消息的标准。MQ 服务器维护一个队列和主题列表,应用可以连接到这些队列和主题,并发送和接收消息。以下是对队列和主题之间的区别的简要描述:
- 队列:队列用于支持点对点消息交换模型。当生产者向队列发送消息时,MQ 服务器将消息保存在队列中,并在下次消费者连接时将消息传递给一个(且仅一个)消费者。
- 主题:主题用于支持发布-订阅模型。任何数量的客户端都可以订阅主题中的消息。当针对该主题的消息到达时,MQ 服务器将它传递给订阅了该消息的所有客户机。当您有多个对同一条信息感兴趣的应用(例如,一个新闻提要)时,这种模型特别有用。
在 JMS 中,生产者连接到 MQ 服务器,并向队列或主题发送消息。消费者还连接到 MQ 服务器,并监听队列或感兴趣的消息主题。在 JMS 1.1 中,API 是统一的,因此生产者和消费者不需要处理不同的 API 来与队列和主题进行交互。在本节中,我们将重点关注使用队列的点对点方式,这是企业中更常用的模式。
从 Spring Framework 4.0 开始,已经实现了对 JMS 2.0 的支持。JMS 2.0 的功能可以通过在类路径中包含 JMS 2.0 JAR 来使用,同时保留对 1.x 的向后兼容性。因此,在本书中,示例将只与 JMS 2.x 相关。
在撰写本文时,ActiveMQ 不支持 JMS 2.0 因此,我们将利用 HornetQ(包含从 2.4.0.Final 开始的 JMS 2.0 支持)作为这个示例中的消息代理,并将使用一个独立的服务器。下载和安装 HornetQ 不在本书讨论范围之内;请参考 http://docs.jboss.org/hornetq/2.4.0.Final/docs/quickstart-guide/html/index.html 的文档。 2
需要几个新的依赖项,这里显示了所需的梯度配置:
//pro-spring-15/build.gradle
ext {
jmsVersion = '2.0'
hornetqVersion = '2.4.0.Final'
spring = [
...
jms : "org.springframework:spring-jms:$springVersion"
]
misc = [
...
Hornetq : "org.hornetq:hornetq-jms-client:$hornetqVersion"
]
...
}
//chapter12/jms-hornetq/build.gradle
dependencies {
compile spring.jms, misc.jms
}
安装服务器后,我们需要在 HornetQ JMS 配置文件中创建一个队列。该文件位于您提取 HornetQ 的目录下。文件的位置是config/stand-alone/non-clustered hornetq-jms.xml,我们需要添加队列定义,如下图所示:
<configuration
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="urn:hornetq /schema/hornetq-jms.xsd">
...
<queue name="prospring5">
<entry name="/queue/prospring5"/>
</queue>
</configuration>
现在,通过运行run.sh脚本(取决于您的操作系统)启动 HornetQ 服务器,并确保服务器启动时没有任何错误。只要确保您在日志中看到类似的东西,没有例外,并确保下划线行在那里。
...
00:36:21,171 INFO [org.hornetq.core.server] HQ221035: Live Server Obtained live lock
00:36:21,841 INFO [org.hornetq.core.server] HQ221003:
trying to deploy queue jms.queue.DLQ
00:36:21,852 INFO [org.hornetq.core.server] HQ221003:
// the queue configured in the previous configuration sample
trying to deploy queue jms.queue.prospring5
00:36:21,853 INFO [org.hornetq.core.server] HQ221003:
trying to deploy queue jms.queue.ExpiryQueue
00:36:21,993 INFO [org.hornetq.core.server] HQ221020:
Started Netty Acceptor version 4.0.13.Final localhost:5455
00:36:21,996 INFO [org.hornetq.core.server] HQ221020:
Started Netty Acceptor version 4.0.13.Final localhost:5445
00:36:21,997 INFO [org.hornetq.core.server] HQ221007: Server is now live
现在必须提供一个 Spring 配置来连接到这个服务器并访问之前配置的prospring5队列。通常,应该有两个配置类,一个用于消息发送者,一个用于消息监听器,但是因为使用配置类的 Spring JMS 配置非常实用,并且不需要很多 beans,所以我们将所有配置放在一个类中,如下所示:
package com.apress.prospring5.ch12.config;
import org.hornetq.api.core.TransportConfiguration;
import org.hornetq.core.remoting.impl.netty.NettyConnectorFactory;
import org.hornetq.core.remoting.impl.netty.TransportConstants;
import org.hornetq.jms.client.HornetQJMSConnectionFactory;
import org.hornetq.jms.client.HornetQQueue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jms.annotation.EnableJms;
import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
import org.springframework.jms.config.JmsListenerContainerFactory;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.listener.DefaultMessageListenerContainer;
import javax.jms.ConnectionFactory;
import java.util.HashMap;
import java.util.Map;
@Configuration
@EnableJms
@ComponentScan("com.apress.prospring5.ch12")
public class AppConfig {
@Bean HornetQQueue prospring5() {
return new HornetQQueue("prospring5");
}
@Bean ConnectionFactory connectionFactory() {
Map<String, Object> connDetails = new HashMap<>();
connDetails.put(TransportConstants.HOST_PROP_NAME, "127.0.0.1");
connDetails.put(TransportConstants.PORT_PROP_NAME, "5445");
TransportConfiguration transportConfiguration = new TransportConfiguration(
NettyConnectorFactory.class.getName(), connDetails);
return new HornetQJMSConnectionFactory(false, transportConfiguration);
}
@Bean
public JmsListenerContainerFactory<DefaultMessageListenerContainer>
jmsListenerContainerFactory() {
DefaultJmsListenerContainerFactory factory =
new DefaultJmsListenerContainerFactory();
factory.setConnectionFactory(connectionFactory());
factory.setConcurrency("3-5");
return factory;
}
@Bean JmsTemplate jmsTemplate() {
JmsTemplate jmsTemplate = new JmsTemplate(connectionFactory());
jmsTemplate.setDefaultDestination(prospring5());
return jmsTemplate;
}
}
javax.jms.ConnectionFactory接口实现由 HornetQ Java 库(HornetQJMSConnectionFactory类)提供,用于创建与 JMS 提供者的连接。然后,声明一个类型为JmsListenerContainerFactory的 bean,它将创建使用普通 JMS 客户机 API 接收 JMS 消息的消息监听器容器。jmsTemplate bean 将用于向prospring5队列发送 JMS 消息。
要接收 JMS 消息,必须声明一个消息监听器组件,提供目的地(即prospring5队列)和 JMS 容器工厂jmsListenerContainerFactory。
在 Spring 中实现 JMS 侦听器
在 Spring 4.1 之前,为了开发一个消息监听器,我们需要创建一个实现javax.jms.MessageListener接口并实现其onMessage()方法的类。在 Spring 4.1 中添加了@JmsListener注释。该注释用于 bean 方法,以将它们标记为指定目的地(队列或主题)上的 JMS 消息侦听器的目标。下面的代码片段描述了SimpleMessageListener类和 bean 声明:
package com.apress.prospring5.ch12;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.TextMessage;
@Component("messageListener")
public class SimpleMessageListener{
private static final Logger logger =
LoggerFactory.getLogger(SimpleMessageListener.class);
@JmsListener(destination = "prospring5", containerFactory =
"jmsListenerContainerFactory")
public void onMessage(Message message) {
TextMessage textMessage = (TextMessage) message;
try {
logger.info(">>> Received: " + textMessage.getText());
} catch (JMSException ex) {
logger.error("JMS error", ex);
}
}
}
这里将该方法命名为onMessage,以使其目的更明显。在用@JmsListener方法注释的onMessage()中,javax.jms.Message接口的一个实例将在消息到达时被传递。在该方法中,消息被强制转换为javax.jms.TextMessage接口的实例,并且使用TextMessage.getText()方法检索文本形式的消息体。有关可能的消息格式列表,请参考当前的 JEE 在线文档。
通过在配置类上使用@EnableJms或者通过使用等价的 XML 元素声明(即<jms:annotation-driven/>)来完成对@JmsListener注释的处理。
现在让我们看看如何向propring5队列发送消息。
在 Spring 中发送 JMS 消息
让我们看看如何在 Spring 中使用 JMS 发送消息。为此,我们将使用类型为org.springframework.jms.core.JmsTemplate的便捷 bean jmsTemplate。首先我们将开发一个MessageSender接口及其实现类SimpleMessageSender。下面的代码片段分别显示了接口和类:
//MessageSender.java
package com.apress.prospring5.ch12;
public interface MessageSender {
void sendMessage(String message);
}
//SimpleMessageSender.java
package com.apress.prospring5.ch12;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.Session;
import javax.jms.TextMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.core.MessageCreator;
import org.springframework.stereotype.Component;
@Component("messageSender")
public class SimpleMessageSender implements MessageSender {
private static final Logger logger =
LoggerFactory.getLogger(SimpleMessageSender.class);
@Autowired
private JmsTemplate jmsTemplate;
@Override
public void sendMessage(final String message) {
jmsTemplate.setDeliveryDelay(5000L);
this.jmsTemplate.send(new MessageCreator() {
@Override
public Message createMessage(Session session)
throws JMSException {
TextMessage jmsMessage = session.createTextMessage(message);
logger.info(">>> Sending: " + jmsMessage.getText());
return jmsMessage;
}
});
}
}
如您所见,注入了一个JmsTemplate实例。在sendMessage()方法中,我们调用了JmsTemplate.send()方法,并就地构造了org.springframework.jms.core.MessageCreator接口的一个实例。在MessageCreator实例中,实现了createMessage()方法来创建一个新的TextMessage实例,该实例将被发送到 HornetQ。
消息侦听器和发送方 bean 声明都是使用组件扫描来获取的。
现在,让我们将发送和接收结合起来,看看 JMS 的运行情况。以下代码片段显示了发送消息和接收消息的主要测试程序:
package com.apress.prospring5.ch12;
import com.apress.prospring5.ch12.config.AppConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;
import java.util.Arrays;
public class JmsHornetQSample {
public static void main(String... args) throws Exception{
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
MessageSender messageSender =
ctx.getBean("messageSender", MessageSender.class);
for(int i=0; i < 10; ++i) {
messageSender.sendMessage("Test message: " + i);
}
System.in.read();
ctx.close();
}
}
程序很简单。运行程序将消息发送到队列。SimpleMessageListener类接收这些消息,您可以在控制台中看到以下输出:
INFO c.a.p.c.SimpleMessageSender - >>> Sending: Test message: 0
INFO c.a.p.c.SimpleMessageSender - >>> Sending: Test message: 1
INFO c.a.p.c.SimpleMessageSender - >>> Sending: Test message: 2
INFO c.a.p.c.SimpleMessageSender - >>> Sending: Test message: 3
INFO c.a.p.c.SimpleMessageSender - >>> Sending: Test message: 4
INFO c.a.p.c.SimpleMessageSender - >>> Sending: Test message: 5
INFO c.a.p.c.SimpleMessageSender - >>> Sending: Test message: 6
INFO c.a.p.c.SimpleMessageSender - >>> Sending: Test message: 7
INFO c.a.p.c.SimpleMessageSender - >>> Sending: Test message: 8
INFO c.a.p.c.SimpleMessageSender - >>> Sending: Test message: 9
INFO c.a.p.c.SimpleMessageListener - >>> Received: Test message: 0
INFO c.a.p.c.SimpleMessageListener - >>> Received: Test message: 1
INFO c.a.p.c.SimpleMessageListener - >>> Received: Test message: 2
INFO c.a.p.c.SimpleMessageListener - >>> Received: Test message: 3
INFO c.a.p.c.SimpleMessageListener - >>> Received: Test message: 4
INFO c.a.p.c.SimpleMessageListener - >>> Received: Test message: 5
INFO c.a.p.c.SimpleMessageListener - >>> Received: Test message: 6
INFO c.a.p.c.SimpleMessageListener - >>> Received: Test message: 7
INFO c.a.p.c.SimpleMessageListener - >>> Received: Test message: 8
INFO c.a.p.c.SimpleMessageListener - >>> Received: Test message: 9
Spring Boot 阿尔特弥斯发酵剂
使用 Spring Boot 使 JMS 应用的开发更加实用的可能性在第九章中有所暗示,其中介绍了一个涉及数据库和队列的分布式事务示例。
当 Spring Boot 检测到 Artemis 在类路径中可用时,它可以自动配置一个javax.jms.ConnectionFactory bean。Am 嵌入式 JMS 代理是自动启动和配置的。Artemis 可用于多种模式,可使用特殊的 Artemis 属性进行配置,这些属性可在application.properties文件中设置。
Artemis 可以在native模式下使用,与代理的连接由netty协议提供。application.properties文件可以是这样的:
spring.artemis.mode=native
spring.artemis.host=0.0.0.0
spring.artemis.port=61617
spring.artemis.user=prospring5
spring.artemis.password=prospring5
使用 Spring Boot 和 Artemis 创建 JMS 应用的最简单方法是使用嵌入式服务器;所需要的只是保存消息的队列的名称。因此,application.properties文件看起来像这样:
spring.artemis.mode=embedded
spring.artemis.embedded.queues=prospring5
这是将在本节的源代码中使用的方法,因为它需要最少的配置定制。使用这种方法,所需要的就是application.properties配置文件和Application类,如下所示:
package com.apress.prospring5.ch12;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.
jms.DefaultJmsListenerContainerFactoryConfigurer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
import org.springframework.jms.config.JmsListenerContainerFactory;
import org.springframework.jms.core.JmsTemplate;
import javax.jms.ConnectionFactory;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.TextMessage;
@SpringBootApplication
public class Application {
private static Logger logger =
LoggerFactory.getLogger(Application.class);
@Bean
public JmsListenerContainerFactory<?>
connectionFactory(ConnectionFactory connectionFactory,
DefaultJmsListenerContainerFactoryConfigurer configurer) {
DefaultJmsListenerContainerFactory factory =
new DefaultJmsListenerContainerFactory();
configurer.configure(factory, connectionFactory);
return factory;
}
public static void main(String... args) throws Exception {
ConfigurableApplicationContext ctx =
SpringApplication.run(Application.class, args);
JmsTemplate jmsTemplate = ctx.getBean(JmsTemplate.class);
jmsTemplate.setDeliveryDelay(5000L);
for (int i = 0; i < 10; ++i) {
logger.info(">>> Sending: Test message: " + i);
jmsTemplate.convertAndSend("prospring5", "Test message: " + i);
}
System.in.read();
ctx.close();
}
@JmsListener(destination = "prospring5", containerFactory = "connectionFactory")
public void onMessage(Message message) {
TextMessage textMessage = (TextMessage) message;
try {
logger.info(">>> Received: " + textMessage.getText());
} catch (JMSException ex) {
logger.error("JMS error", ex);
}
}
}
当然,要实现这一点,必须将 Spring Boot JMS 启动器库用作依赖项,并且 Artemis 服务器必须位于类路径中。梯度配置如下所示:
//pro-spring-15/build.gradle
ext {
bootVersion = '2.0.0.M1'
artemisVersion = '2.1.0'
boot = [
...
starterJms :
"org.springframework.boot:spring-boot-starter-artemis:$bootVersion"
]
testing = [
junit: "junit:junit:$junitVersion"
]
misc = [
...
artemisServer :
"org.apache.activemq:artemis-jms-server:$artemisVersion"
]
...
}
//boot-jms/build.gradle
buildscript {
repositories {
...
}
dependencies {
classpath boot.springBootPlugin
}
}
apply plugin: 'org.springframework.boot'
dependencies {
compile boot.starterJms, misc.artemisServer
}
将spring-boot-starter- artemis声明为依赖项消除了使用@EnableJms处理用@JmsListener注释的方法的需要。jmsTemplate bean 是由 Spring Boot 创建的,默认配置由application.properties文件中设置的属性提供,它不仅可以发送消息,还可以使用receive()方法接收消息,但是这是同步完成的,这意味着jmsTemplate将会阻塞。这就是为什么使用一个显式配置的JmsListenerContainerFactory bean 来创建一个DefaultMessageListenerContainer,它将异步地使用消息,并具有最大的连接效率。
如果你运行Application类,你会在控制台中得到一些输出,和 HornetQ 的输出真的很像。
在 Spring 中使用 RESTful-WS
如今,RESTful-WS 可能是远程访问中使用最广泛的技术。从通过 HTTP 的远程服务调用到支持 Ajax 风格的交互式 web 前端,RESTful-WS 正被广泛采用。RESTful web 服务流行有几个原因。
- 容易理解:RESTful web 服务是围绕 HTTP 设计的。URL 和 HTTP 方法一起指定了请求的意图。例如,带有 GET 的 HTTP 方法的 URL
http://somedomain.com/restful/customer/1意味着客户端想要检索客户信息,其中客户 ID 等于 1。 - 轻量级:与基于 SOAP 的 web 服务相比,RESTful 要轻量级得多,基于 SOAP 的 web 服务包含大量元数据来描述客户端想要调用的服务。对于 RESTful 请求和响应,它只是一个 HTTP 请求和响应,就像任何其他 web 应用一样。
- 防火墙友好:因为 RESTful web 服务被设计为可以通过 HTTP(或 HTTPS)访问,所以应用变得更加防火墙友好,并且容易被远程客户端访问。
在本节中,我们将介绍 RESTful-WS 的基本概念以及 Spring 通过其 Spring MVC 模块对 RESTful-WS 的支持。
RESTful Web 服务简介
RESTful-WS 中的 REST 是具象状态转移的简称,是一种架构风格。REST 定义了一组架构约束,它们共同描述了访问资源的统一接口。这个统一接口的主要概念包括资源的识别和通过表示对资源的操作。对于资源的标识,应该可以通过统一资源标识符(URI)访问一条信息。比如 URL http://somedomain.com/api/singer/1 是一个代表资源的 URI,是一段歌手信息,标识符为 1。如果标识符为 1 的歌手不存在,客户端将得到一个 404 HTTP 错误,就像网站上的“page not found”错误一样。另一个例子, http://somedomain.com/api/singers ,是代表歌手信息列表的资源的 URI。那些可识别的资源可以通过各种表示来管理,如表 12-1 所示。
表 12-1。
Representations for Manipulating Resources
| HTTP 方法 | 描述 | | --- | --- | | 得到 | 获取资源的表示形式。 | | 头 | 与 GET 相同,没有响应体。通常用于获取标题。 | | 邮政 | POST 创建一个新资源。 | | 放 | 将更新放入资源。 | | 删除 | 删除删除资源。 | | 选择 | 选项检索允许的 HTTP 方法。 |对于 RESTful web 服务的详细描述,我们推荐 Ajax 和 REST 食谱:克里斯蒂安·格罗斯的问题解决方法(Apress,2006)。
为样本添加必需的依赖项
为了构建 Spring REST 应用,我们需要添加一些新的依赖项。因为我们将把对象从服务器发送到客户机,所以我们需要一个库来序列化和反序列化它们。我们还想向您展示,在同一个应用中可以使用多种类型的序列化,在本例中是 XML 和 JSON,因此需要它们各自的库。表 12-2 列出了依赖关系及其用途。
表 12-2。
Dependencies for RESTful Web Services
| GroupId:ModuleId | 版本 | 目的 | | --- | --- | --- | | `org.springframework:spring-` `oxm` | RC1 | Spring 对象到 XML 映射模块。 | | `org.codehaus.jackson:jacksonDatabind` | pr3 | 杰克逊 JSON 处理器支持 JSON 格式的数据。 | | `org.codehaus.castor:castor-xml` | 1.4.1 | Castor XML 库将用于 XML 数据的编组和解组。 | | `org.apache.httpcomponents:httpclient` | 4.5.3 | Apache HTTP 组件项目。HTTP 客户端库将用于 RESTful-WS 调用。 |设计 Singer RESTful Web 服务
在开发 RESTful-WS 应用时,第一步是设计服务结构,包括将支持哪些 HTTP 方法,以及不同操作的目标 URL。对于 singer RESTful web 服务,我们希望支持查询、创建、更新和删除操作。对于查询,我们希望支持通过 ID 检索所有歌手或单个歌手。
这些服务将被实现为 Spring MVC 控制器。名字是SingerController类,在包com.apress.prospring5.ch12下。表 12-3 显示了 URL 模式、HTTP 方法、描述和相应的控制器方法。对于 URL,所有的都是相对于http://localhost:8080的。数据格式方面,XML 和 JSON 都支持。将根据客户端 HTTP 请求头的接受媒体类型提供相应的格式。
表 12-3。
XMLHttpRequest Methods and Properties Table
| 统一资源定位器 | HTTP 方法 | 描述 | 控制器方法 | | --- | --- | --- | --- | | `/singer/listdata` | 得到 | 检索到所有歌手 | `listData` | | `/singer/id` | 得到 | 通过 id 检索歌手 | `findBySingerId(...)` | | `/singer` | 邮政 | 创造一个新的歌手 | `create(...)` | | `/singer/id` | 放 | 按 ID 更新歌手 | `update(...)` | | `/singer/id` | 删除 | 按 ID 删除歌手 | `delete(...)` |使用 Spring MVC 公开 RESTful Web 服务
在这一节中,我们将向您展示如何使用 Spring MVC 将 singer 服务公开为 RESTful web 服务,正如上一节中所设计的那样。这个示例建立在 Spring HTTP invoker 示例中使用的一些SingerService类之上。
你已经熟悉了Singer类,这里就不再展示代码了。但是要序列化和反序列化歌手列表,我们需要将它封装在一个容器中。 3 这里可以看到Singers类。它只有一个属性,那就是一个Singer对象的列表。目的是支持将歌手列表(由SingerController类中的listData()方法返回)转换成 XML 或 JSON 格式。
package com.apress.prospring5.ch12;
import com.apress.prospring5.ch12.entities.Singer;
import java.io.Serializable;
import java.util.List;
public class Singers implements Serializable {
private List<Singer> singers;
public Singers() {
}
public Singers(List<Singer> singers) {
this.singers = singers;
}
public List<Singer> getSingers() {
return singers;
}
public void setSingers(List<Singer> singers) {
this.singers = singers;
}
}
配置 Castor XML
为了支持将返回的歌手信息转换成 XML 格式,我们将使用 Castor XML 库( http://castor.codehaus.org )。Castor 支持 POJO 和 XML 转换之间的几种模式,在这个示例中,我们使用一个 XML 文件来定义映射。下面的 XML 片段显示了映射文件(oxm-mapping.xml):
<mapping>
<class name="com.apress.prospring5.ch12.Singers">
<field name="singers"
type="com.apress.prospring5.ch12.entities.Singer"
collection="arraylist">
<bind-xml name="singer"/>
</field>
</class>
<class name="com.apress.prospring5.ch12.entities.Singer"
identity="id">
<map-to xml="singer" />
<field name="id" type="long">
<bind-xml name="id" node="element"/>
</field>
<field name="firstName" type="string">
<bind-xml name="firstName" node="element" />
</field>
<field name="lastName" type="string">
<bind-xml name="lastName" node="element" />
</field>
<field name="birthDate" type="string" handler="dateHandler">
<bind-xml name="birthDate" node="element" />
</field>
<field name="version" type="integer">
<bind-xml name="version" node="element" />
</field>
</class>
<field-handler name="dateHandler"
class="com.apress.prospring5.ch12.DateTimeFieldHandler">
<param name="date-format" value="yyyy-MM-dd"/>
</field-handler>
</mapping>
定义了两个映射。第一个<class>标签映射了Singers类,在这个类中,它的 singers 属性(singers对象的一个List)是使用<bind-xml name="singer"/>标签映射的。然后映射Singer对象(第二个<class>标签内有<map-to xml="singer" />标签)。此外,为了支持从java.util.Date类型(对于Singer的birthDate属性)的转换,我们实现了一个定制的 Castor 字段处理程序。以下代码片段显示了字段处理程序:
package com.apress.prospring5.ch12;
import org.exolab.castor.mapping.GeneralizedFieldHandler;
import org.exolab.castor.mapping.ValidityException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Properties;
public class DateTimeFieldHandler extends GeneralizedFieldHandler {
private static Logger logger =
LoggerFactory.getLogger(DateTimeFieldHandler.class);
private static String dateFormatPattern;
@Override
public void setConfiguration(Properties config) throws ValidityException {
dateFormatPattern = config.getProperty("date-format");
}
@Override
public Object convertUponGet(Object value) {
Date dateTime = (Date) value;
return format(dateTime);
}
@Override
public Object convertUponSet(Object value) {
String dateTimeString = (String) value;
return parse(dateTimeString);
}
@Override
public Class<Date> getFieldType() {
return Date.class;
}
protected static String format(final Date dateTime) {
String dateTimeString = "";
if (dateTime != null) {
SimpleDateFormat sdf =
new SimpleDateFormat(dateFormatPattern);
dateTimeString = sdf.format(dateTime);
}
return dateTimeString;
}
protected static Date parse(final String dateTimeString) {
Date dateTime = new Date();
if (dateTimeString != null) {
SimpleDateFormat sdf =
new SimpleDateFormat(dateFormatPattern);
try {
dateTime = sdf.parse(dateTimeString);
} catch (ParseException e) {
logger.error("Not a valida date:" + dateTimeString, e);
}
}
return dateTime;
}
}
我们扩展了 Castor 的org.exolab.castor.mapping.GeneralizedFieldHandler类,并实现了convertUponGet()、convertUponSet()和getFieldType()方法。在这些方法中,我们实现了逻辑来执行由 Castor 使用的Date和String之间的转换。
此外,我们还定义了一个用于 Castor 的属性文件。以下代码片段显示了文件(castor.properties)的内容:
org.exolab.castor.indent=true
该属性指示 Castor 生成带缩进的 XML,这样在测试时更容易阅读。
实现单控制器
下一步是实现控制器类SingerController。以下代码片段显示了该类的内容,该类实现了表 12-3 中的所有方法:
package com.apress.prospring5.ch12;
import com.apress.prospring5.ch12.entities.Singer;
import com.apress.prospring5.ch12.services.SingerService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
@Controller
@RequestMapping(value="/singer")
public class SingerController {
final Logger logger =
LoggerFactory.getLogger(SingerController.class);
@Autowired
private SingerService singerService;
@ResponseStatus(HttpStatus.OK)
@RequestMapping(value = "/listdata", method = RequestMethod.GET)
@ResponseBody
public Singers listData() {
return new Singers(singerService.findAll());
}
@RequestMapping(value="/{id}", method=RequestMethod.GET)
@ResponseBody
public Singer findSingerById(@PathVariable Long id) {
return singerService.findById(id);
}
@RequestMapping(value="/", method=RequestMethod.POST)
@ResponseBody
public Singer create(@RequestBody Singer singer) {
logger.info("Creating singer: " + singer);
singerService.save(singer);
logger.info("Singer created successfully with info: " + singer);
return singer;
}
@RequestMapping(value="/{id}", method=RequestMethod.PUT)
@ResponseBody
public void update(@RequestBody Singer singer,
@PathVariable Long id) {
logger.info("Updating singer: " + singer);
singerService.save(singer);
logger.info("Singer updated successfully with info: " + singer);
}
@RequestMapping(value="/{id}", method=RequestMethod.DELETE)
@ResponseBody
public void delete(@PathVariable Long id) {
logger.info("Deleting singer with id: " + id);
Singer singer = singerService.findById(id);
singerService.delete(singer);
logger.info("Singer deleted successfully");
}
}
上节课的要点如下:
- 该类用
@Controller进行了注释,表明它是一个 Spring MVC 控制器。 - 类级注释
@RequestMapping(value="/singer")表明这个控制器将被映射到主 web 上下文下的所有 URL。在这个例子中,http://localhost:8080/singer下的所有 URL 都将由这个控制器处理。 - 本章前面实现的服务层中的
SingerService被自动连接到控制器中。 - 每个方法的
@RequestMapping注释指示 URL 模式和它将被映射到的相应 HTTP 方法。例如,listData()方法将被映射到http://localhost:8080/singer/listdataURL,并带有一个 HTTP GET 方法。对于update()方法,它将被映射到 URLhttp://localhost:8080/singer/\protect\T1\textbraceleftid\protect\T1\textbraceright,使用 HTTP PUT 方法。 @ResponseBody注释适用于所有方法。这指示方法的所有返回值都应该直接写入 HTTP 响应流,而不是与视图匹配。- 对于接受路径变量的方法(例如,
findSingerById()方法),路径变量用@PathVariable标注。这指示 Spring MVC 将 URL 中的 path 变量(例如,http://localhost:8080/singer/1)绑定到findSingerById()方法的id参数中。注意对于id参数,类型是Long,而Spring的类型转换系统会自动为我们处理从String到Long的转换。 - 对于
create()和update()方法,Singer参数被标注为@RequestBody。这指示 Spring 自动将 HTTP 请求体中的内容绑定到Singer域对象中。转换将由支持格式的HttpMessageConverter<Object>接口的声明实例(在包org.springframework.http.converter下)完成,这将在本章后面讨论。
从 Spring 4.0 开始,引入了专用于 REST 的控制器注释@RestController。这是一个方便的注释,它本身用@Controller和@ResponseBody进行了注释。当用在控制器类上时,所有用@RequestMapping标注的方法都会自动用@ResponseBody标注。使用该注释编写的SingerController版本将在本章后面介绍。
配置 Spring Web 应用
需要一个 Spring web 应用来解析客户端发送的 REST 请求,因此需要对它进行配置。在本章的前面,我们介绍了一个简单的 web 应用配置。现在,必须用 HTTP message converter beans for XML 和 JSON 来丰富这个配置。
Spring web 应用遵循前端控制器设计模式, 4 ,其中所有请求由单个控制器接收,该控制器随后将它们分派给适当的处理程序(控制器类)。这个中央调度程序是org.springframework.web.servlet.DispatcherServlet的一个实例,由一个AbstractAnnotationConfigDispatcherServletInitializer类注册,这个类需要扩展来替换web.xml配置。在本节的示例中,执行此操作的类WebInitializer如下所示:
package com.apress.prospring5.ch12.init;
import com.apress.prospring5.ch12.config.DataServiceConfig;
import org.springframework.web.servlet.support.
AbstractAnnotationConfigDispatcherServletInitializer;
public class WebInitializer extends
AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[]{
DataServiceConfig.class
};
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[]{
WebConfig.class
};
}
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
}
在 Spring MVC 中,每个DispatchServlet都有自己的WebApplicationContext(然而,在DataServiceConfig.class中定义的所有服务层 beans,称为根WebApplicationContext,也可以用于每个 servlet 自己的WebApplicationContext)。
getServletMappings()方法指示 web 容器(例如,Tomcat)模式/(例如,http://localhost:8080/singer)下的所有 URL 都将由 RESTful servlet 处理。当然,我们可以在那里添加一个上下文,比如/ch12,但是对于本节中需要的示例,我们希望保持 URL 尽可能的短并且目的尽可能的明显。
带有 HTTP 消息转换器的 Spring MVC 配置类(WebConfig类)如下所示:
package com.apress.prospring5.ch12.init;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.
MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.xml.MarshallingHttpMessageConverter;
import org.springframework.oxm.castor.CastorMarshaller;
import org.springframework.web.servlet.config.annotation.
DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {"com.apress.prospring5.ch12"})
public class WebConfig extends WebMvcConfigurer {
@Autowired ApplicationContext ctx;
@Bean
public MappingJackson2HttpMessageConverter
mappingJackson2HttpMessageConverter() {
MappingJackson2HttpMessageConverter
mappingJackson2HttpMessageConverter =
new MappingJackson2HttpMessageConverter();
mappingJackson2HttpMessageConverter.setObjectMapper(objectMapper());
return mappingJackson2HttpMessageConverter;
}
@Override
public void configureDefaultServletHandling(
DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
@Bean
public ObjectMapper objectMapper() {
ObjectMapper objMapper = new ObjectMapper();
objMapper.enable(SerializationFeature.INDENT_OUTPUT);
objMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
objMapper.setDateFormat(df);
return objMapper;
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(mappingJackson2HttpMessageConverter());
converters.add(singerMessageConverter());
}
@Bean MarshallingHttpMessageConverter singerMessageConverter() {
MarshallingHttpMessageConverter mc = new MarshallingHttpMessageConverter();
mc.setMarshaller(castorMarshaller());
mc.setUnmarshaller(castorMarshaller());
List<MediaType> mediaTypes = new ArrayList<>();
MediaType mt = new MediaType("application", "xml");
mediaTypes.add(mt);
mc.setSupportedMediaTypes(mediaTypes);
return mc;
}
@Bean CastorMarshaller castorMarshaller() {
CastorMarshaller castorMarshaller = new CastorMarshaller();
castorMarshaller.setMappingLocation(
ctx.getResource( "classpath:spring/oxm-mapping.xml"));
return castorMarshaller;
}
}
前一节课的重点如下:
@EnableWebMvc注释 5 启用对 Spring MVC 的注释支持(即@Controller注释),并注册 Spring 的类型转换和格式化系统。此外,在该注释的定义下,启用了 JSR-303 验证支持。configureMessageConverters(...)方法 6 声明了将用于支持格式的媒体转换的HttpMessageConverter实例。因为我们将同时支持 JSON 和 XML 作为数据格式,所以声明了两个转换器。第一个是MappingJackson2HttpMessageConverter,它是 Spring 对 Jackson JSON 库的支持。 7 另一个是MarshallingHttpMessageConverter,由spring-oxm模块提供,用于 XML 的编组/解组。在MarshallingHttpMessageConverter中,我们需要定义要使用的编组器和解组器,在本例中是由 Castor 提供的。- 对于
castorMarshallerbean,我们使用 Spring 提供的类org.springframework.oxm.castor.CastorMarshaller,它与 Castor 集成在一起,我们提供 Castor 处理所需的映射位置。 @ComponentScan annotation8 指示 Spring 扫描控制器类的指定包。
现在,服务器端服务完成了。此时,您应该构建包含 web 应用的 WAR 文件,或者如果您使用 IntelliJ IDEA 或 STS 之类的 IDE,启动 Tomcat 实例。
使用 curl 测试 RESTful-WS
让我们对我们实现的 RESTful web 服务做一个快速测试。一个简单的方法是使用curl、、 9 、,这是一个使用 URL 语法传输数据的命令行工具。要使用该工具,只需从网站下载并解压到您的计算机上。
例如,要测试对所有歌手的检索,请在 Windows 中打开命令提示符,或者在 Unix/Linux 中打开终端,将 WAR 部署到服务器上,并启动以下命令:
$ curl -v -H "Accept: application/json" http://localhost:8080/singer/listdata
* Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> GET /singer/listdata HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.43.0
> Accept: application/json
>
< HTTP/1.1 200
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Sat, 17 Jun 2017 17:16:43 GMT
<
{
"singers" : [ {
"id" : 1,
"version" : 0,
"firstName" : "John",
"lastName" : "Mayer",
"birthDate" : "1977-10-16"
}, {
"id" : 2,
"version" : 0,
"firstName" : "Eric",
"lastName" : "Clapton",
"birthDate" : "1945-03-30"
}, {
"id" : 3,
"version" : 0,
"firstName" : "John",
"lastName" : "Butler",
"birthDate" : "1975-04-01"
} ]
* Connection #0 to host localhost left intact
此命令向服务器的 RESTful web 服务发送 HTTP 请求;在这种情况下,它调用SingerController中的listData()方法来检索并返回所有歌手信息。另外,-H选项声明了一个 HTTP header 属性,这意味着客户机希望接收 JSON 格式的数据。运行该命令会为返回的初始填充的 singer 信息生成 JSON 格式的输出。现在让我们来看看 XML 格式;命令和结果如下所示:
$ curl -v -H "Accept: application/xml" http://localhost:8080/singer/listdata
* Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> GET /singer/listdata HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.43.0
> Accept: application/xml
>
< HTTP/1.1 200
< Content-Type: application/xml
< Transfer-Encoding: chunked
< Date: Sat, 17 Jun 2017 17:18:22 GMT
<
<?xml version="1.0" encoding="UTF-8"?>
<singers>
<singer>
<id>1</id>
<firstName>John</firstName>
<lastName>Mayer</lastName>
<birthDate>1977-10-16</birthDate>
<version>0</version>
</singer>
<singer>
<id>2</id>
<firstName>Eric</firstName>
<lastName>Clapton</lastName>
<birthDate>1945-03-30</birthDate>
<version>0</version>
</singer>
<singer>
<id>3</id>
<firstName>John</firstName>
<lastName>Butler</lastName>
<birthDate>1975-04-01</birthDate>
<version>0</version>
</singer>
</singers>
* Connection #0 to host localhost left intact
如你所见,这两个样品只有一个不同之处。接受媒体已从 JSON 更改为 XML。运行该命令会产生 XML 输出。这是因为在 RESTful servlet 的WebApplicationContext中定义了HttpMessageConverterbean,而 Spring MVC 将基于客户机的 HTTP 头的接受媒体信息调用相应的消息转换器,并相应地写入 HTTP 响应。
使用 RestTemplate 访问 RESTful-WS
对于基于 Spring 的应用,RestTemplate类被设计用来访问 RESTful web 服务。在这一节中,我们将展示如何使用类来访问服务器上的 singer 服务。首先让我们看看 Spring 的RestTemplate的基本ApplicationContext配置,如下面的代码片段所示:
package com.apress.prosring5.ch12;
import com.apress.prospring5.ch12.CustomCredentialsProvider;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.MediaType;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.xml.MarshallingHttpMessageConverter;
import org.springframework.oxm.castor.CastorMarshaller;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.List;
@Configuration
public class RestClientConfig {
@Autowired ApplicationContext ctx;
@Bean
public HttpComponentsClientHttpRequestFactory httpRequestFactory() {
HttpComponentsClientHttpRequestFactory httpRequestFactory =
new HttpComponentsClientHttpRequestFactory();
HttpClient httpClient = HttpClientBuilder.create().build();
httpRequestFactory.setHttpClient(httpClient);
return httpRequestFactory;
}
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate(httpRequestFactory());
List<HttpMessageConverter<?>> mcvs = new ArrayList<>();
mcvs.add(singerMessageConverter());
restTemplate.setMessageConverters(mcvs);
return restTemplate;
}
@Bean MarshallingHttpMessageConverter singerMessageConverter() {
MarshallingHttpMessageConverter mc =
new MarshallingHttpMessageConverter();
mc.setMarshaller(castorMarshaller());
mc.setUnmarshaller(castorMarshaller());
List<MediaType> mediaTypes = new ArrayList<>();
MediaType mt = new MediaType("application", "xml");
mediaTypes.add(mt);
mc.setSupportedMediaTypes(mediaTypes);
return mc;
}
@Bean CastorMarshaller castorMarshaller() {
CastorMarshaller castorMarshaller = new CastorMarshaller();
castorMarshaller.setMappingLocation(
ctx.getResource( "classpath:spring/oxm-mapping.xml"));
return castorMarshaller;
}
}
您使用RestTemplate类声明了一个restTemplate bean。该类使用 Castor 注入属性messageConverters和一个MarshallingHttpMessageConverter实例,与服务器端的相同。映射文件将在服务器端和客户端之间共享。此外,对于restTemplate bean,在匿名类MarshallingHttpMessageConverter中,属性supportedMediaTypes被注入了一个MediaType实例,表明唯一支持的媒体是 XML。因此,客户端总是期望 XML 作为返回数据格式,Castor 将帮助执行 POJO 和 XML 之间的转换。
要测试 web 应用支持的所有 REST URLs,JUnit 类更合适,在由RestClientConfig定义的 Spring 应用上下文中执行。下面显示了代码,在 IntelliJ IDEA 或 STS 等智能编辑器中,每种方法都可以单独执行:
package com.apress.prosring5.ch12.test;
import com.apress.prospring5.ch12.Singers;
import com.apress.prospring5.ch12.entities.Singer;
import com.apress.prosring5.ch12.RestClientConfig;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.client.RestTemplate;
import java.util.Date;
import java.util.GregorianCalendar;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {RestClientConfig.class})
public class RestClientTest {
final Logger logger = LoggerFactory.getLogger(RestClientTest.class);
private static final String URL_GET_ALL_SINGERS =
"http://localhost:8080/singer/listdata";
private static final String URL_GET_SINGER_BY_ID =
"http://localhost:8080/singer/{id}";
private static final String URL_CREATE_SINGER =
"http://localhost:8080/singer/";
private static final String URL_UPDATE_SINGER =
"http://localhost:8080/singer/{id}";
private static final String URL_DELETE_SINGER =
"http://localhost:8080/singer/{id}";
@Autowired RestTemplate restTemplate;
@Before
public void setUp() {
assertNotNull(restTemplate);
}
@Test
public void testFindAll() {
logger.info("--> Testing retrieve all singers");
Singers singers = restTemplate.getForObject(URL_GET_ALL_SINGERS,
Singers.class);
assertTrue(singers.getSingers().size() == 3);
listSingers(singers);
}
@Test
public void testFindbyId() {
logger.info("--> Testing retrieve a singer by id : 1");
Singer singer = restTemplate.getForObject(URL_GET_SINGER_BY_ID,
Singer.class, 1);
assertNotNull(singer);
logger.info(singer.toString());
}
@Test
public void testUpdate() {
logger.info("--> Testing update singer by id : 1");
Singer singer = restTemplate.getForObject(URL_UPDATE_SINGER,
Singer.class, 1);
singer.setFirstName("John Clayton");
restTemplate.put(URL_UPDATE_SINGER, singer, 1);
logger.info("Singer update successfully: " + singer);
}
@Test
public void testDelete() {
logger.info("--> Testing delete singer by id : 3");
restTemplate.delete(URL_DELETE_SINGER, 3);
Singers singers = restTemplate.getForObject(URL_GET_ALL_SINGERS,
Singers.class);
Boolean found = false;
for(Singer s: singers.getSingers()) {
if(s.getId() == 3) {
found = true;
}
};
assertFalse(found);
listSingers(singers);
}
@Test
public void testCreate() {
logger.info("--> Testing create singer");
Singer singerNew = new Singer();
singerNew.setFirstName("BB");
singerNew.setLastName("King");
singerNew.setBirthDate(new Date(
(new GregorianCalendar(1940, 8, 16)).getTime().getTime()));
singerNew = restTemplate.postForObject(URL_CREATE_SINGER,
singerNew, Singer.class);
logger.info("Singer created successfully: " + singerNew);
logger.info("Singer created successfully: " + singerNew);
Singers singers = restTemplate.getForObject(URL_GET_ALL_SINGERS,
Singers.class);
listSingers(singers);
}
private void listSingers(Singers singers) {
singers.getSingers().forEach(s -> logger.info(s.toString()));
}
}
声明了用于访问各种操作的 URL,这些 URL 将在后面的示例中使用。注入RestTemplate的实例,然后在testFindAll方法中调用RestTemplate.getForObject()方法(对应于 HTTP GET 方法),传入 URL 和预期的返回类型,这是包含完整歌手列表的Singers类。
确保应用服务器正在运行。运行testFindAll测试方法,测试应该通过并产生以下输出:
INFO c.a.p.c.t.RestClientTest - --> Testing retrieve all singers
INFO c.a.p.c.t.RestClientTest - Singer - Id: 1, First name: John, Last name: Mayer,
Birthday: Sun Oct 16 00:00:00 EET 1977
INFO c.a.p.c.t.RestClientTest - Singer - Id: 2, First name: Eric, Last name: Clapton,
Birthday: Fri Mar 30 00:00:00 EET 1945
INFO c.a.p.c.t.RestClientTest - Singer - Id: 3, First name: John, Last name: Butler,
Birthday: Tue Apr 01 00:00:00 EET 1975
如您所见,在RestTemplate中注册的MarshallingHttpMessageConverter bean 自动将消息转换成 POJO。接下来,让我们尝试通过 ID 检索歌手。在这个方法中,我们使用了一个RestTemplate.getForObject()方法的变体,它也传入我们想要检索的歌手的 ID 作为 URL 中的路径变量(在URL_GET_CONTACT_BY_ID中的{id}路径变量)。如果 URL 有多个路径变量,您可以使用一个实例Map<String,Object>或者使用该方法的 varargs 支持来传入路径变量。对于 varargs,您需要遵循 URL 中声明的路径变量的顺序。运行testFindbyId()测试方法。测试应该通过,您应该会看到以下输出:
INFO c.a.p.c.t.RestClientTest - --> Testing retrieve a singer by id : 1
INFO c.a.p.c.t.RestClientTest - Singer - Id: 1, First name: John, Last name: Mayer,
Birthday: Sun Oct 16 00:00:00 EET 1977
如您所见,检索到了正确的歌手。现在轮到update了。首先,我们检索想要更新的歌手。singer 对象更新后,我们使用对应于 HTTP PUT 方法的RestTemplate.put()方法,传入更新 URL、更新的 singer 对象和要更新的 singer 的 ID。运行testUpdate()产生以下输出(其他输出已被省略):
INFO c.a.p.c.t.RestClientTest - --> Testing update singer by id : 1
INFO c.a.p.c.t.RestClientTest - Singer update successfully: Singer - Id: 1,
First name: John Clayton,
Last name: Mayer, Birthday: Sun Oct 16 00:00:00 EET 1977
接下来是删除操作。调用RestTemplate.delete()方法,它对应于 HTTP DELETE方法,传入 URL 和 ID。然后,检索所有歌手并再次显示以验证删除。运行testDelete()测试方法会产生以下输出(其他输出已被省略):
INFO c.a.p.c.t.RestClientTest - --> Testing delete singer by id : 3
INFO c.a.p.c.t.RestClientTest - Singer - Id: 1,
First name: John Clayton,
Last name: Mayer, Birthday: Sun Oct 16 00:00:00 EET 1977
INFO c.a.p.c.t.RestClientTest - Singer - Id: 2, First name: Eric,
Last name: Clapton, Birthday: Fri Mar 30 00:00:00 EET 1945
如您所见,ID 为 3 的歌手被删除。最后,我们来试试插入操作。构建了一个Singer对象的新实例。然后调用RestTemplate.postForObject()方法,它对应于 HTTP POST 方法,传入 URL、我们想要创建的Singer对象和类类型。再次运行该程序会产生以下输出:
INFO c.a.p.c.t.RestClientTest - --> Testing create singer
INFO c.a.p.c.t.RestClientTest - Singer created successfully: Singer - Id: 4,
First name: BB, Last name: King, Birthday: Mon Sep 16 00:00:00 EET 1940
//listing all singers
INFO c.a.p.c.t.RestClientTest - Singer - Id: 1,
First name: John Clayton, Last name: Mayer,
Birthday: Sun Oct 16 00:00:00 EET 1977
INFO c.a.p.c.t.RestClientTest - Singer - Id: 2, First name: Eric,
Last name: Clapton, Birthday: Fri Mar 30 00:00:00 EET 1945
INFO c.a.p.c.t.RestClientTest - Singer - Id: 4, First name: BB,
Last name: King, Birthday: Mon Sep 16 00:00:00 EET 1940
服务器在服务器上创建并返回给客户机。
用 Spring Security 保护 RESTful-WS
任何远程处理服务都需要安全性来限制未授权方访问服务和检索业务信息或对其进行操作。RESTful-WS 也不例外。在这一节中,我们将演示如何使用 Spring Security 项目来保护服务器上的 RESTful-WS。在这个例子中,我们使用的是 Spring Security 5.0.0.M2(撰写本文时的最新稳定版本),它为 RESTful-WS 提供了一些有用的支持。
使用 Spring Security 来保护 RESTful-WS 是一个三步的过程。首先,在 web 应用部署描述符(web.xml)中,需要添加一个名为springSecurityFilterChain的安全过滤器,但是因为我们没有使用 XML 配置应用,所以过滤器被一个扩展了AbstractSecurityWebApplicationInitializer的类所取代。该类注册DelegatingFilterProxy以在任何其他注册Filter之前使用springSecurityFilterChain。这里显示了实现,该类是空的,因为我们没有对它进行任何定制:
package com.apress.prospring5.ch12.init;
import org.springframework.security.web.
context.AbstractSecurityWebApplicationInitializer;
public class SecurityWebApplicationInitializer
extends AbstractSecurityWebApplicationInitializer {
}
为了安全起见,我们现在需要添加一个 Spring 配置类,我们将在其中声明谁可以访问应用以及他们可以做什么。在这个应用中,事情很简单:我们出于教学目的使用内存认证,所以添加一个名为prospring5的用户,密码为prospring5,角色为REMOTE。
package com.apress.prospring5.ch12.init;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.
builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.
EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.
WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private static Logger logger = LoggerFactory.getLogger(SecurityConfig.class);
@Autowired
protected void configureGlobal(AuthenticationManagerBuilder auth)
throws Exception {
try {
auth.inMemoryAuthentication()
.withUser("prospring5")
.password("prospring5")
.roles("REMOTE");
} catch (Exception e) {
logger.error("Could not configure authentication!", e);
}
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/**").permitAll()
.antMatchers("/rest/**").hasRole("REMOTE").anyRequest().authenticated()
.and()
.formLogin()
.and()
.httpBasic()
.and()
.csrf().disable();
}
}
该类用@EnableWebSecurity注释进行了注释,以支持 Spring web 应用中的安全行为。在configure(...)方法中,我们声明 URL /rest/**下的资源应该受到保护。sessionCreationPolicy()方法用于允许我们配置是否在认证时创建 HTTP 会话。由于我们使用的 RESTful-WS 是无状态的,我们将值设置为SessionCreationPolicy.STATELESS,这指示 Spring Security 不要为所有 RESTful 请求创建 HTTP 会话。这有助于提高 RESTful 服务的性能。
接下来,在antMatchers("/rest/**")中,我们设置只有分配了REMOTE角色的用户才能访问 RESTful 服务。httpBasic()方法指定 RESTful 服务只支持 HTTP 基本认证。
configureGlobal(AuthenticationManagerBuilder auth)方法定义了认证信息。这里我们定义了一个简单的身份验证提供者,它具有硬编码的用户和密码(都设置为remote),并分配了REMOTE角色。在企业环境中,很可能通过数据库或 LDAP 查找来完成身份验证。
.formLogin()方法用于告诉 Spring 生成一个基本的登录表单,该表单可用于测试应用是否得到了正确的保护。登录表单可在http://localhost:8080/login访问。
过滤器springSecurityFilterChain用于让 Spring Security 拦截 HTTP 请求,以进行身份验证和授权检查。因为我们只想保护 RESTful-WS,所以过滤器只应用于 URL 模式/rest/*(参见antMatchers(...)方法)。我们希望保护所有的 REST URLs,但允许用户看到应用的主页(一个简单的 HTML 文件,当在浏览器中访问http://localhost:8080/时显示),所以这是您添加rest应用上下文的时刻,除了将SecurityConfig添加到根上下文应用。
package com.apress.prospring5.ch12.init;
import com.apress.prospring5.ch12.config.DataServiceConfig;
import org.springframework.web.servlet.support.
AbstractAnnotationConfigDispatcherServletInitializer;
public class WebInitializer extends
AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[]{
DataServiceConfig.class, SecurityConfig.class
};
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[]{
WebConfig.class
};
}
@Override
protected String[] getServletMappings() {
return new String[]{"/rest/**"};
}
}
现在安全设置完成了。如果您重新部署项目并运行RestClientTest下的任何测试方法,您将得到以下输出(其他输出已被省略):
Exception in thread "main" org.springframework.web.cient.HttpClientErrorException:
401 Unauthorized
您将获得 HTTP 状态代码 401,这意味着您无权访问该服务。现在让我们配置客户机的RestTemplate来向服务器提供凭证信息。
package com.apress.prosring5.ch12;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.xml.MarshallingHttpMessageConverter;
import org.springframework.oxm.castor.CastorMarshaller;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.List;
@Configuration
public class RestClientConfig {
@Autowired ApplicationContext ctx;
@Bean Credentials credentials(){
return new UsernamePasswordCredentials("prospring5", "prospring5");
}
@Bean
CredentialsProvider provider() {
BasicCredentialsProvider provider =
new BasicCredentialsProvider();
provider.setCredentials( AuthScope.ANY, credentials());
return provider;
}
@Bean
public HttpComponentsClientHttpRequestFactory httpRequestFactory() {
CloseableHttpClient client = HttpClients.custom()
.setDefaultCredentialsProvider(provider()).build();
return new HttpComponentsClientHttpRequestFactory(client);
}
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(httpRequestFactory());
List<HttpMessageConverter<?>> mcvs = new ArrayList<>();
mcvs.add(singerMessageConverter());
restTemplate.setMessageConverters(mcvs);
return restTemplate;
}
@Bean MarshallingHttpMessageConverter singerMessageConverter() {
MarshallingHttpMessageConverter mc = n
ew MarshallingHttpMessageConverter();
mc.setMarshaller(castorMarshaller());
mc.setUnmarshaller(castorMarshaller());
List<MediaType> mediaTypes = new ArrayList<>();
MediaType mt = new MediaType("application", "xml");
mediaTypes.add(mt);
mc.setSupportedMediaTypes(mediaTypes);
return mc;
}
@Bean CastorMarshaller castorMarshaller() {
CastorMarshaller castorMarshaller = new CastorMarshaller();
castorMarshaller.setMappingLocation(ctx.getResource(
"classpath:spring/oxm-mapping.xml"));
return castorMarshaller;
}
}
在restTemplate bean 中,注入了一个引用了httpRequestFactory bean 的构造函数参数。对于httpRequestFactory bean,使用了HttpComponentsClientHttpRequestFactory类,这是 Spring 对 Apache HttpComponents HttpClient 库的支持,我们需要这个库来构建一个CloseableHttpClient的实例,为我们的客户端存储凭证。为了支持凭证的注入,您创建了一个简单的类型为UsernamePasswordCredentials的 bean。UsernamePasswordCredentials类是用prospring5用户名和密码构建的。随着httpRequestFactory被构造并注入到RestTemplate中,所有使用该模板触发的 RESTful 请求都将携带所提供的凭证。现在我们可以简单地再次运行RestClientTest类中的测试方法,您将看到服务像往常一样被调用。
与 Spring Boot 共度 Spring
因为 Spring Boot 让一切都变得更容易开发,所以我们需要添加一个部分,介绍 Spring Boot 如何让 Spring RESTful 服务的开发变得更容易。Singer实体、存储库和服务类与之前相同;没必要改变什么。为了简单起见,并尽可能多地使用默认的 Spring Boot 默认配置,XML 序列化也将被移除。默认情况下支持 JSON 序列化。由于该应用是一个 web 应用,其配置与前面介绍的 Spring Boot web 应用相同,因此我们不再赘述。Spring Boot 应用的Application类和入口点非常简单,如下所示:
package com.apress.prospring5.ch12;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import java.io.IOException;
@SpringBootApplication(scanBasePackages = "com.apress.prospring5.ch12")
public class Application {
private static Logger logger = LoggerFactory.getLogger(Application.class);
public static void main(String args) throws IOException {
ConfigurableApplicationContext ctx =
SpringApplication.run(Application.class, args);
assert (ctx != null);
logger.info("Application Started ...");
System.in.read();
ctx.close();
}
}
正如之前所承诺的,这里是 Spring 4.3 中引入的使用@RestController和 HTTP 方法重写的新的和改进的SingerController:
package com.apress.prospring5.ch12.controller;
import com.apress.prospring5.ch12.entities.Singer;
import com.apress.prospring5.ch12.services.SingerService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping(value = "/singer")
public class SingerController {
final Logger logger =
LoggerFactory.getLogger(SingerController.class);
@Autowired
private SingerService singerService;
@ResponseStatus(HttpStatus.OK)
@GetMapping(value = "/listdata")
public List<Singer> listData() {
return singerService.findAll();
}
@ResponseStatus(HttpStatus.OK)
@GetMapping(value = "/{id}")
public Singer findSingerById(@PathVariable Long id) {
return singerService.findById(id);
}
@ResponseStatus(HttpStatus.CREATED)
@PostMapping(value="/")
public Singer create(@RequestBody Singer singer) {
logger.info("Creating singer: " + singer);
singerService.save(singer);
logger.info("Singer created successfully with info: " + singer);
return singer;
}
@ResponseStatus(HttpStatus.OK)
@PutMapping(value="/{id}")
public void update(@RequestBody Singer singer,
@PathVariable Long id) {
logger.info("Updating singer: " + singer);
singerService.save(singer);
logger.info("Singer updated successfully with info: " + singer);
}
@ResponseStatus(HttpStatus.NO_CONTENT)
@DeleteMapping(value="/{id}")
public void delete(@PathVariable Long id) {
logger.info("Deleting singer with id: " + id);
Singer singer = singerService.findById(id);
singerService.delete(singer);
logger.info("Singer deleted successfully");
}
}
Spring 版本引入了一些与基本 HTTP 方法匹配的@RequestMapping注释的定制。表 12-4 列出了新注释和旧样式@RequestMapping之间的等价关系。
表 12-4。
Annotations for Mapping HTTP Method Requests onto Specific Handler Methods Introduced in Spring 4-3
| 注释 | 旧式等价物 | | --- | --- | | `@GetMapping` | `@RequestMapping(method = RequestMethod.GET)` | | `@PostMapping` | `@RequestMapping(method = RequestMethod.POST)` | | `@PutMapping` | `@RequestMapping(method = RequestMethod.PUT)` | | `@DeleteMapping` | `@RequestMapping(method = RequestMethod.DELETE)` |此外,因为我们使用 JSON,它支持列表和数组,所以不再需要类Singers。
测试应用很简单,因为RestTemplate不需要任何配置。通过调用默认构造函数来创建一个RestTemplate实例所需要的一切。测试方法与之前相同。
package com.apress.prosring5.ch12.test;
import com.apress.prospring5.ch12.entities.Singer;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.client.RestTemplate;
import java.util.Arrays;
import java.util.Date;
import java.util.GregorianCalendar;
import static org.junit.Assert.*;
public class RestClientTest {
final Logger logger =
LoggerFactory.getLogger(RestClientTest.class);
private static final String URL_GET_ALL_SINGERS =
"http://localhost:8080/singer/listdata";
...
RestTemplate restTemplate;
@Before
public void setUp() {
restTemplate = new RestTemplate();
}
@Test
public void testFindAll() {
logger.info("--> Testing retrieve all singers");
Singer singers = restTemplate.getForObject(
URL_GET_ALL_SINGERS, Singer.class);
assertTrue(singers.length == 3);
listSingers(singers);
}
...
}
只需运行Application类,然后逐个执行测试方法。
如果您想确保应用实际工作并且 singer 实例使用 JSON 格式序列化,您可以使用curl来测试这个 Spring Boot 应用。
curl -v -H "Accept: application/json" http://localhost:8080/singer/listdata
* Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> GET /singer/listdata HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.43.0
> Accept: application/json
>
< HTTP/1.1 200
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Sun, 18 Jun 2017 11:14:17 GMT
<
* Connection #0 to host localhost left intact
[{"id":1,"version":1,"firstName":"John Clayton","lastName":"Mayer",
"birthDate":245797200000},{"id":2,"version":0,"firstName":"Eric", "lastName":"Clapton","birthDate":-781326000000},{"id":4, "version":0,"firstName":"BB","lastName":"King",
"birthDate":-924404400000}]
如果输出让您感到麻烦,请记住,如果没有声明显式的 JSON 消息转换器,Date字段将显示为数字,并且响应不会被格式化。
使用 Spring Boot,你也可以很容易地获得资源,但这是一个将在第十六章详细讨论的主题。
Spring 使用 AMQP
远程处理也可以通过使用高级消息队列协议(AMQP)作为传输方式的远程过程调用(RPC)通信来完成。AMQP 是实现面向消息的中间件(MOM)的开放标准协议。
JMS 应用可以在任何操作系统环境中工作,但是它只支持 Java 平台。因此,所有通信应用都必须用 Java 开发。AMQP 标准可用于开发易于交流的多语言应用。
与使用 JMS 类似,AMQP 也使用消息代理通过。在这个例子中,我们使用 RabbitMQ 10 作为 AMQP 服务器。Spring 本身并没有在核心框架中提供远程功能。相反,它们是由一个名为 Spring AMQP 的姊妹项目来处理的, 11 ,我们使用它作为底层的通信 API。Spring AMQP 项目围绕 AMQP 提供了一个基础抽象,并实现了与 RabbitMQ 的通信。在这一章中,我们不会涵盖 AMQP 或 Spring AMQP 的所有特性,只是通过 RPC 通信的远程功能。
Spring AMQP 项目由两部分组成:spring-amqp是基础抽象,spring-rabbit是 RabbitMQ 实现。编写本文时 Spring AMQP 的稳定版本是 2.0.0.M4
首先,你需要从 www.rabbitmq.com/download.html 获取 RabbitMQ 并启动服务器。RabbitMQ 开箱即可满足我们的需求,不需要进行任何配置更改。一旦 RabbitMQ 开始运行,我们需要做的下一件事就是创建一个服务接口。在这个例子中,我们创建了一个简单的天气服务,它返回所提供的州代码的预报。让我们从创建如下所示的WeatherService接口开始:
package com.apress.prospring5.ch12;
public interface WeatherService {
String getForecast(String stateCode);
}
接下来,让我们创建一个WeatherService的实现,它将简单地回复所提供的州的天气预报,或者如果没有可用的预报,则回复不可用的消息,如下所示:
package com.apress.prospring5.ch12;
import org.springframework.stereotype.Component;
@Component
public class WeatherServiceImpl implements WeatherService {
@Override
public String getForecast(String stateCode) {
if ("FL".equals(stateCode)) {
return "Hot";
} else if ("MA".equals(stateCode)) {
return "Cold";
}
return "Not available at this time";
}
}
天气服务代码就绪后,让我们构建配置文件(amqp-rpc-app-context.xml),该文件将配置 AMQP 连接并公开WeatherService,如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/rabbit
http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">
<rabbit:connection-factory id="connectionFactory" host="localhost" />
<rabbit:template id="amqpTemplate" connection-factory="connectionFactory"
reply-timeout="2000" routing-key="forecasts"
exchange="weather" />
<rabbit:admin connection-factory="connectionFactory" />
<rabbit:queue name="forecasts" />
<rabbit:direct-exchange name="weather">
<rabbit:bindings>
<rabbit:binding queue="forecasts" key="forecasts" />
</rabbit:bindings>
</rabbit:direct-exchange>
<bean id="weatherServiceProxy"
class="org.springframework.amqp.remoting.client.AmqpProxyFactoryBean">
<property name="amqpTemplate" ref="amqpTemplate" />
<property name="serviceInterface"
value="com.apress.prospring5.ch12.WeatherService" />
</bean>
<rabbit:listener-container connection-factory="connectionFactory">
<rabbit:listener ref="weatherServiceExporter" queue-names="forecasts" />
</rabbit:listener-container>
<bean id="weatherServiceExporter"
class="org.springframework.amqp.remoting.service.AmqpInvokerServiceExporter">
<property name="amqpTemplate" ref="amqpTemplate" />
<property name="serviceInterface"
value="com.apress.prospring5.ch12.WeatherService" />
<property name="service">
<bean class="com.apress.prospring5.ch12.WeatherServiceImpl"/>
</property>
</bean>
</beans>
我们配置 RabbitMQ 连接以及交换和队列信息。然后,我们通过使用AmqpProxyFactoryBean类创建一个 bean,客户端使用它作为代理来发出 RPC 请求。对于响应,我们使用AmqpInvokerServiceExporter类,它被连接到一个监听器容器中。侦听器容器负责侦听 AMQP 消息,并将它们传递给气象服务。如您所见,在连接、队列、侦听器容器等方面,配置与 JMS 相似。虽然在配置上相似,但 JMS 和 AMQP 是非常不同的传输协议,建议您访问 AMQP 网站 12 了解关于该协议的全部细节。
配置就绪后,让我们创建一个示例类来执行 RPC 调用。
package com.apress.prospring5.ch12;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.support.GenericXmlApplicationContext;
public class AmqpRpcDemo {
private static Logger logger = LoggerFactory.getLogger(AmqpRpcDemo.class);
public static void main(String... args) {
GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
ctx.load("classpath:spring/amqp-rpc-app-context.xml");
ctx.refresh();
WeatherService weatherService = ctx.getBean(WeatherService.class);
logger.info("Forecast for FL: " + weatherService.getForecast("FL"));
logger.info("Forecast for MA: " + weatherService.getForecast("MA"));
logger.info("Forecast for CA: " + weatherService.getForecast("CA"));
ctx.close();
}
}
现在让我们运行示例,您应该会得到以下输出:
INFO c.a.p.c.AmqpRpcDemo - Forecast for FL: Hot
INFO c.a.p.c.AmqpRpcDemo - Forecast for MA: Cold
INFO c.a.p.c.AmqpRpcDemo - Forecast for CA: Not available at this time
当然,XML 配置可以很容易地转换成 Java 配置类。但是其他相关的类也需要做一些修改。WeatherServiceImpl不再需要实现接口,因为它将只声明一个监听器方法,该方法将监听写在forecasts队列上的消息。
package com.apress.prospring5.ch12;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
@Service
public class WeatherServiceImpl {
private static Logger logger =
LoggerFactory.getLogger(WeatherServiceImpl.class);
@RabbitListener(containerFactory="rabbitListenerContainerFactory",
queues="forecasts")
public void getForecast(String stateCode) {
if ("FL".equals(stateCode)) {
logger.info("Hot");
} else if ("MA".equals(stateCode)) {
logger.info("Cold");
} else {
logger.info("Not available at this time");
}
}
}
rabbitListenerContainerFactory bean 的类型为RabbitListenerContainerFactory,用于创建常规的SimpleMessageListenerContainer。但是让我们看看完整的 Java 配置。
package com.apress.prospring5.ch12.config;
import com.apress.prospring5.ch12.WeatherService;
import com.apress.prospring5.ch12.WeatherServiceImpl;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.amqp.remoting.client.AmqpProxyFactoryBean;
import org.springframework.amqp.remoting.service.AmqpInvokerServiceExporter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan("com.apress.prospring5.ch12")
@EnableRabbit
public class RabbitMQConfig {
final static String queueName = "forecasts";
final static String exchangeName = "weather";
@Bean CachingConnectionFactory connectionFactory() {
return new CachingConnectionFactory("127.0.0.1");
}
@Bean RabbitTemplate amqpTemplate() {
RabbitTemplate rabbitTemplate = new RabbitTemplate();
rabbitTemplate.setConnectionFactory(connectionFactory());
rabbitTemplate.setReplyTimeout(2000); rabbitTemplate.setRoutingKey(queueName);
rabbitTemplate.setExchange(exchangeName);
return rabbitTemplate;
}
@Bean Queue forecasts() {
return new Queue(queueName, true);
}
@Bean Binding dataBinding(DirectExchange directExchange, Queue queue) {
return BindingBuilder.bind(queue).to(directExchange).with(queueName);
}
@Bean RabbitAdmin admin() {
RabbitAdmin admin = new RabbitAdmin(connectionFactory());
admin.declareQueue(forecasts());
admin.declareBinding(dataBinding(weather(), forecasts()));
return admin;
}
@Bean DirectExchange weather() {
return new DirectExchange(exchangeName, true, false);
}
@Bean
public SimpleRabbitListenerContainerFactory
rabbitListenerContainerFactory() {
SimpleRabbitListenerContainerFactory factory =
new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory());
factory.setMaxConcurrentConsumers(5);
return factory;
}
}
前面配置中的所有 beans 都可以很容易地与它们的 XML 对应物匹配。新元素是@EnableRabbit注释。当用在用@Configuration注释的类上时,它支持由RabbitListenerContainerFactory bean 在幕后创建的兔子监听器注释端点。
为了测试新的天气服务,我们还必须修改测试程序,amqpTemplate用于向forecasts队列发送消息,在那里WeatherServiceImpl.getForecast(...)将读取这些消息并打印出预测输出。
package com.apress.prospring5.ch12;
import com.apress.prospring5.ch12.config.RabbitMQConfig;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
public class AmqpRpcDemo {
public static void main(String... args) throws Exception {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(RabbitMQConfig.class);
RabbitTemplate rabbitTemplate = ctx.getBean(RabbitTemplate.class);
rabbitTemplate.convertAndSend("FL");
rabbitTemplate.convertAndSend("MA");
rabbitTemplate.convertAndSend("CA");
System.in.read();
ctx.close();
}
}
如果您运行前面的程序,并且 RabbitMQ 服务器已经启动,您将看到以下输出:
[SimpleAsyncTaskExecutor-1] INFO c.a.p.c.WeatherServiceImpl - Hot
[SimpleAsyncTaskExecutor-1] INFO c.a.p.c.WeatherServiceImpl - Cold
[SimpleAsyncTaskExecutor-1] INFO c.a.p.c.WeatherServiceImpl - Not available at this time
利用 AMQP 和 Spring Boot
Spring Boot 也帮助你开发 AMQP 应用;它的首发神器spring-boot-starter-amqp就是为了这个。配置被简化了很多。您不再需要定义RabbitTemplate、RabbitAdmin和SimpleRabbitListenerContainerFactorybean,因为这些 bean 是由 Spring Boot 自动配置和创建的。WeatherServiceImpl的实现变化不大,但是由于SimpleRabbitListenerContainerFactory bean 是由 Spring Boot 处理的,所以不再需要将它作为一个值添加到@RabbitListener注释中。
package com.apress.prospring5.ch12;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
@Service
public class WeatherServiceImpl {
private static Logger logger =
LoggerFactory.getLogger(WeatherServiceImpl.class);
@RabbitListener(queues="forecasts")
public void getForecast(String stateCode) {
if ("FL".equals(stateCode)) {
logger.info("Hot");
} else if ("MA".equals(stateCode)) {
logger.info("Cold");
} else {
logger.info("Not available at this time");
}
}
}
用@SpringBootApplication标注的Application类也被用作配置类和运行器类。
package com.apress.prospring5.ch12;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class Application {
final static String queueName = "forecasts";
final static String exchangeName = "weather";
@Bean Queue forecasts() {
return new Queue(queueName, true);
}
@Bean DirectExchange weather() {
return new DirectExchange(exchangeName, true, false);
}
@Bean Binding dataBinding(DirectExchange directExchange, Queue queue) {
return BindingBuilder.bind(queue).to(directExchange).with(queueName);
}
@Bean CachingConnectionFactory connectionFactory() {
return new CachingConnectionFactory("127.0.0.1");
}
@Bean
SimpleMessageListenerContainer messageListenerContainer() {
SimpleMessageListenerContainer container =
new SimpleMessageListenerContainer();
container.setConnectionFactory(connectionFactory());
container.setQueueNames(queueName);
return container;
}
public static void main(String... args) throws java.lang.Exception {
ConfigurableApplicationContext ctx =
SpringApplication.run(Application.class, args);
RabbitTemplate rabbitTemplate = ctx.getBean(RabbitTemplate.class);
rabbitTemplate.convertAndSend(Application.queueName, "FL");
rabbitTemplate.convertAndSend(Application.queueName, "MA");
rabbitTemplate.convertAndSend(Application.queueName, "CA");
System.in.read();
ctx.close();
}
}
正如您所看到的,也不需要@EnableRabbit注释,尽管配置没有减少多少,但这仍然是一个进步。如果您运行前面的类,您将得到类似的结果,正如您在前面的示例中看到的那样。
DEBUG c.a.p.c.Application - Running with Spring Boot v2.0.0.M1, Spring v5.0.0.RC1
INFO c.a.p.c.Application - No active profile set, falling back to default profiles: default
INFO c.a.p.c.Application - Started Application in 2.211 seconds JVM running for 2.801
[SimpleAsyncTaskExecutor-1] INFO c.a.p.c.WeatherServiceImpl - Cold
[SimpleAsyncTaskExecutor-1] INFO c.a.p.c.WeatherServiceImpl - Hot
[SimpleAsyncTaskExecutor-1] INFO c.a.p.c.WeatherServiceImpl - Not available at this time
摘要
在本章中,我们讨论了基于 Spring 的应用中最常用的远程技术。
如果两个应用都是用 Spring 构建的,那么使用 Spring HTTP invoker 是一个可行的选择。如果需要异步模式或松散耦合模式的集成,JMS 是一种常用的方法。我们讨论了如何在 Spring 中使用 RESTful-WS 来公开服务或使用RestTemplate类访问服务。最后,我们讨论了如何通过 RabbitMQ 使用 Spring AMQP 进行 RPC 风格的远程处理。
每个技术的 Spring Boot,远程、REST、JMS 等都被覆盖,这是你应该寻找的东西。
在下一章,我们将讨论使用 Spring 测试应用;是时候我们阐述一些测试技术来让你的生活变得更容易了。
Footnotes 1
当 Spring 4 发布时,提到了 Burlap 不再处于积极开发中,并且将来会完全停止支持。
2
如果您喜欢 Apache 产品,Apache ActiveMQ Artemis 是一个 JMS 2.0 实现,具有非阻塞架构。它提供了出色的性能。更多详情请点击 https://activemq.apache.org/artemis/ 。此外,代码示例中提供了一个使用 Artemis ActiveMQ 的项目。
3
这是 XML 序列化所需要的;JSON 不需要容器类也能工作。
4
你可以在这里找到这个设计模式的很好的解释: http://www.oracle.com/technetwork/java/frontcontroller-135648.html 。
5
这相当于<mvc:annotation-driven>标签/。
6
这相当于 Spring 3.1 中引入的<mvc:message-converters>标签。
7
你可以在 http://jackson.codehaus.org 找到杰克森 JSON 库官方网站。
8
这相当于<context:component-scan>标签。
9
10
11
http://projects.spring.io/spring-amqp见。
12
参见 AMQP 网站 www.amqp.org 。
十三、Spring 测试
在开发企业应用时,测试是确保完成的应用按预期执行并满足各种需求(体系结构、安全性、用户需求等)的重要方法。每次进行更改时,您都应该确保引入的更改不会影响现有的逻辑。维护一个持续的构建和测试环境对于确保高质量的应用至关重要。对所有代码进行高覆盖率的可重复测试,可以让您以高度的信心部署新的应用和对应用的更改。在企业开发环境中,有多种针对企业应用中每一层的测试,每种测试都有自己的特点和要求。在这一章中,我们将讨论各种应用层测试中涉及的基本概念,尤其是 Spring 驱动的应用的测试。我们还介绍了 Spring 如何让开发人员更容易地实现不同层的测试用例。具体来说,本章涵盖以下主题:
- 企业测试框架:我们简要描述一个企业测试框架。我们讨论各种测试及其目的。我们关注单元测试,针对不同的应用层。
- 逻辑单元测试:最好的单元测试是只测试一个类中方法的逻辑,所有其他的依赖都被正确的行为“模仿”。在这一章中,我们将讨论 Spring MVC 控制器类的逻辑单元测试的实现,借助 Java 模拟库来模拟类的依赖关系。
- 集成单元测试:在企业测试框架中,集成测试指的是针对特定的业务逻辑测试不同应用层中一组类的交互。通常,在集成测试环境中,服务层应该与持久层一起测试,后端数据库可用。然而,随着应用架构的发展和轻量级内存数据库的成熟,现在将服务层与持久层和后端数据库作为一个整体进行“单元测试”是一种常见的做法。例如,在本章中,我们使用 JPA 2,用 Hibernate 和 Spring Data JPA 作为持久性提供者,用 H2 作为数据库。在这种架构中,在测试服务层时,“模仿”Hibernate 和 Spring Data JPA 并不重要。因此,在本章中,我们将讨论服务层以及持久层和 H2 内存数据库的测试。这种测试通常被称为集成单元测试,它位于单元测试和全面集成测试之间。
- 前端单元测试:即使您测试了应用的每一层,在应用部署之后,您仍然需要确保整个应用按预期运行。更具体地说,对于 web 应用,在部署到持续构建环境时,您应该运行前端测试,以确保用户界面正常工作。例如,对于 singer 应用,您应该确保正常功能的每一步都正常工作,并且您还应该测试例外情况(例如,当信息没有通过验证阶段时,应用如何工作)。在本章中,我们简要讨论一个前端测试框架。
介绍测试类别
企业测试框架指的是整个应用生命周期中的测试活动。根据定义的业务和技术需求,在不同的阶段执行不同的测试活动,以验证应用的功能是否按预期运行。
在每个阶段,执行不同的测试用例。有些是自动化的,有些是手动执行的。在每种情况下,结果都由相应的人员进行验证(例如,业务分析师、应用用户等等)。表 13-1 描述了每种测试的特征和目标,以及用于实现测试用例的常用工具和库。
表 13-1。
Different Testing Categories Used in Practice
| 测试类别 | 描述 | 常用工具 | | --- | --- | --- | | 逻辑单元测试 | 逻辑单元测试采用单个对象,自己进行测试,不用担心它在周围系统中扮演的角色。 | 单元测试:JUnit,TestNG 模拟对象:Mockito,EasyMock | | 集成单元测试 | 集成单元测试侧重于在“接近真实”的环境中测试组件之间的交互。这些测试将测试与容器(嵌入式数据库、web 容器等)的交互。 | 嵌入式数据库:H2 数据库测试:DbUnit 内存 web 容器:Jetty | | 前端单元测试 | 前端单元测试侧重于测试用户界面。目标是确保每个用户界面对用户的动作做出反应,并按预期向用户输出。 | 硒 | | 持续构建和代码质量测试 | 应该定期构建应用代码库,以确保代码质量符合标准(例如,注释全部到位,没有空的异常捕捉块,等等)。此外,测试覆盖率应该尽可能高,以确保所有开发的代码行都得到测试。 | 代码质量:PMD,检查风格,FindBugs,声纳测试覆盖率:Cobertura,EclEmma 构建工具:Gradle,Maven 连续构建:Hudson,Jenkins | | 系统集成测试 | 系统集成测试验证新系统中所有程序之间以及新系统与所有外部接口之间的通信准确性。集成测试还必须证明新系统按照功能规范运行,并且在运行环境中有效运行,不会对其他系统产生不利影响。 | IBM Rational Functional Tester,惠普统一功能测试 | | 系统质量测试 | 系统质量测试是为了确保开发的应用满足那些非功能性需求。大多数情况下,这将测试应用的性能,以确保满足系统和工作负载的并发用户的目标需求。其他非功能性需求包括安全性、高可用性特性等等。 | Apache JMeter,HP LoadRunner | | 用户接受度测试 | 用户验收测试模拟新系统的实际工作条件,包括用户手册和程序。用户在测试阶段的广泛参与为用户提供了操作新系统的宝贵培训。程序员或设计者看到新程序的用户体验也是有益的。这种联合参与鼓励用户和操作人员批准系统转换。 | IBM Rational TestManager,惠普质量中心 |在这一章中,我们将重点关注三种单元测试(逻辑单元测试、集成单元测试和前端单元测试)的实现,并展示 Spring TestContext 框架和其他支持工具和库如何帮助开发这些测试用例。
在本章中,当我们展示如何实现样本测试用例时,我们将涵盖 Spring TestContext 框架中最常用的模式以及支持的接口和类,而不是展示 Spring 框架在测试领域提供的所有细节和类列表。
使用 Spring 测试注释
在进入逻辑和集成测试之前,值得注意的是,除了标准注释之外,Spring 还提供了特定于测试的注释(比如@Autowired和@Resource)。这些注释可以在您的逻辑和单元测试中使用,提供各种功能,比如简化的上下文文件加载、概要文件、测试执行时间等等。表 13-2 概述了注释及其用途。
表 13-2。
Description of Enterprise Testing Framework
| 注释 | 描述 | | --- | --- | | `@ContextConfiguration` | 用于确定如何为集成测试加载和配置`ApplicationContext`的类级注释。 | | `@WebAppConfiguration` | 用于指示加载的`ApplicationContext`的类级注释应该是一个`WebApplicationContext`。 | | `@ContextHierarchy` | 指示哪个 bean 配置文件应该是活动的类级注释。 | | `@DirtiesContext` | 类和方法级别的注释,用于指示在测试执行期间上下文已经以某种方式被修改或损坏,并且应该被关闭并为后续测试重新构建。 | | `@TestExecutionListeners` | 用于配置应该向`TestContextManager`注册的`TestExecutionListeners`的类级注释。 | | `@` `TransactionConfiguration` | 用于指示事务配置(如回滚设置和事务管理器)的类级注释(如果您想要的事务管理器没有 bean 名称`transactionManager`)。 | | `@Rollback` | 类和方法级注释,用于指示是否应该为带注释的测试方法回滚事务。类级注释用于测试类的默认设置。 | | `@BeforeTransaction` | 方法级注释,指示在事务开始之前,应该调用带注释的方法,用于标记有`@Transactional`注释的测试方法。 | | `@AfterTransaction` | 方法级注释,指示对于用`@Transactional`注释标记的测试方法,应在事务结束后调用带注释的方法。 | | `@IfProfileValue` | 类级和方法级注释,用于指示测试方法应针对一组特定的环境条件启用。 | | `@ProfileValueSource Configuration` | 用于指定`@IfProfileValue`使用的`ProfileValueSource`的类级注释。如果这个注释没有在测试中声明,那么`SystemProfileValueSource`将被用作默认值。 | | `@Timed` | 方法级注释,用于指示测试必须在指定的时间段内完成。 | | `@` `Repeat` | 方法级注释,用于指示带注释的测试方法应该重复指定的次数。 |实现逻辑单元测试
如前所述,逻辑单元测试是最好的测试。目标是验证单个类的行为,用预期的行为“模仿”该类的所有依赖项。在这一节中,我们通过实现SingerController类的测试用例来演示一个逻辑单元测试,其中服务层被模拟为预期的行为。为了帮助模仿服务层的行为,我们将展示如何使用 Mockito ( http://site.mockito.org/ ),这是一个流行的模仿框架。
Spring 框架为spring-test模块中的集成测试提供了一流的支持。为了给这个部分创建的集成测试提供一个测试环境,您将使用spring-test.jar库。这个库包含了用于 Spring 容器集成测试的有价值的类。
添加必需的依赖项
首先,我们需要将依赖项添加到项目中,如下面的配置示例所示。我们还将在前面章节中创建的类和接口的基础上进行构建,比如Singer、SingerService等等。
\\pro-spring-15\build.gradle
ext {
//spring libs
springVersion = '5.0.0.RC1'
bootVersion = '2.0.0.M1'
//testing libs
mockitoVersion = '2.0.2-beta'
junitVersion = '4.12'
hamcrestVersion = '1.3'
dbunitVersion = '2.5.3'
poiVersion = '3.16'
junit5Version = '5.0.0-M4'
spring = [
test : "org.springframework:spring-test:$springVersion",
...
]
boot = [
starterTest :
"org.springframework.boot:spring-boot-starter-test:$bootVersion",
...
]
testing = [
junit : "junit:junit:$junitVersion",
junit5 : "org.junit.jupiter:junit-jupiter-engine:$junit5Version",
junitJupiter: "org.junit.jupiter:junit-jupiter-api:$junit5Version",
mockito : "org.mockito:mockito-all:$mockitoVersion",
easymock : "org.easymock:easymock:3.4",
jmock : "org.jmock:jmock:2.8.2",
hamcrestCore: "org.hamcrest:hamcrest-core:$hamcrestVersion",
hamcrestLib : "org.hamcrest:hamcrest-library:$hamcrestVersion",
dbunit : "org.dbunit:dbunit:$dbunitVersion"
]
misc = [
...
poi : "org.apache.poi:poi:$poiVersion"
]
...
}
单元测试 Spring MVC 控制器
在表示层,控制器类提供了用户界面和服务层之间的集成。
控制器类中的方法将被映射到 HTTP 请求。在该方法中,请求将被处理,将绑定到模型对象,并将与服务层(通过 Spring 的 DI 注入到控制器类中)交互以处理数据。完成后,根据结果,控制器类将更新模型和视图状态(例如,用户消息、REST 服务的对象等等),并返回 Spring MVC 的逻辑视图(或者模型和视图一起)来解析要显示给用户的视图。
对于单元测试控制器类,主要目标是确保控制器方法正确地更新模型和其他视图状态,并返回正确的视图。因为我们只想测试控制器类的行为,所以我们需要用正确的行为“模拟”服务层。
对于SingerController类,我们想为listData()和create(Singer)方法开发测试用例。在接下来的部分中,我们将讨论这方面的步骤。
测试 listData()方法
让我们为singerController.listData()方法创建第一个测试用例。在这个测试用例中,我们希望确保在调用该方法时,在从服务层检索歌手列表之后,信息被正确地保存到模型中,并且返回正确的对象。下面的代码片段显示了测试用例:
package com.apress.prospring5.ch13;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.List;
import com.apress.prospring5.ch13.entities.Singer;
import com.apress.prospring5.ch13.entities.Singers;
import org.junit.Before;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.ui.ExtendedModelMap;
public class SingerControllerTest {
private final List<Singer> singers = new ArrayList<>();
@Before
public void initSingers() {
Singer singer = new Singer();
singer.setId(1l);
singer.setFirstName("John");
singer.setLastName("Mayer");
singers.add(singer);
}
@Test
public void testList() throws Exception {
SingerService singerService = mock(SingerService.class);
when(singerService.findAll()).thenReturn(singers);
SingerController singerController = new SingerController();
ReflectionTestUtils.setField(singerController,
"singerService", singerService);
ExtendedModelMap uiModel = new ExtendedModelMap();
uiModel.addAttribute("singers", singerController.listData());
Singers modelSingers = (Singers) uiModel.get("singers");
assertEquals(1, modelSingers.getSingers().size());
}
}
首先,测试用例调用initSingers()方法,该方法应用了@Before注释,这向 JUnit 表明该方法应该在运行每个测试用例之前运行(如果您想在整个测试类之前运行一些逻辑,请使用@BeforeClass注释)。在该方法中,用硬编码信息初始化歌手列表。
其次,testList()方法与@Test注释一起应用,这向 JUnit 表明这是一个 JUnit 应该运行的测试用例。在测试用例中,私有变量singerService(类型SingerService)通过使用莫克托的Mockito.mock()方法被模拟(注意import static语句)。Mockito 还提供了when()方法来模仿SingerService.findAll()方法,该方法将由SingerController类使用。
第三,创建一个SingerController类的实例,然后通过使用 Spring 提供的ReflectionTestUtils类的setField()方法,将它的singerService变量(在正常情况下将由 Spring 注入)设置为模拟实例。ReflectionTestUtils提供了一组基于反射的实用方法,用于单元和集成测试场景。此外,还构建了一个ExtendedModelMap类的实例(实现了org.springframework.ui.Model接口)。
接下来,调用SingerController. listData()方法。在调用时,通过调用各种 assert 方法(由 JUnit 提供)来验证结果,以确保歌手信息列表正确保存在视图使用的模型中。
现在我们可以运行测试用例了,它应该会成功运行。您可以通过您的构建系统或 IDE 来验证这一点。我们现在可以继续使用create()方法。
测试 create()方法
下面的代码片段显示了测试create()方法的代码片段:
package com.apress.prospring5.ch13;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.List;
import com.apress.prospring5.ch13.entities.Singer;
import com.apress.prospring5.ch13.entities.Singers;
import org.junit.Before;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.ui.ExtendedModelMap;
public class SingerControllerTest {
private final List<Singer> singers = new ArrayList<>();
@Test
public void testCreate() {
final Singer newSinger = new Singer();
newSinger.setId(999l);
newSinger.setFirstName("BB");
newSinger.setLastName("King");
SingerService singerService = mock(SingerService.class);
when(singerService.save(newSinger)).thenAnswer(new Answer<Singer>() {
public Singer answer(InvocationOnMock invocation) throws Throwable {
singers.add(newSinger);
return newSinger;
}
});
SingerController singerController = new SingerController();
ReflectionTestUtils.setField(singerController, "singerService",
singerService);
Singer singer = singerController.create(newSinger);
assertEquals(Long.valueOf(999l), singer.getId());
assertEquals("BB", singer.getFirstName());
assertEquals("King", singer.getLastName());
assertEquals(2, singers.size());
}
}
模仿SingerService.save()方法是为了模拟在歌手列表中添加一个新的Singer对象。注意org.mockito.stubbing.Answer<T>接口的使用,它用预期的逻辑模拟方法并返回一个值。
然后,调用SingerController.create()方法,并调用断言操作来验证结果。再次运行结果,并注意测试用例的结果。对于create()方法,我们应该创建更多的测试用例来测试各种场景。例如,我们需要测试在保存操作中何时遇到数据访问错误。
到目前为止所涉及的一切都可以用 JMock ( www.jmock.org/ )来完成,使用这个库的SingerControllerTest类的一个版本是本节代码示例的一部分。我们不会在这里讨论这个问题,因为模仿依赖关系的想法才是重点,而不是可以做到这一点的库。?? 1
实现集成测试
在本节中,我们将实现服务层的集成测试。在 singer 应用中,核心服务是类SingerServiceImpl,它是SingerService接口的 JPA 实现。
当对服务层进行单元测试时,您将使用 H2 内存数据库来托管数据模型和测试数据,并使用 JPA 提供程序(Hibernate 和 Spring Data JPA 的存储库抽象)。目标是确保SingerServiceImpl类正确执行业务功能。
在接下来的小节中,我们将展示如何测试一些 finder 方法和SingerServiceImpl类的保存操作。
添加必需的依赖项
为了实现带有数据库的测试用例,我们需要一个库,它可以在执行测试用例之前帮助在数据库中填充所需的测试数据,并且可以轻松地执行必要的数据库操作。此外,为了更容易地准备测试数据,我们将支持以 Microsoft Excel 格式准备测试数据。
为了实现这些目的,需要额外的库。在数据库方面,DbUnit ( http://dbunit.sourceforge.net )是一个可以帮助实现数据库相关测试的通用库。此外,Apache POI ( http://poi.apache.org )项目的库将用于帮助解析在 Microsoft Excel 中准备的测试数据。
为服务层测试配置配置文件
Spring 3.1 中引入的 bean definition profiles 特性对于使用测试组件的适当配置来实现测试用例非常有用。为了方便服务层的测试,我们还将为ApplicationContext配置使用 profile 特性。对于 Singer 应用,我们希望有两个配置文件,如下所示:
- 开发概要文件(dev):带有开发环境配置的概要文件。例如,在开发系统中,后端 H2 数据库将执行数据库创建和初始数据填充脚本。
- 测试概要文件(test):包含测试环境配置的概要文件。例如,在测试环境中,后端 H2 数据库将只执行数据库创建脚本,而数据将由测试用例填充。
让我们为 singer 应用配置配置文件环境。对于 singer 应用,后端配置(即数据源、JPA、事务等)是在配置 XML 文件datasource-tx-jpa.xml中定义的。我们只想为 dev 概要文件配置文件中的数据源。为此,我们需要用概要文件配置包装数据源 bean。以下配置片段显示了所需的更改:
<?xml version="1.0" encoding="UTF-8"?>
<beans ...>
<bean id="transactionManager"
class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="emf"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager" />
<bean id="emf"
class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="jpaVendorAdapter">
<bean class=
"org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
</property>
<property name="packagesToScan" value="com.apress.prospring5.ch13"/>
<property name="jpaProperties">
<props>
<prop key="hibernate.dialect">org.hibernate.dialect.H2Dialect</prop>
<prop key="hibernate.max_fetch_depth">3</prop>
<prop key="hibernate.jdbc.fetch_size">50</prop>
<prop key="hibernate.jdbc.batch_size">10</prop>
<prop key="hibernate.show_sql">true</prop>
</props>
</property>
</bean>
<context:annotation-config/>
<jpa:repositories base-package="com.apress.prospring5.ch13"
entity-manager-factory-ref="emf"
transaction-manager-ref="transactionManager"/>
<beans profile="dev">
<jdbc:embedded-database id="dataSource" type="H2">
<jdbc:script location="classpath:config/schema.sql"/>
<jdbc:script location="classpath:config/test-data.sql"/>
</jdbc:embedded-database>
</beans>
</beans>
如配置片段所示,dataSource bean 用<beans>标签包装,并被赋予值为dev的配置文件属性,这表明数据源仅适用于开发系统。记住,概要文件可以通过将-Dspring.profiles.active=dev作为系统参数传递给 JVM 来激活。
Java 配置版本
随着 Java 配置类的引入,XML 正在慢慢失去市场。正因为如此,本书的重点是 Java 配置类的使用;XML 配置只是为了展示 Spring 配置随时间的演变。前面显示的 XML 配置可以分成两部分:一部分是数据源配置,这是特定于概要文件的,另一部分是事务配置,这是开发和测试配置的常用配置。下面描述这两个类。Java 配置中增加的一项改进是数据库模式的“自动”生成,这是通过将 Hibernate 属性hibernate.hbm2ddl.auto设置为create-drop来实现的。
//DataConfig.java
package com.apress.prospring5.ch13.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import javax.sql.DataSource;
@Profile("dev")
@Configuration
@ComponentScan(basePackages = {"com.apress.prospring5.ch13.init"} )
public class DataConfig {
private static Logger logger = LoggerFactory.getLogger(DataConfig.class);
@Bean
public DataSource dataSource() {
try {
EmbeddedDatabaseBuilder dbBuilder = new EmbeddedDatabaseBuilder();
return dbBuilder.setType(EmbeddedDatabaseType.H2).build();
} catch (Exception e) {
logger.error("Embedded DataSource bean cannot be created!", e);
return null;
}
}
}
//ServiceConfig.class
package com.apress.prospring5.ch13.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.util.Properties;
@Configuration
@EnableJpaRepositories(basePackages = {"com.apress.prospring5.ch13.repos"})
@ComponentScan(basePackages = {"com.apress.prospring5.ch13.entities",
"com.apress.prospring5.ch13.services"})
public class ServiceConfig {
@Autowired
DataSource dataSource;
@Bean
public Properties hibernateProperties() {
Properties hibernateProp = new Properties();
hibernateProp.put("hibernate.dialect",
"org.hibernate.dialect.H2Dialect");
hibernateProp.put("hibernate.hbm2ddl.auto", "create-drop");
hibernateProp.put("hibernate.show_sql", true);
hibernateProp.put("hibernate.max_fetch_depth", 3);
hibernateProp.put("hibernate.jdbc.batch_size", 10);
hibernateProp.put("hibernate.jdbc.fetch_size", 50);
return hibernateProp;
}
@Bean
public PlatformTransactionManager transactionManager() {
return new JpaTransactionManager(entityManagerFactory());
}
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
return new HibernateJpaVendorAdapter();
}
@Bean
public EntityManagerFactory entityManagerFactory() {
LocalContainerEntityManagerFactoryBean factoryBean =
new LocalContainerEntityManagerFactoryBean();
factoryBean.setPackagesToScan("com.apress.prospring5.ch13.entities");
factoryBean.setDataSource(dataSource);
factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
factoryBean.setJpaProperties(hibernateProperties());
factoryBean.setJpaVendorAdapter(jpaVendorAdapter());
factoryBean.afterPropertiesSet();
return factoryBean.getNativeEntityManagerFactory();
}
}
实现基础结构类
在实现单独的测试用例之前,我们需要实现一些类来支持 Excel 文件中测试数据的填充。此外,为了简化测试用例的开发,我们想要引入一个名为@DataSets的定制注释,它接受 Excel 文件名作为参数。我们将开发一个定制的测试执行监听器(Spring testing framework 支持的一个特性)来检查注释的存在,并相应地加载数据。
实现自定义 TestExecutionListener
在spring-test模块中,org.springframework.test.context.TestExecutionListener接口定义了一个监听器 API,它可以拦截测试用例执行的各个阶段中的事件(例如,在测试的类之前和之后,在测试的方法之前和之后,等等)。在测试服务层时,我们将为新引入的@DataSets注释实现一个定制监听器。目标是通过对测试用例的简单注释来支持测试数据的填充。例如,为了测试SingerService.findAll()方法,我们希望代码看起来像下面的代码片段:
@DataSets(setUpDataSet= "/com/apress/prospring5/ch13/SingerServiceImplTest.xls")
@Test
public void testFindAll() throws Exception {
List<Singer> result = singerService.findAll();
...
}
将@DataSets注释应用于测试用例表明,在运行测试之前,需要将测试数据从指定的 Excel 文件加载到数据库中。首先,我们需要定义自定义注释,如下所示:
package com.apress.prospring5.ch13;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DataSets {
String setUpDataSet() default "";
}
自定义注释@DataSets是一个方法级注释。此外,实现TestExecutionListener接口,如下面的代码片段所示,将开发定制的测试监听器类:
package com.apress.prospring5.ch13;
import org.dbunit.IDatabaseTester;
import org.dbunit.dataset.IDataSet;
import org.dbunit.util.fileloader.XlsDataFileLoader;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestExecutionListener;
public class ServiceTestExecutionListener implements
TestExecutionListener {
private IDatabaseTester databaseTester;
@Override
public void afterTestClass(TestContext arg0) throws Exception {
}
@Override
public void afterTestMethod(TestContext arg0) throws Exception {
if (databaseTester != null) {
databaseTester.onTearDown();
}
}
@Override
public void beforeTestClass(TestContext arg0) throws Exception {
}
@Override
public void beforeTestMethod(TestContext testCtx) throws Exception {
DataSets dataSetAnnotation = testCtx.getTestMethod()
.getAnnotation(DataSets.class);
if (dataSetAnnotation == null ) {
return;
}
String dataSetName = dataSetAnnotation.setUpDataSet();
if (!dataSetName.equals("") ) {
databaseTester = (IDatabaseTester)
testCtx.getApplicationContext().getBean("databaseTester");
XlsDataFileLoader xlsDataFileLoader = (XlsDataFileLoader)
testCtx.getApplicationContext().getBean("xlsDataFileLoader");
IDataSet dataSet = xlsDataFileLoader.load(dataSetName);
databaseTester.setDataSet(dataSet);
databaseTester.onSetup();
}
}
@Override
public void prepareTestInstance(TestContext arg0) throws Exception {
}
}
在实现了TestExecutionListener接口之后,还需要实现一些方法。然而,在这种情况下,我们只对方法beforeTestMethod()和afterTestMethod()感兴趣,在这两种方法中,在每个测试方法执行之前和之后,测试数据的填充和清理都将被执行。注意,在每个方法中,Spring 将传入一个TestContext类的实例,这样该方法就可以访问由 Spring 框架引导的底层测试ApplicationContext。
方法beforeTestMethod()特别有趣。首先,它检查测试方法的@DataSets注释是否存在。如果注释存在,测试数据将从指定的 Excel 文件中加载。在这种情况下,IDatabaseTester接口(带有实现类org.dbunit.DataSourceDatabaseTester,我们将在后面讨论)是从TestContext获得的。IDatabaseTester接口由 DbUnit 提供,支持基于给定数据库连接或数据源的数据库操作。
其次,从TestContext中获得一个XlsDataFileLoader类的实例。XlsDataFileLoader类是 DbUnit 对从 Excel 文件加载数据的支持。它在后台使用 Apache POI 库来读取 Microsoft Office 格式的文件。然后,调用XlsDataFileLoader.load()方法从文件中加载数据,这将返回一个IDataSet接口的实例,代表加载的数据集。
最后,调用IDatabaseTester.setDataSet()方法设置测试数据,调用IDatabaseTester.onSetup()方法触发数据的填充。
在afterTestMethod()方法中,调用IDatabaseTester.onTearDown()方法来清理数据。
实现配置类
让我们继续实现测试环境的配置类。以下代码片段显示了使用 Java Config 样式配置的代码:
package com.apress.prospring5.ch13.config;
import javax.sql.DataSource;
import com.apress.prospring5.ch13.init.DBInitializer;
import org.dbunit.DataSourceDatabaseTester;
import org.dbunit.util.fileloader.XlsDataFileLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.*;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
@Configuration
@ComponentScan(basePackages={"com.apress.prospring5.ch13"},
excludeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE,
value = DBInitializer.class)
})
@Profile("test")
public class ServiceTestConfig {
private static Logger logger = LoggerFactory.getLogger(ServiceTestConfig.class);
@Bean
public DataSource dataSource() {
try {
EmbeddedDatabaseBuilder dbBuilder = new EmbeddedDatabaseBuilder();
return dbBuilder.setType(EmbeddedDatabaseType.H2).build();
} catch (Exception e) {
logger.error("Embedded DataSource bean cannot be created!", e);
return null;
}
}
@Bean(name="databaseTester")
public DataSourceDatabaseTester dataSourceDatabaseTester() {
DataSourceDatabaseTester databaseTester =
new DataSourceDatabaseTester(dataSource());
return databaseTester;
}
@Bean(name="xlsDataFileLoader")
public XlsDataFileLoader xlsDataFileLoader() {
return new XlsDataFileLoader();
}
}
ServiceTestConfig类定义了服务层测试的ApplicationContext实现。@ComponentScan注释用于指示 Spring 扫描我们想要测试的服务层 beans。
属性用来确保测试数据库没有被开发条目初始化。
@Profile注释指定了这个类中定义的 beans 属于测试概要文件。
其次,在该类中,声明了另一个dataSource bean,该 bean 只执行 H2 数据库的schema.sql脚本,而不包含任何数据。用于从 Excel 文件加载测试数据的定制测试执行监听器使用了databaseTester和xlsDataFileLoaderbean。注意,dataSourceDatabaseTester bean 是使用为测试环境定义的dataSource bean 构建的。
服务层的单元测试
让我们从单元测试 finder 方法开始,包括SingerService.findAll()和SingerService.findByFirstNameAndLastName()方法。首先,我们需要准备 Excel 格式的测试数据。一种常见的做法是将文件放入与测试用例类相同的文件夹中,使用相同的名称。所以,在这种情况下,文件名是/src/test/java/com/apress/prospring5/ch13/SingerServiceImplTest.xls。
测试数据准备在工作表中。工作表的名称是表格的名称(SINGER),而第一行是表格中的列名。从第二行开始,输入名和姓以及出生日期的数据。我们指定了 ID 列,但没有指定值。这是因为 ID 将由数据库填充。请参阅本书源代码中的 Excel 文件示例。
下面的代码片段显示了带有两个 finder 方法的测试用例的测试类:
package com.apress.prospring5.ch13;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.validation.ConstraintViolationException;
import com.apress.prospring5.ch13.entities.Singer;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.
AbstractTransactionalJUnit4SpringContextTests;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {ServiceTestConfig.class, ServiceConfig.class,
DataConfig.class})
@TestExecutionListeners({ServiceTestExecutionListener.class})
@ActiveProfiles("test")
public class SingerServiceImplTest extends
AbstractTransactionalJUnit4SpringContextTests {
@Autowired
SingerService singerService;
@PersistenceContext
private EntityManager em;
@DataSets(setUpDataSet= "/com/apress/prospring5/ch13/SingerServiceImplTest.xls")
@Test
public void testFindAll() throws Exception {
List<Singer> result = singerService.findAll();
assertNotNull(result);
assertEquals(1, result.size());
}
@DataSets(setUpDataSet= "/com/apress/prospring5/ch13/SingerServiceImplTest.xls")
@Test
public void testFindByFirstNameAndLastName_1() throws Exception {
Singer result = singerService.findByFirstNameAndLastName("John", "Mayer");
assertNotNull(result);
}
@DataSets(setUpDataSet= "/com/apress/prospring5/ch13/SingerServiceImplTest.xls")
@Test
public void testFindByFirstNameAndLastName_2() throws Exception {
Singer result = singerService.findByFirstNameAndLastName("BB", "King");
assertNull(result);
}
}
@RunWith注释与测试控制器类时相同。@ContextConfiguration注释指定ApplicationContext配置应该从ServiceTestConfig、ServiceConfig和DataConfig类中加载。DataConfig类本来不应该在那里,但是它被使用了,只是为了让 Spring profiles 实际工作起来更加明显。@TestExecutionListeners注释表明ServiceTestExecutionListener类应该用于截取测试用例执行生命周期。@ActiveProfiles注释指定了要使用的轮廓。因此,在这种情况下,将加载在ServiceTestConfig类中定义的dataSource bean,而不是在datasource-tx-jpa.xml文件中定义的 bean,因为它属于 dev 概要文件。
此外,该类扩展了 Spring 的AbstractTransactionalJUnit4SpringContextTests类,这是 Spring 对 JUnit 的支持,具有 Spring 的 DI 和事务管理机制。注意,在 Spring 的测试环境中,Spring 会在执行每个测试方法时回滚事务,这样所有的数据库更新操作都会回滚。为了控制回滚行为,您可以在方法级别使用@Rollback注释。
对于findAll()方法有一个测试用例,对于testFindByFirstNameAndLastName()方法有两个测试用例(一个检索结果,一个不检索)。所有的 finder 方法都应用了 Excel 中带有Singer测试数据文件的@DataSets注释。另外,SingerService从ApplicationContext开始被自动连线到测试用例中。代码的其余部分应该是不言自明的。在每个测试用例中应用了各种断言语句,以确保结果是预期的。
运行测试用例,并确保它通过。接下来,让我们测试保存操作。在这种情况下,我们将测试两个场景。一种是成功保存有效的Singer的正常情况,另一种是应该导致抛出正确异常的Singer错误。下面的代码片段显示了两个测试用例的附加代码片段:
package com.apress.prospring5.ch13;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.validation.ConstraintViolationException;
import com.apress.prospring5.ch13.entities.Singer;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.
AbstractTransactionalJUnit4SpringContextTests;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {ServiceTestConfig.class, ServiceConfig.class,
DataConfig.class})
@TestExecutionListeners({ServiceTestExecutionListener.class})
@ActiveProfiles("test")
public class SingerServiceImplTest extends
AbstractTransactionalJUnit4SpringContextTests {
@Autowired
SingerService singerService;
@PersistenceContext
private EntityManager em;
@Test
public void testAddSinger() throws Exception {
deleteFromTables("SINGER");
Singer singer = new Singer();
singer.setFirstName("Stevie");
singer.setLastName("Vaughan ");
singerService.save(singer);
em.flush();
List<Singer> singers = singerService.findAll();
assertEquals(1, singers.size());
}
@Test(expected=ConstraintViolationException.class)
public void testAddSingerWithJSR349Error() throws Exception {
deleteFromTables("SINGER");
Singer singer = new Singer();
singerService.save(singer);
em.flush();
List<Singer> singers = singerService.findAll();
assertEquals(0, singers.size());
}
}
在前面的清单中,看一下testAddSinger()方法。在该方法中,为了确保在Singer表中没有数据,我们调用了由AbstractTransactionalJUnit4SpringContextTests类提供的方便的方法deleteFromTables()来清理该表。请注意,在调用保存操作之后,我们需要显式调用EntityManager.flush()方法来强制 Hibernate 将持久性上下文刷新到数据库,以便findAll()方法可以正确地从数据库中检索信息。
在 Spring 4.3 中引入了SpringJUnit4ClassRunner.class的别名SpringRunner.class。
在第二个测试方法中,即testAddSingerWithJSR349Error()方法,我们测试了带有验证错误的Singer的保存操作。注意,在@Test注释中,传递了一个预期属性,该属性指定这个测试用例预期抛出一个指定类型的异常,在本例中是ConstraintViolationException类。
再次运行测试类,并验证结果是否成功。
注意,我们只讨论了 Spring 测试框架中最常用的类。Spring 的测试框架提供了许多支持类和注释,允许我们在测试用例生命周期的执行过程中应用精细控制。例如,@BeforeTransaction和@AfterTransaction注释允许在 Spring 启动事务之前或者在测试用例的事务完成之后执行某些逻辑。关于 Spring 测试框架各个方面的更详细的描述,请参考 Spring 的参考文档。
删除数据单元
DbUnit 被认为很难使用,因为它需要额外的依赖项和额外的配置类。Spring 的方法会很好,不是吗?幸运的是,在 4.0 之后的 Spring 版本中引入了许多有用的注释。下面的例子中会用到其中一个:@Sql。此注释用于注释测试类或测试方法,以配置在集成测试期间针对给定数据库执行的 SQL 脚本和语句。这意味着可以在不使用 DbUnit 的情况下准备测试数据。因此,测试配置也得到了简化,测试类不需要扩展任何东西。
本节的另一个补充是 JUnit 5 ( http://junit.org/junit5/ )的使用,也称为 JUnit Jupiter。从 Spring 4.3 开始,甚至在第一个稳定版本发布之前,就已经提供了对它的支持。在撰写本文时,当前版本是 5.0.0-M4。JUnit 5 是 JUnit 的下一代。目标是为 JVM 上的开发人员端测试创建一个最新的基础。这包括关注 Java 8 和更新的版本,以及支持许多不同风格的测试。 2
让我们看看配置是如何被修改的。测试概要文件的测试数据源配置类变得简单多了,因为我们现在需要的是一个空的数据库。
package com.apress.prospring5.ch13.config;
import com.apress.prospring5.ch13.init.DBInitializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.*;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import javax.sql.DataSource;
@Configuration
@ComponentScan(basePackages={"com.apress.prospring5.ch13"},
excludeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE,
value = DBInitializer.class)
})
@Profile("test")
public class SimpleTestConfig {
private static Logger logger = LoggerFactory.getLogger(SimpleTestConfig.class);
@Bean
public DataSource dataSource() {
try {
EmbeddedDatabaseBuilder dbBuilder = new EmbeddedDatabaseBuilder();
return dbBuilder.setType(EmbeddedDatabaseType.H2).build();
} catch (Exception e) {
logger.error("Embedded DataSource bean cannot be created!", e);
return null;
}
}
}
测试用例所需的数据和查询将使用 SQL 脚本文件提供。数据将由一个名为test-data.sql的文件提供。其内容如下所示:
insert into singer (first_name, last_name, birth_date,version)
values ('John', 'Mayer', '1977-10-16', 0);
测试数据库的清理将使用clean-up.sql脚本来完成。这个脚本用于清空数据库,以便一个测试方法所必需的数据不会污染另一个测试方法的数据。其内容如下所示:
delete from singer;
测试类将利用一些 JUnit5 注释来展示如何使用它们。每个注释都将在代码部分之后解释。
package com.apress.prospring5.ch13;
import com.apress.prospring5.ch13.config.DataConfig;
import com.apress.prospring5.ch13.config.ServiceConfig;
import com.apress.prospring5.ch13.config.SimpleTestConfig;
import com.apress.prospring5.ch13.entities.Singer;
import com.apress.prospring5.ch13.services.SingerService;
import org.junit.jupiter.api.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.jdbc.SqlConfig;
import org.springframework.test.context.jdbc.SqlGroup;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@SpringJUnitConfig(classes = {SimpleTestConfig.class, ServiceConfig.class,
DataConfig.class})
@DisplayName("Integration SingerService Test")
@ActiveProfiles("test")
public class SingerServiceTest {
private static Logger logger =
LoggerFactory.getLogger(SingerServiceTest.class);
@Autowired
SingerService singerService;
@BeforeAll
static void setUp() {
logger.info("--> @BeforeAll -
executes before executing all test methods in this class");
}
@AfterAll
static void tearDown(){
logger.info("--> @AfterAll -
executes before executing all test methods in this class");
}
@BeforeEach
void init() {
logger.info("--> @BeforeEach -
executes before each test method in this class");
}
@AfterEach
void dispose() {
logger.info("--> @AfterEach -
executes before each test method in this class");
}
@Test
@DisplayName("should return all singers")
@SqlGroup({
@Sql(value = "classpath:db/test-data.sql",
config = @SqlConfig(encoding = "utf-8", separator = ";",
commentPrefix = "--"),
executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD),
@Sql(value = "classpath:db/clean-up.sql",
config = @SqlConfig(encoding = "utf-8", separator = ";",
commentPrefix = "--"),
executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD),
})
public void findAll() {
List<Singer> result = singerService.findAll();
assertNotNull(result);
assertEquals(1, result.size());
}
@Test
@DisplayName("should return singer 'John Mayer'")
@SqlGroup({
@Sql(value = "classpath:db/test-data.sql",
config = @SqlConfig(encoding = "utf-8", separator = ";",
commentPrefix = "--"),
executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD),
@Sql(value = "classpath:db/clean-up.sql",
config = @SqlConfig(encoding = "utf-8", separator = ";",
commentPrefix = "--"),
executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD),
})
public void testFindByFirstNameAndLastNameOne() throws Exception {
Singer result = singerService.findByFirstNameAndLastName("John", "Mayer");
assertNotNull(result);
}
}
使用SpringJUnitJupiterConfig注释创建ApplicationContext。这是一个组合注释,将来自 JUnit Jupiter 的@ExtendWith(SpringExtension.class)与来自 Spring TestContext 框架的@ContextConfiguration结合在一起。
@DisplayName注释是一个典型的 JUnit Jupiter 注释,用于声明带注释的测试类或测试方法的定制显示值。在支持 JUnit 5 的编辑器中,这看起来非常漂亮,如图 13-1 所示。

图 13-1。
IntelliJ IDEA Jnit 5 test run view
这更容易看出SingerService正在按预期工作,对吗?
注释@BeforeAll和@AfterAll的名称不言自明,它们取代了 JUnit 4 的@BeforeClass和@AfterClass。
关于@BeforeEach和@AfterEach也可以这么说;他们取代了 JUnit 4 @Before和@After。@SqlGroup注释用于对多个@Sql注释进行分组。
实现前端单元测试
另一个特别感兴趣的测试领域是在将 web 应用部署到像 Apache Tomcat 这样的 web 容器时,整体测试前端行为。
主要原因是,即使我们测试了应用中的每一层,我们仍然需要确保视图在用户的不同操作下行为正确。当在测试用例的前端重复操作时,自动化前端测试对于节省开发人员和用户的时间是很重要的。
然而,为前端开发一个测试用例是一项具有挑战性的任务,尤其是对于那些具有大量交互式的、丰富的、基于 Ajax 的组件的 web 应用。
介绍硒
Selenium 是一个强大而全面的工具和框架目标,用于自动化基于 web 的前端测试。主要特性是,通过使用 Selenium,我们可以“驱动”浏览器,模拟用户与应用的交互,并执行视图状态的验证。
Selenium 支持常见的浏览器,包括 Firefox、IE 和 Chrome。语言方面,支持 Java,C#,PHP,Perl,Ruby,Python。Selenium 在设计时还考虑到了 Ajax 和富互联网应用(RIA ),使得现代 web 应用的自动化测试成为可能。
如果你的应用有很多前端用户界面,需要运行大量的前端测试,selenium-server模块提供了内置的网格功能,支持在一组计算机之间执行前端测试。
Selenium IDE 是一个 Firefox 插件,可以帮助“记录”用户与 web 应用的交互。它还支持重放,并将脚本导出为各种格式,这有助于简化测试用例的开发。
从版本 2.0 开始,Selenium 集成了 WebDriver API,它解决了许多限制,并提供了一个替代的、更简单的编程接口。其结果是一个全面的面向对象的 API,它为大量的浏览器提供了额外的支持,并改进了对现代高级 web 应用测试问题的支持。
前端 web 测试是一个复杂的主题,超出了本书的范围。从这个简单的概述中,您可以看到 Selenium 如何通过跨浏览器兼容性来帮助自动化用户与 web 应用前端的交互。更多细节请参考 Selenium 的在线文档( http://seleniumhq.org/docs )。
摘要
在这一章中,我们讲述了如何借助常用的框架、库和工具,包括 JUnit、DbUnit 和 Mockito,在基于 Spring 的应用中开发各种单元测试。
首先,我们给出了企业测试框架的高级描述,它显示了在应用开发生命周期的每个阶段应该执行什么测试。其次,我们开发了两种类型的测试,包括逻辑单元测试和集成单元测试。然后,我们简要介绍了前端测试框架 Selenium。
测试企业应用是一个巨大的主题,如果您想更详细地了解 JUnit 库,我们推荐 Petar Tahchiev (Manning,2011)的《JUnit 在行动》一书,该书涵盖了 JUnit 4.8。
如果您对更多 Spring 测试方法感兴趣,您可以在 Pivotal 认证专业 Spring 开发人员考试( www.apress.com/us/book/9781484208120 ,2016 年 4 月)中找到更多相关信息,该考试有一个专门的章节涵盖更多测试库,并与 Spring Boot 测试一起测试 Spring Boot 应用。
Footnotes 1
另一个备选是 EasyMock: http://easymock.org/ 。
2
这是来自官方网站的引用。
十四、Spring 中的脚本支持
在前面的章节中,您看到了 Spring 框架如何帮助 Java 开发人员创建 JEE 应用。通过使用 Spring 框架的 DI 机制及其与每一层的集成(通过 Spring 框架自身模块中的库或通过与第三方库的集成),您可以简化业务逻辑的实现和维护。
然而,到目前为止,我们开发的所有逻辑都是用 Java 语言开发的。尽管 Java 是历史上最成功的编程语言之一,但它仍然因一些弱点而受到批评,包括其语言结构以及在大规模并行处理等领域缺乏全面的支持。
例如,Java 语言的一个特性是所有变量都是静态类型的。换句话说,在 Java 程序中,每个声明的变量都应该有一个与之相关联的静态类型(String、int、Object、ArrayList等等)。但是,在某些情况下,动态类型可能是首选,它受动态语言(如 JavaScript)的支持。
为了满足这些需求,开发了许多脚本语言。其中最流行的包括 JavaScript、Groovy、Scala、Ruby 和 Erlang。几乎所有这些语言都支持动态类型,旨在提供 Java 中没有的特性,以及针对其他特定目的。例如,Scala ( www.scala-lang.org )将函数式编程模式与 OO 模式相结合,并通过参与者和消息传递的概念支持更全面、更可扩展的并发编程模型。此外,Groovy ( http://groovy.codehaus.org )提供了一个简化的编程模型,并支持特定领域语言(DSL)的实现,使得应用代码更容易阅读和维护。
这些脚本语言带给 Java 开发人员的另一个重要概念是闭包(我们将在本章后面详细讨论)。简单地说,闭包是包装在对象中的一段(或一块)代码。像 Java 方法一样,它是可执行的,可以接收参数并返回对象和值。此外,它是一个普通的对象,可以在应用中通过引用传递,就像 Java 中的任何 POJO 一样。
在这一章中,我们将介绍脚本语言背后的一些主要概念,主要关注 Groovy 您将看到 Spring 框架如何无缝地与脚本语言一起工作,为基于 Spring 的应用提供特定的功能。具体来说,本章涵盖以下主题:
- Java 中的脚本支持:在 JCP,JSR-223(Java 平台脚本)支持 Java 中的脚本语言;从 SE 6 开始就可以用 Java 实现了。我们概述了 Java 中的脚本支持。
- Groovy:我们给出了 Groovy 语言的高级介绍,Groovy 是 Java 中最流行的脚本语言之一。
- 结合 Spring 使用 Groovy:Spring 框架为脚本语言提供了全面的支持。从版本 3.1 开始,提供了对 Groovy、JRuby 和 BeanShell 的现成支持。
本章并不打算作为使用脚本语言的详细参考。每种语言都有一本或多本详细讨论它们的设计和用法的书。本章的主要目的是描述 Spring 框架如何支持脚本语言,并通过一个很好的例子来展示在基于 Spring 的应用中除了 Java 之外还使用脚本语言的好处。
在 Java 中使用脚本支持
从 Java 6 开始,Java 平台 API (JSR-223)的脚本被捆绑到 JDK 中。它的目标是为在 JVM 上运行用其他脚本语言编写的逻辑提供一个标准机制。开箱即用,JDK 6 捆绑了名为 Mozilla Rhino 的引擎,该引擎能够评估 JavaScript 程序。本节将向您介绍 JDK 6 中的 JSR-223 支持。
在 JDK 6 中,脚本支持类位于javax.script包中。首先让我们开发一个简单的程序来检索脚本引擎列表。下面的代码片段显示了类的内容:
package com.apress.prospring5.ch14;
import javax.script.ScriptEngineManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ListScriptEngines {
private static Logger logger =
LoggerFactory.getLogger(ListS criptEngines.class);
public static void main(String... args) {
ScriptEngineManager mgr = new ScriptEngineManager();
mgr.getEngineFactories().forEach(factory -> {
String engineName = factory.getEngineName();
String languageName = factory.getLanguageName();
String version = factory.getLanguageVersion();
logger.info("Engine name: " + engineName + " Language: "
+ languageName + " Version: " + version);
});
}
}
创建了一个ScriptEngineManager类的实例,它将从类路径中发现并维护一个引擎列表(换句话说,就是实现javax.script.ScriptEngine接口的类)。然后,通过调用ScriptEngineManager.getEngineFactories()方法来检索ScriptEngineFactory接口的列表。ScriptEngineFactory接口用于描述和实例化脚本引擎。从每个ScriptEngineFactory接口,可以检索关于脚本语言支持的信息。根据您的设置,运行该程序可能会产生不同的输出,您应该会在控制台中看到类似以下内容的内容:
INFO: Engine name: AppleScriptEngine Language: AppleScript Version: 2.5
INFO: Engine name: Oracle Nashorn Language: ECMAScript Version: ECMA - 262 Edition 5.1
让我们编写一个简单的程序来计算一个基本的 JavaScript 表达式。该程序如下所示:
package com.apress.prospring5.ch14.javascript;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class JavaScriptTest {
private static Logger logger =
LoggerFactory.getLogger(JavaScriptTest.class);
public static void main(String... args) {
ScriptEngineManager mgr = new ScriptEngineManager();
ScriptEngine jsEngine = mgr.getEngineByName("JavaScript");
try {
jsEngine.eval("print('Hello JavaScript in Java')");
} catch (ScriptException ex) {
logger.error("JavaScript expression cannot be evaluated!", ex);
}
}
}
这里使用名称JavaScript从ScriptEngineManager类中检索到一个ScriptEngine接口的实例。然后,调用ScriptEngine.eval()方法,传入一个String参数,其中包含一个 JavaScript 表达式。注意,参数也可以是一个java.io.Reader类,它可以从文件中读取 JavaScript。
运行该程序会产生以下结果:
Hello JavaScript in Java
这应该会让您了解如何在 Java 中运行脚本。然而,仅仅使用另一种语言转储一些输出并没有多大意义。在下一节中,我们将介绍 Groovy,这是一种强大而全面的脚本语言。
Groovy 简介
Groovy 由 James Strachan 于 2003 年创立,其主要目标是为 JVM 提供一种敏捷的动态语言,其功能受到了其他流行脚本语言的启发,包括 Python、Ruby 和 Smalltalk。Groovy 构建在 Java 之上,扩展了 Java,并解决了 Java 的一些缺点。
在接下来的几节中,我们将讨论 Groovy 背后的一些主要特性和概念,以及它如何补充 Java 来满足特定的应用需求。注意,这里提到的许多特性在其他脚本语言中也是可用的(例如,Scala、Erlang、Python 和 Clojure)。
动态打字
Groovy(和许多其他脚本语言)和 Java 的一个主要区别是支持变量的动态类型。在 Java 中,所有属性和变量都应该是静态类型的。换句话说,应该用declare语句提供类型。但是,Groovy 支持变量的动态类型。在 Groovy 中,动态类型变量是用关键字def声明的。
让我们通过开发一个简单的 Groovy 脚本来看看这一点。Groovy 类或脚本的文件后缀是groovy。下面的代码片段展示了一个简单的动态类型 Groovy 脚本:
package com.apress.prospring5.ch14
class Singer {
def firstName
def lastName
def birthDate
String toString() {
"($firstName,$lastName,$birthDate)"
}
}
Singer singer = new Singer(firstName: 'John', lastName: 'Mayer',
birthDate: new Date(
(new GregorianCalendar(1977, 9, 16)).getTime().getTime()))
Singer anotherSinger =
new Singer(firstName: 39, lastName: 'Mayer', birthDate: new Date(
(new GregorianCalendar(1977, 9, 16)).getTime().getTime()))
println singer
println anotherSinger
println singer.firstName + 39
println anotherSinger.firstName + 39
这个 Groovy 脚本可以直接在 IDE 中运行,无需编译就可以执行(Groovy 提供了一个名为groovy的命令行工具,可以直接执行 Groovy 脚本),也可以编译成 Java 字节码,然后像其他 Java 类一样执行。Groovy 脚本不需要一个main()方法来执行。此外,不需要与文件名匹配的类声明。
在这个例子中,定义了一个类Singer,使用def关键字将属性设置为动态类型。声明了三个属性。然后,toString()方法被返回字符串的闭包覆盖。
接下来,构造了两个Singer对象的实例,使用 Groovy 提供的简写语法来定义属性。对于第一个Singer对象,firstName属性被赋予一个String,而一个整数被赋予第二个Singer对象。最后,使用println语句(与调用System.out.println()方法相同)打印两个 singer 对象。为了展示 Groovy 如何处理动态类型,定义了两个println语句来打印操作firstName + 39的输出。注意,在 Groovy 中,当向方法传递参数时,括号是可选的。
运行该程序会产生以下输出:
John,Mayer,Sun Oct 16 00:00:00 EET 1977
39,Mayer,Sun Oct 16 00:00:00 EET 1977
John39
78
从输出中,您可以看到,由于firstName是用动态类型定义的,所以当传入一个String或一个Integer作为类型时,对象构造成功。此外,在最后两个println语句中,add 操作被正确地应用于两个对象的firstName属性。在第一个场景中,因为firstName是一个String,所以字符串39被附加到它的后面。对于第二个场景,因为firstName是一个整数,所以将整数 39 加到上面,得到 78。
Groovy 的动态类型支持为在应用逻辑中操作类属性和变量提供了更大的灵活性。
简化语法
Groovy 还提供了简化的语法,这样 Java 中的相同逻辑可以用更少的代码在 Groovy 中实现。一些基本语法如下:
- 结束语句不需要分号。
- 在方法中,
return关键字是可选的。 - 默认情况下,所有方法和类都是公共的。所以,除非需要,否则不需要为方法声明声明
public关键字。 - 在一个类中,Groovy 会自动为声明的属性生成 getter/setter 方法。所以在一个 Groovy 类中,您只需要声明类型和名称(例如,
String firstName或def firstName),并且您可以通过自动使用 getter/setter 方法来访问任何其他 Groovy/Java 类中的属性。此外,您可以简单地访问没有get/set前缀的属性(例如,singer.firstName = 'John')。Groovy 将智能地为您处理它们。
Groovy 还为 Java 集合 API 提供了简化的语法和许多有用的方法。下面的代码片段展示了一些常用于列表操作的 Groovy 操作:
def list = ['This', 'is', 'John Mayer']
println list
assert list.size() == 3
assert list.class == ArrayList
assert list.reverse() == ['John Mayer', 'is', 'This']
assert list.sort{ it.size() } == ['is', 'This', 'John Mayer']
assert list[0..1] == ['is', 'This']
前面的代码只展示了 Groovy 提供的一小部分特性。更详细的描述请参考 Groovy 在线文档 http://groovy.codehaus.org/JN1015-Collections 。
关闭
Groovy 添加到 Java 的最重要的特性之一是对闭包的支持。闭包允许将一段代码包装成一个对象,并在应用中自由传递。闭包是一个强大的特性,它支持智能和动态的行为。为 Java 语言添加闭包支持的要求由来已久。JSR-335(Java 编程语言的 Lambda 表达式)旨在通过向 Java 语言添加闭包和相关特性来支持多核环境中的编程,它已被添加到 Java 8 中,并受到新的 Spring Framework 4 的支持。
下面的代码片段展示了一个在 Groovy 中使用闭包(文件名是Runner.groovy)的简单例子:
def names = ['John', 'Clayton', 'Mayer']
names.each {println 'Hello: ' + it}
这里声明了一个列表。然后,方便的each()方法用于遍历列表中每一项的操作。each()方法的参数是一个闭包,在 Groovy 中用花括号括起来。因此,闭包中的逻辑将应用于列表中的每一项。在闭包中有一个特殊变量,Groovy 用它来表示当前上下文中的项目。所以,闭包会给列表中的每一项加上前缀字符串"Hello: ",然后打印出来。运行该脚本会产生以下输出:
Hello: John
Hello: Clayton
Hello: Mayer
如前所述,闭包可以声明为变量,并在需要时使用。这里显示了另一个示例:
def map = ['a': 10, 'b': 50]
Closure square = {key, value -> map[key] = value * value}
map.each square
println map
在这个例子中,定义了一个映射。然后,声明一个类型为Closure的变量。闭包接受 map 条目的键和值作为参数,逻辑计算键值的平方。运行该程序会产生以下输出:
[a:100, b:2500]
这只是对闭包的简单介绍。在下一节中,我们将使用 Groovy 和 Spring 开发一个简单的规则引擎;也使用闭包。关于在 Groovy 中使用闭包的更详细的描述,请参考位于 http://groovy.codehaus.org/JN2515-Closures 的在线文档。
通过 Spring 使用 Groovy
Groovy 和其他脚本语言给基于 Java 的应用带来的主要好处是支持动态行为。通过使用闭包,可以将业务逻辑打包成一个对象,并像其他变量一样在应用中传递。
Groovy 的另一个主要特性是通过使用其简化的语法和闭包来支持开发 DSL。顾名思义,DSL 是一种针对特定领域的语言,在设计和实现上有非常具体的目标。目标是构建一种不仅能被开发人员理解,而且能被业务分析师和用户理解的语言。大多数时候,领域是一个业务领域。例如,可以为客户分类、销售费用计算、工资计算等定义 DSL。
在这一节中,我们将演示如何使用 Groovy 来实现一个简单的规则引擎,它支持 Groovy 的 DSL。该实现引用了 www.pleus.net/articles/grules/grules.pdf 上关于该主题的优秀文章中的示例,并进行了修改。此外,我们还讨论了 Spring 对可刷新 beans 的支持如何在不需要编译、打包和部署应用的情况下动态更新底层规则。
在这个示例中,我们实现了一个规则,用于根据年龄将特定歌手分类为不同的类别,年龄是根据他们的出生日期属性计算的。
发展歌手领域
如前所述,DSL 的目标是一个特定的领域,大多数时候这个领域指的是某种业务数据。对于我们将要实现的规则,它被设计为应用于歌手信息的领域。
因此,第一步是开发我们希望规则应用到的领域对象模型。这个示例很简单,只包含一个Singer实体类,如下所示。注意,它是一个 POJO 类,就像我们在前面章节中使用的那些。
package com.apress.prospring5.ch14;
import org.joda.time.DateTime;
public class Singer {
private Long id;
private String firstName;
private String lastName;
private DateTime birthDate;
private String ageCategory;
... //getters and setter
@Override
public String toString() {
return "Singer - Id: " + id + ", First name: " + firstName
+ ", Last name: " + lastName + ", Birthday: " + birthDate
+ ", Age category: " + ageCategory;
}
}
这里的Singer类由简单的歌手信息组成。对于ageCategory属性,我们想要开发一个动态规则,可以用来执行分类。该规则将根据birthDate属性计算年龄,然后根据该规则分配ageCategory属性(例如,儿童、青年或成人)。
实施规则引擎
下一步是开发一个简单的规则引擎,用于在域对象上应用规则。首先,我们需要定义一个规则需要包含什么信息。下面的代码片段展示了Rule类,这是一个 Groovy 类(文件名为Rule.groovy):
package com.apress.prospring5.ch14
class Rule {
private boolean singlehit = true
private conditions = new ArrayList()
private actions = new ArrayList()
private parameters = new ArrayList()
}
每个规则都有几个属性。conditions属性定义了规则引擎在处理域对象时应该检查的各种条件。属性定义了当条件匹配时要采取的动作。parameters属性定义了规则的行为,这是不同条件下操作的结果。最后,singlehit属性定义了每当发现条件匹配时,规则是否应该立即结束执行。
下一步是规则执行的引擎。下面的代码片段显示了RuleEngine接口(注意这是一个 Java 接口):
package com.apress.prospring5.ch14;
public interface RuleEngine {
void run(Rule rule, Object object);
}
该接口只定义了一个方法run(),用于将规则应用于域对象参数。
我们将提供 Groovy 中规则引擎的实现。下面的代码片段展示了 Groovy 类RuleEngineImpl(文件名为RuleEngineImpl.groovy):
package com.apress.prospring5.ch14
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
@Component("ruleEngine")
class RuleEngineImpl implements RuleEngine {
Logger logger = LoggerFactory.getLogger(RuleEngineImpl.class);
void run(Rule rule, Object object) {
logger.info "Executing rule"
def exit=false
rule.parameters.each{ArrayList params ->
def paramIndex=0
def success=true
if(!exit){
rule.conditions.each{
logger.info "Condition Param index: " + paramIndex
success = success && it(object,paramsparamIndex)
logger.info "Condition success: " + success
paramIndex++
}
if(success && !exit){
rule.actions.each{
logger.info "Action Param index: " + paramIndex
it(object,paramsparamIndex)
paramIndex++
}
if (rule.singlehit){
exit=true
}
}
}
}
}
}
第一个RuleEngineImpl实现了RuleEngine Java 接口,Spring 的注释像其他 POJO 一样被应用。在run()方法中,规则中定义的参数被传递到一个闭包中进行逐个处理。对于每个参数(它是一个值列表),条件(每个条件都是一个闭包)都是用参数列表和域对象中的相应项逐一检查的。只有当所有条件都匹配时,成功指示器才会变为真。在这种情况下,规则中定义的操作(每个操作也是一个闭包)将在对象上执行,参数列表中有相应的值。最后,如果找到特定参数的匹配,并且singlehit变量为真,那么规则执行将被停止,并将立即退出。
为了允许以更灵活的方式检索规则,让我们定义一个RuleFactory接口,如下所示。注意,这是一个 Java 接口。
package com.apress.prospring5.ch14;
public interface RuleFactory {
Rule getAgeCategoryRule();
}
由于歌手的年龄类别分类只有一个规则,因此该接口只定义了一个方法来检索该规则。
为了使规则引擎对消费者透明,让我们开发一个简单的服务层来包装它。下面的代码片段分别显示了SingerService接口和SingerServiceImpl类。注意,它们是 Java 实现。
//SingerService.java
package com.apress.prospring5.ch14;
public interface SingerService {
void applyRule(Singer singer);
}
//SingerServiceImpl.java
package com.apress.prospring5.ch14;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
@Service("singerService")
public class SingerServiceImpl implements SingerService {
@Autowired
ApplicationContext ctx;
@Autowired
private RuleFactory ruleFactory;
@Autowired
private RuleEngine ruleEngine;
public void applyRule(Singer singer) {
Rule ageCategoryRule = ruleFactory.getAgeCategoryRule();
ruleEngine.run(ageCategoryRule, singer);
}
}
如您所见,所需的 Spring beans 被自动连接到服务实现类中。在applyRule()方法中,从规则工厂获取规则,然后应用到Singer对象。结果是Singer的ageCategory属性将基于规则定义的条件、动作和参数而派生出来。
将规则工厂实现为 Spring 可刷新 Bean
现在我们可以实现规则工厂和年龄分类规则。我们希望能够动态地更新规则,并让 Spring 检查它的变化,并选择它来应用最新的逻辑。Spring 框架为用脚本语言编写的 Spring beans 提供了极好的支持,称为可刷新 bean。我们将看到如何将 Groovy 脚本配置为 Spring bean,并指示 Spring 在以后定期刷新 bean。首先让我们看看 Groovy 中规则工厂的实现。为了允许动态刷新,我们将该类放在一个外部文件夹中,以便可以对其进行修改。我们将这个文件夹称为rules。RuleFactoryImpl类(这是一个 Groovy 类,名为RuleFactoryImpl.groovy)将被放置到这个文件夹中。下面的代码片段显示了类的内容:
package com.apress.prospring5.ch14
import org.joda.time.DateTime
import org.joda.time.Years
import org.springframework.stereotype.Component;
@Component
class RuleFactoryImpl implements RuleFactory {
Closure age = { birthDate -> return
Years.yearsBetween(birthDate, new DateTime()).getYears() }
Rule getAgeCategoryRule() {
Rule rule = new Rule()
rule.singlehit=true
rule.conditions=[ {object, param -> age(object.birthDate) >= param},
{object, param -> age(object.birthDate) <= param}]
rule.actions=[{object, param -> object.ageCategory = param}]
rule.parameters=[
[0,10,'Kid'],
[11,20,'Youth'],
[21,40,'Adult'],
[41,60,'Matured'],
[61,80,'Middle-aged'],
[81,120,'Old']
]
return rule
}
}
该类实现了RuleFactory接口,并且实现了getAgeCategoryRule()方法来提供规则。在规则中,定义了一个名为age的Closure,根据Singer对象的birthDate属性(属于 JodaTime 的DateTime类型)计算年龄。
在该规则中,定义了两个条件。第一个是检查歌手的年龄是否大于或等于所提供的参数值,而第二个检查是针对小于或等于条件。
然后,定义一个动作,将参数中提供的值赋给Singer对象的ageCategory属性。
这些参数定义了条件检查和操作的值。例如,在第一个参数中,意味着当年龄在 0 到 10 之间时,那么值Kid将被赋给Singer对象的ageCategory属性,以此类推。因此,对于每个参数,两个条件将使用前两个值来检查年龄范围,而最后一个值将用于分配ageCategory属性。
下一步是定义 SpringApplicationContext。以下配置片段显示了配置文件(app-context.xml):
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:lang="http://www.springframework.org/schema/lang"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/lang
http://www.springframework.org/schema/lang/spring-lang.xsd">
<context:component-scan base-package="com.apress.prospring5.ch14" />
<lang:groovy id="ruleFactory" refresh-check-delay="5000"
script-source="file:rules/RuleFactoryImpl.groovy"/>
</beans>
配置简单。为了用脚本语言定义 Spring beans,我们需要使用lang-namespace。然后,<lang:groovy>标签用于通过 Groovy 脚本声明一个 Spring bean。script-source属性定义了 Spring 将要加载的 Groovy 脚本的位置。对于可刷新的 bean,应该提供属性refresh-check-delay。在本例中,我们提供了 5000 ms 的值,它指示 Spring 在从最后一次调用开始的时间超过 5 秒时检查文件更改。请注意,Spring 不会每五秒钟检查一次文件。相反,它只会在相应的 bean 被调用时检查文件。
测试年龄类别规则
现在我们准备测试规则。测试程序如下所示,它是一个 Java 类:
package com.apress.prospring5.ch14;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.support.GenericXmlApplicationContext;
public class RuleEngineDemo {
private static Logger logger =
LoggerFactory.getLogger(RuleEngineTest.class);
public static void main(String... args) throws Exception {
GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
ctx.load("classpath:spring/app-context.xml");
ctx.refresh();
SingerService singerService =
ctx.getBean("singerService", SingerService.class);
Singer singer = new Singer();
singer.setId(1l);
singer.setFirstName("John");
singer.setLastName("Mayer");
singer.setBirthDate(
new DateTime(1977, 10, 16, 0, 0, 0, 0));
singerService.applyRule(singer);
logger.info("Singer: " + singer);
System.in.read();
singerService.applyRule(singer);
logger.info("Singer: " + singer);
ctx.close();
}
}
在初始化 Spring 的GenericXmlApplicationContext时,会构造一个Singer对象的实例。然后,获取SingerService接口的实例,将规则应用到Singer对象上,然后将结果输出到控制台。在第二次应用规则之前,程序将暂停以等待用户输入。在暂停期间,我们可以修改RuleFactoryImpl.groovy类,这样 Spring 将刷新 bean,我们可以看到变化的规则在起作用。
运行测试程序会产生以下输出:
00:34:24.814 [main] INFO c.a.p.c.RuleEngineImpl - Executing rule
00:34:24.822 [main] INFO c.a.p.c.RuleEngineImpl - Condition Param index: 0
00:34:24.851 [main] INFO c.a.p.c.RuleEngineImpl - Condition success: true
00:34:24.858 [main] INFO c.a.p.c.RuleEngineImpl - Condition Param index: 1
00:34:24.858 [main] INFO c.a.p.c.RuleEngineImpl - Condition success: false
00:34:24.858 [main] INFO c.a.p.c.RuleEngineImpl - Condition Param index: 0
00:34:24.858 [main] INFO c.a.p.c.RuleEngineImpl - Condition success: true
00:34:24.858 [main] INFO c.a.p.c.RuleEngineImpl - Condition Param index: 1
00:34:24.858 [main] INFO c.a.p.c.RuleEngineImpl - Condition success: false
00:34:24.859 [main] INFO c.a.p.c.RuleEngineImpl - Condition Param index: 0
00:34:24.859 [main] INFO c.a.p.c.RuleEngineImpl - Condition success: true
00:34:24.859 [main] INFO c.a.p.c.RuleEngineImpl - Condition Param index: 1
00:34:24.859 [main] INFO c.a.p.c.RuleEngineImpl - Condition success: true
00:34:24.860 [main] INFO c.a.p.c.RuleEngineImpl - Action Param index: 2
00:34:24.870 [main] INFO c.a.p.c.RuleEngineDemo - Singer: Singer - Id: 1,
First name: John, Last name: Mayer, Birthday: 1977-10-16T00:00:00.000+03:00,
Age category: Adult
从输出中的 logging 语句可以看出,由于歌手的年龄是 39 岁,所以规则会在第三个参数中找到一个匹配(换句话说,[21,40,'Adult'])。结果,ageCategory被设置为Adult。
现在程序暂停了,所以让我们改变RuleFactoryImpl.groovy类中的参数。您可以在下面的代码片段中看到这一修改:
rule.parameters=[
[0,10,'Kid'],
[11,20,'Youth'],
[21,30,'Adult'],
[31,60,'Middle-aged'],
[61,120,'Old']
]
按照指示更改并保存文件。现在,在控制台区域中按 Enter 键,触发规则对同一对象的第二次应用。程序继续运行后,会产生以下输出:
00:48:50.137 [main] INFO c.a.p.c.RuleEngineImpl - Executing rule
00:48:50.137 [main] INFO c.a.p.c.RuleEngineImpl - Condition Param index: 0
00:48:50.137 [main] INFO c.a.p.c.RuleEngineImpl - Condition success: true
00:48:50.138 [main] INFO c.a.p.c.RuleEngineImpl - Condition Param index: 1
00:48:50.138 [main] INFO c.a.p.c.RuleEngineImpl - Condition success: false
00:48:50.138 [main] INFO c.a.p.c.RuleEngineImpl - Condition Param index: 0
00:48:50.138 [main] INFO c.a.p.c.RuleEngineImpl - Condition success: true
00:48:50.138 [main] INFO c.a.p.c.RuleEngineImpl - Condition Param index: 1
00:48:50.138 [main] INFO c.a.p.c.RuleEngineImpl - Condition success: false
00:48:50.138 [main] INFO c.a.p.c.RuleEngineImpl - Condition Param index: 0
00:48:50.139 [main] INFO c.a.p.c.RuleEngineImpl - Condition success: true
00:48:50.139 [main] INFO c.a.p.c.RuleEngineImpl - Condition Param index: 1
00:48:50.139 [main] INFO c.a.p.c.RuleEngineImpl - Condition success: false
00:48:50.139 [main] INFO c.a.p.c.RuleEngineImpl - Condition Param index: 0
00:48:50.139 [main] INFO c.a.p.c.RuleEngineImpl - Condition success: true
00:48:50.139 [main] INFO c.a.p.c.RuleEngineImpl - Condition Param index: 1
00:48:50.139 [main] INFO c.a.p.c.RuleEngineImpl - Condition success: true
00:48:50.139 [main] INFO c.a.p.c.RuleEngineImpl - Action Param index: 2
00:48:50.139 [main] INFO c.a.p.c.RuleEngineDemo - Singer: Singer - Id: 1,
First name: John, Last name: Mayer, Birthday: 1977-10-16T00:00:00.000+03:00,
Age category: Middle-aged
在前面的输出中,您可以看到规则执行在第四个参数(换句话说,[31,60,'Middleaged'])处停止,结果,值Middle-aged被赋给了ageCategory属性。
如果您看一下我们准备这个示例( http://pleus.net/articles/grules/grules.pdf )时参考的那篇文章,它还展示了如何将规则参数外部化到 Microsoft Excel 文件中,以便用户可以自己准备和更新参数文件。
当然,这条规则很简单,但它展示了像 Groovy 这样的脚本语言如何在特定领域帮助补充基于 Spring 的 Java EE 应用,例如,使用 DSL 的规则引擎。
您可能会问,“有没有可能更进一步,将规则存储到数据库中,然后让 Spring 的可刷新 bean 特性检测数据库的变化?”通过为用户(或管理员)提供一个前端来动态地将规则更新到数据库中,而不是上传文件,这有助于进一步简化规则的维护。
实际上,Spring 框架中有一个 JIRA 问题讨论了这一点( https://jira.springsource.org/browse/SPR-5106 )。敬请关注此功能。同时,提供一个用户前端来上传规则类也是一个可行的解决方案。当然,在这种情况下应该非常小心,在将规则上传到生产环境之前应该对其进行彻底的测试。
内嵌动态语言代码
不仅可以从外部源文件执行动态语言代码,还可以将这些代码直接内联到 bean 配置中。虽然这种做法在某些场景中可能是有用的,比如概念的快速验证等等,但是从可维护性的角度来看,使用这种方法构建整个应用并不是一种好的做法。以前面的Rule引擎为例,让我们删除文件RuleEngineImpl.groovy并将代码移动到内联 bean 定义中(在文件app-context.xml中),如下面的代码片段所示:
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:lang="http://www.springframework.org/schema/lang"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/lang
http://www.springframework.org/schema/lang/spring-lang.xsd">
<context:component-scan base-package="com.apress.prospring5.ch14"/>
<lang:groovy id="ruleFactory" refresh-check-delay="5000">
<lang:inline-script>
<![CDATA[
package com.apress.prospring5.ch14
import org.joda.time.DateTime
import org.joda.time.Years
import org.springframework.stereotype.Component;
@Component
class RuleFactoryImpl implements RuleFactory {
Closure age = { birthDate -> return
Years.yearsBetween(birthDate, new DateTime()).getYears() }
Rule getAgeCategoryRule() {
Rule rule = new Rule()
rule.singlehit = true
rule.conditions = [{ object, param -> age(object.birthDate) >= param },
{ object, param -> age(object.birthDate) <= param }]
rule.actions = [{ object, param -> object.ageCategory = param }]
rule.parameters = [
[0, 10, 'Kid'],
[11, 20, 'Youth'],
[21, 40, 'Adult'],
[41, 60, 'Matured'],
[61, 80, 'Middle-aged'],
[81, 120, 'Old']
]
return rule
}
}
]]>
</lang:inline-script>
</lang:groovy>
</beans>
正如您所看到的,我们添加了 ID 为ruleFactory的lang: groovy标签来表示 bean 名称。然后我们使用lang:inline-script标签封装来自RuleFactoryImpl.groovy的 Groovy 代码。Groovy 代码周围是一个CDATA标记,以避免代码被 XML 解析器解析。现在准备好了,继续运行规则引擎示例。正如您所看到的,它的工作方式是一样的,只是我们将 Groovy 代码直接内联到 bean 定义中,而不是让它驻留在外部文件中。使用来自RuleFactoryImpl.groovy的代码也是为了显示当内联大量代码时应用会变得多么笨拙。
摘要
在这一章中,我们介绍了如何在 Java 应用中使用脚本语言,并演示了 Spring 框架对脚本语言的支持如何帮助为应用提供动态行为。
首先我们讨论了 JSR-223(Java 平台脚本),它内置于 Java 6 中,支持开箱即用的 JavaScript 执行。然后,我们介绍了 Groovy,这是 Java 开发人员社区中一种流行的脚本语言。我们还展示了与传统 Java 语言相比,它的一些主要特性。
最后,我们讨论了 Spring 框架中对脚本语言的支持。通过使用 Groovy 的 DSL 支持设计和实现一个简单的规则引擎,我们看到了它的作用。我们还讨论了如何修改规则,并让 Spring 框架通过使用其可刷新 bean 特性自动获取更改,而无需编译、打包和部署应用。此外,我们展示了如何将 Groovy 代码直接内联到配置文件中,以定义 bean 的实现代码。
十五、应用监控
典型的 JEE 应用包含许多层和组件,例如表示层、服务层、持久层和后端数据源。在开发阶段,或者在应用被部署到质量保证(QA)或生产环境之后,我们将希望确保应用处于健康状态,没有任何潜在的问题或瓶颈。
在 Java 应用中,各种区域可能会导致性能问题或服务器资源过载(如 CPU、内存或 I/O)。例如效率低下的 Java 代码、内存泄漏(例如,Java 代码在不释放引用的情况下不断分配新对象,并阻止底层 JVM 在垃圾收集过程中释放内存)、错误计算的 JVM 参数、错误计算的线程池参数、过于宽松的数据源配置(例如,允许太多的并发数据库连接)、不正确的数据库设置以及长时间运行的 SQL 查询。
因此,我们需要了解应用的运行时行为,并确定是否存在任何潜在的瓶颈或问题。在 Java 世界中,许多工具可以帮助监控 JEE 应用的详细运行时行为。大多数都是建立在 Java 管理扩展(JMX)技术之上的。
在这一章中,我们将介绍监控基于 Spring 的 JEE 应用的常用技术。具体来说,本章涵盖以下主题:
- JMX 的 Spring 支持:我们讨论 Spring 对 JMX 的全面支持,并演示如何使用 JMX 工具公开 Spring beans 进行监控。在本章中,我们将展示如何使用
jvisualvmJava 可执行文件(https://visualvm.github.io/?Java_VisualVM)作为应用监控工具。 - 监控 Hibernate 统计数据:Hibernate 和许多其他包提供了支持类和基础设施,用于使用 JMX 公开操作状态和性能指标。我们展示了如何在 Spring 支持的 JEE 应用中启用这些常用组件的 JMX 监控。
- Spring Boot JMX 支持:Spring Boot 为 JMX 支持提供了一个启动库,它带有开箱即用的完整默认配置。
请记住,这一章的目的不是介绍 JMX,假设你对 JMX 有基本的了解。有关详细信息,请参考甲骨文在线资源 http://oracle.com/technetwork/java/javase/tech/javamanagement-140525.html 。
JMX 支持 Spring
在 JMX,为 JMX 监控和管理而公开的类被称为受管 beans(通常称为 MBeans)。Spring 框架支持几种公开 MBeans 的机制。本章着重于将 Spring beans(作为简单的 POJOs 开发的)作为 MBeans 来进行 JMX 监控。
在下面几节中,我们将讨论将包含应用相关统计信息的 bean 公开为 MBean 以进行 JMX 监控的过程。主题包括实现 Spring bean、在 Spring ApplicationContext中将 Spring bean 公开为 MBean,以及使用 VisualVM 来监控 MBean。
向 JMX 出口春豆
例如,我们将使用第十二章中的剩余样本。查看示例应用代码的章节,或者直接跳到本书的源代码附录,它提供了我们将用来构建的源代码。添加了 JMX 之后,您希望公开数据库中歌手的数量,以便进行 JMX 监控。因此,让我们实现接口和类,如下所示:
//AppStatistics.java
package com.apress.prospring5.ch15;
public interface AppStatistics {
int getTotalSingerCount();
}
//AppStatisticsImpl.java
package com.apress.prospring5.ch15;
import com.apress.prospring5.ch12.services.SingerService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
public class AppStatisticsImpl implements AppStatistics {
@Autowired
private SingerService singerService;
@Override
public int getTotalSingerCount() {
return singerService.findAll().size();
}
}
在这个例子中,定义了一个方法来检索数据库中歌手记录的总数。为了将 Spring bean 公开为 JMX,我们需要在 Spring 的ApplicationContext中添加配置。这个 Spring Security 的 web 应用的配置在第十二章中有介绍。现在我们必须添加两个类型为MBeanServer和MBeanExporter的基础设施 bean 来支持 JMX 管理 bean。
package com.apress.prospring5.ch15.init;
...
import javax.management.MBeanServer;
import org.springframework.jmx.export.MBeanExporter;
import org.springframework.jmx.support.MBeanServerFactoryBean;
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {"com.apress.prospring5.ch15"})
public class WebConfig implements WebMvcConfigurer {
//other Web infrastructure specific beans
...
@Bean AppStatistics appStatisticsBean() {
return new AppStatisticsImpl();
}
@Bean
MBeanExporter jmxExporter() {
MBeanExporter exporter = new MBeanExporter();
Map<String, Object> beans = new HashMap<>();
beans.put("bean:name=ProSpring5SingerApp", appStatisticsBean());
exporter.setBeans(beans);
return exporter;
}
}
首先,您为我们想要公开的带有统计信息的 POJO 声明 bean。其次,用实现类MBeanExporter声明jmxExporter bean。
MBeanExporter类是 Spring 框架支持 JMX 的核心类。它负责向 JMX MBean 服务器(实现 JDK 的javax.management.MBeanServer接口的服务器,该接口存在于最常用的 web 和 JEE 容器中,如 Tomcat 和 WebSphere)注册 Spring beans。当将 Spring bean 公开为 MBean 时,Spring 将尝试在服务器中定位一个正在运行的MBeanServer实例,并向它注册 MBean。例如,使用 Tomcat,会自动创建一个MBeanServer实例,因此不需要额外的配置。
在jmxExporter bean 中,属性beans定义了我们想要公开的 Spring beans。这是一个Map,可以在这里指定任意数量的 MBeans。在这种情况下,我们希望公开appStatisticsBean bean,它包含我们希望向管理员显示的 singer 应用的信息。对于 MBean 定义,该键将被用作相应条目值所引用的 Spring bean 的ObjectName值(JDK 的javax.management.ObjectName类)。在之前的配置中,appStatisticsBean会暴露在ObjectName bean:name=Prospring5SingerApp下。默认情况下,bean 的所有公共属性都作为属性公开,所有公共方法都作为操作公开。
现在可以通过 JMX 来监控 MBean 了。让我们继续设置 VisualVM,并使用它的 JMX 客户端进行监控。
使用 Java VisualVM 进行 JMX 监控
VisualVM 是一个有用的工具,它可以在各个方面帮助监控 Java 应用。这是一个免费工具,位于 JDK 安装文件夹的bin文件夹下。您也可以从项目网站下载独立版本。 1 本章我们将使用 JDK 安装版本。
VisualVM 使用一个插件系统来支持各种监控功能。为了支持监控 Java 应用的 mbean,我们需要安装 mbean 插件。要安装插件,请按照下列步骤操作:
- 从 VisualVM 的菜单中,选择工具➤插件。
- 单击可用插件选项卡。
- 单击“检查最新”按钮。
- 选择插件 visual VM-mbean,然后单击 Install 按钮。
图 15-1 描述了插件对话框。完成安装后,验证 Tomcat 已经启动,并且示例应用正在运行。然后在 VisualVM 的左侧 Applications 视图中,您应该能够看到 Tomcat 进程正在运行。

图 15-1。
Overview of global transactions with JTA
默认情况下,VisualVM 会扫描在 JDK 平台上运行的 Java 应用。双击所需的节点将显示监控屏幕。
安装 VisualVM-MBeans 插件后,您将能够看到 MBeans 选项卡。单击此选项卡会显示可用的 MBeans。您应该会看到名为bean的节点。当您展开它时,它将显示暴露的Prospring5SingerApp MBean。
在右侧,您将看到我们在 bean 中实现的方法,带有属性TotalSingerCount(由 bean 中的getTotalSingerCount()方法自动派生)。值为 3,对应于应用启动时我们在数据库中添加的记录数。在常规应用中,这个数字会根据应用运行时添加的歌手数量而变化。
图 15-2 描述了暴露了Prospring5SingerApp MBean 的 MBean 窗口。

图 15-2。
The Prospring5SingerApp MBean exposed in VisualVM
监控休眠统计信息
Hibernate 还支持维护和向 JMX 公开与持久性相关的指标。要实现这一点,在 JPA 配置中,再添加三个 Hibernate 属性,如下所示:
package com.apress.prospring5.ch12.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.util.Properties;
@Configuration
//using components that were introduced in Chapter 12 project
@EnableJpaRepositories(basePackages = {"com.apress.prospring5.ch12.repos"})
@ComponentScan(basePackages = {"com.apress.prospring5.ch12"} )
public class DataServiceConfig {
...
@Bean
public Properties hibernateProperties() {
Properties hibernateProp = new Properties();
hibernateProp.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
hibernateProp.put("hibernate.hbm2ddl.auto", "create-drop");
hibernateProp.put("hibernate.show_sql", true);
hibernateProp.put("hibernate.max_fetch_depth", 3);
hibernateProp.put("hibernate.jdbc.batch_size", 10);
hibernateProp.put("hibernate.jdbc.fetch_size", 50);
hibernateProp.put("hibernate.jmx.enabled", true);
hibernateProp.put("hibernate.generate_statistics", true);
hibernateProp.put("hibernate.session_factory_name", "sessionFactory");
return hibernateProp;
}
...
}
属性hibernate.jmx.enabled用于启用休眠 JMX 行为。
属性hibernate.generate_statistics指示 Hibernate 为其 JPA 持久性提供者生成统计信息,而属性hibernate.session_factory_name定义 Hibernate 统计信息MBean所需的会话工厂的名称。
最后,我们需要将 MBean 添加到 Spring 的MBeanExporter配置中。下面的配置片段显示了我们之前在WebConfig类中创建的更新后的MBean配置。CustomStatistics类是org.hibernate.jmx.StatisticsService的替代品,后者不再是 Hibernate 5 的一部分。 2
package com.apress.prospring5.ch15.init;
...
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {"com.apress.prospring5.ch15"})
public class WebConfig implements WebMvcConfigurer {
...
// JMX beans
@Bean AppStatistics appStatisticsBean() {
return new AppStatisticsImpl();
}
@Bean CustomStatistics statisticsBean(){
return new CustomStatistics();
}
@Autowired
private EntityManagerFactory entityManagerFactory;
@Bean SessionFactory sessionFactory(){
return entityManagerFactory.unwrap(SessionFactory.class);
}
@Bean
MBeanExporter jmxExporter() {
MBeanExporter exporter = new MBeanExporter();
Map<String, Object> beans = new HashMap<>();
beans.put("bean:name=ProSpring5SingerApp", appStatisticsBean());
beans.put("bean:name=Prospring5SingerApp-hibernate", statisticsBean());
exporter.setBeans(beans);
return exporter;
}
}
声明了statisticsBean()方法,将 Hibernate 的org.hibernate.stat.Statistics实现作为核心组件。这就是 Hibernate 支持向 JMX 公开统计数据的方式。
现在休眠统计信息已经启用,并且可以通过 JMX 获得。重新加载应用并刷新 VisualVM 您将能够看到 Hibernate statistics MBean。单击该节点会在右侧显示详细的统计信息。注意,对于不属于 Java 原语类型的信息(例如一个List),您可以单击字段来展开它并显示内容。
在 VisualVM 中,您可以看到许多其他指标,例如EntityNames、SessionOpenCount、SecondCloseCount和QueryExecutionMaxTime。这些数字有助于您理解应用中的持久性行为,并可以帮助您进行故障排除和性能调优练习。
JMX 和 Spring Boot
将以前的应用迁移到 Spring Boot 很容易,并且会提供并自动配置依赖关系。对于 JMX,不需要 starter 依赖项,但是可以添加spring-boot-starter-actuator.jar作为依赖项;如果使用特定于 Spring 的插件,在智能编辑器中监控 Spring 应用并显示应用中的 beans、健康和映射可能会有所帮助。
这个应用将是一个没有接口的 web 应用(因为这是下一章的主题,第十六章),有一个内存数据库和一个使用 Atomikos 的成熟的 JTA 配置。由于在前面的章节中已经介绍了这个实现,所以这里的重点将放在 MBeans 上。
让我们通过使用@ManagedResource向 JMX 服务器注册这个类的实例来升级AppStatisticsImpl类。这很实用,因为默认情况下,Spring Boot 会创建一个 bean ID 为mbeanServer的MBeanServer,并公开任何用 Spring JMX 注释(@ManagedResource、@ManagedAttribute、@ManagedOperation)注释的 bean。这里显示了使用所有前面提到的注释的AppStatisticsImpl的升级版本:
package com.apress.prospring5.ch15;
import com.apress.prospring5.ch15.entities.Singer;
import com.apress.prospring5.ch15.services.SingerService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
@ManagedResource(description = "JMX managed resource",
objectName = "jmxDemo:name=ProSpring5SingerApp")
public class AppStatisticsImpl implements AppStatistics {
@Autowired
private SingerService singerService;
@ManagedAttribute(description = "Number of singers in the application")
@Override
public int getTotalSingerCount() {
return singerService.findAll().size();
}
@ManagedOperation
public String findJohn() {
List<Singer> singers = singerService.
findByFirstNameAndLastName("John", "Mayer");
if (!singers.isEmpty()) {
return singers.get(0).getFirstName() + " "
+ singers.get(0).getLastName();
}
return "not found";
}
}
默认情况下,Spring Boot 会将管理端点公开为org.springframework.boot域下的 JMX mbean。在前面描述的代码片段中,@ManagedResource注释有一个名为objectName的属性,它的值代表 MBean 的域名和名称。因为我们想在 VisualVM 中容易地找到显式创建的受管 beans that 为内部监控提供了自己的自动配置 MBeans),所以我们使用了域jmxDemo。
@ManagedAttribute注释用于将给定的 bean 属性公开为 JMX 属性。@ManagedOperation用于将给定的方法公开为 JMX 操作。因为前面显示的两种方法的注释不同,所以它们将显示在 VisualVM 的不同选项卡中。调用getTotalSingerCount的结果将在属性选项卡上可见。在 Operations 选项卡上,这两种方法都将被描述为可点击的按钮,以便当场调用。可以在元数据选项卡上查看每个注释中作为描述提供的字符串。
图 15-3 描述了 MBean 窗口,其中Prospring5SingerApp MBean 暴露在jmxDemo域下。在它下面,你可以看到org.springframework.boot域名。

图 15-3。
The Prospring5SingerApp MBean exposed in VisualVM
不,不是这样的!必须启用对 JMX 的支持。这是通过用@EnableMBeanExport注释配置类来完成的。该注释支持从 Spring 上下文中默认导出所有标准 MBeans,以及所有带@ManagedResource注释的 beans。基本上,这个注释告诉 Spring Boot 创建一个名为mbeanExporter的MBeanExporter bean。此 Spring Boot 应用的配置类如下所示:
package com.apress.prospring5.ch15;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.EnableMBeanExport;
import java.io.IOException;
@EnableMBeanExport
@SpringBootApplication(scanBasePackages = {"com.apress.prospring5.ch15"})
public class JMXBootApplication {
private static Logger logger = LoggerFactory.
getLogger(JMXBootApplication.class);
public static void main(String args) throws IOException {
ConfigurableApplicationContext ctx =
SpringApplication.run(JMXBootApplication.class, args);
assert (ctx != null);
logger.info("Started ...");
System.in.read();
ctx.close();
}
}
现在,我们可以说就是这样,因为这就是在 Spring Boot 应用中使用 JMX 时要说的全部内容!
摘要
在本章中,我们讨论了监控 Spring 驱动的 JEE 应用的高级主题。首先,我们讨论了 Spring 对 JMX 的支持,这是监控 Java 应用的标准。我们讨论了实现自定义 MBeans 来公开与应用相关的信息,以及公开 Hibernate 等常见组件的统计信息。其次,我们展示了如何在 Spring Boot 应用中使用 JMX,以及 Spring 有多么特别。
Footnotes 1
在撰写本文时,Java VisualVM 的当前版本是 1.3.9。可以在这里找到: http://visualvm.java.net/download.html 。
2
如果你想丰富所提供的实现,代码仍然可以在 GitHub 的 https://github.com/manuelbernhardt/hibernate-core/blob/master/hibernate-core/src/main/java/org/hibernate/jmx/StatisticsService.java 获得。
十六、网络应用
在企业应用中,表示层严重影响用户对应用的接受程度。表示层是进入应用的大门。它让用户执行应用提供的业务功能,并呈现由应用维护的信息视图。用户界面的表现在很大程度上决定了应用的成功。
由于互联网的爆炸性增长(尤其是最近),以及人们使用的各种设备的增加,开发应用的表示层是一项具有挑战性的任务。以下是开发 web 应用时的一些主要考虑事项:
- 性能:性能始终是 web 应用的首要要求。如果用户选择一个功能或点击一个链接,并且需要很长时间来执行(在互联网的世界里,三秒钟就像一个世纪!),用户肯定不会对应用满意。
- 用户友好性:应用应该易于使用和导航,有清晰的指示,不会让用户感到困惑。
- 交互性和丰富性:用户界面应该具有高度的交互性和响应性。此外,演示应该在视觉演示方面丰富,比如图表、仪表板类型的界面等等。
- 可访问性:现在,用户要求可以从任何地方通过任何设备访问应用。在办公室,他们将使用桌面来访问应用。在旅途中,用户将使用各种移动设备(包括笔记本电脑、平板电脑和智能手机)来访问应用。
开发一个 web 应用来满足前面的需求并不容易,但是对于业务用户来说,这是必须的。幸运的是,已经开发了许多新技术和框架来满足这些需求。许多 web 应用框架和库,例如 Spring MVC (Spring Web Flow)、Struts、Tapestry、Java Server Faces (JSF)、Google Web Toolkit (GWT)、jQuery 和 Dojo 等等,都提供了工具和丰富的组件库,可以帮助您开发高度交互式的 Web 前端。此外,许多框架提供了针对包括智能手机和平板电脑在内的移动设备的工具或相应的小部件库。HTML5 和 CSS3 标准的兴起以及大多数 web 浏览器和移动设备制造商对这些最新标准的支持,也有助于简化 web 应用的开发,这些应用需要在任何地方、任何设备上都可用。
在 web 应用开发方面,Spring 提供了全面而密集的支持。Spring MVC 模块为 web 应用开发提供了坚实的基础设施和模型视图控制器(MVC)框架。使用 Spring MVC 时,可以使用各种视图技术(例如,JSP 或 Velocity)。此外,Spring MVC 还集成了许多常见的 web 框架和工具包(例如,Struts 和 GWT)。其他 Spring 项目有助于解决 web 应用的特定需求。例如,当与 Spring Web Flow 项目及其 Spring Faces 模块结合使用时,Spring MVC 为开发具有复杂流的 Web 应用以及使用 JSF 作为视图技术提供了全面的支持。简单地说,在表示层开发方面有很多选择。本章主要讨论 Spring MVC,并讨论我们如何使用 Spring MVC 提供的强大功能来开发高性能的 web 应用。具体来说,本章涵盖以下主题:
- Spring MVC:我们讨论 MVC 模式的主要概念,并介绍 Spring MVC。我们展示了 Spring MVC 的核心概念,包括它的
WebApplicationContext层次结构和请求处理生命周期。 - i18n、区域设置和主题化:Spring MVC 为常见的 web 应用需求提供了全面的支持,包括 i18n(国际化)、区域设置和主题化。我们讨论如何使用 Spring MVC 来开发支持这些需求的 web 应用。
- 视图和 Ajax 支持:Spring MVC 支持许多视图技术。在这一章中,我们将重点介绍如何使用 JavaServer Pages (JSP)和 Tiles 作为 web 应用的视图部分。在 JSP 之上,JavaScript 将用于提供丰富性部分。有许多优秀和流行的 JavaScript 库,如 jQuery 和 Dojo。在这一章中,我们将重点介绍 jQuery 及其子项目 jQuery UI 库的使用,它支持高度交互式 web 应用的开发。
- 分页和文件上传支持:在本章展示如何开发示例时,我们将讨论如何使用 Spring Data JPA 和前端 jQuery 组件在浏览基于网格的数据时提供分页支持。此外,我们还将介绍如何在 Spring MVC 中实现文件上传。我们不讨论与 Apache Commons 文件上传的集成,而是讨论如何将 Spring MVC 与 Servlet 3.1 容器的内置多部分文件上传支持结合使用。
- 安全性:安全性是 web 应用中的一个大话题。我们讨论如何使用 Spring Security 来帮助保护应用并处理登录和注销。
为示例实现服务层
在本章的服务层中,我们仍然使用 singer 应用作为示例。在这一节中,我们将讨论数据模型和服务层的实现,这将贯穿本章。
使用样本的数据模型
您将为本章中的示例使用一个简单的数据模型;它只包含一个用于存储歌手信息的SINGER表。以下 SQL 片段显示了模式创建的脚本(schema.sql):
DROP TABLE IF EXISTS SINGER;
CREATE TABLE SINGER (
ID INT NOT NULL AUTO_INCREMENT
, FIRST_NAME VARCHAR(60) NOT NULL
, LAST_NAME VARCHAR(40) NOT NULL
, BIRTH_DATE DATE
, DESCRIPTION VARCHAR(2000)
, PHOTO BLOB
, VERSION INT NOT NULL DEFAULT 0
, UNIQUE UQ_SINGER_1 (FIRST_NAME, LAST_NAME)
, PRIMARY KEY (ID)
);
如您所见,SINGER表只存储了歌手信息的几个基本字段。值得一提的是二进制大对象(BLOB)数据类型的PHOTO列,它将用于存储使用文件上传的歌手照片。要创建该表,您不需要使用此 SQL 脚本;相反,Hibernate 将根据下面的代码片段描述的Singer实体的配置,生成创建表所必需的 SQL:
package com.apress.prospring5.ch16.entities;
import javax.persistence.*;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size; import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
import static javax.persistence.GenerationType.IDENTITY;
@Entity
@Table(name = "singer")
public class Singer implements Serializable {
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "ID")
private Long id;
@Version
@Column(name = "VERSION")
private int version;
@NotEmpty(message="{validation.firstname.NotEmpty.message}")
@Size(min=3, max=60, message="{validation.firstname.Size.message}")
@Column(name = "FIRST_NAME")
private String firstName;
@NotEmpty(message="{validation.lastname.NotEmpty.message}")
@Size(min=1, max=40, message="{validation.lastname.Size.message}")
@Column(name = "LAST_NAME")
private String lastName;
@Temporal(TemporalType.DATE)
@Column(name = "BIRTH_DATE")
private Date birthDate;
@Column(name = "DESCRIPTION")
private String description;
@Basic(fetch= FetchType.LAZY)
@Lob
@Column(name = "PHOTO")
private byte photo;
public Long getId() {
return id;
}
public int getVersion() {
return version;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public void setId(Long id) {
this.id = id;
}
public void setVersion(int version) {
this.version = version;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public void setBirthDate(Date birthDate) {
this.birthDate = birthDate;
}
public Date getBirthDate() {
return birthDate;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public byte getPhoto() {
return photo;
}
public void setPhoto(byte photo) {
this.photo = photo;
}
@Transient
public String getBirthDateString() {
String birthDateString = "";
if (birthDate != null) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
birthDateString = sdf.format(birthDate);
}
return birthDateString;
}
@Override
public String toString() {
return "Singer - Id: " + id + ", First name: " + firstName
+ ", Last name: " + lastName + ", Birthday: " + birthDate
+ ", Description: " + description;
}
}
如果使用填充脚本来填充SINGER表,该脚本将如下所示:
insert into singer first_name, last_name, birth_date values 'John', 'Mayer', '1977-10-16';
insert into singer first_name, last_name, birth_date values 'Eric', 'Clapton', '1945-03-30';
insert into singer first_name, last_name, birth_date values 'John', 'Butler', '1975-04-01';
insert into singer first_name, last_name, birth_date values 'B.B.', 'King', '1925-09-16';
insert into singer first_name, last_name, birth_date values 'Jimi', 'Hendrix', '1942-11-27';
insert into singer first_name, last_name, birth_date values 'Jimmy', 'Page', '1944-01-09';
insert into singer first_name, last_name, birth_date values 'Eddie', 'Van Halen', '1955-01-26';
insert into singer first_name, last_name, birth_date values 'Saul Slash', 'Hudson', '1965-07-23';
insert into singer first_name, last_name, birth_date values 'Stevie', 'Ray Vaughan', '1954-10-03';
insert into singer first_name, last_name, birth_date values 'David', 'Gilmour', '1946-03-06';
insert into singer first_name, last_name, birth_date values 'Kirk', 'Hammett', '1992-11-18';
insert into singer first_name, last_name, birth_date values 'Angus', 'Young', '1955-03-31';
insert into singer first_name, last_name, birth_date values 'Dimebag', 'Darrell', '1966-08-20';
insert into singer first_name, last_name, birth_date values 'Carlos', 'Santana', '1947-07-20';
但是在官方代码示例中,您会发现所有以前的数据都是通过DBInitializer类插入的。这一次,我们需要更多的测试数据,以便稍后向您展示分页支持。
实现 DAO 层
实体类、repo 和数据库配置组成了名为dao的应用层,它负责数据库对象。
entity 类已经在前面介绍过了,您可能已经注意到了典型的 JPA 注释。然而,有两个新的。
- 添加了一个名为
birthDateString的新瞬态属性(通过对 getter 方法应用@Transient注释),该属性将在后面的示例中用于前端呈现。 - 对于
photo属性,我们使用字节数组作为 Java 数据类型,这对应于 RDBMS 中的 BLOB 数据类型。此外,getter 方法用@Lob和@Basic(fetch=FetchType.LAZY)进行了注释。前一个注释向 JPA 提供者表明它是一个大的对象列,而后一个注释表明应该延迟获取属性,以避免在加载不需要照片信息的类时影响性能。
本章后面还会解释一些验证注释。
因为我们将使用 Spring Data JPA 的存储库支持,所以我们将实现SingerRepository接口,如下所示:
package com.apress.prospring5.ch16.repo;
import org.springframework.data.repository.PagingAndSortingRepository;
public interface SingerRepository extends
PagingAndSortingRepository<Singer, Long> {
}
这个例子没有扩展CrudRepository接口,而是使用了PagingAndSortingRepository,它是CrudRepository的高级扩展,提供了使用分页和排序抽象来检索实体的方法。这非常有用,因为查询返回已经排序的数据,这些数据只需要显示在界面上,不需要额外的更改。
实现服务层
在本节中,我们首先讨论使用 JPA 2、Spring Data JPA 和 Hibernate 作为持久性服务提供者来实现SingerService。然后,我们将介绍 Spring 项目中服务层的配置。下面的代码片段显示了带有我们想要公开的服务的SingerService接口:
package com.apress.prospring5.ch16.services;
import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
public interface SingerService {
List<Singer> findAll();
Singer findById(Long id);
Singer save(Singer singer);
Page<Singer> findAllByPage(Pageable pageable);
}
这些方法应该是不言自明的。这个接口的实现也很简单。因为应用很简单,不需要对数据做其他的修改,所以SingerServiceImpl的作用只是将调用转发给类似的存储库方法。
package com.apress.prospring5.ch16.services;
import java.util.List;
import com.apress.prospring5.ch16.repos.SingerRepository;
import com.apress.prospring5.ch16.entitites.Singer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.google.common.collect.Lists;
@Transactional
@Service("singerService")
public class SingerServiceImpl implements SingerService {
private SingerRepository singerRepository;
@Override
@Transactional(readOnly=true)
public List<Singer> findAll() {
return Lists.newArrayList(singerRepository.findAll());
}
@Override
@Transactional(readOnly=true)
public Singer findById(Long id) {
return singerRepository.findById(id).get();
}
@Override
public Singer save(Singer singer) {
return singerRepository.save(singer);
}
@Autowired
public void setSingerRepository(SingerRepository singerRepository) {
this.singerRepository = singerRepository;
}
@Override
@Transactional(readOnly=true)
public Page<Singer> findAllByPage(Pageable pageable) {
return singerRepository.findAll(pageable);
}
}
实现基本完成,下一步是在 web 项目内配置 Spring 的ApplicationContext中的服务,这将在下一节讨论。
正在配置 SingerService
显然,有两种方法可以做到这一点:XML 和 Java 配置。您可以在本章的源代码中找到这个项目用 XML 配置的版本。如果您有兴趣,可以对其进行分析,但是本节的重点将放在 Java 配置类上。要配置SingerService、数据库访问和事务,您可以使用下面的类(您应该已经从第九章中熟悉了):
package com.apress.prospring5.ch16.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.util.Properties;
@Configuration
@EnableJpaRepositories(basePackages = {"com.apress.prospring5.ch16.repos"})
@ComponentScan(basePackages = {"com.apress.prospring5.ch16"} )
public class DataServiceConfig {
private static Logger logger = LoggerFactory.getLogger(DataServiceConfig.class);
@Bean
public DataSource dataSource() {
try {
EmbeddedDatabaseBuilder dbBuilder = new EmbeddedDatabaseBuilder();
return dbBuilder.setType(EmbeddedDatabaseType.H2).build();
} catch (Exception e) {
logger.error("Embedded DataSource bean cannot be created!", e);
return null;
}
}
@Bean
public Properties hibernateProperties() {
Properties hibernateProp = new Properties();
hibernateProp.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
hibernateProp.put("hibernate.hbm2ddl.auto", "create-drop");
hibernateProp.put("hibernate.show_sql", true);
hibernateProp.put("hibernate.max_fetch_depth", 3);
hibernateProp.put("hibernate.jdbc.batch_size", 10);
hibernateProp.put("hibernate.jdbc.fetch_size", 50);
return hibernateProp;
}
@Bean
public PlatformTransactionManager transactionManager() {
return new JpaTransactionManager(entityManagerFactory());
}
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
return new HibernateJpaVendorAdapter();
}
@Bean
public EntityManagerFactory entityManagerFactory() {
LocalContainerEntityManagerFactoryBean factoryBean =
new LocalContainerEntityManagerFactoryBean();
factoryBean.setPackagesToScan("com.apress.prospring5.ch16.entities");
factoryBean.setDataSource(dataSource());
factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
factoryBean.setJpaProperties(hibernateProperties());
factoryBean.setJpaVendorAdapter(jpaVendorAdapter());
factoryBean.afterPropertiesSet();
return factoryBean.getNativeEntityManagerFactory();
}
}
现在服务层已经完成,可以公开给远程客户端使用了。
MVC 和 Spring MVC 简介
在继续实现表示层之前,让我们先了解一下 MVC 在 web 应用中作为一种模式的一些主要概念,以及 Spring MVC 如何在这一领域提供全面的支持。
在下面的小节中,我们将逐一介绍这些高级概念。首先,我们简单介绍一下 MVC。其次,我们给出了 Spring MVC 及其WebApplicationContext层次的高级视图。最后,我们讨论 Spring MVC 中的请求生命周期。
MVC 简介
MVC 是实现应用表示层的常用模式。MVC 模式的主要原则是为不同的组件定义一个具有清晰职责的架构。顾名思义,MVC 模式中有三个参与者。
- 模型:模型表示业务数据以及用户上下文中应用的“状态”。例如,在一个电子商务网站中,如果用户在网站上购买商品,模型将包括用户配置文件信息、购物车数据和订单数据。
- 视图:这将数据以期望的格式呈现给用户,支持与用户的交互,并支持客户端验证、i18n、样式等等。
- 控制器:控制器处理用户在前端执行的动作请求,与服务层交互,更新模型,并根据执行结果将用户定向到适当的视图。
由于基于 Ajax 的 web 应用的兴起,MVC 模式得到了增强,以提供响应更快、更丰富的用户体验。例如,当使用 JavaScript 时,视图可以“监听”用户执行的事件或动作,然后向服务器提交一个XMLHttpRequest。在控制器端,不是返回视图,而是返回原始数据(例如,XML 或 JSON 格式),JavaScript 应用使用接收到的数据执行视图的“部分”更新。图 16-1 展示了一个常用的 web 应用模式,它可以被视为传统 MVC 模式的增强。普通视图请求的处理方式如下:
-
请求:向服务器提交一个请求。在服务器端,大多数框架(例如 Spring MVC 或 Struts)都有一个 dispatcher(以 servlet 的形式)来处理请求。
-
调用:调度程序根据 HTTP 请求信息和 web 应用配置将请求调度到适当的控制器。
-
服务调用:控制器与服务层交互。
-
Model is populated: The information obtained from the service layer is used by the controller to populate a model.
![A315511_5_En_16_Fig1_HTML.jpg]()
图 16-1。
The MVC pattern in a typical web application
-
创建视图:基于模型,创建一个视图。
-
响应:控制器向用户返回相应的视图。
此外,在一个视图中,Ajax 调用也会发生。例如,假设用户正在浏览网格中的数据。当用户单击下一页时,将发生以下流程,而不是整页刷新:
- 请求:准备一个
XMLHttpRequest并提交给服务器。调度程序将把请求分派给相应的控制器。 - 响应:控制器与服务层交互,响应数据将被格式化并发送给浏览器。这种情况下不涉及视图。浏览器接收数据并对现有视图进行部分更新。
Spring MVC 简介
在 Spring 框架中,Spring MVC 模块提供了对 MVC 模式的全面支持,并支持简化表示层实现的其他特性(例如,主题化、i18n、验证、类型转换和格式化)。
在接下来的章节中,我们将讨论 Spring MVC 的主要概念。主题包括 Spring MVC 的WebApplicationContext层次结构、典型的请求处理生命周期和配置。
Spring MVC WebApplicationContext 层次结构
在 Spring MVC 中,DispatcherServlet是中央 servlet,它接收请求并将它们分发到适当的控制器。在 Spring MVC 应用中,可以有任意数量的用于各种目的的DispatcherServlet实例(例如,处理用户界面请求和 RESTful-WS 请求),每个DispatcherServlet都有自己的WebApplicationContext配置,它定义了 servlet 级别的特征,例如支持 servlet 的控制器、处理程序映射、视图解析、i18n、主题化、验证以及类型转换和格式化。
在 servlet 级别的WebApplicationContext配置之下,Spring MVC 维护一个根WebApplicationContext,它包括应用级别的配置,比如后端数据源、安全性、服务和持久层配置。根WebApplicationContext将对所有 servlet 级别的WebApplicationContexts可用。
我们来看一个例子。假设我们在一个应用中有两个DispatcherServlet实例。一个 servlet 支持用户界面(称为应用 servlet),另一个 servlet 以 RESTful-WS 的形式向其他应用提供服务(称为 RESTful servlet)。在 Spring MVC 中,我们将为两个DispatcherServlet实例定义根WebApplicationContext实例和WebApplicationContext实例的配置。图 16-2 显示了这个场景中 Spring MVC 将维护的WebApplicationContext层次结构。

图 16-2。
Spring MVC WebApplicationContext hierarchy
Spring MVC 请求生命周期
让我们看看 Spring MVC 是如何处理请求的。图 16-3 显示了在 Spring MVC 中处理请求所涉及的主要组件。主要组件及其用途如下:
-
过滤器:过滤器适用于每个请求。下一节将介绍几种常用的过滤器及其用途。
-
dispatcher servlet:servlet 分析请求并将它们分派给适当的控制器进行处理。 1
-
Common services: The common services will apply to every request to provide supports including i18n, theme, and file upload. Their configuration is defined in the
DispatcherServlet’sWebApplicationContext.![A315511_5_En_16_Fig3_HTML.jpg]()
图 16-3。
Spring MVC request life cycle
-
处理程序映射:这将传入的请求映射到处理程序(Spring MVC 控制器类中的一种方法)。从 Spring 2.5 开始,在大多数情况下不需要配置,因为 Spring MVC 会自动注册一个现成的
HandlerMapping实现,该实现基于 HTTP 路径映射处理程序,这些路径是通过控制器类中的类型或方法级别的@RequestMapping注释表达的。 2 -
处理程序拦截器:在 Spring MVC 中,您可以为处理程序注册拦截器,以实现公共检查或逻辑。例如,处理程序拦截器可以检查以确保只有处理程序可以在办公时间被调用。
-
处理程序异常解析器:在 Spring MVC 中,
HandlerExceptionResolver接口(在包org.springframework.web.servlet中定义)被设计用来处理处理程序在请求处理过程中抛出的意外异常。默认情况下,DispatcherServlet注册DefaultHandlerExceptionResolver类(来自包org.springframework.web.servlet.mvc.support)。这个解析器通过设置特定的响应状态代码来处理某些标准的 Spring MVC 异常。您还可以通过使用@ExceptionHandler注释来注释控制器方法,并作为属性传入异常类型,从而实现您自己的异常处理程序。 -
视图解析器:Spring MVC 的
ViewResolver接口(来自包org.springframework.web.servlet)支持基于控制器返回的逻辑名称的视图解析。有许多实现类支持各种视图解析机制。例如,UrlBasedViewResolver类支持逻辑名称到 URL 的直接解析。ContentNegotiatingViewResolver类支持根据客户端支持的媒体类型(如 XML、PDF 和 JSON)动态解析视图。还有许多实现可以集成不同的视图技术,比如 FreeMarker (FreeMarkerViewResolver)、Velocity (VelocityViewResolver)和 JasperReports(jasperreportsviewrolver)。
这些描述只涵盖了一些常用的处理程序和解析程序。要获得完整的描述,请参考 Spring 框架参考文档及其 Javadoc。
Spring MVC 配置
要在 web 应用中启用 Spring MVC,需要一些初始配置,特别是 web 部署描述符web.xml。从 Spring 3.1 开始,Servlet 3.0 web 容器中支持基于代码的配置。这为 web 部署描述符文件(web.xml)中所需的 XML 配置提供了另一种选择。
要为 web 应用配置 Spring MVC 支持,我们需要在 web 部署描述符中执行以下配置:
- 配置根目录
WebApplicationContext - 配置 Spring MVC 所需的 servlet 过滤器
- 在应用中配置 dispatcher servlets
下面的配置类只用几行代码就完成了所有三种配置:
package com.apress.prospring5.ch16.init;
import com.apress.prospring5.ch16.config.DataServiceConfig;
import com.apress.prospring5.ch16.config.SecurityConfig;
import com.apress.prospring5.ch16.config.WebConfig;
import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.filter.HiddenHttpMethodFilter;
import org.springframework.web.servlet.support.
AbstractAnnotationConfigDispatcherServletInitializer;
import javax.servlet.Filter;
public class WebInitializer extends
AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[]{
SecurityConfig.class, DataServiceConfig.class
};
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[]{
WebConfig.class
};
}
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
@Override
protected Filter[] getServletFilters() {
CharacterEncodingFilter cef = new CharacterEncodingFilter();
cef.setEncoding("UTF-8");
cef.setForceEncoding(true);
return new Filter[]{new HiddenHttpMethodFilter(), cef};
}
}
要使用基于代码的配置,必须开发一个实现org.springframework.web.WebApplicationInitializer接口的类。为了更加实用,在前面的例子中,Spring 类AbstractAnnotationConfigDispatcherServletInitializer,一个WebApplicationInitializer的实现,被扩展了,因为它包含了配置使用基于 Java 的 Spring 配置的 Spring web 应用所需的方法的具体实现。
所有实现了WebApplicationInitializer接口的类都将被org.springframework.web.SpringServletContainerInitializer类(它实现了 Servlet 3.0 的javax.servlet.ServletContainerInitializer接口)自动检测到,它会在任何 Servlet 3.0 容器中自动启动。如前面的示例所示,以下方法被覆盖以插入定制的配置:
getRootConfigClasses():类型为AnnotationConfigWebApplicationContext的根应用上下文将使用该方法返回的配置类创建。getServletConfigClasses():将使用该方法返回的配置类创建一个类型为AnnotationConfigWebApplicationContext的 web 应用上下文。getServletMappings():DispatcherServelt的映射(上下文)由该方法返回的字符串数组指定。getServletFilters():正如方法名所示,这个方法将返回一组javax.servlet.Filter的实现,这些实现将应用于每个请求。
但是等等,如果你看前面提到的例子,没有任何地方提到安全过滤器!这怎么可能?这在第十二章中有所涉及,但是如果你跳过了那一章,这里有一个简单的答案:有一个专门的 Spring 类。
package com.apress.prospring5.ch16.init;
import org.springframework.security.web.context.
AbstractSecurityWebApplicationInitializer;
public class SecurityWebApplicationInitializer
extends AbstractSecurityWebApplicationInitializer {
}
通过提供一个扩展了AbstractSecurityWebApplicationInitializer的空类,你基本上是在告诉 Spring 你想要启用DelegatingFilterProxy,所以springSecurityFilterChain将在任何其他注册的javax.servlet.Filter之前被使用。
使用这种方法,当与 Spring 的基于 Java 代码的配置相结合时,可以实现一个基于 Spring 的 web 应用的纯基于 Java 代码的配置,而不需要在web.xml或其他 Spring XML 配置文件中声明任何 Spring 配置。是的,就是这么简单。
回到过滤器,表 16-1 描述了由getServletFilters()返回的数组中的每个过滤器。
表 16-1。
Commonly Used Spring MVC Servlet Filters
| 过滤器类全名 | 描述 | | --- | --- | | `org.springframework.web.filter.CharacterEncodingFilter` | 此过滤器用于指定请求的字符编码。 | | `org.springframework.web.filter.HiddenHttpMethodFilter` | 该过滤器支持除 GET 和 POST 之外的 HTTP 方法(例如,PUT)。 |
虽然这里不需要(因此在配置中不使用),但有一个过滤器实现应该提到:org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter。这个实现将一个 JPA EntityManager绑定到线程,用于请求的整个处理。它适用于 Open EntityManager in View 模式,允许在 web 视图中进行延迟加载,尽管原始事务已经完成。尽管这很实用,但也很危险,因为多个请求可能会消耗所有数据库允许的打开的连接。此外,如果要加载的数据集很大,应用可能会冻结。这就是为什么开发人员不喜欢使用它,而是通过 Ajax 请求调用特定的处理程序,将数据加载到特定于 web 的视图对象(而不是实体)中。
在 Spring MVC 中创建第一个视图
有了服务层和 Spring MVC 配置,我们可以开始实现我们的第一个视图。在本节中,我们将实现一个简单的视图来显示最初由DBInitializer bean 填充的所有歌手。
如前所述,我们将使用 JSPX 来实现视图。JSPX 是格式良好的 XML 格式的 JSP。与 JSP 相比,JSPX 的主要优势如下:
- JSPX 更严格地将代码从视图层中分离出来。例如,不能将 Java“script lets”放在 JSPX 文档中。
- 工具可能会执行即时验证(对 XML 语法),以便可以更早地发现错误。我们需要用列出的依赖项来配置我们的项目。
我们需要使用下面的配置片段中列出的依赖项来配置我们的项目:
\\pro-spring-15\build.gradle
ext {
//spring libs
springVersion = '5.0.0.RC2'
springSecurityVersion = '5.0.0.M2'
h2Version = '1.4.194'
tilesVersion = '3.0.7'
//persistency libraries
hibernateVersion = '5.2.10.Final'
hibernateJpaVersion = '1.0.0.Final'
hibernateValidatorVersion = '5.4.1.Final'
...
spring = [
webmvc : "org.springframework:spring-webmvc:$springVersion",
data : "org.springframework.data:spring-data-jpa:$springDataVersion",
securityWeb :
"org.springframework.security:spring-security-web:$springSecurityVersion",
securityConfig:
"org.springframework.security:spring-security-config:$springSecurityVersion",
securityTaglibs:
"org.springframework.security:spring-security-taglibs:$springSecurityVersion",
...
]
hibernate = [
validator : "org.hibernate:hibernate-validator:$hibernateValidatorVersion",
em : "org.hibernate:hibernate-entitymanager:$hibernateVersion",
jpaApi :
"org.hibernate.javax.persistence:hibernate-jpa-2.1-api:$hibernateJpaVersion",
...
]
misc = [
validation : "javax.validation:validation-api:$javaxValidationVersion",
castor : "org.codehaus.castor:castor-xml:$castorVersion",
io : "commons-io:commons-io:2.5",
tiles : "org.apache.tiles:tiles-jsp:$tilesVersion",
jstl : "jstl:jstl:1.2",
...
]
db = [
h2 : "com.h2database:h2:$h2Version"
]
}
...
\\chapter-16\build.gradle
dependencies {
// we exclude common transitive dependencies
compile spring.contextSupport {
exclude module: 'spring-context'
exclude module: 'spring-beans'
exclude module: 'spring-core'
}
compile spring.securityTaglibs {
exclude module: 'spring-web'
exclude module: 'spring-context'
exclude module: 'spring-beans'
exclude module: 'spring-core'
}
compile spring.securityConfig {
exclude module: 'spring-security-core'
exclude module: 'spring-context'
exclude module: 'spring-beans'
exclude module: 'spring-core'
}
Compile misc.slf4jJcl, misc.logback, misc.lang3, hibernate.em,
hibernate.validator, misc.guava, db.h2, spring.data,
spring.webmvc, misc.castor, misc.validation, misc.tiles,
misc.jacksonDatabind, misc.servlet, misc.io,misc.jstl,
spring.securityTaglibs
}
现在,您应该已经知道前面配置中列出的大多数库了。本书中首次使用的术语解释如下:
spring-webmvc是支持 MVC 的 Spring MVC 模块。spring-security-web是 Spring web 模块,用于增加对 Spring Security 性的支持。它是包含安全标签定义的spring-security-web的直接依赖项,将在 JSP 页面中使用。spring-security-web包含AbstractSecurityWebApplicationInitializer类和其他相关的 Spring 组件,用于保护 web 应用。- 是 Spring Security 模块,包含用于在 Spring 应用中配置安全性的类。
tiles-jsp是 Apache Tiles 模块,包含 Java 类和标签定义,为 web 应用创建 Java 模板。3
配置 DispatcherServlet
下一步是配置DispatcherServlet。这是通过创建一个配置类来实现的,该配置类定义了 Spring web 应用所需的所有基础设施 beans。接下来描述了基于 Java 配置的类的一个片段,包含最少的信息:
package com.apress.prospring5.ch16.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
...
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {"com.apress.prospring5.ch16"})
public class WebConfig implements WebMvcConfigurer {
//Declare the static resources.
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**").addResourceLocations("/")
.setCachePeriod(31556926);
}
@Bean
InternalResourceViewResolver viewResolver(){
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views"); resolver.setSuffix(".jspx" );
resolver.setRequestContextAttribute("requestContext"); return resolver;
}
// <=> <mvc:view-controller .../>
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("singers/list");
}
// <=> <mvc:default-servlet-handler/>
@Override
public void configureDefaultServletHandling(
DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
}
接口WebMvcConfigurer定义了回调方法,为使用@EnableWebMvc启用的 Spring MVC 定制基于 Java 的配置。尽管在一个 Spring 应用中可以有多个基于 Java 的配置类,但是只允许用@EnableWebMvc对其中一个进行注释。在前面的配置中,您可以看到有几个方法被覆盖来定制配置:
addResourceHandlers()方法添加了处理程序,这些处理程序用于提供静态资源,如 web 应用根目录、类路径等下特定位置的图像、JavaScript 和 CSS 文件。在这个定制的实现中,任何带有包含资源的 URL 的请求都将被一个绕过所有过滤器的特殊处理程序处理。configureDefaultServletHandling(..)方法启用了一个处理静态资源的处理器。addViewControllers(..)方法定义了简单的自动化控制器,这些控制器预先配置了响应状态代码和/或呈现响应主体的视图。这些视图没有控制器逻辑,用于呈现欢迎页面、执行简单的站点 URL 重定向、返回 404 状态等等。在前面描述的配置中,您使用这个方法来执行到singers/list视图的重定向。viewResolver(..)方法声明了一个类型为InternalResourceViewResolver的视图解析器,它将符号视图名称与/WEB-INF/views下的*.jspx模板进行匹配。
实现单控制器
配置好DispatcherServlet的WebApplicationContext之后,下一步是实现控制器类。
package com.apress.prospring5.ch16.web;
...
@RequestMapping("/singers")
@Controller
public class SingerController {
private final Logger logger = LoggerFactory.getLogger(SingerController.class);
private SingerService singerService;
private MessageSource messageSource;
@RequestMapping(method = RequestMethod.GET)
public String list(Model uiModel) {
logger.info("Listing singers");
List<Singer> singers = singerService.findAll();
uiModel.addAttribute("singers", singers);
logger.info("No. of singers: " + singers.size());
return "singers/list";
}
@Autowired
public void setSingerService(SingerService singerService) {
this.singerService = singerService;
}
}
注释@Controller被应用于该类,表明它是一个 Spring MVC 控制器。类级别的@RequestMapping注释表示将由控制器处理的根 URL。在这种情况下,所有带有前缀/singers的 URL 都将被分派到这个控制器。在list()方法上,还应用了@RequestMapping注释,此时该方法被映射到 HTTP GET 方法。这意味着带有 HTTP GET 方法的 URL /singers将由该方法处理。在list()方法中,歌手列表被检索并保存到由 Spring MVC 传递给该方法的Model接口中。最后,返回名为singers/list的逻辑视图。在DispatcherServlet配置中,InternalResourceViewResolver被配置为视图解析器,文件有前缀/WEB-INF/views/和后缀.jspx。因此,Spring MVC 将拾取文件/WEB-INF/views/singers/list.jspx作为视图。
实现歌手列表视图
下一步是实现显示歌手信息的视图页面,也就是文件//src/main/webapp/WEB- INF/views/singers/list.jspx。
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<div xmlns:jsp="http://java.sun.com/JSP/Page"
xmlns:c="http://java.sun.com/jsp/jstl/core"
xmlns:spring="http://www.springframework.org/tags"
xmlns:fmt="http://java.sun.com/jsp/jstl/fmt" version="2.0">
<h1>Singer Listing</h1>
<c:if test="${not empty singers}">
<table>
<thead>
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>Birth Date</th>
</tr>
</thead>
<tbody>
<c:forEach items="${singers}" var="singer">
<tr>
<td>${singer.firstName}</td>
<td>${singer.lastName}</td>
<td><fmt:formatDate value="${singer.birthDate}"/></td>
</tr>
</c:forEach>
</tbody>
</table>
</c:if>
</div>
如果您以前使用 JSP 进行过开发,那么您应该对前面的代码片段很熟悉。但是由于这是一个 JSPX 页面,页面内容被嵌入在<div>标签下。此外,正在使用的标记库被声明为 XML 名称空间。
首先,<jsp:directive.page>标签定义了应用于整个 JSPX 页面的属性,而<jsp:output>标签控制 JSPX 文档输出的属性。
其次,标签<c:if>检测模型属性singers是否为空。因为我们已经在数据库中填充了一些歌手信息,singers属性应该包含数据。因此,<c:forEach>标签将在页面的表格中呈现歌手信息。注意使用了<fmt:formatDate>标签来格式化birthDate属性,它的类型是java.utilDate。
测试歌手列表视图
现在我们准备测试歌手列表视图。首先构建和部署应用;然后,为了测试歌手列表视图,打开网络浏览器并访问 URL http://localhost:8080/singers。您应该可以看到歌手列表页面。
现在我们的第一个视图开始工作了。在接下来的小节中,我们将使用更多的视图来丰富应用,并支持 i18n、主题等。
理解 Spring MVC 项目结构
在深入研究 web 应用各个方面的实现之前,让我们先来看看本章中开发的示例 web 应用的项目结构是什么样子的。
通常,在 web 应用中,需要大量文件来支持各种功能。例如,有许多静态资源文件,如样式表、JavaScript 文件、图像和组件库。然后是支持以各种语言呈现界面的文件。当然,web 容器将解析和呈现视图页面,模板框架(例如 Apache Tiles)将使用布局和定义文件来为应用提供一致的外观。
将用于不同目的的文件存储在一个结构良好的文件夹层次结构中始终是一种好的做法,这样可以让您清楚地了解应用正在使用的各种资源,并简化日常维护工作。
表 16-2 描述了本章将要开发的 web 应用的文件夹结构。请注意,这里给出的结构不是强制性的,而是在 web 应用开发的开发人员社区中常用的。
表 16-2。
Sample Web Project Folder Structure Description
| 文件夹名称 | 目的 | | --- | --- | | `ckeditor` | cke editor([`http://ckeditor.com`](http://ckeditor.com))是一个 JavaScript 组件库,提供输入形式的富文本编辑器。我们将使用它来支持歌手描述的富文本编辑。 | | `jqgrid` | jqGrid ( [`http://trirand.com`](http://trirand.com) )是一个构建在 jQuery 之上的组件,它为数据呈现提供了各种基于网格的组件。我们将使用这个库来实现网格,以便显示歌手,并支持 Ajax 风格的分页。 | | `scripts` | 这是所有普通 JavaScript 文件的文件夹。对于本章中的示例,jQuery ( [`http://jquery.org`](http://jquery.org) )和 jQuery UI([`http://jqueryui.com`](http://jqueryui.com))JavaScript 库将用于实现丰富的用户界面。脚本将放在该文件夹中。内部的 JavaScript 库也应该放在这里。 | | `styles` | 该文件夹存储样式表文件和支持样式的相关图像。 | | `WEB-INF/i18n` | 该文件夹存储支持 i18n 的文件。文件`application*.properties`存储布局相关的文本(例如,页面标题、字段标签和菜单标题)。`message*.properties`文件存储各种消息(例如,成功和错误消息以及验证消息)。该示例将支持英语(美国)和中文(香港)。 | | `WEB-INF/layouts` | 该文件夹存储布局视图和定义。这些文件将被 Apache Tiles ( [`http://tiles.apache.org`](http://tiles.apache.org) )模板框架使用。 | | WEB-INF/views | 该文件夹存储应用将使用的视图(在本例中是 JSP 文件)。 |在接下来的部分中,我们将需要各种文件(例如,CSS 文件、JavaScript 文件和图像)来支持实现。这里不会显示 CSS 和 JavaScript 的源代码。鉴于此,我们建议您下载本章源代码的副本,并将其解压缩到一个临时文件夹中,以便您可以将所需的文件直接复制到项目中。
实现国际化(i18n)
当开发 web 应用时,在早期阶段启用 i18n 总是一个好的实践。主要工作是将用户界面文本和消息外化到属性文件中。
即使您可能在第一天没有 i18n 需求,但是将与语言相关的设置外部化是很好的,这样以后当您需要支持更多语言时会更容易。
使用 Spring MVC,启用 i18n 很简单。首先,将语言相关的用户界面设置外化到/WEB-INF/i18n文件夹中的各种属性文件中,如表 16-2 所述。因为我们将同时支持英语(美国)和中文(香港),您将需要四个文件。application.properties和message.properties文件存储默认语言环境的设置,在本例中是英语(美国)。application_zh_HK.properties和message_zh_HK.properties文件存储中文(HK)语言的设置。
在 DispatcherServlet 配置中配置国际化
语言设置就绪后,下一步是为 i18n 支持配置DispatcherServlet实例的WebApplicationContext。下面的配置片段描述了基于 Java 的配置类WebConfig的 beans 和方法,它们被声明为启用和定制国际化支持。
package com.apress.prospring5.ch16.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.mvc.WebContentInterceptor;
import java.util.Locale;
...
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {"com.apress.prospring5.ch16"})
public class WebConfig implements WebMvcConfigurer {
//Declare our static resources.
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**").addResourceLocations("/")
.setCachePeriod(31556926);
}
// <=> <mvc:default-servlet-handler/>
@Override
public void configureDefaultServletHandling(
DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
@Bean
ReloadableResourceBundleMessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource =
new ReloadableResourceBundleMessageSource();
messageSource.setBasenames(
"WEB-INF/i18n/messages",
"WEB-INF/i18n/application");
messageSource.setDefaultEncoding("UTF-8");
messageSource.setFallbackToSystemLocale(false);
return messageSource;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
...
}
@Bean
LocaleChangeInterceptor localeChangeInterceptor() {
return new LocaleChangeInterceptor();
}
@Bean
CookieLocaleResolver localeResolver() {
CookieLocaleResolver cookieLocaleResolver = new CookieLocaleResolver();
cookieLocaleResolver.setDefaultLocale(Locale.ENGLISH);
cookieLocaleResolver.setCookieMaxAge(3600);
cookieLocaleResolver.setCookieName("locale");
return cookieLocaleResolver;
}
...
}
在前面的配置片段中,修改了资源定义以反映新的文件夹结构,如表 16-2 所示。addResourceHandlers(..)方法定义了静态资源文件的位置,这使得 Spring MVC 能够有效地处理这些文件夹中的文件。在标签中,location属性定义了静态资源的文件夹。资源位置/表示 web 应用的根文件夹/src/main/webapp。资源处理器路径/resources/**定义了映射到静态资源的 URL 例如,对于 URL http://localhost:8080/resources/styles/standard.css,Spring MVC 将从文件夹/src/main/webapp/styles中检索文件standard.css。
configureDefaultServletHandling(..)方法支持将DispatcherServlet映射到 web 应用的根上下文 URL,同时仍然允许容器的默认 servlet 处理静态资源请求。
其次,定义了一个带有类LocaleChangeInterceptor的 Spring MVC 拦截器,它拦截所有对DispatcherServlet的请求。拦截器支持带有可配置请求参数的地区切换。在拦截器配置中,定义了名为lang的 URL 参数,用于更改应用的语言环境。
然后,定义了一个类为ReloadableResourceBundleMessageSource的 bean。ReloadableResourceBundleMessageSource类实现了MessageSource接口,该接口从定义的文件(在本例中是/WEB-INF/i18n文件夹中的messages*.properties和application*.properties文件)中加载消息,以支持 i18n。注意属性fallbackToSystemLocale。该属性指示 Spring MVC 在没有找到客户端区域设置的特殊资源包时,是否退回到运行应用的系统的区域设置。
最后,定义了一个带有CookieLocaleResolver类的 bean。该类支持从用户浏览器的 cookie 中存储和检索区域设置。
为 i18n 支持修改歌手列表视图
现在我们可以更改 JSP 页面来显示 i18n 消息。下面的 JSPX 片段显示了修改后的歌手列表视图: 4
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<div xmlns:jsp="http://java.sun.com/JSP/Page"
xmlns:c="http://java.sun.com/jsp/jstl/core"
xmlns:fmt="http://java.sun.com/jsp/jstl/fmt"
xmlns:spring="http://www.springframework.org/tags"
version="2.0">
<jsp:directive.page contentType="text/html;charset=UTF-8"/>
<jsp:output omit-xml-declaration="yes"/>
<spring:message code="label_singer_list" var="label SingerList"/>
<spring:message code="label_singer_first_name" var="labelSingerFirstName"/>
<spring:message code="label_singer_last_name" var="labelSingerLastName"/>
<spring:message code="label_singer_birth_date" var="labelSingerBirthDate"/>
<h1>${label SingerList}</h1>
<c:if test="${not empty singers}">
<table>
<thead>
<tr>
<th>${labelSingerFirstName}</th>
<th>${labelSingerLastName}</th>
<th>${labelSingerBirthDate}</th>
</tr>
</thead>
<tbody>
<c:forEach items="${singers}" var="singer">
<tr>
<td>${singer.firstName}</td>
<td>${singer.lastName}</td>
<td><fmt:formatDate value="${singer.birthDate}"/></td>
</tr>
</c:forEach>
</tbody>
</table>
</c:if>
</div>
如前面的代码片段所示,首先将 Spring 名称空间添加到页面中。然后,<spring:message>标签用于在相应的变量中加载视图所需的消息。最后,页面标题和标签被更改为使用 i18n 消息。
现在构建并重新部署项目,打开浏览器,指向 URL http://localhost:8080/ singers?lang=zh_HK。您将看到中文(香港)区域设置的页面。
因为我们在DispatcherServlet的WebApplicationContext中定义了localeResolver,所以 Spring MVC 会将区域设置存储在浏览器的 cookie(名为locale)中,默认情况下,cookie 会为用户会话保存。如果您想将 cookie 保存更长时间,在localeResolver bean 定义中,您可以通过调用setCookieMaxAge(...)来覆盖从类org.springframework.web.util.CookieGenerator继承的属性cookieMaxAge。
要切换到英语(美国),您可以将浏览器中的 URL 更改为reflect ?lang=en_US,页面将切换回英语(美国)。虽然我们没有提供名为application_en_US.properties的属性文件,但是 Spring MVC 将回退到使用文件application.properties,该文件以默认语言英语存储属性。
使用主题化和模板化
除了 i18n 之外,web 应用还需要合适的外观(例如,商业网站需要专业的外观,而社交网站需要更生动的风格),以及一致的布局,以便用户在使用 web 应用时不会感到困惑。
此外,为了提供一致的布局,需要一个模板框架。在这一节中,我们将使用 Apache Tiles ( http://tiles.apache.org ),一个流行的页面模板框架,用于视图模板支持。Spring MVC 在这方面与 Apache Tiles 紧密集成。Spring 还支持开箱即用的 Velocity 和 FreeMarker,它们是更通用的模板系统,在 web 应用之外对电子邮件模板等也很有用。
在接下来的章节中,我们将讨论如何在 Spring MVC 中启用主题支持,以及如何使用 Apache Tiles 来定义页面布局。
主题支持
Spring MVC 为主题化提供了全面的支持,在 web 应用中启用它很容易。例如,在本章的示例 singer 应用中,我们想要创建一个主题并将其命名为 standard。首先,在文件夹/src/main/resources中,创建一个名为standard .properties的文件,内容如下:
styleSheet=resources/styles/standard.css
这个属性文件包含一个名为styleSheet的属性,它指向用于标准主题的样式表。这个属性文件是主题的ResourceBundle,您可以为您的主题添加任意多的组件(例如,徽标图像位置和背景图像位置)。
下一步是通过修改配置类来配置DispatcherServlet的WebApplicationContext以支持主题化。首先,在addInterceptors(..)方法中,我们需要再添加一个拦截器 bean,如下所示:
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
registry.addInterceptor(themeChangeInterceptor());
}
@Bean
ThemeChangeInterceptor themeChangeInterceptor() {
return new ThemeChangeInterceptor();
}
添加了新的类型为ThemeChangeInterceptor的拦截器,这个类拦截每一个改变主题的请求。
其次,需要 bean 定义:
@Bean
ResourceBundleThemeSource themeSource() {
return new ResourceBundleThemeSource();
}
@Bean
CookieThemeResolver themeResolver() {
CookieThemeResolver cookieThemeResolver = new CookieThemeResolver();
cookieThemeResolver.setDefaultThemeName("standard");
cookieThemeResolver.setCookieMaxAge(3600);
cookieThemeResolver.setCookieName("theme");
return cookieThemeResolver;
}
这里定义了两个 beans。类型为ResourceBundleThemeSource的第一个 bean 负责加载活动主题的ResourceBundle bean。例如,如果活动主题名为standard,bean 将寻找文件standard.properties作为主题的ResourceBundle bean。第二个 bean 的类型是CookieThemeResolver,用于为用户解析活动主题。属性defaultThemeName定义了要使用的默认主题,也就是标准主题。注意,顾名思义,CookieThemeResolver类使用 cookies 为用户存储主题。还有一个SessionThemeResolver类,它存储用户会话中的主题属性。
现在,标准主题已经配置好,可以在我们的视图中使用了。下面的 JSPX 片段显示了带有主题支持的修改后的歌手列表视图(/WEB-INF/views/singers/list.jspx):
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<div xmlns:jsp="http://java.sun.com/JSP/Page"
xmlns:c="http://java.sun.com/jsp/jstl/core"
xmlns:fmt="http://java.sun.com/jsp/jstl/fmt"
xmlns:spring="http://www.springframework.org/tags"
version="2.0">
<jsp:directive.page contentType="text/html;charset=UTF-8"/>
<jsp:output omit-xml-declaration="yes"/>
<spring:message code="label_singer_list" var="labelSingerList"/>
<spring:message code="label_singer_first_name" var="labelSingerFirstName"/>
<spring:message code="label_singer_last_name" var="labelSingerLastName"/>
<spring:message code="label_singer_birth_date" var="labelSingerBirthDate"/>
<head>
<spring:theme code="styleSheet" var="app_css" />
<spring:url value="/${app_css}" var="app_css_url" />
<link rel="stylesheet" type="text/css" media="screen" href="${app_css_url}" />
</head>
<h1>${labelSingerList}</h1>
<c:if test="${not empty singers}">
<table>
<thead>
<tr>
<th>${labelSingerFirstName}</th>
<th>${labelSingerLastName}</th>
<th>${labelSingerBirthDate}</th>
</tr>
</thead>
<tbody>
<c:forEach items="${singers}" var="singer">
<tr>
<td>${singer.firstName}</td>
<td>${singer.lastName}</td>
<td><fmt:formatDate value="${singer.birthDate}"/></td>
</tr>
</c:forEach>
</tbody>
</table>
</c:if>
</div>
视图中添加了一个<head>部分,使用<spring:theme>标签从主题的ResourceBundle中检索styleSheet属性,?? 是样式表文件standard.css。最后,到样式表的链接被添加到视图中。
在将应用重新构建并重新部署到服务器之后,打开浏览器并再次指向 singer list 视图的 URL(http://localhost:8080/singers),您将会看到应用了在standard.css文件中定义的样式。
使用 Spring MVC 的主题支持,您可以轻松地在应用中添加新主题或更改现有主题。
使用 Apache Tiles 查看模板
对于使用 JSP 技术的视图模板,Apache Tiles ( http://tiles.apache.org )是最流行的框架。Spring MVC 与 Tiles 紧密集成。为了对数据使用切片和验证,添加了tiles-jsp、validation-api和hibernate-validator库作为依赖项。
在下面几节中,我们将讨论如何实现页面模板,包括页面布局设计、定义以及布局中组件的实现。
设计模板布局
首先,我们需要定义应用中所需的模板数量以及每个模板的布局。
在本章的 singer 示例中,我们只需要一个模板。布局相当琐碎,如图 16-4 所示。如您所见,该模板需要以下页面组件:
/WEB-INF/views/header.jspx:该页面提供页眉区。/WEB-INF/views/menu.jspx:该页面提供了左侧菜单区域,以及将在本章稍后实现的登录表单。/WEB-INF/views/footer.jspx:该页面提供页脚区。

图 16-4。
Page template with layout components
我们将使用 Apache Tiles 来定义模板,我们需要开发页面模板文件以及布局定义文件,如下所示:
/WEB-INF/layouts/default.jspx:该页面提供了特定模板的整体页面布局。/WEB-INF/layouts/layouts.xml:该文件存储 Apache Tiles 所需的页面布局定义。
实现页面布局组件
定义好布局后,我们就可以实现页面组件了。首先,我们将开发 Apache Tiles 所需的页面模板文件和布局定义文件。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE tiles-definitions PUBLIC
"-//Apache Software Foundation//DTD Tiles Configuration 2.1//EN"
"http://tiles.apache.org/dtds/tiles-config_3_0.dtd">
<tiles-definitions>
<definition name="default" template="/WEB-INF/layouts/default.jspx">
<put-attribute name="header" value="/WEB-INF/views/header.jspx" />
<put-attribute name="menu" value="/WEB-INF/views/menu.jspx" />
<put-attribute name="footer" value="/WEB-INF/views/footer.jspx" />
</definition>
</tiles-definitions>
该文件应该易于理解。有一个名为default的页面模板定义。模板代码在文件default.jspx中。在该页面中,定义了三个组件,分别命名为header、menu和footer。组件的内容将从由value属性提供的文件中加载。有关图块定义的详细描述,请参考项目文档页面( http://tiles.apache.org/ )。
下面的 JSPX 片段显示了default.jspx模板文件的内容:
<html xmlns:jsp="http://java.sun.com/JSP/Page"
xmlns:c="http://java.sun.com/jsp/jstl/core"
xmlns:fn="http://java.sun.com/jsp/jstl/functions"
xmlns:tiles="http://tiles.apache.org/tags-tiles"
xmlns:spring="http://www.springframework.org/tags">
<jsp:output doctype-root-element="HTML"
doctype-system="about:legacy-compat" />
<jsp:directive.page contentType="text/html;charset=UTF-8" />
<jsp:directive.page pageEncoding="UTF-8" />
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=8" />
<spring:theme code="styleSheet" var="app_css" />
<spring:url value="/${app_css}" var="app_css_url" />
<link rel="stylesheet" type="text/css" media="screen" href="${app_css_url}" />
<!-- Get the user locale from the page context
(it was set by Spring MVC's locale resolver) -->
<c:set var="userLocale">
<c:set var="plocale">${pageContext.response.locale}</c:set>
<c:out value="${fn:replace(plocale, '_', '-')}" default="en" />
</c:set>
<spring:message code="application_name" var="app_name" htmlEscape="false"/>
<title><spring:message code="welcome_h3" arguments="${app_name}" /></title>
</head>
<body class="tundra spring">
<div id="headerWrapper">
<tiles:insertAttribute name="header" ignore="true" />
</div>
<div id="wrapper">
<tiles:insertAttribute name="menu" ignore="true" />
<div id="main">
<tiles:insertAttribute name="body"/>
<tiles:insertAttribute name="footer" ignore="true"/>
</div>
</div>
</body>
</html>
该页面基本上是一个 JSP 页面。亮点如下:
<spring:theme>标签放在模板中,它支持模板级别的主题化。<tiles:insertAttribute>标签用于指示需要从其他文件加载的页面组件,如layouts.xml文件所示。
现在让我们实现header、menu和footer组件。这里显示了内容。header.jspx文件非常简单,只包含以下文本:
<div id="header" xmlns:jsp="http://java.sun.com/JSP/Page"
xmlns:spring="http://www.springframework.org/tags"
version="2.0">
<jsp:directive.page contentType="text/html;charset=UTF-8" />
<jsp:output omit-xml-declaration="yes" />
<spring:message code="header_text" var="headerText"/>
<div id="appname">
<h1>${headerText}</h1>
</div>
</div>
menu.jspx文件也很简单,因为这个应用是用最小化的接口设计的,因为它的主要焦点毕竟在 Spring 上。
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<div id="menu" xmlns:jsp="http://java.sun.com/JSP/Page"
xmlns:spring="http://www.springframework.org/tags"
version="2.0">
<jsp:directive.page contentType="text/html;charset=UTF-8" />
<jsp:output omit-xml-declaration="yes" />
<spring:message code="menu_header_text" var="menuHeaderText"/>
<spring:message code="menu_add_singer" var="menuAddsinger"/>
<spring:url value="/singers?form" var="addsingerUrl"/>
<h3>${menuHeaderText}</h3>
<a href="${addsingerUrl}"><h3>${menuAddsinger}</h3></a>
</div>
footer.jspx文件包含改变界面显示语言的 URL。
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<div id="footer" xmlns:jsp="http://java.sun.com/JSP/Page"
xmlns:spring="http://www.springframework.org/tags" version="2.0">
<jsp:directive.page contentType="text/html;charset=UTF-8" />
<jsp:output omit-xml-declaration="yes" />
<spring:message code="home_text" var="homeText"/>
<spring:message code="label_en_US" var="labelEnUs"/>
<spring:message code="label_zh_HK" var="labelZhHk"/>
<spring:url value="/singers" var="homeUrl"/>
<a href="${homeUrl}">${homeText}</a> |
<a href="${homeUrl}?lang=en_US">${labelEnUs}</a> |
<a href="${homeUrl}?lang=zh_HK">${labelZhHk}</a>
</div>
现在对于歌手列表视图,我们可以修改它以适应模板。基本上,我们只需要删除<head>部分,因为它现在在模板页面中,default.jspx。修改和改进后的版本如下所示:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<div xmlns:jsp="http://java.sun.com/JSP/Page"
xmlns:c="http://java.sun.com/jsp/jstl/core"
xmlns:spring="http://www.springframework.org/tags"
xmlns:fmt="http://java.sun.com/jsp/jstl/fmt" version="2.0">
<jsp:directive.page contentType="text/html;charset=UTF-8"/>
<jsp:output omit-xml-declaration="yes"/>
<spring:message code="label_singer_list" var="labelSingerList"/>
<spring:message code="label_singer_first_name" var="labelSingerFirstName"/>
<spring:message code="label_singer_last_name" var="labelSingerLastName"/>
<spring:message code="label_singer_birth_date" var="labelSingerBirthDate"/>
<h1>${labelSingerList}</h1>
<c:if test="${not empty singers}">
<table>
<thead>
<tr>
<th>${labelSingerFirstName}</th>
<th>${labelSingerLastName}</th>
<th>${labelSingerBirthDate}</th>
</tr>
</thead>
<tbody>
<c:forEach items="${singers}" var="singer">
<tr>
<td>${singer.firstName}</td>
<td>${singer.lastName}</td>
<td><fmt:formatDate value="${singer.birthDate}"/></td>
</tr>
</c:forEach>
</tbody>
</table>
</c:if>
</div>
现在模板、定义和组件都准备好了;下一步是配置 Spring MVC 来集成 Apache Tiles。
在 Spring MVC 中配置图块
在 Spring MVC 中配置 Tiles 支持很简单。在DispatcherServlet配置(类WebConfig)中,我们需要做一个修改,用UrlBasedViewResolver类替换InternalResourceViewResolver。以下代码片段仅包含配置切片支持所需的 beans:
package com.apress.prospring5.ch16.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.view.UrlBasedViewResolver;
import org.springframework.web.servlet.view.tiles3.TilesConfigurer;
import org.springframework.web.servlet.view.tiles3.TilesView;
...
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {"com.apress.prospring5.ch16"})
public class WebConfig implements WebMvcConfigurer {
@Bean
UrlBasedViewResolver tilesViewResolver() {
UrlBasedViewResolver tilesViewResolver = new UrlBasedViewResolver();
tilesViewResolver.setViewClass(TilesView.class);
return tilesViewResolver;
}
@Bean
TilesConfigurer tilesConfigurer() {
TilesConfigurer tilesConfigurer = new TilesConfigurer();
tilesConfigurer.setDefinitions( "/WEB-INF/layouts/layouts.xml",
"/WEB-INF/views/**/views.xml"
);
tilesConfigurer.setCheckRefresh(true);
return tilesConfigurer;
}
...
}
在前面描述的配置片段中,定义了一个类为UrlBasedViewResolver的ViewResolver bean,属性viewClass设置为TilesView类,这是 Spring MVC 对 Tiles 的支持。最后,定义一个tilesConfigurer bean 来提供 Tiles 所需的布局配置。
我们需要准备的最后一个配置文件是/WEB-INF/views/singers/views.xml文件,它定义了示例中 singer 应用的视图。文件内容描述如下:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE tiles-definitions PUBLIC "-//Apache Software
Foundation//DTD Tiles Configuration 3.0//EN"
"http://tiles.apache.org/dtds/tiles-config_3_0.dtd">
<tiles-definitions>
<definition extends="default" name="singers/list">
<put-attribute name="body" value="/WEB-INF/views/singers/list.jspx" />
</definition>
</tiles-definitions>
如前所示,逻辑视图名称被映射到要显示的视图的相应的body属性。与在SingerController类中一样,list()方法返回逻辑视图名singers / list,因此 Tiles 将能够将视图名映射到正确的模板和视图体以进行显示。
我们现在可以测试页面了。确保项目已重新生成并部署到服务器上。再次加载歌手列表视图(http://localhost:8080/singers),将显示基于模板的视图。
实现歌手信息的视图
现在,我们可以继续实现视图,允许用户查看歌手的详细信息,创建新的歌手,或者更新现有的歌手信息。
在下面的小节中,我们将讨论 URL 到各种视图的映射,以及视图是如何实现的。我们还讨论了如何在 Spring MVC 中为编辑视图启用 JSR-349 验证支持。
将 URL 映射到视图
首先,我们需要设计如何将各种 URL 映射到相应的视图。在 Spring MVC 中,最佳实践之一是遵循 RESTful 风格的 URL 来映射视图。表 16-3 显示了 URL 到视图的映射,以及处理动作的控制器方法名。
表 16-3。
Mapping of URLs to Views
| 统一资源定位器 | HTTP 方法 | 控制器方法 | 描述 | | --- | --- | --- | --- | | `/singers` | 得到 | `list()` | 列出歌手的信息。 | | `/singers/id` | 得到 | `show()` | 显示单个歌手的信息。 | | `/singers/id?form` | 得到 | `updateForm()` | 显示用于更新现有歌手的编辑表单。 | | `/singers/id?form` | 邮政 | `update()` | 用户更新歌手信息并提交表单。数据将在这里处理。 | | `/singers?form` | 得到 | `createForm()` | 显示创建新歌手的编辑表单。 | | `/singers?form` | 邮政 | `create()` | 用户输入歌手信息并提交表单。数据将在这里处理。 | | `/singers/photo/id` | 得到 | `downloadPhoto()` | 下载歌手的照片。 |实现 Show Singer 视图
现在我们实现了显示歌手信息的视图。实现 show view 分为三个步骤:
- 实现控制器方法。
- 实现显示歌手视图(
/views/singers/show.jspx)。 - 修改视图的视图定义文件(
/views/singers/views.xml)。
下面的代码片段显示了用于显示歌手信息的singerController类的show()方法实现:
package com.apress.prospring5.ch16.web;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
...
@RequestMapping("/singers")
@Controller
public class SingerController {
private final Logger logger = LoggerFactory.getLogger(SingerController.class);
private SingerService singerService;
private MessageSource messageSource;
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public String show(@PathVariable("id") Long id, Model uiModel) {
Singer singer = singerService.findById(id);
uiModel.addAttribute("singer", singer);
return "singers/show";
}
@Autowired
public void setSingerService(SingerService singerService) {
this.singerService = singerService;
}
...
}
在show()方法上,应用于它的@RequestMapping注释表明该方法要用 HTTP GET 方法处理 URL /singers/{id}。在该方法中,@PathVariable注释被应用于参数id,它指示 Spring MVC 将 URL 中的 ID 提取到参数中。然后检索歌手并将其添加到模型中,并返回逻辑视图名singers/show。下一步是实现 show singer 视图/views/singers/show.jspx,如下所示:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<div xmlns:jsp="http://java.sun.com/JSP/Page"
xmlns:c="http://java.sun.com/jsp/jstl/core"
xmlns:spring="http://www.springframework.org/tags"
xmlns:fmt="http://java.sun.com/jsp/jstl/fmt" version="2.0">
<jsp:directive.page contentType="text/html;charset=UTF-8"/>
<jsp:output omit-xml-declaration="yes"/>
<spring:message code="label_singer_info" var="labelSingerInfo"/>
<spring:message code="label_singer_first_name" var="labelSingerFirstName"/>
<spring:message code="label_singer_last_name" var="labelSingerLastName"/>
<spring:message code="label_singer_birth_date" var="labelSingerBirthDate"/>
<spring:message code="label_singer_description" var="labelSingerDescription"/>
<spring:message code="label_singer_update" var="labelSingerUpdate"/>
<spring:message code="date_format_pattern" var="dateFormatPattern"/>
<spring:message code="label_singer_photo" var="labelSingerPhoto"/>
<spring:url value="/singers/photo" var="singerPhotoUrl"/>
<spring:url value="/singers" var="editSingerUrl"/>
<h1>${labelSingerInfo}</h1>
<div id="singerInfo">
<c:if test="${not empty message}">
<div id="message" class="${message.type}">${message.message}</div>
</c:if>
<table>
<tr>
<td>${labelSingerFirstName}</td>
<td>${singer.firstName}</td>
</tr>
<tr>
<td>${labelSingerLastName}</td>
<td>${singer.lastName}</td>
</tr>
<tr>
<td>${labelSingerBirthDate}</td>
<td><fmt:formatDate value="${singer.birthDate}"/></td>
</tr>
<tr>
<td>${labelSingerDescription}</td>
<td>${singer.description}</td>
</tr>
<tr>
<td>${labelSingerPhoto}</td>
<td><img src="${singerPhotoUrl}/${singer.id}"></img></td>
</tr>
</table>
<a href="${editSingerUrl}/${singer.id}?form">${labelSingerUpdate}</a>
</div>
</div>
页面简单;它在页面中显示模型属性singer。
最后一步是修改视图定义文件/views/singers/views.xml,用于映射逻辑视图名称singers/show。
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE tiles-definitions PUBLIC "-//Apache Software
Foundation//DTD Tiles Configuration 3.0//EN"
"http://tiles.apache.org/dtds/tiles-config_3_0.dtd">
<tiles-definitions>
<definition extends="default" name="singers/list">
<put-attribute name="body" value="/WEB-INF/views/singers/list.jspx" />
</definition>
<definition extends="default" name="singers/show">
<put-attribute name="body" value="/WEB-INF/views/singers/show.jspx" />
</definition>
</tiles-definitions>
show singer 视图已完成。现在,我们需要在歌手列表视图中添加一个锚链接,/views/singers/list.jspx,为每个歌手显示歌手视图。修改后的文件内容如下所示:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<div xmlns:jsp="http://java.sun.com/JSP/Page"
xmlns:c="http://java.sun.com/jsp/jstl/core"
xmlns:spring="http://www.springframework.org/tags"
xmlns:fmt="http://java.sun.com/jsp/jstl/fmt" version="2.0">
<jsp:directive.page contentType="text/html;charset=UTF-8"/>
<jsp:output omit-xml-declaration="yes"/>
<spring:message code="label_singer_list" var="labelSingerList"/>
<spring:message code="label_singer_first_name" var="labelSingerFirstName"/>
<spring:message code="label_singer_last_name" var="labelSingerLastName"/>
<spring:message code="label_singer_birth_date" var="labelSingerBirthDate"/>
<h1>${labelSingerList}</h1>
<spring:url value="/singers" var="showSingerUrl"/>
<c:if test="${not empty singers}">
<table>
<thead>
<tr>
<th>${labelSingerFirstName}</th>
<th>${labelSingerLastName}</th>
<th>${labelSingerBirthDate}</th>
</tr>
</thead>
<tbody>
<c:forEach items="${singers}" var="singer">
<tr>
<td>
<a href="${showSingerUrl}/${singer.id}">${singer.firstName}</a>
</td>
<td>${singer.lastName}</td>
<td><fmt:formatDate value="${singer.birthDate}"/></td>
</tr>
</c:forEach>
</tbody>
</table>
</c:if>
</div>
如前所示,我们通过使用<spring:url>标签声明了一个 URL 变量,并为firstName属性添加了一个锚链接。要测试 show singer 视图,在重建和部署后,再次打开 singer list 视图。列表现在应该包括到 show singer 视图的超链接。单击任何链接都会将您带到 show singer 信息视图。
实现编辑歌手视图
让我们实现编辑歌手的视图。它与显示视图相同;首先,我们将方法updateForm()和update()添加到SingerController类中。下面的代码片段显示了这两种方法的修改后的控制器:
package com.apress.prospring5.ch16.web;
...
@RequestMapping("/singers")
@Controller
public class SingerController {
private final Logger logger = LoggerFactory.getLogger(SingerController.class);
private SingerService singerService;
private MessageSource messageSource;
@RequestMapping(value = "/{id}", params = "form", method = RequestMethod.POST)
public String update(@Valid Singer singer, BindingResult bindingResult,
Model uiModel, HttpServletRequest httpServletRequest,
RedirectAttributes redirectAttributes, Locale locale) {
logger.info("Updating singer");
if (bindingResult.hasErrors()) {
uiModel.addAttribute("message", new Message("error",
messageSource.getMessage("singer_save_fail", new Object[]{}, locale)));
uiModel.addAttribute("singer", singer);
return "singers/update";
}
uiModel.asMap().clear();
redirectAttributes.addFlashAttribute("message", new Message("success",
messageSource.getMessage("singer_save_success", new Object[]{}, locale)));
singerService.save(singer);
return "redirect:/singers/" + UrlUtil.encodeUrlPathSegment(
singer.getId().toString(), httpServletRequest);
}
@RequestMapping(value = "/{id}", params = "form", method = RequestMethod.GET)
public String updateForm(@PathVariable("id") Long id, Model uiModel) {
uiModel.addAttribute("singer", singerService.findById(id));
return "singers/update";
}
@Autowired
public void setSingerService(SingerService singerService) {
this.singerService = singerService;
}
@Autowired
public void setMessageSource(MessageSource messageSource) {
this.messageSource = messageSource;
}
...
}
在之前的配置中,亮点如下:
-
MessageSource接口自动连接到控制器中,用于检索支持 i18n 的消息。 -
对于
updateForm()方法,检索歌手并保存到模型中,然后返回逻辑视图singers/update,它将显示编辑歌手视图。 -
当用户更新歌手信息并点击保存按钮时,将触发
update()方法。这个方法需要解释一下。首先,Spring MVC 会尝试将提交的数据绑定到Singer域对象,并自动执行类型转换和格式化。如果发现绑定错误(例如,出生日期以错误的格式输入),错误将被保存到BindingResult界面(在包org.springframework.validation中),并且错误消息将被保存到模型中,重新显示编辑视图。如果绑定成功,数据将被保存,并以redirect:为前缀为显示 singer 视图返回逻辑视图名。注意,我们希望在重定向后显示消息,所以我们需要使用RedirectAttributes.addFlashAttribute()方法(包org.springframework.web.servlet.mvc.support中的一个接口)在 show singer 视图中显示成功消息。在 Spring MVC 中,flash 属性在重定向之前被临时保存(通常在会话中),以便在重定向之后对请求可用,并被立即删除。 -
Message类是一个定制类,它存储从MessageSource检索到的消息和消息类型(即成功或错误),供视图在消息区域显示。下面是Message类的内容:package com.apress.prospring5.ch16.util; public class Message { private String type; private String message; public Message(String type, String message) { this.type = type; this.message = message; } public String getType() { return type; } public String getMessage() { return message; } } -
UrlUtil是一个为重定向编码 URL 的实用程序类。其内容如下图:package com.apress.prospring5.ch16.util; import java.io.UnsupportedEncodingException; import javax.servlet.http.HttpServletRequest; import org.springframework.web.util.UriUtils; import org.springframework.web.util.WebUtils; public class UrlUtil { public static String encodeUrlPathSegment(String pathSegment, HttpServletRequest httpServletRequest) { String enc = httpServletRequest.getCharacterEncoding(); if (enc == null) { enc = WebUtils.DEFAULT_CHARACTER_ENCODING; } try { pathSegment = UriUtils.encodePathSegment(pathSegment, enc); } catch (UnsupportedEncodingException uee) { // } return pathSegment; } }
接下来是编辑歌手视图/views/singers/edit.jspx,我们将使用它来更新和创建新歌手。
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<div xmlns:jsp="http://java.sun.com/JSP/Page"
xmlns:c="http://java.sun.com/jsp/jstl/core"
xmlns:spring="http://www.springframework.org/tags"
xmlns:form="http://www.springframework.org/tags/form"
version="2.0">
<jsp:directive.page contentType="text/html;charset=UTF-8"/>
<jsp:output omit-xml-declaration="yes"/>
<spring:message code="label_singer_new" var="labelSingerNew"/>
<spring:message code="label_singer_update" var="labelSingerUpdate"/>
<spring:message code="label_singer_first_name" var="labelSingerFirstName"/>
<spring:message code="label_singer_last_name" var="labelSingerLastName"/>
<spring:message code="label_singer_birth_date" var="labelSingerBirthDate"/>
<spring:message code="label_singer_description" var="labelSingerDescription"/>
<spring:message code="label_singer_photo" var="labelSingerPhoto"/>
<spring:eval expression="singer.id == null ? labelSingerNew:labelSingerUpdate"
var="formTitle"/>
<h1>${formTitle}</h1>
<div id="singerUpdate">
<form:form modelAttribute="singer" id="singerUpdateForm" method="post">
<c:if test="${not empty message}">
<div id="message" class="${message.type}">${message.message}</div>
</c:if>
<form:label path="firstName">
${labelSingerFirstName}*
</form:label>
<form:input path="firstName" />
<div>
<form:errors path="firstName" cssClass="error" />
</div>
<p/>
<form:label path="lastName">
${labelSingerLastName}*
</form:label>
<form:input path="lastName" />
<div>
<form:errors path="lastName" cssClass="error" />
</div>
<p/>
<form:label path="birthDate">
${labelSingerBirthDate}
</form:label>
<form:input path="birthDate" id="birthDate"/>
<div>
<form:errors path="birthDate" cssClass="error" />
</div>
<p/>
<form:label path="description">
${labelSingerDescription}
</form:label>
<form:textarea cols="60" rows="8" path="description"
id="singerDescription"/>
<div>
<form:errors path="description" cssClass="error" />
</div>
<p/>
<label for="file">
${labelSingerPhoto}
</label>
<input name="file" type="file"/>
<p/>
<form:hidden path="version" />
<button type="submit">Save</button>
<button type="reset">Reset</button>
</form:form>
</div>
</div>
上一个模板的亮点如下:
- 使用了
<spring:eval>标签,使用 Spring 表达式语言测试歌手 ID 是否为null。如果是的话,那就是新晋歌手;否则就是更新。将显示相应的表单标题。 - 表单中使用了各种 Spring MVC
<form>标签来显示标签、输入字段和错误,以防表单提交时绑定不成功。
接下来,将视图映射添加到视图定义文件/views/singers/views.xml。
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE tiles-definitions PUBLIC "-//Apache Software
Foundation//DTD Tiles Configuration 3.0//EN"
"http://tiles.apache.org/dtds/tiles-config_3_0.dtd">
<tiles-definitions>
<definition extends="default" name="singers/update">
<put-attribute name="body" value="/WEB-INF/views/singers/edit.jspx" />
</definition>
...
</tiles-definitions>
编辑视图现在已经完成。让我们重新构建并部署项目。单击编辑链接后,将显示编辑视图。更新信息,然后单击保存按钮。如果绑定成功,您将看到成功消息,并显示 show singer 视图。
实现添加歌手视图
实现添加歌手视图很像编辑视图。因为我们将重用edit.jspx页面,所以我们只需要添加SingerController类中的方法和视图定义。下面的SingerController类片段描述了为新歌手实现保存操作所需的方法:
package com.apress.prospring5.ch16.web;
...
@RequestMapping("/singers")
@Controller
public class SingerController {
private final Logger logger = LoggerFactory.getLogger(SingerController.class);
private SingerService singerService;
private MessageSource messageSource;
@RequestMapping(method = RequestMethod.POST)
public String create(@Valid Singer singer, BindingResult bindingResult,
Model uiModel, HttpServletRequest httpServletRequest,
RedirectAttributes redirectAttributes,Locale locale) {
logger.info("Creating singer");
if (bindingResult.hasErrors()) {
uiModel.addAttribute("message", new Message("error",
messageSource.getMessage("singer_save_fail", new Object{}, locale)));
uiModel.addAttribute("singer", singer);
return "singers/create";
}
uiModel.asMap().clear();
redirectAttributes.addFlashAttribute("message", new Message("success",
messageSource.getMessage("singer_save_success", new Object[]{}, locale)));
logger.info("Singer id: " + singer.getId());
singerService.save(singer);
return "redirect:/singers/";
}
@RequestMapping(params = "form", method = RequestMethod.GET)
public String createForm(Model uiModel) {
Singer singer = new Singer();
uiModel.addAttribute("singer", singer);
return "singers/create";
}
@Autowired
public void setSingerService(SingerService singerService) {
this.singerService = singerService;
}
...
}
接下来,将视图映射添加到视图定义文件/views/singers/views.xml。
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE tiles-definitions PUBLIC "-//Apache Software
Foundation//DTD Tiles Configuration 3.0//EN"
"http://tiles.apache.org/dtds/tiles-config_3_0.dtd">
<tiles-definitions>
<definition extends="default" name="singers/create">
<put-attribute name="body" value="/WEB-INF/views/singers/edit.jspx" />
</definition>
...
</tiles-definitions>
添加视图现在已经完成。在重新构建和部署项目之后,单击菜单区域中的 New Singer 链接。将显示添加歌手视图,允许您输入新歌手的详细信息。
启用 JSR-349 (Bean 验证)
让我们为创建和更新 singer 操作配置 JSR-349 (Bean 验证)支持。首先,将验证约束应用于Singer域对象。在这个例子中,我们只为firstName和lastName属性定义了约束。Singer类已经在本章的开头展示过了,带有验证注释,但是在这一节我们将解释它们。这里我们可以看到这个类的一个片段,其中包含我们感兴趣的带注释的字段:
package com.apress.prospring5.ch16.entities;
import org.hibernate.validator.constraints.NotBlank;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
...
@Entity
@Table(name = "singer")
public class Singer implements Serializable {
...
@NotBlank(message="{validation.firstname.NotBlank.message}")
@Size(min=2, max=60, message="{validation.firstname.Size.message}")
@Column(name = "FIRST_NAME")
private String firstName;
@NotBlank(message="{validation.lastname.NotBlank.message}")
@Size(min=1, max=40, message="{validation.lastname.Size.message}")
@Column(name = "LAST_NAME")
private String lastName;
...
}
这些约束被应用于它们各自的字段。请注意,对于验证消息,您可以使用花括号指定消息键。这将导致从ResourceBundle检索验证消息,从而支持 i18n。
为了在 web 数据绑定过程中启用 JSR-349 验证,我们只需要对SingerController类中的create()和update()方法的参数应用@ Valid注释。以下代码片段显示了这两种方法的签名:
package com.apress.prospring5.ch16.web;
@RequestMapping("/singers")
@Controller
public class SingerController {
...
@RequestMapping(value = "/{id}", params = "form", method = RequestMethod.POST)
public String update(@Valid Singer singer, BindingResult bindingResult, ...
@RequestMapping(method = RequestMethod.POST)
public String create(@Valid Singer singer, BindingResult bindingResult, ...
...
}
我们还希望 JSR-349 验证消息使用与视图相同的ResourceBundle实例。为此,我们需要在DispatcherServlet配置中,在WebConfig类中配置验证器。
package com.apress.prospring5.ch16.config;
import com.apress.prospring5.ch16.util.DateFormatter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.validation.Validator;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.servlet.config.annotation.*;
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {"com.apress.prospring5.ch16"})
public class WebConfig implements WebMvcConfigurer {
@Bean
public Validator validator() {
final LocalValidatorFactoryBean validator =
new LocalValidatorFactoryBean();
validator.setValidationMessageSource(messageSource());
return validator;
}
// <=> <mvc:annotation-driven validator="validator"/>
@Override
public Validator getValidator() {
return validator();
}
@Bean
ReloadableResourceBundleMessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource =
new ReloadableResourceBundleMessageSource();
messageSource.setBasenames(
"WEB-INF/i18n/messages",
"WEB-INF/i18n/application");
messageSource.setDefaultEncoding("UTF-8");
messageSource.setFallbackToSystemLocale(false); return messageSource;
}
...
}
首先,定义了一个验证器 bean,用类LocalValidatorFactoryBean来支持 JSR-349。注意,我们设置了validationMessageSource属性来引用定义的messageSource bean,这指示 JSR-349 验证器通过来自messageSource bean 的代码来查找消息。然后实现getValidator()方法来返回我们定义的验证器 bean。
仅此而已;我们现在可以测试验证了。调出添加歌手视图,然后点击保存按钮。返回的页面现在将向我们显示一个验证错误。
切换到中文(香港)语言,做同样的事情。这一次,信息将以中文显示。
视图基本上是完整的,除了删除操作。我们把这个留给你作为练习。接下来,我们将开始让我们的界面更加丰富。
使用 jQuery 和 jQuery UI
尽管 singer 应用的视图工作良好,但用户界面相当粗糙。例如,对于出生日期字段,如果我们可以添加一个日期选择器来输入歌手的出生日期,而不是让用户手动输入日期字符串,那会好得多。
为了向 web 应用的用户提供更丰富的界面,除非您使用的是在 web 浏览器客户端上需要特殊运行时的富 Internet 应用(RIA)技术(例如,Adobe Flex 需要 Flash,JavaFX 需要 JRE,Microsoft Silverlight 需要 Silverlight),否则您需要使用 JavaScript 来实现这些功能。
然而,用原始 JavaScript 开发 web 前端并不容易。语法与 Java 有很大不同,你还需要处理跨浏览器的兼容性问题。因此,有很多开源 JavaScript 库可以简化这个过程,比如 jQuery 和 Dojo Toolkit。
在接下来的几节中,我们将讨论如何使用 jQuery 和 jQuery UI 来开发更具响应性和交互性的用户界面。我们还讨论了一些用于特定目的的常用 jQuery 插件,比如富文本编辑支持,并讨论了一些用于浏览数据的基于网格的组件。
jQuery 和 jQuery UI 简介
jQuery ( http://jquery.org )是用于 web 前端开发的最流行的 JavaScript 库之一。jQuery 提供了对主要特性的全面支持,包括用于在文档中选择 DOM 元素的健壮的“选择器”语法、复杂的事件模型和强大的 Ajax 支持。
建立在 jQuery 之上的 jQuery UI 库( http://jqueryui.com )提供了丰富的小部件和效果。主要特性包括常用用户界面组件的小部件(日期选择器、自动完成、折叠等等)、拖放、效果和动画、主题等等。
jQuery 社区开发了大量用于特定目的的 jQuery 插件,我们将在本章中讨论其中的两个。
我们在这里讨论的只是 jQuery 的皮毛。关于使用 jQuery 的更多细节,我们推荐 B. M. Harwani (Apress,2010)的《jQuery 食谱:问题解决方案》和 Bear Bibeault 和 Yehuda Katz (Manning,2010)的《jQuery 实际应用》。
在视图中启用 jQuery 和 jQuery UI
为了能够在我们的视图中使用 jQuery 和 jQuery UI 组件,我们需要包含所需的样式表和 JavaScript 文件。如果我们阅读了本章前面的“理解 Spring MVC 项目结构”一节,那么所需的文件应该已经被复制到项目中了。这些是我们需要包含在视图中的主要文件:
/src/main/webapp/scripts/jquery-1.12.4.js:这是核心的 jQuery JavaScript 库。我们在本章中使用的版本是 1.12.4。请注意,这是完整的源代码版本。在生产中,你应该使用 minified 版本(即jquery-1.12.4.min.js),该版本经过优化和压缩,以提高下载和执行性能。/src/main/webapp/scripts/jquery-ui.min.js:这是捆绑了主题样式表的 jQuery UI 库,可以从 jQuery UI Themeroller 页面(http://jqueryui.com/themeroller)定制和下载。我们正在使用的 jQuery UI 版本是 1.12.1。注意,这是 JavaScript 的缩小版。- 这是定制主题的样式表,jQuery UI 将使用它来支持主题化。
要包含前面的文件,我们只需要将它们包含在模板页面中(也就是/layouts/default.jspx)。以下代码片段需要添加到页面中:
<html xmlns:jsp="http://java.sun.com/JSP/Page" ..>
...
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=8" />
<spring:theme code="styleSheet" var="app_css" />
<spring:url value="/${app_css}" var="app_css_url" />
<link rel="stylesheet" type="text/css" media="screen" href="${app_css_url}" />
<spring:url value="/resources/scripts/jquery-1.12.4.js" var="jquery_url" />
<spring:url value="/resources/scripts/jquery-ui.min.js" var="jquery_ui_url" />
<spring:url value="/resources/styles/custom-theme/jquery-ui.theme.min.css"
var="jquery_ui_theme_css" />
<link rel="stylesheet" type="text/css" media="screen"
href="${jquery_ui_theme_css}" />
<script src="${jquery_url}" type="text/javascript"><jsp:text/></script>
<script src="${jquery_ui_url}" type="text/javascript"><jsp:text/></script>
...
</head>
...
</html>
首先,<spring:url>标签用于定义文件的 URL,并将它们存储在变量中。然后,在<head>部分,添加对 CSS 和 JavaScript 文件的引用。注意在<script>标签中使用了<jsp:text/>标签。这是因为 JSP 会自动折叠没有主体的标签。因此,文件中的标签<script ..></script>在浏览器中会以<script .. />结束,这将导致页面中不确定的行为。添加的<jsp:text/>确保了<script>标签不会出现在页面中,因为它避免了意外的问题。
有了这些脚本,我们可以在视图中添加一些更好的东西。对于 edit singer 视图,让我们使按钮看起来更好一些,并为生日字段启用日期选择器组件。下面的代码片段显示了我们需要添加到按钮和日期字段的/views/singers/edit.jspx中的更改:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<div xmlns:jsp="http://java.sun.com/JSP/Page"
xmlns:c="http://java.sun.com/jsp/jstl/core"
xmlns:spring="http://www.springframework.org/tags"
xmlns:form="http://www.springframework.org/tags/form"
version="2.0">
<script type="text/javascript">
$(function(){
$('#birthDate').datepicker({
dateFormat: 'yy-mm-dd',
changeYear: true
});
});
</script>
...
<form:form modelAttribute="singer" id="singerUpdateForm" method="post">
...
<button type="submit" class="ui-button ui-widget
ui-state-default ui-corner-all ui-button-text-only">
<span class="ui-button-text">Save</span>
</button>
<button type="reset" class="ui-button ui-widget
ui-state-default ui-corner-all ui-button-text-only">
<span class="ui-button-text">Reset</span>
</button>
</form:form>
</div>
</div>
$(function(){}语法指示 jQuery 在文档准备好时执行脚本。在该函数中,出生日期输入字段(ID 为birthDate)使用 jQuery UI 的datepicker()函数进行修饰。其次,各种样式类被添加到按钮中。
现在重新部署应用,您将看到新的按钮样式,当您单击出生日期字段时,将显示日期选择器组件。
使用 CKEditor 编辑富文本
对于歌手信息的描述字段,我们使用 Spring MVC <form:textarea>标签来支持多行输入。假设我们想要启用富文本编辑,这是长文本输入(如用户评论)的常见需求。
为了支持这个特性,我们将使用富文本组件库 CKEditor ( http://ckeditor.com ),这是一个与 jQuery UI 集成的常见富文本 JavaScript 组件。这些文件位于示例源代码的文件夹/src/main/we- bapp/ckeditor中。
首先我们需要将所需的 JavaScript 文件包含到模板页面中,default.jspx。以下代码片段向您展示了需要添加到页面中的内容:
<html xmlns:jsp="http://java.sun.com/JSP/Page"
xmlns:c="http://java.sun.com/jsp/jstl/core"
xmlns:fn="http://java.sun.com/jsp/jstl/functions"
xmlns:tiles="http://tiles.apache.org/tags-tiles"
xmlns:spring="http://www.springframework.org/tags">
<head>
<!-- CKEditor -->
<spring:url value="/resources/ckeditor/ckeditor.js" var="ckeditor_url" />
<spring:url value="/resources/ckeditor/adapters/jquery.js"
var="ckeditor_jquery_url" />
<script type="text/javascript" src="${ckeditor_url}"><jsp:text/></script>
<script type="text/javascript" src="${ckeditor_jquery_url}"><jsp:text/></script>
...
</head>
...
</html>
前面的 JSPX 片段包括两个脚本:核心 CKEditor 脚本和带有 jQuery 的适配器。
下一步是在 edit singer 视图中启用 CKEditor。下一个 JSPX 片段显示了页面edit.jspx所需的更改:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<div xmlns:jsp="http://java.sun.com/JSP/Page"
xmlns:c="http://java.sun.com/jsp/jstl/core"
xmlns:spring="http://www.springframework.org/tags"
xmlns:form="http://www.springframework.org/tags/form" version="2.0">
<script type="text/javascript">
$(function(){
$("#singerDescription").ckeditor(
{
toolbar : 'Basic',
uiColor : '#CCCCCC'
}
);
});
</script>
...
</div>
当文档准备好时,歌手描述字段用 CKEditor 修饰。重新部署应用并转到 add singer 页面,description 字段将启用富文本编辑支持。
有关使用和配置 CKEditor 的完整文档,请参考项目文档网站( http://docs.cksource.com/ )。
将 jqGrid 用于带有分页的数据网格
如果系统中只有几个歌手,当前的歌手列表视图是可以的。但是,随着数据增长到数千甚至更多的记录,性能将成为一个问题。
一种常见的解决方案是为数据浏览实现一个支持分页的数据网格组件,以便用户只浏览一定数量的记录,从而避免在浏览器和 web 容器之间传输大量数据。本节演示了使用jqGrid ( http://trirand.com/blog )的数据网格的实现,这是一个流行的基于 JavaScript 的数据网格组件。我们这里用的版本是 4.6.0。
对于分页支持,我们将使用 jqGrid 内置的 Ajax 分页支持,它为每个页面触发一个XMLHttpRequest,并接受页面数据的 JSON 数据格式。为了生成 JSON 数据,您将使用jackson-databind库。
在下面几节中,我们将讨论如何在服务器端和客户端实现分页支持。首先,我们介绍如何在歌手列表视图中实现jqGrid组件。然后,我们讨论如何利用 Spring Data Commons 模块对分页的全面支持,在服务器端实现分页。
在歌手列表视图中启用 jqGrid
要在视图中启用 jqGrid,首先我们需要在模板页面default.jspx中包含所需的 JavaScript 和样式表文件。
<html xmlns:jsp="http://java.sun.com/JSP/Page"
...
<head>
...
<!-- jqGrid -->
<spring:url value="/resources/jqgrid/css/ui.jqgrid.css" var="jqgrid_css" />
<spring:url value="/resources/jqgrid/js/i18n/grid.locale-en.js"
var="jqgrid_locale_url" />
<spring:url value="/resources/jqgrid/js/jquery.jqGrid.min.js" var="jqgrid_url" />
<link rel="stylesheet" type="text/css" media="screen" href="${jqgrid_css}" />
<script type="text/javascript" src="${jqgrid_locale_url}"><jsp:text/></script>
<script type="text/javascript" src="${jqgrid_url}"><jsp:text/></script>
...
</head>
...
</html>
首先,加载特定于网格的 CSS 文件。然后,需要两个 JavaScript 文件。第一个是 locale 脚本(本例中我们使用英语),第二个是jqGrid核心库文件jquery.jqGrid.min.js。
下一步是修改歌手列表视图textitlist.jspx,以使用jqGrid。在这里你可以看到修改后的页面:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<div xmlns:jsp="http://java.sun.com/JSP/Page"
xmlns:c="http://java.sun.com/jsp/jstl/core"
xmlns:spring="http://www.springframework.org/tags"
version="2.0">
<jsp:directive.page contentType="text/html;charset=UTF-8"/>
<jsp:output omit-xml-declaration="yes"/>
<spring:message code="label_singer_list" var="labelSingerList"/>
<spring:message code="label_singer_first_name" var="labelSingerFirstName"/>
<spring:message code="label_singer_last_name" var="labelSingerLastName"/>
<spring:message code="label_singer_birth_date" var="labelSingerBirthDate"/>
<spring:url value="/singers/" var="showSingerUrl"/>
<script type="text/javascript">
$(function(){
$("#list").jqGrid({
url:'${showSingerUrl}/listgrid',
datatype: 'json',
mtype: 'GET',
colNames:['${labelSingerFirstName}', '${labelSingerLastName}',
'${labelSingerBirthDate}'],
colModel :[
{name:'firstName', index:'firstName', width:150},
{name:'lastName', index:'lastName', width:100},
{name:'birthDateString', index:'birthDate', width:100}
],
jsonReader : {
root:"singerData",
page: "currentPage",
total: "totalPages",
records: "totalRecords",
repeatitems: false,
id: "id"
},
pager: '#pager',
rowNum:10, rowList:[10,20,30],
sortname: 'firstName',
sortorder: 'asc',
viewrecords: true,
gridview: true,
height: 250,
width: 500,
caption: '${labelSingerList}',
onSelectRow: function(id){
document.location.href ="${showSingerUrl}/" + id;
}
});
});
</script>
<c:if test="${not empty message}">
<div id="message" class="${message.type}">
${message.message}
</div>
</c:if>
<h2>${labelSingerList}</h2>
<div>
<table id="list"><tr><td/></tr></table>
</div>
<div id="pager"></div>
</div>
我们声明一个 ID 为list的<table>标签来显示网格数据。在表格下面,定义了一个 ID 为 pager 的<div>部分,它是jqGrid的分页部分。在 JavaScript 中,当文档准备好时,我们指示jqGrid将 ID 为list的表格装饰成网格,并提供详细的配置信息。脚本的一些主要亮点如下:
url属性指定了发送XMLHttpRequest的链接,它获取当前页面的数据。datatype属性指定数据格式,在本例中是 JSON。jqGrid也支持 XML 格式。mtype属性定义了要使用的 HTTP 方法,也就是 GET。colNames属性定义了要在网格中显示的数据的列标题,而colModel属性定义了每个数据列的细节。jsonReader属性定义了服务器将返回的 JSON 数据格式。pager属性启用分页支持。- 属性定义了当一行被选中时要采取的动作。在这种情况下,我们将把用户定向到带有歌手 ID 的 show singer 视图。
关于 jqGrid 的配置和使用的详细描述,请参考项目文档网站( http://trirand.com/jqgridwiki/doku.php?id=wiki:jqgriddocs )。
在服务器端启用分页
在服务器端,实现分页需要几个步骤。首先,我们将使用 Spring Data Commons 模块的分页支持。为了实现这一点,我们只需要修改SingerRepository接口来扩展PagingAndSortingRepository<T,ID extends Serializable>接口,而不是CrudRepository<T,ID extends Serializable>接口。你可以在这里看到修改后的界面:
package com.apress.prospring5.ch16.repos;
import com.apress.prospring5.ch16.entities.Singer;
import org.springframework.data.repository.PagingAndSortingRepository;
public interface SingerRepository extends
PagingAndSortingRepository<Singer, Long> {
}
下一步是在SingerService接口中添加一个新方法,以支持按页面检索数据。修改后的界面如下所示:
package com.apress.prospring5.ch16.services;
import java.util.List;
import com.apress.prospring5.ch16.entities.Singer;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
public interface SingerService {
List<Singer> findAll();
Singer findById(Long id);
Singer save(Singer singer);
Page<Singer> findAllByPage(Pageable pageable);
}
如前所示,添加了一个名为findAllByPage()的新方法,将Pageable接口的一个实例作为参数。下面的代码片段展示了SingerServiceImpl类中findAllByPage()方法的实现。该方法返回一个Page<T>接口的实例(属于 Spring Data Commons,在包org.springframework.data.domain中)。正如这个简单场景所预期的,服务方法只调用由PagingAndSortingRepository<T,ID extends Serializable>接口提供的存储库方法findAll(..)。
package com.apress.prospring5.ch16.services;
import java.util.List;
import com.apress.prospring5.ch16.repos.SingerRepository;
import com.apress.prospring5.ch16.entities.Singer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.google.common.collect.Lists;
@Repository
@Transactional
@Service("singerService")
public class SingerServiceImpl implements SingerService {
private SingerRepository singerRepository;
...
@Autowired
public void setSingerRepository(SingerRepository singerRepository) {
this.singerRepository = singerRepository;
}
@Override
@Transactional(readOnly=true)
public Page<Singer> findAllByPage(Pageable pageable) {
return singerRepository.findAll(pageable);
}
}
下一步是最复杂的,这是在SingerController类中实现方法,从jqGrid获取 Ajax 请求页面数据。以下代码片段显示了实现:
package com.apress.prospring5.ch16.web;
....
@RequestMapping("/singers")
@Controller
public class SingerController {
private final Logger logger = LoggerFactory.getLogger(SingerController.class);
private SingerService singerService;
@ResponseBody
@RequestMapping(value = "/listgrid", method = RequestMethod.GET,
produces="application/json")
public SingerGrid listGrid(@RequestParam(value = "page",
required = false) Integer page,
@RequestParam(value = "rows", required = false) Integer rows,
@RequestParam(value = "sidx", required = false) String sortBy,
@RequestParam(value = "sord", required = false) String order) {
logger.info("Listing singers for grid with page: {}, rows: {}",
page, rows);
logger.info("Listing singers for grid with sort: {}, order: {}",
sortBy, order);
// Process order by
Sort sort = null;
String orderBy = sortBy;
if (orderBy != null && orderBy.equals("birthDateString"))
orderBy = "birthDate";
if (orderBy != null && order != null) {
if (order.equals("desc")) {
sort = new Sort(Sort.Direction.DESC, orderBy);
} else
sort = new Sort(Sort.Direction.ASC, orderBy);
}
// Constructs page request for current page
// Note: page number for Spring Data JPA starts with 0,
// while jqGrid starts with 1
PageRequest pageRequest = null;
if (sort != null) {
pageRequest = PageRequest.of(page - 1, rows, sort);
} else {
pageRequest = PageRequest.of(page - 1, rows);
}
Page<Singer> singerPage = singerService.findAllByPage(pageRequest);
// Construct the grid data that will return as JSON data
SingerGrid singerGrid = new SingerGrid();
singerGrid.setCurrentPage(singerPage.getNumber() + 1);
singerGrid.setTotalPages(singerPage.getTotalPages());
singerGrid.setTotalRecords(singerPage.getTotalElements());
singerGrid.setSingerData(Lists.newArrayList(singerPage.iterator()));
return singerGrid;
}
@Autowired
public void setSingerService(SingerService singerService) {
this.singerService = singerService;
}
...
}
该方法处理 Ajax 请求,从请求中读取参数(页码、每页记录数、排序方式和排序顺序)(代码示例中的参数名称是jqGrid的默认值),构造实现Pageable接口的PageRequest类的实例,然后调用SingerService.findAllByPage()方法获取页面数据。然后,SingerGrid类的一个实例被构造并以 JSON 格式返回给jqGrid。下面的代码片段显示了SingerGrid类:
package com.apress.prospring5.ch16.util;
import com.apress.prospring5.ch16.entities.Singer;
import java.util.List;
public class SingerGrid {
private int totalPages;
private int currentPage;
private long totalRecords;
private List<Singer> singerData;
public int getTotalPages() {
return totalPages;
}
public void setTotalPages(int totalPages) {
this.totalPages = totalPages;
}
// other getters and setters
...
}
现在我们准备测试新的歌手列表视图。确保项目被重新构建和部署,然后调用 singer 列表视图。您现在应该会看到歌手列表的增强网格视图。
您可以摆弄网格、浏览页面、更改每页的记录数、通过单击列标题更改排序顺序等等。i18n 也支持,可以试试看有中文标签的格子。
jqGrid还支持数据过滤。例如,我们可以通过名字包含John或者出生日期在日期范围之间来过滤数据。
处理文件上传
歌手信息有一个 BLOB 类型的字段来存储照片,照片可以从客户端上传。本节展示了如何在 Spring MVC 中实现文件上传。
很长一段时间,标准 servlet 规范不支持文件上传。因此,Spring MVC 与其他库(最常见的是 Apache Commons FileUpload 库, http://commons.apache.org/fileupload )合作来实现这个目的。Spring MVC 内置了对 Commons FileUpload 的支持。然而,从 Servlet 3.0 开始,文件上传已经成为 web 容器的内置特性。Tomcat 7 支持 Servlet 3.0,Spring 从 3.1 版本开始也支持 Servlet 3.0 文件上传。
在接下来的小节中,我们将讨论如何使用 Spring MVC 和 Servlet 3.0 实现文件上传功能。
配置文件上传支持
在兼容 Servlet 3.0 和 Spring MVC 的 web 容器中,配置文件上传支持是一个两步过程。
首先,在定义创建DispatcherServlet定义所需的一切的基于 Java 的配置类中,我们需要添加一个类型为StandardServletMultipartResolver的 bean。这是基于 Servlet 3.0 javax.servlet.http.Part API 的MultipartResolver接口的标准实现。下面的代码片段描述了需要添加到WebConfig类的这个 bean 的声明:
package com.apress.prospring5.ch16.config;
...
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {"com.apress.prospring5.ch16"})
public class WebConfig implements WebMvcConfigurer {
...
@Bean StandardServletMultipartResolver multipartResolver() {
return new StandardServletMultipartResolver();
}
...
}
第二步,在 Servlet 3.0 环境中启用MultiParsing;这意味着WebInitializer的实现需要一些改变。在AbstractDispatcherServletInitializer抽象类中定义了一个叫做customizeRegistration(..)的方法,这个抽象类是由AbstractAnnotationConfigDispatcherServletInitializer扩展的类。必须实现这个方法来注册一个javax.servlet.MultipartConfigElement的实例。这里显示了WebInitializer类的增强版本:
package com.apress.prospring5.ch16.init;
import com.apress.prospring5.ch16.config.DataServiceConfig;
import com.apress.prospring5.ch16.config.SecurityConfig;
import com.apress.prospring5.ch16.config.WebConfig;
import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.filter.HiddenHttpMethodFilter;
import org.springframework.web.servlet.support.
AbstractAnnotationConfigDispatcherServletInitializer;
import javax.servlet.Filter;
import javax.servlet.MultipartConfigElement;
import javax.servlet.ServletRegistration;
public class WebInitializer extends
AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[]{
SecurityConfig.class, DataServiceConfig.class
};
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[]{
WebConfig.class
};
}
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
@Override
protected Filter[] getServletFilters() {
CharacterEncodingFilter cef = new CharacterEncodingFilter();
cef.setEncoding("UTF-8");
cef.setForceEncoding(true);
return new Filter[]{new HiddenHttpMethodFilter(), cef};
}
// <=> <multipart-config>
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
registration.setMultipartConfig(getMultipartConfigElement());
}
@Bean
private MultipartConfigElement getMultipartConfigElement() {
return new MultipartConfigElement( null, 5000000, 5000000, 0);
}
}
MultipartConfigElement的第一个参数是存储文件的临时位置。第二个是允许上传的最大文件大小,在本例中是 5MB。第三个表示请求的大小,这里也是 5MB。最后一项表示阈值,超过该阈值后,文件将被写入磁盘。
修改视图以支持文件上传
我们需要修改两个视图来支持文件上传。第一个是支持歌手照片上传的编辑视图(edit.jspx),第二个是显示照片的显示视图(show.jspx)。
下面的 JSPX 片段描述了edit.jspx视图中所需的更改:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<div xmlns:jsp="http://java.sun.com/JSP/Page"
...
<form:form modelAttribute="singer" id="singerUpdateForm" method="post"
enctype="multipart/form-data">
...
<form:label path="description">
${labelSingerDescription}
</form:label>
<form:textarea cols="60" rows="8" path="description" id="singerDescription"/>
<div>
<form:errors path="description" cssClass="error" />
</div>
<p/>
<label for="file">
${labelSingerPhoto}
</label>
<input name="file" type="file"/>
<p/>
...
</form:form>
</div>
在前面的<form:form>标签中,我们需要通过指定属性enctype来启用多部分文件上传支持。接下来,文件上传字段被添加到表单中。
我们还需要修改 show 视图来显示歌手的照片。下面的 JSPX 片段显示了视图show.jspx所需的更改:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<div xmlns:jsp="http://java.sun.com/JSP/Page"
...
<spring:message code="label_singer_photo" var="labelSingerPhoto"/>
<spring:url value="/singers/photo" var="singerPhotoUrl"/>
...
<tr>
<td>${labelSingerDescription}</td>
<td>${singer.description}</td>
</tr>
<tr>
<td>${labelSingerPhoto}</td>
<td><img src="${singerPhotoUrl}/${singer.id}"></img></td>
</tr>
...
</div>
在前面的视图模板中,通过指向照片下载的 URL,在表格中添加了一个新行来显示照片。
修改控制器以支持文件上传
最后一步是修改控制器。我们需要做两个改变。第一个变化是对create()方法的修改,它接受上传文件作为请求参数。第二个变化是基于提供的歌手 ID 实现了一个新的照片下载方法。下面的代码片段显示了修改后的SingerController类:
package com.apress.prospring5.ch16.web;
...
@RequestMapping("/singers")
@Controller
public class SingerController {
private final Logger logger = LoggerFactory.getLogger(SingerController.class);
private SingerService singerService;
@RequestMapping(method = RequestMethod.POST)
public String create(@Valid Singer singer, BindingResult bindingResult,
Model uiModel, HttpServletRequest httpServletRequest,
RedirectAttributes redirectAttributes,
Locale locale, @RequestParam(value="file", required=false) Part file) {
logger.info("Creating singer");
if (bindingResult.hasErrors()) {
uiModel.addAttribute("message", new Message("error",
messageSource.getMessage("singer_save_fail",
new Object[]{}, locale)));
uiModel.addAttribute("singer", singer);
return "singers/create";
}
uiModel.asMap().clear();
redirectAttributes.addFlashAttribute("message", new Message("success",
messageSource.getMessage("singer_save_success",
new Object[]{}, locale)));
logger.info("Singer id: " + singer.getId());
// Process upload file if (file != null) {
logger.info("File name: " + file.getName());
logger.info("File size: " + file.getSize());
logger.info("File content type: " + file.getContentType()); byte[] fileContent = null;
try {
InputStream inputStream = file.getInputStream();
if (inputStream == null) logger.info("File inputstream is null");
fileContent = IOUtils.toByteArray(inputStream);
singer.setPhoto(fileContent);
} catch (IOException ex) {
logger.error("Error saving uploaded file");
}
singer.setPhoto(fileContent);
}
singerService.save(singer);
return "redirect:/singers/";
}
@RequestMapping(value = "/photo/{id}", method = RequestMethod.GET)
@ResponseBody
public byte[] downloadPhoto(@PathVariable("id") Long id) {
Singer singer = singerService.findById(id);
if (singer.getPhoto() != null) {
logger.info("Downloading photo for id: {} with size: {}", singer.getId(),
singer.getPhoto().length);
}
return singer.getPhoto();
}
...
}
首先在create()方法中添加了一个新的接口类型javax.servlet.http.Part的请求参数作为实参,Spring MVC 会根据请求中上传的内容来提供。然后该方法将内容保存到Singer对象的photo属性中。
接下来,添加一个名为downloadPhoto()的新方法来处理文件下载。该方法只是从Singer对象中检索照片字段,并直接写入响应流,这对应于 show 视图中的<img>标签。
要测试文件上传功能,重新部署应用并添加一个带照片的新歌手。完成后,您将能够在显示视图中看到照片。
我们还需要修改编辑功能来更改照片,但我们将在这里跳过这一步,将其作为一个练习留给您。
用 Spring Security 保护 Web 应用
假设现在我们想要保护 singer 应用。只有那些使用有效用户 id 登录应用的用户才能添加新歌手或更新现有歌手。其他被称为匿名用户的用户只能查看歌手信息。
Spring Security 是保护基于 Spring 的应用的最佳选择。尽管 Spring Security 主要用于表示层,但它可以帮助保护应用中的所有层,包括服务层。在接下来的部分中,我们将演示如何使用 Spring Security 来保护 singer 应用。
Spring Security 是在第十二章中介绍的,当时你获得了一个 REST 服务。对于 web 应用,可能性是多种多样的,因为还有一个标记库,可以用来保护页面的某些元素。Spring Security 标签库是spring-security-web模块的一部分。
配置 Spring Security 性
在上一节中,我们提到了在本章中,我们将使用完整的注释配置,使用基于 Java 的配置类。在 Spring 3.x 之前,在 Spring web 应用中启用安全性是通过在 web 部署描述符(web.xml)中配置过滤器来完成的。这个过滤器被命名为springSecurityFilterChain,它适用于任何请求,除了静态组件。在 Spring 4.0 中,引入了一个可以扩展来启用 Spring Security 性的类:AbstractSecurityWebApplicationInitializer。
package com.apress.prospring5.ch16.init;
import org.springframework.security.web.context.
AbstractSecurityWebApplicationInitializer;
public class SecurityWebApplicationInitializer
extends AbstractSecurityWebApplicationInitializer {
}
通过提供一个扩展了AbstractSecurityWebApplicationInitializer的空类,您基本上是在告诉 Spring 您想要启用DelegatingFilterProxy,这样springSecurityFilterChain将在任何其他已注册的javax.servlet.Filter之前被使用。
除此之外,Spring Security 上下文显然必须使用基于 Java 的配置类来配置,并且这个类必须被添加到根配置中。这里描述了SecurityConfig配置类:
package com.apress.prospring5.ch16.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.
authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.
method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.
builders.HttpSecurity;
import org.springframework.security.config.annotation.web.
configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.
WebSecurityConfigurerAdapter;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
try {
auth.inMemoryAuthentication().withUser("user")
.password("user").roles("USER");
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/*").permitAll()
.and()
.formLogin()
.usernameParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/login")
.loginPage("/singers")
.failureUrl("/security/loginfail")
.defaultSuccessUrl("/singers")
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/singers")
.and()
.csrf().disable();
}
}
方法configure(HttpSecurity http)定义了 HTTP 请求的安全配置。.antMatchers("/*").permitAll()链式调用指定允许所有用户进入应用。我们将看到如何通过使用 Spring Security 的标签库和控制器方法安全性隐藏视图中的编辑选项来保护函数。然后.formLogin()定义了对表单登录的支持。此后直到.and()调用的所有调用都是配置登录表单细节的方法。正如我们在布局中讨论的,登录表单将显示在左侧。我们还通过.logout()调用提供了一个注销链接。
在 Spring Security 4 中,引入了在 Spring 表单中使用 CSFR 令牌来防止跨站点请求伪造的可能性。 5 在这个例子中,因为我们想保持简单,所以通过调用.csrf().disable()来禁用 CSFR 令牌的用法。默认情况下,没有 CSFR 元素配置的配置是无效的,任何登录请求都会将您定向到一个 403 错误页面,显示以下内容:
Invalid CSRF Token 'null' was found on the request parameter
'_csrf' or header 'X-CSRF-TOKEN'.
方法configureGlobal(AuthenticationManagerBuilder auth)定义了认证机制。在配置中,我们硬编码了一个分配了角色USER的用户。在生产环境中,应该根据数据库、LDAP 或 SSO 机制对用户进行身份验证。
同样,直到 Spring 3,默认的登录 URL 值是/j_spring_security_check,认证密钥的默认名称是j_username和j_password。从 Spring 4 开始,默认的登录 URL 值是/login,认证密钥的默认名称是username和password。
在之前的配置中,明确设置了用户名、密码和登录 URL,但如果在视图中使用默认名称,则可以跳过配置的以下部分。
.usernameParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/login")|
尽管前面已经描述了WebInitializer类的内容,这里将再次介绍配置,以强调使用SecurityConfig类的地方。
package com.apress.prospring5.ch16.init;
...
public class WebInitializer extends
AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[]{
SecurityConfig.class, DataServiceConfig.class
};
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[]{
WebConfig.class
};
}
...
}
向应用添加登录功能
要将登录表单添加到应用中,必须更改两个视图。以下是视图header.jspx文件,用于显示用户登录时的用户信息:
<div id="header" xmlns:jsp="http://java.sun.com/JSP/Page"
xmlns:spring="http://www.springframework.org/tags"
xmlns:sec="http://www.springframework.org/security/tags"
version="2.0">
<jsp:directive.page contentType="text/html;charset=UTF-8" />
<jsp:output omit-xml-declaration="yes" />
<spring:message code="header_text" var="headerText"/>
<spring:message code="label_logout" var="labelLogout"/>
<spring:message code="label_welcome" var="labelWelcome"/>
<spring:url var="logoutUrl" value="/logout" />
<div id="appname">
<h1>${headerText}</h1>
</div>
<div id="userinfo">
<sec:authorize access="isAuthenticated()">${labelWelcome}
<sec:authentication property="principal.username" />
<br/>
<a href="${logoutUrl}">${labelLogout}</a>
</sec:authorize>
</div>
</div>
首先,为 Spring Security 标签库添加前缀为sec的标签库。然后,添加一个带有<sec:authorize>标签的<div>部分来检测用户是否登录。如果是(即isAuthenticated()表达式返回true,将显示用户名和注销链接。
第二个要修改的视图是menu.jspx文件,其中添加了登录表单;只有用户登录后,新的歌手选项才会显示。
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<div id="menu" xmlns:jsp="http://java.sun.com/JSP/Page"
xmlns:spring="http://www.springframework.org/tags"
xmlns:sec="http://www.springframework.org/security/tags"
version="2.0">
<jsp:directive.page contentType="text/html;charset=UTF-8" />
<jsp:output omit-xml-declaration="yes" />
<spring:message code="menu_header_text" var="menuHeaderText"/>
<spring:message code="menu_add_singer" var="menuAddSinger"/>
<spring:url value="/singers?form" var="addSingerUrl"/>
<spring:message code="label_login" var="labelLogin"/>
<spring:url var="loginUrl" value="/login" />
<h3>${menuHeaderText}</h3>
<sec:authorize access="hasRole('ROLE_USER')">
<a href="${addSingerUrl}"><h3>${menuAddSinger}</h3></a>
</sec:authorize>
<sec:authorize access="isAnonymous()">
<div id="login">
<form name="loginForm" action="${loginUrl}" method="post">
<table>
<caption align="left">Login:</caption>
<tr>
<td>User Name:</td>
<td><input type="text" name="username"/></td>
</tr>
<tr>
<td>Password:</td>
<td><input type="password" name="password"/></td>
</tr>
<tr>
<td colspan="2" align="center">
<input name="submit" type="submit" value="Login"/>
</td>
</tr>
</table>
</form>
</div>
</sec:authorize>
</div>
只有当用户登录并被授予角色USER(在<sec:authorized>标签中指定)时,Add Singer 菜单项才会呈现。第二,如果用户没有登录(第二个<sec:authorized>标记,当表达式isAnonymous()返回 true 时),那么将显示登录表单。
重新部署应用,它将显示登录表单,注意没有显示新的 singer 链接。
在用户名和密码字段中输入用户,然后单击登录按钮。用户信息将显示在标题区域。还显示了新歌手链接。
我们还需要修改 show 视图(show.jspx),以便只为登录用户显示 edit singer 链接,但是我们将在这里跳过这一步,将其作为练习留给您。
当登录信息不正确时,处理此问题的 URL 将位于/security/loginfail。因此,我们需要实现一个控制器来处理这种登录失败的情况。下面的代码片段显示了SecurityController类:
package com.apress.prospring5.ch16.web;
import com.apress.prospring5.ch16.util.Message;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Controller; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.Locale;
@Controller
@RequestMapping("/security")
public class SecurityController {
private final Logger logger = LoggerFactory.getLogger(SecurityController.class);
private MessageSource messageSource;
@RequestMapping("/loginfail")
public String loginFail(Model uiModel, Locale locale) {
logger.info("Login failed detected");
uiModel.addAttribute("message", new Message("error",
messageSource.getMessage("message_login_fail", new Object{}, locale)));
return "singers/list";
}
@Autowired
public void setMessageSource(MessageSource messageSource) {
this.messageSource = messageSource;
}
}
控制器类将处理所有带有前缀security的 URL,而方法loginFail()将处理登录失败的情况。在该方法中,我们将登录失败消息存储在模型中,然后重定向到主页。现在重新部署应用并输入错误的用户信息;主页将再次显示,并显示登录失败消息。
使用注释保护控制器方法
在菜单中隐藏新歌手链接是不够的。比如你直接在浏览器中输入http://localhost:8080/users?form网址,我们仍然可以看到添加歌手页面,尽管你还没有登录。原因是我们没有在 URL 级别保护应用。保护页面的一种方法是配置 Spring Security 过滤器链,只拦截经过身份验证的用户的 URL。但是,这样做会阻止所有其他用户查看歌手列表视图。
解决这个问题的另一种方法是使用 Spring Security 的注释支持,在控制器方法级别应用安全性。通过用@EnableGlobalMethodSecurity(prePostEnabled = true)注释SecurityConfig类来启用方法安全性,并且prePostEnabled属性支持在方法上使用前注释和后注释。
现在我们可以为我们想要保护的控制器方法使用@PreAuthorize注释。下面的代码片段展示了一个保护createForm()方法的例子:
import org.springframework.security.access.prepost.PreAuthorize;
...
@PreAuthorize("isAuthenticated()")
@RequestMapping(params = "form", method = RequestMethod.GET)
public String createForm(Model uiModel) {
Singer singer = new Singer();
uiModel.addAttribute("singer", singer);
return "singers/create";
}
我们使用@PreAuthorize注释来保护createForm()方法,参数是安全需求的表达式。
现在你可以尝试在浏览器中直接输入新歌手的网址,如果你没有登录,Spring Security 会把你重定向到登录页面,也就是在SecurityConfig类中配置的歌手列表视图。
用 Spring Boot 创建 Spring Web 应用
Spring Boot 在书中很早就被介绍了,因为它是快速创建应用的实用工具。在这一节中,我们将介绍如何创建一个完整的 web 应用,其中包含使用百里香叶创建的安全性和网页。Thymeleaf 是一个 XML/XHTML/HTML5 模板引擎,可以在 web 和非 web 环境中工作。把它和 Spring 整合起来真的很容易。它是最适合 Spring 的模板引擎,因为它的创建者和项目负责人 Daniel Fernandez 启动了这个项目,因为他想给 Spring MVC 应有的模板引擎。百里香叶是 JSP 或 Tiles 的实用替代品,SpringSource 团队非常喜欢它,所以知道如何配置和使用它对你未来的职业生涯会很有用。
第一个百里香版本于 2011 年 4 月发布。在撰写本文时,最近发布了 Thymeleaf 3.0.7,包括对 Spring 5 的新集成模块的更新。百里叶的扩展有不少,由百里叶官方团队编写和维护(比如百里叶 Spring Security 扩展 7 和百里叶模块 for Java 8 Time API 兼容性 8 )。
不管怎样,让我们把百里香留到以后,开始创建一个 Spring Boot web 应用。要创建一个成熟的 Spring web 应用,这意味着您需要一个 DAO 层和一个服务层。这意味着您需要特定的 Boot starter 库用于持久性和事务。这里您可以看到一个 Gradle 配置片段,描述了创建应用所需的库。它们中的每一个都将在适当的时候详述。
//pro-spring-15/build.gradle
ext {
//spring libs
bootVersion = '2.0.0.M3'
bootstrapVersion = '3.3.7-1'
thymeSecurityVersion = '3.0.2.RELEASE'
jQueryVersion = '3.2.1'
...
spring = [
...
springSecurityTest:
"org.springframework.security:spring-security-test:$springSecurityVersion"
]
boot = [
...
starterThyme :
"org.springframework.boot:spring-boot-starter-thymeleaf:$bootVersion",
starterSecurity :
"org.springframework.boot:spring-boot-starter-security:$bootVersion"
]
web = [
bootstrap : "org.webjars:bootstrap:$bootstrapVersion",
jQuery : "org.webjars:jquery:$jQueryVersion",
thymeSecurity:
"org.thymeleaf.extras:thymeleaf-extras-springsecurity4:$thymeSecurityVersion"
]
db = [
...
h2 : "com.h2database:h2:$h2Version"
]
}
//chapter16/build.gradle
...
apply plugin: 'org.springframework.boot'
dependencies {
compile boot.starterJpa, boot.starterJta, db.h2, boot.starterWeb,
boot.starterThyme, boot.starterSecurity,
web.thymeSecurity, web.bootstrap, web.jQuery
testCompile boot.starterTest, spring.springSecurityTest
}
设置道层
Spring Boot JPA 库spring-boot-starter-data-jpa包含自动配置的 beanss,如果 H2 库在类路径中,这些 bean 可以用来设置和生成 H2 数据库。开发人员要做的就是开发一个实体类和一个存储库。对于这个例子,事情将保持非常简单,所以我们将使用简单版本的Singer类。
package com.apress.prospring5.ch16.entities;
import org.hibernate.validator.constraints.NotBlank;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.io.Serializable;
import java.util.Date;
import static javax.persistence.GenerationType.IDENTITY;
@Entity
@Table(name = "singer")
public class Singer implements Serializable {
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "ID")
private Long id;
@Version
@Column(name = "VERSION")
private int version;
@NotBlank(message = "{validation.firstname.NotBlank.message}")
@Size(min = 2, max = 60, message = "{validation.firstname.Size.message}")
@Column(name = "FIRST_NAME")
private String firstName;
@NotBlank(message = "{validation.lastname.NotBlank.message}")
@Size(min = 1, max = 40, message = "{validation.lastname.Size.message}")
@Column(name = "LAST_NAME")
private String lastName;
@NotNull
@Temporal(TemporalType.DATE)
@Column(name = "BIRTH_DATE")
private Date birthDate;
@Column(name = "DESCRIPTION")
private String description;
//setters and getters
...
}
此外,因为简单,将使用一个CrudRepository实例的最简单扩展。
package com.apress.prospring5.ch16.repos;
import com.apress.prospring5.ch16.entities.Singer;
import org.springframework.data.repository.CrudRepository;
public interface SingerRepository extends CrudRepository<Singer, Long> {
}
设置服务层
服务层也简单;它由SingerServiceImpl类和DBInitializer类组成,用于初始化数据库并用Singer记录填充数据库。这里描述了SingerServiceImpl类。初始化器类在前面的章节中已经介绍过了,所以不再赘述。
package com.apress.prospring5.ch16.services;
import com.apress.prospring5.ch16.entities.Singer;
import com.apress.prospring5.ch16.repos.SingerRepository;
import com.google.common.collect.Lists;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class SingerServiceImpl implements SingerService {
private SingerRepository singerRepository;
@Override
public List<Singer> findAll() {
return Lists.newArrayList(singerRepository.findAll());
}
@Override
public Singer findById(Long id) {
return singerRepository.findById(id).get();
}
@Override
public Singer save(Singer singer) {
return singerRepository.save(singer);
}
@Autowired
public void setSingerRepository(SingerRepository singerRepository) {
this.singerRepository = singerRepository;
}
}
设置 Web 图层
web 层仅由最简单版本的SingerController组成。@RequestMapping注释被替换为不再需要指定 HTTP 方法的等价注释。该类如下所示:
package com.apress.prospring5.ch16.web;
import com.apress.prospring5.ch16.entities.Singer;
import com.apress.prospring5.ch16.services.SingerService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.List;
@Controller
@RequestMapping(value = "/singers")
public class SingerController {
private final Logger logger = LoggerFactory.getLogger(SingerController.class);
@Autowired SingerService singerService;
@GetMapping
public String list(Model uiModel) {
logger.info("Listing singers");
List<Singer> singers = singerService.findAll();
uiModel.addAttribute("singers", singers);
logger.info("No. of singers: " + singers.size());
return "singers";
}
@GetMapping(value = "/{id}")
public String show(@PathVariable("id") Long id, Model uiModel) {
Singer singer = singerService.findById(id);
uiModel.addAttribute("singer", singer);
return "show";
}
@GetMapping(value = "/edit/{id}")
public String updateForm(@PathVariable Long id, Model model) {
model.addAttribute("singer", singerService.findById(id));
return "update";
}
@GetMapping(value = "/new")
public String createForm(Model uiModel) {
Singer singer = new Singer();
uiModel.addAttribute("singer", singer);
return "update";
}
@PostMapping
public String saveSinger(@Valid Singer singer) {
singerService.save(singer);
return "redirect:/singers/" + singer.getId();
}
}
updateForm和createForm方法返回相同的视图。唯一的区别是,updateForm接收一个现有的Singer实例的 ID 作为参数,它被用作update视图的模型对象。update视图包含一个按钮,单击该按钮将调用createSinger方法。如果Singer实例有一个 ID,则在数据库中执行对象的更新;否则,将创建一个新的Singer实例并保存到数据库中。本例中不包括验证和验证失败的正确处理。这将作为一个练习留给你。
Spring Boot web starter 库spring-boot-starter-web支持发现和创建控制器 beans 的任务。
设置 Spring Security 性
Spring Boot 提供了一个名为spring-boot-starter-security的 Spring Security starter 库。如果该库在类路径中,Spring Boot 会自动使用基本身份验证保护所有 HTTP 端点。但是可以进一步定制默认的安全设置。对于本节,让我们假设使用根(/)上下文(http:\localhost:8080/)只能访问应用的首页(主页)。定制配置如下所示:
package com.apress.prospring5.ch16;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.
AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.
EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.
WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home").permitAll()
.and()
.authorizeRequests().antMatchers("/singers/**").authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("user").password("user").roles("USER");
}
}
创建百里香叶视图
在开始使用百里香模板引擎创建视图之前,让我们先了解一下 Spring Boot web 应用的基本结构。Spring Boot web 应用使用resources目录作为 web 资源目录,因此不需要webapp目录。如果resources目录的内容是按照 Spring Boot 的默认结构要求组织的,那么就不需要编写大量的配置,因为预先配置的 beans 是由 Spring Boot 启动器库提供的。要使用默认配置的百里香叶模板引擎,Spring Boot 百里香叶启动库spring-boot-starter-thymeleaf必须在项目的类路径中。
图 16-5 显示了spring-boot-starter-thymeleaf库的传递依赖,以及你可能会感兴趣的其他启动库。

图 16-5。
Spring Boot starter libraries and starter dependencies
正如你所看到的,Spring Boot 百里香叶启动版 2.0.0.M3 附带了百里香叶 3.0.7,这是在撰写本文时发布的最新版本。
注意,spring-boot-starter-web有一个嵌入式 Tomcat 服务器作为依赖项,用于运行应用。
既然我们已经在类路径中有了所有的库,让我们分析一下resources目录的结构,如图 16-6 所示。默认情况下,Spring Boot 配置百里香引擎从templates目录中读取模板文件。如果单独使用 Spring MVC,而没有 Spring Boot,那么需要通过定义三个 bean 来显式配置百里香引擎:一个类型为SpringResourceTemplateResolver的 bean、一个类型为SpringTemplateEngine的 bean 和一个ThymeleafViewResolver bean。因此,有了 Spring Boot,开发人员所要做的就是开始创建模板并把它们放到目录中。

图 16-6。
Default structure of the resources directory for a Spring Boot web application using Thymeleaf
创建百里香模板很容易,语法也很简单。让我们从一个简单的例子开始。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head lang="en">
<title>Spring Boot Thymeleaf Sample</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
</head>
<body>
<h1>Hello World</h1>
<h2>Just another simple text here</h2>
</body>
</html>
行<html xmlns:th=" http://www.thymeleaf.org ">是百里叶名称空间声明。这很重要,没有它,模板将只是一个简单的静态 HTML 文件。作为一个模板引擎,百里香允许你定义和定制模板将如何处理到一个很好的细节。在幕后,模板由一个(或几个)处理器处理,它可以识别用百里香标准方言编写的某些元素。大多数标准方言由属性处理器组成;这些允许浏览器正确地显示模板,即使文件没有被处理,因为未知的属性将被忽略。让我们考虑下面这个表单输入文本字段的例子:
<input type="text" name="singerName" value="John Mayer" />
前面的声明是一个静态 HTML 组件,浏览器知道如何解释它。如果您使用 Spring form 元素编写相同的元素,它看起来会像这样:
<form:inputText name="singerName" value="${singer.name}" />
浏览器不能显示早期的声明,所以当使用 Apache Tiles 编写模板时,每次我们想在浏览器中查看表单时,我们都必须编译项目。百里香叶通过使用普通的 HTML 元素解决了这个问题,HTML 元素可以使用百里香叶属性进行配置。这是用百里香叶编写的前一个元素的样子:
<input type="text" name="singerName" value="John Mayer"
th:value="${singer.name}" />
通过前面的声明,浏览器能够显示元素,并且您实际上可以为元素设置一个值,这样我们就可以看到组件是如何在页面中组合在一起的。在模板处理过程中,该值将被${singer.name}的评估结果所取代。
正如前面提到的,在为 web 应用编写接口时,我们可能需要隔离公共部分,并将它们包含在其他模板中,以避免代码重复。例如,页眉、页脚和菜单是应用所有页面的公共部分。百里香叶用称为片段的特殊模板来表示这些共同的部分。默认情况下,这些部分模板应该定义在/templates/fragments目录下,但是如果情况需要,该目录的名称可以是不同的。 10 这里描述了定义片段的语法,其中包含了一个footer.html示例:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>
ProSpring5 Singer Boot Application
</title>
</head>
<body>
<div th:fragment="footer" th:align="center">
<p>Copyright (c) 2017 by Iuliana Cosmina and Apress. All rights reserved.</p>
</div>
</body>
</html>
注意声明片段名称的th:fragment属性,通过插入它或替换某些元素,可以在其他模板中使用它。您可以在这里看到使用该片段的语法,这里显示了一个简单的index.html模板:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>
ProSpring5 Singer Boot Application
</title>
</head>
<body>
<div th:replace="∼{fragments/header :: header}">Header</div>
<div class="container">
...
</div>
</body>
</html>
有三种使用片段的方法:
th:replace用指定的片段替换其主机标签。- 插入指定的片段作为其主机标签的主体。
th:include插入这个片段的内容。 11
如果你想了解更多的百里香模板,最好的资源可以在官方网站 http://thymeleaf.org/documentation.html 找到。
这里我们需要介绍的最后一个目录是static目录。static目录包含应用模板的静态资源,比如 CSS 文件和图像。要在百里香模板中包含自定义 CSS 类,可以使用以下语法:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<link href="../../public/css/general.css"
th:href="@{css/general.css}" rel="stylesheet" media="screen"/>
<title>
ProSpring5 Singer Boot Application
</title>
</head>
<body>
...
</body>
</html>
href属性包含相对于模板的路径,在浏览器中打开模板时使用,以便加载general.css样式。在处理模板时使用th:href属性,{css/general.css}被解析为应用的 URL。
使用百里香叶扩展
在本节所述的应用中,使用了两种百里香叶扩展。因为我们正在使用百里香模板构建一个安全的应用,所以需要使用thymeleaf-extras-springsecurity4扩展。例如,当用户未登录时,应显示登录选项,当用户登录时,应显示注销选项。
这是百里叶附加模块,不是百里叶核心的一部分,但它完全由百里叶团队支持。尽管名称中有数字 4,但所需要的只是对一些特殊安全实用程序对象的支持。因此,它与 Spring Security 5.0.x 兼容,Spring Security 5.0 . x 是 Spring Boot 初学者安全库的可传递依赖项。让我们来看一个更加扩展的header.html文件,它包含一个导航菜单,为授权用户和未授权用户提供不同的菜单项:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<link href="../../public/css/bootstrap.min.css"
th:href="@{/webjars/bootstrap/3.3.7-1/css/bootstrap.min.css}"
rel="stylesheet" media="screen"/>
<script src="http://cdn.jsdelivr.net/webjars/jquery/3.2.1/jquery.min.js"
th:src="@{/webjars/jquery/3.2.1/jquery.min.js}"></script>
<title>
ProSpring5 Singer Boot Application
</title>
</head>
<body>
<div class="container">
<div th:fragment="header">
<nav class="navbar navbar-inverse">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="#" th:href="@{/}">ProSpring 5</a>
</div>
<ul class="nav navbar-nav">
<li><a href="#" th:href="@{/singers}">Singers</a></li>
<li><a href="#" th:href="@{/singers/new}">Add Singer</a></li>
</ul>
<ul class="nav navbar-nav navbar-right" >
<li th:if="${#authorization.expression('!isAuthenticated()')}">
<a href="/login" th:href="@{/login}">
<span class="glyphicon glyphicon-log-in"></span> Log in
</a>
</li>
<li th:if="${#authorization.expression('isAuthenticated()')}">
<a href="/logout" th:href="@{#}" onclick="$('#form').submit();">
<span class="glyphicon glyphicon-log-out"></span> Logout
</a>
<form style="visibility: hidden" id="form" method="post" action="#"
th:action="@{/logout}"></form>
</li>
</ul>
</div>
</nav>
</div>
</div>
</body>
</html>
这个模块提供了一个名为org.thymeleaf.extras.springsecurity4.dialect.SpringSecurityDialect的新方言,其中包含了#authorization对象(在前面的示例中使用过),这是一个表达式实用程序对象,具有基于表达式、URL 和访问控制列表检查授权的方法。 12 这种方言是 Spring Boot 配置出来的。
此应用中使用的第二个百里香叶扩展是thymeleaf-extras-java8time。这个依赖项必须显式地添加到项目的类路径中,并为 Java 8 Time API 提供支持。这个扩展也得到官方百里香团队的全力支持。在表达式求值期间,它将一个#temporals(以及其他类似#dates的)对象添加到ApplicationContext中,作为一个实用对象处理器。这意味着可以评估对象图导航语言(OGNL)和 Spring 表达式语言(SpringEL)中的表达式。让我们考虑显示歌手的详细信息所需的模板;它被称为show.html模板。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<link href="../../public/css/bootstrap.min.css"
th:href="@{/webjars/bootstrap/3.3.7-1/css/bootstrap.min.css}"
rel="stylesheet" media="screen"/>
<script src="http://cdn.jsdelivr.net/webjars/jquery/3.2.1/jquery.min.js"
th:src="@{/webjars/jquery/3.2.1/jquery.min.js}"></script>
<title>
ProSpring5 Singer Boot Application
</title>
</head>
<body>
<div th:replace="∼{fragments/header :: header}">Header</div>
<div class="container">
<h1>Singer Details</h1>
<div>
<form class="form-horizontal">
<div class="form-group">
<label class="col-sm-2 control-label">First Name:</label>
<div class="col-sm-10">
<p class="form-control-static" th:text="${singer.firstName}">
Singer First Name
</p>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">Last Name:</label>
<div class="col-sm-10">
<p class="form-control-static" th:text="${singer.lastName}">
Singer Last Name
</p>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">Description:</label>
<div class="col-sm-10">
<p class="form-control-static" th:text="${singer.description}">
Singer Description
</p>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">BirthDate:</label>
<div class="col-sm-10">
<p class="form-control-static"
th:text="${#dates.format(singer.birthDate, 'dd-MMM-yyyy')}">
Singer BirthDate
</p>
</div>
</div>
</form>
</div>
<div th:insert="∼{fragments/footer :: footer}">
(c) 2017 Iuliana Cosmina & Apress</div>
</div>
</body>
</html>
在前面的例子中,java.util.Date类型的singer.birthDate是使用#dates实用程序对象格式化的。在这种情况下,不需要日期格式化程序对象,接受的日期模式被硬编码在模板中。
这不太实际吧?
假设我们定义了一个DateFormatter类,如下面的代码片段所示:
package com.apress.prospring5.ch16.util;
import org.springframework.format.Formatter;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
public class DateFormatter implements Formatter<Date> {
public static final SimpleDateFormat formatter =
new SimpleDateFormat("yyyy-MM-dd");
@Override
public Date parse(String s, Locale locale) throws ParseException {
return formatter.parse(s);
}
@Override
public String print(Date date, Locale locale) {
return formatter.format(date);
}
}
让我们使用一个最小的 web 配置类在应用类路径中配置这个格式化程序。
package com.apress.prospring5.ch16;
import com.apress.prospring5.ch16.util.DateFormatter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/index").setViewName("index");
registry.addViewController("/").setViewName("index");
registry.addViewController("/login").setViewName("login");
}
@Override
public void addFormatters(FormatterRegistry formatterRegistry) {
formatterRegistry.addFormatter(dateFormatter());
}
@Bean
public DateFormatter dateFormatter() {
return new DateFormatter();
}
}
一旦在应用中注册了日期格式化程序,就可以通过双括号语法在百里香模板中使用它。所以这个:
<p class="form-control-static"
th:text="${#dates.format(singer.birthDate, 'dd-MMM-yyyy')}">
Singer BirthDate
</p>
可以改写成这样:
<p class="form-control-static" th:text="${{singer.birthDate}}">
Singer BirthDate
</p>
因此,删除了日期模式的硬编码,可以从外部修改日期格式化程序类,而无需对百里香模板进行任何更改。这样比较实际吧?
使用 Webjars
在最近的百里香模板示例中,您可能已经注意到了<head>元素中的一些奇怪链接。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<link href="../../public/css/bootstrap.min.css"
th:href="@{/webjars/bootstrap/3.3.7-1/css/bootstrap.min.css}"
rel="stylesheet" media="screen"/>
<script src="http://cdn.jsdelivr.net/webjars/jquery/3.2.1/jquery.min.js"
th:src="@{/webjars/jquery/3.2.1/jquery.min.js}"></script>
<title>
ProSpring5 Singer Boot Application
</title>
</head>
<body>
...
</body>
</html>
自从 Bootstrap 13 出现以来,创建一个看起来令人惊叹的 HTML 网页变得更加容易。Bootstrap 是最流行的 HTML、CSS 和 JS 框架,用于在 Web 上开发响应迅速、移动优先的项目。要在模板中使用 Bootstrap,只需链接到模板中的 Bootstrap CSS 文件。很长一段时间,CSS 样式和 JavaScript 代码是 web 应用的一部分,由开发人员手动复制到一个特殊的目录中。但是最近,创建网页最常用的框架,如 jQuery 和 Bootstrap,可以有不同的用法,将它们作为依赖项添加到应用中,打包成 Java 档案,称为 webjars。14web jar 部署在 Maven 中央存储库上,将由您的构建工具(Maven、Gradle 等)自动下载并添加到您的应用中。)一旦它们被声明为项目的依赖项。
在前面的例子中,JQuery 和 Bootstrap webjars 是使用th:href属性声明的。将它们链接在模板文件中可以确保所有的引导类都应用于模板中的元素(例如,class="container-fluid"),并且当模板被处理并且应用被部署到服务器时,所有的 jQuery 函数都是可访问的(onclick="$(’#form’).submit();)。
所有这些都设置好之后,如果您运行下面描述的SingerApplication类并访问http://localhost:8080/,您应该会看到index.html处理过的模板。
package com.apress.prospring5.ch16;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
@SpringBootApplication
public class SingerApplication {
private static Logger logger =
LoggerFactory.getLogger(SingerApplication.class);
public static void main(String... args) throws Exception {
ConfigurableApplicationContext ctx =
SpringApplication.run(SingerApplication.class, args);
assert (ctx != null);
logger.info("Application started...");
System.in.read();
ctx.close();
}
}
请注意,除了@SpringBootApplication之外,不需要任何其他注释。只要在类路径中添加 JPA 和 web Spring Boot 启动库,实体、控制器和存储库就会被自动选取。现在,如果您在浏览器中访问http://localhost:8080/ URL,您应该会看到应用的主页。如果一切顺利,并且 Bootstrap webjar 被正确处理,页面应该如图 16-7 所示。

图 16-7。
The front page of the Spring Boot application built in this section
摘要
在这一章中,我们讨论了许多与使用 Spring MVC 进行 web 开发相关的主题。首先,我们讨论了 MVC 模式的高级概念。然后我们介绍了 Spring MVC 的架构,包括它的WebApplicationContext层次结构、请求处理生命周期和配置。
接下来,我们学习了如何使用 Spring MVC 开发一个示例 singer 管理应用,使用 JSPX 作为视图技术。在开发样本的过程中,我们阐述了不同的领域。主题包括 i18n、主题化和 Apache Tiles 的模板支持。此外,我们还学习了如何使用 jQuery、jQuery UI 和其他 JavaScript 库来丰富界面。示例包括日期选择器、富文本编辑器和支持分页的数据网格。还讨论了如何使用 Spring Security 来保护 web 应用。
我们还体验了 Servlet 3.0 兼容的 web 容器的一些功能,比如基于代码的配置,而不是使用web.xml文件。我们演示了如何在 Servlet 3.0 环境中处理文件上传。
因为 Spring Boot 是 Spring 团队的神童特性,所以必须介绍如何使用它来构建一个成熟的 web 应用。当然,也必须引入最适合 Spring 的模板引擎百里香。
在下一章中,我们将通过介绍 WebSocket 来介绍 Spring 在 web 应用开发方面带来的更多特性。
Footnotes 1
如果你熟悉设计模式,你会发现DispatcherServlet是前端控制器设计模式的一种表达。
2
在 Spring 2.5 中,DefaultAnnotationHandlerMapping是默认的实现。从 Spring 3.1 开始,RequestMappingHandlerMapping已经成为默认的实现,只要遵守 Spring 命名控制器和方法的约定,它就支持请求映射到不带注释定义的处理程序。
3
更多详情请点击 https://tiles.apache.org/ 。
4
请记住,我们还没有在其中引入 Tiles 或 JavaScript。
5
这是一种攻击类型,包括入侵现有会话,以便在 web 应用中执行未经授权的命令。你可以在 https://en.wikipedia.org/wiki/Cross-site_request_forgery 了解更多信息。
6
你可以在官方百里香论坛 http://forum.thymeleaf.org/why-Thymeleaf-td3412902.html 找到完整的讨论。
7
Thymeleaf Extras Spring Security 库提供了一种方言,允许您将 Spring Security(版本 3.x 和 4.x)的几个授权和认证方面集成到基于 Thymeleaf 的应用中。 https://github.com/thymeleaf/thymeleaf-extras-springsecurity见。
8
这是一个百里叶附加模块,不是百里叶核心的一部分(因此它遵循自己的版本模式),但它完全受百里叶团队的支持。 https://github.com/thymeleaf/thymeleaf-extras-java8time见。
9
百里香官方网站提供了一个很好的使用百里香和 Spring 的教程。 http://thymeleaf.org/doc/tutorials/3.0/thymeleafspring.html#spring-mvc-configuration见。
10
一个 web 应用可以有多个主题,每个主题都可以用一组片段来表示。
11
从 3.0 版本开始,不建议这样做。
12
由于这个扩展不在本书的讨论范围内,如果你好奇,可以访问官方网站 https://github.com/thymeleaf/thymeleaf-extras-springsecurity 了解更多信息。
13
这里是官方网站: http://getbootstrap.com/ 。
14
十七、WebSocket
传统上,web 应用利用标准的请求/响应 HTTP 功能来提供客户端和服务器之间的通信。随着 Web 的发展,需要更多的交互能力,其中一些需要来自服务器的推/拉或实时更新。随着时间的推移,已经实现了各种方法,比如连续轮询、长轮询和 Comet。每一种都有其优缺点,WebSocket 协议试图从这些需求和不足中学习,创建一种更简单、更健壮的方法来构建交互式应用。HTML5 WebSocket 规范定义了一个 API,使网页能够使用 WebSocket 协议与远程主机进行双向通信。
本章涵盖了 WebSocket 协议和 Spring 框架提供的主要功能的高级概述。具体来说,本章涵盖以下主题:
- WebSocket 简介:我们提供了 WebSocket 协议的一般介绍。本节不打算作为 WebSocket 协议的详细参考,而是作为一个高层次的概述。 1
- 结合 Spring 使用 WebSocket:在这一节中,我们将深入探讨结合 Spring 框架使用 WebSocket 的一些细节;具体来说,我们将介绍如何使用 Spring 的 WebSocket API,如何利用 SockJS 作为不支持 WebSocket 的浏览器的后备选项,以及如何通过 SockJS/WebSocket 使用简单(或流式)面向文本的消息协议(STOMP)发送消息。
WebSocket 简介
WebSocket 是作为 HTML5 计划的一部分而开发的规范,允许在客户端和服务器之间发送消息的全双工单套接字连接。过去,需要实时更新功能的 web 应用会定期轮询服务器端组件以获取这些数据,打开多个连接或使用长时间轮询。
使用 WebSocket 进行双向通信可以避免为客户机(例如,web 浏览器)和 HTTP 服务器之间的双向通信执行 HTTP 轮询。WebSocket 协议旨在取代所有现有的利用 HTTP 作为传输的双向通信方法。WebSocket 的单套接字模型产生了一个更简单的解决方案,避免了每个客户端需要多个连接,并且开销更少——例如,不需要为每个消息发送一个 HTTP 头。
WebSocket 在其初始握手期间使用 HTTP,这反过来允许它通过标准的 HTTP (80)和 HTTPS (443)端口使用。WebSocket 规范定义了一个ws://和一个wss://方案来表示不安全和安全的连接。WebSocket 协议有两个部分:客户端和服务器之间的握手,然后是数据传输。WebSocket 连接是通过在客户端和服务器之间的初始握手期间,在相同的底层 TCP/IP 连接上发出从 HTTP 到 WebSocket 协议的升级请求来建立的。在通信的数据传输部分,客户端和服务器可以同时向对方发送消息,正如您所想象的,这为您的应用添加更强大的实时通信功能打开了大门。
通过 Spring 使用 WebSocket
从 4.0 版本开始,Spring 框架支持 WebSocket 风格的消息传递,以及作为应用级子协议的 STOMP。在框架内,你可以在spring-websocket模块中找到对 WebSocket 的支持,该模块兼容 JSR-356 (Java WebSocket)。 2
应用开发人员还必须认识到,尽管 WebSocket 带来了令人兴奋的新机会,但并不是所有的 web 浏览器都支持该协议。考虑到这一点,应用必须继续为用户工作,并利用某种后备技术来尽可能好地模拟预期的功能。为了处理这种情况,Spring 框架通过 SockJS 协议提供了透明的回退选项,这一点我们将在本章后面讨论。
与基于 REST 的应用不同,在基于 REST 的应用中,服务由不同的 URL 表示,WebSocket 使用单个 URL 来建立初始握手,数据通过相同的连接流动。这种类型的消息传递功能更符合传统的消息传递系统。从 Spring Framework 4 开始,Message等基于消息的核心接口已经从 Spring Integration 项目迁移到一个名为spring-messaging的新模块中,以支持 WebSocket 风格的消息传递应用。
当我们提到使用 STOMP 作为应用级子协议时,我们指的是通过 WebSocket 传输的协议。WebSocket 本身是一个底层协议,只是将字节转换成消息。应用需要理解通过网络发送的是什么,这就是 STOMP 之类的子协议发挥作用的地方。在初始握手期间,客户机和服务器可以使用Sec-WebSocket-Protocol头来定义要使用的子协议。虽然 Spring 框架提供了对 STOMP 的支持,但是 WebSocket 并没有特别的要求。
既然我们已经了解了什么是 WebSocket 以及 Spring 提供的支持,那么我们可以在哪里使用这项技术呢?考虑到 WebSocket 的单套接字特性及其提供连续双向数据流的能力,WebSocket 非常适合具有高消息传递频率并要求低延迟通信的应用。WebSocket 的良好候选应用可能包括游戏、实时小组协作工具、消息传递系统、对时间敏感的价格信息(如财务更新)等等。在考虑使用 WebSocket 来设计应用时,您必须考虑消息的频率和延迟需求。这将有助于确定是使用 WebSocket 还是 HTTP 长轮询。
使用 WebSocket API
正如本章前面提到的,WebSocket 只是将字节转换成消息,并在客户机和服务器之间传输它们。这些消息仍然需要被应用本身理解,这就是 STOMP 等子协议发挥作用的地方。如果您想直接使用较低级别的 WebSocket API,Spring 框架提供了一个 API,您可以与它进行交互来实现这一目的。当使用 Spring 的 WebSocket API 时,您通常会实现WebSocketHandler接口或者使用方便的子类,比如用于处理二进制消息的BinaryWebSocketHandler,用于 SockJS 消息的SockJsWebSocketHandler,或者用于处理基于String的消息的TextWebSocketHandler。在这个例子中,为了简单起见,我们将使用一个TextWebSocketHandler来通过 WebSocket 传递String消息。让我们先来看看如何利用 Spring WebSocket API 在底层接收和处理 WebSocket 消息。
如果您愿意,本章中的每个示例也可以通过 Java 配置进行配置。在我们看来,XML 名称空间以简洁的方式表示了配置方面,它将贯穿本章。有关 Java 配置的更多信息,请参考参考手册。首先,让我们从添加所需的依赖项开始。以下 Gradle 配置片段列出了这些库:
//pro-spring-15/build.gradle
ext {
springVersion = '5.0.0.RC3'
twsVersion = '9.0.0.M22'
...
spring = [
...
context : "org.springframework:spring-context:$springVersion",
webmvc : "org.springframework:spring-webmvc:$springVersion",
webSocket : "org.springframework:spring-websocket:$springVersion",
messaging : "org.springframework:spring-messaging:$springVersion"
]
...
web = [
...
jacksonDatabind: "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion",
tomcatWsApi : "org.apache.tomcat:tomcat-websocket-api:$twsVersion",
tomcatWsEmbed: "org.apache.tomcat.embed:tomcat-embed-websocket:$twsVersion",
httpclient : "org.apache.httpcomponents:httpclient:$httpclientVersion",
websocket : "javax.websocket:javax.websocket-api:1.1"
]
}
...
//pro-spring-15/chapter17/build.gradle
compile (web.tomcatWsApi) {
exclude module: 'tomcat-embed-core'
}
compile (web.tomcatWsEmbed) {
exclude module: 'tomcat-embed-core'
}
compile spring.context, spring.webSocket, spring.messaging,
spring.webmvc, web.websocket, misc.slf4jJcl,
misc.logback, misc.lang3, web.jacksonDatabind
在下一个配置片段中,您可以看到我们需要配置的WEB-INF/web.xml文件的内容,这样我们就可以将 WebSocket 与标准的 Spring MVC dispatcher servlet 一起使用:
<web-app
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<display-name>Spring WebSocket API Sample</display-name>
<servlet>
<servlet-name>websocket</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/root-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>websocket</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
我们首先利用 Spring 的DispatcherServlet创建 servlet 定义,为它提供一个配置文件(/WEB-INF/spring/root-context.xml)。然后我们提供 servlet 映射,表明所有请求都应该通过DispatcherServlet。
现在让我们继续并创建根上下文文件,它包含 WebSocket 配置,如下所示:
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
http://www.springframework.org/schema/websocket/spring-websocket.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<websocket:handlers>
<websocket:mapping path="/echoHandler" handler="echoHandler"/>
</websocket:handlers>
<mvc:default-servlet-handler/>
<mvc:view-controller path= "/" view-name="/static/index.html" />
<bean id="echoHandler"
class="com.apress.prospring5.ch17.EchoHandler"/>
</beans>
首先,我们配置一个名为index.html的静态资源。该文件包含用于与后端 WebSocket 服务通信的静态 HTML 和 JavaScript。然后,使用websocket名称空间,我们配置我们的处理程序和相应的 bean 来处理请求。在本例中,我们定义了一个处理程序映射,它在/echoHandler接收请求,并使用 ID 为echoHandler的 bean 接收消息,并通过将所提供的消息回显到客户端来做出响应。
前面的配置对您来说可能不太熟悉,因为我们在本书中没有使用很多 XML 配置。所以,让我们切换到 Java 配置类。下面是 Spring MVC 的配置:
package com.apress.prospring5.ch17.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {"com.apress.prospring5.ch17"})
public class WebConfig implements WebMvcConfigurer {
// <=> <mvc:default-servlet-handler/>
@Override
public void configureDefaultServletHandling(
DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
// <=> <mvc:view-controller .../>
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("/static/index.html");
}
}
让我们继续使用替换web.xml来配置DispatcherServlet的类。
package com.apress.prospring5.ch17.config;
import org.springframework.web.servlet.support.
AbstractAnnotationConfigDispatcherServletInitializer;
public class WebInitializer extends
AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[]{
WebSocketConfig.class
};
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[]{
WebConfig.class
};
}
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
}
WebConfig类包含一个 Spring MVC 应用的基础设施,因为我们希望在使用 Java 配置时遵守关注点分离的原则,所以我们需要一个不同的配置类来支持 WebSocket 通信。该类必须实现定义回调方法的WebSocketConfigurer接口,以配置 WebSocket 请求处理。
package com.apress.prospring5.ch17.config;
import com.apress.prospring5.ch17.EchoHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(echoHandler(), "/echoHandler");
}
@Bean
public EchoHandler echoHandler() {
return new EchoHandler();
}
}
需要将@EnableWebSocket注释添加到@Configuration类中,以配置 WebSocket 请求的处理。
现在我们准备实现一个TextWebSocketHandler ( src/main/java/com/apress/prospring5/ch17/EchoHandler.java)的子类来帮助我们以一种方便的方式处理基于字符串的消息,如下所示:
package com.apress.prospring5.ch17;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.io.IOException;
public class EchoHandler extends TextWebSocketHandler {
@Override
public void handleTextMessage(WebSocketSession session,
TextMessage textMessage) throws IOException {
session.sendMessage(new TextMessage(textMessage.getPayload()));
}
}
正如您所看到的,这是一个基本的处理程序,它接收所提供的消息,并简单地将其回显到客户端。接收到的 WebSocket 消息的内容包含在getPayload()方法中。
这差不多就是后端所需的全部内容。鉴于EchoHandler是一个典型的 Spring bean,您可以做任何您在普通 Spring 应用中会做的事情,比如注入服务,来执行这个处理程序可能需要做的任何功能。
现在让我们创建一个简单的前端客户端,在这里我们可以与后端 WebSocket 服务进行交互。前端是一个简单的 HTML 页面,带有一点 JavaScript,使用浏览器的 API 进行 WebSocket 连接;它还包含一些 jQuery 来处理按钮点击事件和数据显示。前端应用将能够连接、断开、发送消息,并在屏幕上显示状态更新。下面的代码片段显示了前端客户端页面的代码(src/main/webapp/static/index.html):
<html>
<head>
<meta charset="UTF-8">
<title>WebSocket Tester</title>
<script language="javascript" type="text/javascript"
src="http://code.jquery.com/jquery-2.1.1.min.js"></script>
<script language="javascript" type="text/javascript">
var ping;
var websocket;
jQuery(function ($) {
function writePing(message) {
$('#pingOutput').append(message + '\n');
}
function writeStatus(message) {
$("#statusOutput").val($("#statusOutput").val() + message + '\n');
}
function writeMessage(message) {
$('#messageOutput').append(message + '\n')
}
$('#connect')
.click(function doConnect() {
websocket = new WebSocket($("#target").val());
websocket.onopen = function (evt) {
writeStatus("CONNECTED");
var ping = setInterval(function () {
if (websocket != "undefined") {
websocket.send("ping");
}
}, 3000);
};
websocket.onclose = function (evt) {
writeStatus("DISCONNECTED");
};
websocket.onmessage = function (evt) {
if (evt.data === "ping") {
writePing(evt.data);
} else {
writeMessage('ECHO: ' + evt.data);
}
};
websocket.onerror = function (evt) {
onError(writeStatus('ERROR:' + evt.data))
};
});
$('#disconnect')
.click(function () {
if (typeof websocket != 'undefined') {
websocket.close();
websocket = undefined;
} else {
alert("Not connected.");
}
});
$('#send')
.click(function () {
if (typeof websocket != 'undefined') {
websocket.send($('#message').val());
} else {
alert("Not connected.");
}
});
});
</script>
</head>
<body>
<h2>WebSocket Tester</h2> Target:
<input id="target" size="40"
value="ws://localhost:8080/websocket-api/echoHandler"/>
<br/>
<button id="connect">Connect</button>
<button id="disconnect">Disconnect</button>
<br/>
<br/>Message:
<input id="message" value=""/>
<button id="send">Send</button>
<br/>
<p>Status output:</p>
<pre><textarea id="statusOutput" rows="10" cols="50"></textarea></pre>
<p>Message output:</p>
<pre><textarea id="messageOutput" rows="10" cols="50"></textarea></pre>
<p>Ping output:</p>
<pre><textarea id="pingOutput" rows="10" cols="50"></textarea></pre>
</body>
</html>
下面的代码片段提供了一个 UI,允许我们回调 WebSocket API 并观察屏幕上出现的实时结果。
构建项目并将其部署到您的 web 容器中。然后导航到http://localhost:8080/websocket- api/index.html调出 UI。单击 Connect 按钮后,您会在状态输出文本区域中注意到一条CONNECTED消息,并且每三秒会在 ping 输出文本区域中显示一条 Ping 消息。继续在信息文本框中输入信息,然后点击发送按钮。该消息将被发送到后端 WebSocket 服务,并显示在消息输出框中。当您发送完消息后,可以随意点击 Disconnect 按钮,您将在状态输出文本区看到一条DISCONNECTED消息。在重新连接到 WebSocket 服务之前,您将无法再发送任何消息或再次断开连接。虽然这个例子利用了底层 WebSocket API 之上的 Spring 抽象,但是您可以清楚地看到这项技术可以为您的应用带来的令人兴奋的可能性。现在让我们看看当浏览器不支持 WebSocket 并且需要一个回退选项时,如何处理这个功能。您可以使用 http://websocket.org/echo.html 等网站测试您的浏览器的兼容性。
使用 SockJS
因为并非所有的浏览器都支持 WebSocket,并且应用仍然需要为最终用户正确运行,所以 Spring 框架提供了一个利用 SockJS 的后备选项。使用 SockJS 将在运行时提供尽可能接近 WebSocket 的行为,而无需更改应用端代码。SockJS 协议通过 JavaScript 库在客户端使用。Spring 框架的spring-websocket模块包含相关的 SockJS 服务器端组件。当使用 SockJS 提供无缝回退选项时,客户端将首先通过使用路径/info向服务器发送 GET 请求,以从服务器获取传输信息。SockJS 将首先尝试使用 WebSocket,然后是 HTTP streaming,最后是 HTTP long polling 作为最后的手段。要了解更多关于 SockJS 及其各种项目的信息,请参见 https://github.com/sockjs 。
通过 websocket 名称空间支持启用 SockJS 很简单,只需要在<websocket:handlers>块中添加一条指令。让我们构建一个与原始 WebSocket API 类似的应用,但是使用 SockJS。src/main/webapp/WEB-INF/spring/root-context.xml文件现在看起来像这样:
<beans ...>
<websocket:handlers>
<websocket:mapping path="/echoHandler" handler="echoHandler"/>
<websocket:sockjs/>
</websocket:handlers>
<mvc:default-servlet-handler/>
<mvc:view-controller path= "/" view-name="/static/index.html" />
<bean id="echoHandler" class="com.apress.prospring5.ch17.EchoHandler"/>
</beans>
注意已经添加了<websocket:sockjs>标签。在最基本的层面上,这就是启用 SockJS 所需的全部内容。我们可以重用 WebSocket API 示例中的EchoHandler类,因为我们将提供相同的功能。
这个<websocket:sockjs/>名称空间标记还提供其他属性来设置配置选项,例如处理会话 cookies(默认情况下启用)、定制客户端库加载位置(在撰写本文时,默认为 https://d1fxtkz8shb9d2.cloudfront.net/sockjs-0.3.4.min.js )、心跳配置、消息大小限制等等。应该根据您的应用需求和传输类型,适当地检查和配置这些选项。在web.xml文件中,不需要添加太多东西来反映我们的 SockJS servlet,如下所示:
<web-app
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<display-name>Spring SockJS API Sample</display-name>
<servlet>
<servlet-name>sockjs</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/root-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>sockjs</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
现在您可能已经猜到了,Java 配置如下。要使用 SockJS 支持 WebSocket 通信,需要做两处修改。首先,我们需要支持异步消息传递,这在前面使用<async-supported>true</async-supported>的配置中是启用的。这是通过用另一个注释:EnableAsync注释一个 Java 配置类(已经用@Configuration注释的类)来完成的。如果我们查看官方的 Spring Javadoc,您会发现这个注释启用了 Spring 的异步方法执行功能,从而为整个 Spring 应用上下文启用了注释驱动的异步处理。 3
package com.apress.prospring5.ch17.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@EnableWebMvc
@EnableAsync
@ComponentScan(basePackages = {"com.apress.prospring5.ch17"})
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureDefaultServletHandling(
DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("/static/index.html");
}
}
第二个变化必须在WebSocketConfig中完成,以使 SockJS 支持我们的处理程序。
package com.apress.prospring5.ch17.config;
import com.apress.prospring5.ch17.EchoHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.*;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(echoHandler(),
"/echoHandler").withSockJS();
}
@Bean
public EchoHandler echoHandler() {
return new EchoHandler();
}
}
接下来,我们将需要创建一个 HTML 页面,就像我们在 WebSocket API 示例中所做的那样,但是这次利用 SockJS 来处理传输协商。最显著的区别是我们直接使用 SockJS 库而不是 WebSocket,并且利用典型的http://方案而不是ws://来连接到端点。简单的 HTML 客户端代码如下所示:
<html>
<head>
<meta charset="UTF-8">
<title>SockJS Tester</title>
<script language="javascript" type="text/javascript"
src="https://d1fxtkz8shb9d2.cloudfront.net/sockjs-0.3.4.min.js">
</script>
<script language="javascript" type="text/javascript"
src="http://code.jquery.com/jquery-2.1.1.min.js">
</script>
<script language="javascript" type="text/javascript">
var ping;
var sockjs;
jQuery(function ($) {
function writePing(message) {
$('#pingOutput').append(message + '\n');
}
function writeStatus(message) {
$("#statusOutput").val($("#statusOutput").val() + message + '\n');
}
function writeMessage(message) {
$('#messageOutput').append(message + '\n')
}
$('#connect')
.click(function doConnect() {
sockjs = new SockJS($("#target").val());
sockjs.onopen = function (evt) {
writeStatus("CONNECTED");
var ping = setInterval(function () {
if (sockjs != "undefined") {
sockjs.send("ping");
}
}, 3000);
};
sockjs.onclose = function (evt) {
writeStatus("DISCONNECTED");
};
sockjs.onmessage = function (evt) {
if (evt.data === "ping") {
writePing(evt.data);
} else {
writeMessage('ECHO: ' + evt.data);
}
};
sockjs.onerror = function (evt) {
onError(writeStatus('ERROR:' + evt.data))
};
});
$('#disconnect')
.click(function () {
if(typeof sockjs != 'undefined') {
sockjs.close();
sockjs = undefined;
} else {
alert("Not connected.");
}
});
$('#send')
.click(function () {
if(typeof sockjs != 'undefined') {
sockjs.send($('#message').val());
} else {
alert("Not connected.");
}
});
});
</script>
</head>
<body>
<h2>SockJS Tester</h2>
Target:
<input id="target" size="40"
value="http://localhost:8080/sockjs/echoHandler"/>
<br/>
<button id="connect">Connect</button>
<button id="disconnect">Disconnect</button>
<br/>
<br/>Message:
<input id="message" value=""/>
<button id="send">Send</button>
<br/>
<p>Status output:</p>
<pre><textarea id="statusOutput" rows="10" cols="50"></textarea></pre>
<p>Message output:</p>
<pre><textarea id="messageOutput" rows="10" cols="50"></textarea></pre>
<p>Ping output:</p>
<pre><textarea id="pingOutput" rows="10" cols="50"></textarea></pre>
</body>
</html>
实现了新的 SockJS 代码后,将项目构建并部署到容器中,并导航到位于http://localhost:8080/sockjs/index.html的 UI,它具有 WebSocket 示例的所有相同特性和功能。要测试 SockJS 回退功能,请尝试在浏览器中禁用 WebSocket 支持。例如,在 Firefox 中,导航到about:config页面,然后搜索network.websocket.enabled。将此设置切换为 false,重新加载示例 UI,然后重新连接。利用 Live HTTP Headers 等工具,您可以检查从浏览器到服务器的流量,以进行验证。验证行为后,将 Firefox 设置network.websocket.enabled切换回 true,重新加载页面,然后重新连接。现在,通过实时 HTTP 报头观看流量将向您展示 WebSocket 握手。在这个简单的例子中,一切都应该像 WebSocket API 一样工作。
使用 STOMP 发送信息
当使用 WebSocket 时,通常会使用 STOMP 这样的子协议作为客户端和服务器之间的通用格式,这样两端都知道会发生什么并做出相应的反应。Spring 框架直接支持 STOMP,我们将在示例中使用这个协议。
STOMP 是一种简单的、基于帧的消息传递协议,它以 HTTP 为模型,可以在任何可靠的双向流网络协议(如 WebSocket)上使用。STOMP 有标准的协议格式;JavaScript 客户端支持在浏览器中发送和接收消息,也可以插入支持 STOMP 的传统消息代理,如 RabbitMQ 和 ActiveMQ。开箱即用,Spring 框架支持一个简单的代理,它处理订阅请求,并在内存中向连接的客户端广播消息。在这个示例中,我们将利用简单的代理,而将全功能的代理设置留给您作为练习。 4
关于 STOMP 协议的完整描述,参见 http://stomp.github.io/stomp-specification-1.2.html 。
在 STOMP 示例中,我们将创建一个简单的股票报价器应用,显示一些预定义的股票代码、它们的当前价格以及价格变化时的时间戳。还可以通过 UI 添加新的股票代码和起始价格。任何连接客户端(即选项卡中的其他浏览器或其他网络上的全新客户端)在订阅消息广播时都会看到相同的数据。每一秒钟,每只股票的价格都会更新为一个新的随机金额,时间戳也会更新。
为了确保您的客户能够使用股票行情应用,即使他们的浏览器不支持 WebSocket,我们也将再次利用 SockJS 来透明地处理任何传输交换。在深入研究代码之前,值得注意的是 STOMP 消息支持是由spring-messaging库提供的。
现在让我们首先创建Stock域对象,它保存股票的信息,如代码和价格,如下所示:
package com.apress.prospring5.ch17;
import java.util.Date;
import java.io.Serializable;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
public class Stock implements Serializable {
private static final long serialVersionUID = 1L;
private static final String DATE_FORMAT = "MMM dd yyyy HH:mm:ss";
private String code;
private double price;
private Date date = new Date();
private DateFormat dateFormat =
new SimpleDateFormat(DATE_FORMAT);
public Stock() { }
public Stock(String code, double price) {
this.code = code;
this.price = price;
}
//setters and getters
...
}
现在我们需要添加一个 MVC 控制器来处理传入的请求,如下所示:
package com.apress.prospring5.ch17;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.stereotype.Controller;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Random;
@Controller
public class StockController {
private TaskScheduler taskScheduler;
private SimpMessagingTemplate simpMessagingTemplate;
private List<Stock> stocks = new ArrayList<Stock>();
private Random random = new Random(System.currentTimeMillis());
public StockController() {
stocks.add(new Stock("VMW", 1.00d));
stocks.add(new Stock("EMC", 1.00d));
stocks.add(new Stock("GOOG", 1.00d));
stocks.add(new Stock("IBM", 1.00d));
}
@MessageMapping("/addStock")
public void addStock(Stock stock) throws Exception {
stocks.add(stock);
broadcastUpdatedPrices();
}
@Autowired
public void setSimpMessagingTemplate(
SimpMessagingTemplate simpMessagingTemplate) {
this.simpMessagingTemplate = simpMessagingTemplate;
}
@Autowired
public void setTaskScheduler(TaskScheduler taskScheduler) {
this.taskScheduler = taskScheduler;
}
private void broadcastUpdatedPrices() {
for(Stock stock : stocks) {
stock.setPrice(stock.getPrice() +
(getUpdatedStockPrice() * stock.getPrice()));
stock.setDate(new Date());
}
simpMessagingTemplate.convertAndSend("/topic/price", stocks);
}
private double getUpdatedStockPrice() {
double priceChange = random.nextDouble() * 5.0;
if (random.nextInt(2) == 1) {
priceChange = -priceChange;
}
return priceChange / 100.0;
}
@PostConstruct
private void broadcastTimePeriodically() {
taskScheduler.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
broadcastUpdatedPrices();
}
}, 1000);
}
}
控制器在这里做一些事情。首先,我们将一些预定义的股票代码和它们的起始价格添加到列表中,用于演示目的。然后我们定义一个方法addStock,它接受一个Stock对象,将其添加到股票列表中,然后将股票广播给所有订阅者。在广播股票时,我们遍历所有添加的股票,更新每只股票的价格,然后通过有线SimpMessagingTemplate将它们发送给/topic/price的所有订户。您还使用了一个TaskExecutor实例,每秒钟向所有订阅的客户端连续广播更新的股票价格列表。
有了控制器,现在让我们创建向客户端显示的 HTML UI(src/main/webapp/static/in-dex.html),如下面的 HTML 片段所示:
<html>
<head>
<title>Stock Ticker</title>
<script src="https://d1fxtkz8shb9d2.cloudfront.net/sockjs-0.3.4.min.js"/>
<script src="http://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.2/stomp.min.js"/>
<script src="http://code.jquery.com/jquery-2.1.1.min.js"/>
<script>
var stomp = Stomp.over(new SockJS("/stomp/ws"));
function displayStockPrice(frame) {
var prices = JSON.parse(frame.body);
$('#price').empty();
for (var i in prices) {
var price = pricesi;
$('#price').append(
$('<tr>').append(
$('<td>').html(price.code),
$('<td>').html(price.price.toFixed(2)),
$('<td>').html(price.dateFormatted)
)
);
}
}
var connectCallback = function () {
stomp.subscribe('/topic/price', displayStockPrice);
};
var errorCallback = function (error) {
alert(error.headers.message);
};
stomp.connect("guest", "guest", connectCallback, errorCallback);
$(document).ready(function () {
$('.addStockButton').click(function (e) {
e.preventDefault();
var jsonstr = JSON.stringify({ 'code': $('.addStock .code').val(),
'price': Number($('.addStock .price').val()) });
stomp.send("/app/addStock", {}, jsonstr);
return false;
});
});
</script>
</head>
<body>
<h1><b>Stock Ticker</b></h1>
<table border="1">
<thead>
<tr>
<th>Code</th>
<th>Price</th>
<th>Time</th>
</tr>
</thead>
<tbody id="price"></tbody>
</table>
<p class="addStock">
Code: <input class="code"/><br/>
Price: <input class="price"/><br/>
<button class="addStockButton">Add Stock</button>
</p>
</body>
</html>
与过去的例子类似,我们在 JavaScript 中混合了一些 HTML 来更新显示。 5 我们利用 jQuery 更新 HTML 数据,利用 SockJS 提供传输选择,利用 STOMP JavaScript 库stomp.js与服务器通信。通过 STOMP 消息发送的数据是以 JSON 格式编码的,这是我们在事件中提取的。在 STOMP 连接上,我们订阅/topic/price来接收股票价格更新。
现在让我们在root-context.xml ( src/main/webapp/WEB-INF/spring/root-context.xml)中配置内置的 STOMP broker。
<beans ...">
<mvc:annotation-driven />
<mvc:default-servlet-handler/>
<mvc:view-controller path= "/" view-name="/static/index.html" />
<context:component-scan base-package="com.apress.prospring5.ch17" />
<websocket:message-broker application-destination-prefix="/app">
<websocket:stomp-endpoint path="/ws">
<websocket:sockjs/>
</websocket:stomp-endpoint>
<websocket:simple-broker prefix="/topic"/>
</websocket:message-broker>
<bean id="taskExecutor"
class="org.springframework.core.task.SimpleAsyncTaskExecutor"/>
</beans>
在大多数情况下,这种配置应该看起来很熟悉。在这个例子中,我们通过使用 WebSocket 名称空间来配置message-broker,定义一个 STOMP 端点,并启用 SockJS。我们还配置了订阅者用来接收消息的前缀。配置的TaskExecutor用于提供控制器类中定义的时间间隔内的股票报价。当使用名称空间支持时,SimpMessagingTemplate会自动为我们创建,并且可以注入到我们的 beans 中。
现在剩下要做的就是配置我们的web.xml文件(src/main/webapp/WEB-INF/web.xml),如下面的配置片段所示:
<web-app
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<display-name>Spring STOMP Sample</display-name>
<servlet>
<servlet-name>stomp</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/root-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>stomp</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
XML 配置已经讨论过了,所以让我们切换到使用 Java 配置类的非传统配置。要启用 Spring 异步调用和任务执行,必须用@EnableAsync注释WebConfig,并且必须声明一个org.springframework.core.task.TaskExecutor类型的 bean。
package com.apress.prospring5.ch17.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@EnableWebMvc
@EnableAsync
@ComponentScan(basePackages = {"com.apress.prospring5.ch17"})
public class WebConfig implements WebMvcConfigurer {
// <=> <mvc:default-servlet-handler/>
@Override
public void configureDefaultServletHandling(
DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
// <=> <mvc:view-controller .../>
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("/static/index.html");
}
@Bean TaskExecutor taskExecutor() {
return new SimpleAsyncTaskExecutor();
}
}
这个职业需要更大的改变。这个类现在必须实现org.springframework.web.s或者扩展AbstractWebSocketMessageBrokerConfigurer抽象类,这将帮助我们决定我们实际上想要实现什么方法。该类现在将定义使用简单消息协议(如来自 WebSocket 客户端的 STOMP)配置消息处理的方法。此外,还必须用一个名为@EnableWebSocketMessageBroker的不同注释对其进行注释,这将使用更高级别的消息传递子协议通过 WebSocket 实现代理支持的消息传递。
package com.apress.prospring5.ch17.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConf import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends
AbstractWebSocketMessageBrokerConfigurer {
// <=> <websocket:stomp-endpoint ... />
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").withSockJS();
}
//<=> websocket:message-broker../>
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.setApplicationDestinationPrefixes("/app");
config.enableSimpleBroker("/topic");
}
}
我们在前面的配置中覆盖的方法是它们被注释的 XML 元素的等价物。它们用于配置 STOMP 端点和消息代理。
摘要
在本章中,我们介绍了 WebSocket 的一般概念。我们讨论了 Spring 框架对低级 WebSocket API 的支持,然后继续使用 SockJS 作为后备选项来选择适当的传输,这取决于客户端浏览器。最后,我们引入了 STOMP 作为 WebSocket 子协议,用于在客户机和服务器之间传递消息。对于所有的例子,我们给出了 XML 和 Java 配置类,因为商业中的总体趋势是完全放弃 XML。
在下一章,我们将讨论 Spring 子项目,您可以将它们混合到您的应用中,以提供更加健壮的功能。
Footnotes 1
有关协议的详细信息,请参考 RFC-6455 中的 http://tools.ietf.org/html/rfc6455 或 https://www.websocket.org/ 。
2
www.oracle.com/technetwork/articles/java/jsr356-1937161.html见。
3
你可以在这里找到 Spring Javadoc 的官方网站: https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/scheduling/annotation/EnableAsync.html 。
4
5
您将 HTML 和 JavaScript 结合起来的原因是为了尽可能保持 Spring MVC 配置的简单,尽管这不符合常识性的编程规则。
十八、Spring 项目:Batch、Integration、XD 等等
本章对 Spring portfolio 中的几个项目进行了高度概括,特别是 Spring Batch、Integration、XD 以及 Spring Framework 第 5 版中添加的几个显著特性。本章并不打算详细介绍每个项目,而是提供足够的信息和示例来帮助您开始。Spring portfolio 包含的项目比本章讨论的要多得多,但是我们觉得这里介绍的项目被广泛使用,而且有些是新的和即将到来的项目。您可以在 http://spring.io/projects 查看 Spring 项目的完整列表。本章涵盖以下主题:
- Spring Batch:我们涵盖了 Spring 批处理框架的核心概念,包括它为开发人员提供了什么,我们还谈到了 Spring Batch 3.0 中新的 JSR-352 支持。
- Spring Integration:许多企业应用中都使用集成模式,Spring Integration 为实现这些模式提供了一个健壮的框架。我们在批处理示例的基础上展示了如何将 Spring Integration 作为工作流的一部分来启动批处理作业。
- Spring XD: Spring XD 将许多现有的 Spring 项目联系在一起,为大数据应用提供一个统一的、可扩展的系统。Spring XD 是一个分布式系统,专注于数据接收、实时分析、批处理和数据导出。我们展示了如何通过 shell 接口中的简单 DSL 利用 Spring XD 来实现批处理和集成示例中的应用。
- Spring 5 中引入的显著特性:关于 Spring Framework 5 的新特性有很多讨论,但是随着正式发布日期的临近,这个列表变得固定了。除了将完整的代码库迁移到 Java 8 等底层特性之外, 1 通过集成 Commons Logging bridge 模块(名为
spring-jcl而不是标准的 Commons Logging 2 并自动检测 Log4j 2.x、SLF4J 和 JUL,没有任何额外的桥),添加候选组件索引作为类路径扫描的替代,等等,还有其他一些值得注意的改进。本章涵盖了其中的三种。- 功能性 Web 框架:
spring-webflux是对spring-mvc的补充,建立在反应式基础上。由于 Reactive Streams API 是 Java 9 的官方组成部分,Spring Framework 5 流支持构建在 Project Reactor (http://projectreactor.io/)之上,它实现了 Reactive Streams API 规范。 - 与 Java 9 的完全互操作性:Spring Framework RC3 版本于 2017 年 7 月推出,并宣布将针对最近的 JDK 9 版本候选进行全面测试。Java 9 引入了很多有趣的特性,包括:Jigsaw 项目/Java 模块化,一个新的支持 HTTP 2 协议和 WebSocket 握手的 HTTP 客户端,一个改进的流程 API,改进的特性语法,如
try-with-resources,diamond 操作符和接口私有方法,一个用于反应式编程的发布-订阅框架,以及一组新的 API。官方的变化和特性列表可以在甲骨文网站上找到, 4 但是基本上只有两个特性与 Spring 相关:JDK 的模块化功能和反应式框架。 - 对 JUnit 5 Jupiter 5 的全面支持:JUnit 5 的 Jupiter 编程和扩展模型在 Spring Framework 5 中得到全面支持,包括对 Spring TestContext 框架中并行测试执行的支持。
- 功能性 Web 框架:
由于这些主题中的每一个都可能有自己的章节甚至书籍,涵盖每个项目及其各种产品的所有细节是不可能的。我们希望这些介绍和基本示例能够引起您的兴趣,从而进一步探索这些主题。
SpringBatch
Spring Batch 是一个批处理框架,是 Spring 项目组合的一部分。它是轻量级的、灵活的,旨在为开发人员提供以最少的努力创建健壮的批处理应用的能力。Spring Batch 附带了许多用于各种技术的现成组件,在大多数情况下,您甚至可以只使用提供的组件来构建您的批处理应用。
典型的批处理应用包括日常发票生成、薪资系统以及提取、转换、加载(ETL)流程。虽然这些是人们可能预先想到的典型示例,但是 Spring Batch 可以用于任何需要无人值守运行的流程,而不仅仅是这些场景。与所有其他 Spring 项目一样,Spring Batch 构建在核心 Spring 框架之上,您可以完全访问它的所有功能。
在较高层次上,批处理作业包含一个或多个步骤。每一步都可以提供执行单个工作单元的能力,这由一个小任务实现来表示,或者参与所谓的面向块的处理。对于面向块的处理,一个步骤利用一个ItemReader来读取某种形式的数据,一个可选的ItemProcessor来对该数据进行任何所需的转换,最后一个ItemWriter将数据写出来。步骤还具有各种配置属性,例如配置块大小(每个事务处理的数据量)、启用多线程执行、跳过限制等能力。可以在步骤级别和作业级别使用侦听器来接收批处理作业生命周期中发生的各种事件的通知,例如,在步骤开始之前、步骤结束时、在面向块的处理场景中等等。
虽然大多数作业可以以单线程、单进程的方式完美运行,但 Spring Batch 还提供了作业的伸缩和并行处理选项。目前,Spring Batch 提供了以下现成的可伸缩性选项:
- 多线程步骤:这是使一个步骤多线程化的最简单的方法。只需将您选择的一个
TaskExecutor实例添加到步骤配置中,面向块的处理设置中的每个项目块都将在其自己的执行线程中进行处理。 - 并行步骤:比方说,在工作开始时,您需要读入两个包含不同数据的大文件。首先,您可以创建两个步骤,一个将在另一个之后执行。如果这两个数据文件加载互不依赖,为什么不同时处理它们呢?对于这种情况,Spring Batch 允许您定义一个包含流元素的 split,并封装这些要并行执行的任务。
- 远程分块:这个可伸缩性选项允许您迈出一步,将工作远程分配给许多远程工作人员,并通过某种持久的中间件(如 AMQP 或 JMS)进行通信。当数据读取不是进程中的瓶颈,而块数据的写入和可选处理是瓶颈时,通常使用远程分块。数据块通过中间件发送,供从节点拾取和处理,然后从节点将数据块的处理状态反馈给主节点。
- 分区:这种可伸缩性选项通常在您想要处理一个范围的数据时使用,为每个范围使用线程。一个典型的场景是一个数据库表,其中填充了具有数字标识符列的数据。通过分区,您可以用一定数量的记录将待处理的数据“分区”到单独的线程中。Spring Batch 为开发人员提供了使用这种分区方案的能力,因为它高度依赖于手边的用例。分区可以在本地线程中完成,也可以外包给远程工作人员(类似于远程分块选项)。
批处理的一个基本但常见的用例是读入某种类型的文件,通常是带分隔符格式(例如,逗号分隔)的平面文件,然后需要将该文件加载到数据库中,在写入数据库之前可以选择处理每个记录。让我们看看如何在 Spring Batch 中实现这个用例。首先,我们需要添加所需的依赖项,如以下梯度配置所示:
//pro-spring-15/build.gradle
ext {
//spring libs
...
springBatchVersion = '4.0.0.M3'
...
spring = [
context : "org.springframework:spring-context:$springVersion",
jdbc : "org.springframework:spring-jdbc:$springVersion",
batchCore : "org.springframework.batch:spring-batch-core:$springBatchVersion"
...
]
misc = [
io : "commons-io:commons-io:2.5",
...
]
db = [
...
dbcp2 : "org.apache.commons:commons-dbcp2:$dbcpVersion",
h2 : "com.h2database:h2:$h2Version",
// needed for the Batch JSR-352 module
hsqldb: "org.hsqldb:hsqldb:2.4.0"
dbcp : "commons-dbcp:commons-dbcp:1.4",
]
}
...
//pro-spring-15/chapter18/build.gradle
dependencies {
if (!project.name.contains("boot")) {
compile(spring.jdbc) {
// exclude these as batchCore will bring them
// on as transitive dependencies
exclude module: 'spring-core'
exclude module: 'spring-beans'
exclude module: 'spring-tx'
}
compile spring.batchCore, db.dbcp2, db.h2, misc.io,
misc.slf4jJcl, misc.logback
}
}
在前面的配置中,您可以看到我们需要添加到 Spring 批处理项目(不是 Spring Boot 项目)的核心依赖项。这就是为什么我们有了f (!project.name.contains("boot"))条件;它防止版本明确设置的库与本章中 Spring Boot 项目的依赖项混合。
有了依赖关系,让我们深入代码。首先,我们创建一个域对象,它基于我们将要读取的文件中的数据表示一个Singer,如下所示:
package com.apress.prospring5.ch18;
public class Singer {
private String firstName;
private String lastName;
private String song;
... // setters & getters
@Override
public String toString() {
return "firstName: " + firstName + ", lastName: "
+ lastName + ", song: " + song;
}
}
接下来,让我们创建一个ItemProcessor的实现,用于将由Singer对象表示的每个歌手的名字、姓氏和歌曲转换为大写,如下所示:
package com.apress.prospring5.ch18;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.stereotype.Component;
@Component("itemProcessor")
public class SingerItemProcessor implements
ItemProcessor<Singer, Singer> {
private static Logger logger =
LoggerFactory.getLogger(SingerItemProcessor.class);
@Override
public Singer process(Singer singer) throws Exception {
String firstName = singer.getFirstName().toUpperCase();
String lastName = singer.getLastName().toUpperCase();
String song = singer.getSong().toUpperCase();
Singer transformedSinger = new Singer();
transformedSinger.setFirstName(firstName);
transformedSinger.setLastName(lastName);
transformedSinger.setSong(song);
logger.info("Transformed singer: " + singer + " Into: " +
transformedSinger);
return transformedSinger;
}
}
请注意,ItemProcessors在面向块的处理场景中不是必需的;只有ItemReader和ItemWriter是。我们在这里使用ItemProcessor作为一个例子,说明如何在写入之前转换数据。
接下来,我们将创建一个驻留在Step级别的StepExecutionListener实现,并告诉您该步骤完成后写入了多少记录,如下面的代码片段所示:
package com.apress.prospring5.ch18;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.listener.StepExecutionListenerSupport;
import org.springframework.stereotype.Component;
@Component
public class StepExecutionStatsListener extends
StepExecutionListenerSupport {
public static Logger logger = LoggerFactory.
getLogger(StepExecutionStatsListener.class);
@Override
public ExitStatus afterStep(StepExecution stepExecution) {
logger.info("--> Wrote: " + stepExecution.getWriteCount()
+ " items in step: " + stepExecution.getStepName());
return null;
}
}
StepExecutionListener也允许我们根据需要修改返回的ExitStatus值;否则,只需返回null以保持不变。至此,我们已经组装好了核心组件,但是在开始配置和调用代码之前,让我们先来看看数据模型和数据本身。这项工作的数据模型很简单(src/main/resources/support/singer.sql),这里显示了包含测试数据的src/main/resources/support/test-data.sql文件:
-- singer.sql
DROP TABLE singer IF EXISTS;
CREATE TABLE singer (
singer_id BIGINT IDENTITY NOT NULL PRIMARY KEY,
first_name VARCHAR(20),
last_name VARCHAR(20),
song VARCHAR(100)
);
-- test-data.sql
John,Mayer,Helpless
Eric,Clapton,Change The World
John,Butler,Ocean
BB,King,Chains And Things
现在,我们需要创建 Spring 批处理配置文件,定义作业,并设置嵌入式数据库和相关的作业组件。由于 XML 配置很麻烦,在本书的前一版本中已经讨论过,所以本章将只关注 Java 配置类。按照常识性编程规则的精神,您将通过为批处理配置和数据源配置分别创建一个单独的配置类来分离它们。下面是DataSourceConfig配置类:
package com.apress.prospring5.ch18.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import javax.sql.DataSource;
@Configuration
public class DataSourceConfig {
private static Logger logger = LoggerFactory.getLogger(DataSourceConfig.class);
@Bean
public DataSource dataSource() {
try {
EmbeddedDatabaseBuilder dbBuilder = new EmbeddedDatabaseBuilder();
return dbBuilder.setType(EmbeddedDatabaseType.H2)
.addScripts("classpath:/org/springframework/batch/core/schema-h2.sql",
"classpath:support/singer.sql" ).build();
} catch (Exception e) {
logger.error("Embedded DataSource bean cannot be created!", e);
return null;
}
}
}
由于这种配置现在应该很熟悉了,我们将只解释schema-h2.sql文件。该文件是spring-batch-core库的一部分,包含创建 Spring 批处理实用程序表所需的 DML 语句。
DataSourceConfig类将被导入到BatchConfig类中,如下所示:
package com.apress.prospring5.ch18.config;
import com.apress.prospring5.ch18.Singer;
import com.apress.prospring5.ch18.StepExecutionStatsListener;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.database.BeanPropertyItemSqlParameterSourceProvider;
import org.springframework.batch.item.database.JdbcBatchItemWriter;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper;
import org.springframework.batch.item.file.mapping.DefaultLineMapper;
import org.springframework.batch.item.file.transform.DelimitedLineTokenizer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.io.ResourceLoader;
import javax.sql.DataSource;
@Configuration
@EnableBatchProcessing
@Import(DataSourceConfig.class)
@ComponentScan("com.apress.prospring5.ch18")
public class BatchConfig {
@Autowired
private JobBuilderFactory jobs;
@Autowired
private StepBuilderFactory steps;
@Autowired DataSource dataSource;
@Autowired ResourceLoader resourceLoader;
@Autowired StepExecutionStatsListener executionStatsListener;
@Bean
public Job job(@Qualifier("step1") Step step1) {
return jobs.get("singerJob").start(step1).build();
}
@Bean
protected Step step1(ItemReader<Singer> reader,
ItemProcessor<Singer,Singer> itemProcessor,
ItemWriter<Singer> writer) {
return steps.get("step1").listener(executionStatsListener)
.<Singer, Singer>chunk(10)
.reader(reader)
.processor(itemProcessor)
.writer(writer)
.build();
}
@Bean
public ItemReader itemReader() {
FlatFileItemReader itemReader = new FlatFileItemReader();
itemReader.setResource(resourceLoader.getResource(
"classpath:support/test-data.csv"));
DefaultLineMapper lineMapper = new DefaultLineMapper();
DelimitedLineTokenizer tokenizer = new DelimitedLineTokenizer();
tokenizer.setNames("firstName","lastName","song");
tokenizer.setDelimiter(",");
lineMapper.setLineTokenizer(tokenizer);
BeanWrapperFieldSetMapper<Singer> fieldSetMapper =
new BeanWrapperFieldSetMapper<>();
fieldSetMapper.setTargetType(Singer.class);
lineMapper.setFieldSetMapper(fieldSetMapper);
itemReader.setLineMapper(lineMapper);
return itemReader;
}
@Bean
public ItemWriter itemWriter() {
JdbcBatchItemWriter<Singer> itemWriter = new JdbcBatchItemWriter<>();
itemWriter.setItemSqlParameterSourceProvider(
new BeanPropertyItemSqlParameterSourceProvider<>());
itemWriter.setSql("INSERT INTO singer (first_name, last_name, song)
VALUES (:firstName, :lastName, :song)");
itemWriter.setDataSource(dataSource);
return itemWriter;
}
}
虽然BatchConfig看起来很大,但它远没有 XML 配置那么大。现在是解释其中定义的每个 bean 的时候了。
- @
EnableBatchProcessing注释的工作方式与所有的@Enable*Spring 注释类似。这为构建批处理作业提供了一个基本配置。通过使用此注释对配置类进行注释,会发生以下情况:- 创建了一个
org.springframework.batch.core.scope.StepScope的实例。这个范围内的对象使用 Spring 容器作为对象工厂,所以每个执行步骤只有一个这样的 bean 实例。 - 一组特定的批处理基础设施 beans 可用于自动连接:
jobRepository(类型为JobRepository)、jobLauncher(类型为jobLauncher)、jobBuilders(类型为JobBuilderFactory、stepBuilders(类型为StepBuilderFactory)。这意味着它们不必被显式声明(像在 XML 中一样)。
- 创建了一个
jobbean 是通过调用JobBuilderFactory.get(...)创建的名为singerJob的批处理作业。- 通过调用
StepBuilderFactory.get(...)创建step1bean,并为面向块的处理进行配置。Spring 容器将自动注入在上下文中找到的ItemReader、ItemProcessor和ItemWriter实例。但是,必须显式设置StepExecutionStatsListenerbean。 - 在
ItemWriter的声明中使用了databasebean,因为这是将用于将Singer实例写入嵌入式数据库的 bean。
最后,我们需要一个驱动程序来启动作业,如下所示:
package com.apress.prospring5.ch18;
import com.apress.prospring5.ch18.config.BatchConfig;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import java.util.Date;
public class SingerJobDemo {
public static void main(String... args) throws Exception {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(BatchConfig.class);
Job job = ctx.getBean(Job.class);
JobLauncher jobLauncher = ctx.getBean(JobLauncher.class);
JobParameters jobParameters = new JobParametersBuilder()
.addDate("date", new Date())
.toJobParameters();
jobLauncher.run(job, jobParameters);
System.in.read();
ctx.close();
}
}
您应该对这段代码很熟悉,因为大部分时间我们都在创建上下文,获取一些 beans,并对它们调用方法。您可能会注意到的一件事是JobParameters对象。该对象封装了用于区分作业实例的参数。作业标识在确定作业的最后状态(如果有)时非常重要,这也影响到其他方面,例如重新启动作业的能力。在这个例子中,我们简单地使用当前日期作为Job参数。JobParameters对象可以有多种类型,这些参数可以作为参考数据在作业中访问。至此,我们已经准备好测试新工作了。编译代码并运行SinxgerJobDemo类。您将在屏幕上看到一些日志语句,感兴趣的如下:
o.s.b.c.l.s.SimpleJobLauncher - Job: [SimpleJob: [name=singerJob]] launched with the
following parameters: [{date=1501418591075}]
o.s.b.c.j.SimpleStepHandler - Executing step: [step1]
c.a.p.c.SingerItemProcessor - Transformed singer: firstName: John, lastName: Mayer,
song: Helpless Into: firstName: JOHN, lastName: MAYER, song: HELPLESS
c.a.p.c.SingerItemProcessor - Transformed singer: firstName: Eric, lastName: Clapton,
song: Change The World Into: firstName: ERIC, lastName: CLAPTON, song: CHANGE THE WORLD
c.a.p.c.SingerItemProcessor - Transformed singer: firstName: John, lastName: Butler,
song: Ocean Into: firstName: JOHN, lastName: BUTLER, song: OCEAN
c.a.p.c.SingerItemProcessor - Transformed singer: firstName: BB, lastName: King,
song: Chains And Things Into: firstName: BB, lastName: KING, song: CHAINS AND THINGS
c.a.p.c.StepExecutionStatsListener - --> Wrote: 4 items in step: step1 o.s.b.c.l.s.SimpleJobLauncher - Job: [SimpleJob: [name=singerJob]] completed with the
following parameters: [{date=1501418591075}] and the following status: [COMPLETED]
仅此而已。您现在已经构建了一个简单的批处理作业,它从 CSV 文件中读取数据,通过ItemProcessor转换数据,将歌手的名字、姓氏和歌曲改为大写,然后将结果写入数据库。您还使用了StepListener来输出该步骤中写入的项目数。更多关于 Spring Batch 的信息,请查看其项目页面 http://projects.spring.io/spring-batch/ 。
JSR-352
JSR-352(Java 平台的批处理应用)深受 Spring Batch 的影响。如果您选择在工作中使用 JSR-352,您会注意到两者之间越来越多的相似之处,如果您已经是 Spring Batch 用户,应该会感到很舒服。在很大程度上,Spring Batch 和 JSR-352 具有相似的结构,并且 Spring Batch 从 Spring Batch 3.0 开始完全支持这种 JSR。像 Spring Batch 一样,JSR-352 作业是通过一个 XML 模式配置的,这种模式被称为作业规范语言(JSL)。因为 JSR-352 定义了一个规范和一个 API,所以没有提供现成的基础设施组件,而您可能已经习惯了使用 Spring Batch。如果你严格遵守 JSR-352 API,这意味着实现 JSR-352 接口并自己编写所有的基础设施组件,如ItemReader和ItemWriter。
在本例中,我们将转换之前的批处理示例,以利用 JSR-352 JSL,但我们将利用相同的ItemReader、ItemProcessor和ItemWriter,并利用 Spring 进行依赖注入等,而不是使用我们自己的基础设施组件。实现这项工作 100%符合 JSR-352 规格将留给你作为一个练习。
在这个示例中,如前所述,我们将重用 pure Spring 批处理示例中的大部分代码,我们将在这里解释一些小的更改。如果您还没有,这将是一个很好的机会让 Spring Batch 示例开始工作,然后继续应用本节中的更改。
对于本例,H2 数据库将被替换为 HSQLDB,DBCP 2 将被替换为 DBCP,因为不支持更新的版本。还有,JSR 要求在src/main/resources/META-INF/batch-jobs/下声明singerJob.xml配置文件,当启动一个作业时,所需要的只是没有.xml扩展名的文件名。所以没错,JSR-352 是无法回避的。必须使用 XML。此示例的 XML 配置文件如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xsi:schemaLocation="
http://www.springframework.org/schema/jdbc
http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/jobXML_1_0.xsd">
<job id="singerJob" version="1.0">
<step id="step1">
<listeners>
<listener ref="stepExecutionStatsListener"/>
</listeners>
<chunk item-count="10">
<reader ref="itemReader"/>
<processor ref="itemProcessor"/>
<writer ref="itemWriter"/>
</chunk>
<fail on="FAILED"/>
<end on="*"/>
</step>
</job>
<jdbc:embedded-database id="dataSource" type="HSQL">
<jdbc:script location="classpath:support/singer.sql"/>
</jdbc:embedded-database>
<!-- no transaction manager needed -->
<bean id="stepExecutionStatsListener" ../>
<bean id="itemReader" ../>
<bean id="itemProcessor" ../>
<bean id="itemWriter" ../>
</beans>
在使用 Spring Batch 时,前面没有显示的 beans 具有相同的定义。您可以在本书的附录中找到完整的 XML 定义。现在,因为我们必须这样做,下面是如何用 Spring Batch XML 配置前面的作业:
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:batch="http://www.springframework.org/schema/batch"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="
http://www.springframework.org/schema/batch
http://www.springframework.org/schema/batch/spring-batch.xsd
http://www.springframework.org/schema/jdbc
http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<batch:job id="singerJob">
<batch:step id="step1">
<batch:tasklet>
<batch:chunk reader="itemReader"
processor="itemProcessor"
writer="itemWriter"
commit-interval="10"/>
<batch:listeners>
<batch:listener ref="stepExecutionStatsListener"/>
</batch:listeners>
</batch:tasklet>
<batch:fail on="FAILED"/>
<batch:end on="*"/>
</batch:step>
</batch:job>
<jdbc:embedded-database id="dataSource" type="H2">
<jdbc:script location="classpath:/org/springframework/batch/core/schema-h2.sql"/>
<jdbc:script location="classpath:support/singer.sql"/>
</jdbc:embedded-database>
<batch:job-repository id="jobRepository"/>
<bean id="jobLauncher"
class="org.springframework.batch.core.launch.support.SimpleJobLauncher"
p:jobRepository-ref="jobRepository"/>
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
p:dataSource-ref="dataSource"/>
<bean id="stepExecutionStatsListener" ../>
<bean id="itemReader" ../>
<bean id="itemProcessor" ../>
<bean id="itemWriter" ../>
</beans>
因此,这两种配置看起来非常相似,除了作业定义使用 JSR-352 JSL,我们能够删除一些 bean(transactionManager、jobRepository和jobLauncher),因为它们已经以某种方式提供给我们了。您还会注意到使用jobXML_1.0.xsd的一个额外的模式定义。对这种模式的支持是通过 JSR-352 API JAR 获得的,当使用 Gradle 这样的构建工具时,这种支持会作为一种可传递的依赖关系自动关闭。如果我们需要手动获取依赖项,请参见本节末尾列出的项目页面。我们需要修改的第二部分是SingerJobDemo类,因为我们现在使用特定于 JSR 352 的代码来调用作业,如下所示:
package com.apress.prospring5.ch18;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.util.Date;
public class SingerJobDemo {
public static void main(String... args) throws Exception {
ApplicationContext applicationContext
= new ClassPathXmlApplicationContext("/spring/singerJob.xml");
Job job = applicationContext.getBean(Job.class);
JobLauncher jobLauncher = applicationContext.getBean(JobLauncher.class);
JobParameters jobParameters = new JobParametersBuilder()
.addDate("date", new Date())
.toJobParameters();
jobLauncher.run(job, jobParameters);
}
}
这与其他例子有些不同,在其他例子中,我们直接使用ApplicationContext和 beans。当创建 JSR-352 作业时,我们使用JsrJobOperator来启动和控制作业。不是用一个JobParameters对象为作业提供参数,而是用一个Properties对象。使用的Properties对象是一个标准的java.util.Properties类,应该使用String键和值来创建作业参数。您可能注意到的另一个有趣的变化是waitForJob()方法。默认情况下,JSR-352 异步启动所有作业。因此,在独立程序中,如图所示,在程序终止之前,我们需要等待作业处于可接受的状态。如果您的代码运行在一个容器中,比如某种应用服务器,那么可能不需要这些代码。现在让我们编译并运行SingerJobDemo类,它将产生以下相关的日志语句:
o.s.b.c.r.s.JobRepositoryFactoryBean - No database type set,
using meta data indicating: HSQL
o.s.b.c.j.c.x.JsrXmlApplicationContext - Refreshing org.springframework.batch.core.jsr.
configuration.xml.JsrXmlApplicationContext@48c76607
o.s.b.c.j.SimpleStepHandler - Executing step: [step1]
c.a.p.c.SingerItemProcessor - Transformed singer: firstName: John, lastName: Mayer,
song: Helpless Into: firstName: JOHN, lastName: MAYER, song: HELPLESS
c.a.p.c.SingerItemProcessor - Transformed singer: firstName: Eric, lastName: Clapton,
song: Change The World Into: firstName: ERIC, lastName: CLAPTON, song: CHANGE THE WORLD
c.a.p.c.SingerItemProcessor - Transformed singer: firstName: John, lastName: Butler,
song: Ocean Into: firstName: JOHN, lastName: BUTLER, song: OCEAN
c.a.p.c.SingerItemProcessor - Transformed singer: firstName: BB, lastName: King,
song: Chains And Things Into: firstName: BB, lastName: KING, song: CHAINS AND THINGS
c.a.p.c.StepExecutionStatsListener - --> Wrote: 4 items in step: step1
o.s.b.c.j.c.x.JsrXmlApplicationContext - Closing ... JsrXmlApplicationContext
日志输出看起来非常相似,现在您已经利用 JSR-352 来定义和运行这个作业,同时使用 Spring 的依赖注入功能和 Spring Batch 的基础设施组件,而不是编写自己的组件。
有关 JSR-352 的更多信息,请参见其位于 https://jcp.org/en/jsr/detail?id=352 的项目页面。
Spring Boot 批次
不出所料,Spring Boo t 来了,它为 Spring Batch 提供了一个特殊的启动库,进一步简化了配置。令人兴奋的是,在类路径中有了 Spring Batch 的 Spring Boot 启动库,您就不需要为依赖关系而烦恼了。不好的地方是,说到配置,嗯,春批细节不能减那么多。
然而,让我们尝试修改我们一直运行到现在的批处理示例,用一个JobExecutionStatsListener类替换StepExecutionStatsListener;这个类将扩展JobExecutionListenerSupport类,它是JobExecutionListener接口的一个空抽象实现,用于在作业生命周期的特定点提供回调。在这种情况下,JobExecutionStatsListener类将查询数据库,检查我们的歌手条目是否确实保存在那里。
这里描述了类JobExecutionStatsListener:
package com.apress.prospring5.ch18;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.listener.JobExecutionListenerSupport;
import org.springframework.batch.core.listener.StepExecutionListenerSupport;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Component;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
@Component
public class JobExecutionStatsListener extends JobExecutionListenerSupport {
public static Logger logger = LoggerFactory.
getLogger(JobExecutionStatsListener.class);
private final JdbcTemplate jdbcTemplate;
@Autowired
public JobExecutionStatsListener(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public void afterJob(JobExecution jobExecution) {
if(jobExecution.getStatus() == BatchStatus.COMPLETED) {
logger.info(" --> Singers were saved to the database. Printing results ...");
jdbcTemplate.query("SELECT first_name, last_name, song FROM SINGER",
(rs, row) -> new Singer(rs.getString(1),
rs.getString(2), rs.getString(3))).forEach(
singer -> logger.info(singer.toString())
);
}
}
}
正如我们所看到的,lambda 表达式在前面的例子中被大量使用只是为了好玩,但是在afterJob回调方法的主体中发生了什么是很明显的。
同样,让我们看看你能用BatchConfig类做些什么。
package com.apress.prospring5.ch18;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.database.
BeanPropertyItemSqlParameterSourceProvider;
import org.springframework.batch.item.database.JdbcBatchItemWriter;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper;
import org.springframework.batch.item.file.mapping.DefaultLineMapper;
import org.springframework.batch.item.file.transform.DelimitedLineTokenizer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.ResourceLoader;
import javax.sql.DataSource;
@Configuration
@EnableBatchProcessing
public class BatchConfig {
@Autowired
private JobBuilderFactory jobs;
@Autowired
private StepBuilderFactory steps;
@Autowired DataSource dataSource;
@Autowired SingerItemProcessor itemProcessor;
@Bean
public Job job(JobExecutionStatsListener listener) {
return jobs.get("singerJob")
.listener(listener)
.flow(step1())
.end()
.build();
}
@Bean
protected Step step1() {
return steps.get("step1")
.<Singer, Singer>chunk(10)
.reader(itemReader())
.processor(itemProcessor)
.writer(itemWriter())
.build();
}
//adding lambda expressions
@Bean
public ItemReader itemReader() {
FlatFileItemReader<Singer> itemReader = new FlatFileItemReader<>();
itemReader.setResource(new ClassPathResource("support/test-data.csv"));
itemReader.setLineMapper(new DefaultLineMapper<Singer>() {{
setLineTokenizer(new DelimitedLineTokenizer() {{
setNames(new String { "firstName", "lastName", "song" });
}});
setFieldSetMapper(new BeanWrapperFieldSetMapper<Singer>() {{
setTargetType(Singer.class);
}});
}});
return itemReader;
}
@Bean
public ItemWriter itemWriter() {
... //same as before
}
}
因此,除了在itemReader bean 的声明中大量使用 lambda 表达式之外,变化最大的 bean 是job bean。执行步骤不再由 Spring 容器基于限定符自动连接,调用flow方法来创建一个新的作业构建器,该构建器将执行一个步骤或一系列步骤。
Spring Boot 的情况差不多就是这样;剩下的就是通过执行典型的Application类来启动应用。
package com.apress.prospring5.ch18;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
@SpringBootApplication
public class Application {
private static Logger logger = LoggerFactory
.getLogger(Application.class);
public static void main(String... args) throws Exception {
ConfigurableApplicationContext ctx =
SpringApplication.run(Application.class, args);
assert (ctx != null);
logger.info("Application started...");
System.in.read();
ctx.close();
}
}
最后,您可以检查由JobExecutionStatsListener类打印的预期结果的日志,如下所示:
o.s.b.c.l.s.SimpleJobLauncher - Job: [FlowJob: [name=singerJob]] launched with
the following parameters: [{}]
o.s.b.c.j.SimpleStepHandler - Executing step: [step1]
c.a.p.c.SingerItemProcessor - Transformed singer: firstName: John, lastName: Mayer,
song: Helpless Into: firstName: JOHN, lastName: MAYER, song: HELPLESS
c.a.p.c.SingerItemProcessor - Transformed singer: firstName: Eric, lastName: Clapton,
song: Change The World Into: firstName: ERIC, lastName: CLAPTON, song: CHANGE THE WORLD
c.a.p.c.SingerItemProcessor - Transformed singer: firstName: John, lastName: Butler,
song: Ocean Into: firstName: JOHN, lastName: BUTLER, song: OCEAN
c.a.p.c.SingerItemProcessor - Transformed singer: firstName: BB, lastName: King,
song: Chains And Things Into: firstName: BB, lastName: KING, song: CHAINS AND THINGS
c.a.p.c.JobExecutionStatsListener - --> Singers were saved to the database. Printing results ...
c.a.p.c.JobExecutionStatsListener - firstName: JOHN, lastName: MAYER, song: HELPLESS
c.a.p.c.JobExecutionStatsListener - firstName: ERIC, lastName: CLAPTON, song: CHANGE THE WORLD
c.a.p.c.JobExecutionStatsListener - firstName: JOHN, lastName: BUTLER, song: OCEAN
c.a.p.c.JobExecutionStatsListener - firstName: BB, lastName: KING, song: CHAINS AND THINGS
o.s.b.c.l.s.SimpleJobLauncher - Job: [FlowJob: [name=singerJob]] completed with the
following parameters: [{}] and the following status: [COMPLETED]
Spring Integration
Spring Integration 项目提供了众所周知的企业集成模式(EIP)的现成实现。Spring Integration 侧重于消息驱动的架构。它为集成解决方案、异步能力和松散耦合的组件提供了一个简单的模型,并且它是为可扩展性和可测试性而设计的。
在其核心,一个Message包装器在框架中起着核心作用。这个 Java 对象的通用包装器与框架使用的元数据(更具体地说是有效负载和头)结合在一起,用于确定如何处理该对象。
通道是管道和过滤器架构中的管道,生产者向该通道发送消息,消费者从该通道接收消息。Message另一方面,端点代表管道和过滤器架构的过滤器,它们将应用代码连接到消息传递框架。Spring Integration 提供的一些现成的Message端点是Transformer、Filter、Router和Splitter,它们各自提供自己的角色和职责。
Spring Integration 还提供了大量的集成端点(在撰写本文时有 20 多个),这些端点可以在文档部分的 http://docs.spring.io/spring-integration/reference/htmlsingle/#endpoint-summary 的“端点快速参考表”中找到。这些端点提供了连接各种资源的能力,例如 AMQP、文件、HTTP、JMX、Syslog 和 Twitter。除了 Spring Integration 提供的现成功能,另一个名为 Spring Integration Extensions 的项目是一个基于社区的贡献模型,位于 https://github.com/spring-projects/spring-batch-extensions ,包含更多的集成可能性,包括亚马逊网络服务(AWS)、阿帕奇卡夫卡(Apache Kafka)、短消息点对点(SMPP)和伏地魔。在开箱即用和扩展项目组件之间,Spring Integration 提供了大量现成的组件,这意味着必须自己编写组件的可能性大大降低。
在这个例子中,我们将建立在前面的批处理例子的基础上,但是这一次我们将引入 Spring Integration 来展示我们如何使用它在给定的时间间隔内监控一个目录。当文件到达时,我们检测该文件并开始批处理作业进行处理。
在这个例子中,我们将再次建立在我们从本章开始的最初的“纯”Spring 批处理项目之上。请确保在继续之前查看并运行该程序,因为我们在这里将只解释新的类和配置修改。
显然,一些新的依赖项必须添加到项目中。
//pro-spring-15/build.gradle
ext {
//spring libs
...
springBatchVersion = '4.0.0.M3'
springIntegrationVersion = '5.0.0.M6'
springBatchIntegrationVersion = '4.0.0.M3'
...
spring = [
context : "org.springframework:spring-context:$springVersion",
jdbc : "org.springframework:spring-jdbc:$springVersion",
batchCore : "org.springframework.batch:spring-batch-core:$springBatchVersion"
batchIntegration :
"org.springframework.batch:spring-batch-integration:$springBatchIntegrationVersion",
integrationFile :
"org.springframework.integration:spring-integration-file:$springIntegrationVersion"
...
]
misc = [
io : "commons-io:commons-io:2.5",
...
]
db = [
...
dbcp2 : "org.apache.commons:commons-dbcp2:$dbcpVersion",
h2 : "com.h2database:h2:$h2Version",
// needed for the Batch JSR-352 module
hsqldb: "org.hsqldb:hsqldb:2.4.0"
dbcp : "commons-dbcp:commons-dbcp:1.4",
]
}
...
//pro-spring-15/chapter18/build.gradle
dependencies {
if (!project.name.contains("boot")) {
compile(spring.jdbc) {
// exclude these as batchCore will bring them
// on as transitive dependencies
exclude module: 'spring-core'
exclude module: 'spring-beans'
exclude module: 'spring-tx'
}
compile spring.batchCore, db.dbcp2, db.h2, misc.io,
spring.batchIntegration, spring.integrationFile,
misc.slf4jJcl, misc.logback
}
}
有了新的依赖项,让我们创建一个充当 Spring Integration transformer 的类。这个 Transformer 实例将从表示找到的文件的入站通道接收一个Message实例,并使用它启动批处理作业,如下面的代码片段所示:
package com.apress.prospring5.ch18;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.integration.launch.JobLaunchRequest;
import org.springframework.messaging.Message;
import java.io.File;
public class MessageToJobLauncher {
private Job job;
private String fileNameKey;
public MessageToJobLauncher(Job job, String fileNameKey) {
this.job = job;
this.fileNameKey = fileNameKey;
}
public JobLaunchRequest toRequest(Message<File> message) {
JobParametersBuilder jobParametersBuilder = new JobParametersBuilder();
jobParametersBuilder.addString(fileNameKey, message.getPayload().
getAbsolutePath());
return new JobLaunchRequest(job, jobParametersBuilder.
toJobParameters());
}
}
现在,让我们修改BatchConfig类来支持批量集成。真正需要的是修改itemReader bean,在每次需要处理新文件时创建。这意味着路径位置必须注入到 bean 中,也意味着 bean 不能再有 singleton 作用域。
package com.apress.prospring5.ch18.config;
...
@Configuration
@EnableBatchProcessing
@Import(DataSourceConfig.class)
@ComponentScan("com.apress.prospring5.ch18")
public class BatchConfig {
... // autowired beans
@Bean
public Job singerJob() {
return jobs.get("singerJob").start(step1()).build();
}
@Bean
protected Step step1() {
...// no change
}
@Bean
@StepScope
public FlatFileItemReader itemReader(
@Value("file://#{jobParameters['file.name']}") String filePath) {
FlatFileItemReader itemReader = new FlatFileItemReader();
itemReader.setResource(resourceLoader.getResource(filePath));
DefaultLineMapper lineMapper = new DefaultLineMapper();
DelimitedLineTokenizer tokenizer = new DelimitedLineTokenizer();
tokenizer.setNames("firstName", "lastName", "song");
tokenizer.setDelimiter(",");
lineMapper.setLineTokenizer(tokenizer);
BeanWrapperFieldSetMapper<Singer> fieldSetMapper =
new BeanWrapperFieldSetMapper<>();
fieldSetMapper.setTargetType(Singer.class);
lineMapper.setFieldSetMapper(fieldSetMapper);
itemReader.setLineMapper(lineMapper);
return itemReader;
}
@Bean
public ItemWriter<Singer> itemWriter() {
...//no change
}
}
@StepScope是一个方便的注释,它相当于@Scope(value="step", proxyMode=TARGET_CLASS),从而使itemReader bean 具有范围proxy,并将这个范围命名为step,这样就可以清楚地看到这个 bean 是如何被使用的。
现在我们已经有了批处理配置,让我们添加集成典型配置。我们将使用 XML 配置文件来实现这一点,因为在编写本文时,这样做实际上更实用:
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:batch-int="http://www.springframework.org/schema/batch-integration"
xmlns:int="http://www.springframework.org/schema/integration"
xmlns:int-file="http://www.springframework.org/schema/integration/file"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/batch-integration
http://www.springframework.org/schema/batch-integration/spring-batch-integration.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/integration
http://www.springframework.org/schema/integration/spring-integration.xsd
http://www.springframework.org/schema/integration/file
http://www.springframework.org/schema/integration/file/spring-integration-file.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<bean name="/BatchConfig" class="com.apress.prospring5.ch18.config.BatchConfig"/>
<context:annotation-config/>
<int:channel id="inbound"/>
<int:channel id="outbound"/>
<int:channel id="loggingChannel"/>
<int-file:inbound-channel-adapter id="inboundFileChannelAdapter" channel="inbound"
directory="file:/tmp/" filename-pattern="*.csv">
<int:poller fixed-rate="1000"/>
</int-file:inbound-channel-adapter>
<int:transformer input-channel="inbound"
output-channel="outbound">
<bean class="com.apress.prospring5.ch18.MessageToJobLauncher">
<constructor-arg ref="singerJob"/>
<constructor-arg value="file.name"/>
</bean>
</int:transformer>
<batch-int:job-launching-gateway request-channel="outbound"
reply-channel="loggingChannel"/>
<int:logging-channel-adapter channel="loggingChannel"/>
</beans>
对配置的主要添加是以int:和batch-int:名称空间为前缀的部分。首先,我们创建几个命名通道来传递数据。然后,我们配置一个inbound-channel-adapter,专门用于以一秒的给定间隔监控指定的目录。然后我们配置Transformer bean,它接收包装在Message中的标准java.io.File对象形式的文件。接下来我们配置job-launching-gateway,它接收来自Transformer的job-launch请求,以实际调用批处理作业。最后但同样重要的是,我们创建了logging-channel-adapter,它将在作业完成后打印出信息通知。正如您通过遵循配置的Channel属性所看到的,每个组件或者通过Channel实例消费消息或者产生消息,或者两者都有。现在让我们创建一个加载配置文件的简单驱动程序类。这个驱动程序类所做的就是用配置加载应用上下文并保持运行,直到您终止该进程,因为它会持续轮询指定的目录。
package com.apress.prospring5.ch18;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.support.GenericXmlApplicationContext;
public class FileWatcherDemo {
private static Logger logger =
LoggerFactory.getLogger(FileWatcherDemo.class);
public static void main(String... args) throws Exception {
GenericXmlApplicationContext ctx
= new GenericXmlApplicationContext(
"classpath:spring/integration-config.xml");
assert (ctx != null);
logger.info("Application started...");
System.in.read();
ctx.close();
}
}
现在编译代码并运行FileWatcherDemo类。当应用启动时,您可能会注意到一些日志消息被打印到屏幕上,但最终不会发生其他事情。这是因为 Spring Integration file adapter 在一个轮询间隔内等待文件被放置到配置好的位置,在它检测到该位置的文件之前,什么都不会发生。在src/main/resources/support/目录下,你会发现四个 CSV 文件,分别命名为singer1.csv到singer4.csv。每一行都包含一个歌手的名和姓,以及该歌手的一个歌名。将这些文件一个一个地复制到tmp目录中,并观察控制台看看会发生什么。
o.s.i.f.FileReadingMessageSource - Created message: [GenericMessage [
payload=/tmp/singers1.csv, headers={file_originalFile=/tmp/singers1.csv,
id=ca0ec15e-b9b6-5dc2-2fc6-44fdb4b433f4, file_name=singers1.csv,
file_relativePath=singers1.csv, timestamp=1501442201624}]]
o.s.b.c.l.s.SimpleJobLauncher - Job: [SimpleJob: [name=singerJob]] launched with
the following parameters: [{file.name=/tmp/singers1.csv}]
o.s.b.c.j.SimpleStepHandler - Executing step: [step1]
c.a.p.c.SingerItemProcessor - Transformed singer: firstName: John, lastName: Mayer,
song: Helpless Into: firstName: JOHN, lastName: MAYER, song: HELPLESS
INFO c.a.p.c.StepExecutionStatsListener - --> Wrote: 1 items in step: step1
o.s.b.c.l.s.SimpleJobLauncher - Job: [SimpleJob: [name=singerJob]] completed with
the following parameters: [{file.name=/tmp/singers1.csv}] and the following
status: [COMPLETED]
o.s.i.h.LoggingHandler - JobExecution: id=1, version=2,
startTime=Sun Jul 30 22:16:41 EEST 2017, endTime=Sun Jul 30 22:16:41 EEST 2017,
lastUpdated=Sun Jul 30 22:16:41 EEST 2017, status=COMPLETED,
exitStatus=exitCode=COMPLETED;exitDescription=, job=[JobInstance: id=1,
version=0, Job=[singerJob]], jobParameters=[{file.name=/tmp/singers1.csv}]
从日志语句中可以看到,Spring Integration 检测到了这个文件,从它创建了一个Message,调用了转换 CSV 文件内容的批处理作业,然后将内容写入内存数据库。虽然这是一个简单的例子,但它展示了如何通过使用 Spring Integration 在各种类型的应用之间构建复杂且解耦的工作流。
关于 Spring Integration 的更多信息,请参见其位于 http://projects.spring.io/spring-integration/ 的项目页面。
Spring XD
Spring XD 是一个可扩展的运行时服务,设计用于分布式数据接收、实时分析、批处理和数据导出。Spring XD 建立在许多现有的 Spring portfolio 项目之上,最著名的是 Spring Framework、Batch 和 Integration。Spring XD 的目标是提供一种统一的方式,将许多系统集成到一个有凝聚力的大数据解决方案中,帮助解决许多常见用例的复杂性。
Spring XD 可以以单一独立模式运行,通常用于开发和测试目的,也可以以完全分布式模式运行,从而提供拥有高可用性主节点和任意数量工作节点的能力。Spring XD 使您能够通过 shell 界面(利用 Spring shell)以及图形 web 界面来管理这些服务。这些接口允许您定义如何组装各种组件来完成您的数据处理需求,方法是通过 shell 应用使用 DSL 类型的语法,或者将数据输入到 web 接口中,这将为您构建定义。
Spring XD DSL 基于几个概念,特别是流、模块、源、处理器、接收器和作业。通过简洁的语法组合这些组件,可以轻松地创建流来连接各种技术,以接收数据、处理数据并最终将数据输出到外部源,甚至可以运行批处理作业进行进一步处理。让我们快速浏览一下这些概念:
- 流定义了数据如何从源流到接收器,并且可以通过任意数量的处理器。DSL 用于定义流;例如,一个基本的源到汇定义可能看起来像
http | file。 - 模块封装了组成流的可重用工作单元。模块根据它们的角色按类型分类。在撰写本文时,Spring XD 包含 source、processor、sink 和 job 类型的模块。
- Spring XD 中的资源要么轮询外部资源,要么由某种事件触发。源只向下游组件提供输出,流中的第一个模块必须是源。
- 处理器本质上类似于我们在 Spring Batch 中看到的处理器。处理器的作用是接受输入,对所提供的对象执行转换或业务逻辑,并返回输出。
- 在源的另一面,接收器接受输入源并将数据输出到其目标资源。水槽是溪流的最后一站。
- 作业是定义 Spring 批处理作业的模块。这些作业的定义方式与我们在本章开始时描述的方式相同,并被部署到 Spring XD 中以提供批处理功能。
- tap,顾名思义,监听流经流的数据,并允许您在单独的流中处理被分接的数据。tap 概念类似于 WireTap 企业集成模式。
如您所料,Spring XD 提供了许多现成的源、处理器、接收器、作业和 tap。作为开发人员,您也不仅仅局限于现成的东西,还可以自由构建自己的模块和组件。有关创建您自己的模块和组件的更多详细信息,请参见参考手册中关于这些定制点的内容:
- 模块:
http://docs.spring.io/spring-xd/docs/current/reference/html/#_creating_a_module - 来源:
http://docs.spring.io/spring-xd/docs/current/reference/html/#creating-a-source-module - 处理器:
http://docs.spring.io/spring-xd/docs/current/reference/html/#creating-a-processor-module - 汇:
http://docs.spring.io/spring-xd/docs/current/reference/html/#creating-a-sink-module - 乔布斯:
http://docs.spring.io/spring-xd/docs/current/reference/html/#creating-a-job-module
在本例中,我们将向您展示如何使用 Spring XD 的现成组件来复制我们用批处理和集成示例创建的内容,所有这些都是通过利用 XD shell 和 DSL 的简单命令行配置来完成的。
在开始之前,您必须安装 Spring XD。请参考 http://docs.spring.io/spring-xd/docs/current/reference/html/#getting-started 的用户手册“入门”部分,该部分提供了在机器上安装 XD 的各种方法的详细信息。您选择的安装方法是个人喜好问题,不会影响示例。一旦安装了 XD,就按照文档中的描述以单节点模式启动运行时。
为了在 XD 中复制我们在批处理和集成示例中创建的内容,我们只需要做一些基本的任务。首先,我们需要创建一个要导入的 CSV 文件,如下所示,并将其放在/tmp/singers.csv中:
John,Mayer,Helpless
Eric,Clapton,Change The World
John,Butler,Ocean
BB,King,Chains And Things
在 Spring XD shell 控制台中,键入以下命令:
job create singerjob --definition "filejdbc --resources=file:///tmp/singers.csv
--names=firstname,lastname,song --tableName=singer
--initializeDatabase=true" --deploy
在控制台中按 Enter 键后,您应该会看到一条消息,说明“成功创建和部署了作业‘singer job’。”如果没有,请在启动单节点 XD 容器的终端中检查控制台输出,以了解更多详细信息。
此时,您已经创建了一个新的作业定义,但是还没有发生任何事情,因为它还没有启动。在 shell 中,键入以下命令启动作业:
job launch singerjob
shell 现在应该会响应一条消息,表明已经成功提交了对singerjob的启动请求。
通过分解提供的 DSL,Spring XD 知道我们想要创建一个批处理作业,该作业通过 JDBC 由job create语句和filejdbc源读取文件并输出到数据库。它还通过使用tableName参数自动为我们创建表格,从names参数中获取列名,并从resources参数中读取数据,在该参数中我们提供了 CSV 文件的文件路径。
如果您想检查导入的数据,使用您最喜欢的数据库工具连接到您在设置过程中使用的数据库(嵌入式或真实 RDBMS ),并从Singer表中选择记录进行验证。如果看不到数据,请检查运行单节点容器的控制台中的日志语句。
此时,我们已经在 shell 中键入了两个命令,但是还没有编写任何代码或复杂的配置。然而,我们不费吹灰之力就将 CSV 文件的内容导入了数据库。我们通过利用 Spring XD 的预构建批处理作业,用简单的命令行 DSL 语法定义它,然后从 shell 启动该作业来实现这一点。
正如您所看到的,Spring XD 提供了许多现成的功能,开发者无需创建一些更常见的用例场景。因为我们在前面的示例中做了一些转换,将这个人的名字和姓氏改为大写,所以我们将它作为练习留给您来进一步探索 Spring XD!
更多关于 Spring XD 的信息,请查看其项目页面 http://projects.spring.io/spring-xd/ 。
Spring Framework 最显著的五个特性
在撰写本文时,Spring Framework RC3 版本已经发布。Spring 5.0 是核心框架的一个主要修订版,附带了用 Java 8 重写的代码库,并在 RC3 版本中开始适应 Java 9。此版本发布了一些主要功能:
- 它带有
spring-webflux模块,构建在支持 RxJava 1.3 和 2.1 的 Reactor 3.1 上,也称为反应式 Web 框架。这是对spring-webmvc的反应式补充,它提供了一个为运行在 Tomcat、Jetty 或 Undertow 上的异步 API 设计的 web 编程模型。 - Kotlin 支持是通过一个完整的空安全 API 为 bean 注册和功能性 web 端点提供的。
- 与 Java EE 8 APIs 的集成包括对 Servlet 4.0、Bean Validation 2.0、JPA 2.2 和 JSON 绑定 API(作为 Spring MVC 中 Jackson/Gson 的替代)的支持。
- 对 JUnit 5 Jupiter 的完全支持将允许开发人员在 JUnit 5 中编写测试和扩展,并与 Spring TestContext 框架并行执行它们。
- Java 9 互操作性是 Spring 团队对 Spring Framework 版本 5 的期望。由于 Oracle 将 Java 9 的发布推迟了几个月,Spring 框架最终与 Java 8 一起开发和发布,并使用 Project Reactor 提供反应式编程支持。但是完全支持 Java 9 的承诺仍然存在,并且 RC3 开始履行这一承诺。
- 还有更多的功能可用。 6
以下部分将仅涵盖前面介绍的列表中的三个功能。
功能性 Web 框架
如前所述,功能性 web 框架(spring-webflux模块)是对spring-webmvc的反应性补充,它提供了一个为异步 API 设计的 web 编程模型。它是根据反应式编程原则构建的。反应式编程可以用最简单的方式解释为“用反应式流编程”流是反应式编程模型的核心,它们用于支持任何事情的异步处理。简而言之,反应式库提供了将任何东西作为流使用的可能性(变量、用户输入、缓存、数据结构等等)。)并因此支持特定于流的操作,例如:过滤一个流以创建另一个流、合并流、将值从一个流映射到另一个流,等等。反应式编程中的反应式部分意味着流将是一个“可观察的”对象,它将被一个组件观察到,并将根据发出的对象对它作出反应。流可以发出三种类型的对象:值、错误或“完成”信号。
要将普通应用转变为反应式应用,第一个逻辑步骤是修改组件以产生和处理数据流。有两种类型的数据流。
-
reactor.core.publisher.Flux7 :这是一个[0..n]元素的流。创建Flux最简单的方法如下:Flux simple = Flux.just("1", "2", "3"); //or from an existing list: List<Singer> Flux<Singer> fromList = Flux.fromIterable(list); -
reactor.core.publisher.Mono:8 这是一串[0..1]元素。创建Mono最简单的方法如下:Mono simple = Mono.just("1"); //or from an existing object of type Singer Mono<Singer> fromObject = Mono.justOrEmpty(singer);
这两个类都是org.reactivestreams.Publisher<T>的实现,此时您可能想知道是否真的需要Mono的实现。答案是肯定的,因为实际原因:根据对流发出的值所做的操作类型,知道基数总是有用的。例如,想象一个反应式存储库类:让findOne方法返回一个Flux有意义吗?
这个简短的介绍应该足以让你掌握反应式编程的基础,并能够理解spring-webflux带来的功能。 9
在本节中,您将使用在第十六章中介绍的使用百里香叶的 Spring Boot web 应用,并将其转换为使用 Spring WebFlux。第一步是添加spring -boot-starter-webflux作为依赖项,去掉不必要的依赖项。在下面的代码片段中,您可以看到所需的、在父项目中配置的以及在webflux-boot模块中使用的库:
//pro-spring-15/build.gradle
ext {
//spring libs
bootVersion = '2.0.0.M3'
springDataVersion = '2.0.0.M3'
junit5Version = '5.0.0-M4'
...
boot = [
...
springBootPlugin:
"org.springframework.boot:spring-boot-gradle-plugin:$bootVersion",
starterWeb :
"org.springframework.boot:spring-boot-starter-web:$bootVersion",
starterTest :
"org.springframework.boot:spring-boot-starter-test:$bootVersion",
starterJpa :
"org.springframework.boot:spring-boot-starter-data-jpa:$bootVersion",
starterJta :
"org.springframework.boot:spring-boot-starter-jta-atomikos:$bootVersion",
starterWebFlux :
"org.springframework.boot:spring-boot-starter-webflux:$bootVersion"
]
testing = [
...
junit5 : "org.junit.jupiter:junit-jupiter-engine:$junit5Version"
]
db = [
...
h2 : "com.h2database:h2:$h2Version"
]
}
//chapter18/webflux-module/build.gradle
buildscript {
repositories {
...
}
dependencies {
classpath boot.springBootPlugin
}
}
apply plugin: 'org.springframework.boot'
dependencies {
compile boot.starterWebFlux, boot.starterWeb,
boot.starterJpa, boot.starterJta, db.h2
testCompile boot.starterTest, testing.junitJupiter, testing.junit5
}
spring-boot-starter-webflux模块依赖于一些自动添加到应用中的反应库。图 18-1 显示了 IntelliJ IDEA 中 Gradle 视图所描述的应用的依赖关系。请注意reactive-streams库。该库包含反应流基本接口、标准规范和四个接口:Publisher、Subscriber、Subscription和Processor。你可以在 www.reactive-streams.org 了解更多信息,因为流式 API 不是本节的重点。本节应用中使用的流实现由reactor-core库提供,您可以在 https://projectreactor.io/ 阅读更多关于它的内容。

图 18-1。
Project dependencies depicted by the Gradle view in IntelliJ IDEA
为了简单起见,安全层将被移除,我们也将移除接口。我们将使用 REST 并使用一组测试类来测试应用。通过删除所有的验证器注释,可以简化Singer类,因为这不是本节的重点。
package com.apress.prospring5.ch18.entities;
import javax.persistence.*;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
import static javax.persistence.GenerationType.IDENTITY;
@Entity
@Table(name = "singer")
public class Singer implements Serializable {
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "ID")
private Long id;
@Version
@Column(name = "VERSION")
private int version;
@Column(name = "FIRST_NAME")
private String firstName;
@Column(name = "LAST_NAME")
private String lastName;
@Temporal(TemporalType.DATE)
@Column(name = "BIRTH_DATE")
private Date birthDate;
public Singer() {
}
public Singer(String firstName,
String lastName, Date birthDate) {
this.firstName = firstName;
this.lastName = lastName;
this.birthDate = birthDate;
}
//setters and getters
...
}
存储库实现将是CrudRepository<Singer, Long>接口的一个空扩展,这里不再赘述。这里的新颖之处在于,这个存储库将被一个反应式存储库实现所使用。这里你可以看到ReactiveSingerRepo接口的内容,它利用Mono和Flux来处理Singer对象:
package com.apress.prospring5.ch18.repos;
import com.apress.prospring5.ch18.entities.Singer;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface ReactiveSingerRepo {
Mono<Singer> findById(Long id);
Flux<Singer> findAll();
Mono<Void> save(Mono<Singer> singer);
}
实现如下所示:
package com.apress.prospring5.ch18.repos;
import com.apress.prospring5.ch18.entities.Singer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Service
public class ReactiveSingerRepoImpl implements ReactiveSingerRepo {
@Autowired
SingerRepository singerRepository;
@Override public Mono<Singer> findById(Long id) {
return Mono.justOrEmpty(singerRepository.findById(id));
}
@Override public Flux<Singer> findAll() {
return Flux.fromIterable(singerRepository.findAll());
}
@Override public Mono<Void> save(Mono<Singer> singerMono) {
return singerMono.doOnNext(singer ->
singerRepository.save(singer)
).thenEmpty((Mono.empty()));
}
}
让我们分别讨论这个类中的每一个方法。
findById方法返回一个简单的流,如果对singerRepository.findById(id)的调用返回一个,它将发出一个Singer对象;否则会发出onComplete信号。这意味着返回的对象将是一个不包含任何内容的Mono<Singer>对象。findAll方法返回一个包含由textitsingerRepository.findAll()方法返回的所有Singer对象的流。save方法接收一个类型为Mono<Singer>实例的参数,其中包含一个Singer实例。声明了一个java.util.function.Consumer<Singer>的实例,这样当Singer对象被成功发出时,它将被singerRepository保存到数据库中。这个方法返回一个空的Mono实例,因为没有什么可返回的。
既然我们有了反应式存储库,我们需要一个反应式处理程序。因为典型的 Spring @Controller只会消耗流并在视图中返回内容,所以我们需要使用一些不同的东西。这个类叫做SingerHandler,描述如下:
package com.apress.prospring5.ch18.web;
import com.apress.prospring5.ch18.entities.Singer;
import com.apress.prospring5.ch18.repos.ReactiveSingerRepo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.BodyInserters.fromObject;
@Component
public class SingerHandler {
@Autowired ReactiveSingerRepo reactiveSingerRepo;
public Mono<ServerResponse> list(ServerRequest request) {
Flux<Singer> singers = reactiveSingerRepo.findAll();
return ServerResponse.ok().contentType(APPLICATION_JSON)
.body(singers, Singer.class);
}
public Mono<ServerResponse> show(ServerRequest request) {
Mono<Singer> singerMono = reactiveSingerRepo.findById(Long
.valueOf(request.pathVariable("id")));
Mono<ServerResponse> notFound = ServerResponse.notFound().build();
return singerMono
.flatMap(singer -> ServerResponse.ok().contentType(APPLICATION_JSON)
.body(fromObject(singer)))
.switchIfEmpty(notFound);
}
public Mono<ServerResponse> save(ServerRequest request) {
Mono<Singer> data = request.bodyToMono(Singer.class);
reactiveSingerRepo.save(data);
return ServerResponse.ok().build(reactiveSingerRepo.save(data));
}
}
请求由一个处理函数处理,该函数将一个ServerRequest作为参数,并返回一个Mono<ServerResponse>。这两个接口是不可变的,并提供对底层 HTTP 消息的访问。两者都是完全反应性的;ServerRequest将身体暴露为通量或单色,Mono<ServerResponse>接受任何反应流作为身体。
ServerRequest还提供对其他 HTTP 相关数据的访问,比如正在处理的 URI、头和路径变量。对主体的访问由bodyToMono(...)方法提供,或者等效的bodyToFlux(...). ServerResponse提供对 HTTP 响应的访问。因为它是不可变的,所以必须使用构建器类和 HTTP 响应状态来创建它。通过调用各种方法来设置标题和正文。前面代码片段中的所有示例都创建了一个状态为 200 (OK)、JSON content-type 和 body 的响应。
但是函数式 web 框架这个名字是从哪里来的呢?兰姆达斯是这个的来源。如果你看看前面的代码片段,你会注意到list和create可以很容易地写成函数。这里显示了两个 lambda 函数:
package com.apress.prospring5.ch18.web;
import com.apress.prospring5.ch18.entities.Singer;
import com.apress.prospring5.ch18.repos.ReactiveSingerRepo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.BodyInserters.fromObject;
@Component
public class SingerHandler {
@Autowired ReactiveSingerRepo reactiveSingerRepo;
public HandlerFunction<ServerResponse> list =
serverRequest -> ServerResponse.ok().contentType(APPLICATION_JSON)
.body(reactiveSingerRepo.findAll(), Singer.class);
public Mono<ServerResponse> show(ServerRequest request) {
Mono<Singer> singerMono = reactiveSingerRepo.findById(
Long.valueOf(request.pathVariable("id")));
Mono<ServerResponse> notFound = ServerResponse.notFound().build();
return singerMono
.flatMap(singer -> ServerResponse.ok()
.contentType(APPLICATION_JSON).body(fromObject(singer)))
.switchIfEmpty(notFound);
}
public HandlerFunction<ServerResponse> save =
serverRequest -> ServerResponse.ok()
.build(reactiveSingerRepo.save(serverRequest.bodyToMono(Singer.class)));
}
HandlerFunction<ServerResponse>接口本质上是一个Function<Request, Response<T>>,没有副作用,因为它直接返回响应,而不是将其作为参数。这使得这种类型的函数非常实用,因为它们更容易测试、组合和优化。
很漂亮,对吧?请记住,当涉及到 lambdas 时,过度使用它们会带来可读性和可维护性的损失。另外,我知道你现在可能会想:好吧,好吧,反应流和 lambdas 很酷,但是映射在哪里呢?容器如何知道哪个函数被映射到一个 HTTP 请求?嗯,有一些新的课程。
下面是 Spring application 类,必须运行它来启动应用,并在之后分析它:
package com.apress.prospring5.ch18;
import com.apress.prospring5.ch18.web.SingerHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.http.server.reactive.ServletHttpHandlerAdapter;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.WebHandler;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
import static org.springframework.web.reactive.function.BodyInserters.fromObject;
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RequestPredicates.POST;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
import static org.springframework.web.reactive.function.server.RouterFunctions.toHttpHandler;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
@SpringBootApplication
public class SingerApplication {
private static Logger logger = LoggerFactory.getLogger(SingerApplication.class);
@Autowired
SingerHandler singerHandler;
private RouterFunction<ServerResponse> routingFunction() {
return route(GET("/test"), serverRequest -> ok().body(fromObject("works!")))
.andRoute(GET("/singers"), singerHandler.list)
.andRoute(GET("/singers/{id}"), singerHandler::show)
.andRoute(POST("/singers"), singerHandler.save)
.filter((request, next) -> {
logger.info("Before handler invocation: " + request.path());
return next.handle(request);
});
}
@Bean
public ServletRegistrationBean servletRegistrationBean() throws Exception {
HttpHandler httpHandler = RouterFunctions.toHttpHandler(routingFunction());
ServletRegistrationBean registrationBean = new ServletRegistrationBean<>
(new ServletHttpHandlerAdapter(httpHandler), "/");
registrationBean.setLoadOnStartup(1);
registrationBean.setAsyncSupported(true);
return registrationBean;
}
public static void main(String... args) throws Exception {
ConfigurableApplicationContext ctx =
SpringApplication.run(SingerApplication.class, args);
assert (ctx != null);
logger.info("Application started...");
System.in.read();
ctx.close();
}
}
通过使用一个RouterFunction,请求处理功能在新的功能性 web 框架中被公开,这个功能性 web 框架通过RouterFunctions.toHttpHandler实用程序方法被用来创建一个HttpHandler。这个HttpHandler用于创建一个ServletRegistrationBean,它是一个ServletContextInitializer,用于在 Servlet 3.0+容器中注册Servlet s。
A RouterFunction评估请求 URI 并检查是否有匹配的处理函数;否则,它返回一个空结果。它的行为类似于@RequestMapping注释,但是RouterFunction的优势在于表达路线不局限于使用注释值定义的内容,并且不分散在一个类中。代码在一个地方,可以很容易地覆盖或替换。编写和组合RouterFunction s 的语法非常灵活;在前面的代码示例中,选择了最实用的语法。 10
对应用进行初始测试的最简单方法是运行SingerApplication并在浏览器中访问RouterFunction中定义的 URIs。例如,如果您访问http://localhost:8080/test,您应该会看到“作品!”文字。
尽管如此,我们还没有提供一个 streams 被用作 streams 的例子,每个值在发出时都被处理。让我们创建一个 REST 控制器,它直接返回歌手的一个Flux对象,但是每两秒钟发出一个对象。
package com.apress.prospring5.ch18;
import com.apress.prospring5.ch18.entities.Singer;
import com.apress.prospring5.ch18.repos.ReactiveSingerRepo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.util.function.Tuple2;
import java.time.Duration;
@SpringBootApplication
@RestController
public class ReactiveApplication {
private static Logger logger = LoggerFactory.getLogger(ReactiveApplication.class);
@Autowired ReactiveSingerRepo reactiveSingerRepo;
@GetMapping(value = "/all", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<Singer> oneByOne() {
Flux<Singer> singers = reactiveSingerRepo.findAll();
Flux<Long> periodFlux = Flux.interval(Duration.ofSeconds(2));
return Flux.zip(singers, periodFlux).map(Tuple2::getT1);
}
@GetMapping(value = "/one/{id}")
public Mono<Singer> one(@PathVariable Long id) {
return reactiveSingerRepo.findById(id);
}
public static void main(String... args) throws Exception {
ConfigurableApplicationContext ctx =
SpringApplication.run(ReactiveApplication.class, args);
assert (ctx != null);
logger.info("Application started...");
System.in.read();
ctx.close();
}
}
MediaType.TEXT_EVENT_STREAM_VALUE表示一种特殊类型的纯文本响应。这确保了服务器将创建一个由text/event-stream Content-Type提供服务的响应,该响应遵循服务器发送的事件格式。响应应该包含一个"data:"行,后跟消息,在本例中是一个Singer对象的文本表示,后跟两个"\n"字符以结束流。这仅适用于一条消息,即发出一个对象。在发射多个物体的情况下,会产生多条"data:"线。以"data:"开头的两个或更多连续行将被视为一条数据,这意味着只有一个消息事件将被触发。每一行都应该以一个"\n"结尾(最后一行除外,它应该以两个结尾)。
oneByOne方法包含一个组合两个流的实现。一个是由反应库返回的Flux实现,包含 singer 实例。另一种是含有秒间隔的蒸汽,用java.time?? 级产生。使用用于将两个流组合在一起的zip方法,等待所有的流发出一个元素,并将这些元素组合成一个输出值(由提供的组合子构造)。操作员将继续这样做,直到任何源完成。在这种情况下,组合子是map(Tuple2::getT1)语句,在这里表示为方法引用;也可以写成map(t -> t.getT1()),这样更容易理解,基本上一个流中的一个元素映射到另一个。
让我们测试两个新的处理函数。有两种方法可以做到这一点,使用浏览器和使用curl命令。在浏览器中,如果你试图访问localhost:8080/,你得到的只是一个空白页面,浏览器会询问你是否要将all保存到一个文件中。如果您这样做,将显示一个弹出窗口,显示文件下载正在进行中。在某个时候,下载将会完成,文件的内容将会在您选择的编辑器中显示出来。如果你打开文件,你会看到它的内容。如果显示不可见字符,您应该能够看到"\n"行终止符。图 18-2 显示了下载文件的内容。但是,除了缓慢的下载时间,我们并没有清楚地看到每两秒钟就有一个歌手被发射出来。这只能通过使用curl命令才能看到,该命令在基于 Unix 的系统上可用。Windows 的话可以试试 PowerShell 里的Invoke-RestMethod。如果您使用前面示例中映射的 URIs 调用curl,您将会看到以下内容。执行curl http://localhost:8080/all的输出需要一段时间,每两秒钟打印一行。

图 18-2。
Response with Content-Type text/event-stream
$ curl http://localhost:8080/one/1
{"id":1,"version":0,"firstName":"John","lastName":"Mayer","birthDate":"1977-10-15"}
$ curl http://localhost:8080/all
data:{"id":1,"version":0,"firstName":"John","lastName":"Mayer","birthDate":"1977-10-15"}
data:{"id":2,"version":0,"firstName":"Eric","lastName":"Clapton","birthDate":"1945-03-29"}
data:{"id":3,"version":0,"firstName":"John","lastName":"Butler","birthDate":"1975-03-31"}
data:{"id":4,"version":0,"firstName":"B.B.","lastName":"King","birthDate":"1925-10-15"}
data:{"id":5,"version":0,"firstName":"Jimi","lastName":"Hendrix","birthDate":"1942-12-26"}
data:{"id":6,"version":0,"firstName":"Jimmy","lastName":"Page","birthDate":"1944-02-08"}
data:{"id":7,"version":0,"firstName":"Eddie","lastName":"Van Halen","birthDate":"1955-02-25"}
data:{"id":8,"version":0,"firstName":"Saul Slash","lastName":"Hudson","birthDate":"1965-08-22"}
data:{"id":9,"version":0,"firstName":"Stevie","lastName":"Ray Vaughan","birthDate":"1954-11-02"}
data:{"id":10,"version":0,"firstName":"David","lastName":"Gilmour","birthDate":"1946-04-05"}
data:{"id":11,"version":0,"firstName":"Kirk","lastName":"Hammett","birthDate":"1992-12-17"}
data:{"id":12,"version":0,"firstName":"Angus","lastName":"Young","birthDate":"1955-04-30"}
data:{"id":13,"version":0,"firstName":"Dimebag","lastName":"Darrell","birthDate":"1966-09-19"}
data:{"id":14,"version":0,"firstName":"Carlos","lastName":"Santana","birthDate":"1947-08-19"}
$
对于http://localhost:8080/one/1 URI,数据被自动转换成文本,因为这是默认的 accept 头,因为只有一个条目被返回。对于http://localhost:8080/all,请求挂起,每条消息在到达时都被转换成文本。注意不同的语法;对于包含多条消息的流,前缀"data:"用于发送的每个元素。
另一种访问 HTTP 请求返回的流的方法是使用反应式 web 客户端。当然,我们为此提供了一个接口。在这里我们解释它是如何使用的。因为我们需要共享上下文,我们将在同一个 Spring Boot Application类中声明客户端,如果您查看控制台,您会看到客户端在启动应用后立即运行。
package com.apress.prospring5.ch18;
...
@SpringBootApplication
@RestController
public class ReactiveApplication {
...
@Bean WebClient client() {
return WebClient.create("http://localhost:8080");
}
@Bean CommandLineRunner clr(WebClient client) {
return args -> {
client.get().uri("/all")
.accept(MediaType.TEXT_EVENT_STREAM)
.exchange()
.flatMapMany(cr -> cr.bodyToFlux(Singer.class))
.subscribe(System.out::println);
};
}
}
WebClient bean 是RestTemplate的反应式、非阻塞替代物,它将在未来的 Spring 版本中取代AsyncRestTemplate。在 Spring 5 中,它已经被标记为不推荐使用。
Spring WebFlux 中有很多新的组件可以用来编写反应式 web 应用;如果我们把它们都放在这里,这本书的篇幅会增加一倍。只需关注 Spring 的官方博客 https://spring.io/blog 就可以快速更新所有引入的新组件及其用法。现在让我们进入另一种选择。
Java 9 互操作性
因为 Java 9 的发布日期不断推迟,Spring 5 不得不基于 Java 8 发布,但是团队继续并行工作,通过与 JDK 的早期版本一起工作,使其适应 Java 9。在撰写本文时,Java 9 的大部分新奇组件已经稳定下来,计划中的 2017 年 9 月发布日期似乎是现实的。因此,与 Java 9 的互操作性在本书中基于早期的访问构建进行了讨论。让我们开始吧。
JDK 模块化
JDK 模块化被认为是 Java 9 中最大的改进。使 JDK 模块化具有可伸缩性的好处,因为现在 Java 可以部署在更小的设备上。模块化功能被称为 Project Jigsaw,计划是将其作为 Java 8 的一部分,但它被推到了 Java 9,因为它被认为不够稳定。因此,Java 9 引入了模块概念,它是一个软件单元,在一个名为module-info.java的文件中进行配置,该文件位于项目的源代码目录中,包含以下信息:
- 应该遵循包命名约定的模块名:按照约定,模块应该与根包同名。
- 一组导出的包:这些被认为是公共 API,可供其他模块使用。如果一个类在导出的包中,它可以在模块外被访问和使用。
- 一组必需的包:这些是这个模块所依赖的模块。由这些模块导出的包中的所有公共类型都是可访问的,并且可以由依赖模块使用。
这基本上意味着可访问性不再由四个 Java 分类器来表示:public、private、default和protected。因为模块配置决定了另一个模块可以从它的依赖模块中使用什么,所以现在有了以下内容:
- 在模块级别对每个阅读该模块的人公开(
exports) - 在模块级别对选定的模块列表公开(是,过滤访问,
export to) - 在模块内部是公共的,对模块内部的每个其他类都是公共的
- 模块内部私有,典型的私有访问
<default>在模块内部,典型的默认访问- 模块内部受保护,典型的受保护访问
这种模式被应用到 JDK,显然,许多循环和非直观的依赖被删除。该操作还涉及一些清理工作,因为有些包超出了 JSE 的范围。 11
这个主题对于 Spring 互操作性非常重要,因为 JDK 的组件可能不再能够被直接访问。直到现在,在本书中,我们并没有真正涉及 Java 9,因为该项目是使用 Java 8 构建的,Java 9 不够稳定,无法使用。但是现在是时候了。因此,更改 JDK 后,Gradle 安装必须升级到最新的 4.2 里程碑版本,以支持 Java 9。此后,项目的第一次构建将失败,并显示以下消息:
$ gradle clean build -x test
> Task :chapter03:collections:compileJava
/workspace/pro-spring-15/chapter03/collections/src/main/java/com/apress/prospring5/ch3/
annotated/CollectionInjection.java:13: error: package javax.annotation is not visible
import javax.annotation.Resource;
^
(package javax.annotation is declared in module java.xml.ws.annotation,
which is not in the module graph)
Note: Some input files use unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
1 error
FAILURE: Build failed with an exception.
这里发生的情况是,包javax.annotation不是导出的包之一,其中的任何注释都不能再使用。这个包和许多相关包不再可用的原因是它们包含了更适合作为 JEE 一部分的企业组件。这些包是名为java.se.ee的模块的一部分,根据前面提到的所有内容,我们只需将下面的模块添加到我们的文件中,一切都将正常工作:
module collections.main {
requires java.se.ee;
}
但事实并非如此,因为如果我们打开java.se.ee module-info.java,我们会发现:
@java.lang.Deprecated(since = "9", forRemoval = true)
module java.se.ee {
requires transitive java.xml.bind;
requires transitive java.activation;
requires transitive java.corba;
requires transitive java.se;
requires transitive java.transaction;
requires transitive java.xml.ws;
requires transitive java.xml.ws.annotation;
}
可以看到,没有包含exports java.xml.ws.annotation;的行,这是包含javax.annotation包的模块。那么,这里的解决方案是什么呢?当然,我们添加了包含该包的 JEE 依赖项。这是在 Gradle 配置文件中定义的:
misc = [
...
jsr250 : "javax:javaee-endorsed-api:7.0"
...
]
当然,在使用该包中的注释的任何地方,都必须添加misc.jsr250。让我们再试一次。根据您收到这本书的时间,您可能还会看到类似这样的内容:
$ gradle clean build -x test
# all good until here
...
:chapter05:aspectj-aspects:compileAspect
[ant:iajc] java.nio.file.NoSuchFileException:
/Library/Java/JavaVirtualMachines/jdk-9.jdk/Contents/Home/jrt-fs.jar
[ant:iajc] at
java.base/sun.nio.fs.UnixException.translateToIOExceptionUnixException.java:92 [ant:iajc] at java.base/sun.nio.fs.UnixException.rethrowAsIOExceptionUnixException.java:111
...
[ant:iajc] /workspace/pro-spring-15/chapter05/aspectj-aspects/src/main/java/com/apress/
prospring5/ch5/MessageWriter.java:5 [error] Implicit super constructor Object is undefined.
Must explicitly invoke another constructor
[ant:iajc] public MessageWriter {
[ant:iajc] ^^^^^^^^^^
[ant:iajc] /workspace/pro-spring-15/chapter05/aspectj-aspects/src/main/java/com/apress/
prospring5/ch5/MessageWriter.java:9 [error] System cannot be resolved
[ant:iajc] System.out.println"foobar!";
[ant:iajc]
[ant:iajc] /workspace/pro-spring-15/chapter05/aspectj-aspects/src/main/java/com/apress/
prospring5/ch5/MessageWriter.java:13 [error] System cannot be resolved
[ant:iajc] System.out.println"foo";
[ant:iajc]
[ant:iajc]
[ant:iajc] 11 errors
:chapter05:aspectj-aspects:compileAspect FAILED
276 actionable tasks: 186 executed, 90 up-to-date
这里发生的事情是因为方面没有被识别,所以aspectj-aspects不能被编译。发生这种情况是因为根据 Gradle aspects插件,需要这样做的 JAR 不在正确的位置。该插件显然已经过时,并且与新的 JDK 内部结构不匹配,但对此有一个小的修正:只需从插件寻找的位置的$JAVA_HOME/libs目录下复制jrt-fs.jar文件。
当构建再次运行时,会有很多不赞成的警告,但至少构建会成功。如果我们愿意,现在我们可以开始为所有模块添加module-info.java文件。让我们以chapter02/hello-world项目为例,定义module-info.java,因为它是一个小项目,应该很容易。
// pro-spring-15/chapter02/hello-world/src/main/java/module-info.java
module com.apress.prospring5.ch2 {
requires spring.context;
requires logback.classic;
exports com.apress.prospring5.ch2;
}
差不多了。为了保持模块名称的简单,Spring 团队决定打破常规;否则,我们将不得不添加requires org.springframework.context,而不是requires spring.context;。 12
Project Jigsaw 增加的模块化不仅仅是分割 JDK 和限制对某些包和组件的访问(反射在非导出模块上不起作用);它在编译时检测循环依赖关系,提高可读性,并检测仅版本不同的重复模块依赖关系,消除应用的冲突或混乱行为。由于所有这些开发上的好处,Java 9 的这一新颖特性值得在本书中单列一节。
用 Java 9 和 Spring WebFlux 进行反应式编程
在几节之前,我们介绍了反应式模型并提到了标准 API。好吧,走开,因为镇上来了一个新男孩!我们向您介绍java.base模块,它导出了包exports java.util.concurrent,其中包含四个功能接口,目的相同,都在java.util.concurrent.Flow最终类中定义。
Flow.Processor,相当于org.reactivestreams.Processor<T>Flow.Publisher,相当于org.reactivestreams.Publisher<T>Flow.Subscriber,相当于org.reactivestreams.Subscriber<T>Flow.Subscription,相当于org.reactivestreams.Subscription<T>
这些接口与 Reactive Streams 项目定义的接口具有相同的功能,用于支持发布-订阅应用的创建,即反应式应用。JDK 9 提供了一个简单的Publisher实现,可以用于简单的用例,也可以根据需求进行扩展。Su bmissionPublisher<T>是Flow.Publisher<>接口的一个实现类,它也实现了AutoCloseable,可以在try-with-resources块中使用。
RxJava 13 是 JVM 的反应式实现之一,旨在支持数据/事件序列,并通过隐藏线程同步和线程安全等底层问题来添加允许流合成和过滤的运算符。在撰写本文时,RxJava 上有两个版本:RxJava 1.x 和 rx Java 2 . x。rx Java 1 . x 是 ReactiveX(反应式扩展) 14 的实现,这是一个用于可观察流的异步编程的 API。RxJava1 将被删除,因为它是在上一节介绍的 Reactive Streams API 的基础上完全重写的。Spring 5 可以支持这两者,但是因为 RxJava 1.x 将在 2018 年被放弃,所以让我们着眼于未来。
在 Project Reactor 的 Spring 反应式编程示例中,我们实现了一个反应式存储库和一个反应式客户端。我们将在这里通过使用 RxJava2 来实现这一点。我们将定义一个名为Rx2SingerRepo的反应式存储库接口。
package com.apress.prospring5.ch18.repos;
import com.apress.prospring5.ch18.entities.Singer;
import io.reactivex.Flowable;
import io.reactivex.Single;
public interface Rx2SingerRepo {
Single<Singer> findById(Long id);
Flowable<Singer> findAll();
Single<Void> save(Single<Singer> singer);
}
仔细观察前面的代码片段后,您可能注意到了等价性:Flowable类是元素流的实现,Single是[0..1]元素流的实现。创建它们也很容易,您会注意到Single类型在创建空流方面有一个小的 API 差异。
Flowable simple = Flowable.just("1", "2", "3");
//or from an existing list: List<Singer>
Flowable<Singer> fromList = Flowable.fromIterable(list);
Single simple = Single.just("1");
//or from an existing object of type Singer
Single<Singer> fromObject = Single.just(null);
因此,让我们看看 RxJava2 的反应式存储库实现是什么样子的:
package com.apress.prospring5.ch18.repos;
import com.apress.prospring5.ch18.entities.Singer;
import io.reactivex.Flowable;
import io.reactivex.Single;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class Rx2SingerRepoImpl implements Rx2SingerRepo {
@Autowired
SingerRepository singerRepository;
@Override public Single<Singer> findById(Long id) {
return Single.just(singerRepository.findById(id).get());
}
@Override public Flowable<Singer> findAll() {
return Flowable.fromIterable(singerRepository.findAll());
}
@Override public Single<Void> save(Single<Singer> singerSingle) {
singerSingle.doOnSuccess(singer -> singerRepository.save(singer));
return Single.just(null);
}
}
正如我们所看到的,语法是相似的,但是需要考虑 API 中的一些小差异。如果我们也要重写映射方法,它们看起来应该是这样的:
package com.apress.prospring5.ch18;
...
@SpringBootApplication
@RestController
public class Rx2ReactiveApplication {
private static Logger logger = LoggerFactory.getLogger(Rx2ReactiveApplication.class);
@Autowired Rx2SingerRepo rx2SingerRepo;
@GetMapping(value = "/all", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flowable<Singer> all() {
Flowable<Singer> singers = rx2SingerRepo.findAll();
Flowable<Long> periodFlowable = Flowable.interval(2, TimeUnit.SECONDS);
return singers.zipWith(periodFlowable, (singer, aLong) -> {
Thread.sleep(aLong);
return singer;
});
}
@GetMapping(value = "/one/{id}")
public Single<Singer> one(@PathVariable Long id) {
return rx2SingerRepo.findById(id);
}
...
}
用 RxJava2 实现 zipping 函数好像有点复杂,不是吗?
为了测试映射,您仍然可以使用 Spring reactive WebClient,这非常酷。或者,您可以使用 JDK 9 的新 HTTP 客户端进行测试。
import jdk.incubator.http.HttpClient;
import jdk.incubator.http.HttpRequest;
import jdk.incubator.http.HttpResponse;
...
URI oneURI = new URI("http://localhost:8080/one/1");
HttpClient client = HttpClient
.newBuilder()
.build();
HttpRequest httpRequest = HttpRequest.newBuilder().GET().build();
HttpResponse httpResponse = client.send(httpRequest,
HttpResponse.BodyHandler.asString());
System.out.println(httpResponse.statusCode());
System.out.println(httpResponse.body());
只要确保在您的module-info.java文件的jdk.incubator.httpclient模块中添加一个依赖项。
这就是关于 Java 9 互操作性的全部内容。与 Spring 框架相关的最大变化是模块化和 Spring 已经支持的新的反应式 API rx Java 2。其他所有 Java 9 的新奇之处都包含在另一本非常有趣的新书《Java 9 揭秘》中。 十五
JUnit 5 Jupiter 的 Spring 支持
JUnit 5 在第十三章中有所提及,现在是时候深入探讨一下了。如果你想知道这些大惊小怪是怎么回事,你可以在 http://junit.org/junit5/docs/current/user-guide/#overview 的官方文档中找到最简单的答案(见图 18-3 )。

图 18-3。
JUnit 5
JUnit 平台是在 JVM 上启动测试框架的基础。它附带了一个控制台启动器,用于从命令行启动平台,并为 Gradle 和 Maven 构建插件。这个启动器可以用来发现、过滤和执行测试;因此,Gradle 测试任务不需要万全之策或定制。此外,第三方库如 Spock、Cucumber 和 FitNesse 可以通过提供定制的TestEngine来插入 JUnit 平台的启动基础设施。
JUnit Jupiter 是新编程模型(基于驻留在org.junit.jupiter.api包中的一组新的 JUnit5 注释)和扩展模型(Extension API 及其@ExtendWith注释旨在取代 JUnit 4 的Runner @Rule和@ClassRule)的组合,用于在 JUnit 5 中编写测试和扩展。Jupiter 子项目为在平台上运行基于 Jupiter 的测试提供了一个TestEngine。
正如您可能猜到的那样,JUnit Vintage 为在平台上运行基于 JUnit 3 和 JUnit 4 的测试提供了一个TestEngine。
但是,让我们通过测试SingerHandler类来看看它的运行情况。我们将从阴性测试方法开始。具体来说,让我们设法获得一个不存在的歌手。
package com.apress.prospring5.ch18;
import com.apress.prospring5.ch18.entities.Singer;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.ExchangeFunction;
import org.springframework.web.reactive.function.client.ExchangeFunctions;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class SingerHandlerTest {
private static Logger logger = LoggerFactory.getLogger(SingerHandlerTest.class);
public static final String HOST = "localhost";
public static final int PORT = 8080;
private static ExchangeFunction exchange;
@BeforeAll
public static void init(){
exchange = ExchangeFunctions.create(new ReactorClientHttpConnector());
}
@Test
public void noSinger(){
//get singer
URI uri = URI.create(String.format("http://%s:%d/singers/99", HOST, PORT));
logger.debug("GET REQ: "+ uri.toString());
ClientRequest request = ClientRequest.method(HttpMethod.GET, uri).build();
Mono<Singer> singerMono = exchange.exchange(request)
.flatMap(response -> response.bodyToMono(Singer.class));
Singer singer = singerMono.block();
assertNull(singer);
}
...
}
ExchangeFunction是一个功能接口,用来将一个ClientRequest换成一个延时的ClientResponse,可以作为WebClient的替代。使用这样的实现,可以将请求发送到正在运行的服务器,并且可以分析响应。在前面的示例中,您可能注意到了以下情况:
@BeforeAll注释类似于 JUnit classic 中的@BeforeClass;因此,用它注释的方法必须在当前类中任何用@Test(或类似RepeatedTest的派生注释)注释的方法之前执行。它的特别之处在于,如果在类级别使用了@TestInstance(Lifecycle.PER_CLASS)注释,它可以在非静态方法上设置。- 包
org.junit.jupiter.api中的@Test注释等同于 JUnit classic 中的相同注释;唯一的区别是这个注释没有声明任何属性,因为 JUnit Jupiter 中的测试扩展基于它们自己的专用注释进行操作。 - 来自类
org.junit.jupiter.api.Assertions的assertNull语句也类似于 JUnit classic 中的相同注释。对于Assertions类中的所有静态函数,都有类似的实现和一些额外的实现,它们非常适合与 Java 8 lambdas 一起使用。
让我们看一个更详细的例子。这一次,我们测试 singer 实例的编辑。首先,我们通过用分组断言检查firstName和lastName,测试我们肯定检索到了正确的歌手。
package com.apress.prospring5.ch18;
import com.apress.prospring5.ch18.entities.Singer;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.ExchangeFunction;
import org.springframework.web.reactive.function.client.ExchangeFunctions;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class SingerHandlerTest {
private static Logger logger = LoggerFactory.getLogger(SingerHandlerTest.class);
public static final String HOST = "localhost";
public static final int PORT = 8080;
private static ExchangeFunction exchange;
@BeforeAll
public static void init(){
exchange = ExchangeFunctions.create(new ReactorClientHttpConnector());
}
@Test
public void editSinger() {
//get singer
URI uri = URI.create(String.format("http://%s:%d/singers/1", HOST, PORT));
logger.debug("GET REQ: "+ uri.toString());
ClientRequest request = ClientRequest.method(HttpMethod.GET, uri).build();
Mono<Singer> singerMono = exchange.exchange(request)
.flatMap(response -> response.bodyToMono(Singer.class));
Singer singer = singerMono.block();
assertNotNull(singer);
assertAll("singer",
() -> assertEquals("John", singer.getFirstName()),
() -> assertEquals("Mayer", singer.getLastName()));
logger.info("singer:" + singer.toString());
//edit singer
singer.setFirstName("John Clayton");
uri = URI.create(String.format("http://%s:%d/singers", HOST, PORT));
logger.debug("UPDATE REQ: "+ uri.toString());
request = ClientRequest.method(HttpMethod.POST, uri)
.body(BodyInserters.fromObject(singer)).build();
Mono<ClientResponse> response = exchange.exchange(request);
assertEquals(HttpStatus.OK, response.block().statusCode());
logger.info("Update Response status: " + response.block().statusCode());
}
}
我们可以做得更多。在前面的例子中,我们可以通过上面的assetNotNull断言来决定assertEquals断言的执行。在前一个例子中,如果返回了错误的歌手,两个assertEquals语句都会被执行并失败。 16 声明assertNotNull执行依赖的前一个assertEquals执行的代码如下所示:
assertAll("singer", () -> {
assertNotNull(singer);
assertAll("singer",
() -> assertEquals("John", singer.getFirstName()),
() -> assertEquals("Mayer", singer.getLastName()));
});
我们将在这里停下来讨论特定于 JUnit 的测试组件,然后继续讨论更多的 Spring 测试新特性。除了WebClient接口之外,对于反应式编程模型,spring-test现在包括了用于集成 Spring WebFlux 测试支持的WebTestClient。新的WebTestClient,类似于MockMvc,不需要运行服务器。这并不意味着它不能和现有的一起使用。在下面的示例中,WebTestClient用于从SingerApplication类运行的应用:
package com.apress.prospring5.ch18;
import com.apress.prospring5.ch18.entities.Singer;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.FluxExchangeResult;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Mono;
import java.util.Date;
import java.util.GregorianCalendar;
import static org.junit.jupiter.api.Assertions.*;
public class AnotherSingerHandlerTest {
private static WebTestClient client;
@BeforeAll
public static void init() {
client = WebTestClient
.bindToServer()
.baseUrl("http://localhost:8080")
.build();
}
@Test
public void getSingerNotFound() throws Exception {
client.get().uri("/singers/99").exchange().expectStatus().isNotFound()
.expectBody().isEmpty();
}
@Test
public void getSingerFound() throws Exception {
client.get().uri("/singers/1").exchange().expectStatus().isOk()
.expectBody(Singer.class).consumeWith(seer -> {
Singer singer = seer.getResponseBody();
assertAll("singer", () ->
{
assertNotNull(singer);
assertAll("singer",
() -> assertEquals("John", singer.getFirstName()),
() -> assertEquals("Mayer", singer.getLastName()));
});
});
}
@Test
public void getAll() throws Exception {
client.get().uri("/singers").accept(MediaType.TEXT_EVENT_STREAM)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBodyList(Singer.class).consumeWith(Assertions::assertNotNull);
}
@Test
public void create() throws Exception {
Singer singer = new Singer();
singer.setFirstName("Ed");
singer.setLastName("Sheeran");
singer.setBirthDate(new Date(
(new GregorianCalendar(1991, 2, 17)).getTime().getTime()));
client.post().uri("/singers").body(Mono.just(singer), Singer.class)
.exchange().expectStatus().isOk();
}
}
WebTestClient是测试 WebFlux 服务器端点的主要组件。它有一个类似于WebClient的 API,并将大部分工作委托给内部的WebClient,后者主要关注于提供一个测试环境。要在实际运行的服务器上运行集成测试,必须调用bindToServer()方法。前面的例子很简单。WebTestClient绑定到实际应用运行的地址;因此,它不需要定义自己的映射或路由功能。但在某些情况下,当这是必要的,这是可以做到的,因为有更多的配置方法可用。以下代码片段中的测试方法通过调用bindToRouterFunction(function)来使用自定义的RouterFunction:
@Test
public void testCustomRouting(){
RouterFunction function = RouterFunctions.route(
RequestPredicates.GET("/test"),
request -> ServerResponse.ok().build()
);
WebTestClient
.bindToRouterFunction(function)
.build().get().uri("/test")
.exchange()
.expectStatus().isOk()
.expectBody().isEmpty();
}
这里值得一提的另一个特性是,使用 Spring JUnit 5,集成测试可以并行运行。现在我们不得不离开Singer应用,这样我们就可以保持代码样本的准确性。为了向您展示如何并行运行测试,将使用一个简单的 Spring Boot 应用,它只声明一个类型为FluxGenerator的 bean,顾名思义,它生成Flux实例。这里描述了应用和简单 bean 类型FluxGenerator的配置/入口点的代码:
//FluxGenerator.java
package com.apress.prospring5.ch18;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
@Component
public class FluxGenerator {
public Flux<String> generate(String... args){
return Flux.just(args);
}
}
//Application.java
package com.apress.prospring5.ch18;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;
@SpringBootApplication
public class Application {
private static final Logger logger = LoggerFactory.getLogger(Application.class);
public static void main(String args) throws Exception {
ConfigurableApplicationContext ctx =
new SpringApplicationBuilder(Application.class)
.run(args);
assert (ctx != null);
logger.info("Application started...");
System.in.read();
ctx.close();
}
}
接下来,我们将创建两个测试类,IntegrationOneTest和IntegrationTwoTest,每个测试类都将声明两个测试方法;他们不会测试任何东西,只会使用FluxGenerator bean 获得一个Flux实例并打印其内容。这里描述了这两个类,正如您所看到的,两组值是不同的,因此我们可以在命令行中跟踪执行,并确保它是并行执行的。
//IntegrationOneTest.java
package com.apress.prospring5.ch18.test;
import com.apress.prospring5.ch18.FluxGenerator;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class IntegrationOneTest {
private final Logger logger = LoggerFactory.getLogger(IntegrationOneTest.class);
@Autowired FluxGenerator fluxGenerator;
@Test
public void test1One() {
fluxGenerator.generate("1", "2", "3").collectList().block()
.forEach(s -> executeSlow(2000, s) );
}
@Test
public void test2One() {
fluxGenerator.generate("11", "22", "33").collectList().block()
.forEach(s -> executeSlow(1000, s));
}
private void executeSlow(int duration, String s) {
try {
Thread.sleep(duration);
logger.info(s);
} catch (InterruptedException e) {
}
}
}
//IntegrationTwoTest.java
package com.apress.prospring5.ch18.test;
... // same imports as above
@RunWith(SpringRunner.class)
@SpringBootTest
public class IntegrationTwoTest {
private final Logger logger =
LoggerFactory.getLogger(IntegrationTwoTest.class);
@Autowired FluxGenerator fluxGenerator;
@Test
public void test1One() {
fluxGenerator.generate(2, "a", "b", "c").collectList().block()
.forEach(logger::info);
}
@Test
public void test2One() {
fluxGenerator.generate(3, "aa", "bb", "cc").collectList().block()
.forEach(logger::info);
}
}
这里定义了执行这些测试的类。它包含两个测试方法——一个并行执行测试,另一个以线性方式一个接一个地执行测试。
package com.apress.prospring5.ch18.test;
import org.junit.experimental.ParallelComputer;
import org.junit.jupiter.api.Test;
import org.junit.runner.Computer;
import org.junit.runner.JUnitCore;
public class ParallelTests {
@Test
void executeTwoInParallel() {
final Class<?> classes = {
IntegrationOneTest.class, IntegrationTwoTest.class
};
JUnitCore.runClasses(new ParallelComputer(true, true), classes);
}
@Test
void executeTwoLinear() {
final Class<?> classes = {
IntegrationOneTest.class, IntegrationTwoTest.class
};
JUnitCore.runClasses(new Computer(), classes);
}
}
JUnitCore是运行测试的门面。它支持 JUnit 4、3.8 测试和混合测试。它接收一个Computer实例或者一个ParallelComputer实例的扩展作为参数。Computer类用于以一个接一个的线性方式正常执行测试。ParallelComputer实例允许并行运行测试,它的构造函数接收两个布尔参数。第一个是类的并行执行;第二个是方法。在前面的例子中,我们将它们都设置为"true"来告诉 JUnit runner 我们希望类和方法并行执行。这就是为什么IntegrationOneTest与其对应的IntegrationTwoTest类略有不同。
如果您运行executeTwoInParallel方法,您应该在控制台中看到一个类似于此处描述的输出的日志:
...
17:29:30.460 [pool-2-thread-2] INFO c.a.p.c.t.IntegrationTwoTest - aa
17:29:30.460 [pool-2-thread-2] INFO c.a.p.c.t.IntegrationTwoTest - bb
17:29:30.460 [pool-2-thread-1] INFO c.a.p.c.t.IntegrationTwoTest - a
17:29:30.460 [pool-2-thread-2] INFO c.a.p.c.t.IntegrationTwoTest - cc
17:29:30.460 [pool-2-thread-1] INFO c.a.p.c.t.IntegrationTwoTest - b
17:29:30.460 [pool-2-thread-1] INFO c.a.p.c.t.IntegrationTwoTest - c
17:29:31.463 [pool-1-thread-2] INFO c.a.p.c.t.IntegrationOneTest - 11
17:29:32.461 [pool-1-thread-1] INFO c.a.p.c.t.IntegrationOneTest - 1
17:29:32.468 [pool-1-thread-2] INFO c.a.p.c.t.IntegrationOneTest - 22
17:29:33.472 [pool-1-thread-2] INFO c.a.p.c.t.IntegrationOneTest - 33
17:29:34.466 [pool-1-thread-1] INFO c.a.p.c.t.IntegrationOneTest - 2
17:29:36.471 [pool-1-thread-1] INFO c.a.p.c.t.IntegrationOneTest - 3
...
这里我们想涉及的最后一个主题是 JUnit 5 扩展 API。其核心是Extension接口,它只是组件的一个标记接口,可以通过@ExtendWith显式注册或通过 Java 的ServiceLoader机制自动注册。在 Spring 5 中,SpringExtension被添加到了spring-test模块中,该模块实现了很多@ExtendWith的 Jupiter 接口衍生物,以将 Spring TestContext 框架集成到 JUnit 5 的 Jupiter 编程模型中。要使用这个扩展,只需用@ExtendWith(SpringExtension.class)、@SpringJUnitConfig或@SpringJUnitWebConfig注释一个基于 JUnit Jupiter 的测试类。
让我们看一个简单的例子。我们将创建一个类namedTestConfig,它将声明一个类型为FluxGenerator的 bean,并创建一个测试类来测试这个 bean。这里描述了两个类:
//TestConfig.java
package com.apress.prospring5.ch18.test.config;
import com.apress.prospring5.ch18.FluxGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TestConfig {
@Bean FluxGenerator generator(){
return new FluxGenerator();
}
}
package com.apress.prospring5.ch18.test;
import com.apress.prospring5.ch18.Application;
import com.apress.prospring5.ch18.FluxGenerator;
import com.apress.prospring5.ch18.test.config.TestConfig;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
//JUnit5IntegrationTest.java
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = TestConfig.class)
public class JUnit5IntegrationTest {
@Autowired FluxGenerator fluxGenerator;
@Test
public void testGenerator() {
List<String> list = fluxGenerator.generate("2", "3")
.collectList().block();
assertEquals(2, list.size());
}
}
在 Spring JUnit 5 支持方面还有很多工作要做。因此,本节介绍的源代码可能会在 Spring 的未来版本中进行一些修改。这个区域太新了,甚至还没有在 Spring 文档中介绍过, 17 可能会在将来的某个时候更新。
摘要
在这一章中,我们提供了 Spring portfolio 中几个项目的高级概述。我们看了一下 Spring Batch、JSR-352、Integration、XD、WebFlux 和 Spring 对 JUnit 5 的支持,每一个都提供了自己独特的功能,旨在简化开发人员手头的特定任务。这些项目中有些是新的,有些已经被证明是稳定和可靠的,是其他框架的理想基础。我们鼓励您更深入地了解这些项目,因为我们认为它们将极大地简化您的 Java 项目。
Footnotes 1
目的是让 Spring 5 与 Java 9 完全兼容,但是由于它的发布已经晚了 18 个月,Spring 团队决定坚持使用 Java 8。不过 Java 9 计划在 2017 年 9 月发布,5.x 版本很可能会与之完全集成。
2
您可能还记得,由于运行时发现算法有问题,所以项目设置是为了避免 Commons 日志记录;你可以在 Spring 的官方参考页面https//docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#overview-avoiding-commons-logging上读到这个。
3
4
https://docs.oracle.com/javase/9/whatsnew/toc.htm见。
5
你可以在这里找到官方网站: http://junit.org/junit5/docs/current/user-guide/ 。
6
为了跟踪 Spring 项目的发布和内容,我们建议您关注 Spring 官方博客 https://spring.io/blog 。
7
你可以在 https://projectreactor.io/docs/core/release/reference/#flux 找到详细的解释。
8
你可以在 https://projectreactor.io/docs/core/release/reference/#mono 找到详细的解释。
9
如果您需要更多关于反应式编程模型和反应式流编程的资源,您可能希望尝试参考文档、代码示例和 Project Reactor 在 https://projectreactor.io/docs 的 Javadoc,或者在 https://gist.github.com/staltz/868e7e9bc2a7b8c1f754 的 reactive programming introduction。
10
如果你想深入了解这一点,在 Spring 官方博客上有一篇关于它的博文: https://spring.io/blog/2016/09/22/new-in-spring-5-functional-web-framework 。
11
点击 https://blog.codecentric.de/en/2015/11/first-steps-with-java9-jigsaw-part-1/ 了解更多信息。
12
看看导致这个决定的讨论: https://jira.spring.io/browse/SPR-14579 。
13
https://github.com/ReactiveX/RxJava见。
14
15
你可以从 www.apress.com/la/book/9781484225912 订购。
16
试着把 URI 改成http://%s:%d/singers/2。
17
Spring 参考的测试章节甚至没有提到 JUnit 5;参见https://docs.spring.io/spring/docs/current/pring-framework-reference/htmlsingle/#testing。









浙公网安备 33010602011771号