Java注解使用与自定义

零、背景

曾几何时,XML 一直是 Java 各大框架配置元数据(meta data) 的主要途径。但作为一种集中式的元数据管理工具,配置项与作用代码距离太过 “遥远”,非常不利于代码的维护和调试。再加上 XML 本身复杂的语法结构,往往令码农们大感头疼。一种与作用代码耦合在一起的元数据配置方式呼之欲出。于是 注解 (Annotations)就在 JDK 5 中正式出现在开发者的视线之中了。

一、什么是注解

注解是元数据的一种形式,它提供有关程序的数据,该数据不属于程序本身。注解对其注释的代码操作没有直接影响。换句话说,注解携带元数据,并且会引入一些和元数据相关的操作,但不会影响被注解的代码的逻辑。

/**
 * The common interface extended by all annotation types.  Note that an
 * interface that manually extends this one does <i>not</i> define
 * an annotation type.  Also note that this interface does not itself
 * define an annotation type.
 *
 * More information about annotation types can be found in section 9.6 of
 * <cite>The Java&trade; Language Specification</cite>.
 *
 * The {@link java.lang.reflect.AnnotatedElement} interface discusses
 * compatibility concerns when evolving an annotation type from being
 * non-repeatable to being repeatable.
 *
 * @author  Josh Bloch
 * @since   1.5
 */
public interface Annotation {
    ...
}

Java中所有的注解都扩展自 Annotation这个接口,注解本质就是一个接口。比如我们最常见的重写@Override,它没有参数,所以是一个标记注解。

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

1. 元注解

@Override为例,我们发现上面有两个注解@Target@Retention,像这种注解的注解被称为元注解。Java中的元注解有以下几个,

@Target

这个注解标识了被修饰注解的作用对象,看源码,

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    /**
     * Returns an array of the kinds of elements an annotation type
     * can be applied to.
     * @return an array of the kinds of elements an annotation type
     * can be applied to
     */
    ElementType[] value();
}

value是一个数组,说明该注解的作用对象可以是多个,取值对象在ElementType中,

public enum ElementType {
    /** Class, interface (including annotation type), or enum declaration */
    TYPE,

    /** Field declaration (includes enum constants) */
    FIELD,

    /** Method declaration */
    METHOD,

    /** Formal parameter declaration */
    PARAMETER,

    /** Constructor declaration */
    CONSTRUCTOR,

    /** Local variable declaration */
    LOCAL_VARIABLE,

    /** Annotation type declaration */
    ANNOTATION_TYPE,

    /** Package declaration */
    PACKAGE,

    /**
     * Type parameter declaration
     *
     * @since 1.8
     */
    TYPE_PARAMETER,

    /**
     * Use of a type
     *
     * @since 1.8
     */
    TYPE_USE,

    /**
     * Module declaration.
     *
     * @since 9
     */
    MODULE
}

不同的值代表被注解可修饰的范围,例如TYPE只能修饰类、接口和枚举定义,METHOD只能修饰方法,比如@Override只能注解方法。这其中有个很特殊的值叫做 ANNOTATION_TYPE, 是专门表示元注解的。

@Retention

该注解指定了被修饰的注解的生命周期。定义如下:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    /**
     * Returns the retention policy.
     * @return the retention policy
     */
    RetentionPolicy value();
}

value返回了一个RetentionPolicy枚举类型,

public enum RetentionPolicy {
    /**
     * Annotations are to be discarded by the compiler.
     */
    SOURCE,

    /**
     * Annotations are to be recorded in the class file by the compiler
     * but need not be retained by the VM at run time.  This is the default
     * behavior.
     */
    CLASS,

    /**
     * Annotations are to be recorded in the class file by the compiler and
     * retained by the VM at run time, so they may be read reflectively.
     *
     * @see java.lang.reflect.AnnotatedElement
     */
    RUNTIME
}
  • SOURCE:表示注解编译时可见(在原文件有效),编译完后就被丢弃。这种注解一般用于在编译期做一些事情,比如可以让注解处理器生成一些代码,或者是让注解处理器做一些额外的类型检查,等等;
  • CLASS:表示在编译完后写入 class 文件(在class文件有效),但在类加载后被丢弃。这种注解一般用于在类加载阶段做一些事情,比如Android的资源类型检查(@ColorRes、@DrawableRes、@Px);
  • RUNTIME:表示注解会一直起作用。
    @Override就是编译时可见,编译成class之后丢失。
@Documented

编译器在生成Java文档时将被注解的元素包含进去。这意味着使用了@Documented注解的注解,其注解的信息会被包含在生成的文档中,方便开发者查阅。@Documented是一个标记注解,没有成员。

@Inherited

用于指示子类是否继承父类的注解。当一个注解被标注为 @Inherited 时,如果一个类使用了被 @Inherited 标注的注解,那么其子类也会自动继承这个注解。这在一些框架或库中很有用,可以自动继承某些特性或行为。需要注意的是,@Inherited annotation类型是被标注过的class的子类所继承。类并不从它所实现的接口继承annotation,方法并不从它所重载的方法继承annotation


一个注解可以有多个target(ElementType),但只能有一个retention(RetentionPolicy)。

2. Java内置注解

以下基于JDK18

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

这个注解没有任何取值,只能修饰方法,而且RetentionPolicy 为 SOURCE,说明这是一个仅在编译阶段起作用的注解。在编译阶段,如果一个类的方法被 @Override 修饰,编译器会在其父类中查找是否有同签名函数,如果没有则编译报错。可见这确实是一个除了在编译阶段就没什么用的注解。

@Deprecated
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, MODULE, PARAMETER, TYPE})
public @interface Deprecated {
    /**
     * Returns the version in which the annotated element became deprecated.
     * The version string is in the same format and namespace as the value of
     * the {@code @since} javadoc tag. The default value is the empty
     * string.
     *
     * @return the version string
     * @since 9
     */
    String since() default "";

    /**
     * Indicates whether the annotated element is subject to removal in a
     * future version. The default value is {@code false}.
     *
     * @return whether the element is subject to removal
     * @since 9
     */
    boolean forRemoval() default false;
}

这个注解能修饰所有的类型,永久存在,有两个参数:since用来表示从哪个版本开始废弃;forRemoval用来表示被注解的元素将来是否可能被移除。这个注解的作用是,告诉使用者被修饰的代码不推荐使用了,可能会在下一个软件版本中移除。这个注解仅仅起到一个通知机制,如果代码调用了被@Deprecated修饰的代码,编译器在编译时输出一个编译告警。

@SuppressWarnings
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, MODULE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
    /**
     * The set of warnings that are to be suppressed by the compiler in the
     * annotated element.  Duplicate names are permitted.  The second and
     * successive occurrences of a name are ignored.  The presence of
     * unrecognized warning names is <i>not</i> an error: Compilers must
     * ignore any warning names they do not recognize.  They are, however,
     * free to emit a warning if an annotation contains an unrecognized
     * warning name.
     *
     * <p> The string {@code "unchecked"} is used to suppress
     * unchecked warnings. Compiler vendors should document the
     * additional warning names they support in conjunction with this
     * annotation type. They are encouraged to cooperate to ensure
     * that the same names work across multiple compilers.
     * @return the set of warnings to be suppressed
     */
    String[] value();
}

这个注解的主要作用是压制编译告警的,有一个字符串数组的参数。可以在类型、属性、方法、参数、构造函数和局部变量前使用,声明周期是编译期。
比如下面这个方法使用了magic number,使用@SuppressWarnings("MagicNumber")进行压制,在编译期就不会有告警。


二、自定义注解与注解处理器

1. 自定义注解

使用@interface标识CustomeAnnotation是一个注解,表示扩展自java.lang.annotation.Annotation

public @interface CustomeAnnotation {}

然后需要指定注解的作用域与生命周期,使用元注解@Target@Retention来注解CustomeAnnotation

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomeAnnotation {}

上面表示CustomeAnnotation注解只能作用于方法上,并且其所携带的元数据会保留到运行时。
CustomeAnnotation没有携带元数据,意义不大,下面给注解添加参数(元数据)

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

注解中每一个方法就是声明了一个配置参数,方法的名称就是参数的名称,返回值类型就是参数的类型。使用default定义参数默认值。
注解参数的可支持数据类型

  1. 所有基本数据类型(int, float, boolean, byte, double, char, long, short)
  2. String类型
  3. Class类型
  4. enum类型
  5. Annotation类型
  6. 以上所有类型的数组

Annotation类型里面的参数该怎么设定:
第一,只能用public或默认(default)这两个访问权修饰。上面例子把方法设为defaul默认类型;
第二,参数成员只能用基本类型byte,short,char,int,long,float,double,boolean八种基本数据类型和String,Enum,Class,Annotations等数据类型,以及这一些类型的数组。例如,上面例子中参数名就是value,参数类型就是String。
第三,如果只有一个参数成员,最好把参数名称设为 value,后加小括号。
使用的时候通过value = <你的元数据>传递参数,如果只有一个参数,可以不写参数名,如果有多个,则需要显示声明参数名。

public class AnnotationTest{

    @CustomeAnnotation("This is main function")
    public static void main(String[] args) {
        try {
            Class cls = AnnotationTest.class;
            Method method = cls.getMethod("main", String[].class);
            CustomeAnnotation anno = method.getAnnotation(CustomeAnnotation.class);
            System.out.println(anno.value());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

打印:
This is main function

上面的demo,CustomeAnnotation的生命周期必须是RUNTIME,否则获取到的anno为null。

2. 自定义注解处理器

先创建一个Java library,必须为java库,不然会找不到javax包下的相关资源。



定义一个注解处理器 CustomProcessor ,每一个处理器都是继承于AbstractProcessor,并要求必须复写 process() 方法,通常我们使用复写以下4个方法。

/**
 * 每一个注解处理器类都必须有一个空的构造函数,默认不写就行
 */
public class CustomProcessor extends AbstractProcessor {
    /**
     * init()方法会被注解处理器工具调用,并输入ProcessingEnvironment参数。
     * ProcessingEnvironment 提供很多有用的工具类Elements,Types和Filter
     * @param processingEnvironment 提供给process用来访问工具框架的环境
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        System.out.print("init");
    }

    /**
     * 相当于每个处理器的主函数main(),在这里写扫描、评估和处理注解的代码,以及自定义处理逻辑。
     * 输入参数RoundEnvironment可以查询出包含特定注解的被注解元素
     * @param set 请求处理注解类型
     * @param roundEnvironment 有关当前和以前的信息环境
     * @return 返回true,则这些注解已声明并且不要求后续Processor处理它们;
     *          返回false,则这些注解未声明并且可能要求后续Processor处理它们;
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        System.out.print("process");
        return false;
    }

    /**
     * 这里必须指定,这个注解处理器是注册给那个注解的。
     * 注意:它的返回值是一个字符串的集合,包含本处理器想要处理注解的注解类型的合法全程。
     * @return 注解器所支持的注解类型集合,如果没有这样的类型,则返回一个空集合。
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> supportedAnnotations = new LinkedHashSet<>();
        supportedAnnotations.add(ClassAnnotation.class.getCanonicalName());
        supportedAnnotations.add(RuntimeAnnotation.class.getCanonicalName());
        supportedAnnotations.add(SourceAnnotation.class.getCanonicalName());
        return supportedAnnotations;
    }

    /**
     * 指定Java版本,通常这里使用SourceVersion.latestSupported(),
     * 默认返回SourceVersion.RELEASE_6
     * @return 使用的Java版本
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}

也可以使用注解的方式来指定Java版本和注解类型,

@SupportedAnnotationTypes({
        "com.example.annotation.ClassAnnotation",
        "com.example.annotation.RuntimeAnnotation",
        "com.example.annotation.SourceAnnotation"
})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class CustomProcessor extends AbstractProcessor {...}

接下来注册注解处理器。

  1. 在main 目录下新建 resources 资源文件夹;
  2. 在 resources文件夹下建立 META-INF/services 目录文件夹;
  3. 在 META-INF/services 目录文件夹下创建 javax.annotation.processing.Processor 文件;
  4. 在 javax.annotation.processing.Processor 文件写入注解处理器的全称,包括包路径;



    javax.annotation.processing.Processor文件如下配置,

com.example.processor.CustomProcessor

这个注册配置看起来比较麻烦,所以google提供了插件快速配置。注解处理器module的build.gradle中配置

android {
    ...
}

dependencies {
    annotationProcessor('com.google.auto.service:auto-service:1.0')
    implementation('com.google.auto.service:auto-service-annotations:1.0')
}

然后自定义注解处理器使用@AutoService注解

// 注意Processor包名
@AutoService(javax.annotation.processing.Processor.class)
@SupportedAnnotationTypes({
        "com.example.annotation.ClassAnnotation",
        "com.example.annotation.RuntimeAnnotation",
        "com.example.annotation.SourceAnnotation"
})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class CustomProcessor extends AbstractProcessor {...}

这样就不用上面哪些注册配置了。

3. 自定义注解处理器的使用

在需要的module的build.gradle中加上自定义处理器module的依赖,注意使用的是annotationProcessor

annotationProcessor(project(":lib_annotation_processor"))

sync之后编译module,会在build窗口中看见打印,如果没有打印,执行clean -> make project


4. init方法详解

/**
 * init()方法会被注解处理器工具调用,并输入ProcessingEnvironment参数。
 * ProcessingEnvironment 提供很多有用的工具类Elements,Types和Filter
 * @param processingEnvironment 提供给process用来访问工具框架的环境
 */
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
    super.init(processingEnvironment);
    System.out.print("init");
}

当我们编译程序时注解处理器工具会调用此方法并且提供实现ProcessingEnvironment接口的对象作为参数,

方法 说明
Elements getElementUtils() 返回实现Elements接口的对象,用于操作元素的工具类
Filer getFiler() 返回实现Filer接口的对象,用于创建文件、类和辅助文件
Messager getMessager() 返回实现Messager接口的对象,用于报告错误信息、警告提醒
Map getOptions() 返回指定的参数选项
Types getTypeUtils() 返回实现Types接口的对象,用于操作类型的工具类

Element是一个接口,表示一个程序元素,比如包、类或者方法。

5. process方法详解

注解处理过程是一个有序的循环过程。在每次循环中,一个处理器可能被要求去处理那些在上一次循环中产生的源文件和类文件中的注解。第一次循环的输入是运行此工具的初始输入。这些初始输入,可以看成是虚拟的第0次的循环的输出。这也就是说我们实现的process方法有可能会被调用多次,因为我们生成的文件也有可能会包含相应的注解。例如,我们的源文件为SourceActivity.class,生成的文件为Generated.class,这样就会有三次循环,第一次输入为SourceActivity.class,输出为Generated.class;第二次输入为Generated.class,输出并没有产生新文件;第三次输入为空,输出为空。

/**
 * 相当于每个处理器的主函数main(),在这里写扫描、评估和处理注解的代码,以及自定义处理逻辑。
 * 输入参数RoundEnvironment可以查询出包含特定注解的被注解元素
 * @param set 请求处理注解类型
 * @param roundEnvironment 有关当前和以前的信息环境
 * @return 返回true,则这些注解已声明并且不要求后续Processor处理它们;
 *          返回false,则这些注解未声明并且可能要求后续Processor处理它们;
 */
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    System.out.print("process");
    return false;
}

我们可以通过RoundEnvironment接口获取注解元素。process方法会提供一个实现RoundEnvironment接口的对象。

方法 说明
boolean processingOver() 如果循环处理完成返回true,否则返回false
Set<? extends Element> getRootElements() 获取每个处理循环需要处理的元素集合
Set<? extends Element> getElementsAnnotatedWith(TypeElement a) 返回被指定注解类型注解的元素集合
Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> a) 返回被指定注解类型注解的元素集合
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容