Java依赖注入(DI)设计模式详解:实战教程与代码示例

这是文章《Java依赖注入 – DI设计模式示例教程》的第1部分(共3部分)。

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

Java依赖注入

Java的依赖注入在理论上似乎很难理解,所以我会举一个简单的例子,然后我们将看到如何使用依赖注入模式来实现应用程序的松耦合和可扩展性。假设我们有一个应用程序,我们使用EmailService来发送电子邮件。通常情况下,我们会像下面这样实现。

package com.Olivia.java.legacy;

public class EmailService {

	public void sendEmail(String message, String receiver){
		// 发送邮件的逻辑
		System.out.println("邮件已发送至 "+receiver+ ",内容为="+message);
	}
}

EmailService类持有将电子邮件消息发送到接收方电子邮件地址的逻辑。我们的应用代码将如下所示。

package com.Olivia.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.Olivia.java.legacy;

public class MyLegacyTest {

	public static void main(String[] args) {
		MyApplication app = new MyApplication();
		app.processMessages("Hi Pankaj", "scdev@abc.com");
	}

}

乍一看,上述实现似乎没有问题。但是,上述代码逻辑存在一定的限制。

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

可以说,我们可以通过在MyApplication类中添加一个要求电子邮件服务作为参数的构造函数,来取消电子邮件服务实例的创建。

package com.Olivia.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.Olivia.java.dependencyinjection.service;

public interface MessageService {

	void sendMessage(String msg, String rec);
}

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

package com.Olivia.java.dependencyinjection.service;

public class EmailServiceImpl implements MessageService {

	@Override
	public void sendMessage(String msg, String rec) {
		// 发送邮件的逻辑
		System.out.println("邮件已发送至 "+rec+ ",内容为="+msg);
	}

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

public class SMSServiceImpl implements MessageService {

	@Override
	public void sendMessage(String msg, String rec) {
		// 发送短信的逻辑
		System.out.println("短信已发送至 "+rec+ ",内容为="+msg);
	}

}

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

Java依赖注入 – 服务消费者

我们不需要消费者类的基接口,但是我会有一个声明消费者类合同的消费者接口。

package com.Olivia.java.dependencyinjection.consumer;

public interface Consumer {

	void processMessages(String msg, String rec);
}

我的消费者类实现如下。

package com.Olivia.java.dependencyinjection.consumer;

import com.Olivia.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);
	}

}

请注意,我们的应用程序类仅仅使用服务,而不是初始化服务,这样可以更好地实现”关注点分离”。同时,使用服务接口使我们能够通过模拟消息服务来轻松测试应用程序,并在运行时而不是编译时绑定服务。现在我们可以写Java依赖注入器类,这些类会初始化服务和消费类。

Java 依赖注入 – 注入器类

这是文章《Java依赖注入 – DI设计模式示例教程》的第2部分(共3部分)。

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

package com.Olivia.java.dependencyinjection.injector;

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

public interface MessageServiceInjector {

	public Consumer getConsumer();
}

现在,针对每项服务,我们需要创建相应的注入器类,如下所示:

package com.Olivia.java.dependencyinjection.injector;

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

public class EmailServiceInjector implements MessageServiceInjector {

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

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

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

public class SMSServiceInjector implements MessageServiceInjector {

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

}

接下来,让我们通过一个简单的程序示例,了解客户端应用程序如何使用这个应用程序。

package com.Olivia.java.dependencyinjection.test;

import com.Olivia.java.dependencyinjection.consumer.Consumer;
import com.Olivia.java.dependencyinjection.injector.EmailServiceInjector;
import com.Olivia.java.dependencyinjection.injector.MessageServiceInjector;
import com.Olivia.java.dependencyinjection.injector.SMSServiceInjector;

public class MyMessageDITest {

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

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

package com.Olivia.java.dependencyinjection.test;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import com.Olivia.java.dependencyinjection.consumer.Consumer;
import com.Olivia.java.dependencyinjection.consumer.MyDIApplication;
import com.Olivia.java.dependencyinjection.injector.MessageServiceInjector;
import com.Olivia.java.dependencyinjection.service.MessageService;

public class MyDIApplicationJUnitTest {

	private MessageServiceInjector injector;
	@Before
	public void setUp(){
		//mock the injector with anonymous class
		injector = new MessageServiceInjector() {
			
			@Override
			public Consumer getConsumer() {
				//mock the message service
				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", "scdev@abc.com");
	}
	
	@After
	public void tear(){
		injector = null;
	}

}

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

package com.Olivia.java.dependencyinjection.consumer;

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

public class MyDIApplication implements Consumer{

	private MessageService service;
	
	public MyDIApplication(){}

	//setter dependency injection	
	public void setService(MessageService service) {
		this.service = service;
	}

	@Override
	public void processMessages(String msg, String rec){
		//do some msg validation, manipulation logic etc
		this.service.sendMessage(msg, rec);
	}

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

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

public class EmailServiceInjector implements MessageServiceInjector {

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

}

依赖注入是实现控制反转(IoC)的一种方式,通过将对象绑定从编译时移动到运行时来实现。在Java中,我们可以通过工厂模式、模板方法设计模式、策略模式和服务定位器模式来实现IoC。Spring依赖注入、Google Guice和Java EE CDI框架通过使用Java Reflection API和Java注解来简化依赖注入的过程。我们只需在配置XML文件或类中注释字段、构造函数或setter方法即可。

其中一个最好的setter依赖注入的例子是Struts2的Servlet API Aware接口。使用构造函数基于依赖注入还是基于setter的依赖注入是一个设计决策,取决于您的需求。例如,如果我的应用程序没有服务类根本无法工作,我会更倾向于选择基于构造函数的DI。否则,当确实需要时,我会选择基于setter方法的DI来使用它。

Java依赖注入的好处

在Java中使用依赖注入的一些好处是:

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

Java 依赖注入的缺点

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

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

下载依赖注入项目

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

bannerAds