Java 基础:Annotation

322 阅读6分钟
原文链接: blog.csdn.net

注解(Annotation)就是一种程序可识别的注释。 本文介绍注解的使用。

文章目录

一、概述

注解:程序可识别的注释

注解的格式:

@xxx(value = yyy)
@xxx(yyy)
@xxx(aaa = bbb, ccc = ddd)

识别的方式:

  • 编码期直接识别(@Override)
  • 编译期通过注解处理器识别
  • 运行期通过反射识别

二、注解的分类

2.1 JDK 自带注解

  • @Override  重写, 标识覆盖它的父类的方法
  • @Deprecated   已过期,表示方法是不被建议使用的
  • @Suppvisewarnings   压制警告,抑制警告

2.2 元注解

元注解用来生成自定义注解

  • @Target

    指明注解可以被写在哪里,取值为 ElementType 的枚举值。 注解没有指明 @Target 的话,取值为除了 TYPE_PARAMETER 的所有地方。

    类型 代表的地方
    TYPE 类、接口(包括注解)、枚举
    FIELD 本地变量、枚举值
    METHOD 方法(不包括构造方法)
    PARAMETER 参数
    CONSTRUCTOR 构造方法
    LOCAL_VARIABLEPE 局部变量
    ANNOTATION_TYPE 注解(TYPE 的一部分)
    PACKAGE 包(用在 package-info.java)
    TYPE_PARAMETER 类型参数(1.8新加入,泛型中的类型参数)
    TYPE_USE 类型使用(1.8新加入,任何使用类型的地方)
  • @Retention

    指明 @xxx 的字面值在代码中保留的最后时期,取值为 RetentionPolicy 的枚举值。默认是 CLASS

    类型 代表的时期
    source 源码(编译时丢失)
    class class文件(JVM 类加载时丢失)
    runtime 运行时(不会丢失)
  • @Documented 指明注解应该被 javadoc 工具记录,注解信息会被包括在生成的文档中。

  • @Inherited 可以让注解被继承,但这并不是真的继承,只是通过使用 @Inherited,可以让子类 Class 对象使用 getAnnotations() 获取父类被 @Inherited 修饰的注解

2.3 自定义注解

使用元注解来生成自定义注解

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface MyCustom {
    int value();
}

三、注解的语法

3.1 定义注解

使用 @interface 来定义一个注解:

public @interface MyCustom {
}

它与接口非常相似,我们查看一下反编译后的注解类:

public interface MyCustom extends Annotation {
}

可以发现,最终实现也是作为一个接口来实现的,但注解与接口不同,它不支持继承,即定义注解时无法使用 extends 关键字来继承其他注解。

3.2 定义元素

注解中可以定义元素(相当于定义一个有返回值的方法):

public @interface MyCustom {
	int value()
}

注解元素支持的类型:

  • 所有基本类型(byte、short、int、long、float、double、char、boolean,它们的包装类型不行)
  • String
  • Class
  • enum
  • Annotation
  • 上述类型的数组(List 不行)

元素可以有默认值:

public @interface MyCustom {
	int value() default 1;
}

默认值的限制:

  • 如果没有默认值,那在使用时必须提供
  • 对于非基本类型的元素,无论是默认值,还是在使用时提供的值,都不能以null作为值。为了绕开这个限制,只能定义一些特殊的值,例如空字符串或负数,表示某个元素不存在。

3.3 使用注解

在注解可放置的地方放置注解,并提供元素值:

@MyCustom(value = 2)
class MyClass {   
}

当注解只有一个名为 value 的元素时,赋值时 value = 可以省略:

@MyCustom(2)
class MyClass {   
}

四、注解的处理

4.1 注解处理器

在编译时,可以通过注解处理器获取注解的信息。

4.1.1 定义注解

创建一个注解定义模块 annotation。定义一个注解 @BindView。

@Target(ElementType.FIELD)
public @interface BindView {
    int value();
}

4.1.2 定义注解处理器

创建一个注解处理模块 processor,它依赖于 annotation。

implementation project(':annotation')

注解处理器继承于 AbstractProcessor。一般来说,要实现四个方法(更多见这里):

  • init(processingEnvironment)
    初始化注解处理器,processingEnvironment 中会提供一些实用工具,如用来报告错误、警报和其他通知的 Messager;用来创建新源、类或辅助文件的 Filer 等。
  • getSupportedSourceVersion
    设置该处理器支持的最新的 jdk 版本。一般返回 SourceVersion.latestSupported()。
  • getSupportedAnnotationTypes
    设置该处理器要处理的注解类型,要提供包名+类名。
  • process(annotationSet, roundEnvironment)
    注解的处理逻辑。
    注解处理分为多个轮次,每一轮次该方法都会调用 process 方法。
    它的两个参数 annotationSet 是要处理的注解类型,roundEnvironment 是上一轮次的环境信息,可以拿到注解修饰的语句信息。
    如果返回 true,则后续 Processor 不会再处理 annotationSet 中的注解;如果返回 false,则后续 Processor 还会处理它们。
@AutoService(Processor.class)
public class MyAnnotationProcessor extends AbstractProcessor {
    private Messager mMessager;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mMessager = processingEnvironment.getMessager();
        mMessager.printMessage(Diagnostic.Kind.NOTE, "MyAnnotationProcessor init");
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        mMessager.printMessage(Diagnostic.Kind.NOTE, "MyAnnotationProcessor getSupportedSourceVersion");
        return SourceVersion.latestSupported();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        mMessager.printMessage(Diagnostic.Kind.NOTE, "MyAnnotationProcessor getSupportedAnnotationTypes");
        Set<String> strings = new HashSet<>();
        strings.add(BindView.class.getCanonicalName());
        return strings;
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        mMessager.printMessage(Diagnostic.Kind.NOTE, "MyAnnotationProcessor process " + roundEnvironment.hashCode());
        Set<? extends Element> elementSet = roundEnvironment.getElementsAnnotatedWith(BindView.class);
        mMessager.printMessage(Diagnostic.Kind.NOTE, "MyAnnotationProcessor 要处理的注解 " + set);
        mMessager.printMessage(Diagnostic.Kind.NOTE, "MyAnnotationProcessor 注解修饰的语句 " + elementSet);
        if (elementSet != null) {
            for (Element element : elementSet) {
                VariableElement variableElement = (VariableElement) element;
                Name name = variableElement.getSimpleName();
                BindView bindView = variableElement.getAnnotation(BindView.class);
                mMessager.printMessage(Diagnostic.Kind.NOTE, "MyAnnotationProcessor 注解修饰的语句信息和注解元素值 " + name
                    + ", " + bindView.value());
            }
        }
        return true;
    }
}

4.1.3 使用注解

主模块 app,它依赖于 annotation、processor。

implementation project(':annotation')
annotationProcessor project(':processor')

添加注解

public class MainActivity extends AppCompatActivity {

    @BindView(1)
    private TextView mTextView1;

    @BindView(2)
    private TextView mTextView2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }
}

4.1.4 注解处理器执行

make project,输出:

> Task :app:compileDebugJavaWithJavac
注: MyAnnotationProcessor init
注: MyAnnotationProcessor getSupportedSourceVersion
注: MyAnnotationProcessor getSupportedAnnotationTypes
注: MyAnnotationProcessor getSupportedAnnotationTypes 			// 第一轮
注: MyAnnotationProcessor process 1845469711
注: MyAnnotationProcessor 要处理的注解 [com.gdeer.annotation.BindView]
注: MyAnnotationProcessor 注解修饰的语句 [mTextView1, mTextView2]
注: MyAnnotationProcessor 注解修饰的语句信息和注解元素值 mTextView1, 1
注: MyAnnotationProcessor 注解修饰的语句信息和注解元素值 mTextView2, 2
注: MyAnnotationProcessor getSupportedAnnotationTypes 			// 第二轮
注: MyAnnotationProcessor process 977562649
注: MyAnnotationProcessor 要处理的注解 []
注: MyAnnotationProcessor 注解修饰的语句 []
注: MyAnnotationProcessor getSupportedAnnotationTypes 		// 第三轮
注: MyAnnotationProcessor process 2034412128
注: MyAnnotationProcessor 要处理的注解 []
注: MyAnnotationProcessor 注解修饰的语句 []

可以看到进行了三轮注解处理,每一轮都会调到 process 方法,每次 process 方法中的 roundEnvironment 都不相同。因为第一次 process 返回了 true,所以之后两轮中都没有要处理的注解,所以都没有进行处理。

4.1.4 生成 java 文件

从注解中获取信息后,我们就可以通过这些信息生成新的 java 文件,这里可以借助 JavaPoet 工具,将编辑好的文件信息写入 init 方法的 processingEnvironment 中提供的 Filer 中。这样,就可以在 /app/build/generated/source/apt/debug 目录下看到生成的文件。

JavaPoet 相关操作可参考这里:JavaPoet 的使用

4.2 反射

在运行时,可以通过反射获取注解的信息。

定义一个 UseCase 注解(Retention 为 RUNTIME,否则在运行时找不到该注解):

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
    int id();

    String description() default "no description";
}

使用 UseCase 注解:

public class PasswordUtils {
    @UseCase(id = 47, description = "password must contain at least one numeric")
    public boolean validatePassword(String password) {
        return (password.matches("sdfsfd"));
    }

    @UseCase(id = 48)
    public String encryptPassword(String password) {
        return new StringBuilder(password).reverse().toString();
    }

    @UseCase(id = 49, description = "New passwords can't equal previously used ones")
    public boolean checkForNewPassword(List<String> prevPassword, String password) {
        return !prevPassword.contains(password);
    }
}

处理 UseCase 注解:

使用 declaredMethod.getAnnotation(UseCase.class) 获取 declaredMethod 方法上的 @UseCase 注解。

public class UseCaseTracker {
    public static void trackUseCases(List<Integer> useCases, Class<?> cl) {
        for (Method declaredMethod : cl.getDeclaredMethods()) {
            UseCase useCase = declaredMethod.getAnnotation(UseCase.class);
            if (useCase != null) {
                System.out.println(useCase.id() + " " + useCase.description());
                useCases.remove(Integer.valueOf(useCase.id()));
            }
        }

        for (Integer useCase : useCases) {
            System.out.println("missing " + useCase);
        }
    }

    public static void main(String[] args) {
        List<Integer> useCases = new ArrayList<>();
        Collections.addAll(useCases, 47, 48, 49, 50);
        trackUseCases(useCases, PasswordUtils.class);
    }
}

输出:

47 password must contain at least one numeric
48 no description
49 New passwords can't equal previously used ones
missing 50