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("12345@gmail.com", "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); } }
-
Module中的依赖关系,最终将形成整个应用程序的依赖图
-
创建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("injector_guice@gmail.com", "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("sunrise@gmail.com", "Hello lucy!"); verify(mockService).sendEmail("sunrise@gmail.com", "Hello lucy!"); }
疑问:如果没有使用@Inject标识构造函数,能否成功注入依赖?
- 如果没有使用@Inject标识构造函数,Guice将默认使用public类型的无参构造函数
- 无参构造函数,自然不能通过构造函数的入参注入依赖
- 例如,上面的代码去除@Inject注解,最终将使用第一个构造创建EmailClient对象。此时,EmailService不再是通过依赖注入的,而是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
-
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成功注入了QQEmailService依赖
-
同样的,Field Injection也破坏了成员变量的不可变性
2.4 其他的依赖注入方式
- Guice官方文档Injections部分,还介绍了很多其他的依赖注入方式,可以自行学习
- 从笔者对开源代码的有限阅读来看,基本都是使用
Constructor Injection
实现依赖注入
3. 总结
- 笔者喜欢按照自己的思维习惯进行学习总结,因此会与Guice官方文档的组织顺序不一致
- 将依赖注入作为第一篇学习总结,是因为后面很多的知识都将涉及依赖注入