Java9-依赖注入指南-全-
Java9 依赖注入指南(全)
原文:
zh.annas-archive.org/md5/0593194d17238b4e29f1300a3702767d译者:飞龙
前言
依赖注入是一种设计模式,它允许我们移除硬编码的依赖关系,使我们的应用程序松耦合、可扩展和可维护。我们可以通过将依赖解析从编译时移动到运行时来实现依赖注入。
这本书将为您提供一个一站式指南,帮助您使用 Java 9 的最新特性以及 Spring 5 和 Google Guice 等框架编写松耦合的代码。
这本书面向的对象
这本书是为希望了解如何在他们的应用程序中实现依赖注入的 Java 开发者准备的。假设您对 Spring 和 Guice 框架以及 Java 编程有一定的了解。
这本书涵盖的内容
第一章,为什么需要依赖注入?,为您详细介绍了各种概念,如依赖倒置原则(DIP)、控制反转(IoC)和依赖注入(DI)。它还讨论了 DI 在实际应用中的常见用例。
第二章,Java 9 中的依赖注入,使您熟悉 Java 9 特性和其模块化框架,并解释了如何使用服务加载器概念实现 DI。
第三章,使用 Spring 进行依赖注入,教您如何在 Spring 框架中管理依赖注入。它还描述了使用 Spring 实现 DI 的另一种方法。
第四章,使用 Google Guice 进行依赖注入,讨论了 Guice 及其依赖机制,并教我们 Guice 框架的依赖绑定和多种注入方法。
第五章,Scopes,介绍了 Spring 和 Guice 框架中定义的不同作用域。
第六章,面向切面编程和拦截器,展示了面向切面编程(AOP)的目的,以及它是如何通过将重复代码从应用程序中隔离出来,并使用 Spring 框架动态地将其插入来解决不同的设计问题的。
第七章,IoC 模式和最佳实践,概述了各种可用于实现 IoC 的设计模式。除此之外,您还将了解在注入 DI 时应该遵循的最佳实践和反模式。
要充分利用这本书
-
如果您了解 Java、Spring 和 Guice 框架,那将很有帮助。这将有助于您理解依赖注入
-
在开始之前,我们假设您已经在系统上安装了 Java 9 和 Maven
下载示例代码文件
您可以从www.packtpub.com的账户下载本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
在www.packtpub.com登录或注册。
-
选择 SUPPORT 标签。
-
点击代码下载与勘误。
-
在搜索框中输入书名,并遵循屏幕上的说明。
下载文件后,请确保使用最新版本的以下软件解压缩或提取文件夹:
-
WinRAR/7-Zip for Windows
-
Zipeg/iZip/UnRarX for Mac
-
7-Zip/PeaZip for Linux
本书代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Java-9-Dependency-Injection。我们还有其他来自我们丰富图书和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!
下载彩色图像
我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:www.packtpub.com/sites/default/files/downloads/Java9DependencyInjection_ColorImages.pdf。
使用的约定
本书使用了多种文本约定。
CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“将下载的WebStorm-10*.dmg磁盘映像文件作为系统上的另一个磁盘挂载。”
代码块设置如下:
module javaIntroduction {
}
任何命令行输入或输出都应如下编写:
$ mkdir css
$ cd css
粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词在文本中如下所示。以下是一个示例:“从管理面板中选择系统信息。”
警告或重要说明如下所示。
技巧和窍门如下所示。
联系我们
我们欢迎读者的反馈。
一般反馈:请发送电子邮件至feedback@packtpub.com,并在邮件主题中提及书名。如果您对本书的任何方面有疑问,请通过questions@packtpub.com发送电子邮件给我们。
勘误:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将非常感激您能向我们报告。请访问www.packtpub.com/submit-errata,选择您的书,点击勘误提交表单链接,并输入详细信息。
盗版: 如果你在互联网上以任何形式遇到我们作品的非法副本,如果你能提供位置地址或网站名称,我们将不胜感激。请通过copyright@packtpub.com与我们联系,并附上材料的链接。
如果你有兴趣成为作者:如果你在某个领域有专业知识,并且你对撰写或为书籍做出贡献感兴趣,请访问 authors.packtpub.com.
评论
请留下评论。一旦你阅读并使用了这本书,为何不在你购买它的网站上留下评论呢?潜在的读者可以查看并使用你的客观意见来做出购买决定,Packt 公司可以了解你对我们的产品有何看法,我们的作者也可以看到他们对书籍的反馈。谢谢!
想了解更多关于 Packt 的信息,请访问 packtpub.com.
第一章:为什么需要依赖注入?
在软件开发中,非常常见的情况是,其他人可能已经找到了解决你所面临问题的有效解决方案。
作为一名开发者,你不需要每次都重新发明轮子。相反,你需要参考已经建立起来的实践和方法。你猜到了我们在谈论什么吗?没错:设计 模 式。
本章旨在涵盖以下有趣的主题:
-
设计模式是什么以及它们的益处
-
依赖注入原则(DIP)
-
控制反转(IoC)——一种实现 DIP 的设计方法
-
实现 IoC 的各种设计模式
-
依赖注入(DI)
-
实现 DI 的多种方式
-
如何使用 IoC 容器来应用 DI
设计模式
根据定义,设计模式是一套经过验证的事实上的行业标准最佳实践,用于解决重复出现的问题。设计模式不是现成的解决方案。相反,它们是实现和运用最佳解决方案的方式或模板,以解决你的问题。
同样真实的是,如果一个设计模式没有正确实现,它可能会造成很多问题,而不是解决你期望解决的问题。因此,了解哪种设计模式(如果有的话)适合特定场景非常重要。
设计模式是描述问题和解决方法的通用范式。它通常不是语言特定的。设计模式可以保护你免受开发后期通常出现的设计问题。
使用设计模式有许多优势,如下所示:
-
提高软件的可重用性
-
开发周期变得更快
-
使代码更易于阅读和维护
-
提高效率和增强整体软件开发
-
提供了描述问题和最佳解决方案的通用词汇,以更抽象的方式
你还可以数出更多。在接下来的章节中,我们将深入了解如何通过遵循某些原则和模式,使你的代码模块化、松散耦合、独立、可测试和可维护。
本章将深入探讨关于依赖倒置原则(DIP)、控制反转范式和 DI 设计模式的理念。
大多数开发者将设计原则和设计模式这两个术语互换使用,尽管它们之间是有区别的。
设计原则:一般来说,这是一条关于如何正确和错误地设计你的应用的指南。设计原则总是谈论应该做什么,而不是如何做。
设计模式:针对常见问题的通用和可重用解决方案。设计模式讨论的是如何在给定的软件设计环境中通过提供清晰的方法论来解决这些问题。
使你的代码更干净、可读、解耦、可维护和模块化的第一步是学习被称为DIP的设计原则。
依赖倒置原则
DIP 为你的代码提供高级指导,以使其松散耦合。它说以下内容:
-
高级模块不应该依赖于低级模块来执行其责任。两者都应该依赖于抽象。
-
抽象不应该依赖于细节。细节应该依赖于抽象。
当在依赖代码中进行更改时,变化总是有风险的。DIP 讨论的是将一块代码(依赖)与它不直接相关的程序主体隔离开来。
为了减少耦合,DIP 建议消除低级模块对高级模块的直接依赖以执行其责任。相反,让高级模块依赖于抽象(一个合同),它形成了通用的低级行为。
这样,低级模块的实际实现可以改变,而无需在高级模块中进行任何更改。这为系统带来了极大的灵活性和分子性。只要任何低级实现都绑定在抽象上,高级模块就可以调用它。
让我们看看一个示例次优设计,在这个设计中我们可以应用 DIP 来改进应用程序的结构。
考虑一个场景,你正在设计一个模块,该模块仅生成本地商店的资产负债表。你从数据库中获取数据,使用复杂的业务逻辑进行处理,并将其导出为 HTML 格式。如果你以程序化的方式设计它,那么系统的流程将类似于以下图示:

一个单独的模块负责获取数据,应用业务逻辑以生成资产负债表数据,并将其导出为 HTML 格式。这不是最佳设计。让我们将整个功能分成三个不同的模块,如图所示:

-
获取数据库模块: 这将从数据库中获取数据
-
导出 HTML 模块: 这将导出 HTML 格式的数据
-
资产负债表模块: 这将从数据库模块获取数据,处理它,并将其交给导出模块以导出为 HTML
在这种情况下,资产负债表模块是一个高级模块,而获取数据库和导出 HTML 是低级模块。
FetchDatabase模块的代码应该看起来像以下片段:
public class FetchDatabase {
public List<Object[]> fetchDataFromDatabase(){
List<Object[]> dataFromDB = new ArrayList<Object[]>();
//Logic to call database, execute a query and fetch the data
return dataFromDB;
}
}
ExportHTML模块将获取数据列表并将其导出为 HTML 文件格式。代码应该如下所示:
public class ExportHTML {
public File exportToHTML(List<Object[]> dataLst){
File outputHTML = null;
//Logic to iterate the dataLst and generate HTML file.
return outputHTML;
}
}
我们父模块——从获取数据库模块获取数据并发送到导出 HTML 模块的BalanceSheet模块——的代码应该如下所示:
public class BalanceSheet {
private ExportHTML exportHTML = new ExportHTML();
private FetchDatabase fetchDatabase = new FetchDatabase();
public void generateBalanceSheet(){
List<Object[]> dataFromDB =
fetchDatabase.fetchDataFromDatabase();
exportHTML.exportToHTML(dataFromDB);
}
}
初看之下,这个设计看起来不错,因为我们已经将获取和导出数据的责任分离到单独的子模块中。好的设计可以适应任何未来的变化而不会破坏系统。这个设计在未来的任何变化中会不会使我们的系统变得脆弱呢?让我们来看看。
经过一段时间,您需要从外部网络服务以及数据库中获取数据。此外,您需要将数据导出为 PDF 格式而不是 HTML 格式。为了实现这一变化,您将创建新的类/模块来从网络服务获取数据,并按照以下代码片段导出 PDF:
// Separate child module for fetch the data from web service.
public class FetchWebService {
public List<Object[]> fetchDataFromWebService(){
List<Object[]> dataFromWebService = new ArrayList<Object[]>();
//Logic to call Web Service and fetch the data and return it.
return dataFromWebService;
}
}
// Separate child module for export in PDF
public class ExportPDF {
public File exportToPDF(List<Object[]> dataLst){
File pdfFile = null;
//Logic to iterate the dataLst and generate PDF file
return pdfFile;
}
}
为了适应新的获取和导出数据的方式,资产负债表模块需要某种形式的标志。根据该标志的值,相应的子模块将在资产负债表模块中实例化。BalanceSheet 模块的更新代码如下:
public class BalanceSheet {
private ExportHTML exportHTML = null;
private FetchDatabase fetchDatabase = null;
private ExportPDF exportPDF = null;
private FetchWebService fetchWebService = null;
public void generateBalanceSheet(int inputMethod, int outputMethod){
//1\. Instantiate the low level module object.
if(inputMethod == 1){
fetchDatabase = new FetchDatabase();
}else if(inputMethod == 2){
fetchWebService = new FetchWebService();
}
//2\. fetch and export the data for specific format based on flags.
if(outputMethod == 1){
List<Object[]> dataLst = null;
if(inputMethod == 1){
dataLst = fetchDatabase.fetchDataFromDatabase();
}else{
dataLst = fetchWebService.fetchDataFromWebService();
}
exportHTML.exportToHTML(dataLst);
}else if(outputMethod ==2){
List<Object[]> dataLst = null;
if(inputMethod == 1){
dataLst = fetchDatabase.fetchDataFromDatabase();
}else{
dataLst = fetchWebService.fetchDataFromWebService();
}
exportPDF.exportToPDF(dataLst);
}
}
}
伟大的工作!我们的应用程序能够处理两种不同的输入和输出方法来生成资产负债表。但是等等;当你未来需要添加更多方法(获取和导出数据)时会发生什么?例如,你可能需要从谷歌驱动器获取数据并将资产负债表导出为 Excel 格式。
对于每个新的输入和输出方法,您需要更新您的主体模块,即资产负债表模块。当一个模块依赖于另一个具体实现时,它在该模块上被认为是紧密耦合的。这违反了基本原则:对扩展开放,但对修改封闭。
让我们回顾一下 DIP 讨论的内容:高级模块不应依赖于低级模块来履行其职责。两者都应依赖于抽象。
这是我们设计中的基本问题。在我们的案例中,资产负债表(高级)模块紧密依赖于获取数据库和导出 HTML 数据(低级)模块。
正如我们所看到的,原则总是显示设计问题的解决方案。它不讨论如何实现它。在我们的案例中,DIP 讨论的是消除低级模块对高级模块的紧密依赖。
但我们如何做到这一点?这就是 IoC 出现的地方。IoC 展示了在模块之间定义抽象的方法。简而言之,IoC 是实现 DIP 的方式。
控制反转
IoC 是一种设计方法,用于通过将控制流从主程序反转到某个其他实体或框架来构建软件工程中的松耦合系统。
在这里,控制指的是程序除了其主要活动之外处理的任何其他活动,例如创建和维护依赖对象、管理应用程序流程等。
与过程式编程风格不同,其中程序一次性处理多个不相关的事物,IoC 定义了一个指导方针,即您需要根据责任将主程序分解为多个独立的程序(模块),并以一种方式安排它们,使它们松耦合。
在我们的例子中,我们将功能分解为独立的模块。缺失的部分是如何安排它们以实现解耦,我们将学习 IoC 如何实现这种安排。通过反转(改变)控制,您的应用程序变得解耦、可测试、可扩展和可维护。
通过 IoC 实现 DIP
DIP 建议高级模块不应依赖于低级模块。两者都应依赖于抽象。IoC 提供了一种在高级模块和低级模块之间实现抽象的方法。
让我们看看如何在我们的资产负债表示例中通过 IoC 应用 DIP。基本的设计问题是高级模块(资产负债表)紧密依赖于低级(获取和导出数据)模块。
我们的目标是打破这种依赖。为了实现这一点,IoC 建议反转控制。在 IoC 中,反转控制可以通过以下方式实现:
-
反转接口:确保高级模块定义接口,而低级模块遵循它
-
反转对象创建:将依赖关系从主模块更改为其他程序或框架
-
反转流程:改变应用程序的流程
反转接口
反转接口意味着将交互控制从低级模块反转到高级模块。您的高级模块应该决定哪些低级模块可以与之交互,而不是不断改变自己以集成每个新的低级模块。
在反转接口后,我们的设计将如下所示:

在这个设计中,资产负债表模块(高级)通过通用接口与获取数据和导出数据(低级)模块进行交互。这种设计的明显好处是,您可以在不更改资产负债表模块(高级)的情况下添加新的获取数据和导出数据(低级)模块。
只要低级模块与接口兼容,高级模块就会很高兴与之合作。在这个新设计中,高级模块不依赖于低级模块,两者都通过抽象(接口)进行交互。将接口与实现分离是实现 DIP 的前提条件。
让我们根据这个新设计更改我们的代码。首先,我们需要创建两个接口:获取数据和导出数据如下:
public interface IFetchData {
//Common interface method to fetch data.
List<Object[]> fetchData();
}
public interface IExportData {
//Common interface method to export data.
File exportData(List<Object[]> listData);
}
接下来,所有低级模块必须按照以下片段实现这些接口:
public class FetchDatabase implements IFetchData {
public List<Object[]> fetchData(){
List<Object[]> dataFromDB = new ArrayList<Object[]>();
//Logic to call database, execute a query and fetch the data
return dataFromDB;
}
}
public class FetchWebService implements IFetchData {
public List<Object[]> fetchData(){
List<Object[]> dataFromWebService = new ArrayList<Object[]>();
//Logic to call Web Service and fetch the data and return it.
return dataFromWebService;
}
}
public class ExportHTML implements IExportData{
public File exportData(List<Object[]> listData){
File outputHTML = null;
//Logic to iterate the listData and generate HTML File
return outputHTML;
}
}
public class ExportPDF implements IExportData{
public File exportData(List<Object[]> dataLst){
File pdfFile = null;
//Logic to iterate the listData and generate PDF file
return pdfFile;
}
}
最后,资产负债表模块需要依赖接口与低级模块交互。因此,更新的BalanceSheet模块应如下所示:
public class BalanceSheet {
private IExportData exportDataObj= null;
private IFetchData fetchDataObj= null;
public Object generateBalanceSheet(){
List<Object[]> dataLst = fetchDataObj.fetchData();
return exportDataObj.exportData(dataLst);
}
}
您可能已经注意到,generateBalanceSheet()方法变得更加简单。它允许我们无需任何更改即可与额外的获取和导出模块一起工作。这要归功于反转接口的机制,使得这一切成为可能。
这个设计看起来很完美;但仍然有一个问题。如果您注意到了,资产负债表模块仍然保留着创建低级模块对象(exportDataObj和fetchDataObj)的责任。换句话说,对象创建的依赖性仍然存在于高级模块中。
因此,即使在实现接口反转之后,资产负债表模块也不是完全解耦于底层模块。您最终将根据某些标志使用 if/else 块实例化底层模块,而高级模块会不断变化以添加额外的底层模块集成。
为了克服这个问题,您需要将对象创建从您的高级模块反转到其他实体或框架。这是实现 IoC 的第二种方式。
对象创建反转
一旦模块之间的抽象设置完成,就无需在高级模块中保留创建依赖对象的逻辑。让我们通过另一个例子来了解对象创建设计反转的重要性。
假设您正在设计一款战争游戏。您的玩家可以使用各种武器射击敌人。您为每种武器创建了单独的类(底层模块)。在游戏过程中,您的玩家可以根据获得的分数添加武器。
此外,玩家可以更换武器。为了实现接口反转,我们创建了一个名为Weapon的接口,所有武器模块都将实现该接口,具体如下所示:

假设游戏开始时有三种武器,您将它们保留在游戏中。如果您在玩家模块中保留武器创建代码,选择武器的逻辑将如下所示:
public class Player {
private Weapon weaponInHand;
public void chooseWeapon(int weaponFlag){
if(weaponFlag == 1){
weaponInHand = new SmallGun();
}else if(weaponFlag ==2){
weaponInHand = new Rifle();
}else{
weaponInHand = new MachineGun();
}
}
public void fireWeapon(){
if(this.weaponInHand !=null){
this.weaponInHand.fire();
}
}
}
由于玩家模块负责创建武器对象,我们在chooseWeapon()方法中传递一个标志。让我们假设,在一段时间内,您向游戏中添加了一些更多的武器。每次添加新武器时,您都需要更改Player模块的代码。
解决这个问题的方法是反转对象创建过程,从主模块到另一个实体或框架。
让我们先把这个解决方案应用到我们的Player模块。更新的代码如下:
public class Player {
private Weapon weaponInHand;
public void chooseWeapon(Weapon setWeapon){
this.weaponInHand = setWeapon;
}
public void fireWeapon(){
if(this.weaponInHand !=null){
this.weaponInHand.fire();
}
}
}
您可以观察到以下事项:
-
在
chooseWeapon()方法中,我们通过接口传递武器对象。Player模块不再处理武器对象的创建。 -
这样,
Player(高级)模块就完全解耦于Weapon(底层)模块。 -
两个模块通过由高级模块定义的接口进行交互。
-
对于系统中新添加的任何武器,您无需在玩家模块中进行任何更改。
让我们将这个解决方案(反转创建对象)应用到我们的资产负债表模块。BalanceSheet模块的更新代码如下所示:
public class BalanceSheet {
private IExportData exportDataObj= null;
private IFetchData fetchDataObj= null;
//Set the fetch data object from outside of this class.
public void configureFetchData(IFetchData actualFetchDataObj){
this.fetchDataObj = actualFetchDataObj;
}
//Set the export data object from outside of this class.
public void configureExportData(IExportData actualExportDataObj){
this.exportDataObj = actualExportDataObj;
}
public Object generateBalanceSheet(){
List<Object[]> dataLst = fetchDataObj.fetchData();
return exportDataObj.exportData(dataLst);
}
}
这里有一些快速观察:
-
获取数据和导出数据模块的对象是在资产负债表模块外部创建的,并通过
configureFetchData()和configureExportData()方法传递 -
资产负债表模块现在已完全解耦于获取数据和导出数据模块
-
对于任何新的获取和导出数据类型,资产负债表模块无需进行任何更改
在这个时候,DIP 和 IoC 之间的关系可以按照以下图示描述:

最后,我们通过IoC实现了DIP,并解决了模块之间相互依赖的最基本问题之一。
但等等,还有一些事情还没有完成。我们已经看到,将对象创建与主模块分离可以消除适应变化的风险,并使代码解耦。但我们还没有探讨如何从外部代码创建并传递依赖对象到模块中。有各种反转对象创建的方法。
反转对象创建的不同方法
我们已经看到,对象创建反转如何帮助我们解耦模块。您可以通过以下多种设计模式实现对象创建反转:
-
工厂模式
-
服务定位器
-
依赖注入
通过工厂模式实现对象创建反转
工厂模式负责从使用它的客户端创建对象。它生成遵循公共接口的类的对象。客户端只需传递它想要的实现类型,工厂就会创建该对象。
如果我们将工厂模式应用于我们的平衡表示例,对象创建反转的过程如下所示:

-
客户端(在我们的例子中,是平衡表模块)与工厂交谈——嘿,工厂,你能给我一个 fetch 数据对象吗?这是类型。
-
工厂接收类型,创建对象,并将其传递给客户端(平衡表模块)。
-
该工厂只能创建相同类型的对象。
-
工厂类对其客户端来说是一个完整的黑盒。它们知道这是一个用于获取对象的静态方法。
平衡表模块可以从FetchDataFactory获取FetchData对象。FetchDataFactory的代码如下:
public class FetchDataFactory {
public static IFetchData getFetchData(String type){
IFetchData fetchData = null;
if("FROM_DB".equalsIgnoreCase(type)){
fetchData = new FetchDatabase();
}else if("FROM_WS".equalsIgnoreCase(type)){
fetchData = new FetchWebService();
}else {
return null;
}
return fetchData;
}
}
要使用此工厂,您需要更新平衡表模块的configureFetchData()方法如下:
//Set the fetch data object from Factory.
public void configureFetchData(String type){
this.fetchDataObj = FetchDataFactory.getFetchData(type);
}
对于导出数据,您需要根据以下代码片段创建一个单独的工厂:
public class ExportDataFactory {
public static IExportData getExportData(String type){
IExportData exportData = null;
if("TO_HTML".equalsIgnoreCase(type)){
exportData = new ExportHTML();
}else if("TO_PDF".equalsIgnoreCase(type)){
exportData = new ExportPDF();
}else {
return null;
}
return exportData;
}
}
如果引入了新的 fetch 数据或导出数据类型,您只需在其相应的工厂类中更改即可。
通过服务定位器实现对象创建反转
服务定位器模式与工厂模式大致相同。服务定位器可以找到现有的对象并将其发送给客户端,而不是像工厂模式那样每次都创建一个新的对象。我们不会深入细节,只是简要地看看服务定位器如何创建对象。服务定位器的流程可以按照以下图示描述:

-
客户端依赖于服务定位器来查找服务。在这里,服务意味着任何类型的依赖
-
服务定位器获取服务的名称,并将服务的对象返回给客户端
如果我们的资产负债表模块使用服务定位器,configureFetchData() 方法的代码将如下所示:
//Set the fetch data object from ServiceLocator.
public void configureFetchData(String type){
this.fetchDataObj = FetchDataServiceLocator.Instance.getFetchData(type);
}
与获取数据类似,你需要为导出数据设计一个单独的服务定位器。对于任何新的获取数据或导出数据类型,都需要在服务定位器中进行更改。
反转对象创建的另一种方法是 DI。
依赖注入
DI 是将对象创建过程从你的模块反转到其他代码或实体的方法之一。术语 注入 指的是将依赖对象传递到软件组件的过程。
由于 DI 是实现 IoC 的方法之一,它依赖于抽象来设置依赖。客户端对象不知道在编译时将使用哪个类来提供功能。依赖将在运行时解决。
依赖对象不会直接调用客户端对象;相反,客户端对象将在需要时调用依赖对象。这类似于好莱坞原则:不要调用我们,当我们需要时我们会调用你。
依赖注入类型
在 DI 中,你需要设置客户端对象中的入口点,以便可以注入依赖。基于这些入口点,DI 可以通过以下类型实现:
-
构造函数注入
-
设置器注入
-
接口注入
构造函数注入
这是注入依赖最常见的方式。在这种方法中,你需要通过客户端对象的公共构造函数传递依赖对象。请注意,在构造函数注入的情况下,你需要将所有依赖对象传递给客户端对象的构造函数。
构造函数注入可以控制实例化的顺序,从而降低循环依赖的风险。所有必需的依赖都可以通过构造函数注入传递。
在我们的 BalanceSheet 示例中,我们需要在构造函数中传递两个对象,因为它有两个依赖:一个是用于获取数据,另一个是用于导出数据类型,如下面的代码片段所示:
public class BalanceSheet {
private IExportData exportDataObj= null;
private IFetchData fetchDataObj= null;
//All dependencies are injected from client's constructor
BalanceSheet(IFetchData fetchData, IExportData exportData){
this.fetchDataObj = fetchData;
this.exportDataObj = exportData;
}
public Object generateBalanceSheet(){
List<Object[]> dataLst = fetchDataObj.fetchData();
return exportDataObj.exportData(dataLst);
}
}
所有依赖都从客户端对象的构造函数注入。由于构造函数只调用一次,很明显,依赖对象将在客户端对象存在期间不会改变。如果客户端使用构造函数注入,那么有时扩展和重写它可能会很困难。
设置器注入
如其名所示,在这里,依赖注入是通过公开的设置方法来实现的。任何在客户端对象实例化时不需要的依赖项被称为 可选依赖。它们可以在客户端对象创建后稍后设置。
设置器注入 是可选或条件依赖的完美选择。让我们将设置器注入应用于 BalanceSheet 模块。
代码将如下所示:
public class BalanceSheet {
private IExportData exportDataObj= null;
private IFetchData fetchDataObj= null;
//Setter injection for Export Data
public void setExportDataObj(IExportData exportDataObj) {
this.exportDataObj = exportDataObj;
}
//Setter injection for Fetch Data
public void setFetchDataObj(IFetchData fetchDataObj) {
this.fetchDataObj = fetchDataObj;
}
public Object generateBalanceSheet(){
List<Object[]> dataLst = fetchDataObj.fetchData();
return exportDataObj.exportData(dataLst);
}
}
对于每个依赖项,你需要单独放置设置方法。由于依赖项是通过设置方法设置的,因此提供依赖项的对象或框架需要在适当的时间调用设置方法,以确保在客户端对象开始使用之前依赖项是可用的。
接口注入
接口注入定义了依赖提供者与客户端之间通信的方式。它抽象了传递依赖的过程。依赖提供者定义了一个所有客户端都需要实现的接口。这种方法并不常用。
从技术上讲,接口注入和设置注入是相同的。它们都使用某种方法来注入依赖。然而,对于接口注入,方法是依赖提供者对象定义的。
让我们将接口注入应用于我们的资产负债表模块:
public interface IFetchAndExport {
void setFetchData(IFetchData fetchData);
void setExportData(IExportData exportData);
}
//Client class implements interface
public class BalanceSheet implements IFetchAndExport {
private IExportData exportDataObj= null;
private IFetchData fetchDataObj= null;
//Implements the method of interface injection to set dependency
@Override
public void setFetchData(IFetchData fetchData) {
this.fetchDataObj = fetchData;
}
//Implements the method of interface injection to set dependency
@Override
public void setExportData(IExportData exportData) {
this.exportDataObj = exportData;
}
public Object generateBalanceSheet(){
List<Object[]> dataLst = fetchDataObj.fetchData();
return exportDataObj.exportData(dataLst);
}
}
我们已创建接口IFetchAndExport并定义了注入依赖的方法。依赖提供者类知道如何通过这个接口传递依赖。我们的客户端对象(资产负债表模块)实现这个方法来设置依赖。
IoC 容器
到目前为止,我们讨论了扮演依赖提供者角色的代码或框架。它可以是我们自定义的任何代码或完整的IoC容器。一些开发者将其称为DI 容器,但我们将简单地称之为容器。
如果我们编写自定义代码来提供依赖项,直到我们只有一个依赖级别,事情才会变得顺利。考虑我们的客户端类也依赖于某些其他模块的情况。这会导致链式或嵌套依赖。
在这种情况下,通过手动代码实现依赖注入将变得相当复杂。这就是我们需要依赖容器的地方。
容器负责创建、配置和管理对象。你只需进行配置,容器就会轻松地处理对象的实例化和依赖管理。你不需要编写任何自定义代码,例如我们在使用工厂模式或服务定位器模式实现IoC时编写的代码。
因此,作为一个开发者,你的生活很酷。你只需给出关于依赖项的提示,容器就会处理其余的事情,你就可以专注于实现业务逻辑。
如果我们选择容器为我们的资产负债表模块设置依赖项,容器将首先创建所有依赖项的对象。然后,它将创建资产负债表类的对象,并在其中传递依赖项。容器会默默地完成所有这些事情,并给你一个所有依赖项都已设置的资产负债表模块的对象。这个过程可以用以下图表来描述:

总之,使用容器而不是手动代码管理依赖的优势如下:
-
将对象创建的过程从你的代码中隔离出来,使你的代码更加清晰和易于阅读。
-
从您的客户端模块中移除对象连接(设置依赖项)代码。容器将负责对象连接。
-
使您的模块实现 100%的松散耦合。
-
管理模块的整个生命周期。当您想在应用程序执行中为各种范围配置对象时,例如请求、会话等,这非常有帮助。
-
替换依赖项只需配置即可——不需要在代码中进行任何更改。
-
这是一种更集中的方式来处理对象的生命周期和依赖管理。当您想在依赖项中应用一些通用逻辑时,例如在 Spring 中的 AOP,这非常有用。我们将在第六章中看到有关 AOP 的详细信息,面向切面编程和拦截器。
-
您的模块可以受益于容器提供的先进功能。
Spring、Google Guice 和 Dagger 是目前可用于 Java 的 IoC 容器之一。从企业版 6.0 版本开始,Java 引入了 上下文依赖注入(CDI),这是一个企业版中的依赖注入框架。它与 Spring 的基于注解的 DI 实现大致相似。在所有上述容器中,Spring 是目前最流行和最广泛使用的 IoC 容器。
摘要
在软件范式下,始终建议将整个系统分解成可以独立执行特定任务的小模块。DIP 是构建模块化系统的重要原则之一。在本章中,我们看到了高级模块不应依赖于低级模块,两者都应依赖于抽象(DIP 的概念)。
我们详细学习了如何通过 IoC 实现 DIP。设置控制反转可以使系统松散耦合。我们还学习了各种设计模式,如工厂、服务定位器和依赖注入,以实现 IoC。
之后,我们学习了依赖注入模式的多种类型。最后,我们讨论了 IoC 容器及其在构建模块化系统中的有用性。
在下一章中,我们将讨论 Java 9 中的模块化概念和依赖注入。
第二章:Java 9 中的依赖注入
在上一章中,我们通过编写代码了解了依赖注入原则、不同场景下的 IOC 以及不同类型的依赖注入。
在本章中,我们将了解 Java 9 提供的新特性。具体来说,我们将学习 Java 9 中的模块化,模块化框架,Java 9 提供的模块类型,以及我们将看到使用模块的依赖注入。
本章将主要涵盖以下主题:
-
Java 9 简介
-
Java 9 中的模块化框架
-
使用 Java 9 模块化框架进行依赖注入
Java 9 简介
在学习 Java 9 的新特性之前,我们需要了解 Java 的一个重要组成部分,那就是Java 开发工具包(JDK)。
JDK 是一个包含Java 标准版(Java SE)、Java 企业版(Java EE)、Java 微型版(Java ME)平台以及不同的工具(如 javac、Java 控制台、JAR、JShell、Jlink)的集合,它为开发、调试和监控基于 Java 的应用程序提供了所有库。
Java 9 在 JDK 的不同类别中推出了近 100 个新特性和增强,如工具、安全、部署、性能调整、核心库 API 更改和 javadoc。
关键特性
让我们简要地看看 Java 9 的一些关键特性,这些特性将改变 Java 软件开发:
-
Java 平台模块系统(JPMS)
-
JShell(REPL)——Java Shell
-
JLink——模块链接器
-
多版本 JAR 文件
-
Stream API 增强
-
栈跟踪 API
-
带有便捷工厂方法的不可变集合
-
支持 HTTP 2.0
Java 平台模块系统
Java 平台模块系统(JPMS)的引入是 Java 9 和 JPMS 的一个关键特性,也是游戏规则改变者,JPMS 是在 Jigsaw 项目下开发的。
项目 Jigsaw 的主要目标如下:
-
可伸缩的 JDK:直到 Java 8,JDK 的工程是稳固的,包含了许多组件,这使得维护和开发变得麻烦。JDK 9 被划分为一系列独立的模块,这允许我们的应用程序只包含所需的模块,从而有助于减少运行时大小。
-
强健的封装和安全:如果需要,模块可以从模块中明确地暴露出来。另一方面,另一个模块必须明确定义需要从模块中获取哪些特定的包。这样,模块可以为了安全目的封装特定的包。
-
依赖关系:现代模块框架允许我们定义模块之间以及所有所需子模块之间的明确依赖关系,所有所需的子模块依赖关系都可以在编译时区分。
-
现代重建允许我们包含模块的运行时镜像,这为 JDK 提供了更好的性能。它还从运行时镜像中移除了
tools.jar和rt.jar。 -
为了保护运行时图像的内部结构,使用了一个未使用的 URI 策略来命名模块、资源和类。
我们将在 Java 9 的模块化框架 部分详细讨论 JPMS。
JShell (REPL) – Java Shell
在早期的 JDK 中,我们没有使用命令行界面的便利来运行代码。为了学习新的功能,例如正则表达式 API 的 matches 函数等,我们必须编写必要的 Java 骨架,public static void main(String[] args),并经历编译和执行阶段。
Java 9 引入了 JShell,这是一个命令行工具。它使用 读取-评估-打印循环(REPL)原则提供命令行界面与 Java 平台交互,并提供一种无需编写必要骨架的交互式运行程序的方式。
JShell 提出了一个解析器,它可以解析提交的代码并识别不同类型,如变量、声明的方法、循环等,并将它们全部组合在一个虚拟骨架中,形成一个完整的 Java 程序以传递给编译器。基于输入的编译器,它将其转换为字节码。在这个过程中,不会创建文件,所以所有内容都将保存在内存中。最后,生成的字节码被 JVM 用于加载和执行。
JShell 位于发货 JDK 9 的 bin 目录中。使用命令界面,进入 bin 目录并输入命令 JShell 以启动工具:

让我们考虑一个我们通常在 IDE 中编写的程序。这是一个简单的程序,用于将字符串消息打印为大写:
module javaIntroduction {
}
package com.packt.java9dependency.examples;
public class Main {
public static void main(String[] args) {
String s = "hello java module system".toUpperCase();
System.out.println(s);
}
}
现在,我们可以通过直接在 JShell 工具中编写语句来快速输出前面的字符串消息,声明变量和 println 语句是不必要的。JShell 提供了各种命令功能,使得开发者编写快速代码片段时生活更加轻松。

JLink – 模块链接器
当我们谈论模块化系统时,一个立即的问题就是模块的依赖关系将如何组织,以及它将对最终部署产生什么影响?
JLink 工具旨在在编译时间和运行时间之间提供可选阶段,称为链接时间,它将一组模块及其传递依赖项链接到创建运行时图像。JLink 使部署更加简单,并减少了应用程序的大小。
jLink 的调用语法如下:
jlink --module-path <modulepath> --add-modules <modules> --limit-modules <modules> --output <path>
--module-path - jLink use module path for finding modules such as modular jars, JMOD files
--add-modules - Mention module which needs to include in default set of modules for run time image, by default set of modules in empty.
--limit-modules - Use this option to limits modules, which is required for our application.
--output - Final resulting run time image will be stored in output directory
--help - list details about jLink options
--version - show the version number
多版本 JAR 文件
我们已经看到了许多第三方库,支持多个 Java 版本并具有向后兼容性。正因为如此,它们不使用 JDK 新版本中引入的最新 API 功能。自从 Java 8 以来,没有设施来定义基于条件的平台依赖来使用新功能。
Java 9 引入了多版本 JAR 概念。它允许开发者为每个类创建替代版本,这些版本仅在运行特定 Java 版本时使用。

前面的图示展示了多版本 jar 的结构。它包含资源、类以及用于元数据的 Meta-INF 目录。这个元数据文件包含特定版本的详细信息,用于将 jar 文件编码,以提供在目标 Java 平台上运行的多版本库的兼容方法。
为了继续前面的例子,我们可以看到 javaMutipleJar 库在根级别有三个类,Class1.class、Class2.class 和 Class3.class,使用 Java 8 构建。如果我们在这个不支持 MARJAR 的 JDK 中部署这个 jar,那么只有根级别的类将是可见的,后续平台类将被忽略。
此外,Class1.class 和 Class2.class 想要使用 Java 9 特性,因此只有这些类会与 Java 9 编译捆绑。当 Java 10 来临时,如果 Class1.class 想要使用 Java 10 特性,那么,如图中所述,由于 MARJAR 概念,它将与 Java 10 平台捆绑。
最终,多版本 jar 概念帮助第三方库和框架开发者轻松地分离特定 JDK 的新 API 使用,以支持迁移,同时继续支持旧版本。
Stream API 的增强
Stream 是一个包含元素以顺序形式存在的管道,用于支持对数据集合的聚合操作。Stream API 是 Java 8 的一个主要特性,它提供了基于标准的过滤和顺序或并行执行,所有这些统称为 Stream 的内部迭代。
Java 9 增加了四个新方法,以使 Stream API 在迭代操作方面更加完善。dropWhile 和 takeWhile 方法是默认方法,而 iterate 和 ofNullable 是 java.util.stream 接口中的静态方法。让我们来讨论 takeWhile 方法的用法。
Stream API 语法:
default Stream<T> takeWhile(Predicate<? super T> predicate)

takeWhile() 方法返回与有序流中谓词匹配的最长前缀。从前面的代码中可以看出,takeWhile 返回前三个元素,因为它们与谓词匹配。
对于无序流,takeWhile() 方法返回一个前缀元素,直到谓词条件为真。如果谓词条件返回假,则停止迭代,并返回一个元素列表,这些元素在谓词评估直到条件首次失败。
堆栈跟踪 API
为了调试异常,我们通过遍历堆栈跟踪来查看异常的根本原因。在 Java 9 之前,我们都使用 Thread.getStackTrace() 来获取以数组形式存在的 StackTraceElement 对象。
StackTraceElement: StackTraceElement 的每个元素都是一个单独的 StackFrame,它提供了关于类名、方法名、文件名和异常生成的行号等详细信息。除了第一个 StackFrame 之外,所有其他元素都代表从应用程序的起点到异常生成的点的调用方法。这在我们需要审计生成的错误日志时非常有用。
Java 9 StackWalker API 提供了过滤、断言和跳过堆栈跟踪中某些类等几个功能。我们可以在任何时刻获取当前线程的完整堆栈跟踪或简短堆栈跟踪。
StackWalker 提供了各种用于捕获有关堆栈信息的方法,例如:
-
forEach: 对于当前线程,它返回每个 StackFrame 流以执行操作
-
getInstance(): 这将返回 StackWalker 的当前实例
-
walk(): 这用于为当前线程的每个 StackFrame 打开一个顺序流,其中我们可以应用如限制、跳过和过滤等函数
List<StackFrame> stack = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE).walk((s) -> s.collect(Collectors.toList()));
com.packt.java9dependency package:
List<StackFrame> frames = StackWalker.getInstance().walk(s -> s.dropWhile(f -> f.getClassName().startsWith("com.packt.java9dependency")).limit(10).collect(Collectors.toList()));
带有便利工厂方法的不可变集合
许多时候我们直接从 factory 方法返回的集合中添加或删除元素。这个集合是不可变的,将这些项目添加到这些集合中会引发一个名为 UnSupportedOperationException 的异常。
为了避免此类情况,我们通过使用 collections.unmodifiableXXX() 方法创建不可变集合对象。这些方法也很繁琐,例如,需要编写多个语句来添加单个项目,然后将它们添加到不可变的 List、Set 或 Map 中:
Before Java 9,
List<String> asiaRegion = new ArrayList<String>();
asiaRegion.add("India");
asiaRegion.add("China");
asiaRegion.add("SriLanka");
List<String> unmodifiableAsiaRegionList = Collections.unmodifiableList(asiaRegion);
Java 9 提供了便利的不可变工厂方法,如 List.of(), Set.of() 和 Map.of(),以解决之前提到的问题:
After Java 9,
List<String> asiaRegion = List.of("India","China","SriLanka");
Set<Integer> immutableSet = Set.of(10, 15, 20, 25);
HTTP/2.0 支持
我们过去使用 HttpURLConnection 连接服务器,它在单个请求/响应周期中工作,这最终增加了网页加载时间和延迟。
此外,较旧 JDK 的 HTTP/1.1 与 JAVA 9 的 HTTP/2 之间的区别在于,在客户端和服务器之间传输数据时数据被分帧。HTTP/2 使用 HttpClient API 通过服务器推送功能推送数据,这使得我们可以优先发送加载网页所需的数据。以下示例显示了 GET 方法的 HTTP 交互:
//Get the HttpClient object
HttpClient client = HttpClient.newHttpClient();
// GET Method call
HttpResponse<String> getResponse = client.send(
HttpRequest
.newBuilder(new URI("http://www.xyz.com/")
.GET()
.build(),
BodyHandler.asString()
);
//Response of call
int responseStatus = getResponse.statusCode();
String body = responseStatus.body();
Java 9 的模块框架
在上一节中,我们简要讨论了几个 Java 9 的特性。现在,在本节中,我们将学习模块框架及其在 Java 9 中的使用方法。
什么是模块化?
在我们转向 Java 平台模块系统之前,让我们了解现实世界中模块化的含义。
模块化是一种将系统划分为称为模块的较小部分的设计。如今,家庭中安装了模块化厨房。这类厨房包括多个单元或模块,如壁柜、橱柜、水槽等,所有这些不同的部件都是在工厂中制造的。如果任何单元在任何时候出现损坏,那么只需要更换或修理该模块。
另一个熟悉的模块化系统是墙上的电源插座,它允许你插入不同类型的电器,如微波炉、搅拌机、冰箱、电视等,它们都是设计来完成特定任务的。这些设备在任何插座上都能工作,无论它是在我们的家中还是邻居的家中,它们只是在插入时执行任务和功能。
在计算机系统方面,模块化是多个独立模块在一个单一系统中组合和链接的概念。它增加了可用性,消除了代码的重复,并使系统松散耦合。类似于电源插座的概念,模块应该在不关心它们被插入到应用程序中的位置的情况下执行任务。

Java 平台模块系统
Java 平台模块系统(JPMS)也称为 JSR 376,是在 Jigsaw 项目下实现的。我们必须了解为什么我们需要 Java 中的模块化系统以及当前 JDK 中的变化。
需要 Java 模块化系统
运行小型或大型应用程序都需要一个最高支持 Java 8 的运行环境,因为 JDK 是构建来支持单体设计的。所有库都紧密耦合在一起,并且部署时需要完整的 JDK。
可维护性:我们都希望应用程序能够松散耦合、高度统一、易于阅读和理解。因此,我们使用类和包。随着 Java 在规模和包方面的指数级增长,每天都在提供更好的功能,但包之间的依赖性却受到了影响。因此,我们需要一些新的东西,它比包更好地维护我们的代码库。
JAR 地狱:在 Java 9 之前,JVM 没有考虑到类路径上的 JAR 是如何依赖于另一个 JAR 的。它本质上加载了一堆 JAR 文件,但不会验证它们的依赖关系。当缺少 JAR 时,JVM 在运行时中断执行。JAR 文件没有定义可访问性约束,如公开或私有。类路径上所有 JAR 文件的全部内容对类路径上的其他 JAR 文件都是完全可见的。无法声明 JAR 中的某些类是私有的。所有类和方法都与类路径公开相关。有时,我们有一些包含单个类多个版本的 JAR 文件。Java ClassLoader 只加载这个类的单个版本,并且它不决定哪个版本。这导致我们程序的工作方式存在不确定性。这个问题被称为JAR 地狱。Java 9 中提出的模块路径概念倾向于阐明由类路径引起的问题。
隐式依赖:我们都见过几次NoClassDefFoundError错误。它发生在 JVM 无法发现它正在执行的代码所依赖的类时。找到依赖代码和丢失的依赖关系很简单,但不在 classLoader 中的依赖关系很难识别,因为同一个类可能被多个 class loaders 堆叠。当前的 JAR 框架无法表达哪个 JAR 文件是依赖的,以便 JVM 理解和解决依赖关系。
缺乏强封装:Java 的可视性修饰符为同一包中的类提供了强大的封装。当前的 Java 封装与 ClassPath 一起工作,其中每个公开类对其他类都是可见的,因为几个关键的 JDK API 类对其他类是公开的。
所有的前述问题都通过 Java 9 的模块概念得到了解决。
模块化 JDK
与 Java 8 相比,JDK 9 的文件夹结构已经改变;JDK 9 没有 JRE,它是单独安装到一个不同的文件夹中。在 JDK 9 中,我们可以看到一个名为 jmod 的新文件夹,其中包含所有 Java 平台模块。从 Java 9 开始,rt.jar和tool.jar在 JDK 中不可用:

所有 Java 模块,src,都可在..\jdk-9.0.1\lib\src文件夹中找到,每个模块都包含module-info.java。以下是一个显示 JDK 如何包含所有模块的图表:

每个其他模块都隐式或显式地依赖于java.base模块。它遵循有向无环图依赖关系,这意味着模块之间不允许存在循环依赖。
JDK 9 中的所有模块默认都依赖于基础模块,称为java.base模块。它包括java.lang包。
我们可以使用命令-- list-modules列出 Java 9 的所有模块。每个模块名称后面跟着一个版本号,格式为-@9,以指示该模块属于 Java 9。JDK 9 特定的模块以关键字jdk为前缀,例如jdk.compiler,而 JAVA SE 特定的模块以java关键字开头。
什么是模块?
当我们讨论模块化系统时,你可能会立即问什么是模块?模块是一组具有自描述属性的代码、数据和资源。它包含一系列包和类型,如类、抽象类、接口等,最重要的是,每个模块都包含一个module-info.java文件。
模块可以显式声明需要导出到其他模块的包,以及为了编译和运行需要从其他模块获取的内容。这也有助于我们在出错时识别缺少的模块。
模块的架构
模块是 JPMS 的主要构建块。模块类似于 JAR 文件,但具有额外的特性,例如:
-
模块名称: 一个用于全局识别的唯一名称;可以使用反向 URL 命名约定来定义名称
-
声明对其他模块的依赖
-
需要导出为包的 API 声明

模块描述符(module-info.java)
module-info.java是模块化系统中的一个重要文件,它包含描述模块行为的模块元数据。它是一个 Java 文件,但它与传统 Java 文件不同。它有自己的语法,并且编译成module-info.class文件。
以下是我们创建module-info.java时必须遵循的语法:
module <module-name> {
requires <module-name-1>;
requires <module-name-2>;
.
.
requires <module-name-n>;
exports <package-1>;
.
. exports <package-n>;
}
以下是一个module-info.java文件的示例,其中每个模块包含一个唯一的 ID 和可选的模块描述符详细信息:
module com.packt.java9dependency.chapter2 {
exports com.packt.java9dependency.chapter2.services;
requires com.packt.java9dependency.chapter1;
requires java.sql;
}
让我们了解这里提到的不同模块描述符:
-
requires
: 此描述符指定一个模块依赖于另一个模块以运行此模块,这种关系称为模块依赖关系。在运行时,模块只能看到所需的模块,这称为可读性。 -
requires transitive
: 这意味着表示对另一个模块的依赖,并且还要保证其他模块在读取你的模块时读取该依赖——称为隐式可读性。例如,模块 A 读取模块 B,模块 B 读取模块 C,那么模块 B 需要声明 requires transitive,否则模块 A 将无法编译,除非它们显式地读取模块 C。 -
requires static
: 通过使用静态关键字,依赖关系将在编译时进行验证,在运行时是可选的。 -
exports
: 此描述符用于将自身的包导出到其他模块。 -
exports
to : 通过使用此类描述符语句,我们将包导出到特定的模块,而不是所有模块。这被称为有条件导出。 -
opens <包名>:用于定义的打开描述符,只有公共类型的包在运行时才能通过反射访问其他模块中的代码。
-
opens <包名> to <模块名>:一个有资格的打开。这仅打开一个特定的模块,该模块可以在运行时仅通过反射访问公共类型包。
-
uses <服务接口>:为该模块定义的模块指令定义了服务。这使得模块成为服务消费者。服务实现可以位于同一模块或另一个模块中。
-
provide <服务接口> with
, :指定模块包含在模块的uses描述符中定义的接口的服务实现。这使得模块成为服务提供者。
在创建模块描述符时,我们需要理解以下重要要点:
-
module-info只能有一个模块名称;这意味着导出或需求子句不是必需的。 -
如果模块描述符只有导出,那么这意味着它只向其他模块导出声明的包,并且它不依赖于任何其他模块。我们可以称它们为独立模块。例如,
java.base模块。 -
与前一点相反,模块描述符可能包含导出和需求子句,这意味着该模块正在向其他模块导出包,并且它也依赖于其他模块进行自己的编译。
-
模块描述符中可能有零个或多个导出或需求子句。
当我们创建项目时,JDK 8 将一个 JDK jar 文件添加到我们的项目类路径中。但是,当我们创建 Java 9 模块项目时,JDK 模块将被添加到模块路径中。
模块类型
模块有多种类型:
命名应用程序模块:这是一个我们可以创建的简单模块。任何第三方库都可以是应用程序模块。
平台模块:正如我们所见,JDK 9 本身迁移到了模块化架构。所有现有功能都将作为不同的模块提供,例如 java.sql、java.desktop、java.transaction。这些被称为平台模块。所有模块都隐式依赖于 java.base 模块。
自动模块:一个预 Java 9 的 JAR 文件,未迁移到模块,可以放置在模块路径中而不需要模块描述符。这些被称为自动模块。这些 JAR 文件隐式导出所有包供其他模块使用,并读取其他模块以及未命名的模块。因为自动模块没有唯一的名称,JDK 根据文件名生成依赖项,通过删除版本号和扩展名。例如,文件 postgresql-42.1.4.jar 作为模块将是 postgresql。
未命名的模块:JDK 9 并不删除类路径。因此,放置在类路径上的所有 JAR 文件和类都被称为未命名的模块。这些模块可以读取所有模块并导出所有包,因为它们没有名称。这个模块不能被命名应用程序模块读取或要求。
在 JDK 9 中,我们有两条模块路径和类路径。现在我们可能会问哪个 JAR 放在哪里?所以,答案是,一个包含应用程序模块的模块化 JAR 放入 --module-path,一个包含未命名模块的模块化 JAR 可以放入 --class-path。同样,一个非模块化 JAR 可以迁移到一个自动模块中,并放入 --module-path。如果一个 JAR 包含一个未命名的模块,那么它位于 --class-path。
使用 Java 9 模块框架进行依赖注入
我们将要学习的最后一个主题是分子性和 Java 9 模块的基础。现在,我们将学习如何编写模块以及如何在模块中处理依赖注入。
Java 9 引入了服务加载器的概念,这与 IoC 和依赖注入相关。新的模块系统不提供依赖注入,但可以通过服务加载器和 SPI(服务提供者接口)模式实现相同的功能。现在我们将看看这如何在 Java 9 中工作。
带有服务加载器的模块
服务是一组接口和类,统称为库,它提供特定的功能。简单来说,我们可以称之为 API。服务有多种用途,它们被称为服务提供者(或者说实现)。利用这个服务的客户端将不会与实现有任何接触。这可以通过利用底层概念来实现。
Java 有 ClassLoader,它简单地加载类并在运行时创建类的实例。与 Java 9 模块相比,java.util.ServiceLoader 能够在运行时找到一个服务的所有服务提供者。ServiceLoader 类允许 API 和客户端应用程序解耦。服务加载器将实例化所有实现服务的服务提供者,并使其可供客户端使用。
让我们以一个包含 API 和 API 的不同实现的 Notification 应用程序为例。我们将创建三个模块,第一个是一个包含服务的(API)模块,第二个将是一个提供者(实现)模块,最后一个将是一个用于访问服务的客户端模块。
服务(API)模块
创建的名为 com.packt.service.api 的 API 模块包含一个 NotificationService 接口,用于发送通知和加载服务提供者。为了使这个接口成为一个服务提供者接口(SPI),我们必须在 module-info.java 中提到 'use' 子句。我们的模块代码如下:
NotificationService.java
package com.packt.service.api;
import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;
public interface NotificationService {
/* Loads all the service providers */
public static List<NotificationService> getInstances() {
ServiceLoader<NotificationService> services = ServiceLoader.load(NotificationService.class);
List<NotificationService> list = new ArrayList<>();
services.iterator().forEachRemaining(list::add);
return list;
}
/* Send notification with provided message and recipient */
boolean sendNotification(String message, String recipient);
}
module-info.java 将如下所示:
module com.packt.service.api {
exports com.packt.service.api;
uses com.packt.service.api.NotificationService;
}
以下是需要遵循的命令行步骤,用于 com.packt.service.api 模块 JAR。假设在 com.packt.service.api 模块中会有一个输出目录:

服务提供者(实现)模块
现在,创建一个名为 com.packt.service.impl 的服务提供者模块来实现 NotificationService 服务 API,为此我们应在 module-info.java 文件中定义一个 "provides ... with" 子句。provides 关键字用来提及服务接口名称,而 with 关键字用来提及我们想要加载的实现。如果模块在模块描述符文件中没有提供语句,服务加载器将不会加载该模块。'provides...with' 语句的语法如下:
provides <service-interface> with <service-implementation>
为了向收件人发送短信,我们创建了两个实现类,SMSServiceImpl.java 和 EmailServiceImpl,通过实现 NotificationService:
SMSServiceImpl.java
package com.packt.service.impl;
import com.packt.service.api.NotificationService;
public class SMSServiceImpl implements NotificationService {
public boolean sendNotification(String message, String recipient) {
// Code to send SMS
System.out.println("SMS has been sent to Recipient :: " + recipient + " with Message :: "+message);
return true;
}
}
此提供者模块的模块描述符如下所示:
module-info.java
module com.packt.service.impl {
requires com.packt.service.api;
provides com.packt.service.api.NotificationService with com.packt.service.impl.SMSServiceImpl;
}
为了生成 com.packt.service.impl 模块的 jar 文件,我们必须将服务 API 模块的 notification-api.jar 复制到 lib 文件夹中,以便在编译时解决依赖关系。以下命令的输出将是 sms-service.jar:

服务提供者规则:
-
它总是有一个无参构造函数。这个构造函数被
ServiceLoader类用来通过反射实例化服务提供者。 -
提供者必须是一个公共的具体类。它不应该是一个抽象类或内部类。
-
实现类的出现必须与服务接口一致。
服务客户端应用程序
现在,创建一个名为 com.packt.client 的客户端应用程序,通过调用 getInstances() 方法列出所有 NotificationService 的实现。客户端应用程序在 module-info.java 中只需要 requires com.packt.service.api 模块作为依赖项。但我们必须将 notification-api.jar 和 sms-service.jar 复制到 lib 文件夹中,以解决服务 API 和服务提供者模块的编译时依赖关系。我们的 ClientApplication.java 和 module-info.java 将如下所示:
ClientApplication.java
package com.packt.client;
import java.util.List;
import com.packt.service.api.NotificationService;
public class ClientApplication {
public static void main(String[] args) {
List<NotificationService> notificationServices = NotificationService.getInstances();
for (NotificationService services : notificationServices) {
services.sendNotification("Hello", "1234567890");
}
}
}
对于我们的客户端应用程序,我们只需要在 module-info.java 文件中提及 com.packt.service.api 的 requires 子句:
module-info.java
module com.packt.client {
requires com.packt.service.api;
}
以下是需要运行以运行我们的客户端应用程序的命令。在输出中,我们将从 SMSServiceImpl.java 获得一条消息:

使用命令行界面编写模块化代码
作为一种传统,让我们创建一个简单的模块,命名为 helloApp,它将包含一个简单的消息,并且将被另一个名为 helloClient 的模块所需要。在这里,我们将使用命令行界面来创建和运行该模块。
创建一个名为 com.packt.helloapp 的 helloApp 模块文件夹和一个名为 com\packt\helloapp 的包文件夹:
mkdir com.packt.helloapp
mkdir com.packt.helloapp\com\packt\helloapp
现在,在包名 com.packt.helloapp\com\packt\helloapp 下创建一个 HelloApp.java 组件类,并在根文件夹 com.packt.helloapp 中创建一个 modue-info.java 文件:
HelloApp.java
package com.packt.helloapp;
public class HelloApp {
public String sayHelloJava() {
return "Hello Java 9 Module System";
}
}
module-info.java
module com.packt.helloapp {
// define exports or requires.
}
现在,我们将创建另一个名为helloClient的模块。创建一个名为com.packt.hello.client的helloClient模块,并使用名为com\packt\hello\client的包:
mkdir com.packt.hello.client
mkdir com.packt.hello.client\com\packt\hello\client
让我们在com.packt.hello.client\com\packt\hello\client包下创建另一个名为HelloClient.java的组件类,并在根目录com.packt.hello.client下创建一个module-info.java文件:
helloClient.java
package com.packt.hello.client;
public class HelloClient {
public static void main (String arg[]) {
//code
}
}
module-info.java
module com.packt.hello.client {
//define exports or requires
}
这两个模块都是独立模块,因此它们之间没有依赖关系。但是,如果我们想在HelloClient类中使用名为sayHelloJava()的方法,那么我们必须导入该模块,否则将会在编译时出现错误package com.packt.helloapp is not visible。
定义模块之间的依赖关系
要使用HelloApp,我们需要从helloApp模块导出包com.packt.helloapp,并将helloApp模块包含在helloClient模块中:
module com.packt.helloapp {
exports com.packt.helloapp;
}
module com.packt.hello.client {
requires com.packt.helloapp;
}
从前面的代码中,第一个模块描述符中的exports关键字表示包可供其他模块导出。如果一个包被显式导出,那么它只能被其他模块访问。如果在同一个模块中某些包没有被导出,那么它们就不能被其他模块访问。
第二个模块描述符使用requires关键字来指示该模块依赖于com.packt.helloapp模块,这被称为 Java 9 模块的依赖注入。
最后,HelloClient类将如下所示:
HelloClient.java
package com.packt.hello.client;
import com.packt.HelloApp;
public class HelloClient {
public static void main (String arg[]) {
HelloApp helloApp = new HelloApp();
System.out.println(helloApp.sayHelloJava());
}
}
创建了两个模块后,最终的树结构如下:

但是等等,我们只写了代码,还没有编译和运行它。让我们在下一节中完成这个操作。
编译和运行模块
让我们先编译HelloApp模块,然后编译HelloClient模块。在运行命令之前,请确保 Java 9 ClassPath 已设置。要编译模块代码,需要运行以下命令:
javac -d output com.packt.helloapp\com\packt\helloapp\HelloApp.java com.packt.helloapp\module-info.java
编译成功后,它将在输出目录中生成HelloApp.class和module-info.class。
由于我们的HelloApp模块被HelloClient模块所依赖,我们应该生成com.packt.helloapp模块 jar 并将其包含在HelloClient模块中。要在mlib文件夹中创建 jar,请运行以下 jar 命令:
jar -c -f mlib\com.packt.helloapp.jar -C output .
现在,通过运行以下命令删除输出目录,然后为第二个模块再次创建输出目录:
rmdir /s output
为了编译HelloClient模块,我们需要提供一个com.packt.hellpapp.jar的引用和javac命令,并提供一种方法来传递module-path以引用其他模块。在这里,我们将mlib目录作为模块路径传递。没有module-path,com.packt.hello.client模块的编译将无法进行:
javac --module-path mlib -d output com.packt.hello.client\module-info.java
javac --module-path mlib -d output com.packt.hello.client\com\packt\hello\client\HelloClient.java
现在,让我们使用以下命令运行模块:
java –-module-path “mlib;output” -m com.packt.hello.client/com.packt.hello.client.HelloClient
输出结果如下:

在前面的示例结束时,我们学习了如何创建模块并在 Java 模块中定义依赖注入。以下图表显示了模块之间的依赖关系:

摘要
在这里,Java 9 中的依赖注入之旅就此结束。让我们总结一下本章所学的内容。首先,我们学习了 Java 9 中引入的关键特性,例如 Java 平台模块系统、JShell、JLink 工具、JAR 的多版本发布、增强的流 API、堆栈跟踪 PI、不可变集合方法以及 HTTP 2.0。
其次,在 Java 9 的模块化框架部分,我们学习了模块化的含义以及模块化设计在 Java 应用中的必要性。我们还详细了解了 JPMS 如何将早期的 JDK 转变为模块化 JDK。
之后,我们了解了一个模块化系统的重要元素,即模块。我们看到了如何通过不同的模块描述符和模块类型来定义模块结构。
最后,我们学习了如何使用命令编写简单的模块,以了解 Java 9 中模块间的依赖注入是如何工作的。
在下一章中,我们将详细讨论 Spring 框架中依赖注入的概念。
第三章:使用 Spring 进行依赖注入
到目前为止,我们已经学习了为什么模块化在编写更干净、可维护的代码中如此重要。在第一章,“为什么需要依赖注入?”,我们学习了依赖倒置原则(DIP)、IoC(一种实现 DIP 的设计方法)以及各种设计模式来实现 IoC。依赖注入(DI)是实现 IoC 的设计模式之一。
在第二章,“Java 9 中的依赖注入”,我们学习了模块化框架和 DI 在 Java 9 中的实现方式。在本章中,我们将继续我们的旅程,学习在 Spring 中实现 DI——这是最受欢迎和最广泛使用的框架之一,用于实现企业应用程序。
在本章中,我们将探讨以下主题:
-
对 Spring 框架的简要介绍
-
Spring 中的 Bean 管理
-
如何使用 Spring 实现 DI
-
自动装配:自动解决依赖的功能
-
基于注解的 DI 实现
-
基于 Java 配置的 DI 实现
对 Spring 框架的简要介绍
Spring 是一个轻量级和开源的企业框架,它早在 2003 年就创建了。模块化是 Spring 框架的核心。正因为如此,Spring 可以从表示层到持久化层使用。
好处在于,Spring 不会强迫您在所有层都使用 Spring。例如,如果您在持久化层使用 Spring,您可以在控制器层的表示层自由使用任何其他框架。
Spring 的另一个优点是其基于普通老式 Java 对象(POJO)模型的框架。与其他框架不同,Spring 不会强迫您的类扩展或实现 Spring API 的任何基类或接口;然而,Spring 提供了一套类,用于使用其他框架,例如 ORM 框架、日志框架、Quartz 定时器以及其他第三方库,这些库将帮助您将这些框架与 Spring 集成。
更多内容:Spring 允许您在不更改代码的情况下更改类似的框架。例如,您可以通过更改配置来选择不同的持久化框架。这也适用于与 Spring 集成的第三方 API。
Spring 是一个基于 POJO 的框架;一个 servlet 容器就足以运行您的应用程序,不需要完整的应用服务器。
Spring 框架架构
Spring 是一个模块化框架。这为您选择所需的模块提供了极大的灵活性,而不是将所有模块都集成到您的代码中。Spring 包含大约 20 个模块,它们逻辑上分为以下几层:
-
核心容器层
-
数据访问/集成层
-
Web 层
-
测试层
-
杂项层
核心容器层
作为框架的主要部分,核心容器涵盖了以下模块:
Spring 核心:正如其名所示,它提供了框架的核心功能,包括 IoC 容器和 DI 机制。IoC 容器将配置和依赖关系管理从应用程序代码中隔离出来。
Spring beans:此模块提供 bean 工厂以创建和管理 bean(对象)的生命周期。它是一个工厂模式的实现。
Spring 上下文:此模块建立在核心和 bean 模块之上。其入口点是加载配置和访问对象。在 bean 模块之上,上下文模块提供了一些额外的功能,如事件传播、动态创建上下文、国际化等。
Spring 表达式语言(SpEL):这是一个用于在 JSP 中动态访问和操作对象的表达式语言。它是 JSP 2.1 规范中表达式语言(EL)的扩展。使用 SpEL 可以使 JSP 代码更简洁、更易读、更易于维护。使用 SpEL 的主要好处包括:
-
轻松设置和获取对象的属性值
-
它可以直接调用控制器方法以获取数据
-
它用于直接从 Spring 的应用程序上下文(IoC 容器)检索对象
-
它支持各种列表操作,如投影、选择、迭代和聚合
-
它提供逻辑和算术运算
数据访问/集成层
Spring 数据访问和集成层用于数据操作和其他集成。它包括以下模块:
-
事务:此模块帮助以编程和声明性方式维护事务。此模块支持 ORM 和 JDBC 模块。
-
对象 XML 映射(OXM):此模块提供对象/XML 处理的抽象,可用于各种 OXM 实现,如 JAXB、XMLBeans 等,以与 Spring 集成。
-
对象关系映射(ORM):Spring 不提供自己的 ORM 框架;相反,它通过此模块促进与 Hibernate、JPA、JDO 等 ORM 框架的集成。
-
Java 数据库连接(JDBC):此模块提供处理 JDBC 的所有底层样板代码。您可以使用它使用标准 JDBC API 与数据库交互。
-
Java 消息服务(JMS):此模块支持 Spring 中消息系统的集成。
Spring 网络层
Spring 网络层用于创建基于 Web 的应用程序。它由以下模块组成:
-
Web:此模块提供基本的 Web 相关功能,如多部分文件上传(借助 JSP 中的 Spring 自定义标签)。它还负责在 Web 上下文中初始化 IoC 容器。
-
Servlet:此模块为基于 Web 的应用程序提供 Spring MVC(模型-视图-控制器)的实现。它提供了视图(表示层)与模型(业务逻辑)的清晰分离,并通过控制器控制它们之间的流程。
-
端口:此模块为端口提供 MVC 实现,主要用于门户环境。
Spring 测试
这提供了对 JUnit 和 TestNg 等单元测试框架的支持,以进行单元和集成测试。我们将在本章后续部分看到如何使用 Spring 进行单元测试,所以请继续阅读。
杂项
一些额外的模块也是 Spring 框架的一部分:
-
方面和 AOP:这些模块提供了一种动态地在多个应用层应用通用逻辑(在 AOP 术语中称为关注点)的功能
-
仪表:此模块提供类仪表化和类加载器实现功能
-
消息传递:此模块为与基于 STOMP 的客户端通信提供对流文本导向消息协议(STOMP)的支持
Spring 容器中的 Bean 管理
当任何软件应用正在执行时,会创建一组对象,它们相互交互以实现特定的业务目标。作为一个基于 POJO 的编程模型,Spring 框架将你的应用程序中类的所有对象视为 POJO 或豆(以 Spring 友好的方式)。
这些对象或豆类应该以独立的方式存在,以便可以在不引起其他对象变化涟漪效应的情况下重用或更改。以这种方式松散耦合,它还提供了无需过多担心任何依赖项即可进行测试的好处。
Spring 提供了一个 IoC 容器,用于自动化向你的类提供外部依赖的过程。你需要提供关于你的客户端和依赖项的指令(以配置的形式)。Spring 将在运行时管理和解决所有依赖项。此外,Spring 提供了一个功能,可以在各种应用范围内保持依赖项的可用性,例如请求、会话、应用等。
在了解依赖注入之前,理解 Spring 如何管理对象的生命周期(创建、管理和销毁的过程)是至关重要的。在 Spring 中,所有这些责任都由 Spring IoC 容器执行。
Spring IoC 容器
在 Spring 中,org.springframework.beans.factory.BeanFactory接口定义了基本的 IoC 容器,而org.springframework.context.ApplicationContext接口则代表一个高级 IoC 容器。ApplicationContext是BeanFactory的超集。它通过在BeanFactory的基本 IoC 功能之上提供一些额外的企业级功能来实现这一点。
为了适应不同类型的应用程序,Spring 提供了ApplicationContext的多种实现。对于独立应用程序,你可以使用FileSystemXmlApplicationContext或ClassPathXmlApplicationContext类。它们都是ApplicationConext的实现。
在使用 Spring 的时候,您需要传递一个 XML 文件作为这些容器的入口点。此文件称为 Spring 应用程序上下文文件。当 Spring 容器启动时,它会加载此 XML 文件并开始配置您的 bean(要么是文件中的基于 XML 的 bean 定义,要么是 POJO Java 类中的基于注解的定义)。
-
FileSystemXmlApplicationContext:此容器加载 Spring XML 文件并处理它。您需要提供 XML 文件的完整路径。 -
ClassPathXmlApplicationContext:此容器的工作方式类似于FileSystemXmlApplicationContext;然而,它假定 Spring XML 文件位于CLASSPATH中。您不需要为它传递根级路径。 -
WebXmlApplicationContext:此容器通常用于 Web 应用程序中。
Spring 的 IoC 容器负责实例化、配置、维护和收集 bean(您应用程序中的对象)。您需要向 IoC 容器提供有关您想要组装的对象的配置元数据。以下图示展示了 IoC 容器如何完成这项工作的一个高级流程:

我们提供 Pojo 类(或 bean 定义)和 配置元数据(一组指令)作为输入。Spring IoC 容器将创建和管理对象(或 bean),以便它们产生一个可用的系统。简而言之,IoC 容器执行所有低级任务(管理 bean 和依赖项),这样您就可以在您的 POJO 类中编写业务逻辑。
配置
您需要向 Spring 容器提供有关如何根据您的应用程序需求配置 bean 的说明。这些说明应以配置元数据的形式提供,并且它们应向 IoC 容器说明以下内容:
-
实例化:如何从 bean 定义中创建对象。
-
生命周期:这些对象可用到何时。
-
依赖项:它们是否需要其他人?
Spring 提供了大量的灵活性,甚至在定义配置元数据方面也是如此。您可以通过以下三种方式将其提供给 IoC 容器:
-
XML 格式:Spring 应用程序上下文(XML)文件中关于 bean 的配置元数据的一个或多个条目。
-
Java 注解:将配置元数据以注解的形式放入 Java 类中。
-
纯 Java 代码:从 3.0 版本开始,Spring 开始支持使用 Java 代码定义配置。您可以使用 Java 而不是 XML 文件在应用程序类之外定义 bean。
当 Spring 应用程序启动时,它将首先加载应用程序上下文(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">
<!-- All your bean and its configuration metadata goes here -->
<bean id="..." class="...">
</bean>
</beans>
此文件必须存在于基于 XML 和基于注解的配置元数据中。在基于 XML 的配置中,您需要在此文件顶级<beans>元素下使用<bean>元素定义您的 bean。可以定义一个或多个<bean>条目。配置元数据将与<bean>元素一起使用。
在前面的 bean 定义中,id属性定义了该 bean 的标识。容器将使用它来指出特定的 bean,因此它必须是唯一的。而class属性定义了 bean 的类型,您需要在这里给出其完全限定的类名。
每个 bean 都与一个实际对象通过class属性相关联。您可以定义任何类型的类的 bean,例如您的自定义服务层类、DAO 层类、表示层类等。Spring 的容器将使用class属性来实例化对象,并应用与相应<bean>元素关联的配置元数据。
在基于注解的配置中,您的元数据将被定义为实际的 Java 类,并在该(XML)文件中;您只需使用<context:component-scan base-package="org.example"/>元素指定基本包名。我们将在本章的下一个部分,基于注解的依赖注入中看到更多关于这个内容。
容器在行动
为了轻松理解基于 Spring 的应用程序的流程,我们将以独立的应用程序容器为例:ClassPathXmlApplicationContext,或者FileSystemXmlApplicationContext。处理 Spring 的整体过程包括以下三个步骤:
-
定义 POJOs
-
创建带有配置元数据的应用程序上下文(XML)文件
-
初始化容器
定义 POJOs:正如我们在本章前面的部分所看到的,Spring 将您的应用程序中的每个对象视为 POJO。因此,首先您需要定义 POJOs。我们将使用简单的示例来理解以下片段中的概念:
package com.packet.spring.contaner.check;
public class Car{
public void showType(){
System.out.println("This is patrol car..");
}
}
提供应用程序上下文(XML)文件:创建一个 XML 文件,并将其命名为application-context.xml。为了简化,我们使用基于 XML 的配置元数据。我们将在接下来的部分中看到另外两种设置配置元数据的方式(基于注解和基于 Java 代码)。
在应用程序上下文文件(application-context.xml)中为每个模块类定义<bean>,并包括它们的配置元数据,如下面的片段所示:
<?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">
<!-- All your bean and its configuration metadata goes here -->
<bean id="myCar" class="com.packet.spring.contaner.check.Car">
</bean>
</beans>
我们为我们的 POJO Car定义了<bean>,ID 为myCar。Spring 容器使用此 ID 来获取Car bean 的对象。
初始化容器:在基于 Web 的应用程序中,当应用程序被加载到 servlet 容器中时,容器(WebXmlApplicationContext)将由 web 监听器初始化。在独立应用程序的情况下,你需要使用以下代码片段初始化容器(ClassPathXmlApplicationContext或FileSystemXmlApplicationContext):
ApplicationContext context = new ClassPathXmlApplicationContext("application-context.xml");
FileSystemXmlApplicationContext container).
ClassPathXmlApplicationContext和FileSystemXmlApplicationContext容器还有其他重载的构造函数,例如无参数构造函数和字符串数组参数构造函数,后者用于加载多个应用程序上下文(XML)文件。
Spring 容器加载到内存后不久,它就会处理应用程序上下文(XML)文件,并为相应的<bean>定义创建对象。你可以通过容器使用以下代码片段获取你的 bean 实例:
// create and configure beans
ApplicationContext context = new ClassPathXmlApplicationContext("application-context.xml");
// retrieve configured instance
Car carObj = context.getBean("myCar");
// use configured instance
carObj.showType();
当你调用getBean方法时,容器内部会调用其构造函数来创建对象,这相当于调用new()操作符。这就是 Spring 的 IoC 容器如何在 Spring 的应用上下文(XML)文件中为每个<bean>定义创建、维护和组装对象的方式。
默认情况下,Spring 以单例模式创建每个<bean>元素的对象。这意味着除非你明确告诉它不要这样做,否则容器只为每个<bean>创建并保留一个对象。当你使用getBean()方法请求<bean>对象时,它每次都会提供第一次创建后的同一对象的引用。
当容器创建与<bean>定义对应的对象时,你不需要实现任何特定的接口,也不需要以特定的方式扩展任何类或代码。只需指定<bean>的class属性就足够了。Spring 能够创建任何类型的对象。
Spring 中的依赖注入(DI)
在了解 Spring 如何管理 bean 生命周期之后,接下来我们将学习 Spring 如何提供和维护你的应用程序中的依赖。
DI(依赖注入)是一个将依赖对象提供给需要它的其他对象的过程。在 Spring 中,容器提供依赖。创建和管理依赖的流程从客户端反转到容器。这就是我们称之为IoC 容器的原因。
Spring IoC 容器使用依赖注入(DI)机制在运行时提供依赖。在第一章,“为什么需要依赖注入?”中,我们看到了各种 DI 类型,如构造函数、setter 方法和基于接口的。让我们看看如何通过 Spring 的 IoC 容器实现基于构造函数和 setter 的 DI。
基于构造函数的 DI
基于构造函数的依赖注入通常用于在对象实例化之前传递必需的依赖项。它通过具有不同参数的构造函数由容器提供,每个参数代表一个依赖项。
当容器启动时,它会检查 <bean> 是否定义了基于构造函数的依赖注入。它将首先创建依赖对象,然后将它们传递给当前对象的构造函数。我们将通过使用日志的经典示例来理解这一点。在代码的各个地方放置日志语句以跟踪执行流程是一种良好的实践。
假设你有一个名为 EmployeeService 的类,你需要在它的每个方法中添加日志。为了实现关注点的分离,你将日志功能放入一个名为 Logger 的独立类中。为了确保 EmployeeService 和 Logger 之间是独立且松散耦合的,你需要将 Logger 对象注入到 EmployeeService 对象中。让我们看看如何通过构造函数注入来实现这一点:
public class EmployeeService {
private Logger log;
//Constructor
public EmployeeService(Logger log) {
this.log = log;
}
//Service method.
public void showEmployeeName() {
log.info("showEmployeeName method is called ....");
log.debug("This is Debuggin point");
log.error("Some Exception occured here ...");
}
}
public class Logger {
public void info(String msg){
System.out.println("Logger INFO: "+msg);
}
public void debug(String msg){
System.out.println("Logger DEBUG: "+msg);
}
public void error(String msg){
System.out.println("Logger ERROR: "+msg);
}
}
public class DIWithConstructorCheck {
public static void main(String[] args) {
ApplicationContext springContext = new ClassPathXmlApplicationContext("application-context.xml");
EmployeeService employeeService = (EmployeeService) springContext.getBean("employeeService");
employeeService.showEmployeeName();
}
}
根据前面的代码,当这些对象通过 Spring 配置时,EmployeeService 对象期望 Spring 容器通过构造函数注入 Logger 对象。为了实现这一点,你需要按照以下代码片段设置配置元数据:
<?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">
<!-- All your bean and its configuration metadata goes here -->
<bean id="employeeService" class="com.packet.spring.constructor.di.EmployeeService">
<constructor-arg ref="logger"/>
</bean>
<bean id="logger" class="com.packet.spring.constructor.di.Logger">
</bean>
</beans>
在前面的配置中,Logger bean 通过 constructor-arg 元素注入到 employee bean 中。它有一个 ref 属性,用于指向具有匹配 id 值的其他 bean。此配置指示 Spring 将 Logger 对象传递给 EmployeeService bean 的构造函数。
你可以按任何顺序在这里放置 <bean> 定义。Spring 将根据需要创建 <bean> 对象,而不是根据它们在这里定义的顺序。
对于多个构造函数参数,你可以传递额外的 <constructor-arg> 元素。只要引用的对象类型(bean 的类属性)不模糊,顺序并不重要。
Spring 还支持使用原始构造函数参数进行依赖注入。Spring 提供了从应用程序上下文(XML)文件传递原始值到构造函数的功能。假设你想要创建一个具有默认值的 Camera 类对象,如下面的代码片段所示:
public class Camera {
private int resolution;
private String mode;
private boolean smileShot;
//Constructor.
public Camera(int resolution, String mode, boolean smileShot) {
this.resolution = resolution;
this.mode = mode;
this.smileShot = smileShot;
}
//Public method
public void showSettings() {
System.out.println("Resolution:"+resolution+"px mode:"+mode+" smileShot:"+smileShot);
}
}
Camera 类有三个属性:resolution、mode 和 smileShot。它的构造函数接受三个原始参数来创建具有默认值的相机对象。你需要以下方式提供配置元数据,以便 Spring 可以创建具有默认原始值的 Camera 对象实例:
<bean id="camera" class="com.packet.spring.constructor.di.Camera">
<constructor-arg type="int" value="12" />
<constructor-arg type="java.lang.String" value="normal" />
<constructor-arg type="boolean" value="false" />
</bean>
我们在 <bean> 下传递三个 <constructor-arg> 元素,对应于每个构造函数参数。由于这些是原始类型,Spring 在传递值时并不知道其类型。因此,我们需要显式传递 type 属性,它定义了原始构造函数参数的类型。
对于原始类型,只要类型不是模糊的,就没有固定的顺序来传递构造函数参数的值。在前面的例子中,所有三种类型都是不同的,所以 Spring 智能地选择正确的构造函数参数,无论你以什么顺序传递它们。
现在我们正在Camera类中添加一个名为flash的额外属性,如下所示:
//Constructor.
public Camera(int resolution, String mode, boolean smileShot, boolean flash) {
this.resolution = resolution;
this.mode = mode;
this.smileShot = smileShot;
this.flash = flash;
}
在这种情况下,构造函数参数smileShot和flash具有相同的类型(布尔型),你按照以下片段从 XML 配置中传递构造函数参数值:
<constructor-arg type="java.lang.String" value="normal"/>
<constructor-arg type="boolean" value="true" />
<constructor-arg type="int" value="12" />
<constructor-arg type="boolean" value="false" />
在前面的场景中,Spring 将选择以下内容:
-
用于分辨率的整数值
-
字符串值用于模式
-
序列中第一个布尔值(true)用于第一个布尔参数——
smileShot -
序列中第二个布尔值(false)用于第二个布尔参数——
flash
简而言之,对于构造函数参数中的相似类型,Spring 将选择序列中第一个出现的值。所以在这种情况下,顺序很重要。
这可能会导致逻辑错误,因为你将错误的值传递给了正确的参数。为了避免这种意外的错误,Spring 提供了在<constructor-arg>元素中定义零基索引的功能,如下所示:
<constructor-arg type="java.lang.String" value="normal"
index="1"/>
<constructor-arg type="boolean" value="true" index="3"/>
<constructor-arg type="int" value="12" index="0"/>
<constructor-arg type="boolean" value="false" index="2"/>
这更易于阅读且错误率更低。现在 Spring 将选择smileShot的最后一个值(index=2)和flash参数的第二个值(index=3)。索引属性解决了两个具有相同类型的构造函数参数的歧义。
如果你在<constructor-arg>中定义的type与该索引处构造函数参数的实际类型不兼容,那么 Spring 将引发错误。所以使用索引属性时,请确保这一点。
基于 setter 的依赖注入
基于 setter 的依赖注入通常用于可选依赖。在基于 setter 的依赖注入的情况下,容器首先通过调用无参数构造函数或静态factory方法创建你的 bean 实例。然后通过每个 setter 方法传递这些依赖。通过 setter 方法注入的依赖可以在应用程序的后期阶段重新注入或更改。
我们将通过以下代码库来理解基于 setter 的依赖注入:
public class DocumentBase {
private DocFinder docFinder;
//Setter method to inject dependency.
public void setDocFinder(DocFinder docFinder) {
this.docFinder = docFinder;
}
public void performSearch() {
this.docFinder.doFind();
}
}
public class DocFinder {
public void doFind() {
System.out.println(" Finding in Document Base ");
}
}
public class DIWithSetterCheck {
public static void main(String[] args) {
ApplicationContext springContext = new ClassPathXmlApplicationContext("application-context.xml");
DocumentBase docBase = (DocumentBase) springContext.getBean("docBase");
docBase.performSearch();
}
}
DocumentBase类依赖于DocFinder,我们通过 setter 方法传递它。你需要根据以下片段定义 Spring 的配置元数据:
<bean id="docBase" class="com.packet.spring.setter.di.DocumentBase">
<property name="docFinder" ref="docFinder" />
</bean>
<bean id="docFinder" class="com.packet.spring.setter.di.DocFinder">
</bean>
基于 setter 的依赖注入可以通过在<bean>标签下的<property>元素中定义。name属性表示setter方法的名称。在我们的例子中,property元素的name属性是docFinder,因此 Spring 会调用setDocFinder方法来注入依赖。查找setter方法的模式是在set前缀前加上并使第一个字符大写。
<property>元素的name属性是区分大小写的。所以,如果你将名称设置为docfinder,Spring 将尝试调用setDocfinder方法并显示错误。
就像构造函数 DI 一样,Setter DI 也支持根据以下代码片段提供原始类型的值:
<bean id="docBase" class="com.packet.spring.setter.di.DocumentBase">
<property name="buildNo" value="1.2.6" />
</bean>
由于setter方法只接受一个参数,因此不存在参数歧义的范围。无论你在这里传递什么值,Spring 都会将其转换为setter方法参数的实际原始类型。如果不兼容,它将显示错误。
使用工厂方法的 Spring DI
到目前为止,我们已经看到 Spring 容器负责创建bean的实例。在某些场景中,你需要使用自定义代码来控制创建bean的实例。Spring 通过factory方法支持此功能。
你可以在factory方法中编写自定义逻辑来创建实例,并仅指示 Spring 使用它。当 Spring 遇到此类指令时,它将调用factory方法来创建实例。因此,factory方法类似于回调函数。
factory方法有两种类型:静态的和实例(非静态)。
静态工厂方法
当你想要以静态方式将创建实例的逻辑封装到自定义方法中时,你可以使用静态factory方法。在这种情况下,Spring 将使用<bean>的Class属性来调用factory方法并生成实例。让我们通过以下示例来理解这一点:
public class SearchableFactory {
private static SearchableFactory searchableFactory;
//Static factory method to get instance of Searchable Factory.
public static SearchableFactory getSearchableFactory() {
if(searchableFactory == null) {
searchableFactory = new SearchableFactory();
}
System.out.println("Factory method is used: getSearchableFactory() ");
return searchableFactory;
}
}
public class DIWithFactoryCheck {
public static void main(String[] args) {
ApplicationContext springContext = new ClassPathXmlApplicationContext("application-context.xml");
SearchableFactory searchableFactory = (SearchableFactory)springContext.getBean("searchableFactory");
}
}
SearchableFactory class has one static method, getSearchableFactory, which returns the object of the same class. This behaves as a factory method. The preceding code can be configured in Spring, as per the following snippet:
<bean id="searchableFactory" class="com.packet.spring.factory.di.SearchableFactory" factory-method="getSearchableFactory">
</bean>
在之前的配置中,Spring 将始终使用getSearchableFactory方法来创建 bean 的实例,而不考虑任何作用域。
实例(非静态)工厂方法
你可以使用实例factory方法将创建实例的控制权从容器转移到你的自定义对象。实例factory方法和静态factory方法之间的唯一区别是,前者只能使用 bean 的实例来调用。让我们通过以下示例来理解这一点:
public class Employee {
private String type;
public Employee(String type) {
this.type = type;
}
public void showType() {
System.out.println("Type is :"+type);
}
}
public class Developer extends Employee {
public Developer(String type) {
super(type);
}
}
public class Manager extends Employee {
public Manager(String type) {
super(type);
}
}
//Factory Bean who has Factory method.
public class EmployeeService {
//Instance Factory method
public Employee getEmployee(String type) {
Employee employee = null;
if("developer".equalsIgnoreCase(type)) {
employee = new Developer("developer");
}else if("manager".equalsIgnoreCase(type)) {
employee = new Manager("manager");
}
return employee;
}
}
public class SalaryService {
private Employee employee;
public void setEmployee(Employee employee) {
this.employee = employee;
}
public void showEmployeeType() {
if(this.employee !=null) {
this.employee.showType();
}
}
}
public class DIWithInstanceFactoryCheck {
public static void main(String[] args) {
ApplicationContext springContext = new ClassPathXmlApplicationContext("application-context.xml");
SalaryService salaryService = (SalaryService)springContext.getBean("salaryService");
salaryService.showEmployeeType();
}
}
Employee是一个具有type实例变量的泛型类。Developer和Manager扩展了Employee,并在构造函数中传递type。EmployeeService是一个具有factory方法的类:getEmployee。此方法接受一个 String 参数,并生成Developer或Manager对象。
这些对象可以根据以下代码片段进行配置:
<bean id="employeeService" class="com.packet.spring.factory.di.EmployeeService">
</bean>
<bean id="developerBean" factory-method="getEmployee" factory-bean="employeeService">
<constructor-arg value="developer"></constructor-arg>
</bean>
<bean id="salaryService" class="com.packet.spring.factory.di.SalaryService">
<property name="employee" ref="developerBean"/>
</bean>
employeeService使用正常的 bean 定义。developerBean使用factory-method和factory-bean属性定义。factory-bean属性表示定义factory方法的 bean 的引用。
在之前的案例中,developerBean是通过在employeeService的bean上调用factory方法getEmployee来创建的。通过<constructor-arg>传递给developerBean的参数实际上传递给了factory方法。你也会注意到,我们没有为developerBean定义一个class属性,因为当为bean定义factory-bean时,Spring 会将类视为factory方法(在factorybean 中定义)返回的类型,而不是将类视为bean的类。
这样,developerBean是一种由另一个类的factory方法生成的虚拟bean。最后,我们创建了salaryService,并将developerBean作为 setter 注入。当你执行此代码时,它显示的类型为developer。这就是我们如何使用实例factory方法。
factory方法返回的类的类型不必与定义factory方法的类相同。如果你使用了不同的类,你需要在调用ApplicationContext的getBean()方法时使用从factory方法返回的类进行类型转换。
默认情况下,Spring 为每个bean使用singleton作用域。这意味着 Spring 将为每个bean创建一个对象。对于prototype等其他作用域,每次你调用getBean方法时,Spring 都会创建一个新的实例。但是,如果你指定了哪个factory方法,Spring 将始终调用factory方法来获取对象。
在我们的案例中,我们使用factory方法来确保 Spring 无论bean的作用域如何,都只为我们的bean创建一个对象。这只是其中一个例子。这样,你可以在创建实例时使用任何自定义逻辑的factory方法。它基本上封装了对象实例化过程。
Spring 中的自动装配
到目前为止,我们已经学习了如何定义与<bean>一起的配置元数据来设置依赖关系。如果一切都在不提供任何配置形式说明的情况下解决,那会怎么样?这是一个很酷的想法,好消息是 Spring 支持它。
这个功能被称为自动装配(在 Spring 术语中),它自动化了绑定bean之间关系的过程。这大大减少了在属性或构造函数参数中提供配置元数据的工作量。
可以通过在基于 XML 的配置元数据中定义<bean>元素的autowire属性来启用自动装配功能。它可以指定以下三种模式:名称、类型和构造函数。默认情况下,所有bean的自动装配都是关闭的。
按名称自动装配
如其名所示,在这种模式下,Spring 通过名称进行bean的装配。Spring 会寻找与需要自动装配的属性具有相同名称(ID)的bean。换句话说,依赖关系会与具有相同名称(ID 属性值)的bean自动绑定。让我们通过以下示例来理解这一点:
public class UserService {
public void getUserDetail() {
System.out.println(" This is user detail ");
}
}
public class AccountService {
private UserService userService=null;
public void setUserService(UserService userService) {
this.userService = userService;
}
//Setter DI method.
public void processUserAccount() {
if(userService !=null) {
userService.getUserDetail();
}
}
}
public class DIAutoWireCheck {
public static void main(String[] args) {
ApplicationContext springContext = new ClassPathXmlApplicationContext("application-context.xml");
AccountService accountService = (AccountService)springContext.getBean("accountService");
accountService.processUserAccount();
}
}
在前面的代码中,AccountService 依赖于 UserService。AccountService 有一个 setter 方法,Spring 将通过这个方法注入 UserService 的依赖。前面的场景可以在 Spring 中配置如下:
<bean id="userService" class="com.packet.spring.autowire.di.UserService">
</bean>
<bean id="accountService" class="com.packet.spring.autowire.di.AccountService" autowire="byName">
</bean>
在典型的基于 setter 的 DI 配置中,我们会在 accountService bean 上使用 <property> 元素,并将 ref 属性定义为引用 userService bean。但在前面的例子中,我们没有使用属性元素,并且 userService 仍然通过 Spring 注入到 accountService 中。
这种魔法是通过 autowire="byName" 属性实现的。它是如何工作的呢?当 Spring 读取 <bean> 中的这个属性时,它会尝试寻找与属性(setter 方法)名称相同的 name(id) 的 bean。如果找到了,它将把这个 bean 注入到当前 bean 的 setter 方法中,该 setter 方法定义了 autowire 属性。
在我们的例子中,autowire="byName" 设置在 accountService bean 上,该 bean 有一个 setUserService 的 setter 方法来设置 userService 的实例。Spring 会尝试找到任何 ID 为 userService 的 bean,如果找到了,它将通过这个 setter 方法注入 userService bean 的实例。
在这种情况下,自动装配是通过 setter 方法名称而不是属性名称来发生的。例如,如果你将 setter 方法名称设置为 setUserService1,Spring 将尝试找到 ID 为 userService1 的 bean,而不考虑实际的属性名称。
按类型自动装配
在这种模式下,Spring 根据类型绑定 bean。在这里,类型指的是 <bean> 的 class 属性。Spring 会寻找与需要自动装配的属性类型相同的 bean。换句话说,依赖项会自动绑定到具有相同类型的 bean 上。
如果存在多个相同类型的 bean,Spring 会抛出异常。如果 Spring 找不到匹配类型的 bean,则不会发生任何事情;简单地,该属性将不会被设置。让我们通过以下示例来理解这一点:
public class EmailService {
public void sendEmail() {
System.out.println(" Sending Email ..!! ");
}
}
public class HRService {
private EmailService emailService = null;
//Setter DI method.
public void setEmailService(EmailService emailService) {
this.emailService = emailService;
}
public void initiateSeparation() {
//Business logic for sepration process
if(emailService !=null) {
emailService.sendEmail();
}
}
}
在前面的代码中,HRService 依赖于 EmailService。HRService 有一个 setter 方法,Spring 将通过这个方法注入 EmailService 的依赖。前面的场景可以在 Spring 中配置如下:
<!-- Example of autowire byType -->
<bean id="emailService" class="com.packet.spring.autowire.di.EmailService">
</bean>
<bean id="hrService" class="com.packet.spring.autowire.di.HRService" autowire="byType">
</bean>
当 Spring 读取 hrService bean 中的 autowire="byType" 属性时,它将尝试寻找与 hrService 属性类型相同的 bean。Spring 期望只有一个这样的 bean。如果找到了,它将注入这个 bean。
由于这是按类型自动装配,Spring 依赖于属性的类型来注入依赖项,而不是 setter 方法的名称。Spring 只期望该方法应该将依赖项的引用作为参数传递,以便将其设置到 bean 的属性中。
按构造函数自动装配
这种模式与按类型自动装配相同。唯一的区别是,在这种情况下,自动装配发生在构造函数参数而不是 bean 的属性上。当 Spring 遇到构造函数模式的自动装配时,它将尝试搜索并绑定 bean 的构造函数参数与类型完全相同的单个 bean。让我们通过以下示例来理解这一点:
public class StudentService {
public void getStudentDetail() {
System.out.println(" This is Student details.. ");
}
}
public class ExamService {
private StudentService studentService;
private String examServiceType;
public ExamService(StudentService studentService, String examServiceType) {
this.studentService=studentService;
this.examServiceType = examServiceType;
}
public void getExamDetails() {
if(studentService !=null) {
//Business logic to get exam details.
studentService.getStudentDetail();
}
}
}
在前面的代码中,ExamService依赖于StudentService。ExamService有一个构造函数,Spring 将通过它注入StudentService的依赖项。在 Spring 中,可以按照以下方式配置之前的场景:
<!-- Example of autowire by Constructor -->
<bean id="studentService" class="com.packet.spring.autowire.di.StudentService">
</bean>
<bean id="examService" class="com.packet.spring.autowire.di.ExamService" autowire="constructor">
<constructor-arg value="Science Exam"/>
</bean>
当 Spring 扫描examService bean 的autowire="constructor"属性时,它将搜索并注入与examService构造函数类型相同的任何 bean。在我们的例子中,我们使用StudentService类的一个构造函数参数,因此 Spring 将注入我们在之前 XML 文件中定义的studentService bean 的实例。
与按类型自动装配类似,如果存在多个与构造函数参数类型匹配的 bean,Spring 将抛出错误。在autowire = constructor模式下,您仍然可以通过之前配置中显示的<constructor-arg>元素传递任何额外的参数。如果我们没有在这里使用自动装配,我们将通过<constructor-arg>元素传递studentService。
尽管有上述优点,自动装配功能在使用时仍需谨慎。以下是在使用时需要考虑的几点:
-
自动装配不能应用于原始类型。
-
如果存在多个相同类型的 bean,使用按类型和构造函数自动装配时将导致错误,尽管有避免这种情况的选项。
-
由于自动装配是由 Spring 静默执行的,当 Spring 应用程序上下文文件中定义了大量的 bean 时,有时很难找到逻辑问题。
-
人们仍然更喜欢显式映射而不是自动装配,因为显式映射在某种程度上更准确、更清晰,也更易于阅读。
基于注解的依赖注入
从一开始,定义 Spring 配置最常见的方式就是基于 XML 的。但随着复杂性的增加,在尖括号丛林中导航 bean 变得力不从心,因此需要第二种定义配置的方法。因此,Spring 开始支持注解。
基于注解的配置是 XML 配置的替代方案,它依赖于字节码元数据。Spring 从 2.5 版本开始支持注解。使用注解后,配置从 XML 移动到组件类。注解可以声明在类、方法或字段级别。
让我们通过注解来理解定义配置的过程。我们首先通过 XML 配置来理解这个过程,然后在接下来的章节中将逐步转向基于注解的配置。
通过 XML 配置进行依赖注入
总是以最常见的选择开始总是好的。所以首先,我们将举一个纯 XML 配置的例子,如下面的片段所示:
public class Professor {
private String name;
//Constructor
public Professor() {
System.out.println("Object of Professor is created");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class Subject {
private Professor professor;
public Subject() {
System.out.println("Object of Subject is created");
}
//Setter injection method
public void setProfessor(Professor professor) {
System.out.println("setting the professor through setter method injection ");
this.professor = professor;
}
public void taughtBy() {
if(professor !=null) {
System.out.println("This subject is taught by "+professor.getName());
}
}
}
public class DIAutoWireCheck {
public static void main(String[] args) {
ApplicationContext springContext = new ClassPathXmlApplicationContext("application-context.xml");
Subject subject = (Subject)springContext.getBean("subject");
subject.taughtBy();
}
}
Subject class depends on Professor. The object of the Professor class is injected into Subject through the setter injection. The XML-based configuration can be done with Spring as follows:
<bean id="professor" class="com.packet.spring.annotation.di.Professor">
<property name="name" value="Nilang" />
</bean>
<bean id="subject" class="com.packet.spring.annotation.di.Subject">
<property name="professor" ref="professor" />
</bean>
Professor bean 的对象将被创建,然后通过 setter 注入设置name属性。由于name属性是原始类型,我们直接给出了值。一旦professor bean 的对象准备好,它将通过 setter 注入注入到subject bean 的对象中。回想一下,在基于 XML 的配置中,setter 注入可以通过<property>元素的ref属性来执行。一旦你运行这段代码,你将得到类似于以下输出的结果:
...
INFO: Loading XML bean definitions from class path resource [application-context.xml]
Object of Professor is created
Object of Subject is created
setting the professor through setter method injection
This subject is taught by Nilang
这是一种典型的基于 XML 的元数据,我们希望将其转换为基于注解的配置。在之前的示例中,我们将首先使用@Autowired注解。它与其 XML 对应物autowire的工作方式类似,可以在字段、构造函数和方法级别进行配置。
定义注解
让我们来为之前的示例定义@Autowired。我们的目标是移除带有@Autowired注解的subject bean 的 XML 配置<property name="professor" ref="professor" />。让我们修改Subject类和 Spring 应用程序(XML)上下文文件中的一个setter方法,如下所示:
//Updated setter injection method
@Autowired
public void setProfessor(Professor professor) {
System.out.println("setting the professor through setter method injection ");
this.professor = professor;
}
//Updated XML configuration
<bean id="professor" class="com.packet.spring.annotation.di.Professor">
<property name="name" value="Nilang" />
</bean>
<bean id="subject" class="com.packet.spring.annotation.di.Subject">
</bean>
我们期望自动将Professor对象注入到subject中,因为我们使用了@Autowired注解。当你运行这段代码时,你将得到类似于以下输出的结果:
INFO: Loading XML bean definitions from class path resource [application-context.xml]
Object of Professor is created
Object of Subject is created
快速观察:只有Professor和Subject bean 的对象被创建,没有调用任何 setter 方法。尽管使用了@Autowired注解,但依赖注入并没有自动进行。这是因为,如果没有经过处理,注解什么也不做。就像没有插上电源的电子设备一样。你无法对它做任何事情。
通过在 Java 类中声明注解配置,Spring 是如何知道这些配置的?当我们谈论基于注解的依赖注入时,这应该是你的第一个问题。答案是,我们需要让 Spring 知道我们定义的注解,这样 Spring 才能使用它来完成这项工作。
激活基于注解的配置
基于注解的配置默认是关闭的。你需要通过在应用程序上下文(XML)文件中定义<context:annotation-config/>元素来启用它。当 Spring 读取此元素时,它将激活在定义此元素的应用程序上下文中定义的所有注解的动作。换句话说,Spring 将在定义了<context:annotation-config />元素的应用程序上下文中激活所有定义在当前应用程序上下文中的 bean 上的注解。
让我们更新配置并重新运行之前的代码。你将得到类似于以下输出的结果:
// Updated Configuration
<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:annotation-config />
<bean id="professor" class="com.packet.spring.annotation.di.Professor">
<property name="name" value="Nilang" />
</bean>
<bean id="subject" class="com.packet.spring.annotation.di.Subject">
</bean>
</beans>
//Output
...
INFO: Loading XML bean definitions from class path resource [application-context.xml]
Object of Professor is created
Object of Subject is created
setting the professor through setter method injection
This subject is taught by Nilang
要在 Spring XML 配置文件中启用<context:annotation-config />,您需要包含一些特定于上下文的模式定义,例如xmlns:context,并将context-specific xsd添加到schemaLocation中。
现在一切按预期工作。professor Bean 的对象被正确地注入到subject Bean 的对象中。这正是我们希望通过注解实现的目标。但是等等。我们刚刚移除了一个元素(<property>)并添加了新的元素——<context:annotation-config />。
理想情况下,基于注解的配置应该完全取代基于 XML 的配置。在先前的案例中,我们仍在基于 XML 的配置中定义<bean>定义。如果您将其删除,Spring 将不会创建任何 Bean,也不会对您为这些 Bean 定义的注解执行任何操作。这是因为<context:annotation-config />仅适用于在 Spring 的应用程序上下文(XML)文件中定义的<bean>。因此,如果没有<bean>定义,即使您定义了注解,注解也没有意义。
将 Java 类定义为带有注解的
解决这个问题的方法是定义应用程序上下文(XML)文件中的<context:component-scan>。当 Spring 读取此元素时,它将开始从由其属性base-package定义的 Java 包中扫描 Bean。您可以通过声明类级别的注解@Component来指示 Spring 将 Java 类视为<bean>。简而言之,定义基于注解的配置是一个两步过程,如下所示:
-
扫描包:这可以通过读取
<context:component-scan>的base-package属性来完成。Spring 将开始扫描该 Java 包中的类。 -
定义 Bean:在指定的 Java 包中的 Java 类中,Spring 只会将具有类级别注解的类视为
<bean>——即定义了@Component注解的类。让我们修改我们的示例以包含此配置:
//Updated Spring application context (XML) file
<?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:annotation-config />
<context:component-scan base-package="com.packet.spring.annotation.di"/>
</beans>
//Updated Professor class
@Component
public class Professor {
@Value(value="Nilang")
private String name;
//Constructor
public Professor() {
System.out.println("Object of Professor is created");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
//Updated Subject class
@Component
public class Subject {
private Professor professor;
public Subject() {
System.out.println("Object of Subject is created");
}
//Setter injection method
@Autowired
public void setProfessor(Professor professor) {
System.out.println("setting the professor through setter method injection ");
this.professor = professor;
}
public void taughtBy() {
if(professor !=null) {
System.out.println("This subject is taught by "+professor.getName());
}
}
}
Spring 将通过@Component注解将Professor和Subject类视为<bean>,因此不需要在应用程序上下文(XML)文件中定义它们。您可以使用@Value注解为原始属性注入值。在前面的示例中,我们直接在属性级别使用@Value注解设置了Professor Bean 的name属性的值。或者,您可以根据以下片段在 setter 方法级别将原始值注入到property中:
@Autowired
public void setName(@Value("Komal") String name) {
this.name = name;
}
您需要在 setter 方法上设置@Autowired注解,并使用@Value注解注入该属性的值。当您运行此代码时,您将得到期望的输出,类似于我们使用纯 XML 配置所得到的结果。
元素 <context:component-scan> 执行 <context:annotation-config /> 可以执行的所有操作。如果您在应用程序上下文(XML)文件中同时保留这两个元素,则没有害处;但是在这种情况下,<context:component-scan> 就足够了,您可以省略 <context:annotation-config />。
您可以将多个包传递给 <context:component-scan>,作为逗号分隔的字符串到其 base-package 属性。更重要的是,您可以在 <context:component-scan> 上定义各种过滤器(包括和排除),以扫描特定的子包并消除其他包。
配置可以通过注释或 XML 进行,或者混合使用两者。通过 XML 配置的 DI 在基于注释的 DI 之后执行。因此,基于 XML 的配置可能会覆盖基于注释的配置,以进行 Bean 属性(setter)连接。
到目前为止,我们已经学习了 @Autowired、@Component 和 @Value 注释。我们将看到一些在 DI 中经常使用的其他注释,如下所示:
注释—@Required:@Required 注释可以应用于一个 Bean 的 setter 方法。它表示 Spring 必须通过自动装配或显式设置属性值来从 setter 方法中填充属性的值。换句话说,Bean 属性必须在配置时填充。如果未满足此条件,容器将抛出异常。
作为替代,您可以使用具有属性 required 的 @Autowired 注释—@Autowired (required=false)。当您将其设置为 false 时,如果找不到合适的 Bean,Spring 将忽略此属性进行自动装配。
注释—@Qualifier:默认情况下,@Autowired 注释与 Bean 类的类型一起工作。当配置了多个具有相同类型(类)的 Bean 时,如果您尝试使用属性自动装配它,Spring 将显示错误。在这种情况下,您需要使用 @Qualifier 注释。它将帮助从相同类型的可用 Bean 中连接特定的 Bean。您需要与 @Autowired 一起指定 @Qualifier 以通过声明一个确切的 Bean 来消除混淆。让我们通过以下示例来理解这一点:
public class Professor {
private String name;
//Constructor
public Professor() {
System.out.println("Object of Professor is created");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@Component
public class Subject {
private Professor professor;
public Subject() {
System.out.println("Object of Subject is created");
}
//Setter injection method
@Autowired
@Qualifier("professor1")
public void setProfessor(Professor professor) {
System.out.println("setting the professor through setter method injection ");
this.professor = professor;
}
public void taughtBy() {
if(professor !=null) {
System.out.println("This subject is taught by "+professor.getName());
}
}
}
//Updated Application context (XML) file.
<context:component-scan base-package="com.packet.spring.annotation.di"/>
<bean id="professor1" class="com.packet.spring.annotation.di.Professor">
<property name="name" value="Ramesh" />
</bean>
<bean id="professor2" class="com.packet.spring.annotation.di.Professor">
<property name="name" value="Nilang" />
</bean>
在前面的代码中,@Qualifier 注释与 @Autowired 一起添加,Subject 类中的值为 professor1。这表示 Spring 将自动装配 id = professor1 的 professor Bean。在 Spring 配置文件中,我们已定义了两个具有不同 ID 值的 Professor 类型的 Bean。如果没有 @Qualifier 注释,Spring 将抛出错误。前面的代码产生如下输出:
...
org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [application-context.xml]
Object of Professor is created
Object of Subject is created
Object of Professor is created
setting the professor through setter method injection
Object of Professor is created
This subject is taught by Ramesh
现在 Spring 将注入具有与 @Qualifier 注释值相似的 ID 的 Bean 对象。在前面的情况下,id = professor1 的 Bean 对象被注入到 Subject 中。
您可能会惊讶我们在这里使用了基于 XML 的配置。完全有可能在 Java 类中使用注解来定义这个配置,但如果你需要定义具有不同 ID 的相同类型的多个 bean,建议使用 XML 配置。
带有工厂方法的注解
我们已经看到了如何创建和配置factory方法以使用基于 XML 的配置来生成 bean。Spring 也支持对factory方法的注解。让我们以相同的示例为例,了解如何编写factory方法的注解:
public class Employee {
private String type;
public Employee(String type) {
this.type = type;
}
public void showType() {
System.out.println("Type is :"+type);
}
}
public class Developer extends Employee {
public Developer(String type) {
super(type);
}
}
public class Manager extends Employee {
public Manager(String type) {
super(type);
}
}
@Component
public class EmployeeService {
//Instance Factory method with annotation
@Bean("developerBean")
public Employee getEmployee(@Value("developer")String type) {
Employee employee = null;
if("developer".equalsIgnoreCase(type)) {
employee = new Developer("developer");
}else if("manager".equalsIgnoreCase(type)) {
employee = new Manager("manager");
}
System.out.println("Employee of type "+type+" is created");
return employee;
}
}
@Component
public class SalaryService {
private Employee employee;
@Autowired
public void setEmployee(@Qualifier("developerBean")Employee employee) {
this.employee = employee;
}
public void showEmployeeType() {
if(this.employee !=null) {
this.employee.showType();
}
}
}
public class DIWithAnnotationFactoryCheck {
public static void main(String[] args) {
ApplicationContext springContext = new ClassPathXmlApplicationContext("application-context.xml");
SalaryService salaryService = (SalaryService)springContext.getBean("salaryService");
salaryService.showEmployeeType();
}
}
Employee是一个基类,具有type属性。Developer和Manager扩展了Employee类,并在相应的构造函数中设置了type属性。EmployeeService和SalaryService被定义为带有@Component注解的组件类。Spring 会将它们都视为<bean>。
EmployeeService作为一个工厂 bean 工作,它有一个getEmployee方法。这个方法有一个@Bean注解。@Bean注解表示一个factory方法。在这个方法中,我们使用@Value注解将原始值developer注入到type参数中。这个方法根据type参数生成Developer或Manager对象。
在前面的代码中,我们向@Bean注解提供了developerBean值。这指示 Spring 创建一个<bean>,其id = developerBean,类为Employee——这是getEmployee(工厂)方法的返回类型。简而言之,前面代码中的factory方法等同于以下 XML 配置:
<bean id="employeeService" class="com.packet.spring.annotation.factory.di.EmployeeService">
</bean>
<bean id="developerBean" factory-method="getEmployee" factory-bean="employeeService">
<constructor-arg value="developer"></constructor-arg>
</bean>
我们还有一个组件类:SalaryService。它有一个setEmployee方法,该方法接受Employee对象作为参数。我们给这个方法的参数指定了一个限定符developerBean。由于这个方法被声明为自动装配的,Spring 将注入类型为Employee且id=developerBean的对象,该对象由EmployeeService中的factory方法生成。因此,综上所述,前面的所有 Java 代码等同于以下 XML 配置:
<bean id="employeeService" class="com.packet.spring.annotation.factory.di.EmployeeService">
</bean>
<bean id="developerBean" factory-method="getEmployee" factory-bean="employeeService">
<constructor-arg value="developer"></constructor-arg>
</bean>
<bean id="salaryService"
class="com.packet.spring.annotation.factory.di.SalaryService">
<property name="employee" ref="developerBean"/>
</bean>
使用 Java 配置进行 DI
到目前为止,我们已经看到了如何使用 XML 和注解来定义配置。Spring 还支持完全在 Java 类中定义配置,不再需要 XML。您需要提供负责创建 bean 的 Java 类。简而言之,这是一个 bean 定义的来源。
被注解为@Configuration的类将被视为 Spring IoC 容器的 Java 配置。这个类应该声明实际配置和实例化由容器管理的 bean 对象的方法。所有这些方法都应该使用@Bean注解。Spring 会将所有这样的@Bean注解方法视为 bean 的来源。这些方法类似于factory方法。让我们通过以下简单示例来理解这一点:
@Configuration
public class JavaBaseSpringConfig {
@Bean(name="professor")
public Professor getProfessor() {
return new Professor();
}
@Bean(name="subjectBean")
public Subject getSubject() {
return new Subject();
}
}
在此代码中,JavaBaseSpringConfig 是一个配置类。@Bean 注解中的 name 属性等同于 <bean> 元素的 id 属性。此配置等同于以下 XML 配置:
<beans>
<bean id="professor" class="com.packet.spring.javaconfig.di.Professor"/>
<bean id="subjectBean" class="com.packet.spring.javaconfig.di.Subject"/>
</beans>
一旦你在配置类中定义了所有的 Bean,它就可以通过容器加载应用程序上下文。Spring 提供了一个名为 AnnotationConfigApplicationContext 的独立应用程序上下文来加载配置类和管理 Bean 对象。它可以如下使用:
public class DIWithJavaConfigCheck {
public static void main(String[] args) {
ApplicationContext springContext = new AnnotationConfigApplicationContext(JavaBaseSpringConfig.class);
Professor professor = (Professor)springContext.getBean("professor");
Subject subject = (Subject)springContext.getBean("subjectBean");
}
}
你需要在 AnnotationConfigApplicationContext 的构造函数中传递一个配置类,获取 Bean 的其余过程与其他应用程序上下文相同。此外,使用 Java 配置连接 Bean 的方式没有变化。例如,在之前的代码中,Professor 类型的对象可以按照以下片段注入到 Subject 类型的对象中:
public class Professor {
private String name;
//Constructor
public Professor() {
System.out.println("Object of Professor is created");
}
public String getName() {
return this.name;
}
@Autowired
public void setName(@Value("Komal") String name) {
this.name = name;
}
}
public class Subject {
private Professor professor;
public Subject() {
System.out.println("Object of Subject is created");
}
//Setter injection method
@Autowired
public void setProfessor(Professor professor) {
System.out.println("setting the professor through setter method injection ");
this.professor = professor;
}
public void taughtBy() {
if(professor !=null) {
System.out.println("This subject is taught by "+professor.getName());
}
}
}
你可以注意到在之前的代码中,我们没有为 Professor 和 Subject 类添加 @Component 注解。这是因为实例创建的逻辑在配置类的函数中,所以没有必要要求 Spring 显式扫描 Java 包。
Spring 仍然支持扫描特定 Java 包以创建 Bean,而不是使用新操作符自己创建。为此,你需要应用以下更改:
@Configuration
@ComponentScan(basePackages="com.packet.spring.javaconfig.di")
public class JavaBaseSpringConfig {
}
@Component("professor")
public class Professor {
private String name;
//Constructor
public Professor() {
System.out.println("Object of Professor is created");
}
public String getName() {
return this.name;
}
@Autowired
public void setName(@Value("Komal") String name) {
this.name = name;
}
}
@Component("subjectBean")
public class Subject {
private Professor professor;
public Subject() {
System.out.println("Object of Subject is created");
}
//Setter injection method
@Autowired
public void setProfessor(Professor professor) {
System.out.println("setting the professor through setter method injection ");
this.professor = professor;
}
public void taughtBy() {
if(professor !=null) {
System.out.println("This subject is taught by "+professor.getName());
}
}
}
Professor and Subject classes as components by declaring the @Component annotation. We also instructed the configuration class—JavaBaseSpringConfig, to scan the specific Java package with annotation @ComponentScan, and pass the Java package value in the basePackage attribute. In both of the previous cases, you will get identical output. This is equivalent to the following XML configuration:
<?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">
<bean id="professor" class="com.packet.spring.javaconfig.di.Professor">
<property name="name" value="komal" />
</bean>
<bean id="subjectBean" class="com.packet.spring.javaconfig.di.Subject" autowire="byType">
</bean>
</beans>
总之,以下图表显示了 Spring IoC 容器如何管理对象创建和依赖管理的过程:

摘要
在本章中,我们学习了如何使用 Spring 实现依赖注入——目前开发企业应用程序最受欢迎的框架之一。我们看到了 Spring 容器在管理 Bean 生命周期中扮演的重要角色。
我们还学习了如何定义基于 XML 和注解的配置。我们还深入探讨了不同类型的依赖注入(DI),例如基于 setter 的注入和基于构造函数的注入。
如果你想在创建 Bean 实例时编写自定义逻辑,你现在可以使用 Spring 中的 factory 方法。我们还学习了如何以各种模式自动绑定 Bean,例如按名称、类型和构造函数自动装配。
在 Java 配置的帮助下,你可以用零 XML 构建 Spring 应用程序。我们在上一节中看到了使用 Java 配置的各种技术。
我们将继续我们的旅程,学习如何在 Google Guice 中实现依赖注入,这是另一个流行的框架,它提供容器以实现松散耦合的系统。我们将在下一章中探讨它们。
第四章:使用 Google Guice 进行依赖注入
自从我们的旅程开始,我们已经学习了 DI 模式的理念以理解 IoC,对 Java 9 的模块化框架及其 DI 机制有了初步的了解,在上一章中,我们通过各种示例获得了最广泛使用的 Spring 框架的知识,以理解 DI。
在本章中,我们将看到 Google Guice 框架及其 DI 机制的基础知识,我们还将学习在 Guice 中定义 DI 时的各种注入类型和绑定技术。
Google Guice 框架简介
我们学习了 DI 在软件工程中的好处,但在实现 DI 时,明智地选择框架也很重要,因为每个框架都有其自身的优缺点。开源社区中有各种基于 Java 的依赖注入框架,例如 Dagger、Google Guice、Spring DI、JAVA EE 8 DI 和 PicoContainer。
在这里,我们将详细了解 Google Guice(发音为juice),这是一个轻量级的 DI 框架,帮助开发者模块化应用程序。Guice 封装了 Java 5 引入的注解和泛型特性,以使代码类型安全。它使对象能够以更少的努力连接在一起并进行测试。注解帮助您编写错误倾向和可重用的代码。
在 Guice 中,new关键字被@inject替换以进行依赖注入。它允许在构造函数、字段和方法(任何具有多个参数的方法)级别进行注入。使用 Guice,我们可以定义自定义作用域和循环依赖,并且它具有与 Spring 和 AOP 拦截器集成的功能。
此外,Guice 还实现了Java 规范请求(JSR)330,并使用 JSR-330 提供的标准注解。Guice 的第一个版本由 Google 在 2007 年推出,最新版本是 Guice 4.1。
Guice 配置
为了使我们的编码简单,在本章中,我们将使用 Maven 项目来理解 Guice DI。让我们使用以下参数创建一个简单的 Maven 项目:groupid: com.packt.guice.id,artifactId : chapter4,和version : 0.0.1-SNAPSHOT。通过在pom.xml文件中添加Guice 4.1.0依赖项,我们的最终pom.xml将看起来像这样:
<project
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.packt.guice.di</groupId>
<artifactId>chapter4</artifactId>
<packaging>jar</packaging>
<version>0.0.1-SNAPSHOT</version>
<name>chapter4</name>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<version>4.1.0</version>
</dependency>
</dependencies>
<build>
<finalName>chapter2</finalName>
</build>
</project>
对于本章,我们使用了 JDK 9,但不是作为一个模块项目,因为 Guice 库不是作为 Java 9 模块 jar 提供的。
依赖注入和 JSR-330
在深入研究 Guice 注入之前,让我们再次简要地看看 DI 模式,以及 JSR-330。
依赖注入机制使一个对象能够将依赖传递给另一个对象。在 Java 中,使用 DI,我们可以将依赖解析从编译时移动到运行时。DI 消除了两个 Java 类之间的硬依赖;这使我们能够尽可能多地重用类,并且类可以独立测试。
Java 规范请求-330:在 Java 类中定义依赖关系有不同的方式,但@Inject和@Named是 JSR-330 中最常用的注解,用于描述 Java 类中的依赖关系。根据 JSR-330,对象可以被注入到类的构造函数、方法的参数和字段级别。根据最佳实践,应避免静态字段和方法级别注入,以下是一些原因:
-
只有在通过 DI 第一次创建类对象时,才会注入静态字段,这使得静态字段对构造函数不可访问
-
在运行时,如果静态字段被标记为 final,编译器会报错
-
当类的第一个实例被创建时,只会调用静态方法
根据 JSR-330,注入可以按以下顺序执行:首先,构造函数注入;然后是字段注入;最后是方法级别注入。但是,你不能期望方法或字段按照它们在类中的声明顺序被调用。
构造函数不能使用注入的成员变量,因为字段和方法参数的注入仅在调用构造函数之后发生。
简单 DI 的示例
NotificationService代表一个通用的服务接口,用于向不同的系统发送数据:
public interface NotificationService {
boolean sendNotification(String message, String recipient);
}
之前的接口通过传递消息和接收者详细信息并返回布尔类型来定义sendNotification()方法的签名。SMSService.java是这个接口的实体实现,用于发送短信通知:
public class SMSService implements NotificationService {
public boolean sendNotification(String message, String recipient) {
// Code for sending SMS
System.out.println("SMS message has been sent to " + recipient);
return true;
}
}
上一节课实现了通过接受消息和接收者详细信息来发送短信的代码。现在,我们创建一个客户端应用程序,NotificationClient.java,它将使用NotificationService来初始化实际的 SMSService。同一个对象可以用来向不同的系统发送通知,包括电子邮件或自定义通知:
public class NotificationClient {
public static void main(String[] args) {
NotificationService notificationService = new SMSService();
notificationService.sendNotification("Hello", "1234567890");
}
}
在之前的示例中,尽管实现和接口是松散耦合的,但我们需要在客户端应用程序中手动创建类的真实实现的一个实例。在这种情况下,在编译时,客户端应用程序知道与接口相关的执行类将如何绑定。
这正是 Google Guice 所做的;它从客户端应用程序代码中获取实例作为服务,并注入客户端之间的依赖关系,然后通过简单的配置机制注入服务。
让我们通过在下一个主题中使用不同的 API 来查看 Guice 中依赖注入的一个示例。
Guice 中的基本注入
我们已经看到了一个基本的 DI 实现,现在是时候了解 Guice 中的注入是如何工作的了。让我们重写一个使用 Guice 的示例通知系统,同时我们还将看到 Guice 中的一些不可或缺的接口和类。我们有一个名为NotificationService的基本接口,它期望以消息和接收者详细信息作为参数:
public interface NotificationService {
boolean sendNotification(String message, String recipient);
}
SMSService具体类是NotificationService接口的实现。在这里,我们将@Singleton注解应用到实现类上。当你考虑到服务对象将通过注入器类创建时,这个注解被提供以允许它们理解服务类应该是一个单例对象。由于 Guice 支持 JSR-330,可以使用javax.inject或com.google.inject包中的注解:
import javax.inject.Singleton;
import com.packt.guice.di.service.NotificationService;
@Singleton
public class SMSService implements NotificationService {
public boolean sendNotification(String message, String recipient) {
// Write code for sending SMS
System.out.println("SMS has been sent to " + recipient);
return true;
}
}
同样,我们也可以通过实现NotificationService接口来实施另一个服务,例如向社交媒体平台发送通知。
现在是定义消费者类的时候了,在这里我们可以初始化应用程序的服务类。在 Guice 中,@Inject注解将用于定义基于setter以及基于constructor的依赖注入。这个类的实例被用来通过可访问的通信服务发送通知。我们的AppConsumer类定义了如下基于setter的注入:
import javax.inject.Inject;
import com.packt.guice.di.service.NotificationService;
public class AppConsumer {
private NotificationService notificationService;
//Setter based DI
@Inject
public void setService(NotificationService service) {
this.notificationService = service;
}
public boolean sendNotification(String message, String recipient){
//Business logic
return notificationService.sendNotification(message, recipient);
}
}
Guice 需要识别要应用哪个服务实现,因此我们应该通过扩展AbstractModule类来配置它,并为configure()方法提供一个实现。以下是一个注入器配置的示例:
import com.google.inject.AbstractModule;
import com.packt.guice.di.impl.SMSService;
import com.packt.guice.di.service.NotificationService;
public class ApplicationModule extends AbstractModule{
@Override
protected void configure() {
//bind service to implementation class
bind(NotificationService.class).to(SMSService.class);
}
}
在前面的类中,模块实现确定在确定NotificationService变量的地方注入SMSService的一个实例。同样,如果需要,我们只需要为新服务实现定义一个绑定。在 Guice 中的绑定类似于 Spring 中的连接:
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.packt.guice.di.consumer.AppConsumer;
import com.packt.guice.di.injector.ApplicationModule;
public class NotificationClient {
public static void main(String[] args) {
Injector injector = Guice.createInjector(new ApplicationModule());
AppConsumer app = injector.getInstance(AppConsumer.class);
app.sendNotification("Hello", "9999999999");
}
}
在前面的程序中,Injector对象是通过Guice类的createInjector()方法创建的,通过传递ApplicationModule类的实现对象。通过使用注入器的getInstance()方法,我们可以初始化AppConsumer类。在创建AppConsumer对象的同时,Guice 注入了所需的服务类实现(在我们的例子中是SMSService)。以下是运行前面代码的结果:
SMS has been sent to Recipient :: 9999999999 with Message :: Hello
因此,这是与其它 DI 相比 Guice 依赖注入的工作方式。Guice 采用了代码优先的依赖注入技术,并且不需要管理多个 XML 文件。
让我们通过编写一个JUnit测试用例来测试我们的客户端应用程序。我们可以简单地模拟SMSService的服务实现,因此不需要实现实际的服务。MockSMSService类看起来是这样的:
import com.packt.guice.di.service.NotificationService;
public class MockSMSService implements NotificationService {
public boolean sendNotification(String message, String recipient) {
System.out.println("In Test Service :: " + message + "Recipient :: " + recipient);
return true;
}
}
以下是对客户端应用程序的 JUnit 4 测试用例:
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.packt.guice.di.consumer.AppConsumer;
import com.packt.guice.di.impl.MockSMSService;
import com.packt.guice.di.service.NotificationService;
public class NotificationClientTest {
private Injector injector;
@Before
public void setUp() throws Exception {
injector = Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
bind(NotificationService.class).to(MockSMSService.class);
}
});
}
@After
public void tearDown() throws Exception {
injector = null;
}
@Test
public void test() {
AppConsumer appTest = injector.getInstance(AppConsumer.class);
Assert.assertEquals(true, appTest.sendNotification("Hello There", "9898989898"));;
}
}
注意,我们通过AbstractModule的匿名类实现将MockSMSService类绑定到NotificationService。这是在setUp()方法中完成的,该方法在测试方法运行之前运行一段时间。
Guice API 和阶段
我们已经看到了使用各种 Guice API 的 DI 示例,包括接口和类。因此,现在是时候了解主要的 API 和架构了。Guice 架构分为两个阶段:启动和运行时。
启动阶段
在启动阶段,Module、AbstractModule、Binder、Injector、Guice和Provider等 API 在 Guice 依赖注入中发挥着重要作用。让我们详细了解每个 API,从模块接口开始。
模块接口
这是一个特殊的接口,您可以使用它来告诉 Guice 哪些实现与哪些接口相对应。模块是保存一组绑定的对象。在软件的一个部分中可以有多个模块。注入器与模块交互以获取可行的绑定。
模块是通过使用一个具有名为Module.configure()的方法的接口来表示的,该方法应该通过应用程序的重写来填充绑定。如果我们通过实现Module接口重写我们的ApplicationModule,那么它将看起来像这样:
import com.google.inject.Module;
import com.packt.guice.di.impl.SMSService;
import com.packt.guice.di.service.NotificationService;
public class ApplicationModule implements Module{
@Override
protected void configure(Binder binder) {
//bind NotificationService to SMSService implementation class
//Guice will create a single instance of SMSService for every Injection
binder.bind(NotificationService.class).to(SMSService.class);
}
}
抽象模块类
为了改进,有一个名为AbstractModule的抽象类,它直接扩展了模块接口,因此应用程序可以依赖于AbstractModule而不是模块。
强烈建议将模块扩展到AbstractModule的使用。它提供了更可读的配置,并且还引导我们避免在绑定上过度调用方法。
在我们的示例ApplicationModule中,为了配置 Guice 而不是实现模块接口,我们使用了AbstractModule,其中 Guice 将我们的模块传递给绑定接口。
如果一个应用程序有一个预定的配置数量,它们可以被合并到一个单独的模块中。对于这样的应用程序,每个包或每个应用程序一个模块可能是一个合适的系统。
绑定器
此接口主要包含与绑定相关的信息。绑定通常由一个接口和一个具体实现的映射组成。例如,如果我们考虑一个用于创建自定义模块的模块接口的实现,那么接口NotificationService的引用绑定到了SMSService实现。
在编码时,请注意,接口和实现类的对象都传递给了bind()和to()方法:
binder.bind(NotificationService.class).to(SMSService.class);
第二种方式是通过编写以下代码直接将接口与其实例绑定:
binder.bind(NotificationService.class).to(new SMSService());
注入器
Injector接口创建并维护对象图,跟踪每种类型的依赖关系,并使用绑定来注入它们。注入器保持一组默认绑定,它们从中获取配置细节以在对象之间建立和维护关系。考虑以下代码,它将返回AppConsumer类的实现:
AppConsumer app = injector.getInstance(AppConsumer.class);
我们也可以通过调用Injector.getBindings()方法来获取与注入器关联的所有绑定,该方法返回一个包含绑定对象的映射:
Map<Key, Binding> bindings = injector.getBindings()
从这个例子中,我们可以得出结论,每个绑定都有一个匹配的key对象,该对象由 Google Guice 类内部创建并保留。
Guice
Guice是一个最终类,是 Guice 框架的入口点。它通过提供一组模块来创建注入器:
//From NotificationClient.java
Injector injector = Guice.createInjector(new ApplicationModule());
//Syntax from actual Guice Class
Injector injector = Guice.createInjector(
new ModuleA(),
new ModuleB(),
. . .
new ModuleN(args)
);
createInjector() method takes ApplicationModule() as an argument; the same method also takes a varargs, which means we can pass zero or more modules separated by a comma.
提供者
默认情况下,每当应用程序需要一个对象的实例时,Guice 都会实例化并返回它;但在某些情况下,如果对象创建过程需要定制,那么 Guice 提供者会进行定制。提供者接口在创建对象方面遵循传统的工厂设计模式。例如,考虑我们的ApplicationModule类绑定过程:
binder.bind(NotificationService.class).to(new SMSProvider());
通过编写上述代码行,SMSProvider类提供了将返回NotificationService类型对象的工厂方法。假设我们想要定制ServiceConnection类的对象创建和维护过程,如下所示:
public class ServiceConnection {
public void startService(){
System.out.println("Start SMS Notification Service");
}
public void stopService(){
System.out.println("Stop SMS Notification Service");
}
}
现在让我们编写一个简单的Provider接口,它符合 Guice 的Provider,用于创建和返回ServiceConnection对象。以下是这个接口的代码:
import com.google.inject.Provider;
public class SMSProvider implements Provider{
@Override
public ServiceConnection get() {
// Write some custom logic here.
ServiceConnection serviceConnection = new ServiceConnection();
// Write some custom logic here.
return serviceConnection;
}
}
每个自定义提供者类都应该实现Provider接口,并且必须重写get()方法以返回以自定义方式创建的对象。目前,模块应该知道自定义提供者类,这样 Guice 就会请求SMSProvider创建实例,而不是自己创建。以下是一个包含测试客户端代码的模块片段:
import javax.inject.Provider;
import com.google.inject.Binder;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Module;
public class NotificationClientTest {
public static void main(String args[]){
Injector injector = Guice.createInjector(
new Module(){
@Override
public void configure(Binder binder) {
binder.bind(ServiceConnection.class).toProvider((Class<? extends Provider<? extends ServiceConnection>>) SMSProvider.class);
}
}
);
ServiceConnection serviceConnection =
injector.getInstance(ServiceConnection.class);
serviceConnection.startService();
serviceConnection.stopService();
}
}
我们已经单独看到了主要 API 的使用,它在 Guice 的启动阶段起着重要的作用。以下是我们应用程序的序列图,它说明了 Guice 依赖管理的完整流程:

运行时阶段
现在,我们将能够利用启动阶段创建的注入器来注入对象并检查我们的绑定。Guice 的运行时阶段由一个包含一些绑定的注入器组成:

上述图定义了每个绑定的组件。每个键唯一识别每个绑定。键由一个类型和一个可选的注解组成,客户端依赖于该类型。注解可以用来区分相同类型的几个绑定。
每个绑定都有一个提供者,它提供了一个必需类型的实例。我们可以提供一个类,Guice 将为我们创建其实例。我们也可以给 Guice 一个实例来绑定该类。如果我们提供自己的提供者,Guice 可以注入依赖。
默认情况下,没有绑定有作用域;但它是可选的,并且对于每次注入,Guice 都会创建一个新的实例,这与 Spring 的Prototype类似。Guice 还提供了一个定义自定义作用域的设施,以控制 Guice 是否创建新的实例。在这种情况下,我们可以为每个HttpSession创建一个实例。
Guice 注解
Guice 附带了一组有用的注解,这些注解用于在应用程序中包含元数据值。现在,让我们研究本节将要讨论的注解。
注入
Guice 提供了 @Inject 注解来指示消费者依赖于特定的依赖项。注入器负责使用对象图初始化此依赖项。Guice 从注解中获取提示,表明它需要参与类构造阶段。@Inject 注解可以用于类的构造器、方法或字段。考虑以下代码:
//Field level injection
@Inject
private NotificationService notificationService;
//Constructor level Injection
@Inject
public AppConsumer(NotificationService service){
this.notificationService=service;
}
//Method level injection
@Inject
public void setService(NotificationService service) {
this.notificationService = service;
}
由...提供
NotificationService.java;
@ProvidedBy(SMSProvider.class)
public interface NotificationService{
}
//@ProvidedBy is equivalent to toProvider() binding like below,
bind(NotificationService.class).toProvider(SMSProvider.class)
实现者
这个注解关注于一个给接口提供实现的类对象。例如,如果 NotificationService 接口有多个用途,并且我们希望 SMSService 作为默认实现,那么我们可以这样编写代码:
@ImplementedBy(SMSService.class)
public interface NotificationService{
boolean sendNotification(String message, String recipient);
}
@Named
为每个具体用途创建未使用的注解类型并不提供太多价值,因为拥有此类注解的唯一原因是为了检查客户端所需的实现类实例。为了支持此类功能,我们有一个内置的绑定注解 @Named,它接受一个字符串。有 Names.named() 方法,当传递名称作为参数时返回 @Named 注解:
bind(NotificationService.class).annotatedWith(Names.named("SMS"))
.to(SMSService.class);
我们建议谨慎使用 @Named,因为编译器无法检查字符串。
@Singleton 是另一个有用的注解,我们将在第五章 作用域 中详细讨论。
Guice 中的绑定
在上一个主题中,我们了解了绑定过程及其在 Guice DI 中的重要性。每个对 bind() 方法的绑定调用都会进行类型检查,因此编译器可以报告你使用不正确类型时的错误。
Guice 提供了不同类型的绑定技术,这些技术可以在模块中使用。可用的绑定类型包括:链接绑定、实例绑定;未指定绑定。构造器绑定、内置绑定、即时绑定以及提供者绑定。
链接绑定
Linked binding 帮助将类型映射到其实现。链接绑定的例子包括对其实现类的接口,以及子类到超类。
在这里,NotificationService 在 ApplicationModule 类中被绑定到 SMSService 实例。这种绑定确认将一个接口绑定到其实现:
bind(NotificationService.class).to(SMSService.class);
当我们调用 injector.getInstance(ApplicationModule.class) 时,它将使用 SMSService。如果需要绑定到 NotificationService 的特定实现,例如 EmailService,那么我们只需要基本更改绑定:
bind(NotificationService.class).to(EmailService.class);
我们甚至可以定义从类型到其任何子类型(如执行类或扩展类)的链接。你确实可以将具体的 SMSService 课程链接到一个子类:
bind(SMSService.class).to(SMSDatabase.class);
一个基本需要理解的是,链接绑定确实可以被链式调用。例如,如果我们需要SMSService与一个扩展SMSService的特定类进行连接,那么我们就会做类似这样的事情:
public class ApplicationModule implements AbstractModule{
@Override
protected void configure() {
//Linked binding as chain
bind(NotificationService.class).to(SMSService.class);
bind(SMSService.class).to(SMSDataBase.class);
}
}
在这里,如果请求NotificationService,那么注入器将返回SMSDataBase实例。
实例绑定
实例绑定有助于将一个类型绑定到该类型的特定实例。这通常只对没有依赖关系的对象有帮助;例如,值对象:
Public class SearchModule extends AbstractModule{
@Override
protected void configure() {
bind(SearchParameters.class).toInstance(new SearchParameters());
}
}
避免使用.toInstance与制作复杂的对象,因为它可能会减慢应用程序的启动速度。你可以使用@Provides技术。
未指定目标的绑定
可以不指定目标创建的绑定称为未指定绑定。这些实际上是向注入器发出关于类型的信号,以便依赖关系被急切地安排。在未指定绑定中,我们不需要to子句:
bind(SampleConcreteClass.class).in(Singleton.class);
//Another way to define untargeted binding
bind(String.class).toInstance("./alerts/");
在这个语句中,注入器会急切地准备一个 String 类的实例,其值为./alerts/。当依赖注入需要注入 String 类的实例时,它将注入这个特定的实例。这种绑定在定义具体类和由@ImplementedBy或@ProvidedBy注解的类型时非常有用。
构造函数绑定
这种类型的绑定将一个类型绑定到一个构造函数。这个特定的情况发生在@Inject注解不能应用于目标构造函数时。可能的原因包括:
-
如果我们使用第三方类
-
几个参与依赖注入的构造函数
为了解决这个问题,我们在模块中有了toConstructor()绑定。在这里,如果找不到构造函数,模块将反射选择我们的目标构造函数并处理异常:
public class SampleModule extends AbstractModule {
@Override
protected void configure() {
try {
bind(NotificationService.class).toConstructor(
SMSService.class.getConstructor(SMSDatabaseConnection.class));
} catch (NoSuchMethodException e) {
e.getPrintStackTrace();
}
}
}
在前面的代码中,SMSService应该有一个接受单个SMSDatabaseConnection参数的构造函数。Guice 将构造这个构造函数来满足绑定,因此构造函数不需要@Inject注解。如果我们选择反射构造函数,那么我们需要处理getConstructor() API 抛出的受检异常。
每个构造函数绑定的范围是独立的。如果我们为同一个构造函数创建不同的单例绑定,每个绑定都会产生其自己的实例。
内置绑定
如其名所示,这些是在注入器中自动覆盖的绑定。让注入器来创建这些绑定,因为自己尝试绑定它们将是一个错误。日志记录器就是这样一个例子。
- 日志记录器:
java.util.logging.Logger在 Guice 中有一个内置绑定。这个绑定自然地将日志记录器的标题设置为注入日志记录器的类的标题:
@Singleton
public class SMSDatabaseLog implements DatabaseLog {
private final Logger logger;
@Inject
public SMSDatabaseLog(Logger logger) {
this.logger = logger;
}
public void loggerException(UnreachableException e) {
//Below message will be logged to the SMSDatabaseLog by logger.
logger.warning("SMS Database connection exception, " + e.getMessage());
}
实时绑定
这些可能是 Guice 自动创建的绑定。当没有明确的绑定时,注入器将努力创建一个绑定,这是一个即时(JIT)绑定或隐式绑定。
默认构造函数:默认情况下,不会调用无参数构造函数来获取准备注入的实例。偶尔,在我们的示例中,没有明确的绑定作为创建 Client 实例的方式。在任何情况下,注入器都会调用默认构造函数来返回客户端的实例。
带有 @Inject 的构造函数:如果构造函数有 @Inject 注解,那么它也适用于隐式绑定。它还包括无参数和公共构造函数:
//Constructor Based Injector
@Inject
public AppConsumer(NotificationService notificationService){
this.service = notificationService;
}
绑定注解
有时我们想要为同一类型使用多个绑定。在我们之前的例子中,NotificationService 绑定到 SMSService,这实际上是无用的,因为接口只绑定到一个执行。如果我们需要客户端能够利用任何实现,那么我们需要在 configure() 方法中编写几个绑定语句,为了使这成为可能,我们可以编写如下代码:
bind(NotificationService.class).annotatedWith(“sms”).to(SMSService.class);
bind(NotificationService.class).annotatedWith(“email”).to(EmailService.class);
从前面的语句中,Guice 知道何时将 NotificationService 接口绑定到 SMSService,何时绑定到 EmailService。
调用 SMSService 实现的客户端代码将如下所示:
AppConsumer app = injector.getInstance(@Named("sms") AppConsumer.class);
并且调用 EmailService 实现如下:
AppConsumer app = injector.getInstance(@Named("email") AppConsumer.class);
为了支持这种情况,绑定支持非强制性的 绑定注解。一个 键 是注解和类型唯一组合的对。以下是为 SMS 注解定义绑定注解的基本代码:
@BindingAnnotation @Target({ FIELD, PARAMETER, METHOD }) @Retention(RUNTIME)
public @interface SMS{}
从前两行来看,让我们看看元注解:
-
@BindingAnnotation用于告知 Guice 这是一个绑定的说明。如果我们为同一成员定义不同的绑定,那么 Guice 可能会生成错误。 -
@Target和@Retention是在 Java 中创建自定义注解时常用的注解。@Target帮助定位字段、参数和方法,@Retention(RUNTIME)分别在运行时可用。
Guice 注入
既然我们已经知道了依赖注入是什么,让我们来看看 Google Guice 如何提供注入。
我们已经看到注入器通过从模块中读取配置来帮助解决依赖关系,这些模块被称为 绑定。注入器 正在为请求的对象准备图表。
依赖注入由注入器通过各种类型的注入来管理:
-
构造函数注入
-
方法注入
-
字段注入
-
可选注入
-
静态注入
构造函数注入
构造函数注入可以通过在构造函数级别使用 @Inject 注解来实现。这个构造函数应该将类依赖项作为参数来识别。此时,多个构造函数将把参数分配给它们的最终字段:
public class AppConsumer {
private NotificationService notificationService;
//Constructor level Injection
@Inject
public AppConsumer(NotificationService service){
this.notificationService=service;
}
public boolean sendNotification(String message, String recipient){
//Business logic
return notificationService.sendNotification(message, recipient);
}
}
如果我们的类没有带有 @Inject 的构造函数,那么它将被视为一个不带参数的默认构造函数。当我们有一个单构造函数且类接受其依赖项时,构造函数注入将完美工作,并且对单元测试很有帮助。这也很容易,因为 Java 正在维护构造函数调用,所以你不必担心对象以未初始化的状态到达。
方法注入
Guice 允许我们通过使用 @Inject 注解来定义方法级别的注入。这与 Spring 中可用的设置器注入类似。在这种方法中,依赖项作为参数传递,并在方法调用之前由注入器解决。方法名称和参数数量 不影响 方法注入:
private NotificationService notificationService;
//Setter Injection
@Inject
public void setService(NotificationService service) {
this.notificationService = service;
}
当我们不希望控制类的实例化时,这可能很有价值。此外,如果我们有一个需要一些依赖项的父类,我们也可以利用它。(在构造函数注入中实现这一点是困难的。)
字段注入
字段可以通过 Guice 中的 @Inject 注解进行注入。这是一个简单且简短的注入方式,但如果与 private 访问修饰符一起使用,则会使字段不可测试。建议避免以下情况:
@Inject private NotificationService notificationService;
可选注入
Guice 提供了一种声明可选注入的方法。方法和字段可能是可选的,这会导致 Guice 在依赖项不可访问时静默地忽略它们。可以通过提及 @Inject(optional=true) 注解来使用 可选注入:
public class AppConsumer {
private static final String DEFAULT_MSG = "Hello";
private string message = DEFAULT_MSG;
@Inject(optional=true)
public void setDefaultMessage(@Named("SMS") String message) {
this.message = message;
}
}
静态注入
静态注入 在我们需要将静态工厂实现迁移到 Guice 时很有帮助。它使得对象能够通过获取注入类型而不被注入本身的方式,主要参与依赖注入。在一个模块中,为了在创建注入器时指示要注入的类,请使用 requestStaticInjection()。例如,NotificationUtil 是一个提供静态方法 timeZoneFormat 的实用类,该方法接受一个给定格式的字符串,并返回日期和时间区。TimeZoneFormat 字符串在 NotificationUtil 中是硬编码的,我们将尝试静态注入此实用类。
考虑到我们有一个私有的静态字符串变量 timeZonFmt,它具有设置器和获取器方法。我们将使用 @Inject 进行设置器注入,并使用 @Named 参数。
NotificationUtil 将看起来像这样:
@Inject static String timezonFmt = "yyyy-MM-dd'T'HH:mm:ss";
@Inject
public static void setTimeZoneFmt(@Named("timeZoneFmt")String timeZoneFmt){
NotificationUtil.timeZoneFormat = timeZoneFmt;
}
现在,SMSUtilModule 应该看起来像这样:
class SMSUtilModule extends AbstractModule{
@Override
protected void configure() {
bindConstant().annotatedWith(Names.named(timeZoneFmt)).to(yyyy-MM-dd'T'HH:mm:ss);
requestStaticInjection(NotificationUtil.class);
}
}
由于它面临着与静态工厂相同的大量问题,因此不建议在常规使用中采用此 API。它也难以测试,并且使依赖项变得不确定。
摘要
因此,这就是 Google Guice 的全部内容。总结本章,我们开始于基本的依赖注入。之后,我们学习了 Guice 中基本依赖注入的工作方式,并提供了示例。
然后,我们研究了 Guice 的各个阶段以及 API 在每个阶段中的作用。我们得到了这样的想法,与 Spring 不同,在 Guice 中没有要求维护独立的 XML 文件,因为所有与设置相关的数据都通过模块组件得到了很好的类型化。
在本章的中间部分,我们探讨了 Guice 中可用的主要注解和不同的绑定类型,而在本章的最后,我们学习了不同类型的注入方式。
在下一章中,我们将熟练掌握 Spring 和 Google Guice 框架提供的不同作用域。
第五章:作用域
在这次旅程中,我们通过示例学习了 Java 9、Spring 和 Google Guice 中的依赖注入概念。
在第三章的使用 Spring 进行依赖注入和第四章的使用 Google Guice 进行依赖注入中,我们遇到了作用域这个词,它是 Spring beans 和 Google Guice 的一个重要元素。因此,让我们了解什么是作用域,以及为什么在讨论依赖注入时它很重要。
在本章中,我们将首先了解 Spring 提供的各种作用域,以及它们如何为 Spring beans 定义。我们还将了解 bean 作用域与依赖注入之间的关系。最后,我们将探讨 Google Guice 中可用的作用域。我们将讨论的主题如下:
-
Spring 中 bean 作用域的介绍
-
如何定义 bean 作用域
-
依赖注入和 bean 作用域
-
如何选择 bean 作用域
-
Google Guice 中的作用域
Spring 中 bean 作用域的介绍
在第三章的使用 Google Guice 进行依赖注入中,我们学习了与依赖注入一起的不同 Spring 模块。在 Spring 中,bean 是应用程序的骨干,由 Spring IOC 容器管理。bean 是使用我们可以传递给 IOC 容器的元数据配置创建的类或对象。在学习作用域之前,让我们在 Spring 中定义一个 bean。
Bean 定义
bean的元数据具有其自己的属性,这些属性具有独立的 bean 定义。以下是一些这些 bean 定义的例子:
-
类:这将用于创建 bean,我们必须指定一个类名,为该类创建 bean。
-
名称:如果我们想为 bean 定义不同的别名,那么我们使用
name属性,借助分隔符,例如逗号(,)或分号(;)。当我们有基于 XML 的配置时,我们可以使用name和/或id属性作为 bean 的标识符。具有id属性的 bean 更受欢迎,因为它与实际的 XML ID 元素相映射。 -
构造函数参数:构造函数参数用于通过在构造函数中传递参数作为参数来注入依赖项,这在第三章中看到,使用 Spring 进行依赖注入。
-
属性:我们可以在 Spring bean 中直接通过键值对传递属性以进行注入。如果我们需要将某些固定值传递给 bean,这有时是有用的。
-
自动装配模式:自动装配可以用来减少属性和构造函数参数的使用。要启用自动装配模式,我们需要在 Spring bean 中使用
autowire属性。属性可以具有byName、byType和constructor等值。 -
懒加载初始化模式:默认情况下,Spring bean 以单例作用域创建,以贪婪模式初始化所有属性。如果 bean 以懒加载模式定义,则 IOC 容器会在第一次请求到来时创建 bean 实例,而不是在启动过程中。
-
初始化方法:Spring 初始化在 IOC 容器设置所有属性之后进行。在基于 XML 的配置中,我们可以通过定义
init-method属性来定义init方法。init方法应该是 void 类型,且没有参数。可以使用@PostConstruct注解来初始化方法。 -
销毁方法:在 bean 生命周期结束时,如果我们需要关闭资源或想在 bean 销毁前执行操作,我们可以使用 XML 配置中 bean 的
destroy-method属性。也可以使用@PreDestroy注解代替 XML 属性。
以下配置文件包含不同类型的 bean 定义语法,因此application-context.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">
<!-- A simple bean definition with ID and Class Name-->
<bean id = "..." class = "...">
<!-- Bean configuration and properties like constructor-arg -->
</bean>
<!-- Bean definition using Name attribute instead of ID attribute -->
<bean name = "..." class = "...">
<!-- Bean configuration and properties like constructor-arg -->
</bean>
<!-- Ban definition with constructor arguments -->
<bean id="..." class="...">
<constructor-arg ref="..."/>
<constructor-arg ref="..."/>
</bean>
<!-- Ban definition for autowiring using byName mode -->
<bean id="..." class="..." autowire="byName">
<!-- Bean configuration and properties like constructor-arg -->
</bean>
<!-- Ban definition for defining scope -->
<bean id="..." class="..." scope="prototype">
<!-- Bean configuration and properties like constructor-arg -->
</bean>
<!-- Ban definition with lazy initialization mode -->
<bean id = "..." class = "..." lazy-init = "true">
<!-- Bean configuration and properties like constructor-arg -->
</bean>
<!-- Bean definition which has initialization method -->
<bean id = "..." class = "..." init-method = "init">
<!-- Bean configuration and properties like constructor-arg -->
</bean>
<!-- Bean definition which has destruction method -->
<bean id = "..." class = "..." destroy-method = "destroy">
<!-- Bean configuration and properties like constructor-arg -->
</bean>
</beans>
当作用域是单例时,懒加载最有效。对于原型作用域,bean 默认以懒加载模式初始化。
Spring 作用域
我们已经了解了 bean 定义如何使用不同的属性,作用域是 bean 定义中的一个属性。在继续学习作用域类型之前,一个问题浮现在脑海中:什么是作用域?
如果我们从 Spring 的角度来看,作用域的意义是,定义 bean 的生命周期并在 bean 使用的特定上下文中定义该 bean 的可见性。当对象的作用域结束时,它将被视为超出作用域,并且不能再注入到不同的实例中。
根据牛津高阶英汉双解大词典,作用域意味着“某事物处理或与之相关的区域或主题范围。”
Spring 有七个作用域,其中五个用于 Web 应用程序开发。以下是一个Bean 作用域的图示:

单例作用域
Spring 容器创建的每个 bean 都有一个默认的单例作用域;Spring 将其视为 bean 的一个实例,并为容器内缓存中的每个对该 bean 的请求提供服务。在依赖注入中,定义为单例的 bean 作为共享 bean 从缓存中注入。
单例bean 作用域限制在 Spring 容器内,与 Java 中的单例模式相比,Java 中的单例模式在每个ClassLoader中只会创建特定类的一个实例。此作用域在 Web 应用程序以及独立应用程序中都很有用,无状态 bean 也可以利用单例作用域。
如果有三个 bean 具有不同的 ID 但具有相同的单例作用域,那么将创建该类的三个实例,在 bean ID 方面,因为单例 bean 只有一个实例被创建:

原型作用域
当我们需要创建多个 bean 实例时,我们使用原型作用域。原型作用域的 bean 主要用于有状态的 bean。因此,在每次请求时,IoC 容器都会创建 bean 的新实例。这个 bean 可以被注入到另一个 bean 中,或者通过调用容器的getBean()方法来使用。
但是,容器在初始化后不会维护原型 bean 的记录。我们必须实现一个自定义的BeanPostProcessor来释放原型 bean 占用的资源。在原型作用域的情况下,不会调用生命周期中的destroy方法,而是对所有对象调用初始的回调方法,无论作用域如何:

到目前为止,我们已经看到了单例和原型作用域。两者都可以用于独立和 Web 应用程序,但还有五个更多的作用域仅在 Web 应用程序中工作。如果我们使用这些作用域与ClassPathXmlApplicationContext一起,那么它将抛出一个IllegalStateException,表示未知的作用域。
要使用请求、会话、全局会话、应用程序和 websocket 作用域,我们需要使用一个感知网络的上下文实现(XmlWebApplicationContext)。让我们详细看看所有这些网络作用域。
请求作用域
在 Web 应用程序中,如果 bean 的作用域被定义为请求,则客户端的每个 HTTP 请求都会得到一个新的 bean 实例。在 HTTP 请求完成时,bean 将立即被视为超出作用域,并将释放内存。如果服务器有 100 个并发请求,那么将有 100 个不同的 bean 类实例可用。如果一个实例发生变化,它不会影响其他实例。以下是请求作用域的图像:

会话作用域
会话是一组交互式信息,也称为在网站特定时间框架内客户端和服务器之间的转换。在Apache Tomcat服务器中,一个会话的默认时间框架是 30 分钟,包括用户所做的所有操作。
Spring 会话 bean 作用域类似于 HTTP 会话;IoC 容器为每个用户会话创建一个 bean 的新实例。在用户登出时,其会话 bean 将超出作用域。就像请求一样,如果有 50 个用户同时使用一个网站,那么服务器将有 50 个活跃会话,Spring 容器也将有 50 个不同的 bean 类实例:

之前的图像说明了所有用户的 HTTP 请求都包含在一个会话中,并且所有请求在该会话作用域中可能都有单个 bean 实例的生存期访问。会话实例将被销毁,就像之前一样,会话在服务器上被销毁/退出。
应用程序作用域
应用程序作用域仅在 Web 应用程序中工作。在运行时,IoC 容器为每个 Web 应用程序创建单个 bean 定义的实例。以下定义应用程序作用域的两种方式:
// 1) XML way to configure define application scope
<bean id="..." class="com.packt.scope.applicationBeanTest" scope="application" />
// 2) Java config using annotation
@Component
@Scope("application")
public class applicationBeanTest {
}
//or
@Component
@ApplicationScope
public class applicationBeanTest {
}
这与单例范围相同,但主要区别在于单例范围 bean 在每个ApplicationContext中作为单例工作,而应用范围 bean 在每个ServletContext中作为单例工作。这些 bean 存储在ServletContext的属性中。
全局会话范围
全局会话范围类似于会话范围。唯一的区别是它将在组件应用程序中使用。当我们的应用程序基于 JSR-168、JSR-286 和 JSR-362 门户规范构建时,可以使用全局会话。将会有多个站点/应用程序在单个组件容器下工作。
组件容器有不同的组件,每个都有自己的组件上下文和组件会话。组件会话与组件边界一起工作,但当我们需要在多个站点之间共享共同信息时,我们可以定义具有globalSession范围的 bean。Spring 为门户应用程序提供了独立的组件 MVC 模块:
// 1) XML way to configure define application scope
<bean id="..." class="com.packt.scope.globalBeanTest" scope="globalSession" />
// 2) Java config using annotation
@Component
@Scope("globalSession")
public class globalBeanTest {
}
//or
@Component
@GlobalSessionScope
public class globalBeanTest {
}
考虑一个由服务器站点组成的内部网络应用程序。用户可以是多个站点的成员。在这种情况下,具有共同信息的用户偏好可以存储为登录用户的全局会话,并且将在多个站点和组件之间使用。以下图像显示了全局会话如何在组件容器之间共享:

WebSocket 范围
当使用 WebSocket 协议启用客户与远程站点之间的双向通信时,将使用此范围。它主要适用于多个用户同时执行操作的应用程序。
在这里,使用 HTTP 请求进行初始握手,一旦建立,TCP 端口将保持打开状态,以便客户端和服务器进行通信。WebSocket 组件类似于单例,并注入到 Spring 控制器中。与典型的 WebSocket 会话相比,WebSocket 组件的生命周期更长。以下示例显示了如何使用范围注解和传统的 XML 配置声明 WebSocket:
//Using @Scope annotation
@Scope(scopeName = "websocket")
//Using XML configuration
<bean id="..." class="com.packt.scope.WebsocketExampleTest" scope="websocket" />
如何定义 bean 范围
因此,我们了解了不同的范围及其用法。现在,让我们看看如何在编码中使用它们。我们将主要通过示例查看单例和原型 bean 范围。
Spring 提供了两种不同的方式来编写应用程序:一种是通过传统的 XML 元数据配置,另一种是使用注解的 Java 配置。让我们看看 XML 配置是如何使用的。
XML 元数据配置
在 Spring 中,bean 配置是在我们选择的 XML 文件中声明的。该文件由 IoC 容器用于初始化应用程序上下文,同时,所有 bean 定义都是基于提供的属性初始化的。
使用单例范围
单例作用域是在主要应用程序中非常常见的作用域。在这里,我们将开始使用单例作用域。首先,我们将创建一个名为EmailService的 bean 类,它包含一个简单的getter/setter方法和一个带有print语句的构造方法:
package com.packt.springbean;
public class EmailService {
private String emailContent;
private String toAddress;
public EmailService() {
System.out.print(" \n Object of EmailService is Created !!! ");
}
public String getEmailContent() {
return emailContent;
}
public void setEmailContent(String emailContent) {
this.emailContent = emailContent;
}
public String getToAddress() {
return toAddress;
}
public void setToAddress(String toAddress) {
this.toAddress = toAddress;
}
}
每个 Spring 应用程序都需要一个上下文文件来描述 bean 的配置。之前提到的 bean 类的配置可以如下在application-context.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="emailService" class="com.packt.springbean.EmailService"
scope="singleton" />
</beans>
在这里,在 bean 定义中,我们已经将emailService作为 ID 属性,并提供了com.packt.springbean.EmailService作为类名,以指向我们的 bean 类所在的包路径。为了学习目的,我们使用了具有singleton值的scope属性。
如果在 bean 定义中没有定义scope属性,那么默认情况下,Spring IoC 容器将使用单例作用域创建 bean 的实例。现在,让我们检查如果我们尝试两次访问EmailService bean 会发生什么。为此,让我们使用SpringBeanApplication.java类:
//SpringBeanApplication.java
package com.packt.springbean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class SpringBeanApplication {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext(new String[] { "application-context.xml" });
// Retrieve emailService bean first time.
EmailService emailServiceInstanceA = (EmailService) context.getBean("emailService");
emailServiceInstanceA.setEmailContent("Hello, How are you?");
emailServiceInstanceA.setToAddress("krunalpatel1410@yahoo.com");
System.out.println("\n Email Content : " + emailServiceInstanceA.getEmailContent() + " sent to "+ emailServiceInstanceA.getToAddress() );
// Retrieve emailService bean second time.
EmailService emailServiceInstanceB = (EmailService) context.getBean("emailService");
System.out.println("\n Email Content : " + emailServiceInstanceB.getEmailContent() + " sent to "+ emailServiceInstanceB.getToAddress() );
}
}
在一个独立的应用程序中,使用ClassPathXMLApplicationContext通过传递一个字符串数组中的上下文文件作为参数来获取 Spring 上下文。Spring IoC 容器初始化应用程序上下文,并返回它的一个对象。
通过在getBean()方法中以参数形式传递一个 bean name来检索 bean。在先前的例子中,我们使用getBean()方法获取了两个EmailService bean 的实例。但是,第一次我们只是将值设置到一个 bean 中,并且通过写入打印消息来获取相同的值。甚至构造函数也只创建 bean 对象一次。
因此,当我们运行SpringBeanApplication时,输出将如下所示:
Feb 09, 2018 6:45:15 AM org.springframework.context.support.AbstractApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@6fc6f14e: startup date [Fri Feb 09 06:45:15 IST 2018]; root of context hierarchy
Feb 09, 2018 6:45:15 AM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [application-context.xml]
Object of EmailService is Created !!!
Email Content : Hello, How are you? sent to krunalpatel1410@yahoo.com
Email Content : Hello, How are you? sent to krunalpatel1410@yahoo.com
由于EmailService bean 具有单例作用域,因此第二个实例emailServiceInstanceB打印的消息将包含由emailServiceInstanceA设置的值,即使它是通过一个新的getBean()方法获取的。Spring IoC 容器为每个容器创建并维护一个 bean 的单例实例;无论你使用getBean()多少次回收它,它都将持续返回相同的实例。
使用原型作用域
正如我们所见,原型作用域用于每次请求时获取一个新实例的 bean。为了理解原型作用域,我们将使用相同的 bean 类,EmailService,并且我们只需要更改application-context.xml中emailService bean 的作用域属性值:
<?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="emailService" class="com.packt.springbean.EmailService"
scope="prototype" />
</beans>
用于单例作用域的代码将与之前相同,而前述代码的输出将如下所示:
Feb 09, 2018 7:03:20 AM org.springframework.context.support.AbstractApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@6fc6f14e: startup date [Fri Feb 09 07:03:20 IST 2018]; root of context hierarchy
Feb 09, 2018 7:03:20 AM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [application-context.xml]
Object of EmailService is Created !!!
Email Content : Hello, How are you? sent to krunalpatel1410@yahoo.com
Object of EmailService is Created !!!
Email Content : null sent to null
从输出中可以看出,EmailService构造函数被调用了两次,并且每次调用getBean()方法时都会获取一个新的实例。对于第二个实例,emailServiceInstanceB,我们得到一个null值,因为我们还没有为它设置任何值。
使用注解的 Java 配置
一旦在 Java 1.5 中引入了注解,Spring 框架也在 2.5 版本中添加了对注解的支持。
Spring 提供了几个标准注解,这些注解用于应用程序中的.stereotype 类。通过使用这些注解,我们不需要在 XML 文件中维护 bean 定义。我们只需要在 Spring XML 配置中写一行 <context:component-scan>,Spring IoC 容器就会扫描定义的包,并在应用程序上下文中注册所有注解类及其 bean 定义。
具体来说,@Component 和 @Service 用于扫描提供的包中的 beans。在这里,我们将使用 @Service 注解,因为 @Service 注解比 @Component 注解更专业。它并没有比 @Component 解释提供任何额外的行为,但最好在服务层类中选择 @Service 而不是 @Component,因为它能更好地表明期望。
对于单例和原型 beans,我们的 application-context.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:annotation-config /> -->
<context:component-scan base-package="com.packt.springbeanannotation" />
</beans>
带注解的单例范围
@Scopes 注解用于指示 bean 的范围,无论是单例、原型、请求、会话还是一些自定义范围。
要使 EmailService bean 类成为单例,我们需要用 @Scope 和 @Service 注解该类。因此,我们的 EmailService 类将如下所示:
package com.packt.springbeanannotation;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
@Service
@Scope("singleton")
public class EmailService {
private String emailContent;
private String toAddress;
public EmailService() {
System.out.print(" \n Object of EmailService is Created !!! ");
}
public String getEmailContent() {
return emailContent;
}
public void setEmailContent(String emailContent) {
this.emailContent = emailContent;
}
public String getToAddress() {
return toAddress;
}
public void setToAddress(String toAddress) {
this.toAddress = toAddress;
}
}
我们将使用相同的 SpringBeanApplication.java 类来测试我们的注解更改,输出也将与 XML 配置示例相同。
带注解的原型范围
要使用注解的原型范围,我们只需要在 @Scope 注解中提及 prototype 而不是 singleton。因此,我们的 EmailService.java 类将保持不变,除了我们更改注解值,它将如下所示:
@Service
@Scope("prototype")
public class EmailService {
...
}
与 XML 示例输出类似,每次调用时它也会创建一个新的实例。以类似的方式,我们可以使用其他范围,如请求、会话、应用程序和全局会话,使用 XML 元数据或注解。
依赖注入和 bean 范围
我们理解每个范围都有不同的边界。现在,我们将编写一个 REST 控制器,通过编写简单的 Spring Boot 应用程序来了解不同的范围 beans 如何通过其他引用 beans 注入。
在下面的图中,StudentController 注入了所有其他类的引用。具有 session 范围的 ClassDetail 类有两个单例和原型的引用,学生应用程序还包含一些其他类之间的关联。使用 Autowired 注解来满足 beans 之间的依赖关系。为了澄清,Spring 控制器始终以单例范围创建:

由于我们正在编写一个带有 REST 的 Spring Boot 应用程序。我们需要创建一个 Maven 项目,pom.xml 文件的配置将是:
<?xml version="1.0" encoding="UTF-8"?>
<project
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.packt.java9.beanscope</groupId>
<artifactId>spring-beanscope-test</artifactId>
<version>0.1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.8.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<properties>
<java.version>9</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
我们从StudentController类开始,注入了四个具有不同作用域定义的 bean:
package com.packt.java9.beanscope.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.packt.java9.beanscope.beans.PrototypeBeanScope;
import com.packt.java9.beanscope.beans.RequestBeanScope;
import com.packt.java9.beanscope.beans.SessionBeanScope;
import com.packt.java9.beanscope.beans.SingletonBeanScope;
@RestController
public class StudentController {
public StudentController() {
System.out.println(" ::::::::::::::::::::: StudentController Initialized :::::::::::::::: ");
}
@Autowired
PrototypeBeanScope prototypeBeanScope;
@Autowired
SessionBeanScope sessionBeanScope;
@Autowired
RequestBeanScope requestBeanScope;
@Autowired
SingletonBeanScope singletonBeanScope;
@RequestMapping("/")
public String index() {
sessionBeanScope.printClassDetail();
requestBeanScope.printAddress();
return " Greetings from Student Department !!";
}
}
为了更好地可视化每个作用域,我创建了具有作用域名称的简单接口,这也有助于我们在一个 bean 中添加另一个 bean 的依赖项时。使用@Scope注解将StudentDetailbean 指定为单例,并且它实现了SingletonBeanScope接口。这个类已经被注入了一个PrototypeBeanScopebean。此外,我们正在打印静态整型变量increment的增量值,以跟踪单例 bean 在构造函数中初始化的次数。其他所有 bean 类都有相同的写法。StudentDetail.java将如下所示:
package com.packt.java9.beanscope.beans;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
@Service
@Scope("singleton")
public class StudentDetail implements SingletonBeanScope {
/* Inject PrototypeBeanScope to observer prototype scope behaviour */
@Autowired
PrototypeBeanScope prototypeBeanScope;
private static int increment = 0;
/**
* Every time this bean is initialized, created variable will be increases by
* one.
*/
public StudentDetail() {
super();
System.out.println(" \n ::::::: Object of StudentDetail bean is created " + (++increment) + " times ::::::: ");
}
}
SubjectPreference.java被定义为原型 bean 作用域如下:
package com.packt.java9.beanscope.beans;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
@Scope("prototype")
public class SubjectPreference implements PrototypeBeanScope {
private static int increment = 0;
/**
* Every time this bean is initialized, created variable will be increases by
* one.
*/
public SubjectPreference() {
System.out.println(" \n ::::::: Object of SubjectPreference with Prototype scope is created " + (++increment)
+ " Times ::::::: \n ");
}
}
请求作用域和会话作用域仅在具有 web 感知的应用程序上下文中工作。Address.java被注解为请求作用域:
package com.packt.java9.beanscope.beans;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class Address implements RequestBeanScope {
private static int increment = 0;
/* Inject PrototypeBeanScope to observer prototype scope behaviour */
@Autowired
PrototypeBeanScope prototypeBeanScope;
/**
* Every time this bean is initialized, created variable will be increases by
* one.
*/
public Address() {
System.out.println(
" \n ::::::: Object of Address bean with Request scope created " + (++increment) + " Times ::::::: ");
}
public void printAddress() {
System.out.println("\n :::::::::::::: RequestbeanScope :: printAddress() Called :::::::::::::: ");
}
}
同样,session作用域在ClassDetail.java类中使用:
package com.packt.java9.beanscope.beans;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Repository;
@Repository
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class ClassDetail implements SessionBeanScope {
/* Inject SingletonBeanScope to observer session scope behaviour */
@Autowired
SingletonBeanScope singletonBeanScope;
/* Inject PrototypeBeanScope to observer prototype scope behaviour */
@Autowired
PrototypeBeanScope prototypeBeanScope;
private static int increment = 0;
/**
* Every time this bean is initialized, created variable will be increases by
* one.
*/
public ClassDetail() {
System.out.println(" \n ::::::: Object of ClassDetail bean with session scope created " + (++increment)
+ " Times ::::::: ");
}
public void printClassDetail() {
System.out.println("\n ::::::::: Session Bean - PrintMessage Method Called ::::::::::::::::::: ");
System.out.println("\n ::::::::: SessionBeanScope :: printClassDetail() Called ::::::::::::::: ");
}
}
使用额外的proxyMode属性来创建一个中介,该中介将被 Spring 作为依赖项注入,并且当需要时 Spring 将启动targetbean。注意,当初始化 web 应用程序设置时,没有动态请求。
在成功运行后,我们将看到以下控制台日志:

下面的分析是输出:
-
StudentDetailbean 只创建一次,它最多是一个单例类,并在应用程序启动时加载。 -
随后,使用原型作用域创建了
SubjectPreference对象。它被注入到StudentDetail单例 bean 中,因此它也与其一起初始化。在这里,我们都知道原型作用域 bean 每次被调用时都会被创建。 -
在这里,
StudentDetail单例 bean 依赖于由SubjectPreference类实现的PrototypeBeanScope,依赖项在实例化时解决。因此,首先创建SubjectPreference的第一个实例,然后将其注入到StudentDetail单例 bean 中。 -
Spring 容器只初始化一次
StudentController类,因为默认情况下controller是一个单例。 -
由于
StudentController注入了PrototypeBeanScope的引用,因此再次创建了一个SubjectPreferencebean 的实例。控制器也有SingletonbeanScopebean 的引用,但不会再次创建该实例,因为它已经加载。 -
由于此时没有 HTTP 请求或 HTTP 会话,因此没有创建
SessionScopeBean和RequestScopeBean的实例。
要检查request和session作用域,请在浏览器中访问http://localhost:8080并观察控制台日志:

日志显示,由于ClassDetail和Address类分别定义了会话和请求范围,因此为这两个类各创建了一个实例。ClassDetail和Address类也注入了PrototypeBeanScope,因此SubjectPreference实例被创建了两次,总共四次。
再次输入http://localhost:8080 URL:

它将创建一个标记为请求范围的Address类的新实例,并且将创建一个具有原型范围的SubjectPreference类的新实例。它不会创建ClassDetail类的实例,因为我们没有创建新的会话,我们的会话仍在进行中。
要启动一个新的会话,我们需要关闭浏览器并转到 URL。打开另一个浏览器,并转到 URL:

这样,将创建两个新的会话,ClassDetail类将创建总共三个实例,以及两个Address类实例和两个SubjectPreference类实例。
如果我们需要将请求范围 bean 注入到具有更长生命周期的范围的另一个 bean 中,您可以选择在范围 bean 中注入 AOP 代理。我们需要注入一个中间对象,该对象公开与范围对象相同的公共接口。但可以从适用范围中恢复目标对象,并提供对真实对象的方法调用。
此外,该 bean 正在通过一个可序列化的中间代理。相同的 bean 随后可以通过反序列化重新获取target单例 bean。标记为单例的 bean 使用<aop:scoped-proxy/>。
同样,当使用原型 bean 范围时,对共享代理的每次方法调用都会导致创建一个新的目标实例,此时调用将被发送到该实例。
当 Spring 容器为经过<aop:scoped-proxy/>组件检查的 bean 创建代理时,默认情况下会创建一个基于 CGLIB 的类代理。
如何选择 bean 范围
Spring 中的每个范围都有不同的特性,作为程序员,我们需要知道如何利用这些范围。
在一个应用程序中,如果我们有一个无状态对象并且对对象创建过程没有影响,那么使用范围是不必要的。相反,如果一个对象有状态,那么建议使用单例等范围。
当依赖注入在业务中使用时,单例范围并没有增加太多价值。尽管单例节省了对象创建(以及随后的垃圾收集),但同步需要我们初始化一个单例 bean。单例最有价值的是:
-
状态 ful bean 的配置
-
查找构建成本高昂的对象
-
与资源关联的数据库连接池对象
如果考虑并发性,使用单例或会话作用域定义的类必须是线程安全的,并且注入到这些类中的任何内容也应该是线程安全的。另一方面,请求作用域不能是线程安全的。
Google Guice 中的作用域
我们在 Spring 框架中看到的大部分作用域在 Google Guice 中也存在。作用域定义了代码应在特定上下文中工作,在 Guice 中,注入器管理作用域上下文。默认作用域(无作用域)、单例、会话和请求是 Guice 中的主要作用域。
默认作用域
默认情况下,Guice 为每个依赖项注入一个新的独立实例的对象(类似于 Spring 中的原型作用域),而 Spring 默认提供单例。
让我们考虑一个有三口之家且每人都有自己的私家车的房子的例子。每次他们调用injector.getInstance()方法时,每个家庭成员都可以获得一个新的汽车对象实例:
home.give("Krunal", injector.getInstance(Car.class));
home.give("Jigna", injector.getInstance(Car.class));
home.give("Dirgh", injector.getInstance(Car.class));

单例作用域
如果我们只想创建类的单个实例,那么可以使用@Singleton注解来标记实现类。只要单例对象存在,注入器就存在于上下文中,但在同一应用程序中,可能存在多个注入器,在这种情况下,每个注入器都与单例作用域对象的实例相关联:
@Singleton
public class DatabaseConnection{
public void connectDatabase(){
}
public void disconnectDatabase(){
}
}
另一种配置作用域的方法是在模块中使用绑定语句:
public class ApplicationModule extends AbstractModule{
@Override
protected void configure() {
//bind service to implementation class
bind(NotificationService.class).to(SMSService.class).in(Singleton.class);
}
}
当我们在模块中使用链接绑定时,作用域仅适用于绑定源,而不适用于目标。例如,我们有一个名为UserPref的类,它实现了Professor和Student接口。这将创建两个实例:一个用于Professor,另一个用于Student:
bind(Professor.class).to(UserPref.class).in(Singleton.class);
bind(Student.class).to(UserPref.class).in(Singleton.class);
这是因为单例作用域应用于绑定类型级别,即Professor和Student,而不是目标类型UserPref。
急切单例
Guice 提供了特殊的语法来创建具有单例作用域的对象,并且初始化为急切模式而不是懒模式。以下是其语法:
bind(NotificationService.class).to(SMSService.class).asEagerSingleton();
热切的单例模式可以更快地发现初始化问题,并确保最终用户获得可靠、智能的体验。懒单例模式可以加速编辑-编译-运行的开发周期。我们可以利用阶段枚举来指示应该使用哪个过程。
以下表格定义了单例和受支持对象初始化模式的语法阶段使用:
| 语法 | 生产 | 开发 |
|---|---|---|
| @Singleton | eager* | lazy |
| .asEagerSingleton() | eager | eager |
| .in(Singleton.class) | eager | lazy |
| .in(Scopes.SINGLETON) | eager | lazy |
Guice 仅对定义为单例的模块急切创建单例实例。
@SessionScoped和@RequestedScoped作用域的功能和行为在 Guice 中与 Spring 相同,并且仅在用于 Web 应用程序时适用。
摘要
我们以 Spring Bean 定义属性开始本章,这是学习的重要部分,因为整个 IoC 容器依赖于 Bean 的初始化。之后,我们通过语法学习了作用域的分类。
在我们的旅程中,我们学习了如何在 Spring 中使用 XML 元数据和 Java 配置来配置作用域。没有依赖注入,我们无法完成本章。这就是为什么,通过编写一个Spring Boot应用程序,我们试图理解主作用域在独立应用以及 Web 应用中的工作方式。
我们故意跳过了第四章中的作用域主题,即使用 Google Guice 进行依赖注入。因此,在本章中,我们用基本作用域涵盖了 Google Guice 的作用域。Spring 和 Google Guice 的作用域几乎相同,但对象初始化的默认行为不同。Spring 使用单例创建实例,而 Guice 使用原型作用域创建。
在下一章中,我们将探讨 Spring 中一个重要的特性,称为面向切面编程。
第六章:面向切面编程和拦截器
到目前为止,我们已经了解了依赖注入的概念及其在 Spring 和 Google Guice 等流行框架中的实现。我们还学习了如何根据业务需求对 bean 进行范围划分,以控制对象创建过程。在本章中,我们将学习实现关注点分离的另一种方法:面向切面编程(AOP)。
AOP 通过将重复代码从应用程序中隔离出来并动态插入,解决了设计问题的一部分。AOP 与控制反转(IoC)一起,为应用程序带来了模块化。AOP 有助于以分层方式组织您的应用程序,这在传统的面向对象方法中是不可能的。
AOP 允许您拦截业务代码的流程,并直接注入一系列功能,而无需触及或更改原始代码。这使得您的应用程序与这些常见功能松散耦合。在我们深入探讨这个概念之前,让我们首先了解场景、问题以及如何使用 AOP 作为有效的解决方案。
在本章中,我们将发现和讨论以下有趣的主题:
-
AOP 是什么,以及您可以使用 AOP 解决哪些问题
-
如何在 Spring 框架中实现 AOP
-
选择 AOP 框架和配置风格
AOP 简介
在编写任何软件应用程序时,最佳实践是根据业务用例将代码划分为多个独立的模块。例如,您为所有与员工相关的功能编写一个Employee Service类,为所有与人力资源相关的功能编写一个HRService类,等等。
通常,整个应用程序由一组独立的类组成,这些类跨越多个垂直领域,并且不共享相同的类层次结构。此图展示了这一场景:

尽管每个垂直领域都是独立的,但您需要在所有这些领域实现一些共同的项目,例如事务管理、会话管理、审计日志、安全、缓存或任何基于规则的定制处理机制。
如果您希望使用传统方法在垂直领域实现这些常见服务,您需要手动将这些服务放入这些类的每个方法中。以日志机制为例,为此,您需要在每个方法的开始和结束时编写一些代码。
这导致了代码重复,因为相同的逻辑需要被放置多次。这导致在应用程序开发过程的后期引入任何更改时,维护变得噩梦般。让我们了解如何。
假设,根据你的业务需求,你在每个更新和删除方法之后添加审计日志。你将方法名称及其时间放入日志中。现在,假设你的业务需要在日志中放置当前登录用户的名称。在这种情况下,你需要手动在几个方法中更新日志细节。
这只是一个例子。你最终需要对分布在多个垂直领域的每个通用服务进行代码更改。有效的解决方案是将它们与垂直领域隔离开来。在一个地方实现它们,并根据某些规则或标准,在需要时将它们插入到其他核心业务类中。
从本质上讲,业务逻辑的核心部分不需要知道跨越多个类的通用内容已被包含、删除或更改,并且可以像以前一样继续工作。将通用功能(AOP 范式中的横切关注点)分离出来,在不接触或修改核心业务逻辑的情况下打开和关闭它们,最终会增加模块化,并在任何应用程序的维护方面带来极大的灵活性。AOP 旨在提供实现这一解决方案的方法。AOP 主要用于提供声明性服务。
要理解面向切面编程(AOP)的概念,关键是要了解 AOP 范式中所使用的术语:
-
关注点(Concern):这是我们希望在应用程序中实现的行为或功能。例如,人力资源管理员工管理是两个功能,因此被视为 AOP 中的关注点。
-
切面(Aspect):用非常简单的话来说,这是跨越同一或不同层次结构中多个类的通用行为。换句话说,跨越多个关注点的通用概念称为切面。在我们的例子中,日志机制在 AOP 术语中被称为切面。
-
连接点(Join-point):这是在应用程序执行流程中需要应用通知(Advice)的点。例如,方法调用或需要处理异常的地方可以是连接点。
-
通知(Advice):这是 AOP 框架在特定连接点上执行的操作。从概念上讲,它是在该连接点上实现的通用功能。应用通知的过程可以通过指定各种类型来控制,例如
around、before、after、throws等。 -
切入点(Point-cut):这是一个描述适用连接点模式的表达式。换句话说,AOP 框架将在由切入点(例如,
set*表示所有以单词set开头的方法)描述的连接点(方法)上应用通知(Advice)(通用功能)。我们可以说切入点是选择系统中连接点的筛选标准。
在大多数情况下,开发人员会在连接点和切入点之间感到困惑。让我们通过一个现实生活中的例子来理解这种区别。假设你想买食用油,你去了百货商店。你到达了杂货区,发现各种来自不同来源的可食用油,如葵花籽油、花生油、棉籽油、大米品牌、玉米等等。
您的要求是选择轻油(就低胆固醇而言)来满足您的日常需求,因此您选择葵花籽油或大米品牌油。在这种情况下,所有可用的食用油都是连接点,而您根据需求选择的葵花籽油/大米品牌油被认为是切入点。简而言之,所有可用的选项都被认为是连接点,而您根据需求选择的那个被称为切入点。
-
目标对象:这是实现公共功能的对象。换句话说,这是由一系列方面应用通知的对象。
-
AOP-代理:代理是一种设计模式,用于封装对象并控制对其的访问。AOP 框架创建一个代理/动态对象以实现各种方面(以通知的形式)。简而言之,AOP 创建了一个代理对象,它看起来像创建代理的对象,但具有一些额外的功能。在Spring 框架中,AOP 代理通过 JDK 或 CGLIB 库提供。
-
织入:正如我们所见,AOP 背后的主要思想是将公共行为(或方面)插入业务类中,而不修改它们。将此类方面与其他类链接以应用通知的过程称为织入。
织入可以在编译或运行时进行。Spring AOP支持加载时织入,而AspectJ框架支持编译时和加载时织入。
-
编译时织入:在这种类型的织入中,链接方面的过程是在编译时执行的。AOP 框架将方面应用到您的 Java 源文件上,并创建一个二进制类文件,该文件与这些方面交织在一起。AspectJ 使用一个特殊的编译器来实现编译时织入。
-
编译后(或二进制)织入:这与编译时织入类似。链接方面的过程是在预编译的类或 JAR 文件上执行的。织入的方面可以是源形式或二进制形式。这同样可以通过一个特殊的编译器来完成。编译时和编译后织入都可以通过 AspectJ 实现。
-
运行时织入:编译时和编译后织入发生在实际类文件被加载到内存之前,而运行时(或加载时)织入发生在目标类被类加载器加载到 JVM 之后。运行时织入器由 Spring AOP 和 AspectJ 框架都支持。
织入过程可以通过以下图表表示:

Spring AOP
Spring AOP完全是用 Java 开发的。它不需要我们修改或控制类加载器层次结构。正因为这种适应性,您可以使用 Spring AOP 于 servlet 容器或应用服务器。目前,Spring AOP 仅支持在方法级别应用通知。换句话说,Spring AOP 支持方法级别的连接点。
Spring 支持 AOP 与它的 IoC 能力相结合。您可以使用常规的 Bean 定义来定义方面,并通过 AOP 特定的配置来织入它们。换句话说,IoC 用于定义方面,而 AOP 用于将它们织入其他对象。Spring 使用这两者来解决常见问题。这就是 Spring AOP 与其他 AOP 框架不同的地方。
Spring AOP 是一个基于代理的框架,并支持对象的运行时织入。它可以通过基于 XML 或 AspectJ 注解的配置来使用。
基于 XML 模式的 Spring AOP
就像类是面向对象编程范式的单元一样,方面是 AOP 的单元。在面向方面的编程模型中,通过方面实现模块化。如果您希望选择基于 XML 的 AOP 配置,Spring 支持使用aop命名空间标签来定义方面。您需要按照以下方式定义aop命名空间标签:
<?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/aop
http://www.springframework.org/schema/aop/spring-aop.xsd"
>
为了区分 AOP 特定的配置,您需要在 Spring 上下文(XML)文件内的<aop:config>元素中指定所有 AOP 相关的工件,如方面、切点、通知等。允许存在多个<aop:config>元素。
声明方面
Spring AOP 的第一步是决定并定义方面。在基于 XML 的配置中,方面被构想为一个简单的 Java 类,您需要将其声明为 Spring 应用程序上下文(XML)文件中的 Bean 定义。《aop:aspect》元素用于定义方面:
<aop:config>
<aop:aspect id="myLoggin" ref="loggingAspect"></aop:aspect>
</aop:config>
<bean id="loggingAspect"
class="com.packet.spring.aop.aspects.LogginAspect">
</bean>
由于方面是一种 Java 类的形式,它可以被定义为普通的 Spring Bean,然后可以通过<aop:aspect>元素的ref属性进行配置。状态和行为与方面的类的字段和方法相关联,而切点和通知信息则在 XML 中进行配置。在先前的例子中,我们将日志定义为方面。
定义方面之后,下一步是通过切点定义连接点。Spring AOP 仅支持方法级别的连接点。
在基于 XML 模式的 AOP 中,Spring 通过<aop:config>声明启用自动代理机制。您不需要显式定义任何自动代理的内容;然而,如果您通过其他机制(如 AutoProxyCreator)启用自动代理,您应该选择这些选项之一以避免任何运行时问题。
声明切点
就回忆一下,连接点是我们想要应用通知的地方,而点设计符代表匹配连接点的模式。点设计符必须在<aop:config>元素内定义。点设计符可以在<aop:aspect>元素内或外部声明。如果它在<aop:aspect>外部定义,它可以在多个方面和通知之间共享。
点设计符允许通知独立于面向对象层次结构应用于目标对象。通过 Spring AOP 通知进行事务管理是一个真正的例子,其中事务通知应用于跨越多个对象层次结构的方法(add/update/delete方法)。这个片段是编写点设计符的可能方法之一:
<aop:pointcut id="employeeServiceMethods"
expression="execution(* com.packet.spring.aop.service.*.*(..))" />
点设计符通过其id属性唯一标识。expression属性表示匹配连接点的模式(或过滤器)。expression属性的值由两个组件组成:
-
点设计符
-
模式
点设计符
点设计符(PCD)是一个关键字(初始单词),告诉 Spring AOP 如何匹配点设计符。Spring AOP 支持各种点设计符:
-
execution: 这用于匹配方法执行(连接点)。这是一个主要设计符,在处理 Spring AOP 时通常使用。 -
within: 这个设计符限制了仅匹配特定类型的连接点。它不如执行灵活。例如,不允许指定返回类型或方法参数映射。如果within的模式达到 Java 包级别,它将匹配该包中所有类的所有方法。如果模式指向一个特定的类,那么这个设计符将覆盖该类中的所有方法。 -
this: 这限制了连接点的匹配,仅限于表达式中的给定类型的 bean。换句话说,this设计符比within设计符更窄一步,并期望你指定一个特定的类类型作为模式。不允许定义任何通配符模式。 -
target: 这限制了匹配连接点,其中目标对象是表达式中的给定类型的实例。目标设计符似乎与this设计符相似,但它们的使用有所不同。让我们来理解这一点。
正如我们所见,Spring AOP 通过 JDK 或 CGLIB 库创建代理对象。如果目标对象实现了接口,Spring AOP 使用基于 JDK 的代理;否则,它选择 CGLIB。当 CGLIB 提供代理时(即,你的目标对象没有实现接口),你应该使用this设计符;当 JDK 提供代理时(target对象实现了接口),你应该使用目标设计符。
-
args:这个指示符通常用于匹配方法参数。它允许我们传递通配符来匹配包、Java 类、返回类型或方法名称。 -
@target:这个 PCD(点切割定义)过滤了具有给定类型注解的对象类的连接点。尽管名称相同,但@target指示符与target指示符不同。它们在匹配连接点方面有所不同,如下所示:-
target指示符:如果表达式中的目标对象是给定类型的实例,则匹配目标对象。 -
@target指示符:如果目标对象的类具有给定类型的注解,则匹配目标对象。
-
-
@within:这个指示符限制了匹配的连接点在具有给定注解的类型内部。它允许我们使用通配符来匹配点切割。 -
@annotation:这个 PCD 用于匹配具有给定注解类型的点切割。这对于在具有自定义注解的类上构建点切割非常有用。 -
@args:这个指示符限制了匹配连接点,只有当实际运行时作为方法参数传递的对象具有给定类型的注解时。这有助于将连接点选择缩小到目标类中可用的重载方法中的特定方法。
模式
模式是匹配可能连接点的过滤条件。它告诉 Spring AOP 要匹配什么。模式通常在 PCD 之后、括号内书写,它是一种 AOP 中的正则表达式,用于选择所需的连接点。
Spring AOP 只支持方法级别的连接点,并且使用模式来选择目标对象的具体方法。一个模式由以下表达式按相同顺序组成:
-
访问修饰符:对于 Spring AOP,唯一可能的值是
public。这个表达式是可选的。 -
返回类型:这是返回类型的完全限定名称。使用
*(星号)表示允许任何返回类型。 -
Java 包:可以使用 Java 包名称。
-
Java 类名称:可以使用 Java 类名称。在这个表达式中使用
*表示它适用于给定包下的所有 Java 类。 -
方法名称:可以指定方法名称。在这个表达式中使用
*将包括给定类中的所有方法。 -
方法参数:可以指定参数类型。使用
..(两个点)表示给定方法可以接受任意数量的参数。 -
异常详情:抛出声明。
模式的格式与方法签名完全相同。让我们通过查看一些示例来理解前面表达式的含义。
示例 1:以下表达式将匹配满足以下条件的EmployeeService类中的所有公共方法:
-
包含任何返回值的方法,包括 void
-
包含空参数方法在内的任何参数的方法:

示例 2:以下表达式将匹配满足以下条件的所有公共方法:
-
任何返回值的方法,包括无返回值的方法
-
任何参数的方法,包括空参数方法
-
直接位于
com.packet.spring.aop.serviceJava 包下的所有类的成员方法:

示例 3:以下表达式将匹配所有满足以下条件的公共方法:
-
任何返回值的方法,包括无返回值的方法
-
任何参数的方法,包括空参数方法
-
位于
com.packet.spring.aop.serviceJava 包及其子包下的所有类的成员方法:

示例 4:以下表达式将匹配EmployeeService类的所有公共方法,满足以下条件:
-
只返回
String类型的方法 -
任何参数的方法,包括空参数方法:

示例 5:以下表达式将匹配EmployeeService类的所有公共方法,满足以下条件:
-
任何返回值的方法,包括无返回值的方法
-
有两个参数的方法,第一个参数是
String类型,第二个参数是Long类型,且顺序与方法参数相同:

示例 6:以下表达式将匹配EmployeeService类的所有公共方法,满足以下条件:
-
任何返回值的方法,包括无返回值的方法
-
有一个或多个参数的方法,其中第一个参数只能是
String类型:

示例 7:以下表达式将匹配满足以下条件的特定公共方法:
-
方法名称以
find单词开头 -
任何返回值的方法,包括无返回值的方法
-
只有一个参数类型为
String的方法 -
位于
com.packet.spring.aop.serviceJava 包下的所有类的成员方法:

声明通知(拦截器)
下一步是定义通知。这是在连接点上执行的操作。通知也被称为拦截器。Spring AOP 支持各种类型的通知,如此处所示:
-
前置通知:这种类型的通知是在连接点执行开始之前执行的。在发生异常的情况下,Spring 将停止进一步执行此通知。
-
返回后通知:正如其名所示,这种类型的通知是在连接点执行完成后执行的(无论是正常退出,还是在异常情况下从连接点退出)。
-
环绕通知:这种类型的通知是在连接点周围执行的(在通知方法之前和/或之后)。因此,您有控制权来执行连接点并返回原始方法值,或者绕过流程并返回自定义值。通常,环绕通知用于在方法的主要逻辑之前和/或之后执行某些逻辑。因此,它是通知中最强大的一种类型。
-
返回后通知:这与返回后通知类似。区别在于它是在连接点的正常退出时执行的。
-
抛出通知之后:这和后置通知类似,但它在执行通知方法的执行过程中发生异常时执行。
在基于 XML 的 Spring AOP 中,当你定义<aop:config>内的 AOP 元素时,需要小心 AOP 元素的顺序。例如,<aop:pointcut>必须在<aop:aspect>元素之前或其中定义,否则 Spring 将显示错误。同样,AOP 通知必须在<aop:aspect>元素内定义。
实现前置通知
到目前为止,我们已经看到了如何声明方面、切入点和建议。让我们将它们放在一起,了解它们是如何协同工作的。假设我们想在属于com.packet.spring.aop.service包的所有类的所有方法的开头放置一个记录器。配置如下:
<aop:config>
<aop:pointcut id="employeeServiceMethods"
expression="execution(* com.packet.spring.aop.service.*.*(..))" />
<aop:aspect id="myLoggin" ref="loggingAspect">
<aop:before pointcut-ref="employeeServiceMethods"
method="printStartLog"/>
</aop:aspect>
</aop:config>
<bean id="loggingAspect" class="com.packet.spring.aop.aspects.LoggingAspect">
</bean>
<bean id="employeeService" class="com.packet.spring.aop.service.EmployeeService">
</bean>
我们定义了一个切入点,它匹配com.packet.spring.aop.service包下所有类的所有公共方法,无论参数和返回值如何。接下来,我们定义了日志方面 bean,并将其引用通过ref属性传递给<aop:aspect>。
在方面中,我们之前已经定义了通知(<aop:before>)并在那里提供了切入点引用。printStartLog方法在方面 bean 中定义如下:
package com.packt.spring.aop.aspects;
import org.aspectj.lang.JoinPoint;
public class LoggingAspect {
public void printStartLog(JoinPoint joinPoint) {
System.out.println(" ****** Starting Method '"+joinPoint.getSignature().getName()+"' of "+joinPoint.getTarget().getClass());
}
}
printStartLog is the advice method. It takes a parameter of type JoinPoint, which represents the join-points that we associated with the aspect. This class provides metadata about the target object, such as its method (on which this advice is woven), class, and other attributes.
在printStartLog方法中不需要传递JoinPoint参数。即使你不传递JoinPoint参数,它也能正常工作。但是,它提供了关于目标对象的元数据。从这个意义上说,它是有用的。例如,在前面的案例中,我们显示了织入方法的名称和其类名。
目标类EmployeeService在com.packet.spring.aop.service包中定义如下:
package com.packt.spring.aop.service;
public class EmployeeService {
public void generateSalarySlip() {
System.out.println("Generating payslip");
}
public String showTotalEmployee(String test) {
System.out.println(" The string is -->"+test);
return test;
}
public void findEmployee(String employeeId) {
System.out.println(" finding employee based on employeeId ");
}
}
当我们从 Spring 的应用程序上下文文件中获取EmployeeService对象并调用这些方法时,AOP 将拦截所有这些方法,并在每个方法的执行之前插入日志(我们保存在LoggingAspect的printStartLog中):
package com.packt.spring.aop.aspects.main;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.packet.spring.aop.report.api.IExportPaySlip;
import com.packet.spring.aop.service.EmployeeService;
import com.packet.spring.aop.service.HRService;
public class SpringAOPInXMLCheck {
public static void main(String[] args) {
ApplicationContext springContext = new ClassPathXmlApplicationContext("application-context.xml");
EmployeeService employeeService = (EmployeeService)springContext.getBean("employeeService");
employeeService.generateSalarySlip();
employeeService.showTotalEmployee("test");
employeeService.findEmployee("abc123");
}
}
你将得到以下输出:
-------------------------------------------
****** Starting Method 'generateSalarySlip' of class com.packet.spring.aop.service.EmployeeService
Generating payslip
****** Starting Method 'showTotalEmployee' of class com.packet.spring.aop.service.EmployeeService
The string is -->test
****** Starting Method 'findEmployee' of class com.packet.spring.aop.service.EmployeeService
finding employee based on employeeId
你可以观察到 Spring AOP 如何捕获EmployeeService类的每个方法,并在每个方法的开头添加日志。它动态地打印method名称和class名称。
实现后置通知
就像前置通知一样,我们也可以实现其他通知类型。让我们以后置通知和环绕通知为例。对于后置通知,只需在LoggingAspect类中添加一个方法,如下所示:
//After advice method.
public void printEndLog(JoinPoint joinPoint) {
System.out.println(" ****** End of Method '"+joinPoint.getSignature().getName());
}
在后置通知中,我们只是打印method名称。我们还需要更新应用程序上下文文件中的方面配置。只需为我们的日志方面添加一个后置通知条目,如下所示:
<aop:aspect id="myLoggin" ref="loggingAspect">
<aop:before pointcut-ref="employeeServiceMethods" method="printStartLog"/>
<aop:after pointcut-ref="employeeServiceMethods" method="printEndLog"/>
</aop:aspect>
你将得到以下输出:
-----------------------------------
****** Starting Method 'generateSalarySlip' of class com.packet.spring.aop.service.EmployeeService
Generating payslip
****** End of Method 'generateSalarySlip
****** Starting Method 'showTotalEmployee' of class com.packet.spring.aop.service.EmployeeService
The string is -->test
****** End of Method 'showTotalEmployee
****** Starting Method 'findEmployee' of class com.packet.spring.aop.service.EmployeeService
finding employee based on employeeId
****** End of Method 'findEmployee
这次,你可以观察到 AOP 如何拦截每个方法,并实现后置通知和前置通知。
实现环绕通知
Spring AOP 还提供了环绕建议,它是一次性结合了前置和后置。如果你需要在前后处理某些事情,你可以简单地实现环绕建议,而不是分别实现前置和后置建议。要实现环绕建议,只需在LoggingAspect中添加一个额外的方法,如下所示:
//Around advice method.
public void printAroundLog(ProceedingJoinPoint proceedingJointPoint) throws Throwable {
System.out.println("----- Starting of Method "+proceedingJointPoint.getSignature().getName());
proceedingJointPoint.proceed();
System.out.println("----- ending of Method "+proceedingJointPoint.getSignature().getName());
}
对于附近的建议,Spring AOP 提供ProceedingJoinPoint对象而不是JoinPoint对象。proceedingJoinPoint.proceed()将简单地调用target对象上的方法,你可以在proceedingJoinPoint.proceed()调用上方放置前置逻辑,并在其旁边放置后置逻辑。
ProceedingJoinPoint只能用于环绕建议。如果你尝试将其用于前置或后置建议,你会得到一个错误。对于它们,你应该只使用连接点。
最后一步是更新配置,为方面插入环绕建议,如下所示:
<aop:aspect id="myLoggin" ref="loggingAspect">
<aop:around pointcut-ref="employeeServiceMethods" method="printAroundLog"/>
</aop:aspect>
你将得到以下输出:
----------------------------------
----- Starting of Method generateSalarySlip
Generating payslip
----- ending of Method generateSalarySlip
----- Starting of Method showTotalEmployee
The string is -->test
----- ending of Method showTotalEmployee
----- Starting of Method findEmployee
finding employee based on employeeId
----- ending of Method findEmployee
这就是环绕建议的实现方式,以及它如何一起完成前置和后置建议的工作。
不要同时使用前置、后置和环绕建议。如果你需要在方法开始处放置一些额外的代码,请仅使用前置建议,而不是环绕。虽然使用环绕建议可以单独使用前置建议实现的功能,但这不是一个好主意。只有当你想在方法前后添加内容时才使用环绕建议。
实现返回后建议
返回后建议的工作方式与返回建议相同。唯一的区别是,此建议仅在正常退出时在匹配的方法上执行。如果发生异常,则此建议不会应用。
我们将查看一个场景来理解这个建议的需求。假设你希望在target类上执行特定方法成功时向相关人员发送消息(电子邮件)。由于这是一个新的关注点(在target方法成功执行时发送消息),我们已创建了一个新的方面类,如下所示:
package com.packt.spring.aop.aspects;
import org.aspectj.lang.JoinPoint;
public class SendMessage {
//Advice method after successful existing of target method.
public void sendMessageOnSuccessExit(JoinPoint joinPoint) {
System.out.println(" ****** Method '"+joinPoint.getSignature().getName()+"' of "+joinPoint.getTarget().getClass()+" is executed successfully...");
}
}
SendMessage方面有一个名为sendMessageOnSuccessExit的方法。我们希望这个方法在target类的正常方法退出(没有异常)时被调用。你可以在该方法中编写发送消息(电子邮件)的逻辑。新的方面将在应用程序上下文(XML)文件中按如下方式配置:
<aop:pointcut id="hrServiceMethods"
expression="execution(* com.packet.spring.aop.service.HRService.*(..))" />
<aop:aspect id="sendMsg" ref="sendMsgAspect">
<aop:after-returning pointcut-ref="hrServiceMethods"
method="sendMessageOnSuccessExit"/>
</aop:aspect>
我们创建了一个新的切入点,它将匹配HRService类的所有方法。这个类将如下所示:
package com.packt.spring.aop.service;
public class HRService {
public void showHolidayList() {
System.out.println("This is holiday list method...");
}
public void showMyLeave() throws Exception {
System.out.println("Showing employee leaves...");
throw new Exception();
}
}
当你从 Spring 获取HRService对象并调用showHolidayList方法时,你将得到以下输出:
This is holiday list method...
****** Method 'showHolidayList' of class com.packet.spring.aop.service.HRService is executed successfully...
如果target方法返回一个值,并且你想使用 AOP 修改它,你可以通过返回后建议来完成。为此,你需要在<aop:aspect>元素中指定参数名,如下所示:
<aop:aspect id="sendMsg" ref="sendMsgAspect">
<aop:after-returning pointcut-ref="hrServiceMethods"
returning="retVal"
method="sendMessageOnSuccessExit"/>
</aop:aspect>
在此代码中,returning 属性的值表示 sendMessageOnSuccessExit 方法必须声明一个名为 retVal 的参数。Spring AOP 在应用此建议时,将target对象的方法的返回值传递给此参数(retVal)。因此,target对象的方法的返回值类型必须与建议方法中参数的类型(在我们的例子中是retVal)兼容。让我们按照以下方式更新 SendMessage 建议的 showHoliday 方法:
public String showHolidayList() {
System.out.println("This is holiday list method...");
return "holidayList";
}
此方法的返回值类型为 String。要更新返回值,您需要按照以下方式更改建议方法:
//Advice method after successful existing of target method.
public String sendMessageOnSuccessExit(JoinPoint joinPoint,String retVal) {
System.out.println(" ****** Method '"+joinPoint.getSignature().getName()+"' of "+joinPoint.getTarget().getClass()+" is executed successfully...");
System.out.println(" The return value is -->"+retVal);
return "Successfully exited with return val is -->"+retVal;
}
当您从 Spring 获取HRService对象并调用其showHolidayList()方法时,您将得到以下更新的返回值:
This is holiday list method...
****** Method 'showHolidayList' of class com.packet.spring.aop.service.HRService is executed successfully...
The return value is -->holidayList
实现 AfterThrowing 建议
AfterThrowing 建议 会在目标对象的匹配方法抛出异常时执行。当您想在方法执行过程中发生的异常上采取行动时,这也很有用。让我们按照以下方式创建 AfterThrowing 建议:
<aop:aspect id="sendMsg" ref="sendMsgAspect">
<aop:after-returning pointcut-ref="hrServiceMethods"
returning="retVal" method="sendMessageOnSuccessExit"/>
<aop:after-throwing pointcut-ref="hrServiceMethods"
method="sendMessageOnErrorExit"/>
</aop:aspect>
sendMessageOnErrorExit 建议方法将在 sendMessage 切面中定义如下:
//Advice method on existing of target method with some error / exception
public void sendMessageOnErrorExit(JoinPoint joinPoint) {
System.out.println(" ****** Method '"+joinPoint.getSignature().getName()+"'
of "+joinPoint.getTarget().getClass()+" has some error ...");
}
为了确保应用此建议,target类中的方法必须存在异常。因此,让我们在target类(HRService)中添加一个抛出异常的方法,如下所示:
public void showMyLeave() throws Exception {
System.out.println("Showing employee leaves...");
throw new Exception();
}
当您从 Spring 获取HRService对象并调用showMyLeave方法时,您将得到以下输出:
Showing employee leaves...
****** Method 'showMyLeave' of class com.packet.spring.aop.service.HRService has some error ...
java.lang.Exception
at com.packet.spring.aop.service.HRService.showMyLeave(HRService.java:12)
at com.packet.spring.aop.service.HRService$$FastClassBySpringCGLIB$$a3eb49fe.invoke(<generated>)
...
基于@AspectJ 注解的 Spring AOP
Spring 允许通过 @AspectJ 注解支持另一种支持 AOP 的方式。这是使用带有 AOP 特定注解的常规 Java 类定义切面的 XML 配置的替代方案。Spring 在 AspectJ 5 版本中引入了@AspectJ 风格。尽管使用@AspectJ,Spring 简化了与 AspectJ 5 相同的注解,但其底层框架是纯 Spring AOP。由于这种安排,不需要依赖 AspectJ 编译器或编织器。
要使用@AspectJ 注解进行 Spring AOP,您需要通过配置在 Spring 中启用其支持,并开启自动代理。自动代理是一种在 Spring bean 对象上创建代理的机制,该对象上织入了至少一个切面。这允许 Spring AOP 拦截方法并在匹配的切入点应用建议。
正如我们在 第三章 “使用 Spring 的依赖注入” 中所看到的,Spring 支持在应用程序上下文(XML)文件或 Java 配置中配置注解。同样,@AspectJ 配置可以使用这两种选项中的任何一种。使用基于 Java 的配置,您可以使用以下代码启用 Spring AOP 注解:
@Configuration
@EnableAspectJAutoProxy
public class SpringConfig {
}
或者,您可以选择应用程序上下文(XML)文件来启用@Aspect注解。这可以通过<aop:aspectj-autoproxy/>元素实现。
Spring AOP 创建一个代理对象以嵌入客户代码;然而,当你将任何类定义为带有 @Aspect 注解(或 XML 配置)的 aspect 时,Spring 不会创建该类的代理。简而言之,aspect 类不能成为另一个 aspect 通知的目标。Spring 将省略所有此类 aspect 类的自动代理。
声明 aspect
使用 @AspectJ 注解风格声明 Aspect 的概念与基于 XML 架构的 AOP 声明有些类似。只是为了回顾一下,Java 类可以在 Spring AOP 框架中作为 Aspect。在基于注解的 Spring AOP 中,你可以使用 @Aspect 注解将任何 bean 声明为 Aspect,如下所示:
package com.packt.spring.aop.aspects;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class SessionCheck {
}
SessionCheck 类在应用程序上下文(XML)文件中定义为常规 Spring bean,如下所示:
<aop:aspectj-autoproxy/>
<bean id="sessionCheckAspect" class="com.packt.spring.aop.aspects.SessionCheck">
</bean>
Aspect 类可以像任何其他 Java 类一样拥有方法和字段。Spring 并不对仅通过在应用程序上下文(XML)文件中定义的 bean 来定义 aspect 有限制。如果你已经通过 Java 包扫描(使用 <context:component-scan> 元素)使用了 bean 自动检测,Spring 会智能地检测带有 @Aspect 注解的 Aspect 类。Aspect 类可以包含切点和通知声明。
@Aspect 注解本身不足以让 Spring 自动检测类。你仍然需要使用 @Component 或任何其他 stereotypes 注解。@Aspect 将类(由 @Component 或等效注解自动检测)视为 Spring AOP 的 aspect。
声明切点
切点是正则表达式或模式,用于过滤我们想要应用通知的连接点。由于 Spring AOP 只支持方法级别的连接点,你可以将切点视为在 Spring beans 上匹配方法执行。在 @AspectJ 注解风格中,切点是通过 Aspect 类(使用 @Aspect 注解声明)的方法声明的。这些方法被称为 切点签名。@Pointcut 注解用于定义此类方法,如下所示:
@Aspect
public class SessionCheck {
@Pointcut("execution( * com.packt.spring.aop.service.*.*(..))") // Expression
private void validateSession() {// Point-cut signature
}
}
在此代码中,validateSession 方法代表一个切点签名,而 @Pointcut 注解用于描述一个切点表达式。前面的切点应用于 com.packt.spring.aop.service 包下所有具有参数和返回值的公共方法。代表切点签名的必须具有 void 作为返回类型。前面的基于注解的 AOP 与基于 XML 的 AOP 配置等效,如下所示:
<aop:config>
<!-- point cut declaration -->
<aop:pointcut id="checkValidUser"
expression="execution(* com.packet.spring.aop.service.*.*(..))" />
<!-- aspect configuration -->
<aop:aspect id="mySessionCheck" ref="checkSessionAspect">
//Advice declaration goes here.
</aop:aspect>
<!-- spring bean represents an aspect -->
<bean id="checkSessionAspect"
class="com.packet.spring.aop.aspects.SessionCheck">
</bean>
</aop:config>
就像基于 XML 的 AOP 一样,你可以使用各种切点类型(切点设计符),如 within、this、target、args、@annotation、@target、@within 和 @args,在注解风格的 AOP 中使用。除此之外,Spring 还支持一个额外的切点类型(PCD)称为 bean,它匹配特定 bean 或一组 bean 上的方法执行。它可以声明如下:
@Pointcut("bean(batchOperation)")
private void captureBatchProcess() {
}
此切点应用于在应用程序上下文(XML)文件中定义的 ID 或名称为batchOperation的 bean。如果使用通配符(只允许*),则此切点可以应用于多个 bean。
就像基于 XML 的配置一样,基于注解的 AOP 也支持将切点与and、or和negated操作相结合。在@AspectJ风格的 AOP 中,你可以通过其名称(签名)来引用切点,在与其他切点结合时,或者通过 XML 模式中的 Aspect 声明来引用它们。多个切点可以按如下方式组合:
@Pointcut("execution( * com.packt.spring.aop.report.*.*(..))")
private void checkSession() {
}
@Pointcut("args(String)")
private void printUserName() {
}
@Pointcut("checkSession() && printUserName()")
private void userSession() {
}
第一个切点checkSession将应用于com.packt.spring.aop.report包下任何类的所有公共方法。第二个切点printUserName将应用于具有单个String类型参数的任何公共方法,而第三个切点userSession适用于com.packt.spring.aop.report包下任何类的所有具有单个String类型参数的公共方法。我们在第三个切点定义中使用了第一个和第二个切点的名称(切点签名)来组合它们。
创建具有简单表达式的较小切点是一种常见的做法,并通过将它们与and、or和negated操作相结合来构建复杂切点。通过引用其名称,切点的定义非常简单,但在与其他切点结合时却非常强大。
由于切点是按method名称引用的,Java 方法的可见性应用于切点。例如,private切点用于同一类型,protected切点用于同一包,而public切点可以在任何地方应用(即可以在不同层次结构中的其他 Aspect 类中引用)。这在构建具有多个模块的企业应用程序时提供了极大的灵活性,当你想在各种 Aspect 之间共享一组操作时。你可以在公共 Aspect 中创建公共切点,这些切点可以与其他模块中的 Aspect 共享。
声明通知
使用@AspectJ风格注解声明通知与基于 XML 的 AOP 配置类似。XML 配置将被 Aspect 类中的注解声明所取代。只是为了回顾一下,通知是在配置了它的切点上执行的操作。通知可以在 Aspect 类中如下声明:
@Before("execution(* com.packt.spring.aop.report.*.*(..))")
public void displayUserName() {
System.out.println(" Displaying the user name of logged in user --");
}
使用@Before注解通过displayUserName方法声明前置通知。在@Before注解内定义的点切点表达式称为内联的,因为它是在同一位置声明的。你还可以在@Before注解中放置切点的引用(使用@Pointcut注解单独声明)如下所示:
@Pointcut("execution( * com.packt.spring.aop.report.*.*(..))")
private void checkSession() {
}
@Before("checkSession()")
public void displayUserName() {
System.out.println(" Displaying the user name of logged in user --");
}
这就是在@Before通知中引用具有method签名checkSession()的切点。前面的配置等同于以下基于 XML 的配置:
<aop:config>
<!-- point cut declaration -->
<aop:pointcut id="checkSessionPointcut"
expression="execution( * com.packt.spring.aop.report.*.*(..))" />
<!-- aspect and advice configuration -->
<aop:aspect id="mySessionCheck" ref="checkSessionAspect">
<aop:before pointcut-ref="checkSessionPointcut"
method="displayUserName"/>
</aop:aspect>
<!-- spring bean represents an aspect -->
<bean id="checkSessionAspect"
class="com.packet.spring.aop.aspects.SessionCheck">
</bean>
</aop:config>
正如我们所见,点切签名(point-cut signature)的访问修饰符将决定其可见性。你可以将任何公共点切(public point-cut)引用到不同Aspect类的建议(advice)中,如下所示:
//New Aspect class.
@Aspect
public class PermissionCheck {
@Pointcut("within(com.packt.spring.aop.report.*)")
public void checkReportPermission() {
}
}
//Define After advice within SessionCheck aspect class.
@After("com.packt.spring.aop.aspects.PermissionCheck.checkReportPermission()")
public void checkResourcePermission() {
System.out.println("This is resource permission checker ..");
}
这就是如何引用在某个其他方面类中定义的公共点切的方法。你需要使用完全限定的类名,并在method名之间加上点号。我们已经看到了前置和后置建议类型(before and after advice types)的例子。你可以定义其他建议类型,例如环绕建议(around advice)、返回后建议(after returning advice)和抛出后建议(after throwing advice),这些与我们在先前主题中看到的基于模式的 AOP 类似。
声明一个顾问
Spring 提供了一种机制来定义建议和方面作为一个单一单元。它被称为顾问。它仅在 Spring AOP 中可用,而不在原生 AspectJ 中。在这个模型中,你需要定义一个实现建议接口之一的类作为建议。顾问可以使用以下方式在应用程序上下文(XML)文件中的<aop:advisor>元素中定义:
<aop:config>
<aop:pointcut id="loggingPointcut" expression="execution(*
com.packt.spring.aop.dao.*.*(..))" />
<aop:advisor advice-ref="loggingAdvice"
pointcut-ref="loggingPointcut" id="loggingInterceptorAdvisor" />
</aop:config>
<bean id="loggingAdvice" class="com.packt.spring.aop.advisor.LoggingAdvisor" />
你需要在<aop:config>元素内定义一个顾问(advisor),使用<aop:advisor>元素。你可以使用point-cut-ref属性定义一个点切顾问。在先前的例子中,我们定义了一个内联点切。如果你遵循基于注解的 AOP,你可以按照以下方式引用在方面类(aspect class)中定义的任何公共点切:
<aop:advisor advice-ref="loggingAdvice"
pointcut-ref= "com.packt.spring.aop.aspects.PermissionCheck.checkReportPermission()" id="loggingInterceptorAdvisor" />
在这个例子中,我们引用的是在PermissionCheck方面类中定义的具有其签名(checkReportPermission())的点切。
我们还定义了一个具有LoggingAdvisor类的 bean,这是一个顾问类,并在<aop:advisor>元素中使用advice-ref属性进行引用。LogginAdvisor类的定义如下:
public class LoggingAdvisor implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] args, Object target) throws
Throwable {
System.out.println("****************** Starting "+method.getName()+" method
*****************");
}
}
这个顾问类实现了MethodBeforeAdvise接口的before方法。它等同于实现前置建议。Spring AOP 提供了其他一系列的建议接口,如MethodInterceptor、ThrowsAdvice和AfterReturningAdvice,分别对应实现环绕建议、抛出后建议和返回后建议。
选择 AOP 框架和配置风格
一旦你确定将使用方面编程模型来实现或实现你的需求,这个问题就会出现:你应该选择 Spring AOP 还是完整的 AspectJ 框架?
在选择框架之后,接下来会面临选择配置风格的问题。例如,在 AspectJ 框架的情况下,你会选择 AspectJ 代码风格还是@AspectJ注解风格?同样,在 Spring AOP 的情况下,你会选择 Spring XML 文件还是基于@AspectJ的注解风格来定义各种工件,如方面(aspects)、点切(point-cuts)、建议(advice)和顾问(advisors)?
根据特定的风格选择合适的框架来定义配置取决于许多因素,例如项目需求、开发工具的可用性、团队的专业知识、现有系统代码与 AOP 框架的兼容性、你希望多快实现 AOP,以及性能开销。
Spring AOP 与 AspectJ 语言
在本章中,我们主要探讨了 Spring AOP。AspectJ 是实现 AOP 的另一种解决方案;然而,Spring AOP 比 AspectJ 更简单,因为它不需要你在开发和构建过程中引入 AspectJ 编译器或编织器。
Spring AOP 的引入是为了在整个 IoC 容器中提供一个简单的 AOP 实现,以解决通用问题,而 AspectJ 是一个功能齐全的 AOP 框架。AspectJ 本质上很强大,但比 Spring AOP 更复杂。AspectJ 支持编译时、编译后和运行时编织,而 Spring AOP 只通过代理支持运行时编织。
如果你的业务需要在 Spring bean 项目的操作集上提供建议,Spring AOP 是你的正确选择。如果你遇到需要拦截并实现非由 Spring 容器管理的对象上的建议的需求,你应该选择 AspectJ 编程框架。
Spring AOP 只支持方法级别的连接点。如果你需要在除了方法之外的其他连接点(如字段、setter 或 getter 方法)上提供任何建议,你应该考虑 AspectJ。在选择这些框架时,你应该考虑的另一个点是性能。Spring AOP 使用代理,因此应该几乎没有运行时开销。与 Spring AOP 相比,使用 AspectJ 时开销更小。
Spring AOP 的 XML 与 @AspectJ-style 注解
当有多个选项可供选择时,总是一个困境,这里也是如此。在使用 Spring AOP 时,你应该选择哪种选项:基于 XML 还是基于注解?每种风格都有其优点和局限性。在选择适合你需求的正确选项之前,你应该考虑它们两个。
XML 风格非常知名,自从 Spring 框架的演变以来就被广泛使用。几乎所有的 Spring 开发者都熟悉它。选择 XML 风格意味着所有的配置都在一个中心位置。这将有助于以更清晰的方式识别系统中定义了多少工件(方面、切入点、和一系列建议)。这将有助于独立地更改配置(例如,更改切入点的表达式)。
另一方面,使用 XML 风格时,信息会被分散在不同的地方。配置是在 XML 中完成的,而实际的实现是在相应的 bean 类中定义的。当使用 @AspectJ-style 注解时,将只有一个模块,即 Aspect,它声明所有工件,如切入点、建议等,都被很好地封装。
基于 XML 的 AOP 还有其他限制,例如,声明为方面的 Bean 将仅是单例。此外,在与其他切入点结合时,您不能通过其签名(名称)引用切入点。例如,以下是一个使用注解的切入点声明:
@Pointcut("execution( * com.packt.spring.aop.report.*.*(..))")
private void checkSession() {
}
@Pointcut("args(String)")
private void printUserName() {
}
@Pointcut("checkSession() && printUserName()")
private void userSession() {
}
checkSession和printUserName的切入点签名(名称)被用来将它们组合并形成一个新的表达式,即userSession。基于 XML 的配置的缺点是您不能像这样组合切入点表达式。
在这些事实的基础上,Spring AOP 允许您将基于 XML 的配置与@AspectJ 风格的注解声明混合使用。例如,您可以使用注解定义切入点和一个方面,并在基于 XML 的配置中声明建议(拦截器)集合。它们都可以共存而不会出现任何问题。
摘要
在本章中,我们学习了实现关注点分离的重要方法之一,即 AOP。从概念上讲,我们正在消除业务代码中横切关注点的依赖,并以即插即用的方式以及更受控的方式通过 AOP 应用它们。它解决了我们从未能够用传统 AOP 模型解决的问题。
通过一个示例,我们理解了 AOP 的需求,即当通用功能发生变化时,我们需要不断更改业务代码。我们还看到了在 AOP 中使用的各种术语,这对于理解底层概念至关重要。
在学习 AOP 理论不久后,我们开始了 Spring AOP 的旅程,以理解其实际概念。首先,我们学习了如何在 XML 文件中定义 AOP 配置,随后声明了各种工件,如方面、切入点和建议。通过各种示例和代码示例展示了切入点表达式和建议类型。
接下来,我们学习了如何使用@AspectJ注解风格定义 Spring AOP 配置,随后通过注解声明方面、建议和切入点。我们还学习了如何通过实现各种接口来定义顾问。最后,我们看到了 Spring AOP 和 AspectJ 框架的优缺点,以及如何选择 Spring AOP 的配置风格:XML 或注解。
我们将继续学习各种设计模式,这些模式可以帮助您实现 IoC,以及在使用依赖注入时最佳实践、模式和反模式。
第七章:IoC 模式和最佳实践
现在你已经到达了这一章节,你应该知道什么是依赖注入(DI),为什么它如此重要,它在 Java 的最近版本中是如何体现的,以及如何使用流行的框架,如 Spring 和 Google Guice,以及各种作用域来实现它。
据说,直到用最佳的方法和实践应用,知道某事是不够的。只有当知识以正确的方式实施时,知识才是力量。不恰当的方法可能会造成大混乱。
软件行业正朝着模块化发展。DI 和控制反转(IoC)容器的概念正是由于这一点而创建的,这也是为什么它们今天如此受欢迎。尽管如此,许多开发者不知道如何充分利用 DI。
在这一章中,我们将通过学习正确的模式和最佳实践来探索 DI 的真实优势,将这些我们在前几章中获得的 DI 专业知识应用到实践中。这一章的目的不是做任何新的事情;相反,我们将学习如何以正确的方式做事。
在这一章中,我们将涵盖以下主题:
-
实现 IoC 的各种模式
-
配置样式
-
使用 setter 方法与构造函数进行注入
-
循环依赖
-
最佳实践和反模式
实现 IoC 的各种模式
让我们回顾一下依赖倒置原则(DIP)的内容:高级模块不应该依赖于低级模块;两者都应该依赖于抽象。这是使任何应用程序模块化和可调整的基本要求。
在设计任何系统时,我们应该确保高级类不直接实例化低级类;相反,它们应该依赖于抽象(接口或抽象类)而不是直接依赖于其他具体类。DIP 没有指定这是如何发生的,因此需要一种技术来分离低级模块和高级模块。IoC 提供了这种技术。
实现 IoC 有多种模式,包括将对象创建过程从你的类反转到其他类,以及减少模块或类之间的耦合。让我们讨论这些模式,更多地关注它们如何解耦模块并实现关注点的分离:
-
工厂方法模式
-
服务定位器模式
-
模板方法模式
-
策略模式
所有这些模式封装了特定的责任,这使得系统模块化。
工厂方法模式
工厂方法模式关注定义一个用于创建依赖对象的接口(或抽象类)方法。这个方法被称为工厂方法。持有工厂方法的类(或接口)将被视为抽象创建者。实际的对象创建过程不会在工厂方法中直接发生。
具体创建者(实现工厂方法的类)将决定实例化哪个依赖类。简而言之,依赖对象是在运行时决定的。这个过程已在以下图中描述:

工厂模式的实现是一个四步过程:
-
声明产品(抽象产品类型)。
-
创建具体产品。
-
定义工厂方法 – 创建者。
-
创建具体创建者(具体子类)。
让我们通过一个例子来理解这些步骤。假设你正在为一家消息服务提供商开发一个应用程序。最初,公司为蜂窝设备提供短信服务。因此,你应用程序代码的第一个版本仅处理短信消息分发,假设大部分代码是在SMS类中编写的。
逐渐地,服务变得流行,你希望添加其他大量消息服务,如电子邮件、WhatsApp 和其他社交媒体消息服务。这需要代码更改,因为你已经将所有代码添加到了SMS类中。在将来,每当引入新的消息服务到系统中时,都需要这种代码更改。
工厂方法模式建议,这个问题的解决方案将是通过将对象创建过程从客户端代码(使用 new 运算符)倒置到特定方法:工厂方法。工厂方法定义了一个公共接口,它返回一个抽象产品类型。具体产品的创建是在实现工厂方法的子类中完成的。在前面的图中,从工厂方法返回的对象被称为Product。首先,让我们为前面的例子定义一个抽象产品类型及其具体实现。
定义产品(抽象类型)及其具体实现
在我们的案例中,**MessageApp**接口代表一个抽象产品类型。每个消息应用程序的实现将位于它们各自的 concrete 类中,这些类是具体产品类型,例如**SMSMessage**、**EmailMessage**和**WhatsAppMessage**。这种关系用以下图表示:

产品(抽象类型)和所有具体产品类应如下所示:
// Product (abstract type)
public interface MessageApp {
void sendMessage(String message);
}
//Concrete Product
public class EmailMessage implements MessageApp {
@Override
public void sendMessage(String message) {
//Mail specific implementation
System.out.println("Sending eMail message ...."+message);
}
}
//Concrete Product
public class SMSMessage implements MessageApp {
@Override
public void sendMessage(String message) {
//SMS specific implementation.
System.out.println("sending SMS message ..."+message);
}
}
//Concrete Product
public class WhatsAppMessage implements MessageApp {
@Override
public void sendMessage(String message) {
//Whatsapp specific implementation
System.out.println("Sending Whatsapp message ..."+message);
}
}
定义工厂方法(创建者接口)及其具体实现
下一步是创建一个类并定义一个返回抽象产品类型(在我们的案例中是MessageApp)的工厂方法。这个类被认为是抽象创建者。工厂方法可以是接口或抽象方法的形式。所有具体创建者都必须实现这个工厂方法。以下图描述了这些组件之间的完整关系:

在这里,**MessagingService**是创建者,而**EmailServices**、**SMSServices**和**WhatsAppServices**是具体创建者。每个具体创建者生产相应的具体产品类型。
工厂方法和其具体的实现类应如下所示:
//Abstract creator
public abstract class MessagingService {
//This is Factory method.
public abstract MessageApp createMessageApp();
}
//Concrete creator
public class EmailServices extends MessagingService{
@Override
public MessageApp createMessageApp() {
return new EmailMessage();
}
}
//Concrete creator
public class SMSServices extends MessagingService {
@Override
public MessageApp createMessageApp() {
return new SMSMessage();
}
}
//Concrete creator
public class WhatsAppServices extends MessagingService {
@Override
public MessageApp createMessageApp() {
return new WhatsAppMessage();
}
}
在前面的例子中,我们使用了抽象类,但你也可以使用接口作为工厂方法(抽象创建者)。如果你计划提供任何公共方法,你可以选择抽象类,否则接口是一个合适的选择。
最后,提供特定实现的工厂类如下所示:
public class MessagingFactory {
public MessageApp getMessageApp(MessageType messageType) {
MessageApp messageApp = null;
// 1.Based on messageType value, create concrete implementation.
// 2.Call factory method on each of them to get abstract product type - MessageApp in our case
// 3.call common method on abstract product type to execute desire operation.
switch(messageType) {
case SMSType:
messageApp = new SMSServices().createMessageApp();
break;
case EmailType:
messageApp = new EmailServices().createMessageApp();
break;
case WhatsAppType:
messageApp = new WhatsAppServices().createMessageApp();
break;
default: System.out.println(" Unknown message type .. Please provide valid message type ");
}
return messageApp;
}
}
这个类根据特定的enum类型返回具体的实现。以下代码片段展示了客户端代码如何使用工厂方法:
public class Client {
public static void main(String[] args) {
MessagingFactory messagingFactory = new MessagingFactory();
MessageApp smsApp = messagingFactory.getMessageApp(MessageType.SMSType);
MessageApp emailApp = messagingFactory.getMessageApp(MessageType.EmailType);
MessageApp whatsAppApp = messagingFactory.getMessageApp(MessageType.WhatsAppType);
smsApp.sendMessage(" Hello ");
emailApp.sendMessage(" this is test ");
whatsAppApp.sendMessage(" Good Morning");
}
}
这可以用以下图表来描述:

使用工厂方法模式,你可以将产品创建过程从客户端类中抽象出来。这样,工厂方法模式消除了具体产品类对整个系统的依赖。此外,工厂方法将实际的对象创建过程委托给具体的创建者。只要客户端代码知道类型,工厂类就会提供该类型的依赖对象。这样,工厂方法允许客户端代码依赖于抽象而不是具体实现。这就是通过工厂方法模式实现 IoC 的方式。
服务定位器模式
服务定位器模式涉及通过引入一个中介来从客户端对象中移除依赖。客户端对象将通过中介来获取所需的依赖。我们将这个中介称为服务定位器,或者简称定位器。
服务定位器涉及通过抽象层获取服务的过程。理想情况下,定位器应持有所有服务(依赖),并通过单个接口提供它们。它是一种中央存储库,通常通过字符串或接口类型来查找服务。
服务定位器描述了如何注册和定位服务,而不是告诉我们如何实例化它。它允许应用程序为给定的契约注册具体的实现。你可以通过编程或通过配置添加服务。服务定位器的实现如下所示:

这是一种非常简单的服务定位器模式。ModuleA依赖于由Service Locator提供的ServiceB和ServiceC。然而,你可以使Service Locator更加抽象,以便它可以处理任何类型的服务。让我们了解如何做到这一点。
总是暴露任何服务的一个接口是一个好主意。以下代码片段将展示两个这样的服务接口及其实现:
public interface CompressionAlgorithm {
void doCompress();
}
public interface EncryptionAlgorithm {
void doEncryption();
}
public class RARCompression implements CompressionAlgorithm {
@Override
public void doCompress() {
System.out.println(" Compressing in RAR format ... ");
}
}
public class ZIPCompression implements CompressionAlgorithm {
@Override
public void doCompress() {
System.out.println(" Compressing in ZIP format ... ");
}
}
我们希望从服务定位器获取压缩和加密服务。我们将编写ServiceLocator类,它是一个单例,允许我们注册这些服务。一旦完成,客户端就可以通过服务接口类型来获取服务。ServiceLocator类将如下所示:
public class ServiceLocator {
// Map which holds all services.
private Map<Class<?>,Map<String,Object>> serviceRegistry = new HashMap<Class<?>,Map<String,Object>>();
private static ServiceLocator serviceLocator;
// private constructor to make this class singleton
private ServiceLocator() {
}
//Static method to get only existing instance. If no instance is there, create the new one.
public static ServiceLocator getInstance() {
if(serviceLocator == null) {
serviceLocator = new ServiceLocator();
}
return serviceLocator;
}
public <T> void registerService(Class<T> interfaceType, String key, Object serviceObject) {
Map<String,Object> serviceOfSameTypeMap = serviceRegistry.get(interfaceType);
if(serviceOfSameTypeMap !=null) {
serviceRegistry.get(interfaceType).put(key, serviceObject);
}else {
serviceOfSameTypeMap = new HashMap<String,Object>();
serviceOfSameTypeMap.put(key, serviceObject);
serviceRegistry.put(interfaceType, serviceOfSameTypeMap);
}
}
public <T> T getSerivce(Class<T> interfaceType, String key) {
Map<String,Object> serviceOfSameTypeMap = serviceRegistry.get(interfaceType);
if(serviceOfSameTypeMap != null) {
T service = (T)serviceOfSameTypeMap.get(key);
if(service !=null) {
return service;
}else {
System.out.println(" Service with key "+ key +" does not exist");
return null;
}
}else {
System.out.println(" Service of type "+ interfaceType.toString() + " does not exist");
return null;
}
}
}
对于注册服务来说,使用接口不是强制性的,但这是一个好的实践。在未来,如果引入了相同接口的新服务或引入了全新的接口的一组新服务,它们可以很容易地适应,而不会影响客户端代码。
此外,有了界面,客户端代码更加通用,你只需更改键值就可以更改实现,使系统更加灵活和松散耦合。最后,客户端代码中使用了服务定位器,正如以下代码片段所示:
public class ServiceLocatorDemo {
public static void main(String[] args) {
ServiceLocator locator = ServiceLocator.getInstance();
initializeAndRegisterServices(locator);
CompressionAlgorithm rarCompression = locator.getSerivce(CompressionAlgorithm.class, "rar");
rarCompression.doCompress();
CompressionAlgorithm zipCompression = locator.getSerivce(CompressionAlgorithm.class, "zip");
zipCompression.doCompress();
EncryptionAlgorithm rsaEncryption = locator.getSerivce(EncryptionAlgorithm.class, "rsa");
rsaEncryption.doEncryption();
EncryptionAlgorithm aesEncryption = locator.getSerivce(EncryptionAlgorithm.class, "aes");
aesEncryption.doEncryption();
}
private static void initializeAndRegisterServices( ServiceLocator locator ) {
CompressionAlgorithm rarCompression = new RARCompression();
CompressionAlgorithm zipCompression = new ZIPCompression();
EncryptionAlgorithm rsaEncryption = new RSAEncrption();
EncryptionAlgorithm aesEncryption = new AESEncrption();
locator.registerService(CompressionAlgorithm.class, "rar", rarCompression);
locator.registerService(CompressionAlgorithm.class, "zip", zipCompression);
locator.registerService(EncryptionAlgorithm.class, "rsa", rsaEncryption);
locator.registerService(EncryptionAlgorithm.class, "aes", aesEncryption);
}
}
服务定位器解耦了类与其依赖关系。这种安排的直接好处是,依赖关系可以替换,而几乎不需要或(理想情况下)不需要代码更改。这样,服务定位器模式就颠倒了客户端代码到定位器组件的控制流。这就是 IoC 是如何实现的。
在服务定位器模式中,你需要确保在对象开始使用服务之前,所有服务都 readily available。
初看之下,工厂方法模式和服务定位器模式似乎工作方式相似。然而,它们之间有一些区别,如下所示:
-
构建成本:如果工厂方法中的类实例化过程非常昂贵(从资源消耗的角度来看),那么在工厂方法中创建新对象将导致性能问题。简而言之,工厂方法中的构建成本可能会影响整体系统性能。在服务定位器模式中,所有依赖对象都是在应用程序启动时(理想情况下)创建的。客户端可以从预先实例化的注册表中获取依赖服务。
-
现有对象与新建对象:有时,你需要每次都使用相同的对象。在工厂方法模式中,我们每次都返回一个新的实例,而服务定位器模式返回依赖服务的现有实例给调用者。
-
所有权:由于工厂类向调用者返回一个全新的实例,因此所有权属于调用者类,而服务定位器定位并返回服务的现有实例,因此返回对象的拥有权属于服务定位器。
模板方法模式
模板方法模式涉及定义算法的通用结构,然后允许子类在不改变完整结构的情况下更改或重新定义算法的一部分。换句话说,模板方法模式在一系列操作中定义了一个函数,允许子类在不改变完整结构的情况下重新定义几个步骤。
在此模式中,基类声明了具有占位符的通用过程,并允许子类提供这些占位符的具体实现,同时保持整体结构不变。
让我们通过一个例子来理解模板方法模式。假设您正在编写一个程序来获取行数据,验证它,格式化它,并将其插入到数据库中。最初,行数据以 CSV 文件的形式提供,因此您创建了一个名为ProcessCSVData的类。此类包含以下步骤的逻辑:
-
读取 CSV 文件
-
验证数据
-
格式化数据
-
将数据插入到数据库中
一年后,引入了更多原始数据格式,如 HTML、XML、文本和 Excel。对于这些格式中的每一个,如果您创建一个单独的类,您将最终拥有大量的相似代码。显然,这些类在文件格式上相当不同,而它们在数据验证、格式化和插入到数据库中的其他逻辑上是相同的。
考虑使用这些类的客户端代码。您需要提供大量的if...else条件来选择特定的实现。这不是一个好的设计。为了实现可重用性,消除代码重复并使算法结构完整是至关重要的。如果所有这些类都共享一个公共基类,则可以通过使用多态来解决这个问题。
要实现模板方法模式,您需要确定算法中哪些步骤是通用的,哪些是变体或可定制的。通用步骤应在基类中实现,而变体步骤应放置在基类中,带有默认实现或根本无实现。变体步骤将被视为占位符或扩展点,必须由具体派生类提供。
在我们的例子中,从文件中读取数据是唯一的可变步骤,因此我们将它保留在基类中,并在方法中使用默认(或无)实现。这被认为是模板方法。所有具体子类都必须提供此模板方法的实现(从相应格式中读取文件)。其他步骤,如验证、格式化和插入到数据库中,是通用或不变的,因此保持它们在基类中不变。此实现由以下图表描述:

以下代码片段表示此实现:
public abstract class ProcessData {
//Template method
public abstract void readFile();
public void validate() {
System.out.println(" Validating data ..");
}
public void format() {
System.out.println(" Formatting data ..");
}
public void insertInDB() {
System.out.println(" Inserting data into Database ..");
}
}
实现的子类应如下所示:
public class ProcessExcelData extends ProcessData{
@Override
public void readFile() {
System.out.println(" Reading Excel file");
}
}
public class ProcessHTMLData extends ProcessData{
@Override
public void readFile() {
System.out.println(" Reading HTML file");
}
}
public class ProcessTEXTData extends ProcessData{
@Override
public void readFile() {
System.out.println(" Reading Text file");
}
}
public class ProcessXMLData extends ProcessData{
@Override
public void readFile() {
System.out.println(" Reading Excel file");
}
}
最后,使用模板方法的客户端代码应如下所示:
public class TemplateDemo {
public static void main(String args[]) {
ProcessData processData = new ProcessExcelData();
processData.readFile();
processData.validate();
processData.format();
processData.insertInDB();
processData = new ProcessHTMLData();
processData.readFile();
processData.validate();
processData.format();
processData.insertInDB();
}
}
在客户端代码中,我们只使用了两个子类。同样,您也可以使用剩余的两个子类。您将得到以下输出:
Reading Excel file
Validating data ..
Formatting data ..
Inserting data into Database ..
Reading HTML file
Validating data ..
Formatting data ..
Inserting data into Database ..
模板方法模式允许框架定义程序的不变部分,并指定所有可能的定制选项的钩子或占位符。这样,框架成为产品的中心点,而定制化被视为核心功能之上的附加功能或附加组件。
为每个模板方法编写的定制化代码将从通用框架或组件中获取通用功能。换句话说,每个客户的定制化都从通用框架接收控制流。这种倒置的控制机制被亲切地命名为好莱坞原则——“不要调用我们,我们会调用你”。这就是通过模板方法模式实现 IoC 的方式。
策略模式
策略模式定义了一组算法,封装每个算法,并在运行时使它们可互换。这种模式让实现独立于使用它的客户端。简而言之,你可以通过在运行时更改算法来更改类的输出。策略模式侧重于创建一个具有不同实现且遵循相同行为契约的接口。
让我们用一个例子来理解这个模式。假设你正在开发一个将文档上传到云中的应用程序。最初,你被提供了一个 Google Drive 上传。你可能编写了 GoogleDriveCloud 类并将所有逻辑放在那里。
在稍后的某个阶段,你决定支持上传到更多云平台,如 Dropbox、OneDrive 和 Amazon S3。在这个时候,你为每个平台编写了单独的类,例如 DropboxCloud、OneDriveCloud 和 AmazoneS3Cloud。
所有这些类都用于将文档上传到相应的云。当你使用它们在代码中时,你可能会根据某些条件编写选择特定实现的代码。
在前述情况下,CloudUpload 类与每个云实现紧密耦合,这不是一个好的设计。当你试图在未来适应更多的云支持时,你可以考虑这些问题。每个新的实现都需要修改 CloudUpload 类。这是对开放-封闭原则的明显违反:它讨论的是对扩展开放但对修改封闭。
这种情况可以通过策略模式来缓解。该模式涉及定义一组相关的算法(各种云平台的实现)并将它们封装在独立于宿主类(CloudUpload)的类中。解决方案用以下图表描述:

前述图表的实现将如下所示:
//Interface
public interface Cloud {
void upload();
}
//Concrete Algorithm
public class GoogleDriveCloud implements Cloud {
@Override
public void upload() {
System.out.println(" Uploading on Google Drive ");
}
}
//Concrete Algorithm
public class DropboxCloud implements Cloud {
@Override
public void upload() {
System.out.println(" Uploading on Dropbox ");
}
}
//Concrete Algorithm
public class OneDriveCloud implements Cloud {
@Override
public void upload() {
System.out.println(" Uploading on OneDrive ");
}
}
//Concrete Algorithm
public class AmazoneS3Cloud implements Cloud {
@Override
public void upload() {
System.out.println(" Uploading on Amazone S3 ");
}
}
我们已经声明了一个名为 Cloud 的接口,它将由每个具体类实现。CloudUpload 类代表一个 Context 类。它持有 Cloud 的引用,该引用通过以下构造函数提供:
public class CloudUpload {
private final Cloud cloud;
public CloudUpload(Cloud cloud) {
this.cloud = cloud;
}
public void upload() {
this.cloud.upload();
}
}
在这个设计中,每个云实现类只具有将文档上传到特定云的逻辑,遵循单一职责原则。CloudUpload类没有直接引用任何具体类,而是引用类型Cloud,它包含实际的实现。在这种情况下,我们遵循以下原则:面向接口编程,而不是面向实现。
在应用策略模式之后,你现在可以通过创建CloudUpload类的对象并在构造函数中传递适当的实现来上传文档,如下面的代码片段所示:
public class StrategyDemo {
public static void main(String[] args) {
CloudUpload googleCloud = new CloudUpload(new GoogleDriveCloud());
googleCloud.upload();
CloudUpload dropBpxCloud = new CloudUpload(new DropboxCloud());
dropBpxCloud.upload();
CloudUpload oneDriveCloud = new CloudUpload(new OneDriveCloud());
oneDriveCloud.upload();
CloudUpload amazoneS3Cloud = new CloudUpload(new AmazoneS3Cloud());
amazoneS3Cloud.upload();
}
}
如果未来需要额外的云实现支持,CloudUpload类不会发生变化。单元测试变得简单直接。CloudUpload类只知道如何处理策略类(Cloud实现),而不是通过条件块来选择特定的实现。
这样,策略模式帮助我们实现可插拔的行为。选择Cloud实现的逻辑现在已从CloudUpload类中移除。这就是如何借助策略模式实现 IoC。
配置样式
几乎所有的 IoC 容器都允许你选择代码或基于文件(XML)的配置来声明依赖项。尽管它们服务于相同的目的,但你可能会对在特定场景下哪种选项最好感到困惑。
例如,基于文件(主要是 XML)的配置适用于需要部署到多个环境的应用程序。另一方面,存在一些特定场景,在这些场景中,基于代码的配置被优先选择。识别这两种配置之间的差异将有助于你选择哪一种更适合你。
基于文件(XML)与基于代码的配置
基于 XML 的配置的好处是,你可以在不重新编译、构建和部署应用程序代码的情况下更改依赖项。这在需要交换相同类型依赖项的情况下听起来很有用。但再次强调,这真的是你想要的吗?换句话说,如果你没有在运行时动态更改依赖项实现的要求,那么基于文件的配置并不那么有用。
然而,基于文件的配置通常更难以阅读和分析,尤其是在它变得庞大而笨拙时。基于 XML 的配置在编译时不会警告你任何错误。这些错误只能在运行时被发现,而且修复它们相当棘手且耗时。另一方面,基于代码的配置支持编译时错误检查。这意味着如果构建成功,你就完成了,并且在运行时不会遇到任何惊喜。
使用 setter 方法与构造函数进行注入
依赖注入有两种简单的方法——基于 setter 或基于构造函数的依赖注入。这两种方法执行相同的操作——注入依赖项,但在对象生命周期的不同时间。一个发生在对象实例化期间,而另一个发生在显式调用 setter 方法时。
当你使用这两种选项实现依赖注入时,一个非常明显的困境就会出现。理解这些差异很重要,因为它反映了面向对象编程环境中的基本问题:我们是使用构造函数参数初始化字段变量,还是通过 setter 方法来初始化?
基于构造函数的依赖注入
使用构造函数传递依赖项在描述创建对象所需内容方面更为清晰。如果允许,你可以编写多个版本的构造函数,每个构造函数接受不同组合的依赖项对象。
除了使用构造函数初始化字段外,你还可以通过不提供 setter 方法来隐藏它们。这种安排的优势在于,你可以确保通过构造函数设置的依赖项将可用于对象的生命周期。这很重要,因为如果你不希望某个依赖项在对象出生时被更改,那么使用构造函数初始化而不提供 setter 将使其不可变。基于构造函数的依赖注入将在加载上下文时决定依赖注入的顺序。
通过构造函数传递依赖项将管理对象创建图的顺序,并最终降低循环依赖的风险。相反,对于基于构造函数的依赖注入,Spring 不允许你使用代码生成库(CGLIB)创建代理。你需要使用基于接口的代理或无参数构造函数。
你应该将传递依赖到构造函数的方法作为你的首选方法。理想情况下,所有活动/强制依赖项都必须通过构造函数传递。
基于 setter 的依赖注入
基于 setter 的依赖注入的基本思想是,一旦对象被创建(主要是使用无参数构造函数),就可以调用 setter 方法来提供依赖项以形成一个对象图,或者只是为了测试目的提供模拟对象。
如果只有几个构造函数参数,基于构造函数的依赖注入是合适的。如果有大量的构造函数参数,看起来会很混乱。即使有多个构造函数版本,帮助也不大。在这种情况下,你应该依赖于基于 setter 的依赖注入。
理想情况下,所有可选或条件依赖都应该通过基于 setter 的依赖注入来提供。这种方法的缺点是,你必须确保在客户端对象开始使用之前调用 setter 方法。使用 setter 方法的另一个风险是,在执行后期修改的依赖项可能会导致意外的或模糊的结果,这有时很难追踪。此外,如果使用 setter 方法配置不当,你可能会遇到循环依赖,这在运行时可能会遇到。
循环依赖
循环或循环依赖是一种情况,其中两个或多个独立模块或组件依赖于彼此以正常工作。这被称为相互递归。循环依赖通常在定义模块或组件之间的依赖关系时在模块化框架中发生。
“循环依赖”这个术语在领域模型中非常常见,其中一组对象相互关联。类之间的循环依赖不一定有害。事实上,在特定情况下,它们是合适的。以一个处理领域对象,如学生和课程的示例,你可能需要一个Student类来获取学生已报名的课程,以及一个Course类来获取在该课程上报名的学生列表。很明显,Student和Course类是相互依赖的,但如果在这种情况下需要循环依赖,那么尝试移除它可能会引入其他问题。
在软件设计环境中,软件组件或模块之间的循环依赖会产生负面影响,因此被视为不良实践。这可能是设计问题。一般来说,管理不善的依赖关系的软件设计比具有清晰分层模块结构的软件设计更难维护。在设计系统时,以模块化方式设计,你需要记住可能会出现的问题,特别是由于循环依赖引起的问题。
循环依赖问题
循环依赖可以在软件程序中创建许多冗余效果。在设计方面,首先是相互依赖的模块之间的紧密耦合,这使得重用单个模块变得更加困难或不可能。一般来说,有几个原因你应该避免对象之间的循环引用。它会导致以下问题:
-
没有依赖层次和可重用性: 通常,我们用代码所在的层来量化代码;例如,高级、低级等。每一层只应设置对下面层的依赖(如果有的话)。通常,当你定义模块之间的依赖关系时,会创建一个依赖图或层次结构,但在循环依赖的情况下,这会被消除。这意味着没有依赖层次。例如,假设你有以下依赖层次:
-
模块 A 依赖于模块 B
-
模块 B 依赖于模块 C
-
假设目前模块 C 没有依赖
-
根据这种安排,我们可以将模块 A 识别为顶层,模块 B 位于中间级别,模块 C 位于层次结构的底层。假设在一段时间后,我们需要让模块 C 依赖于模块 A(无论出于什么原因)。
当这种情况发生时,高低级别之间不再有区分,这意味着不再存在层次结构。所有模块处于同一级别。此外,由于它们存在循环依赖,它们也不再是独立的。这种情况形成了一个单一的巨大虚拟模块,它被划分为相互依赖的部分。您无法独立使用其中任何一个。
-
更改复制:循环依赖会引发一系列的变化。例如,如果某个模块发生任何变化,这可能会影响其他模块,从而对整体软件架构产生不良影响,例如编译错误和逻辑程序错误。由于其本质,循环依赖可能会产生其他不可预测的问题,例如无限递归。
-
可读性和可维护性:具有循环引用的代码自然比没有循环引用的代码更难以理解和阅读。这种代码本质上是脆弱的,容易出错。确保您的代码没有循环依赖将使代码易于操作,并使代码能够轻松适应变化,从而实现易于维护。从单元测试的角度来看,具有循环依赖的代码更难以测试,因为它无法被隔离。
原因和解决方案
如我们所见,循环依赖通常是由于不良的设计/编码实践造成的。在大型的软件应用开发中,程序员可能会偏离上下文并产生循环引用。
为了克服这个问题,您可以借助各种工具来查找不想要的循环依赖。这应该是一个持续的活动,并从开发周期的开始就应用。例如,Eclipse 有一个名为 Java Dependency Viewer 的插件,可以帮助查看类和 Java 包之间的依赖关系。
通过遵循某些模式和原则可以解决循环依赖问题,这些模式和原则将在以下章节中讨论。
单一职责原则
让我们了解如何通过应用单一职责原则来消除循环依赖。假设您正在跟踪系统中的三个模块:
-
薪酬模块
-
员工模块
-
人力资源模块
薪酬模块生成薪酬并通过电子邮件发送。生成薪酬依赖于员工模块。为了获取一些细节,例如评估过程和奖励积分,员工模块依赖于人力资源模块。此时,依赖层次结构将如图所示:

在某个时间点,比如说您需要在人力资源模块中实现电子邮件功能。由于电子邮件功能存在于薪资模块中,您决定将薪资模块的依赖关系赋予人力资源模块。在这个时刻,依赖关系图看起来像以下图表:

这种情况形成了一个循环依赖。为了避免这种情况,您需要遵循单一职责原则。这个原则指出,一个模块或类应该承担功能单一部分的职责。该模块或类应该完全拥有该功能,并且必须完全封装。模块提供的所有服务不得偏离主要功能。
在我们的例子中,薪资模块不仅生成薪资,还发送电子邮件。这是违反单一职责原则的。当一个模块执行多个职责时,可能会出现不良的依赖管理,这可能导致以下情况:
-
代码重复:您可能在多个模块中编写相似和通用的功能。例如,在这种情况下,您可以在人力资源模块中编写电子邮件发送功能以避免循环依赖,但最终会导致代码重复,这会在以后引发维护问题。
-
循环依赖:正如我们在前面的案例中看到的。
您需要编写一个名为“工具模块”的独立模块,并将电子邮件发送功能放在其中。在重构此代码后,人力资源模块和薪资模块现在都依赖于工具模块。这就是如何通过遵循单一职责原则来消除循环依赖。
将依赖的设置从构造函数推迟到设置器
让我们通过提供从构造函数到设置器的依赖关系来了解如何解决循环依赖。存在一个特殊情况,由于循环依赖,甚至无法创建领域模型的对象。例如,假设您正在开发一个为tyre制造商的应用程序;这些轮胎用于汽车。根据汽车的最大速度,您需要设置轮胎的最小轮辋尺寸。为此,您创建了Car和Tyre类,如下面的代码片段所示:
public class Car {
private Tyre tyre;
private int maxSpeed;
public Car(Tyre tyre) {
this.tyre = tyre;
setMaxSpeed(150);
}
public int getMaxSpeed() {
return maxSpeed;
}
public void setMaxSpeed(int maxSpeed) {
this.maxSpeed = maxSpeed;
}
}
public class Tyre {
private Car car;
private int minRimSize;
public Tyre(Car car) {
this.car = car;
if(this.car.getMaxSpeed()>100 && this.car.getMaxSpeed()<200) {
setMinRimSize(15);
}else if(this.car.getMaxSpeed()<100) {
System.out.println("Minimum RIM size is 14");
setMinRimSize(14);
}
}
public int getMinRimSize() {
return minRimSize;
}
public void setMinRimSize(int minRimSize) {
this.minRimSize = minRimSize;
}
}
如您所见,Car和Tyre类相互依赖。依赖关系通过构造函数传递,这就是为什么它是循环依赖的原因。您无法为它们中的任何一个创建对象。为了处理这种情况,您需要在每种情况下将设置依赖关系从构造函数推迟到设置器。我们决定在Car类中进行此更改,如下面的代码片段所示:
public class Car{
private Tyre tyre;
private int maxSpeed;
public Car() {
}
public void setTyre(Tyre tyre) {
this.tyre = tyre;
}
public Tyre getTyre() {
return tyre;
}
public int getMaxSpeed() {
return maxSpeed;
}
public void setMaxSpeed(int maxSpeed) {
this.maxSpeed = maxSpeed;
}
}
将Tyre的依赖关系从构造函数移动到设置器方法。在Tyre类中,您需要将当前类的引用(Tyre)设置到Car对象中,如下面的代码片段所示:
public class Tyre {
private Car car;
private int minRimSize;
public Tyre(Car car) {
this.car = car;
this.car.setTyre(this);
if(this.car.getMaxSpeed()>100 && this.car.getMaxSpeed()<200) {
System.out.println("Minimum RIM size is 15");
setMinRimSize(15);
}else if(this.car.getMaxSpeed()<100) {
System.out.println("Minimum RIM size is 14");
setMinRimSize(14);
}
}
public int getMinRimSize() {
return minRimSize;
}
public void setMinRimSize(int minRimSize) {
this.minRimSize = minRimSize;
}
}
现在一切都已解决。您可以首先创建Car类型的对象,然后创建Tyre类型的对象,以便您可以传递car对象的引用给它。客户端代码如下所示:
public class CircularDependencyWithSetterDemo {
public static void main(String[] args) {
Car car = new Car();
car.setMaxSpeed(120);
Tyre tyre = new Tyre(car);
car.setMaxSpeed(90);
tyre = new Tyre(car);
}
}
类和包的迁移
循环依赖的一个可能原因是 Java 包中某些类的一个依赖链。比如说,com.packt.util与不同的包交叉,到达同一包中的其他类,com.packt.util。这是一个可以通过移动类和重新组织包来解决的问题。您可以使用现代 IDE 执行此类重构活动。
Spring 框架中的循环依赖
让我们探讨在 Spring 框架中循环依赖是如何发生的以及如何处理它。Spring 提供了一个 IoC 容器,该容器加载所有豆类并尝试按特定顺序创建对象,以确保它们正常工作。例如,假设我们有三个豆类,以下是他们依赖关系的层次结构:
-
Employee豆类 -
HRService豆类 -
CommonUtilService豆类
Employee豆类依赖于HRService豆类,而HRService豆类又依赖于CommonUtilService豆类。
在这种情况下,CommonUtilService被视为低级豆类,而Employee豆类被视为高级豆类。Spring 将首先为所有低级豆类创建对象,以便创建CommonUtilService豆类,然后它将创建HRService豆类(并将CommonUtilService豆类的对象注入其中),然后它将创建Employee豆类的对象(并将HRService豆类的对象注入其中)。
现在,您需要使CommonUtilService豆类依赖于Employee。这是一个循环依赖。此外,所有依赖关系都是通过构造函数设置的。
在循环依赖的情况下,高级和低级模块之间的区别消失了。这意味着 Spring 将陷入困境,不知道应该先实例化哪个豆类,因为它们相互依赖。结果,Spring 将引发BeanCurrentlyInCreationException错误。
这只会在构造函数注入的情况下发生。如果依赖关系是通过 setter 方法设置的,即使豆类相互依赖,这个问题也不会发生。这是因为上下文加载时,没有依赖关系存在。
让我们为这个创建代码并看看 Spring 是如何检测循环依赖的。代码如下:
@Component("commonUtilService")
public class CommonUtilService {
private Employee employee;
public CommonUtilService(Employee employee) {
this.employee = employee;
}
}
@Component("employee")
public class Employee {
private HRService hrService;
public Employee(HRService hrService) {
this.hrService=hrService;
}
}
@Component("hrService")
public class HRService {
private CommonUtilService commonUtilService;
public HRService(CommonUtilService commonUtilService) {
this.commonUtilService=commonUtilService;
}
}
Java 配置和客户端代码如下所示:
@Configuration
@ComponentScan(basePackages="com.packt.spring.circulardependency.model.simple")
public class SpringConfig {
}
public class SpringCircularDependencyDemo {
public static void main(String[] args) {
ApplicationContext springContext = new AnnotationConfigApplicationContext(SpringConfig.class);
Employee employee = (Employee) springContext.getBean("employee");
HRService hrService = (HRService) springContext.getBean("hrService");
CommonUtilService commonUtilService = (CommonUtilService) springContext.getBean("commonUtilService");
}
}
运行此代码后,所有豆类都会出现BeanCurrentlyInCreationException错误,如下所示:
Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'employee' defined in file
为了避免这种情况,您需要重新设计前面的结构。在少数情况下,可能由于遗留代码的设计限制,无法更改结构。在这种情况下,Spring 提供了一些解决方案,如下所述。
相比构造函数注入,使用 setter/field 注入
这可能是最简单、最直接的选择。在循环依赖中,如果构造函数注入创建了循环引用,你可以在 setter 方法中延迟 DI。这允许 Spring 无问题地加载 bean 上下文。更新的代码如下所示:
@Component("employee")
public class Employee {
private HRService hrService;
@Autowired
public void setHrService(HRService hrService) {
this.hrService = hrService;
System.out.println(" HRService dependency is set ");
}
}
@Component("hrService")
public class HRService {
private CommonUtilService commonUtilService;
@Autowired
public void setCommonUtilService(CommonUtilService commonUtilService) {
this.commonUtilService = commonUtilService;
System.out.println(" CommonUtilService dependency is set ");
}
}
@Component("commonUtilService")
public class CommonUtilService {
private Employee employee;
@Autowired
public void setEmployee(Employee employee) {
this.employee = employee;
System.out.println(" Employee dependency is set ");
}
}
所有依赖都通过带有 @Autowired 注解的 setter 方法设置。Spring 首先创建所有三个实例,然后使用 setter 方法设置它们。
在 bean 的字段上设置 @Autowired 注解与 setter 注入等效。如果你在类的字段上使用 @Autowired 注解,Spring 不会对循环依赖发出抱怨。
使用 @Lazy 注解
另一种解决方案是使用 @Lazy 注解。这个注解将指示 Spring 仅在需要时加载 bean,而不是在上下文加载时。Spring 将在上下文加载期间创建一个代理 bean 并将其传递给另一个对象。更新的代码如下所示:
@Component("employee")
public class Employee {
private HRService hrService;
public Employee(@Lazy HRService hrService) {
this.hrService=hrService;
}
public void displayEmployeeName() {
System.out.println(" Employee name is Nilang ");
}
}
@Component("hrService")
public class HRService {
private CommonUtilService commonUtilService;
public HRService(@Lazy CommonUtilService commonUtilService) {
this.commonUtilService=commonUtilService;
}
}
@Component("commonUtilService")
public class CommonUtilService {
private Employee employee;
public CommonUtilService(@Lazy Employee employee) {
this.employee = employee;
}
public void showEmployeeNameFromDependency() {
this.employee.displayEmployeeName();
}
}
构造函数依赖通过 @Lazy 注解设置。这段代码将正常运行,实际依赖仅在调用时注入。为了演示这一点,我们在 Employee 实例中创建了一个 displayEmployeeName 方法,我们将使用来自 CommonUtilService 实例的依赖引用来调用它,如下面的代码片段所示:
ApplicationContext springContext = new AnnotationConfigApplicationContext(SpringConfigForLazy.class);
Employee employee = (Employee) springContext.getBean("employee");
HRService hrService = (HRService) springContext.getBean("hrService");
CommonUtilService commonUtilService = (CommonUtilService) springContext.getBean("commonUtilService");
commonUtilService.showEmployeeNameFromDependency();
当调用 showEmployeeNameFromDependency 方法时,它将内部调用 CommonUtilService 中员工引用的 displayEmployeeName 方法。当发生这种情况时,Spring 实际上会注入依赖。你将得到以下输出:
Employee name is Nilang
最佳实践和反模式
到目前为止,我们讨论了使用 IoC 容器来实现 DI,但最常见的错误之一是在没有进行真正的 DI 的情况下使用 IoC 容器。这听起来可能有些奇怪,但这是一个事实。在没有正确理解底层概念的情况下,这样的错误是可能发生的。
理想情况下,DI 实现应该仅在应用程序启动时引用 IoC 容器。如果开发者自己包装 IoC 容器并将其传递给其他组件以减少任何依赖,这并不是一个好的设计。让我们用一个例子来理解这个问题。
要注入什么 - 容器本身还是只是依赖?
当你尝试将容器本身包装在单例类或公共静态方法中,以向其他组件或模块提供依赖时,就会发生注入容器的情况,如下面的代码片段所示:
public class AccountService {
//Service method.
public void getVariablePay() {
System.out.println("getting variable pay..");
}
}
public class HRService {
public int getLeaveInGivenMonth(int monthNo) {
System.out.println(" getting no of leaves for month "+monthNo);
return 2; // just for demo purpose.
}
}
/* ServiceManager serves like dependency supplier */
public class ServiceManager {
private static ApplicationContext springContext = new ClassPathXmlApplicationContext("application-context.xml");
private ServiceManager() {
}
//This method will return the dependency
public static Object getDependentService(String serviceName) {
Object dependency = null;
if(springContext !=null) {
dependency = springContext.getBean(serviceName);
}
return dependency;
}
}
public class EmployeeService {
private AccountService accountService;
private HRService hrService;
//constructor
public EmployeeService() {
if(ServiceManager.getDependentService("accountService") !=null) {
accountService = (AccountService) ServiceManager.getDependentService("accountService");
}
if(ServiceManager.getDependentService("hrService") !=null) {
hrService = (HRService) ServiceManager.getDependentService("hrService");
}
}
public void generateRewardPoints() {
if(hrService !=null && accountService !=null) {
int noOfLeaves = this.hrService.getLeaveInGivenMonth(8);
System.out.println("No of Leaves are : "+noOfLeaves);
this.accountService.getVariablePay();
//Some complex logic to generate rewards points based on variable pay and total leave
//taken in given month.
}
}
}
这与服务定位器模式等价。在这段代码中,ServiceManager 类持有容器的引用。它将通过其静态方法返回依赖(服务)。EmployeeService 类使用 ServiceManager 获取其依赖(HRService 和 AccountService)。乍一看,这似乎非常合适,因为我们不希望 EmployeeService 与 HRService 和 AccountService 紧密耦合。
虽然我们在前面的代码中移除了依赖的耦合,但这并不是我们所说的 DI。前一个案例中的基本错误是,我们没有提供依赖,而是依赖其他类来提供它。实际上,我们移除了一个实体的依赖,但添加了另一个。这是使用 IoC 容器非常糟糕且没有正确实现 DI 的一个经典例子。
ServiceManager 类是一个单例类,通过其静态方法提供依赖。我们不是将 HRService 和 AccountService 注入到 EmployeeService 中,而是依赖 ServiceManager 提供依赖。
你可能会争辩说,前面的方法将多个依赖项替换为单个类,并将有效地减少依赖。然而,DI 的好处并没有完全实现。当该类发生变化时,对 ServiceManager 的紧密依赖问题才会显现出来。例如,如果你更改 HRManager 或 AccoutService 类的配置,你需要更改 ServiceManager 的代码。
这种情况的一个副作用是,从单元测试的角度来看,事情并不清晰。DI 的好处是,只需查看类的构造函数,就应该知道它依赖于什么,这样你就可以在单元测试时非常容易地注入模拟对象。
在这种情况下,情况正好相反。理想情况下,调用者应该提供依赖,但在这个案例中,调用者没有提供任何东西,而组件(EmployeeService)通过使用自己的单例类来获取依赖。EmployeeService 类的构造函数将是空的,你可能只有在彻底查看其源代码后才能确定其依赖。
之前的设计更像是服务定位器的实现。然而,服务定位器还有一些其他的局限性,如下所示:
-
隔离性: 注册到注册表中的服务对于调用者或客户端类来说最终是黑盒。这导致系统可靠性降低,因为很难识别和纠正依赖服务中发生的错误。
-
并发性: 服务定位器有一个独特的服务注册表,如果它被并发组件访问,可能会导致性能瓶颈。
-
依赖解析: 对于客户端代码,服务定位器提供的注册表有点像黑盒,这可能在运行时引起问题,例如,如果依赖项尚未注册,或者存在任何特定的依赖项问题。
-
可维护性: 在服务定位器中,由于服务实现的代码与客户端代码是隔离的,因此不清楚何时新的更改会在运行时破坏此功能。
-
可测试性: 服务定位器将所有服务存储在注册表中,这使得单元测试变得稍微困难一些,因为所有测试可能都需要依赖注册表来显式设置各种模拟服务类。
我们的目标是使客户端代码与其依赖项或提供依赖项的任何类完全解耦。在前面的例子中,我们希望打破 EmployeeService 与其依赖项之间的耦合。
让我们改进前面的设计,并重写 EmployeeService 类,如下面的代码片段所示:
public class EmployeeService {
private AccountService accountService;
private HRService hrService;
//constructor
public EmployeeService(AccountService accountService,HRService hrService) {
this.accountService = accountService;
this.hrService = hrService;
}
public void generateRewardPoints() {
if(hrService !=null && accountService !=null) {
int noOfLeaves = this.hrService.getLeaveInGivenMonth(8);
System.out.println("No of Leaves are : "+noOfLeaves);
this.accountService.getVariablePay();
//Some complex logic to generate rewards points based on variable pay and total leave
//taken in given month.
}
}
}
现在,EmployeeService 类不再依赖于 HRService 和 AccountService 类。这正是我们想要达到的目标。你的业务代码不应该了解其依赖项。这是 IoC 容器的职责来提供它们。现在这段代码更易于阅读和理解。依赖关系可以通过查看构造函数来预测。
如果你希望实例化 EmployeeService,你只需要传递 HRService 和 AccountService 类的对象。在进行单元测试时,你可以传递模拟对象并测试这些服务之间的集成。现在这个过程变得非常简单。这是 DI 的正确实现和含义。
过度注入
每个设计模式都解决特定设计问题,但任何单一模式并不一定适用于你遇到的每一个情况。你应用的模式或方法应该是因为它是给定问题的正确选择,而不仅仅是因为你了解它并希望实现它。
依赖注入是一种模式(而不是一个框架),因此你需要考虑合适的场景来实现它。有可能使 DI 变得冗余。在代码中注入一切并不是必要的。如果你这样做,代码解耦的目的就无法得到正确实现;相反,依赖图变得无效。
显然,DI 在代码维护方面提供了很大的灵活性,以更有意义和有用的方式执行单元测试,从而实现模块化。然而,你应该只在真正需要时利用其灵活性。DI 的目的是减少耦合,而不是包装和提供每个依赖项,这不是一个明智的决定。
例如,假设你需要一个Calendar对象来执行各种日历相关操作。传统的方法是使用静态方法——例如Calendar类的getInstance方法,例如Calendar.getInstance()。这是Calendar类中的一个静态工厂,用于创建对象。
如果你尝试通过 DI 传递Calendar对象,你将不会得到任何新东西。所有传递Calendar对象的方法(整个调用链——从注入位置到使用位置)都将有额外的参数。这最终给程序员带来了传递Calendar对象的负担。此外,Calendar对象没有通过抽象注入,因此参数是Calendar类型,而不是任何抽象或接口类型。这意味着改变实现没有明显的优势,因为我们传递的是具体类型而不是抽象类型(因为在 Java 中的Calendar类中这是不可能的)。
理想情况下,任何 Java、第三方库或仅提供静态功能且可以在所有组件或模块中通用的自定义类,都应使用静态方式(类引用)或单例机制(如果需要实例)来使用,而不是将它们注入到类中。
另一个例子是在 Java 中使用Logger。获取 logger 实例的典型方式是调用Logger类的getLogger静态方法,并传递你想要提供日志功能的类。在这种情况下,通过 DI 传递Logger对象将是过度设计。
不仅如此,使用 DI 注入此类库将导致仅通过构造函数、方法或属性注入依赖项的组件可用的功能减少。此外,几乎不可能提供任何有意义的抽象,这些抽象可以轻松应用于任何此类库。这将使你在实现上无法获得任何有意义的灵活性。
当你需要提供具有不同依赖配置的依赖项,或者当你想支持同一依赖项的不同实现时,请选择 DI 模式。如果不需要混合依赖项或提供不同的实现,那么 DI 不是合适的解决方案。
在没有容器的情况下实现 IoC
现在,我们深知 DI(依赖注入)的目的是通过构造函数、setter 方法或属性为组件提供依赖,从而使它们与依赖服务分离。传统的理解是,这只能通过使用 IoC(控制反转)容器来实现。然而,并非所有情况都如此。
理想情况下,IoC 容器应该用于配置和解决复杂应用中相对较大的依赖项集合。如果你处理的是一个只有少数组件和依赖项的简单应用,不使用容器是明智的。相反,你可以手动配置依赖项。
此外,在集成容器困难的传统系统中,你可以选择手动提供依赖项。你可以实现各种模式,例如工厂方法、服务定位器、策略或模板方法模式来管理依赖项。
摘要
在本章中,我们学习了一些关于管理依赖项的最佳实践和模式的重要观点。尽管已经证明 DI 通过解耦客户端代码与其依赖项,为代码带来了更大的灵活性和模块化,但还有一些事情我们应该遵循,以充分利用它。
在开始时,我们学习了除了依赖注入(DI)之外的其他模式,这些模式有助于我们实现控制反转(IoC)。你绝对可以在代码中使用它们来解耦无法使用 IoC 容器的模块。例如,在一个无法通过 IoC 容器管理依赖项的传统代码中,这些模式对于实现 IoC 非常有用。
我们熟悉了各种配置选项,并学习了如何选择正确的选项。我们还看到了在配置依赖项时使用的注入风格。在处理依赖项管理时,一个非常明显的问题是循环引用,这会导致循环依赖。我们已经观察到了循环依赖引起的问题、其成因以及如何在编码中避免它们。
最后,我们深入探讨了在使用 DI 时应遵循的最佳实践、模式和反模式。如果我们知道如何做某事,并不意味着它总是适用。DI 也是如此。它是一个模式,因此应该以正确的方式使用来解决特定问题。它可能并不适用于所有情况。
我们在这里暂停一下。我们希望你喜欢在整本书中学习 DI 的旅程。我们试图尽可能简单明了地传达基本原理。


浙公网安备 33010602011771号