转载

前端初学SpringBoot系列(三)自定义注解的学习与练习

前面两节学习了 springboot 的基本使用,其中大量使用了注解来减少代码量,想必大家都觉得挺奇怪的吧。

所以第三节,稍微停顿一下增删改查的脚步,补补一些基础(๑•̀ㅂ•́)و✧。

对于前端来说,注解这个概念很陌生,如此神秘的力量是如何发挥作用的呢,今天学习一下 java 中的注解:一种形如 @xxx 的东东,xxx一般是大写字母开头。

什么是注解

注解 Annotation 是在 java源码 中对于类、方法、字段、方法参数的一种 特殊注释

  • 是一种注释???

之所以说它是 注释 ,是因为 注解 本身并不会对代码逻辑造成任何影响,对于如何使用注解去完成对应的功能是工具或者说某些容器的事,从这一点出发,感觉挺像注释的, 但是它又是很特殊的

  • 特殊在哪里???

注释不会被编译器处理,直接原样复制忽略掉了,但注解可以被编译器打包进 class 文件中,所以注解被理解为用作标注的元数据。

注解的分类

一般来说注解分为三类:

  1. 编译器默认使用的注解。
  • 例如我们经常在实体类中见到的各种 @Overrider ,这个基础注解直译就是覆写。
前端初学SpringBoot系列(三)自定义注解的学习与练习
  • @SuppressWarnings ,告诉编译器忽略此处的警告。

  • 特点。通过上面两个小:chestnut:可以发现,这一类的注解编译器使用而已,对于真实的代码跑起来后并不需要,因此这一类注解的特点就是:不会被编译进 class 文件,编译后编译器就忽略掉这些代码了。

  1. 底层库处理时需要用到的注解,这类注解会被编译进 class文件中 ,但是距离我们一线开发者很遥远,目前不需要关注。

  2. 程序运行时需要读取并产生副作用的注解,这是我们一线开发者的需要经常使用的注解。

注解有啥用

有了上面的小小的基础后,我们基本可以发现,注解可以在程序运行时告诉编译器,它有一些副作用,能帮助开发者做一些工作,而且写完之后到处使用,开发者仅仅需要打一个标签就行。

定义一个注解

上面我们了解了注解的基本情况,大约有了点认识,接下来看一下 java 官方的定义,毕竟要整点正规军的东西。 官方使用 @interface 来定义一个新的注解,基本格式大约如下:

publice @interface Annotation {
  String value() default "";
  // 多个参数...
}
复制代码

几个小约定

default
value

元注解

◔ ‸◔? :question:这还没理解注解,咋还冒出来个 元注解 呢,因为这个是定义注解的第一步:smile: 所谓的 元注解 就是:能够解释其他注解的注解,这样的注解我们就可以称呼它为 meta annotation 。我们自定义注解需要用到一些重要的 元注解 ,下面介绍几个元注解:

  1. @Target

这个注解告诉编译器我的代码在 哪个位置 被使用:

ElementType.TYPE
ElementType.FIELD
ElementType.METHOD
ElementType.CONSTRUCTURE
ElementType.PARAMETER

一个小:chestnut:,假如你要定义一个 用在方法 上的注解,那么就使用 @Target(ElementType.METHOD)

@Target(ElementType.METHOD)
public @interface Annotation {
  String value() default "";
}
复制代码

假如你要想定义一个注解 用在方法或者字段上 的注解,可以使用 @Target({ElementType.METHOD, ElementType.FIELD})

@Target({
  ElementType.METHOD,
  ElementType.FIELD
})
public @interface Annotation {
  String value() default "";
}
复制代码
  1. @Retention 这个元注解 极其重要 ,它定义了注解的生命周期,即自定义的注解在代码的什么阶段被使用。
RetentionPolicy.SOURCE
RetentionPolicy.CLASS
RetentionPolicy.RUNTIME

当然了,如果你一不小心忘了使用这个元注解,那么默认为 CLASS 。在我们开发中,我们自定义的注解都是 RUNTIME 的元注解。

@Retention(RetentionPolicy.RUNTIME)
public @interface Annotation {
  String value() default "";
}
复制代码
  1. @Repeatable 这个元注解是说自定义的注解可否被重复使用。一线开发比较少用。
  2. @Inherited 这个元注解是说子类可否继承父类定义的注解,但是它只能对 @Target(ElementType.TYPE) 类型的注解生效,而且只是针对 class

综上所述,自定义注解时,最重要的就是必须设置 @Target @Retention ,以上一节的 mybatis 中的基础注解 @Select 为例:

前端初学SpringBoot系列(三)自定义注解的学习与练习
可以发现它生命周期是在 RUNTIME ,适用范围在 METHOD

上,另一个元注解就比较陌生啦。

所以啊o_O, java 中注解千千万,以后遇到陌生注解再说,目前够用(〃'▽'〃)……

使用注解

在实战中使用,在模拟中练习 是最好的学习方式,本节尝试手写一个自定义注解去体会体会注解的奥妙,不过再开始写 BUGS 之前,还有一些理论知识需要补充: 上一节 注解的定义 中解释了 @Retentions 元注解能够规定注解的三个生命周期,那个这三个生命周期要干啥呢:

  • SOURCE 生命周期的注解编译期使用,也就是说我们只关心使用就行。
  • CLASS 仅在build之后中的 class 文件中存在,与我们一线开发关系也不大。
  • RUNTIME 是我们经常要使用并且可以充分发挥我们程序员才智的阶段。

一个小小的tips

对于前端来说,下面的知识很陌生(说得好像其他知识你不陌生一样 ): java 中build后都是class文件,注解继承自 java.lang.annotation.Annotation ,至于如何读取注解,需要继续学习 反射API ,这就是下一节需要补充的知识了,这一节我们假装:smile:会用了。

反射API 基本操作

既然我们要读取 Annotation ,一般有以下几个步骤:

  1. 首先我们要先判断它存不存在。常见的判断的API如下
Class.isAnnotationPresent(Class)
Field.isAnnotationPresent(Class)
Method.isAnnotationPresent(Class)
Constructor.isAnnotationPresent(Class)
//判断@Test注解是否存在与Test中
Test.class.isAnnotationPresent(Class)
复制代码
  1. 存在的话,我们读取注解
Class.getAnnotation(Class)
Field.getAnnotation(Class)
Method.getAnnotation(Class)
Constructor.getAnnotation(Class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
  String value() default "";
}
//获取定义在Demo类上的@Test注解
Test test = Demo.class.getAnnotation(Test.class)
String value = test.value()
//...
复制代码

练习自定义注解

有了上述的基础知识之后,我们开始练习一下,手写一个简单的注解,实现判断类中的字段的最大值最小值

  1. 定义一个注解 @Range
package com.wushao;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({
        ElementType.FIELD,
        ElementType.TYPE
})
public @interface Range {
    int min() default 0;

    int max() default 255;
}
复制代码
  1. 应用在一个类中和类中字段中
package com.wushao;

@Range(min = 1)
public class Person {
    //name这个字符串长度必须在1-20之间
    @Range(min = 1, max = 20)
    public String name;
    
    //city这个字符串长度最大为10,有个默认最小值0
    @Range(max = 10)
    public String city;

    @Range(min = 1, max = 10)
    public int age;

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

    public Person(String name, String city, int age) {
        this.name = name;
        this.city = city;
        this.age = age;
    }

}

复制代码
  1. Main 入口函数中简单的测试
package com.wushao;


import java.lang.reflect.Field;

public class Main {

    public static void main(String[] args) {
        Person p1 = new Person("wushao", "Qingdao", 20);
        Person p2 = new Person("", "Shanghai", 0);
        Person p3 = new Person("gaoyuayuan", "Beijing", 199);
        Range range = Person.class.getAnnotation(Range.class);
        System.out.println("Person的注解:" + range);
        range.max();
        for (Person p : new Person[] {p1, p2, p3}) {
            try {
                check(p);
                System.out.println("Person " + p + " checked ok.");
            } catch (IllegalArgumentException | ReflectiveOperationException e) {
                System.out.println("Person " + p + " checked failed: " + e);
            }
        }


    }
    // 类中其他方法必须使用static关键字修饰,并且抛出以下两个错误
    static void check(Person person) throws IllegalArgumentException, ReflectiveOperationException {       
        //遍历person类中的所有字段
        for (Field field: person.getClass().getFields()) {
            //获取定义在Field中的注解`@Range`
            Range range = field.getAnnotation(Range.class);
            //如果存在这个注解进行操作
            if (range != null) {
                //获取不同Field字段的值
                Object value = field.get(person);
                //TODO: 核心判断逻辑
            }
        }
    }
}
复制代码

上面的 TODO 中的校验函数是挺重要的

static void check(Person person) throws IllegalArgumentException, ReflectiveOperationException {
        for (Field field: person.getClass().getFields()) {
            Range range = field.getAnnotation(Range.class);
            if (range != null) {
                Object value = field.get(person);
                // 判断字段值是否是String类型的
                if (value instanceof  String) {
                    String s = (String) value;
                    System.out.println("s: "+ s);
                    //如果字段的值不符合注解的最大最小值抛出一个异常,会被`Main`函数的catch:pig2:
                    if (s.length() < range.min() || s.length() > range.max()) {
                        throw new IllegalArgumentException("Invalid field is: " + field.getName());
                    }
                }
            }
        }
    }

复制代码

简单的执行一下,上面的demo实例发现打印如下:

前端初学SpringBoot系列(三)自定义注解的学习与练习
发现在检验到 p2 这个人的时候,报错了,因为他的name为空,长度不满足注解要求的1-20之间。

学习过程中感谢 廖雪峰 和 菜鸟教程

原文  https://juejin.im/post/5f197f4b5188252e3c4dc301
正文到此结束
Loading...