Java依赖注入

Java依赖注入设计原则允许我们移除硬编码依赖和让我们的应用低耦合,可扩展和可维护。我们可以通过在Java中实现依赖注入将依赖关系从编译时移到运行时来解析。

Java依赖注入似乎很难通过理论来掌握。所以我将通过一些简单的例子,然后我们将会看到如何在应用里使用依赖注入模式来实现低耦合和可扩展性。

一个不使用依赖注入实现的应用案例

假如说我们有一个通过调用EmailService类来发送邮件的应用。一般来说我们会向下面这样来实现它。

public class EmailService {
        public void sendEmail(String message, String receiver){
                //logic to send email
                System.out.println("Email sent to "+receiver+ " with Message="+message);
        }
}

EmailService类接受邮件地址实现发送邮件消息的逻辑。我们的MyApplication类如下。

public class MyApplication {

        private EmailService email = new EmailService();
        
        public void processMessages(String msg, String rec){
                //do some msg validation, manipulation logic etc
                this.email.sendEmail(msg, rec);
        }
}

我们的客户端通过使用MyApplication类来发送邮件消息。

public class MyLegacyTest {

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

}

咋一看,通过上面方式实现似乎没什么问题。但是上面代码逻辑有某些限制。

  • MyApplication类负责初始化邮件服务并且使用它。这会导致硬编码依赖。如果我们将来想切换其他一些高级的邮件服务,这需要在MyApplication类中修改代码。这会让我们的应用很难扩展并且如果我们的邮件服务在很多类中被用到这将会更加的难。
  • 如果我们希望在将来通过提供其他的一些消息通知方式来扩展我们的应用,比如短信或者微信那么我们将需要另外写一个应用。这在应用类和客户端也将涉及到修改代码。
  • 测试应用将会变得很难因为我们的应用是直接创建邮件服务实例。我们无法通过在测试类中模拟创建这些对象。

有人说我们可以通过构造器传入需要的邮件服务作为参数的方法来从MyApplication类中移除邮件服务实例的创建。

public class MyApplication {

        private EmailService email = null;
        
        public MyApplication(EmailService svc){
                this.email=svc;
        }
        
        public void processMessages(String msg, String rec){
                //do some msg validation, manipulation logic etc
                this.email.sendEmail(msg, rec);
        }
}

但是在这种情况,我们要求客户端应用或者测试类来初始化这些邮件服务,这不是一种好的设计决策。

现在让我们来看看怎么样通过实现Java依赖注入模式来解决通过上面方式实现出现的这些问题。依赖注入在Java中至少需要以下内容:

  1. 服务组件应当被设计成基类或者接口。最好是选择定义服务契约的接口或抽象类。
  2. 消费者类应该根据服务接口编写。
  3. 注入器类将初始化服务,然后是消费者类。

Java依赖注入-服务组件

在我们的例子中,我们可以通过MessageService接口来声明服务实现的契约。

public interface MessageService {

        void sendMessage(String msg, String rec);
}

现在让我们通过Email和SMS服务来实现上面接口。

public class EmailServiceImpl implements MessageService {

        @Override
        public void sendMessage(String msg, String rec) {
                //logic to send email
                System.out.println("Email sent to "+rec+ " with Message="+msg);
        }

}

public class SMSServiceImpl implements MessageService {

        @Override
        public void sendMessage(String msg, String rec) {
                //logic to send SMS
                System.out.println("SMS sent to "+rec+ " with Message="+msg);
        }

}

我们的依赖注入java服务写好了,现在我们可以写消费类。

Java依赖注入-服务消费

我们不需要为消费者类提供基本接口,但是我将拥有一个消费者接口为消费者类声明合约。

public interface Consumer {

        void processMessages(String msg, String rec);
}

消费者类实现如下。

public class MyDIApplication implements Consumer{

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

}

注意到我们的application类刚使用服务。它没有初始化服务导致更好的“关注点分类”。还有使用服务接口允许我们通过模拟MessageService并在运行时绑定服务而不是编译时来更加简单的测试应用。

现在我们将准备写java依赖注入器类来初始化服务和消费者类。

Java依赖注入-注入器类

让我们实现一个带有返回消费者类的方法声明的MessageServiceInjector接口。

public interface MessageServiceInjector {

        public Consumer getConsumer();
}

现在对于每一个服务,我们可以如下来创建注入器类。

public class EmailServiceInjector implements MessageServiceInjector {

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

}

public class SMSServiceInjector implements MessageServiceInjector {

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

}

现在让我们通过一个简单的程序来看客户端怎样使用application。

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

}

如你所见我们的application类只负责使用服务。服务类在注入器中被创建。 还有如果我们将来希望扩展我们的应用来支持发送微信消息。我们只需要再写一个服务类和注入器类。

因此依赖注入实现解决了硬编码带来的问题并且帮助我们使应用灵活和易扩展。现在让我们看看怎样通过模拟注入器和服务类来简单的测试我们的应用。

Java依赖注入-模拟注射器和服务的JUnit测试用例

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", "pankaj@abc.com");
        }
        
        @After
        public void tear(){
                injector = null;
        }

}

你可以看到我们使用匿名类来模拟注入器和服务类,然后我可以轻易地测试 application的方法。这里为以上的测试类使用了JUnit4,所以如果运行以上测试类,先确认在你项目的构建路径包含它。

在上面的这些application类中,我们使用构造器来注入依赖关系,另外的方式是使用setter方法。对于setter方法依赖注入的实现如下。

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

}

public class EmailServiceInjector implements MessageServiceInjector {

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

}

一个最好的setter依赖注入的例子是 Struts2 Servlet API Aware interfaces

到底是使用基于构造器依赖注入还是基于setter方法依赖注入取决于你的需求。举个例子,如果没有服务类我的应用完全不能运行,那么我会偏向基于构造器的DI,否则我会选择基于setter方法的DI,只有在真正需要才会使用它。

Java中的依赖注入是一种通过使对象从编译时绑定移到运行时绑定来实现控制反转(Inversion of control IoC)的一种方式。我们可以通过工厂模式(Factory Pattern), 模板方法设计模式(Template Method Design Pattern), 策略模式(Strategy Pattern)还有服务定位模式(Service Locator pattern)来实现IoC。

Spring依赖注入,Google Guice还有Java EE CDI框架通过使用Java Reflection API和Java注解来促进依赖注入的过程。我们只需要注解该域,构造器或者setter方法然后在配置xml文件或者配置类中配置它们。

Java依赖注入的好处

一些使用Java依赖注入的好处如下:

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

Java依赖注入的缺点

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

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

以上就是Java中的依赖注入模式。当我们控制服务时,了解和使用它是很好的。

原文链接:https://www.journaldev.com/2394/java-dependency-injection-design-pattern-example-tutorial