【学习笔记】Java注解&自定义注解

一、注解简介

1.1 什么叫注解

注解(Annotation)是放在Java源码的类、方法、字段、参数前的一种特殊“注释”,这类注释会被编译器直接忽略,可以被编译器打包进入class文件。因此,注解是一种用作标注的“元数据”,相关信息被存储在 Annotation 的 “name=value” 结构对中。(什么是元数据:元数据[metadata] ,就是“关于数据的数据”的意思,可以用来创建文档,跟踪代码的依赖性,执行编译时格式检查,代替已有的配置文件等等)

在java中,Annotation 是一个接口,程序可以通过反射来获取指定程序元素的 Annotation 对象,然后通过 Annotation 对象来获取注解里面的元数据。

1.2 为什么要用注解

从元数据描述方式的演化角度来看:
在注解出现之前,程序的元数据只是通过 java 注释和 javadoc 来描述,但这种方式比较简单。为了分离代码和配置,引入了XML描述元数据的方式,但XML和代码是松耦合的(在某些情况下甚至是完全分离的),会导致XML 的维护越来越糟糕。应用开发者们又希望使用一些和代码紧耦合的东西来描述这些元数据信息,因此在JDK 5.0 及以后版本引入了注解(Annotation)。Annotation的功能比 java注释、javadoc功能强大的多,又贴近代码,不会出现XML那样和代码松耦合的情况。

从标准化元数据描述方式看:
在使用注解之前,开发人员通常使用他们自己的方式定义元数据:使用标记 interfaces、注释、transient 关键字等等,每个程序员按照自己的方式定义元数据。有了Annotation 之后,大家都用这种标准的方式,实现了元数据描述的标准化。

注解和XML使用的场景:
目前,许多框架将 XML 和 Annotation 两种方式结合使用,平衡两者之间的利弊。那什么场景使用XML,什么场景使用注解呢?
假如你想为应用设置很多的常量或参数,这种情况下,XML 是一个很好的选择,因为它不会同特定的代码相连。如果你想把某个方法声明为服务,那么使用 Annotation 会更好一些,因为这种情况下需要注解和方法紧密耦合起来。可以根据自己场景来判断该使用哪种方式。

1.3 注解的分类

1.3.1 按照运行机制划分

从Java代码的运行机制流程来看:Java源码 —> Class文件 —> JVM中Runtime ,因此可以分为三类:

1. SOURCE类型的注解 (RetentionPolicy.SOURCE):只在源码中存在,编译成 class 文件就不存在了。这类注解主要由编译器使用,因此我们一般只使用,不编写。

比如 :
@Override:让编译器检查该方法是否正确地实现了覆写;
@SuppressWarnings:告诉编译器忽略此处代码产生的警告。

2. CLASS类型注解 (RetentionPolicy.CLASS):在源码和 class 文件中都存在。这类注解主要由底层工具库使用,涉及到class的加载,一般我们很少用到,不必自己处理。

比如有些工具会在加载class的时候,对class做动态修改,实现一些特殊的功能。
这类注解会被编译进入.class文件,但加载结束后并不会存在于内存中。

3. RUNTIME类型的注解 (RetentionPolicy.RUNTIME):在运行阶段还起作用,甚至会影响运行逻辑的注解。它们在加载后一直存在于JVM中,这也是最常用的注解,不但要使用,还经常需要编写。

像 @Autowired 自动注入的这样一种注解就属于运行时注解,它会在程序运行的时候把你的成员变量自动的注入进来。

1.3.2 按照注解的参数个数分类

1. 标记注解:没有变量,只有名称标识。这种类型仅仅使用自身的存在与否来为开发者提供信息,如 @annotation

2. 单值注解:在标记注解的基础上提供一段数据。如 @annotation(“data”)

3. 完整注解:可以包括多个数据成员,每个数据成员由名称和值构成。如 @annotation(value1 = “data1”, value2 = “data2”)

1.3.3 按照使用方法和用途划分

1. 元注解: 是给注解进行注解,可以理解为注解的注解就是元注解。

2. JDK 内置注解:Java 目前只内置了五种标准注解和五种元注解。

3. 第三方的注解:这一类注解是我们接触最多和作用最大的一类。

4. 自定义注解:可以看作是开发者自己编写的注解。

1.4 JDK元注解

元注解(meta-annotation) 是指注解的注解。元注解是 Java 定义的用于创建注解的工具,它们本身也是注解。在 java.lang.annotation 包下,JDK 提供了 5 个标准的元注解类型:@Retention、@Target、@Inherited、@Documented、@Repeatable。

1.4.1 @Retention

@Retention: 注解的保留策略。该注解指明了被它所注解的注解被保留的时间长短。@Retention 包含一个名为 value 的成员变量,该 value 成员变量是 RetentionPolicy,RetentionPolicy 是枚举类型,值有如下几个:

  • RetentionPolicy.SOURCE:注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视,不记录在 class 文件中。

  • RetentionPolicy.CLASS:注解只被保留到编译进行的时候,它并不会被加载到 JVM 中。这是默认行为,所有没有用@Retention 注解的注解,都会采用这种策略。

  • RetentionPolicy.RUNTIME:注解可以保留到程序运行的时候,它会被加载进入到 JVM中,程序可以通过反射获取该注解的信息。

1.4.2 @Target

@Target: 注解的作用目标。该注解指定注解用于修饰哪些程序元素。@Target 也包含一个名为 value 的成员变量,该 value 成员变量类型为 ElementType[],ElementType 也为枚举类型,值有如下几个:

  • ElementType.TYPE: 修饰类型,比如接口、类、枚举、注解

  • ElementType.FIELD: 修饰属性,比如成员变量、枚举常量

  • ElementType.METHOD: 修饰方法

  • ElementType.PARAMETER:修饰方法内的参数

  • ElementType.CONSTRUCTOR:修饰构造方法

  • ElementType.LOCAL_VARIABLE:修饰局部变量

  • ElementType.ANNOTATION_TYPE:修饰注解

  • ElementType.PACKAGE:修饰包

  • ElementType.TYPE_PARAMETER:修饰类型参数(Java8 新增)

  • ElementType.TYPE_USE:修饰任何类型(Java8 新增)

1.4.3 @Inherited

@Inherited: 指定注解具有继承性。但是它并不是说注解本身可以继承,而是说如果一个超类被 @Inherited 注解过的注解进行注解的话,那么如果它的子类没有被任何注解应用的话,那么这个子类就继承了超类的注解。

1.4.4 @Documented

@Documented: 注解将被包含在 Javadoc 中。该注解的作用是在用 Javadoc 命令生成 API 文档时能够将注解中的元素包含到 Javadoc 中去。

1.4.5 @Repeatable (Java8新增)

@Repeatable: 表示可重复注解。@Repeatable 是 Java 8 才加进来的,所以算是一个新的特性。在实际应用中,可能会出现需要对同一个声明式或者类型加上相同的注解(包含不同的属性值)的情况。

例如系统中除了管理员之外,还添加了超级管理员这一权限,对于某些只能由这两种角色调用的特定方法,可以使用可重复注解。

1.5 JDK 内置注解

在 java.lang 包下,JDK 提供了 5 个基本注解:@Override、@Deprecated、@SuppressWarnings、@SafeVarargs、@FunctionalInterface。

1.5.1 @Override

@Override 用于标注重写了父类的方法。

当我们想要复写父类中的方法时,我们需要使用该注解去告知编译器我们想要复写这个方法,这样一来当父类中的方法移除或者发生更改时编译器将提示错误信息。对于子类中被 @Override 修饰的方法,如果存在对应的被重写的父类方法,则正确;如果不存在,则报错。@Override 只能作用于方法,不能作用于其他程序元素。

1.5.2 @Deprecated

@Deprecated 用于表示某个程序元素(类、方法等)已过时。

当我们希望编译器知道某一方法不建议使用时,我们应该使用这个注解。Java 在 javadoc 中推荐使用该注解,我们应该提供为什么该方法不推荐使用以及替代的方法。如果使用了被 @Deprecated 修饰的类或方法等,编译器会发出警告。

1.5.3 @SuppressWarnings

@SuppressWarnings 用于抑制编译器的警告。

这个仅仅是告诉编译器忽略特定的警告信息,例如在泛型中使用原生数据类型。指示被 @SuppressWarnings 修饰的程序元素(以及该程序元素中的所有子元素,例如类以及该类中的方法)取消显示指定的编译器警告。例如,常见的 @SuppressWarnings(“unchecked”)。

@SuppressWarnings 注解的常见参数值主要有以下几种:

  • deprecation:使用了不赞成使用的类或方法时的警告(使用 @Deprecated 使得编译器产生的警告);
  • unchecked:执行了未检查的转换时的警告,例如当使用集合时没有用泛型 (Generics) 来指定集合保存的类型; 关闭编译器警告
  • fallthrough:当 switch 程序块直接通往下一种情况而没有 break 语句时的警告;
  • path:在类路径、源文件路径等中有不存在的路径时的警告;
  • serial:当在可序列化的类上缺少 serialVersionUID 定义时的警告;
  • finally:任何 finally 子句不能正常完成时的警告;
  • all:关于以上所有情况的警告。

1.5.4 @SafeVarargs

@SafeVarargs 是 JDK 7 专门为抑制堆污染警告提供的。

1.5.5 @FunctionalInterface (Java8新增)

@FunctionalInterface 是 Java8 中新增的函数式接口。Java8 规定:如果接口中只有一个抽象方法(可以包含多个 default 方法或多个 static 方法),该接口称为函数式接口。

1.6 使用注解相关注意事项:

1. value 特权: 如果使用注解时只需要为 value 成员变量指定值,则使用注解时可以直接在该注解的括号中指定 value 值,而无需使用 name=value 的形式(如@SuppressWarnings(“unchecked”))。

2. 坚持使用 @Override 注解:如果在每个方法中使用 @Override 注解来声明要覆盖父类声明,编译器就可以替你防止大量的错误。

其他的后续在陆续补充·····

二、自定义注解

在日常开发中,使用自定义注解可分为三步:

  • step1: 定义注解
  • step2: 处理注解
  • step3: 使用注解

下面用需求-demo的形式来说明。

需求描述:希望有个方便的方法检测用户输入的 person 对象的 年龄值是否符合要求(0~200岁)、姓名的字符串长度是否符合要求(1 ~ 20个字符)

解决方案:给Person类新增个校验方法,该方法校验 年龄、name 是否符合要求时,会用注解相关的知识。

2.1 定义注解

2.1.1 理论:如何定义注解

注解也是一种特殊的类,使用ide生成一个注解:
在这里插入图片描述

public @interface Range {
}

定义新注解使用 @interface 关键字,其定义过程与定义接口非常类似。使用 @interface 自定义注解时,自动继承了java.lang.annotation.Annotation 接口,由编译程序自动完成其他细节。一些需要注意的点:

  1. 在定义注解时,不能继承其他的注解或接口。

  2. @interface 用来声明一个注解,其中的每一个方法实际上是声明了一个配置参数。

  3. 方法的名称就是参数的名称,返回值类型就是参数的类型(返回值类型只能是基本类型、Class、String、Enum)。可以通过 default 来声明参数的默认值。

定义注解格式:

public @interface 注解名 {
    定义体
}

自定义注解就需要用到上面所介绍到的几种JDK元注解,可以看出元注解就是用来注解其它注解。自定义注解和接口类似,只能定义方法。Annotation 的成员变量在 Annotation 定义中是以无参的方法形式来声明的,其方法名和返回值类型定义了该成员变量的名字和类型。注解参数(即返回值类型)的可支持数据类型:

  • 所有基本数据类型(int、float、boolean、byte、double、char、long、short)
  • String 类型
  • Class 类型
  • Enum 类型
  • Annotation 类型
  • 以上所有类型的数组

2.1.2 实践:定义一个@Range注解

这里我们定义一个 @Range注解:

  • 我们希望对 person对象的 年龄、姓名 字段做校验,所以 Target 是 字段,即ElementType.FIELD ;
  • 我们要在程序运行时的某个阶段做校验,Retention的策略应该是 RetentionPolicy.RUNTIME;
  • 校验字段是否属于某个范围(年龄的取值、姓名的字符长度),应该有个 最大值、最小值信息,所以在 @Range里我们定义两个参数方法:minValue 和maxValue。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Range {
    int minValue() default 0;
    int maxValue() default 250;
}

这样一个用来检查元素上下限值的注解就定义好了,下面看看怎么处理。

2.2 处理注解

2.2.1 理论:处理注解的常用方法

因为注解定义后也是一种class,所有的注解都继承自java.lang.annotation.Annotation,因此,读取注解,需要使用反射API。

2.2.1.1 判断某个注解是否存在于Class、Field、Method或Constructor
// 检查某个 类 是否使用了某个 AnnotationName 注解
boolean result =  Class.isAnnotationPresent(AnnotationName.Class)

// 检查某个 构造方法 是否使用了某个 AnnotationName 注解
boolean result = Constructor.isAnnotationPresent(AnnotationName.Class)

// 检查某个 方法 是否使用了某个 AnnotationName 注解
boolean result = Method.isAnnotationPresent(AnnotationName.Class)

// 检查某个 字段 是否使用了某个 AnnotationName 注解
boolean result = Field.isAnnotationPresent(AnnotationName.Class)

例如:

// 判断@Report是否存在于Person类:
if(Person.class.isAnnotationPresent(Report.class)){
	//doSomthing;
};
2.2.1.2 使用反射API读取Annotation

使用反射API读取Annotation:

// 获取某个 类 的 AnnotationName 注解
AnnotationName annotation = Class.getAnnotation(AnnotationName.Class)

// 获取某个 构造方法 的 AnnotationName 注解
AnnotationName annotation = Constructor.getAnnotation(AnnotationName.Class)

// 获取某个 方法 的 AnnotationName 注解
AnnotationName annotation = Method.getAnnotation(AnnotationName.Class)

// 获取某个 字段 的 AnnotationName 注解
AnnotationName annotation = Field.getAnnotation(AnnotationName.Class)

例如:

// 获取Person定义的@Report注解:
Report report = Person.class.getAnnotation(Report.class);

//获取注解的参数:
int type = report.type();
String level = report.level();
2.2.1.3 总结: 使用反射API读取Annotation的两种方法

方法一:先判断Annotation是否存在,如果存在,就直接读取:

Class cls = Person.class;

if (cls.isAnnotationPresent(Report.class)) {
    Report report = cls.getAnnotation(Report.class);
    ...
}

方法二: 先直接读取Annotation,如果Annotation不存在,将返回null:

Class cls = Person.class;
Report report = cls.getAnnotation(Report.class);
if (report != null) {
   ...
}

读取方法、字段和构造方法的Annotation和Class类似。但要读取方法参数的Annotation就比较麻烦一点,这个可以看附录文档,这里先不细说。

2.2.2 实践:处理使用@Range注解的对象

目前可知,这个@Range注解会用到两种类型的字段上:数值型(年龄) 和 字符串型(姓名),在处理注解时,我们需要分别针对这两种情况处理。我们可以写个专门处理 @Range 注解的工具类,方便在各处使用:

public class RangeCheckUtils {
    /**
     *   检查 Integer/String 类型的字段长度是否符合要求
     * @param object
     * @throws IllegalArgumentException
     * @throws ReflectiveOperationException
     */
    public static void checkFiledRange(Object object) throws IllegalArgumentException, ReflectiveOperationException {
        //遍历传入对象所有的Filed
        for(Field field : object.getClass().getDeclaredFields()){
            //通过字段获取该字段上的注解 @Range 
            Range range = field.getAnnotation(Range.class);
            if(null != range){
                //获取注解 @Range 定义的范围值
                int minValue = range.minValue();
                int maxValue = range.maxValue();
                //获取 filed 的值
                field.setAccessible(true);
                Object fieldValue = field.get(object);
                // 按类型检查传入对象的 String 或 Integer 类型字段的范围是否符合要求
                if(fieldValue instanceof String){
                    String stringValue = (String) fieldValue;
                    //字符型类型,检查字符串长度
                    if(stringValue.length() < minValue || stringValue.length() > maxValue){
                        throw new  IllegalArgumentException("The length of a string filed is invalid, invalid filed: "
                            + field.getName());
                    }
                }
                if(fieldValue instanceof Integer){
                    int integerValue = (Integer)fieldValue;
                    //数字型类型,检查数字大小
                    if(integerValue < minValue || integerValue > maxValue){
                        throw new IllegalArgumentException("The value of a integer filed is invalid, invalid filed: "
                            + field.getName());
                    }
                }
            }
        }
    }

}

如上所示,接受到输入对象后:

  1. 先使用反射获取这个对象里的所有字段;
  2. 再分别针对每个字段做处理,获取字段上的注解信息;
  3. 根据字段类型分类处理

2.3 使用注解

2.3.1 实践:在Person类上使用注解

我们的目标是,判断person对象的 姓名、年龄 是否符合要求,所以我们会在 Person 类的这两个字段上使用这个注解:

public class Person {
    /**
     * 姓名的 字符串长度范围为 1~20 个字符
     * 这里使用注解设置name的长度范围
     */
    @Range(minValue = 1, maxValue = 20)
    private String name;

    /**
     * 年龄的范围是 0 ~ 200
     * 这里使用注解设置age的大小范围
     * 注解Range 的 minValue = 0, 故可以省去
     */
    @Range(maxValue = 200)
    private Integer age;

    /**
     * 性别不需要做检验
     */
    private String gender;

    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 String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    @Override
    public String toString() {
        return "Person{" +
            "name='" + name + '\'' +
            ", age=" + age +
            ", gender='" + gender + '\'' +
            '}';
    }
}

这里我们在 Person的 age、name 两个属性字段上添加了 @Range 注解,其中 name 的最大最小值由我们自己指定,age的最小值和 @Range 注解中的最小值相同,可以省去,只用写个最大值即可; gender字段不需要范围校验,不用注解。

2.3.2 验证:测试注解功能

现在我们测试下@Range注解的功能是否符合预期,测试代码如下:

public class TestAnnotation {

    public static void main(String[] args){
        //case1: name 和 age 都符合要求的person对象
        try {
            Person person1 = new Person();
            person1.setAge(20);
            person1.setName("BigBear");
            person1.setGender("male");
            //检查person的字段是否符合要求
            RangeCheckUtils.checkFiledRange(person1);
            System.out.println("case1 check pass ! person1 info: " + person1.toString());
        }catch (Exception e){
            System.out.println("case1 check failed!  exception info: " + e.getMessage());
        }

        //case2: name不符合要求的person对象
        try {
            Person person2 = new Person();
            person2.setAge(20);
            person2.setName("BigBearBigBearBigBearBigBearBigBear");
            person2.setGender("male");
            //检查person的字段是否符合要求
            RangeCheckUtils.checkFiledRange(person2);
            System.out.println("case2 check pass ! person1 info: " + person2.toString());
        }catch (Exception e){
            System.out.println("case2 check failed!  exception info: " + e.getMessage());
        }


        //case3: age不符合要求的person对象
        try {
            Person person3 = new Person();
            person3.setAge(270);
            person3.setName("BigBear");
            person3.setGender("male");
            //检查person的字段是否符合要求
            RangeCheckUtils.checkFiledRange(person3);
            System.out.println("case3 check pass ! person1 info: " + person3.toString());
        }catch (Exception e){
            System.out.println("case3 check failed!  exception info: " + e.getMessage());
        }

        //case4: age 和 name 都不符合要求的person对象
        try {
            Person person4 = new Person();
            person4.setAge(-1);
            person4.setName("BigBearBigBearBigBearBigBearBigBear");
            person4.setGender("male");
            //检查字段是否符合要求
            RangeCheckUtils.checkFiledRange(person4);
            System.out.println("case4 check pass ! person1 info: " + person4.toString());
        }catch (Exception e){
            System.out.println("case4 check failed!  exception info: " + e.getMessage());
        }
    }
}

对应的输出结果:

case1 check pass ! person1 info: Person{name='BigBear', age=20, gender='male'}

case2 check failed!  exception info: The length of a string filed is invalid, invalid filed: name

case3 check failed!  exception info: The value of a integer filed is invalid, invalid filed: age

case4 check failed!  exception info: The length of a string filed is invalid, invalid filed: name

可见, RangeCheckUtils.checkFiledRange(person) 工具方法成功的通过注解找到了需要检查的 person 对象字段,并检查出是否符合预期。

三、一些思考

从注解的功能来看,注解 就像一个标签,我们在需要的地方贴上这个标签,并简单记录上一些描述信息;然后在适当的地方,处理这些标签(处理逻辑由标签的类型、记录的信息 来决定)。

四、参考文档

ref1. 廖雪峰的官方网站:注解
ref2. Java 注解(Annotation)详解

  • 1
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值