注解与依赖注入

916 阅读6分钟

注解

注解分类

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

  1. 标准注解
  • @Override:对覆盖超类中的方法进行标注,如果被标注的方法并没有实际覆盖超类中的方法,则编译器会发出警告
  • @Deprecated:对不鼓励使用或已经过时的方法添加注解,当编程人员使用这些方法时,会在编译时显示错误信息
  • @SuppressWarning:选择性的取消特定代码段中的警告
  • @SafeVarargs:JDK7新增,用于声明可变长度参数的方法,其在与泛型类一起使用时不会出现类型安全问题
  1. 元注解
  • @Target:注解所修饰的对象范围
  • @Inherited:表示注解可以被继承
  • @Documented:表示这个注解应该被JavaDoc工具记录
  • @Retention:用来声明注解的保留策略
  • @Repeatable:JDK8新增,允许一个注解在同一声明类型中多次使用

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

  • ElementType.TYPE:修饰类,接口和枚举类型
  • ElementType.FIELD:修饰成员变量
  • ElementType.METHOD:修饰方法
  • ElementType.PARAMETER:修饰参数
  • ElementType.CONSTRUCTOR:修饰构造方法
  • ElementType.LOCAL_VARIABLE:修饰局部变量
  • ElementType.ANNOTATION_TYPE:修饰注解
  • ElementType.PACKAGE:修饰包
  • ElementType.TYPE_PARAMETER:类型参数声明
  • ElementType.TYPE_USE:使用类型

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

  • RetentionPolicy.SOURCE:源码级注解。注解信息只会保留在Java源码中,源码在编译后注解信息被丢弃,不会保留在.class文件中
  • RetentionPolicy.CLASS:编译时注解。注解信息会保留在Java源码和.class文件中,运行Java程序时,JVM会丢弃该注解信息,不会保留在JVM中
  • RetentionPolicy.RUNTIME:运行时注解。运行Java程序时,JVM会保留该注解信息,可通过反射获取该注解信息

定义注解

1. 基本定义

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

public @interface Swordsman {
}

定义完注解后,就可以在类中使用

@Swordsman
public class test{
}

2. 定义成员变量

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

public @interface Swordsman {
    String name();
    int age();
}

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

public class test{
    @Swordsman(name = "zwj", age = 23) public void fighting(){
        
    }
}

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

public @interface Swordsman {
    String name() default "zwj";
    int age() default 23;
}

所以使用时可无需显示指定

public class test{
    @Swordsman public void fighting(){

    }
}

3. 定义运行时注解

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

@Retention(RetentionPolicy.RUNTIME)
public @interface Swordsman {
    String name() default "zwj";
    int age() default 23;
}

4. 定义编译时注解

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

@Retention(RetentionPolicy.CLASS)
public @interface Swordsman {
    String name() default "zwj";
    int age() default 23;
}

注解处理器

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

1. 运行时注解处理器

处理运行时注解需要用到反射机制,首先定义运行时注解,如下

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface GET {
    String value() default "";
}

定义了GET注解,应用于方法,接下来应用这个注解

public class AnnotationTest {
    @GET(value = "http://baidu.com/59.102.53.24") public String getIpMsg(){
        return "";
    }
    
    @GET(value = "http://baidu.com") public String getIp(){
        return "";
    }
}

上面代码为@GET的成员变量赋值,接下来编写注解处理器

public class AnnotationProcessor {
    public static void main(String[] args) {
        Method[] methods = AnnotationTest.class.getDeclaredMethods();
        for(Method method : methods){
            GET get = method.getAnnotation(GET.class);
            System.out.println(get.value());
        }
    }
}

上面代码用了两个反射方法,都属于AnnotatedElement接口,Class、Method、Field等类都实现了这个接口。调用getAnnotation方法返回指定类型的注解对象,也就是GET,最后调用GET的value方法返回从GET对象中所提取的元素的值即可


2. 编译时注解处理器

处理编译时注解的步骤稍微多些,仍旧需要先定义注解。在项目中新建一个Java Library来存放注解,这个Library命名为annotations。接下来定义注解,如下

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView{
    int value() default 1;
}

这段代码中定义的注解类似于ButterKnife的@BindView注解。接下来编写注解处理器,再次新建一个Java Library存放注解处理器,这个Library名为processor,先来配置其gradle

plugins {
    id 'java-library'
}

apply plugin:'java'

java {
    sourceCompatibility = JavaVersion.VERSION_1_7
    targetCompatibility = JavaVersion.VERSION_1_7
}

dependencies {
    implementation fileTree(includes: ['*.jar'], dir: 'libs')
    implementation project(':annotations')
}

接下来编写注解处理器ClassProcessor,继承自AbstractProcessor,如下

public class ClassProcessor extends AbstractProcessor {

    @Override public synchronized void init(ProcessingEnvironment processingEnvironment){
        super.init(processingEnvironment);
    }

    @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return true;
    }

    @Override public Set<String> getSupportedAnnotationTypes(){
        Set<String> annotations = new LinkedHashSet<>();
        annotations.add(BindView.class.getCanonicalName());
        return annotations;
    }

    @Override public SourceVersion getSupportedSourceVersion(){
        return SourceVersion.latestSupported();
    }
}

介绍下重写的四个方法的作用

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

接下来编写还未实现的process方法,如下

@Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    Messager messager = processingEnv.getMessager();
    for (Element element : roundEnvironment.getElementsAnnotatedWith(BindView.class)) {
        if (element.getKind() == ElementKind.FIELD){
            messager.printMessage(Diagnostic.Kind.NOTE, "printMessage:" + element.toString());
        }
    }
    return true;
}

这里用到了Messager的printMessage来打印注解修饰的成员变量的名称,这个名称会在Console窗口中打印

接着,为了能使用注解处理器,我们需要一个服务文件来注册它,采用谷歌开源的AutoService,可以帮助开发者生成META-INF.services/javax.annotation.processing.Processor文件。点击“File” -> “Project Structure”,搜索“auto-service”来查找该库并添加

implementation 'com.google.auto.service:auto-service:1.0.1'

之后在注解处理器ClassProcessor中添加注解即可

@AutoService(Processor.class) public class ClassProcessor extends AbstractProcessor {
    ···
}

最后就到了应用注解,我们要在主工程项目中引用注解,首先要在主工程app项目的build.gradle中引用annotations和processor两个库,如下

implementation project(':annotations')
implementation project(':processor')

随后在MainActivity中应用注解,如下

public class MainActivity extends AppCompatActivity {

    @BindView(value = R.id.tv_text) TextView textView;

    @Override protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

最后Make Project,可以发现在Gradle Console窗口中运行如下截图,打印出了具体的注解信息

image.png

除此之外,我们也可以使用android-apt插件,我们的主工程项目app中引用了processor 库,但是注解处理器只在编译期间需要用到,编译处理完后就没有实际作用了,而主工程项目添加了这个库,就会引入很多不必要的麻烦,为了处理这个问题我们引入插件android-apt,主要有以下两个作用

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

接下来介绍如何使用,首先需要在整个项目的build.gradle中添加如下语句

buildScript {
    ···
    dependencies {
        ···
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
}

然后,在主工程app的build.gradle中以apt方式引入注解处理器processor,如下

apply plugin: 'com.neenbedankt.android-apt'

dependencies{
    apt project(':processor')
}

依赖注入原理

说完了注解,下面说说依赖注入,想要了解依赖注入,先从控制反转说起

1. 控制反转

对象之间的耦合是无法避免的,但随着工业级应用越来越庞大,对象之间的依赖关系越来越复杂,经常会出现对象之间的多重依赖关系。为了解决耦合度过高的问题,有人提出了IOC理论,即控制反转,用于实现对象之间的解耦。在软件系统引入IOC之前,如果对象A依赖于对象B,那么对象A在运行到某一固定时刻需要自行创建B对象,无论是创建还是使用,控制权都在自己手上。引入IOC后,对象A和B可以失去直接联系,当A运行至需要B时,IOC容器会创建一个对象B,注入到A所需的地方,通过引入IOC的前后对比,可以看出对象A获得B的过程由主动转为了被动,此为控制反转的由来。于是,由于依赖对象的过程被反转了,控制反转有了一个更合适的名字,即依赖注入,指的是IOC容器在运行期间,动态将某种依赖关系注入到对象中


2. 依赖注入的方式

编写代码时我们会发现有一些类是依赖于其他类的,下面举一个汽车与零件的例子,汽车Car包含了Engine等组件,如下

public class Car{
    private Engine engine;
    public Car(){
        engine = new PetrolEngine();
    }
}

这段代码本身没有错,但是Car和Engine高度耦合,因为Car需要创造Engine对象,而且还需要知道PetrolEngine的存在,如果引擎更改为DieselEngine,还需要修改Car的构造方法,以上问题都可以通过依赖注入来解决,依赖注入有三种常见实现方式,如下

  1. 构造方法注入。往Car的构造方法里传递Engine对象
public class Car{
    private Engine engine;
    public Car(Engine engine){
        this.engine = engine;
    }
}
  1. setter方法注入。通过set方法传递Engine对象
public class Car{
    private Engine engine;
    public void set(Engine engine){
        this.engine = engine;
    }
}
  1. 接口注入 在接口中定义需要注入的信息,并通过接口完成注入,接口代码如下
public interface ICar{
    public void setEngine(Engine engine);
}

然后Car类实现接口

public class Car implements ICar{
    private Engine engine;
    
    @Override public void setEngine(Engine engine){
        this.engine = engine;
    }
} 

这样一来,Car和Engine解耦了,Car不关心Engine的实现,即使Engine需要修改,Car也无需做任何修改