Spring学习指南 (第3版)

978-7-115-48237-2
作者: [印度] J. 夏尔马(J. Sharma )阿西施·萨林(Ashish Sarin)
译者: 周密
编辑: 吴晋瑜
分类: Spring

图书目录:

详情

本书面对初学者,帮助读者更轻松地学会如何使用Spring框架。从介绍Spring框架入手,介绍了Beans的配置,依赖注入,定制Bean,基于Java的容器,AOP,Spring Data、Spring MVC等知识。最新的第3版针对Spring 4.3,并加入了基于Java的配置和Spring Data的章节。

图书摘要

版权信息

书名:Spring学习指南 (第3版)

ISBN:978-7-115-48237-2

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

著    [印度] J. 夏尔马(J. Sharma )

     阿西施•萨林 (Ashish Sarin)

译    周 密

责任编辑 吴晋瑜

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315


Simplified Chinese translation copyright ©2018 by Posts and Telecommunications Press

All Rights Reserved.

Getting started with Spring Framework,by J. Sharma and Ashish Sarin.

Copyright © 2016 by J. Sharma and Ashish Sarin.

本书中文简体版由J. Sharma和Ashish Sarin授权人民邮电出版社出版。未经出版者书面许可,对本书的任何部分不得以任何方式或任何手段复制和传播。

版权所有,侵权必究。


Spring框架是以简化J2EE应用程序开发为特定目标而创建的,是当前最流行的Java开发框架。

本书从介绍Spring框架入手,针对Spring 4.3和Java 8介绍bean的配置、依赖注入、定义bean、基于Java的容器、AOP、Spring Data、Spring MVC等知识,旨在帮助读者更轻松地学习Spring框架的方法。

本书适合Web开发者和想使用Spring的初学者参考,也可供对 Web开发和Spring感兴趣的读者参考。


Spring框架可以说是当前Java开发的事实标准,但是大多数高校教材中并没有涵盖相关内容,这使得很多Java开发人员只能在工作中靠口口相传或者自学来了解Spring框架,虽然最终可以掌握,但是由于缺乏系统性的指导,难免在花费大量时间之余走很多的弯路。

本书是Spring框架的入门指南,兼具系统性和实用性,全面介绍了Spring框架的设计思想和模块构成,并针对每个模块都给出了应用场景以及相应的源代码示例,以引导开发者掌握Spring框架的使用。

本书适合有一定Java基础的学生或者初级开发人员学习,也可供对Spring框架掌握不够系统或不了解新版本Spring框架功能的资深开发人员参考。

在翻译过程中,我尽量遵循原文意思,力求使译文贴合中文阅读习惯,但碍于自身水平有限,难免会有不尽如人意的地方,还请广大读者不吝指正,谢谢!

最后,感谢人民邮电出版社的各位编辑老师对我的鼓励,感谢推荐我承接翻译工作的高博老师,也感谢在本书翻译过程中给予我理解与支持的家人,我爱你们!

周 密 

2018年5月


本书有许多示例项目,你可以从GitHub项目中自行寻找。你可以将示例项目作为单个ZIP文件下载,也可以使用Git检索出示例项目。

如果你在阅读本书时发现有IMPORT chapter<chapter-number>/<project name>__这样的标识,那么应该将指定的项目导入你的Eclipse或者 IntelliJ IDEA IDE(或者任何其他正在使用的IDE)。这些示例项目使用Maven 3.x 版本作为项目的构建工具,因此,你在每个项目中都可以找到一个pom.xml文件。在整套源码的根目录中也有一个pom.xml文件,该文件是用来一次性构建全部项目的。

通过参考附录B可以了解导入和运行示例项目所需的步骤。

在每个程序示例中都会指明示例项目的名称(使用 Project 标签)和源文件位置(使用 Source location 标签)。如果没有在程序示例中指明Project标签和Source location 标签,你可以认为程序示例中的代码并不是从示例项目中摘录出来的,纯粹只是为了帮助你理解。

粗体用于强调术语。

Comic Sans MS用于程序示例、Java代码、XML中的配置细节和属性文件。

Comic Sans MS用于在程序示例中突出重要的代码或者配置。

注意

这样的一个标注突出了一个重要的观点或概念。

你可以在Google Groups论坛中向作者发送反馈和问题。


本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。

作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,单击“提交勘误”,输入勘误信息,单击“提交”按钮即可。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,将赠予您异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。

我们的联系邮箱是contact@epubit.com.cn。

如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线提交投稿(直接访问www.epubit.com/selfpublish/submission即可)。

如果您是学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。

“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT技术图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。

异步社区

微信服务号


在传统的Java企业级应用开发中,创建结构良好、易于维护和易于测试的应用程序是开发者的职责。开发者用各式各样的设计模式来解决这些应用的非业务需求。这不但导致开发者生产效率低下,而且对开发应用的质量造成了不良影响。

Spring框架(简称Spring)是SpringSource出品的一个用于简化Java企业级应用开发的开源的应用程序框架。它提供了开发一个结构良好的、可维护和易于测试的应用所需的基础设施,当使用Spring框架时,开发者只需要专注于编写应用的业务逻辑,从而提高了开发者的生产效率。你可以使用Spring框架开发独立的Java应用程序、Web应用程序、Applet,或任何其他类型的Java应用程序。

本章首先介绍Spring框架的模块和它们的优点。Spring框架的核心是提供了依赖注入(Dependency Injection,DI)机制的控制翻转(Inversion of Control,IoC)容器。本章将介绍Spring的DI机制以及IoC容器,并展示如何使用Spring开发一个独立的Java应用。在本章的结尾,我们来看一些以Spring框架为基础的SpringSource项目。有了本章的铺垫,我们可以在后面的章节更深入地探究Spring框架。

注意

在本书中,我们将以一个名为MyBank的网上银行应用为例,介绍Spring框架的功能。

Spring框架由多个模块组成,它们根据应用开发功能进行分组。表1-1列出了Spring框架中的各个模块组,并描述了其中一些重要模块组所提供的功能。

表1-1 Spring框架中的各个模块组

模块组

描述

Core container

包含构成Spring框架基础的模块。该组中的spring-core和spring-beans模块提供了Spring的DI功能和IoC容器实现。spring-expressions模块为在Spring应用中通过Spring 表达式语言(见第6章)配置应用程序对象提供了支持

AOP and instrumentation

包含支持AOP(面向切面编程)和类工具模块。The spring-aop模块提供Spring的AOP功能,spring-instrument模块提供了对类工具的支持

Messaging

包含简化开发基于消息的应用的spring-messaging模块

Data Access/Integration

包含简化与数据库和消息提供者交互的模块。spring-jdbc模块简化了用JDBC与数据库的交互,spring-orm模块提供了与ORM(对象关系映射)框架的集成,如JPA和Hibernate。spring-jms模块简化了与JMS提供者的交互。
此模块组还包含spring-tx模块,该模块提供了编程式与声明式事务管理

Web

包含简化开发Web和portlet应用的模块。spring-web和spring-webmvc模块都是用于开发Web应用和RESTful的Web服务的。spring-websocket模块支持使用WebSocket开发Web应用

Test

包含spring-test模块,该模块简化了创建单元和集成测试

由表1-1可知,Spring涵盖了企业应用程序开发的各个方面,可以使用Spring开发Web应用程序、访问数据库、管理事务、创建单元和集成测试等。在设计Spring框架模块时,你只需要引入应用程序所需要的模块。例如,在应用程序中使用Spring的DI功能,只需要引入Core container组中的模块。看完本书之后,你会发现更多关于Spring模块和示例的细节,来展示如何把它们应用在开发工作中。

在Spring框架中,JAR文件的命名惯例如下:

spring-<short-module-name>-<spring-version>.jar. 

其中, <short-module-name> Spring模块的简称,如aop、 beans、 context、 expressions等。而<spring-version> 是Spring框架的版本。

根据这个命名惯例,Spring 4.3.0.RELEASE版本中JAR文件的名字为spring-aop-4.3.0.RELEASE.jar、 spring-beans-4.3.0.RELEASE.jar等。

图1-1显示了Spring模块之间的依赖关系。

图1-1 Spring模块之间的依赖关系

从图1-1可知,Core container组所包含的模块是Spring框架的中心,其他模块都依赖于它。同等重要的是AOP and instrumentation组所包含的模块,因为它们提供了Spring框架中其他模块的AOP功能。

现在你对Spring所涵盖的应用程序开发有了一些基本的概念,让我们来看看Spring的IoC容器。

一个Java应用程序由互相调用以提供应用程序行为的一组对象组成。某个对象调用的其他对象称为它的依赖项。例如,如果对象X调用了对象Y和Z,那么Y和Z就是对象X的依赖项。DI是一种设计模式,其中对象的依赖项通常被指定为其构造函数和setter方法的参数。并且,这些依赖项将在这些对象创建时注入该对象中。

在Spring应用程序中,Spring IoC容器(也称为Spring容器)负责创建应用程序对象并注入它们的依赖项。Spring容器创建和管理的应用对象称为bean。由于Spring容器负责将应用程序对象组合在一起,因此不需要实现诸如工厂或者服务定位器等设计模式来构成应用。因为创建和注入依赖项的不是应用程序的对象,而是Spring容器,所以DI也称为控制反转(IoC)。

假设Mybank应用程序(这是示例应用程序的名称)包含FixedDepositController和 FixedDepositService两个对象,FixedDepositController对象依赖于FixedDepositService对象,如程序示例1-1所示。

程序示例1-1 FixedDepositController 类

public class FixedDepositController {
  private FixedDepositService fixedDepositService;

  public FixedDepositController() {
    fixedDepositService = new FixedDepositService();
  }

  public boolean submit() {
    //-- 保存定期存款明细
    fixedDepositService.save(.....);
  }
}

在程序示例1-1中, FixedDepositController的构造函数创建了一个FixedDepositService的实例后用于FixedDepositController的submit方法。因为FixedDepositController调用了FixedDepositService,所以FixedDepositService就是FixedDepositController的一个依赖项。

若要将FixedDepositController配置为一个Spring bean,首先需要修改在程序示例1-1中的FixedDepositController类,让它接收FixedDepositService依赖作为构造函数参数或者setter方法的参数。修改后的FixedDepositController类,如程序示例1-2所示。

程序示例1-2 FixedDepositController 类—— FixedDepositService作为构造函数参数传递

public class FixedDepositController {
  private FixedDepositService fixedDepositService;

  public FixedDepositController(FixedDepositService fixedDepositService) {
    this.fixedDepositService = fixedDepositService;
  }

  public boolean submit() {
    //--保存定期存款明细
    fixedDepositService.save(.....);
  }
}

程序示例1-2表明 FixedDepositService示例现在已经作为构造函数参数传递到FixedDepositController 实例中。现在的FixedDepositService类可以配置为一个Spring bean。注意,FixedDepositController 类并没有实现或者继承任何Spring的接口或者类。

在基于Spring的应用程序中,有关应用程序对象及其依赖项的信息都是由配置元数据来指定的。Spring IoC容器读取应用程序的配置元数据来实例化应用程序对象并注入它们的依赖项。程序示例1-3展示了一个包含MyController和MyService两个类的应用的配置元数据(XML格式)。

程序示例1-3 配置元数据

<beans .....>
  <bean id="myController" class="sample.spring.controller.MyController">
    <constructor-arg ref="myService" />
  </bean>

  <bean id="myService" class="sample.spring.service.MyService"/>
</beans>

在程序示例1-3中,每个<bean>元素定义了一个由Spring容器管理的应用对象,而<constructor-arg>元素指定MyService实例作为一个构造函数的参数传递给MyController。在本章后面的部分将会介绍<bean>元素,而<constructor-arg>元素的介绍会放在第2章。

Spring容器读取应用程序的配置元数据(见程序示例1-3)后,创建由<bean>元素定义的应用程序并注入它们的依赖项。Spring容器使用Java反射API创建应用程序对象并注入其依赖项。图1-2总结了Spring容器的工作原理。

图1-2 Spring容器读取应用程序的配置元数据并创建一个配置完整的应用程序

Spring容器的配置元数据可以通过XML(见程序示例1-3)、Java注释(见第6章)以及Java代码(见第7章)来指定。

由于Spring容器负责创建和管理应用程序对象,企业服务(如事务管理、安全性、远程访问等)可以通过Spring容器透明地应用到对象上。Spring容器的这种增强应用程序对象附加功能的能力让我们可以使用简单的Java对象(也称为Plain Old Java Objects,POJO)作为应用对象。对应于POJO的Java类称作POJO类,也就是不实现或继承框架特定的接口或类的Java类。需要这些POJO的企业服务,如事务管理、安全、远程访问等,由Spring容器透明地提供。

现在你知道Spring容器是如何工作的了,下面再通过几个例子来看看使用Spring开发应用的好处。

在前面的章节中,我们介绍了Spring带来的以下好处。

1)Spring负责应用程序对象的创建并注入它们的依赖项,简化了Java应用程序的组成。

2)Spring推动了以POJO的形式来开发应用程序。

Spring提供了一个负责样板代码的抽象层,以此简化与以下模块的交互,如JMS提供者、JNDI、MBean服务器、邮件服务器和数据库等。

让我们快速地通过几个例子来更好地理解使用Spring开发应用程序有哪些好处。

如果你正在使用Spring开发一个需要事务的应用程序,那么可以使用Spring的声明式事务管理来管理事务。

MyBank应用程序中的FixedDepositService类,如程序示例1-4所示。

程序示例1-4 FixedDepositService 类

public class FixedDepositService {
  public FixedDepositDetails getFixedDepositDetails( ..... ) { ..... }
  public boolean createFixedDeposit(FixedDepositDetails fixedDepositDetails) { ..... }
}

FixedDepositService类是用来定义定期存款业务中创建和取回明细方法的POJO类,图1-3展示了创建一笔新的定期存款的表单。

一位客户在上面的表单中输入了定期存款金额、存期和电子邮箱ID信息,并单击SAVE按钮来创建一笔新的定期存款,此时会调用在FixedDepositService中的createFixedDeposit方法(见程序示例1-4)来创建存款,createFixedDeposit 方法从该客户银行账户中扣除他输入的金额并创建一笔等额的定期存款。

图1-3 创建定期存款的HTML表单

假定关于客户银行余额的信息存在数据表BANK_ACCOUNT_DETAILS中,定期存款的明细存在数据表FIXED_DEPOSIT_DETAILS中。如果客户创建了一笔金额为x的定期存款,应该在BANK_ACCOUNT_DETAILS表中减去x并在FIXED_DEPOSIT_DETAILS表中插入一条记录来反映这笔新加的定期存款。如果BANK_ACCOUNT_DETAILS表没有更新或者新的记录没有插入FIXED_DEPOSIT_DETAILS表,这会让系统处于不一致状态。这意味着createFixedDeposit方法必须在一个事务中执行。

由Mybank应用程序所使用的数据库是一个事务性资源。以传统的方式在一个工作单元中执行一组数据库的修改操作时,先要禁用JDBC连接的自动提交模式,然后执行SQL语句,最后提交(或回滚)事务。用传统的方式在createFixedDeposit方法中管理数据库事务的方法如程序示例1-5所示。

程序示例1-5 以编程方式使用JDBC连接对象管理数据库事务

import java.sql.Connection;
import java.sql.SQLException;

public class FixedDepositService {
   public FixedDepositDetails getFixedDepositDetails( ..... ) { ..... }

   public boolean createFixedDeposit(FixedDepositDetails fixedDepositDetails) {  
           Connection con = ..... ;
           try {
               con.setAutoCommit(false);
               //-- 执行修改数据库表的SQL语句
               con.commit();
           } catch(SQLException sqle) {
               if(con != null) {
                   con.rollback();
               }
           }
           .....
      }
}

程序示例1-5展示了如何在createFixedDeposit方法中以编程方式使用JDBC连接对象管理数据库事务。这种方式适合只涉及单个数据库的应用场景。具体资源相关的事务,如与JDBC连接相关的事务,称为本地事务

当多个事务性资源都有涉及,使用JTA(Java事务API)来管理事务时,例如要在同一个事务中将JMS消息发送到消息中间件(一种事务资源)并更新数据库(另一种事务资源),则必须使用一个JTA事务管理器管理事务。JTA事务也称为全局(或分布式)事务。要使用JTA,需要先从JNDI中获取 UserTransaction对象(这是JTA API的一部分),并编程开始和提交(或回滚)事务。

如你所见,可以使用JDBC连接(本地事务)或userTransaction(对于全局事务)对象以编程方式管理事务。但是请注意,本地事务无法在全局事务中运行。这意味着如果要在createFixedDeposit数据库更新方法(见程序示例1-5)使之成为JTA事务的一部分,则需要修改createFixedDeposit方法,用UserTransaction对象进行事务管理。

Spring通过提供一个抽象层来简化事务管理,从而提供管理本地和全局事务的一致方法。这意味着如果用Spring的事务抽象写createfixeddeposit方法(见程序示例1-5),那么从本地切换到全局事务管理时不需要修改方法,反之亦然。Spring的事务抽象将在第8章详细说明。

Spring提供了使用声明式事务管理的选项,你可以在一个方法上使用Spring的@Transactional注解并让Spring来处理事务,如程序示例1-6所示。

程序示例1-6 使用@Transactional 注解

import org.springframework.transaction.annotation.Transactional;

public class FixedDepositService {
  public FixedDepositDetails getFixedDepositDetails( ..... ) { ..... }

 @Transactional
  public boolean createFixedDeposit(FixedDepositDetails fixedDepositDetails) { ..... }
}

程序示例1-6表明,FixedDepositService类没有实现任何接口或继承任何Spring特定的类以得到Spring的事务管理能力。Spring框架透明地通过@Transactional注解为createFixedDeposit方法提供事务管理功能。这说明Spring是一个非侵入性的框架,因为它不需要应用对象依赖于Spring特定的类或接口。由于事务管理是由Spring接管的,因此不需要直接使用事务管理API来管理事务。

对于任何Java应用程序来说,安全都是一个重要的方面。Spring Security是一个SpringSourc置于Spring 框架顶层的项目,它提供了身份验证和授权功能,可以用来保护Java应用程序。下面以3个在Mybank应用程序中认证过的用户角色为例进行说明,即LOAN_CUSTOMER、SAVINGS_ACCOUNT_CUSTOMER 和APPLICATION_ADMIN。调用FixedDepositService类(见示例1-6)中createFixedDeposit方法的客户必须是相关的SAVINGS_ACCOUNT_CUSTOMER或者拥有APPLICATION_ADMIN角色。而使用Spring Security时,你可以通过在createFixedDeposit方法上添加Spring Security的 @Secured注解来轻松地解决这个问题, 如程序示例1-7所示。

程序示例 1-7 使用@Secured注解的 createFixedDeposit 方法

import org.springframework.transaction.annotation.Transactional;
import org.springframework.security.access.annotation.Secured;

public class FixedDepositService {
         public FixedDepositDetails getFixedDepositDetails( ..... ) { ..... }

         @Transactional
         @Secured({ "SAVINGS_ACCOUNT_CUSTOMER", "APPLICATION_ADMIN" })
         public boolean createFixedDeposit(FixedDepositDetails fixedDepositDetails) { ..... }
}

如果用@Secured 给一个方法加注解,安全特性将被Spring Security框架透明地应用到该方法上。程序示例1-7表明,为了实现方法级别的安全,你无须继承或实现任何Spring特定类或接口,而且不需要在业务方法中写安全相关的代码。

我们将在第16章详细讨论Spring Security框架。

Spring对JMX的支持可以让你非常简单地将JMX技术融合到应用程序中。

假设Mybank应用程序的定期存款功能应该只在每天早上9点到下午6点的时间段提供给客户。为了满足这个要求,需要在FixedDepositService中增加一个变量,以此作为一个标志表明定期存款服务是否活跃。程序示例1-8显示了使用活跃变量的FixedDepositService类。

程序示例1-8 使用活跃变量的FixedDepositService类

public class FixedDepositService {
   private boolean active;

  public FixedDepositDetails getFixedDepositDetails( ..... ) {
         if(active) { ..... }
  }
  public boolean createFixedDeposit(FixedDepositDetails fixedDepositDetails) { 
         if(active) { ..... }
  }
  public void activateService() {
         active = true;
  }
  public void deactivateService() {
         active = false;
  }
}

程序示例1-8表明,FixedDepositService类中加了一个名为active的变量。如果active变量的值为true, getFixedDepositDetails和createFixedDeposit方法将按照预期工作。如果active变量的值为false, getFixedDepositDetails和createFixedDeposit方法将抛出一个异常,表明定期存款服务当前不可用。activateService和deactivateService方法分别将active变量的值置为true和false。

那么,谁调用activateService和deactivateService方法呢?假设有一个名为Bank App Scheduler的调度应用程序,分别在上午9:00和下午6:00执行activateservice和deactivateservice方法。Bank App Scheduler应用使用JMX (Java管理扩展) API与FixedDepositService实例远程交互。

Bank App Scheduler使用JMX改变FixedDepositService中active变量的值,你需要将FixedDepositService实例在一个可被管理的bean(或者称为MBean)服务器上注册为一个MBean,并将FixedDepositService中的 activateService和deactivateService方法暴露为JMX操作方法。在Spring中,你可以通过在一个类上添加Spring的@ManagedResource 注释来将一个类的实例注册到MBean服务器上,并且可以使用Spring的@ManagedOperation注释将该类的方法暴露为JMX操作方法。

在程序示例1-9中展示了使用@ManagedResource和@ManagedOperation注释将FixedDepositService类的实例注册到MBean服务器,并将activateService和deactivateService方法暴露为JMX操作方法。

程序示例1-9 使用Spring JMX支持的FixedDepositService类

import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.export.annotation.ManagedResource;

@ManagedResource(objectName = "fixed_deposit_service:name=FixedDepositService")
public class FixedDepositService {
  private boolean active;

  public FixedDepositDetails getFixedDepositDetails( ..... ) {
    if(active) { ..... }
  }
  public boolean createFixedDeposit(FixedDepositDetails fixedDepositDetails) { 
    if(active) { ..... }
  }

 @ManagedOperation
  public void activateService() {
    active = true;
  }

 @ManagedOperation
  public void deactivateService() {
    active = false;
  }
}

程序示例1-9表明FixedDepositService类将它的实例注册到MBean服务器并暴露它的方法为JMX操作方法时并没有直接使用JMX API。

Spring的JMS支持简化了从JMS提供者发送和接收消息。

在MyBank应用程序中,当客户通过电子邮件提交一个接收其定期存款明细的请求时, FixedDepositService将请求的明细发送到JMS消息中间件(比如ActiveMQ),而请求随后由消息侦听器处理。Spring通过提供一个抽象层来简化与JMS提供者的交互。程序示例1-10展示了FixedDepositService类如何通过Spring的JmsTemplate将请求的明细发送到JMS提供者。

程序示例1-10 发送JMS消息的FixedDepositService类

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.core.JmsTemplate;

public class FixedDepositService {
  @Autowired
 private transient JmsTemplate jmsTemplate;
  .....
  public boolean submitRequest(Request request) {
      jmsTemplate.convertAndSend(request);
  }
}

在程序示例1-10中,FixedDepositService定义了一个JmsTemplate类型的变量,这个变量使用了Spring的@Autowired 注释。现在,你可以认为@Autowired注释提供了一个JmsTemplate实例。这个JmsTemplate实例知道JMS消息发送的目的地。如何配置这个JmsTemplate的细节会在第10章介绍。FixedDepositService类的submitRequest方法调用了JmsTemplate的convertAndSend方法,把请求的明细(由submitRequest方法的Request参数表示)作为一个JMS消息发送到JMS提供者。

这也再一次表明,如果使用Spring框架向JMS提供者发送消息,并不需要直接处理JMS API。

Spring的缓存抽象提供了在应用程序中使用缓存的一致方法。

使用缓存解决方案来提高应用程序的性能是很常见的。MyBank 应用使用一个缓存产品以提高读取定期存款明细操作的性能。Spring框架通过抽象缓存相关的逻辑来简化与不同缓存解决方案的交互。

程序示例1-11展示了FixedDepositService类的getFixedDepositDetails方法使用Spring的缓存抽象功能来缓存定期存款明细。

程序示例1-11 将定期存款明细缓存的FixedDepositService类

import org.springframework.cache.annotation.Cacheable;

public class FixedDepositService {

  @Cacheable("fixedDeposits")
  public FixedDepositDetails getFixedDepositDetails( ..... ) { ..... }

  public boolean createFixedDeposit(FixedDepositDetails fixedDepositDetails) { ..... }
}

在程序示例1-11中,Spring的@Cacheable注解表明由getFixedDepositDetails方法返回的定期存款明细 将被缓存起来,如果使用同样的参数来调用getFixedDepositDetails方法,getFixedDepositDetails 方法并不会实际运行,而是直接返回缓存中的定期存款明细。这表明,如果使用Spring框架,则不需要在类中编写与缓存相关的逻辑。Spring的缓存抽象在第10章中详细介绍。

在这一部分中,我们看到Spring框架通过透明地向POJO提供服务的方式简化了企业应用程序的开发,从而将开发者从底层API的细节中解放出来。Spring还提供了与各种标准框架,如Hibernate、 Quartz、 JSF、Struts和 EJB等的简单集成,使得Spring成为企业应用程序开发的理想选择。

现在,我们已经看到了一些使用Spring框架的好处,下面来看如何开发一个简单的Spring应用程序。

在这一部分,我们来关注一个使用Spring的DI功能的简单的Spring应用程序。在一个应用程序中使用Spring的DI功能,需要遵循以下步骤:

1)确定应用程序对象及其依赖关系;

2)根据步骤1中确定的应用程序对象创建POJO类;

3)创建描述应用程序对象及其依赖项的配置元数据;

4)创建一个Spring IoC容器的实例并将配置元数据传递给它;

5)从Spring IoC容器实例中访问应用程序对象。

现在让我们来看看上述步骤在Mybank应用程序中是如何体现的。

前面讨论过,Mybank应用程序展示了创建一笔定期存款的表单(见图1-3)。图1-4的时序图显示了当用户提交表单时出现的应用程序对象(以及它们之间的交互):

图1-4 MyBank的应用程序对象及其依赖项

在图1-4所示的时序图中, FixedDepositController代表当这个表单提交时接受请求的WebController,而FixedDepositDetails对象包含定期存款明细,FixedDepositController调用FixedDepositService (服务层对象)的 createFixedDeposit方法。然后,FixedDepositService调用FixedDepositDao 对象(数据访问对象)来把定期存款明细保存到应用程序的数据存储区。因此,我们可以从图中理解FixedDepositService 是FixedDepositController对象的依赖项,而FixedDepositDao是FixedDepositService对象的依赖项。

 含义 

chapter 1/ch01-bankapp-xml (这个项目是一个使用Spring DI功能的简单的Spring应用程序。执行项目中BankApp类中的main方法即可运行应用程序)。

一旦已经确定了应用程序对象,下一步就是根据这些应用程序对象创建POJO类。ch01-bankapp-xml项目中已经包含了对应于FixedDepositController、FixedDepositService和FixedDepositDao这些应用程序对象的POJO类。ch01-bankapp-xml项目是一个使用Spring DI功能的简化版 Mybank应用程序。你可以将ch01-bankapp-xml项目导入IDE中,接下来我们来看这个项目中包含的文件。

在1.3节中,我们讨论了把依赖项作为构造函数参数或作为setter方法参数传递给应用程序对象。程序示例1-12展示了一个FixedDepositService的实例(FixedDepositController的依赖项)是如何作为一个setter 方法的参数传递给FixedDepositController类的。

程序示例1-12 FixedDepositController 类

Project – ch01-bankapp-xml
Source location - src/main/java/sample/spring/chapter01/bankapp

package sample.spring.chapter01.bankapp;
.....
public class FixedDepositController {
  .....
   private FixedDepositService fixedDepositService;
  .....
   public void setFixedDepositService(FixedDepositService fixedDepositService) {
    logger.info("Setting fixedDepositService property");
    this.fixedDepositService = fixedDepositService;
  }
  .....   
  public void submit() {
    fixedDepositService.createFixedDeposit(new FixedDepositDetails( 1, 10000, 
      365, "someemail@something.com"));
  }
  .....
}

在程序示例1-12中,FixedDepositService这个依赖项是通过setFixedDepositService方法被传递给FixedDepositController的。我们马上就能看到setFixedDepositService的setter方法被Spring调用。

注 意

如果观察FixedDepositController、FixedDepositService和FixedDepositDao类,你会发现这几个类都没有实现任何Spring特定的接口或继承任何Spring指定的类。

现在让我们看看如何在配置元数据中指定应用程序对象及其依赖关系。

我们在1.3节中了解到,Spring容器读取指定了应用程序对象及其依赖项的配置元数据,将应用程序对象实例化并注入它们的依赖项。在本节中,我们将首先介绍配置元数据中包含的其他信息,然后深入研究如何用XML方式指定配置元数据。

配置元数据指定应用程序所需的企业服务(如事务管理、安全性和远程访问)的信息。例如,如果想让Spring来管理事务,你需要在配置元数据中配置对Spring的PlatformTransactionManager接口的一个实现。PlatformTransactionManager实现负责管理事务(更多关于Spring的事务管理功能详见第8章)。

如果应用程序和消息中间件(如ActiveMQ)、数据库(如MySQL)、电子邮件服务器等进行交互,那么这些简化了与外部系统交互的Spring的特定对象也是在配置元数据中定义的。例如,如果应用程序需要向ActiveMQ 发送或接收JMS消息,你可以在配置元数据中配置Spring的JmsTemplate 类来简化和ActiveMQ的交互。在程序示例1-10中可以看到,如果使用JmsTemplate向JMS提供者发送消息,你不需要处理低级别的JMS API(更多关于Spring对与JMS提供者交互的支持详见第10章)。

你可以通过XML文件或者通过POJO类中的注解将配置元数据提供给Spring容器。从Spring 3.0版本开始,你也可以通过在Java类上添加Spring的@Configuration注解来将配置元数据提供给Spring容器。在本节中,我们将介绍如何通过XML方式指定配置元数据。在第6章和第7章中,我们将分别介绍如何通过POJO类中的注解和通过对Java类的@Configuration注解来配置元数据。

通过创建一个包含应用程序对象及其依赖项信息的应用程序上下文XML文件,可以按照XML格式将配置元数据提供给应用程序。程序示例1-13呈现了一个应用程序上下文XML文件的大体样式。下面的XML展示了MyBank应用程序的应用程序上下文XML文件由FixedDepositController、 FixedDepositService以及FixedDepositDao(见图1-4以了解这些对象如何相互作用)等对象组成。

程序示例1-13 applicationContext.xml——MyBank的应用程序上下文XML文件

Project – ch01-bankapp-xml
Source location - src/main/resources/META-INF/spring

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<beans xmlns = "http://www.springframework.org/schema/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="controller" 
            class="sample.spring.chapter01.bankapp.FixedDepositController">
       <property name="fixedDepositService" ref="service" />
    </bean>

   <bean id="service" class="sample.spring.chapter01.bankapp.FixedDepositService">
     <property name="fixedDepositDao" ref="dao" />
   </bean>

   <bean id="dao" class="sample.spring.chapter01.bankapp.FixedDepositDao"/>
</beans>

以下是关于应用程序上下文XML文件的要点。

1)在spring-beans.xsd schema(也被称为Spring的bean schema)中定义的<beans>元素是应用程序上下文的XML文件的根元素。spring-beans.xsd schema在Spring 框架发布的spring-beans-4.3.0.RELEASE.jar JAR包中。

2)每个<bean>元素配置一个由Spring容器管理的应用程序对象。在Spring框架的术语中,一个<bean>元素代表一个bean定义。Spring容器创建的基于bean定义的对象称为一个bean。id特性指定bean的唯一名称,class特性指定bean的完全限定类名。还可以使用<bean>元素的name特性来指定bean的别名。在MyBank应用程序中,FixedDepositController、FixedDepositService和 FixedDepositDao为应用程序对象,因此我们有三个<bean>元素——每个应用程序对象对应一个<bean>元素。由于Spring容器管理着由<bean>元素配置的应用程序对象,Spring容器也就需要承担创建并注入它们的依赖关系的责任。不需要直接创建由<bean>元素定义的应用程序对象实例,而是应该从Spring容器中获取它们。在本节后面的部分,我们将介绍如何获取由Spring容器管理的应用程序对象。

3)没有和MyBank应用程序中的FixedDepositDetails域对象相对应的<bean>元素。这是因为域对象通常不是由Spring容器管理的,它们由应用程序所使用的ORM框架(如Hibernate)创建,或者通过使用new运算符以编程方式创建它们。

4)<property>元素指定由<bean>元素配置的bean的依赖项(或者配置属性)。<property> 元素对应于bean类中的JavaBean风格的setter 方法,该方法由Spring容器调用以设置bean的依赖关系(或配置属性)。

现在让我们来介绍一下如何通过setter 方法注入依赖项。

为了理解如何通过在bean类中定义的setter 方法注入依赖,我们再来观察一下MyBank应用程序中的FixedDepositController类。

程序示例1-14 FixedDepositController 类

Project – ch01-bankapp-xml
Source location - src/main/java/sample/spring/chapter01/bankapp

package sample.spring.chapter01.bankapp;

import org.apache.log4j.Logger;

public class FixedDepositController {
  private static Logger logger = Logger.getLogger(FixedDepositController.class);

  private FixedDepositService fixedDepositService;

  public FixedDepositController() {
    logger.info("initializing");
  }

  public void setFixedDepositService(FixedDepositService fixedDepositService) {
    logger.info("Setting fixedDepositService property");
    this.fixedDepositService = fixedDepositService;
  }
  .....
}

程序示例1-14 表明FixedDepositController类中声明了一个类型为FixedDepositService、名称为fixedDepositService的实例变量。这个fixedDepositService变量由setFixedDepositService 方法设定——一种针对fixedDepositService变量的JavaBean风格的setter 方法。这是一个基于setter 方法的DI示例,其中的setter 方法满足依赖项。

图1-5描述了在applicationContext.xml 文件中对FixedDepositController类的bean定义(见程序示例1-13)。

图1-5 使用<property> 元素定义依赖项

前文的bean定义表明,FixedDepositController bean通过<property>元素定义了它对于FixedDepositService bean的依赖。Spring容器在bean创建时会调用bean类中JavaBean风格的setter方法,该方法与<property>元素的name特性对应。<property>元素的引用特性标识可以分辨需要创建具体哪个Spring bean的实例。引用特性的值必须与配置元数据中的<bean>元素的id特性值(或由name特性指定的名称之一)匹配。

在图1-5中,<property>元素的name特性的值为fixedDepositService,这意味着<property>元素对应于FixedDepositController类(见程序示例1-14)中setFixedDepositService的setter方法。由于<property>元素的ref特性的值是service,因此<property>元素是指id特性的值为service的<bean>元素。现在,id特性的值为service的<bean>元素是FixedDepositService bean(见程序示例1-13)。Spring容器创建了一个FixedDepositService类(一个依赖项)的实例,并将FixedDepositService实例作为调用FixedDepositController(一个依赖对象)的setFixedDepositService方法(一个用于fixedDepositService变量的JavaBean风格的setter方法)的参数。

在FixedDepositController应用程序对象的上下文中,图1-6总结了<property>元素的name和ref特性的用途。

图1-6 <property>元素的name特性对应于一个满足bean依赖关系的JavaBean风格的setter方法,ref特性指的是另一个bean

图1-6显示了name特性的fixedDepositService值对应于FixedDepositController类的setFixedDepositService方法,ref特性的service值对应于id值为service的bean。

图1-7总结了Spring容器如何根据MyBank应用程序的applicationContext.xml文件(见程序示例1-13)提供的配置元数据创建bean并注入它们的依赖项。该图显示了Spring IoC容器创建FixedDepositController、FixedDepositService和FixedDepositDao bean并注入其依赖项的步骤顺序。在尝试创建bean之前,Spring容器读取并验证由applicationContext.xml文件提供的配置元数据。由Spring容器创建bean的顺序取决于它们在applicationContext.xml文件中的定义顺序。Spring容器确保在调用setter方法之前完全配置了一个bean的依赖关系。例如,FixedDepositController bean依赖于FixedDepositService bean,因此,Spring容器会在调用FixedDepositController bean的setFixedDepositService方法之前配置好FixedDepositService bean。

图1-7 Spring IoC容器创建bean并注入其依赖关系的顺序

  注意

通过一个bean的名称(id特性的值)或类型(class特性的值)或由bean类实现的接口来引用bean定义是相当普遍的。例如,你可以将“FixedDepositController bean”称为“控制器bean”。而且,如果FixedDepositController类实现了FixedDepositControllerIntf接口,那么可以将“FixedDepositController bean”称为“FixedDepositControllerIntf bean”。

到目前为止,我们已经看到的这些bean定义指示Spring容器调用bean类的无参数构造函数来创建bean实例,并使用基于setter的DI来注入依赖关系。在第2章中,我们将介绍指示Spring容器通过类中定义的工厂方法创建bean实例的bean定义。另外,我们将介绍如何通过构造函数参数注入依赖关系(也称为基于构造函数的DI)。

现在来看看如何创建Spring容器的实例,并将配置元数据传递给它。

Spring的ApplicationContext对象表示Spring容器的一个实例。Spring提供了一些ApplicationContext接口的内置实现,如ClassPathXmlApplicationContext、FileSystemXmlApplicationContext、XmlWebApplicationContext、XmlPortletApplicationContext等。ApplicationContext实现的选择取决于如何定义配置元数据(使用XML、注释或Java代码)以及应用程序类型(独立、Web或Portlet应用程序)。例如,ClassPathXmlApplicationContext和FileSystemXmlApplicationContext类适用于以XML格式提供配置元数据的独立应用程序,XmlWebApplicationContext适用于以XML格式提供配置元数据的Web应用程序,AnnotationConfigWebApplicationContext适用于通过Java代码以编程方式提供配置元数据的Web应用程序,等等。

由于MyBank应用程序是一个独立的应用程序,因此可以使用ClassPathXmlApplicationContext或FileSystemXmlApplicationContext类来创建一个Spring容器的实例。应该注意到,ClassPathXmlApplicationContext类从指定的类路径位置加载应用程序上下文XML文件,FileSystemXmlApplicationContext类从文件系统上的指定位置加载应用程序上下文XML文件。

MyBank应用程序中的BankApp类展示了使用ClassPathXmlApplicationContext类创建一个Spring容器的实例(见程序示例1-15)。

程序示例1-15 BankApp 类

Project – ch01-bankapp-xml
Source location - src/main/java/sample/spring/chapter01/bankapp

package sample.spring.chapter01.bankapp;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class BankApp {
  .....
  public static void main(String args[]) {
     ApplicationContext context = new ClassPathXmlApplicationContext(
 "classpath:META-INF/spring/applicationContext.xml");
     .....
  }
}

程序示例1-15展示了BankApp中负责引导Spring容器的main方法,其中应用程序上下文XML文件的类路径位置传递给了ClassPathXmlApplicationContext类中的构造函数。创建ClassPathXmlApplicationContext实例的结果是在应用程序上下文XML文件中创建的那些bean都是单个范围并被设置为预实例化的。在第2章中,我们将讨论bean的范围,以及使用Spring容器预实例化或者延迟实例化bean的含义。现在,你可以假设在MyBank应用程序的applicationContext.xml文件中定义的bean是singleton范围的,并设置为预实例化。这意味着在创建ClassPathXmlApplicationContext的实例时,在applicationContext.xml文件中定义的bean也会被创建。

现在我们已经看到如何创建一个Spring容器的实例,下面来看如何从Spring容器中检索bean实例。

通过<bean>元素定义的应用程序对象由Spring容器创建和管理。可以通过调用ApplicationContext接口的getBean方法来访问这些应用程序对象的实例。

程序示例1-16展示了BankApp类的main方法,它从Spring容器中检索FixedDepositController bean实例并调用其方法。

程序示例1-16 BankApp 类

Project – ch01-bankapp-xml
Source location - src/main/java/sample/spring/chapter01/bankapp

package sample.spring.chapter01.bankapp;

import org.apache.log4j.Logger;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class BankApp {
  private static Logger logger = Logger.getLogger(BankApp.class);

  public static void main(String args[]) {
     ApplicationContext context = new ClassPathXmlApplicationContext(
      "classpath:META-INF/spring/applicationContext.xml");

       FixedDepositController fixedDepositController = 
 (FixedDepositController) context.getBean("controller");
 logger.info("Submission status of fixed deposit : " + fixedDepositController.submit());
 logger.info("Returned fixed deposit info : " + fixedDepositController.get());
  }
}

首先调用ApplicationContext的getBean方法,从Spring容器中检索FixedDepositController bean的一个实例,然后调用FixedDepositController bean的submit和get方法。要从Spring容器检索其实例的bean的名称是传递给getBean方法的参数。传递给getBean方法的bean的名称必须是要检索的bean的id或name特性的值。如果没有指定名称的bean注册到Spring容器中,getBean方法将抛出异常。

在程序示例1-16中,要配置FixedDepositController实例,我们没有以编程方式创建FixedDepositService的实例并将其设置在FixedDepositController实例上,也没有创建一个FixedDepositDao的实例并将其设置在FixedDepositService实例上。这是因为创建依赖项并将它们注入依赖对象中的任务是由Spring容器处理的。

如果进入ch01-bankapp-xml项目并执行BankApp类的main方法,将在控制台上看到以下输出内容。

INFO  sample.spring.chapter01.bankapp.FixedDepositController - initializing
INFO  sample.spring.chapter01.bankapp.FixedDepositService - initializing
INFO  sample.spring.chapter01.bankapp.FixedDepositDao - initializing
INFO  sample.spring.chapter01.bankapp.FixedDepositService - Setting fixedDepositDao property
INFO sample.spring.chapter01.bankapp.FixedDepositController - Setting fixedDepositService property
INFO  sample.spring.chapter01.bankapp.BankApp - Submission status of fixed deposit : true
INFO  sample.spring.chapter01.bankapp.BankApp - Returned fixed deposit info : id :1, deposit amount : 
10000.0, tenure : 365, email : someemail@something.com

由上面的输出可知,Spring容器将所有在MyBank应用程序的applicationContext.xml文件中定义的bean都创建了一个实例。此外,Spring容器使用基于setter的DI将FixedDepositService的实例注入FixedDepositController实例中,并将FixedDepositDao的实例注入FixedDepositService实例中。

下面来看在Spring框架之上构建的一些框架。

虽然SpringSource有许多以Spring框架作为基础的框架,但我们将介绍一些广泛流行的框架。有关更全面的框架列表以及有关单个框架的更多详细信息,建议读者自行访问SpringSource网站查找。

构建在Spring框架之上的SpringSource框架的高级概述见表1-2。

表1-2  构建在Spring框架之上的SpringSource框架的高级概述

框架

描述

Spring Security

企业应用认证和授权框架。你需要在应用程序上下文XML文件中配置几个bean,以将身份验证和授权功能合并到应用程序中

Spring Data

提供一致的编程模型来与不同类型的数据库进行交互。例如,你可以使用它与非关系数据库(如MongoDB或Neo4j)进行交互,还可以通过它来使用JPA访问关系数据库

Spring Batch

如果应用程序需要批量处理,则使用此框架

Spring Integration

为应用程序提供企业应用程序集成(EAI)功能

Spring Social

如果应用程序需要与社交媒体网站(如Facebook和Twitter)进行交互,那么你将发现此框架非常有用

由于表1-1中提到的框架构建在Spring框架之上,所以在使用任何这些框架之前,请确保它们与你正在使用的Spring框架版本兼容。

在本章中,我们介绍了使用Spring框架的好处。我们还研究了一个简单的Spring应用程序,它展示了如何在xml格式中指定配置元数据,如何创建Spring容器实例并从中获取bean。在下一章中,我们将讨论Spring框架的一些基础概念。


在上一章中,我们看到Spring容器调用bean类的无参数构造函数来创建一个bean实例,而基于setter的DI用于设置bean依赖关系。在本章中,我们将进一步介绍以下内容:

Spring如何通过支持“面向接口编程”的设计方法来提高应用程序的可测试性是本章首先要介绍的内容。

在1.5节中,我们看到一个依赖于其他类的POJO类包含了对其依赖项的具体类的引用。例如,Fixed DepositController类包含对FixedDepositService类的引用,FixedDepositService类包含对FixedDepositDao类的引用。如果这个依赖于其他类的类直接引用其依赖项的具体类,则会导致类之间的紧密耦合。这意味着如果要替换其依赖项的其他实现,则需要更改这个依赖于其他类的类本身。

我们知道Java接口定义了其实现类应遵循的契约。因此,如果一个类依赖于其依赖项实现的接口,那么当替换不同的依赖项实现时,类不需要改变。一个类依赖于由其依赖项所实现的接口的应用程序设计方法称为“面向接口编程”。这种设计方法使得依赖类与其依赖项之间松耦合。由依赖项类实现的接口称为依赖接口。

和“面向类编程”相比,“面向接口编程”是更加良好的设计实践,图2-1中的类图表明ABean类依赖于BBean接口而不是BBeanImpl类(BBean接口的实现)。

图2-1 和“面向类编程”相比,“面向接口编程”是更加良好的设计实践

图2-2中的类图显示了FixedDepositService类如何使用“面向接口编程”的设计方法轻松地切换与数据库交互的策略。

在图2-2中,FixedDepositJdbcDao单纯地使用JDBC,而FixedDepositHibernateDao使用Hibernate ORM进行数据库交互。如果FixedDepositService直接依赖于FixedDepositJdbcDao或FixedDepositHibernateDao,当需要切换与数据库交互的策略时,则需要在FixedDepositService类中进行必要的更改。FixedDepositService依赖于FixedDepositJdbcDao和FixedDepositHibernateDao类实现FixedDepositDao接口(依赖接口)。现在,通过使用单纯的JDBC或Hibernate ORM框架,你可以向FixedDepositService实例提供FixedDepositJdbcDao或FixedDepositHibernateDao的实例。

图2-2 FixedDepositService依赖于FixedDepositDao接口,由FixedDepositJdbcDao和FixedDepositHibernateDao类实现

由于FixedDepositService依赖于FixedDepositDao接口,因此将来可以支持其他数据库交互策略。如果决定使用iBATIS(现在更名为MyBatis)持久性框架进行数据库交互,那么可以使用iBATIS,而不需要对FixedDepositService类进行任何更改,只需创建一个实现FixedDepositDao接口的FixedDepositIbatisDao类,并将FixedDepositIbatisDao的实例提供给FixedDepositService实例。

现在来看看“面向接口编程”是如何提高依赖类的可测试性的。

在图2-2中,FixedDepositService类保留了对FixedDepositDao接口的引用。FixedDepositJdbcDao和FixedDepositHibernateDao是FixedDepositDao接口的具体实现类。现在,为了简化FixedDepositService类的单元测试,我们可以把原来对具体数据库操作的实现去掉,用一个实现了FixedDepositDao接口但是不需要数据库的代码来代替。

如果FixedDepositService类直接引用FixedDepositJdbcDao或FixedDepositHibernateDao类,那么测试FixedDepositService类则需要设置数据库以进行测试。这表明通过对依赖接口的模拟依赖类实现,你可以减少针对单元测试的基础设施设置的工作量。

现在来看看Spring如何在应用程序中支持“面向接口编程”的设计方法。

要在Spring应用程序中使用“面向接口编程”的设计方法,你需要执行以下操作:

现在来看看根据“面向接口编程”设计方法修改后的MyBank应用程序。

 含义 

chapter 2/ch02-bankapp-interfaces (本项目展示了如何在创建Spring应用程序中应用“面向接口编程”设计方法。要运行应用程序,请执行本项目的BankApp类的main方法)。

使用“面向接口编程”设计方法的MyBank应用程序

图2-3所示的类图描述了根据“面向接口编程”设计方法修改后的MyBank应用程序。

图2-3 使用“面向接口编程”设计方法的MyBank应用程序

图2-3显示了一个类依赖于依赖项实现的接口,而不依赖于具体的依赖项实现类。例如,FixedDepositControllerImpl类依赖于FixedDepositService接口,FixedDepositServiceImpl类依赖于Fixed DepositDao接口。

程序示例2-1展示了基于图2-3设计的FixedDepositServiceImpl类。

程序示例2-1 FixedDepositService 类

Project – ch02-bankapp-interfaces
Source location - src/main/java/sample/spring/chapter02/bankapp

package sample.spring.chapter02.bankapp;

public class FixedDepositServiceImpl implements FixedDepositService {
 private FixedDepositDao fixedDepositDao;
  .....
  public void setFixedDepositDao(FixedDepositDao fixedDepositDao) {    
    this.fixedDepositDao = fixedDepositDao;
  }

  public FixedDepositDetails getFixedDepositDetails(long id) {
    return fixedDepositDao.getFixedDepositDetails(id);
  }

  public boolean createFixedDeposit(FixedDepositDetails fdd) {
    return fixedDepositDao.createFixedDeposit(fdd);
  }
}

在程序示例2-1中,FixedDepositServiceImpl类包含对FixedDepositDao接口的引用。要注入到FixedDepositServiceImpl实例中的FixedDepositDao具体实现,则在应用程序上下文XML文件中指定。如图2-3所示,可以注入以下FixedDepositDao接口的具体实现:FixedDepositIbatisDao、FixedDepositJdbcDao和FixedDepositHibernateDao。

程序示例2-2展示了将FixedDepositHibernateDao注入到FixedDepositServiceImpl中的applicationContext.xml文件。

程序示例2-2 applicationContext.xml MyBank的应用程序上下文XML文件

Project – ch02-bankapp-interfaces
Source location - src/main/resources/META-INF/spring

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<beans .....>

  <bean id="controller" 
      class="sample.spring.chapter02.bankapp.controller.FixedDepositControllerImpl">
     <property name="fixedDepositService" ref="service" />
  </bean>

  <bean id="service" class="sample.spring.chapter02.bankapp.service.FixedDepositServiceImpl">
       <property name="fixedDepositDao" ref="dao" />
  </bean>

  <bean id="dao" class="sample.spring.chapter02.bankapp.dao.FixedDepositHibernateDao"/>
</beans>

上述的applicationContext.xml文件显示了FixedDepositHibernateDao(一个FixedDepositDao接口的实现)的一个实例被注入FixedDepositServiceImpl中。现在,如果决定使用iBATIS代替Hibernate进行持久化,那么所需要做的就是将dao bean定义的class特性修改为FixedDepositIbatisDao类的完全限定名。

到目前为止,我们已经介绍了bean定义的示例,Spring容器通过调用bean类的无参构造函数来创建bean实例。下面来了解Spring容器如何使用静态或实例工厂方法来创建bean实例。

Spring容器可以创建和管理任何类的实例,而不管类是否提供无参数构造函数。在2.4节中,我们将介绍在构造函数中可以接受一个或多个参数的bean类的定义。如果现有的Java应用程序用工厂类来创建对象实例,那么仍然可以使用Spring容器来管理由这些工厂创建的对象。

现在来介绍一下Spring容器如何调用类的静态或实例工厂方法来管理返回的对象实例。

图2-3展示了如何使用FixedDepositHibernateDao、FixedDepositIbatisDao和FixedDepositJdbcDao类实现FixedDepositDao接口。程序示例2-3中的FixedDepositDaoFactory类定义了一个静态工厂的方法,该静态方法根据传入的参数来创建和返回FixedDepositDao实例。

程序示例2-3 FixedDepositDaoFactory类

public class FixedDepositDaoFactory {
  private FixedDepositDaoFactory() { }

   public static FixedDepositDao getFixedDepositDao(String daoType, ...) {
    FixedDepositDao fixedDepositDao = null;

    if("jdbc".equalsIgnoreCase(daoType)) {
        fixedDepositDao = new FixedDepositJdbcDao();
    }
    if("hibernate".equalsIgnoreCase(daoType)) {
        fixedDepositDao = new FixedDepositHibernateDao();
    }
    .....
    return fixedDepositDao;
  }
}

如程序示例2-3所示,FixedDepositDaoFactory类定义了一个getFixedDepositDao静态方法,该方法根据daoType参数的值创建并返回FixedDepositJdbcDao、FixedDepositHibernateDao或FixedDepositIbatisDao类的实例。

在程序示例2-4中,FixedDepositDaoFactory类的bean定义指示Spring容器调用FixedDepositDaoFactory的getFixedDepositDao方法,以获取FixedDepositJdbcDao类的实例。

程序示例2-4 FixedDepositDaoFactory类的bean定义

<bean id="dao" class="sample.spring.FixedDepositDaoFactory" 
         factory-method="getFixedDepositDao">
 <constructor-arg index=”0” value="jdbc"/>
     ...
</bean>

在上述bean定义中,class特性指定了定义静态工厂方法的类的完全限定名称。factory-method特性指定了Spring容器调用的获取FixedDepositDao对象实例的静态工厂方法的名称。<constructor-arg>元素在Spring的bean schema中定义,用于传递构造函数的参数以及静态和实例工厂方法的参数。index特性指的是构造函数中,也可以是静态或实例工厂方法的参数的位置。在上述bean定义中,index特性值为0意味着<constructor-arg>元素为getFixedDepositDao工厂方法的第一个参数(即daoType), 而value特性指定了参数值。如果工厂方法接受多个参数,则需要为每个参数定义一个<constructor-arg>元素。

需要着重注意的是,调用ApplicationContext的getBean方法来获取dao bean(见程序示例2-4)将会调用FixedDepositDaoFactory的getFixedDepositDao工厂方法。这意味着调用getBean(“dao”)返回由getFixedDepositDao工厂方法创建的FixedDepositDao实例,而不是FixedDepositDaoFactory类的实例。

现在我们已经看到创建了一个FixedDepositDao实例的工厂类的配置,程序示例2-5将展示如何将FixedDepositDao的实例注入FixedDepositServiceImpl类中。

程序示例2-5 注入由静态工厂方法创建的对象

<bean id="service" class="sample.spring.chapter02.bankapp.FixedDepositServiceImpl">
   <property name=“fixedDepositDao" ref="dao" />
</bean>

<bean id="dao" class="sample.spring.chapter02.basicapp.FixedDepositDaoFactory" 
     factory-method="getFixedDepositDao">
     <constructor-arg index=”0” value="jdbc"/>
</bean>

在程序示例2-5中,<property>元素将FixedDepositDaoFactory的getFixedDepositDao工厂方法返回的FixedDepositDao实例注入FixedDepositServiceImpl实例中。如果将上面显示的FixedDepositServiceImpl类的bean定义与程序示例2-2中所示的bean定义进行对比,你会发现它们完全相同。这表明,无论Spring容器如何(使用无参数构造函数或静态工厂方法)创建bean实例,bean的依赖项都会以相同的方式指定。

现在来介绍一下Spring容器是如何通过调用实例工厂方法来将bean实例化的。

程序示例2-6展示了FixedDepositDaoFactory类,它定义了用于创建和返回FixedDepositDao实例的实例工厂方法。

程序示例2-6 FixedDepositDaoFactory 类

public class FixedDepositDaoFactory {
  public FixedDepositDaoFactory() {
  }

  public FixedDepositDao getFixedDepositDao(String daoType, ...) {
    FixedDepositDao fixedDepositDao = null;

    if("jdbc".equalsIgnoreCase(daoType)) {
         fixedDepositDao = new FixedDepositJdbcDao();
    }
    if("hibernate".equalsIgnoreCase(daoType)) {
         fixedDepositDao = new FixedDepositHibernateDao();
    }
    .....
    return fixedDepositDao;
  }
}

如果类定义了一个实例工厂方法,则该类必须定义一个public构造函数,以便Spring容器可以创建该类的实例。在程序示例2-6中,FixedDepositDaoFactory类定义了一个public无参构造函数。FixedDepositDaoFactory的getFixedDepositDao方法是一个创建并返回FixedDepositDao实例的实例工厂方法。

程序示例2-7展现了如何指示Spring容器调用FixedDepositDaoFactory的getFixedDepositDao方法来获取FixedDepositDao的一个实例。

程序示例2-7 调用FixedDepositDaoFactory的getFixedDepositDao方法的配置

<bean id="daoFactory" class="sample.spring.chapter02.basicapp.FixedDepositDaoFactory" /> 

<bean id="dao" factory-bean="daoFactory" factory-method="getFixedDepositDao">
     <constructor-arg index="0" value="jdbc"/>
</bean>

<bean id="service" class="sample.spring.chapter02.bankapp.FixedDepositServiceImpl">
   <property name=“fixedDepositDao" ref="dao" />
</bean>

在程序示例2-7中,FixedDepositDaoFactory类(包含实例工厂方法的类)被配置为常规的Spring bean,并且使用单独的<bean>元素来配置实例工厂方法的详细信息。要配置实例工厂方法的详细信息,请使用<bean>元素的factory-bean和factory-method特性。factory-bean特性是指定义实例工厂方法的bean、factory-method特性指定实例工厂方法的名称。在程序示例2-7中,<property>元素将FixedDepositDaoFactory的getFixedDepositDao工厂方法返回的FixedDepositDao实例注入FixedDepositServiceImpl实例中。

static工厂方法一样,可以使用<constructor-arg>元素将参数传递给实例工厂方法。注意,在程序示例2-7中,调用ApplicationContext的getBean方法获取dao bean将会导致调用FixedDepositDaoFactory的getFixedDepositDao工厂方法。

下面介绍如何设置由静态和实例工厂方法创建的bean的依赖项。

注入由工厂方法创建的bean的依赖项

可以将bean依赖项作为参数传递给工厂方法,也可以使用基于setter的DI来注入由静态或实例工厂方法返回的bean实例的依赖项。

用于定义databaseInfo特性的FixedDepositJdbcDao类如程序示例2-8所示。

程序示例2-8 FixedDepositJdbcDao 类

public class FixedDepositJdbcDao {
  private DatabaseInfo databaseInfo;
  .....
  public FixedDepositJdbcDao() { }

  public void setDatabaseInfo(DatabaseInfo databaseInfo) {
    this. databaseInfo = databaseInfo;
  } 
  .....
}

在程序示例2-8中,databaseInfo表示通过setDatabaseInfo方法赋值的FixedDepositJdbcDao类的依赖项。

FixedDepositDaoFactory类定义了一个负责创建和返回FixedDepositJdbcDao类的实例的工厂方法,如程序示例2-9所示。

程序示例2-9 FixedDepositDaoFactory类

public class FixedDepositDaoFactory {
  public FixedDepositDaoFactory() {
  }

  public FixedDepositDao getFixedDepositDao(String daoType) {
    FixedDepositDao fixedDepositDao = null;

    if("jdbc".equalsIgnoreCase(daoType)) {
        fixedDepositDao = new FixedDepositJdbcDao();
    }
    if("hibernate".equalsIgnoreCase(daoType)) {
         fixedDepositDao = new FixedDepositHibernateDao();
    }
    .....
    return fixedDepositDao;
  }
}

在程序示例2-9中,getFixedDepositDao方法是用于创建FixedDepositDao实例的实例工厂方法。如果daoType参数的值为jdbc,则getFixedDepositDao方法将创建一个FixedDepositJdbcDao的实例。请注意,getFixedDepositDao方法没有设置FixedDepositJdbcDao实例的databaseInfo特性。

正如我们在程序示例2-7中看到的,bean定义指示Spring容器通过调用FixedDepositDaoFactory类的getFixedDepositDao实例工厂方法来创建FixedDepositJdbcDao的实例,如程序示例2-10所示。

程序示例2-10 调用FixedDepositDaoFactory的getFixedDepositDao方法的配置

<bean id="daoFactory" class="FixedDepositDaoFactory" />

<bean id="dao" factory-bean="daoFactory" factory-method="getFixedDepositDao">
     <constructor-arg index="0" value="jdbc"/>
 </bean>

dao bean定义指示Spring容器调用FixedDepositDaoFactory的getFixedDepositDao方法,该方法创建并返回FixedDepositJdbcDao的实例。但是,FixedDepositJdbcDao的databaseInfo特性并没有设置。如果需要设置databaseInfo特性,可以在getFixedDepositDao方法返回的FixedDepositJdbcDao实例上执行基于setter的DI,如程序示例2-11所示。

程序示例2-11 调用FixedDepositDaoFactory的getFixedDepositDao方法并设置返回的FixedDepositJdbcDao实例的databaseInfo特性的配置

<bean id="daoFactory" class="FixedDepositDaoFactory" />

<bean id="dao" factory-bean="daoFactory" factory-method="getFixedDepositDao">
     <constructor-arg index="0" value="jdbc"/>
     <property name="databaseInfo" ref="databaseInfo"/>
</bean>

<bean id="databaseInfo" class="DatabaseInfo" />

在程序示例2-11的bean定义中,<property>元素用于设置由getFixedDepositDao实例工厂方法返回的FixedDepositJdbcDao实例的databaseInfo特性。

注意

与实例工厂方法一样,可以使用<property>元素将依赖关系注入静态工厂方法返回的bean实例中。

在Spring中,依赖注入是通过将参数传递给bean的构造函数和setter方法来实现的。我们在前面的章节中介绍过,通过setter方法注入依赖的DI技术称为基于setter的DI。在本节中,我们将介绍依赖项作为构造函数参数传递的DI技术(又称为基于构造函数的DI)。

下面通过一个例子比较一下在基于setter的DI和基于构造函数的DI技术中指定bean依赖项的区别。

在基于setter的DI中,<property>元素用于指定bean依赖项。假设MyBank应用程序包含一个PersonalBankingService服务,该服务允许客户检索银行账户对账单、检查银行账户明细、更新联系电话、更改密码和联系客户服务。PersonalBankingService类使用JmsMessageSender(用于发送JMS消息)、EmailMessageSender(用于发送电子邮件)和WebServiceInvoker(用于调用外部Web服务)对象来完成其预期功能。程序示例2-12展示了PersonalBankingService类。

程序示例2-12 PersonalBankingService类

public class PersonalBankingService {
  private JmsMessageSender jmsMessageSender;
  private EmailMessageSender emailMessageSender;
  private WebServiceInvoker webServiceInvoker;
  .....
  public void setJmsMessageSender(JmsMessageSender jmsMessageSender) {
    this.jmsMessageSender = jmsMessageSender;
  }

  public void setEmailMessageSender(EmailMessageSender emailMessageSender) {
    this.emailMessageSender = emailMessageSender;
  }

  public void setWebServiceInvoker(WebServiceInvoker webServiceInvoker) {
    this.webServiceInvoker = webServiceInvoker;
  }
  .....
}

在程序示例2-12中,PersonalBankingService类的每个依赖项(JmsMessageSender、EmailMessageSender和WebServiceInvoker)都定义了一个setter方法。

PersonalBankingService类为其依赖项定义了setter方法,因此使用了基于setter的DI,如程序示例2-13所示。

程序示例2-13 PersonalBankingService类的bean定义及其依赖项

   <bean id="personalBankingService" class="PersonalBankingService">
     <property name="emailMessageSender" ref="emailMessageSender" />
     <property name="jmsMessageSender" ref="jmsMessageSender" />
     <property name="webServiceInvoker" ref="webServiceInvoker" />
   </bean>

   <bean id="jmsMessageSender" class="JmsMessageSender">
     .....
   </bean>
   <bean id="webServiceInvoker" class="WebServiceInvoker" />
     .....
   </bean>
   <bean id="emailMessageSender" class="EmailMessageSender" />
     .....
   </bean>

在PersonalBankingService bean的定义中,为PersonalBankingService类的每个依赖项都指定了一个<property>元素。

下面介绍如何使用基于构造函数的DI来对PersonalBankingService类建模。

在基于构造函数的DI中,bean的依赖项作为参数传递给bean类的构造函数。程序示例2-14展示了一个PersonalBankingService类的修改版本,其构造函数接收JmsMessageSender、EmailMessageSender和WebServiceInvoker对象。

程序示例2-14 PersonalBankingService 类

public class PersonalBankingService {
  private JmsMessageSender jmsMessageSender;
  private EmailMessageSender emailMessageSender;
  private WebServiceInvoker webServiceInvoker;
  .....
  public PersonalBankingService(JmsMessageSender jmsMessageSender, 
 EmailMessageSender emailMessageSender, 
 WebServiceInvoker webServiceInvoker) {
 
    this.jmsMessageSender = jmsMessageSender;
    this.emailMessageSender = emailMessageSender;
    this.webServiceInvoker = webServiceInvoker;
  }
  .....
}

PersonalBankingService类的构造函数的参数代表PersonalBankingService类的依赖项。程序示例2-15展示了如何通过<constructor-arg>元素来提供这些依赖项。

程序示例2-15 PersonalBankingService 的bean定义

<bean id="personalBankingService" class="PersonalBankingService">
  <constructor-arg index="0" ref="jmsMessageSender" />
 <constructor-arg index="1" ref="emailMessageSender" />
 <constructor-arg index="2" ref="webServiceInvoker" />
</bean>

<bean id="jmsMessageSender" class="JmsMessageSender">
  .....
</bean>
<bean id="webServiceInvoker" class="WebServiceInvoker" />
  .....
</bean>
<bean id="emailMessageSender" class="EmailMessageSender" />
  .....
</bean>

在程序示例2-15中,<constructor-arg>元素指定了传递给PersonalBankingService实例的构造函数参数的详细信息。index特性指定了构造函数参数中的索引: 如果index特性值为0,则表示<constructor-arg>元素对应于第一个构造函数参数;如果index特性值为1,则表示<constructor-arg>元素对应于第二个构造函数参数,以此类推。如果构造函数参数与继承无关,则不需要指定index特性。例如,如果JmsMessageSender、WebServiceInvoker和EmailMessageSender是不同的对象,则不需要指定index特性。与< property >元素的情况一样,<constructor-arg>元素的ref特性用于传递对bean的引用。

下面介绍如何结合使用基于构造函数的DI以及基于setter的DI。

基于构造函数和基于setter的DI机制的结合使用

如果bean类需要结合使用基于构造函数的ID机制和基于setter的DI机制,则可以使用<constructor-arg>和<property>元素的组合来注入依赖关系。

程序示例2-16展示了PersonalBankingService类的一个版本,其依赖项作为参数注入构造函数和setter方法中。

程序示例2-16 PersonalBankingService 类

public class PersonalBankingService {
  private JmsMessageSender jmsMessageSender;
  private EmailMessageSender emailMessageSender;
  private WebServiceInvoker webServiceInvoker;
  .....
  public PersonalBankingService(JmsMessageSender jmsMessageSender, 
 EmailMessageSender emailMessageSender) {
    this.jmsMessageSender = jmsMessageSender;
    this.emailMessageSender = emailMessageSender;
  }

  public void setWebServiceInvoker(WebServiceInvoker webServiceInvoker) {
    this.webServiceInvoker = webServiceInvoker;
  }
  .....
}

在PersonalBankingService类中,jmsMessageSender和emailMessageSender依赖项作为构造函数注入,而webServiceInvoker依赖关系通过setWebServiceInvoker setter方法注入。以下bean定义表明,<constructor-arg>和<property>元素用于注入PersonalBankingService类的依赖项,如程序示例2-17所示。

程序示例2-17 基于构造函数和基于setter两种DI机制的结合

   <bean id="dataSource" class="PersonalBankingService">
        <constructor-arg index="0" ref="jmsMessageSender" />
 <constructor-arg index="1" ref="emailMessageSender" />
 <property name="webServiceInvoker" ref="webServiceInvoker" />
   </bean>

可以看到,<property>和<constructor-arg>元素用于传递依赖项(对其他bean的引用)到setter方法和构造函数中。也可以使用这些元素来传递bean所需的配置信息(单纯的String值)。

以EmailMessageSender类为例,在该类中需要使用电子邮件服务器地址、用户名和密码三项来对连接电子邮件服务器进行身份验证。可以使用<property>元素设置EmailMessageSender bean的属性,如程序示例2-18所示。

程序示例2-18 EmailMessageSender类和相应的bean定义

public class EmailMessageSender {
 private String host;
 private String username;
 private String password;
  .....
  public void setHost(String host) {
    this.host = host;
  }

  public void setUsername(String username) {
    this.username = username;
  }

  public void setPassword(String password) {
    this.password = password;
  }
  .....
}

  <bean id="emailMessageSender" class="EmailMessageSender">
       <property name="host" value="smtp.gmail.com"/>
 <property name="username" value="myusername"/>
 <property name="password" value="mypassword"/>
  </bean>

在程序示例2-18中,我们已经使用<property>元素来设置EmailMessageSender bean的主机(host)、用户名(username)和密码(password)特性。由name特性标识的bean特性的String值通过value特性来指定。主机、用户名和密码特性表示EmailMessageSender bean所需的配置信息。在第3章中,我们将看到如何设置原始类型(如int、long等)、集合类型(如java.util.List、java.util.Map等)和自定义类型(如地址)的特性。

程序示例2-19展示了将配置信息(如主机、用户名和密码)作为构造函数参数接收的EmailMessageSender类(以及相应的bean定义)的修改版本。

程序示例2-19 EmailMessageSender类和相应的bean定义

public class EmailMessageSender {
  private String host;
 private String username;
 private String password;
   .....
   public EmailMessageSender(String host, String username, String password) {
     this.host = host;
     this.username = username;
     this.password = password;
   }
   .....
}
   <bean id="emailMessageSender" class="EmailMessageSender">
        <constructor-arg index="0" value="smtp.gmail.com"/>
 <constructor-arg index="1" value="myusername"/>
 <constructor-arg index="2" value="mypassword"/>
   </bean> 

在程序示例2-19中,<constructor-arg>元素用于传递EmailMessageSender bean所需的配置详细信息。由index特性标识的构造函数参数的String值通过value特性来指定。

到目前为止,我们已经看到,<constructor-arg>元素用于注入bean依赖项,并为String类型构造函数参数传递值。在第3章中,我们将看到如何为原始类型(如int、long等),集合类型(如java.util.List、java.util.Map等)以及自定义类型(如地址)的构造函数参数指定值。

现在我们已经了解了如何指示Spring容器创建bean并执行DI,接下来介绍bean的各种作用域。

你可能需要指定一个bean的范围,以控制所创建的bean实例是否可以共享(singleton范围),还是每次从Spring容器请求bean时都创建一个新的bean实例(prototype 范围)。bean的范围由<bean>元素的scope特性定义。如果没有指定scope特性,则表示该bean是singleton范围的。

在Web应用程序场景中,Spring允许你指定其他的范围:request、session、websocket、application和globalSession。这些范围决定了bean实例的生命周期。例如,request范围的bean的生命周期仅限于单个HTTP请求。在本章中,我们的讨论仅限于singleton和prototype两种范围。request、session、application和globalSession范围在第12章中描述。

 含义 

chapter 2/ch02-bankapp-scopes (这个项目展示了singleton和prototype范围的bean的使用,若要运行应用程序,执行该项目的BankApp类的main方法,该项目还包含2个JUnit测试可以执行,分别是PrototypeTest和SingletonTest)。

singleton范围是应用程序上下文XML文件中定义的所有bean的默认范围。singleton范围bean的实例在创建Spring容器时创建,并且在Spring容器被销毁时销毁。Spring容器会为每个singleton范围bean创建唯一的实例,该实例由依赖于它的所有bean共享。

程序示例2-20展示了ch02-bankapp-scope项目的applicationContext.xml文件,其中所有的bean都是singleton范围的。

程序示例2-20 singleton范围bean的applicationContext.xml示例

Project – ch02-bankapp-scopes
Source location - src/main/resources/META-INF/spring

<beans ..... >
  <bean id="controller" 
      class="sample.spring.chapter02.bankapp.controller.FixedDepositControllerImpl">
    <property name="fixedDepositService" ref="service" />
  </bean>

  <bean id="service" 
     class="sample.spring.chapter02.bankapp.service.FixedDepositServiceImpl">
    <property name="fixedDepositDao" ref="dao" />
  </bean>

  <bean id="dao" class="sample.spring.chapter02.bankapp.dao.FixedDepositDaoImpl" />
  .....
</beans>

在程序示例2-20的applicationContext.xml文件中,因为没有为<bean>元素指定scope特性,所以controller、service和dao bean都是singleton范围的。这意味着Spring容器只分别创建了FixedDepositControllerImpl、FixedDepositServiceImpl和FixedDepositDaoImpl类的一个实例。由于这些bean是singleton范围的,Spring容器在每次使用ApplicationContext的getBean方法检索其中一个bean时返回的都是该bean的同一个实例。

  注意

如果未指定scope特性或scope特性的值为singleton,则表示该bean为singleton范围。

程序示例2-21展示了ch02-bankapp-scope项目的SingletonTest(JUnit测试类)类的testInstances方法。testInstances方法测试了对ApplicationContext的getBean方法的多次调用返回的是controller bean的相同实例或不同实例。

程序示例2-21 SingletonTest JUnit测试类

Project – ch02-bankapp-scopes
Source location - src/test/java/sample/spring/chapter02/bankapp

package sample.spring.chapter02.bankapp;

import static org.junit.Assert.assertSame;
import org.junit.BeforeClass;
import org.junit.Test;

import sample.spring.chapter02.bankapp.controller.FixedDepositController;

public class SingletonTest {
  private static ApplicationContext context;

  @BeforeClass
 public static void init() {
    context = new ClassPathXmlApplicationContext(
         "classpath:META-INF/spring/applicationContext.xml");
  }

  @Test
 public void testInstances() {
    FixedDepositController controller1 = (FixedDepositController) context.getBean("controller");
    FixedDepositController controller2 = (FixedDepositController) context.getBean("controller");
    assertSame("Different FixedDepositController instances", controller1, controller2);
  }
  .....
}

在程序示例2-21中,JUnit的@BeforeClass注释指定了在类中的任何测试方法(即用JUnit的@Test注释来注释的方法)之前调用init方法。这意味着@BeforeClass注释方法只被调用一次,而@Test注释的方法只有在执行@BeforeClass注释的方法后才能执行。请注意,init方法是一种静态方法。init方法通过将配置元数据(见程序示例2-20)传递给ClassPathXmlApplicationContext的构造函数来创建ApplicationContext对象的实例。testInstances方法获取controller bean的两个实例,并通过使用JUnit的assertSame断言来检查这两个实例是否相同。由于controller bean是singleton范围的,因此controller1 bean和controller2 bean的实例是一样的。因为这个原因,SingletonTest的testInstances测试在执行时不会有任何断言错误。

图2-4描述了当你多次调用ApplicationContext的getBean方法时,Spring容器返回的是相同的controller bean实例。

图2-4 对singleton范围bean的多个请求来说,从Spring容器返回的是bean的同一个实例

获取controller bean的多次调用返回了controller bean的同一个实例,如图2-4所示。

 注意 

在图2-4中,controller bean实例由一个双层的矩形表示。顶部的矩形展示了该bean的名称(即<bean>元素的id特性的值),底部的矩形展示了该bean的类型(也就是<bean>元素的class特性的值)。在本书的其余部分,我们将使用此约定来表示在Spring容器中的bean实例。

singleton范围的bean实例在依赖它的bean之间共享。程序示例2-22展示了SingletonTest(一个JUnit测试类)的testReference方法,该方法用于检查FixedDepositController实例引用的FixedDepositDao实例是否与直接调用ApplicationContext的getBean方法获得的相同。

程序示例2-22 SingletonTest JUnit测试类的testReference方法

Project – ch02-bankapp-scopes
Source location - src/test/java/sample/spring/chapter02/bankapp

package sample.spring.chapter02.bankapp;

import static org.junit.Assert.assertSame;
import org.junit.Test;    

public class SingletonTest {
  private static ApplicationContext context;
  .....
  @Test
  public void testReference() {
    FixedDepositController controller = (FixedDepositController) context.getBean("controller");

    FixedDepositDao fixedDepositDao1 = 
           controller.getFixedDepositService().getFixedDepositDao();
    FixedDepositDao fixedDepositDao2 = (FixedDepositDao) context.getBean("dao");
    assertSame("Different FixedDepositDao instances", fixedDepositDao1, fixedDepositDao2);
  }
}

在程序示例2-22中,testReference方法首先检索FixedDepositController bean引用的FixedDepositDao实例(参见程序示例2-22中的fixedDepositDao1变量),然后使用 ApplicationContext的getBean方法直接检索FixedDepositDao bean的另一个实例(见程序示例2-22中的fixedDepositDao2变量)。如果执行testReference测试,将看到测试成功通过,因为fixedDepositDao1和fixedDepositDao2实例是相同的。

图2-5展示了FixedDepositController实例引用的FixedDepositDao实例与通过在ApplicationContext上调用getBean(“dao”)方法返回的实例相同。

图2-5 singleton范围的bean实例在依赖它的bean之间共享

FixedDepositController bean实例引用的FixedDepositDao实例和通过调用ApplicationContext的getBean方法直接获取的一个实例是相同的,如图2-5所示。如果多个bean依赖于singleton范围的bean,那么所有依赖的bean共享相同的singleton范围的bean实例。

下面来看多个Spring容器之间是否会共享同一个singleton范围的bean实例。

singleton范围的bean和多个Spring容器

singleton范围bean实例的范围仅限于Spring容器实例。这意味着如果使用相同的配置元数据创建Spring容器的两个实例,则每个Spring容器都将得到属于其自己的singleton bean实例集合。

程序示例2-23展示了SingletonTest类的testSingletonScope方法,该方法用于测试从两个不同的Spring容器实例检索到的FixedDepositController bean实例是否相同。

程序示例2-23 SingletonTest(JUnit测试类)的testSingletonScope 方法

Project – ch02-bankapp-scopes
Source location - src/test/java/sample/spring/chapter02/bankapp

package sample.spring.chapter02.bankapp;

import static org.junit.Assert.assertNotSame;

public class SingletonTest {
  private static ApplicationContext context;
  .....
  @BeforeClass
 public static void init() {
 context = new ClassPathXmlApplicationContext(
 "classpath:META-INF/spring/applicationContext.xml");
  }

  @Test
  public void testSingletonScope() {
 ApplicationContext anotherContext = new ClassPathXmlApplicationContext(
 "classpath:META-INF/spring/applicationContext.xml");

    FixedDepositController fixedDepositController1 = (FixedDepositController) anotherContext
         .getBean("controller");

    FixedDepositController fixedDepositController2 = 
          (FixedDepositController) context .getBean("controller");

    assertNotSame("Same FixedDepositController instances", 
 fixedDepositController1, fixedDepositController2);
  }
}

SingletonTest的init方法(用JUnit的@BeforeClass以注释)在执行任何@Test注释方法之前都会创建一个ApplicationContext(由context变量标识)的实例,testSingletonScope方法使用相同的application- Context.xml文件再创建另一个Spring容器(由anotherContext变量标识)的实例。在testSingletonScope中会从两个Spring容器中分别检索出一个FixedDepositController bean的实例,并检查它们是否不相同。如果执行testSingletonScope测试,则会发现测试成功通过,因为从上下文实例检索的FixedDepositController bean实例与从anotherContext实例检索的bean实例不同。

图2-6描述了testSingletonScope方法所展示的行为。每个Spring容器都会创建其自己的controller bean实例,如图2-6所示。这就是当调用getBean(“controller”)方法时,context和anotherContext返回的controller bean是不同实例的原因。

testSingletonScope方法展示了每个Spring容器都创建了其自己的singleton范围的bean实例。需要注意的是,Spring容器会为每个bean定义都创建一个singleton范围的bean实例。程序示例2-24展示了FixedDepositDaoImpl类的多个bean定义。

图2-6 每个Spring容器都创建其自己的controller bean实例

程序示例2-24 applicationContext.xml中对同一个类的多个bean定义

Project – ch02-bankapp-scopes
Source location - src/main/resources/META-INF/spring

  <bean id="dao" class="sample.spring.chapter02.bankapp.dao.FixedDepositDaoImpl" />
  <bean id="anotherDao" 
            class="sample.spring.chapter02.bankapp.dao.FixedDepositDaoImpl" />

程序示例2-24中展示的bean定义用于FixedDepositDaoImpl类。由于未指定范围特性,示例中展示的bean定义代表singleton范围的bean。Spring容器将dao和anotherDao视为两个不同的bean定义,并分别为它们创建相应的FixedDepositDaoImpl实例。

程序示例2-25中展示了singleton范围的testSingletonScopePerBeanDef方法,该方法用于测试对应于dao和anotherDao bean定义的FixedDepositDaoImpl实例是否是相同的。

程序示例2-25 SingletonTest(JUnit测试类)的testSingletonScopePerBeanDef方法

Project – ch02-bankapp-scopes
Source location - src/test/java/sample/spring/chapter02/bankapp

package sample.spring.chapter02.bankapp;

import static org.junit.Assert.assertNotSame;

public class SingletonTest {
  private static ApplicationContext context;
  .....
  @Test
  public void testSingletonScopePerBeanDef() {
    FixedDepositDao fixedDepositDao1 = (FixedDepositDao) context.getBean("dao");
    FixedDepositDao fixedDepositDao2 = (FixedDepositDao) context.getBean("anotherDao");
    assertNotSame("Same FixedDepositDao instances", fixedDepositDao1, fixedDepositDao2);
  }
}

在程序示例2-25中,fixedDepositDao1和fixedDepositDao2变量表示Spring容器分别根据dao和anotherDao bean定义创建的FixedDepositDaoImpl类的实例。由于fixedDepositDao1(对应于dao bean定义)和fixedDepositDao2(对应于anotherDao bean定义)是不同的实例,如果执行testSingletonScopePerBeanDef测试,不会产生任何断言错误。

图2-7总结了每个bean定义都有一个相应的singleton范围的bean。

图2-7 每个bean定义都有一个singleton范围的bean实例

图2-7展示了在Spring容器中每个bean定义都存在一个singleton范围的bean实例。

前面提到,默认情况下,singleton范围的bean是预实例化的,这意味着在创建Spring容器的实例时,将创建一个singleton范围bean的实例。下面来看如何对一个singleton范围的bean进行延迟初始化。

延迟初始化一个singleton范围的bean

只有在第一次请求时,才指示Spring容器创建singleton范围的bean实例。程序示例2-26中的lazyBean定义展示了如何指示Spring容器延迟初始化一个singleton范围的bean。

程序示例2-26 延迟初始化一个singleton范围的bean

<bean id="lazyBean" class="example.LazyBean" lazy-init="true"/>

<bean>元素的lazy-init特性用于指定bean实例是否需要延迟初始化。如果该值为true(如程序示例2-26所示的bean定义的情况),则Spring容器将在第一次接收到对该bean的请求时对其初始化。

图2-8所示的时序图展示了lazy-init特性如何影响singleton bean实例的创建。

图2-8 当应用程序第一次请求时,会创建一个延迟初始化的singleton bean实例

在图2-8中,BeanA表示未被设置为延迟初始化的singleton bean,LazyBean表示被设置为延迟初始化的singleton bean。当创建Spring容器时,BeanA也被实例化,因为它没有被设置为延迟初始化。此外,LazyBean将在第一次调用ApplicationContext的getBean方法来检索LazyBean的实例时被实例化。

 注意 

可以使用<beans>元素的default-lazy-init特性来指定应用程序上下文XML文件中定义的bean的默认初始化策略。如果<bean>元素的lazy-init特性与<beans>元素的default-lazy-init特性指定的值不同,则该bean使用lazy-init特性指定的值。

作为一个singleton bean,可以通过Spring容器进行延迟初始化或预实例化,你可能会考虑此时是否应该将singleton bean定义为延迟初始化或预实例化。在大多数应用场景中,在创建Spring容器之前预先实例化singleton bean以发现配置问题是有益的。程序示例2-27展示了一个被设置为延迟初始化的aBean的singleton bean,这个aBean依赖于bBean bean。

程序示例2-27 延迟初始化的singleton bean

public class ABean {
  private BBean bBean;

  public void setBBean(BBean bBean) {
    this.bBean = bBean;
  }
  .....
}

<bean id="aBean" class="ABean" lazy-init="true">
  <property name="bBean" value="bBean" />
</bean>

<bean id="bBean" class="BBean" />

在程序示例2-27中,ABean的bBean property引用了BBean bean。请注意,ABean的bBean property是用<property>元素的value特性而不是ref特性来指定的。如果通过传递包含上述bean定义的XML文件来创建ApplicationContext实例,则不会抛出任何错误。但是,当尝试通过调用ApplicationContext的getBean方法获取aBean bean时,将收到以下错误消息。

Caused by: java.lang.IllegalStateException: Cannot convert value of type [java.lang.String] to required 
type [BBean] for property 'bBean: no matching editors or conversion strategy found

显示上述错误消息是因为Spring容器无法将ABean的bBean property的String值转换为BBean类型。这突出了用指定value特性取代指定ref特性来配置<bean>元素的问题。如果aBean被定义为预实例化(而不是延迟初始化),则上述配置问题可能在创建ApplicationContext的实例时就被捕获,而不是尝试从ApplicationContext获取一个aBean bean的实例时。

现在来了解一下Spring中prototype范围的bean。

prototype范围的bean与singleton范围的bean不同,因为Spring容器总是返回一个prototype范围bean的新实例。prototype范围的bean的另一个独特之处在于它们总是被延迟初始化。

程序示例2-28中的FixedDepositDetails bean表示一个prototype范围的bean,该配置来源于ch02-bankapp-scopes项目的applicationContext.xml文件。

程序示例2-28 prototype范围bean的applicationContext.xml示例

Project – ch02-bankapp-scopes
Source location - src/main/resources/META-INF/spring

<bean id="fixedDepositDetails"
     class="sample.spring.chapter02.bankapp.domain.FixedDepositDetails"
        scope="prototype" />

<bean>元素的scope特性值设置为prototype,如程序示例2-28所示。这意味着fixedDepositDetails bean是一个prototype范围的bean。

PrototypeTest(JUnit测试类)的testInstances方法展示了从Spring容器检索的fixedDepositDetails bean的两个实例是不同的,如程序示例2-29所示。

程序示例2-29 PrototypeTest(JUnit测试类)的testInstances方法

Project – ch02-bankapp-scopes
Source location - src/test/java/sample/spring/chapter02/bankapp

package sample.spring.chapter02.bankapp;

import static org.junit.Assert.assertNotSame;

public class PrototypeTest {
  private static ApplicationContext context;
  .....
  @Test
  public void testInstances() {
    FixedDepositDetails fixedDepositDetails1 = 
        (FixedDepositDetails)context.getBean("fixedDepositDetails");
    FixedDepositDetails fixedDepositDetails2 = 
        (FixedDepositDetails) context.getBean("fixedDepositDetails");

    assertNotSame("Same FixedDepositDetails instances",
 fixedDepositDetails1, fixedDepositDetails2);
  }
}

因为从ApplicationContext获取的两个FixedDepositDetails实例(fixedDepositDetails1和fixedDeposit Details2)不同,如果执行testInstances测试,则将看到测试在没有任何断言错误的情况下通过。

现在来了解如何为一个bean选择正确的范围(singleton或prototype)。

如果一个bean不会保持任何会话状态(也就是说,它是无状态的),那么它应该定义为一个singleton范围的bean。如果一个bean保持对话状态,它应该定义为一个prototype范围的bean。MyBank应用程序的FixedDepositServiceImpl、FixedDepositDaoImpl和FixedDepositControllerImpl bean本质上是无状态的,因此,它们定义为singleton范围的bean。MyBank应用程序的FixedDepositDetails bean(域对象)维护会话状态,因此,它定义为prototype范围的bean。

 注意 

如果你在应用程序中使用ORM框架(如Hibenate或iBATIS),则域对象由ORM框架创建,或者使用new运算符在应用程序代码中以编程方式创建。正是由于这个原因,如果应用程序使用ORM框架进行持久化,域对象将不会在应用程序上下文XML文件中被定义。

在本章中,我们讨论了Spring 框架的一些基础知识。我们研究了“面向接口编程”的设计方法,创建bean实例的不同方法,基于构造函数的DI和bean的作用域。在下一章中,我们将介绍如何设置bean属性和构造函数参数的不同类型(如int、long、Map、Set等)。


相关图书

深入浅出Spring Boot 3.x
深入浅出Spring Boot 3.x
云原生Spring实战Spring Boot与?Kubernetes实践
云原生Spring实战Spring Boot与?Kubernetes实践
Spring实战(第6版)
Spring实战(第6版)
Java研发自测入门与进阶
Java研发自测入门与进阶
Spring核心技术和案例实战
Spring核心技术和案例实战
Java EE企业级应用开发实战(Spring Boot+Vue+Element)
Java EE企业级应用开发实战(Spring Boot+Vue+Element)

相关文章

相关课程