1、前言

 

Java EE下用Spring boot框架后,开始面向注解编程,可以说Spring boot就是建立在注解之上。那么什么是注解呢?

  • Java 注解(Annotation)又称 Java 标注,是JDK5.0引入的一种注释机制。 注解是元数据的一种形式,提供有关于程序但不属于程序本身的数据。注解对它们注解的代码的操作没有直接影响。
  • Annotation(注解)就是Java提供了一种元程序中的元素关联任何信息和着任何元数据(metadata)的途径和方法。Annotion(注解)是一个接口,程序可以通过反射来获取指定程序元素的Annotion对象,然后通过Annotion对象来获取注解里面的元数据。

 

注解可以实现以下功能:

 

  1. 生成文档。这是最常见的,也是java 最早提供的注解。常用的有@param @return 等
  2. 跟踪代码依赖性,实现替代配置文件功能。比如Dagger 2依赖注入,未来java开发,将大量注解配置,具有很大用处;
  3. 在编译时进行格式检查。如@override 放在方法前,如果你这个方法并不是覆盖了超类方法,则编译时就能检查出。

Annotation的成员在Annotation类型中以无参数的方法的形式被声明。其方法名和返回值定义了该成员的名字和类型。在此有一个特定的默认语法:允许声明任何Annotation成员的默认值:一个Annotation可以将name=value对作为没有定义默认值的Annotation成员的值,当然也可以使用name=value对来覆盖其它成员默认值。这一点有些近似类的继承特性,父类的构造函数可以作为子类的默认构造函数,但是也可以被子类覆盖。

Annotation能被用来为某个程序元素(类、方法、成员变量等)关联任何的信息。需要注意的是,这里存在着一个基本的规则:Annotation不能影响程序代码的执行,无论增加、删除 Annotation,代码都始终如一的执行。

 

2、注解的本质

 

所有的注解类型都继承自这个普通的接口(Annotation),所以注解的本质就是一个继承了 Annotation 接口的接口。

package java.lang.annotation;
public interface Annotation {
    boolean equals(Object obj);
    int hashCode();
    String toString();
    Class<? extends Annotation> annotationType();
}

接口的具体实现类是Java运行时动态生成的代理类,通过反射获取注解的时候,返回的是Java运行时动态生成的代理对象,通过代理对象调用自定义注解(接口)的方法,会最终调用AnnotationInvocationHandler 的invoke方法。该方法会从memberValues这个Map中索引出对应的值,而memberValues的来源是Java 常量池。

 一个注解准确意义上来说,只不过是一种特殊的注释而已,如果没有解析它的代码,它可能连注释都不如。使用注解的过程中,很重要的一部分就是创建使用注解处理器。

那注解用来注释什么呢?类,成员函数,成员属性,接口!注释一旦完成,再结合Java的反射特性,运行的时候就可以动态的获取到注解的标注信息,然后执行其他的逻辑,例如面向切面编程等等。

 

解析一个类或者方法的注解往往有两种形式,一种是编译期直接的扫描,一种是运行期反射。

编译期的扫描指的是编译器在对 java代码编译字节码的过程中会检测到某个类或者方法被一些注解修饰,这时它就会对于这些注解进行某些处理。典型的就是注解 @Override,一旦编译器检测到某个方法被修饰了 @Override 注解,编译器就会检查当前方法的方法签名是否真正重写了父类的某个方法,也就是比较父类中是否具有一个同样的方法签名。这一种情况只适用于那些编译器已经熟知的注解类,比如 JDK 内置的几个注解,而对于自定义的注解,编译器是不知道这个注解的作用的,当然也不知道该如何处理,往往只是会根据该注解的作用范围来选择是否编译进字节码文件,仅此而已。

 

区分Annotation和Annotation类型:

Annotation:Annotation使用了在java5.0所带来的新语法,它的行为十分类似public、final这样的修饰符。每个Annotation具有一个名字和成员个数>=0。每个Annotation的成员具有被称为name=value对的名字和值(就像javabean一样),name=value装载了Annotation的信息。

Annotation类型:Annotation类型定义了Annotation的名字、类型、成员默认值。一个Annotation类型可以说是一个特殊的java接口,它的成员变量是受限制的,而声明Annotation类型时需要使用新语法。当我们通过java反射api访问Annotation时,返回值将是一个实现了该annotation类型接口的对象,通过访问这个对象我们能方便的访问到其Annotation成员。

 

3、注解处理器类库(java.lang.reflect.AnnotatedElement)

如果没有用来读取注解的方法和工作,那么注解也就不会比注释更有用处了。使用注解的过程中,很重要的一部分就是创建于使用注解处理器。Java SE5扩展了反射机制的API,以帮助程序员快速的构造自定义注解处理器。Java使用Annotation接口来代表程序元素前面的注解,该接口是所有Annotation类型的父接口。除此之外,Java在java.lang.reflect 包下新增了AnnotatedElement接口,该接口代表程序中可以接受注解的程序元素,该接口主要有如下几个实现类:

  • Class:类定义
  • Constructor:构造器定义
  • Field:累的成员变量定义
  • Method:类的方法定义
  • Package:类的包定义

 

java.lang.reflect包下主要包含一些实现反射功能的工具类,实际上,java.lang.reflect 包所有提供的反射API扩充了读取运行时Annotation信息的能力。当一个Annotation类型被定义为运行时的Annotation后,该注解才能是运行时可见,当class文件被装载时被保存在class文件中的Annotation才会被虚拟机读取。AnnotatedElement接口是所有程序元素(Class、Method、Constructor、Field和Package)的父接口,所以程序通过反射获取了某个类的AnnotatedElement对象之后,程序就可以调用该对象的如下四个个方法来访问Annotation信息:

 

方法1:<T extends Annotation> T getAnnotation(Class<T> annotationClass): 返回改程序元素上存在的、指定类型的注解,如果该类型注解不存在,则返回null。

方法2:Annotation[] getAnnotations():返回该程序元素上存在的所有注解。

方法3:boolean is AnnotationPresent(Class<?extends Annotation> annotationClass):判断该程序元素上是否包含指定类型的注解,存在则返回true,否则返回false.

方法4:Annotation[] getDeclaredAnnotations():返回直接存在于此元素上的所有注释。与此接口中的其他方法不同,该方法将忽略继承的注释。(如果没有注释直接存在于此元素上,则返回长度为零的一个数组。)该方法的调用者可以随意修改返回的数组;这不会对其他调用者返回的数组产生任何影响。

 

4、元注解

在定义注解时,注解类也能够使用其他的注解声明。对注解类型进行注解的注解类,我们称之为 meta-annotation(元注解)。一般的,我们在定义自定义注解时,需要指定的元注解有两个 :@Target和@Retention。(另外@Documented 与 @Inherited 元注解,前者用于被javadoc工具提取成文档,后者表示允许子类继承父类中定义的注解。)

 

4.1、什么是元注解

java.lang.annotation 提供了四种元注解,专门注解其他的注解(在自定义注解的时候,需要使用到元注解):

 

  • @Target – 注解用于什么地方
  • @Retention – 什么时候使用该注解
  • @Documented – 注解是否将包含在JavaDoc中
  • @Inherited – 是否允许子类继承该注解

 

 @Retention – 负责定义该注解的生命周期

  • RetentionPolicy.SOURCE : 在编译阶段丢弃。这些注解在编译结束之后就不再有任何意义,所以它们不会写入字节码。@Override, @SuppressWarnings都属于这类注解。
  • RetentionPolicy.CLASS : 在类加载的时候丢弃。在字节码文件的处理中有用。注解默认使用这种方式。
  • RetentionPolicy.RUNTIME : 始终不会丢弃,运行期也保留该注解,因此可以使用反射机制读取该注解的信息。我们自定义的注解通常使用这种方式。

 

@Target :表示该注解用于什么地方。默认值为任何元素,表示该注解用于什么地方。可用的ElementType 参数包括

  ● ElementType.CONSTRUCTOR: 用于描述构造器

  ● ElementType.FIELD: 成员变量、对象、属性(包括enum实例)

  ● ElementType.LOCAL_VARIABLE: 用于描述局部变量

  ● ElementType.METHOD: 用于描述方法

  ● ElementType.PACKAGE: 用于描述包

  ● ElementType.PARAMETER: 用于描述参数

  ● ElementType.TYPE: 用于描述类、接口(包括注解类型) 或enum声明

 

一个例子:

//@Target(ElementType.TYPE) 只能在类上标记该注解
@Target({ElementType.TYPE,ElementType.FIELD}) // 允许在类与类属性上标记该注解
@Retention(RetentionPolicy.SOURCE) //注解保留在源码中
public @interface Lance {
}

@Documented – 一个简单的Annotations 标记注解,表示是否将注解信息添加在java 文档中,没有成员。

@Inherited – 定义该注释和子类的关系

  • @Inherited 元注解是一个标记注解,@Inherited 阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited 修饰的annotation 类型被用于一个class,则这个annotation 将被用于该class 的子类。

实现例子:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface Column {
    public String name() default "fieldName";
    public String setFuncName() default "setField";
    public String getFuncName() default "getField";
    public boolean defaultDBValue() default false;
}

 

5、注解语法

 

注解的定义:注解通过@interface 关键字进行定义。注解的定义格式是 :public @interface 注解名 {定义体}

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyAnnotation {
      String value() default "";
}

注解的定义的形式跟接口很类似,不过前面多了一个 @ 符号。上面的代码就创建了一个名字为MyAnnotaion 的注解。

 

写注解的时候需要注意:

 

1、注解的成员变量只能使用基本类型、 String、 enum枚举、Class类型、注解类型以及这些类型的数组,比如int可以,但Integer这种包装类型就不行

2、成员函数只能用public 或默认这两个访问修饰符

3、注解上面的注解@Target、 @Retention,称为 “元注解”,元注解就是专门用于给注解添加注解的注解,元注解就是天生就有的注解,直接用于注解的定义上,注意元注解不可少

4、注解里面的方法可以有默认值 default, 最好把参数名称设为"value",后加小括号

5、注解元素必须有确定的值,要么在定义注解的默认值中指定,要么在使用注解时指定,非基本类型的注解元素的值不能为null。因此, 使用空字符串或0作为默认值是一种常用的做法。这个约束使得处理器很难表现一个元素的存在或缺失的状态,因为每个注解的声明中,所有元素都存在,并且都具有相应的值,为了绕开这个约束,我们只能定义一些特殊的值,例如空字符串或者负数,一次表示某个元素不存在,在定义注解时,这已经成为一个习惯用法,像下面这样:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interfaceMyAnnotation{
    public int id() default -1;
    public String name() default "";
    public String address() default "";
}

 

6、注解类型元素

在上文元注解中,允许在使用注解时传递参数。我们也能让自定义注解的主体包含annotation type element (注解类型元素) 声明,它们看起来很像方法,可以定义可选的默认值。

@Target({ElementType.TYPE,ElementType.FIELD})
@Retention(RetentionPolicy.SOURCE)
public @interface Test {
    String value(); //无默认值
    int age() default 1; //有默认值
}
//注意:在使用注解时,如果定义的注解中的类型元素无默认值,则必须进行传值。
@Test("test") //如果只存在value元素需要传值的情况,则可以省略:元素名=
@Test(value="test",age = 2)
int i;

 

7、注解应用实例

 

没有注解加持的时候,如果我们想对下面的类进行校验:

class Teacher  {
    private Long id;
    private String name;
    private String mobilephone;
}

没有注解的话,检验逻辑应该是:

@PostMapping("/add")
public String addTeacher(@RequestBody Teacher teacher) {
    if(teacher == null)
        return "传入的对象为null,请传值";
    if(teacher.getId()==null || "".equals(teacher.getName()))
        return "传入的ID为空,请传值";
    if(teacher.getName()==null || "".equals(teacher.getName()))
        return "传入的姓名为空,请传值";
    if( teacher.getMobile()==null||"".equals(teacher.getMobile()))
        return"传入的手机号为空,请传值";
    return "SUCCESS";
}

如果成员变量比较多的话,就很繁琐,这个时候就可以使用注解来解决数据的校验工作, 比如说校验mobilephone这个字段。

class Teacher  {
    private Long id;
    private String name;
    @NotNull(message = "传入电话号码为null,请传值")
    @NotEmpty(message = "传入电话号码为,请空传值")
    @Length(min =11, max =11, message ="传入的手机号长度有误,必须为11位")
    private String mobilephone;
}

按照下述步骤来写一个简单的注解:

 

7.1、定义一个注解

@Target({ElementType.TYPE,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Length {
    int max();
    int min();
    String errorMsg();
}

 

7.2、使用上述定义的注解

class Teacher  {
    private Long id;
    private String name;
    @Length(min =11, max =11, errorMsg ="传入的手机号长度有误,必须为11位")
    private String mobilephone;
}

  

 

7.3、获取注解并验证注解(注解处理器,最重要!!)

Annotation是被动的元数据,永远不会有主动行为,但凡Annotation起作用的场合都是有一个执行机制或者调用者通过反射获得了这个元数据然后根据它采取行动。

利用反射特性在运行时获取注解的信息(注意注解的本质还是一个接口)

//注解处理器
//最重要的最关键的:注解处理器
class TeacherProcessor{
    //传入对象(表明注解在对象上)
    public static String  validate(Object object) throws Exception{
        //首先通过反射获取到object对象的字段信息
        //对上面的字段就可以获取到Teacher类中的Id,name和mobilephone三个字段
        Field[] fields = object.getClass().getDeclaredFields();
        for(Field field : fields) {
            //if 判断:检查该字段上有没有注解Length
            if(field.isAnnotationPresent(Length.class)){
                Length length = field.getAnnotation(Length.class);
                //设置一下权限,保证能够通过反射得到私有成员变量mobilephone
                field.setAccessible(true);
                //通过反射获取字段实际值的长度
                int value = ( (String) (field.get(object) )).length();
                //将字段值的长度和注解上面传入的值进行对比
                if(value < length.min() || value > length.max()){
                    return length.errorMsg();
                }
            }
        }
        return null;
    }
    //直接传入类对象(表明注解在类上)
    public static String  validate(Class<?> clazz) throws NoSuchMethodException{
        //首先通过反射获取到object对象的字段信息
        //对上面的字段就可以获取到Teacher类中的Id,name和mobilephone三个字段
        Field[] fields = clazz.getDeclaredFields();
        for(Field field : fields) {
            //if 判断:检查该字段上有没有注解Length
            if(field.isAnnotationPresent(Length.class)){
                Length length = field.getAnnotation(Length.class);
                //设置一下权限,保证能够通过反射得到私有成员变量mobilephone
                field.setAccessible(true);
               // field.get
                //通过反射获取字段实际值的长度
              //  int value = ( (String) field.get(cla) ).length();
                //将字段值的长度和注解上面传入的值进行对比
                if(11 < length.min() || 11 > length.max()){
                    return length.errorMsg();
                }
            }
        }
        return null;
    }
}

  

7.4、验证结果

//验证结果
public class TestTecherAnnotation {
    public static void main(String[] args) throws Exception {
        Teacher teacher = new Teacher();
        teacher.setMobilephone("123456789101");
        TeacherProcessor.validate(teacher);
    }
}

 

7.5 使用注解的例子(综合)

import java.lang.annotation.*;
import java.lang.reflect.Field;
/**
* Copyright@www.jrliu.com.
* Author:jrliu
* Date:2019/10/29
* Description:
*/
//定义一个注解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@interface Length {
    int max() ;
    int min() ;
    String errorMsg() default "";
}
 
//来针对具体的类使用注解
class Teacher  {
    private Long id;
    private String name;
    @Length(min =11, max =11, errorMsg ="传入的手机号长度有误,必须为11位")
    private String mobilephone;
    public String getMobilephone() {
        return mobilephone;
    }
    public void setMobilephone(String mobilephone) {
        this.mobilephone = mobilephone;
    }
}
 
//注解处理器
//最重要的最关键的:注解处理器
class TeacherProcessor{
    //传入对象
    public static String  validate(Object object) throws Exception{
        //首先通过反射获取到object对象的字段信息
        //对上面的字段就可以获取到Teacher类中的Id,name和mobilephone三个字段
        Field[] fields = object.getClass().getDeclaredFields();
        for(Field field : fields) {
            //if 判断:检查该字段上有没有注解Length
            if(field.isAnnotationPresent(Length.class)){
                Length length = field.getAnnotation(Length.class);
                //设置一下权限,保证能够通过反射得到私有成员变量mobilephone
                field.setAccessible(true);
                //通过反射获取传入对象的字段实际值的长度
                int value = ( (String) (field.get(object) )).length();
                //将字段值的长度和注解上面传入的值进行对比
                if(value < length.min() || value > length.max()){
                    return length.errorMsg();
                }
            }
        }
        return null;
    }
}
 
//验证结果
public class TestTecherAnnotation {
    public static void main(String[] args) throws Exception {
        Teacher teacher = new Teacher();
        teacher.setMobilephone("123456789101");
        String message = TeacherProcessor.validate(teacher);
        System.out.println("注解信息:\n" + message);
    }
}

  

8、Java中常见的注解

 

@Override标记类型注解,它被用作标注方法。它说明了被标注的方法重载了父类的方法,起到了断言的作用。如果我们使用了这种注解在一个没有覆盖父类方法的方法时,java 编译器将以一个编译错误来警示。 这个annotaton常常在我们试图覆盖父类方法而确又写错了方法名时发挥威力。使用方法极其简单:在使用此annotation时只要在被修饰的方法前面加上@Override即可。

 

@Deprecated当一个类型或者类型成员使用@Deprecated修饰的话,编译器将不鼓励使用这个被标注的程序元素。所以使用这种修饰具有一定的“延续性”:如果我们在代码中通过继承或者覆盖的方式使用了这个过时的类型或者成员,虽然继承或者覆盖后的类型或者成员并不是被声明为@Deprecated,但编译器仍然要报警。

 

@SuppressWarnings 不是一个标记类型注解。它有一个类型为String[] 的成员,这个成员的值为被禁止的警告名,同时编译器忽略掉无法识别的警告名。 

 

@SuppressWarnings 被用于有选择的关闭编译器对类、方法、成员变量、变量初始化的警告。 在java5.0,sun提供的javac编译器为我们提供了-Xlint选项来使编译器对合法的程序代码提出警告,此种警告从某种程度上代表了程序错误。例如当我们使用一个generic collection类而又没有提供它的类型时,编译器将提示出"unchecked warning"的警告。通常当这种情况发生时,我们就需要查找引起警告的代码。如果它真的表示错误,我们就需要纠正它。例如如果警告信息表明我们代码中的switch语句没有覆盖所有可能的case,那么我们就应增加一个默认的case来避免这种警告。

 

有时我们无法避免这种警告,例如,我们使用必须和非generic的旧代码交互的generic collection类时,我们不能避免这个unchecked warning。此时@SuppressWarning就要派上用场了,在调用的方法前增加@SuppressWarnings修饰,告诉编译器停止对此方法的警告。

 SuppressWarning不是一个标记注解。它有一个类型为String[]的成员,这个成员的值为被禁止的警告名。对于javac编译器来讲,被-Xlint选项有效的警告 名也同样对@SuppressWarings有效,同时编译器忽略掉无法识别的警告名。

 

 SuppressWarnings注解的常见参数值的简单说明:

    1.deprecation:使用了不赞成使用的类或方法时的警告;

    2.unchecked:执行了未检查的转换时的警告,例如当使用集合时没有用泛型 (Generics) 来指定集合保存的类型;

    3.fallthrough:当 Switch 程序块直接通往下一种情况而没有 Break 时的警告;

    4.path:在类路径、源文件路径等中有不存在的路径时的警告;

    5.serial:当在可序列化的类上缺少 serialVersionUID 定义时的警告;

    6.finally:任何 finally 子句不能正常完成时的警告;

              7.all:关于以上所有情况的警告。

 

9、完整的一个注解范例

import java.lang.annotation.*;
import java.lang.reflect.Field;
 
/*** Author:jrliu
* Date:2020/2/29
* Description:定义注解并且测试注解使用
*/
//水果名称注释
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@interface FruitName{
    public String value() default "";
}
//水果颜色注释
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@interface FruitColor{
    //颜色枚举
    public enum Color {RED, GREEN, YELLOW};
    //颜色属性
    Color getFruitColor() default Color.GREEN;
}
@Retention(RetentionPolicy.RUNTIME)
//水果供应商注解
@interface FruitProvider {
    //供应商编号
    public int id() default -1;
    //供应商名称
    public String name() default "";
    //供应商地址
    public String address() default "";
}
//来使用注解
class Apple{
    @FruitName("Apple")
    private String appleName;
    @FruitColor(getFruitColor = FruitColor.Color.RED)
    private String appleColor;
    @FruitProvider(id = 1, name = "红富士集团", address = "红富士大厦")
    private String appleProvider;
    public String getAppleName() {
        return appleName;
    }
    public void setAppleName(String appleName) {
        this.appleName = appleName;
    }
    public String getAppleColor() {
        return appleColor;
    }
    public void setAppleColor(String appleColor) {
        this.appleColor = appleColor;
    }
    public String getAppleProvider() {
        return appleProvider;
    }
    public void setAppleProvider(String appleProvider) {
        this.appleProvider = appleProvider;
    }
}
//最重要的最关键的:注解处理器
class FruitProcessor{
    public static void getFruitInfo(Class<?> clazz){
        String strFruitName = " 水果名称:";
        String strFruitColor = " 水果颜色:";
        String strFruitProvicer = "水果供应商信息:";
        Field[] fields = clazz.getDeclaredFields();
        for(Field field: fields) {
            field.setAccessible(true);
            if(field.isAnnotationPresent(FruitName.class)) {
                FruitName fruitName = (FruitName) field.getAnnotation(FruitName.class);
                strFruitName = strFruitName + fruitName.value();
                System.out.println(strFruitName);
            } else if(field.isAnnotationPresent(FruitColor.class)) {
                FruitColor fruitColor = (FruitColor)field.getAnnotation(FruitColor.class);
                strFruitColor = strFruitColor +  fruitColor.getFruitColor().toString();
                System.out.println(strFruitColor);
            } else if(field.isAnnotationPresent(FruitProvider.class)){
                FruitProvider fruitProvider = (FruitProvider)field.getAnnotation(FruitProvider.class);
                strFruitProvicer = strFruitProvicer + " 供应商编号" + fruitProvider.id() +"供应商名称"+ fruitProvider.name() +  "供应商地址" + fruitProvider.address() ;
                System.out.println(strFruitProvicer);
            }
        }
    }
}
//验证测试结果。
public class TestFruitAnnotation {
    public static void main(String[] args) {
        FruitProcessor.getFruitInfo(Apple.class);
    }
}