• 35. 【Java教程】注解


    本小节我们将学习 Java5 引入的一种机制 —— 注解(Annotation)。通过本小节的学习,你将了解什么是注解注解的作用Java内置注解有哪些以及注解的分类,如何自定义注解,如何处理注解等内容。

    1. 什么是注解

    Java 注解(Annotation)又称为 Java 标注,是 Java5开始支持加入源代码的特殊语法元数据。Java 语言中的类、方法、变量、参数和包等都可以被标注。Java 标注可以通过反射获取标注的内容。在编译器生成class文件时,标注可以被嵌入到字节码中。Java 虚拟机可以保留标注内容,在运行时可以获取到标注内容。

    注解是一种用于做标注的“元数据”,什么意思呢?你可以将注解理解为一个标签,这个标签可以标记类、方法、变量、参数和包。

    回想我们学习继承时,子类若重写了父类的方法,可以在子类重写的方法上使用@Override注解:

    @Override 注解标注在子类重写的方法上,可检查该方法是否正确地重写了父类的方法,如有错误将会编译报错。

    2. 注解的作用

    2.1 内置的注解

    我们先看一下 Java 提供了哪些内置注解,以及这些注解的作用。(大致了解即可)

    Java 定义了一套注解,共有 10 个,5 个在 java.lang 包中,剩下 5 个在 java.lang.annotation 包中。

    2.1.1 用在代码的注解
    • @Override:检查该方法是否正确地重写了父类的方法。如果重写错误,会报编译错误;

    • @Deprecated:标记过时方法。如果使用该方法,会报编译警告;

    • @SuppressWarnings:指示编译器去忽略注解中声明的警告;

    • @SafeVarargs:Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告;

    • @FunctionalInterface:Java 8 开始支持,标识一个匿名函数或函数式接口。

    2.1.2 用在其他注解的注解

    此类注解也称为元注解(meta annotation),在下面学习定义注解的时候,我们将会详细讲解。

    • @Retention:标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问;

    • @Documented:标记这些注解是否包含在用户文档中;

    • @Target:标记这个注解应该是哪种 Java 成员;

    • @Inherited:标记这个注解是继承于哪个注解类;

    • @Repeatable:Java 8 开始支持,标识某注解可以在同一个声明上使用多次。

    2.2 分类

    Java 注解可以分为 3 类:

    1. 由编译器使用的注解:如@Override@Deprecated@SupressWarnings等;

    2. 由工具处理.class文件使用的注解:比如有些工具会在加载class的时候,对class做动态修改,实现一些特殊的功能。这类注解会被编译进入.class文件,但加载结束后并不会存在于内存中。这类注解只被一些底层库使用,一般我们不必自己处理;

    3. 在程序运行期间能够读取的注解:它们在加载后一直存在于JVM中,这也是最常用的注解。

    3. 定义注解

    学会使用注解非常简单,很多框架都会提供丰富的注解文档(例如 Spring)。但关键的一点在于定义注解,知道如何定义注解,才能看懂别人定义的注解。

    下面我们来定义一个注解。

    想要定义一个注解,通常可分为 3 步:

    1. 创建注解;

    2. 定义注解的参数和默认值;

    3. 用元注解配置注解。

    关于这 3 个步骤是什么意思,如何来做,我们下面将来详细讲解。

    3.1 创建注解

    注解通过@interface关键字来定义。例如,我们想要定义一个可用于检查字符串长度的注解,实例如下:

    1. public @interface Length {
    2. }

    Tips:通过 @interface 关键字定义注解,通过关键字 interface定义接口。注意两者不要混淆。

    IDEA中,我们可以在新建 Java 类的时候,选择新建注解:

    输入我们要定义的注解的名称(遵循类命名规范),即可创建一个注解:

    3.2 定义参数和默认值

    注解创建完成后,可以向注解添加一些要接收的参数,下面我们为@Length注解添加 3 个参数:

    1. public @interface Length {
    2. int min() default 0;
    3. int max() default Integer.MAX_VALUE;
    4. String message() default "长度不合法";
    5. }

    注解的参数类似无参数方法。另外参数的类型可以是基本数据类型、String类型、枚举类型、Class类型、Annotation类型以及这些类型的数组。

    如果注解中只有一个参数,或者这个参数是最常用的参数,那么应将此参数命名为value。在调用注解时,如果参数名称是value,且只有一个参数,那么可以省略参数名称。(由于此注解没有最常用特征的参数,没有使用value

    可以使用default关键字来指定参数的默认值,推荐为每个参数都设定一个默认值。

    3.3 用元注解配置注解

    在前面学习 Java 内置的注解的时候,我们已经了解了元注解,元注解就是用于修饰其他注解的注解。

    通常只需使用这些内置元注解,就可以基本满足我们自定义注解的需求。下面我们将会详解 Java 内置的 5 个元注解,你将会了解为什么需要这些元注解。

    3.3.1 @Retention

    Retention译为保留。@Retention注解定义了一个注解的生命周期(我们前面对于 Java 注解的分类,就是通过其生命周期来划定界限的)。它可以有如下几种取值:

    • RetentionPolicy.SOURCE:注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视;

    • RetentionPolicy.CLASS:注解只被保留到编译进行的时候,它并不会被加载到 JVM 中;

    • RetentionPolicy.RUNTIME:注解可以保留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们。

    下面我们使用@Retention注解来指定我们自定义的注解@Length的生命周期,实例如下:

    1. import java.lang.annotation.Retention;
    2. import java.lang.annotation.RetentionPolicy;
    3. @Retention(RetentionPolicy.RUNTIME)
    4. public @interface Length {
    5. int min() default 0;
    6. int max() default Integer.MAX_VALUE;
    7. String message() default "长度不合法";
    8. }

    上面的代码中,我们指定 @Length 注解可以在程序运行期间被获取到。

    3.3.2 @Documented

    这个元注解的作用很简单,标注了此注解的注解,能够将注解中的元素包含到 Javadoc 中去。因此不做过多解释。

    3.3.3 @Target

    @Target 注解是最为常用的元注解,我们知道注解可以被应用于类、方法、变量、参数和包等处,@Target 注解可以指定注解能够被应用于源码中的哪些位置,它可以有如下几种取值:

    • ElementType.ANNOTATION_TYPE:可以给一个注解进行注解;

    • ElementType.CONSTRUCTOR:可以给构造方法进行注解;

    • ElementType.FIELD:可以给属性进行注解;

    • ElementType.LOCAL_VARIABLE:可以给局部变量进行注解;

    • ElementType.METHOD:可以给方法进行注解;

    • ElementType.PACKAGE:可以给一个包进行注解;

    • ElementType.PARAMETER:可以给一个方法内的参数进行注解;

    • ElementType.TYPE:可以给一个类型进行注解,比如类、接口、枚举。

    例如,我们定义注解@Length只能用在类的属性上,可以添加一个@Target(ElementType.FIELD)

    1. import java.lang.annotation.ElementType;
    2. import java.lang.annotation.Retention;
    3. import java.lang.annotation.RetentionPolicy;
    4. import java.lang.annotation.Target;
    5. @Retention(RetentionPolicy.RUNTIME)
    6. @Target(ElementType.FIELD)
    7. public @interface Length {
    8. int min() default 0;
    9. int max() default Integer.MAX_VALUE;
    10. String message() default "长度不合法";
    11. }

    @Target注解的参数也可以接收一个数组。例如,定义注解@Length可以用在属性或局部变量上:

    1. import java.lang.annotation.ElementType;
    2. import java.lang.annotation.Retention;
    3. import java.lang.annotation.RetentionPolicy;
    4. import java.lang.annotation.Target;
    5. @Retention(RetentionPolicy.RUNTIME)
    6. @Target({ElementType.FIELD, ElementType.LOCAL_VARIABLE})
    7. public @interface Length {
    8. int min() default 0;
    9. int max() default Integer.MAX_VALUE;
    10. String message() default "长度不合法";
    11. }

    至此,我们就完成了 @Length 注解的定义。下面,我们再来看下剩余的两个元注解。

    3.3.4 @Inherited

    使用@Inherited定义子类是否可继承父类定义的注解。@Inherited仅针对@Target(ElementType.TYPE)类型的注解有效,并且仅针对类的继承有效,对接口的继承无效:

    1. @Inherited
    2. @Target(ElementType.TYPE)
    3. public @interface TestAnnotation {
    4. String value() default "test";
    5. }

    在使用的时候,如果一个类用到了@TestAnnotation

    1. @TestAnnotation("测试注解")
    2. public class Pet {
    3. }

    则它的子类默认也定义了该注解:

    1. public class Cat extends Pet {
    2. }
    3.3.5 @Repeatable

    使用@Repeatable这个元注解可以定义注解是否可重复。

    例如,一个注解用于标注一个人的角色,他可以是学生,也可以是生活委员。

    1. @Target(ElementType.TYPE)
    2. @Repeatable(Roles.class)
    3. public @interface Role {
    4. String value() default "";
    5. }
    6. @Target(ElementType.TYPE)
    7. public @interface Roles {
    8. Role[] value();
    9. }

    @Repeatable 元注解标注了@Role。而 @Repeatable 后面括号中的类相当于一个容器注解,按照规定,它里面必须要有一个 value 的属性,属性类型是一个被 @Repeatable 注解过的注解数组。

    经过@Repeatable修饰后,在某个类型声明处,就可以添加多个@Role注解:

    1. @Role("学生")
    2. @Role("生活委员")
    3. public class Student {
    4. }

    4. 处理注解

    4.1 尝试使用注解

    我们已经完成了@Length注解的定义:

    1. import java.lang.annotation.ElementType;
    2. import java.lang.annotation.Retention;
    3. import java.lang.annotation.RetentionPolicy;
    4. import java.lang.annotation.Target;
    5. @Retention(RetentionPolicy.RUNTIME)
    6. @Target({ElementType.FIELD, ElementType.LOCAL_VARIABLE})
    7. public @interface Length {
    8. int min() default 0;
    9. int max() default Integer.MAX_VALUE;
    10. String message() default "长度不合法";
    11. }

    现在就可以在字段上标注这个注解了:

    1. public class Student {
    2. // 标注注解
    3. @Length(min = 2, max = 5, message = "昵称的长度必须在2~5之间")
    4. private String nickname;
    5. public Student(String nickname) {
    6. this.setNickname(nickname);
    7. }
    8. public String getNickname() {
    9. return nickname;
    10. }
    11. public void setNickname(String nickname) {
    12. this.nickname = nickname;
    13. }
    14. public static void main(String[] args) {
    15. // 实例化对象
    16. Student student = new Student("我的名字很长很长");
    17. System.out.println(student.getNickname());
    18. }
    19. }

    上面代码中,我们在Student类中的nickname字段上标注了@Length注解,限定了其长度。

    那么现在,是不是将其标注在字段上面就可以自动检查字段的长度了呢?答案是否定的。

    运行过程如下,昵称长度不合法,但并没有抛出任何异常:

    Java 的注解本身对代码逻辑没有任何影响,它只是一个标注。想要检查字段的长度,就要读取注解,处理注解的参数,可以使用反射机制来处理注解。

    4.2 通过反射读取注解

    我们先来学习一下通过反射读取注解内容相关的 API

    通过反射,判断某个注解是否存在于ClassFieldMethodConstructor

    • Class.isAnnotationPresent(Class)

    • Field.isAnnotationPresent(Class)

    • Method.isAnnotationPresent(Class)

    • Constructor.isAnnotationPresent(Class)

    isAnnotationPresent()方法的返回值是布尔类型,例如,判断Student类中的nickname字段上,是否存在@Length注解:

    boolean isLengthPresent = Student.class.getDeclaredField("nickname").isAnnotationPresent(Length.class);
    

    通过反射,获取 Annotation 对象:

    使用反射 API 读取Annotation:

    • Class.getAnnotation(Class)

    • Field.getAnnotation(Class)

    • Method.getAnnotation(Class)

    • Constructor.getAnnotation(Class)

    例如,获取nickname字段标注的@Length注解:

    Length annotation = Student.class.getDeclaredField("nickname").getAnnotation(Length.class);
    

    通过反射读取注解的完整实例如下:

    1. public class Student {
    2. // 标注注解
    3. @Length(min = 2, max = 5, message = "昵称的长度必须在2~6之间")
    4. private String nickname;
    5. public Student(String nickname) {
    6. this.setNickname(nickname);
    7. }
    8. public String getNickname() {
    9. return nickname;
    10. }
    11. public void setNickname(String nickname) {
    12. this.nickname = nickname;
    13. }
    14. public static void main(String[] args) throws NoSuchFieldException {
    15. boolean isLengthPresent = Student.class.getDeclaredField("nickname").isAnnotationPresent(Length.class);
    16. if (isLengthPresent) {
    17. Length annotation = Student.class.getDeclaredField("nickname").getAnnotation(Length.class);
    18. // 获取注解的参数值
    19. int min = annotation.min();
    20. int max = annotation.max();
    21. String message = annotation.message();
    22. // 打印参数值
    23. System.out.println("min=" + min);
    24. System.out.println("max=" + max);
    25. System.out.println("message=" + message);
    26. } else {
    27. System.out.println("没有在nickname字段上找到@Length注解");
    28. }
    29. }
    30. }

    运行结果:

    1. min=2
    2. max=5
    3. message=昵称的长度必须在2~6之间

    运行过程如下:

    4.3 编写校验方法

    获取到了注解以及其内容,我们就可以编写一个校验方法,来校验字段长度是否合法了。我们在Student类中新增一个checkFieldLength()方法,用于检查字段长度是否合法,如果不合法则抛出异常。

    完整实例如下:

    1. import java.lang.reflect.Field;
    2. public class Student {
    3. // 标注注解
    4. @Length(min = 2, max = 5, message = "昵称的长度必须在2~5之间")
    5. private String nickname;
    6. public Student(String nickname) {
    7. this.setNickname(nickname);
    8. }
    9. public String getNickname() {
    10. return nickname;
    11. }
    12. public void setNickname(String nickname) {
    13. this.nickname = nickname;
    14. }
    15. public void checkFieldLength(Student student) throws IllegalAccessException {
    16. // 遍历所有Field
    17. for (Field field: student.getClass().getDeclaredFields()) {
    18. // 获取注解
    19. Length annotation = field.getAnnotation(Length.class);
    20. if (annotation != null) {
    21. // 获取字段
    22. Object o = field.get(student);
    23. if (o instanceof String) {
    24. String stringField = (String) o;
    25. if (stringField.length() < annotation.min() || stringField.length() > annotation.max()) {
    26. throw new IllegalArgumentException(field.getName() + ":" + annotation.message());
    27. }
    28. }
    29. }
    30. }
    31. }
    32. public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
    33. Student student = new Student("小");
    34. student.checkFieldLength(student);
    35. }
    36. }

    运行结果:

    1. Exception in thread "main" java.lang.IllegalArgumentException: nickname昵称的长度必须在2~5之间
    2. at Student.checkFieldLength(Student.java:32)
    3. at Student.main(Student.java:41)

    运行过程如下:

    5. 小结

    通过本小节的学习,我们知道了注解是 Java 语言的一种标注,Java 内置 10 个注解,要大致了解每个注解的作用。使用@interface关键字自定义注解;为注解定义参数的时候要注意其参数的类型,推荐为每个参数都设置默认值;想要自定义注解,必须了解 Java 中内置的 5 个元注解如何使用。

    我们自定义的注解通常是用于运行时读取的,因此必须使用@Retention(RetentionPolicy.RUNTIME)进行标注。本小节的概念较多且较为抽象,建议读者亲手去编写几个注解,再来阅读本节内容会有更好的理解。

  • 相关阅读:
    “蔚来杯“2022牛客暑期多校训练营2,签到题GJK
    STM32的光敏检测自动智能窗帘控制系统proteus设计
    24、订单和购物车-srv服务
    京东产品上架如何批量上传商品素材?
    408每日一练:第一天
    java计算机毕业设计智慧机场管理系统源程序+mysql+系统+lw文档+远程调试
    在Django框架中自定义模板过滤器的方法
    高性能MySql笔记
    如何设计高可用架构
    红日靶机vulnstack第二关
  • 原文地址:https://blog.csdn.net/u014316335/article/details/139449962