如何理解注解?
注解(Annotation)作为一种元数据,由 Java 1.5 开始引入。和 Java 中的注释有所不同,注释是给人阅读的,而注解的信息可以在程序运行时获取到。Java 注解也可以作为 XML 或者 Properties 的补充,如 Spring 3.x 开始引入注解,在配置类中标记了 @Bean 的方法的返回对象可以作为 Spring 中的 Bean 对象。
注解的使用场景有哪些?
注解主要有以下3个使用场景。
- 提供给 Java 编译器使用。例如 Java 编译器在编译时可以检查标记了 @Override 注解的方法的父类中是否具有相同签名的方法,如果没有则会编译失败。
- 提供给注解处理器使用。例如开源框架 lombok 和 mapstruct 就使用了注解处理器。lombok 简化了代码,减少了代码中的 set/get/toString 等方法。而 mapstruct 则将运行时通过反射获取成员成员变量进行 copy 对象的行为提前到了编译器,减少了反射的调用,提高了性能。
- 通过反射在运行期获取注解信息,改变程序行为。如 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]
- 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 还提供了一些其他的注解。具体如下:
注解名称 | 作用目标 | 生效范围 | 作用 |
@Override | METHOD | SOURCE | 父类如果不存在相同签名的方法则编译失败 |
@Deprecated | CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE | RUNTIME | 表示标记的元素已经过时,未来可能会被移除 |
@SuppressWarnings | TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE | SOURCE | 抑制编译器产生的警告 |
@SafeVarargs | CONSTRUCTOR,METHOD | RUNTIME | Java 7 新增,表示构造器或方法不会对变长参数执行不安全的操作 |
@FunctionalInterface | TYPE | RUNTIME | java 8 新增,表示接口为函数式接口 |
注解与反射
注解作为元数据,需要通过反射获取注解的信息。前面的文章 Java 基础知识之 Java 反射 对注解进行了简单说明,这里再次进行总结。
可注解元素具体如下图所示,可注解的元素都实现了接口 AnnotatedElement ,可以看到,包、类、成员变量、成员方法、构造方法、方法参数、类型变量等都可以进行注解。
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)