Java注解使用与自定义

背景

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

什么是注解

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

kotlin 复制代码
 /**
* 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,它没有参数,所以是一个标记注解

less 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

1. 元注解

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

@Target

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

less 复制代码
@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中,

php 复制代码
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

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

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

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

php 复制代码
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)。

Java内置注解

以下基于JDK18

@Override

less 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

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

@Deprecated

less 复制代码
@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

less 复制代码
@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

java 复制代码
public @interface CustomeAnnotation {}

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

less 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomeAnnotation {}

上面表示CustomeAnnotation注解只能作用于方法上,并且其所携带的元数据会保留到运行时。

CustomeAnnotation没有携带元数据,意义不大,下面给注解添加参数(元数据)

less 复制代码
@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 = <你的元数据>传递参数,如果只有一个参数,可以不写参数名,如果有多个,则需要显示声明参数名。

vbnet 复制代码
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个方法。

typescript 复制代码
 /** * 每一个注解处理器类都必须有一个空的构造函数,默认不写就行 */
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版本和注解类型,

scala 复制代码
@SupportedAnnotationTypes({
        "com.lotus.annotation.ClassAnnotation",
        "com.lotus.annotation.RuntimeAnnotation",
        "com.lotus.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.lotus.processor.CustomProcessor

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

scss 复制代码
android {
    ...
}

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

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

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

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

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

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

less 复制代码
annotationProcessor(project(":lib_annotation_processor"))

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

4. init方法详解

java 复制代码
 /** * 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,输出并没有产生新文件;第三次输入为空,输出为空。

typescript 复制代码
 /** * 相当于每个处理器的主函数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) 返回被指定注解类型注解的元素集合
相关推荐
醉颜凉19 分钟前
【NOIP提高组】潜伏者
java·c语言·开发语言·c++·算法
阿维的博客日记23 分钟前
java八股-jvm入门-程序计数器,堆,元空间,虚拟机栈,本地方法栈,类加载器,双亲委派,类加载执行过程
java·jvm
qiyi.sky24 分钟前
JavaWeb——Web入门(8/9)- Tomcat:基本使用(下载与安装、目录结构介绍、启动与关闭、可能出现的问题及解决方案、总结)
java·前端·笔记·学习·tomcat
lapiii35828 分钟前
图论-代码随想录刷题记录[JAVA]
java·数据结构·算法·图论
RainbowSea31 分钟前
4. Spring Cloud Ribbon 实现“负载均衡”的详细配置说明
java·spring·spring cloud
程序员小明z31 分钟前
基于Java的药店管理系统
java·开发语言·spring boot·毕业设计·毕设
爱敲代码的小冰1 小时前
spring boot 请求
java·spring boot·后端
Lyqfor1 小时前
云原生学习
java·分布式·学习·阿里云·云原生
曾经的三心草1 小时前
Mysql之约束与事件
android·数据库·mysql·事件·约束
程序猿麦小七1 小时前
今天给在家介绍一篇基于jsp的旅游网站设计与实现
java·源码·旅游·景区·酒店