Google Guice 1:如何实现依赖注入

1. 待完善的邮箱程序

1.1 手动注入依赖

  • 前一篇博文《谈谈自己对依赖注入的理解》,笔者只是基于依赖注入的思想,为EmailClient预留了依赖注入的入口

    到目前为止,我们只是让dependent class预留了依赖注入的入口,要想实现依赖的自动注入,还需要依赖注入框架的辅助

  • 如果不借助依赖注入框架,依赖的注入只能依靠程序员手动实现

    public class Main {
        public static void main(String[] args) {
            EmailService emailService = new GoogleEmailService();
            EmailClient emailClient = new EmailClient(emailService);
    
            // 发送邮件
            emailClient.sendEmail("[email protected]", "Hello lucy !");
        }
    }
    

1.2 通过Google Guice注入依赖

  • 使用Google Guice,一个轻量级的Java依赖注入框架,可以实现依赖的自动注入

    Guice is a lightweight dependency injection (DI) framework for Java.

  • 声明依赖注入:需要为EmailClient的构造函数添加@Inject注解,表明这是一个需要依赖注入的构造函数

    public class EmailClient  {
        private EmailService service;
    
        @Inject
        public EmailClient(EmailService service) {
            this.service = service;
        }
        // 其他方法省略
    }
    
  • 定义依赖关系:EmailService只是一个接口,EmailClient实际依赖EmailService的哪个具体实现,需要在Module中进行定义

    public class EmailModule extends AbstractModule {
    
        @Override
        protected void configure() {
            // QQEmailService绑定为EmailService的实现
            bind(EmailService.class).to(QQEmailService.class);
        }
    }
    
  • 创建Injector,获取实例:基于自定义的Module创建Injector,Injector内部包含整个应用程序的依赖关系。

  • 当我们从Injector请求一个实例时,Injector将找出要创建的对象,以及创建这个对象所需的依赖,然后返回一个 “完整” 的对象

    public class Main {
        public static void main(String[] args) {
            // 将Module传递给Injector,让其了解依赖图
            Injector injector = Guice.createInjector(new EmailModule());
            // 向Injector请求一个现成的EmailClient
            EmailClient emailClient = injector.getInstance(EmailClient.class);
            // 使用EmailClient
            emailClient.sendEmail("[email protected]", "Hello lucy !");
        }
    }
    
  • 执行结果如下,EmailClient使用的是EmailModule中绑定的QQEmailService

2. Guice的依赖注入方式

2.1 Constructor Injection

  • 上面代码示例,就是Constructor Injection,它实现了依赖注入和类实例化的整合,是Guice 最推荐 的一种依赖注入方式

重点解读

依赖注入和类实例化的整合

  • 使用@Inject标识构造函数,告诉Injector这既是是依赖注入的入口,还是创建实例对象的入口

  • 如果该类还存在其他构造函数,Injector只会调用@Inject标识的构造函数来创建实例对象

    public class EmailClient  {
        private final EmailService service;
    
        public EmailClient() {
            System.out.println("EmailClient: no-args constructor");
            this.service = new GoogleEmailService(); // 默认使用GoogleEmailService
        }
    
        @Inject
        public EmailClient(EmailService service) {
            System.out.printf("EmailClient: constructor with %s\n", service.getClass().getSimpleName());
            this.service = service;
        }
        // 省略其他代码
    }
    
  • 最终,执行结果如下

Guice最推荐的一种依赖注入方式

  • 因为使用这种方式,方便进行单元测试,具体可以查看Guice官网:BINDING_ALREADY_SET

    @Test
    public void sendEmailTest() {
        // mock出EmailService并注入factory
        EmailService mockService = mock(EmailService.class);
        // 创建client,通过构造函数直接注入mock出来的EmailService
        EmailClient emailClient = new EmailClient(mockService);
        // 调用emailClient.sendEmail()方法,将调用mock的sendEmail()方法
        emailClient.sendEmail("[email protected]", "Hello lucy!");
        verify(mockService).sendEmail("[email protected]", "Hello lucy!");
    }
    

疑问:如果没有使用@Inject标识构造函数,能否成功注入依赖?

  • 如果没有使用@Inject标识构造函数,Guice将默认使用public类型的无参构造函数

  • QQEmailService内含一个默认的无参构造函数,因此Guice在识别到如下绑定关系时,成功创建一个QQEmailService的实例并注入到EmailClient中

    bind(EmailService.class).to(QQEmailService.class);
    
  • 上面的EmailClient也是如下,去除@Inject注解,将使用第一个构造创建EmailClient对象。

  • 如果某个类没有public类型的无参构造函数,则Guice无法为其创建实例对象,程序执行将报错

2.2 Method Injection

  • 还可以使用@Inject注解标记setter方法,实现依赖的自动注入。这样的依赖注入方式,叫做Method Injection

    public class EmailClient {
        private EmailService service;// 不能再使用final进行标识
    
        public EmailClient() {
            System.out.println("EmailClient: no-args constructor");
        }
    
        @Inject
        public void setService(EmailService emailService) {
            System.out.println("EmailClient: method injection");
            this.service = emailService;
        }
        // 其他代码省略
    }
    
  • 最终,执行结果如下,可知:Guice在调用 无参构造函数 创建完实例对象后,还通过 setter方法 自动注入了EmailService
    Google Guice 1:如何实现依赖注入_第1张图片

  • 缺点: Method Injection破坏了成员变量的稳定性

    • 要想通过Method Injection为类的成员变量赋予新实例,则不能再使用final对成员变量进行修饰
    • 而大多数开源项目中,成员变量都是使用final进行修饰,以保证其不可变性
    • 因此,很多开源项目都是通过Constructor Injection实现依赖注入

2.3 Field Injection

  • 同时,如果使用Method Injection实现依赖注入,对于一个大量依赖其他类的dependent class来说,则需要定义大量的setter方法,这是十分不友好的

  • 这时,可以使用Field Injection:在定义成员变量时使用@Inject注解

    public class EmailClient {
        @Inject
        private EmailService service;// 不能再使用final进行标识
    
        public EmailClient() {
            System.out.println("EmailClient: no-args constructor");
        }
        // 其他代码省略
    }
    // 同时,在QQEmailService中增加如下构造函数
    public QQEmailService() {
        System.out.println("This is QQEmailService ...");
    }
    
  • 最终,Guice创建EmailClient实例的动作如下:

    • 使用无参构造函数创建EmailClient实例 → \rightarrow 发现EmailClient存在Field Injection
    • 该Field Injection是EmailClient对EmailService的依赖 → \rightarrow EmailService是一个接口,为其绑定的实现类是QQEmailService
    • 使用无参构造函数创建QQEmailService实例 → \rightarrow 将QQEmailService实例注入到EmailClient实例
      Google Guice 1:如何实现依赖注入_第2张图片
  • 同样的,Field Injection也破坏了成员变量的不可变性

2.4 其他的依赖注入方式

  • Guice官方文档Injections部分,还介绍了很多其他的依赖注入方式,可以自行学习
  • 从笔者对开源代码的有限阅读来看,基本都是使用Constructor Injection实现依赖注入

3. 总结

  • 笔者喜欢按照自己的思维习惯进行学习总结,因此会与Guice官方文档的组织顺序不一致
  • 将依赖注入作为第一篇学习总结,是因为后面很多的知识都将涉及依赖注入

你可能感兴趣的:(java相关,java)