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)框架
图 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。
图 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
图 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 消息传递
图 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