Pankaj Kumar 2022-08-04
Java 依赖注入简介
Java 依赖注入(Dependency Injection,简称 DI)设计模式允许我们移除硬编码的依赖关系,使应用程序具有松耦合、可扩展和可维护的特性。我们可以使用依赖注入将依赖解析从编译期推迟到运行时。
仅靠理论很难理解依赖注入,因此本文将通过一个简单示例来说明如何使用依赖注入模式实现应用程序的松耦合与可扩展性。
假设我们有一个应用程序,它使用 EmailService 来发送电子邮件。通常我们会这样实现:
package com.journaldev.java.legacy;
public class EmailService {
public void sendEmail(String message, String receiver) {
// 发送邮件的逻辑
System.out.println("Email sent to " + receiver + " with Message=" + message);
}
}
EmailService 类包含了向接收者邮箱地址发送邮件消息的逻辑。我们的应用程序代码可能如下所示:
package com.journaldev.java.legacy;
public class MyApplication {
private EmailService email = new EmailService();
public void processMessages(String msg, String rec) {
// 执行一些消息验证、处理等逻辑
this.email.sendEmail(msg, rec);
}
}
客户端代码使用 MyApplication 类来发送邮件:
package com.journaldev.java.legacy;
public class MyLegacyTest {
public static void main(String[] args) {
MyApplication app = new MyApplication();
app.processMessages("Hi Pankaj", "pankaj@abc.com");
}
}
乍看之下,上述实现似乎没有问题。但实际上存在以下限制:
- 硬编码依赖:
MyApplication类负责初始化EmailService并使用它。如果将来要切换到其他更高级的邮件服务,就需要修改MyApplication的代码。这使得应用程序难以扩展,若多个类都使用了该服务,改动会更加困难。 - 功能扩展困难:如果想为应用增加短信或 Facebook 消息等功能,需要编写新的应用类,这会涉及大量代码修改,包括客户端代码。
- 测试困难:由于应用直接创建了
EmailService实例,无法在测试中轻松模拟(mock)这些对象。
有人可能会建议通过构造函数传入服务实例:
public class MyApplication {
private EmailService email = null;
public MyApplication(EmailService svc) {
this.email = svc;
}
public void processMessages(String msg, String rec) {
this.email.sendEmail(msg, rec);
}
}
但这样又把初始化服务的责任推给了客户端或测试类,这不是良好的设计。
使用 Java 依赖注入解决上述问题
要实现依赖注入,至少需要以下三个组成部分:
- 服务组件:应基于接口或抽象类设计,以定义服务契约。
- 消费者类:应基于服务接口编写,而非具体实现。
- 注入器类(Injector):负责初始化服务并将其注入到消费者类中。
1. 服务组件(Service Components)
首先定义一个通用的消息服务接口:
package com.journaldev.java.dependencyinjection.service;
public interface MessageService {
void sendMessage(String msg, String rec);
}
然后分别实现邮件和短信服务:
// 邮件服务实现
public class EmailServiceImpl implements MessageService {
@Override
public void sendMessage(String msg, String rec) {
System.out.println("Email sent to " + rec + " with Message=" + msg);
}
}
// 短信服务实现
public class SMSServiceImpl implements MessageService {
@Override
public void sendMessage(String msg, String rec) {
System.out.println("SMS sent to " + rec + " with Message=" + msg);
}
}
2. 服务消费者(Service Consumer)
定义消费者接口:
package com.journaldev.java.dependencyinjection.consumer;
public interface Consumer {
void processMessages(String msg, String rec);
}
实现消费者类,并通过构造函数注入服务:
import com.journaldev.java.dependencyinjection.service.MessageService;
public class MyDIApplication implements Consumer {
private MessageService service;
public MyDIApplication(MessageService svc) {
this.service = svc;
}
@Override
public void processMessages(String msg, String rec) {
// 消息处理逻辑
this.service.sendMessage(msg, rec);
}
}
注意:应用类只负责使用服务,不负责创建服务,实现了更好的“关注点分离”。
3. 注入器类(Injector Classes)
定义注入器接口:
package com.journaldev.java.dependencyinjection.injector;
import com.journaldev.java.dependencyinjection.consumer.Consumer;
public interface MessageServiceInjector {
public Consumer getConsumer();
}
分别为每种服务实现注入器:
// 邮件服务注入器
public class EmailServiceInjector implements MessageServiceInjector {
@Override
public Consumer getConsumer() {
return new MyDIApplication(new EmailServiceImpl());
}
}
// 短信服务注入器
public class SMSServiceInjector implements MessageServiceInjector {
@Override
public Consumer getConsumer() {
return new MyDIApplication(new SMSServiceImpl());
}
}
客户端使用示例
package com.journaldev.java.dependencyinjection.test;
import com.journaldev.java.dependencyinjection.consumer.Consumer;
import com.journaldev.java.dependencyinjection.injector.*;
public class MyMessageDITest {
public static void main(String[] args) {
String msg = "Hi Pankaj";
String email = "pankaj@abc.com";
String phone = "4088888888";
MessageServiceInjector injector = null;
Consumer app = null;
// 发送邮件
injector = new EmailServiceInjector();
app = injector.getConsumer();
app.processMessages(msg, email);
// 发送短信
injector = new SMSServiceInjector();
app = injector.getConsumer();
app.processMessages(msg, phone);
}
}
可以看到:
- 应用类只负责使用服务;
- 服务由注入器创建;
- 若需新增 Facebook 消息功能,只需添加新的服务实现和注入器,无需修改现有代码。
单元测试:使用 Mock 对象
依赖注入使得单元测试变得非常容易。例如,使用 JUnit 4 编写测试:
package com.journaldev.java.dependencyinjection.test;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import com.journaldev.java.dependencyinjection.consumer.Consumer;
import com.journaldev.java.dependencyinjection.consumer.MyDIApplication;
import com.journaldev.java.dependencyinjection.injector.MessageServiceInjector;
import com.journaldev.java.dependencyinjection.service.MessageService;
public class MyDIApplicationJUnitTest {
private MessageServiceInjector injector;
@Before
public void setUp() {
// 使用匿名类模拟注入器和服务
injector = new MessageServiceInjector() {
@Override
public Consumer getConsumer() {
return new MyDIApplication(new MessageService() {
@Override
public void sendMessage(String msg, String rec) {
System.out.println("Mock Message Service implementation");
}
});
}
};
}
@Test
public void test() {
Consumer consumer = injector.getConsumer();
consumer.processMessages("Hi Pankaj", "pankaj@abc.com");
}
@After
public void tear() {
injector = null;
}
}
通过匿名类轻松模拟服务行为,无需真实依赖。
Setter 方法注入 vs 构造函数注入
除了构造函数注入,也可以使用 Setter 方法注入:
public class MyDIApplication implements Consumer {
private MessageService service;
public MyDIApplication() {}
// Setter 注入
public void setService(MessageService service) {
this.service = service;
}
@Override
public void processMessages(String msg, String rec) {
this.service.sendMessage(msg, rec);
}
}
对应的注入器:
public class EmailServiceInjector implements MessageServiceInjector {
@Override
public Consumer getConsumer() {
MyDIApplication app = new MyDIApplication();
app.setService(new EmailServiceImpl());
return app;
}
}
选择建议:
- 如果应用必须依赖某服务才能正常工作,推荐使用构造函数注入;
- 如果服务是可选的或按需使用,可考虑Setter 注入。
Struts2 中的 Servlet API Aware 接口就是 Setter 注入的典型例子。
依赖注入与控制反转(IoC)
依赖注入是实现**控制反转(Inversion of Control, IoC)**的一种方式。IoC 还可通过以下模式实现:
- 工厂模式(Factory Pattern)
- 模板方法模式(Template Method)
- 策略模式(Strategy Pattern)
- 服务定位器模式(Service Locator)
而像 Spring、Google Guice、Java EE CDI 等框架,通过 Java 反射(Reflection) 和 注解(Annotations) 自动完成依赖注入,开发者只需在字段、构造函数或方法上添加注解,并在配置文件或类中声明即可。
依赖注入的优点
- 关注点分离(Separation of Concerns)
- 减少样板代码:依赖初始化由注入器统一处理
- 组件可配置:易于扩展新功能
- 便于单元测试:可轻松使用 Mock 对象
依赖注入的缺点
- 过度使用可能导致维护困难:因为依赖关系在运行时才确定
- 隐藏依赖关系:编译时无法发现缺失的依赖,可能在运行时报错
总结
依赖注入是一种强大的设计模式,能显著提升 Java 应用的灵活性和可测试性。当你能够控制服务的生命周期时,建议合理使用依赖注入。