Java基础注解(二)

注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制。注解是一种元数据,注解本身对被标注的代码没有任何直接的影响,它为程序提供了一些额外的数据信息,注解通常使用在如下场景

  • 为编译器提供信息:编译器可通过注解去探测错误或者压制警告,如@Deprecated @SuppressWarnings("unchecked")
  • 编译时处理:在程序编译时,一些软件工具可通过程序上标注的注解信息去生成一些代码,如Project Lombok的@Getter @Setter
  • 运行时处理:有些注解可以在运行时保留,经常结合反射API执行一些逻辑处理。

使用示例

代码中通过 @ 字符标注一个注解,紧随其后的是注解名,如@Override,Override是注解名

@Override
void mySuperMethod() { 
  ... 
}

注解可以包含一些属性,使用时属性可以命名或者匿名,属性有对应的值

Java
@Author(
   name = "Benjamin Franklin",
   date = "3/27/2003"
)
class MyClass {
  ... 
}
@SuppressWarnings(value = "unchecked")
void myMethod() {
  ... 
}

当注解只有一个命名属性时,使用时则可以省略属性名赋值

@SuppressWarnings("unchecked")
void myMethod() {
  ... 
}

当注解没有任何属性时,圆括号则可以省略

@Override
void mySuperMethod() { 
  ... 
}

注解可以应用在Java类、方法、字段以及某些程序单元上

// 应用在局部变量上
myString = (@NonNull String) str
// 应用在类声明上
@RestController
public class ContainerInstanceController {
}
// 多个注解应用在方法上
@AssistAnnotation
@RateLimiterAnnotation("CreateContainerGroup")
@RequestMapping(params = {"Action=CreateContainerGroup"})
public CreateContainerGroupResp createContainerInstance(@ContainerGroupParam CreateKciParam param) {
    return containerInstanceService.createContainerInstance(param);
}
// 应用在字段声明上
@SkipMappingValueAnnotation
private String podUUID;

注解定义

在Java中通过@interface来声明一个注解类型

public @interface 注解名称{
    属性类型 属性名称() default 属性默认值;
}

如自定义注解RateLimiterAnnotation

public @interface RateLimiterAnnotation {

    String value();

    long ttl() default 0L;
}

由于interface在Java中是用于定义接口的,注解通过在interface前添加@进行声明,因此其属性必须是方法式签名,而不是普通的属性字段,如果属性有默认值则通过紧随其后的default关键字定义。在Java中注解是一种特殊类型的interface,其Class中方法isInterface一定返回true。

public static void main(String[] args) {
    System.out.println(RateLimiterAnnotation .class.isAnnotation()); // true
    System.out.println(RateLimiterAnnotation .class.isInterface()); // true
}

所有的注解都默认隐式继承java.lang.annotation.Annotation

元注解

应用到其它注解上的注解则称为元注解,这些元注解主要定义在java.lang.annotation中,主要包括:

  • @Retention
  • @Target
  • @Inherited
  • @Documented
  • @Repeatable

元注解就是用来约束注解的注解,@Documented注解通常用于控制Javcdoc对注解类型的API文档生成,不用关注,定义注解时添加上就行。

保留策略

@Retention定义注解保留策略(生命周期概念)

annotation

  • RetentionPolicy.SOURCE 只保留在java源文件中,编译器编译后,class文件中不会保留。

  • RetentionPolicy.CLASS 默认值,保留在class文件中,但是JVM运行时被忽略。

  • RetentionPolicy.RUNTIME 保留在class文件中,JVM运行时也保留。

分别创建三种保留策略的注解,进行验证

@Retention(RetentionPolicy.SOURCE)
public @interface RetentionSourceAnnotation {
}

@Retention(RetentionPolicy.CLASS)
public @interface RetentionClassAnnotation {
}

@Retention(RetentionPolicy.RUNTIME)
public @interface RetentionRuntimeAnnotation {
}

创建个测试类,将三个注解都作用与类中的name字段上

public class TestAnnotationRetentionDemo {

    @RetentionSourceAnnotation
    @RetentionClassAnnotation
    @RetentionRuntimeAnnotation
    private String name;
}

编译TestAnnotationRetentionDemo源代码

javac -d target/classes -sourcepath src/main/java \
src/main/java/com/example/train/annotation/TestAnnotationRetentionDemo.java

通过idea等工具查看生成的字节码文件TestAnnotationRetentionDemo.class

public class TestAnnotationRetentionDemo {
    @RetentionRuntimeAnnotation
    @RetentionClassAnnotation
    private String name;
}    

字节码文件中,没有出现保留策略为RetentionPolicy.SOURCE的注解RetentionSourceAnnotation,那如何验证@RetentionRuntimeAnnotation@RetentionClassAnnotation的区别?

  • 反编字节码文件

    javap -v target/classes/com/example/train/annotation/TestAnnotationRetentionDemo.class
    

    annotation

  • 运行时通过反射验证
    运行main方法,此时JVM运行起来了,通过反射获取字段name上的注解信息并打印

    public static void main(String[] args) throws NoSuchFieldException {
        // 通过反射获取字段信息
        Field name = TestAnnotationRetentionDemo.class.getDeclaredField("name");
        // 获取作用于name字段上的全部注解信息
        Annotation[] annotations = name.getDeclaredAnnotations();
        if (annotations != null && annotations.length > 0) {
            for (Annotation annotation : annotations) {
                System.out.println(annotation.toString());
            }
        }
    }
    

    最终只输出了 @com.example.train.annotation.RetentionRuntimeAnnotation()

三种保留策略应用场景是什么?

  • RetentionPolicy.SOURCE
    这种保留策略的注解使用很广泛,经常用于开发一些工具插件、Java类库,作用就是用来自动生成代码。
@Retention(RetentionPolicy.SOURCE)
public @interface Setter {
}

将注解Setter应用到如下包含非常多属性字段的Student类上,我们不希望直接在源代码中直接编写大量的setXxx方法,那样不容易维护,代码量也非常庞大。

@Setter
public class Student {

    private String name;

    private int age;
    
    ...
}

我们希望使用Javac编译代码后,字节码文件中能自动生成每个字段的set方法,如下Student.class内容代码。

public class Student {

    private String name;

    private int age;
    
    ...

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        this.age = age;
    }
    
    ...
}

在Java中有一种注解处理技术,Annotation Processing Tool(APT),它可以在编译期基于源代码文件,生成新的代码内容,编译完成后最终输出满足要求的字节码文件(偷梁换柱)。基于javax.annotation.processing.AbstractProcessor编写一个自定义的注解处理器Processor,在javac编译代码时,将编写好的注解处理器进行注册使用即可,使用这种注解技术的框架类库非常多,如JPAProject Lombok

参考资料:

https://docs.oracle.com/javase/8/docs/api/javax/annotation/processing/Processor.html

https://www.baeldung.com/java-annotation-processing-builder

  • RetentionPolicy.CLASS
    那这种保留策略的注解使用范围肯定比RetentionPolicy.SOURCE更广,除了上面说的通过API自动生成代码,这种类型注解还保留在class文件中,因此可以通过编写自定义ClassLoader来定制化处理字节码内容,最终生成Class元数据。

annotation

  • RetentionPolicy.RUNTIME
    通常我们定义的处理业务逻辑的注解都应该是这种保留策略,因为应用必须在JVM中运行起来了,才能实现我们的逻辑处理。

作用范围

@Target用于约束注解的使用范围,即可以标注在哪些目标元素上。

annotation

Target的作用范围,请参考java.lang.annotation.ElementType

  • 如果定义注解时不指定Target,则注解可以作用于类中各个元素上
  • 常用的Target取值是类、构造方法、普通方法、字段
  • 元注解是用来约束其它注解的,因此元注解的作用目标范围是注解类型,即ElementType.ANNOTATION_TYPE

定义注解时,使用元注解@Target限定了作用范围,那编译器会严格检查,不符合时会编译错误。

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

TargetMethodAnnotation的作用目标是方法上,直接作用于Class上,会编译错误。

// 编译错误,@TargetMethodAnnotation not applicable to type
@TargetMethodAnnotation
public class TestAnnotationTargetDemo {
}

注解的Target应该使用ElementType.Type,才能作用于类上,或者Target指定多个目标范围。

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface TargetMethodAnnotation {
}

继承

@Inherited标记的注解表明注解类型可以从父类继承,通过Class中方法getAnnotations方法获取从父类中继承的注解。

// 有@Inherited注解,表示该注解可以从父类继承
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface InheritedAnnotation {
}
// 不可继承的注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface NonInheritedAnnotation {
}

将两个注解分别作用于父类声明上

// 父类
@InheritedAnnotation
@NonInheritedAnnotation
public class TestAnnotationInheritedParent {
}

public class TestAnnotationInheritedChild extends TestAnnotationInheritedParent {
}

通过反射获取TestAnnotationInheritedChild上注解,注意: 反射是JVM运行中才能用的技术,因此注解的保留策略必须是Retention.RUNTIME

// com.example.train.annotation.TestAnnotationInheritedChild
public static void main(String[] args) {
    // 通过.class语法获取Class对象
    Class clazz = TestAnnotationInheritedChild.class;
    // 获取注解,包括从父类继承的注解
    Annotation[] annotations = clazz.getAnnotations();
    if (annotations != null && annotations.length > 0) {
        for (Annotation annotation : annotations) {
            System.out.println(annotation.toString());
        }
    }
}

程序最终输出 @com.example.train.annotation.InheritedAnnotation()@Inherited类型的注解可以从直接父类或者间接父类中继承。

重复注解

通过元注解@Repeatable修饰的注解,可以多次作用于某个具体的元素类型上。

场景:自定义一个定时调度任务的注解@Schedule,现在有个任务期望它周期性的在每月第一天执行,同时天23点执行。

public class TaskSchedule {
    
    @Schedule(dayOfMonth = 1) // 每月第一天执行
    @Schedule(hour = 23) // 每天23点执行
    public void autoCleanResource() {
        
    }
}

@Schedule注解定义如下

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
@Inherited
public @interface Schedule {
    int dayOfMonth() default 1;
    int hour() default 0;
}

上面这个注解@Schedule就是一个可重复作用于方法上的注解,这种场景在应用开发中非常常见,在Java8之前,编译器会自动处理,但是Java8之后要求严格,必须自定义一个容器注解来存储可重复作用的注解,否则编译TaskSchedule类时,编译器会报错:Schedule dose not have a valid Repeatable annotation

annotation

如何定义容器注解? 通常按规范在Schedule名称后以复数形式命名,即Schedules,注意@Retention、@Target必须与注解@Schedule保持一致,然后内部有一个value属性,类型则是@Schedule的数组形式。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
@Inherited
public @interface Schedules {
    Schedule[] value();
}

然后修改@Shcedule注解,通过元注解@Repeatable明确其容器注解是@Schedules

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(Schedules.class)
@Documented
@Inherited
public @interface Schedule {
    int dayOfMonth() default 1;
    int hour() default 0;
}

TaskSchedule此时编译不会有任何错误提示。

public class TaskSchedule {

    @Schedule(dayOfMonth = 1)
    @Schedule(hour = 23)
    public void autoCleanResource() {

    }
}

查看编译后的代码TaskSchedule.class,内容如下

public class TaskSchedule {
    public TaskSchedule() {
    }

    @Schedules({@Schedule(
    dayOfMonth = 1
), @Schedule(
    hour = 23
)})
    public void autoCleanResource() {
    }
}

自定义注解实践

在应用开发中,我们经常定义的注解保留策略通常是RetentionPolicy.RUNTIME,然后在程序运行过程中,通过反射API中的一些方法可以检索获取类、方法或字段上的注解信息,获取注解信息的API方法主要由java.lang.reflect.AnnotatedElement接口定义,而Class、Field、Method则是AnnotatedElement的方法实现者。

annotation

获取元素注解API

获取注解相关的主要方法

// 获取元素直接声明的注解,不包括继承的注解信息
Annotation[] getDeclaredAnnotations();

// 获取元素注解,包括从父类继承的注解信息
Annotation[] getAnnotations();

// 获取元素指定类型的注解,包括从父类继承的注解信息
<T extends Annotation> T getAnnotation(Class<T> annotationClass);

// 获取元素直接指定类型的注解,不包括从父类继承的注解信息
default <T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass){
  ...
}
// 这个方法与getAnnotation区别就是Repeatable注解,该方法会去容器注解查找
// 而getAnnotation(Schedule.class)直接返回null
default <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass) {
  ...
}

// 与getDeclaredAnnotation类似,多了Repeatable注解的查找
default <T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass) {
  ...
}

// 判断元素上是否存在指定类型的注解,包括父类上注解
default boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {
  ...
}

现在主要验证下getAnnotation(Class annotationClass)与getAnnotationsByType(Class annotationClass),前面的TaskSchedule类进行测试。

public class TestAnnotationReflectApi {

    public static void main(String[] args) throws NoSuchMethodException {
        // 通过.class获取Class对象
        Class clazz = TaskSchedule.class;
        // 获取autoCleanResource方法信息
        Method method = clazz.getDeclaredMethod("autoCleanResource");
        System.out.println("====getDeclaredAnnotations 打印开始");
        Annotation[] annotations = method.getDeclaredAnnotations();
        print(annotations);
        System.out.println("====getDeclaredAnnotations 打印结束");
        System.out.println("====getAnnotation 开始打印");
        Schedule schedule = method.getAnnotation(Schedule.class);
        if (Objects.nonNull(schedule)) {
            System.out.println(schedule.toString());
        }
        System.out.println("====getAnnotation 打印结束");
        annotations = method.getAnnotationsByType(Schedule.class);
        System.out.println("====getAnnotationsByType 打印开始");
        print(annotations);
        System.out.println("====getAnnotationsByType 打印结束");
    }

    private static void print(Annotation[] annotations) {
        if (annotations != null && annotations.length > 0) {
            for (Annotation annotation : annotations) {
                System.out.println(annotation.toString());
            }
        }
    }
}

程序运行后,输出如下日志

====getDeclaredAnnotations 打印开始
@com.example.train.annotation.Schedules(value=[@com.example.train.annotation.Schedule(hour=0, dayOfMonth=1), @com.example.train.annotation.Schedule(hour=23, dayOfMonth=1)])
====getDeclaredAnnotations 打印结束
====getAnnotation 开始打印
====getAnnotation 打印结束
====getAnnotationsByType 打印开始
@com.example.train.annotation.Schedule(hour=0, dayOfMonth=1)
@com.example.train.annotation.Schedule(hour=23, dayOfMonth=1)
====getAnnotationsByType 打印结束

总结:重复注解需要使用getAnnotationsByType方法获取。

自定义注解应用

自定义一个作用于字段上的注解,辅助程序对Bean字段内容长度检查。

@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.FIELD)
public @interface LengthLimitAnnotation {
    int max() default 1000;
    int min() default 0;
}

将LengthLimitAnnotation应用到Container类上

public class Container {
    
    @LengthLimitAnnotation(max = 20, min = 1)
    private String name;
    
    @LengthLimitAnnotation(max = 50, min = 5)
    private String image;
    
    private String restartPolicy;
    
    // get/set方法省略
    ...
}

创建Container实例对象

Container container = new Container();
container.setImage("http://hub.registry.example.com/example/nginx:v1.0");
container.setName("2345436523456543456754323456543234565434565434");

程序通过对象字段上的注解进行属性内容检查,是否在注解约束的范围内。

public class TestLengthLimitAnnotation {

    public static void main(String[] args) throws IllegalAccessException {
        Container container = new Container();
        container.setImage("http://hub.registry.example.com/example/nginx:v1.0");
        container.setName("2345436523456543456754323456543234565434565434");
        // 获取class对象
        Class clazz = container.getClass();
        // 获取对象所有字段
        Field[] fields = clazz.getDeclaredFields();
        if (fields == null || fields.length == 0) {
            return;
        }
        // 检查字段长度是否符合范围
        for (Field f : fields) {
            checkFieldLength(f, container);
        }
    }

    private static void checkFieldLength(Field field, Object obj) throws IllegalAccessException {
        if (Objects.isNull(field)) {
            return;
        }
        // 获取字段上标记的注解LengthLimitAnnotation
        LengthLimitAnnotation limitAnnotation = field.getDeclaredAnnotation(LengthLimitAnnotation.class);
        // 没有注解,表示该字段无须进行内容长度检查
        if (Objects.isNull(limitAnnotation)) {
            return;
        }
        // 如果私有属性你,强制访问
        if (!Modifier.isPublic(field.getModifiers())) {
            field.setAccessible(true);
        }
        // 获取字段值
        String value = (String) field.get(obj);
        int valueLen = 0;
        if (Objects.nonNull(value) && value.length() > 0) {
            valueLen = value.length();
        }
        // 获取字段上注解约束的范围长度
        int max = limitAnnotation.max();
        int min = limitAnnotation.min();
        String name = field.getName();
        // 不满足要求,抛出运行时异常
        if (valueLen > max) {
            throw new RuntimeException(name + " length can't exceed " + max);
        }
        if (valueLen < min) {
            throw new RuntimeException(name + " length can't less than " + min);
        }
    }
}

程序运行,抛出错误

Exception in thread "main" java.lang.RuntimeException: name length can't exceed 20
        at com.example.train.annotation.TestLengthLimitAnnotation.checkFieldLength(TestLengthLimitAnnotation.java:56)
        at com.example.train.annotation.TestLengthLimitAnnotation.main(TestLengthLimitAnnotation.java:26)