- 示例工程
- 教程
【免费下载链接】java-design-patterns
Design patterns implemented in Java
依赖注入(Dependency Injection,DI)是 Java 世界中用于解耦对象依赖创建与使用的最重要设计模式之一,本指南以当前仓库 java-design-patterns 中 dependency-injection 模块的完整源码与测试为实证基础,系统讲解该模式的核心思想、构造器注入/Setter 注入/框架注入三种落地方式,以及它在 Spring、Jakarta EE、Google Guice 等框架中的真实应用。读完本文,你将能够识别违反依赖倒置的坏代码,并亲手把手动装配、Guice 模块绑定等手段应用到自己的项目中。
模式概览:别名与意图
Dependency Injection 模式在业界还有两个常见别名:
- Inversion of Control(IoC,控制反转)
- Dependency Inversion(依赖倒置)
模式的核心意图是:将“对象所需依赖的创建过程”与“依赖的使用过程”解耦,从而使代码更加灵活、更加易于测试。仓库中 App.java 的类注释进一步点明了 IoC 的两条具体规则:
- 高层模块不应依赖低层模块,两者都应依赖抽象;
- 抽象不应依赖细节,细节应依赖抽象。
也就是说,依赖注入的本质是让对象不再自己“造”依赖,而是从外部“接收”依赖。
现实世界的类比:餐厅与供应商
设想一家高档餐厅,主厨做菜需要各种各样的食材。他不必为每种食材亲自跑去找各自的制造商,而是依赖一位可靠的供应商,由供应商每天从各个制造商那里采购新鲜食材送上门。这样主厨就能专注于烹饪,无需操心采购。
在 Dependency Injection 模式中,供应商扮演"注入器(Injector)"的角色,把所需依赖(食材)提供给"对象"——主厨。主厨可以使用这些食材,却不必知道它们的来源,依赖的获取与使用被清晰分离。这种思路同时提升了厨房(以及软件系统)的效率、灵活性与可维护性。
用一句最简单的话概括:
依赖注入把“依赖的获取”与“使用者的自身行为”分离开来。
维基百科对它的定义是:在软件工程中,依赖注入是一种对象接收其所需的其他对象(称为依赖)的技术。
模式时序图
下图展示了依赖注入的核心交互流程——客户端(Wizard)通过注入器获得抽象依赖(Tobacco)并调用其方法:
程序示例:三种注入方式的渐进实现
仓库中的示例围绕一位老巫师展开:他时不时想填满烟斗抽上一斗烟,但他不希望被锁定在单一烟草品牌上,而是想随时互换不同品牌。下面我们沿着源码逐步实现。
1. 抽象与具体实现:Tobacco 体系
首先定义Tobacco抽象类与具体品牌类。注意当前仓库版本中的Tobacco使用了 Lombok 的@Slf4j注解(Tobacco.java):
@Slf4j public abstract class Tobacco { public void smoke(Wizard wizard) { LOGGER.info("{} smoking {}", wizard.getClass().getSimpleName(), this.getClass().getSimpleName()); } } public class SecondBreakfastTobacco extends Tobacco { } public class RivendellTobacco extends Tobacco { } public class OldTobyTobacco extends Tobacco { }三个具体类 SecondBreakfastTobacco、RivendellTobacco、OldTobyTobacco 均继承自Tobacco,它们共享smoke(Wizard)的日志行为,同时又是可互换的具体实现。
2. Wizard 接口与违反 IoC 的反例:SimpleWizard
所有巫师都实现统一的 Wizard 接口:
public interface Wizard { void smoke(); }接着是SimpleWizard——它是一段故意违反控制反转原则的反面教材(SimpleWizard.java):
public class SimpleWizard implements Wizard { private final OldTobyTobacco tobacco = new OldTobyTobacco(); public void smoke() { tobacco.smoke(this); } }可以看到,SimpleWizard在字段初始化时直接new了一个具体实现OldTobyTobacco:它依赖的是“细节”而非“抽象”,既无法在测试中替换为 Mock/Stub,也无法在运行时切换品牌。这正是 DI 模式要解决的问题。
3. 构造器注入:AdvancedWizard
AdvancedWizard通过构造器接收依赖,字段为final,一旦注入即不可变(AdvancedWizard.java):
public class AdvancedWizard implements Wizard { private final Tobacco tobacco; public AdvancedWizard(Tobacco tobacco) { this.tobacco = tobacco; } @Override public void smoke() { tobacco.smoke(this); } }它只依赖抽象Tobacco,具体品牌由调用方在构造时决定——创建与使用被彻底分离,这也让单元测试可以自由注入任意品牌。
4. Setter 注入:AdvancedSorceress
AdvancedSorceress展示的是Setter 注入变体,借助 Lombok 的@Setter注解生成setTobacco(...)方法(AdvancedSorceress.java):
@Setter public class AdvancedSorceress implements Wizard { private Tobacco tobacco; @Override public void smoke() { tobacco.smoke(this); } }与构造器注入相比,Setter 注入允许在对象创建之后再更换依赖,适合依赖需要在运行时动态切换的场景;代价是字段无法声明为final,失去了不可变性保证。
5. 框架注入:Guice 与 TobaccoModule
第四种方式把模式推进到框架层面:使用Google Guice完成自动装配。首先定义一个 Guice 模块,将抽象绑定到具体实现(TobaccoModule.java):
public class TobaccoModule extends AbstractModule { @Override protected void configure() { bind(Tobacco.class).to(RivendellTobacco.class); } }bind(Tobacco.class).to(RivendellTobacco.class)意味着:只要容器需要Tobacco,就自动提供RivendellTobacco实例。接下来GuiceWizard通过构造器上的@Inject注解声明注入点(GuiceWizard.java):
public class GuiceWizard implements Wizard { private final Tobacco tobacco; @Inject public GuiceWizard(Tobacco tobacco) { this.tobacco = tobacco; } @Override public void smoke() { tobacco.smoke(this); } }6. 运行入口 App 与程序输出
最终,App.java 的main方法一次性演示了上述四种风格:
public static void main(String[] args) { var simpleWizard = new SimpleWizard(); simpleWizard.smoke(); var advancedWizard = new AdvancedWizard(new SecondBreakfastTobacco()); advancedWizard.smoke(); var advancedSorceress = new AdvancedSorceress(); advancedSorceress.setTobacco(new SecondBreakfastTobacco()); advancedSorceress.smoke(); var injector = Guice.createInjector(new TobaccoModule()); var guiceWizard = injector.getInstance(GuiceWizard.class); guiceWizard.smoke(); }程序输出(时间戳因运行时刻而异):
11:54:05.205 [main] INFO com.iluwatar.dependency.injection.Tobacco -- SimpleWizard smoking OldTobyTobacco 11:54:05.207 [main] INFO com.iluwatar.dependency.injection.Tobacco -- AdvancedWizard smoking SecondBreakfastTobacco 11:54:05.207 [main] INFO com.iluwatar.dependency.injection.Tobacco -- AdvancedSorceress smoking SecondBreakfastTobacco 11:54:05.308 [main] INFO com.iluwatar.dependency.injection.Tobacco -- GuiceWizard smoking RivendellTobacco注意最后一行:SimpleWizard因硬编码只抽OldTobyTobacco,而GuiceWizard抽的是RivendellTobacco——这正体现了“由容器决定依赖,而不是由使用者决定”的控制反转。
如何在仓库中运行与验证
dependency-injection模块是一个独立 Maven 模块,其 pom.xml 声明了以下依赖与配置:
- 日志:
slf4j-api+logback-classic; - 测试:
junit-jupiter-engine(scope 为 test); - 依赖注入框架:
com.google.inject:guice; - 打包插件
maven-assembly-plugin将主类配置为com.iluwatar.dependency.injection.App。
在仓库根目录(java-design-patterns)下,可使用随仓库提供的 Maven Wrapper 运行:
# 运行该模块的全部单元测试 ./mvnw -pl dependency-injection -am test # 打包可执行 jar(assembly 插件会生成含主类的可运行包) ./mvnw -pl dependency-injection -am package若直接运行主类,可执行java -cp ... com.iluwatar.dependency.injection.App(classpath 需包含模块编译产物与 guice、slf4j 等依赖,实践中最简单的方式是使用上面的 assembly 打包产物)。
测试如何验证注入行为
仓库为每种注入方式都配备了单元测试,它们共同印证了“依赖可注入、可替换”这一核心主张。
AdvancedWizardTest.java 验证构造器注入:将三种烟草依次通过构造器传入AdvancedWizard,断言每次日志都精确匹配AdvancedWizard smoking <品牌名>,并且没有额外输出:
List<Tobacco> tobaccos = List.of(new OldTobyTobacco(), new RivendellTobacco(), new SecondBreakfastTobacco()); tobaccos.forEach(tobacco -> { final AdvancedWizard advancedWizard = new AdvancedWizard(tobacco); advancedWizard.smoke(); String lastMessage = appender.getLastMessage(); assertEquals("AdvancedWizard smoking " + tobacco.getClass().getSimpleName(), lastMessage); }); assertEquals(tobaccos.size(), appender.getLogSize());GuiceWizardTest.java 则更进一步,包含两个用例:
testSmokeEveryThingThroughConstructor:直接向构造器传参验证@Inject构造器本身的行为;testSmokeEveryThingThroughInjectionFramework:在测试中动态构建匿名AbstractModule,把Tobacco.class分别绑定到三种品牌,再由Guice.createInjector(...).getInstance(GuiceWizard.class)获取实例并验证日志。该用例证明:只需改动绑定配置即可更换依赖实现,业务代码GuiceWizard完全无需改动。
这些测试依赖utils.InMemoryAppender(InMemoryAppender.java)在内存中捕获日志,从而断言行为而无需依赖真实控制台输出——这本身就是“依赖抽象、可被替换”思想在测试基建上的体现。
何时使用依赖注入模式
根据仓库文档,以下场景适合采用 DI:
- 需要降低类与类之间的耦合、提升应用模块化程度时;
- 对象创建过程复杂,或应当与类的使用过程分离时;
- 应用需要更轻松的单元测试,允许依赖以 Mock 或 Stub 形式注入时;
- 在使用管理对象生命周期与依赖的框架(如 Spring、Jakarta EE,即原 Java EE)时。
Java 中的真实应用
- Spring、Jakarta EE 与 Google Guice等框架大量使用依赖注入来管理组件生命周期与依赖关系;
- 需要灵活架构、组件易于互换的桌面应用与 Web 应用,广泛受益于 DI 带来的可配置性。
优点与权衡
优点:
- 提升模块化程度与关注点分离;
- 简化单元测试——依赖可轻松被 Mock 替换;
- 通过促进松耦合提升灵活性与可维护性。
权衡与代价:
- 可能使配置复杂化,尤其是在大型项目中;
- 对不熟悉 DI 概念或框架的开发者存在学习曲线;
- 需要谨慎管理对象的生命周期与作用域(scope),否则容易引发作用域泄漏或内存问题。
相关设计模式
在 java-design-patterns 仓库中,与 DI 密切相关的模式包括:
- 工厂方法(Factory Method) 与 抽象工厂(Abstract Factory):用于创建将被 DI 机制注入的实例,两者常配合使用;
- 服务定位器(Service Locator):DI 的一种替代方案,用于定位服务或组件,但对“查找过程”的解耦效果不如 DI 彻底;
- 单例(Singleton):常与 DI 结合使用,为整个应用提供服务的单一实例。
参考资料
本文内容以仓库文档与源码为准,以下为文档中列出的延伸阅读书目(供深入学习参考):
- Clean Code: A Handbook of Agile Software Craftsmanship(Robert C. Martin)
- Dependency Injection: Design Patterns Using Spring and Guice(Dhanji R. Prasanna)
- Dependency Injection Principles, Practices, and Patterns(Steven van Deuren / Mark Seemann)
- Google Guice: Agile Lightweight Dependency Injection Framework
- Java 9 Dependency Injection: Write Loosely Coupled Code with Spring 5 and Guice
- Java Design Pattern Essentials
- Pro Java EE Spring Patterns: Best Practices and Design Strategies
- Spring in Action(Craig Walls)
- 示例工程
- 教程
【免费下载链接】java-design-patterns
Design patterns implemented in Java
相关推荐
Java 设计模式之依赖注入(Dependency Injection):以 java-design-patterns 仓库源码深度解析松耦合实现
Java 设计模式之依赖注入(Dependency Injection):以 java design patterns 仓库源码深度解析松耦合实现 依赖注入(D
示例工程教程Java 依赖注入(Dependency Injection)模式实战:以 java-design-patterns 为例从手动注入到 Google Guice
Java 依赖注入(Dependency Injection)模式实战:以 java design patterns 为例从手动注入到 Google Guice
示例工程教程java-design-patterns 中的依赖注入(Dependency Injection)模式:以 Wizard 与 Tobacco 为例解析控制反转与三种注入方式
java design patterns 中的依赖注入(Dependency Injection)模式:以 Wizard 与 Tobacco 为例解析控制反转与
示例工程教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考