依赖注入神器:Dagger2详解系列
序言
Dagger2是啥
Dagger2是啥,Google告诉我们:
Dagger is a fully static, compile-time dependency injection framework for both Java and Android. It is an adaptation of an earlier versioncreated by Square and now maintained by Google.
Dagger aims to address many of the development and performance issues that have plagued reflection-based solutions. More details can be found in this talk(slides) by +Gregory Kick.
那么长一段话,看的我是泪流满面:
Dagger是为Android和Java平台提供的一个完全静态的,在编译时进行依赖注入的框架,原来是由Square公司维护的然后现在把这堆东西扔给Google维护了。Dagger解决了基于反射带来的开发和性能上的问题(因为Dagger并没有用反射来做依赖注入)blabla。。。。说了那么多,其实就是告诉我们这家伙可以用来搞依赖注入哦
依赖注入,搞过Spring的人肯定都知道这是啥,SpringMVC里用到了大量依赖注入,鉴于gay们都是做Android,让我们共饮此杯,聊一聊依赖注入。
依赖注入
我们在做项目时,经常需要在一个对象里去创建另一个对象的示例,这种行为是产生耦合的常见形式,对于一个大型项目来说,过多的相互依赖会导致代码难以维护,很容易就会碰到修改一个小需求需要大面积的修改各种代码,特别是代码原来不是自己维护的,撸着代码的你就开始问候别人家的亲友了。。。。
举个栗子
我们现在有家Coffee Shop
这个是我们的业务核心咖啡机CoffeeMachine
/**
* 这是一个制作Coffee的例子
* CoffeeMaker是对制作Coffee过程的一个封装
* 制作Coffee需要实现CoffeeMarker的makeCoffee方法
*/
public class CoffeeMachine {
private CoffeeMaker maker;
public CoffeeMachine(){
maker = new SimpleMaker();
}
public String makeCoffee(){
return maker.makeCoffee();
}
}
CoffeeMaker
public interface CoffeeMaker {
String makeCoffee();
}
SimpleCoffeeMaker
//这个是自动咖啡机
public class SimpleMaker implements CoffeeMaker {
@Override
public String makeCoffee() {
return "Coffee is made by SimperMarker";
}
}
小店新开张,我们的咖啡都是咖啡机做出来的,物美价廉啦。。。。
这家CoffeeShop很简单,在CoffeeMachine中可以看到,CoffeeMachine持有了一个CoffeeMaker接口,而具体制作Coffee的过程是由实现了CoffeeMaker的自动咖啡机SimpleMaker实现的,CoffeeMaker是在构造方法中new 出了一个实现CoffeeMaker接口的SimpleCoffee。当前的功能很简单,这么写看着也没什么问题。
随着业务的扩展,消费人群改变了,自动咖啡机也完全不能满足现有客户的需求,这个时候我们的CoffeeShop该进行业务升级咯 。
经过董事会决定,公司决定投入大笔资金,雇佣咖啡师来制作咖啡。
我们的咖啡师:
public class Cooker {
String name; //咖啡师名字
String coffeeKind; //制作咖啡的类型
public Cooker(String name,String coffeeKind){
this.name = name;
this.coffeeKind = coffeeKind;
}
public String make(){
return name +" make " + coffeeKind; //咖啡师制作Coffee的过程
}
}
这时候SimpleMarker升级了 :
public class SimpleMaker implements CoffeeMaker {
Cooker cooker; //现在需要咖啡师来制作咖啡了
public SimpleMaker(Cooker cooker){
this.cooker = cooker;
}
@Override
public String makeCoffee() {
return cooker.make();
}
}
基于目前的情况,我们制作咖啡的流程发生了变化,原来的业务随着Cooker的加入发生了改变,但细心的小伙伴会发现目前还有一个地方受到了影响,那就是我们的CoffeeMachie,看这段代码:
private CoffeeMaker maker;
//构造方法
public CoffeeMachine(Cooker cooker){
maker = new SimpleMaker(cooker);
}
我们的SimpleMake升级了,业务波动影响到了我们的CoffeeMachine,这时候不得不对CoffeeMachine也进行修改:
public class CoffeeMachine {
private CoffeeMaker maker;
public CoffeeMachine(Cooker cooker){
maker = new SimpleMaker(cooker);
}
public String makeCoffee(){
return maker.makeCoffee();
}
}
这时候我们的CoffeeMachine就懵逼了,你的业务升级就升级呗,为毛我制造CoffeeMachine的过程也要变动,非常的不愿意。。但迫于老板的压力。最后还是给整改了。很明显,这是一个不合适的流程,
简单的一个业务的升级,还要找我们的机器制造厂来帮你修改,那如果业务非常复杂,引用了SimpleMaker的可不仅仅是CoffeeMachine一个,那是不是每个引用的地方都需要进行修改,业务庞大的情况下,
这种修改就是致命的,不仅需要做大量没有意义的体力劳动来修改,还可能导致大片业务代码的变动直接增加测试的成本,其他接收这个需求的开发GG直接得跪键盘了,一个SimpleMakere的
改动对CoffeeMachine产生了直接的影响,肯定有什么地方是不对的。原因就是CoffeeMachine里的CoffeeMaker是自己new出来的。这就是一个很不好的地方。
这种糟糕的实例引用的方式我们称之为硬初始化(Hard init),和硬编码(Hard coding)一样,都是糟糕代码滋生的好方法,Hard init不仅增加了各个模块的耦合,还让单元测试变得更加困难了。
那么该用什么方法来尽量地降低各个模块的耦合,避免new对象带来的问题呢。我们知道,类的初始化可以描述成new,get和set,new就是我们上面说的Hard init,容易增加各个模块之间的耦合,而
get,则可以看做是工厂模式,工厂模式是new的一个升级版本,相对硬初始化来说,工厂模式把对象的创建都集中在工厂里了,对于需要依赖的类来说,无需再考虑对象的 创建工作了,只需要关注
如何从工厂里获得,在发生修改时也不会有太多的改动,和以前的方案比起来要好了不少。但工厂模式的对象创建依然非常的不灵活,对象的实现完全取决于工厂,会导致原来的依赖由具体的对象变
为相应的工厂,本质上还是有依赖关系的!!!对,工厂模式并没有改变本质的依赖关系,而且,对于简单职责的工厂来说,抽出一层工厂似乎并不会太麻烦,但当我们的工厂中提供的类的实现复杂
起来时,又回到了最初的问题上,我们是在工厂中new对象还是继续给工厂中的内容再提供一个工厂呢,这种层层嵌套会让我们的代码变得干涩难懂,也会有设计过度的嫌疑。。。说了半天,工厂模
式在解决依赖问题还是有点尴尬呀。那么最后还有一个set,也就是我们的依赖注入了,依赖注入的依赖是从外部传递过来的,而且在Java平台上很多时候都是通过反射或者动态编译来提供依赖注入,
这样就更加剥离的各个部分的耦合性,也让上述两种方式只能望其项背了。
先将依赖注入的最基本的原理说明白,依赖注入主要有三种途径,eg
public class CoffeeMachinWithInjection implements InjectMaker{
private CoffeeMaker maker;
/*依赖注入的3种常见形式
*No.1 构造函数注入
*/
public CoffeeMachinWithInjection(CoffeeMaker maker){
this.maker = maker;
}
//No.2 Setter注入
public void setMaker(CoffeeMaker maker){
this.maker = maker;
}
// //No.3 接口注入
@Override
public void injectMaker(CoffeeMaker maker) {
this.maker = maker;
}
public String makeCoffee(){
return maker.makeCoffee();
}
}
其中,InjectMarker接口内容如下:
public interface InjectMaker {
void injectMaker(CoffeeMaker maker);
}
是不是很简单,说白了就是不要在需要依赖的类中通过new来创建依赖而是通过方法提供的参数注入进来,这样我们的需要依赖的类和提供依赖的类的实现方法分隔开了,一切又变得如此美好咯。
不过这种手动提供依赖也是很繁杂的工作,充满的浓浓的重复体力劳动的气息,如何来尽量减少这些冗余代码的制作呢,答案就是Dagger!
第一部分到此结束。
Dagger2的使用
上一篇我们简单介绍了依赖注入是什么,并用简单的栗子说明了依赖注入的使用,本篇主要来分享Dagger2的简单使用,为运用到项目中做进一步的铺垫。
引入Dagger2
首先,我们需要将Dagger2的依赖写入我们的gradle中,具体配置如下:
apply plugin: 'com.neenbedankt.android-apt'
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.4'
}
}
dependencies {
apt 'com.google.dagger:dagger-compiler:2.0'
compile 'com.google.dagger:dagger:2.0'
}
把 apply plugin和dependencies放在app的gradle里,把buildscript放在项目的gradle里即可,之后我们就可以开始Dagger之旅了。
注解的使用
看过Dagger2的人应该都知道,Dagger2是通过注解来实现依赖注入的,所以,在使用Dagger2之前,我们需要了解这些注解的含义,如果对注解是什么还不清楚的同学可以Google一下,在这就不细说了。Dagger2中主要有6种注解,我们把它拆为4+2,前四种通俗易懂,后两种理解起来就有一定难度了。
四个基础
这里说的四个基础,指的是四种基础的注解,他们分别是:
- @Inject Inject主要有两个作用,一个是使用在构造函数上,通过标记构造函数让Dagger2来使用(Dagger2通过Inject标记可以在需要这个类实例的时候来找到这个构造函数并把相关实例new出来)从而提供依赖,另一个作用就是标记在需要依赖的变量让Dagger2为其提供依赖。
@Provide 用Provide来标注一个方法,该方法可以在需要提供依赖时被调用,从而把预先提供好的对象当做依赖给标注了@Injection的变量赋值。provide主要用于标注Module里的方法
@Module 用Module标注的类是专门用来提供依赖的。有的人可能有些疑惑,看了上面的@Inject,需要在构造函数上标记才能提供依赖,那么如果我们需要提供的类构造函数无法修改怎么办,比如一些jar包里的类,我们无法修改源码。这时候就需要使用Module了。Module可以给不能修改源码的类提供依赖,当然,能用Inject标注的通过Module也可以提供依赖
@Component Component一般用来标注接口,被标注了Component的接口在编译时会产生相应的类的实例来作为提供依赖方和需要依赖方之间的桥梁,把相关依赖注入到其中。
这些标注看起来可能比较抽像,为了方便各位理解,送图一张来说明这些标注的作用和之间的关系:
图片主要分为三部分,左边的是依赖提供者,比如我们用Module标注的类或者用Injection标注的构造函数,右边的是依赖的需求方,例如我们用inject标注的变量,而Component则是连接两者的桥梁,Component从依赖提供者提供依赖,并把这些依赖注入相关的类中,Dagge正如其名,就像把匕首让依赖能够非常犀利的注入到需要它的地方。
说了那么多前言,虽然这些注解都有各自独特的作用,单用起来其实很简单,接下来我们将进一步地讲解这些标注的作用,just show you code。在使用之前,只要大致明白这些标注的意义就行了,简单的依赖注入通过这几个标注就能完成。
简单的栗子
还是从我们的CoffeeShop说起,现在,我们有一个Activity:
public class OriginActivity extends Activity implements View.OnClickListener{
@BindView(R.id.btnMakeCoffee)
Button btnMakeCoffee;
@BindView(R.id.tvCoffee)
TextView tvCoffee;
CoffeeMachine coffeeMachine;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_simple);
ButterKnife.bind(this);
Cooker cooker = new Cooker("James","Espresso");
coffeeMachine = new CoffeeMachine(cooker);
initView();
}
private void initView(){
btnMakeCoffee.setOnClickListener(this);
}
private String makeCoffee(){
return coffeeMachine.makeCoffee();
}
@Override
public void onClick(View v) {
if(R.id.btnMakeCoffee == v.getId()){
tvCoffee.setText(makeCoffee());
}
}
}
可以看到,我们的CoffeeMachine还是通过在在onCreate中new出来的,这就是上一篇中说的不好的味道,我们试着用Dagger2来注入这些依赖。
需要注入依赖的是: CoffeeMachine coffeeMachine,怎么让Dagger2知道这个东西需要注入依赖呢,很简单,
@Inject
CoffeeMachine coffeeMachine;
是不是和上面ButterKnif给View注入的很相似,就是酱紫。这就是我们的依赖需求方。我们还需要依赖提供方
上面提到过,提供依赖可以给相关依赖类的构造函数添加@Inject注解,这样Dagger2就能找到它并用来提供依赖,所以我们看CoffeeMachie这个类
public class CoffeeMachine {
private CoffeeMaker maker;
public CoffeeMachine(Cooker cooker){
maker = new SimpleMaker(cooker);
}
public String makeCoffee(){
return maker.makeCoffee();
}
}
这个时候我们需要给他的构造函数增加一个@Inject
@Inject
public CoffeeMachine(Cooker cooker){
maker = new SimpleMaker(cooker);
}
不过看起来还是不太对,为啥,因为我们在它的构造函数里又new了一个对象,这样又是不好的味道,这个maker应该被注入进去。
我们再让Dagger2来提供CoffeeMaker的依赖,还是像刚才一样,我们去修改SimpleMaker的构造函数,不过在此之前,需要先修改一下CoffeeMachine的构造函数
public class CoffeeMachine {
private CoffeeMaker maker;
public CoffeeMachine(CoffeeMaker maker){
this.marker = maker
}
public String makeCoffee(){
return maker.makeCoffee();
}
}
然后我们再修改SimpleMaker
public class SimpleMaker implements CoffeeMaker {
Cooker cooker;
@Inject
public SimpleMaker(Cooker cooker){
this.cooker = cooker;
}
@Override
public String makeCoffee() {
return cooker.make();
}
}
这次变聪明了,SimpleMaker的构造函数中Cooker的依赖也是注入进来的,这个时候我们还需要提供Cooker的依赖
but,先打住一下,我们前面说了,除了构造函数提供依赖,还能用Module提供,所以,机智的我们这次用Module来提供依赖。
我们需要创建一个Module:
@Module
public class SimpleModule {
@Provides
Cooker provideCooker(){
return new Cooker("James","Espresso");
}
}
Module的写法是不是很简单,只要提供一个Module类,给类打上@Module的注解,然后再添加一个提供Cooker依赖的provideCooker方法,返回一个new Cooker(“James”,”Espresso”)即可,当然provideCooker需要添加@Provide注解这样Dagger2才能在需要Cooker的时候找到它来提供毅力啊,这里有个小细节,
我们需要提供Cooker的依赖,返回类型是Cooker就行了,方法名叫什么都行,但最好是以provide开头,这样能增加代码的可读性。
提供依赖的东西都写完了,最后需要一个把依赖注入到需要依赖的地方,这个工具就是Component
@Component(modules = SimpleModule.class)
public interface SimpleComponent {
void inject(SimpleActivity simpleActivity);
}
用@Component注解这个接口能让Dagger2在需要注入时找到这个工具,同时还告诉Dagger2提供依赖的是SimpleModule这个类
当然,如果需要用这个Module给SimpleActivity注入我们的CoffeeMachine,还需要一个inject方法,里面传入我们的SimpleActivity对象的实例。
这里需要注意,我们必须传入SimpleActivity自己,不能把SimpleActivity的父类定义到方法里作为参数来接收SimpleActivity,从语法上来说是正确的,但你会发现这样做没办法完成注入,这是Dagger2的一个坑,别给踩着了。
注入器也制作完了,就剩下最后一步:“注入”。
这步也很简单,只需要在我们的onCreate里这么写:
private SimpleComponent simpleComponent;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
simpleComponent = DaggerSimpleComponent.builder().simpleModule(getModule()).build();
simpleComponent.inject(this);
}
这里有个地方需要注意一下,DaggerSimpleComponent是什么,我们压根就没写这个东西。对,这就是Dagger2在编译时生成的类,Dagger2就是通过这些编译生成的东西完成了依赖的注入,那么Dagger2在编译时产生的类是怎么工作的呢,我们将在下一篇来叙述这个问题。
这里再说明一个问题,我们有两种方式可以提供依赖,一个是注解了@Inject的构造方法,一个是在Module里提供的依赖,那么Dagger2是怎么选择依赖提供的呢,规则是这样的:
- 步骤1:查找Module中是否存在创建该类的方法。
- 步骤2:若存在创建类方法,查看该方法是否存在参数
- 步骤2.1:若存在参数,则按从步骤1开始依次初始化每个参数
- 步骤2.2:若不存在参数,则直接初始化该类实例,一次依赖注入到此结束
- 步骤3:若不存在创建类方法,则查找Inject注解的构造函数,看构造函数是否存在参数
- 步骤3.1:若存在参数,则从步骤1开始依次初始化每个参数
- 步骤3.2:若不存在参数,则直接初始化该类实例,一次依赖注入到此结束
概括一下就是从注解了@Inject的对象开始,从Module和注解过的构造方法中获得实例,若在获取该实例的过程中需要其他类的实例,则继续获取被需要类的实例对象的依赖,同样是从Module和标注过的构造方法中获取,并不断递归这个过程直到所有被需要的类的实例创建完成,在这个过程中Module的优先级高于注解过的构造方法。明白了这个原理之后老奶奶都会用这几个标签了。
@Scope和@Qulifier
上面把四个简单的注解的用法都讲完了,但很多时候这几个注解并不能涵盖我们所有的场景,这时就需要@Scope和@Qulifier来帮忙了。
有的同学可能在用Module的时候会有疑惑,为什么方法怎么命名都行,那时怎么区分它为谁提供依赖呢。答案是根据返回类型来确定的,当某个对象需要注入依赖时,Dagger2就会根据Module中标记了@Provide的方法的返回值来确定由谁为这个变量提供实例。那问题来了,如果有两个一样的返回类型,该用谁呢。我们把这种场景叫做依赖迷失,见名知意,Dagger这时候就不知道用谁来提供依赖,自然就迷失了。所以我们引入了@Qulifier这个东西,通过自定义Qulifier,可以告诉Dagger2去需找具体的依赖提供者。
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface A {}
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface B {}
我们定义了两个注解,@A和@B,他们都是用@Qulifiier标注的
再看看我们的Module
@Module
public class SimpleModule {
@Provides
@A
Cooker provideCookerA(){
return new Cooker("James","Espresso");
}
@Provides
@B
Cooker provideCookerB(){
return new Cooker("Karry","Machiato");
}
}
再看看具体的使用
public class ComplexMaker implements CoffeeMaker {
Cooker cookerA;
Cooker cookerB;
@Inject
public ComplexMaker(@A Cooker cookerA,@B Cooker cookerB){
this.cookerA = cookerA;
this.cookerB = cookerB;
}
@Override
public String makeCoffee() {
return cooker.make();
}
}
cookerA.make();//James make Espresso
cookerB.make();//Karry make Machiato
这样说是不是很简单,相信大家很快就能理解@Qulifier的作用和用法
接着说@Scope,@Scope就要难理解点了,继续来看栗子,我们定义一个Scope注解
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface PerActivity {}
我们把定义的@PerActivity用到Module里
““
@Module
public class ActivityModule {
@Provides
CoffeeShop provideCoffeeShop(){
return CoffeeShop.getInstance();
}
@Provides
@PerActivity
CookerFactory provideCookerFactory(){
return new CookerFactory();
}
@Provides
CookerFactoryMulty provideCookerFactoryMulty(){
return new CookerFactoryMulty();
}
}
““
这个Module提供了CoffeeShop,CookerFactory和CookerFacotryMulty的依赖
public class CoffeeShop {
private static CoffeeShop INSTANCE;
private CoffeeShop(){
Log.d("TAG","CoffeeShop New Instance");
}
public static CoffeeShop getInstance(){
if(INSTANCE == null){
INSTANCE = new CoffeeShop();
}
return INSTANCE;
}
}
public class CookerFactory {
public CookerFactory(){
Log.d("TAG","CookerFactory New Instance");
}
}
public class CookerFactoryMulty {
public CookerFactoryMulty(){
Log.d("TAG","CookerFactoryMulty New Instance");
}
}
我们在这三个对象的构造方法里都加了Log,当他们的实例产生时能看到相关的Log,再看我们用到的地方,在MainActivity里给每个类都写两个变量,
public class MainActivity extends Activity {
ActivityComponent activityComponent;
@Inject
CoffeeShop coffeeShop1;
@Inject
CoffeeShop coffeeShop2;
@Inject
CookerFactory cookerFactory1;
@Inject
CookerFactory cookerFactory2;
@Inject
CookerFactoryMulty cookerFactoryMulty1;
@Inject
CookerFactoryMulty cookerFactoryMulty2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
activityComponent = DaggerActivityComponent.builder()
.activityModule(provideModule())
.applicationComponent(MyApplication.getComponent()).build();
activityComponent.inject(this);
coffeeFactory.run();
}
private ActivityModule provideModule(){
return new ActivityModule();
}
}
ok,来看下运行的结果把
07-11 16:53:27.978 1927-1927/? D/TAG﹕ CoffeeShop New Instance
07-11 16:53:27.978 1927-1927/? D/TAG﹕ CookerFactory New Instance
07-11 16:53:27.978 1927-1927/? D/TAG﹕ CookerFactoryMulty New Instance
07-11 16:53:27.978 1927-1927/? D/TAG﹕ CookerFactoryMulty New Instance
从Log中可以看到,CoffeeShop和CookerFactory的类都只new过一次,而CookerFactoryMulty被new了两次
再回头看我们的Module,其中CoffeeShop的依赖是通过单例模式提供的,只打一条Log很容易理解,而CookerFactory相对于CookerFactoryMulty来说内容几乎是一模一样,只多加一个@PerActivity的注解,但却比它少打了一次Log,这是为什么呢。哈哈,客官们,这就是@Scope神秘的地方,他通过自定义@Scope注解提供了单例,正如上面的CookerFactory,虽然并未用单例来提供依赖,但却和用单例提供依赖的CoffeeShop一样,两个对象的实例都是同一个,这就是Scope的作用,提供局部单例的功能,局部范围是啥,那就是它生命周期范围内。
OK,上面所有注解的作用和用法都说完了,再回头看看是不是超级简单,这些东西并没有想象中的那么难懂,只要结合上面的栗子,我相信大家都能明白了。其实dagger2正真的难点并不是理解这些注解的作用和用法,而是如何在我们项目中来灵活地运用它。要做到这点,光靠这两篇内容是不够的,我们除了了解这些注解,还要明白它们是如何工作的,比如Scope为什么能实现局部单例等,这是要通过源码分析才能明白,同时,要运用到项目中,我们还得明白如何确定我们的最小粒度,Component,Module是怎么划分的等,这就涉及到Component的依赖,继承等内容,在接下来的篇幅中,我将通过源码分析和项目实战,来继续详解Dagger2的用法,敬请期待。