重识 Java 注解

简介: 如何理解注解?注解(Annotation)作为一种元数据,由 Java 1.5 开始引入。和 Java 中的注释有所不同,注释是给人阅读的,而注解的信息可以在程序运行时获取到。Java 注解也可以作为 XML 或者 Properties 的补充,如 Spring 3.x 开始引入注解,在配置类中标记了 @Bean 的方法的返回对象可以作为 Spring 中的 Bean 对象。

如何理解注解?

注解(Annotation)作为一种元数据,由 Java 1.5 开始引入。和 Java 中的注释有所不同,注释是给人阅读的,而注解的信息可以在程序运行时获取到。Java 注解也可以作为 XML 或者 Properties 的补充,如 Spring 3.x 开始引入注解,在配置类中标记了 @Bean 的方法的返回对象可以作为 Spring 中的 Bean 对象。


注解的使用场景有哪些?

注解主要有以下3个使用场景。


1.提供给 Java 编译器使用。例如 Java 编译器在编译时可以检查标记了 @Override 注解的方法的父类中是否具有相同签名的方法,如果没有则会编译失败。


2.提供给注解处理器使用。例如开源框架 lombok 和 mapstruct 就使用了注解处理器。lombok 简化了代码,减少了代码中的 set/get/toString 等方法。而 mapstruct 则将运行时通过反射获取成员成员变量进行 copy 对象的行为提前到了编译器,减少了反射的调用,提高了性能。


3.通过反射在运行期获取注解信息,改变程序行为。如 Spring 中 使用注解可以取代 xml 中对 bean 的配置,减少了琐碎的配置,提高了开发效率。


注解的定义语法

本质: Java 注解相关的类所在的包为java.lang.annotation,本质是一个继承了java.lang.annotation.Annotation的接口。


元注解:注解可以用在类、成员变量、方法等,同时注解也可以用在注解上,用在注解上的注解就是元注解。如 @Documented 可用于生成文档。


注解基本语法

注解基本语法如下:


[modifier] @interface AnnotationName {
}


modifier 是可选的访问权限修饰符,和类的访问权限修饰符一样,如 public。

@interface 是 Java 注解定义固定的语法。

AnnotationName 是注解的标识符,遵循 Java 命名规则。

AnnotationBody 是注解体,可以用来定义一些属性。

自定义注解示例如下:


public @interface MyAnnotation {
}


为注解定义属性

为了给注解携带其他信息,需要为注解定义属性。一个注解可以有0个到多个的属性。每个属性的语法如下:


[public] PropertyType PropertyName() [default DefaultValue]
• 1
[public] PropertyType PropertyName() [default DefaultValue]


public 为可选的访问权限修饰符,默认为 public 。

PropertyType为属性的类型。可以使用的类型有:基本类型、String、Class、枚举类型、注解类型、上述类型的数组。


PropertyName 为属性名称,遵循 Java 命名规范,后面的括号为固定的语法。

default 表示属性的默认值,值由 DefaultValue 指定。

示例如下:


public @interface MyAnnotation {
    public String name();
    public int age() default 1;
}


注解目标

定义了上述的注解后,注解仍不能直接使用,因为并未指明注解可以使用的范围,为了达到这个目的,则需要使用 Java 提供的元注解 @Target

使用方式如下:


@Target(ElementType.TYPE)
public @interface MyAnnotation {
    public String name();
    public int age() default 1;
}


其中 @Target 注解又有自己的 类型为 ElementType 的 value 属性。ElementType 源码如下。


public enum ElementType {
    /** 类、接口(包括注解)、枚举 */
    TYPE,
    /** 字段 */
    FIELD,
    /** 方法 */
    METHOD,
    /** 方法参数 */
    PARAMETER,
    /** 构造方法 */
    CONSTRUCTOR,
    /** 本地变量 */
    LOCAL_VARIABLE,
    /** 注解类型 */
    ANNOTATION_TYPE,
    /** 包 */
    PACKAGE,
    /**
     * 类型参数
     *
     * @since 1.8
     */
    TYPE_PARAMETER,
    /**
     * 类型名称
     *
     * @since 1.8
     */
    TYPE_USE
}


注解保留策略

注解除了可以指定使用的目标,还具有自己的生命周期。注解的生命周期使用元注解 @Retention 指定。使用方式如下:


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyAnnotation {
    public String name();
    public int age() default 1;
}


@Retention 有一个类型为 RetentionPolicy 的 value 属性。RetentionPolicy 源码如下:


public enum RetentionPolicy {
    /**
     * 注解保留在源代码中,字节码不包含注解信息
     */
    SOURCE,
    /**
     * 注解保留在源代码和字节码中,运行时无法获取到
     */
    CLASS,
    /**
     * 注解在源代码、字节码、运行时都存在
     */
    RUNTIME
}


注解的继承

注解本身并不可以被继承,这里说的继承是子类继承了使用具有 @Inherited 标记的注解的父类,则子类中可以获取到父类中对应的注解信息。

@Inherited 源码如下:


@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {
}


可以看到,@Inherited 只能用于注解类型。@Inherited 使用示例如下:


public class AnnotationTest {
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Inherited  //表示子类可以继承父类中该注解
    public @interface InheritedAnnotation {
        int value();
    }
    @InheritedAnnotation(1)    //只有在父类中标记的注解才会被继承,接口中标记的不会被继承
    static class Super{
    }
    // 子类继承了使用 @Inherited 标记的注解的父类
    // @InheritedAnnotation(2) 子类可以覆盖父类中的注解
    static class Child extends Super{
    }
    public static void main(String[] args) {
        System.out.println(Child.class.getAnnotation(InheritedAnnotation.class));
    }
}


打印结果如下:


@com.zzuhkp.AnnotationTest$InheritedAnnotation(value=1)

@InheritedAnnotation 注解被 @Inherited 注解标注,父类中标注了 @InheritedAnnotation 注解,子类成功获取了注解信息。


可重复注解

可重复注解是 Java 8 新增的一个特性,相同的注解可用在同一个元素中。在 Java 8 之前,为了达到这样的效果,通常定义一个包含属性名为value,类型为注解数组类型的注解,具体如下:


@MyAnnotations(value = {@MyAnnotation,@MyAnnotation})
public class AnnotationTest {
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface MyAnnotation {
    }
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface MyAnnotations {
        MyAnnotation[] value();
    }
}


Java 8 及之后,添加了元注解 @Repeatable ,@Repeatable的value值为注解的容器,这样标记了 @Repeatable 注解的注解就可以用在相同的元素上,具体如下:


@MyAnnotation
@MyAnnotation
// 标记多个时获取到的是该注解的容器注解
public class AnnotationTest {
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Repeatable(MyAnnotations.class)  //MyAnnotations为MyAnnotation的容器
    public @interface MyAnnotation {
    }
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface MyAnnotations {
        MyAnnotation[] value();
    }
    public static void main(String[] args) {
        Arrays.stream(AnnotationTest.class.getAnnotations()).forEach(System.out::println);
    }
}


程序打印如下:


@com.zzuhkp.AnnotationTest$MyAnnotations(value=[@com.zzuhkp.AnnotationTest$MyAnnotation(), @com.zzuhkp.AnnotationTest$MyAnnotation()])


可以看到,可重复注解只是语法糖,如果标记了多个相同的注解,则获取到的为该注解的容器注解。

将注解改为一个,即


@MyAnnotation
//@MyAnnotation
public class AnnotationTest {
  ...
}


则打印结果为:


@com.zzuhkp.AnnotationTest$MyAnnotation()


如果只标记了一个注解,则获取的直接和标记的注解相同。

标记一个注解和标记多个相同的注解在运行时获取到的实际注解类型有所不同,这点需要进行留意。


Java 内建的注解

除了上述提到用于创建注解的元注解 @Documented、@Inherited、@Retention、@Repeatable ,Java 还提供了一些其他的注解。具体如下:


image.png

image.png


注解与反射

注解作为元数据,需要通过反射获取注解的信息。前面的文章 Java 基础知识之 Java 反射 对注解进行了简单说明,这里再次进行总结。


可注解元素具体如下图所示,可注解的元素都实现了接口 AnnotatedElement ,可以看到,包、类、成员变量、成员方法、构造方法、方法参数、类型变量等都可以进行注解。


image.png

AnnotatedElement 源码如下:


public interface AnnotatedElement {
  // 给定的注解是否存在
    default boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {
        return getAnnotation(annotationClass) != null;
    }
    //获取给定类型的注解
    <T extends Annotation> T getAnnotation(Class<T> annotationClass);    
  //获取所有的注解
    Annotation[] getAnnotations();
  //java 8 新增,根据类型获取可重复注解的数组,如果不是可重复注解或可重复注解只存在一个,则返回长度为1的数组
    default <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass) {
         ... 省略部分代码
     }
  // 获取直接定义给定类型的注解
    default <T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass) {
         ... 省略部分代码
     }
  // java 8 新增,获取给定类型的直接定义的可重复注解数组
    default <T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass) {
        ... 省略部分代码
    }
  //获取直接定义的注解数组
    Annotation[] getDeclaredAnnotations();
}


透过字节码看注解


定义注解如下:


@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyAnnotation {
    String value() default "hello";
}


使用 javap 反编译后的代码如下:


public interface com.zzuhkp.MyAnnotation extends java.lang.annotation.Annotation {
  public abstract java.lang.String value();
}


可以看到,我们自己定义了注解编译后变成了继承 Annotation 的一个接口,并且属性也变成了抽象方法。可以想到,我们获取到的注解实例是 jvm 生成的注解的实现。


注解应用实例

没有使用泛型的情况下,直接根据 AnnotatedElement 类上的方法获取注解即可。如果需要获取泛型中的注解,则需要先获取泛型的类型参数,然后获取类型参数上的注解。示例如下:


public class AnnotationTest {
    @MyAnnotation("commonField")
    private static String commonField;
    @MyAnnotation("genericField")
    private static Map<@MyAnnotation("map key") String, @MyAnnotation("map value") Integer> genericField;
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.FIELD, ElementType.TYPE_USE})
    public @interface MyAnnotation {
        String value() default "hello";
    }
    public static void main(String[] args) throws NoSuchFieldException {
        System.out.println("commonField上的注解如下:");
        Field commonField = AnnotationTest.class.getDeclaredField("commonField");
        Arrays.stream(commonField.getDeclaredAnnotations()).forEach(System.out::println);
        System.out.println("genericField上的注解如下:");
        Field genericField = AnnotationTest.class.getDeclaredField("genericField");
        Arrays.stream(genericField.getDeclaredAnnotations()).forEach(System.out::println);
        Type[] actualTypeArguments = ((ParameterizedType) genericField.getGenericType()).getActualTypeArguments();
        AnnotatedType[] annotatedActualTypeArguments = ((AnnotatedParameterizedType) genericField.getAnnotatedType())
            .getAnnotatedActualTypeArguments();
        for (int i = 0; i < actualTypeArguments.length; i++) {
            System.out.println("genericField 上泛型参数 " + actualTypeArguments[i].getTypeName() + " 上的注解如下:");
            Arrays.stream(annotatedActualTypeArguments[i].getAnnotations()).forEach(System.out::println);
        }
    }
}


打印结果为:


commonField上的注解如下:
@com.zzuhkp.AnnotationTest$MyAnnotation(value=commonField)
genericField上的注解如下:
@com.zzuhkp.AnnotationTest$MyAnnotation(value=genericField)
genericField 上泛型参数 java.lang.String 上的注解如下:
@com.zzuhkp.AnnotationTest$MyAnnotation(value=map key)
genericField 上泛型参数 java.lang.Integer 上的注解如下:
@com.zzuhkp.AnnotationTest$MyAnnotation(value=map value)


目录
相关文章
|
2月前
|
Java Maven 编译器
Java编译器注解运行和自动生成代码问题之@AutoService工作问题如何解决
Java编译器注解运行和自动生成代码问题之@AutoService工作问题如何解决
110 1
|
2月前
|
Java API 编译器
Java编译器注解运行和自动生成代码问题之编译时通过参数设置选项值问题如何解决
Java编译器注解运行和自动生成代码问题之编译时通过参数设置选项值问题如何解决
|
7天前
|
Arthas Java 测试技术
Java字节码文件、组成,jclasslib插件、阿里arthas工具,Java注解
Java字节码文件、组成、详解、分析;常用工具,jclasslib插件、阿里arthas工具;如何定位线上问题;Java注解
Java字节码文件、组成,jclasslib插件、阿里arthas工具,Java注解
|
2天前
|
Java 编译器 测试技术
|
13天前
|
存储 JSON 前端开发
【Java】用@JsonFormat(pattern = “yyyy-MM-dd“)注解,出生日期竟然年轻了一天
在实际项目中,使用 `@JsonFormat(pattern = &quot;yyyy-MM-dd&quot;)` 注解导致出生日期少了一天的问题,根源在于夏令时的影响。本文详细解析了夏令时的概念、`@JsonFormat` 注解的使用方法,并提供了三种解决方案:在注解中添加 `timezone = GMT+8`、修改 JVM 参数 `-Duser.timezone=GMT+08`,以及使用 `timezone = Asia/Shanghai
10 0
【Java】用@JsonFormat(pattern = “yyyy-MM-dd“)注解,出生日期竟然年轻了一天
|
22天前
|
Java
Java系列之 IDEA 为类 和 方法设置注解模板
这篇文章介绍了如何在IntelliJ IDEA中为类和方法设置注解模板,包括类模板的创建和应用,以及两种不同的方法注解模板的创建过程和实际效果展示,旨在提高代码的可读性和维护性。
|
22天前
|
存储 缓存 Java
Java本地高性能缓存实践问题之使用@CachePut注解来更新缓存中数据的问题如何解决
Java本地高性能缓存实践问题之使用@CachePut注解来更新缓存中数据的问题如何解决
|
22天前
|
Java 编译器 开发者
【Java 第八篇章】注解
从JDK5起,Java引入注解作为元数据支持,区别于注释,注解可在编译、类加载和运行时被读取处理。注解允许开发者在不影响代码逻辑的前提下嵌入补充信息。核心概念包括`Annotation`接口、`@Target`定义适用范围如方法、字段等,`@Retention`设定生命周期,如仅存在于源码或运行时可用。Java提供了内置注解如`@Override`用于检查方法重写、`@Deprecated`标记废弃元素、`@SuppressWarnings`抑制警告。自定义注解可用于复杂场景,例如通过反射实现字段验证。
13 0
|
2月前
|
编译器 Java
Java编译器注解运行和自动生成代码问题之重写init方法的问题如何解决
Java编译器注解运行和自动生成代码问题之重写init方法的问题如何解决
|
2月前
|
Java 测试技术 Maven
Java编译器注解运行和自动生成代码问题之在编译时需要设置-proc:none参数问题如何解决
Java编译器注解运行和自动生成代码问题之在编译时需要设置-proc:none参数问题如何解决