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

2023-05-04 20:52:39 浏览数 (2)

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依赖注入的缺陷
  • 滥用有可能难以维护,因为很多错误都从编译器转移到了运行时
  • 依赖注入隐藏了服务类的依赖,可能导致运行时错误,而这之前是可能在编译器就能发现的

0 人点赞