Spring5-设计模式-全-

Spring5 设计模式(全)

原文:zh.annas-archive.org/md5/cef1d328c32409118676dec32b96e4bb

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

《Spring 5 设计模式》适合所有希望学习 Spring 用于企业应用程序的 Java 开发者。因此,企业 Java 开发者将特别发现它在理解 Spring 框架中使用的设计模式和它在企业应用程序中解决常见设计问题方面的有用性,并且他们将完全欣赏本书中提供的示例。在阅读本书之前,读者应具备 Core Java、JSP、Servlet 和 XML 的基本知识。

Spring 5 框架是由 Pivotal 新推出的,引入了响应式编程。Spring 5 引入了许多来自其先前版本的新特性和增强功能。我们将在书中讨论所有这些内容。“Spring 5 设计模式”将为您提供关于 Spring 框架的深入见解。

今天 Spring 框架的伟大之处在于,所有公司都已经将其作为企业应用程序开发的主要框架。对于 Spring 来说,无需外部企业服务器即可开始使用。

编写本书的目标是讨论 Spring 框架背后使用的所有设计模式以及它们如何在 Spring 框架中实现。在这里,作者还向您提供了一些在应用程序设计和开发中必须使用的最佳实践。

本书包含 12 章,涵盖了从基础知识到更复杂的设计模式(如响应式编程)的所有内容。

《Spring 5 设计模式》分为三个部分。第一部分向您介绍设计模式和 Spring 框架的基本知识。第二部分深入到前端之后,展示了 Spring 在应用程序后端中的位置。第三部分通过展示如何使用 Spring 构建 Web 应用程序并介绍 Spring 5 响应式编程的新特性来扩展这一点。这部分还展示了如何在企业应用程序中处理并发。

本书涵盖的内容

第一章,《Spring Framework 5.0 及设计模式入门》,概述了 Spring 5 框架及其所有新特性,包括一些 DI 和 AOP 的基本示例。您还将了解 Spring 优秀产品组合的概览。

第二章,《GOF 设计模式概览 - 核心设计模式》,概述了 GOF 设计模式家族的核心设计模式,包括一些适用于应用程序设计的最佳实践。您还将了解使用设计模式解决常见问题的概览。

第三章,《结构模式和行为的考虑》,概述了 GOF 设计模式家族的结构和行为设计模式,包括一些适用于应用程序设计的最佳实践。您还将了解使用设计模式解决常见问题的概览。

第四章, 使用依赖注入模式连接豆芽,探讨了依赖注入模式以及应用程序中 Spring 配置的细节,展示了您应用程序中各种配置方式。这包括使用 XML、注解、Java 和混合配置。

第五章, 理解 Bean 生命周期和使用的模式,概述了由 Spring 容器管理的 Spring Bean 生命周期,包括对 Spring 容器和 IoC 的理解。您还将了解 Spring Bean 生命周期回调处理程序和后处理器。

第六章, 使用代理和装饰器模式进行 Spring 面向切面编程,探讨了如何使用 Spring AOP 将横切关注点从它们所服务的对象中解耦。本章还为后续章节奠定了基础,在这些章节中,您将使用 AOP 提供声明式服务,例如事务、安全和缓存。

第七章, 使用 Spring 和 JDBC 模板模式访问数据库,探讨了如何使用 Spring 和 JDBC 访问数据;在这里,您将看到如何使用 Spring 的 JDBC 抽象和 JDBC 模板以比原生 JDBC 更简单的方式查询关系数据库。

第八章, 使用 Spring ORM 和事务实现模式访问数据库,展示了 Spring 如何与 ORM 框架集成,例如 Hibernate 和其他 Java 持久化 API(JPA)的实现,以及 Spring 事务管理。此外,它还包含了 Spring Data JPA 提供的即时查询生成魔法。

第九章, 使用缓存模式提高应用程序性能,展示了如何通过避免使用数据库(如果所需数据 readily available)来提高应用程序性能。因此,我将向您展示 Spring 如何提供对缓存数据的支持。

第十章, 使用 Spring 在 Web 应用程序中实现 MVC 模式,简要概述了使用 Spring MVC 开发 Web 应用程序。您将学习 MVC 模式、前端控制器模式、Dispatcher Servlet 以及基于 Spring 框架原则的 Spring MVC 基础知识。您将了解如何编写控制器来处理 Web 请求,并看到如何透明地将请求参数和有效负载绑定到您的业务对象,同时提供验证和错误处理。本章还简要介绍了 Spring MVC 中的视图和视图解析器。

第十一章,实现响应式设计模式,探讨了响应式编程模型,即使用异步数据流进行编程。您将看到如何在 Spring Web 模块中实现响应式系统。

第十二章,实现并发模式,更深入地探讨了在 Web 服务器内部处理多个连接时的并发性。正如我们在我们的架构模型中所概述的,请求处理与应用程序逻辑解耦。

您需要这本书什么

这本书可以在没有电脑或笔记本电脑的情况下阅读,在这种情况下,您需要的只是这本书本身。尽管为了跟随书中的示例,您需要 Java 8,您可以从www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html下载。您还需要您喜欢的 IDE 来处理示例,但我使用了软件 Spring Tool Suite;根据您的系统操作系统,从spring.io/tools/sts/all下载 Spring Tool Suite(STS)的最新版本。Java 8 和 STS 在多种平台上运行——Windows、macOS 和 Linux。

这本书面向谁

Spring 5 设计模式是为所有希望学习 Spring 用于企业应用的 Java 开发者而设计的。因此,企业 Java 开发者会发现它在理解 Spring 框架中使用的设计模式以及它如何解决企业应用中的常见设计问题特别有用,并且他们将完全欣赏本书中提供的示例。在阅读此书之前,读者应具备 Core Java、JSP、Servlet 和 XML 的基本知识。

规范

在这本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL 和用户输入如下所示:“在我们的代码中,我们有一个TransferServiceImpl类,其构造函数接受两个参数:”

代码块设置如下:

    public class JdbcTransferRepository implements TransferRepository{ 
      JdbcTemplate jdbcTemplate; 
      public setDataSource(DataSource dataSource) { 
        this.jdbcTemplate = new JdbcTemplate(dataSource); 
    } 
     // ... 
   } 

新术语重要词汇以粗体显示。

警告或重要注意事项如下所示。

小技巧和技巧看起来像这样。

读者反馈

我们欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢什么或不喜欢什么。读者的反馈对我们来说非常重要,因为它帮助我们开发出您真正能从中受益的标题。

要向我们发送一般反馈,只需发送电子邮件至 feedback@packtpub.com,并在邮件主题中提及书的标题。

如果您在某个主题上具有专业知识,并且您有兴趣撰写或为书籍做出贡献,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在您已经是 Packt 图书的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。

下载示例代码

您可以从您的账户中下载本书的示例代码文件。www.packtpub.com。如果您在其他地方购买了此书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. 使用您的电子邮件地址和密码登录或注册我们的网站。

  2. 将鼠标指针悬停在顶部的“支持”标签上。

  3. 点击“代码下载与勘误表”。

  4. 在搜索框中输入书籍名称。

  5. 选择您想要下载代码文件的书籍。

  6. 从下拉菜单中选择您购买此书的来源。

  7. 点击“代码下载”。

文件下载完成后,请确保您使用最新版本解压缩或提取文件夹:

  • Windows 下的 WinRAR / 7-Zip

  • Mac 下的 Zipeg / iZip / UnRarX

  • Linux 下的 7-Zip / PeaZip

本书代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Spring5-Design-Patterns。我们还有其他丰富的图书和视频代码包可供选择,网址为github.com/PacktPublishing/。请查看它们!

勘误表

尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现了错误——可能是文本或代码中的错误——如果您能向我们报告,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击“勘误提交表单”链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误表中的现有勘误列表。

要查看之前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在“勘误”部分。

侵权

互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过 copyright@packtpub.com 与我们联系,并提供涉嫌盗版材料的链接。

我们感谢您在保护我们作者和我们为您提供有价值内容的能力方面的帮助。

问题

如果您在这本书的任何方面遇到问题,您可以联系我们的 questions@packtpub.com,我们将尽力解决问题。

第一章:开始使用 Spring Framework 5.0 和设计模式

本章将帮助你通过模块更好地理解 Spring Framework,并使用负责 Spring 成功的设计模式。本章将涵盖 Spring Framework 的每一个主要模块。我们将从 Spring Framework 的介绍开始。我们将查看 Spring 5 中引入的新特性和增强功能。我们还将了解 Spring Framework 主要模块中使用的设计模式。

在本章结束时,你将了解 Spring 的工作原理,以及 Spring 如何通过使用设计模式解决企业应用设计层面的常见问题。你将知道如何通过使用 Spring 和设计模式来提高应用程序组件之间的松散耦合,以及如何通过使用 Spring 和其模式简化应用程序开发。

本章将涵盖以下主题:

  • Spring Framework 简介

  • 使用 Spring 和其模式简化应用程序开发

    • 利用 POJO 模式的力量

    • 注入依赖

    • 将方面应用于解决横切关注点

    • 使用模板模式消除样板代码

  • 使用工厂模式创建用于包含 bean 的 Spring 容器

    • 使用应用程序上下文创建容器

    • 容器中 bean 的生命周期

  • Spring 模块

  • Spring Framework 5.0 中的新特性

介绍 Spring Framework

在 Java 的早期阶段,有许多为大型企业应用提供企业解决方案的重型企业 Java 技术。然而,由于与框架紧密耦合,维护这些应用并不容易。几年前,除了 Spring 之外,所有 Java 技术都很重,就像 EJB 一样。当时,Spring 被引入作为一种替代技术,特别是为 EJB 设计的,因为与现有的其他 Java 技术相比,Spring 提供了一个非常简单、更精简、更轻量级的编程模型。Spring 通过使用许多可用的设计模式来实现这一点,但它专注于普通旧 Java 对象POJO)编程模型。这种模型为 Spring Framework 提供了简单性。它还通过使用代理模式和装饰者模式,通过依赖注入DI)模式和面向切面编程AOP)模式赋予了这些想法力量。

Spring 框架是一个开源的应用程序框架,也是一个基于 Java 的平台,它为开发企业级 Java 应用程序提供了全面的基础设施支持。因此,开发者不需要关心应用程序的基础设施;他们应该专注于应用程序的业务逻辑,而不是处理应用程序的配置。所有基础设施、配置和元配置文件,无论是基于 Java 的配置还是基于 XML 的配置,都由 Spring 框架处理。因此,这个框架使你在使用 POJOs 编程模型而不是非侵入式编程模型构建应用程序时更加灵活。

Spring 的控制反转IoC)容器是整个框架的核心。它有助于将应用程序的不同部分粘合在一起,从而形成一个连贯的架构。Spring MVC 组件可以用来构建一个非常灵活的 Web 层。IoC 容器简化了使用 POJOs 的业务层开发。

Spring 简化了应用程序开发,减少了对外部 API 的依赖。让我们看看作为应用程序开发者,你如何从 Spring 平台中受益的一些例子:

  • 所有应用程序类都是简单的 POJO 类--Spring 不是侵入式的。它不需要你在大多数用例中扩展框架类或实现框架接口。

  • Spring 应用程序不需要 Java EE 应用服务器,但它们可以部署在服务器上。

  • 你可以使用 Spring 框架中的事务管理来执行数据库中的方法,而不需要任何第三方事务 API。

  • 使用 Spring,你可以将 Java 方法用作请求处理器方法或远程方法,就像 servlet API 中的service()方法一样,但无需处理 servlet 容器的 servlet API。

  • Spring 允许你在应用程序中不使用Java 消息服务JMS)API 的情况下,将本地java方法用作消息处理器方法。

  • Spring 还允许你在应用程序中不使用Java 管理扩展JMX)API 的情况下,将本地java方法用作管理操作。

  • Spring 作为你的应用程序对象的容器。你的对象不必担心找到和建立彼此之间的连接。

  • Spring 实例化 bean 并将你的对象的依赖注入到应用程序中--它作为 bean 的生命周期管理器。

使用 Spring 及其模式简化应用程序开发

使用传统的 Java 平台开发企业应用程序时,在组织基本构建块作为独立组件以供应用程序重用时存在许多限制。为基本和通用功能创建可重用组件是最佳设计实践,因此你不能忽视它。为了解决应用程序中的可重用性问题,你可以使用各种设计模式,例如工厂模式、抽象工厂模式、建造者模式、装饰者模式和服务定位器模式,将这些基本构建块组合成一个连贯的整体,例如类和对象实例,以促进组件的可重用性。这些模式解决了常见的递归应用问题。Spring 框架简单地内部实现这些模式,为你提供了一个以正式化方式使用的框架。

企业应用程序开发中存在许多复杂性,但 Spring 的创建是为了解决这些问题,并使开发人员能够简化开发过程。Spring 不仅限于服务器端开发,它还帮助简化了项目构建、可测试性和松耦合等方面。Spring 遵循 POJO 模式,即 Spring 组件可以是任何类型的 POJO。组件是一段自包含的代码,理想情况下可以在多个应用程序中重用。

由于本书专注于 Spring 框架采用的简化 Java 开发的全部设计模式,我们需要讨论或至少提供一些基本的设计模式和最佳实践,以设计企业应用程序开发的基础设施。Spring 使用以下策略使 Java 开发变得简单且可测试:

  • Spring 利用POJO 模式的力量,以轻量级和最小侵入式开发企业应用程序

  • 它利用依赖注入模式(DI 模式)的力量,以松耦合的方式使系统接口面向对象

  • 它利用装饰者和代理设计模式的力量,通过方面和常见约定进行声明式编程

  • 它利用模板设计模式的力量,通过方面和模板消除样板代码

在本章中,我将解释这些想法中的每一个,并展示 Spring 如何简化 Java 开发的具体示例。让我们从探索 Spring 如何通过使用 POJO 模式鼓励面向 POJO 的开发来保持最小侵入性开始。

利用 POJO 模式的力量

对于 Java 开发,有许多其他框架通过强制你扩展或实现它们现有的类或接口来锁定你;Struts、Tapestry 和 EJB 的早期版本都采用了这种方法。这些框架的编程模型基于侵入式模型。这使得你的代码在系统中查找错误变得更加困难,有时甚至会使你的代码变得难以理解。然而,如果你正在使用 Spring 框架,你不需要实现或扩展其现有的类和接口,因此这只是一个基于 POJO 的实现,遵循非侵入式编程模型。这使得你的代码更容易在系统中查找错误,并保持代码的可理解性。

Spring 允许你使用非常简单的非 Spring 类进行编程,这意味着不需要实现特定的 Spring 类或接口,所以基于 Spring 的应用程序中的所有类都是简单的 POJO(Plain Old Java Objects)。这意味着你可以编译并运行这些文件,而无需依赖 Spring 库;甚至无法识别这些类正在被 Spring 框架使用。在基于 Java 的配置中,你会使用 Spring 注解,这是基于 Spring 的应用程序的最坏情况。

让我们通过以下示例来探讨这个问题:

    package com.packt.chapter1.spring; 
    public class HelloWorld { 
      public String hello() { 
        return "Hello World"; 
      } 
    } 

上述类是一个简单的 POJO 类,没有任何特殊指示或实现与框架相关,使其成为 Spring 组件。因此,这个类在 Spring 应用程序中可以像在非 Spring 应用程序中一样正常工作。这是 Spring 非侵入式编程模型的美妙之处。Spring 使 POJO 更加强大的另一种方式是通过使用依赖注入模式与其他 POJO 协作。让我们看看依赖注入是如何帮助解耦组件的。

在 POJO 之间注入依赖关系

术语“依赖注入”(dependency injection)并不新鲜——它被 PicoContainer 所使用。依赖注入是一种设计模式,它促进了 Spring 组件之间的松散耦合——也就是说,在不同的协作 POJO 之间。因此,通过将依赖注入应用于复杂的编程,你的代码将变得更加简单、易于理解,并且易于测试。

在你的应用程序中,许多对象根据你的要求协同工作以实现特定的功能。这些对象之间的协作实际上被称为依赖注入。在工作组件之间注入依赖关系可以帮助你在没有紧密耦合的情况下对应用程序中的每个组件进行单元测试。

在一个运行中的应用程序中,最终用户想要看到的是输出。为了创建输出,应用程序中的几个对象协同工作,有时会相互耦合。因此,当你编写这些复杂的应用程序类时,请考虑这些类的可重用性,并尽可能使这些类保持独立。这是编码的最佳实践之一,将有助于你独立地对这些类进行单元测试。

依赖注入的工作原理以及它如何使开发和测试变得容易

让我们来看看您应用程序中 DI 模式的实现。它使事情变得易于理解,松散耦合,并在整个应用程序中进行测试。假设我们有一个简单的应用程序(比你在大学课程中可能制作的Hello World示例更复杂)。每个类都在共同努力执行某些业务任务,并帮助构建业务需求和期望。这意味着应用程序中的每个类都有其业务任务的责任度量,以及其他协作对象(其依赖项)。让我们看看以下图像。这种对象之间的依赖关系可能会在依赖对象之间创建复杂性和紧密耦合:

图片

TransferService组件传统上依赖于另外两个组件:TransferRepositoryAccountRepository

典型的应用程序系统由几个部分组成,它们共同执行一个用例。例如,考虑下面的TransferService类。

使用直接实例化的TransferService

    package com.packt.chapter1.bankapp.transfer; 
    public class TransferService { 
      private AccountRepository accountRepository; 
      public TransferService () { 
        this.accountRepository = new AccountRepository(); 
      } 
      public void transferMoney(Account a, Account b) { 
        accountRepository.transfer(a, b); 
      } 
    } 

TransferService对象需要一个AccountRepository对象来从账户a向账户b进行转账。因此,它直接创建了一个AccountRepository对象的实例并使用它。但是,直接实例化增加了耦合度,并将对象创建代码分散到应用程序中,这使得维护变得困难,并且难以为TransferService编写单元测试,因为在这种情况下,每当您想通过使用assert进行单元测试来测试TransferService类的transferMoney()方法时,AccountRepository类的transfer()方法也可能被意外调用。但是,开发者并不了解AccountRepositoryTransferService类的依赖;至少,开发者无法使用单元测试来测试TransferService类的transferMoney()方法。

在企业应用程序中,耦合是非常危险的,它把你推向一个无法在未来对应用程序进行任何增强的情况,任何此类应用程序的进一步更改都可能产生大量错误,而修复这些错误可能会产生新的错误。紧密耦合的组件是这些应用程序中主要问题的原因之一。不必要的紧密耦合代码使你的应用程序难以维护,随着时间的推移,其代码将不会被重用,因为其他开发者无法理解它。但有时,企业应用程序需要一定程度的耦合,因为在现实世界的案例中,完全解耦的组件是不可能的。应用程序中的每个组件都对某个角色和业务需求承担一些责任,以至于应用程序中的所有组件都必须了解其他组件的责任。这意味着有时需要耦合,但我们必须非常小心地管理所需组件之间的耦合。

使用工厂辅助模式处理依赖组件

让我们尝试使用工厂模式来处理依赖对象的方法。这个设计模式基于 GOF(GoF,即设计模式之父)的工厂设计模式,通过工厂方法创建对象实例。因此,这种方法实际上集中了新操作符的使用。它根据客户端代码提供的信息创建对象实例。这种模式在依赖注入策略中得到了广泛的应用。

使用工厂辅助的TransferService

    package com.packt.chapter1.bankapp.transfer; 
    public class TransferService { 
      private AccountRepository accountRepository; 
      public TransferService() { 
        this.accountRepository = 
          AccountRepositoryFactory.getInstance("jdbc"); 
      } 
      public void transferMoney(Account a, Account b) { 
        accountRepository.transfer(a, b); 
      } 
    } 

在前面的代码中,我们使用工厂模式创建了一个AccountRepository对象。在软件工程中,应用程序设计和开发的最佳实践之一是面向接口编程P2I)。根据这一实践,具体的类必须实现一个接口,该接口在客户端代码中被调用者使用,而不是使用具体的类。通过使用 P2I,你可以改进前面的代码。因此,我们可以轻松地用接口的不同实现来替换它,而对客户端代码的影响很小。所以面向接口编程为我们提供了一种涉及低耦合的方法。换句话说,没有直接依赖于具体实现,导致低耦合。让我们看看下面的代码。在这里,AccountRepository是一个接口,而不是一个类:

    public interface AccountRepository{ 
      void transfer(); 
      //other methods 
    } 

因此,我们可以根据我们的需求来实现它,并且它依赖于客户端的基础设施。假设我们在开发阶段需要一个AccountRepository,使用 JDBC API。我们可以提供JdbcAccountRepositry接口的具体实现,如下所示:

    public class JdbcAccountRepositry implements AccountRepositry{ 
      //...implementation of methods defined in AccountRepositry 
      // ...implementation of other methods 
    } 

在这种模式中,对象由工厂类创建,以便于维护,并避免将对象创建的代码散布到其他业务组件中。使用工厂助手,还可以使对象创建可配置。这种技术为紧密耦合提供了解决方案,但我们仍然在业务组件中添加工厂类以获取协作组件。所以让我们在下一节看看依赖注入模式,并看看如何解决这个问题。

使用依赖注入模式对依赖组件进行操作

根据依赖注入模式,依赖对象在由某个工厂或第三方在对象创建时提供其依赖项。这个工厂以这种方式协调系统中的每个对象,即每个依赖对象不需要创建它们的依赖项。这意味着我们必须专注于定义依赖项,而不是解决企业应用程序中协作对象的依赖项。让我们看看下面的图像。你会了解到依赖项被注入到需要它们的对象中:

图片

应用程序中不同协作组件之间的依赖注入

为了说明这一点,让我们在下一节看看TransferService--一个TransferServiceAccountRepositoryTransferRepository有关联。在这里,TransferService能够通过TransferRepository的任何实现方式来转账,也就是说,我们可以使用JdbcTransferRepositoryJpaTransferRepository,具体取决于部署环境。

TransferServiceImpl足够灵活,可以接受任何它被提供的TransferRepository

    package com.packt.chapter1.bankapp; 
    public class TransferServiceImpl implements TransferService { 
      private TransferRepository transferRepository; 
      private AccountRepository  accountRepository; 
      public TransferServiceImpl(TransferRepository transferRepository,
       AccountRepository  accountRepository) { 
         this.transferRepository =
          transferRepository;//TransferRepository is injected 
         this.accountRepository  = accountRepository; 
         //AccountRepository is injected 
       } 
       public void transferMoney(Long a, Long b, Amount amount) { 
         Account accountA = accountRepository.findByAccountId(a); 
         Account accountB = accountRepository.findByAccountId(b); 
         transferRepository.transfer(accountA, accountB, amount); 
       } 
    } 

在这里,你可以看到TransferServiceImpl没有创建它自己的存储库实现。相反,我们在构造时作为构造函数参数提供了存储库的实现。这是一种称为构造函数注入的依赖注入类型。在这里,我们将存储库接口类型作为构造函数的参数传递。现在TransferServiceImpl可以使用任何存储库的实现,无论是 JDBC、JPA 还是模拟对象。重点是TransferServiceImpl没有耦合到任何特定的存储库实现。使用什么类型的存储库从一个账户转账到另一个账户无关紧要,只要它实现了存储库接口。如果你使用 Spring 框架的依赖注入模式,松耦合是其关键好处之一。依赖注入模式始终促进 P2I,因此每个对象通过其关联的接口而不是关联的实现来了解其依赖项,因此依赖项可以轻松地用该接口的另一个实现替换,而不是更改其依赖类实现。

Spring 提供了从其部分组装此类应用程序系统的支持:

  • 部件无需担心找到彼此

  • 任何部分都可以轻松替换

通过在应用程序部分或组件之间创建关联来组装应用程序系统的方法被称为连接。在 Spring 中,有多种方法可以将协作组件连接起来,以形成一个应用程序系统。例如,我们可以使用 XML 配置文件或 Java 配置文件。

现在让我们看看如何使用 Spring 将TransferRepositoryAccountRepository的依赖注入到TransferService中:

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

    xsi:schemaLocation="http://www.springframework.org/schema/beans 
    http://www.springframework.org/schema/beans/spring-beans.xsd"> 
    <bean id="transferService"  
     class="com.packt.chapter1.bankapp.service.TransferServiceImpl"> 
         <constructor-arg ref="accountRepository"/> 
         <constructor-arg ref="transferRepository"/> 
    </bean> 
    <bean id="accountRepository" class="com.
     packt.chapter1.bankapp.repository.JdbcAccountRepository"/> 
    <bean id="transferRepository" class="com.  
     packt.chapter1.bankapp.repository.JdbcTransferRepository"/>     

    </beans> 

在这里,TransferServiceImplJdbcAccountRepositoryJdbcTransferRepository被声明为 Spring 中的 bean。对于TransferServiceImpl bean,它通过构造函数参数传递对AccountRepositoryTransferRepository bean 的引用来构建。你可能想知道 Spring 还允许你使用 Java 表达式来表示相同的配置。

Spring 提供了基于 Java 的配置作为 XML 的替代方案:

    package com.packt.chapter1.bankapp.config; 

    import org.springframework.context.annotation.Bean; 
    import org.springframework.context.annotation.Configuration; 

    import com.packt.chapter1.bankapp.repository.AccountRepository; 
    import com.packt.chapter1.bankapp.repository.TransferRepository; 
    import 
     com.packt.chapter1.bankapp.repository.jdbc.JdbcAccountRepository; 
    import 
     com.packt.chapter1.bankapp.repository.jdbc.JdbcTransferRepository; 
    import com.packt.chapter1.bankapp.service.TransferService; 
    import com.packt.chapter1.bankapp.service.TransferServiceImpl; 

    @Configuration 
    public class AppConfig { 

     @Bean 
     public TransferService transferService(){ 
       return new TransferServiceImpl(accountRepository(),
       transferRepository()); 
     } 
     @Bean 
     public AccountRepository accountRepository() { 
       return new JdbcAccountRepository(); 
     } 
     @Bean 
     public TransferRepository transferRepository() { 
       return new JdbcTransferRepository(); 
     } 
    } 

不论是使用基于 XML 的还是基于 Java 的配置,依赖注入模式的优点都是相同的:

  • 依赖注入促进了松耦合。你可以使用最佳实践 P2I*移除硬编码的依赖项,并且你可以通过使用工厂模式和其内置的可交换和可插拔实现来从应用程序外部提供依赖项。

  • 依赖注入模式促进面向对象编程的复合设计,而不是继承编程

虽然TransferService依赖于AccountRepositoryTransferRepository,但它并不关心在应用程序中使用的是哪种类型的实现(JDBC 或 JPA)。只有 Spring,通过其配置(XML 或 Java),知道所有组件是如何结合在一起,以及如何使用 DI 模式实例化并带有所需依赖项的。DI 使得在不更改依赖类的情况下更改这些依赖项成为可能--也就是说,我们可以使用 JDBC 实现或 JPA 实现,而无需更改AccountService的实现。

在 Spring 应用程序中,应用程序上下文的一个实现(Spring 为基于 Java 的提供了AnnotationConfigApplicationContext,为基于 XML 的提供了ClassPathXmlApplicationContext)加载 bean 定义并将它们连接到 Spring 容器中。Spring 应用程序上下文在启动时创建和连接 Spring beans。查看基于 Java 配置的 Spring 应用程序上下文实现--它加载位于应用程序类路径中的 Spring 配置文件(Java 的AppConfig.java和 XML 的Spring.xml)。在下面的代码中,TransferMain类的main()方法使用AnnotationConfigApplicationContext类来加载配置类AppConfig.java并获取AccountService类的对象。

Spring 提供了基于 Java 的配置作为 XML 的替代方案:

    package com.packt.chapter1.bankapp; 

    import org.springframework.context.ConfigurableApplicationContext; 
    import 
     org.springframework.context.annotation
     .AnnotationConfigApplicationContext; 

    import com.packt.chapter1.bankapp.config.AppConfig; 
    import com.packt.chapter1.bankapp.model.Amount; 
    import com.packt.chapter1.bankapp.service.TransferService; 

    public class TransferMain { 

      public static void main(String[] args) { 
        //Load Spring context 
        ConfigurableApplicationContext applicationContext = 
          new AnnotationConfigApplicationContext(AppConfig.class); 
         //Get TransferService bean 
         TransferService transferService = 
          applicationContext.getBean(TransferService.class); 
           //Use transfer method 
         transferService.transferAmmount(100l, 200l,
          new Amount(2000.0)); 
         applicationContext.close(); 
      } 

    }    

在这里,我们对依赖注入(DI)模式进行了简要介绍。您将在本书的后续章节中了解更多关于 DI 模式的内容。现在,让我们看看另一种使用 Spring 的声明性编程模型通过方面和代理模式简化 Java 开发的方法。

应用横切关注点的方面

在 Spring 应用程序中,依赖注入(DI)模式为我们提供了协作软件组件之间的松耦合,但 Spring 中的面向切面编程(Spring AOP)使您能够捕获在整个应用程序中重复出现的常见功能。因此,我们可以这样说,Spring AOP 促进了松耦合,并允许以下列出的横切关注点以最优雅的方式分离。它允许通过声明方式透明地应用这些服务。使用 Spring AOP,您可以编写自定义方面并声明性地配置它们。

您的应用程序中许多地方都需要通用的功能:

  • 记录和跟踪

  • 事务管理

  • 安全

  • 缓存

  • 错误处理

  • 性能监控

  • 自定义业务规则

列出的组件不是您核心应用程序的一部分,但这些组件有一些额外的职责,通常被称为横切关注点,因为它们往往跨越多个组件的系统,而不仅仅是它们的内核职责。如果您将这些组件与您的核心功能放在一起,从而在不进行模块化的情况下实现横切关注点,这将有两个主要问题:

  • 代码纠缠:关注点的耦合意味着横切关注点代码,如安全关注点、事务关注点和记录关注点,与您的应用程序中业务对象的代码耦合在一起。

  • 代码分散:代码分散指的是相同关注点在模块中分散。这意味着您的安全、事务和记录的关注点代码分散在系统的所有模块中。换句话说,您可以说系统中有相同关注点代码的重复。

下面的图示说明了这种复杂性。业务对象与横切关注点过于紧密地结合在一起。不仅每个对象都知道它正在被记录、被保护并参与事务上下文,而且每个对象还负责执行仅分配给它的那些服务:

图片

横切关注点,如记录、安全和事务,通常散布在那些任务不是其主要关注点的模块中

Spring AOP 使横切关注点的模块化成为可能,以避免纠缠和分散。你可以通过声明性方式将这些模块化关注点应用于应用程序的核心业务组件,而不会影响上述组件。方面确保 POJO 保持简单。Spring AOP 通过使用代理设计模式来实现这一魔法。我们将在本书的后续章节中进一步讨论代理设计模式。

Spring AOP 是如何工作的

以下要点描述了 Spring AOP 的工作:

  • 实现你的主线应用程序逻辑:专注于核心问题意味着,当你编写应用程序业务逻辑时,你不需要担心在业务代码之间添加额外的功能,如日志记录、安全和事务——Spring AOP 会处理这些。

  • 编写方面以实现你的横切关注点:Spring 提供了许多开箱即用的方面,这意味着你可以在 Spring AOP 中以独立单元的形式编写额外的功能作为方面。这些方面作为横切关注点具有额外的责任,超出了应用程序逻辑代码。

  • 将方面织入你的应用程序:将横切行为添加到正确的位置,即在编写方面以添加额外责任之后,你可以通过声明性方式将这些方面注入到应用程序逻辑代码的正确位置。

让我们看看 Spring 中 AOP 的一个示例:

图片

基于 AOP 的系统演进——这使应用程序组件能够专注于它们特定的业务功能

在前面的图中,Spring AOP 将横切关注点(例如,安全、事务和日志记录)从业务模块(即BankServiceCustomerServiceReportingService)中分离出来。这些横切关注点在应用程序运行时应用于业务模块的预定义点(前面图中的条纹)。

假设你想使用LoggingAspect的服务在调用TransferServicetransferAmmount()方法之前和之后记录消息。以下列表显示了你可能使用的LoggingAspect类。

LoggingAspect调用用于为TransferService记录系统:

    package com.packt.chapter1.bankapp.aspect; 

    import org.aspectj.lang.annotation.After; 
    import org.aspectj.lang.annotation.Aspect; 
    import org.aspectj.lang.annotation.Before; 

    @Aspect 
    public class LoggingAspect { 

     @Before("execution(* *.transferAmount(..))") 
     public void logBeforeTransfer(){ 
       System.out.println("####LoggingAspect.logBeforeTransfer() 
       method called before transfer amount####"); 
     } 

     @After("execution(* *.transferAmount(..))") 
     public void logAfterTransfer(){ 
       System.out.println("####LoggingAspect.logAfterTransfer() method
       called after transfer amount####"); 
     } 
    } 

LoggingAspect转换为方面 bean,你只需要在 Spring 配置文件中将其声明为一种即可。此外,为了使其成为一个方面,你必须向这个类添加@Aspect注解。以下是更新后的AppConfig.java文件,已修改为声明LoggingAspect为方面。

声明LoggingAspect为方面并启用 Spring AOP 的Apsect代理功能:

    package com.packt.chapter1.bankapp.config; 

    import org.springframework.context.annotation.Bean; 
    import org.springframework.context.annotation.Configuration; 
    import
     org.springframework.context.annotation.EnableAspectJAutoProxy; 

    import com.packt.chapter1.bankapp.aspect.LoggingAspect; 
    import com.packt.chapter1.bankapp.repository.AccountRepository; 
    import com.packt.chapter1.bankapp.repository.TransferRepository; 
    import
     com.packt.chapter1.bankapp.repository.jdbc.JdbcAccountRepository; 
    import
     com.packt.chapter1.bankapp.repository.jdbc.JdbcTransferRepository; 
    import com.packt.chapter1.bankapp.service.TransferService; 
    import com.packt.chapter1.bankapp.service.TransferServiceImpl; 

    @Configuration 
    @EnableAspectJAutoProxy 
    public class AppConfig { 

      @Bean 
      public TransferService transferService(){ 
        return new TransferServiceImpl(accountRepository(),
        transferRepository()); 
      } 
      @Bean 
      public AccountRepository accountRepository() { 
        return new JdbcAccountRepository(); 
      } 
      @Bean 
      public TransferRepository transferRepository() { 
        return new JdbcTransferRepository(); 
      } 
      @Bean 
      public LoggingAspect loggingAspect() { 
        return new LoggingAspect(); 
      } 
    } 

在这里,我们使用基于 Java 的 Spring AOP 配置来声明LoggingAspect bean 作为方面。首先,我们声明LoggingAspect为一个 bean。然后,我们使用@Aspect注解标注这个 bean。

我们使用@Before注解注释LoggingAspectlogBeforeTransfer()方法,以便在执行transferAmount()之前调用此方法。这被称为前置通知。然后,我们使用@After注解注释LoggingAspect的另一个方法,声明应在transferAmount()执行之后调用logAfterTransfer()方法。这被称为后置通知

使用@EnableAspectJAutoProxy来启用应用中的 Spring AOP 功能。这个注解实际上强制你将对在 spring 配置文件中定义的一些组件应用代理。我们将在第六章中更详细地讨论 Spring AOP,使用代理和装饰器模式的 Spring 面向切面编程。现在,只需知道你已经要求 Spring 在TransferService类的transferAmount()方法之前和之后调用LoggingAspectlogBeforeTransfer()logAfterTransfer()方法。现在,从这个例子中我们可以提取出两个重要的点:

  • LoggingAspect仍然是一个 POJO(如果你忽略@Aspect注解或使用基于 XML 的配置)--它没有任何表明它应该用作方面的信息。

  • 重要的是要记住,LoggingAspect可以应用于TransferService,而无需TransferService显式调用它。实际上,TransferServiceLoggingAspect的存在一无所知。

让我们转向另一种 Spring 简化 Java 开发的方式。

应用模板模式以消除样板代码

在企业应用的一个阶段,我们看到了一些看起来像我们在同一应用中之前已经编写过的代码。这实际上是样板代码。这是我们在同一应用中反复编写以实现不同部分应用中常见需求的代码。不幸的是,Java API 中有许多地方涉及大量的样板代码。当使用 JDBC 从数据库查询数据时,可以看到一个常见的样板代码示例。如果你曾经使用过 JDBC,你可能已经编写了一些处理以下内容的代码:

  • 从连接池中检索连接

  • 创建PreparedStatement对象

  • 绑定 SQL 参数

  • 执行PreparedStatement对象

  • ResultSet对象中检索数据并填充数据容器对象

  • 释放所有数据库资源

让我们看看以下代码,它包含 Java JDBC API 的样板代码:

    public Account getAccountById(long id) { 
      Connection conn = null; 
      PreparedStatement stmt = null; 
      ResultSet rs = null; 
      try { 
        conn = dataSource.getConnection(); 
        stmt = conn.prepareStatement( 
          "select id, name, amount from " + 
          "account where id=?"); 
        stmt.setLong(1, id); 
        rs = stmt.executeQuery(); 
        Account account = null; 
        if (rs.next()) { 
          account = new Account(); 
          account.setId(rs.getLong("id")); 
          account.setName(rs.getString("name")); 
          account.setAmount(rs.getString("amount")); 
        } 
        return account; 
      } catch (SQLException e) { 
      } finally { 
          if(rs != null) { 
            try { 
              rs.close(); 
            } catch(SQLException e) {} 
          } 
          if(stmt != null) { 
            try { 
              stmt.close(); 
            } catch(SQLException e) {} 
          } 
          if(conn != null) { 
            try { 
              conn.close(); 
            } catch(SQLException e) {} 
          } 
        } 
      return null; 
    } 

在前述代码中,我们可以看到 JDBC 代码查询数据库以获取账户名称和金额。对于这个简单的任务,我们必须创建一个连接,然后创建一个语句,最后查询结果。我们还需要捕获SQLException,这是一个检查型异常,尽管如果它被抛出,你实际上能做的事情并不多。最后,我们必须清理混乱,关闭连接、语句和结果集。这也可能迫使它处理 JDBC 的异常,所以你在这里也必须捕获SQLException。这种样板代码严重影响了可重用性。

Spring JDBC 通过使用模板设计模式解决了样板代码的问题,通过从模板中移除常见代码,使得生活变得非常简单。这使得数据访问代码非常干净,并防止了诸如连接泄漏等令人烦恼的问题,因为 Spring 框架确保所有数据库资源都得到适当的释放。

Spring 中的模板设计模式

让我们看看如何在 Spring 中使用模板设计模式:

  • 定义算法的轮廓或骨架
  1. 将具体实现的细节留到以后再说。

  2. 隐藏大量的样板代码。

  • Spring 提供了许多模板类:

  • JdbcTemplate

  • JmsTemplate

  • RestTemplate

  • WebServiceTemplate

  • 大多数隐藏了低级资源管理

让我们看看之前使用 Spring 的JdbcTemplate的相同代码,以及它是如何移除样板代码的。

使用JdbcTemplates让你的代码专注于任务:

    public Account getAccountById(long id) { 
      return jdbcTemplate.queryForObject( 
        "select id, name, amoount" + 
        "from account where id=?", 
         new RowMapper<Account>() { 
           public Account mapRow(ResultSet rs, 
            int rowNum) throws SQLException { 
              account = new Account(); 
              account.setId(rs.getLong("id")); 
              account.setName(rs.getString("name")); 
              account.setAmount(rs.getString("amount")); 
              return account; 
            } 
         }, 
      id); 
    } 

如前述代码所示,与样板代码相比,这个新的getAccountById()版本要简单得多,这里的方法专注于从数据库中选择账户,而不是创建数据库连接、创建语句、执行查询、处理 SQL 异常,最后还要关闭连接。使用模板,你需要在模板的queryForObject()方法中提供 SQL 查询和一个RowMapper,用于将结果集数据映射到模板的域对象。模板负责完成这个操作的所有事情,比如数据库连接等等。它还在框架后面隐藏了大量的样板代码。

在本节中,我们看到了 Spring 如何利用面向 POJO 的开发和 DI 模式、使用代理模式的 Aspect 模式以及模板方法设计模式等模式的力量来攻击 Java 开发的复杂性。

在下一节中,我们将探讨如何使用 Spring 容器来创建和管理应用程序中的 Spring beans。

使用工厂模式通过 Spring 容器管理 beans

Spring 为我们提供了一个容器,我们的应用程序对象就生活在这个 Spring 容器中。如图所示,这个容器负责创建和管理对象:

在 Spring 应用程序中,我们的应用程序对象就生活在这个 Spring 容器中

Spring 容器还根据其配置将许多对象连接在一起。它配置了一些初始化参数,并管理它们的完整生命周期,从开始到结束。

基本上,Spring 容器有两种不同的类型:

  • Bean 工厂

  • 应用程序上下文

Bean 工厂

在 Spring 框架中,org.springframework.beans.factory.BeanFactory接口提供了 bean 工厂,这是 Spring 的 IoC 容器。XmlBeanFactory是这个接口的实现类。这个容器从 XML 文件读取配置元数据。它基于 GOF 工厂方法设计模式--以复杂的方式创建、管理、缓存和连接应用程序对象。bean 工厂只是一个对象池,其中对象由配置创建和管理。对于小型应用程序来说,这已经足够了,但企业应用程序需要更多功能,因此 Spring 提供了具有更多功能的 spring 容器版本。

在下一节中,我们将了解应用程序上下文以及 Spring 如何在应用程序中创建它。

应用程序上下文

在 Spring 框架中,org.springframework.context.ApplicationContext接口还提供了 Spring 的 IoC 容器。它只是一个 bean 工厂的包装,提供了额外的应用程序上下文服务,例如支持 AOP,因此支持声明式事务、安全性和支持国际化所需的消息资源,以及将应用程序事件发布给感兴趣的事件监听器的能力。

创建具有应用程序上下文的容器

Spring 提供了多种应用程序上下文的版本作为 bean 容器。ApplicationContext接口有多个核心实现,如下所示:

  • FileSystemXmlApplicationContext: 这个类是ApplicationContext的一个实现,它从文件系统中位于配置文件(XML)加载应用程序上下文 bean 定义。

  • ClassPathXmlApplicationContext: 这个类是ApplicationContext的一个实现,它从应用程序类路径中位于配置文件(XML)加载应用程序上下文 bean 定义。

  • AnnotationConfigApplicationContext: 这个类是ApplicationContext的一个实现,它从应用程序的类路径中的配置类(基于 Java)加载应用程序上下文 bean 定义。

Spring 为你提供了一个具有网络意识的ApplicationContext接口实现,如下所示:

  • XmlWebApplicationContext: 这个类是一个具有网络意识的ApplicationContext实现,它从包含在 Web 应用程序中的配置文件(XML)加载应用程序上下文 bean 定义。

  • AnnotationConfigWebApplicationContext: 这个类是一个具有网络意识的ApplicationContext实现,它从基于一个或多个 Java 配置类加载 Spring Web 应用程序上下文 bean 定义。

我们可以使用这些实现中的任何一个来将 bean 加载到 bean 工厂中。这取决于我们的应用程序配置文件的位置。例如,如果你想从文件系统中的特定位置加载你的配置文件spring.xml,Spring 为你提供了一个FileSystemXmlApplicationContext类,该类在文件系统中的特定位置查找配置文件spring.xml

    ApplicationContext context = new
     FileSystemXmlApplicationContext("d:/spring.xml"); 

同样,你也可以通过使用 Spring 提供的ClassPathXmlApplicationContext类从你的应用程序的类路径中加载你的应用程序配置文件spring.xml。它会在类路径的任何位置(包括 JAR 文件)查找配置文件spring.xml

    ApplicationContext context = new 
     ClassPathXmlApplicationContext("spring.xml"); 

如果你使用的是 Java 配置而不是 XML 配置,你可以使用AnnotationConfigApplicationContext

    ApplicationContext context = new 
     AnnotationConfigApplicationContext(AppConfig.class); 

在加载配置文件并获取应用程序上下文后,我们可以通过调用应用程序上下文的getBean()方法从 Spring 容器中获取 bean:

    TransferService transferService = 
     context.getBean(TransferService.class); 

在下一节中,我们将了解 Spring bean 的生命周期以及 Spring 容器如何对 Spring bean 进行响应以创建和管理它。

容器中 bean 的生命

Spring 应用程序上下文使用工厂方法设计模式,根据给定的配置以正确的顺序在容器中创建 Spring bean。因此,Spring 容器有责任管理 bean 的生命周期,从创建到销毁。在正常的 Java 应用程序中,Java 的new关键字用于实例化 bean,并且它就绪可以使用。一旦 bean 不再使用,它就有资格进行垃圾回收。但在 Spring 容器中,bean 的生命周期更为复杂。以下图像显示了典型 Spring bean 的生命周期:

图片

Spring 容器中 Spring bean 的生命周期如下:

  1. 加载所有 bean 定义,创建一个有序图。

  2. 实例化和运行BeanFactoryPostProcessors(你可以在这里更新 bean 定义)。

  3. 实例化每个 bean。

  4. Spring 将值和 bean 引用注入到 bean 的属性中。

  5. 如果任何 bean 实现了它,Spring 会将 bean 的 ID 传递给BeanNameAware接口的setBeanName()方法。

  6. 如果任何 bean 实现了它,Spring 会将 bean 工厂本身的引用传递给BeanFactoryAwaresetBeanFactory()方法。

  7. 如果任何 bean 实现了它,Spring 会将应用程序上下文本身的引用传递给ApplicationContextAwaresetApplicationContext()方法。

  8. BeanPostProcessor是一个接口,Spring 允许你使用你的 bean 实现它,并在 Spring bean 容器中初始化器调用之前通过调用其postProcessBeforeInitialization()方法修改 bean 的实例。

  9. 如果您的 bean 实现了InitializingBean接口,Spring 将调用其afterPropertiesSet()方法来初始化应用程序的任何过程或加载资源。这取决于您指定的初始化方法。还有其他方法可以实现这一步骤,例如,您可以使用<bean>标签的init-method@Bean注解的initMethod属性,以及 JSR 250 的@PostConstruct注解。

  10. BeanPostProcessor是一个接口,Spring 允许您使用自己的 bean 来实现它。它通过调用其postProcessAfterInitialization()方法,在 Spring bean 容器中初始化器被调用后修改 bean 的实例。

  11. 现在您的 bean 已经准备好在步骤中使用,并且您的应用程序可以通过使用应用程序上下文的getBean()方法来访问这个 bean。您的 bean 在应用程序上下文中保持活跃,直到通过调用应用程序上下文的close()方法来关闭它。

  12. 如果您的 bean 实现了DisposibleBean接口,Spring 将调用其destroy()方法来销毁应用程序的任何过程或清理资源。还有其他方法可以实现这一步骤--例如,您可以使用<bean>标签的destroy-method@Bean注解的destroyMethod属性,以及 JSR 250 的@PreDestroy注解。

  13. 这些步骤展示了 Spring 容器中 Spring bean 的生命周期。

  14. 下一个部分将描述 Spring 框架提供的模块。

Spring 模块

Spring 框架为特定的一组功能提供了几个不同的模块,并且它们在某种程度上相互独立。这个系统非常灵活,因此开发者可以选择仅用于企业应用程序的模块。例如,开发者可以使用 Spring DI 模块,并用非 Spring 组件构建应用程序的其余部分。因此,Spring 提供了与其他框架和 API 一起工作的集成点--例如,您只能使用 Spring Core DI 模式与 Struts 应用程序一起使用。如果开发团队更擅长使用 Struts,那么在应用程序的其他部分使用 Spring 组件和功能(如 JDBC 和事务)的同时,可以使用 Struts MVC 而不是 Spring MVC。因此,虽然开发人员需要与 Struts 应用程序一起部署所需的依赖项,但无需添加整个 Spring 框架。

下面是整个模块结构的概述:

图片

Spring 框架的各个模块

让我们看看 Spring 的每个模块,并看看它们如何融入更大的图景。

核心 Spring 容器

Spring 框架的这个模块使用了大量的设计模式,例如工厂方法设计模式、DI 模式、抽象工厂设计模式、单例设计模式、原型设计模式等。所有其他 Spring 模块都依赖于此模块。当您配置应用程序时,您会隐式地使用这些类。它也被称为 IoC 容器,是 Spring 对依赖注入支持的核心,它管理着 Spring 应用程序中 bean 的创建、配置和管理。您可以通过使用 BeanFactory 的实现或 ApplicationContext 的实现来创建 Spring 容器。此模块包含 Spring bean 工厂,这是 Spring 提供 DI 的部分。

Spring 的 AOP 模块

Spring AOP 是一个基于 Java 的 AOP 框架,集成了 AspectJ。它使用动态代理进行方面织入,并专注于使用 AOP 解决企业级问题。本模块基于代理和装饰器设计模式。此模块使跨切面关注点的模块化成为可能,以避免纠缠和消除散布。类似于 DI,它支持核心业务服务和跨切面之间的松耦合。您可以在应用程序中声明式地实现自定义方面并配置它们,而不会影响业务对象的代码。它在代码中提供了很大的灵活性;您可以在不触及业务对象代码的情况下删除或更改方面逻辑。这是 Spring 框架中非常重要的一个模块,因此我将在本书的第六章,“使用代理和装饰器模式的 Spring 面向切面编程”中详细讨论。

Spring DAO - 数据访问和集成

Spring DAO 和 Spring JDBC 通过使用模板来移除通用代码,使得生活变得非常简单。这些模板实现了 GOF 模板方法设计模式,并提供了合适的扩展点以插入自定义代码。如果您正在使用传统的 JDBC 应用程序,您必须编写大量的样板代码,例如创建数据库连接、创建语句、查找结果集、处理 SQLException,最后关闭连接。如果您正在使用具有 DAO 层的 Spring JDBC 框架,那么您不需要编写样板代码,这与传统的 JDBC 应用程序不同。这意味着 Spring 允许您保持应用程序代码的清洁和简单。

Spring 的 ORM

Spring 还为 ORM 解决方案提供支持,并为 ORM 工具提供集成,以便在关系数据库中轻松持久化 POJO 对象。此模块实际上为 Spring DAO 模块提供了一个扩展。类似于基于 JDBC 的模板,Spring 为与领先的 ORM 产品(如 Hibernate、JPA、OpenJPA、TopLink、iBATIS 等)一起工作提供了 ORM 模板。

Spring web MVC

Spring 为企业级 Web 应用程序提供了一个 Web 和远程模块。此模块有助于构建高度灵活的 Web 应用程序,充分利用 Spring IOC 容器的全部优势。此 Spring 模块使用 MVC 架构模式、前端控制器模式和 DispatcherServlet 模式等模式,并且与 servlet API 无缝集成。Spring Web 模块非常可插拔和灵活。我们可以添加任何视图技术,如 JSP、FreeMarker、Velocity 等。我们还可以通过 spring IOC 和 DI 与其他框架(如 Struts、Webwork 和 JSF)集成。

Spring Framework 5.0 的新特性

Spring 5.0 是 Spring 可用的新鲜版本。Spring 5.0 中有很多令人兴奋的新特性,包括以下内容:

  • 支持 JDK 8 + 9 和 Java EE 7 基线:

Spring 5 支持最低 Java 8 要求,因为整个框架代码库都是基于 Java 8 的。

Spring 框架要求至少 Java EE 7 才能运行 Spring Framework 5.0 应用程序。这意味着它需要 Servlet 3.1、JMS 2.0、JPA 2.1。

  • 弃用和移除的包、类和方法:

在 Spring 5.0 中,一些包已被移除或弃用。spring-aspects 模块中有一个名为 mock.static 的包已被移除,因此不再支持 AnnotationDrivenStaticEntityMockingControl

web.view.tiles2orm.hibernate3/hibernate4 等包也已在 Spring 5.0 中被移除。现在,在最新的 Spring 框架中,使用 Tiles 3 和 Hibernate 5。

Spring 5.0 框架不再支持 Portlet、Velocity、JasperReports、XMLBeans、JDO、Guava(等等)。

Spring 5.0 已经移除了 Spring 早期版本中的一些弃用类和方法。

  • 添加新的反应式编程模型:

这种编程模型已经在 Spring 5.0 框架中引入。让我们看看以下关于反应式编程模型的列表。

Spring 5 引入了 Spring-core 模块的 DataBuffer 和具有非阻塞语义的编码器/解码器抽象到反应式编程模型中。

使用反应式模型,Spring 5.0 为 HTTP 消息编解码器实现提供了 Spring-web 模块,支持 JSONJackson)和 XMLJAXB)。

Spring 反应式编程模型添加了一个新的 spring-web-reactive 模块,为 @Controller 编程模型提供了反应式支持,将反应式流适配到 Servlet 3.1 容器,以及非 Servlet 运行时,如 Netty 和 Undertow。

Spring 5.0 还引入了一个新的 WebClient,在客户端提供反应式支持以访问服务。

如此列出,您可以看到 Spring 框架 5.0 中有很多令人兴奋的新特性和增强。因此,在这本书中,我们将通过示例和它们采用的设计模式来探讨这些新特性。

摘要

在阅读完本章之后,你现在应该对 Spring 框架及其最常用的设计模式有一个良好的概述。我指出了 J2EE 传统应用中存在的问题,以及 Spring 如何通过使用大量设计模式和良好实践来解决问题并简化 Java 开发,创建一个应用。Spring 的目标是使企业级 Java 开发更加容易,并促进松耦合代码。我们还讨论了 Spring AOP 用于解决横切关注点,以及 DI 模式用于与松耦合和可插拔的 Spring 组件一起使用,这样对象就不需要知道它们的依赖从何而来或如何实现。Spring 框架是最佳实践和有效对象设计的推动者。Spring 框架有两个重要特性——首先,它有一个 Spring 容器来创建和管理 Bean 的生命周期;其次,它为几个模块和集成提供支持,以帮助简化 Java 开发。

第二章:GOF 设计模式概述 - 核心设计模式

在本章中,您将获得 GOF 设计模式的概述,包括一些制作应用程序设计的最佳实践。您还将了解使用设计模式进行常见问题解决的方法。

我将解释 Spring 框架常用的设计模式,以实现更好的设计和架构。我们都在一个全球化的世界中,这意味着如果我们有市场上的服务,它们可以在全球范围内访问。简单来说,现在是分布式计算系统的时代。所以首先,什么是分布式系统?它是一个被分成更小部分的应用程序,这些部分在不同的计算机上同时运行,并且这些小部分通过网络进行通信,通常使用协议。这些小部分被称为。所以如果我们想创建一个分布式应用程序,n-层架构是那种类型应用程序的更好选择。但是开发n-层分布式应用程序是一项复杂且具有挑战性的工作。将处理分配到单独的层可以导致更好的资源利用。它还支持将任务分配给最适合在该层工作和开发的专业人士。在开发分布式应用程序中存在许多挑战,其中一些在此详细说明:

  • 层之间的集成

  • 事务管理

  • 企业数据并发处理

  • 应用程序的安全性等

因此,我在这本书中的重点是通过对 Spring 框架应用模式和最佳实践来简化 Java EE 应用程序的设计和开发。在这本书中,我将涵盖一些常见的 GOF 设计模式,以及 Spring 如何采用这些模式为上述企业应用程序问题提供最佳解决方案,因为分布式对象的设计对于经验丰富的专业人士来说也是一个极其复杂的工作。在制定最终解决方案之前,您需要考虑关键问题,如可伸缩性、性能、事务等。该解决方案被描述为一种模式。

本章结束时,您将了解设计模式如何提供最佳解决方案来解决任何与设计相关和开发相关的问题,以及如何以最佳实践开始开发。在这里,您将获得更多关于 GOF 设计模式的想法,以及实际生活中的例子。您将了解 Spring 框架如何内部实现这些设计模式以提供最佳的企业解决方案。

本章将涵盖以下要点:

  • 介绍设计模式的力量

  • 常见 GOF 设计模式概述

    • 核心设计模式

      • 创建型设计模式

      • 结构化设计模式

      • 行为设计模式

    • J2EE 设计模式

      • 展示层的设计模式

      • 业务层的设计模式

      • 集成层的设计模式

  • Spring 应用程序开发的最佳实践

介绍设计模式的力量

那么什么是设计模式呢?实际上,设计模式这个短语与任何编程语言都没有关联,它也不提供针对特定语言的解决方案。设计模式与重复性问题的解决方案相关。例如,如果任何问题频繁发生,那么针对该问题的解决方案已经被有效地使用过。任何不可重用的解决方案都不能被视为模式,但问题必须频繁发生,才能有可重用的解决方案,才能被视为模式。因此,设计模式是描述软件设计中常见问题重复解决方案的软件工程概念。设计模式还代表了经验丰富的面向对象软件开发者所使用的最佳实践。

当你为应用程序进行设计时,你应该考虑所有常见问题的解决方案,这些解决方案被称为设计模式。设计模式的理解必须在开发团队中良好,以便员工能够有效地进行沟通。实际上,你可能熟悉一些设计模式;然而,你可能没有使用众所周知的名称来描述它们。这本书将带你通过逐步的方法,并在学习设计模式概念的同时,展示使用 Java 的示例。

设计模式有三个主要特征:

  • 设计模式是特定于特定场景的,而不是特定于某个平台。因此,其上下文是问题存在的周围条件。上下文必须在模式中予以记录。

  • 设计模式已经发展成提供针对软件开发过程中遇到的一些问题的最佳解决方案。因此,这应该限制在考虑的上下文中。

  • 设计模式是针对考虑中的问题的解决方案

例如,如果一个开发者正在引用 GOF 单例设计模式并表明使用单个对象,那么所有涉及的开发者都应该理解你需要设计一个在应用程序中只会有一个实例的对象。因此,单例设计模式将由一个对象组成,开发者可以相互告知程序正在遵循单例模式。

常见 GoF 设计模式概述

作者 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 通常被称为 GoF,或四人帮。他们出版了一本名为《设计模式:可复用面向对象软件元素》的书,这标志着软件开发中设计模式概念的诞生。

在本章中,你将学习什么是 GOF 模式以及它们如何帮助解决面向对象设计中遇到的常见问题。

四人帮GoF)模式是 23 个经典的软件设计模式,为软件设计中的常见问题提供重复的解决方案。这些模式在书籍《设计模式:可复用面向对象软件元素》中定义。这些模式分为两大类:

  • 核心设计模式

  • J2EE 设计模式

此外,核心设计模式也被细分为三个主要的设计模式类别,如下所示:

  • 创建型设计模式:这个类别下的模式提供了一种在构造函数无法满足需求时构建对象的方法。对象的创建逻辑是隐藏的。基于这些模式的程序在根据您的需求和应用程序的使用案例决定对象创建方面更加灵活。

  • 结构型设计模式:这个类别下的模式处理类或对象的组合。在企业应用中,有两种常用的技术在面向对象系统中重用功能:一种是类继承,另一种是继承的对象组合概念。继承的对象组合概念用于组合接口,并定义组合对象以获得新功能的方法。

  • 行为设计模式:这个类别下的模式描述了类或对象之间交互和分配责任的方式。这些设计模式特别关注对象之间的通信。行为设计模式用于控制并简化企业应用中的复杂应用流程。

现在,让我们看看另一个类别,即JEE 设计模式。这是设计模式的另一个主要类别。通过应用 Java EE 设计模式,可以极大地简化应用设计。Java EE 设计模式已在 Sun 的 Java Blueprints 中进行了记录。这些 Java EE 设计模式提供了经过时间考验的解决方案指南和最佳实践,用于 Java EE 应用程序不同层中的对象交互。这些设计模式特别关注以下列出的层:

  • 呈现层的设计模式

  • 业务层的设计模式

  • 集成层的设计模式

让我们在下一节中探索创建型设计模式。

创建型设计模式

让我们看看这个类别背后的设计模式,以及 Spring 框架如何采用它们来提供组件之间的松耦合,并创建和管理 Spring 组件的生命周期。创建型设计模式与对象创建的方法相关联。对象的创建逻辑对调用者来说是隐藏的。

我们都知道如何在 Java 中使用new关键字创建对象,如下所示:

     Account account = new Account(); 

但这种方法在某些情况下并不适用,因为它是一种硬编码创建对象的方式。根据程序的性质,创建对象也不是最佳实践。在这里,创建型设计模式提供了根据程序性质创建对象的灵活性。

现在,让我们看看这个类别下的不同设计模式。

工厂设计模式

定义一个用于创建对象的接口,但让子类决定要实例化的类。工厂方法允许一个类将实例化推迟到子类。

  • GOF 设计模式

工厂设计模式是一种创建型设计模式。工厂设计模式也被称为工厂方法设计模式。根据这种设计模式,你得到一个类的对象,而不向客户端暴露底层逻辑。它通过使用公共接口或抽象类为新调用者分配一个新对象。这意味着设计模式隐藏了对象实现的实际逻辑,如何创建它,以及在哪里实例化它。因此,客户端不需要担心创建、管理和销毁对象——工厂模式负责这些任务。工厂模式是 Java 中最常用的设计模式之一。

让我们看看工厂模式的优点:

  • 工厂模式通过使用接口而不是将特定于应用程序的类绑定到应用程序代码中,促进了协作组件或类之间的松耦合。

  • 使用这种模式,你可以在运行时获取实现接口的类的对象。

  • 对象的生命周期由该模式实现的工厂管理。

现在,让我们讨论一些应该应用工厂设计模式的常见问题:

  • 这种模式减轻了开发者创建和管理对象的负担。

  • 这种模式消除了协作组件之间的紧密耦合,因为一个组件不知道它将需要创建哪些子类。

  • 避免硬编码创建类的对象。

在 Spring 框架中实现工厂设计模式

Spring 框架透明地使用这种工厂设计模式,通过 BeanFactoryApplicationContext 接口来实现 Spring 容器。Spring 的容器基于工厂模式创建 Spring 应用程序中的 Spring bean,并管理每个 Spring bean 的生命周期。BeanFactoryApplicationContext 是工厂接口,Spring 有很多实现类。getBean() 方法是工厂方法,它根据需要提供 Spring bean。

让我们看看工厂设计模式的示例实现。

工厂设计模式的示例实现

有两个类 SavingAccountCurrentAccount 实现了一个 Account 接口。因此,您可以创建一个 Factory 类,该类有一个方法,该方法接受一个或多个参数,其返回类型是 Account。这个方法被称为工厂方法,因为它创建了 CurrentAccountSavingAccount 的实例。Account 接口用于松耦合。因此,根据工厂方法中传递的参数,它选择实例化哪个子类。这个工厂方法将具有其超类作为其返回类型:

图片

工厂设计模式的 UML 图

让我们通过以下示例来查看这个设计模式。在这里,我将创建一个 Account 接口和一些实现 Account 接口的具体类:

    package com.packt.patterninspring.chapter2.factory;
    public interface Account { 
      void accountType(); 
   } 

现在让我们创建 SavingAccount.java,它将实现 Account 接口:

    package com.packt.patterninspring.chapter2.factory; 
    public class SavingAccount implements Account{ 
      @Override 
      public void accountType() { 
         System.out.println("SAVING ACCOUNT"); 
      } 
    } 

CurrentAccount.java 相同,它也将实现 Account 接口:

    package com.packt.patterninspring.chapter2.factory; 
    public class CurrentAccount implements Account { 
      @Override 
      public void accountType() { 
         System.out.println("CURRENT ACCOUNT"); 
      } 
    } 

现在将定义一个 AccountFactory 工厂类。AccountFactory 根据作为工厂方法参数给出的账户类型生成具体类的对象,无论是 SavingAccount 还是 CurrentAccount

AccountFactory.java 是一个用于生产 Account 类型对象的工厂:

    package com.packt.patterninspring.chapter2.factory.pattern; 
    import com.packt.patterninspring.chapter2.factory.Account; 
    import com.packt.patterninspring.chapter2.factory.CurrentAccount; 
    import com.packt.patterninspring.chapter2.factory.SavingAccount; 
    public class AccountFactory { 
      final String CURRENT_ACCOUNT = "CURRENT"; 
      final String SAVING_ACCOUNT  = "SAVING"; 
      //use getAccount method to get object of type Account    
      //It is factory method for object of type Account 
      public Account getAccount(String accountType){   
         if(CURRENT_ACCOUNT.equals(accountType)) {   
               return new CurrentAccount();   
         }
         else if(SAVING_ACCOUNT.equals(accountType)){   
               return new SavingAccount();   
         }    
         return null;   
      }   
    } 

FactoryPatternMainAccountFactory 的主调用类,用于获取 Account 对象。它将传递一个参数给工厂方法,该参数包含账户类型的信息,例如 SAVINGCURRENTAccountFactory 返回您传递给工厂方法的类型对象。

让我们创建一个演示类 FactoryPatterMain.java 来测试工厂方法设计模式:

    package com.packt.patterninspring.chapter2.factory.pattern; 
    import com.packt.patterninspring.chapter2.factory.Account; 
    public class FactoryPatterMain { 
      public static void main(String[] args) { 
         AccountFactory accountFactory = new AccountFactory(); 
         //get an object of SavingAccount and call its accountType()
         method. 
         Account savingAccount = accountFactory.getAccount("SAVING"); 
         //call accountType method of SavingAccount 
         savingAccount.accountType(); 
         //get an object of CurrentAccount and call its accountType() 
         method. 
         Account currentAccount = accountFactory.getAccount("CURRENT"); 
         //call accountType method of CurrentAccount 
         currentAccount.accountType(); 
      } 
    } 

您可以测试此文件,并在控制台上查看输出,它应该看起来像这样:

图片

现在我们已经看到了工厂设计模式,让我们转向它的一个不同变体——抽象工厂设计模式。

抽象工厂设计模式

提供一个接口,用于创建相关或依赖对象家族,而不指定它们的具体类 - GOF 设计模式

抽象工厂模式属于创建型设计模式。与工厂方法设计模式相比,这是一个高级设计模式。根据这个设计模式,您只需定义一个接口或抽象类来创建一个相关依赖对象,而不指定其具体子类。因此,在这里,抽象工厂返回一个类工厂。让我为您简化一下。您有一组工厂方法设计模式,您只需将这些工厂通过工厂设计模式放在一个工厂下,这意味着它只是一个工厂的工厂。而且没有必要将所有工厂的知识带入工厂中——您可以使用顶级工厂来编写程序。

在抽象工厂模式中,一个接口负责创建相关对象的工厂,而不明确指定它们的类。每个生成的工厂都可以按照工厂模式提供对象。

抽象工厂模式的优点如下:

  • 抽象工厂设计模式提供了组件家族之间的松耦合。它还隔离了客户端代码与具体类。

  • 这种设计模式比工厂模式(Factory pattern)更高级。

  • 这种模式在对象构建时间上提供了更好的跨应用程序一致性。

  • 这种模式可以轻松地交换组件家族。

应该应用抽象工厂设计模式的常见问题

当你在应用程序中设计用于对象创建的工厂模式时,有时你希望一组相关的对象以特定的约束条件被创建,并在应用程序中跨相关对象应用所需的逻辑。你可以通过在工厂内部为相关对象集创建另一个工厂来实现这种设计,并应用所需的约束。你也可以将逻辑编程到一组相关对象中。

当你想定制相关对象的实例化逻辑时,可以使用这种设计模式。

在 Spring 框架中实现抽象工厂设计模式

在 Spring 框架中,FactoryBean接口基于抽象工厂设计模式。Spring 提供了大量的该接口实现,例如ProxyFactoryBeanJndiFactoryBeanLocalSessionFactoryBeanLocalContainerEntityManagerFactoryBean等。FactoryBean也有助于 Spring 构建它自己难以构建的对象。通常这用于构建具有许多依赖关系的复杂对象。当构建逻辑本身高度动态且依赖于配置时,也可能被使用。

例如,在 Spring 框架中,FactoryBean的一个实现是LocalSessionFactoryBean,它用于获取与 hibernate 配置关联的 bean 的引用。这是一个关于数据源的具体配置。在获取SessionFactory对象之前应该应用它。你可以使用LocalSessionFactoryBean以一致的方式应用特定的数据源配置。你还可以将 FactoryBean 的getObject()方法的结果注入到任何其他属性中。

让我们创建一个抽象工厂设计模式的示例实现。

抽象工厂设计模式的示例实现

我将创建一个BankAccount接口以及实现这些接口的一些具体类。在这里,我还创建了一个抽象工厂类AbstractFactory。我有一些工厂类,BankFactoryAccountFactory;这些类扩展了AbstractFactory类。我还会创建一个FactoryProducer类来创建工厂。

让我们通过以下图像看看这个设计模式:

抽象工厂设计模式的 UML 图

创建一个演示类 AbstractFactoryPatternMain;它使用 FactoryProducer 来获取 AbstractFactory 对象。在这里,我传递了 ICICIYES 等信息给 AbstractFactory 以获取 Bank 对象,我还传递了 SAVINGCURRENT 等信息给 AbstractFactory 以获取 Account 类型。

这里是 Bank.java 的代码,它是一个接口:

    package com.packt.patterninspring.chapter2.model; 
    public interface Bank { 
      void bankName(); 
    } 

现在让我们创建 ICICIBank.java,它实现了 Bank 接口:

    package com.packt.patterninspring.chapter2.model; 
    public class ICICIBank implements Bank { 
      @Override 
      public void bankName() { 
        System.out.println("ICICI Bank Ltd."); 
      } 
    } 

让我们再创建一个 YesBank.java,一个实现 Bank 接口的类:

    package com.packt.patterninspring.chapter2.model; 
    public class YesBank implements Bank{ 
      @Override 
      public void bankName() { 
         System.out.println("Yes Bank Pvt. Ltd."); 
      } 
   } 

在这个例子中,我使用了与本书中工厂模式示例相同的 Account 接口和实现类。

AbstractFactory.java 是一个抽象类,用于获取 BankAccount 对象的工厂:

    package com.packt.patterninspring.chapter2.abstractfactory.pattern; 
    import com.packt.patterninspring.chapter2.model.Account; 
    import com.packt.patterninspring.chapter2.model.Bank; 
    public abstract class AbstractFactory { 
      abstract Bank getBank(String bankName); 
      abstract Account getAccount(String accountType); 
    } 

BankFactory.java 是一个工厂类,它扩展了 AbstractFactory,根据给定信息生成具体类的对象:

    package com.packt.patterninspring.chapter2.abstractfactory.pattern; 
    import com.packt.patterninspring.chapter2.model.Account; 
    import com.packt.patterninspring.chapter2.model.Bank; 
    import com.packt.patterninspring.chapter2.model.ICICIBank; 
    import com.packt.patterninspring.chapter2.model.YesBank; 
    public class BankFactory extends AbstractFactory { 
      final String ICICI_BANK = "ICICI"; 
      final String YES_BANK   = "YES"; 
      //use getBank method to get object of name bank    
      //It is factory method for object of name bank 
      @Override 
      Bank getBank(String bankName) { 
         if(ICICI_BANK.equalsIgnoreCase(bankName)){   
               return new ICICIBank();   
         } 
         else if(YES_BANK.equalsIgnoreCase(bankName)){   
               return new YesBank();   
         }   
         return null; 
      } 
      @Override 
      Account getAccount(String accountType) { 
         return null; 
      } 
    } 

AccountFactory.java 是一个工厂类,它扩展了 AbstractFactory.java,根据给定信息生成具体类的对象:

     package com.packt.patterninspring.chapter2.abstractfactory.pattern; 
     import com.packt.patterninspring.chapter2.model.Account; 
     import com.packt.patterninspring.chapter2.model.Bank; 
     import com.packt.patterninspring.chapter2.model.CurrentAccount; 
     import com.packt.patterninspring.chapter2.model.SavingAccount; 
     public class AccountFactory extends AbstractFactory { 
       final String CURRENT_ACCOUNT = "CURRENT"; 
       final String SAVING_ACCOUNT  = "SAVING"; 
       @Override 
       Bank getBank(String bankName) { 
          return null; 
      } 
      //use getAccount method to get object of type Account    
      //It is factory method for object of type Account 
      @Override 
      public Account getAccount(String accountType){   
        if(CURRENT_ACCOUNT.equals(accountType)) {   
               return new CurrentAccount();   
        }
        else if(SAVING_ACCOUNT.equals(accountType)){   
               return new SavingAccount();   
        }    
        return null;   
      } 
    } 

FactoryProducer.java 是一个类,它创建一个工厂生成器类,通过传递一些信息(如 BankAccount)来获取工厂:

    package com.packt.patterninspring.chapter2.abstractfactory.pattern; 
    public class FactoryProducer { 
      final static String BANK    = "BANK"; 
      final static String ACCOUNT = "ACCOUNT"; 
      public static AbstractFactory getFactory(String factory){ 
         if(BANK.equalsIgnoreCase(factory)){ 
               return new BankFactory(); 
         }
         else if(ACCOUNT.equalsIgnoreCase(factory)){ 
               return new AccountFactory(); 
         } 
         return null; 
       } 
    } 

FactoryPatterMain.java 是抽象工厂设计模式的一个演示类。FactoryProducer 是一个类,用于获取 AbstractFactory,以便通过传递一些信息(如类型)来获取具体类的工厂:

    package com.packt.patterninspring.chapter2.factory.pattern; 
    import com.packt.patterninspring.chapter2.model.Account; 
    public class FactoryPatterMain { 
      public static void main(String[] args) { 
         AccountFactory accountFactory = new AccountFactory(); 
         //get an object of SavingAccount and call its accountType() 
         method. 
         Account savingAccount = accountFactory.getAccount("SAVING"); 
         //call accountType method of SavingAccount 
         savingAccount.accountType(); 
         //get an object of CurrentAccount and call its accountType() 
         method. 
         Account currentAccount = accountFactory.getAccount("CURRENT"); 
         //call accountType method of CurrentAccount 
         currentAccount.accountType(); 
      } 
    } 

你可以通过测试这个文件并在控制台上查看输出来测试这个文件:

现在我们已经看到了抽象工厂设计模式,让我们转向它的一个不同变体--单例设计模式。

单例设计模式

确保一个类只有一个实例,并提供一个全局访问点 - GOF 设计模式

单例模式是一个创建型设计模式,它是 Java 中最简单的模式之一。根据单例设计模式,类为每个调用提供相同的单个对象--也就是说,它限制了一个类的实例化只能有一个对象,并为该类提供了一个全局访问点。因此,该类负责创建对象,并确保对于每个客户端对该对象的调用,只创建一个对象。这个类不允许直接实例化这个类的对象。它允许你只能通过一个公开的静态方法来获取对象实例。

当系统需要精确地一个对象来协调操作时,这很有用。你可以使用两种形式创建一个单例模式,如下所示:

  • 早期实例化:在加载时创建实例

  • 延迟实例化:在需要时创建实例

单例模式的优点:

  • 它提供了对关键(通常是重量级对象)类的控制器访问,例如数据库的连接类和 Hibernate 中的 SessionFactory

  • 它节省了大量内存

  • 它是多线程环境中的一个非常高效的设计

  • 它更加灵活,因为类控制了实例化过程,并且类有改变实例化过程的灵活性

  • 它具有低延迟

应该应用单例模式的常见问题

单例模式只解决一个问题——如果你有一个只能有一个实例的资源,并且你需要管理这个单例,那么你需要一个单例。通常,如果你想在分布式和多线程环境中使用给定配置创建数据库连接,如果不遵循单例设计,那么每个线程可能会创建一个具有不同配置对象的新的数据库连接。使用单例模式,系统中的每个线程都将获得具有相同配置对象的相同数据库连接对象。它主要用于多线程和数据库应用程序。它用于日志记录、缓存、线程池、配置设置等。

Spring 框架中的单例设计模式实现

Spring 框架提供了一个单例作用域的 bean 作为单例模式。它与单例模式类似,但并不完全等同于 Java 中的单例模式。根据单例模式,Spring 框架中的作用域 bean 表示每个容器和每个 bean 的单个 bean 实例。如果你在单个 Spring 容器中为特定类定义了一个 bean,那么 Spring 容器将创建一个由该 bean 定义指定的类的单个实例。

让我们创建一个单例设计模式的示例应用程序。

单例设计模式的示例实现

在下面的代码示例中,我将创建一个带有创建该类实例(如果不存在)的方法的类。如果实例已经存在,它将简单地返回该对象的引用。我还考虑了线程安全性,因此在创建该类的对象之前,我使用了同步块。

让我们看看单例设计模式的 UML 图:

    package com.packt.patterninspring.chapter2.singleton.pattern; 
    public class SingletonClass { 
      private static SingletonClass instance = null; 
      private SingletonClass() { 
      } 
      public static SingletonClass getInstance() { 
        if (instance == null) { 
          synchronized(SingletonClass.class){   
               if (instance == null) { 
                  instance = new SingletonClass(); 
               } 
          } 
        } 
       return instance; 
      } 
    } 
  } 

在前面的代码中需要注意的一点是,我编写了一个 SingletonClass 类的私有构造函数,以确保无法创建该类的对象。这个例子基于懒加载初始化,这意味着程序在第一次需要时才创建实例。因此,你也可以通过立即实例化对象来提高应用程序的运行时性能。让我们看看具有立即初始化的相同 SingletonClass

    package com.packt.patterninspring.chapter2.singleton.pattern; 
    public class SingletonClass { 
      private static final SingletonClass INSTANCE = 
         new SingletonClass(); 
      private SingletonClass() {} 
      public static SingletonClass getInstance() { 
        return INSTANCE; 
      } 
    } 

现在我们已经了解了单例设计模式,让我们转向它的一个不同变体——原型设计模式。

原型设计模式

使用原型实例指定要创建的对象类型,并通过复制此原型来创建新对象 - GOF 设计模式

原型模式属于 GOF 模式在软件开发中的创建型设计模式家族。此模式通过使用对象的克隆方法来创建对象。它由一个原型实例确定。在企业应用程序中,对象的创建在创建和初始化对象的初始属性方面代价高昂。如果这种类型的对象已经在你的手中,那么你可以选择原型模式;你只需复制一个现有的类似对象,而不是创建它,这样可以节省时间。

此模式涉及实现一个原型接口,它创建当前对象的克隆。当直接创建对象代价高昂时,使用此模式。例如,假设对象是在代价高昂的数据库操作之后创建的。我们可以缓存对象,在下一个请求时返回其克隆,并在需要时更新数据库,从而减少数据库调用。

原型设计模式的优点

以下列表显示了使用原型模式的好处:

  • 使用原型模式可以减少创建耗时对象的时间

  • 此模式减少了子类的数量

  • 此模式在运行时添加和删除对象

  • 此模式动态配置应用程序的类

让我们看看原型设计模式的 UML 类结构。

UML 类结构

以下 UML 图展示了原型设计模式的所有组件:

图片

原型设计模式的 UML 图

让我们按照以下要点列出这些组件:

  • 原型:原型是一个接口。它使用克隆方法来创建此接口类型的实例。

  • 具体原型:这是实现克隆自身操作的 Prototype 接口的具体类。

  • 客户端:这是一个 调用者 类,通过调用原型接口的 clone 方法来创建一个原型接口的新对象。

让我们看看原型设计模式的示例实现。

原型设计模式的示例实现

我将创建一个抽象的 Account 类和扩展 Account 类的具体类。定义 AccountCache 类作为下一步,它将账户对象存储在 HashMap 中,并在请求时返回它们的克隆。创建一个实现 Clonable 接口的抽象类。

    package com.packt.patterninspring.chapter2.prototype.pattern;
    public abstract class Account implements Cloneable{
      abstract public void accountType();
      public Object clone() {
        Object clone = null;
        try {
          clone = super.clone();
        }
        catch (CloneNotSupportedException e) {
          e.printStackTrace();
        }
        return clone;
      }
    }

现在,让我们创建扩展前面类的具体类:

这是 CurrentAccount.java 文件:

    package com.packt.patterninspring.chapter2.prototype.pattern;
    public class CurrentAccount extends Account {
      @Override
      public void accountType() {
        System.out.println("CURRENT ACCOUNT");
      }
    }

这是 SavingAccount.java 应该看起来像的:

    package com.packt.patterninspring.chapter2.prototype.pattern;
    public class SavingAccount extends Account{
      @Override
      public void accountType() {
        System.out.println("SAVING ACCOUNT");
      }
    }

让我们在 AccountCache.java 文件中创建一个类来获取具体类:

    package com.packt.patterninspring.chapter2.prototype.pattern;
    import java.util.HashMap;
    import java.util.Map;
    public class AccountCache {
       public static Map<String, Account> accountCacheMap =
           new HashMap<>();
       static{
         Account currentAccount = new CurrentAccount();
         Account savingAccount = new SavingAccount();
         accountCacheMap.put("SAVING", savingAccount);
         accountCacheMap.put("CURRENT", currentAccount);
       }
     }

PrototypePatternMain.java 是一个演示类,我们将使用它来测试设计模式 AccountCache,通过传递一些信息,如类型,来获取 Account 对象,然后调用 clone() 方法:

    package com.packt.patterninspring.chapter2.prototype
         .pattern;
    public class PrototypePatternMain {
      public static void main(String[] args) {
        Account currentAccount = (Account) 
          AccountCache.accountCacheMap.get("CURRENT").clone();
       currentAccount.accountType();
       Account savingAccount = (Account) 
         AccountCache.accountCacheMap.get("SAVING") .clone();
       savingAccount.accountType();
     }
   }

我们已经讨论到这里,做得很好。现在让我们看看下一个设计模式。

Builder 设计模式

将复杂对象的构建与其表示分离,以便相同的构建过程可以创建不同的表示。- GOF 设计模式

Builder 设计模式用于逐步构建一个复杂对象,最终返回完整的对象。对象的创建逻辑和过程应该是通用的,这样你就可以用它来创建同一对象类型的不同具体实现。这个模式简化了复杂对象的构建,并隐藏了对象构建的细节,从而保护客户端调用代码。当你使用这个模式时,请记住你必须一步一步地构建它,这意味着你必须将对象构建过程分解成多个阶段,与像抽象工厂和工厂方法模式这样的其他模式不同,这些模式可以在一个步骤中完成对象的构建。

Builder 模式的优点:

  • 这个模式为你提供了构建和表示对象之间的完全隔离

  • 这个模式允许你在多个阶段构建对象,因此你对构建过程有更大的控制权

  • 这个模式提供了改变对象内部表示的灵活性

UML 类结构

以下 UML 图显示了 Builder 设计模式的所有组件:

Builder 设计模式的 UML 图:

  • 构建器(Builder)(AccountBuilder):这是一个用于创建 Account 对象细节的抽象类或接口。

  • 具体构建器(ConcreteBuilder):这是一个实现,通过实现 Builder 接口来构建和组装账户的细节。

  • 导演(Director):这是使用 Builder 接口构建对象。

  • 产品(Account):这代表正在构建的复杂对象。AccountBuilder 构建账户的内部表示,并定义了组装的过程。

在 Spring 框架中实现 Builder 模式

Spring 框架在一些功能中透明地实现了 Builder 设计模式。以下类基于 Spring 框架中的 Builder 设计模式:

  • 嵌入式数据库构建器(EmbeddedDatabaseBuilder)

  • AuthenticationManagerBuilder

  • UriComponentsBuilder

  • BeanDefinitionBuilder

  • MockMvcWebClientBuilder

应该应用 Builder 模式的常见问题

在企业应用程序中,你可以应用 Builder 模式,其中对象创建是通过多个步骤完成的。在每一步中,你完成一部分过程。在这个过程中,你设置一些必需的参数和一些可选的参数,在最终步骤之后,你将得到一个复杂对象。

建造者模式是一种对象创建的软件设计模式。其目的是抽象构建步骤,以便不同的步骤实现可以构建不同表示的对象。通常,建造者模式用于根据组合模式构建产品。

Builder 设计模式的示例实现

在下面的代码示例中,我将创建一个包含 AccountBuilder 作为内部类的 Account 类。AccountBuilder 类有一个创建此类实例的方法:

    package com.packt.patterninspring.chapter2.builder.pattern; 
    public class Account { 
      private String accountName; 
      private Long accountNumber; 
      private String accountHolder; 
      private double balance; 
      private String type; 
      private double interest; 
      private Account(AccountBuilder accountBuilder) { 
         super(); 
         this.accountName = accountBuilder.accountName; 
         this.accountNumber = accountBuilder.accountNumber; 
         this.accountHolder = accountBuilder.accountHolder; 
         this.balance = accountBuilder.balance; 
         this.type = accountBuilder.type; 
         this.interest = accountBuilder.interest; 
      } 
      //setters and getters 
       public static class AccountBuilder { 
         private final String accountName; 
         private final Long accountNumber; 
         private final String accountHolder; 
         private double balance; 
         private String type; 
         private double interest; 
         public AccountBuilder(String accountName, 
            String accountHolder, Long accountNumber) { 
            this.accountName = accountName; 
            this.accountHolder = accountHolder; 
            this.accountNumber = accountNumber; 
         } 
         public AccountBuilder balance(double balance) { 
            this.balance = balance; 
            return this; 
         } 
         public AccountBuilder type(String type) { 
            this.type = type; 
            return this; 
         } 
         public AccountBuilder interest(double interest) { 
            this.interest = interest; 
            return this; 
         } 
         public Account build() { 
            Account user =  new Account(this); 
            return user; 
         } 
       } 
       public String toString() { 
       return "Account [accountName=" + accountName + ", 
          accountNumber=" + accountNumber + ", accountHolder=" 
          + accountHolder + ", balance=" + balance + ", type="
          + type + ", interest=" + interest + "]"; 
       } 
    } 

AccountBuilderTest.java 是一个演示类,我们将用它来测试设计模式。让我们看看如何通过向对象传递初始信息来构建一个 Account 对象:

     package com.packt.patterninspring.chapter2.builder.pattern; 
     public class AccountBuilderTest { 
       public static void main(String[] args) { 
         Account account = new Account.AccountBuilder("Saving
            Account", "Dinesh Rajput", 1111l) 
              .balance(38458.32) 
              .interest(4.5) 
              .type("SAVING") 
              .build(); 
         System.out.println(account); 
       } 
     } 

你可以测试这个文件,并在控制台上查看输出:

图片

现在,我们已经看到了 Builder 设计模式。在即将到来的 第三章,考虑结构和行为模式,我将探索 GOF 设计模式家族的另一个部分。

摘要

在阅读本章之后,读者现在应该对 GOF 创建型设计模式的概述及其最佳实践有一个很好的了解。我强调了不使用设计模式在企业级应用开发中产生的问题,以及 Spring 如何通过使用创建型设计模式和应用程序中的良好实践来解决这些问题。在本章中,我只提到了 GOF 设计模式的三个主要类别之一——创建型设计模式类别。创建型设计模式用于创建对象实例,并在企业应用中通过工厂、抽象工厂、建造者、原型和单例模式以特定方式在创建时间施加约束。在下一章中,我们将探讨 GOF 设计模式的另外两个类别——结构型设计模式和行为型设计模式。结构型设计模式通过处理类或对象的组合来设计企业应用的结构,从而降低应用复杂性,提高应用的复用性和性能。适配器模式、桥接模式、组合模式、装饰器模式、外观模式和享元模式都属于这个模式类别。行为型设计模式描述了类或对象之间交互和分配责任的方式。属于这个类别的模式特别关注对象之间的通信。让我们在下一章中继续完成剩余的 GOF 模式。

第三章:结构和行为模式的考虑

您已经在第二章中看到了 GOF 模式家族的创建型设计模式的实现和示例,GOF 设计模式概述 - 核心设计模式。现在,在本章中,您将获得 GOF 设计模式其他部分的概述,它们是结构和行为设计模式,包括一些应用设计的最佳实践。您还将了解如何使用这些设计模式解决常见的问题。

在本章结束时,您将了解这些设计模式如何提供最佳解决方案来解决对象组合以及应用中工作对象之间责任委派的设计和开发相关的问题。您将了解 Spring 框架如何内部实现结构和行为设计模式以提供最佳企业解决方案。

本章将涵盖以下要点:

  • 实现结构设计模式

  • 实现行为设计模式

  • J2EE 设计模式

检查核心设计模式

让我们继续我们的核心设计模式之旅:

  • 结构设计模式:这个类别下的模式处理类或对象的组合。在企业应用中,有两种常见的技巧用于在面向对象系统中重用功能,如下所示:

    • 继承:它用于从其他类继承常用的状态和行为。

    • 组合:它用于将其他对象作为类的实例变量来组合。它定义了如何组合对象以获得新的功能。

  • 行为设计模式:这个类别下的模式描述了类或对象如何相互交互和分配责任。这些模式定义了企业应用中对象之间的通信方法。因此,在这里,您将学习如何使用行为模式来简化复杂的流程控制。此外,您还将使用行为模式来封装算法并在运行时动态选择它们。

结构设计模式

在上一节中,我们讨论了创建型设计模式以及它们如何根据业务需求提供最佳的对象创建解决方案。创建型设计模式只为在应用中创建对象提供了解决方案,但对于这些对象如何在应用中合并以实现特定的业务目标,结构型设计模式就派上用场了。在本章中,我们将探讨结构型模式,以及这些模式如何通过继承或组合来定义应用中对象之间的关系。结构型模式可以帮助你解决许多与对象结构相关的问题。它们展示了如何以灵活和可扩展的方式将系统的不同部分粘合在一起。结构型模式帮助你确保当某个部分发生变化时,整个结构不需要改变;例如,在汽车中,你可以更换不同供应商的轮胎,而不会影响汽车的其他部分。它们还展示了如何将系统中的某些部分(虽然不兼容但需要使用)重新塑造成兼容的部分。

适配器设计模式

将一个类的接口转换为客户端期望的另一个接口。适配器使得原本因为接口不兼容而无法协作的类能够一起工作。

-GoF 设计模式:可重用面向对象软件的元素

根据这个设计模式,适配器设计模式属于结构型设计模式。根据这个设计模式,两个不兼容的类因为接口不兼容而无法协作,这个模式充当了两个不兼容接口之间的桥梁。当应用中的两个功能在功能上不兼容,但根据业务需求需要集成时,就会使用这个模式。

在现实生活中,有许多我们可以使用适配器模式的例子。假设你有不同类型的电源插头,如圆柱形和矩形插头,如下图所示。如果你需要将矩形插头插入圆柱形插座,并且满足电压要求,可以使用适配器:

适配器设计模式示例

适配器模式的好处

让我们来看看在应用中使用适配器设计模式的好处。

  • 适配器模式允许你与两个或更多不兼容的对象进行通信和交互

  • 这个模式促进了你应用中现有功能的可重用性

适配器模式的一般要求

以下是这个设计模式解决设计问题的常见要求:

  • 如果你打算在你的应用中使用这个模式,就需要使用一个具有不兼容接口的现有类。

  • 在你的应用中,这个模式还有另一个用途,那就是当你想要创建一个与具有不兼容接口的类协作的可重用类时。

  • 有几个现有的子类可供使用,但通过为每个子类创建子类来适配它们的接口是不切实际的。一个对象适配器可以适配其父类的接口。

让我们看看 Spring 是如何在内部实现适配器设计模式的。

Spring 框架中适配器设计模式的实现

Spring 框架使用适配器设计模式在框架中透明地实现了很多功能。以下是根据 Spring 框架中的适配器设计模式列出的一些类:

  • JpaVendorAdapter

  • HibernateJpaVendorAdapter

  • HandlerInterceptorAdapter

  • MessageListenerAdapter

  • SpringContextResourceAdapter

  • ClassPreProcessorAgentAdapter

  • RequestMappingHandlerAdapter

  • AnnotationMethodHandlerAdapter

  • WebMvcConfigurerAdapter

适配器模式的 UML 图

让我们理解前面的 UML 图,该图说明了适配器设计模式的组件:

  • 目标接口:这是将被客户端使用的期望接口类

  • 适配器类:这个类是一个包装类,它实现了期望的目标接口,并修改了从适配者类可用的特定请求

  • 适配者类:这是适配器类用来重用现有功能并对其进行修改以适应所需用途的类

  • 客户端:这个类将与适配器类交互

让我们看看以下适配器设计模式的示例实现。

适配器设计模式的示例实现

我将创建一个示例来展示适配器设计模式的实际演示,所以让我们讨论这个示例,我创建这个示例是基于通过支付网关进行支付。假设我有一个旧的支付网关和最新的高级支付网关,这两个网关之间没有关系,所以我的需求是,我想在更改现有源代码的同时,从旧的支付网关迁移到高级支付网关。我创建了一个适配器类来解决这个问题。这个适配器类作为两个不同支付网关之间的桥梁,让我们看看以下代码:

现在让我们为旧的支付网关创建一个接口:

    package com.packt.patterninspring.chapter3.adapter.pattern; 
    import com.packt.patterninspring.chapter3.model.Account; 
    public interface PaymentGateway { 
      void doPayment(Account account1, Account account2); 
    } 

现在让我们为旧的支付网关PaymentGateway.java创建一个实现类:

    package com.packt.patterninspring.chapter3.adapter.pattern; 
    import com.packt.patterninspring.chapter3.model.Account; 
    public class PaymentGatewayImpl implements PaymentGateway{ 
      @Override 
      public void doPayment(Account account1, Account account2){ 
         System.out.println("Do payment using Payment Gateway"); 
      } 
    } 

以下接口及其实现为支付网关提供了新的和高级的功能:

    package com.packt.patterninspring.chapter3.adapter.pattern; 
    public interface AdvancedPayGateway { 
      void makePayment(String mobile1, String mobile2); 
    } 

现在让我们为高级支付网关接口创建一个实现类:

    package com.packt.patterninspring.chapter3.adapter.pattern; 
    import com.packt.patterninspring.chapter3.model.Account; 
    public class AdvancedPaymentGatewayAdapter implements 
       AdvancedPayGateway{ 
      private PaymentGateway paymentGateway; 
      public AdvancedPaymentGatewayAdapter(PaymentGateway
         paymentGateway) { 
        this.paymentGateway = paymentGateway; 
      } 
      public void makePayment(String mobile1, String mobile2) { 
         Account account1 = null;//get account number by 
             mobile number mobile  
         Account account2 = null;//get account number by 
            mobile number mobile  
         paymentGateway.doPayment(account1, account2); 
      } 
    } 

让我们看看以下这个模式的演示类:

    package com.packt.patterninspring.chapter3.adapter.pattern; 
    public class AdapterPatternMain { 
      public static void main(String[] args) { 
        PaymentGateway paymentGateway = new PaymentGatewayImpl(); 
        AdvancedPayGateway advancedPayGateway = new 
           AdvancedPaymentGatewayAdapter(paymentGateway); 
        String mobile1 = null; 
        String mobile2 = null; 
        advancedPayGateway.makePayment(mobile1, mobile2); 
      } 
    } 

在前面的类中,我们有旧的支付网关对象作为PaymentGateway接口,但我们通过使用AdvancedPaymentGatewayAdapter适配器类将这个旧的支付网关实现转换为高级支付网关形式。让我们运行这个演示类并查看以下输出:

既然我们已经看到了适配器设计模式,让我们转向它的一个不同变体——桥接设计模式。

桥接设计模式

将抽象与其实现解耦,以便它们可以独立变化

  • GoF 设计模式:可重用面向对象软件的元素

在软件工程中,最受欢迎的观念之一是首选组合而非继承。桥接设计模式促进了这一流行观念。类似于适配器模式,这个模式也属于 GoF 设计模式中的结构设计模式家族。桥接模式的方法是将客户端代码使用的抽象与其实现解耦;这意味着它将抽象和其实现分离成独立的类层次。此外,桥接模式偏好组合而非继承,因为继承并不总是灵活的,它会破坏封装,所以对实现者所做的任何更改都会影响客户端代码使用的抽象。

桥接为软件开发中两个不同独立组件之间的通信提供了一种方式,桥接结构为你提供了解耦抽象类和实现类(即接口)的方式。所以对实现类或实现者(即接口)所做的任何更改都不会影响抽象类或其精炼的抽象类。它是通过在接口和抽象之间使用组合来实现的。桥接模式使用接口作为抽象类和实现类之间的桥梁。你可以在两种类型的类中做出更改,而不会对客户端代码产生影响。

桥接模式的优点

以下为桥接设计模式的优点:

  • 桥接设计模式允许你分离实现和抽象

  • 此设计模式提供了在客户端代码中无副作用地更改两种类型类的灵活性

  • 此设计模式允许通过它们之间的抽象来隐藏实际的实现细节

桥接设计模式解决的问题

以下是由桥接设计模式解决的常见问题:

  • 移除了功能抽象与其实现之间的永久绑定

  • 你可以在不影响抽象和客户端代码的情况下修改实现类

  • 你可以使用子类扩展抽象及其实现

在 Spring 框架中实现桥接设计模式

以下 Spring 模块基于桥接设计模式:

  • ViewRendererServlet:它是一个桥接 Servlet,主要用于 Portlet MVC 支持

  • 桥接设计模式:桥接设计模式用于 Spring 的日志处理过程

让我们看看桥接设计模式的示例实现。

桥接设计模式的示例实现

让我们看看以下示例,我们将演示桥接设计模式的使用。假设你希望在银行系统中开设两种类型的账户,一种是储蓄账户,另一种是活期账户。

不使用桥接设计模式的系统

让我们看看一个不使用桥接设计模式的例子。在以下图中,你可以看到银行和账户接口之间的关系:

不使用桥接设计模式的系统

让我们创建一个不使用桥接设计模式的设计。首先创建一个接口或一个抽象类,Bank。然后创建它的派生类:IciciBankHdfcBank。要在银行开户,首先决定账户类的类型--储蓄账户活期账户,这些类扩展了特定的银行类(HdfcBankIciciBank)。在这个应用程序中存在一个简单的深度继承层次结构。那么与前面的图相比,这个设计有什么问题呢?你会注意到,在这个设计中,有两部分,一部分是抽象部分,另一部分是实现部分。客户端代码与抽象部分交互。只有当更新抽象部分时,客户端代码才能访问实现部分的新更改或新功能,这意味着抽象、实现和部分之间是紧密耦合的。

现在我们来看看如何使用桥接设计模式来改进这个例子:

使用桥接设计模式的系统

在以下图中,我们使用桥接设计模式在BankAccount接口之间建立关系:

使用桥接设计模式的系统

桥接设计模式的 UML 结构

让我们看看以下图,桥接设计模式是如何解决这些设计问题的,正如我们在没有使用桥接设计模式的例子中所看到的。桥接模式将抽象和实现分离成两个类层次结构:

桥接设计模式的 UML 图

我们有一个Account接口,它充当桥接实现者,具体的类SavingAccountCurrentAccount实现了Account接口。Bank是一个抽象类,它将使用Account对象。

让我们创建一个桥接实现者接口。

以下是Account.java文件:

    package com.packt.patterninspring.chapter3.bridge.pattern; 
    public interface Account { 
      Account openAccount(); 
      void accountType(); 
    } 

创建具体的桥接实现类以实现implementer接口。让我们创建一个SavingAccount类作为Account的实现。

以下是SavingAccount.java文件:

    package com.packt.patterninspring.chapter3.bridge.pattern; 
    public class SavingAccount implements Account { 
      @Override 
      public Account openAccount() { 
         System.out.println("OPENED: SAVING ACCOUNT "); 
         return new SavingAccount(); 
      } 
      @Override 
      public void accountType() { 
        System.out.println("##It is a SAVING Account##"); 
      } 
    } 

创建一个实现Account接口的CurrentAccount类。

以下是CurrentAccount.java文件:

    package com.packt.patterninspring.chapter3.bridge.pattern; 
    public class CurrentAccount implements Account { 
      @Override 
      public Account openAccount() { 
        System.out.println("OPENED: CURRENT ACCOUNT "); 
        return new CurrentAccount(); 
      } 
      @Override 
      public void accountType() { 
        System.out.println("##It is a CURRENT Account##"); 
      } 
    } 

在桥接设计模式中创建抽象,但首先创建接口Bank

以下是Bank.java文件:

    package com.packt.patterninspring.chapter3.bridge.pattern; 
    public abstract class Bank { 
      //Composition with implementor 
      protected Account account; 
      public Bank(Account account){ 
         this.account = account; 
      } 
      abstract Account openAccount(); 
    } 

让我们实现Bank接口的第一个抽象,并查看以下Bank接口的实现类。

以下为IciciBank.java文件:

    package com.packt.patterninspring.chapter3.bridge.pattern; 
    public class IciciBank extends Bank { 
      public IciciBank(Account account) { 
        super(account); 
      } 
      @Override 
      Account openAccount() { 
        System.out.print("Open your account with ICICI Bank"); 
        return account; 
      } 
    } 

让我们实现Bank接口的第二个抽象,并查看以下Bank接口的实现类。

以下为HdfcBank.java文件:

    package com.packt.patterninspring.chapter3.bridge.pattern; 
      public class HdfcBank extends Bank { 
        public HdfcBank(Account account) { 
          super(account); 
        } 
        @Override 
        Account openAccount() { 
          System.out.print("Open your account with HDFC Bank"); 
          return account; 
        } 
      } 

创建一个桥接设计模式的演示类。

以下为BridgePatternMain.java文件:

    package com.packt.patterninspring.chapter3.bridge.pattern; 
    public class BridgePatternMain { 
      public static void main(String[] args) { 
         Bank icici = new IciciBank(new CurrentAccount()); 
         Account current = icici.openAccount(); 
         current.accountType(); 
         Bank hdfc = new HdfcBank(new SavingAccount()); 
         Account saving = hdfc.openAccount(); 
         saving.accountType(); 
      } 
    } 

让我们运行这个演示类,并在控制台看到以下输出:

既然我们已经看到了桥接设计模式,让我们转向它的一个不同变体——组合设计模式。

组合设计模式

将对象组合成树结构以表示部分-整体层次结构。组合允许客户端以统一的方式处理单个对象和对象的组合。

-GoF 设计模式

在软件工程中,组合模式属于结构设计模式。根据这个模式,客户端将同一类型的对象组作为一个单一对象处理。组合设计模式背后的思想是将一组对象组合成树结构,以表示更大结构应用的一个模块。并且对于客户端来说,这个结构是一个单一的单元或实例。

组合设计模式背后的动机是系统中的对象被分组到树结构中,而树结构是节点-叶子和分支的组合。在树结构中,节点有许多叶子和其他节点。叶子没有任何东西,这意味着在树中没有叶子的子节点。叶子被视为树结构数据的终点。

让我们看一下以下图示,它以节点和叶子的形式表示树结构中的数据:

使用节点和叶子表示的树结构数据

组合模式解决的问题

作为一名开发者,设计一个应用程序以便客户端可以跨应用程序统一访问你的对象,即使这个对象是由对象组合而成的或是一个单独的对象,这会更加困难。这个设计模式解决了困难,并允许你以这样的方式设计对象,你可以将这个对象用作对象的组合以及单个个体对象。

这个模式解决了在创建层次树结构时面临的挑战,为客户端提供了一个统一的方式来访问和操作树中的对象。组合模式是一个好的选择;在这种情况下,将原始数据和组合数据视为同质化的是更简单的。

组合设计模式的 UML 结构

组合设计模式基于将相似类型的对象组合成树结构,正如你所知,每个树有三个主要部分:分支、节点和叶子。因此,让我们看看以下在这个设计模式中使用的术语。

组件:它基本上是树的分支,分支上有其他分支、节点和叶子。组件为所有组件提供抽象,包括组合对象。在组合模式中,组件基本声明为对象的接口。

叶子:它是实现所有组件方法的对象。

组合:它在树结构中表示为一个节点,它包含其他节点和叶子,它代表一个组合组件。它有添加子节点的方法,即它代表同一类型对象的集合。它还有为子节点提供其他组件方法。

让我们看看以下这个设计模式的 UML 图:

组合设计模式的 UML 图

组合设计模式的优点

  • 此模式提供了在现有组件更改的情况下动态添加新组件的灵活性。

  • 此模式允许您创建一个包含单个和组合对象的类层次结构

组合设计模式的示例实现

在以下示例中,我正在实现一个Account接口,它可以是一个SavingAccountCurrentAccount,或者是由几个账户组成的组合。我有一个CompositeBankAccount类,它充当组合模式的行为类。让我们看看以下代码示例。

创建一个Account接口,它将被视为组件:

    public interface Account { 
      void accountType(); 
    } 

创建一个SavingAccount类和一个CurrentAccount类,作为组件的实现,并将它们也视为叶子:

以下是SavingAccount.java文件:

    public class SavingAccount implements Account{ 
      @Override 
      public void accountType() { 
        System.out.println("SAVING ACCOUNT"); 
      } 
    } 

以下是CurrentAccount.java文件:

    public class CurrentAccount implements Account { 
      @Override 
      public void accountType() { 
         System.out.println("CURRENT ACCOUNT"); 
      } 
    } 

创建一个CompositeBankAccount类,它将被视为组合类并实现Account接口:

以下是CompositeBankAccount.java文件:

     package com.packt.patterninspring.chapter3.composite.pattern; 
     import java.util.ArrayList; 
     import java.util.List; 
     import com.packt.patterninspring.chapter3.model.Account; 
     public class CompositeBankAccount implements Account { 
       //Collection of child accounts. 
       private List<Account> childAccounts = new ArrayList<Account>(); 
       @Override 
       public void accountType() { 
         for (Account account : childAccounts) { 
               account.accountType(); 
         } 
       } 
       //Adds the account to the composition. 
          public void add(Account account) { 
            childAccounts.add(account); 
          } 
          //Removes the account from the composition. 
          public void remove(Account account) { 
            childAccounts.remove(account); 
         } 
       } 

创建一个CompositePatternMain类,它也将被视为客户端:

以下是CompositePatternMain.java文件:

    package com.packt.patterninspring.chapter3.composite.pattern; 
    import com.packt.patterninspring.chapter3.model.CurrentAccount; 
    import com.packt.patterninspring.chapter3.model.SavingAccount; 
    public class CompositePatternMain { 
      public static void main(String[] args) { 
         //Saving Accounts 
         SavingAccount savingAccount1 = new SavingAccount(); 
         SavingAccount savingAccount2 = new SavingAccount(); 
         //Current Account 
         CurrentAccount currentAccount1 = new CurrentAccount(); 
         CurrentAccount currentAccount2 = new CurrentAccount(); 
         //Composite Bank Account 
         CompositeBankAccount compositeBankAccount1 = new
         CompositeBankAccount(); 
         CompositeBankAccount compositeBankAccount2 = new
         CompositeBankAccount(); 
         CompositeBankAccount compositeBankAccount = new
         CompositeBankAccount(); 
         //Composing the bank accounts 
         compositeBankAccount1.add(savingAccount1); 
         compositeBankAccount1.add(currentAccount1); 
         compositeBankAccount2.add(currentAccount2); 
         compositeBankAccount2.add(savingAccount2); 
         compositeBankAccount.add(compositeBankAccount2); 
         compositeBankAccount.add(compositeBankAccount1); 
         compositeBankAccount.accountType(); 
      } 
    } 

让我们运行这个演示类,并在控制台看到以下输出:

现在我们已经讨论了组合设计模式,让我们转向装饰器设计模式。

装饰器设计模式

动态地为对象附加额外的职责。装饰器为扩展功能提供了灵活的替代子类化方法。

  • GOF 设计模式

在软件工程中,所有 GOF 结构模式的共同意图是在灵活的企业应用程序中简化对象和类之间的复杂关系。装饰者模式是这些模式中的一种特殊类型的设计模式,它属于结构设计模式,允许你在运行时动态或静态地为单个对象添加和移除行为,而不会改变同一类中其他相关对象的现有行为。这种设计模式在不违反单一职责原则或面向对象编程的 SOLID 原则的情况下完成这一点。

此设计模式使用组合而不是继承来处理对象关联;它允许你将功能划分为具有独特关注区域的不同的具体类。

装饰者设计模式的好处

  • 此模式允许你动态和静态地扩展功能,而不改变现有对象的结构

  • 通过使用此模式,你可以动态地为对象添加新的责任

  • 此模式也被称为包装者

  • 此模式使用组合来维护对象关系以保持 SOLID 原则

  • 此模式通过为每个新的特定功能编写新类来简化编码,而不是更改应用程序的现有代码

装饰者模式解决的问题

在企业应用程序中,可能存在业务需求或未来计划通过添加新功能来扩展产品的行为。为了实现这一点,你可以使用继承来扩展对象的行为。但是,继承应该在编译时完成,并且该方法也适用于该类的其他实例。由于代码修改,违反了开闭原则。为了避免违反 SOLID 原则,你可以动态地为对象附加新的责任。这就是装饰者设计模式出现并以非常灵活的方式解决这个问题的情形。让我们看看以下如何将这种设计模式应用到实际案例研究中的例子。

考虑到一家银行向客户提供多种具有不同优惠的账户。它将客户分为三个类别——老年人、特权客户和年轻人。银行为老年人推出储蓄账户计划——如果他们在该银行开设储蓄账户,他们将获得最高 1000 美元的医疗保险。同样,银行也为特权客户提供意外保险,最高可达 1600 美元,透支额度为 84 美元。年轻人没有这样的计划。

为了解决新的需求,我们可以为SavingAccount添加新的子类;每个子类代表一个具有额外优惠的储蓄账户装饰,这就是我们现在的设计看起来像这样:

图片

不使用装饰器设计模式进行继承的应用程序设计

由于我将为SavingAccount添加更多福利方案,因此此设计将非常复杂,但银行推出相同的方案时会发生什么情况针对CurrentAccount?显然,这种设计是有缺陷的,但这是装饰器模式的理想用例。此模式允许您添加运行时动态行为。在这种情况下,我将创建一个抽象的AccountDecorator类来实现Account。此外,我将创建SeniorCitizen类和Privilege类,它们扩展了AccountDecorator,因为年轻人没有额外的福利,所以 SavingAccount 类没有扩展AccountDecorator。这就是设计将如何进行:

图片

使用装饰器设计模式进行应用程序设计

前面的图示通过创建AccountDecorator作为模式中的装饰器,关注观察AccountAccountDecorator之间的关系。这种关系如下:

  • AccountDecoratorAccount之间存在is-a关系,即正确的类型继承

  • AccountDecoratorAccount之间存在has-a关系,即为了在不更改现有代码的情况下添加新行为而进行的组合

让我们看看 UML 结构

图片

装饰器设计模式的 UML 图

参与该模式的类和对象包括:

  • 组件Account):它是为可以动态添加责任的对象提供的接口

  • 具体组件SavingAccount):它是组件接口的具体类,并定义了一个可以附加额外责任的对象

  • 装饰器AccountDecorator):它有一个指向Component对象的引用,并定义了一个符合组件接口的接口

  • 具体装饰器SeniorCitizen 和 Privilege):它是装饰器的具体实现,并为组件添加了责任

实现装饰器模式

让我们看看以下代码来演示装饰器设计模式。

创建一个组件类:

以下为Account.java文件:

    package com.packt.patterninspring.chapter3.decorator.pattern; 
    public interface Account { 
       String getTotalBenefits(); 
    } 

创建具体的组件类:

以下为SavingAccount.java文件:

    package com.packt.patterninspring.chapter3.decorator.pattern; 
    public class SavingAccount implements Account { 
      @Override 
      public String getTotalBenefits() { 
         return "This account has 4% interest rate with per day
           $5000 withdrawal limit"; 
      } 
    } 

让我们为 Account 组件创建另一个具体类:

以下为CurrentAccount.java文件:

    package com.packt.patterninspring.chapter3.decorator.pattern; 
    public class CurrentAccount implements Account { 
      @Override 
      public String getTotalBenefits() { 
         return "There is no withdrawal limit for current account"; 
      } 
    } 

让我们为 Account 组件创建一个Decorator类。这个装饰器类将其他运行时行为应用到 Account 组件类中。

以下为AccountDecorator.java文件:

    package com.packt.patterninspring.chapter3.decorator.pattern; 
    public abstract class AccountDecorator implements Account { 
      abstract String applyOtherBenefits(); 
    } 

让我们创建一个ConcreteDecorator类来实现 AccountDecorator 类。以下类SeniorCitizen扩展了AccountDecorator类,以访问其他运行时行为,如applyOtherBenefits()

以下为SeniorCitizen.java文件:

    package com.packt.patterninspring.chapter3.decorator.pattern; 
    public class SeniorCitizen extends AccountDecorator { 
      Account account; 
      public SeniorCitizen(Account account) { 
         super(); 
         this.account = account; 
      } 
      public String getTotalBenefits() { 
         return account.getTotalBenefits() + " other benefits are 
             "+applyOtherBenefits(); 
      } 
      String applyOtherBenefits() { 
         return " an medical insurance of up to $1,000 for Senior 
         Citizen"; 
      } 
    } 

让我们创建另一个ConcreteDecorator类来实现AccountDecorator类。以下类Privilege扩展了AccountDecorator类,以便访问其他运行时行为,如applyOtherBenefits()。

以下为Privilege.java文件:

    package com.packt.patterninspring.chapter3.decorator.pattern; 
    public class Privilege extends AccountDecorator { 
      Account account; 
      public Privilege(Account account) { 
         this.account = account; 
      } 
      public String getTotalBenefits() { 
         return account.getTotalBenefits() + " other benefits are    
            "+applyOtherBenefits(); 
      } 
      String applyOtherBenefits() { 
        return " an accident insurance of up to $1,600 and
           an overdraft facility of $84"; 
        } 
      } 

现在我们编写一些测试代码,看看装饰器模式在运行时是如何工作的:

以下为DecoratorPatternMain.java文件:

    package com.packt.patterninspring.chapter3.decorator.pattern; 
    public class DecoratorPatternMain { 
      public static void main(String[] args) { 
         /*Saving account with no decoration*/ 
         Account basicSavingAccount = new SavingAccount(); 
         System.out.println(basicSavingAccount.getTotalBenefits()); 
         /*Saving account with senior citizen benefits decoration*/ 
         Account seniorCitizenSavingAccount = new SavingAccount(); 
         seniorCitizenSavingAccount = new 
            SeniorCitizen(seniorCitizenSavingAccount); 
         System.out.println
        (seniorCitizenSavingAccount.getTotalBenefits()); 
         /*Saving account with privilege decoration*/ 
         Account privilegeCitizenSavingAccount = new SavingAccount(); 
         privilegeCitizenSavingAccount = new
            Privilege(privilegeCitizenSavingAccount); 
         System.out.println
        (privilegeCitizenSavingAccount.getTotalBenefits()); 
      } 
    } 

让我们运行这个演示类,并在控制台看到以下输出:

Spring 框架中的装饰器设计模式

Spring 框架使用装饰器设计模式构建重要的功能,如事务、缓存同步和安全相关任务。让我们看看 Spring 如何透明地实现此模式的一些功能:

  • 将建议编织到 Spring 应用程序中。它通过 CGLib 代理使用装饰器模式。它通过在运行时生成目标类的子类来工作。

  • BeanDefinitionDecorator:它用于通过应用自定义属性来装饰 bean 定义。

  • WebSocketHandlerDecorator:它用于使用附加行为装饰 WebSocketHandler。

现在我们转向另一个 GOF 设计模式——外观设计模式。

外观设计模式

为子系统中的一组接口提供一个统一的接口。外观定义了一个更高层次的接口,使得子系统更容易使用。

  • GOF 设计模式

外观设计模式不过是接口的接口,用于简化客户端代码与子系统类之间的交互。这种设计属于 GOF 结构设计模式。

外观模式的优点:

  • 此模式简化了客户端与子系统交互的复杂性

  • 此模式将所有业务服务合并为单个接口,以便使其更易于理解

  • 此模式减少了客户端代码对系统内部工作的依赖

了解何时使用外观模式

假设你正在设计一个系统,这个系统拥有大量独立的类,并且还有一组要实现的服务。这个系统将会非常复杂,因此外观模式应运而生,简化了更大系统的复杂性,并简化了客户端代码与大型复杂系统的一个子系统中的类之间的交互。

假设你想开发一个具有大量服务的银行企业应用程序以执行任务,例如,AccountService用于通过accountId获取AccountPaymentService用于支付网关服务,以及TransferService用于从一个账户向另一个账户转账。应用程序的客户端代码与所有这些服务交互,以将资金从一个账户转账到另一个账户。这就是不同的客户端如何与银行系统的转账过程交互。如下面的图所示,这里你可以看到直接与子系统类交互的客户端代码,客户端还应该了解子系统类的内部工作原理,因此这简单地违反了 SOLID 设计原则,因为客户端代码与银行应用程序的子系统类紧密耦合:

没有外观设计模式的银行应用程序子系统

而不是客户端代码直接与子系统的类交互,你可以引入另一个接口,这使得子系统更容易使用,如下面的图所示。这个接口被称为“外观”接口,它基于外观模式,并且是与子系统交互的简单方式:

带有外观设计模式的银行应用程序子系统

实现外观设计模式

让我们查看以下列表以演示外观设计模式。

为你的银行应用程序创建子系统服务类:让我们看看以下子系统的PaymentService类。

以下为PaymentService.java文件:

    package com.packt.patterninspring.chapter3.facade.pattern; 
    public class PaymentService { 
      public static boolean doPayment(){ 
         return true; 
      } 
    } 

让我们为子系统创建另一个服务类AccountService

以下为AccountService.java文件:

   package com.packt.patterninspring.chapter3.facade.pattern; 
   import com.packt.patterninspring.chapter3.model.Account; 
   import com.packt.patterninspring.chapter3.model.SavingAccount; 
   public class AccountService { 
     public static Account getAccount(String accountId) { 
        return new SavingAccount(); 
     } 
   } 

让我们为子系统创建另一个服务类TransferService

以下为TransferService.java文件:

    package com.packt.patterninspring.chapter3.facade.pattern; 
    import com.packt.patterninspring.chapter3.model.Account; 
    public class TransferService { 
      public static void transfer(int amount, Account fromAccount,
            Account toAccount) { 
        System.out.println("Transfering Money"); 
      } 
    } 

创建一个外观服务类以与子系统交互:让我们看看以下子系统的外观接口,然后将其实现为应用程序中的全局银行服务。

以下为BankingServiceFacade.java文件:

    package com.packt.patterninspring.chapter3.facade.pattern; 
    public interface BankingServiceFacade { 
       void moneyTransfer(); 
    } 

以下为BankingServiceFacadeImpl.java文件:

    package com.packt.patterninspring.chapter3.facade.pattern; 
    import com.packt.patterninspring.chapter3.model.Account; 
    public class BankingServiceFacadeImpl implements 
        BankingServiceFacade{ 
      @Override 
      public void moneyTransfer() { 
         if(PaymentService.doPayment()){ 
               Account fromAccount = AccountService.getAccount("1"); 
               Account toAccount   = AccountService.getAccount("2"); 
               TransferService.transfer(1000, fromAccount, toAccount); 
         } 
      } 
    } 

创建外观的客户端:

以下为FacadePatternClient.java文件:

    package com.packt.patterninspring.chapter3.facade.pattern; 
    public class FacadePatternClient { 
      public static void main(String[] args) { 
        BankingServiceFacade serviceFacade = new 
          BankingServiceFacadeImpl(); 
        serviceFacade.moneyTransfer(); 
      } 
    } 

外观设计模式的结构化建模语言(UML)结构

参与该模式的类和对象是:

  • 外观(BankingServiceFacade

这是一个了解哪些子系统类负责请求的外观接口。该接口负责将客户端请求委派给适当的子系统对象。

  • 子系统类(AccountServiceTransferServicePaymentService

这些接口实际上是银行流程系统应用的功能子系统。它们负责处理门面对象分配的过程。这个类别的接口没有对门面对象的引用;它们没有门面实现的细节。它们与门面对象完全独立。

让我们看看以下关于此模式的 UML 图:

图片

门面设计模式的 UML 图

Spring 框架中的门面模式

在企业应用中,如果你在 Spring 应用中工作,门面模式通常用于应用的业务服务层来整合所有服务。你还可以在持久层的 DAO 上应用此模式。

既然我们已经了解了门面设计模式,让我们转向它的一个不同变体——代理设计模式。

代理设计模式

为另一个对象提供一个代理或占位符,以控制对其的访问。

  • GOF 设计模式

代理设计模式提供了一个具有另一个类功能的对象,同时拥有它。此模式属于 GOF 设计模式的结构设计模式。此设计模式的目的是向外界提供一个替代类,以及其功能。

代理模式的用途

让我们看看以下要点:

  • 此模式隐藏了实际对象对外界。

  • 此模式可以提高性能,因为它是在需要时创建对象。

代理设计模式的 UML 结构

让我们看看以下关于此模式的 UML 图:

图片

代理设计模式的 UML 图

现在,让我们看看这个 UML 图的不同组件:

  • Subject:代理和真实主题需要实现的实际接口。

  • RealSubjectSubject的真实实现。它是由代理表示的真实对象。

  • 代理:它是一个代理对象,也是真实对象Subject的实现。它维护对真实对象的引用。

实现代理设计模式

让我们看看以下代码来演示代理模式。

创建一个 Subject。

以下为Account.java文件:

    public interface Account { 
      void accountType(); 
    } 

创建一个实现 Subject 的 RealSubject 类,让我们看看以下类作为代理设计模式的 RealSubject 类。

以下为SavingAccount.java文件:

    public class SavingAccount implements Account{ 
       public void accountType() { 
          System.out.println("SAVING ACCOUNT"); 
       } 
    } 

创建一个实现 Subject 并具有真实主题的代理类

以下为ProxySavingAccount.java文件:

    package com.packt.patterninspring.chapter2.proxy.pattern; 
    import com.packt.patterninspring.chapter2.model.Account; 
    import com.packt.patterninspring.chapter2.model.SavingAccount; 
    public class ProxySavingAccount implements Account{ 
      private Account savingAccount; 
      public void accountType() { 
         if(savingAccount == null){ 
               savingAccount = new SavingAccount(); 
         } 
         savingAccount.accountType(); 
      }  
    } 

Spring 框架中的代理模式

Spring 框架在 Spring AOP 模块中透明地使用了代理设计模式。正如我在第一章,“Spring Framework 5.0 入门与设计模式”中讨论的那样。在 Spring AOP 中,你创建对象的代理以在 Spring 应用程序中的切入点处应用横切关注点。在 Spring 中,其他模块也实现了代理模式,例如 RMI、Spring 的 HTTP Invoker、Hessian 和 Burlap。

让我们看看下一节关于行为设计模式及其底层模式和示例。

行为设计模式

行为设计模式的目的是一组对象之间的交互和协作,以执行单个对象无法独立完成的任务。对象之间的交互应该是松耦合的。此类别下的模式描述了类或对象之间交互和分配责任的方式。让我们在下一节中看看行为设计模式的不同变体。

责任链设计模式

通过给多个对象一个处理请求的机会,避免将请求的发送者与其接收者耦合。将接收对象链式连接,并将请求沿着链传递,直到某个对象处理它。

  • GOF 设计模式

责任链设计模式属于 GOF 模式家族中的行为设计模式。根据此模式,请求的发送者和接收者是解耦的。发送者将请求发送到接收者链,链中的任何接收者都可以处理请求。在此模式中,接收者对象具有另一个接收者对象的引用,以便如果它不处理请求,则将相同的请求传递给其他接收者对象。

例如,在银行系统中,你可以在任何地方使用任何自动柜员机提取现金,因此它是责任链设计模式的一个活生生的例子。

此模式有以下优点:

  • 此模式减少了系统中发送者和接收者对象之间的耦合,以处理请求。

  • 此模式更灵活地分配责任给另一个引用对象。

  • 此模式通过组合使用对象链,这些对象作为一个单一单元工作。

让我们看看以下 UML 图,它显示了责任链设计模式的所有组件:

图片

责任链设计模式的 UML 图

  • 处理者:这是系统中处理请求的抽象类或接口。

  • 具体处理者:这些是具体类,它们实现处理者以处理请求,或者将相同的请求传递给处理链中的下一个后续处理者。

  • 客户端:这是主应用程序类,用于向链上的处理对象发起请求。

Spring 框架中的责任链模式

Spring Security 项目在 Spring 框架中实现了责任链模式。Spring Security 允许你通过使用安全过滤器链在你的应用程序中实现身份验证和授权功能。这是一个高度可配置的框架。你可以通过这个过滤器链添加你的自定义过滤器来自定义功能,因为责任链设计模式。

现在我们已经看到了责任链设计模式,让我们转向它的一个不同变体--命令设计模式。

命令设计模式

将请求封装为一个对象,从而让你用不同的请求参数化客户端,排队或记录请求,并支持可撤销操作

-GOF 设计模式

命令设计模式属于 GOF 模式中的行为模式家族,这是一个非常简单的数据驱动模式,它允许你将请求数据封装到对象中,并将该对象作为命令传递给调用者方法,然后作为另一个对象返回给调用者。

以下列出了使用命令模式的优点:

  • 这种模式使你能够在系统组件发送者和接收者之间传输数据。

  • 这种模式允许你通过执行操作来参数化对象。

  • 你可以轻松地在系统中添加新命令,而无需更改现有类。

让我们看看以下 UML 图,展示了命令设计模式的所有组件:

命令设计模式的 UML 图

  • 命令: 它是一个接口或抽象类,在系统中执行一个操作。

  • 具体命令: 它是 Command 接口的具体实现,并定义了一个将要执行的操作。

  • 客户端: 这是一个主类,它创建一个 ConcreteCommand 对象并设置其接收者。

  • 调用者: 它是一个调用者,用于调用请求以携带命令对象。

  • 接收者: 它是一个简单的处理方法,通过 ConcreteCommand 执行实际操作。

Spring 框架中的命令设计模式

Spring MVC 在 Spring 框架中实现了命令设计模式。在你的企业应用程序中使用 Spring 框架时,你经常看到通过使用命令对象应用命令模式的理念。

现在我们已经看到了命令设计模式,让我们转向它的一个不同变体--解释器设计模式。

解释器设计模式

给定一种语言,定义其语法的表示,以及一个使用该表示来解释该语言句子的解释器。

-GOF 设计模式

解释器设计模式允许你在编程中解释表达式语言,为它的语法定义一个表示。这种模式属于 GOF 模式中的行为设计模式家族。

以下列出了使用解释器模式的优点:

  • 这个模式允许你轻松地更改和扩展语法。

  • 使用表达式语言非常简单

让我们看看以下 UML 图展示了解释器设计模式的所有组件:

图片

解释器设计模式的 UML 图

  • 抽象表达式: 它是一个接口,通过使用interpret()操作执行任务。

  • 终结表达式: 它是上述接口的一个实现,并为终结表达式实现了interpret()操作。

  • 非终结表达式: 它也是上述接口的一个实现,并为非终结表达式实现了interpret()操作。

  • 上下文: 它是一个String表达式,包含对解释器全局的信息。

  • 客户端: 它是调用解释器操作的主要类。

Spring 框架中的解释器设计模式

在 Spring 框架中,解释器模式与Spring 表达式语言SpEL)一起使用。Spring 从 3.0 版本开始添加了这个新特性,你可以在使用 Spring 框架的企业应用程序中使用它。

现在我们已经看到了解释器设计模式,让我们转向它的另一个变体--迭代器设计模式。

迭代器设计模式

提供了一种按顺序访问聚合对象元素的方法,而不暴露其底层表示。

-GOF 设计模式

在编程语言中,这是一个非常常用的设计模式,就像在 Java 中一样。这个模式来自 GOF 模式的行为设计模式家族。这个模式允许你在不知道其内部表示的情况下,按顺序访问集合对象中的项。

迭代器模式有以下优点:

  • 容易访问集合中的项。

  • 你可以使用多个来访问集合中的项,因为它支持大量的遍历变体。

  • 它提供了一个统一的接口来遍历集合中的不同结构。

让我们看看以下 UML 图展示了迭代器设计模式的所有组件:

图片

迭代器设计模式的 UML 图

  • 迭代器: 它是一个用于访问和遍历集合中项的接口或抽象类。

  • 具体迭代器: 它是迭代器接口的一个实现。

  • 聚合: 它是一个用于创建迭代器对象的接口。

  • 具体聚合: 它是聚合接口的实现,它实现了迭代器创建接口,以返回适当的具体迭代器的实例。

Spring 框架中的迭代器设计模式

Spring 框架还通过CompositeIterator类扩展了迭代器模式。主要这个模式用于 Java 集合框架中按顺序迭代元素。

现在我们已经看到了迭代器设计模式,让我们转向它的一个不同变体--观察者设计模式。

观察者 设计模式

定义对象之间的一对多依赖关系,以便当一个对象改变状态时,所有依赖对象都会被通知并自动更新

  • GOF 设计模式

观察者模式是非常常见的设计模式之一,这种模式是 GOF 模式中行为设计模式家族的一部分,它处理应用程序中对象的责任以及它们在运行时如何相互通信。根据这种模式,有时对象会在应用程序中的对象之间建立一对多的关系,即如果一个对象被修改,它将自动通知其他依赖对象。

例如,Facebook 的帖子评论是观察者设计模式的一个例子。如果你评论了你朋友的帖子,那么每次有人再次评论同一帖子时,你都会收到这个帖子的通知。

观察者模式提供了解耦对象之间的通信。它使对象之间的关系主要是一对多的关系。在这个模式中,有一个被称为主题的对象。每当这个主题的状态发生变化时,它将相应地通知其依赖对象列表。这个依赖对象列表被称为观察者。以下图示说明了观察者模式:

观察者设计模式的使用场景

使用观察者模式的以下列表列出了其优点:

  • 这种模式为主体和观察者之间提供了解耦的关系

  • 它提供了广播支持

让我们看看以下 UML 图展示了观察者设计模式的所有组件:

观察者设计模式的 UML 图

  • 主题(Subject):它是一个接口。它知道其观察者的信息。

  • 具体主题(ConcreteSubject):它是主题的具体实现,它在其状态变化时知道所有需要通知的观察者。

  • 观察者(Observer):它是一个接口,用于通知主题的变化。

  • 具体观察者(ConcreteObserver):它是观察者的具体实现,它保持其状态与主题状态的一致性。

Spring 框架中的观察者模式

在 Spring 框架中,观察者设计模式用于实现 ApplicationContext 的事件处理功能。Spring 提供了 ApplicationEvent 类和 ApplicationListener 接口,以在 Spring ApplicationContext 中启用事件处理。任何实现 ApplicationListener 接口的 Spring 应用程序中的 bean,每当事件发布者发布 ApplicationEvent 时,它都会收到一个 ApplicationEvent。在这里,事件发布者是主题,实现 ApplicationListener 的 bean 是观察者。

现在我们已经了解了观察者设计模式,让我们转向它的一个不同变体——模板设计模式。

模板设计模式

在操作中定义算法的骨架,将一些步骤推迟到子类。模板方法允许子类重新定义算法的某些步骤,而不改变算法的结构。

60; -GOF 设计模式

在模板设计模式中,一个抽象类封装了一些定义到其方法中。该方法允许你在不重写它的前提下覆盖方法的部分。你可以使用其具体类将类似类型的行为应用于你的应用程序。这个设计模式属于 GOF 模式的行为设计模式家族。

以下列出了使用模板模式的益处:

  • 通过重用代码,它减少了应用程序中的样板代码。

  • 此模式创建了一个模板或方式,用于重用多个类似算法以执行某些业务需求。

让我们看看以下 UML 图展示了模板设计模式的组件:

模板设计模式的 UML 图

  • 抽象类(AbstractClass):这是一个包含定义算法骨架的模板方法的抽象类。

  • 具体类(ConcreteClass):这是一个具体子类,实现了执行算法特定基本步骤的操作。

让我们看看下一节关于企业分布式应用中的 J2EE 设计模式。

JEE 设计模式

它是设计模式的主要类别之一。通过应用 Java EE 设计模式,应用设计可以极大地简化。Java EE 设计模式已在 Sun 的 Java Blueprints 中进行了记录。这些 Java EE 设计模式提供了经过时间考验的解决方案指南和最佳实践,用于指导 Java EE 应用程序不同层中的对象交互。这些设计模式特别关注以下列出的层:

  • 展示层的设计模式

  • 业务层的设计模式

  • 集成层的设计模式

这些设计模式特别关注以下列出的层。

  • 展示层的设计模式

    • 视图助手(View Helper):它将视图与企业 J2EE 应用程序的业务逻辑分离。

    • 前端控制器(Front Controller):它提供了一个处理所有传入 J2EE Web 应用程序请求的单一点,它将请求转发到特定的应用程序控制器以访问表示层资源。

    • 应用程序控制器(Application Controller):请求实际上由应用程序控制器处理,它充当前端控制器助手。它负责与业务模型和视图组件的协调。

    • 分发视图(Dispatcher View):它与视图相关,它执行时没有业务逻辑,为下一个视图准备响应。

    • 拦截过滤器(Intercepting filters) -在 J2EE Web 应用程序中,你可以配置多个拦截器来预处理和后处理用户的请求,例如跟踪和审计用户的请求。

  • 业务层的设计模式:

    • 业务代表-它作为应用程序控制器和业务逻辑之间的桥梁。

    • 应用服务-它提供业务逻辑,以实现模型作为简单的 Java 对象,用于表示层

  • 集成层的设计模式:

    • 数据访问对象-它是为了访问业务数据而实现的,它将企业应用程序中的数据访问逻辑与业务逻辑分开。

    • Web 服务断路器-它封装了访问外部应用程序资源的逻辑,并且作为 Web 服务公开。

摘要

在阅读本章之后,读者现在应该对 GOF 设计模式和它们的最佳实践有一个很好的了解。我强调了如果你在企业的应用程序中没有实现设计模式会出现的各种问题,以及 Spring 如何通过使用许多设计模式和良好的实践来创建应用程序来解决这些问题。在前一章中,我也提到了 GOF 设计模式的三个主要类别,如创建型设计模式;它对于创建对象实例很有用,并且可以通过工厂、抽象工厂、建造者、原型和单例模式以特定方式在企业的应用程序创建时间应用一些约束。第二个主要类别是结构型设计模式,它通过处理类或对象的组合来设计企业的应用程序结构,从而减少应用程序的复杂性,并提高应用程序的可重用性和性能。适配器模式、桥接模式、组合模式、装饰器模式和外观模式都属于这个模式类别。最后,还有一个主要的设计模式类别是行为型设计模式,它描述了类或对象之间交互和分配责任的方式。属于这个类别的模式特别关注对象之间的通信。

第四章:使用依赖注入模式连接 Wiring Beans

在上一章中,你通过示例和使用案例学习了四人帮GOF)设计模式。现在,我们将更详细地探讨在 Spring 应用程序中注入 Bean 和配置依赖项,你将看到配置 Spring 应用程序依赖项的各种方法。这包括使用 XML、注解、Java 和混合配置。

每个人都喜欢看电影,对吧?嗯,如果不喜欢电影,那戏剧、剧集或者剧院呢?有没有想过如果团队成员之间不交流会发生什么?我说的是团队,不仅仅是演员,还有布景团队、化妆人员、视听人员、音响系统人员等等。不用说,每个成员都对最终产品有重要的贡献,并且这些团队之间需要大量的协调。

一部大片是数百人共同努力实现共同目标的产品。同样,优秀的软件是几个对象共同工作以满足某些商业目标的应用程序。作为一个团队,每个对象都必须意识到其他对象,并相互沟通以完成他们的工作。

在银行系统中,转账服务必须了解账户服务,账户服务必须了解账户存储库,等等。所有这些组件共同工作,使银行系统可行。在第一章,使用框架 5.0 和设计模式入门中,你看到了使用传统方法创建的相同银行示例,即使用构造和直接对象初始化创建对象。这种传统方法导致代码复杂,难以重用和单元测试,并且与其他代码高度耦合。

但在 Spring 中,对象有责任在没有必要寻找和创建他们工作中所需的依赖对象的情况下完成他们的工作。Spring 容器负责寻找或创建其他依赖对象,并与它们的依赖项协作。在之前的银行系统示例中,转账服务依赖于账户服务,但它不必创建账户服务,因此依赖项由容器创建,并传递给应用程序中的依赖对象。

在本章中,我们将讨论基于 Spring 的应用程序背后的故事,参考依赖注入DI)模式,以及它是如何工作的。在本章结束时,你将了解你的基于 Spring 的应用程序中的对象是如何相互建立关联的,以及 Spring 是如何为完成任务连接这些对象的。你还将学习许多在 Spring 中连接 Bean 的方法。

本章将涵盖以下主题:

  • 依赖注入模式

  • 依赖注入模式的类型

  • 使用抽象工厂模式解决依赖关系

  • 查找方法注入模式

  • 使用工厂模式配置 Bean

  • 配置依赖

  • 在应用程序中配置依赖的常见最佳实践

依赖注入模式

在任何企业应用程序中,为了实现业务目标,工作对象之间的协调非常重要。应用程序中对象之间的关系代表了对象的依赖性,因此每个对象都会通过应用程序中依赖对象的协调来完成工作。这种对象之间所需的依赖关系往往很复杂,并且与应用程序中的紧密耦合编程相关。Spring 通过使用依赖注入模式提供了解决应用程序紧密耦合代码的方案。依赖注入是一种设计模式,它促进了应用程序中松耦合的类。这意味着系统中的类依赖于其他类的行为,而不是依赖于类的对象实例化。依赖注入模式还促进了面向接口编程而不是面向实现编程。对象依赖应该基于接口,而不是具体类,因为松耦合的结构为你提供了更大的可重用性、可维护性和可测试性。

使用依赖注入模式解决问题

在任何企业应用程序中,一个常见的问题是如何配置和连接不同的元素以实现业务目标——例如,如何将不同成员编写的 Web 层控制器与服务和仓库接口绑定在一起,而不了解 Web 层的控制器。因此,有一些框架通过使用轻量级容器组装来自不同层的组件来解决这个问题。这类框架的例子有 PicoContainer 和 Spring 框架。

PicoContainer 和 Spring 容器使用多种设计模式来解决不同层不同组件的组装问题。在这里,我将讨论其中之一——依赖注入模式。依赖注入为我们提供了一个解耦和松耦合的系统。它确保了依赖对象的构建。在下面的例子中,我们将演示依赖注入模式如何解决与不同层组件协作相关的常见问题。

没有依赖注入

在下面的 Java 示例中,首先让我们看看两个类之间的依赖关系是什么?请看以下类图:

图片

TransferService 方法 transferAmount()与 AccountRepository 和 TransferRepository 有依赖关系,这些依赖关系是通过直接实例化仓库类实现的。

如前图所示,TransferService类包含两个成员变量,AccountRepositoryTransferRepository。这些变量由TransferService构造函数初始化。TransferService控制使用哪个仓库实现。它还控制它们的构建。在这种情况下,TransferService被认为在以下示例中有一个硬编码的依赖:

下面是TransferServiceImpl.java文件:

    public class TransferServiceImpl implements TransferService { 
      AccountRepository accountRepository; 
      TransferRepository transferRepository; 
      public TransferServiceImpl(AccountRepository accountRepository, 
      TransferRepository transferRepository) { 
        super(); 
        // Specify a specific implementation in the constructor 
        instead of using dependency injection
        this.accountRepository = new JdbcAccountRepository(); 
        this.transferRepository = new JdbcTransferRepository(); 
      } 
      // Method within this service that uses the accountRepository and 
      transferRepository
      @Override 
      public void transferAmmount(Long a, Long b, Amount amount) { 
        Account accountA = accountRepository.findByAccountId(a); 
        Account accountB = accountRepository.findByAccountId(b); 
        transferRepository.transfer(accountA, accountB, amount); 
      } 
    }   

在前面的例子中,TransferServiceImpl类依赖于两个类,即AccountRepositoryTransferRepositoryTransferServiceImpl类有两个依赖类的成员变量,并通过其构造函数使用 JDBC 实现的仓库,如JdbcAccountRepositoryJdbcTransferRepository来初始化它们。TransferServiceImpl类与仓库的 JDBC 实现紧密耦合;如果将 JDBC 实现更改为 JPA 实现,您还必须更改您的TransferServiceImpl类。

根据 SOLID 原则(单一职责原则、开闭原则、里氏替换原则、接口隔离原则、依赖倒置原则),一个类在应用中应该只有一个职责,但在前面的例子中,TransferServiceImpl类还负责构建JdbcAccountRepositoryJdbcTransferRepository类的对象。我们无法在类中使用直接实例化对象。

在我们第一次尝试避免在TransferServiceImpl类中直接实例化逻辑时,我们可以使用一个创建TransferServiceImpl实例的Factory类。根据这个想法,TransferServiceImpl最小化了AccountRepositoryTransferRepository的依赖——之前我们有一个紧密耦合的仓库实现,但现在它只引用接口,如以下图所示:

图片

TransferService 类在transferAmount()方法中依赖于AccountRepositoryTransferRepository,使用的是仓库类的Factory

但是,TransferServiceImpl类再次与RepositoryFactory类的实现紧密耦合。此外,这个过程不适合我们拥有更多依赖项的情况,这会增加Factory类或Factory类的复杂性。仓库类也可能有其他依赖。

以下代码使用Factory类来获取AccountRepositoryTransferRepository类:

下面是TransferServiceImpl.java文件:

    package com.packt.patterninspring.chapter4.bankapp.service;
    public class TransferServiceImpl implements TransferService { 
      AccountRepository accountRepository; 
      TransferRepository transferRepository; 
      public TransferServiceImpl(AccountRepository accountRepository,
      TransferRepository transferRepository) { 
        this.accountRepository = RepositoryFactory.getInstance(); 
        this.transferRepository = RepositoryFactory.getInstance(); 
      }
    @Override
    public void transferAmount(Long a, Long b, Amount amount) { 
      Account accountA = accountRepository.findByAccountId(a); 
      Account accountB = accountRepository.findByAccountId(b); 
      transferRepository.transfer(accountA, accountB, amount); 
    } 
   } 

在前面的代码示例中,我们已经最小化了紧密耦合,并从TransferServiceImpl类中移除了直接对象实例化,但这不是最佳解决方案。

使用依赖注入模式

工厂模式避免了直接实例化一个类的对象,我们还需要创建另一个模块来负责连接类之间的依赖。这个模块被称为 依赖注入器,它基于 控制反转IoC)模式。根据 IoC 框架,容器负责对象的实例化,以及解决应用程序中类之间的依赖。这个模块为其作用域下定义的对象具有自己的构建和销毁生命周期。

在以下图中,我们使用了依赖注入模式来解决 TransferServiceImpl 类的依赖:

使用依赖注入设计模式来解决 TransferService 的依赖。

在以下示例中,我们使用了一个接口来解决依赖:

以下为 TransferServiceImpl.java 文件:

    package com.packt.patterninspring.chapter4.bankapp.service; 
    public class TransferServiceImpl implements TransferService { 
      AccountRepository accountRepository; 
      TransferRepository transferRepository; 
      public TransferServiceImpl(AccountRepository accountRepository, 
      TransferRepository transferRepository) { 
        this.accountRepository = accountRepository; 
        this.transferRepository = transferRepository; 
     } 
     @Override 
     public void transferAmmount(Long a, Long b, Amount amount) { 
       Account accountA = accountRepository.findByAccountId(a); 
       Account accountB = accountRepository.findByAccountId(b); 
       transferRepository.transfer(accountA, accountB, amount); 
     } 
    } 

TransferServiceImpl 类中,我们将 AccountRepositoryTransferRepository 接口的引用传递给了构造函数。现在 TransferServiceImpl 类与实现仓库类的实现(使用任何风味,无论是 JDBC 还是 JPA 仓库接口的实现)松散耦合,框架负责将依赖项与相关的依赖类连接起来。松耦合为我们提供了更高的可重用性、可维护性和可测试性。

Spring 框架实现了依赖注入模式来解决 Spring 应用程序中类之间的依赖。Spring DI 基于 IoC 概念,即 Spring 框架有一个容器,它创建、管理和销毁对象;它被称为 Spring IoC 容器。位于 Spring 容器内的对象被称为 Spring beans。在 Spring 应用程序中连接 beans 的方式有很多。让我们看看配置 Spring 容器的三种最常见方法。

在以下部分,我们将探讨依赖注入模式的类型;你可以使用其中任何一个来配置依赖。

依赖注入模式的类型

以下是可以注入到你的应用程序中的依赖注入类型:

  • 基于构造函数的依赖注入

  • 基于设置器的依赖注入

基于构造函数的依赖注入模式

依赖注入是一种设计模式,用于解决依赖类之间的依赖关系,而依赖关系不过是对象属性。注入器必须通过使用构造函数注入或设置器注入中的一种方式来为依赖对象构造。构造函数注入是在创建对象时满足这些对象属性的一种方式,以实例化对象。一个对象有一个公共构造函数,它接受依赖类作为构造函数参数以注入依赖。你可以在依赖类中声明多个构造函数。以前,仅使用 PicoContainer 框架进行基于构造函数的依赖注入来解决问题。目前,Spring 框架也支持构造函数注入来解决依赖关系。

构造函数注入模式的优点

如果你在 Spring 应用程序中使用构造函数注入,以下是一些优点:

  • 基于构造函数的依赖注入更适合强制依赖,并且它创建了一个强依赖合同

  • 基于构造函数的依赖注入比其他方式提供了更紧凑的代码结构

  • 它支持通过将作为构造函数参数传递给依赖类的依赖项进行测试

  • 它倾向于使用不可变对象,并且不会破坏信息隐藏原则

构造函数注入模式的缺点

以下是基于构造函数的这种注入模式的唯一缺点:

  • 它可能会导致循环依赖。(循环依赖意味着依赖类和依赖类之间也是相互依赖的,例如,类 A 依赖于类 B,而类 B 也依赖于类 A)

基于构造函数的依赖注入模式示例

让我们看看以下基于构造函数的依赖注入的示例。在以下代码中,我们有一个 TransferServiceImpl 类,它的构造函数接受两个参数:

    public class TransferServiceImpl implements TransferService { 
      AccountRepository accountRepository; 
      TransferRepository transferRepository; 
      public TransferServiceImpl(AccountRepository accountRepository,    
      TransferRepository transferRepository) { 
        this.accountRepository = accountRepository; 
        this.transferRepository = transferRepository; 
      } 
      // ... 
    }

仓库也将由 Spring 容器管理,因此容器将按照以下方式将用于数据库配置的 datasource 对象注入到它们中:

以下是 JdbcAccountRepository.java 文件:

    public class JdbcAccountRepository implements AccountRepository{ 
      JdbcTemplate jdbcTemplate; 
      public JdbcAccountRepository(DataSource dataSource) { 
        this.jdbcTemplate = new JdbcTemplate(dataSource); 
      } 
      // ... 
    }

以下是 JdbcTransferRepository.java 文件:

    public class JdbcTransferRepository implements TransferRepository{ 
      JdbcTemplate jdbcTemplate; 
      public JdbcTransferRepository(DataSource dataSource) { 
        this.jdbcTemplate = new JdbcTemplate(dataSource); 
      } 
       // ... 
    }

你可以在前面的代码中看到,作为 AccountRepositoryTransferRepository 的仓库的 JDBC 实现。这些类也具有一个参数的构造函数,用于通过 DataSource 类注入依赖。

让我们看看在企业应用程序中实现依赖注入的另一种方式,即设置器注入。

基于设置器的依赖注入

容器注入器有另一种方式来连接依赖对象。在 setter 注入中,满足这些依赖关系的一种方式是在依赖类中提供一个 setter 方法。对象有公开的 setter 方法,该方法接受依赖类作为方法参数以注入依赖项。对于基于 setter 的依赖注入,依赖类的构造器不是必需的。如果你更改依赖类的依赖项,不需要进行任何更改。Spring 框架和 PicoContainer 框架支持 setter 注入以解决依赖关系。

setter 注入的优势

如果你在 Spring 应用程序中使用 setter 注入模式,以下是其优势:

  • Setter 注入比构造器注入更易读

  • Setter 注入解决了应用程序中的循环依赖问题

  • Setter 注入允许在尽可能晚的时候创建昂贵的资源或服务,并且仅在需要时

  • Setter 注入不需要更改构造器,但依赖项通过公开的属性传递,这些属性是公开的

setter 注入的缺点

以下为 setter 注入模式的缺点:

  • 在 setter 注入模式中,安全性较低,因为它可以被覆盖

  • 基于 setter 的依赖注入不提供与构造器注入一样紧凑的代码结构

  • 在使用 setter 注入时,务必小心,因为它不是必需的依赖项

基于 setter 的依赖注入示例

让我们看看以下基于 setter 的依赖注入的示例。以下TransferServiceImpl类,具有一个参数的 setter 方法,该参数为存储库类型:

以下为TransferServiceImpl.java文件:

    public class TransferServiceImpl implements TransferService { 
      AccountRepository accountRepository; 
      TransferRepository transferRepository; 

      public void setAccountRepository(AccountRepository 
      accountRepository) { 
        this.accountRepository = accountRepository; 
      } 
      public void setTransferRepository(TransferRepository  
      transferRepository) { 
        this.transferRepository = transferRepository; 
      } 
      // ... 
    } 

同样,让我们定义以下存储库实现的 setter,如下所示:

以下为JdbcAccountRepository.java文件:

    public class JdbcAccountRepository implements AccountRepository{ 
      JdbcTemplate jdbcTemplate; 
      public setDataSource(DataSource dataSource) { 
        this.jdbcTemplate = new JdbcTemplate(dataSource); 
    } 
     // ... 
   } 

以下为JdbcTransferRepository.java文件:

    public class JdbcTransferRepository implements TransferRepository{ 
      JdbcTemplate jdbcTemplate; 
      public setDataSource(DataSource dataSource) { 
        this.jdbcTemplate = new JdbcTemplate(dataSource); 
   } 
    // ... 
  } 

你可以在前面的代码中看到作为AccountRepositoryTransferRepository的存储库的 JDBC 实现。这些类有一个 setter 方法,接受一个参数以注入DataSource类的依赖项。

构造器注入与 setter 注入及最佳实践

Spring 框架为这两种依赖注入模式提供支持。构造器和 setter 注入模式都在系统中组装元素。setter 注入和构造器注入之间的选择取决于你的应用程序需求和处理的问题。让我们看看以下表格,其中列出了构造器和 setter 注入之间的一些差异,以及一些选择适合你应用程序的最佳实践。

构造器注入 setter 注入
具有构造函数参数的类;有时它非常紧凑,并且清楚地表明它创建的内容。 这里,对象被构建了,但并不清楚其属性是否已初始化。
当依赖项是必需的时候,这是一个更好的选择。 当依赖项不是必需的时候,这更适合。
它允许你隐藏不可变的对象属性,因为它没有为这些对象属性提供 setter。为了确保对象的不可变性,请使用构造函数注入模式而不是 setter 注入。 它不能确保对象的不可变性。
它会在你的应用程序中创建循环依赖。 它解决了应用程序中循环依赖的问题。在这种情况下,setter 注入比构造函数更好。
在应用中,这不适用于标量值依赖。 如果你有字符串和整数等简单的参数作为依赖项,则使用 setter 注入更好,因为每个 setter 名称都表明了该值应该做什么。

在下一节中,你将学习如何配置注入器以查找 bean 并将它们连接起来,以及注入器如何管理 bean。在这里,我将使用 Spring 配置来实现依赖注入模式。

使用 Spring 配置依赖注入模式

在本节中,我将解释在应用程序中配置依赖关系所需的过程。主流的注入器有 Google Guice、Spring 和 Weld。在本章中,我使用 Spring 框架,因此,我们将在这里看到 Spring 配置。以下图表是 Spring 工作的高级视图:

使用依赖注入模式如何工作

在前面的图表中,配置指令是应用程序的元配置。在这里,我们在你的应用程序类(POJOs)中定义依赖关系,初始化 Spring 容器,通过结合 POJOs 和配置指令来解析依赖关系,最终,你将有一个完全配置和可执行的系统或应用程序。

正如你在前面的图表中所看到的,Spring 容器创建你的应用程序中的 bean,并通过 DI 模式组装它们之间的关系。Spring 容器根据我们提供给框架的配置来创建 bean,因此,告诉 Spring 创建哪些 bean 以及如何将它们连接在一起是你的责任。

Spring 在配置 Spring bean 的依赖关系方面非常灵活。以下是你应用程序元数据配置的三个方法:

  1. 基于 Java 配置的依赖注入模式——这是一个在 Java 中显式配置。

  2. 基于注解配置的依赖注入模式——这是一个隐式 bean 发现和自动连接。

  3. 基于 XML 的依赖注入模式——它是在 XML 中的显式配置。

Spring 提供了三种在 Spring 中连接豆的选择。你必须选择其中之一,但没有一个选择是任何应用程序的最佳匹配。这取决于你的应用程序,你也可以将这些选择混合匹配到一个单一的应用程序中。现在让我们详细讨论基于 Java 配置的依赖注入模式。

基于 Java 配置的依赖注入模式

从 Spring 3.0 开始,它提供了一个基于 Java 的 Spring 配置来连接 Spring 豆类。看看以下基于 Java 的配置类 (AppConfig.java),以定义 Spring 豆类及其依赖关系。基于 Java 的配置对于依赖注入是一个更好的选择,因为它更强大且类型安全。

创建一个 Java 配置类 - AppConfig.java

让我们为我们的示例创建一个 AppConfig.java 配置类:

    package com.packt.patterninspring.chapter4.bankapp.config; 
    import org.springframework.context.annotation.Configuration; 
    @Configuration 
    public class AppConfig { 
         //.. 
    } 

前面的 AppConfig 类被 @Configuration 注解标记,这表示它是一个包含豆定义详细信息的应用程序配置类。该文件将由 Spring 应用程序上下文加载以创建应用程序的豆。

现在我们来看看如何在 AppConfig 中声明 TransferServiceAccountRepositoryTransferRepository 这三个豆类。

在配置类中声明 Spring 豆

在基于 Java 的配置中声明豆时,你必须编写一个在配置类中创建所需类型对象的方 法,并使用 @Bean 注解该方法。让我们看看在 AppConfig 类中做出的以下更改来声明豆类:

    package com.packt.patterninspring.chapter4.bankapp.config; 
    import org.springframework.context.annotation.Bean; 
    import org.springframework.context.annotation.Configuration; 
    @Configuration 
    public class AppConfig { 
      @Bean 
      public TransferService transferService(){ 
        return new TransferServiceImpl(); 
      } 
     @Bean 
     public AccountRepository accountRepository() { 
       return new JdbcAccountRepository(); 
     } 
     @Bean 
     public TransferRepository transferRepository() { 
       return new JdbcTransferRepository(); 
     } 
   } 

在前面的配置文件中,我声明了三个方法来创建 TransferServiceAccountRepositoryTransferRepository 的实例。这些方法被 @Bean 注解标记,表示它们负责实例化、配置和初始化一个将被 Spring IoC 容器管理的新对象。容器中的每个豆都有一个唯一的豆 ID;默认情况下,豆的 ID 与 @Bean 注解的方法名相同。在前面的例子中,豆将被命名为 transferServiceaccountRepositorytransferRepository。你也可以通过使用 @Bean 注解的 name 属性来覆盖默认行为,如下所示:

    @Bean(name="service") 
    public TransferService transferService(){ 
     return new TransferServiceImpl(); 
    } 

现在 "service" 是该豆 TransferService 的豆名。

让我们看看如何在 AppConfig 中为 TransferServiceAccountRepositoryTransferRepository 豆类注入依赖关系。

注入 Spring 豆类

在前面的代码中,我声明了 TransferServiceAccountRepositoryTransferRepository 这三个豆类;这些豆类没有依赖关系。但实际上,TransferService 豆类依赖于 AccountRepositoryTransferRepository。让我们看看在 AppConfig 类中做出的以下更改来声明这些豆类:

    package com.packt.patterninspring.chapter4.bankapp.config; 
    import org.springframework.context.annotation.Bean; 
    import org.springframework.context.annotation.Configuration; 
    @Configuration 
    public class AppConfig { 
      @Bean 
      public TransferService transferService(){ 
        return new TransferServiceImpl(accountRepository(), 
        transferRepository()); 
     } 
     @Bean 
     public AccountRepository accountRepository() { 
       return new JdbcAccountRepository(); 
     } 
     @Bean 
     public TransferRepository transferRepository() { 
       return new JdbcTransferRepository(); 
     } 
    } 

在前面的示例中,在基于 Java 的配置中连接豆子的最简单方法是通过引用所引用的 bean 的方法。transferService()方法通过调用接受AccountRepositoryTransferRepository作为参数的构造函数来构建TransferServiceImpl类的实例。在这里,似乎TransferServiceImpl类的构造函数通过调用accountRepository()transferRepository()方法来分别创建AccountRepositoryTransferRepository的实例,但这并不是实际创建实例的调用。Spring 容器创建了AccountRepositoryTransferRepository的实例,因为accountRepository()transferRepository()方法被注解了@Bean。任何其他 bean 方法对 bean 方法的调用都将被 Spring 拦截,以确保通过该方法返回 Spring beans 的默认单例作用域(这将在第五章,理解 Bean 生命周期和使用的模式)而不是允许它再次被调用。

使用 Java 配置依赖注入模式的最佳方法

在前面的配置示例中,我声明了transferService()bean 方法通过使用其参数构造函数来构建TransferServiceImpl类的实例。bean 方法accountRepository()transferRepository()作为构造函数的参数传递。但在企业应用中,许多配置文件依赖于应用程序架构的层。假设服务层和基础设施层有自己的配置文件。这意味着accountRepository()transferRepository()方法可能位于不同的配置文件中,而transferService()bean 方法可能位于另一个配置文件中。将 bean 方法传递给构造函数不是使用 Java 进行依赖注入模式配置的好做法。让我们看看配置依赖注入的最佳不同方法:

    package com.packt.patterninspring.chapter4.bankapp.config; 
    import org.springframework.context.annotation.Bean; 
    import org.springframework.context.annotation.Configuration; 
    @Configuration 
    public class AppConfig { 
      @Bean 
      public TransferService transferService(AccountRepository 
      accountRepository, TransferRepository transferRepository){ 
        return new TransferServiceImpl(accountRepository, 
        transferRepository); 
     } 
     @Bean 
     public AccountRepository accountRepository() { 
       return new JdbcAccountRepository(); 
     } 
     @Bean 
     public TransferRepository transferRepository() { 
       return new JdbcTransferRepository(); 
     } 
    } 

在前面的代码中,transferService()方法请求AccountRepositoryTransferRepository作为参数。当 Spring 调用transferService()来创建TransferServicebean 时,它会自动将AccountRepositoryTransferRepository注入到配置方法中。使用这种方法,transferService()方法仍然可以将AccountRepositoryTransferRepository注入到TransferServiceImpl的构造函数中,而不需要明确引用accountrepository()transferrepository()``@Bean方法。

现在我们来看看基于 XML 的配置的依赖注入模式。

基于 XML 的配置的依赖注入模式

Spring 从一开始就提供了基于 XML 的配置依赖注入。这是配置 Spring 应用程序的主要方式。据我所知,每个开发者都应该了解如何使用 XML 与 Spring 应用程序一起使用。在本节中,我将参考基于 XML 的配置,解释与前面基于 Java 配置的章节中讨论的相同示例。

创建 XML 配置文件

在基于 Java 配置的章节中,我们创建了一个带有 @Configuration 注解的 AppConfig 类。同样,对于基于 XML 的配置,我们现在将创建一个以 <beans> 元素为根的 applicationContext.xml 文件。以下最简单的示例显示了基于 XML 的配置元数据的基本结构:

下面的 applicationContext.xml 文件:

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

     xsi:schemaLocation="http://www.springframework.org/schema/beans   
     http://www.springframework.org/schema/beans/spring-beans.xsd"> 

     <!-- Configuration for bean definitions go here --> 

    </beans> 

前面的 XML 文件是应用程序的配置文件,其中包含有关 Bean 定义的具体信息。此文件也由 ApplicationContext 的 XML 风格实现加载,以为您应用程序创建 Bean。让我们看看如何在前面提到的 XML 文件中声明 TransferServiceAccountRepositoryTransferRepository Bean。

在 XML 文件中声明 Spring Bean

与 Java 一样,我们必须通过使用 Spring 的基于 XML 的配置中的 Spring-beans 架构元素作为 <bean> 元素来将一个类声明为 Spring Bean。<bean> 元素是 JavaConfig 的 @Bean 注解的 XML 类似物。我们向基于 XML 的配置文件添加以下配置:

    <bean id="transferService"    
     class="com.packt.patterninspring.chapter4.
     bankapp.service.TransferServiceImpl"/> 
    <bean id="accountRepository"   
     class="com.packt.patterninspring.chapter4.
     bankapp.repository.jdbc.JdbcAccountRepository"/> 
    <bean id="transferService"   
     class="com.packt.patterninspring.chapter4.
     bankapp.repository.jdbc.JdbcTransferRepository"/> 

在前面的代码中,我创建了一个非常简单的 Bean 定义。在这个配置中,<bean> 元素有一个 id 属性来标识单个 Bean 定义。class 属性表示为完全限定的类名,以创建此 Bean。id 属性的值指的是协作对象。因此,让我们看看如何配置协作 Bean 以解决应用程序中的依赖关系。

注入 Spring Bean

Spring 提供了这两种方式来定义 DI 模式,以在应用程序中将依赖项注入到依赖 Bean 中:

  • 使用构造函数注入

  • 使用设置器注入

使用构造函数注入

对于构造函数注入的 DI 模式,Spring 提供了两个基本选项,即 <constructor-arg> 元素和 Spring 3.0 中引入的 c-namespace。c-namespace 在应用程序中具有更少的冗余,这是它们之间的唯一区别--你可以选择任何一个。以下是如何使用构造函数注入注入协作 Bean 的示例:

    <bean id="transferService"   
     class="com.packt.patterninspring.chapter4.
     bankapp.service.TransferServiceImpl"> 
     <constructor-arg ref="accountRepository"/> 
     <constructor-arg ref="transferRepository"/> 
    </bean> 
    <bean id="accountRepository"    
     class="com.packt.patterninspring.chapter4.
     bankapp.repository.jdbc.JdbcAccountRepository"/> 
    <bean id="transferRepository"       
     class="com.packt.patterninspring.chapter4.
     bankapp.repository.jdbc.JdbcTransferRepository"/> 

在前面的配置中,TransferService<bean> 元素有两个 <constructor-arg>。这表示它需要将 ID 为 accountRepositorytransferRepository 的 Bean 的引用传递给 TransferServiceImpl 的构造函数。

截至 Spring 3.0,c-namespace 类似地,在 XML 中表达构造函数参数有更简洁的方式。为了使用此命名空间,我们必须在 XML 文件中添加其模式,如下所示:

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

     xsi:schemaLocation="http://www.springframework.org/schema/beans
     http://www.springframework.org/schema/beans/spring-beans.xsd"> 

    <bean id="transferService"    
     class="com.packt.patterninspring.chapter4.
     bankapp.service.TransferServiceImpl"  
     c:accountRepository-ref="accountRepository" c:transferRepository-
     ref="transferRepository"/> 
    <bean id="accountRepository"    
     class="com.packt.patterninspring.chapter4.
     bankapp.repository.jdbc.JdbcAccountRepository"/> 
    <bean id="transferRepository"    
     class="com.packt.patterninspring.chapter4.
     bankapp.repository.jdbc.JdbcTransferRepository"/> 

     <!-- more bean definitions go here --> 

    </beans> 

让我们看看如何使用设置注入来设置这些依赖项。

使用设置注入

使用注入,Spring 还提供了两种基本选项,即 <property> 元素和 Spring 3.0 中引入的 p-namespace。p-namespace 还减少了应用程序中的代码冗余,这是它们之间的唯一区别,您可以选择任何一个。让我们按照以下方式使用设置注入注入协作 bean:

    <bean id="transferService"       
     class="com.packt.patterninspring.chapter4.
     bankapp.service.TransferServiceImpl"> 
     <property name="accountRepository"  ref="accountRepository"/> 
     <property name="transferRepository" ref="transferRepository"/> 
    </bean> 
    <bean id="accountRepository"      
     class="com.packt.patterninspring.chapter4.
     bankapp.repository.jdbc.JdbcAccountRepository"/> 
    <bean id="transferRepository"   
     class="com.packt.patterninspring.chapter4.
     bankapp.repository.jdbc.JdbcTransferRepository"/> 

在前面的配置中,TransferService<bean> 元素有两个 <property> 元素,它告诉它将 accountRepositorytransferRepository 这两个 ID 的 bean 的引用传递给 TransferServiceImpl 的设置方法,如下所示:

    package com.packt.patterninspring.chapter4.bankapp.service; 

    import com.packt.patterninspring.chapter4.bankapp.model.Account; 
    import com.packt.patterninspring.chapter4.bankapp.model.Amount; 
    import com.packt.patterninspring.chapter4.bankapp.
     repository.AccountRepository; 
    import com.packt.patterninspring.chapter4.bankapp.
     repository.TransferRepository; 

    public class TransferServiceImpl implements TransferService { 
      AccountRepository accountRepository; 
      TransferRepository transferRepository; 

      public void setAccountRepository(AccountRepository   
      accountRepository) { 
        this.accountRepository = accountRepository; 
      } 
      public void setTransferRepository(TransferRepository 
      transferRepository) { 
         this.transferRepository = transferRepository; 
      } 
      @Override 
      public void transferAmmount(Long a, Long b, Amount amount) { 
        Account accountA = accountRepository.findByAccountId(a); 
        Account accountB = accountRepository.findByAccountId(b); 
        transferRepository.transfer(accountA, accountB, amount); 
      } 
    } 

在前面的文件中,如果您使用此 Spring bean 而没有设置方法,则 accountRepositorytransferRepository 属性将被初始化为 null,而没有注入依赖项。

截至 Spring 3.0,p-namespace 类似地,在 XML 中表达属性有更简洁的方式。为了使用此命名空间,我们必须在 XML 文件中添加其模式,如下所示:

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

      xsi:schemaLocation="http://www.springframework.org/schema/beans 
      http://www.springframework.org/schema/beans/spring-beans.xsd"> 

    <bean id="transferService"    
     class="com.packt.patterninspring.chapter4.bankapp.
     service.TransferServiceImpl"  
     p:accountRepository-ref="accountRepository" p:transferRepository-
     ref="transferRepository"/> 
    <bean id="accountRepository"   
     class="com.packt.patterninspring.chapter4.
     bankapp.repository.jdbc.JdbcAccountRepository"/> 
    <bean id="transferRepository"   
     class="com.packt.patterninspring.chapter4.
     bankapp.repository.jdbc.JdbcTransferRepository"/> 

    <!-- more bean definitions go here --> 

    </beans> 

现在我们来看看基于注解配置的依赖注入模式。

基于注解的配置的依赖注入模式

如前两个部分所讨论的,我们定义了基于 Java 和 XML 配置的 DI 模式,这两个选项明确定义了依赖关系。它通过在 AppConfig Java 文件中使用 @Bean 注解的方法或 XML 配置文件中的 <bean> 元素标签来创建 Spring bean。通过这些方法,您还可以为那些位于应用程序之外的类创建 bean,即存在于第三方库中的类。现在让我们讨论另一种创建 Spring bean 的方法,通过使用类型注解的隐式配置来定义它们之间的依赖关系。

什么是类型注解?

Spring 框架为您提供了一些特殊的注解。这些注解用于在应用程序上下文中自动创建 Spring bean。主要的类型注解是 @Component。通过使用此注解,Spring 提供了更多类型元注解,如 @Service,用于在服务层创建 Spring bean,@Repository,用于在 DAO 层创建用于存储库的 Spring bean,以及 @Controller,用于在控制层创建 Spring bean。这在上面的图中有所展示:

通过使用这些注解,Spring 以以下两种方式创建自动连接:

  • 组件扫描:在这种情况下,Spring 会自动搜索 Spring IoC 容器中要创建的 bean

  • 自动装配:在这个中,Spring 会自动在 Spring IoC 容器中搜索 bean 依赖项

隐式地,DI 模式配置减少了应用的冗余,并最小化了显式配置。让我们通过之前讨论的相同示例来演示组件扫描和自动装配。在这里,Spring 将通过发现它们来创建 TransferServiceTransferRepositoryAccountRepository 的 bean,并按照定义的依赖关系自动将它们注入到对方。

使用 Stereotype 注解创建可自动搜索的 bean

让我们看看下面的 TransferService 接口。它的实现使用了 @Component 注解。请参考以下代码:

    package com.packt.patterninspring.chapter4.bankapp.service; 
    public interface TransferService { 
      void transferAmmount(Long a, Long b, Amount amount); 
    } 

前面的接口对于这种配置方法并不重要--我只是为了在应用中实现松耦合而取的。让我们看看它的实现,如下所示:

    package com.packt.patterninspring.chapter1.bankapp.service; 
    import org.springframework.stereotype.Component; 
    @Component 
    public class TransferServiceImpl implements TransferService { 
      @Override 
      public void transferAmmount(Long a, Long b, Amount amount) { 
         //business code here 
      } 
   } 

你可以在前面的代码中看到 TransferServiceImpl 使用了 @Component 注解。这个注解用于标识这个类为一个组件类,这意味着,它有资格被扫描并创建这个类的 bean。现在没有必要通过使用 XML 或 Java 配置来显式配置这个类作为一个 bean 了--Spring 现在负责创建 TransferServiceImpl 类的 bean,因为它使用了 @Component 注解。

如前所述,Spring 为 @Component 注解提供了元注解,如 @Service@Repository@Controller。这些注解基于应用不同层的特定责任。在这里,TransferService 是服务层类;作为 Spring 配置的最佳实践,我们必须使用特定的注解 @Service 来标注这个类,而不是使用通用的注解 @Component 来创建这个类的 bean。以下是这个类使用 @Service 注解的代码:

    package com.packt.patterninspring.chapter1.bankapp.service; 
    import org.springframework.stereotype.Service; 
    @Service 
    public class TransferServiceImpl implements TransferService { 
      @Override 
      public void transferAmmount(Long a, Long b, Amount amount) { 
         //business code here 
      } 
    } 

让我们看看应用中的其他类--这些是 AccountRepository 的实现类--以及 TransferRepository 接口是应用 DAO 层工作的仓库。*作为最佳实践**,这些类应该使用 @Repository 注解而不是像下面展示的那样使用 @Component 注解。

JdbcAccountRepository.java 实现了 AccountRepository 接口:

    package com.packt.patterninspring.chapter4.bankapp.repository.jdbc; 
    import org.springframework.stereotype.Repository; 
    import com.packt.patterninspring.chapter4.bankapp.model.Account; 
    import com.packt.patterninspring.chapter4.bankapp.model.Amount; 
    import com.packt.patterninspring.chapter4.bankapp.repository.
      AccountRepository; 
    @Repository 
    public class JdbcAccountRepository implements AccountRepository { 
      @Override 
      public Account findByAccountId(Long accountId) { 
        return new Account(accountId, "Arnav Rajput", new   
        Amount(3000.0)); 
      } 
    } 

并且 JdbcTransferRepository.java 实现了 TransferRepository 接口:

    package com.packt.patterninspring.chapter4.bankapp.repository.jdbc; 
    import org.springframework.stereotype.Repository; 
    import com.packt.patterninspring.chapter4.bankapp.model.Account; 
    import com.packt.patterninspring.chapter4.bankapp.model.Amount; 
    import com.packt.patterninspring.chapter4.bankapp.
      repository.TransferRepository; 
    @Repository 
    public class JdbcTransferRepository implements TransferRepository { 
      @Override 
      public void transfer(Account accountA, Account accountB, Amount 
      amount) { 
        System.out.println("Transfering amount from account A to B via 
        JDBC implementation"); 
      } 
    } 

在 Spring 中,你必须在你的应用中启用组件扫描,因为它默认是禁用的。你必须创建一个配置 Java 文件,并使用 @Configuration@ComponentScan 注解它。这个类用于搜索带有 @Component 注解的类,并从它们中创建 bean。

让我们看看 Spring 如何扫描带有任何 stereotypes 注解的类。

使用组件扫描搜索 bean

使用组件扫描在 Spring 应用程序中搜索豆类所需的最小配置如下:

    package com.packt.patterninspring.chapter4.bankapp.config; 

    import org.springframework.context.annotation.ComponentScan; 
    import org.springframework.context.annotation.Configuration; 

    @Configuration 
    @ComponentScan 
    public class AppConfig { 

    } 

AppConfig 类定义了一个与上一节中基于 Java 的 Spring 配置相同的 Spring 连接配置类。这里有一点需要注意--AppConfig 文件有一个额外的 @ComponentScan,因为之前它只有 @Configuration 注解。配置文件 AppConfig 被注解为 @ComponentScan 以启用 Spring 中的组件扫描。@ComponentScan 注解默认扫描与配置类同一包下被 @Component 注解的类。由于 AppConfig 类位于 com.packt.patterninspring.chapter4.bankapp.config 包中,Spring 将仅扫描此包及其子包。但我们的组件应用程序类位于 com.packt.patterninspring.chapter1.bankapp.servicecom.packt.patterninspring.chapter4.bankapp.repository.jdbc 包中,而这些不是 com.packt.patterninspring.chapter4.bankapp.config 的子包。在这种情况下,Spring 允许通过设置组件扫描的基础包来覆盖 @ComponentScan 注解的默认包扫描。让我们指定一个不同的基础包。你只需要在 @ComponentScanvalue 属性中指定包,如下所示:

    @Configuration 
    @ComponentScan("com.packt.patterninspring.chapter4.bankapp") 
    public class AppConfig { 

    } 

或者,你可以使用 basePackages 属性定义基础包,如下所示:

    @Configuration 
    @ComponentScan(basePackages="com.packt.patterninspring.
    chapter4.bankapp") 
    public class AppConfig { 

    } 

@ComponentScan 注解中,basePackages 属性可以接受一个字符串数组,这意味着我们可以定义多个基础包以扫描应用程序中的组件类。在之前的配置文件中,Spring 将扫描 com.packt.patterninspring.chapter4.bankapp 包下的所有类,以及此包下的所有子包。作为最佳实践, 总是定义组件类存在的基础包。例如,在以下代码中,我定义了服务和存储库组件的基础包:

    package com.packt.patterninspring.chapter4.bankapp.config; 
    import org.springframework.context.annotation.ComponentScan; 
    import org.springframework.context.annotation.Configuration; 
    @Configuration 
    @ComponentScan(basePackages=       
    {"com.packt.patterninspring.chapter4.
    bankapp.repository.jdbc","com.packt.patterninspring.
    chapter4.bankapp.service"}) 
    public class AppConfig { 

    } 

现在 Spring 只扫描 com.packt.patterninspring.chapter4.bankapp.repository.jdbccom.packt.patterninspring.chapter4.bankapp.service 包,以及如果存在的话,它们的子包。而不是像早期示例中那样进行广泛的扫描。

与将 @ComponentScanbasePackages 属性指定为简单的字符串值相比,Spring 允许你通过以下方式指定它们:

    package com.packt.patterninspring.chapter4.bankapp.config; 
    import org.springframework.context.annotation.ComponentScan; 
    import org.springframework.context.annotation.Configuration; 
    import com.packt.patterninspring.chapter4.bankapp.
     repository.AccountRepository; 
    import com.packt.patterninspring.chapter4.
     bankapp.service.TransferService; 
    @Configuration 
    @ComponentScan(basePackageClasses=   
    {TransferService.class,AccountRepository.class}) 
    public class AppConfig { 

    } 

如前述代码所示,basePackages 属性已被替换为 basePackageClasses。现在 Spring 将识别那些将使用 basePackageClasses 作为组件扫描基础包的包中的组件类。

它应该找到TransferServiceImplJdbcAccountRepositoryJdbcTransferRepository类,并在 Spring 容器中自动创建这些类的 bean。明确地说,没有必要为这些类定义创建 Spring bean 的方法。让我们通过 XML 配置打开组件扫描,然后你可以使用 Spring 的 context 命名空间中的<context:component-scan>元素。以下是一个启用组件扫描的最小 XML 配置:

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

    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.packt.patterninspring.chapter4.bankapp" /> 
    </beans> 

在前面的 XML 文件中,<context:component-scan>元素与基于 Java 的配置中的@ComponentScan注解相同。

为自动连接注解 bean

Spring 提供了自动 bean 连接的支持。这意味着 Spring 会通过在应用上下文中查找其他协作 bean 来自动解决依赖 bean 所需的依赖。Bean 自动连接是 DI 模式配置的另一种方式。它减少了应用程序的冗余,但配置分散在整个应用程序中。Spring 的@Autowired注解用于自动 bean 连接。这个@Autowired注解表示应该为这个 bean 执行自动连接。

在我们的例子中,我们有一个TransferService,它依赖于AccountRepositoryTransferRepository。它的构造函数被@Autowired注解,表示当 Spring 创建TransferServicebean 时,它应该通过使用注解的构造函数来实例化这个 bean,并传入两个其他 bean,即AccountRepositoryTransferRepository,它们是TransferServicebean 的依赖。让我们看看以下代码:

    package com.packt.patterninspring.chapter4.bankapp.service; 
    import org.springframework.beans.factory.annotation.Autowired; 
    import org.springframework.stereotype.Service; 
    import com.packt.patterninspring.chapter4.bankapp.model.Account; 
    import com.packt.patterninspring.chapter4.bankapp.model.Amount; 
    import com.packt.patterninspring.chapter4.bankapp.
     repository.AccountRepository; 
    importcom.packt.patterninspring.chapter4.
     bankapp.repository.TransferRepository; 
    @Service 
    public class TransferServiceImpl implements TransferService { 
      AccountRepository accountRepository; 
      TransferRepository transferRepository; 
    @Autowired 
    public TransferServiceImpl(AccountRepository accountRepository, 
    TransferRepository transferRepository) { 
      super(); 
      this.accountRepository = accountRepository; 
      this.transferRepository = transferRepository; 
    } 
    @Override 
    public void transferAmmount(Long a, Long b, Amount amount) { 
      Account accountA = accountRepository.findByAccountId(a); 
      Account accountB = accountRepository.findByAccountId(b); 
      transferRepository.transfer(accountA, accountB, amount); 
    } 
   } 

注意--截至 Spring 4.3 版本,如果你在那个类中只定义了一个带有参数的构造函数,那么就不再需要@Autowired注解。如果一个类有多个参数构造函数,那么你必须在这其中的任何一个上使用@Autowired注解。

@Autowired注解不仅限于构造函数;它可以与 setter 方法一起使用,也可以直接在字段中使用,即一个autowired类属性直接。让我们看看以下代码行,用于 setter 和字段注入。

使用 setter 方法@Autowired

在这里,你可以使用@Autowired注解来注释 setter 方法setAccountRepositorysetTransferRepository。这个注解可以用于任何方法。没有特别的原因只能用它来注释 setter 方法。请参考以下代码:

    public class TransferServiceImpl implements TransferService { 
      //... 
      @Autowired 
      public void setAccountRepository(AccountRepository  
      accountRepository) { 
        this.accountRepository = accountRepository; 
      } 
      @Autowired 
      public void setTransferRepository(TransferRepository 
      transferRepository) { 
        this.transferRepository = transferRepository; 
      } 
      //... 
    } 

使用@Autowired注解字段

你可以注释那些对于实现业务目标所必需的类属性。让我们看看以下代码:

    public class TransferServiceImpl implements TransferService { 
      @Autowired 
      AccountRepository accountRepository; 
      @Autowired 
      TransferRepository transferRepository; 
      //... 
    } 

在前面的代码中,@Autowired 注解通过 type 解析依赖关系,如果属性名称与 Spring 容器中 bean 的名称相同,则通过 name 解析。默认情况下,@Autowired 依赖关系是一个必需的依赖关系——如果依赖关系未解析,则会抛出异常,无论我们是否使用构造函数或设置方法。您可以通过使用此注解的 required 属性来覆盖 @Autowired 注解的必需行为。您可以通过将此属性设置为布尔值 false 来设置此属性,如下所示:

    @Autowired(required = false) 
    public void setAccountRepository(AccountRepository
    accountRepository) { 
      this.accountRepository = accountRepository; 
    }  

在前面的代码中,我们已经将 required 属性设置为布尔值 false。在这种情况下,Spring 将尝试执行自动装配,但如果没有匹配的 bean,它将保留 bean 未连接。但作为代码的最佳实践,您应该避免将其值设置为 false,除非绝对必要。

自动装配依赖注入模式及消除歧义

@Autowiring 注解减少了代码的冗余,但当容器中存在两个相同类型的 bean 时,可能会产生一些问题。让我们看看在这种情况下会发生什么,以下是一个示例:

    @Service 
    public class TransferServiceImpl implements TransferService { 
    @Autowired 
    public TransferServiceImpl(AccountRepository accountRepository) { 
    ... } 
    } 

上述代码片段显示,TransferServiceImpl 类依赖于一个类型为 AccountRepository 的 bean,但 Spring 容器中包含两个相同类型的 bean,即以下内容:

    @Repository 
    public class JdbcAccountRepository implements AccountRepository 
    {..} 
   @Repository 
   public class JpaAccountRepository implements AccountRepository {..} 

如前所述的代码所示,存在 AccountRepository 接口的两个实现——一个是 JdbcAccountRepository,另一个是 JpaAccountRepository。在这种情况下,Spring 容器将在应用程序启动时抛出以下异常:

    At startup: NoSuchBeanDefinitionException, no unique bean of type 
    [AccountRepository] is defined: expected single bean but found 2... 

自动装配依赖注入模式中的消除歧义

Spring 提供了一个额外的注解 @Qualifier,以克服自动装配依赖注入中的歧义问题。让我们看看以下带有 @Qualifier 注解的代码片段:

    @Service 
    public class TransferServiceImpl implements TransferService { 
    @Autowired 
    public TransferServiceImpl( @Qualifier("jdbcAccountRepository")
    AccountRepository accountRepository) { ... } 

现在我已经通过使用 @Qualifier 注解按名称而不是按类型连接了依赖关系。因此,Spring 将为 TransferServiceImpl 类搜索名为 "jdbcAccountRepository" 的 bean 依赖关系。我已经给出了以下 bean 名称:

    @Repository("jdbcAccountRepository") 
    public class JdbcAccountRepository implements AccountRepository 
    {..} 
    @Repository("jpaAccountRepository") 
    public class JpaAccountRepository implements AccountRepository {..} 

@Qualifier,也适用于方法注入和字段注入的组件名称,除非存在相同接口的两个实现,否则不应显示实现细节。

让我们现在讨论一些最佳实践,以选择适合您的 Spring 应用程序的 DI 模式配置。

使用抽象工厂模式解决依赖关系

如果你想为 Bean 添加if...else条件配置,你可以这样做,如果你使用 Java 配置,还可以添加一些自定义逻辑。但在 XML 配置的情况下,无法添加if...then...else条件。Spring 通过使用抽象工厂模式为 XML 配置中的条件提供了解决方案。使用工厂创建你想要的 Bean,并在工厂的内部逻辑中使用任何复杂的 Java 代码。

在 Spring 中实现抽象工厂模式(FactoryBean 接口)

Spring 框架提供了FactoryBean接口作为抽象工厂模式的实现。FactoryBean是一种封装有趣对象构建逻辑的模式的类。FactoryBean接口提供了一种定制 Spring IoC 容器实例化逻辑的方式。你可以为自身是工厂的对象实现此接口。实现FactoryBean接口的 Bean 会被自动检测。

此接口的定义如下:

    public interface FactoryBean<T> { 
     T getObject() throws Exception; 
     Class<T> getObjectType(); 
     boolean isSingleton(); 
   } 

根据此接口的前面定义,使用 FactoryBean 进行依赖注入会导致getObject()方法透明地被调用。isSingleton()方法对于单例返回true,否则返回falsegetObjectType()方法返回getObject()方法返回的对象类型。

Spring 中 FactoryBean 接口的实现

FactoryBean在 Spring 中被广泛使用,如下所示:

  • EmbeddedDatabaseFactoryBean

  • JndiObjectFactoryBean

  • LocalContainerEntityManagerFactoryBean

  • DateTimeFormatterFactoryBean

  • ProxyFactoryBean

  • TransactionProxyFactoryBean

  • MethodInvokingFactoryBean

FactoryBean 接口的示例实现

假设你有一个TransferService类,其定义如下:

    package com.packt.patterninspring.chapter4.bankapp.service; 
    import com.packt.patterninspring.chapter4.
     bankapp.repository.IAccountRepository; 
    public class TransferService { 
      IAccountRepository accountRepository; 
      public TransferService(IAccountRepository accountRepository){ 
        this.accountRepository = accountRepository; 
      } 
      public void transfer(String accountA, String accountB, Double 
      amount){ 
        System.out.println("Amount has been tranferred"); 
      } 
    } 

你有一个定义如下FactoryBean

    package com.packt.patterninspring.chapter4.bankapp.repository; 
    import org.springframework.beans.factory.FactoryBean; 
    public class AccountRepositoryFactoryBean implements 
    FactoryBean<IAccountRepository> { 
      @Override 
      public IAccountRepository getObject() throws Exception { 
        return new AccountRepository(); 
      } 
      @Override 
      public Class<?> getObjectType() { 
        return IAccountRepository.class; 
      } 
      @Override 
      public boolean isSingleton() { 
        return false; 
      } 
    }  

你可以使用假设的AccountRepositoryFactoryBean像这样连接AccountRepository实例:

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

    xsi:schemaLocation="http://www.springframework.org/schema/beans 
    http://www.springframework.org/schema/beans/spring-beans.xsd"> 

    <bean id="transferService" class="com.packt.patterninspring.
     chapter4.bankapp.service.TransferService"> 
     <constructor-arg ref="accountRepository"/> 
    </bean> 
    <bean id="accountRepository"    
     class="com.packt.patterninspring.chapter4.
      bankapp.repository.AccountRepositoryFactoryBean"/> 
    </beans> 

在前面的例子中,TransferService类依赖于AccountRepository Bean,但在 XML 文件中,我们已将AccountRepositoryFactoryBean定义为accountRepository Bean。AccountRepositoryFactoryBean类实现了 Spring 的FactoryBean接口。FactoryBeangetObject方法的结果将被传递,而不是实际的FactoryBean本身。Spring 通过注入FactoryBeangetObjectType()方法返回的对象,以及FactoryBeangetObjectType()方法返回的对象类型;此 Bean 的作用域由FactoryBeanisSingleton()方法决定。

以下是在 Java 配置中FactoryBean接口的相同配置:

    package com.packt.patterninspring.chapter4.bankapp.config; 
    import org.springframework.context.annotation.Bean; 
    import org.springframework.context.annotation.Configuration; 
    import com.packt.patterninspring.chapter4.bankapp.
     repository.AccountRepositoryFactoryBean; 
    import com.packt.patterninspring.chapter4.
     bankapp.service.TransferService; 
    @Configuration 
    public class AppConfig { 
      public TransferService transferService() throws Exception{ 
        return new TransferService(accountRepository().getObject()); 
      } 
    @Bean 
    public AccountRepositoryFactoryBean accountRepository(){ 
      return new AccountRepositoryFactoryBean(); 
    } 
    } 

作为 Spring 容器中的其他正常 Bean,Spring 的FactoryBean也具有任何其他 Spring Bean 的所有其他特性,包括所有 Spring 容器中的 Bean 都享有的生命周期钩子和服务。

配置 DI 模式的最佳实践

以下是为配置 DI 模式的最佳实践:

  • 配置文件应按类别分开。应用 bean 应与基础设施 bean 分开。目前,这有点难以遵循。

图片

  • 总是指定组件名称;永远不要依赖容器生成的名称。

  • 给出模式的功能描述、应用位置和解决的问题的名称是一个最佳实践。

  • 组件扫描的最佳实践如下:

    • 组件在启动时扫描,并且它还扫描 JAR 依赖项。

    • 不良实践:它扫描了comorg的所有包。这增加了应用程序的启动时间。避免此类组件扫描:

                @ComponenttScan (( {{ "org", "com" }} ))
    • 优化:它只扫描我们定义的特定包。
                 @ComponentScan ( {  
                  "com.packt.patterninspring.chapter4.
                  bankapp.repository", 
                 "com.packt.patterninspring.chapter4.bankapp.service"} 
                  ) 
  • 选择隐式配置的最佳实践:

    • 为经常更改的 bean 选择基于注解的配置

    • 它允许非常快速的开发

    • 这是一个编辑配置的单一点

  • 通过 Java 配置选择显式的最佳实践:

    • 它集中在一个地方

    • 编译器强制执行的强类型检查

    • 可以用于所有类

  • Spring XML 最佳实践:XML 已经存在很长时间了,XML 配置中有很多快捷方式和有用的技术,它们如下列出:

    • 工厂方法(factory-method)和工厂 bean(factory-bean)属性

    • Bean 定义继承

    • 内部 Bean

    • p 和 c 命名空间

    • 将集合用作 Spring Bean

摘要

在阅读本章之后,你现在应该对 DI 设计模式以及应用这些模式的最佳实践有一个很好的了解。Spring 处理管道部分,因此,你可以通过使用依赖注入模式来专注于解决领域问题。DI 模式释放了对象解决其依赖关系的负担。你的对象得到了它工作所需的一切。DI 模式简化了你的代码,提高了代码的可重用性和可测试性。它促进了面向接口的编程,并隐藏了依赖项的实现细节。DI 模式允许集中控制对象的生命周期。你可以通过两种方式配置 DI——显式配置和隐式配置。显式配置可以通过 XML 或基于 Java 的配置进行配置;它提供集中式配置。但隐式配置基于注解。Spring 为基于注解的配置提供了类型化注解。这种配置减少了应用程序中代码的冗长性,但它分散在应用程序文件中。

在即将到来的第五章《理解 Bean 生命周期和使用的模式》中,我们将探讨 Spring 容器中 Spring bean 的生命周期。

第五章:理解 Bean 生命周期和使用的模式

在上一章中,你看到了 Spring 如何在容器中创建 bean。你还学习了如何使用 XML、Java 和注解来配置依赖注入模式。在本章中,我们将更深入地探讨,不仅限于在 Spring 应用程序中注入 bean 和配置依赖项。在这里,你将探索容器中 bean 的生命周期和范围,并了解 Spring 容器如何使用 XML、注解和 Java 在定义的 Spring bean 配置上工作。Spring 不仅允许我们控制从特定 bean 定义创建的对象的 DI 模式的各种配置和要注入的依赖值,还控制从特定 bean 定义创建的 bean 的生命周期和范围。

当我写这一章时,我两岁半的儿子 Arnav 走过来,开始在我的手机上玩电子游戏。他穿着一件印有有趣引语的 T 恤,这些线条描述了他的一天。这些线条是这样的—

我的完美一天:醒来,玩电子游戏,吃饭,玩电子游戏,吃饭,玩电子游戏,然后睡觉

实际上,这些线条完美地反映了他每天的生活周期,因为他醒来,玩耍,吃饭,然后再玩耍,最后睡觉。通过这个例子,我只是想证明万物都有生命周期。我们可以讨论蝴蝶、星星、青蛙或植物的生命周期。但让我们谈谈更有趣的事情——bean 的生命周期!

Spring 容器中的每个 bean 都有其生命周期和作用域。Spring 容器管理 Spring 应用程序中 bean 的生命。我们可以通过使用 Spring 感知接口在某些阶段自定义它。本章将讨论容器中 bean 的生命,以及它是如何在其生命周期的各个阶段使用设计模式进行管理的。到本章结束时,你将对容器中 bean 生命周期及其各个阶段有一个相当的了解。你还将了解 Spring 中许多类型的 bean 作用域。本章将涵盖以下要点:

  • Spring bean 的生命周期及其阶段,如下所示:

    • 初始化阶段

    • 使用阶段

    • 销毁阶段

  • Spring 回调

  • 理解 bean 作用域

    • 单例模式

    • 原型模式

    • 自定义作用域

    • 其他 bean 作用域

现在,让我们花一点时间看看 Spring 如何在 Spring 应用程序中管理 bean 从创建到销毁的生命周期

Spring bean 生命周期及其阶段

在 Spring 应用程序中,生命周期这一术语适用于任何应用程序类——独立 Java、Spring Boot 应用程序或集成/系统测试。生命周期也适用于所有三种依赖注入样式——XML、注解和 Java 配置。你根据业务目标定义 Bean 的配置。但是 Spring 创建这些 Bean 并管理 Spring Bean 的生命周期。Spring 通过 ApplicationContext 加载 Bean 配置;一旦创建应用程序上下文,初始化阶段就完成了。让我们看看 Spring 是如何加载 Java 或 XML 配置文件的。

  • 初始化阶段

  • 使用阶段

  • 销毁阶段

请参考以下图表:

如前图所示,每个 Spring 容器在完整生命周期中都会经历这三个阶段。每个阶段都有一些针对每个 Spring 容器(根据配置)要执行的操作。Spring 介入以管理你的应用程序生命周期。它在所有三个阶段中都发挥着重要作用。

现在,让我们花一点时间看看 Spring 在第一个、初始化阶段是如何工作的。

使用阶段

在这个阶段,首先 Spring 加载任何样式(XML、注解和 Java 配置)的所有配置文件。这个阶段为 Bean 的使用做准备。应用程序在这个阶段完成之前是不可用的。实际上,这个阶段创建了应用程序服务以供使用,并为 Bean 分配系统资源。Spring 提供 ApplicationContext 来加载 Bean 配置;一旦创建应用程序上下文,初始化阶段就完成了。让我们看看 Spring 是如何加载 Java 或 XML 配置文件的。

从配置创建应用程序上下文

Spring 提供了多个 ApplicationContext 的实现来加载各种配置文件样式。这些将在下面列出:

  • 对于 Java 配置,使用以下内容:
        ApplicationContext context = new    
        AnnotationConfigApplicationContext(AppConfig.class); 
  • 对于 XML 配置,实现如下:
        ApplicationContext context = new  
        ClassPathXmlApplicationContext("applicationContext.xml"); 

在前面的代码中,Spring 通过 AnnotationConfigApplicationContext 类加载 Java 配置文件,通过 ClassPathXmlApplicationContext 类加载 XML 配置文件以用于 Spring 容器。对于所有类型的配置,Spring 的行为都是相同的。你可以在应用程序中使用任何配置样式。以下图表显示了这一阶段的确切情况:

如前图所示,初始化阶段分为以下两个步骤:

  • 加载 Bean 定义

  • 初始化 Bean 实例

加载 Bean 定义

在这一步中,所有配置文件--@Configuration 类或 XML 文件--都被处理。对于基于注解的配置,所有带有 @Components 注解的类都会被扫描以加载 bean 定义。所有 XML 文件都会被解析,bean 定义会被添加到 BeanFactory 中。每个 bean 都会按其 id 进行索引。Spring 提供了多个 BeanFactoryPostProcessor bean,因此,它会调用以解决运行时依赖,例如从外部属性文件中读取值。在 Spring 应用程序中,BeanFactoryPostProcessor 可以修改任何 bean 的定义。以下图表描述了这一步:

图片

如前图所示,Spring 首先加载 bean 定义,然后调用 BeanFactoryProcessor 对某些 bean 进行相应的定义修改。让我们通过一个例子来看一下。我们有两个配置文件--AppConfig.javaInfraConfig.java,它们被定义为如下:

  • 以下是对 AppConfig.java 文件的描述:
        @Configuration 
        public class AppConfig { 
          @Bean 
          public TransferService transferService(){ ... } 
          @Bean 
          public AccountRepository accountRepository(DataSource 
          dataSource){ ... } 
        } 
  • 以下是对 InfraConfig.java 文件的描述:
        @Configuration 
        public class InfraConfig { 
          @Bean 
          public DataSource dataSource () { ... } 
        } 

这些 Java 配置文件被 ApplicationContext 载入容器,并按其 id 进行索引,如下所示:

图片

在最后一个图中,Spring beans 按其 ID 索引到 Spring 的 BeanFactory 中,然后,该 BeanFactory 对象被传递给 BeanFactoryPostProcessorpostProcess() 方法。BeanFactoryPostProcessor 可以修改某些 bean 的定义;这取决于开发者提供的 bean 配置。让我们看看 BeanFactoryPostProcessor 的工作原理以及如何在我们的应用程序中覆盖它:

  1. BeanFactoryPostProcessor 在 bean 实际创建之前,对 bean 定义或配置元数据进行操作。

  2. Spring 提供了几个有用的 BeanFactoryPostProcessor 实现,例如读取属性和注册自定义作用域。

  3. 您可以编写自己的 BeanFactoryPostProcessor 接口实现。

  4. 如果在一个容器中定义了 BeanFactoryPostProcessor,它将只应用于该容器中的 bean 定义。

以下是对 BeanFactoryPostProcessor 的代码片段:

    public interface BeanFactoryPostProcessor { 
      public void postProcessBeanFactory
        (ConfigurableListableBeanFactory 
        beanFactory); 
    } 

现在我们来看一下 BeanFactoryPostProcessor 扩展点的以下示例:

读取外部属性文件(database.properties

在这里,我们将使用 DataSource bean 来配置数据库值,如 usernamepassworddb urldriver,如下所示:

    jdbc.driver=org.hsqldb.jdbcDriver 
    jdbc.url=jdbc:hsqldb:hsql://production:9002 
    jdbc.username=doj 
    jdbc.password=doj@123 

以下是在配置文件中的 DataSource bean 定义:

    @Configuration 
    @PropertySource ( "classpath:/config/database.properties" ) 
    public class InfraConfig { 
     @Bean 
     public DataSource dataSource( 
     @Value("${jdbc.driver}") String driver, 
     @Value("${jdbc.url}") String url, 
     @Value("${jdbc.user}") String user, 
     @Value("${jdbc.password}") String pwd) { 
       DataSource ds = new BasicDataSource(); 
       ds.setDriverClassName( driver); 
       ds.setUrl( url); 
       ds.setUser( user); 
       ds.setPassword( pwd )); 
       return ds; 
    } 
   } 

那么,在前面的代码中,我们是如何解析 @Value${..} 变量的呢?我们需要一个 PropertySourcesPlaceholderConfigurer 来评估它们。这是一个 BeanFactoryPostProcessor。如果您使用 XML 配置,<context:property-placeholder/> 命名空间会为您创建一个 PropertySourcesPlaceholderConfigurer

在加载配置文件时,加载 Bean 定义是一个一次性过程,但 Bean 实例的初始化阶段会在容器中的每个 Bean 上执行。让我们看看应用中 Bean 实例的初始化过程。

初始化 Bean 实例

在将 Bean 定义加载到BeanFactory之后,Spring IoC 容器为应用实例化 Bean;以下图显示了流程:

图片

如前图所示,容器中的每个 Bean 都会执行 Bean 初始化步骤。我们可以将 Bean 创建过程总结如下:

  • 默认情况下,每个 Bean 都会被急切地实例化。除非标记为懒加载,否则它会按照正确的顺序创建,并注入其依赖项。

  • Spring 提供了多个BeanPostProcessor,因此,每个 Bean 都会经历一个后处理阶段,例如BeanFactoryPostProcessor,它可以修改 Bean 定义。然而,BeanPostProcessor可以改变 Bean 的实例。

  • 在这个阶段执行完成后,Bean 就完全初始化并准备好使用。它通过其id被跟踪,直到上下文被销毁,除了原型 Bean。

在下一节中,我们将讨论如何通过使用BeanPostProcessor来自定义 Spring 容器的行为。

使用 BeanPostProcessor 自定义 Bean

BeanPostProcessor是 Spring 中的一个重要扩展点。它可以以任何方式修改 Bean 实例。它用于启用如 AOP 代理等强大功能。你可以在你的应用中编写自己的BeanPostProcessor来创建自定义的post-processor--该类必须实现BeanPostProcessor接口。Spring 提供了BeanPostProcessor的几个实现。在 Spring 中,BeanPostProcessor接口有两个回调方法,如下:

    public interface BeanPostProcessor { 
      Object postProcessBeforeInitialization(Object bean, String 
      beanName) throws BeansException; 
      Object postProcessAfterInitialization(Object bean, String 
      beanName) throws BeansException; 
    } 

你可以通过实现BeanPostProcessor接口的这两种方法来提供自己的自定义逻辑,用于 Bean 实例化、依赖解析等。你可以配置多个BeanPostProcessor实现来向 Spring 容器添加自定义逻辑。你还可以通过设置 order 属性来管理这些BeanPostProcessor的执行顺序。BeanPostProcessor在 Spring 容器实例化 Bean 之后工作。BeanPostProcessor的作用域在 Spring 容器内,这意味着在一个容器中定义的 Bean 不会被另一个容器中定义的BeanPostProcessor后处理。

Spring 应用程序中的任何类都被注册为容器的后处理器;它由 Spring 容器为每个 bean 实例创建。Spring 容器在容器初始化方法(初始化 Bean 的afterPropertiesSet()和 bean 的init方法)之前调用postProcessBeforeInitialization()方法。它还在任何 bean 初始化回调之后调用postProcessAfterInitialization()方法。Spring AOP 使用后处理器提供代理包装逻辑(代理设计模式),尽管我们可以通过使用后处理器执行任何操作。

Spring 的ApplicationContext自动检测实现BeanPostProcessor接口的 bean,并将这些 bean 注册为后处理器。这些 bean 在创建任何其他 bean 时被调用。让我们探索以下BeanPostProcessor的示例。

让我们按照以下方式创建一个自定义的后处理器

    package com.packt.patterninspring.chapter5.bankapp.bpp; 
    import org.springframework.beans.BeansException; 
    import org.springframework.beans.factory.config.BeanPostProcessor; 
    import org.springframework.stereotype.Component; 
    @Component 
    public class MyBeanPostProcessor implements 
    BeanPostProcessor { 
      @Override 
      public Object postProcessBeforeInitialization
      (Object bean, String beanName) throws BeansException { 
        System.out.println("In After bean Initialization 
        method. Bean name is "+beanName); 
        return bean; 
      } 
      public Object postProcessAfterInitialization(Object bean, String  
      beanName) throws BeansException { 
        System.out.println("In Before bean Initialization method. Bean 
        name is "+beanName); 
        return bean; 
        } 
   }  

此示例说明了基本用法,这里此示例显示一个后处理器将字符串打印到系统控制台,对于容器中注册的每个 bean。此MyBeanPostProcessor类使用@Component注解,这意味着此类与应用程序上下文中的其他 bean 类相同,现在运行以下演示类。请参考以下代码:

    public class BeanLifeCycleDemo { 
      public static void main(String[] args) { 
        ConfigurableApplicationContext applicationContext = new 
        AnnotationConfigApplicationContext(AppConfig.class); 
        applicationContext.close(); 
      } 
    }

这是我们将在控制台上获得的输出:

如前所述的输出所示,Spring 容器中每个 bean 的方法都会打印出回调方法的字符串。Spring 为一些特定功能提供了许多预实现的BeanPostProcessor,如下所示:

  • RequiredAnnotationBeanPostProcessor

  • AutowiredAnnotationBeanPostProcessor

  • CommonAnnotationBeanPostProcessor

  • PersistenceAnnotationBeanPostProcessor

XML 配置中的<context:annotation-config/>命名空间启用了在同一应用程序上下文中定义的多个后处理器

现在,让我们继续下一节,看看我们如何通过使用BeanPostProcessor来启用初始化器扩展点。

初始化器扩展点

这个特殊的“后处理器”情况会导致调用init@PostConstruct)方法。内部,Spring 使用多个BeanPostProcessorsBPPsCommonAnnotationBeanPostProcessor来启用初始化。以下图表展示了初始化器和 BPPs 之间的关系。

现在,让我们看看以下 XML 中初始化器扩展点的示例:

<context:annotation-config/>命名空间明确启用了许多后处理器,让我们看看以下 XML 配置文件:

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

     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-4.3.xsd"> 
     <context:annotation-config/> 
     <bean id="transferService"    
     class="com.packt.patterninspring.chapter5.
     bankapp.service.TransferService"/> 
     <bean id="accountRepository"   
     class="com.packt.patterninspring.chapter5.
     bankapp.repository.JdbcAccountRepository" 
     init-method="populateCache"/> 
    </beans> 

在前面的配置代码中,你可以看到我定义了一些豆类,其中之一是accountRepository仓库,它具有init方法属性;这个属性有一个值,populateCache。这实际上就是accountRepository豆的initializer方法。如果通过<context:annotation-config/>命名空间显式启用了post-processor,容器会在豆初始化时调用这个方法。让我们看看下面的JdbcAccountRepository类,如下所示:

    package com.packt.patterninspring.chapter5.bankapp.repository; 
    import com.packt.patterninspring.chapter5.bankapp.model.Account; 
    import com.packt.patterninspring.chapter5.bankapp.model.Amount; 
    import com.packt.patterninspring.chapter5.
    bankapp.repository.AccountRepository; 
    public class JdbcAccountRepository implements AccountRepository { 
      @Override 
      public Account findByAccountId(Long accountId) { 
        return new Account(accountId, "Arnav Rajput", new  
        Amount(3000.0)); 
    } 
    void populateCache(){ 
      System.out.println("Called populateCache() method"); 
    } 
   }

在 Java 配置中,我们可以使用@Bean注解的initMethod属性如下:

    @Bean(initMethod = "populateCache") 
    public AccountRepository accountRepository(){ 
      return new JdbcAccountRepository(); 
    }

在基于注解的配置中,我们可以使用JSR-250注解@PostConstruct如下:

    @PostConstruct 
    void populateCache(){ 
      System.out.println("Called populateCache() method"); 
    } 

我们已经看到了豆生命周期中的第一个阶段,Spring 通过使用 XML、Java 和注解配置来加载豆定义,然后,Spring 容器以正确的顺序在 Spring 应用程序中初始化每个豆。下面的图给出了配置生命周期第一个阶段的概述:

图片

最后一个图显示了由ApplicationContext的相应实现加载的任何风格的 Spring 豆元数据——XML、注解或 Java。所有 XML 文件都被解析并加载了豆定义。在注解配置中,Spring 扫描所有组件,并加载豆定义。在 Java 配置中,Spring 读取所有的@Bean方法来加载豆定义。从所有配置风格加载豆定义之后,BeanFactoryPostProcessor出现来修改一些豆的定义,然后容器实例化豆。最后,BeanPostProcessor在豆上工作,它可以修改和改变豆对象。这是初始化阶段。现在让我们看看豆在其生命周期中的下一个使用阶段。

豆的使用阶段

在 Spring 应用程序中,所有 Spring 豆有 99.99%的时间都处于这个阶段。如果初始化阶段成功完成,那么 Spring 豆就会进入这个阶段。在这里,豆被客户端作为应用服务使用。这些豆处理客户端请求,并执行应用行为。在使用阶段,让我们看看如何在应用中使用时调用从上下文中获得的豆。请参考以下代码:

    //Get or create application context from somewhere 
    ApplicationContext applicationContext = new    
    AnnotationConfigApplicationContext(AppConfig.class); 

    // Lookup the entry point into the application 
    TransferService transferService =    
    context.getBean(TransferService.class); 
    // and use it 
    transferService.transfer("A", "B", 3000.1); 

假设return服务返回一个原始对象,那么它可以直接调用;这里没有特别之处。但如果你的豆被包装在代理中,那么事情就更有趣了。让我们通过以下图来更清楚地理解这一点:

图片

在前面的图中,你可以看到通过Proxy类调用的service方法;它是在init阶段由专门的BeanPostProcessor创建的。它将你的 Bean 包装在动态代理中,从而为你的 Bean 添加行为,这是透明的。它是装饰器设计模式和代理设计模式的一个实现。

让我们看看 Spring 如何在 Spring 应用程序中为你的 Bean 创建代理。

使用代理在 Spring 中实现装饰器和代理模式

Spring 在 Spring 应用程序中使用两种类型的代理。以下是由 Spring 使用的代理类型:

  • JDK 代理:这也被称为动态代理。它的 API 内置在 JDK 中。对于这个代理,需要Java接口。

  • CGLib 代理:这并不是 JDK 内置的。然而,它包含在 Spring JARS 中,并在接口不可用时使用。它不能应用于 final 类或方法。

让我们看看以下图中两个代理的特性:

这就是关于 Spring Bean 生命周期中的使用阶段的所有内容。现在让我们转到生命周期的下一个阶段,即销毁阶段。

Bean 的销毁阶段

在这个阶段,Spring 会释放应用程序服务获取的任何系统资源。这些资源有资格进行垃圾回收。当你关闭应用程序上下文时,销毁阶段完成。让我们看看这个阶段的以下代码行:

    //Any implementation of application context 

    ConfigurableApplicationContext applicationContext = new   
    AnnotationConfigApplicationContext(AppConfig.class); 

    // Destroy the application by closing application context. 
    applicationContext.close(); 

在前面的代码中,当你在这个阶段调用applicationContext.close()方法时,你认为会发生什么?发生的进程如下所示:

  • 任何实现org.springframework.beans.factory.DisposableBean接口的 Bean 在销毁时都会从容器中收到一个回调。DisposableBean接口指定了一个单一的方法:
        void destroy() throws Exception; 
  • 如果指示调用它们的destroy方法,bean实例将被销毁。Bean 必须定义一个destroy方法,即一个无参数返回void的方法。

  • 然后,上下文会自我销毁,并且这个上下文将不再可用。

  • 只有 GC(垃圾回收器)实际上销毁对象,并且记住,它仅在ApplicationContext/JVM 正常退出时被调用。它不会为原型 Bean 调用。

让我们看看如何使用 XML 配置来实现它:

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

      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-
      4.3.xsd"> 
      <context:annotation-config/> 
      <bean id="transferService" 
      class="com.packt.patterninspring.chapter5.
      bankapp.service.TransferService"/> 
      <bean id="accountRepository"   
      class="com.packt.patterninspring.chapter5.
      bankapp.repository.JdbcAccountRepository" 
      destroy-method="clearCache"/> 
   </beans> 

在配置中,accountRepositoryBean 有一个名为clearCachedestroy方法:

    package com.packt.patterninspring.chapter5.bankapp.repository; 
    import com.packt.patterninspring.chapter5.bankapp.model.Account; 
    import com.packt.patterninspring.chapter5.bankapp.model.Amount; 
    import com.packt.patterninspring.chapter5.bankapp.
      repository.AccountRepository; 
    public class JdbcAccountRepository implements AccountRepository { 
     @Override 
    public Account findByAccountId(Long accountId) { 
      return new Account(accountId, "Arnav Rajput", new
      Amount(3000.0)); 
    } 
    void clearCache(){ 
      System.out.println("Called clearCache() method"); 
    } 
   } 

让我们用 Java 看看同样的配置。在 Java 配置中,我们可以使用@Bean注解的destroyMethod属性,如下所示:

    @Bean (destroyMethod="clearCache") 
    public AccountRepository accountRepository() { 
      return new JdbcAccountRepository(); 
    } 

我们可以使用注解做同样的事情。注解需要通过使用<context:component-scan ... />激活annotation-config或组件扫描器,如下所示:

    public class JdbcAccountRepository { 
      @PreDestroy 
      void clearCache() { 
        // close files, connections... 
        // remove external resources... 
      } 
    } 

你现在已经看到了 Spring bean 生命周期的所有阶段。在初始化阶段,有初始化和代理的 Bean 后处理器。在使用阶段,Spring beans 使用了代理的魔法。最后,在销毁阶段,它允许应用程序干净地终止。

现在你已经了解了 bean 的生命周期,让我们来学习 bean 的作用域,以及如何在 Spring 容器中创建自定义 bean 作用域。

理解 bean 作用域

在 Spring 中,每个 bean 在容器中都有一个作用域。你可以控制不仅 bean 元数据和其生命周期,还可以控制该 bean 的作用域。你可以创建一个自定义的 bean 作用域,并将其注册到容器中。你可以通过配置基于 XML、注解或 Java 的 bean 定义来决定 bean 的作用域。

Spring 应用程序上下文通过使用单例作用域来创建所有 bean。这意味着每次都是同一个 bean;无论它被注入到另一个 bean 中多少次或被其他服务调用多少次,都不会改变。正因为这种单例行为,作用域减少了实例化的成本。它适用于应用中的无状态对象。

在 Spring 应用程序中,有时需要保存某些对象的状态,这些对象不适合重用。对于这种需求,将 bean 作用域声明为单例是不安全的,因为它可能在稍后重用时引起意外问题。Spring 为这种需求提供了另一个作用域,这被称为 Spring bean 的原型作用域。

Spring 定义了多个作用域,bean 可以在这些作用域下创建,如下所示:

单例 bean 作用域

在 Spring 中,任何具有单例作用域的 bean,在应用程序上下文中只创建一个 bean 实例,它在整个应用程序中定义。这是 Spring 容器的默认行为。但它与Gang of FourGoF)模式书籍中定义的单例模式不同。在 Java 中,单例意味着在 JVM 中每个特定类的每个对象。但在 Spring 中,它意味着每个 Spring IoC 容器中每个 bean 定义的每个 bean 实例。这将在以下图中解释:

正如前图所示,相同的对象实例由 bean 定义accountRepository注入到同一 IoC 容器中的其他协作 bean。Spring 将所有单例 bean 实例存储在缓存中,所有协作 bean 都会从缓存中获取该对象依赖项。

原型 bean 作用域

在春季,任何定义为原型范围的 bean,每次被注入到其他协作 bean 时,都会为其创建一个 bean 实例。以下图示说明了 Spring 的原型范围:

如前图所示,accountRepository类被配置为一个原型 bean,每次该 bean 被注入到其他 bean 时,容器都会为其创建一个新的实例。

会话 bean 范围

在 Web 环境中,为每个用户会话只创建一个新实例。

考虑以下 XML 配置的 bean 定义:

    <bean id="..." class="..." scope="session"/> 

请求 bean 范围

在 Web 环境中,为每个请求只创建一个新实例。

考虑以下 XML 配置的 bean 定义:

    <bean id="..." class="..." scope="request"/>

Spring 中的其他范围

Spring 还有其他更专业的范围,如下所示:

  • WebSocket 范围

  • 刷新范围

  • 线程范围(已定义,但默认未注册)

Spring 还支持为 bean 创建自己的自定义范围。我们将在下一节中讨论这个问题。

自定义范围

我们可以为任何 bean 创建一个自定义范围,并将此范围注册到应用程序上下文中。让我们通过以下示例看看如何创建一个自定义 bean 范围。

创建自定义范围

在 Spring IoC 容器中创建您的客户范围时,Spring 提供了org.springframework.beans.factory.config.Scope接口。您必须实现此接口以创建自己的自定义范围。请看以下MyThreadScope类,作为 Spring IoC 容器中的自定义范围:

    package com.packt.patterninspring.chapter5.bankapp.scope; 
    import java.util.HashMap; 
    import java.util.Map; 
    import org.springframework.beans.factory.ObjectFactory; 
    import org.springframework.beans.factory.config.Scope; 

    public class MyThreadScope implements Scope { 
      private final ThreadLocal<Object> myThreadScope = new
      ThreadLocal<Object>() { 
        protected Map<String, Object> initialValue() { 
          System.out.println("initialize ThreadLocal"); 
          return new HashMap<String, Object>(); 
         } 
       }; 
    @Override 
    public Object get(String name, ObjectFactory<?> objectFactory) { 
      Map<String, Object> scope = (Map<String, Object>)
      myThreadScope.get(); 
      System.out.println("getting object from scope."); 
      Object object = scope.get(name); 
      if(object == null) { 
        object = objectFactory.getObject(); 
        scope.put(name, object); 
      } 
      return object; 
    } 
    @Override 
    public String getConversationId() { 
      return null; 
    } 
    @Override 
    public void registerDestructionCallback(String name, Runnable
    callback) { 

    } 
    @Override 
    public Object remove(String name) { 
      System.out.println("removing object from scope."); 
      @SuppressWarnings("unchecked") 
      Map<String, Object> scope = (Map<String, Object>)
      myThreadScope.get(); 
      return scope.remove(name); 
     } 
     @Override 
     public Object resolveContextualObject(String name) { 
       return null; 
     } 
    } 

在前面的代码中,我们已经按照如下方式覆盖了Scope接口的多个方法:

  • Object get(String name, ObjectFactory objectFactory): 此方法从底层范围返回对象

  • Object remove(String name): 此方法从底层范围中移除对象

  • void registerDestructionCallback(String name, Runnable destructionCallback): 此方法注册销毁回调,并在具有此自定义范围的指定对象销毁时执行

现在让我们看看如何将这个自定义范围注册到 Spring IoC 容器中,以及如何在 Spring 应用程序中使用它。

您可以使用CustomScopeConfigurer类声明性地将此自定义 bean 范围注册到 Spring IoC 容器中,如下所示:

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

      xsi:schemaLocation="http://www.springframework.org/schema/beans  
      http://www.springframework.org/schema/beans/spring-beans.xsd"> 
      <bean   class="org.springframework.beans.factory.
      config.CustomScopeConfigurer"> 
      <property name="scopes"> 
        <map> 
          <entry key="myThreadScope"> 
            <bean class="com.packt.patterninspring.chapter5.
            bankapp.scope.MyThreadScope"/> 
          </entry> 
        </map> 
      </property> 
     </bean> 
     <bean id="myBean" class="com.packt.patterninspring.chapter5.
      bankapp.bean.MyBean" scope="myThreadScope">  
      <property name="name" value="Dinesh"></property> 
    </bean> 
    </beans> 

如前配置文件所示,我已使用CustomScopeConfigurer类将名为myThreadScope的自定义 bean 范围注册到应用程序上下文中。我正在使用的这个自定义范围与 XML 配置中 bean 标签的 scope 属性类似,类似于单例或原型范围。

摘要

在阅读本章之后,您现在应该对容器中 Spring bean 的生命周期以及容器中的几种 bean 范围有一个很好的了解。您现在知道在容器中 Spring bean 的生命周期有三个阶段。第一个是初始化阶段。在这个阶段,Spring 从 XML、Java 或注解配置中加载 bean 定义。加载这些 bean 后,容器构建每个 bean,并对此 bean 应用后处理逻辑。

下一个阶段是使用阶段,在这个阶段中,Spring 的 bean 已经准备好被使用,Spring 展示了代理模式的魔法。

最后,最后一个阶段是销毁阶段。在这个阶段,当应用程序调用 Spring 的ApplicationContextclose()方法时,容器会调用每个 bean 的清理方法来释放资源。

在 Spring 中,你可以控制不仅 bean 的生命周期,还可以在容器中控制 bean 的作用域。Spring IoC 容器中 bean 的默认作用域是单例(Singleton),但你可以通过在 XML 中定义 bean 标签的 scope 属性或在 Java 中使用@Scope注解来覆盖默认作用域,以定义其他作用域原型。你还可以创建自己的自定义作用域,并将其注册到容器中。

现在我们将转向这本书的魔法章节,即 Spring 面向切面编程AOP)。正如依赖注入有助于将组件与其协作的其他组件解耦一样,AOP 有助于将你的应用程序组件与应用程序中跨越多个组件的任务解耦。让我们继续到下一章,介绍使用代理和装饰器设计模式的 Spring 面向切面编程。

第六章:Spring 面向切面编程(AOP)与代理和装饰者模式

在你开始阅读本章之前,我想与你分享一些事情;当我撰写本章时,我的妻子 Anamika 正在拍自拍并上传到几个社交媒体网站,如 Facebook 和 WhatsApp。她跟踪着点赞的数量,然而上传更多的照片会消耗更多的移动数据,而移动数据是需要付费的。我很少使用社交媒体,因为我更喜欢避免向互联网公司支付更多费用。每个月,互联网公司都知道他们应该向我们收取多少费用。现在考虑一下,如果互联网使用、总通话时长和账单计算都由我们精心计划和管理的会怎样?可能一些沉迷于互联网的用户会管理它,而我对此真的毫无头绪。

计算互联网使用和通话的账单是一个重要的功能,但对于大多数互联网用户来说,它仍然不是主要的。对于像我妻子这样的人,拍自拍、上传照片到社交媒体、在 YouTube 上观看视频,这些都是大多数互联网用户积极参与的事情。管理和计算他们的互联网账单对互联网用户来说是一种被动行为。

同样,一些企业应用模块就像我们互联网使用的互联网账单计算器。在应用中,有一些模块具有重要的功能,需要在应用的多个位置放置。但是,在每一个点显式调用这些功能是不太可能的。例如,日志记录、安全和事务管理对于你的应用来说很重要,但你的业务对象并没有积极参与其中,因为你的业务对象需要专注于它们被设计用于解决的业务领域问题,并将某些方面留给其他人处理。

在软件开发中,在应用中的特定点需要执行特定的任务。这些任务或函数被称为跨切面关注点。在一个应用中,所有的跨切面关注点都与该应用的业务逻辑分离。Spring 提供了一个模块面向切面编程AOP)来将这些跨切面关注点从业务逻辑中分离出来。

如同第四章中所述的使用依赖注入模式连接豆芽,你学习了如何使用依赖注入来配置和解决应用中协作对象之间的依赖关系。而依赖注入(DI)推崇面向接口编程和将应用对象解耦,Spring AOP 则推崇将应用的业务逻辑与跨切面关注点解耦。

在我们的 bankapp 示例中,从一个账户向另一个账户转账是业务逻辑,但在我们的 bankapp 应用程序中记录此活动和确保交易安全是横切关注点。这意味着记录、安全和事务是方面应用的常见示例。

在本章中,您将探索 Spring 对方面的支持。它将涵盖以下要点:

  • Spring 中的代理模式

  • 适配器设计模式用于处理运行时编织

  • 装饰器设计模式

  • 面向切面编程

  • AOP 解决的问题

  • 核心 AOP 概念

  • 定义切入点

  • 实现建议

  • 创建方面

  • 理解 AOP 代理

在我们进一步讨论 Spring AOP 之前,让我们首先了解 Spring AOP 框架下实现的模式,并看看这些模式是如何应用的。

Spring 中的代理模式

代理设计模式提供了一个具有另一个类功能的对象。此模式属于 GOF 设计模式的结构设计模式。根据 GOF 模式,为另一个对象提供一个代理或占位符以控制对其的访问。此设计模式的目的是为外部世界提供一个具有其功能的不同类。

在 Spring 中使用装饰器模式代理类

如您在第三章中看到的,“考虑结构和行为模式”,根据 GOF 书籍,动态地为对象附加额外责任。装饰器为扩展功能提供了一种灵活的替代子类化的方法。 此模式允许您在运行时或静态地动态地向单个对象添加和删除行为,而不会改变同一类中其他相关对象的现有行为。

在 Spring AOP 中,CGLIB 用于在应用程序中创建代理。CGLIB 代理通过在运行时生成目标类的子类来工作。Spring 配置此生成的子类将方法调用委托给原始目标--子类用于实现装饰器模式,编织建议。

Spring 提供了两种在应用程序中创建代理的方法。

  • CGLIB 代理

  • JDK 代理或动态代理

让我们看看以下表格:

JDK 代理 CGLIB 代理
也称为动态代理 NOT built into JDK
API 内置在 JDK 中 包含在 Spring JARs 中
要求:Java 接口(s) 当接口不可用时使用
所有的代理接口 不能应用于 final 类或方法

让我们看看以下图:

注意--CGLIB 代理有一个需要考虑的问题,那就是 final 方法不能被建议,因为它们不能被重写。

在下一节中,我们将学习更多关于横切关注点的内容。

什么是横切关注点?

在任何应用程序中,都存在一些在许多地方都需要使用的通用功能。但这个功能与应用程序的业务逻辑无关。假设你在应用程序的每个业务方法之前执行基于角色的安全检查。这里的安全性是一个横切关注点。它对于任何应用程序都是必需的,但从业务角度来看,它是一个简单的通用功能,我们必须在应用程序的许多地方实现。以下是企业应用程序的横切关注点的例子。

  • 日志和跟踪

  • 事务管理

  • 安全性

  • 缓存

  • 错误处理

  • 性能监控

  • 定制业务规则

让我们看看我们如何通过使用 Spring AOP 的方面来实现这些横切关注点。

什么是面向切面编程?

如前所述,面向切面编程AOP)使得横切关注点的模块化成为可能。它补充了面向对象编程OOP),这是另一种编程范式。OOP 以类和对象作为关键元素,而 AOP 以方面作为关键元素。方面允许你在应用程序的多个位置对某些功能进行模块化。这种类型的功能被称为横切关注点。例如,安全性是应用程序中的一个横切关注点,因为我们必须在多个方法中应用它以实现安全性。同样,事务和日志记录也是应用程序的横切关注点,还有更多。让我们在下面的图中看看这些关注点是如何应用到业务模块中的:

图片

如前图所示,有三个主要业务模块,分别是TransferServiceAccountServiceBankService。所有业务模块都需要一些共同的功能,例如安全性事务管理和日志

让我们看看如果我们不使用 Spring AOP,在应用程序中我们会面临哪些问题。

AOP 解决的问题

如前所述,方面使得横切关注点的模块化成为可能。所以如果你没有使用方面,那么某些横切功能的模块化是不可能的。这往往会导致横切功能与业务模块混合。如果你使用面向对象的一个共同原则来重用共同的功能,如安全性、日志和事务管理,你需要使用继承或组合。但是在这里使用继承可能会违反 SOLID 原则中的单一职责原则,并增加对象层次。此外,组合在应用程序中可能难以处理。这意味着,未能模块化横切关注点会导致以下两个主要问题:

  • 代码纠缠

  • 代码分散

代码纠缠

它是在应用中关注点的耦合。当横切关注点与应用的业务逻辑混合时,就会发生代码纠缠。它促进了横切模块和业务模块之间的紧密耦合。让我们看看以下代码,以了解代码纠缠的更多内容:

    public class TransferServiceImpl implements TransferService { 
      public void transfer(Account a, Account b, Double amount) { 
        //Security concern start here 
        if (!hasPermission(SecurityContext.getPrincipal()) { 
          throw new AccessDeniedException(); 
        } 
        //Security concern end here 

        //Business logic start here 
        Account aAct = accountRepository.findByAccountId(a); 
        Account bAct = accountRepository.findByAccountId(b); 
        accountRepository.transferAmount(aAct, bAct, amount); 
        ... 
      } 
    } 

如前述代码所示,安全关注点代码(高亮显示)与应用的业务逻辑代码混合。这种情况是代码纠缠的一个例子。在这里,我们只包括了安全关注点,但在企业应用中,你必须实现多个横切关注点,如日志记录、事务管理等。在这种情况下,管理代码和修改代码将变得更加复杂,如图中所示可能会引起代码中的关键错误:

图片

在前述图中,你可以看到有三个横切关注点分布在TransferService业务类和与AccountService业务逻辑混合的横切关注点逻辑中。这种关注点与应用逻辑之间的耦合称为代码纠缠。让我们看看如果我们使用方面处理横切关注点时,另一个主要问题是什么。

代码分散

这意味着相同的关注点分散在应用的不同模块中。代码分散促进了关注点代码在应用模块中的重复。让我们看看以下代码,以了解代码分散的更多内容:

    public class TransferServiceImpl implements TransferService { 
      public void transfer(Account a, Account b, Double amount) { 
        //Security concern start here 
        if (!hasPermission(SecurityContext.getPrincipal()) { 
          throw new AccessDeniedException(); 
        } 
        //Security concern end here 

        //Business logic start here 
        ... 
      } 
    } 

    public class AccountServiceImpl implements AccountService { 
      public void withdrawl(Account a, Double amount) { 
        //Security concern start here 
        if (!hasPermission(SecurityContext.getPrincipal()) { 
          throw new AccessDeniedException(); 
        } 
        //Security concern end here 

        //Business logic start here 
        ... 
      } 
    } 

如前述代码所示,应用有两个模块,TransferServiceAccountService。这两个模块都有相同的横切关注点代码用于安全。在两个业务模块中加粗高亮的代码是相同的,这意味着这里存在代码重复。以下图示说明了代码分散:

图片

在前述图中,有三个业务模块TransferServiceAccountServiceBankService。每个业务模块都包含横切关注点,如安全日志事务管理。所有模块在应用中都有相同的关注点代码。实际上,这是在应用中关注点代码的重复。

Spring AOP 为 Spring 应用中的这两个问题提供了解决方案,即代码纠缠和代码分散。方面(Aspects)允许将横切关注点模块化,以避免纠缠并消除分散。让我们在下一节中看看 AOP 是如何解决这些问题的。

AOP 如何解决问题

Spring AOP 允许您将横切关注点逻辑与主线应用逻辑分开。这意味着,您可以实现主线应用逻辑,并且只关注应用的核心问题。您还可以编写方面来实现您的横切关注点。Spring 提供了许多开箱即用的方面。在创建方面之后,您可以将这些方面(即横切行为)添加到应用程序的正确位置。让我们看看以下图示,它说明了 AOP 的功能:

图片

如前图所示,所有方面如安全、日志和事务方面都在应用程序中单独实现。我们已经将这些方面添加到应用程序的正确位置。现在,我们的应用逻辑与关注点分离了。让我们看看以下部分,它定义了核心 AOP 概念,并在您的应用程序中使用 AOP 的术语。

核心 AOP 术语和概念

与其他技术一样,AOP 有其自己的词汇。让我们开始学习一些核心 AOP 概念和术语。Spring 在 Spring AOP 模块中使用了 AOP 范式。但不幸的是,Spring AOP 框架中使用的术语是 Spring 特定的。这些术语用于描述 AOP 模块和功能,但它们并不直观。尽管如此,这些术语被用来理解 AOP。如果没有理解 AOP 习语,您将无法理解 AOP 功能。基本上,AOP 是用通知、切入点(pointcut)和连接点(join point)来定义的。让我们看看以下图示,它说明了核心 AOP 概念以及它们如何在框架中相互关联:

图片

在前图中,您可以看到一个 AOP 功能,它被称为通知,并且它在多个点实现。这些点被称为连接点,它们是通过表达式定义的。这些表达式被称为切入点。让我们通过一个例子详细理解这些术语(还记得我妻子的互联网账单故事吗?)。

通知(Advice)

互联网公司根据互联网公司使用的数据使用量(以 MB 或 GB 为单位)来计算账单。互联网公司有一份客户名单,并且他们还为公司计算互联网账单。因此,计算账单并发送给客户是互联网公司的核心工作,而不是客户的工作。同样,每个方面都有自己的主要工作和完成这项工作的目的。在 AOP 中,方面的这项工作被称为通知。

如你所知,建议是一项工作,方面将执行这项工作,因此当思考何时执行这项工作以及这项工作将包含什么内容时,会涌现一些问题。这项工作是在调用业务方法之前执行吗?或者是在调用业务方法之后执行?或者是在方法调用前后都执行?或者是在业务方法抛出异常时执行。有时,这个业务方法也被称为建议方法。让我们看看 Spring 方面使用的以下五种建议类型:

  • 之前: 建议的工作在调用建议方法之前执行。

如果建议抛出异常,目标将不会被调用——这是有效使用之前建议的情况。

  • 之后: 建议的工作在建议方法完成之后执行,无论目标是否抛出异常。

  • 返回后: 建议的工作在建议方法成功完成后执行。例如,如果业务方法返回而没有抛出异常。

  • 抛出后: 如果建议方法通过抛出异常退出,建议的工作将执行。

  • 环绕: 这是 Spring AOP 中最强大的建议之一,这个建议围绕建议方法,在建议方法被调用之前和之后提供一些建议的工作。

简而言之,建议的工作代码将在每个选定的点上执行,即连接点,让我们来看看 AOP 的另一个术语。

连接点

互联网公司为许多客户提供互联网服务。每个客户都有一个互联网套餐,这个套餐需要用于他们的账单计算。借助每个互联网套餐,公司可以潜在地为所有客户计算互联网账单。同样,你的应用程序可能有多个地方可以应用建议。这些应用程序中的地方被称为连接点。连接点是在程序执行中的一个点,例如方法调用或异常抛出。在这些点上,Spring 方面会在你的应用程序中插入关注的功能。让我们看看 AOP 如何知道连接点,并讨论 AOP 概念的另一个术语。

切入点

互联网公司根据互联网数据的使用情况(例如,我妻子需要更多的数据)制定了许多互联网套餐,因为任何互联网公司都无法为所有客户提供相同的套餐或为每个客户提供一个独特的套餐。相反,每个套餐都分配给客户的一个子集。同样,建议也不必应用到应用程序中的所有连接点上。你可以定义一个表达式来选择应用程序中的一个或多个连接点。这个表达式被称为切入点。它有助于缩小方面建议的连接点。让我们看看 AOP 的另一个术语,即方面。

方面

一家互联网公司知道哪个客户有什么互联网套餐。基于这些信息,互联网公司计算互联网账单并发送给客户。在这个例子中,互联网公司是一个方面,互联网套餐是切入点,客户是连接点,公司计算互联网账单是一个通知。同样,在你的应用中,方面是一个封装了切入点和通知的模块。方面知道它做什么;在哪里以及何时在应用中执行。让我们看看 AOP 是如何将方面应用于业务方法的。

线程编织

编织是一种将方面与业务代码结合的技术。这是一个通过创建新的代理对象将方面应用于目标对象的过程。编织可以在编译时、类加载时或在运行时进行。Spring AOP 通过使用代理模式使用运行时编织。

你在 AOP 中看到了很多术语。无论你学习任何 AOP 框架,无论是 AspectJ 还是 Spring AOP,你都必须了解这些术语。Spring 使用了 AspectJ 框架来实现 Spring AOP 框架。Spring AOP 支持 AspectJ 的有限功能。Spring AOP 提供基于代理的 AOP 解决方案。Spring 只支持方法连接点。现在你对 Spring AOP 及其工作原理有了基本了解,让我们继续探讨如何在 Spring 的声明式 AOP 模型中定义切入点。

定义切入点

如前所述,切入点用于定义应用通知的点。因此,切入点是应用中方面最重要的元素之一。让我们了解如何定义切入点。在 Spring AOP 中,我们可以使用表达式语言来定义切入点。Spring AOP 使用 AspectJ 的点切表达式语言来选择应用通知的位置。Spring AOP 支持 AspectJ 中可用的切入点设计器的一个子集,因为正如你所知,Spring AOP 是基于代理的,某些设计器不支持基于代理的 AOP。让我们看看以下表格中 Spring AOP 支持的设计器。

Spring 支持的 AspectJ 设计器 描述
execution 它通过方法执行匹配连接点,是 Spring AOP 支持的初级切入点设计器。
within 它通过限制在特定类型内匹配连接点。
this 它限制匹配到 bean 引用是给定类型实例的连接点。
target 它限制匹配到目标对象是给定类型的连接点。
args 它限制匹配到参数是给定类型实例的连接点。
@target 它限制匹配到目标对象具有给定类型注解的连接点。
@args 它限制匹配到运行时实际参数的类型具有给定类型注解的连接点。
@within 它限制匹配到目标对象声明的类型具有给定类型注解的连接点。
@annotation 它将匹配限制在具有给定注解的连接点的主语上。

如前所述,Spring 支持切入点设计器,其中 execution 是主要切入点设计器。因此,在这里我将向您展示如何使用 execution 设计器定义切入点。让我们看看如何在应用程序中编写切入点表达式。

编写切入点

我们可以使用 execution 设计器编写切入点如下:

  • execution(<方法模式>): 该方法必须匹配以下定义的模式

  • 可以使用以下运算符连接起来创建组合切入点: && (与), || (或), ! (非)

  • 方法模式:以下为方法模式:

    • [Modifiers] ReturnType [ClassType]

    • MethodName ([Arguments]) [throws ExceptionType]

在前面的方法模式中,方括号 [ ] 内的值,即修饰符、ClassType、参数和异常都是可选值。在使用 execution 设计器定义每个切入点时,没有必要定义它。没有方括号的值,如 ReturnTypeMethodName 是必须定义的。

让我们定义一个 TransferService 接口:

    package com.packt.patterninspring.chapter6.bankapp.service; 
    public interface TransferService { 
      void transfer(String accountA, String accountB, Long amount); 
    } 

TransferService 是用于从一个账户转移到另一个账户的金额的服务。假设您想编写一个触发 TransferServicetransfer() 方法的日志方面。以下图示了一个可以用于在 transfer() 方法执行时应用建议的切入点表达式:

如前图所示,您可以看到,我使用了 execution() 设计器来选择 TransferServicetransfer() 方法。在前图的先前表达式中,我在表达式的开头使用了星号。这意味着该方法可以返回任何类型。在星号之后,我指定了完全限定的类名和 transfer() 方法名。作为方法参数,我使用了双点 (..),这意味着切入点可以选择名为 transfer() 的方法,没有参数或任何数量的参数。

让我们看看以下一些更多的切入点表达式来选择连接点:

  • 任何类或包:

    • execution(void transfer*(String)): 任何以 transfer 开头,接受单个 String 参数且返回类型为 void 的方法

    • execution( transfer(*))*: 任何名为 transfer() 的方法,接受单个参数

    • execution( transfer(int, ..))*: 任何名为 transfer 的方法,其第一个参数为整数(".." 表示可能跟有零个或多个参数)

  • 限制为类:

    • execution(void com.packt.patterninspring.chapter6.bankapp.service.TransferServiceImpl.*(..)): TransferServiceImpl 类中的任何无返回值的方法,包括任何子类,但如果使用了不同的实现,则会被忽略。
  • 限制为接口:

    • execution(void com.packt.patterninspring.chapter6.bankapp.service.TransferService.transfer(*)): 任何接受一个参数的 void transfer() 方法,在实现 TransferService 的任何对象中,这是一个更灵活的选择——如果实现发生变化,它仍然可以工作。
  • 使用注解

    • execution(@javax.annotation.security.RolesAllowed void transfer*(..)): 任何以 transfer 开头并带有 @RolesAllowed 注解的 void 方法。
  • 与包一起工作

    • execution(* com..bankapp.*.*(..)): combankapp 之间有一个目录

    • execution(* com.*.bankapp.*.*(..)): bankappcom 之间可能有多个目录

    • execution(* *..bankapp.*.*(..)): 任何名为 bankapp 的子包

现在你已经看到了编写切入点的基础,让我们看看如何编写通知并声明使用这些切入点的方面

创建方面

如我之前所说,方面 是 AOP 中最重要的术语之一。方面将应用中的切入点和建议合并。让我们看看如何在应用中定义方面。

你已经将 TransferService 接口定义为你的方面切入点的主题。现在让我们使用 AspectJ 注解来创建一个方面。

使用注解定义方面

假设在你的银行应用中,你想要为审计和跟踪生成一个资金转账服务的日志,以了解客户的行为。一个业务如果不了解其客户就不会成功。无论你从商业的角度考虑什么,审计都是必要的,但它并不是业务本身的中心功能;它是一个单独的关注点。因此,将审计定义为应用于转账服务的方面是有意义的。让我们看看以下代码,它展示了定义这个关注点的 Auditing 类:

    package com.packt.patterninspring.chapter6.bankapp.aspect; 

    import org.aspectj.lang.annotation.AfterReturning; 
    import org.aspectj.lang.annotation.AfterThrowing; 
    import org.aspectj.lang.annotation.Aspect; 
    import org.aspectj.lang.annotation.Before; 

    @Aspect 
    public class Auditing { 

      //Before transfer service 
      @Before("execution(* com.packt.patterninspring.chapter6.bankapp.
      service.TransferService.transfer(..))")  
      public void validate(){ 
        System.out.println("bank validate your credentials before 
        amount transferring"); 
      } 

      //Before transfer service 
      @Before("execution(* com.packt.patterninspring.chapter6.bankapp.
      service.TransferService.transfer(..))")  
      public void transferInstantiate(){ 
        System.out.println("bank instantiate your amount 
        transferring"); 
      } 

      //After transfer service 
      @AfterReturning("execution(* com.packt.patterninspring.chapter6.
      bankapp.service.TransferService.transfer(..))") 
      public void success(){ 
        System.out.println("bank successfully transferred amount"); 
      } 

      //After failed transfer service 
      @AfterThrowing("execution(* com.packt.patterninspring.chapter6.
      bankapp.service.TransferService.transfer(..))") 
      public void rollback() { 
        System.out.println("bank rolled back your transferred amount"); 
      } 
    } 

正如你所见,Auditing 类被 @Aspect 注解所标注。这意味着这个类不仅仅是一个 Spring Bean,它还是应用的一个方面。Auditing 类有一些方法,这些方法就是通知,并在这些方法中定义了一些逻辑。正如我们所知,在开始从一个账户向另一个账户转账之前,银行会验证(validate())使用凭证,然后实例化(transferInstantiate())这个服务。验证成功(success())后,金额被转账,银行会审计它。但如果金额转账在任何情况下失败,那么银行应该回滚(rollback())这笔金额。

正如你所见,Auditing 方面的所有方法都被注解了通知注解,以指示这些方法应该在何时被调用。Spring AOP 提供了五种类型的通知注解来定义通知。让我们在以下表中查看:

注解 通知
@Before 它用于前置通知,advice 的方法在建议方法被调用之前执行。
@After 它用于后建议,建议的方法在受建议方法正常或异常执行后执行。
@AfterReturning 它用于返回后建议,建议的方法在受建议方法成功完成后执行。
@AfterThrowing 它用于抛出后建议,建议的方法在方法通过抛出异常异常终止后执行。
@Around 它用于环绕建议,建议的方法在调用受建议方法之前和之后执行。

让我们看看建议的实现以及它们如何在应用程序中工作。

建议实现

如您所知,Spring 提供了五种类型的建议,让我们逐一看看它们的工作流程。

建议类型 - 在前

让我们看看以下关于前建议的图示。此建议在目标方法之前执行:

如您在图中看到的,前建议首先执行,然后调用目标方法。正如我们所知,Spring AOP 是基于代理的。因此,创建了一个目标类的代理对象。它是基于代理设计模式和装饰器设计模式。

在前建议示例

让我们看看@Before注解的使用:

    //Before transfer service 
    @Before("execution(* com.packt.patterninspring.chapter6.
    bankapp.service.TransferService.transfer(..))")  
    public void validate(){ 
      System.out.println("bank validate your credentials before amount 
      transferring"); 
    } 

    //Before transfer service 
    @Before("execution(* com.packt.patterninspring.chapter6.
    bankapp.service.TransferService.transfer(..))")  
    public void transferInstantiate(){ 
      System.out.println("bank instantiate your amount transferring"); 
    } 

注意 - 如果建议抛出异常,则目标方法不会被调用 - 这是有效使用前建议的情况。

现在您已经看到了前建议,让我们看看另一种类型的建议。

建议类型:返回后

让我们看看以下关于返回后建议的图示。此建议在目标方法成功执行后执行:

如您在图中看到的,返回后建议在目标成功返回后执行。如果目标在应用程序中抛出任何异常,则此建议将不会执行。

返回后建议示例

让我们看看@AfterReturning注解的使用:

    //After transfer service 
    @AfterReturning("execution(* com.packt.patterninspring.chapter6.
    bankapp.service.TransferService.transfer(..))") 
    public void success(){ 
      System.out.println("bank successfully transferred amount"); 
    } 

现在您已经看到了返回后建议,让我们转向 Spring AOP 中的另一种类型建议。

建议类型:抛出后

让我们看看以下关于抛出后建议的图示。此建议在目标方法异常终止后执行。这意味着target方法抛出任何异常,然后此建议将被执行。请参考以下图表:

如您在图中看到的,抛出后建议在目标抛出异常后执行。如果目标在应用程序中没有抛出任何异常,则此建议将不会执行。

抛出后建议示例

让我们看看@AfterThrowing注解的使用:

    //After failed transfer service 
    @AfterThrowing("execution(* com.packt.patterninspring.chapter6.
    bankapp.service.TransferService.transfer(..))") 
    public void rollback() { 
      System.out.println("bank rolled back your transferred amount"); 
    } 

您还可以使用带有抛出属性的@AfterThrowing注解,它仅在抛出正确的异常类型时调用建议:

    //After failed transfer service 
    @AfterThrowing(value = "execution(*       
    com.packt.patterninspring.chapter6.
    bankapp.service.TransferService.transfer(..))", throwing="e")) 
    public void rollback(DataAccessException e) { 
      System.out.println("bank rolled back your transferred amount"); 
    } 

每当TransferService类抛出DataAccessException类型的异常时,都会执行。

@AfterThrowing 建议不会阻止异常的传播。然而,它可以抛出不同类型的异常。

建议类型:在后

让我们看看下面的图来了解 AfterAdvice。这个建议在 Target 方法正常或异常终止后执行。无论 Target 方法是否抛出异常或无异常执行,都没有关系:

图片

如图中所示,在 target 方法通过抛出任何异常或正常终止后,将执行后建议。

After 建议 示例

让我们看看 @After 注解的使用:

    //After transfer service 
    @After ("execution(* com.packt.patterninspring.chapter6.
    bankapp.service.TransferService.transfer(..))") 
    public void trackTransactionAttempt(){ 
      System.out.println("bank has attempted a transaction"); 
    } 

使用 @After 注解,无论目标是否抛出异常,都会被调用。

建议类型 - Around

让我们看看下面的图来了解 AroundAdvice。这个建议在调用 Target 方法之前和之后都执行。这是 Spring AOP 中非常强大的建议。Spring 框架的许多功能都是通过使用这个建议实现的。这是 Spring 中唯一一个具有停止或继续目标方法执行能力的建议。请参考以下图表:

图片

如前图所示,AroundAdvice 执行了两次,第一次是在被建议的方法执行之前,第二次是在被建议的方法被调用之后。此外,这个建议还调用了 proceed() 方法来在应用程序中执行被建议的方法。让我们看看下面的例子:

Around 建议示例

让我们看看 @Around 注解的使用:

    @Around(execution(*    com.packt.patterninspring.chapter6.
    bankapp.service.TransferService.createCache(..))) 
    public Object cache(ProceedingJoinPoint point){ 
    Object value = cacheStore.get(CacheUtils.toKey(point)); 
    if (value == null) { 
      value = point.proceed(); 
      cacheStore.put(CacheUtils.toKey(point), value); 
    } 
    return value; 
   } 

在这里,我使用了 @Around 注解和一个 ProceedingJoinPoint,它继承自 Join Point 并添加了 proceed() 方法。正如您在这个例子中所看到的,这个建议只有在值尚未在缓存中时才会继续执行目标。

您已经看到了如何在应用程序中使用注解实现建议,如何创建方面以及如何通过注解定义切入点。在这个例子中,我们使用 Auditing 作为方面类,并且它被 @Aspect 注解所标记,但如果您没有启用 Spring 的 AOP 代理行为,这个注解将不起作用。

让我们看看下面的 Java 配置文件,AppConfig.java,您可以通过在类级别应用 @EnableAspectJAutoProxy 注解来开启自动代理:

    package com.packt.patterninspring.chapter6.bankapp.config; 

    import org.springframework.context.annotation.Bean; 
    import org.springframework.context.annotation.ComponentScan; 
    import org.springframework.context.annotation.Configuration; 
    import org.springframework.context.annotation.
      EnableAspectJAutoProxy; 

    import com.packt.patterninspring.chapter6.bankapp.aspect.Auditing; 

    @Configuration 
    @EnableAspectJAutoProxy 
    @ComponentScan 
    public class AppConfig { 
      @Bean 
      public Auditing auditing() { 
         return new Auditing(); 
      } 
   } 

如果您正在使用 XML 配置,让我们看看如何在 Spring 中连接您的豆子以及如何通过使用 Spring AOP 命名空间中的 <aop:aspectj-autoproxy> 元素来启用 Spring AOP 功能:

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

      xsi:schemaLocation="http://www.springframework.org/schema/aop 
      http://www.springframework.org/schema/aop/spring-aop.xsd 
      http://www.springframework.org/schema/beans 
      http://www.springframework.org/schema/beans/spring-beans.xsd 
      http://www.springframework.org/schema/context 
      http://www.springframework.org/schema/context/spring-
      context.xsd"> 
      <context:component-scan base- 
      package="com.packt.patterninspring.chapter6.bankapp" /> 
      <aop:aspectj-autoproxy /> 
      <bean class="com.packt.patterninspring.chapter6.
      bankapp.aspect.Auditing" /> 
    </beans> 

让我们看看如何在 Spring XML 配置文件中声明方面。

使用 XML 配置定义方面

如我们所知,我们可以在基于 XML 的配置中配置豆子,同样您也可以在 XML 配置中声明方面。Spring 提供了另一个 AOP 命名空间,它提供了许多用于在 XML 中声明方面的元素,让我们在以下表中看看:

注解 并行 XML 元素 XML 元素的目的
@Before <aop:before> 它定义了前建议。
@After <aop:after> 它定义了后建议。
@AfterReturning <aop:after-returning> 它定义了返回后通知。
@AfterThrowing <aop:after-throwing> 它定义了抛出后通知。
@Around <aop:around> 它定义了环绕通知。
@Aspect <aop:aspect> 它定义了一个方面。
@EnableAspectJAutoProxy <aop:aspectj-autoproxy> 它通过 @AspectJ 启用注解驱动的方面。
@Pointcut <aop:pointcut> 它定义了一个切入点。
-- <aop:advisor> 它定义了 AOP 顾问
-- <aop:config> 它是顶级 AOP 元素

正如你在前面的表中所见,许多 AOP 命名空间元素与基于 Java 的配置中可用的相应注解并行。让我们看看以下基于 XML 的配置中的相同示例,首先看看方面类 Auditing。让我们移除以下代码中显示的所有 AspectJ 注解:

    package com.packt.patterninspring.chapter6.bankapp.aspect; 

    public class Auditing { 
      public void validate(){ 
        System.out.println("bank validate your credentials before 
        amount transferring"); 
      } 
      public void transferInstantiate(){ 
        System.out.println("bank instantiate your amount 
        transferring"); 
      } 
      public void success(){ 
        System.out.println("bank successfully transferred amount"); 
      } 
      public void rollback() { 
        System.out.println("bank rolled back your transferred amount"); 
      } 
    } 

正如你在前面的代码中所见,现在我们的方面类没有表明它是一个方面类。它是一个基本的 Java POJO 类,包含一些方法。让我们在下一节中看看如何在 XML 配置中声明通知:

    <aop:config> 
      <aop:aspect ref="auditing"> 
        <aop:before pointcut="execution(*    
        com.packt.patterninspring.chapter6.bankapp.
        service.TransferService.transfer(..))"  
        method="validate"/> 
        <aop:before pointcut="execution(*  
        com.packt.patterninspring.chapter6.bankapp.
        service.TransferService.transfer(..))"  
        method="transferInstantiate"/> 
        <aop:after-returning pointcut="execution(*  
        com.packt.patterninspring.chapter6.
        bankapp.service.TransferService.transfer(..))"  
        method="success"/> 
        <aop:after-throwing pointcut="execution(*  
        com.packt.patterninspring.chapter6.bankapp.
        service.TransferService.transfer(..))"  
        method="rollback"/> 
      </aop:aspect> 
    </aop:config> 

正如你所见,<aop-config> 使用了一个顶级元素。在 <aop:config> 中,你声明其他元素,如 <aop:aspect>,这个元素有 ref 属性,它引用了 POJO 实例 Auditing。这表明 Auditing 是应用程序中的一个方面类。现在 <aop-aspect> 元素有通知和切入点元素。所有逻辑都与我们在 Java 配置中定义的相同。

让我们在下一节中看看 Spring 如何创建 AOP 代理。

理解 AOP 代理

如你所知,Spring AOP 是基于代理的。这意味着 Spring 创建代理来在业务逻辑(即 target 对象)之间编织方面。它是基于代理和装饰器设计模式的。让我们看看 TransferServiceImpl 类作为 TransferService 接口的一个实现:

    package com.packt.patterninspring.chapter6.bankapp.service; 
    import org.springframework.stereotype.Service; 
    public class TransferServiceImpl implements TransferService { 
      @Override 
      public void transfer(String accountA, String accountB, Long 
      amount) { 
        System.out.println(amount+" Amount has been tranfered from 
        "+accountA+" to "+accountB); 
      } 
    } 

调用者通过对象引用直接调用此服务(transfer() 方法),让我们看看以下图解以了解更多:

正如你所见,调用者可以直接调用服务并执行分配给它的任务。

但你将这个 TransferService 声明为方面的目标。由于这样做,事情略有变化。现在这个由代理包装的类,客户端代码实际上并不直接调用这个服务,而是通过这个代理进行路由。让我们看看下面的图解。

正如你在前面的图解中所见,Spring 按以下顺序将 AOP 代理应用于对象:

  1. Spring 创建了一个编织方面和目标的代理。

  2. 代理也实现了目标接口,即 TransferServive 接口。

  3. 所有对传输服务方法 transfer() 的调用都通过代理拦截器路由。

  4. 匹配的通知被执行。

  5. 然后 target 方法被执行。

如前所述列表,这是当你调用由 Spring 创建的代理的方法时的流程。

您在本章中已经看到了 Spring AOP 框架,它实际上使用基于代理的方面织入实现了 AspectJ 框架的一部分。我认为,这为 Spring AOP 提供了很好的知识。

摘要

在本章中,我们看到了 Spring AOP 框架并使用了该模块背后的设计模式。AOP 是一个非常强大的范式,它补充了面向对象编程。面向切面编程AOP)模块化了跨切面关注点,如日志记录、安全和事务。方面是一个带有 @Aspect 注解的 Java 类。它定义了一个包含跨切面行为的模块。此模块与应用程序的业务逻辑分离。我们可以在应用程序中与其他业务模块一起重用它,而无需进行任何更改。

在 Spring AOP 中,行为被实现为一个建议方法。您在 Spring 中已经学到,有五种类型,分别是 Before、AfterThrowing、AfterReturning、After 和 Around。Around 建议是一个非常强大的建议,通过使用 Around 建议实现了有趣的功能。您已经学到了如何使用加载时织入来织入这些建议。

您已经看到了如何在 Spring 应用程序中声明切入点,切入点选择建议应用的位置。

现在,我们将转向关键部分,看看 Spring 如何在后台工作以连接数据库并读取应用程序的数据。从下一章开始,您将看到如何使用 Spring 中的 JDBC 模板构建应用程序。

第七章:使用 Spring 和 JDBC 模板模式访问数据库

在前面的章节中,你学习了关于 Spring 核心模块的知识,如 Spring IoC 容器、DI 模式、容器生命周期以及使用的设计模式。你还看到了 Spring 如何使用 AOP 来施展魔法。现在是时候进入真实 Spring 应用程序的战场,使用持久化数据了。你还记得大学时期你第一次处理数据库访问的应用程序吗?那时,你可能不得不编写无聊的样板代码来加载数据库驱动程序、初始化你的数据访问框架、打开连接、处理各种异常,以及关闭连接。你还必须非常小心这段代码。如果出了任何问题,即使你在这无聊的代码上投入了大量时间,你也不会在你的应用程序中建立数据库连接。

因为我们总是试图使事情变得更好、更简单,所以我们必须关注解决数据访问中繁琐工作的解决方案。Spring 为数据访问中的繁琐和无聊工作提供了一个解决方案--它移除了数据访问的代码。Spring 提供了数据访问框架,以集成各种数据访问技术。它允许你直接使用 JDBC 或任何 对象关系映射ORM)框架,如 Hibernate,以持久化你的数据。Spring 处理你应用程序中数据访问工作的所有底层代码;你只需编写你的 SQL、应用程序逻辑,并管理你应用程序的数据,而不是花费时间编写创建和关闭数据库连接的代码,等等。

现在,你可以选择任何技术,例如 JDBC、Hibernate、Java 持久性 APIJPA)或其他技术来持久化你应用程序的数据。无论你选择什么,Spring 都为你应用程序提供对这些技术的支持。在本章中,我们将探讨 Spring 对 JDBC 的支持。它将涵盖以下内容:

  • 设计数据访问的最佳方法

  • 实现模板设计模式

  • 传统 JDBC 的问题

  • 使用 Spring 的 JdbcTemplate 解决问题

  • 配置数据源

  • 使用对象池设计模式来维护数据库连接

  • 通过 DAO 模式抽象数据库访问

  • JdbcTemplate 一起工作

  • Jdbc 回调接口

  • 在应用程序中配置 JdbcTemplate 的最佳实践

在我们继续讨论 JDBC 和模板设计模式之前,让我们首先看看在分层架构中定义数据访问层最佳的方法。

设计数据访问的最佳方法

在前面的章节中,您已经看到 Spring 的一个目标是通过遵循 OOPs 编码到接口的原则之一来开发应用程序。任何企业应用程序都需要读取数据并将数据写入任何类型的数据库,为了满足这一需求,我们必须编写持久化逻辑。Spring 允许您避免在应用程序的所有模块中分散持久化逻辑。为此,我们可以为数据访问和持久化逻辑创建不同的组件,这个组件被称为数据访问对象DAO)。让我们看看,在以下图中,创建分层应用程序模块的最佳方法:

图片

如前图所示,为了更好的方法,许多企业应用程序由以下三个逻辑层组成:

  • 服务层(或应用程序层):应用程序的这一层公开高级应用程序功能,如用例和业务逻辑。所有应用程序服务都定义在这里。

  • 数据访问层:应用程序的这一层定义了对应用程序数据存储(如关系型或 NoSQL 数据库)的接口。这一层包含具有数据访问逻辑并将数据持久化到应用程序中的类和接口。

  • 基础设施层:应用程序的这一层向其他层公开低级服务,例如通过使用数据库 URL、用户凭据等来配置 DataSource。此类配置属于这一层。

在前图中,您可以看到服务层数据访问层协作。为了避免应用程序逻辑和数据访问逻辑之间的耦合,我们应该通过接口公开它们的功能,因为接口促进了协作组件之间的解耦。如果我们通过实现接口使用数据访问逻辑,我们可以在不修改服务层中的应用程序逻辑的情况下,为应用程序配置任何特定的数据访问策略。以下图显示了设计我们的数据访问层的正确方法:

图片

如前图所示,您的应用程序服务对象,即TransferService,不处理自己的数据访问。相反,它们将数据访问委托给存储库。存储库的接口,即您应用程序中的AccountRepository,使其与服务对象松散耦合。您可以配置任何实现变体——无论是AccountRepository的 Jpa 实现(JpaAccountRepository),还是AccountRepository的 Jdbc 实现(JdbcAccountRepository)。

Spring 不仅在分层架构中不同层工作的应用组件之间提供松耦合,还帮助企业分层架构应用管理资源。让我们看看 Spring 如何管理资源,以及 Spring 使用什么设计模式来解决资源管理问题。

资源管理问题

让我们通过一个真实例子来理解资源管理问题。你肯定在线上点过披萨。如果是这样,从下单到披萨送达的过程中涉及哪些步骤?这个过程有很多步骤——我们首先访问披萨公司的在线门户,选择披萨的大小和配料。然后,我们下订单并结账。订单由最近的披萨店接受;他们相应地准备我们的披萨,相应地加上配料,将披萨包在袋子里,送餐员来到你的地方并将披萨交给你,最后,你和你的朋友一起享用披萨。尽管这个过程有很多步骤,但你只积极参与其中的一两个步骤。披萨公司负责烹饪披萨并顺利交付。你只在你需要的时候参与,其他步骤由披萨公司负责。正如你在例子中看到的,管理这个过程涉及许多步骤,我们还需要相应地分配资源到每个步骤,以确保它被视为一个完整的任务,没有任何流程中断。这是一个强大的设计模式——模板方法模式的完美场景。Spring 框架通过实现这个模板设计模式来处理应用 DAO 层中的此类场景。让我们看看如果我们不使用 Spring,而是使用传统应用会面临什么问题。

在传统应用中,我们使用 JDBC API 从数据库访问数据。这是一个简单的应用,我们使用 JDBC API 访问和持久化数据,对于这个应用,以下步骤是必需的:

  1. 定义连接参数。

  2. 访问数据源,并建立连接。

  3. 开始一个事务。

  4. 指定 SQL 语句。

  5. 声明参数,并提供参数值。

  6. 准备并执行语句。

  7. 设置循环以遍历结果。

  8. 为每个迭代执行工作——执行业务逻辑。

  9. 处理任何异常。

  10. 提交或回滚事务。

  11. 关闭连接、语句和结果集。

如果你使用 Spring 框架来处理相同的应用程序,那么你必须编写前面列表中某些步骤的代码,而 Spring 则负责所有涉及低级过程(如建立连接、开始事务、处理数据层中的任何异常和关闭连接)的步骤。Spring 通过使用模板方法设计模式来管理这些步骤,我们将在下一节中学习。

实现模板设计模式

在一个操作中定义算法的框架,将一些步骤推迟到子类中。模板方法允许子类重新定义算法的某些步骤,而不改变算法的结构。

-GOF 设计模式

我们在第三章“考虑结构和行为模式”中讨论了模板方法设计模式。它被广泛使用,属于 GOF 设计模式家族的结构设计模式。此模式定义了算法的轮廓或框架,并将细节留给后续的具体实现。此模式隐藏了大量样板代码。Spring 提供了许多模板类,如JdbcTemplateJmsTemplateRestTemplateWebServiceTemplate。大多数情况下,此模式隐藏了之前在披萨示例中讨论的低级资源管理。

在示例中,过程是从在线门户订购外卖披萨。披萨公司为每位顾客遵循一些固定的步骤,例如接收订单、准备披萨、根据顾客的规格添加配料,并将其送到顾客的地址。我们可以添加这些步骤,或将这些步骤定义为特定的算法。然后,系统可以相应地实施此算法。

Spring 通过实现此模式来访问数据库中的数据。在数据库或任何其他技术中,有一些步骤始终是通用的,例如建立与数据库的连接、处理事务、处理异常,以及每个数据访问过程所需的某些清理操作。但也有一些步骤不是固定的,而是取决于应用程序的需求。定义这些步骤是开发者的责任。但是,Spring 允许我们将数据访问过程的固定部分和动态部分分别作为模板和回调分开。所有固定步骤都属于模板,而动态自定义步骤属于回调。以下图详细描述了这两个部分:

如前图所示,数据访问过程中的所有固定部分都封装到了 Spring 框架的模板类中,包括打开和关闭连接、打开和关闭语句、处理异常和管理资源。但像编写 SQL 语句、声明连接参数等步骤则是回调的一部分,而回调由开发者处理。

Spring 提供了多种模板方法设计模式的实现,例如JdbcTemplateJmsTemplateRestTemplateWebServiceTemplate,但在这章中,我将仅解释其 JDBC API 的实现,即JdbcTemplate。还有一个JdbcTemplate-NamedParameterJdbcTemplate的变体,它包装了一个JdbcTemplate以提供命名参数而不是传统的 JDBC "?"占位符。

传统 JDBC 的问题

以下是我们每次使用传统 JDBC API 时必须面对的问题:

  • 由于代码易出错而导致冗余结果:传统的 JDBC API 需要编写大量繁琐的代码来处理数据访问层。让我们看看以下代码来连接数据库并执行所需的查询:
        public List<Account> findByAccountNumber(Long accountNumber) { 
          List<Account> accountList = new ArrayList<Account>(); 
          Connection conn = null; 
          String sql = "select account_name,
          account_balance from ACCOUNT where account_number=?"; 
          try { 
            DataSource dataSource = DataSourceUtils.getDataSource(); 
            conn = dataSource.getConnection(); 
            PreparedStatement ps = conn.prepareStatement(sql); 
            ps.setLong(1, accountNumber); 
            ResultSet rs = ps.executeQuery(); 
            while (rs.next()) { 
              accountList.add(new Account(rs.getString(
                "account_name"), ...)); 
            } 
          } catch (SQLException e) { /* what to be handle here? */ } 
          finally { 
            try { 
              conn.close(); 
            } catch (SQLException e) { /* what to be handle here ?*/ } 
          } 
          return accountList; 
        } 

如前代码所示,有一些行被突出显示;只有这些粗体代码才是重要的——其余的都是样板代码。此外,这段代码在应用程序中处理 SQLException 的方式效率低下,因为开发者不知道应该在哪里处理。现在让我们看看传统 JDBC 代码中的另一个问题。

  • 导致异常处理不佳:在前面的代码中,应用程序中的异常处理非常糟糕。开发者不清楚应该处理哪些异常。SQLException 是一个检查型异常,这意味着它强制开发者处理错误,但如果无法处理,则必须声明它。这是一种非常糟糕的异常处理方式,中间方法必须从代码中的所有方法声明异常(s)。这是一种紧密耦合的形式。

解决 Spring 的 JdbcTemplate 问题

Spring 的JdbcTemplate解决了上一节中列出的两个问题。JdbcTemplate极大地简化了 JDBC API 的使用,并消除了重复的样板代码。它缓解了常见的错误原因,并正确处理 SQLExceptions,而不牺牲功能。它提供了对标准 JDBC 构造的完全访问。让我们看看使用 Spring 的JdbcTemplate类来解决这两个问题的相同代码:

  • 使用 JdbcTemplate 从应用程序中删除冗余代码:假设您想获取银行账户的数量。如果您使用JdbcTemplate类,则需要以下代码:
        int count = jdbcTemplate.queryForObject("SELECT COUNT(*)
         FROM ACCOUNT", Integer.class); 

如果您想访问特定用户 ID 的账户列表:

        List<Account> results = jdbcTemplate.query(someSql,
         new RowMapper<Account>() { 
           public Account mapRow(ResultSet rs, int row) throws 
            SQLException { 
              // map the current row to an Account object 
            } 
        }); 

如前代码所示,您不需要编写打开和关闭数据库连接、准备执行查询的语句等代码。

  • 数据访问异常:Spring 提供了一个一致的异常层次结构来处理技术特定的异常,如 SQLException,并将其封装到自己的异常类层次结构中,其中DataAccessException作为根异常。Spring 将这些原始异常包装成不同的未检查异常。现在 Spring 不会强迫开发者在开发时间处理这些异常。Spring 提供了DataAccessException层次结构来隐藏你是在使用 JPA、Hibernate、JDBC 还是类似的技术。实际上,它是一个子异常的层次结构,而不是一个针对所有情况的单一异常。它在所有支持的数据访问技术中都是一致的。以下图表描述了 Spring 数据访问异常的层次结构:

图片

  • 如前图所示,Spring 的DataAccessException扩展了RuntimeException,即它是一个未检查的异常。在企业应用中,未检查的异常可以被抛到调用层次结构中的最佳处理位置。好事是应用中的方法之间并不知道这一点。

首先,让我们讨论如何在 Spring 应用中配置数据源以连接数据库,然后再声明模板和仓库。

配置数据源和对象池模式

在 Spring 框架中,DataSource 是 JDBC API 的一部分,它提供了数据库的连接。它隐藏了连接池、异常处理和事务管理问题的大量样板代码,从而从应用代码中抽象出来。作为开发者,你只需让它专注于你的业务逻辑即可。无需担心连接池、异常处理和管理事务;在生产环境中如何设置容器管理的数据源是应用管理员的职责。你只需编写代码,并测试这些代码。

在企业应用中,我们可以通过几种方式检索 DataSource。我们可以使用 JDBC 驱动来检索 DataSource,但在生产环境中创建 DataSource 不是最佳方法。因为性能是应用开发期间的关键问题之一,Spring 通过实现对象池模式以非常高效的方式为应用提供 DataSource。对象池模式表明“对象的创建比重用更昂贵。”

Spring 允许我们在应用中实现对象池模式以重用 DataSource 对象。你可以使用应用服务器和容器管理的池(JNDI),或者你可以使用第三方库如 DBCP、c3p0 等来创建容器。这些池有助于更好地管理可用的数据源。

在你的 Spring 应用中,有几种配置数据源 bean 的选项,如下所示:

  • 使用 JDBC 驱动配置数据源

  • 实现对象池设计模式以提供数据源对象

    • 使用 JNDI 配置数据源

    • 使用池连接配置数据源

      • 通过实现 Builder 模式创建嵌入式数据源
  • 让我们看看如何在 Spring 应用程序中配置数据源 bean。

使用 JDBC 驱动程序配置数据源

使用 JDBC 驱动程序配置数据源 bean 是 Spring 中最简单的数据源。Spring 提供以下三个数据源类:

  • DriverManagerDataSource:对于每个连接请求都创建一个新的连接

  • SimpleDriverDataSource:与DriverManagerDataSource类似,但它直接与 JDBC 驱动程序一起工作

  • SingleConnectionDataSource:对于每个连接请求都返回相同的连接,但它不是一个池化数据源

让我们看看以下代码,用于在您的应用程序中使用 Spring 的DriverManagerDataSource类配置数据源 bean:

在基于 Java 的配置中,代码如下:

    DriverManagerDataSource dataSource = new DriverManagerDataSource(); 
    dataSource.setDriverClassName("org.h2.Driver"); 
    dataSource.setUrl("jdbc:h2:tcp://localhost/bankDB"); 
    dataSource.setUsername("root"); 
    dataSource.setPassword("root"); 

在基于 XML 的配置中,代码将如下所示:

    <bean id="dataSource"
     class="org.springframework.jdbc.datasource
     .DriverManagerDataSource"> 
     <property name="driverClassName" value="org.h2.Driver"/> 
     <property name="url" value="jdbc:h2:tcp://localhost/bankDB"/> 
     <property name="username" value="root"/> 
     <property name="password" value="root"/> 
    </bean> 

在前面代码中定义的数据源是一个非常简单的数据源,我们可以在开发环境中使用它。它不是一个适合生产环境的数据源。我个人更喜欢使用 JNDI 来配置生产环境的数据源。让我们看看怎么做。

让我们实现对象池设计模式,通过配置使用 JNDI 的数据源来提供数据源对象。

在 Spring 应用程序中,您可以通过使用 JNDI 查找来配置数据源。Spring 提供了来自 Spring 的 JEE 命名空间的<jee:jndi-lookup>元素。让我们看看这个配置的代码。

在 XML 配置中,代码如下所示:

    <jee:jndi-lookup id="dataSource"
     jndi-name="java:comp/env/jdbc/datasource" /> 

在 Java 配置中,代码如下:

    @Bean 
    public JndiObjectFactoryBean dataSource() { 
      JndiObjectFactoryBean jndiObject = new JndiObjectFactoryBean(); 
      jndiObject.setJndiName("jdbc/datasource"); 
      jndiObject.setResourceRef(true); 
      jndiObject.setProxyInterface(javax.sql.DataSource.class); 
      return jndiObject; 
    } 

类似于 WebSphere 或 JBoss 这样的应用服务器允许您通过 JNDI 配置数据源以进行准备。甚至像 Tomcat 这样的 Web 容器也允许您通过 JNDI 配置数据源以进行准备。这些服务器管理您应用程序中的数据源。这很有益,因为数据源的性能会更高,因为应用服务器通常都是池化的。并且它们可以完全在应用程序外部进行管理。这是配置通过 JNDI 检索数据源的最佳方式之一。如果您在生产环境中无法通过 JNDI 查找来检索,您可以选择另一个更好的选项,我们将在下面讨论。

使用池连接配置数据源

以下开源技术提供了池化数据源:

  • Apache Commons DBCP

  • c3p0

  • BoneCP

以下代码配置了 DBCP 的BasicDataSource

基于 XML 的 DBCP 配置如下:

    <bean id="dataSource" 
      class="org.apache.commons.dbcp.BasicDataSource"
       destroy-method="close"> 
      <property name="driverClassName" value="org.h2.Driver"/> 
      <property name="url" value="jdbc:h2:tcp://localhost/bankDB"/> 
      <property name="username" value="root"/> 
      <property name="password" value="root"/> 
      <property name="initialSize" value="5"/> 
      <property name="maxActive" value="10"/> 
    </bean> 

基于 Java 的 DBCP 配置如下:

    @Bean 
    public BasicDataSource dataSource() { 
      BasicDataSource dataSource = new BasicDataSource(); 
      dataSource.setDriverClassName("org.h2.Driver"); 
      dataSource.setUrl("jdbc:h2:tcp://localhost/bankDB"); 
      dataSource.setUsername("root"); 
      dataSource.setPassword("root"); 
      dataSource.setInitialSize(5); 
      dataSource.setMaxActive(10); 
      return dataSource; 
    } 

如前述代码所示,还有许多其他属性是为池化数据源提供者引入的。Spring 中BasicDataSource类的属性列表如下:

  • initialSize: 这是池初始化时创建的连接数量。

  • maxActive: 这是池初始化时可以从池中分配的最大连接数。如果您将此值设置为 0,则表示没有限制。

  • maxIdle: 这是池中可以空闲的最大连接数,而无需释放额外的连接。如果您将此值设置为 0,则表示没有限制。

  • maxOpenPreparedStatements: 这是在池初始化时可以从语句池中分配的最大准备语句数量。如果您将此值设置为 0,则表示没有限制。

  • maxWait: 这是抛出异常之前等待连接返回池中的最大时间。如果您将其设置为 1,则表示无限期等待。

  • minEvictableIdleTimeMillis: 这是连接在池中保持空闲状态的最长时间,在此之后它才有资格被回收。

  • minIdle: 这是池中可以保持空闲的最小连接数,而无需创建新的连接。

实现构建器模式以创建嵌入式数据源

在应用程序开发中,嵌入式数据库非常有用,因为它不需要一个单独的数据库服务器,您的应用程序可以连接到它。Spring 为嵌入式数据库提供另一个数据源。它对于生产环境来说并不足够强大。我们可以使用嵌入式数据源进行开发和测试环境。在 Spring 中,jdbc 命名空间如下配置嵌入式数据库 H2

在 XML 配置中,H2 配置如下:

    <jdbc:embedded-database id="dataSource" type="H2"> 
     <jdbc:script location="schema.sql"/> 
     <jdbc:script location="data.sql"/> 
    </jdbc:embedded-database> 

在 Java 配置中,H2 配置如下:

    @Bean 
    public DataSource dataSource(){ 
      EmbeddedDatabaseBuilder builder =
        new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2); 
      builder.addScript("schema.sql"); 
      builder.addScript("data.sql"); 
      return builder.build(); 
    } 

如前述代码所示,Spring 提供了 EmbeddedDatabaseBuilder 类。它实际上实现了构建器设计模式来创建 EmbeddedDatabaseBuilder 类的对象。

让我们看看下一节中的另一个设计模式。

使用 DAO 模式抽象数据库访问

数据访问层在业务层和数据库之间作为一个方面工作。数据访问依赖于业务调用,并且根据数据源的不同而变化,例如数据库、平面文件、XML 等。因此,我们可以通过提供一个接口来抽象所有访问。这被称为数据访问对象模式。从应用程序的角度来看,它访问关系数据库或使用 DAO 解析 XML 文件时没有区别。

在早期版本中,EJB 提供了由容器管理的实体豆;它们是分布式、安全且事务性的组件。这些豆对客户端非常透明,也就是说,对于应用程序中的服务层,它们具有自动持久性,无需关心底层数据库。但通常,实体豆提供的功能对于你的应用程序来说不是必需的,因为你需要将数据持久化到数据库中。由于这个原因,实体豆的一些非必需功能,如网络流量,增加了,影响了应用程序的性能。当时,实体豆需要在 EJB 容器中运行,这就是为什么很难测试。

简而言之,如果你正在使用传统的 JDBC API 或更早的 EJB 版本,你将在应用程序中遇到以下问题:

  • 在传统的 JDBC 应用程序中,你将业务层逻辑与持久化逻辑合并。

  • 持久层或 DAO 层对于服务层或业务层来说并不一致。但在企业应用程序中,DAO 应该对服务层保持一致。

  • 在传统的 JDBC 应用程序中,你必须处理大量的样板代码,如创建和关闭连接、准备语句、处理异常等。这降低了可重用性并增加了开发时间。

  • 使用 EJB,实体豆被视为应用程序的额外开销,并且很难测试。

让我们看看 Spring 如何解决这些问题。

基于 Spring 框架的 DAO 模式

Spring 提供了一个全面的 JDBC 模块来设计和开发基于 JDBC 的 DAO。这些应用程序中的 DAO 负责处理 JDBC API 的所有样板代码,并帮助提供一致的数据访问 API。在 Spring JDBC 中,DAO 是一个通用的对象,用于访问业务层的数据库,并为业务层的服务提供一致的接口。DAO 类背后的主要目标是抽象出业务层服务底层数据访问逻辑。

在我们之前的例子中,我们看到了披萨公司如何帮助我们理解资源管理问题,现在,我们将继续使用我们的银行应用程序。让我们看看以下示例,了解如何在应用程序中实现 DAO。假设,在我们的银行应用程序中,我们想要获取城市中某个分支行的总账户数。为此,我们首先为 DAO 创建一个接口。正如之前讨论的那样,这促进了面向接口的编程。这是设计原则的最佳实践之一。这个 DAO 接口将被注入到业务层的服务中,我们可以根据应用程序中的底层数据库创建 DAO 接口的多个具体类。这意味着我们的 DAO 层将保持与业务层的一致性。让我们创建一个如下所示的 DAO 接口:

    package com.packt.patterninspring.chapter7.bankapp.dao; 
    public interface AccountDao { 
      Integer totalAccountsByBranch(String branchName); 
    } 

让我们看看使用 Spring 的 JdbcDaoSupport 类实现 DAO 接口的具体实现:

    package com.packt.patterninspring.chapter7.bankapp.dao; 

    import org.springframework.jdbc.core.support.JdbcDaoSupport; 
    public class AccountDaoImpl extends JdbcDaoSupport implements
     AccountDao { 
       @Override 
       public Integer totalAccountsByBranch(String branchName) { 
         String sql = "SELECT count(*) FROM Account WHERE branchName =
          "+branchName; 
         return this.getJdbcTemplate().queryForObject(sql,
          Integer.class); 
       } 
    } 

在前面的代码中,您可以看到 AccountDaoImpl 类实现了 AccountDao DAO 接口,并扩展了 Spring 的 JdbcDaoSupport 类以简化基于 JDBC 的开发。此类通过 getJdbcTemplate() 为其子类提供 JdbcTemplateJdbcDaoSupport 类与数据源相关联,并为 DAO 提供用于使用的 JdbcTemplate 对象。

使用 JdbcTemplate

如您之前所学的,Spring 的 JdbcTemplate 解决了应用程序中的两个主要问题。它解决了冗余代码问题以及应用程序中数据访问代码的糟糕异常处理。如果没有 JdbcTemplate,查询一行数据所需的代码只有 20%,但 80% 是样板代码,用于处理异常和管理资源。如果您使用 JdbcTemplate,则无需担心 80% 的样板代码。简而言之,Spring 的 JdbcTemplate 负责以下:

  • 连接的获取

  • 参与事务

  • 语句的执行

  • 处理结果集

  • 处理任何异常

  • 连接的释放

让我们看看在应用程序中何时使用 JdbcTemplate,以及如何创建它。

何时使用 JdbcTemplate

JdbcTemplate 在独立应用程序中非常有用,在任何需要 JDBC 的情况下都适用。它适合在实用程序或测试代码中清理混乱的遗留代码。此外,在任何分层应用程序中,您都可以实现存储库或数据访问对象。让我们看看如何在应用程序中创建它。

在应用程序中创建 JdbcTemplate

如果您想在 Spring 应用程序中创建 JdbcTemplate 类的实例以访问数据,您需要记住它需要一个 DataSource 来创建数据库连接。让我们创建一个模板一次,并重用它。不要为每个线程创建一个,它构建后是线程安全的:

    JdbcTemplate template = new JdbcTemplate(dataSource); 

让我们使用以下 @Bean 方法在 Spring 中配置一个 JdbcTemplate 对象:

    @Bean 
    public JdbcTemplate jdbcTemplate(DataSource dataSource) { 
      return new JdbcTemplate(dataSource); 
    } 

在前面的代码中,我们使用构造函数注入在 Spring 应用程序中将 DataSource 注入到 JdbcTemplate 对象中。被引用的 dataSource 对象可以是 javax.sql.DataSource 的任何实现。让我们看看如何在基于 JDBC 的存储库中使用 JdbcTemplate 对象来访问应用程序中的数据库。

实现基于 JDBC 的存储库

我们可以使用 Spring 的 JdbcTemplate 类在 Spring 应用程序中实现存储库。让我们看看如何基于 JDBC 模板实现存储库类:

    package com.packt.patterninspring.chapter7.bankapp.repository; 

    import java.sql.ResultSet; 
    import java.sql.SQLException; 

    import javax.sql.DataSource; 

    import org.springframework.jdbc.core.JdbcTemplate; 
    import org.springframework.jdbc.core.RowMapper; 
    import org.springframework.stereotype.Repository; 

    import com.packt.patterninspring.chapter7.bankapp.model.Account; 
    @Repository 
    public class JdbcAccountRepository implements AccountRepository{ 

      JdbcTemplate jdbcTemplate; 

      public JdbcAccountRepository(DataSource dataSource) { 
        super(); 
        this.jdbcTemplate = new JdbcTemplate(dataSource); 
      } 

      @Override 
      public Account findAccountById(Long id){ 
        String sql = "SELECT * FROM Account WHERE id = "+id; 
        return jdbcTemplate.queryForObject(sql,
         new RowMapper<Account>(){ 
           @Override 
           public Account mapRow(ResultSet rs, int arg1) throws
           SQLException { 
             Account account = new Account(id); 
             account.setName(rs.getString("name")); 
             account.setBalance(new Long(rs.getInt("balance"))); 
             return account; 
           } 
         }); 
      } 
    } 

在前面的代码中,使用构造函数注入将 DataSource 对象注入到 JdbcAccountRepository 类中。通过使用此数据源,我们创建了一个 JdbcTemplate 对象以访问数据。JdbcTemplate 提供以下方法来从数据库访问数据:

  • queryForObject(..): 这是一个针对简单 Java 类型(intlongStringDate ...)和自定义域对象的查询。

  • queryForMap(..): 当期望单行时使用。JdbcTemplateResultSet 的每一行作为 Map 返回。

  • queryForList(..): 当期望多行时使用。

注意,queryForIntqueryForLong 自 Spring 3.2 以来已被弃用;你可以直接使用 queryForObject 代替(API 在 Spring 3 中得到改进)。

通常,将关系数据映射到域对象中非常有用,例如,将 ResultSet 映射到最后一行代码中的 Account。Spring 的 JdbcTemplate 通过使用回调方法支持这一点。让我们在下一节讨论 Jdbc 回调接口。

Jdbc 回调接口

Spring 提供了以下三个 JDBC 回调接口:

  • 实现 RowMapper:Spring 提供了一个 RowMapper 接口,用于将 ResultSet 的单行映射到对象。它可以用于单行和多行查询。自 Spring 3.0 起它是参数化的:
      public interface RowMapper<T> { 
        T mapRow(ResultSet rs, int rowNum) 
        throws SQLException; 
      } 
  • 让我们通过一个示例来理解这一点。

创建 RowMapper 类

在以下示例中,一个类,AccountRowMapper,实现了 Spring Jdbc 模块的 RowMapper 接口:

    package com.packt.patterninspring.chapter7.bankapp.rowmapper; 

    import java.sql.ResultSet; 
    import java.sql.SQLException; 
    import org.springframework.jdbc.core.RowMapper; 
    import com.packt.patterninspring.chapter7.bankapp.model.Account; 
    public class AccountRowMapper implements RowMapper<Account>{ 
      @Override 
      public Account mapRow(ResultSet rs, int id) throws SQLException { 
        Account account = new Account(); 
        account.setId(new Long(rs.getInt("id"))); 
        account.setName(rs.getString("name")); 
        account.setBalance(new Long(rs.getInt("balance"))); 
        return account; 
      } 
    } 

在前面的代码中,一个类,AccountRowMapper,将结果集的一行映射到域对象。这个行映射器类实现了 Spring Jdbc 模块的 RowMapper 回调接口。

使用 JdbcTemplate 查询单行

现在我们来看看行映射器如何在以下代码中将单行映射到应用程序中的域对象:

    public Account findAccountById(Long id){ 
      String sql = "SELECT * FROM Account WHERE id = "+id; 
      return jdbcTemplate.queryForObject(sql, new AccountRowMapper()); 
    } 

在这里,不需要为 Account 对象添加类型转换。AccountRowMapper 类将行映射到 Account 对象。

查询多行

以下代码展示了行映射器如何将多行映射到域对象列表:

    public List<Account> findAccountById(Long id){ 
      String sql = "SELECT * FROM Account "; 
      return jdbcTemplate.queryForList(sql, new AccountRowMapper()); 
    } 

ResultSet 的每一行都映射到域对象时,RowMapper 是最佳选择。

实现 RowCallbackHandler

当没有返回对象时,Spring 提供了一个更简单的 RowCallbackHandler 接口。它用于将行流式传输到文件中,将行转换为 XML,并在添加到集合之前进行过滤。但 SQL 中的过滤效率更高,对于大型查询来说比 JPA 等效更快。让我们看看以下示例:

    public interface RowCallbackHandler { 
      void processRow(ResultSet rs) throws SQLException; 
    } 

使用 RowCallbackHandler 的示例

以下代码是应用程序中 RowCallbackHandler 的一个示例:

    package com.packt.patterninspring.chapter7.bankapp.callbacks; 
    import java.sql.ResultSet; 
    import java.sql.SQLException; 
    import org.springframework.jdbc.core.RowCallbackHandler; 
    public class AccountReportWriter implements RowCallbackHandler { 
      public void processRow(ResultSet resultSet) throws SQLException { 
        // parse current row from ResultSet and stream to output 
        //write flat file, XML 
      } 
    } 

在前面的代码中,我们已经创建了一个 RowCallbackHandler 实现类;AccountReportWriter 类实现了这个接口来处理从数据库返回的结果集。让我们看看以下代码如何使用 AccountReportWriter 回调类:

    @Override 
    public void generateReport(Writer out, String branchName) { 
      String sql = "SELECT * FROM Account WHERE branchName = "+
       branchName; 
      jdbcTemplate.query(sql, new AccountReportWriter()); 
    } 

当回调方法对于每一行都不应返回值时,RowCallbackHandler 是最佳选择,尤其是在大型查询中。

实现 ResultSetExtractor

Spring 提供了一个 ResultSetExtractor 接口,用于一次性处理整个 ResultSet。在这里,你负责迭代 ResultSet,例如,将整个 ResultSet 映射到单个对象。让我们看看以下示例:

    public interface ResultSetExtractor<T> { 
      T extractData(ResultSet rs) throws SQLException,
      DataAccessException; 
    } 

使用 ResultSetExtractor 的示例

以下行代码在应用程序中实现了ResultSetExtractor接口:

    package com.packt.patterninspring.chapter7.bankapp.callbacks; 

    import java.sql.ResultSet; 
    import java.sql.SQLException; 
    import java.util.ArrayList; 
    import java.util.List; 

    import org.springframework.dao.DataAccessException; 
    import org.springframework.jdbc.core.ResultSetExtractor; 

    import com.packt.patterninspring.chapter7.bankapp.model.Account; 

    public class AccountExtractor implements
     ResultSetExtractor<List<Account>> { 
       @Override 
       public List<Account> extractData(ResultSet resultSet) throws
        SQLException, DataAccessException { 
          List<Account> extractedAccounts = null; 
          Account account = null; 
          while (resultSet.next()) { 
            if (extractedAccounts == null) { 
              extractedAccounts = new ArrayList<>(); 
              account = new Account(resultSet.getLong("ID"),
               resultSet.getString("NAME"), ...); 
            } 
            extractedAccounts.add(account); 
          } 
          return extractedAccounts; 
       } 
    } 

前面的类AccountExtractor实现了ResultSetExtractor接口,并用于创建数据库返回的结果集的全部数据的对象。让我们看看如何在您的应用程序中使用这个类:

    public List<Account> extractAccounts() { 
      String sql = "SELECT * FROM Account"; 
      return jdbcTemplate.query(sql, new AccountExtractor()); 
    } 

上一段代码负责访问银行的全部账户,并使用AccountExtractor类准备账户列表。这个类实现了 Spring Jdbc 模块的ResultSetExtractor回调接口。

ResultSet的多行映射到单个对象时,ResultSetExtractor是最好的选择。

Jdbc 和配置 JdbcTemplate 的最佳实践

一旦配置,JdbcTemplate类的实例就是线程安全的。作为在 Spring 应用程序中配置JdbcTemplate的最佳实践,它应该在 DAO 类的构造函数注入或 setter 注入中构建数据源 bean,通过将数据源 bean 作为JdbcTemplate类的构造函数参数传递。这会导致 DAO 看起来部分如下:

    @Repository 
    public class JdbcAccountRepository implements AccountRepository{ 
      JdbcTemplate jdbcTemplate; 

      public JdbcAccountRepository(DataSource dataSource) { 
        super(); 
        this.jdbcTemplate = new JdbcTemplate(dataSource); 
      } 
      //... 
    } 
    Let's see some best practices to configure a database and write
    the code for the DAO layer: 
  • 如果您想在应用程序开发时配置嵌入式数据库,作为最佳实践,嵌入式数据库将始终被分配一个唯一生成的名称。这是因为 Spring 容器通过配置一个类型为javax.sql.DataSource的 bean 来提供嵌入式数据库,并且该数据源 bean 被注入到数据访问对象中。

  • 总是使用对象池;这可以通过两种方式实现:

    • 连接池:它允许池管理器在关闭后保持连接在中。

    • 语句池化:它允许驱动程序重用已准备的语句对象。

      • 仔细选择提交模式

      • 考虑移除应用程序的自动提交模式,并使用手动提交来更好地控制提交逻辑,如下所示:

                  Connection.setAutoCommit(false); 

摘要

没有数据的应用程序就像没有燃料的汽车。数据是应用程序的核心。有些应用程序可能在没有数据的情况下存在于世界上,但这些应用程序只是展示应用程序,如静态博客。数据是应用程序的重要组成部分,您需要为您的应用程序开发数据访问代码。此代码应该非常简单、健壮且可定制。

在传统的 Java 应用程序中,您可以使用 JDBC 来访问数据。这是一个非常基本的方法,但有时,定义规范、处理 JDBC 异常、建立数据库连接、加载驱动程序等操作非常混乱。Spring 通过删除样板代码并简化 JDBC 异常处理来简化这些事情。您只需在应用程序中编写要执行的 SQL,其余的由 Spring 框架管理。

在本章中,您已经了解了 Spring 如何为数据访问和数据持久化提供后端支持。JDBC 很有用,但直接使用 JDBC API 是一项繁琐且容易出错的任务。JdbcTemplate简化了数据访问,并强制执行一致性。使用 Spring 进行数据访问遵循分层架构原则——高层不应了解数据管理。它通过数据访问异常隔离SQLException,并创建一个层次结构以使它们更容易处理。

在下一章中,我们将继续讨论使用 ORM 框架(如 Hibernate 和 JPA)进行数据访问和持久化。

第八章:使用 Spring ORM 和事务实现模式访问数据库

在 第七章,“使用 Spring 和 JDBC 模板模式访问数据库”,我们学习了如何使用 JBDC 访问数据库以及 Spring 如何通过使用模板模式和回调从开发人员端到框架端移除样板代码。在本章中,我们将学习使用 对象关系映射ORM)框架访问数据库并管理应用程序中事务的更高级步骤。

当我的儿子 Arnav 一岁半的时候,他经常玩一个模拟手机。但随着他长大,他的需求也超出了模拟手机,变成了智能手机。

类似地,当你的应用程序在业务层只有少量数据时,JDBC 工作得很好,但随着应用程序的增长和复杂化,将表映射到应用程序中的对象变得困难。JDBC 是数据访问世界中的小型模拟手机。但是,对于复杂的应用程序,我们需要能够将对象属性映射到数据库列的对象关系映射解决方案。我们还需要在数据访问层为我们的应用程序提供更复杂的平台,这些平台可以独立于数据库技术为我们创建查询和语句,并且我们可以声明式或程序化地定义它们。

许多 ORM 框架可用于为应用程序的数据访问层提供服务。此类服务的例子包括对象关系映射、数据的延迟加载、数据的预加载、级联等。这些 ORM 服务让您免于编写大量用于错误处理和应用程序资源管理的代码。ORM 框架减少了开发时间,并有助于编写无错误的代码,这样您就可以只关注业务需求。Spring 不实现自己的 ORM 解决方案,但它为 Hibernate、Java 持久化 APIJPA)、iBATIS 和 Java 数据对象JDO)等许多持久化框架提供支持。Spring 还提供了与 ORM 框架的集成点,以便我们可以轻松地将 ORM 框架集成到我们的 Spring 应用程序中。

Spring 在您的应用程序中为所有这些技术提供支持。在本章中,我们将探讨 Spring 对 ORM 解决方案的支持,并涵盖以下主题:

  • ORM 框架和使用的模式

    • 数据访问对象模式

    • 在 Spring 中使用工厂设计模式创建 DAO

    • 数据映射模式

    • 领域模型模式

    • 懒加载模式的代理

    • Hibernate 模板模式

  • 将 Hibernate 与 Spring 集成

    • 在 Spring 容器中配置 Hibernate 的 SessionFactory

    • 基于纯 Hibernate API 实现 DAO

    • Spring 中的事务管理策略

    • 声明式事务实现和划分

    • 程序化事务实现和划分

    • 应用程序中 Spring ORM 和事务模块的最佳实践

在我们继续讨论更多关于 ORM 框架的内容之前,让我们首先看看应用中数据访问层(DAL)所使用的一些设计模式。

ORM 框架和使用的模式

Spring 支持多个 ORM 框架,例如 Hibernate、Java 持久化 API(JPA)、iBATIS 和Java 数据对象(JDO)。通过在应用中使用任何 ORM 解决方案,您可以轻松地将关系数据库中的数据持久化和以 POJO 对象的形式访问。Spring ORM 模块是之前讨论的 Spring JDBC DAO 模块的扩展。Spring 提供了 ORM 模板,如基于 JDBC 的模板,以在集成层或数据访问层中工作。以下是 Spring 框架支持的 ORM 框架和集成:

  • Hibernate

  • Java 持久化 API

  • Java 数据对象

  • iBATIS

  • 数据访问对象实现

  • 事务策略

您可以使用 Spring 的依赖注入功能来配置应用中的 ORM 解决方案。Spring 还为您数据访问应用中的 ORM 层添加了重要的增强功能。以下是在创建 ORM DAO 时使用 Spring 框架的好处:

  • 更易于开发和测试:Spring 的 IoC 容器管理 ORM DAO 的 bean。您可以通过使用 Spring 的依赖注入功能轻松地交换 DAO 接口的实现。它还使得在隔离环境中测试持久化相关代码变得容易。

  • 常见数据访问异常:Spring 提供了一致的数据异常层次结构,以处理持久化层的异常。它封装了 ORM 工具的所有已检查异常,并将这些异常转换为与任何特定 ORM 解决方案无关的未检查的一般异常,这些异常是数据库特定的。

  • 通用资源管理:如DataSource、数据库连接、Hibernate 的SessionFactory、JPA 的EntityManagerFactory等资源由 Spring IoC 容器管理。Spring 还使用 JTA 管理本地或全局事务。

  • 集成事务管理:Spring 在您的应用中提供了声明式和程序式事务管理。对于声明式事务管理,您可以使用@Transactional注解。

Spring 与 ORM 解决方案的集成主要采用应用层之间的松耦合方式;也就是说,业务层和数据访问层。这是一种清晰的应用分层,且独立于任何特定的数据库和事务技术。应用中的业务服务不再依赖于数据访问和特定的交易策略。因为 Spring 管理了集成层中使用的资源,所以您不需要为特定的数据访问技术查找资源。Spring 为 ORM 解决方案提供了模板,以移除样板代码,并且为所有 ORM 解决方案提供了一致的方法。

在第七章“使用 Spring 和 JDBC 模板模式访问数据库”中,你看到了 Spring 如何解决应用程序集成层中的两个主要问题。第一个问题是管理应用程序资源时的冗余代码,第二个问题是在开发时处理检查异常。同样,Spring ORM 模块也提供了这两个问题的解决方案,我们将在以下章节中讨论。

资源和事务管理

在 Spring JDBC 模块中,资源管理,如连接处理、语句处理和异常处理,由 Spring 的 JdbcTemplate 管理。它还翻译数据库特定的 SQL 错误代码为有意义的非检查异常类。对于 Spring ORM 模块也是如此--Spring 通过使用相应的 Spring 事务管理器在企业应用程序中管理本地和全局事务。Spring 为所有支持的 ORM 技术提供事务管理器。例如,Spring 为 Hibernate 提供 Hibernate 事务管理器,为 JPA 提供 JPA 事务管理器,并为全局或分布式事务提供 JTA 支持。

一致的异常处理和转换

在 Spring JDBC 模块中,Spring 提供了DataAccessException来处理所有类型的数据库特定的 SQL 错误代码,并生成有意义的异常类。在 Spring ORM 模块中,正如我们所知,Spring 支持多个 ORM 解决方案的集成,例如在 DAO 中使用 Hibernate、JPA 或 JDO,这些持久化技术根据技术提供自己的原生异常类,如HibernateExceptionPersistenceExceptionJDOException。这些 ORM 框架的原生异常是不受检查的异常,因此我们不需要在应用程序中处理它们。除非应用程序是强 ORM 基础,或者不需要任何特殊的异常处理,否则 DAO 服务的调用者无法进行特定的处理。Spring 在整个 ORM 框架中提供了一致的方法;在 Spring 应用程序中,你不需要为任何 ORM 实现特定的代码。它通过使用@Repository注解来实现异常转换。如果 Spring 应用程序中的任何类被@Repository注解标注,那么这个类就有资格进行 Spring DataAccessException的转换。以下是一个针对AccountDaoImpl类的示例代码:

    @Repository 
    public class AccountDaoImpl implements AccountDao { 
      // class body here... 
    } 

    <beans> 
      <!-- Exception translation bean post processor --> 
      <bean class="org.springframework.dao.annotation.
      PersistenceExceptionTranslationPostProcessor"/> 
      <bean id="accountDao" class="com.packt.patterninspring.chapter8.
      bankapp.dao.AccountDaoImpl"/> 
    </beans> 

如前述代码所示,PersistenceExceptionTranslationPostProcessor类是一个 bean 后处理器,它自动搜索所有异常转换器,并在容器中建议所有标注了@Repository注解的已注册 bean。它将这些发现的异常转换器应用于这些标注的 bean,并且这些转换器可以拦截并适当地转换抛出的异常。

让我们看看 Spring ORM 模块中实现的一些更多设计模式,以提供企业应用集成层最佳的企业解决方案。

数据访问对象模式

数据访问对象DAO)模式是 J2EE 应用持久层中一个非常流行的设计模式。它将业务逻辑层和持久层分离。DAO 模式基于封装和抽象的面向对象原则。使用 DAO 模式的环境是根据底层供应商实现和存储类型(如面向对象数据库、平面文件、关系数据库等)来访问和持久化数据。使用 DAO 模式,你可以创建一个 DAO 接口,并实现这个 DAO 接口来抽象和封装对数据源的所有访问。这个 DAO 实现管理数据库的资源,如与数据源的连接。

DAO 接口对所有底层数据源机制都非常通用,并且不需要为任何低级持久技术的变化而改变。这个模式允许你在不影响企业应用中的业务逻辑的情况下采用任何不同的数据访问技术。让我们看看以下图表来了解 DAO 模式:

正如前图所示,以下参与者参与了这个模式:

  • BusinessObject:这个对象在业务层工作,是数据访问层的客户端。它需要数据来进行业务建模,并为应用中的辅助器或控制器准备 Java 对象。

  • DataAccessObject:这是 DAO 模式的主要对象。这个对象隐藏了所有底层数据库实现(针对BusinessObject)的低级实现。

  • DataSource:这也是一个对象,用于包含关于底层数据库实现的所有低级信息,如 RDBMS、平面文件或 XML。

  • TransferObject:这是一个对象,用作数据载体。这个对象被DataAccessObject用来将数据返回给业务对象。

让我们看看以下 DAO 模式的示例,其中AccountDaoDataAccessObject接口,而AccountDaoImplAccountDao接口的实现类:

    public interface AccountDao { 
      Integer totalAccountsByBranch(String branchName); 
    } 

    public class AccountDaoImpl extends JdbcDaoSupport implements
    AccountDao { 
      @Override 
      public Integer totalAccountsByBranch(String branchName) { 
        String sql = "SELECT count(*) FROM Account WHERE branchName = 
        "+branchName; 
        return this.getJdbcTemplate().queryForObject(sql,   
        Integer.class); 
       } 

    } 

在 Spring 中使用工厂设计模式创建 DAO

如我们所知,有很多设计模式在 Spring 框架中发挥作用。如第二章“GOF 设计模式概述”中讨论的,核心设计模式,工厂模式是一种创建型设计模式,它用于在不向客户端暴露底层逻辑的情况下创建对象,并使用公共接口或抽象类将新对象分配给调用者。你可以通过使用Factory方法和抽象工厂设计模式使 DAO 模式更加灵活。

让我们看看在我们的例子中,我们是在哪里实现这个策略,即一个工厂为单个数据库实现生成 DAO。请参考以下图表:

图片

你可以在前面的图表中看到,AccountDao对象是由AccountDaoFactory生成的,而AccountDaoFactoryAccountDao的工厂。我们可以随时更改底层数据库,这样我们就不需要更改业务代码--工厂负责这些事情,Spring 提供了在 bean 工厂和 DAO 工厂中维护所有 DAO 的支持。

数据映射模式

一层映射器,它在对象和数据库之间移动数据,同时保持它们彼此以及映射器本身的独立性

  • 马丁·福勒:《企业应用架构模式》

ORM 框架提供了对象和关系型数据库之间的映射,因为我们知道,对象和关系型数据库中的表在为应用程序存储数据时有不同的方式。此外,对象和表都有结构化数据的方法。在你的 Spring 应用程序中,如果你使用任何 ORM 解决方案,如 Hibernate、JPA 或 JDO,那么你就不需要担心对象和关系型数据库之间的映射机制。让我们通过以下图表来了解数据映射模式:

图片

如前图所示,对象Account通过AccountMapper映射到关系型数据库。它就像 Java 对象和应用程序中底层数据库之间的中介层。让我们看看数据访问层中使用的另一个模式。

领域模型模式

领域对象模型,它结合了行为和数据。

  • 由马丁·福勒:《企业应用架构模式》

领域模型是一个具有行为和数据的对象,因此,行为定义了企业应用的业务逻辑,而数据是关于业务输出的信息。领域模型结合了数据和过程。在企业应用中,数据模型位于业务层之下,以插入业务逻辑,并从业务行为返回数据。让我们通过以下图表来更清晰地了解这一点:

图片

如前图所示,我们根据业务需求在我们的应用程序中定义了两个领域模型。将资金从一个账户转移到另一个账户的业务行为已在 TransferService 类中定义。TransferServiceAccountService 类属于企业应用程序中的领域模型模式。

懒加载模式的代理

懒加载是一种设计模式,这种设计模式在企业应用程序中由一些 ORM 解决方案(如 Hibernate)使用,以延迟对象的初始化,直到它被另一个对象在需要时调用。这种设计模式的目的在于优化应用程序的内存。Hibernate 中的懒加载设计模式是通过使用虚拟代理对象实现的。在懒加载演示中,我们使用了一个代理,但这不是代理模式的一部分。

Spring 的 Hibernate 模板模式

Spring 提供了一个辅助类来访问 DAO 层的数据--这个类基于 GoF 模板方法设计模式。Spring 提供了 HibernateTemplate 类,用于提供数据库操作,如 savecreatedeleteupdateHibernateTemplate 类确保每个事务只使用一个 Hibernate 会话。

让我们在下一节中看看 Spring 对 Hibernate 的支持。

将 Hibernate 与 Spring 集成

Hibernate 是一个持久化 ORM 框架,它是开源的,不仅提供了 Java 对象和数据库表之间的简单对象关系映射,而且还为您的应用程序提供了许多高级功能,以改进性能,并有助于更好地利用资源,如缓存、懒加载、预加载和分布式缓存。

Spring 框架提供了对 Hibernate 框架的全面支持,Spring 还内置了一些库以充分利用 Hibernate 框架。我们可以使用 Spring 的 DI 模式和 IoC 容器来配置应用程序中的 Hibernate。

让我们在下一节中看看如何在 Spring IoC 容器中配置 Hibernate。

在 Spring 容器中配置 Hibernate 的 SessionFactory

作为在任何企业应用程序中配置 Hibernate 和其他持久化技术的最佳方法,业务对象应与硬编码的资源查找(如 JDBC DataSource 或 Hibernate SessionFactory)分离。您可以在 Spring 容器中将这些资源定义为 bean。但是,业务对象需要这些资源的引用,如 SessionFactory 和 JDBC DataSource,以便访问它们。让我们看看以下具有 SessionFactory 以访问应用程序数据的 DAO 类:

    public class AccountDaoImpl implements AccountDao { 
      private SessionFactory sessionFactory; 

      public void setSessionFactory(SessionFactory sessionFactory) { 
        this.sessionFactory = sessionFactory; 
      } 
       //..... 
    } 

如前所述的代码所示,DAO 类 AccountDaoImpl 遵循依赖注入模式。它通过注入 Hibernate 的 SessionFactory 对象来访问数据,并且很好地适应了 Spring IoC 容器。在这里,Hibernate 的 SessionFactory 是一个单例对象;它生成 Hibernate org.hibernate.Session 接口的主对象。SessionFactory 管理 Hibernate 的 Session 对象,并负责打开和关闭 Session 对象。Session 接口具有实际的数据访问功能,如 saveupdatedelete 和从数据库中加载 load 对象。在应用程序中,AccountDaoImp 或任何其他存储库使用这个 Hibernate Session 对象来执行其所有持久化需求。

Spring 提供了内置的 Hibernate 模块,你可以在应用程序中使用 Spring 的 Hibernate 会话工厂 bean。

org.springframework.orm.hibernate5.LocalSessionFactoryBean 这个 bean 是 Spring 中 FactoryBean 接口的实现。LocalSessionFactoryBean 基于抽象工厂模式,并在应用程序中生成 Hibernate 的 SessionFactory。你可以在应用程序的 Spring 上下文中将 Hibernate 的 SessionFactory 配置为一个 bean,如下所示:

    @Bean 
    public LocalSessionFactoryBean sessionFactory(DataSource 
    dataSource) { 
      LocalSessionFactoryBean sfb = new LocalSessionFactoryBean(); 
      sfb.setDataSource(dataSource); 
      sfb.setPackagesToScan(new String[] {   
        "com.packt.patterninspring.chapter8.bankapp.model" }); 
        Properties props = new Properties(); 
        props.setProperty("dialect", 
        "org.hibernate.dialect.H2Dialect"); 
        sfb.setHibernateProperties(props); 
        return sfb; 
    } 

在前面的代码中,我们通过使用 Spring 的 LocalSessionFactoryBean 类将 SessionFactory 配置为一个 bean。这个 bean 方法接受 DataSource 作为参数;DataSource 指定了如何以及在哪里找到数据库连接。我们还为 LocalSessionFactoryBean 指定了一个名为 "com.packt.patterninspring.chapter8.bankapp.model" 的包,以进行扫描,并将 SessionFactory 的属性设置为 hibernateProperties 以确定我们将处理的应用程序中的数据库类型。

在配置 Spring 应用程序上下文中的 Hibernate SessionFactory bean 之后,让我们看看如何实现应用程序持久化层的 DAO。

基于纯 Hibernate API 实现 DAO

让我们创建以下 DAO 实现类:

    package com.packt.patterninspring.chapter8.bankapp.dao; 

    import org.hibernate.SessionFactory; 
    import org.springframework.stereotype.Repository; 
    import org.springframework.beans.factory.annotation.Autowired; 
    @Repository 
    public class AccountDaoImpl implements AccountDao { 
      @Autowired 
      private SessionFactory sessionFactory; 

      public void setSessionFactory(SessionFactory sessionFactory) { 
        this.sessionFactory = sessionFactory; 
      } 

      @Override 
      public Integer totalAccountsByBranch(String branchName) { 
        String sql = "SELECT count(*) FROM Account WHERE branchName =
        "+branchName; 
        return this.sessionFactory.getCurrentSession().createQuery(sql,
        Integer.class).getSingleResult(); 
      } 
      @Override 
      public Account findOne(long accountId) { 
        return (Account)   
        this.sessionFactory.currentSession().
        get(Account.class, accountId); 
      } 
      @Override 
      public Account findByName(String name) { 
        return (Account) this.sessionFactory.currentSession().
        createCriteria(Account.class) 
        .add(Restrictions.eq("name", name)) 
        .list().get(0); 
      } 
      @Override 
      public List<Account> findAllAccountInBranch(String branchName) { 
       return (List<Account>) this.sessionFactory.currentSession() 

       .createCriteria(Account.class).add(Restrictions.eq("branchName",  
       branchName)).list(); 
      }  
    } 

如前所述的代码所示,AccountDaoImpl 是一个 DAO 实现类,它通过使用 @Autowired 注解注入 Hibernate 的 SessionFactory bean。前面描述的 DAO 实现将抛出未检查的 Hibernate PersistenceExceptions--不希望这些异常传播到服务层或其他 DAO 的使用者。但是,Spring AOP 模块允许将其转换为 Spring 的丰富、供应商中立的 DataAccessException 层次结构--它隐藏了使用的访问技术。Spring 通过在 DAO 实现类上使用 @Repository 注解提供此功能,你只需要定义一个 Spring 提供的 BeanPostProcessor,即 PersistenceExceptionTranslationPostProcessor

让我们在我们的 Hibernate DAO 实现类中添加一个异常转换;我们可以通过只需将 PersistenceExceptionTranslationPostProcessor bean 添加到 Spring 应用程序上下文中来完成此操作,如下所示:

    @Bean 
    public BeanPostProcessor persistenceTranslation() { 
      return new PersistenceExceptionTranslationPostProcessor(); 
    } 

前面注册的 bean PersistenceExceptionTranslationPostProcessor 负责为带有 @Repository 注解的 bean 添加一个顾问,并且它将任何在代码中捕获的平台特定异常重新抛出为 Spring 特定的未检查数据访问异常。

让我们看看在下一节中,Spring 是如何管理 Spring 应用程序的业务层和持久层的事务的。

Spring 的事务管理策略

Spring 为 Spring 应用程序的事务管理提供了全面的支持。这是 Spring 框架最吸引人的特性之一。大多数情况下,这个特性迫使软件行业使用 Spring 框架来开发企业应用程序。Spring 框架提供了一种一致的方式来管理应用程序中的事务,无论使用哪种持久化技术,如 Java 事务 API、JDBC、Hibernate、Java 持久化 API 和 Java 数据对象。Spring 支持声明式事务管理和程序化事务管理。

Java 事务有两种类型,具体如下:

  • 本地事务 - 单个资源:由底层资源管理的本地事务;这些是资源特定的。让我们借助以下图表来解释这一点:

图片

如您在前面图表中看到的,应用程序和数据库平台之间存在一个事务,以确保每个任务单元都遵循数据库的 ACID 属性。

  • 全局(分布式)事务 - 多个:由单独的、专门的交易管理器管理的全局事务,使您能够与多个事务性资源一起工作。请查看以下图表,以了解有关全局或分布式事务的更多信息:

图片

如您在最后一张图表中看到的,事务管理器在应用程序中与多种数据库技术一起工作。全局事务独立于特定平台的数据持久化技术。

Spring 为 Java 应用程序中的两种事务类型提供相同的 API。Spring 框架通过声明式配置事务或程序化配置事务,在任何环境中都提供一致的编程模型。

让我们继续到下一节,看看如何在 Spring 应用程序中配置事务。

声明式事务划分和实现

Spring 支持声明式事务管理。Spring 将事务边界与事务实现分离。边界通过 Spring AOP 声明性表达。我们始终建议在您的 Spring 应用程序中使用 Spring 的声明性事务边界和实现,因为声明性编程模型允许您从代码中替换外部事务边界 API,并且您可以通过使用 Spring AOP 事务拦截器来配置它。事务基本上是横切关注点;这种声明性事务模型允许您将应用程序的业务逻辑与重复的事务边界代码分开。

如前所述,Spring 为 Spring 应用程序中的事务处理提供了一个一致的模型,并提供了一个PlatformTransactionManager接口来隐藏实现细节。Spring 框架中提供了这个接口的几个实现,其中一些将在下面列出:

  • DataSourceTransactionManager

  • HibernateTransactionManager

  • JpaTransactionManager

  • JtaTransactionManager

  • WebLogicJtaTransactionManager

  • WebSphereUowTransactionManager

以下是一个关键接口:

    public interface PlatformTransactionManager { 
      TransactionStatus getTransaction( 
        TransactionDefinition definition) throws TransactionException; 
       void commit(TransactionStatus status) throws   
         TransactionException; 
       void rollback(TransactionStatus status) throws
         TransactionException; 
   } 

在前面的代码中,getTransaction()方法返回一个TransactionStatus对象。此对象包含事务的状态;要么它是新的,要么它返回当前调用堆栈中的现有状态。这取决于TransactionDefinition参数。正如在 JDBC 或 ORM 模块中一样,Spring 也提供了一种一致的方式来处理任何事务管理器抛出的异常。getTransaction()方法抛出一个TransactionException异常,这是一个未检查的异常。

Spring 在应用程序中使用相同的 API 处理全局和本地事务。从本地事务切换到应用程序中的全局事务只需要非常小的更改——那就是更改事务管理器。

部署事务管理器

在您的 Spring 应用程序中部署事务有两个步骤。第一步是您必须实现或配置一个预实现的 Spring 事务管理器类与您的应用程序。第二步是声明事务边界,即您想在何处放置 Spring 事务。

第一步 - 实现事务管理器

就像任何其他 Spring bean 一样,为所需的实现创建 bean。您可以根据需要配置事务管理器,以支持任何持久化技术,如 JDBC、JMS、JTA、Hibernate、JPA 等。但在下面的示例中,这里是一个使用 JDBC 的DataSource管理器的管理器:

在 Java 配置中,让我们看看如何在应用程序中定义transactionManager bean:

    @Bean 
    public PlatformTransactionManager transactionManager(DataSource 
    dataSource) { 
      return new DataSourceTransactionManager(dataSource); 
    } 

在 XML 配置中,bean 可以创建如下:

    <bean id="transactionManager" 
     class="org.springframework.jdbc.datasource.
     DataSourceTransactionManager"> 
     <property name="dataSource" ref="dataSource"/> 
    </bean> 

在前面的代码中,我们使用了dataSource豆;必须在其他地方定义一个dataSource豆。豆 ID,"transactionManager",是默认名称。我们可以更改它,但之后必须在每个地方指定替代名称,这并不容易做到!

步骤 2 - 声明事务划分

作为最佳方法,应用的服务层是划分事务的最佳位置。让我们在以下代码中看看:

    @Service 
    public class TransferServiceImpl implements TransferService{ 
      //... 
      @Transactional 
      public void transfer(Long amount, Long a, Long b){ 
        // atomic unit-of-work 
      } 
      //... 
    } 

正如你可以在前面的代码中看到,TransferServiceImpl是我们应用服务层的服务类。这个服务是划分工作单元事务的最佳位置。Spring 提供了@Transactional注解来划分事务;这个注解可以在应用中服务类的类级别或方法级别使用。让我们看看类级别的@Transactional

    @Service 
    @Transactional 
    public class TransferServiceImpl implements TransferService{ 
      //... 
      public void transfer(Long amount, Account a, Account b){ 
        // atomic unit-of-work 
      } 
      public Long withdraw(Long amount,  Account a){ 
        // atomic unit-of-work 
      } 
      //... 
    } 

如果你将@Transactional注解声明在类级别,这个服务中的所有业务方法都将变为事务方法。

注意--如果你正在使用@Transactional注解,方法可见性应该是公开的。如果你使用这个注解在非公开方法上,例如protectedprivatepackage-visible,不会抛出错误或异常,但这个被注解的方法不会显示事务行为。

但仅使用这个注解在 Spring 应用中是不够的。我们必须通过在 Spring 的 Java 配置文件中使用@EnableTransactionManagement注解来启用 Spring 框架的事务管理功能,或者我们可以在 XML 配置文件中使用<tx:annotation-driven/>命名空间。让我们看看以下代码,例如:

    @Configuration 
    @EnableTransactionManagement 
    public class InfrastructureConfig { 

      //other infrastracture beans definitions 

     @Bean 
     public PlatformTransactionManager transactionManager(){ 
         return new DataSourceTransactionManager(dataSource()); 
     } 
   } 

正如你可以在前面的代码中看到,InfrastructureConfig是 Spring 应用的 Java 配置文件--在这里,我们定义了基础设施相关的豆,以及一个transactionManager豆也在这里定义。这个带有另一个注解的配置类是@EnableTransactionManagement--这个注解在应用中定义了一个 Bean 后处理器,并代理@Transactional豆。现在,让我们看看以下图:

图片

正如你在前面的图中看到的,TransferServiceImpl类被 Spring 代理包装。

但要了解应用中@Transactional豆的确切行为,让我们看看以下步骤:

  1. 目标对象被包装在一个代理中;它使用我们已在第六章中讨论的环绕通知。Spring 面向切面编程与代理和装饰器模式。

  2. 代理实现了以下行为:

  3. 在进入业务方法之前开始事务。

  4. 在业务方法结束时提交。

  5. 如果业务方法抛出RuntimeException,则回滚--这是 Spring 事务的默认行为,但你也可以为检查和自定义异常覆盖它。

  6. 事务上下文现在绑定到应用程序中的当前线程。

  7. 所有步骤都由配置控制,无论是 XML、Java 还是注解。

现在看看以下带有相关事务管理器的本地 JDBC 配置图:

图片

在前面的图中,我们使用 JDBC 和一个数据源事务管理器定义了一个本地数据源。

在下一节中,我们将讨论如何在应用程序中程序化地实现和划分事务。

程序化事务划分和实现

Spring 允许您通过使用TransactionTemplatePlatformTransactionManager实现直接在应用程序中实现和划分事务。但是,声明式事务管理被高度推荐,因为它提供了干净的代码和非常灵活的配置。

让我们看看如何在应用程序中程序化地实现事务:

    package com.packt.patterninspring.chapter8.bankapp.service; 

    import org.springframework.beans.factory.annotation.Autowired; 
    import org.springframework.stereotype.Service; 
    import org.springframework.transaction.PlatformTransactionManager; 
    import org.springframework.transaction.TransactionStatus; 
    import org.springframework.transaction.support.TransactionCallback; 
    import org.springframework.transaction.support.TransactionTemplate; 

    import com.packt.patterninspring.chapter8.bankapp.model.Account; 
    import com.packt.patterninspring.chapter8.bankapp.
      repository.AccountRepository; 

    @Service 
    public class AccountServiceImpl implements AccountService { 
      //single TransactionTemplate shared amongst all methods in this 
      instance 
      private final TransactionTemplate transactionTemplate; 
      @Autowired 
      AccountRepository accountRepository; 

      // use constructor-injection to supply the 
      PlatformTransactionManager 
      public AccountServiceImpl(PlatformTransactionManager 
      transactionManager) { 
        this.transactionTemplate = new 
        TransactionTemplate(transactionManager); 
      } 

      @Override 
      public Double cheeckAccountBalance(Account account) { 
        return transactionTemplate.execute(new 
        TransactionCallback<Double>() { 
          // the code in this method executes in a transactional 
          context 
          public Double doInTransaction(TransactionStatus status) { 
            return accountRepository.checkAccountBalance(account); 
          } 
        });  } 
    } 

在前面的应用程序代码中,我们明确使用了TransactionTemplate来在事务上下文中执行应用程序逻辑。TransactionTemplate也是基于模板方法设计模式的,并且与 Spring 框架中的其他模板(如 JdbcTemplate)有相同的方法。类似于 JdbcTemplate,TransactionTemplate也使用回调方法,并且它使应用程序代码免于管理事务资源的样板代码。我们在服务类构造函数中构建了TransactionTemplate类的对象,并将PlatformTransactionManager对象作为参数传递给TransactionTemplate类的构造函数。我们还编写了一个包含应用程序业务逻辑代码的TransactionCallback实现,这显示了应用程序逻辑和事务代码之间的紧密耦合。

在本章中,我们已经看到 Spring 如何高效地管理企业应用程序中的事务。现在,让我们研究一些最佳实践,我们在任何企业应用程序工作中都必须牢记。

应用程序中 Spring ORM 和事务模块的最佳实践

以下是我们设计和开发应用程序时必须遵循的实践:

避免在 DAO 实现中使用 Spring 的HibernateTemplate辅助类,并在您的应用程序中使用SessionFactoryEntityManager。由于 Hibernate 的上下文会话能力,直接在 DAO 中使用SessionFactory。此外,使用getCurrentSession()方法来访问事务当前会话,以便在应用程序中执行持久化操作。请参考以下代码:

    @Repository 
    public class HibernateAccountRepository implements 
    AccountRepository { 
      SessionFactory sessionFactory; 
      public HibernateAccountRepository(SessionFactory 
      sessionFactory) { 
        super(); 
        this.sessionFactory = sessionFactory; 
      } 
     //... 
   } 

在您的应用程序中,始终使用@Repository注解来表示数据访问对象或存储库;它提供异常转换。请参考以下代码:

    @Repository 
    public class HibernateAccountRepository{//...} 

即使服务中的业务方法只委托其责任到相应的 DAO 方法,服务层也必须保持独立。

总是在应用程序的服务层实现事务,而不是在 DAO 层--这是事务的最佳位置。请参考以下代码:

    @Service 
    @Transactional 
    public class AccountServiceImpl implements AccountService {//...} 

声明式事务管理在应用程序中更强大、更方便配置,并且是 Spring 应用程序中高度推荐使用的方法。它将横切关注点与业务逻辑分离。

总是在服务层抛出运行时异常,而不是检查型异常。

注意@Transactional注解的 readOnly 标志。当服务方法只包含查询时,将事务标记为readOnly=true

摘要

在第七章《使用 Spring 和 JDBC 模板模式访问数据库》中,我们了解到 Spring 提供了基于 GOF 模板方法设计模式的JdbcTemplate类。这个类处理了传统 JDBC API 下所有必要的样板代码。但是当我们使用 Spring JDBC 模块时,将表映射到对象变得非常繁琐。在本章中,我们看到了将对象映射到关系数据库中的解决方案--通过在复杂应用程序中使用 ORM,我们可以利用关系数据库做更多的事情。Spring 支持与多个 ORM 解决方案集成,如 Hibernate、JPA 等。这些 ORM 框架使得数据持久化的声明式编程模型成为可能,而不是使用 JDBC 编程模型。

我们还探讨了在数据访问层或集成层中实现的一些设计模式。这些模式作为 Spring 框架的一个特性实现,如代理模式用于懒加载,外观模式用于与业务层集成,DAO 模式用于数据访问,等等。

在下一章中,我们将看到如何通过使用 Spring 对缓存模式的支持来提高我们应用程序在生产环境中的性能。

第九章:使用缓存模式提高应用程序性能

在前面的章节中,我们看到了 Spring 如何在后端工作以访问应用程序的数据。我们还看到了 Spring JDBC 模块如何提供JdbcTemplate辅助类以进行数据库访问。Spring 提供了与 ORM 解决方案(如 Hibernate、JPA、JDO 等)集成的支持,并管理应用程序的事务。现在,在本章中,我们将看到 Spring 如何提供缓存支持以提高应用程序性能。

当你深夜从办公室回家时,你是否经常面对你妻子连珠炮般的问题?是的,我知道当你疲惫不堪时回答这么多问题是件很烦人的事。当你被反复问相同的问题时,那就更加烦人了。

有些问题可以用来回答,但对于某些问题,你必须详细解释。考虑一下,如果你在一段时间后再次被问到另一个长问题会发生什么!同样,在应用程序中也有一些无状态组件,这些组件被设计成反复提出相同的问题以完成每个任务。类似于你妻子提出的一些问题,系统中的一些问题需要一段时间才能获取适当的数据——它可能背后有一些复杂的逻辑,或者可能需要从数据库中获取数据,或者调用远程服务。

如果我们知道某个问题的答案不太可能频繁变化,我们可以在稍后当相同系统再次询问该问题时记住这个答案。再次通过相同的渠道获取答案是没有意义的,因为它会影响应用程序的性能,并且是对你资源的浪费。在企业应用程序中,缓存是一种存储那些频繁需要答案的方法,这样我们就可以从缓存中获取,而不是每次都通过适当的渠道获取相同问题的答案。在本章中,我们将讨论 Spring 的缓存抽象功能,以及 Spring 如何声明性地支持缓存实现。它将涵盖以下要点:

  • 什么是缓存?

  • 我们在哪里做这个缓存?

  • 理解缓存抽象

  • 通过代理模式启用缓存

  • 基于声明性注解的缓存

  • 基于声明性 XML 的缓存

  • 配置缓存存储

  • 实现自定义缓存注解

  • 缓存最佳实践

让我们开始吧。

什么是缓存?

简单来说,缓存是我们存储预处理的程序信息的内存块。在这个上下文中,一个键值存储,例如一个映射,可能是应用程序中的缓存。在 Spring 中,缓存是一个接口,用于抽象和表示缓存。缓存接口提供了一些方法来将对象放入缓存存储,它可以基于给定的键从缓存存储中检索对象,它可以更新缓存存储中给定键的对象,也可以从缓存存储中删除给定键的对象。这个缓存接口提供了许多操作缓存的功能。

我们在哪里使用缓存?

我们在方法总是为相同的参数返回相同结果的情况下使用缓存。这种方法可以执行任何操作,例如即时计算数据、执行数据库查询、通过 RMI、JMS 和 Web 服务请求数据等。必须从参数生成一个唯一键。这就是缓存键。

理解缓存抽象

在 Java 应用程序中,基本上,缓存是应用于 Java 方法以减少缓存中相同信息的执行次数。这意味着,每当这些 Java 方法被调用时,缓存抽象会根据给定的参数将这些方法的行为应用于缓存。如果给定参数的信息已经在缓存中,则无需执行目标方法即可返回。如果所需信息不在缓存中,则执行目标方法,并将结果缓存并返回给调用者。缓存抽象还提供了其他与缓存相关的操作,如更新和/或删除缓存中的内容。当应用程序中的数据有时发生变化时,这些操作非常有用。

Spring 框架通过使用org.springframework.cache.Cacheorg.springframework.cache.CacheManager接口为 Spring 应用程序提供缓存抽象。缓存需要使用实际的存储来存储缓存数据。但缓存抽象只提供缓存逻辑。它不提供任何物理存储来存储缓存数据。因此,开发人员需要在应用程序中实现实际的缓存存储。如果您有一个分布式应用程序,那么您需要相应地配置您的缓存提供程序。这取决于您应用程序的使用案例。您可以为分布式应用程序在节点之间复制相同的数据,或者您可以创建一个集中式缓存。

市场上有一些缓存提供程序,您可以根据应用程序需求使用它们。以下是一些例子:

  • Redis

  • OrmLiteCacheClient

  • Memcached

  • 内存缓存

  • Aws DynamoDB Cache Client

  • Azure Cache Client

在您的应用程序中实现缓存抽象,您必须注意以下任务:

  • 缓存声明:这意味着您必须识别应用程序中需要缓存的方法,并使用缓存注解标注这些方法,或者您可以使用 Spring AOP 通过 XML 配置来实现。

  • 缓存配置:这意味着您必须配置缓存数据的实际存储位置——即数据存储和读取的地方

现在我们来看看如何在 Spring 应用程序中启用 Spring 的缓存抽象。

通过代理模式启用缓存

您可以通过以下两种方式启用 Spring 的缓存抽象:

  • 使用注解

  • 使用 XML 命名空间

Spring 通过使用 AOP(面向切面编程)透明地将缓存应用于 Spring bean 的方法。Spring 在您声明需要缓存的方法的 Spring bean 周围应用代理。此代理为 Spring bean 添加了动态的缓存行为。以下图示说明了缓存行为:

图片

在前述图中,您可以看到 Spring 将代理(Proxy)应用于AccountServiceImpl类以添加缓存行为。Spring 使用 GoF 代理模式在应用程序中实现缓存。

让我们看看如何在 Spring 应用程序中启用此功能。

使用注解启用缓存代理

如您所知,Spring 提供了许多功能,但它们大多数都是默认禁用的。在使用之前,您必须启用这些功能。如果您想在应用程序中使用 Spring 的缓存抽象,您必须启用此功能。如果您使用 Java 配置,您可以通过将@EnableCaching注解添加到您的配置类之一来启用 Spring 的缓存抽象。以下配置类显示了@EnableCaching注解:

    package com.packt.patterninspring.chapter9.bankapp.config; 

    import org.springframework.cache.CacheManager; 
    import org.springframework.cache.annotation.EnableCaching; 
    import org.springframework.cache.concurrent.
      ConcurrentMapCacheManager; 
    import org.springframework.context.annotation.Bean; 
    import org.springframework.context.annotation.ComponentScan; 
    import org.springframework.context.annotation.Configuration; 

    @Configuration 
    @ComponentScan(basePackages=   
    {"com.packt.patterninspring.chapter9.bankapp"}) 
    @EnableCaching //Enable caching 
    public class AppConfig { 

     @Bean 
     public AccountService accountService() { ... } 

     //Declare a cache manager 
     @Bean 
     public CacheManager cacheManager() { 
         CacheManager cacheManager = new ConcurrentMapCacheManager(); 
         return cacheManager; 
    } 
   } 

在前述 Java 配置文件中,我们向配置类AppConfig.java添加了@EnableCaching注解;此注解指示 Spring 框架为应用程序启用 Spring 缓存行为。

现在我们来看看如何通过使用 XML 配置来启用 Spring 的缓存抽象。

使用 XML 命名空间启用缓存代理

如果您使用 XML 配置应用程序,您可以使用 Spring 的缓存命名空间中的<cache:annotation-driven>元素启用注解驱动的缓存,如下所示:

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

     xsi:schemaLocation="http://www.springframework.org/schema/jdbc 
     http://www.springframework.org/schema/jdbc/spring-jdbc-4.3.xsd 
     http://www.springframework.org/schema/cache   
     http://www.springframework.org/schema/cache/spring-cache-4.3.xsd 
     http://www.springframework.org/schema/beans     
     http://www.springframework.org/schema/beans/spring-beans.xsd 
     http://www.springframework.org/schema/context    
     http://www.springframework.org/schema/context/spring-context.xsd 
     http://www.springframework.org/schema/aop 
     http://www.springframework.org/schema/aop/spring-aop-4.3.xsd 
     http://www.springframework.org/schema/tx 
     http://www.springframework.org/schema/tx/spring-tx-4.3.xsd"> 

     <!-- Enable caching --> 
     <cache:annotation-driven /> 

     <context:component-scan base-   
     package="com.packt.patterninspring.chapter9.bankapp"/> 

     <!-- Declare a cache manager --> 
     <bean id="cacheManager" 
     class="org.springframework.cache.concurrent.
     ConcurrentMapCacheManager" /> 
   </beans> 

如前述配置文件所示,无论您使用 Java 配置还是 XML 配置,注解@EnableCaching和命名空间<cache:annotation-driven>通过创建一个具有触发 Spring 缓存注解的切入点(pointcuts)的方面(aspect),从而启用 Spring 的缓存抽象。

让我们看看如何使用 Spring 的缓存注解来定义缓存边界。

基于声明性注解的缓存

在 Spring 应用程序中,Spring 的抽象提供了以下注解用于缓存声明:

  • @Cacheable:这表示在执行实际方法之前,查看该方法的返回值是否在缓存中。如果值可用,则返回此缓存值,如果值不可用,则调用实际方法,并将返回值放入缓存。

  • @CachePut:这更新缓存而不检查值是否可用。它总是调用实际方法。

  • @CacheEvict:这负责触发缓存驱逐。

  • @Caching:这用于将多个注解分组应用于方法。

  • @CacheConfig:这表示 Spring 在类级别上共享一些常见的缓存相关设置。

让我们现在更详细地查看每个注解。

@Cacheable注解

@Cacheable标记一个方法进行缓存。其结果存储在缓存中。对于所有后续具有相同参数的方法调用,它将使用键从缓存中获取数据。方法将不会执行。以下是一些@Cacheable属性:

  • :这是要使用的缓存名称

  • :这是每个缓存数据项的键

  • 条件:这是一个用于评估真或假的 SpEL 表达式;如果为假,则缓存的结果不会应用于方法调用

  • 除非:这同样是一个 SpEL 表达式;如果为真,则防止返回值被放入缓存

您可以使用 SpEL 和方法参数。让我们看看以下代码,这是@Cacheable注解最简单的声明。它需要与该方法关联的缓存名称。请参考以下代码:

    @Cacheable("accountCache ") 
    public Account findAccount(Long accountId) {...} 

在前面的代码中,findAccount方法被标记为@Cacheable注解。这意味着此方法与一个缓存相关联。缓存名称为accountCache。每当调用特定accountId的方法时,都会检查该方法的返回值是否在缓存中。您也可以像下面这样为缓存提供多个名称:

    @Cacheable({"accountCache ", "saving-accounts"}) 
    public Account findAccount(Long accountId) {...} 

@CachePut注解

如前所述,@Cacheable@CachePut注解都有相同的目标,即填充缓存。但它们的工作方式略有不同。@CachePut标记一个方法进行缓存,并将结果存储在缓存中。对于具有相同参数的每个方法调用,它总是调用实际方法,而不检查该方法的返回值是否在缓存中。以下是一些@CachePut属性:

  • :这是要使用的缓存名称

  • :这是每个缓存数据项的键

  • 条件:这是一个用于评估真或假的 SpEL 表达式;如果为假,则缓存的结果不会应用于方法调用

  • 除非:这同样是一个 SpEL 表达式;如果为真,则防止返回值被放入缓存

您还可以使用 SpEL 和方法参数为@CachePut注解。以下代码是@CachePut注解的最简单声明:

    @CachePut("accountCache ") 
    public Account save(Account account) {...} 

在前面的代码中,当调用save()时,它会保存Account。然后返回的 Account 被放置在accountCache缓存中。

如前所述,缓存是通过方法根据方法的参数来填充的。实际上这是一个默认的缓存键。在@Cachable注解的情况下,findAccount(Long accountId)方法有一个accountId参数,accountId被用作此方法的缓存键。但在@CachePut注解的情况下,save()的唯一参数是一个 Account。它被用作缓存键。使用Account作为缓存键似乎并不合适。在这种情况下,您需要缓存键是新建 Account 的 ID 而不是 Account 本身。因此,您需要自定义键生成行为。让我们看看您如何自定义缓存键。

自定义缓存键

您可以通过使用@Cacheable@CachePut注解的 key 属性来自定义缓存键。缓存键是通过使用对象属性作为以下代码片段中突出显示的键属性来通过 SpEL 表达式派生的。让我们看看以下示例:

    @Cacheable(cacheNames=" accountCache ", key="#accountId") 
    public Account findAccount(Long accountId) 

    @Cacheable(cacheNames=" accountCache ", key="#account.accountId") 
    public Account findAccount(Account account) 

    @CachePut(value=" accountCache ", key="#account.accountId") 
    Account save(Account account); 

您可以在前面的代码片段中看到,我们是如何使用@Cacheable注解的 key 属性来创建缓存键的。

让我们看看这些注解在 Spring 应用中的另一个属性。

条件缓存

Spring 的缓存注解允许您通过使用@Cacheable@CachePut注解的条件属性来关闭某些情况下的缓存。这些注解提供了一个 SpEL 表达式来评估条件值。如果条件表达式的值为真,则方法将被缓存。如果条件表达式的值为假,则方法不会被缓存,每次都会执行,而不进行任何缓存操作,无论缓存中的值或使用的参数是什么。让我们看一个例子。以下方法只有在传入的参数值大于或等于2000时才会被缓存:

    @Cacheable(cacheNames="accountCache", condition="#accountId >=   
    2000") 
    public Account findAccount(Long accountId); 

@Cacheable@CachePut注解还有一个属性--unless。这同样提供了一个 SpEL 表达式。这个属性可能看起来与条件属性相同,但它们之间有一些区别。与条件不同,unless表达式是在方法调用之后评估的。它阻止值被放入缓存。让我们看看以下示例--我们只想在银行名称不包含 HDFC 时进行缓存:

    @Cacheable(cacheNames="accountCache", condition="#accountId >= 
    2000", unless="#result.bankName.contains('HDFC')") 
    public Account findAccount(Long accountId); 

如您在前面的代码片段中所见,我们使用了两个属性--conditionunless。但unless属性有一个 SpEL 表达式,为#result.bankName.contains('HDFC')。在这个表达式中,结果是 SpEL 扩展或缓存 SpEL 元数据。以下是在 SpEL 中可用的缓存元数据列表:

表达式 描述
#root.methodName 缓存方法的名称
#root.method 被缓存的的方法,即被调用的方法
#root.target 它评估被调用的目标对象
#root.targetClass 它评估被调用的目标对象的类
#root.caches 当前方法执行的缓存数组
#root.args 传递给缓存方法的参数数组
#result 缓存方法的返回值;仅在@CachePutunless表达式中可用

Spring 的@CachePut@Cacheable注解不应在同一个方法上使用,因为它们有不同的行为。@CachePut注解强制执行缓存方法以更新缓存。但@Cacheable注解仅在方法的返回值不在缓存中时才执行缓存方法。

您已经看到了如何在 Spring 应用程序中使用 Spring 的@CachePut@Cacheable注解向缓存中添加信息。但如何从缓存中移除这些信息呢?Spring 的缓存抽象提供了另一个用于从缓存中移除已缓存数据的注解——@CacheEvict注解。让我们看看如何使用@CacheEvict注解从缓存中移除缓存数据。

@CacheEvict注解

Spring 的缓存抽象不仅允许填充缓存,还允许从缓存中移除已缓存的的数据。在应用程序中存在一个阶段,您必须从缓存中移除过时或未使用的数据。在这种情况下,您可以使用@CacheEvict注解,因为它与@Cacheable注解不同,不会向缓存中添加任何内容。@CacheEvict注解仅用于执行缓存清除。让我们看看这个注解是如何使AccountRepositoryremove()方法成为缓存清除的:

    @CacheEvict("accountCache ") 
    void remove(Long accountId); 

如您在前面的代码片段中所见,当调用remove()方法时,与参数accountId关联的值将从accountCache缓存中移除。以下是一些@Cacheable属性:

  • value: 这是一个要使用的缓存名称数组

  • key: 这是一个 SpEL 表达式,用于评估要使用的缓存键

  • condition: 这是一个 SpEL 表达式,用于评估真或假;如果为假,则缓存的结果不会被应用于方法调用

  • allEntries: 如果此属性的值为真,则从缓存中删除所有条目

  • beforeInvocation: 这意味着如果此属性的值为真,则在方法调用之前从缓存中删除条目;如果此属性的值为假(默认值),则在方法调用成功后删除条目

我们可以在任何方法上使用@CacheEvict注解,甚至是void方法,因为它只从缓存中删除值。但是,对于@Cacheable@CachePut注解,我们必须使用非void返回值的方法,因为这些注解需要缓存结果。

@Caching注解

Spring 的缓存抽象允许你通过在 Spring 应用程序中使用@Caching注解来使用同一类型的多个注解来缓存一个方法。@Caching注解将@Cacheable@CachePut@CacheEvict等注解组合为同一方法。例如:

    @Caching(evict = {  
      @CacheEvict("accountCache "),  
      @CacheEvict(value="account-list", key="#account.accountId") }) 
      public List<Account> findAllAccount(){ 
      return (List<Account>) accountRepository.findAll(); 
   } 

@CacheConfig注解

Spring 的缓存抽象允许你在类级别上使用@CacheConfig注解,以避免在每个方法中重复提及。在某些情况下,将缓存的定制应用于所有方法可能相当繁琐。在这里,你可以使用@CacheConfig注解来处理类的所有操作。例如:

     @CacheConfig("accountCache ") 
     public class AccountServiceImpl implements AccountService { 

      @Cacheable 
      public Account findAccount(Long accountId) { 
        return (Account) accountRepository.findOne(accountId); 
      } 
    } 

你可以在前面的代码片段中看到,@CacheConfig注解在类级别上使用,并允许你将与所有cacheable方法共享的accountCache缓存。

由于 Spring 的缓存抽象模块使用代理,你应该只将缓存注解用于具有公共可见性的方法。在所有非公共方法中,这些注解不会引发任何错误,但带有这些注解的非公共方法不会显示任何缓存行为。

我们已经看到 Spring 还提供了 XML 命名空间来配置和实现 Spring 应用程序中的缓存。让我们在下一节中看看如何实现。

基于声明性 XML 的缓存

为了将缓存配置代码与业务代码分离,并保持 Spring 特定注解与源代码之间的松耦合,基于 XML 的缓存配置比基于注解的配置更为优雅。因此,要使用 XML 配置 Spring 缓存,请使用缓存命名空间和 AOP 命名空间,因为缓存是一种 AOP 活动,它背后使用的是代理模式来实现声明性缓存行为。

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

     xsi:schemaLocation="http://www.springframework.org/schema/cache  
     http://www.springframework.org/schema/cache/spring-cache-4.3.xsd 
     http://www.springframework.org/schema/beans
     http://www.springframework.org/schema/beans/spring-beans.xsd 
     http://www.springframework.org/schema/context
     http://www.springframework.org/schema/context/spring-context.xsd 
     http://www.springframework.org/schema/aop
     http://www.springframework.org/schema/aop/spring-aop-4.3.xsd"> 

     <!-- Enable caching --> 
     <cache:annotation-driven /> 

     <!-- Declare a cache manager --> 
     <bean id="cacheManager"class="org.springframework.cache.
     concurrent.ConcurrentMapCacheManager" /> 
    </beans> 

你可以在前面的 XML 文件中看到,我们已经包含了cacheaop命名空间。缓存命名空间通过以下元素定义缓存配置:

XML 元素 缓存描述
<cache:annotation-driven> 它等同于 Java 配置中的@EnableCaching,并用于启用 Spring 的缓存行为。
<cache:advice> 它定义了缓存建议
<cache:caching> 它等同于@Caching注解,并用于在缓存建议中组合一组缓存规则
<cache:cacheable> 它等同于@Cacheable注解;它使任何方法可缓存
<cache:cache-put> 它等同于@CachePut注解,并用于填充缓存
<cache:cache-evict> 它等同于@CacheEvict注解,并用于缓存清除。

让我们基于 XML 配置的以下示例:

创建一个配置文件,spring.xml如下

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

    xsi:schemaLocation="http://www.springframework.org/schema/cache 
    http://www.springframework.org/schema/cache/spring-cache-4.3.xsd 
    http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd 
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context.xsd 
    http://www.springframework.org/schema/aop
    http://www.springframework.org/schema/aop/spring-aop-4.3.xsd"> 

   <context:component-scan base- 
    package="com.packt.patterninspring.chapter9.bankapp.service, 
    com.packt.patterninspring.chapter9.bankapp.repository"/> 

    <aop:config> 
    <aop:advisor advice-ref="cacheAccount" pointcut="execution(*
    com.packt.patterninspring.chapter9.bankapp.service.*.*(..))"/> 
   </aop:config> 

   <cache:advice id="cacheAccount"> 
     <cache:caching> 
       <cache:cacheable cache="accountCache" method="findOne" /> 
         <cache:cache-put cache="accountCache" method="save" 
          key="#result.id" /> 
         <cache:cache-evict cache="accountCache" method="remove" /> 
         </cache:caching> 
      </cache:advice> 

   <!-- Declare a cache manager --> 
   <bean id="cacheManager" class="org.springframework.cache.concurrent.
    ConcurrentMapCacheManager" /> 
   </beans> 

在前面的 XML 配置文件中,高亮显示的代码是 Spring 缓存配置。在缓存配置中,您首先看到的是声明的<aop:config>然后<aop:advisor>,它们引用了 ID 为cacheAccount的建议,并且还有一个匹配建议的点切表达式。建议是用<cache:advice>元素声明的。此元素可以有多个<cache:caching>元素。但,在我们的例子中,我们只有一个<cache:caching>元素,它包含一个<cache:cacheable>元素、一个<cache:cache-put>和一个<cache:cache-evict>元素;每个都声明了从点切中的方法作为可缓存的。

让我们看看带有缓存注解的应用程序的Service类:

    package com.packt.patterninspring.chapter9.bankapp.service; 

    import org.springframework.beans.factory.annotation.Autowired; 
    import org.springframework.cache.annotation.CacheEvict; 
    import org.springframework.cache.annotation.CachePut; 
    import org.springframework.cache.annotation.Cacheable; 
    import org.springframework.stereotype.Service; 

    import com.packt.patterninspring.chapter9.bankapp.model.Account; 
    import com.packt.patterninspring.chapter9.
    bankapp.repository.AccountRepository; 

    @Service 
    public class AccountServiceImpl implements AccountService{ 

    @Autowired 
    AccountRepository accountRepository; 

    @Override 
    @Cacheable("accountCache") 
    public Account findOne(Long id) { 
      System.out.println("findOne called"); 
      return accountRepository.findAccountById(id); 
    } 

    @Override 
    @CachePut("accountCache") 
    public Long save(Account account) { 
      return accountRepository.save(account); 
    } 

    @Override 
    @CacheEvict("accountCache") 
    public void remove(Long id) { 
      accountRepository.findAccountById(id); 
    } 

   } 

在前面的文件定义中,我们使用了 Spring 的缓存注解在应用程序中创建缓存。现在让我们看看如何在应用程序中配置缓存存储。

配置缓存存储

Spring 的缓存抽象提供了大量的存储集成。Spring 为每个内存存储提供CacheManager。您只需将CacheManager与应用程序配置即可。然后CacheManager负责控制和管理工作缓存。让我们探索如何在应用程序中设置CacheManager

设置 CacheManager

您必须在应用程序中指定一个缓存管理器用于存储,以及提供给CacheManager的某些缓存提供者,或者您可以编写自己的CacheManager。Spring 在org.springframework.cache包中提供了几个缓存管理器,例如ConcurrentMapCacheManager,它为每个缓存存储单元创建一个ConcurrentHashMap

    @Bean 
    public CacheManager cacheManager() { 
      CacheManager cacheManager = new ConcurrentMapCacheManager(); 
      return cacheManager; 
    }

SimpleCacheManagerConcurrentMapCacheManager和其他是 Spring 框架缓存抽象的缓存管理器。但 Spring 提供了与第三方缓存管理器集成的支持,我们将在下一节中看到。

第三方缓存实现

Spring 的SimpleCacheManager适用于测试,但没有缓存控制选项(溢出、驱逐)。因此,我们必须使用如下的第三方替代方案:

  • Terracotta 的 EhCache

  • Google 的 Guava 和 Caffeine

  • Pivotal 的 Gemfire

让我们看看第三方缓存管理器的一种配置。

基于 Ehcache 的缓存

Ehcache是最受欢迎的缓存提供者之一。Spring 允许您通过在应用程序中配置EhCacheCacheManager来与 Ehcache 集成。例如,以下 Java 配置:

    @Bean 
    public CacheManager cacheManager(CacheManager ehCache) { 
      EhCacheCacheManager cmgr = new EhCacheCacheManager(); 
      cmgr.setCacheManager(ehCache); 
      return cmgr; 
    } 
    @Bean  
    public EhCacheManagerFactoryBean ehCacheManagerFactoryBean() { 
      EhCacheManagerFactoryBean eh = new EhCacheManagerFactoryBean(); 
      eh.setConfigLocation(new  
      ClassPathResource("resources/ehcache.xml")); 
      return eh; 
    } 

在前面的代码中,bean 方法cacheManager()创建了一个EhCacheCacheManager的对象,并将其与 Ehcache 的CacheManager相关联。第二个 bean 方法ehCacheManagerFactoryBean()创建并返回一个EhCacheManagerFactoryBean的实例。因为它是一个工厂 bean,所以它将返回一个CacheManager的实例。一个 XML 文件ehcache.xml包含了 Ehcache 配置。让我们参考以下代码中的ehcache.xml

    <ehcache> 
       <cache name="accountCache" maxBytesLocalHeap="50m"
        timeToLiveSeconds="100"> 
       </cache> 
    </ehcache> 

ehcache.xml文件的内容因应用程序而异,但您至少需要声明一个最小缓存。例如,以下 Ehcache 配置声明了一个名为accountCache的缓存,最大堆存储为 50 MB,存活时间为 100 秒:

基于 XML 的配置

让我们为 ECache 创建基于 XML 的配置,并且在这里配置的是EhCacheCacheManager。请参考以下代码:

    <bean id="cacheManager"    
     class="org.springframework.cache.ehcache.EhCacheCacheManager" 
     p:cache-manager-ref="ehcache"/> 

    <!-- EhCache library setup --> 
    <bean id="ehcache" 
      class="org.springframework.cache.ehcache.
      EhCacheManagerFactoryBean" p:config-
      location="resources/ehcache.xml"/> 

类似地,在 XML 配置的情况下,您必须为 ehcache 配置缓存管理器,配置EhCacheManagerFactoryBean类,并将config-location值设置为ehcache.xml,其中包含上一节中定义的 Ehcache 配置。请参考以下代码:

有许多第三方缓存存储支持与 Spring 框架的集成。在本章中,我只讨论了 ECache 管理器。

在下一节中,我们将讨论 Spring 如何允许您创建自己的自定义缓存注解。

创建自定义缓存注解

Spring 的缓存抽象允许您为您的应用程序创建自定义缓存注解,以便识别缓存方法用于缓存填充或缓存删除。Spring 的@Cacheable@CacheEvict注解用作元注解来创建自定义缓存注解。让我们看看以下代码中应用程序中的自定义注解:

    @Retention(RetentionPolicy.RUNTIME) 
    @Target({ElementType.METHOD}) 
    @Cacheable(value="accountCache", key="#account.id") 
    public @interface SlowService { 
    } 

在前面的代码片段中,我们定义了一个名为SlowService的自定义注解,该注解被 Spring 的@Cacheable注解标注。如果我们想在应用程序中使用@Cacheable,那么我们必须按照以下代码进行配置:

    @Cacheable(value="accountCache", key="#account.id") 
    public Account findAccount(Long accountId) 

让我们用以下代码替换前面的配置,使用我们定义的自定义注解:

    @SlowService 
    public Account findAccount(Long accountId) 

如您所见,我们只使用@SlowService注解来使方法在应用程序中可缓存。

现在让我们继续到下一节,我们将看到在应用程序缓存实现时应考虑的最佳实践。

在 Web 应用程序中应使用的顶级缓存最佳实践

在您的企业 Web 应用程序中,适当使用缓存可以使网页渲染非常快,最小化数据库访问次数,并减少服务器资源(如内存、网络等)的消耗。缓存是将过时数据存储在缓存内存中,以提升应用程序性能的非常强大的技术。以下是在设计和开发 Web 应用程序时应考虑的最佳实践:

  • 在您的 Spring Web 应用程序中,应将 Spring 的缓存注解(如@Cacheable@CachePut@CacheEvict)应用于具体类,而不是应用程序接口。然而,您也可以使用基于接口的代理来注解接口方法。请记住,Java 注解不是从接口继承的,这意味着如果您通过设置属性proxy-target-class="true"使用基于类的代理,那么 Spring 缓存注解不会被代理识别。

  • 如果您已对任何方法使用@Cacheable@CachePut@CacheEvict注解,那么如果您想从应用程序中的缓存中受益,请不要通过同一类的另一个方法直接调用它。这是因为,在直接调用缓存方法时,Spring AOP 代理永远不会应用。

  • 在企业应用程序中,Java Maps 或任何键/值集合永远不应该用作缓存。任何键/值集合都不能作为缓存。有时,开发人员将 Java Map 用作自定义缓存解决方案,但这不是缓存解决方案,因为缓存提供的不仅仅是键/值存储,如下所示:

    • 缓存提供驱逐策略

    • 您可以设置缓存的最高大小限制

    • 缓存提供持久存储

    • 缓存提供弱引用键

    • 缓存提供统计信息

      • Spring 框架提供了在应用程序中实现和配置缓存解决方案的最佳声明式方法。因此,始终使用缓存抽象层——它为应用程序提供了灵活性。我们知道@Cacheable注解允许您将业务逻辑代码与缓存横切关注点分离。

      • 在应用程序中使用缓存时务必小心。始终在确实需要的地方使用缓存,例如在 Web 服务或昂贵的数据库调用中,因为每个缓存 API 都有开销。

      • 在应用程序中实现缓存时,您必须确保缓存中的数据与数据存储保持同步。您可以使用像 Memcached 这样的分布式缓存管理器来实施适当的缓存策略,以提供相当的性能。

      • 如果由于数据库查询缓慢,从数据库中获取数据非常困难,那么您应该只将缓存作为第二选择。这是因为,每当我们在应用程序中使用缓存行为时,首先会在缓存中检查值,如果没有找到,则执行实际方法,所以这将是多余的。

  • 在本章中,我们看到了缓存如何帮助提高应用程序的性能。缓存主要在应用程序的服务层工作。在你的应用程序中,有一个由方法返回的数据;如果应用程序代码从相同的要求中反复调用它,我们可以缓存这些数据。缓存是一种避免为相同要求执行应用程序方法的好方法。当这个方法第一次被调用时,特定参数的方法返回值会存储在缓存中。对于相同参数的相同方法的后续调用,值将从该缓存中检索。通过避免执行一些资源消耗和时间消耗的操作,如执行数据库查询,缓存提高了应用程序的性能。

摘要

Spring 为 Spring 应用程序提供了缓存管理器来管理缓存。在本章中,你已经看到了如何为特定的缓存技术定义缓存管理器。Spring 提供了一些用于缓存的注解,例如@Cacheable@CachePut*和@CacheEvict,我们可以在我们的 Spring 应用程序中使用它们。我们还可以通过使用 XML 配置来配置 Spring 应用程序中的缓存。Spring 框架提供了缓存命名空间来实现这一点。使用<cache:cacheable><cache:cache-put><cache:cache-evict>元素代替相应的注解。

Spring 通过使用面向切面编程使得在应用程序中管理缓存成为可能。缓存是 Spring 框架的一个横切关注点。这意味着,缓存作为 Spring 应用程序的一个方面。Spring 通过使用 Spring AOP 模块的环绕通知来实现缓存。

在下一章第十章,使用 Spring 在 Web 应用程序中实现 MVC 模式,我们将探讨 Spring 如何在 Web 层和 MVC 模式中使用。

第十章:在 Spring 中使用 MVC 模式实现 Web 应用程序

在本书的最后一两个章节中,我们看到了所有示例都是基于使用 Spring 框架的独立应用程序。我们看到了 Spring 如何工作以提供重要功能,例如依赖注入模式、bean 生命周期管理、AOP、缓存管理,以及使用 JDBC 和 ORM 模块在后台的 Spring。在本章中,我们将看到 Spring 在网络环境中是如何工作的,以解决任何 Web 应用程序的一些常见问题,如工作流程、验证和状态管理。

与 Spring 框架中的其他模块一样,Spring 引入了它自己的网络框架,称为 Spring Web MVC。它基于 模型-视图-控制器MVC)模式。Spring Web MVC 支持表示层,并帮助您构建灵活且松散耦合的基于 Web 的应用程序。Spring MVC 模块解决了企业应用程序中测试 Web 组件的问题。它允许您在不使用应用程序中的请求和响应对象的情况下编写测试用例。在这里,我们将进一步讨论它。

在本章中,我们不仅将讨论 Spring MVC 的内部结构,还将讨论 Web 应用程序的不同层。我们将在这里看到 MVC 模式的实现,包括它是什么,以及为什么我们应该使用它。在本章中,我们将探讨关于 Spring MVC 网络框架的以下主题:

  • 在 Web 应用程序上实现 MVC 模式

  • 实现 MVC 模式

  • DispatcherServlet 配置为前端控制器模式

  • 启用 Spring MVC 和代理

  • 接受请求参数

  • 处理网页的表单

  • 在 MVC 模式中实现视图

  • 在 Web 应用程序中创建 JSP 视图

  • 视图助手模式

  • 使用 Apache Tiled ViewResolver 的组合视图模式

让我们详细看看上述所有主题。

在 Web 应用程序中实现 MVC 模式

模型视图控制器模式MVC 模式)是一个 J2EE 设计模式。它最初由 Trygve Reenskaug 在他自己的项目中引入,以分离应用程序的不同组件。当时,他在基于桌面的应用程序上使用了这种模式。这种模式的主要方法是促进软件行业关注点分离的原则。MVC 模式将系统划分为三种类型的组件。系统中的每个组件都有特定的职责。让我们看看这个模式中的这三个组件:

  • 模型:在 MVC 模式中,模型负责维护视图所需的数据,以便在任何视图模板中渲染。简而言之,我们可以这样说,模型是一个数据对象,例如银行系统中的 SavingAccount,或者任何银行分支的账户列表。

  • 视图:在 MVC 模式中,视图负责在 Web 应用程序中将模型渲染到自身,以表示页面。它以可读的格式向用户展示模型数据。有几种技术可以提供视图,例如 JSP、JSF 页面、PDF、XML 等。

  • 控制器:这是 MVC 模式中的一个实际可操作的组件。在软件中,控制器代码控制视图和模型之间的交互。例如,表单提交或点击链接等交互都是企业应用程序中控制器的一部分。控制器还负责创建和更新模型,并将此模型转发到视图进行渲染。

看看下面的图,以了解更多关于 MVC 模式的信息:

图片

正如你可以在前面的图中看到的那样,应用程序中有三个组件,每个组件都有自己的职责。正如我们之前所说的,MVC 模式就是关注点的分离。在软件系统中,关注点的分离对于使组件灵活且易于通过干净的代码结构进行测试非常重要。在 MVC 模式中,用户通过视图组件与控制器组件交互,而控制器组件触发实际操作以准备模型组件。那个模型组件将更改传播到视图,最终,视图组件在用户面前渲染模型。这就是 MVC 模式实现背后的整个理念。这种 MVC 模式的方法非常适合大多数应用程序,尤其是桌面应用程序。这种 MVC 模式也被称为 Model 1 架构。

但如果你正在使用企业级 Web 应用程序,事情将略不同于桌面应用程序,因为由于 HTTP 协议的无状态特性,在请求生命周期中保持模型可能会相当困难。让我们在下一节中看看 MVC 模式的另一个修改版本,以及 Spring 框架如何采用它来创建企业级 Web 应用程序。

带有 Spring 的 Model 2 架构 MVC 模式

Model 1 架构对于 Web 应用程序来说并不非常直接。Model 1 也有分散的导航控制,因为在这个架构中,每个用户都有一个单独的控制器,以及不同的逻辑来确定下一页。那时对于 Web 应用程序,Model 1 架构使用 Servlet 和 JSP 作为开发 Web 应用程序的主要技术。

对于 Web 应用程序,MVC 模式作为模型 2 架构实现。这个模式提供了集中的导航控制逻辑,以便轻松测试和维护 Web 应用程序,并且它还比模型 1 架构的 Web 应用程序提供了更好的关注点分离。基于模型 1 架构的 MVC 模式和基于模型 2 架构修改的 MVC 模式之间的区别在于后者包含一个前端控制器,该控制器将所有传入的请求委派给其他控制器。这些控制器处理传入的请求,返回模型,并选择视图。查看以下图示以更好地理解模型 2 架构的 MVC 模式:

如前图所示,为 MVC 模式引入了一个新的组件,即前端控制器。它实现为一个javax.servlet.Servlet,例如 struts 中的ActionServlet,JSF 中的FacesServlet和 Spring MVC 中的DispatcherServlet。它处理传入的请求,并将请求委派给特定的应用程序控制器。该应用程序控制器创建和更新模型,并将其委派给前端控制器进行渲染。最后,前端控制器确定特定的视图,并渲染该模型数据。

前端控制器设计模式

前端控制器设计模式是一个 J2EE 模式;它为以下应用程序设计问题提供了解决方案:

  • 在基于模型 1 架构的 Web 应用程序中,需要太多的控制器来处理太多的请求。维护和重用它们都很困难。

  • 每个请求在 Web 应用程序中都有自己的入口点;每个请求应该有一个单一的入口点。

  • JSP 和 Servlet 是模型 1 MVC 模式的主要组件,因此,这些组件处理动作和视图,违反了单一职责原则。

前端控制器为 Web 应用程序上述的设计问题提供了解决方案。在 Web 应用程序中,它作为主组件,将所有请求路由到框架控制。这意味着太多的请求都落在单个控制器(前端控制器)上,然后,这些请求被委派给特定的控制器。前端控制器提供集中控制,提高了可重用性和可管理性,因为通常只有资源注册在 Web 容器中。这个控制器不仅处理太多的请求,还有以下职责:

  • 它初始化框架以适应请求

  • 它加载所有 URL 的映射以及处理请求的组件

  • 它为视图准备映射

让我们看看以下关于前端控制器的图示:

正如您在前面的图中可以看到的,所有应用请求都落在前端控制器上,并且它将这些请求委派给配置的应用控制器。

Spring 框架提供了一个基于 MVC 模式的模块,即 Model 2 架构实现。Spring MVC 模块通过引入org.springframework.web.servlet.DispatcherServlet类提供了开箱即用的前端控制器模式实现。这是一个简单的servlet类,是 Spring MVC 框架的骨干。而且这个 Servlet 与 Spring IoC 容器集成,以利用 Spring 的依赖注入模式。Spring 的 Web 框架使用 Spring 进行其自身的配置,所有控制器都是 Spring Bean;这些控制器是可测试的工件。

在本章中,让我们深入 Spring MVC 的内部,并更仔细地观察 Spring MVC 框架中的org.springframework.web.servlet.DispatcherServlet,以及它是如何处理 Web 应用的所有传入请求的。

处理请求的生命周期

您是否曾经玩过一款木质迷宫棋盘游戏,一种带有钢球轴承的迷宫谜题?您可能在童年时玩过。这是一款非常疯狂的游戏。这个游戏的目标是通过相互连接的弯曲路径将所有钢球轴承发送到木质迷宫棋盘的中心,这些弯曲路径在中心附近有切口,通向第二个弯曲路径。所有球都需要通过这些弯曲路径之间的切口导航到木质迷宫棋盘的中心。如果一个钢球到达中心,那么我们必须小心这个球,以确保在尝试将另一个球移动到中心时,它不会离开中心。您可以在以下图中看到这一点:

图片

从直观上看,Spring MVC 框架与这款木质迷宫棋盘游戏相似。Spring MVC 框架不是通过移动钢球轴承通过各种弯曲路径和切口,而是通过各种组件如前端控制器(即分发 Servlet)、处理器映射、控制器和视图解析器来移动 Web 应用请求。

让我们看看 Spring MVC 框架中 Web 应用的请求处理流程。Spring Web MVC 的DispatcherServlet的请求处理工作流程在以下图中展示:

图片

如您所知,前端控制器在 Model 2 MVC 模式中扮演着非常重要的角色,因为它负责处理所有传入的 Web 应用请求,并为浏览器准备响应。在 Spring MVC 框架中,org.springframework.web.servlet.DispatcherServlet 扮演着 Model 2 MVC 模式的前端控制器角色。正如您在最后一张图中可以看到的,这个DispatcherServlet 使用了许多其他组件来履行其自身角色。让我们看看 Spring MVC 框架中逐步的请求处理过程:

  1. 用户点击浏览器或提交应用程序的 Web 表单。请求离开浏览器,可能带有一些附加信息或常见信息。这个请求到达 Spring 的DispatcherServlet,它是一个简单的servlet类,与其他基于 Java 的 Web 应用程序类似。它是 Spring MVC 框架的前端控制器,将所有传入的请求通过单一点进行集中处理。Spring MVC 框架通过使用这个前端控制器来集中控制请求流程。

  2. 请求到达 Spring 的DispatcherServlet后,它将请求委托给 Spring MVC 控制器,即应用程序控制器。尽管在一个 Spring Web 应用程序中可能有多个控制器,但每个请求都必须委托给特定的控制器。为此,Spring 的DispatcherServlet借助在 Web 应用程序中配置的处理映射来提供帮助。处理映射通过使用 URL 和请求参数来确定特定的控制器。

  3. 一旦 Spring 的DispatcherServlet借助处理映射配置确定了特定的应用程序控制器,DispatcherServlet将请求调度到所选控制器。这是负责根据用户的请求及其参数处理信息的实际控制器。

  4. Spring MVC 的控制器通过使用应用程序的业务服务来执行业务逻辑,并创建一个模型,该模型封装了要返回给用户并显示在浏览器中的信息。这个模型根据用户的请求携带信息。但是,这个模型尚未格式化,我们可以使用任何视图模板技术来在浏览器中渲染模型信息。这就是为什么 Spring MVC 的控制器还返回一个逻辑视图名称以及模型。为什么它返回一个逻辑视图名称?这是因为 Spring MVC 的控制器并未绑定到任何特定的视图技术,如 JSP、JSF、Thymeleaf 等。

  5. 再次强调,Spring MVC 的DispatcherServlet借助视图解析器;该解析器在 Web 应用程序中配置,用于解析视图。根据配置的ViewResolver,它解析实际的视图名称,而不是逻辑视图名称。现在DispatcherServlet也有了视图,可以渲染模型信息。

  6. Spring MVC 的DispatcherServlet将模型渲染到视图中,并生成用户可读的模型信息格式。

  7. 最后,这些信息生成一个响应,并通过DispatcherServlet返回给用户的浏览器。

如您所见,处理应用程序请求涉及多个步骤和组件。其中大部分组件与 Spring MVC 框架相关,并且每个组件都有其特定的职责来处理请求。

到目前为止,你已经了解到DispatcherServlet是使用 Spring MVC 处理请求的关键组件。它是 Spring Web MVC 的核心。它是一个前端控制器,类似于 Struts 的ActionServlet / JSF 的FacesServlet,协调所有请求处理活动。它委托给 Web 基础设施 bean,并调用用户 Web 组件。它也非常灵活、可配置和完全可定制。它非常灵活,因为该 servlet 使用的所有组件都是所有基础设施 bean 的接口。以下表格列出了 Spring MVC 框架提供的部分涉及接口:

Spring MVC 组件 在请求处理中的作用
org.springframework.web.multipart.MultipartResolver 它处理多部分请求,例如文件上传
org.springframework.web.servlet.LocaleResolver 它处理区域解析和修改
org.springframework.web.servlet.ThemeResolver 它处理主题解析和修改
org.springframework.web.servlet.HandlerMapping 它将所有传入请求映射到处理器对象。
org.springframework.web.servlet.HandlerAdapter 它基于适配器模式,用于执行处理器对象
org.springframework.web.servlet.HandlerExceptionResolver 它处理处理器执行过程中抛出的异常
org.springframework.web.servlet.ViewResolver 它将逻辑视图名称转换为实际的视图实现

上述表格中列出的组件在 Web 应用程序的请求处理生命周期中工作于 Spring MVC 框架。在下一节中,我们将看到如何配置 Spring MVC 的主要组件,即DispatcherServlet。我们还将更详细地了解基于 Java 或 XML 的不同实现和配置方式。

将 DispatcherServlet 配置为前端控制器

在基于 Java 的 Web 应用程序中,所有 servlet 都在web.xml文件中定义。它在启动时由 Web 容器加载,并将每个 servlet 映射到特定的 URL 模式。同样,org.springframework.web.servlet.DispatcherServlet是 Spring MVC 的核心;它需要在同一文件web.xml中进行配置,并在 Web 应用的启动时加载。在启动时,DispatcherServlet被调用以通过 Java、XML 或基于注解的方式加载 bean 的配置来创建 Spring 的org.springframework.web.context.WebApplicationContext。servlet 试图从这个 Web 应用程序上下文中获取所有必需的组件。它有责任通过所有其他组件路由请求。

WebApplicationContextApplicationContext 的 Web 版本,如本书前几章所述。它具有一些额外的能力,对于 Web 应用来说,除了 ApplicationContext 之外,如特定于 servlet 的作用域请求、会话等。WebApplicationContext 绑定在 ServletContext 中;你也可以通过使用 RequestContextUtils 类的静态方法来访问它。让我们看看以下代码片段:

ApplicationContext webApplicationContext = RequestContextUtils.findWebApplicationContext(request);

由 XML 配置定义

正如你所知,web.xml 是任何 Web 应用的根文件,位于 WEB-INF 目录中。它包含一个 servlet 规范,并包含所有需要启动的 servlet 配置。让我们看看 Web 应用中 DispatcherServlet 配置所需的代码,如下所示:

    <web-app version="3.0" 

    xsi:schemaLocation=http://java.sun.com/xml/ns/javaee 
    http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd      
    metadata-complete="true"> 
      <servlet> 
         <servlet-name>bankapp</servlet-name> 
         <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> 
         <load-on-startup>1</load-on-startup> 
      </servlet> 
      <servlet-mapping> 
         <servlet-name>bankapp</servlet-name> 
         <url-pattern>/*</url-pattern> 
      </servlet-mapping> 
   </web-app> 

前述代码是使用基于 XML 的配置在 Spring Web 应用中配置 DispatcherServlet 所需的最小代码。

web.xml 文件中没有什么特别之处;通常,它只定义一个与传统的 Java Web 应用非常相似的 servlet 配置。但是,DispatcherServlet 会加载一个包含应用 spring beans 配置的文件。默认情况下,它会从 WEB-INF 目录加载一个名为 [servletname]-servlet.xml 的文件。在我们的例子中,文件名应该是 bankapp-servlet.xml,位于 WEB-INF 目录中。

由 Java 配置定义

在本章中,我们将使用 Java 而不是 XML 来配置我们的 Web 应用在 servlet 容器中的 DispatcherServlet。Servlet 3.0 及以后的版本支持基于 Java 的启动,因此,我们可以避免使用 web.xml 文件。相反,我们可以创建一个实现 javax.servlet.ServletContainerInitializer 接口的 Java 类。Spring MVC 提供了一个 WebApplicationInitializer 接口,以确保你的 Spring 配置在任何 Servlet 3 容器中都被加载和初始化。但是,Spring MVC 框架通过提供一个 WebApplicationInitializer 接口的抽象类实现,使这个过程变得更加简单。通过使用这个抽象类,你只需映射你的 servlet 映射,并提供根和 MVC 配置类。我个人更喜欢在我的 Web 应用中使用这种方式进行配置。以下是这个配置类的代码:

    package com.packt.patterninspring.chapter10.bankapp.web; 

    import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; 

    import com.packt.patterninspring.chapter10.bankapp.config.AppConfig; 
    import com.packt.patterninspring.chapter10.bankapp.web.mvc.SpringMvcConfig; 

    public class SpringApplicationInitilizer extends AbstractAnnotationConfigDispatcherServletInitializer
    { 
     // Tell Spring what to use for the Root context: as ApplicationContext - "Root" configuration 
      @Override 
      protected Class<?>[] getRootConfigClasses() { 
         return new Class <?>[]{AppConfig.class}; 
      } 
     // Tell Spring what to use for the DispatcherServlet context: WebApplicationContext- MVC 
     configuration 
     @Override 
     protected Class<?>[] getServletConfigClasses() { 
         return new Class <?>[]{SpringMvcConfig.class}; 
     } 

     // DispatcherServlet mapping, this method responsible for URL pattern as like in web.xml file 
     <url-pattern>/</url-pattern> 
     @Override 
     protected String[] getServletMappings() { 
         return new String[]{"/"}; 
     } 
    } 

如前述代码所示,SpringApplicationInitializer 类扩展了 AbstractAnnotationConfigDispatcherServletInitializer 类。它只从开发者那里获取所需的信息,并且所有与 DispatcherServlet 相关的配置都由这个类使用 Servlet 容器接口来配置。请查看以下图表,以了解更多关于 AbstractAnnotationConfigDispatcherServletInitializer 类及其在应用程序中配置 DispatcherServlet 的实现:

图片

你已经看到 SpringApplicationInitilizer 类覆盖了 AbstractAnnotationConfigDispatcherServletInitializer 类的三个方法,即 getServletMappings()getServletConfigClasses()getRootConfigClasses()getServletMappings() 方法定义了 servlet 映射--在我们的应用程序中,它映射到 "/"getServletConfigClasses() 方法要求 DispatcherServlet 使用在 SpringMvcConfig 配置类中定义的 bean 加载其应用程序上下文。此配置文件包含与 Web 组件(如控制器、视图解析器和处理器映射)相关的 bean 定义。Spring Web 应用程序还有一个应用程序上下文,它是由 ContextLoaderListener 创建的。因此,另一个方法 getRootConfigClasses() 加载了其他 bean,如服务、存储库、数据源以及其他在 AppConfig 配置类中定义的应用程序中间层和数据层所需的 bean。

Spring 框架提供了一个监听器类--ContextLoaderListener。它负责启动后端应用程序上下文。

让我们查看以下图表,以了解在启动 servlet 容器后关于 Spring Web 应用程序设计的更多信息:

图片

如你在最后一张图中看到的,getServletConfigClasses() 方法返回的 Web 组件 bean 定义配置类由 DispatcherServlet 加载,而 getRootConfigClasses() 方法返回的其他应用程序 bean 定义配置类由 ContextLoaderListener 加载。

基于 Java 的 Web 配置仅在部署到支持 Servlet 3.0 的服务器上时才有效,例如 Apache Tomcat 7 或更高版本

让我们看看在下一节中如何启用 Spring MVC 框架的更多功能。

启用 Spring MVC

有许多方法可以配置 DispatcherServlet 以及其他 Web 组件。Spring MVC 框架有许多默认未启用的功能,例如 HttpMessageConverter,支持使用 @Valid 验证 @Controller 输入,等等。因此,我们可以通过使用基于 Java 的配置或 XML 配置来启用这些功能。

要启用 MVC Java 配置,将注解 @EnableWebMvc 添加到你的 @Configuration 类之一,如下所示:

 import org.springframework.context.annotation.Configuration; 
    import org.springframework.web.servlet.config.annotation.EnableWebMvc; 
    @Configuration 
    @EnableWebMvc 
    public class SpringMvcConfig { 
    } 

在 XML 配置中,我们可以使用 MVC 命名空间,其中有一个 <mvc:annotation-driven> 元素,你可以使用它来启用基于注解的 Spring MVC。

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

    xsi:schemaLocation=" 
        http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans.xsd 
        http://www.springframework.org/schema/mvc 
        http://www.springframework.org/schema/mvc/spring-mvc.xsd"> 

    <mvc:annotation-driven/> 

    </beans> 

Spring MVC 的高级功能可以在 Spring Web 应用程序中通过使用 @EnableWebMvc 注解或使用 XML 命名空间 <mvc:annotation-driven/> 来启用。Spring MVC 框架还允许您通过扩展 WebMvcConfigurerAdapter 类或实现 WebMvcConfigurer 接口来在 Java 中自定义默认配置。让我们看看添加更多配置后的修改后的配置文件:

    package com.packt.patterninspring.chapter10.bankapp.web.mvc; 

    import org.springframework.context.annotation.Bean; 
    import org.springframework.context.annotation.ComponentScan; 
    import org.springframework.context.annotation.Configuration; 
    import org.springframework.web.servlet.ViewResolver; 
    import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer; 
    import org.springframework.web.servlet.config.annotation.EnableWebMvc; 
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; 
    import org.springframework.web.servlet.view.InternalResourceViewResolver; 

    @Configuration 
    @ComponentScan(basePackages = {" com.packt.patterninspring.chapter10.bankapp.web.controller"})   
    @EnableWebMvc 
    public class SpringMvcConfig extends WebMvcConfigurerAdapter{ 

    @Bean 
    public ViewResolver viewResolver(){ 
         InternalResourceViewResolver viewResolver = new InternalResourceViewResolver(); 
         viewResolver.setPrefix("/WEB-INF/view/"); 
         viewResolver.setSuffix(".jsp"); 
         return viewResolver; 
    } 

    @Override 
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { 
         configurer.enable(); 
    } 

   }

如前述代码所示,配置类 SpringMvcConfig 被注解为 @Configuration@ComponentScan@EnableWebMvc。在这里,com.packt.patterninspring.chapter10.bankapp.web.controller 包将被扫描以查找组件。这个类扩展了 WebMvcConfigurerAdapter 类,并重写了 configureDefaultServletHandling() 方法。我们还配置了一个 ViewResolver bean。

到目前为止,你已经学习了 MVC 模式和架构是什么,以及如何设置 DispatcherServlet 并启用 Spring Web 应用程序的基本 Spring MVC 组件。在下一节中,我们将讨论如何在 Spring 应用程序中实现控制器,以及这些控制器如何处理 Web 请求。

实现控制器

如我们在 MVC 模式中所见,控制器也是 MVC 模式中的关键组件之一。它们负责执行实际请求、准备模型,并将此模型连同逻辑视图名称发送到前端控制器。在 Web 应用程序中,控制器在 Web 层和核心应用程序层之间工作。在 Spring MVC 框架中,控制器更像是有方法的 POJO 类;这些方法被称为处理器,因为它们被 @RequestMapping 注解所标记。让我们看看如何在 Spring Web 应用程序中定义控制器类。

使用 @Controller 定义控制器

让我们为我们的银行应用程序创建一个控制器类。HomeController 是一个控制器类,它处理 / 的请求并渲染银行应用程序的首页:

    package com.packt.patterninspring.chapter10.bankapp.web.controller; 

    import org.springframework.stereotype.Controller; 
    import org.springframework.web.bind.annotation.RequestMapping; 
    import org.springframework.web.bind.annotation.RequestMethod; 

    @Controller 
    public class HomeController { 

      @RequestMapping(value = "/", method = RequestMethod.GET) 
      public String home (){ 
         return "home"; 
     } 
   } 

如前述代码所示,HomeController类包含home()方法。它是一个处理器方法,因为它被@RequestMapping注解标记。它指定此方法处理所有映射到/ URL 的请求。另一个需要注意的事情是我们的控制器类HomeController也被@Controller注解标记。正如我们所知,@Controller是一个 stereotypes 注解,它也被用来在 Spring IoC 容器中创建 bean,类似于@Component注解的其他元注解,如@Service@Repository。是的,这个注解指定任何类作为控制器,并给这个类添加一些 Spring MVC 的更多功能。你也可以使用@Component注解代替@Controller来在 Web 应用程序中创建 Spring beans,但在这个情况下,那个 bean 没有像在 Web 层的异常处理、处理器映射等功能一样的 Spring MVC 框架的能力。

让我们更仔细地看看@RequestMapping注解,以及@RequestMapping注解的复合变体。

使用@RequestMapping映射请求

之前定义的HomeController类只有一个处理器方法,此方法被@RequestMapping注解标记。在这里,我使用了这个注解的两个属性——一个是 value 属性,用于将 HTTP 请求映射到/模式,另一个属性是一个支持 HTTP GET方法的方法。我们可以使用一个处理器方法定义多个 URL 映射。让我们在以下代码片段中看看这个例子:

    @Controller 
    public class HomeController { 

     @RequestMapping(value = {"/", "/index"}, method = RequestMethod.GET) 
     public String home (){ 
         return "home"; 
    } 
   } 

在前面的代码中,@RequestMapping注解的 value 属性有一个字符串值数组。现在,这个处理器方法被映射到两个 URL 模式,例如//index。Spring MVC 的@RequestMapping注解支持多种 HTTP 方法,如GETPOSTPUTDELETE等。截至版本 4.3,Spring 组合了@RequestMapping变体,现在提供了映射常见 HTTP 方法的简单方法,如下所示的表达式:

    @RequestMapping + HTTP GET = @GetMapping 
    @RequestMapping + HTTP POST = @PostMapping 
    @RequestMapping + HTTP PUT = @PutMapping 
    @RequestMapping + HTTP DELETE = @DeleteMapping 

这是修改后的HomeController,带有复合注解映射:

    @Controller 
    public class HomeController { 

      @GetMapping(value = {"/", "/index"}) 
      public String home (){ 
         return "home"; 
      } 
   } 

我们可以在两个位置使用@RequestMapping注解:在类级别,以及在方法级别。让我们看看这个例子:

方法级别的@RequestMapping

Spring MVC 允许你在方法级别使用@RequestMapping注解,将此方法作为 Spring 网络应用程序中的处理器方法。让我们看看如何在以下类中使用它:

     package com.packt.patterninspring.
       chapter10.bankapp.web.controller; 
     import org.springframework.stereotype.Controller; 
     import org.springframework.ui.ModelMap; 
     import org.springframework.web.bind.annotation.RequestMapping; 
     import org.springframework.web.bind.annotation.RequestMethod; 

     import com.packt.patterninspring.chapter10.bankapp.model.User; 

     @Controller 
     public class HomeController { 

      @RequestMapping(value = "/", method = RequestMethod.GET) 
      public String home (){ 
         return "home"; 
      } 

      @RequestMapping(value = "/create", method = RequestMethod.GET) 
      public String create (){ 
         return "addUser"; 
      } 

      @RequestMapping(value = "/create", method = RequestMethod.POST) 
      public String saveUser (User user, ModelMap model){ 
         model.put("user", user); 
         return "addUser"; 
      } 
    } 

如您在前面的代码中所见,我使用了@RequestMapping注解,并带有三个方法home()create()saveUser()。在这里,我还使用了这个注解的“value”和“method”属性。其中,“value”属性包含请求映射和请求 URL,“method”属性用于定义 HTTP 请求方法,如 GET 或 POST。映射规则通常是基于 URL 的,并且可选地使用通配符,如下所示:

    - /create 
    - /create/account 
    - /edit/account 
    - /listAccounts.htm - Suffix ignored by default. 
    - /accounts/* 

在前面的示例中,处理方法有一些参数,因此我们可以传递任何类型和数量的参数。Spring MVC 将这些参数作为请求参数处理。让我们首先看看如何在类级别上定义@RequestMapping,然后我们将讨论请求参数。

类级别的@RequestMapping

Spring MVC 允许您在类级别上使用@RequestMapping注解。这意味着我们可以使用@RequestMapping注解控制器类,如下面的代码片段所示:

    package com.packt.patterninspring.chapter10.bankapp.web.controller; 

    import org.springframework.stereotype.Controller; 
    import org.springframework.ui.ModelMap; 
    import org.springframework.web.bind.annotation.RequestMapping; 
    import org.springframework.web.bind.annotation.RequestMethod; 

    @Controller 
    @RequestMapping("/") 
    public class HomeController { 

     @RequestMapping(method=GET) 
     public String home() { 
         return "home"; 
     } 
   } 

如您在前面的代码中所见,HomeController类被注解了@RequestMapping@Controller注解。但是 HTTP 方法仍然定义在处理方法之上。类级别的映射应用于此控制器下定义的所有处理方法。

在 Spring MVC 配置之后,我们创建了一个控制器类,其中包含处理方法。在深入更多细节之前,让我们测试这个控制器。在这本书中,我没有使用任何 JUnit 测试用例,所以在这里,我将在 Tomcat 容器上运行这个 Web 应用程序。您可以在浏览器中看到以下输出:

图片

最后一张图片是我们银行管理系统Web 应用程序的首页。

在 Spring 3.1 之前,Spring MVC 使用两步将请求映射到处理方法。首先,通过DefaultAnnotationHandlerMapping选择控制器,然后通过AnnotationMethodHandlerAdapter将实际方法映射到传入的请求。但是从 Spring 3.1 开始,Spring MVC 通过使用RequestMappingHandlerMapping直接一步将请求映射到处理方法。

在下一节中,我们将看到如何定义处理方法,以及 Spring MVC 中处理方法的允许返回类型和参数。

定义@RequestMapping处理方法

在 Spring MVC 框架中,@RequestMapping处理方法在定义签名方面非常灵活。您可以在任何顺序中传递任意数量的参数。这些方法支持大多数类型的参数,并且在返回类型方面也非常灵活。它可以有多个返回类型,其中一些如下列出:

  • 支持的方法参数类型

    • 请求或响应对象(Servlet API)

    • 会话对象(Servlet API)

    • java.util.Locale

    • java.util.TimeZone

    • java.io.InputStream / java.io.Reader

    • java.io.OutputStream / java.io.Writer

    • java.security.Principal

    • @PathVariable

    • @RequestParam

    • @RequestBody

    • @RequestPart

    • java.util.Map / org.springframework.ui.Model / org.springframework.ui.ModelMap

    • org.springframework.validation.Errors / org.springframework.validation.BindingResult

  • 支持的方法返回类型:

    • ModelAndView

    • Model

    • Map

    • View

    • String

    • void

    • HttpEntity<?>ResponseEntity<?>

    • HttpHeaders

    • Callable<?>

    • DeferredResult<?>

我列出了一些支持的返回类型和方法参数类型。看起来 Spring MVC 在定义请求处理方法方面非常灵活和可定制,与其他 MVC 框架不同。

在 Spring MVC 框架中,处理方法甚至可以以任何顺序传递参数,但在 Errors 或 BindingResult 参数的情况下,我们必须将这些参数放在前面,然后是模型对象,以便立即绑定,因为处理方法可能有任意数量的模型对象,Spring MVC 为每个对象创建单独的 Errors 或 BindingResult 实例。例如:

无效位置 @PostMapping

public String saveUser(@ModelAttribute ("user") User user, ModelMap model, BindingResult result){...}

有效位置 @PostMapping

public String saveUser(@ModelAttribute ("user") User user, BindingResult result, ModelMap model){...}

让我们看看如何在下一节中如何将模型数据传递到视图层。

将模型数据传递到视图

到目前为止,我们已经实现了一个非常简单的 HomeCotroller 并对其进行了测试。但在网络应用程序中,我们也向视图层传递了模型数据。我们传递到模型中的模型数据(简单来说,它是一个 Map),控制器与逻辑视图名称一起返回该模型。正如你所知道的那样,Spring MVC 支持处理方法的多种返回类型。让我们看看以下示例:

    package com.packt.patterninspring.chapter10.bankapp.web.controller; 

    import java.util.List; 

    import org.springframework.beans.factory.annotation.Autowired; 
    import org.springframework.stereotype.Controller; 
    import org.springframework.ui.ModelMap; 
    import org.springframework.web.bind.annotation.GetMapping; 
    import org.springframework.web.bind.annotation.PostMapping; 

    import com.packt.patterninspring.chapter10.bankapp.model.Account; 
    import com.packt.patterninspring.chapter10.bankapp.service.AccountService; 

    @Controller 
    public class AccountController { 

     @Autowired 
     AccountService accountService; 

     @GetMapping(value = "/open-account") 
     public String openAccountForm (){ 
         return "account"; 
     } 

     @PostMapping(value = "/open-account") 
     public String save (Account account, ModelMap model){ 
         account = accountService.open(account); 
         model.put("account", account); 
         return "accountDetails"; 
     } 

     @GetMapping(value = "/all-accounts") 
     public String all (ModelMap model){ 
         List<Account> accounts = accountService.findAllAccounts(); 
         model.put("accounts", accounts); 
         return "accounts"; 
     } 
   } 

如前例所示,AccountController 类有三个处理方法。两个处理方法返回模型数据和逻辑视图名称。但在这个例子中,我正在使用 Spring MVC 的 ModelMap,因此,我们不需要强制返回逻辑视图,它将自动与响应绑定。

接下来,你将学习如何接受请求参数。

接受请求参数

在 Spring 网络应用程序中,有时我们只是从服务器端读取数据,就像我们的例子一样。读取所有账户的数据是一个简单的读取调用,不需要请求参数。但如果你想要获取特定账户的数据,你必须将账户 ID 与请求参数一起传递。同样,为了在银行创建新的账户,你必须将账户对象作为参数传递。在 Spring MVC 中,我们可以以下方式接受请求参数:

  • 获取查询参数

  • 通过路径变量获取请求参数

  • 获取表单参数

让我们逐一查看这些方法。

获取查询参数

在 Web 应用程序中,我们可以从请求中获取请求参数——在我们的例子中是账户 ID,如果你想要访问特定账户的详细信息。让我们使用以下代码从请求参数中获取账户 ID:

    @Controller 
    public class AccountController { 
      @GetMapping(value = "/account") 
      public String getAccountDetails (ModelMap model, HttpServletRequest request){ 
         String accountId = request.getParameter("accountId"); 
         Account account = accountService.findOne(Long.valueOf(accountId)); 
         model.put("account", account); 
         return "accountDetails"; 
     } 
    }  

在前面的代码片段中,我使用了传统的访问请求参数的方式。Spring MVC 框架提供了一个注解@RequestParam来访问请求参数。让我们使用@RequestParam注解将请求参数绑定到控制器中的方法参数。以下代码片段展示了@RequestParam注解的使用。它从请求中提取参数,并执行类型转换:

    @Controller 
    public class AccountController { 
     @GetMapping(value = "/account") 
     public String getAccountDetails (ModelMap model, @RequestParam("accountId") long accountId){ 
         Account account = accountService.findOne(accountId); 
         model.put("account", account); 
         return "accountDetails "; 
    } 
   }  

在前面的代码中,我们通过使用@RequestParam注解来访问请求参数,你也可以注意到我没有使用从StringLong的类型转换,这将由这个注解自动完成。这里还有一个需要注意的事项,即使用这个注解的参数默认是必需的,但 Spring 允许你通过使用@RequestParam注解的required属性来覆盖这个行为。

    @Controller 
    public class AccountController { 
      @GetMapping(value = "/account") 
      public String getAccountDetails (ModelMap model,  
         @RequestParam(name = "accountId") long accountId 
         @RequestParam(name = "name", required=false) String name){ 
         Account account = accountService.findOne(accountId); 
         model.put("account", account); 
         return " accountDetails "; 
     } 
   } 

现在我们来看看如何使用路径变量来获取请求路径的一部分作为输入。

通过路径变量获取请求参数

Spring MVC 允许你通过 URI 传递参数,而不是通过请求参数传递。传递的值可以从请求 URL 中提取。它基于 URI 模板。这不是 Spring 特有的概念,许多框架通过使用{...}占位符和@PathVariable注解来实现这一点。它允许创建没有请求参数的干净 URL。以下是一个示例:

    @Controller 
    public class AccountController { 
      @GetMapping("/accounts/{accountId}") 
      public String show(@PathVariable("accountId") long accountId, Model model) { 
         Account account = accountService.findOne(accountId); 
         model.put("account", account); 
         return "accountDetails"; 
     } 
     ... 
   } 

在上一个处理器中,方法可以像这样处理请求:

http://localhost:8080/Chapter-10-Spring-MVC-pattern/account?accountId=1000 

图片

但在前面的示例中,处理器方法可以处理如下请求:

http://localhost:8080/Chapter-10-Spring-MVC-pattern/accounts/2000 

图片

我们在前面的代码和图像中已经看到了如何通过使用请求参数或路径参数来传递一个值。如果你在请求中传递少量数据,这两种方式都是可行的。但在某些情况下,我们必须向服务器传递大量数据,例如表单提交。让我们看看如何编写处理表单提交的控制器方法。

处理网页的表单形式

正如你所知,在任何 Web 应用程序中,我们都可以从服务器发送和接收数据。在 Web 应用程序中,我们通过填写表单并提交这个表单到服务器来发送数据。Spring MVC 也通过显示表单、验证表单数据以及提交这些表单数据来为客户端提供表单处理的支持。

基本上,Spring MVC 首先处理表单显示和表单处理。在银行管理应用程序中,你需要创建一个新的用户,并在银行中开设一个新的账户,因此,让我们创建一个控制器类,AccountController,它包含一个用于显示开户表单的单个请求处理方法,如下所示:

    package com.packt.patterninspring.chapter10.bankapp.web.controller; 

    import org.springframework.stereotype.Controller; 
    import org.springframework.web.bind.annotation.GetMapping; 

    @Controller 
    public class AccountController { 

     @GetMapping(value = "/open-account") 
     public String openAccountForm (){ 
         return "accountForm"; 
    } 
   } 

openAccountForm()方法的@GetMapping注解声明它将处理对/open-account的 HTTP GET 请求。这是一个简单的方法,没有输入,只返回一个名为accountForm的逻辑视图。我们已经配置了InternalResourceViewResolver,这意味着将调用/WEB-INF/views/accountForm.jsp中的 JSP 来渲染开户表单。

这是您现在将使用的 JSP 代码:

    <%@ taglib prefix = "c" uri = "http://java.sun.com/jsp/jstl/core" %> 
    <html> 
     <head> 
         <title>Bank Management System</title> 
         <link rel="stylesheet" type="text/css" href="<c:url value="/resources/style.css" />" > 
     </head> 
     <body> 
         <h1>Open Account Form</h1> 
          <form method="post"> 
           Account Number:<br> 
           <input type="text" name="id"><br> 
           Account Name:<br> 
           <input type="text" name="name"><br> 
           Initial Balance:<br> 
           <input type="text" name="balance"><br> 
           <br> 
           <input type="submit" value="Open Account"> 
           </form>  
    </body> 
  </html>    

如您在前面的代码中可以看到,我们有一个开户表单。它包含一些字段,例如AccountIdAccount NameInitial Balance。这个 JSP 页面有一个用于表单的<form>标签,而这个<form>标签没有任何 action 参数。这意味着当我们提交这个表单时,它将使用POST HTTP 方法调用将表单数据发送到相同的 URI /open-account。以下截图显示了账户表单:

图片

让我们添加另一个方法来处理对 HTTP POST方法具有相同 URI /open-account的调用。

实现表单处理控制器

让我们通过在 Web 应用程序中为 URI /open-account添加另一个处理方法来查看相同的AccountController类:

    package com.packt.patterninspring.chapter10.bankapp.web.controller; 

    import java.util.List; 

    import org.springframework.beans.factory.annotation.Autowired; 
    import org.springframework.stereotype.Controller; 
    import org.springframework.ui.ModelMap; 
    import org.springframework.web.bind.annotation.GetMapping; 
    import org.springframework.web.bind.annotation.PathVariable; 
    import org.springframework.web.bind.annotation.PostMapping; 

    import com.packt.patterninspring.chapter10.bankapp.model.Account; 
    import com.packt.patterninspring.chapter10.bankapp.service.AccountService; 

    @Controller 
     public class AccountController { 

       @Autowired 
       AccountService accountService; 

       @GetMapping(value = "/open-account") 
       public String openAccountForm (){ 
         return "accountForm"; 
       } 

       @PostMapping(value = "/open-account") 
       public String save (Account account){ 
         accountService.open(account); 
         return "redirect:/accounts/"+account.getId(); 
       } 

       @GetMapping(value = "/accounts/{accountId}") 
       public String getAccountDetails (ModelMap model, @PathVariable Long accountId){ 
         Account account = accountService.findOne(accountId); 
         model.put("account", account); 
         return "accountDetails"; 
       } 
    } 

如您在前面的代码中可以看到,我们在AccountController方法中添加了两个额外的处理方法,并且将服务AccountService注入到这个控制器中,以便将账户详情保存到数据库中。每当处理来自开户表单的POST请求时,控制器接受账户表单数据,并通过注入的账户服务将其保存到数据库中。它将账户表单数据作为账户对象接受。您可能也会注意到,在通过 HTTP POST方法处理表单数据后,处理方法将重定向到账户详情页面。在POST提交后重定向也是一个更好的实践,以防止意外地两次提交表单。以下是在提交请求后显示的屏幕截图:

图片

如您在浏览器的前面输出中可以看到,提交账户表单后,这个页面被渲染。因为我们添加了一个请求处理方法,这个处理方法处理了请求,并渲染了包含账户详情的另一个网页。以下 JSP 页面作为前面输出的视图被渲染:

    <%@ taglib prefix = "c" uri = "http://java.sun.com/jsp/jstl/core" %> 
    <html> 
     <head> 
         <title>Bank Management System</title> 
         <link rel="stylesheet" type="text/css" href="<c:url value="/resources/style.css" />" > 
     </head> 
     <body> 
         <h1>${message} Account Details</h1> 
           <c:if test="${not empty account }"> 
               <table border="1"> 
                     <tr> 
                           <td>Account Number</td> 
                           <td>Account Name</td> 
                           <td>Account Balance</td> 
                     </tr> 
                     <tr> 
                           <td>${account.id }</td> 
                           <td>${account.name }</td> 
                           <td>${account.balance }</td> 
                     </tr> 
               </table> 
           </c:if> 
      </body> 
    </html> 

在这段最后的代码中,处理方法将Account对象发送到模型,并返回逻辑视图名称。这个 JSP 页面渲染从响应中获取的Account对象。

这里需要注意的一点是,账户对象有 ID、名称和余额属性,这些属性将从与账户表单中字段名称相同的请求参数中填充。如果任何对象属性名称与 HTML 表单的字段名称匹配,则此属性将使用 NULL 值初始化。

使用命令设计模式进行数据绑定

将请求封装为一个对象,从而让您可以使用不同的请求参数化客户端,排队或记录请求,并支持可撤销操作。

  • GOF 设计模式

您在 第三章 中学习了命令设计模式,考虑结构性和行为模式。它是 GOF 模式行为模式家族的一部分。它是一个非常简单的数据驱动模式。它允许您将请求数据封装到对象中,并将该对象作为命令传递给调用者方法,该方法将命令作为另一个对象返回给调用者。

Spring MVC 实现了命令设计模式(Command Design pattern),将来自网页表单的请求数据作为一个对象绑定,并将该对象传递给控制器类中的请求处理器方法。在这里,我们将探讨如何使用此模式将请求数据绑定到对象,并探讨使用数据绑定的好处和可能性。在以下类中,Account Java Bean 是一个具有三个属性(idnamebalance)的简单对象:

    package com.packt.patterninspring.chapter10.bankapp.model; 

    public class Account{ 

     Long id; 
     Long balance; 
     String name; 

     public Long getId() { 
         return id; 
     } 
     public void setId(Long id) { 
         this.id = id; 
     } 
     public Long getBalance() { 
         return balance; 
     } 
     public void setBalance(Long balance) { 
           this.balance = balance; 
     } 
     public String getName() { 
         return name; 
     } 
     public void setName(String name) { 
         this.name = name; 
     } 
     @Override 
     public String toString() { 
         return "Account [id=" + id + ", balance=" + balance + ", name=" + name + "]"; 
     } 

    } 

要么我们提交与对象属性名称相同的输入文本字段名称的网页表单,要么我们以 http://localhost:8080/Chapter-10-Spring-MVC-pattern/account?id=10000 的形式接收请求。在这两种情况下,在幕后,Spring 调用 Account 类的设置方法来绑定请求数据或网页表单数据到对象。Spring 还允许您绑定索引集合,如 List、Map 等。

我们还可以自定义数据绑定。Spring 提供了两种自定义数据绑定的方式:

  • 全局自定义:它为特定的命令对象在整个 Web 应用程序中自定义数据绑定行为

  • 按控制器自定义:它为特定命令对象针对每个控制器类自定义数据绑定行为

在这里,我将仅讨论针对特定控制器的自定义。让我们看看以下代码片段,用于自定义 Account 对象的数据绑定:

    package com.packt.patterninspring.chapter10.bankapp.web.controller; 

    .... 
    .... 
    @Controller 
    public class AccountController { 

     @Autowired 
     AccountService accountService; 
     .... 
     .... 
     @InitBinder 
     public void initBinder(WebDataBinder binder) { 
         binder.initDirectFieldAccess(); 
         binder.setDisallowedFields("id"); 
         binder.setRequiredFields("name", "balance"); 
     } 
     .... 
     .... 
    } 

如前述代码所示,AccountController 有一个 initBinder(WebDataBinder binder) 方法,该方法被 @InitBinder 注解标记。此方法必须具有 void 返回类型,并且有一个 org.springframework.web.bind.WebDataBinder 作为方法参数。WebDataBinder 对象有多个方法;我们在前述代码中使用了其中一些。WebDataBinder 用于自定义数据绑定。

使用 @ModelAttributes 自定义数据绑定

Spring MVC 提供了一个额外的注解 @ModelAttributes,用于将数据绑定到 Command 对象。这是绑定数据并自定义数据绑定的一种方法。此注解允许您控制 Command 对象的创建。在 Spring MVC 应用程序中,此注解可以用于方法和方法参数。让我们看看以下示例:

  • 在方法上使用 @ModelAttribute

我们可以在方法上使用ModelAttribute注解来创建一个用于我们表单的对象,如下所示:

    package com.packt.patterninspring.chapter10.bankapp.web.controller; 
    .... 
    .... 
    @Controller 
    public class AccountController { 
      .... 
      @ModelAttribute 
      public Account account () { 
         return new Account(); 
     } 
      .... 
   } 
  • 在方法参数上使用@ModelAttribute

我们还可以在方法参数上使用这个注解。在这种情况下,处理方法的方法参数是从模型对象中查找的。如果它们在模型中不可用,则将使用默认构造函数创建:

    package com.packt.patterninspring.chapter10.bankapp.web.controller; 
    .... 
    .... 
    @Controller 
    public class AccountController { 
      ... 
      @PostMapping(value = "/open-account") 
      public String save (@ModelAttribute("account") Account account){ 
         accountService.open(account); 
         return "redirect:/accounts/"+account.getId(); 
    } 
    .... 
  } 

正如您在最后的代码片段中所看到的,@ModelAttribute注解被用于方法参数上。这意味着Account对象是从模型对象中获取的。如果它不存在,它将使用默认构造函数创建。

@ModelAttribute注解放在一个方法上时,这个方法将在请求处理方法被调用之前被调用。

到目前为止,我们已经了解了 Spring MVC 如何以传统方式或通过使用@RequestParam@PathVariable注解来处理请求和请求参数。我们还看到了如何处理表单网页,并在控制器层中将表单数据绑定到对象来处理POST请求。现在让我们来看看如何验证提交的表单数据对于业务是否有效或无效。

验证表单输入参数

在一个 Web 应用程序中,验证表单数据非常重要,因为最终用户可以提交任何内容。假设在一个应用程序中,用户通过填写账户名称提交账户表单,那么它可以在银行创建新的账户,账户持有人名称。因此,我们必须在数据库中创建新记录之前确保表单数据的有效性。您不需要在处理方法中处理验证逻辑。Spring 提供了对 JSR-303 API 的支持。从 Spring 3.0 开始,Spring MVC 支持这个 Java 验证 API。在您的 Spring Web 应用程序中配置 Java 验证 API 不需要太多配置——您只需将此 API 的实现添加到您的应用程序类路径中,例如 Hibernate Validator。

Java 验证 API 有几个注解来验证Command对象的属性。我们可以对Command对象属性的值施加约束。在本章中,我没有探索所有这些注解,但让我们看看以下使用一些这些注解的示例:

    package com.packt.patterninspring.chapter10.bankapp.model; 

    import javax.validation.constraints.NotNull; 
    import javax.validation.constraints.Size; 

    public class Account{ 

     // Not null 
     @NotNull 
     Long id; 
     // Not null 
     @NotNull 
     Long balance; 
     // Not null, from 5 to 30 characters 
     @NotNull 
     @Size(min=2, max=30) 
     String name; 

     public Long getId() { 
         return id; 
     } 
     public void setId(Long id) { 
         this.id = id; 
     } 
     public Long getBalance() { 
         return balance; 
     } 
     public void setBalance(Long balance) { 
         this.balance = balance; 
     } 
     public String getName() { 
         return name; 
     } 
     public void setName(String name) { 
         this.name = name; 
     } 
     @Override 
     public String toString() { 
         return "Account [id=" + id + ", balance=" + balance + ", name=" + name + "]"; 
     } 

    } 

正如您在前面代码中所看到的,Account 类的属性现在被注解为@NotNull,以确保值不能为 null,并且一些属性也被注解为@Size,以确保字符数在最小和最大长度之间。

仅注解Account对象的属性是不够的。我们必须注解AccountController类的 save()方法参数,如下所示:

    package com.packt.patterninspring.chapter10.bankapp.web.controller; 
    .... 
    .... 
    @Controller 
    public class AccountController { 

     .... 
     @PostMapping(value = "/open-account") 
     public String save (@Valid @ModelAttribute("account") Account account, Errors errors){ 
         if (errors.hasErrors()) { 
               return "accountForm"; 
         } 
         accountService.open(account); 
         return "redirect:/accounts/"+account.getId(); 
     } 
     .... 
    } 

如前述代码所示,Account 参数现在被注解为 @Valid,以指示 Spring 命令对象具有应强制执行的验证约束。让我们看看在提交带有无效数据的 Web 开户表单时的输出:

图片

由于我在提交此表单时没有填写数据,它已被重定向到同一页面,并显示验证错误。Spring 还允许你通过将这些消息配置到属性文件中来自定义这些验证消息。

到目前为止,在本章中,你已经了解了 MVC 模式的控制器组件。你还学习了如何在 Web 应用程序中创建和配置它。让我们在下一节中探索 MVC 模式的另一个组件,视图。

在 MVC 模式中实现视图

视图是 MVC 模式中最重要的组件。控制器将模型和逻辑视图名称一起返回给前端控制器。前端控制器使用配置的视图解析器解析到实际视图。Spring MVC 提供了多个视图解析器来支持多种视图技术,如 JSP、Velocity、FreeMarker、JSF、Tiles、Thymeleaf 等。你必须根据你在 Web 应用程序中使用的视图技术来配置视图解析器。请查看以下图表,以了解 Spring MVC 中视图模式的相关信息:

图片

如上图所示,Spring MVC 的前端控制器根据不同的视图技术有多个视图解析器。但本章中,我们将只使用 JSP 作为视图技术,因此,我们将只探索与 JSP 相关的视图解析器,即 InternalResourveViewResolver

视图渲染 Web 输出。对于 JSP、XSLT、模板方法(Velocity、FreeMarker)等,都有许多内置视图可用。Spring MVC 还提供了用于创建 PDF、Excel 工作表等的视图支持类。

控制器通常在 String MVC 中返回一个 逻辑视图名称,但 Spring 的 ViewResolvers 会根据视图名称选择视图。让我们看看如何在 Spring MVC 应用程序中配置 ViewResolver

在 Spring MVC 中定义 ViewResolver

在 Spring MVC 中,DispatcherServlet 会委托给 ViewResolver 来根据视图名称获取视图实现。默认的 ViewResolver 将视图名称视为一个相对于 Web 应用的文件路径,即一个 JSP--/WEB-INF/views/account.jsp。我们可以通过向 DispatcherServlet 注册一个 ViewResolver 实例来覆盖这个默认设置。在我们的 Web 应用程序中,我们使用了 InternalResourceViewResolver,因为它与 JSP 视图相关,但在 Spring MVC 中还有其他几个选项,如前文所述。

实现视图

以下代码展示了 MVC 模式中的视图渲染:

accountDetails.jsp:

    <%@ taglib prefix = "c" uri = "http://java.sun.com/jsp/jstl/core" %> 
    <html> 
     <head> 
         <title>Bank Management System</title> 
         <link rel="stylesheet" type="text/css" href="<c:url value="/resources/style.css" />" > 
     </head> 
    <body> 
         <h1>${message} Account Details</h1> 
           <c:if test="${not empty account }"> 
               <table border="1"> 
                     <tr> 
                           <td>Account Number</td> 
                           <td>Account Name</td> 
                           <td>Account Balance</td> 
                     </tr> 
                     <tr> 
                           <td>${account.id }</td> 
                           <td>${account.name }</td> 
                           <td>${account.balance }</td> 
                     </tr> 
               </table> 
           </c:if> 
      </body> 
    </html> 

如前代码所示,当控制器返回accountDetails作为逻辑视图名称时,Spring MVC 将渲染此视图。但是它是如何被 Spring MVC 解析的呢?让我们看看 Spring 配置文件中ViewResolver的配置。

在 Spring MVC 中注册视图解析器

让我们注册与 JSP 相关的ViewResolver,即在 Spring Web 应用程序中配置InternalResourceViewResolver,如下所示:

     package com.packt.patterninspring.chapter10.bankapp.web.mvc; 

     import org.springframework.context.annotation.Bean; 
     import org.springframework.context.annotation.ComponentScan; 
     import org.springframework.context.annotation.Configuration; 
     import org.springframework.web.servlet.ViewResolver; 
     import org.springframework.web.servlet.config.annotation.EnableWebMvc; 
     import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; 
     import org.springframework.web.servlet.view.InternalResourceViewResolver; 

     @Configuration 
     @ComponentScan(basePackages = {"com.packt.patterninspring.chapter10.bankapp.web.controller"})       
     @EnableWebMvc 
     public class SpringMvcConfig extends WebMvcConfigurerAdapter{ 
       .... 
        @Bean 
         public ViewResolver viewResolver(){ 
         InternalResourceViewResolver viewResolver = new InternalResourceViewResolver(); 
         viewResolver.setPrefix("/WEB-INF/views/"); 
         viewResolver.setSuffix(".jsp"); 
         return viewResolver; 
     } 
      .... 
   } 

如前代码所示,假设控制器返回逻辑视图名称,accountDetails。所有视图的 JSP 文件都放置在 Web 应用程序的/WEB-INF/views/目录中。accountDetails.jsp视图文件用于显示账户详情。根据前面的配置文件,实际视图名称是通过将前缀/WEB-INF/views/和后缀.jsp添加到应用程序控制器返回的逻辑视图名称中得到的。如果应用程序控制器返回accountDetails作为逻辑视图名称,那么ViewResolver通过添加前缀和后缀将其转换为物理视图名称;最后,在我们的应用程序中变为/WEB-INF/views/accountDetails.jsp。以下图示说明了 Spring MVC 的前端控制器如何在 Spring Web 应用程序中解析视图:

最后一个图示展示了 Spring MVC 请求流程的全貌,包括 MVC 模式的所有组件(模型视图控制器)以及前端控制器模式。任何请求,无论是 HTTP GET还是POST,首先到达前端控制器,实际上在 Spring MVC 中是DispatcherServlet。Spring Web 应用程序中的控制器负责生成和更新模型,而模型是 MVC 模式中的另一个组件。最后,控制器将模型以及逻辑视图名称返回给DispatcherServlet。它将与配置的视图解析器协商,解析视图的物理路径。视图是 MVC 模式中的另一个组件。

在下一节中,我们将详细阐述视图助手模式,以及 Spring 如何在 Spring Web 应用程序中支持该模式。

视图助手模式

视图助手模式将静态视图,如 JSP,与业务模型数据的处理分离。视图助手模式通过适配模型数据和视图组件在表示层中使用。视图助手可以根据业务需求格式化模型数据,但不能为业务生成模型数据。以下图示说明了视图助手模式:

我们知道视图是 MVC 模式中的一个静态和格式化的组件,但有时我们需要在表示层进行一些业务处理。如果你使用 JSP,那么你可以在视图层使用脚本片段进行业务处理,但使用脚本片段并不是最佳实践,因为它促进了视图和业务逻辑之间的紧密耦合。但是,一些基于视图助手模式的视图助手类接管了在表示层进行业务处理的职责。基于视图助手模式的一些技术包括以下内容:

  • JavaBeans View 助手

  • LibraryView 助手标签

    • 使用 JSTL 标签

    • 使用 spring 标签

    • 使用第三方标签库

在本章中,我们使用的以下标签库:

    <%@ taglib prefix = "c" uri = "http://java.sun.com/jsp/jstl/core" %> 
    <c:if test="${not empty account }"> 
     .... 
     ....         
    </c:if> 

    <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> 
    <form:form method="post" commandName="account"> 
    .... 
    ... 
   </form:form> 

如前代码所示,我使用了 JSTL 标签库来检查模型中的账户是否为空,并使用 Spring 标签库在 Web 应用程序中创建开户表单。

在下一节中,你将了解组合视图模式,以及 Spring MVC 如何支持它在 Web 应用程序中实现。

使用 Apache tile 视图解析器的组合视图模式

在 Web 应用程序中,视图是最重要的组件之一。开发这个组件并不像看起来那么简单。维护它非常复杂,是一项艰巨的任务。每次我们为 Web 应用程序创建视图时,我们总是关注视图组件的可重用性。我们可以定义一些静态模板,这些模板可以在同一 Web 应用程序的其他视图页面中重用。根据 GOF 模式的组合设计模式,我们为特定的视图组件组合子视图组件。组合视图模式促进了视图的可重用性,由于有多个子视图而不是创建一个庞大而复杂的视图,因此易于维护。以下图表说明了组合视图模式:

如前图所示,我们可以创建多个子视图来在 Web 应用程序中创建视图,并且这些子视图将在整个 Web 应用程序中重用。

Spring MVC 通过 SiteMesh 和 Apache Tiles 等框架提供对组合视图模式实现的支持。在这里,我们将使用 Spring MVC 应用程序来探索 Apache Tiles。让我们看看如何配置 Apache Tiles 的 ViewResolver

配置 Tiles 视图解析器

让我们在 Spring MVC 应用程序中配置 Apache Tiles。为了配置它,我们必须在 Spring 配置文件中配置两个 Bean,如下所示:

    package com.packt.patterninspring.chapter10.bankapp.web.mvc; 
    ..... 
    @Configuration 
    @ComponentScan(basePackages = {"com.packt.patterninspring.chapter10.bankapp.web.controller"})       
    @EnableWebMvc 
    public class SpringMvcConfig extends WebMvcConfigurerAdapter{ 
    ..... 
    @Bean 
    public TilesConfigurer tilesConfigurer() { 
         TilesConfigurer tiles = new TilesConfigurer(); 
         tiles.setDefinitions(new String[] { 
               "/WEB-INF/layout/tiles.xml" 
         }); 
         tiles.setCheckRefresh(true); 
         return tiles; 
    } 

    @Bean 
    public ViewResolver viewResolver() { 
         return new TilesViewResolver(); 
    } 
     ... 
   } 

在此先前的配置文件中,我们配置了两个 bean,TilesConfigurerTilesViewResolver bean。第一个 bean,TilesConfigurer,负责定位和加载 tile 定义,并且通常协调 Tiles。第二个 bean,TilesViewResolver,负责将逻辑视图名称解析为 tile 定义。应用程序中的tiles.xml XML 文件包含了 tile 定义。让我们看看以下代码,用于 tiles 配置文件:

    <tiles-definitions> 
      <definition name="base.definition" template="/WEB-INF/views/mainTemplate.jsp"> 
        <put-attribute name="title" value=""/> 
        <put-attribute name="header" value="/WEB-INF/views/header.jsp"/> 
        <put-attribute name="menu" value="/WEB-INF/views/menu.jsp"/> 
        <put-attribute name="body" value=""/> 
        <put-attribute name="footer" value="/WEB-INF/views/footer.jsp"/> 
      </definition> 

      <definition extends="base.definition" name="openAccountForm"> 
        <put-attribute name="title" value="Account Open Form"/> 
        <put-attribute name="body" value="/WEB-INF/views/accountForm.jsp"/> 
      </definition> 

      <definition extends="base.definition" name="accountsList"> 
        <put-attribute name="title" value="Employees List"/> 
        <put-attribute name="body" value="/WEB-INF/views/accounts.jsp"/> 
      </definition> 
      ... 
      ... 
    </tiles-definitions> 

在先前的代码中,<tiles-definitions>元素有多个<definition>元素。每个<definition>元素定义一个 tile,每个 tile 引用一个 JSP 模板。一些<definition>元素扩展了基本 tile 定义,因为基本 tile 定义具有网络应用程序中所有视图的通用布局。

让我们看看基本定义模板,即mainTemplate.jsp

    <%@ taglib uri="http://www.springframework.org/tags" prefix="s" %> 
    <%@ taglib uri="http://tiles.apache.org/tags-tiles" prefix="t" %> 
    <%@ page session="false" %> 
    <html>   
      <head>   
        <title> 
          <tiles:insertAttribute name="title" ignore="true"/> 
        </title> 
      </head> 
      <body> 
         <table border="1″ cellpadding="2″ cellspacing="2″ align="left"> 
               <tr> 
                     <td colspan="2″ align="center"> 
                           <tiles:insertAttribute name="header"/> 
                     </td> 
               </tr> 
               <tr> 
                     <td> 
                           <tiles:insertAttribute name="menu"/> 
                     </td> 
                     <td> 
                           <tiles:insertAttribute name="body"/> 
                     </td> 
               </tr> 
               <tr> 
                     <td colspan="2″  align="center"> 
                           <tiles:insertAttribute name="footer"/> 
                     </td> 
               </tr> 
         </table> 
       </body>   
    </html> 

在此先前的 JSP 文件中,我使用了tiles标签库中的<tiles:insertAttribute> JSP 标签来插入其他模板。

现在我们来看看一些用于设计和开发网络应用程序的最佳实践。

网络应用程序设计的最佳实践

在设计和开发网络应用程序时,以下是一些必须考虑的最佳实践:

  • 由于 Spring DI 模式和 Spring 的非常灵活的 MVC 模式,Spring MVC 是设计和开发网络应用程序的最佳选择。Spring 的DispatcherServlet也非常灵活和可定制。

  • 在任何使用 MVC 模式的网络应用程序中,前端控制器应该是通用的,并且尽可能轻量。

  • 在网络应用程序的各个层级之间保持清晰的关注点分离非常重要。分离层级可以改善应用程序的整洁设计。

  • 如果应用层与其他层有太多的依赖关系,作为一个最佳做法,引入另一个层以减少该层的依赖性。

  • 永远不要将 DAO 对象注入到网络应用程序的控制器中;始终将服务对象注入到控制器中。DAO 对象必须通过服务层注入,以便服务层与数据访问层通信,而表示层与服务层通信。

  • 应用层,如服务、DAO 和表示层,必须是可插拔的,并且不能与实现绑定,也就是说,使用接口可以减少与具体实现的实际耦合,因为我们知道松耦合的分层应用程序更容易测试和维护。

  • 强烈建议将 JSP 文件放置在 WEB-INF 目录中,因为这个位置不会被任何客户端访问。

  • 总是在 JSP 文件中指定命令对象的名字。

  • JSP 文件不得有任何业务逻辑和业务处理。对于此类需求,我们强烈建议使用视图辅助类,如标签、库、JSTL 等。

  • 从基于模板的视图(如 JSP)中移除编程逻辑。

  • 创建可重用的组件,这些组件可以用于在视图之间组合模型数据。

  • MVC 模式的每个组件都必须有一个一致的行为,这是 MVC 引入它的原因。这意味着控制器应该遵循单一职责原则。控制器只负责委托业务逻辑调用和视图选择。

  • 最后,保持配置文件命名的统一性。例如,控制器、拦截器和视图解析器等 Web 组件必须在单独的配置文件中定义。其他应用程序组件,如服务、存储库等,必须定义在另一个单独的文件中。同样,对于安全考虑也是如此。

概述

在本章中,您已经了解到 Spring 框架如何让您开发一个灵活且松散耦合的基于 Web 的应用程序。Spring 在您的 Web 应用程序中使用了注解来实现接近 POJO 的开发模型。您了解到,使用 Spring MVC,您可以通过开发处理请求的控制器来创建基于 Web 的应用程序,而这些控制器非常容易测试。在本章中,我们介绍了 MVC 模式,包括其起源和解决的问题。Spring 框架实现了 MVC 模式,这意味着对于任何 Web 应用程序,都有三个组件——模型(Model)、视图(View)和控制器(Controller)。

Spring MVC 实现了应用程序控制器和前端控制器模式。Spring 的调度器 Servlet(org.springframework.web.servlet.DispatcherServlet)在基于 Web 的应用程序中充当前端控制器。这个调度器或前端控制器通过使用处理器映射将所有请求路由到应用程序控制器。在 Spring MVC 中,控制器类具有极其灵活的请求处理方法。这些处理方法处理 Web 应用程序的所有请求。正如我们在本章中解释的那样,有几种处理请求参数的方法。@RequestParam注解是处理请求参数的一种方法,而且在不使用测试用例中的 http 请求对象的情况下也非常容易测试。

在本章中,我们探讨了请求处理工作流程,并讨论了在这个工作流程中扮演角色的所有组件。DispatcherServlet可以被认为是 Spring MVC 中的主要组件;它在 Spring MVC 中扮演前端控制器的角色。另一个主要组件是视图解析器,它负责将模型数据渲染到任何视图模板,如 JSP、Thymeleaf、FreeMarker、velocity、pdf、xml 等,这取决于 Web 应用程序中配置的视图解析器。Spring MVC 为几种视图技术提供了支持,但在这个章节中,我们简要地介绍了如何使用 JSP 为您的控制器编写视图。我们还可以使用 Apache tiles 添加一致的布局到您的视图中。

最后,我们讨论了 Web 应用架构,并探讨了 Web 应用中的不同层次,如领域、用户界面、Web、服务和数据访问。我们创建了一个小型银行管理 Web 应用,并将其部署到 Tomcat 服务器上。

第十一章:实现响应式设计模式

在本章中,我们将探讨 Spring 5 框架最重要的特性之一,即响应式模式编程。Spring 5 框架通过 Spring Web Reactive 模块引入了这一新特性。我们将在本章中讨论这个模块。在此之前,让我们先了解一下响应式模式。什么是响应式模式,为什么它在当今越来越受欢迎?我将从微软公司首席执行官萨蒂亚·纳德拉的以下声明开始我的响应式模式讨论:

现在的每一家企业都是软件公司,都是数字公司。

我们在这里将要讨论的主题如下:

  • 为什么选择响应式模式?

  • 响应式模式原则

  • 阻塞调用

  • 非阻塞调用

  • 反压

  • 使用 Spring 框架实现响应式模式

  • Spring Web Reactive 模块

多年来理解应用需求

如果你回顾 10 到 15 年前,互联网用户非常少,与今天的在线门户相比,用户在线的门户网站也少得多。如今,我们无法想象没有电脑或没有任何在线系统的生活。简而言之,我们在个人和商业用途上对电脑和在线计算变得极其依赖。每一个商业模式都在向数字化转变。印度总理纳伦德拉·莫迪先生启动了“数字印度”运动,以确保通过改进在线基础设施、增加互联网连接以及使国家在技术领域数字化,政府的服务能够以电子方式提供给公民。

所有这些都意味着互联网用户数量正在急剧增加。根据爱立信移动性报告,

预计到 2018 年,物联网(IoT)将超过移动电话,成为最大的连接设备类别。

移动互联网用户数量增长迅速,且没有迹象表明这种增长会很快放缓。在这些领域,根据定义,服务器端必须同时处理数百万个连接设备。以下表格比较了现在的基础设施和应用需求与 10 年前的需求:

需求 现在 十年前
服务器节点 需要超过 1000 个节点。 10 个节点就足够了。
响应时间 服务请求并返回响应需要毫秒级时间。 响应需要几秒钟。
维护停机时间 目前,不需要或零维护停机时间。 需要数小时的维护停机时间。
数据量 当前应用程序的数据量从 PBs 增加到 TBs。 数据量在 GBs。

你可以在前一张表中看到资源需求的变化。这些需求增加了,因为我们现在期望在秒内立即得到响应。同时,分配给计算机的任务复杂性也增加了。这些任务不仅仅是数学意义上的纯计算,还包括从大量数据中请求响应。因此,现在我们必须通过设计单台计算机的形式为多核 CPU,可能是在多插槽服务器中组合,来关注这类系统的性能。我们首先想到的是使系统保持响应。这是响应式特性的第一个——响应性。我们将在本章中进一步探讨这一点,以及以下主题:

  • 为什么选择响应式模式

  • 响应式模式原则

  • 阻塞调用

  • 非阻塞调用

  • 反压

  • 使用 Spring 框架实现响应式模式

  • Spring Web 响应式模块

  • 在服务器端实现响应式

  • 在客户端实现响应式

  • 请求和响应体类型转换

本章将教你如何在面对任何可变负载、部分故障、程序失败等情况时使系统保持响应。如今,系统被分布在不同节点上以高效地处理请求。

让我们详细探讨上述主题。

理解响应式模式

现在,现代应用必须更加健壮、更加弹性、更加灵活,并且更好地满足组织的需求,因为,在最近几年里,应用的需求发生了巨大的变化。正如我们在上一张表中看到的,10 到 15 年前,一个大型应用有 10 个服务器节点,处理请求的响应时间在秒级,我们需要的维护和部署停机时间大约是几个小时,数据量在千兆级别。但今天,一个应用需要数千个服务器节点,因为它可以通过多个渠道如移动设备访问。服务器响应时间预期在毫秒级,部署和维护的停机时间接近于 0%。数据量已从千兆增长到拍字节。

十年以上的系统无法满足当今应用的需求;我们需要一个能够在应用层或系统层满足所有用户需求的系统,这意味着我们需要一个响应式的系统。响应性是响应式模式的一个特性。我们希望系统必须具有响应性、弹性、弹性和消息驱动性。我们把这些系统称为响应式系统。这些系统更加灵活、松散耦合且可扩展。

系统必须对故障做出反应并保持可用性,即它应该是弹性的,并且系统必须对可变负载条件做出反应,而不会过载。系统应该对事件做出反应——无论是事件驱动的还是基于消息的。如果所有这些属性都与一个系统相关联,那么它将是响应式的,也就是说,如果一个系统对其用户做出反应,它就是响应式的。要创建一个响应式系统,我们必须关注系统级和应用级。让我们首先看看所有响应式特性。

响应式模式特性

以下是对响应式模式的原则:

  • 响应式:这是今天每个应用程序的目标。

  • 弹性:这是使应用程序响应式所必需的。

  • 可伸缩性:这也是使应用程序响应式所必需的;没有弹性和可伸缩性,就不可能实现响应性。

  • 基于消息的:基于消息的架构是可伸缩和弹性的应用程序的基础,最终,它使系统响应式。基于消息的架构可以是基于事件驱动的或基于演员的编程模型。

上述提到的点是响应式模式的核心原则。让我们详细探讨响应式模式的每个原则,并了解为什么所有这些原则必须一起应用,才能在现代应用环境中构建一个具有高质量软件的响应式系统,该系统能在毫秒内处理数百万个并行请求而不会出现任何故障。让我们首先通过以下图表来理解这些原则:

图片

如前图所示,为了使系统响应式,我们需要可伸缩性和弹性。为了使系统可伸缩和弹性,我们需要应用程序的事件驱动或基于消息的架构。最终,这些原则,可伸缩性、弹性和事件驱动架构使系统能够对客户端做出响应。让我们详细看看这些特性。

响应性

当我们说一个系统或应用程序是响应式的,这意味着该应用程序或系统在所有条件下都能在给定时间内快速响应用户,无论是处于良好状态还是不良状态。这确保了用户体验的一致性和积极性。

对于一个系统来说,响应性是必需的,以确保可用性和实用性。一个响应性系统意味着在系统发生故障时,无论是由于外部系统还是流量激增,故障都能迅速检测到,并在短时间内得到有效处理,而用户不会意识到故障。最终用户必须能够通过提供快速和一致的反应时间与系统交互。用户在与系统交互时不应遇到任何故障,并且系统必须向用户提供一致的服务质量。这种一致的行为解决了故障,并在系统中建立了最终用户的信心。在各种条件下,快速性和积极的用户体验使系统具有响应性。这取决于反应式应用程序或系统的另外两个特征,即弹性和可伸缩性。另一个特征,即事件驱动或消息驱动架构,为响应性系统提供了整体基础。以下图展示了响应性系统:

图片

正如你可以在前面的图中看到的那样,一个响应性系统依赖于系统的弹性和可伸缩性,而这些又依赖于其事件驱动架构。让我们看看反应式应用程序的其他特征。

弹性

当我们设计和开发一个系统时,我们必须考虑所有条件——无论是好是坏。如果我们只考虑好的条件,那么我们可能会实施一个系统,这个系统在短短几天后就会失败。主要的应用程序故障会导致停机、数据丢失,并损害你在市场上的应用程序声誉。

因此,我们必须关注每一个条件,以确保应用程序在所有条件下都具有响应性。这样的系统或应用程序被称为弹性系统。

每个系统都必须具有弹性以确保响应性。如果一个系统不具备弹性,那么在发生故障后它将无法响应。因此,一个系统在面临故障时也必须具有响应性。在整个系统中,故障可能存在于应用程序或系统的任何组件中。因此,系统中的每个组件都必须相互隔离,以便在组件故障时,我们可以恢复它而不会影响整个系统。单个组件的恢复是通过复制实现的。如果一个系统具有弹性,那么它必须具备复制、遏制、隔离和委派。看看下面的图,它说明了反应式应用程序或系统的弹性特征:

图片

正如你可以在前面的图中看到的那样,通过复制、遏制、隔离和委派来实现弹性。让我们详细讨论这些点:

  • 复制:这确保了在组件故障时,在必要时提供高可用性。

  • 隔离:这意味着每个组件的失败必须被隔离,这通过尽可能多地解耦组件来实现。隔离对于系统自我修复是必要的。如果你的系统已经实现了隔离,那么你可以轻松地测量每个组件的性能,并检查内存和 CPU 的使用情况。此外,一个组件的失败不会影响整个系统或应用程序的响应性。

  • 包容:解耦的结果是包含失败。这有助于避免整个系统出现故障。

  • 委托:在失败后,每个组件的恢复委托给另一个组件。只有在我们的系统是可组合的情况下才可能实现。

现代应用程序不仅依赖于内部基础设施,还通过网络协议与其他网络服务集成。因此,我们的应用程序必须在核心上具有弹性,以便在各种现实条件下的相反条件下保持响应性。我们的应用程序不仅需要在应用层面上具有弹性,还需要在系统层面上具有弹性。

让我们看看反应式模式的另一个原则。

可扩展

弹性和可扩展性共同使系统持续响应。可扩展的系统或弹性的系统可以在不同的工作负载下轻松升级。通过增加和减少分配给处理这些输入的资源,可以按需使反应式系统可扩展。它通过提供与应用程序可扩展性相关的实时性能来支持多种扩展算法。我们可以通过使用成本效益高的软件和廉价的基础硬件(例如,云)来实现可扩展性。

一个应用程序如果可以根据其使用情况进行扩展,则被认为是可扩展的,以下是一些扩展方式:

  • 向上扩展:它利用了多核系统中的并行性。

  • 向外扩展:它利用了多服务器节点。位置透明性和弹性对于这一点很重要。

最小化共享可变状态对于可扩展性非常重要。

弹性和可扩展性是相同的!可扩展性主要关于高效使用已存在的资源,而弹性则是当系统需求变化时,根据需要向应用程序添加新资源。因此,最终,系统可以通过使用现有资源或向系统添加新资源来保持响应性。

让我们看看具有弹性和可扩展性的最终基础,即消息驱动架构。

消息驱动架构

消息驱动架构是响应式应用程序的基础。消息驱动应用程序可以是事件驱动和基于 actor 的应用程序。它也可以是这两种架构的组合——事件驱动和基于 actor 架构。

在事件驱动架构中,事件和事件观察者扮演主要角色。事件发生,但不是指向特定地址;事件监听器监听这些事件,并采取行动。但在消息驱动架构中,消息有适当的指向目的地的方向。让我们看看以下图解消息驱动和事件驱动架构的图:

图片

正如您在前面的图中可以看到,在事件驱动架构中,如果发生事件,则监听器会监听它。但在消息驱动架构中,一个生成的消息通信有一个可寻址的接收者和单一目的。

异步消息驱动架构通过在组件之间建立限制,作为反应式系统的基础。它确保了松散耦合、隔离和位置透明性。组件之间的隔离完全取决于它们之间的松散耦合。隔离和松散耦合构成了弹性和恢复力的基础。

一个大型系统由多个组件组成。这些组件要么有较小的应用,要么可能具有反应性。这意味着反应式设计原则必须应用于所有规模级别,以使大型系统可组合。

传统上,大型系统由多个线程组成,这些线程通过共享同步状态进行通信。它往往具有强耦合性,难以组合,并且也倾向于阻塞阶段。但现在,所有大型系统都是由松散耦合的事件处理器组成的。事件可以异步处理而不阻塞。

让我们看看阻塞和非阻塞编程模型。

用非常简单的术语来说,反应式编程完全是关于非阻塞应用程序,这些应用程序是异步和事件驱动的,并且需要少量线程进行垂直扩展,而不是水平扩展。

阻塞调用

在一个系统中,一个调用可能正在占用资源,而其他调用正在等待相同的资源。这些资源在另一个完成使用后释放。

让我们来谈谈技术术语——实际上,阻塞调用意味着应用程序或系统中一些需要较长时间完成的操作,例如使用阻塞驱动器的文件 I/O 操作和数据库访问。以下是在系统中 JDBC 操作的阻塞调用图:

图片

正如您在前面的图中可以看到,这里用红色显示的阻塞操作是用户调用 servlet 获取数据,然后移动到与数据库服务器的 JDBC 和 DB 连接。直到那时,当前线程等待来自数据库服务器的结果集。如果数据库服务器有延迟,那么等待时间可能会增加。这意味着线程执行依赖于数据库服务器的延迟。

让我们看看如何使这成为一个非阻塞执行。

非阻塞调用

程序的非阻塞执行意味着线程在竞争资源时不需要等待。资源的非阻塞 API 允许在不需要等待阻塞调用(如数据库访问和网络调用)的情况下调用资源。如果调用时资源不可用,则它将转到其他工作,而不是等待阻塞资源。当阻塞资源可用时,系统会得到通知。

查看以下图表,它展示了如何通过非阻塞线程执行来访问数据的 JDBC 连接:

图片

如前图所示,线程执行不会等待来自数据库服务器的结果集。线程为数据库服务器创建数据库连接和 SQL 语句。如果数据库服务器在响应中存在延迟,则线程会继续执行其他工作,而不是被阻塞等待资源可用。

背压

在过载条件下,响应式应用程序永远不会放弃。背压是响应式应用程序的一个关键方面。它是一种确保响应式应用程序不会压倒消费者的机制。它测试响应式应用程序的各个方面。它测试系统在任何负载下都能优雅地响应。

背压机制确保系统在负载下具有弹性。在背压条件下,系统通过应用其他资源来帮助分配负载,从而使自己可扩展。

到目前为止,我们已经看到了响应式模式的原则;这些是使系统在晴朗或阴霾的天空下都能响应的必要条件。让我们看看,在下一节中 Spring 5 如何实现响应式编程。

使用 Spring 5 框架实现响应式

Spring 框架最新版本最突出的特性是新的响应式堆栈 Web 框架。响应式是带我们走向未来的更新。随着每一天的过去,这个技术领域越来越受欢迎,这就是为什么 Spring 框架 5.0 被推出时具有响应式编程的能力。这一新增功能使得 Spring 框架的最新版本便于以事件循环风格处理,这允许使用少量线程进行扩展。

Spring 5 框架通过内部使用 reactor 来实现其自身的响应式支持,从而实现了响应式编程模式。Reactor 是一个扩展了基本 Reactive Streams 的响应式流实现。Twitter 通过使用 Reactive Streams 实现了响应式传递。

响应式流

Reactive Streams 提供了一种异步流处理的协议或规则,具有非阻塞的反馈压力。这个标准也被 Java 9 以 java.util.concurrent.Flow 的形式采用。Reactive Streams 由四个简单的 Java 接口组成。这些接口是 PublisherSubscriberSubscriptionProcessor。但 Reactive Streams 的主要目标是处理反馈压力。如前所述,反馈压力是一个允许接收器从发射器询问数据量的过程。

您可以使用以下 Maven 依赖项在您的应用程序开发中添加 Reactive Streams:

    <dependency> 
      <groupId>org.reactivestreams</groupId> 
      <artifactId>reactive-streams</artifactId> 
      <version>1.0.1</version> 
   </dependency> 
   <dependency> 
      <groupId>org.reactivestreams</groupId> 
      <artifactId>reactive-streams-tck</artifactId> 
      <version>1.0.1</version> 
   </dependency> 

前面的 Maven 依赖代码为您的应用程序添加了所需的 Reactive Streams 库。在接下来的章节中,我们将看到 Spring 如何在 Spring 和 Spring MVC 框架的 Web 模块中实现 Reactive Streams。

Spring Web 反应式模块

截至 Spring 5.0 框架,Spring 引入了一个新的反应式编程模块——spring-web-reactive 模块。它基于 Reactive Streams。基本上,此模块使用具有反应式编程的 Spring MVC 模块,因此,您仍然可以在 Web 应用程序中使用 Spring MVC 模块,无论是单独使用还是与 spring-web-reactive 模块一起使用。

这个新的模块在 Spring 5.0 框架中包含了对基于反应式-web-functional 编程模型的支持。它还支持基于注解的编程模型。Spring-web-reactive 模块包含对反应式 HTTP 和 WebSocket 客户端调用反应式服务器应用程序的支持。它还使反应式 Web 客户端能够与反应式 Web 应用程序建立反应式 HTTP 连接。

下面的图显示了具有赋予 Spring Web 应用程序反应性行为的组件的 Spring-web-reactive 模块:

图片

如您在前面图中所见,有两个并行模块——一个用于传统的 Spring MVC 框架,另一个用于 Spring-reactive Web 模块。图左侧是 Spring-MVC 相关组件,如 @MVC 控制器、spring-web-mvc 模块Servlet API 模块Servlet 容器。图右侧是 spring-web-reactive 相关组件,如 Router Functions、spring-web-reactive 模块、HTTP/Reactive Streams、Tomcat 的反应式版本等。Spring-web-reactive 相关组件,如 Router Functionsspring-web-reactive 模块HTTP/Reactive Streams、Tomcat 的反应式版本等。

在前面的图中,您必须关注模块的位置。同一级别的每个模块都有传统 Spring MVC 和 Spring-web-reactive 模块的比较。这些比较如下所示:

  • 在 Spring Web 反应式模块中,路由函数与 Spring MVC 模块中的 @Controller@RestController@RequestMapping 注解类似。

  • Spring-web-reactive 模块与 Spring-web-MVC 模块并行。

  • 在传统的 Spring MVC 框架中,我们使用 Servlet API 在 Servlet 容器中为 HttpServletRequestHttpServletResponse。但在 Spring-web-reactive 框架中,我们使用 HTTP/Reactive Streams,在 tomcat 服务器的反应式支持下创建 HttpServerRequestHttpServerResponse

  • 我们可以使用 Servlet 容器来支持传统的 Spring MVC 框架,但对于 Spring-web-reactive 应用程序则需要一个支持反应式的服务器。Spring 提供了对 Tomcat、Jetty、Netty 和 Undertow 的支持。

在 第十章 中,使用 Spring 在 Web 应用程序中实现 MVC 模式,您学习了如何使用 Spring MVC 模块实现 Web 应用程序。现在让我们看看如何通过使用 Spring Web 反应式模块来实现反应式 Web 应用程序。

在服务器端实现反应式 Web 应用程序

Spring 反应式 Web 模块支持两种编程模型——基于注解或基于函数的编程模型。让我们看看这些模型在服务器端是如何工作的:

  • 基于注解的编程模型:它基于 MVC 注解,如 @Controller@RestController@RequestMapping 等。Spring MVC 框架支持这些注解,用于在 Web 应用程序中进行服务器端编程。

  • 函数式编程模型:这是 Spring 5 框架支持的一种新的编程范式。它基于 Java 8 Lambda 风格的路线和处理。Scala 也提供了函数式编程范式。

以下是我们必须为基于 Spring Boot 的反应式 Web 应用程序添加的 Maven 依赖项:

    <parent> 
       <groupId>org.springframework.boot</groupId> 
       <artifactId>spring-boot-starter-parent</artifactId> 
       <version>2.0.0.M3</version> 
       <relativePath/> <!-- lookup parent from repository --> 
    </parent> 

    <properties> 
       <project.build.sourceEncoding>UTF-
        8</project.build.sourceEncoding> 
       <project.reporting.outputEncoding>UTF
        -8</project.reporting.outputEncoding> 
       <java.version>1.8</java.version> 
    </properties> 

    <dependencies> 
       <dependency> 
          <groupId>org.springframework.boot</groupId> 
          <artifactId>spring-boot-starter-webflux</artifactId> 
       </dependency> 

       <dependency> 
          <groupId>org.springframework.boot</groupId> 
          <artifactId>spring-boot-starter-test</artifactId> 
          <scope>test</scope> 
       </dependency> 
       <dependency> 
          <groupId>io.projectreactor</groupId> 
          <artifactId>reactor-test</artifactId> 
          <scope>test</scope> 
       </dependency> 
    </dependencies> 

如您在先前的 Maven 依赖配置文件中所见,我们已将 spring-boot-starter-webfluxreactor-test 依赖项添加到应用程序中。

让我们基于基于注解的编程模型创建一个反应式 Web 应用程序。

基于注解的编程模型

您可以使用在 第十章 中使用过的相同注解,使用 Spring 在 Web 应用程序中实现 MVC 模式。Spring MVC 的 @Controller@RestController 注解在反应式端也得到了支持。到目前为止,传统 Spring MVC 和带有反应模块的 Spring Web 之间没有区别。真正的区别始于 @Controller 注解配置声明之后,即当我们深入到 Spring MVC 的内部工作,从 HandlerMappingHandlerAdapter 开始。

传统 Spring MVC 和 Spring Web Reactive 在请求处理机制上的主要区别在于。Spring MVC 无反应式处理请求时使用 Servlet API 的阻塞HttpServletRequestHttpServletResponse接口,但 Spring Web Reactive 框架是非阻塞的,它操作于反应式的ServerHttpRequestServerHttpResponse而不是HttpServletRequestHttpServletResponse

让我们看看以下使用反应式控制器的示例:

    package com.packt.patterninspring.chapter11.
      reactivewebapp.controller; 

    import org.reactivestreams.Publisher; 
    import org.springframework.beans.factory.annotation.Autowired; 
    import org.springframework.web.bind.annotation.GetMapping; 
    import org.springframework.web.bind.annotation.PathVariable; 
    import org.springframework.web.bind.annotation.PostMapping; 
    import org.springframework.web.bind.annotation.RequestBody; 
    import org.springframework.web.bind.annotation.RestController; 

    import com.packt.patterninspring.chapter11.
      reactivewebapp.model.Account; 
    import  com.packt.patterninspring.chapter11.
      reactivewebapp.repository.AccountRepository; 

    import reactor.core.publisher.Flux; 
    import reactor.core.publisher.Mono; 

    @RestController 
    public class AccountController { 

      @Autowired 
      private AccountRepository repository; 

      @GetMapping(value = "/account") 
      public Flux<Account> findAll() { 
        return repository.findAll().map(a -> new 
          Account(a.getId(), a.getName(),
           a.getBalance(), a.getBranch())); 
      } 

      @GetMapping(value = "/account/{id}") 
      public Mono<Account> findById(@PathVariable("id") Long id) { 
        return repository.findById(id) 
         .map(a -> new Account(a.getId(), a.getName(), a.getBalance(),
            a.getBranch())); 
      } 

      @PostMapping("/account") 
      public Mono<Account> create(@RequestBody 
        Publisher<Account> accountStream) { 
        return repository 
          .save(Mono.from(accountStream) 
          .map(a -> new Account(a.getId(), a.getName(), a.getBalance(),
             a.getBranch()))) 
          .map(a -> new Account(a.getId(), a.getName(), a.getBalance(),
             a.getBranch())); 
      } 
    } 

如您在前面AccountController.java控制器的代码中看到的,我使用了相同的 Spring MVC 注解,如@RestController来声明控制器类,@GetMapping@PostMapping分别用于创建GETPOST请求方法的请求处理方法。

让我们关注处理方法的返回类型。这些方法以MonoFlux类型返回值。这些都是由 Reactor 框架提供的反应式流类型。此外,处理方法使用发布者类型获取请求体。

Reactor 是由 Pivotal 开源团队开发的 Java 框架。它直接建立在反应式流之上,因此不需要桥接。Reactor IO 项目提供了对低级网络运行时(如 Netty 和 Aeron)的包装。根据 David Karnok 的反应式编程代际分类,Reactor 是“第四代”库。

让我们看看使用函数式编程模型来处理请求的相同控制器类。

函数式编程模型

函数式编程模型使用具有函数式接口(如RouterFunctionHandlerFunction)的 API。它使用 Java 8 Lambda 风格编程,通过路由和请求处理来代替 Spring MVC 注解。它们是创建 Web 应用程序的简单但强大的构建块。

以下是一个函数式请求处理的示例:

    package com.packt.patterninspring.chapter11.web.reactive.function; 

    import static org.springframework.http.MediaType.APPLICATION_JSON; 
    import static org.springframework.web.reactive.
      function.BodyInserters.fromObject; 

    import org.springframework.web.reactive.
      function.server.ServerRequest; 
    import org.springframework.web.reactive.
      function.server.ServerResponse; 

    import com.packt.patterninspring.chapter11.
      web.reactive.model.Account; 
    import com.packt.patterninspring.chapter11.
      web.reactive.repository.AccountRepository; 

    import reactor.core.publisher.Flux; 
    import reactor.core.publisher.Mono; 

    public class AccountHandler { 

      private final AccountRepository repository; 

      public AccountHandler(AccountRepository repository) { 
         this.repository = repository; 
      } 

      public Mono<ServerResponse> findById(ServerRequest request) { 
        Long accountId = Long.valueOf(request.pathVariable("id")); 
        Mono<ServerResponse> notFound = 
          ServerResponse.notFound().build(); 
        Mono<Account> accountMono =
         this.repository.findById(accountId); 
        return accountMono 
         .flatMap(account ->    ServerResponse.ok().contentType
         (APPLICATION_JSON).body(
            fromObject(account))) 
         .switchIfEmpty(notFound); 
      }  

      public Mono<ServerResponse> findAll(ServerRequest request) { 
       Flux<Account> accounts = this.repository.findAll(); 
       return ServerResponse.ok().contentType
       (APPLICATION_JSON).body(accounts, 
         Account.class); 
      } 

      public Mono<ServerResponse> create(ServerRequest request) { 
        Mono<Account> account = request.bodyToMono(Account.class); 
        return  ServerResponse.ok().build(this.
        repository.save(account)); 
      } 
    } 

在前面的代码中,类文件AccountHandler.java基于函数式反应式编程模型。在这里,我使用了 Reactor 框架来处理请求。使用了两个函数式接口ServerRequestServerResponse来处理请求并生成响应。

让我们看看这个应用程序的仓储类。下面的AccountRepositoryAccountRepositoryImpl类对于两种应用程序类型——基于注解和基于函数式编程模型——都是相同的。

让我们创建一个接口AccountRepository.java类,如下所示:

    package com.packt.patterninspring.chapter11.
      reactivewebapp.repository; 

    import com.packt.patterninspring.chapter11.
      reactivewebapp.model.Account; 

    import reactor.core.publisher.Flux; 
    import reactor.core.publisher.Mono; 

    public interface AccountRepository { 

      Mono<Account> findById(Long id); 

      Flux<Account> findAll(); 

      Mono<Void> save(Mono<Account> account); 
    }

前面的代码是一个接口,让我们用AccountRepositoryImpl.java类来实现这个接口,如下所示:

    package com.packt.patterninspring.chapter11.
      web.reactive.repository; 

    import java.util.Map; 
    import java.util.concurrent.ConcurrentHashMap; 

    import org.springframework.stereotype.Repository; 

    import com.packt.patterninspring.chapter11.web.
      reactive.model.Account; 

    import reactor.core.publisher.Flux; 
    import reactor.core.publisher.Mono; 

    @Repository 
    public class AccountRepositoryImpl implements AccountRepository { 

      private final Map<Long, Account> accountMap = new 
      ConcurrentHashMap<>();  

      public AccountRepositoryImpl() { 
        this.accountMap.put(1000l, new Account(1000l,
        "Dinesh Rajput", 50000l,
          "Sector-1")); 
        this.accountMap.put(2000l, new Account(2000l, 
        "Anamika Rajput", 60000l,
          "Sector-2")); 
        this.accountMap.put(3000l, new Account(3000l, 
        "Arnav Rajput", 70000l,
           "Sector-3")); 
        this.accountMap.put(4000l, new Account(4000l,
       "Adesh Rajput", 80000l,
           "Sector-4")); 
      } 

      @Override 
      public Mono<Account> findById(Long id) { 
        return Mono.justOrEmpty(this.accountMap.get(id)); 
      } 

      @Override 
      public Flux<Account> findAll() { 
        return Flux.fromIterable(this.accountMap.values()); 
      } 

      @Override 
      public Mono<Void> save(Mono<Account> account) { 
        return account.doOnNext(a -> { 
          accountMap.put(a.getId(), a); 
          System.out.format("Saved %s with id %d%n", a, a.getId()); 
        }).thenEmpty(Mono.empty()); 
         // return accountMono; 
      } 
    } 

如前述代码所示,我们创建了AccountRepository类。这个类只有三个方法:findById()findAll()save()。我们根据业务需求实现了这些方法。在这个仓库类中,我特别使用了 Flux 和 Mono 反应类型,使其成为一个基于响应式的应用程序。

让我们为基于函数的编程模型创建服务器。在基于注解的编程中,我们使用简单的 Tomcat 容器来部署 Web 应用程序。但针对这种基于函数的编程,我们必须创建一个 Server 类来启动 Tomcat 服务器或 Reactor 服务器,如下所示:

    package com.packt.patterninspring.chapter11.web.reactive.function; 

    //Imports here 

    public class Server { 

      public static final String HOST = "localhost"; 

      public static final int TOMCAT_PORT = 8080; 
      public static final int REACTOR_PORT = 8181; 

      //main method here, download code for GITHUB 

      public RouterFunction<ServerResponse> routingFunction() { 
         AccountRepository repository = new AccountRepositoryImpl(); 
         AccountHandler handler = new AccountHandler(repository); 

         return nest(path("/account"), nest(accept(APPLICATION_JSON), 
           route(GET("/{id}"), handler::findById) 
           .andRoute(method(HttpMethod.GET), handler::findAll) 
           ).andRoute(POST("/").and(contentType
           (APPLICATION_JSON)), handler::create)); 
      } 

      public void startReactorServer() throws InterruptedException { 
         RouterFunction<ServerResponse> route = routingFunction(); 
         HttpHandler httpHandler = toHttpHandler(route); 

         ReactorHttpHandlerAdapter adapter = new
            ReactorHttpHandlerAdapter(httpHandler); 
         HttpServer server = HttpServer.create(HOST, REACTOR_PORT); 
         server.newHandler(adapter).block(); 
      } 

      public void startTomcatServer() throws LifecycleException { 
         RouterFunction<?> route = routingFunction(); 
         HttpHandler httpHandler = toHttpHandler(route); 

         Tomcat tomcatServer = new Tomcat(); 
         tomcatServer.setHostname(HOST); 
         tomcatServer.setPort(TOMCAT_PORT); 
         Context rootContext = tomcatServer.addContext("",
           System.getProperty("java.io.tmpdir")); 
         ServletHttpHandlerAdapter servlet = new
           ServletHttpHandlerAdapter(httpHandler); 
         Tomcat.addServlet(rootContext, "httpHandlerServlet", servlet); 
         rootContext.addServletMapping("/", "httpHandlerServlet"); 
         tomcatServer.start(); 
      } 
    } 

如前述Server.java类文件所示,我添加了 Tomcat 和 Reactor 服务器。Tomcat 服务器使用端口 8080,但 Reactor 服务器使用端口8181

这个Server.java类有三个方法。第一个方法routingFunction()负责通过AccountHandler类处理客户端请求。它依赖于AccountRepository类。第二个方法startReactorServer()负责通过 Reactor 服务器的ReactorHttpHandlerAdapter类启动 Reactor 服务器。这个类将HttpHandler类的对象作为构造函数参数来创建请求处理器映射。同样,第三个方法startTomcatServer()负责启动 Tomcat 服务器。它通过一个 Reactor 适配器类ServletHttpHandlerAdapter绑定到HttpHandler对象上。

你可以将这个服务器类文件作为 Java 应用程序运行,并通过在浏览器中输入 URL http://localhost:8080/account/ 来查看输出:

图片

你也可以为 Reactor 服务器使用相同的 URL 和端口8181,如下所示,你将得到相同的结果:

http://localhost:8181/account/

在本节中,你学习了如何使用 Spring-web-reactive 模块创建一个响应式 Web 应用程序。我们通过使用两种编程范式:基于注解和基于函数的编程范式来创建 Web 应用程序。

在下一节中,我们将讨论客户端代码,以及客户端如何访问响应式 Web 应用程序。

实现响应式客户端应用程序

Spring 5 框架引入了一个功能和响应式的 WebClient。它是一个完全非阻塞和响应式的 Web 客户端,是RestTemplate的替代品。它以响应式的ClientHttpRequestClientHttpRespones的形式创建网络输入和输出。它以Flux<DataBuffer>的形式创建请求和响应的主体,而不是InputStreamOutputStream

让我们看看创建Client.java类的客户端代码:

    package com.packt.patterninspring.chapter11.web.reactive.function; 

    //Imports here 

    public class Client { 

      private ExchangeFunction exchange = ExchangeFunctions.create(new
        ReactorClientHttpConnector()); 

      public void findAllAccounts() { 
        URI uri = URI.create(String.format("http://%s:%d/account",
        Server.HOST,
          Server.TOMCAT_PORT)); 
        ClientRequest request = ClientRequest.method(HttpMethod.GET,  
        uri).build(); 

        Flux<Account> account = exchange.exchange(request) 
        .flatMapMany(response -> response.bodyToFlux(Account.class)); 

         Mono<List<Account>> accountList = account.collectList(); 
         System.out.println(accountList.block()); 
      } 

      public void createAccount() { 
        URI uri = URI.create(String.format("http://%s:%d/account",
        Server.HOST,
           Server.TOMCAT_PORT)); 
        Account jack = new Account(5000l, "Arnav Rajput", 500000l,
        "Sector-5"); 

        ClientRequest request = ClientRequest.method(HttpMethod.POST,
        uri) 
        .body(BodyInserters.fromObject(jack)).build(); 

        Mono<ClientResponse> response = exchange.exchange(request); 

        System.out.println(response.block().statusCode()); 
      } 
    }   

前面的类Client.javaServer.java的 Web 客户端类。它有两个方法。第一个方法是findAllAccounts()。它从账户仓库中检索所有账户。它使用org.springframework.web.reactive.function.clientClientRequest接口创建一个对http://localhost:8080/account/ URI 的GET http 方法请求。通过使用org.springframework.web.reactive.function.clientExchangeFunction接口,它调用服务器,并以 JSON 格式检索结果。同样,另一个方法createAccount()通过使用带有POST方法的 URI http://localhost:8080/account/在服务器中创建一个新的账户。

让我们运行 Client 类作为 Java 应用程序,并在控制台上查看输出,如下所示:

它创建了一个新记录,并以 JSON 列表的形式检索所有五个记录。

AsyncRestTemplate也支持非阻塞交互。主要区别在于它不能支持非阻塞流,例如 Twitter 流,因为从根本上说,它仍然基于并依赖于InputStreamOutputStream

在下一节中,我们将讨论反应式 Web 应用程序中的请求和响应体参数。

请求和响应体转换

在第十章《使用 Spring 在 Web 应用程序中实现 MVC 模式》中,我们讨论了请求体和响应体的消息转换,无论是从 Java 到 JSON,还是从 JSON 到 Java 对象,以及更多。同样,在反应式 Web 应用程序的情况下,也需要进行转换。Spring 核心模块提供了反应式编码器和解码器,以启用 Flux 字节序列化和反序列化到类型对象。

让我们看看以下示例,了解请求体类型转换。开发者不需要强制进行类型转换——Spring 框架在两种方法中自动为您转换:基于注解的编程和基于功能的编程。

  • Account account:这意味着在调用控制器之前,账户对象被反序列化,而不会阻塞。

  • Mono account:这意味着AccountController可以使用 Mono 来声明逻辑。账户对象首先被反序列化,然后执行此逻辑。

  • Flux accounts:这意味着在输入流场景中,AccountController可以使用 Flux。

  • Single account:这与 Mono 非常相似,但在这里控制器使用了 RxJava。

  • Observable accounts:这也与 Flux 非常相似,但在这个情况下,控制器使用了 RxJava 的输入流。

在前面的列表中,您已经看到了在反应式编程模型中 Spring 框架的类型转换。让我们看看以下示例中响应体的返回类型:

  • Account:它不会阻塞给定的 Account 进行序列化;意味着一个同步的、非阻塞的控制器方法。

  • void:这特定于基于注解的编程模型。当方法返回时,请求处理完成;这意味着一个同步的、非阻塞的控制器方法。

  • Mono:当 Mono 完成时,它不会阻塞给定的 Account 进行序列化。

  • Mono:这意味着当 Mono 完成时,请求处理完成。

  • Flux:这在流式场景中使用,可能 SSE 依赖于请求的内容类型。

  • Flux:这使 SSE 流式传输成为可能。

  • Single:与上面相同,但使用 RxJava。

  • Observable:与上面相同,但使用 RxJava Observable 类型。

  • Flowable:与上面相同,但使用 RxJava 2 Flowable 类型。

在前面的列表中,你看到了处理方法的返回类型。Spring 框架在响应式编程模型中进行类型转换。

摘要

在本章中,你学习了响应式模式及其原则。这并不是编程中的新创新——它是一个非常古老的概念,但它非常适合现代应用的需求。

响应式编程有四个原则:响应性、弹性、弹性和消息驱动架构。响应性意味着系统必须在所有条件下都有响应性:无论是奇数条件还是偶数条件。

Spring 5 框架通过使用 Reactor 框架和响应式流来支持响应式编程模型。Spring 引入了一个新的响应式 Web 模块,即 spring-web-reactive。它通过使用 Spring MVC 的注解,如 @Controller@RestController@RequestMapping,或者通过使用 Java 8 Lambda 表达式的函数式编程方法,为 Web 应用提供了响应式编程方法。

在本章中,我们通过使用 spring web reactive 模块创建了一个 Web 应用。这个应用的代码可以在 GitHub 上找到。在下一章中,你将学习关于并发模式实现的内容。

第十二章:实现并发模式

在第十一章《实现反应式设计模式》中,我们讨论了反应式设计模式及其如何满足当今应用的需求。Spring 5 框架为 Web 应用引入了反应式 Web 应用模块。在本章中,我们将探讨一些并发设计模式以及这些模式如何解决多线程应用中的常见问题。Spring 5 框架的反应式模块也为多线程应用提供了解决方案。

如果你是一名软件工程师或者正在成为软件工程师的过程中,你必须知道“并发”这个术语。在几何属性中,共点圆或形状是指那些具有共同中心点的形状。这些形状在尺寸上可能不同,但有一个共同的中心或中点。

在软件编程方面,这个概念是相似的。在技术或编程中,“并发编程”一词意味着程序执行多个并行计算的能力,以及程序在单个时间间隔内处理多个外部活动的能力。

当我们谈论软件工程和编程时,并发模式是那些帮助处理多线程编程模型的设计模式。以下是一些并发模式:

  • 使用并发模式处理并发

  • 活跃对象模式

  • 监视对象模式

  • 半同步/半异步模式

  • 领导/跟随模式

  • 线程特定存储

  • 反应器模式

  • 并发模块的最佳实践

现在我们将深入探讨这五种并发设计模式。

活跃对象模式

并发设计模式的活跃对象类型区分了方法执行与方法调用。这个模式的工作是增强并发性,同时简化对位于不同且可区分的控制线程中的对象的同步访问。它用于处理同时到达的多个客户端请求,并且也有助于提高服务质量。让我们看看以下图表,这些图表展示了并发和多线程应用中的活跃对象设计模式:

图片

正如你在前面的图中可以看到,以下是这个并发设计模式的组件:

  • 代理:这是对客户端可见的活跃对象。代理宣传其接口。

  • 仆人:在代理的接口中定义了一个方法。仆人是其实施的提供者。

  • 激活列表:这是一个序列化的列表,包含代理插入的方法请求对象。这个列表允许仆人并发运行。

那么,这个设计模式是如何工作的呢?答案是,每个并发对象都属于或存在于一个单独的控制线程中。这也独立于客户端的控制线程。这意味着它会调用其方法,也就是说,方法执行和方法调用都在单独的控制线程中进行。然而,客户端将这个过程视为一个普通的方法。为了在运行时将客户端的请求传递给仆人,两者都必须在单独的线程中运行。

在这个设计模式中,代理在收到请求后所做的是设置一个方法请求对象并将其插入激活列表中。这个方法执行两个任务;保持方法请求对象并跟踪它可以在哪个方法请求上执行。请求参数和任何其他信息都包含在方法请求对象中,以便稍后执行所需的方法。这个激活列表反过来帮助代理和仆人并发运行。

让我们在下一节中看看另一个并发设计模式,即监控对象模式。

监控对象模式

监控对象模式是另一种并发设计模式,有助于多线程程序的执行。它是一种设计模式,旨在确保在单个时间间隔内,只有一个方法在一个对象中运行,为此,它同步并发方法执行。

与活动对象设计模式不同,监控对象模式没有单独的控制线程。每个接收到的请求都在客户端自己的控制线程中执行,并且直到方法返回,访问将被阻塞。在单个时间间隔内,一个同步方法可以在一个监控器中执行。

以下是由监控对象模式提供的解决方案:

  • 同步边界由对象的接口定义,并确保在单个对象中只有一个活动方法。

  • 必须确保所有对象都检查需要同步的每个方法,并透明地序列化它们,而不让客户端知道。另一方面,操作是互斥的,但它们像普通方法调用一样被调用。等待和信号原语用于实现条件同步。

  • 为了防止死锁并使用可用的并发机制,当对象的方法在执行过程中阻塞时,必须允许其他客户端访问该对象。

  • 当控制线程被方法自愿中断时,必须始终保持不变量。

让我们看看下面的图示,它展示了在并发应用程序中监控对象设计模式的更多内容:

图片

在此前面的图中,客户端对象调用具有多个同步方法的监控对象,以及与监控条件和监控锁关联的监控对象。让我们如下探索此并发设计模式的每个组件:

  • 监控对象: 此组件公开了同步到客户端的方法

  • 同步方法: 这些方法实现了由对象接口导出的线程安全函数

  • 监控条件: 此组件与监控锁一起决定同步方法是否应该继续其处理或挂起

活动对象和监控对象模式是并发设计模式的分支。

现在,我们将讨论的其他类型的并发模式是并发架构模式的分支。

半同步/半异步模式

半同步和半异步的任务是区分两种处理类型(异步和同步),以简化程序而不影响其性能。

为了在队列层之间进行处理,引入了两个相互通信的层,用于异步和同步服务。

每个并发系统都包含异步和同步服务。为了使这些服务能够相互通信,半同步/半异步模式将系统中的服务分解为层。使用队列层,这两个服务相互传递消息以进行交互通信。

让我们看看以下图,它说明了这些设计模式:

图片

如您在前面的图中所见,有三个层--同步服务层队列层异步服务层。同步层包含与队列层同步工作的服务,并且此查询使用异步服务在异步服务层异步执行。此层的异步服务使用基于外部事件资源。

如您在前面的图中所见,这里包含三个层。让我们看看这些层:

  • 同步任务层: 此层中的任务是活动对象。高级输入和输出操作由这些任务执行,它们将数据同步地传输到队列层。

  • 队列层: 此层提供了同步和缓冲,这是在同步和异步任务层之间所需的。

  • 异步任务层: 此层中的任务处理来自外部源的事件。这些任务不包含单独的控制线程。

我们已经讨论了并发模式的半同步和半异步设计模式。让我们转向另一种并发模式,即领导者/跟随者模式。

领导者/跟随者模式

在并发模型中,对事件源中的服务请求的检测、解复用、调度和处理都是高效进行的,其中许多多个线程逐个处理以使用事件源上的集合。Half-Sync/Half-Async 的另一种替代方案是领导者/跟随者模式。这种模式可以用作替代 Half-Sync/Half-Async 和主动对象模式以改进性能。使用此模式的条件是在处理多个线程的请求时,既没有排序也没有同步约束:

图片

此模式的重点工作是并发或同时处理多个事件。由于与并发相关的开销,可能无法将每个单独的套接字句柄与单独的线程连接。此设计的高亮特点是,通过使用此模式,可以解复用线程和事件源之间的关联。当事件到达事件源时,此模式建立线程池。这是为了有效地共享一组事件源。这些事件源轮流解复用到达的事件。此外,事件被同步调度到应用程序服务进行处理。在由领导者/跟随者模式结构化的线程池中,只有一个线程等待事件的发生;其他线程排队等待。当一个线程检测到事件时,跟随者被提升为领导者。然后它处理该线程并将事件调度到应用程序处理器。

在这种模式中,处理线程可以并发运行,但只允许一个线程等待即将发生的新事件。

让我们看看下一节中另一个基于并发的设计模式。

反应器模式

反应器模式用于处理由单个或多个输入源并发接收到的服务请求。接收到的服务请求随后由服务处理器解复用,并调度到相关的请求处理器。所有反应器系统通常都发现于单线程中,但它们也被说存在于多线程环境中。

使用这种模式的关键好处是应用程序组件可以被划分为多个部分,例如模块化或可重用。此外,这允许系统在不增加多个线程的额外复杂性的情况下实现简单的粗粒度并发。

让我们看看以下关于反应器设计模式的图示:

图片

如前图所示,调度器使用解复用器来通知处理器,处理器通过 I/O 事件执行实际的工作。反应器通过调度适当的处理器来响应 I/O 事件。处理器执行非阻塞操作。前图展示了此设计模式的以下组件:

  • 资源:这些是通过它们提供输入或消耗输出的资源。

  • 同步事件解多路复用器:通过事件循环阻塞所有资源。当有同步操作可能启动时,资源将通过解多路复用器发送到调度器,而不会阻塞。

  • 调度器:此组件处理请求处理器的注册或注销。资源通过调度器分发到相应的请求处理器。

  • 请求处理器:此处理由调度器分发的请求。

现在,我们继续到下一个也是最后一个并发模式,即线程特定存储模式。

线程特定存储模式

单个逻辑全局访问点可以用来检索线程本地的对象。这种并发设计模式允许多个线程执行此功能。这样做无需在每个访问对象时产生锁定开销。有时,这种特定的模式可以被视为所有并发设计模式中的对立面。这是因为线程特定的存储通过防止线程间共享可用资源来解决了几个复杂性。

该方法似乎由应用程序线程在普通对象上调用。实际上,它是在线程特定的对象上调用。多个应用程序线程可以使用单个线程特定的对象代理来访问与每个应用程序线程关联的唯一线程特定对象。代理使用应用程序线程标识符来区分它封装的线程特定对象。

并发模块的最佳实践

在执行并发时,程序员必须考虑以下列表中的考虑事项。让我们看看以下最佳实践,当有机会与并发应用程序模块一起工作时应该考虑。

  • 获取执行器:用于获取执行器的 Executor 框架提供了 executors 工具类。各种类型的执行器提供特定的线程执行策略。以下有三个示例:

    • newCachedThreadPool(): 如果可用,此方法使用先前构建的线程创建一个线程池。通过使用此类线程池,增强了使用短暂异步任务的应用程序的性能。

    • newSingleThreadExecutor(): 这里使用一个在无界队列中运行的工人线程来创建一个执行器。在这种类型中,任务被添加到队列中,然后依次执行。如果此线程在执行过程中失败,将创建一个新的线程来替换失败的线程,以便任务可以无中断地执行。

    • ExecutorService newFixedThreadPool(int nThreads):在这种情况下,固定数量的线程在共享的无界队列中操作,用于创建线程池。在线程中,任务正在被积极处理。当池中的所有线程都处于活动状态并且提交了新任务时,任务将添加到队列中,直到有线程可用于处理新任务。如果在执行器关闭之前线程失败,将创建一个新的线程来执行任务的执行。请注意,这些线程池仅在执行器处于活动状态或开启时存在。

  • 尽可能使用协作同步构造:建议尽可能使用协作同步构造。

  • 无必要冗长任务和过度订阅:冗长任务众所周知会导致死锁、饥饿,甚至阻止其他任务正常工作。较大的任务可以被分解成较小的任务以实现良好的性能。过度订阅也是避免死锁和饥饿等方法之一。使用这种方法,可以创建比可用线程数更多的线程。当冗长任务包含大量延迟时,这非常高效。

  • 使用并发内存管理函数:如果在某种情况下,可以使用后续的并发内存管理函数,强烈建议使用它。这些可以在使用具有短生命周期的对象时使用。AllotFree等函数用于释放内存和分配,无需内存屏障或使用锁。

  • 使用 RAII 管理并发对象的生命周期:RAII 是资源获取即初始化的缩写。这是管理并发对象生命周期的有效方法。

这就是关于并发及其设计模式的所有内容,这些设计模式可以用来处理和实现并发。这些是并发程序中最常见的五种设计模式。还讨论了一些执行并发模块的最佳实践。希望这能提供信息量,并帮助你理解并发模式是如何工作的!

摘要

在本章中,你学习了几个并发设计模式,并看到了这些模式的使用案例。在这本书中,我只涵盖了并发设计模式的基础。我们包括了活动对象、监控对象、半同步/半异步、领导者/跟随者、线程特定存储和反应器模式。这些都是应用多线程环境中的并发设计模式的一部分。我们还讨论了一些在应用中使用并发设计模式时的最佳实践考虑。

posted @ 2025-09-11 09:51  绝不原创的飞龙  阅读(27)  评论(0)    收藏  举报