Java 依赖注入 – DI 设计模式示例教程

Java 依赖注入 设计模式允许我们去除硬编码的依赖关系,使我们的应用程序松耦合、可扩展和可维护。我们可以在 Java 中实现 依赖注入,将依赖关系的解析从编译时移到运行时。

Java 依赖注入

Java 依赖注入在理论上似乎难以理解,因此我将举一个简单的例子,然后我们将看看如何使用依赖注入模式来实现应用程序中的松耦合和可扩展性。假设我们有一个应用程序,在其中使用 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", "[email protected]");
	}

}

乍一看,上述实现似乎没有什么问题。但是上述代码逻辑有一定的局限性。

  • MyApplication类负责初始化电子邮件服务,然后使用它。这导致了硬编码的依赖关系。如果我们将来想要切换到其他高级电子邮件服务,将需要在MyApplication类中进行代码更改。这使得我们的应用程序难以扩展,如果电子邮件服务在多个类中使用,则会更加困难。
  • 如果我们想要扩展我们的应用程序以提供额外的消息功能,比如短信或Facebook消息,那么我们需要为此编写另一个应用程序。这将涉及到应用程序类和客户端类的代码更改。
  • 测试应用程序将非常困难,因为我们的应用程序直接创建电子邮件服务实例。我们无法在测试类中模拟这些对象。

有人可能会说,我们可以通过在MyApplication类中添加一个需要电子邮件服务作为参数的构造函数来删除电子邮件服务实例的创建。

package com.journaldev.java.legacy;

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依赖注入模式来解决上述实现中的所有问题。Java中的依赖注入至少需要以下内容:

  1. 服务组件应设计为基类或接口。最好优先使用定义服务合同的接口或抽象类。
  2. 消费者类应根据服务接口编写。
  3. 注入器类将初始化服务,然后是消费者类。

Java 依赖注入 – 服务组件

对于我们的情况,我们可以有声明服务实现合同的MessageService

package com.journaldev.java.dependencyinjection.service;

public interface MessageService {

	void sendMessage(String msg, String rec);
}

现在假设我们有实现上述接口的电子邮件和短信服务。

package com.journaldev.java.dependencyinjection.service;

public class EmailServiceImpl implements MessageService {

	@Override
	public void sendMessage(String msg, String rec) {
		//发送电子邮件的逻辑
		System.out.println("Email sent to "+rec+ " with Message="+msg);
	}

}
package com.journaldev.java.dependencyinjection.service;

public class SMSServiceImpl implements MessageService {

	@Override
	public void sendMessage(String msg, String rec) {
		//发送短信的逻辑
		System.out.println("SMS sent to "+rec+ " with Message="+msg);
	}

}

我们的依赖注入 Java 服务已准备就绪,现在我们可以编写我们的消费者类。

Java 依赖注入 – 服务消费者

我们不需要为消费者类定义基本接口,但我将创建一个Consumer接口,声明消费者类的合同。

package com.journaldev.java.dependencyinjection.consumer;

public interface Consumer {

	void processMessages(String msg, String rec);
}

我的消费者类实现如下。

package com.journaldev.java.dependencyinjection.consumer;

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);
	}

}

请注意,我们的应用程序类只是使用服务。它不初始化导致更好的“关注点分离”的服务。同时,使用服务接口允许我们通过模拟MessageService轻松测试应用程序,并在运行时绑定服务,而不是在编译时绑定。现在,我们准备编写Java依赖注入器类,这些类将初始化服务并处理消费者类。

Java依赖注入 – 注入器类

让我们创建一个名为MessageServiceInjector的接口,其中包含返回Consumer类的方法声明。

package com.journaldev.java.dependencyinjection.injector;

import com.journaldev.java.dependencyinjection.consumer.Consumer;

public interface MessageServiceInjector {

	public Consumer getConsumer();
}

现在,对于每个服务,我们都需要创建类似下面的注入器类。

package com.journaldev.java.dependencyinjection.injector;

import com.journaldev.java.dependencyinjection.consumer.Consumer;
import com.journaldev.java.dependencyinjection.consumer.MyDIApplication;
import com.journaldev.java.dependencyinjection.service.EmailServiceImpl;

public class EmailServiceInjector implements MessageServiceInjector {

	@Override
	public Consumer getConsumer() {
		return new MyDIApplication(new EmailServiceImpl());
	}

}
package com.journaldev.java.dependencyinjection.injector;

import com.journaldev.java.dependencyinjection.consumer.Consumer;
import com.journaldev.java.dependencyinjection.consumer.MyDIApplication;
import com.journaldev.java.dependencyinjection.service.SMSServiceImpl;

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.EmailServiceInjector;
import com.journaldev.java.dependencyinjection.injector.MessageServiceInjector;
import com.journaldev.java.dependencyinjection.injector.SMSServiceInjector;

public class MyMessageDITest {

	public static void main(String[] args) {
		String msg = "Hi Pankaj";
		String email = "[email protected]";
		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消息传递,我们只需编写服务类和注入器类。因此,依赖注入实现解决了硬编码依赖的问题,并帮助我们使应用程序更加灵活和易于扩展。现在,让我们看看如何通过模拟注入器和服务类轻松测试我们的应用程序类。

Java依赖注入 – 使用模拟注入器和服务的JUnit测试案例

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", "[email protected]");
	}
	
	@After
	public void tear(){
		injector = null;
	}

}

如您所见,我正在使用匿名类模拟注入器和服务类,并且可以轻松地测试我的应用程序方法。我正在使用JUnit 4进行上述测试类,所以如果您运行上述测试类,请确保它在您的项目构建路径中。我们已经使用构造函数在应用程序类中注入了依赖项,另一种方法是使用setter方法在应用程序类中注入依赖项。对于setter方法依赖注入,我们的应用程序类将实现如下。

package com.journaldev.java.dependencyinjection.consumer;

import com.journaldev.java.dependencyinjection.service.MessageService;

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);
	}

}
package com.journaldev.java.dependencyinjection.injector;

import com.journaldev.java.dependencyinjection.consumer.Consumer;
import com.journaldev.java.dependencyinjection.consumer.MyDIApplication;
import com.journaldev.java.dependencyinjection.service.EmailServiceImpl;

public class EmailServiceInjector implements MessageServiceInjector {

	@Override
	public Consumer getConsumer() {
		MyDIApplication app = new MyDIApplication();
		app.setService(new EmailServiceImpl());
		return app;
	}

}

依赖注入的最佳示例之一是Struts2 Servlet API Aware接口。使用构造函数注入还是使用setter方法注入是一个设计决策,取决于您的需求。例如,如果我的应用程序没有服务类根本无法工作,那么我会更喜欢基于构造函数的DI;否则,我会选择基于setter方法的DI,仅在真正需要时使用它。Java中的依赖注入是通过将对象绑定从编译时移动到运行时来实现应用程序中的控制反转IoC)的一种方式。我们可以通过工厂模式模板方法设计模式策略模式和服务定位器模式来实现IoC。Spring依赖注入Google GuiceJava EE CDI框架通过使用Java反射APIJava注解来简化依赖注入的过程。我们只需要在字段、构造函数或setter方法上进行注解,并将它们配置在配置XML文件或类中。

Java依赖注入的优势

在Java中使用依赖注入的一些优势:

  • 关注点分离
  • 应用类中的样板代码减少,因为所有初始化依赖关系的工作都由注入器组件处理
  • 可配置的组件使应用程序易于扩展
  • 使用模拟对象进行单元测试变得很容易

Java依赖注入的缺点

Java依赖注入也有一些缺点:

  • 如果过度使用,可能会导致维护问题,因为更改的影响在运行时才能知道。
  • Java中的依赖注入隐藏了服务类的依赖关系,可能导致在编译时可以捕获的运行时错误。

下载依赖注入项目

关于Java中的依赖注入模式就是这些。在我们控制服务时了解并使用它是很好的。

Source:
https://www.digitalocean.com/community/tutorials/java-dependency-injection-design-pattern-example-tutorial