Java依赖注入模式允许我们摆脱硬编码,使我们的应用更加松耦合、增强扩展性以及可维护性。通过依赖注入我们可以降低从编译到运行时的依赖性。
Java依赖注入
Java的依赖注入仅仅通过理论是很难解明白的,所以我们通过几个简单的示例来描述它,怎样利用依赖注入模式降低我们应用之间的耦合性和增强可扩展性。
假设我们的应用需要通过 EmailService 去发送email,通常情况下,我们是这样实现的:
代码语言:javascript复制package com.byron4j.hightLevel.java8.pattern.di;
/**
*
* <pre>
* 通常用于发送emai的服务
* </pre>
* @author Byron.Y.Y
*/
public class EmailService {
/*
* 发送email的方法
*/
public void sendEmail(String message, String receiver){
//发送email的业务逻辑
System.out.println("发送消息:" message "给接收者:" receiver);
}
}
EmailService 类是提供发送email的服务类,在我们的应用中,可能会这样使用发送email的服务:
代码语言:javascript复制package com.byron4j.hightLevel.java8.pattern.di;
/**
*
* <pre>
* 我们的应用,利用EmailService来发送email
* </pre>
* @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:
代码语言:javascript复制package com.byron4j.hightLevel.java8.pattern.di;
/**
*
* <pre>
* 调用应用提供的处理eamil的服务
* </pre>
* @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
代码语言:javascript复制package com.byron4j.hightLevel.java8.pattern.di;
/**
*
* <pre>
* 为那些需要使用email服务的应用提供专有的构造器
* </pre>
* @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 来指定服务规范。
代码语言:javascript复制package com.byron4j.hightLevel.java8.pattern.di.pattern;
/**
*
* <pre>
* 该接口用于制定服务规范
* </pre>
* @author Byron.Y.Y
*/
public interface MessageService {
/**发送消息的服务*/
void sendMessage(String msg, String rec);
}
现在我们可以有Email和SMS 的两种发送消息的服务实现。
代码语言:javascript复制package com.byron4j.hightLevel.java8.pattern.di.pattern.impl;
import com.byron4j.hightLevel.java8.pattern.di.pattern.MessageService;
/**
*
* <pre>
* 发送email的服务实现,实现了消息服务的规范
* </pre>
* @author Byron.Y.Y
*/
public class EmailServiceImpl implements MessageService {
@Override
public void sendMessage(String msg, String rec) {
System.out.println("发送邮件给 " rec " ,内容为:" msg);
}
}
代码语言:javascript复制package com.byron4j.hightLevel.java8.pattern.di.pattern.impl;
import com.byron4j.hightLevel.java8.pattern.di.pattern.MessageService;
/**
*
* <pre>
* 发送短信的服务实现
* </pre>
* @author Byron.Y.Y
*/
public class SMSServiceImpl implements MessageService {
@Override
public void sendMessage(String msg, String rec) {
System.out.println("发送短信给 " rec " ,内容为:" msg);
}
}
我们的可用于依赖注入的服务实现已经开发完毕,接下来我们需要编写消费服务的类。
Java依赖注入—-服务调用者(消费者)
我们需要制定服务消费的规范Consumer 。
代码语言:javascript复制package com.byron4j.hightLevel.java8.pattern.di.pattern;
/**
* <pre>
* 制定消费服务的规范
* </pre>
* @author Byron.Y.Y
*/
public interface Consumer {
void processMessages(String msg, String rec);
}
以及消费的具体实现:
代码语言:javascript复制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;
/**
*
* <pre>
* 服务消费的具体实现----使用构造器注入的方式
* </pre>
* @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 的方法。
代码语言:javascript复制package com.byron4j.hightLevel.java8.pattern.di.pattern;
/**
*
* <pre>
* 依赖注入服务
* </pre>
* @author Byron.Y.Y
*/
public interface MessageServiceInjector {
/**获得消费类*/
public Consumer getConsumer();
}
现在为每一个服务,我们都可以创建其依赖注入类了:
代码语言:javascript复制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;
/**
*
* <pre>
* email服务的依赖注入类
* </pre>
* @author Byron.Y.Y
*/
public class EmailServiceInjector implements MessageServiceInjector {
@Override
public Consumer getConsumer() {
return new MyDIApplication(new EmailServiceImpl());
}
}
代码语言:javascript复制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;
/**
*
* <pre>
* SMS服务的依赖注入类
* </pre>
* @author Byron.Y.Y
*/
public class SMSServiceInjector implements MessageServiceInjector {
@Override
public Consumer getConsumer() {
return new MyDIApplication(new SMSServiceImpl());
}
}
现在,在客户端的简单实用实例如下:
代码语言:javascript复制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注入服务
代码语言:javascript复制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;
/**
*
* <pre>
* 单元测试类,MOCK服务,负责注入
* </pre>
* @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方法来注入服务
代码语言:javascript复制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;
/**
*
* <pre>
* 使用setter注入
* </pre>
* @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);
}
}
代码语言:javascript复制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;
/**
*
* <pre>
* 属性注入的注入器
* </pre>
* @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依赖注入的缺陷
- 滥用有可能难以维护,因为很多错误都从编译器转移到了运行时
- 依赖注入隐藏了服务类的依赖,可能导致运行时错误,而这之前是可能在编译器就能发现的