Java注解一篇吃透

115 阅读9分钟

Java注解一篇吃透

什么是注解?

Java 注解又称 Java 标注,是 JDK5 中出现的新特性,注解是放在Java源码的类、方法、字段、参数前的一种特殊“注释”,注释会被编译器直接忽略,注解则可以被编译器打包进入class文件,进行执行(一些注解不会被编译器编译进入 .class 文件,它们在编译后就被编译后扔掉),更深入的理解,注解就是代码的元数据(metadata),在 JDK5 的官方文档中就把注解描述为元数据

image-20230312131819370.png

元数据就是描述数据的数据,类似于 Windows 系统下的文件属性

为什么会出现注解?

Many APIs require a fair amount of boilerplate code. For example, in order to write a JAX-RPC web service, you must provide a paired interface and implementation. This boilerplate could be generated automatically by a tool if the program were “decorated” with annotations indicating which methods were remotely accessible.

Other APIs require “side files” to be maintained in parallel with programs. For example JavaBeans requires a BeanInfo class to be maintained in parallel with a bean, and Enterprise JavaBeans (EJB) requires a deployment descriptor. It would be more convenient and less error-prone if the information in these side files were maintained as annotations in the program itself.

出自 JDK5注解官方文档

翻译:

许多 API 需要大量的样板代码。例如,为了编写 JAX-RPC Web 服务,您必须提供成对的接口和实现。如果程序被“装饰”了说明哪些方法可以远程访问的注释,则该样板可以由工具自动生成。

其他 API 需要与程序并行维护“辅助文件”。例如,JavaBeans 需要 BeanInfo 类与 bean 并行维护,而 Enterprise JavaBeans (EJB) 需要部署描述符。如果这些辅助文件中的信息在程序本身中作为注释进行维护,将会更加方便且不易出错。

注解的分类

我们先来了解元注解再来学习其他注解,可以帮助我们能更好理解注解以及前面所留下的问题,为什么一些注解不会进入编译后的 .class 文件中

元注解

元注解就是标注注解的注解,元注解都可以在 java.lang.annotation 包中找到,JDK8官方文档

image-20230313110100683.png

我们先来了解 JDK5 出现的注解

@Documented

@Documented 注解表明这个注解应该被 javadoc工具记录. 默认情况下,javadoc是不包括注解的. 但如果声明注解时指定了 @Documented,则它会被 javadoc 之类的工具处理, 所以注解类型信息也会被包括在生成的文档中,是一个标记注解,没有成员。

Javadoc 是自动生成java文档的工具,在 idea 的 tools 中可以使用

image-20230313115643679.png

(注意高版本的 idea 在使用低版本的 JDK 会出现 javadoc: 错误 - 无效的标记: --source-path错误,请自行提升项目的 JDK 版本)

案例:

先定义一个自定义注解(后面会将)

package com.hfly.annotation;
​
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Documented//此时加上了 @Documented 注解
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface MyDocumented {
    String value() default "这是@Documented注解";
}

在方法上标注该注解

package com.hfly;
​
import com.hfly.annotation.MyDocumented;
​
public class Annotation {
​
    @MyDocumented
    public static void main(String[] args) {
​
    }
}
​

查看生成的文档

image-20230313120129341.png

可以看的 main 方法上出现了注解 @MyDocumented


现在我们删除 MyDocumented 注解上的 @Documented 元注解,重新生成文档,查看 main 方法

image-20230313120441614.png

可以看到 main 方法上的,@MyDocumented 注解消失

@Inherited

@Inherited 的作用在于类的继承,如果一个注解使用了 @Inherited 元注解,并且该注解被标记在一个父类 A 上,则 A 类的子类会自动继承该注解

注意:被 @Inherited 标注的注解,只有作用于类上自动继承效果才会生效,总用于字段和方法上无效

案例:

创建一个自定义注解 MyInherited

@Retention(RetentionPolicy.RUNTIME)
@Inherited 
public @interface MyInherited {
    String value();
}

创建一个父类 A

@MyInherited("使用 @MyInherited ,作用于类上")
public class A {
   
}

创建一个子类 B

public class B extends A{
}

使用反射进行测试(后面文章会详细介绍,此时无需担心)

public static void main(String[] args) throws NoSuchFieldException {
        Class<A> a = A.class;
        System.out.println("A类:"+a.getAnnotation(MyInherited.class).value());
        Class<B> b = B.class;
        System.out.println("B类:"+b.getAnnotation(MyInherited.class).value());
    }

image-20230315001628044.png

此时,删除 MyInherited 注解上的 @Inherited 元注解,观察结果

image-20230315001732577.png

@Retention

@Retention 元注解来标注一个注解的保留时间,如果一个注解未标注 @Retention 注解,则默认的保留策略为:RetentionPolicy.CLASS

此前我们留下的伏笔,一些注解不会被编译进入 .class 文件,就是因为这个注解在从中作祟

我们来观察 RetentionPolicy 的源码

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文件,但不需要在 VM 运行时保留,这是默认的方案
     */
    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
     * 注解会被编译器加载进class文件,并且在 vm 运行时保留,所以我们可以通过反射的方式读取他们
     */
    RUNTIME
}

下面我就通过三个案例来演示三种不同的保留策略

首先我们先创建一个注解

@Retention(RetentionPolicy.SOURCE)//此时的保留策略为 SOURCE
public @interface MyRetention {
}

在 main方法上标注注解

public class Annotation {
    @MyRetention
    public static void main(String[] args) throws NoSuchFieldException, NoSuchMethodException {
        Class<Annotation> annotation = Annotation.class;
        System.out.println(annotation.getMethod("main", String[].class).getAnnotation(MyRetention.class));
    }
}

观察编译后的.class文件

image-20230315005259896.png

修改保留等级为 CLASS,观察编译后的文件

image-20230315005413107.png

当执行结果为 null

继续修改保留等级为 RUNTIME,观察编译后的文件,编译后的文件和上图一致,当执行结果从 null 变为了 @xxxxxx.xxxx.MyRetention()

@Target

@Target 用于标记一个注解的使用范围,不标记 @Target 元注解的注解可以作用在任何地方,如果要标记的使用范围必须使用 java.lang.annotation.ElementType 枚举类型的一个或多个值,java.lang.annotation.ElementType 包含的作用范围有

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
     * 泛型,出自JDK8
     * @since 1.8
     */
    TYPE_PARAMETER,

    /**
     * Use of a type
     * 只要是型态名称,都可以进行标注,出自JDK8
     * @since 1.8
     */
    TYPE_USE
}

具体实例参考:StackOVerflow注解文章

上文的文章中没有 TYPE_PARAMETER 和 TYPE_USE

@Target(ElementType.TYPE_PARAMETER)
public @interface Email {}
public class MailBox<@Email T> {
    ...
}
@Target(ElementType.TYPE_USE)
public @interface Test {}
List<@Test Comparable> list1 = new ArrayList<>();   
List<? extends Comparable> list2 = new ArrayList<@Test Comparable>();
@Test String text;
text = (@Test String) new Object();
java.util. @Test Scanner console;
console = new java. util. @Test11 Scanner(System.in);

以上的代码均来源于 【JDK8】Annotation 功能增强


我们再来了解一下 JDK8 新增的注解

@Native

使用 @Native 注解修饰成员变量,则表示这个变量可以被本地代码引用,常常被代码生成工具使用。编译后被编译器抛弃,使用不多

自我认为 @Native 并不属于元注解,但是多数人都称为元注解,我们来看一下 @Native 的源码:

@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface Native {
}

可以看到 @Native 的作用范围不是注解而是字段,但是我还是将他放入元注解类中,分类只要大家认可就行,哈哈

@Repeatable

@Repeatable 注解的作用是允许对相同的字段重使用一个注解,当一个注解没有标记 @Repeatable 的时候,只能对一个字段使用一次,可以使用注解容器对一个字段进行同注解的多次标注

public @interface MyRepeatable {
}
//注解容器
public @interface MyRepeatables {
    MyRepeatable[] value();
}
public class Annotation {
    @MyRepeatables( value = {@MyRepeatable,@MyRepeatable})
    private String a;
    @MyRetention
    public static void main(String[] args) throws NoSuchFieldException, NoSuchMethodException {

    }
}

使用 @Repeatable 后

@Repeatable(MyRepeatables.class)
public @interface MyRepeatable {
}
//注解容器
public @interface MyRepeatables {
    MyRepeatable[] value();
}
public class Annotation {
    @MyRepeatable
    @MyRepeatable
    private String a;
    @MyRetention
    public static void main(String[] args) throws NoSuchFieldException, NoSuchMethodException {

    }
}

还是要使用到注解容器,存储多个注解,我们在使用不需要去管注解容器,方便我们操作

Java自带的标准注解

Java自带的标准注解有很多,我就介绍常用的几种

@Override

最常用的注解,来进行方法的重写检查

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

编译后会被编译器丢弃

@SuppressWarnings

告诉编译器忽略此处代码产生的警告

关于注解可以阅读下面这篇文章

Java魔法堂:注解用法详解——@SuppressWarnings

还有更多的 Java 自带的标准注解,请自行探索

自定义注解

自定义注解就是用户自己定义的注解,现在你会看上面元注解样式阶段可以看到很多自定义注解

使用 @interface 自定义注解,自动实现 java.lang.annotation.Annotation 接口,@interface用来声明一个注解,其中的每一个方法实际上是声明了一个配置参数。方法的名称就是参数的名称,返回值类型就是参数的类型(返回值类型只能是基本类型、Class、String、enum)。可以通过default来声明参数的默认值。

基本格式

//一些元注解
.....
public @interface 注解名 {
	类型 配置名() [defult 默认值]
    ......
}

案例(参数校验-注解反射版)

这个案例我仅使用原生的java注解和反射,其中涉及到反射的部分如果目前没学过可不用关注,只是通过一个案例来给你介绍注解在工程中的作用,可以等你学完反射后再来了解本案例。

我们先来定义两个自定义注解

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AgeCheck {
    String value() default "数据不合法";

    int max() default 100;

    int min() default 0;
}
@Target({ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface NotNull {
    String value() default "数据不能为空";
}

再来定义一个实体类

public class Student {

    @NotNull(value = "姓名不能为空")
    private String name;
    @AgeCheck
    @NotNull("年龄不能为空")
    private Integer age;

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

    public Integer getAge() {
        return age;
    }

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

创建一个测试类(后期我会用代理进行改进)

public class Annotation {
    public static void main(String[] args) throws NoSuchFieldException, NoSuchMethodException, IllegalAccessException {
        Student student = new Student();
        student.setAge(-1);
        Class<? extends Student> studentClass = student.getClass();
        for (Field field : studentClass.getDeclaredFields()) {
            //判断字段是否被 @NotNull 标记
            if (field.isAnnotationPresent(NotNull.class)){
                //获取字段上标记的 @NotNull 注解实例
                NotNull annotation = field.getAnnotation(NotNull.class);
                //将 private 修饰的字段设置为 可见
                field.setAccessible(true);
                Object o = field.get(student);
                if (o==null){
                    System.out.println(annotation.value());
                    //抛出异常
                    continue;
                }
            }
            if (field.isAnnotationPresent(AgeCheck.class)){
                AgeCheck annotation = field.getAnnotation(AgeCheck.class);
                field.setAccessible(true);
                Integer i = (Integer)field.get(student);
                if(i<annotation.min()||i>annotation.max()){
                    System.out.println(annotation.value());
                }
            }
        }

    }
}

本文主要讲解了 Java注解,以及一个案例来实现参数校验,本文的标题虽然是 Java注解一篇吃透,但还需要自己前身体验才能有所效果,实践出真知。