Java依赖注入(DI)实例详解

Java依赖注入模式允许我们摆脱硬编码,使我们的应用更加松耦合、增强扩展性以及可维护性。通过依赖注入我们可以降低从编译到运行时的依赖性。

Java依赖注入

Java的依赖注入仅仅通过理论是很难解明白的,所以我们通过几个简单的示例来描述它,怎样利用依赖注入模式降低我们应用之间的耦合性和增强可扩展性。

假设我们的应用需要通过 EmailService 去发送email,通常情况下,我们是这样实现的:

package com.byron4j.hightLevel.java8.pattern.di;

/**

*

*

* 通常用于发送emai的服务

*

* @author Byron.Y.Y

*/

public class EmailService {

/*

* 发送email的方法

*/

public void sendEmail(String message, String receiver){

//发送email的业务逻辑

System.out.println("发送消息:" + message + "给接收者:" + receiver);

}

}

EmailService 类是提供发送email的服务类,在我们的应用中,可能会这样使用发送email的服务:

package com.byron4j.hightLevel.java8.pattern.di;

/**

*

*

* 我们的应用,利用EmailService来发送email

*

* @author Byron.Y.Y

*/

public class MyApplication {

private EmailService emailService = new EmailService();

public void processMessages(String msg, String rec){

//做一些信息验证、操作逻辑等等

this.emailService.sendEmail(msg, rec);

}

}

在我们的客户端,则可能使用我们的应用MyApplication 来处理email:

package com.byron4j.hightLevel.java8.pattern.di;

/**

*

*

* 调用应用提供的处理eamil的服务

*

* @author Byron.Y.Y

*/

public class MyLegacyTest {

public static void main(String[] args) {

MyApplication myApplication = new MyApplication();

//纯属虚拟,勿投诉

myApplication.processMessages("马云,你好!", "mayun@taobao.com");

}

}

初窥以上代码,貌似没什么缺陷,但是业务逻辑上有几个限制。

MyApplication 类需要负责初始化emailService并且使用它。这样就导致了硬编码依赖。如果以后我们想使用其他更好的email服务进行发送email,我们不得不去修改MyApplication 的代码。这使得我们的应用难以扩展,如果emailService需要在更多的类中使用,可维护性则更差了。

如果我们需要扩展出其他的发送消息的方式如SMS、Facebook message等,迫使我们需要写一个其他的application,这需要服务端以及客户端都需要修改相关代码。

测试application将会变得很麻烦,因为我们的应用是直接创建emailService实例的。 我们根本无法在测试用例中MOCK出这个emailService对象。

一个较好的方案,我们可以不在MyApplication 中直接创建emailService实例,而是让那些需要使用该发送eamil服务的应用通过构造器的参数去设置emailService

package com.byron4j.hightLevel.java8.pattern.di;

/**

*

*

* 为那些需要使用email服务的应用提供专有的构造器

*

* @author Byron.Y.Y

*/

public class MyApplication4Constructor {

private EmailService email = null;

public MyApplication4Constructor(EmailService svc){

this.email=svc;

}

public void processMessages(String msg, String rec){

//做一些信息验证、操作逻辑等等

this.email.sendEmail(msg, rec);

}

}

尽管如此,我们还是得需要在客户端或者测试用例中去初始化emailService实例,显然这并不是我们所理想的。

现在,我们想想怎么利用Java DI依赖注入模式前面的问题……

1 服务组件需要设计成基类 or 接口( 实际中我们更多的是使用抽象类或者接口来规约服务规范 )

2 服务实现需要实现服务组件约定的服务规范

3 注入类Injector Class负责初始化服务以及服务实现

Java依赖注入—-Service组件

在这个设计中,我们使用 MessageService 来指定服务规范。

package com.byron4j.hightLevel.java8.pattern.di.pattern;

/**

*

*

* 该接口用于制定服务规范

*

* @author Byron.Y.Y

*/

public interface MessageService {

/**发送消息的服务*/

void sendMessage(String msg, String rec);

}

现在我们可以有Email和SMS 的两种发送消息的服务实现。

package com.byron4j.hightLevel.java8.pattern.di.pattern.impl;

import com.byron4j.hightLevel.java8.pattern.di.pattern.MessageService;

/**

*

*

* 发送email的服务实现,实现了消息服务的规范

*

* @author Byron.Y.Y

*/

public class EmailServiceImpl implements MessageService {

@Override

public void sendMessage(String msg, String rec) {

System.out.println("发送邮件给 "+rec+ " ,内容为:"+msg);

}

}

package com.byron4j.hightLevel.java8.pattern.di.pattern.impl;

import com.byron4j.hightLevel.java8.pattern.di.pattern.MessageService;

/**

*

*

* 发送短信的服务实现

*

* @author Byron.Y.Y

*/

public class SMSServiceImpl implements MessageService {

@Override

public void sendMessage(String msg, String rec) {

System.out.println("发送短信给 "+rec+ " ,内容为:"+msg);

}

}

我们的可用于依赖注入的服务实现已经开发完毕,接下来我们需要编写消费服务的类。

Java依赖注入—-服务调用者(消费者)

我们需要制定服务消费的规范Consumer 。

package com.byron4j.hightLevel.java8.pattern.di.pattern;

/**

*

* 制定消费服务的规范

*

* @author Byron.Y.Y

*/

public interface Consumer {

void processMessages(String msg, String rec);

}

以及消费的具体实现:

package com.byron4j.hightLevel.java8.pattern.di.pattern.impl;

import com.byron4j.hightLevel.java8.pattern.di.pattern.Consumer;

import com.byron4j.hightLevel.java8.pattern.di.pattern.MessageService;

/**

*

*

* 服务消费的具体实现----使用构造器注入的方式

*

* @author Byron.Y.Y

*/

public class MyDIApplication implements Consumer {

/**接口形式--多态--服务属性*/

MessageService messageService;

/**构造器注入服务属性*/

public MyDIApplication(MessageService messageService) {

super();

this.messageService = messageService;

}

@Override

public void processMessages(String msg, String rec) {

//发送消息,取决于具体的服务实现的行为

this.messageService.sendMessage(msg, rec);

}

}

请注意,上面我们仅仅是使用到了service,并没有初始化它,尽量达到“关注点分离”—– 对于我来说我仅仅是使用它这就是我能做且只能做的分内事,那么我不应该去生成它那不是我的职责范围另外,使用接口服务的形式,我们可以更好的测试应用,MOCK MessageService 并在运行时绑定service而不是在编译期。

现在,我们可以编写Java依赖注入类了——–用来初始化service、consumer

Java依赖注入—-注入类

我们编写一个MessageServiceInjector 接口,声明一个获得Consumer 的方法。

package com.byron4j.hightLevel.java8.pattern.di.pattern;

/**

*

*

* 依赖注入服务

*

* @author Byron.Y.Y

*/

public interface MessageServiceInjector {

/**获得消费类*/

public Consumer getConsumer();

}

现在为每一个服务,我们都可以创建其依赖注入类了:

package com.byron4j.hightLevel.java8.pattern.di.pattern.impl;

import com.byron4j.hightLevel.java8.pattern.di.pattern.Consumer;

import com.byron4j.hightLevel.java8.pattern.di.pattern.MessageServiceInjector;

/**

*

*

* email服务的依赖注入类

*

* @author Byron.Y.Y

*/

public class EmailServiceInjector implements MessageServiceInjector {

@Override

public Consumer getConsumer() {

return new MyDIApplication(new EmailServiceImpl());

}

}

package com.byron4j.hightLevel.java8.pattern.di.pattern.impl;

import com.byron4j.hightLevel.java8.pattern.di.pattern.Consumer;

import com.byron4j.hightLevel.java8.pattern.di.pattern.MessageServiceInjector;

/**

*

*

* SMS服务的依赖注入类

*

* @author Byron.Y.Y

*/

public class SMSServiceInjector implements MessageServiceInjector {

@Override

public Consumer getConsumer() {

return new MyDIApplication(new SMSServiceImpl());

}

}

现在,在客户端的简单实用实例如下:

package com.byron4j.hightLevel.java8.pattern.di.pattern.impl;

import com.byron4j.hightLevel.java8.pattern.di.pattern.Consumer;

import com.byron4j.hightLevel.java8.pattern.di.pattern.MessageServiceInjector;

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;

//Send email

injector = new EmailServiceInjector();

app = injector.getConsumer();

app.processMessages(msg, email);

//Send SMS

injector = new SMSServiceInjector();

app = injector.getConsumer();

app.processMessages(msg, phone);

}

}

运行结果如下:

发送邮件给 pankaj@abc.com ,内容为:Hi Pankaj 发送短信给 4088888888 ,内容为:Hi Pankaj

至此,你会发现我们的application仅仅负责使用service。 So,依赖注入解决硬编码问题,使我们的应用变得更加灵活易扩展了。

再来看看我们的测试如何更加容易MOCK了吧。

Java依赖注入—-单元测试MOCK注入服务

package com.byron4j.hightLevel.java8.pattern.di.pattern.impl;

import org.junit.After;

import org.junit.Before;

import org.junit.Test;

import com.byron4j.hightLevel.java8.pattern.di.pattern.Consumer;

import com.byron4j.hightLevel.java8.pattern.di.pattern.MessageService;

import com.byron4j.hightLevel.java8.pattern.di.pattern.MessageServiceInjector;

/**

*

*

* 单元测试类,MOCK服务,负责注入

*

* @author Byron.Y.Y

*/

public class MyDIApplicationJUnitTest {

/**注入类*/

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 testInjector(){

Consumer consumer = injector.getConsumer();

consumer.processMessages("Hi Pankaj", "pankaj@abc.com");

}

@After

public void clean(){

//回收

injector = null;

}

}

在上述测试类中,我们使用了匿名内部类来mock 注入器和服务,使得测试接口服务变得容易些。

我们是使用构造器来注入服务的、另外一种方式是在application类中使用setter方法来注入服务

package com.byron4j.hightLevel.java8.pattern.di.pattern.impl;

import com.byron4j.hightLevel.java8.pattern.di.pattern.Consumer;

import com.byron4j.hightLevel.java8.pattern.di.pattern.MessageService;

/**

*

*

* 使用setter注入

*

* @author Byron.Y.Y

*/

public class MyDIApplication4Setter implements Consumer{

private MessageService service;

public MyDIApplication4Setter(){}

//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.byron4j.hightLevel.java8.pattern.di.pattern.impl;

import com.byron4j.hightLevel.java8.pattern.di.pattern.Consumer;

import com.byron4j.hightLevel.java8.pattern.di.pattern.MessageServiceInjector;

/**

*

*

* 属性注入的注入器

*

* @author Byron.Y.Y

*/

public class EmailServiceInjector4Setter implements MessageServiceInjector{

@Override

public Consumer getConsumer() {

MyDIApplication4Setter app = new MyDIApplication4Setter();

app.setService( new EmailServiceImpl() );

return app;

}

}

具体采用构造器注入还是setter注入方式,取决于你的需求。假如我的应用不能离开服务类而运作那么会采用构造器注入,否则采用setter注入方式。

依赖注入总结

依赖注入( DI )的方式可以达到控制反转( IOC )的目的,将对象从绑定从编译器转移到运行时。我们也可以通过工厂模式、模板模式或者策略模式等方式达到控制反转 ( IOC )。

Spring依赖注入、Google Guice和Java EE CDI框架通过反射、注解技术使得依赖注入变得更简单。我们要做的仅仅是在属性、构造器或setter中添加某些注解。

Java依赖注入的好处

关注点分离

减少样板代码,因为所有服务的初始化都是通过我们的注入器组件来完成的

可配置化的组件使得应用更易于扩展

单元测试更易于MOCK对象

Java依赖注入的缺陷

滥用有可能难以维护,因为很多错误都从编译器转移到了运行时

依赖注入隐藏了服务类的依赖,可能导致运行时错误,而这之前是可能在编译器就能发现的