注解与依赖注入框架

前言

在许多程序设计语言中,比如Java、C#。依赖注入是一种比较流行的设计模式,在android开发中也有很多实用的依赖注入框架,可以帮助我们少些一些样板代码,达到各个类之间解耦的目的。

注解

从JDK 5开始,Java增加了注解,注解就是代码里的特殊标记,这些标记可以在编译、类加载、运行时被读取,并执行相应的处理。通过使用注解,开发人员可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充的信息。代码分析工具和部署工具可以通过这些补充信息进行验证、处理或者进行部署。

注解分类

注解分为标准注解和元注解。

  • 标准注解

    标准注解有以下4种。

    • @Override:对覆盖超类中的方法进行标记,如果被标记的方法并没有实际覆盖超类中的方法,则编译器会发出错误警告。
    • @Deprecated:对不鼓励使用或者已过时的方法添加注解,当编译人员使用这些方法时,将会在编译时显示提示信息。
    • @SuppressWarnings:选择性的取消特定代码段中的警告。
    • @SafeVarargs:JDK1.7新增,用来声明使用了可变长参数的方法,其在与泛型类一起使用时不会出现类型安全问题。
  • 元注解

    除了标准注解,还有元注解,它用来创建新的注解。元注解有以下几种。

    • @Targe:注解所修饰的对象范围。
    • @Inherited:表示注解可以被继承。
    • Documented:表示这个注解应该被JavaDoc工具记录。
    • Retention:用来声明注解的保留策略。
    • Repeatable:JDK 8新增,允许一个注解在同一声明类型(类、属性或者方法)上多次使用。

    其中@Targe注解取值是一个ElementType类型的数组,其中有以下几种取值,对应不同的对象范围。

    • ElementType.TYPE:能修饰类、接口或枚举类型。
    • ElementType.FIELD:能修饰成员变量。
    • ElementType.METHORD:能修饰方法。
    • ElementType.PARAAMETER:能修饰参数。
    • ElementType.CONSTRUCTOR:能修饰构造函数。
    • ElementType.LOCAL_VARIABLE:能修饰局部变量。
    • ElementType.ANNOTATION_TYPR:能修饰注解。
    • ElementType.PACKAGE:能修饰包。
    • ElementType.TYPE_PARAMENTER:类型参数声明。
    • ElementType.TYPE_USE:使用类型。

    其中@Retention注解有3种类型,分别表示不同级别的保留策略。

    • RetentionPolicy.SOURCE:源码级注解。注解信息只会保留在.java源码中,源码在编译后,注解信息被丢弃,不会保留在.class中。
    • RetentionPolicy.CLASS:编译时注解。注解信息会保留在.java源码以及.class中。当运行Java程序时,JVM会丢弃该注解信息,不会保留在JVM中。
    • RetentionPolicy.RUNTIME:运行时注解。当运行Java程序时,JVM也会保留该注解信息,可以通过反射获取该注解信息。
定义注解
  • 基本定义

    定义新的注解类型使用@interface关键字,这与定义一个接口很像,如下所示。

    1
    2
    3
    public @interface Swordsman {
    ...
    }

    定义完注解后,就可以在程序中使用该注解。

    1
    2
    3
    4
    @Swordsman
    public class AnnotationTest {
    ...
    }
  • 定义成员变量

    注解只有成员变量,没有方法。注解的成员变量在注解定义中以“无形参的方法”形式来声明,其“方法名”定义了该成员变量的名字,其返回值定义了该成员变量的类型。

    1
    2
    3
    4
    public @interface Swordsman {
    String name();
    int age();
    }

    上面的代码定义了两个成员变量,这两个成员变量以方法的形式来定义。定义了成员变量后,使用该注解时就应该为该注解的成员变量指定值。

    1
    2
    3
    4
    5
    6
    public class AnnotationTest {
    @Swordsman(name = "username", age = 20)
    public void fighting() {
    ...
    }
    }

    也可以在定义注解的成员变量时,使用default关键字为其指定默认值,如下所示。

    1
    2
    3
    4
    public @interface Swordsman {
    String name() default "username";
    int age() default 20;
    }

    因为注解定义了默认值,所以使用时可以不为这些成员变量指定值,而是直接使用默认值。

    1
    2
    3
    4
    5
    public class AnnotationTest {
    public void fighting() {
    ...
    }
    }
  • 定义运行时注解

    可以用@Retention来设定注解的保留策略,这3个策略的生命周期长度为SOURCE < CLASS < RUNTIME。生命周期短的能起作用的地方,生命周期长的一定也能起作用。一般如果在运行时去动态获取注解信息,那只能用RetentionPolicy.RUNTIME;如果要在编译时进行一些预处理操作,比如生成一些辅助代码,就用RetentionPolicy.CLASS;如果只是做一些检查型的操作,比如@Override和@SuppressWarnings,则可选用RetentionPolicy.SOURCE。当设定为RetentionPolicy.RUNTIME时,这个注解就是运行时注解,如下所示。

    1
    2
    3
    4
    5
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Swordsman {
    String name() default "username";
    int age() default 20;
    }
  • 定义编译时注解

    同样地,如果将@Retention的保留策略设定为RetentionPolicy.CLASS,这个注解就是编译时注解,如下所示。

    1
    2
    3
    4
    5
    @Retention(RetentionPolicy.CLASS)
    public @interface Swordsman {
    String name() default "username";
    int age() default 20;
    }
注解处理器

如果没有处理注解的工具,那么注解也不会有什么大的用处。对于不同的注解有不同的注解处理器。虽然注解处理器的编写会千变万化,但是其也有处理标准,比如,针对运行时注解会采用反射机制处理,针对编译时注解会采用AbstractProcessor来处理。

  • 运行时注解处理器

    处理运行时注解需要使用到反射机制。首先我们要定义运行时注解,如下所示。

    1
    2
    3
    4
    5
    6
    @Documented
    @Target(METHOD)
    @Retention(RUNTIME)
    public @interface GET {
    String value() default "";
    }

    上面的代码是Retrofit中定义的@GET注解。其定义了@Target(METHOD),这等效于@Target(ElementType.METHOD),意味着GET注解应用于方法。接下来应用该注解,如下所示。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class AnnotationText {
    @GET(value = "http://ip.taobao.com/59.108.54.37")
    public String getIpMsg() {
    return "";
    }
    @GET(value = "http://ip.taobao.com/")
    public String getIp() {
    return "";
    }
    }

    上面的代码为@GET的成员变量赋值。接下来写一个简单的注解处理器,如下所示。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class AnnotationProcessor {
    public static void main(String[] args) {
    Method[] methods = AnnotationTest.class.getDeclaredMethods();
    for (Method m : methods) {
    GET get = m.getAnnotation(GET.class);
    Systen.out.println(get.value());
    }
    }
    }

    上面的代码用到了两个反射方法:getDeclaredMethods和getAnnotation,它们都属于AnnotatedElement接口,Class、Method和Field等类都实现了该接口。调用getAnnotation方法返回指定类型的注解对象,也就是GET。最后调用了GET的value方法返回从GET对象中提取的元素的值。输出结果为:

    1
    2
    http://ip.taobao.com/59.108.54.37
    http://ip.taobao.com/
  • 编译时注解处理器

    处理编译时注解的步骤如下。

    ① 定义注解

    这里首先在项目中新建一个Java Library来专门存放注解,这个Library名为annotations。接下来定义注解,如下所示。

    1
    2
    3
    4
    5
    @Retention(CLASS)
    @Targe(FIELD)
    public @interface BindView {
    int value() default 1;
    }

    ② 编写注解处理器

    在项目中在新建一个Java Library来存放注解处理器,这个Library名为processor。接着来配置processor库的build.gradle。

    1
    2
    3
    4
    5
    6
    7
    apply plugin: 'java'
    dependencies {
    compile fileTree(include: ['*.jar'], dir, 'libs')
    compile project(':annotations')
    }
    sourceCompatibility = "1.7"
    targetCompatibility = "1.7"

    接下来编写注解处理器ClassProcessor,它继承AbstractProcessor,如下所示。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public class ClassProcessor extends AbstractProcessor {
    @Override
    public synchronized void init(ProcessingEnviroment processingEnv) {
    super.init(processingEnv);
    }
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    ...
    return true;
    }
    @Override
    public Set<String> getSupportedAnnotationTypes() {
    Set<String> anotations = new LinkedHashSet<String>();
    annotations.add(BindView.class.getCanonicalName());
    return annotations;
    }
    @Override
    public SourceVersion getSupportedSourceVersion() {
    return SourceVersion.lastestSupported();
    }
    }

    首先分别介绍这4个方法的作用。

    • init:被注解处理工具调用,并输入ProcessingEnviroment参数。ProcessingEnviroment提供很多有用的工具类,比如Element、Types、Filer和Messager等。
    • process:相当于每个处理器的主函数main(),在这里写你的描述、评估和处理注解的代码,以及生成Java文件。输入参数RoundEnviroment,可以让你查询出包含特定注解的被注解元素。
    • getSupportedAnnotationTypes:这是必须指定的方法,指定这个注解处理器是注册给哪个注解的。它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称。
    • getSupportedSourceVersion:用来指定使用的Java版本,通常这里返回SourceVersion.lastestSupported()。

    在Java 7 之后,也可以使用注解来代替getSupportedAnnotationTypes方法和getSupportedSourceVersion方法,如下所示。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @SupportedSourceVersion(SourceVersion.RELEASE_8)
    @SupportedAnnotationTypes("com.example.annotation.cls.BindView")
    public class ClassProcessor extends AbstractProcessor {
    @Override
    public synchronized void init(ProcessingEnviroment processingEnv) {
    super.init(processingEnv);
    }
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    ...
    return true;
    }
    }

    但是考虑到android兼容性的问题,不建议采用这种注解的方式。接下来编写还未实现的process方法,如下所示。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    Messager messager = processingEnv.getMessager();
    for (Element elemnt : roundEnv.getElementsAnnotatedWith(BindView.class)) {
    if (element.getKind() == ElementKind.FIELD) {
    messager.printMessage(Diagnostic.Kind.NOTE, "printMessaage:" + element.toStrintg());
    }
    }
    ...
    return true;
    }

    这里用到Messager的printMessage方法来打印出注解修饰的成员变量的名称。

    ③ 注册注解处理器

    为了能使用注解处理器,需要用一个服务文件来注册它。要创建这个服务文件,首先在processor库的main目录下resources资源文件夹,接下来在resources中再建立META-INF/services目录文件夹。最后在META-INF/services中创建javax.annotation.processing.Processor文件,这个文件中的内容是注解处理器的名称。也可以使用Google开源的AutoService,它用来帮助开发者生成META-INF/service/javax.annotation.processing.Processor文件。首先需要添加一个开源库,也可以在processor的build.gradle中直接添加如下代码。

    1
    2
    3
    4
    dependencies {
    ...
    complie 'com.google.auto.service:auto-service:1.0-rc2'
    }

    最后在注解处理器ClassProcessor中添加@AutoService(Processor.class)就可以了。

    1
    2
    3
    4
    @AutoService(Processor.class)
    public class ClassProcessor extends AbstractProcessor {
    ...
    }

    ④ 应用注解

    接下来在我们定位主工程项目(app)中引用注解。首先要在主工程项目的build.gradle中引用annotations和processor这两个库。

    1
    2
    3
    4
    5
    dependencies {
    ...
    complie project(':annotations')
    complie project(':processor')
    }

    接下来在MainActivity中应用注解,如下所示。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class MainActivity extends AppCompatActivity {
    @BindView(value = R.id.tv_text)
    TextView tv_text;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    }
    }

    最后,clean project在make project,在Gradle Console窗口中打印的结果就是@BindView注解修饰的成员变量名:tv_text。

    ⑤ 使用android-apt插件

    我们在主工程项目(app)中引用了processor库,但注解处理器只在编译处理期间需要用到,编译处理完后就没有实际作用了,而在主工程项目添加了这个库会引入很多不必要的文件。为了处理这个问题我们需要引入插件android-apt。它主要有两个作用:

    • 仅仅在编译时期去依赖注解处理器所在函数库并进行工作,但不会打包到APK中。
    • 为注解处理器生成的代码设置好路径,以便android studio能够找到它。

    接下来介绍如何使用它。首先需要在整个工程(project)的build.gradle中添加如下语句。

    1
    2
    3
    4
    5
    6
    7
    buildscript {
    ...
    dependencies {
    ...
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
    }

    接下来在主工程项目(app)的build.gradle中以apt的方式引入注解处理器processor,如下所示。

    1
    2
    3
    4
    5
    6
    7
    8
    ...
    apply plugin: 'com.neenbedankt.android-apt'
    ...
    dependencies {
    ...
    // complie project(':processor')
    apt project(':processor')
    }

依赖注入的原理

控制反转与依赖注入
  • 控制反转

    Inversion of Control(IoC)。IoC理论提出的观点大体是这样的:借助于“第三方”实现具有依赖关系的对象之间的解耦。在软件系统没有引入IoC容器之前,对象之间相互依赖,假设对象A依赖与对象B,那么对象A在初始化或者运行到某一点的时候,自己必须手动去创建对象B或使用已经创建的对象B,无论是创建对象B还是使用对象B,控制权都在自己手上。软件系统在引入IoC容器之后,情形就完全改变了,由于IoC容器的加入,对象A与对象B之间失去了直接联系,所以,当对象A运行到需要对象B的时候,IoC容器会主动创建一个对象B注入到对象A需要的地方。通过引入IoC容器前后的对比,可以看出:对象A获得依赖对象B的过程,由主动行为变为被动行为,控制权颠倒过来了,这就是控制反转这个名称的由来。

  • 依赖注入

    控制反转之后,获得依赖对象的过程由自身管理变为由IoC容器主动注入。于是,控制反转有了另一个更合适的名字,叫做依赖注入(Dependency Injection),简称DI。所谓依赖注入,是指IoC容器在运行期间,动态地将某种依赖关系注入到对象中去。

依赖注入的实现方式

编写代码时常常会发现有一些类是依赖于其他类的,所以类A可能需要一个类B的引用或对象。举个例子。

1
2
3
4
5
6
public class Car {
private Engine mEngine;
public Car() {
mEngine = new PetrolEngine();
}
}

代码本身没有错,但是Car和Engine高度耦合,在Car中需要自己创建Engine,并且Car还需要Engine的实现方式,也就是Engine的实现类PetrolEngine的存在。另外,一旦Engine的类型变为其他的实现比如DieselEngine,则需要修改Car的构造方法。以上问题需要用依赖注入来解决。接下来,就用依赖注入的3种常用方式来改造上面的代码。

  • 构造方法注入

    通过Car的构造方法,向Car传递了Engine对象,如下所示。

    1
    2
    3
    4
    5
    6
    public class Car {
    private Engine mEngine;
    public Car(Engine engine) {
    this.mEngine = engine;
    }
    }
  • Setter方法注入

    通过Car的set方法向Car传递Engine对象,如下所示。

    1
    2
    3
    4
    5
    6
    public class Car {
    private Engine mEngine;
    public void set(Engine engine) {
    this.mEngine = engine;
    }
    }
  • 接口注入

    在接口中定义需要注入的信息,并通过接口完成注入,如下所示。

    1
    2
    3
    public interface ICar {
    public void setEngine(Engine engine);
    }
    1
    2
    3
    4
    5
    6
    7
    public class Car implements ICar {
    private Engine mEngine;
    @Override
    public void setEngine(Engine engine) {
    this.mEngine = engine;
    }
    }

通过以上3种注入方法,明显将Car和Engine解耦了。Car不关心Engine的实现,即使Engine的类型变换了,Car也无须做任何修改。

依赖注入框架

android目前主流的依赖注入框架有ButterKnife和Dagger2。