注解与依赖注入框架

Android开发中常用到的知识

Posted by wushiqian on July 25, 2019

注解

什么是注解

Java文档中的定义:

​ An annotation is a form of metadata, that can be added to Java source code. Classes, methods, variables, parameters and packages may be annotated. Annotations have no direct effect on the operation of the code they annotate.

注解是一种元数据, 可以添加到java代码中. 类、方法、变量、参数、包都可以被注解,注解对注解的代码没有直接影响。注解并没有什么魔法, 之所以产生作用, 是对其解析后做了相应的处理. 注解仅仅只是个标记罢了。

​ 定义注解用的关键字是@interface

元注解

​ java内置的注解有Override, Deprecated, SuppressWarnings等, 作用相信大家都知道.

​ 现在查看Override注解的源码

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

​ 发现Override注解上面有两个注解, 这就是元注解。元注解就是用来定义注解的注解。其作用就是定义注解的作用范围, 使用在什么元素上等等, 下面来详细介绍。

元注解共有四种@Retention, @Target, @Inherited, @Documented

  • @Retention 保留的范围,默认值为CLASS. 可选值有三种

  • SOURCE, 只在源码中可用
  • CLASS, 在源码和字节码中可用
  • RUNTIME, 在源码,字节码,运行时均可用
  • @Target 可以用来修饰哪些程序元素,如 TYPE, METHOD, CONSTRUCTOR, FIELD, PARAMETER等,未标注则表示可修饰所有
  • @Inherited 是否可以被继承,默认为false
  • @Documented 是否会保存到 Javadoc 文档中

其中, @Retention 是定义保留策略, 直接决定了我们用何种方式解析. SOURCE 级别的注解是用来标记的, 比如 Override , SuppressWarnings 。 我们真正使用的类型是 CLASS (编译时)和 RUNTIME (运行时)

注解处理器

虚处理器AbstractProcessor

我们首先看一下处理器的API。每一个处理器都是继承于AbstractProcessor,如下所示:

package com.example;

public class MyProcessor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment env){ }

    @Override
    public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }

    @Override
    public Set<String> getSupportedAnnotationTypes() { }

    @Override
    public SourceVersion getSupportedSourceVersion() { }

}
  • init(ProcessingEnvironment env): 每一个注解处理器类都必须有一个空的构造函数。然而,这里有一个特殊的init()方法,它会被注解处理工具调用,并输入ProcessingEnviroment参数。ProcessingEnviroment提供很多有用的工具类Elements, Types和Filer。
  • process(Set<? extends TypeElement> annotations, RoundEnvironment env): 这相当于每个处理器的主函数main()。你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。输入参数RoundEnviroment,可以让你查询出包含特定注解的被注解元素。后面我们将看到详细的内容。
  • getSupportedAnnotationTypes(): 这里你必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称。换句话说,你在这里定义你的注解处理器注册到哪些注解上。
  • getSupportedSourceVersion(): 用来指定你使用的Java版本。通常这里返回SourceVersion.latestSupported()。然而,如果你有足够的理由只支持Java 6的话,你也可以返回SourceVersion.RELEASE_6。大多数推荐使用前者。在Java 7中,你也可以使用注解来代替getSupportedAnnotationTypes() 和 getSupportedSourceVersion() ,像这样:
@SupportedSourceVersion(SourceVersion.latestSupported())
@SupportedAnnotationTypes({
   // 合法注解全名的集合
 })
public class MyProcessor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment env){ }

    @Override
    public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }
}

​ 因为兼容的原因,特别是针对Android平台,建议使用重载getSupportedAnnotationTypes() 和 getSupportedSourceVersion() 方法代替@SupportedAnnotationTypes 和@SupportedSourceVersion 。

​ 接下来的你必须知道的事情是,注解处理器是运行它自己的虚拟机JVM中。javac 启动一个完整 Java 虚拟机来运行注解处理器。你可以使用任何你在其他 java 应用中使用的的东西。使用 guava 。如果你愿意,你可以使用依赖注入工具,例如dagger 或者其他你想要的类库。即使是一个很小的处理,也要像其他 Java应用一样,注意算法效率,以及设计模式。

依赖注入是实现程序解耦的一种方式

控制反转(Inversion of Control,英文缩写为IoC)是一个重要的面向对象编程的法则来削减计算机程序的耦合问题.控制反转一般分为两种类型,依赖注入(Dependency Injection,简称DI)和依赖查找(Dependency Lookup)。依赖注入应用比较广泛。

一个比较形象的比喻和解释是:

  1. 原始社会里,几乎没有社会分工。需要斧子的人(调用者)只能自己去磨一把斧子(被调用者)。对应的情形为:Java程序里的调用者自己创建被调用者。

  2. 进入工业社会,工厂出现。斧子不再由普通人完成,而在工厂里被生产出来,此时需要斧子的人(调用者)找到工厂,购买斧子,无须关心斧子的制造过程。对应Java程序的简单工厂的设计模式。

  3. 进入“按需分配”社会,需要斧子的人不需要找到工厂,坐在家里发出一个简单指令:需要斧子。斧子就自然出现在他面前。对应Spring的依赖注入。

第一种情况下,Java实例的调用者创建被调用的Java实例,必然要求被调用的Java类出现在调用者的代码里。无法实现二者之间的松耦合。

第二种情况下,调用者无须关心被调用者具体实现过程,只需要找到符合某种标准(接口)的实例,即可使用。此时调用的代码面向接口编程,可以让调用者和被调用者解耦,这也是工厂模式大量使用的原因。但调用者需要自己定位工厂,调用者与特定工厂耦合在一起。

第三种情况下,调用者无须自己定位工厂,程序运行到需要被调用者时,系统自动提供被调用者实例。

​ 在程序中,一个对象中的方法需要依赖另一个对象,该对象中保存其所依赖对象的实例,生成依赖对象的方式不再该对象中通过new创建,而是调用者外部创建依赖对象,通过一定的方式进行传入。

举个例子

public class Classes {

    // 依赖类
    private Boy boy;

    public Classes(){
        // 在当前对象中直接 new 出依赖类
        boy = new Boy();
    }

    public void run(){
        boy.run();
    }

}
public class Boy {

    String name;

    public Boy(){

    }

    public void run(){

    }
}

有一个班级,班级中有一个boy。直接在班级中new出boy。班级中有一个run()方法,其内部实际调用的是boy的run()。

此时看着无大碍,那么如果boy发生了变化,其构造方法发生了变化,需要传入一个姓名。那么需要修改代码:

public class Boy {

    String name;

    public Boy(String name ){
        // 修改了构造方法
        this.name = name;
    }

    public void run(){

    }
}
public class Classes {

    // 依赖类
    private Boy boy;


    public Classes(){
        //  因为Boy的构造方法发生变化,所以需要修改该处代码
        boy = new Boy("lilei");
    }



    public void run(){
        boy.run();
    }

}

修改了Boy的构造方法之后,因为Classes依赖Boy,所以其内部也需要修改。

如果又发生了变化,Boy的姓名更改了,又要修改Classes中的代码。。。这样的话,一个还是不明显,当工程量很浩大时。

此时,我们可以将Boy该对象的实例化交给其调用者,通过某种方式传入进来。这种模式就是依赖注入。

依赖注入常见的有三种方式:

  • 构造方法注入。
  • Setter方式注入。
  • 接口注入。

ButterKnife

Dagger2