• 欢迎光临~

秒懂 Java之注解类型(@Annotation)

开发技术 开发技术 2022-08-07 次浏览

[版权申明]非商业目的注明出处可自由转载
出自:shusheng007

概述

照例先啰嗦几句,刚开始接触Java的时候,某一天发现调用的一个方法被划了一个删除横线,查看这个方法的源代码的时候发现除了上面有一句@Deprecated代码外,和其他方法没有区别,所以我断定就是这货起的作用,当时觉得好神奇,于是乎我开始了对Java注解的了解,这个过程是不连续的,最近比较闲,所以总结一下。

理解Java注解

注解就相当于对源代码打的标签,给代码打上标签和删除标签对源代码没有任何影响。有的人要说了,你尽几把瞎扯,没有影响,打这些标签干毛线呢?其实不是这些标签自己起了什么作用,而且外部工具通过访问这些标签,然后根据不同的标签做出了相应的处理。这是注解的精髓,理解了这一点一切就变得不再那么神秘。
例如我们写代码用的IDE(例如 IntelliJ Idea),它检查发现某一个方法上面有@Deprecated这个注解,它就会在所有调用这个方法的地方将这个方法标记为删除。

访问和处理Annotation的工具统称为APT(Annotation Processing Tool)

基本语法

注解可以分为以下3类

基本注解

Java内置的注解共有5个

@Override:让编译器检查被标记的方法,保证其重写了父类的某一个方法。此注解只能标记方法。源码如下:

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

@Deprecated:标记某些程序元素已经过时,程序员请不要再使用了。源码如下:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}

@SuppressWarnings :告诉编译器不要给老子显示警告,老子不想看,老子清楚的知道自己在干什么。源码如下:

@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
    String[] value();
}           

其内部有一个String数组,根据传入的值来取消相应的警告:
deprecation:使用了不赞成使用的类或方法时的警告;
unchecked:执行了未检查的转换时的警告,例如当使用集合时没有用泛型 (Generics) 来指定集合保存的类型;
fallthrough:当 Switch 程序块直接通往下一种情况而没有 Break 时的警告;
path:在类路径、源文件路径等中有不存在的路径时的警告;
serial:当在可序列化的类上缺少 serialVersionUID 定义时的警告;
finally:任何 finally 子句不能正常完成时的警告;
all:关于以上所有情况的警告。

@SafeVarargs(Java7 新增) @SuppressWarnings可以用在各种需要取消警告的地方,而 @SafeVarargs主要用在取消参数的警告。就是说编译器如果检查到你对方法参数的操作,有可能发生问题时会给出警告,但是你很自(任)性,老子不要警告,于是你就加上了这个标签。源码如下:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD})
public @interface SafeVarargs {}

其实这个注解是专为取消堆污染警告设置的,因为Java7会对可能产生堆污染的代码提出警告,什么是堆污染?且看下面代码

 @SafeVarargs
 private static void method(List<String>... strLists) {
     List[] array = strLists;
     List<Integer> tmpList = Arrays.asList(42);
     array[0] = tmpList; //非法操作,但是没有警告
     String s = strLists[0].get(0); //ClassCastException at runtime!
 }

如果不使用 @SafeVarargs,这个方法在编译时候是会产生警告的 : “...使用了未经检查或不安全的操作。”,用了就不会有警告,但是在运行时会抛异常。

@FunctionalInterface(Java8 新增): 标记型注解,告诉编译器检查被标注的接口是否是一个函数接口,即检查这个接口是否只包含一个抽象方法,只有函数接口才可以使用Lambda表达式创建实例。源码如下:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FunctionalInterface {}

元注解

用来给其他注解打标签的注解,即用来注解其他注解的注解。元注解共有6个。从上面的基本注解的源代码中就会看到使用了元注解来注解自己。

@Retention:用于指定被此元注解标注的注解的保留时长,源代码如下:

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.ANNOTATION_TYPE)
    public @interface Retention {
        RetentionPolicy value();
    }

从源代码中可以看出,其有一个属性value,返回一个枚举RetentionPolicy 类型,有3种类型:

  • RetentionPolicy.SOURCE: :注解信息只保留在源代码中,编译器编译源码时会将其直接丢弃。
  • RetentionPolicy.CLASS::注解信息保留在class文件中,但是虚拟机VM不会持有其信息。
  • RetentionPolicy.RUNTIME::注解信息保留在class文件中,而且VM也会持有此注解信息,所以可以通过反射的方式获得注解信息。

@Target:用于指定被此元注解标注的注解可以标注的程序元素,源码如下:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    ElementType[] value();
}

从源码中可以看出,其有一个属性value,返回一个枚举ElementType类型的数组,这个数组的值就代表了可以使用的程序元素。

public enum ElementType {
   /**标明该注解可以用于类、接口(包括注解类型)或enum声明*/
   TYPE,

   /** 标明该注解可以用于字段(域)声明,包括enum实例 */
   FIELD,

   /** 标明该注解可以用于方法声明 */
   METHOD,

   /** 标明该注解可以用于参数声明 */
   PARAMETER,

   /** 标明注解可以用于构造函数声明 */
   CONSTRUCTOR,

   /** 标明注解可以用于局部变量声明 */
   LOCAL_VARIABLE,

   /** 标明注解可以用于注解声明(应用于另一个注解上)*/
   ANNOTATION_TYPE,

   /** 标明注解可以用于包声明 */
   PACKAGE,

   /**
    * 标明注解可以用于类型参数声明(1.8新加入)
    */
   TYPE_PARAMETER,

   /**
    * 类型使用声明(1.8新加入)
    */
   TYPE_USE
}

例如@Override注解使用了 @Target(ElementType.METHOD),那么就意味着,它只能注解方法,不能注解其他程序元素。

当注解未指定Target值时,则此注解可以用于任何元素之上,多个值使用{}包含并用逗号隔开,下面代码表示,此Annotation既可以注解构造函数、字段和方法:

@Target(value={CONSTRUCTOR, FIELD, METHOD})

值得注意的是TYPE_PARAMETERTYPE_USE是Java8 加入的新类型,在Java8之前,只能在声明各种程序元素时使用注解,而TYPE_PARAMETER允许使用注解修饰参数类型,TYPE_USE允许使用注解修饰任意类型。

//TYPE_PARAMETER 修饰类型参数
class A<@Parameter T> { }

//TYPE_USE则可以用于标注任意类型(不包括class)

//用于父类或者接口
class Image implements @Rectangular Shape { }

//用于构造函数
new @Path String("/usr/bin")

//用于强制转换和instanceof检查,注意这些注解中用于外部工具,它们不会对类型转换或者instanceof的检查行为带来任何影响。
String path=(@Path String)input;
if(input instanceof @Path String)

//用于指定异常
public Person read() throws @Localized IOException.

//用于通配符绑定
List<@ReadOnly ? extends Person>
List<? extends @ReadOnly Person>

@NotNull String.class //非法,不能标注class
import java.lang.@NotNull String //非法,不能标注import

虽然Java8 提供了类型注解,但是没有提供APT,所以需要框架自己实现。

@Documented:将被标注的注解生成到javadoc中。

@Inherited:其让被修饰的注解拥有被继承的能力。如下,我们有一个用@Inherited修饰的注解@InAnnotation,那么这个注解就拥有了被继承的能力。

@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface InAnnotation{
}

@InAnnotation
class Base{}

class Son extends Base{}

当使用此注解修饰一个基类Base, 其子类Son 并没有使用任何注解修饰,但是其已经拥有了@InAnnotation这个注解,相当于Son 已经被@InAnnotation修饰了

@Repeatable :使被修饰的注解可以重复的注解某一个程序元素。例如下面的代码中@ShuSheng这个自定义注解使用了@Repeatable修饰,所以其可以按照下面的语法重复的注解一个类。

@ShuSheng(name="frank",age=18)
@ShuSheng(age = 20)
public class AnnotationDemo{}

如何定义一个重复注解呢,如下所示,我们需要先定义一个容器,例如ShuShengs ,然后将其作为参数传入@Repeatable中。

@Repeatable(ShuShengs.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ShuSheng {
    String name() default "ben";
    int age();
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ShuShengs {
    ShuSheng[] value();
}

自定义注解

通过前面的讲解,很容易得出如何自定义一个注解。注解是以关键字@interface 来定义的,下面我们自定义一个注解。
注解按照有无成员变量可以分为:

  • 标记Annotation:无成员变量,只利用自身是否存在来提供信息。

    @Target(ElementType.METHOD)//只能应用于方法上。
    @Retention(RetentionPolicy.RUNTIME)//保存到运行时
    public @interface Test {
    }
  • 元数据Annotation:有一个或者多个成员变量,可以接收外界信息。

    @Target(ElementType.TYPE)//只能应用于类型上,包括类,接口。
    @Retention(RetentionPolicy.RUNTIME)//保存到运行时
    public @interface Table {
        String name() default "";
    }

    以上就是我们定义的两种注解,那么如何使用呢

//在类上使用该注解
@Table (name = "MEMBER")
public class Member {
    @Test 
    public void method()
    {...}
}

如何使用注解

就像我们文章开头说的,当我们使用注解修饰了程序元素后,这种Annotation不会自己起作用,的需要APT的帮助,那么这些APT就需要读取代码中的属性信息,那么如何读取呢?答案是通过反射

Annotation接口是所有注解的父接口(需要通过反编译查看),在java.lang.reflect 反射包下存在一个叫AnnotatedElement接口,其表示程序中可以接受注解的程序元素,例如 类,方法,字段,构造函数,包等等。而Java为使用反射的主要类实现了此接口,如反射包内的Constructor类、Field类、Method类、Package类和Class类。

当我们通过反射技术获取到反射包内的那些类型的实例后,就可以使用AnnotatedElement接口的中的API方法来获取注解的信息了。

  • <T extends Annotation> T getAnnotation(Class<T> annotationClass); : 返回该元素上存在的指定类型的注解,如果不存在则返回 null。
  • default <T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass){} :返回该元素上存在的直接修饰该元素的指定类型的注解,如果不存在则返回null.
  • Annotation[] getAnnotations();:返回该元素上存在的所有注解。
  • Annotation[] getDeclaredAnnotations();:返回该元素上存在的直接修饰该元素的所有注解。
  • default <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass){}:该方法功能与前面getAnnotation方法类似,但是由于Java8 加入了重复注解功能,因此需要此方法获取修饰该程序元素的指定类型的多个Annotation

获取注解简单示例

首先我们定义了两个注解@Master@ShuSheng@ShuSheng是一个可重复注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Master {
}

@Repeatable(ShuShengs.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ShuSheng {
    String name() default "ben";
    int age();
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ShuShengs {
    ShuSheng[] value();
}

然后我们定义了两个类,使用定义好的注解来修饰,如下

@Master
public class AnoBase {
}

@ShuSheng(name="frank",age=18)
@ShuSheng(age = 20)
public class AnnotationDemo extends AnoBase{
}

最后我们来调用相关函数获取相应的结果
private static void getAnnotation()

   {
       Class<?> cInstance=AnnotationDemo.class;

       //获取AnnotationDemo上的重复注解
       ShuSheng[] ssAons= cInstance.getAnnotationsByType(ShuSheng.class);
       System.out.println("重复注解:"+Arrays.asList(ssAons).toString());

       //获取AnnotationDemo上的所有注解,包括从父类继承的
       Annotation[] allAno=cInstance.getAnnotations();
       System.out.println("所有注解:"+Arrays.asList(allAno).toString());

       //判断AnnotationDemo上是否存在Master注解
       boolean isP=cInstance.isAnnotationPresent(Master.class);
       System.out.println("是否存在Master: "+isP);
   }

执行结果如下:

重复注解:[@top.ss007.ShuSheng(name=frank, age=18), @top.ss007.ShuSheng(name=ben, age=20)]
所有注解:[@top.ss007.ShuShengs(value=[@top.ss007.ShuSheng(name=frank, age=18), @top.ss007.ShuSheng(name=ben, age=20)])]
是否存在Master: false

自定义注解处理器(APT)

了解完注解与反射的相关API后,就可以更进一步。下面的实例自定义了一个APT,完成通过注解构建SQL语句的功能。此处代码来自此处。下面代码要求对数据库有初步认识。

先定义相关的注解

/**
 * 用来注解表
 */
@Target(ElementType.TYPE)//只能应用于类上
@Retention(RetentionPolicy.RUNTIME)//保存到运行时
public @interface DBTable {
    String name() default "";
}

/**
 * 注解Integer类型的字段
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLInteger {
    //该字段对应数据库表列名
    String name() default "";
    //嵌套注解
    Constraints constraint() default @Constraints;
}

/**
 * 注解String类型的字段
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLString {
    //对应数据库表的列名
    String name() default "";
    //列类型分配的长度,如varchar(30)的30
    int value() default 0;
    Constraints constraint() default @Constraints;
}

/**
 * 约束注解
 */
@Target(ElementType.FIELD)//只能应用在字段上
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraints {
    //判断是否作为主键约束
    boolean primaryKey() default false;
    //判断是否允许为null
    boolean allowNull() default false;
    //判断是否唯一
    boolean unique() default false;
}

/**
 * 数据库表Member对应实例类bean
 */
@DBTable(name = "MEMBER")
public class Member {
    //主键ID
    @SQLString(name = "ID",value = 50, constraint = @Constraints(primaryKey = true))
    private String id;

    @SQLString(name = "NAME" , value = 30)
    private String name;

    @SQLInteger(name = "AGE")
    private int age;

    @SQLString(name = "DESCRIPTION" ,value = 150 , constraint = @Constraints(allowNull = true))
    private String description;//个人描述

   //省略set get.....
}

上述定义4个注解,分别是@DBTable(用于类上)、@Constraints(用于字段上)、 @SQLString(用于字段上)、@SQLString(用于字段上)并在Member类中使用这些注解,这些注解的作用的是用于帮助注解处理器生成创建数据库表MEMBER的构建语句,在这里有点需要注意的是,我们使用了嵌套注解@Constraints,该注解主要用于判断字段是否为null或者字段是否唯一。接下来就需要编写我们自己的注解处理器了。

public class TableCreator {

  public static String createTableSql(String className) throws ClassNotFoundException {
    Class<?> cl = Class.forName(className);
    DBTable dbTable = cl.getAnnotation(DBTable.class);
    //如果没有表注解,直接返回
    if(dbTable == null) {
      System.out.println(
              "No DBTable annotations in class " + className);
      return null;
    }
    String tableName = dbTable.name();
    // If the name is empty, use the Class name:
    if(tableName.length() < 1)
      tableName = cl.getName().toUpperCase();
    List<String> columnDefs = new ArrayList<String>();
    //通过Class类API获取到所有成员字段
    for(Field field : cl.getDeclaredFields()) {
      String columnName = null;
      //获取字段上的注解
      Annotation[] anns = field.getDeclaredAnnotations();
      if(anns.length < 1)
        continue; // Not a db table column

      //判断注解类型
      if(anns[0] instanceof SQLInteger) {
        SQLInteger sInt = (SQLInteger) anns[0];
        //获取字段对应列名称,如果没有就是使用字段名称替代
        if(sInt.name().length() < 1)
          columnName = field.getName().toUpperCase();
        else
          columnName = sInt.name();
        //构建语句
        columnDefs.add(columnName + " INT" +
                getConstraints(sInt.constraint()));
      }
      //判断String类型
      if(anns[0] instanceof SQLString) {
        SQLString sString = (SQLString) anns[0];
        // Use field name if name not specified.
        if(sString.name().length() < 1)
          columnName = field.getName().toUpperCase();
        else
          columnName = sString.name();
        columnDefs.add(columnName + " VARCHAR(" +
                sString.value() + ")" +
                getConstraints(sString.constraint()));
      }
    }
    //数据库表构建语句
    StringBuilder createCommand = new StringBuilder(
            "CREATE TABLE " + tableName + "(");
    for(String columnDef : columnDefs)
      createCommand.append("n    " + columnDef + ",");

    // Remove trailing comma
    String tableCreate = createCommand.substring(
            0, createCommand.length() - 1) + ");";
    return tableCreate;
  }

  /**
   * 判断该字段是否有其他约束
   * @param con
   * @return
   */
  private static String getConstraints(Constraints con) {
    String constraints = "";
    if(!con.allowNull())
      constraints += " NOT NULL";
    if(con.primaryKey())
      constraints += " PRIMARY KEY";
    if(con.unique())
      constraints += " UNIQUE";
    return constraints;
  }

  public static void main(String[] args) throws Exception {
    String[] arg={"com.zejian.annotationdemo.Member"};
    for(String className : arg) {
      System.out.println("Table Creation SQL for " +
              className + " is :n" + createTableSql(className));
    }
  }
}

输出结果为:

Table Creation SQL for com.zejian.annotationdemo.Member is :
CREATE TABLE MEMBER(
        ID VARCHAR(50) NOT NULL PRIMARY KEY,
        NAME VARCHAR(30) NOT NULL,
        AGE INT NOT NULL,
        DESCRIPTION VARCHAR(150)
        );

常用场景

Annotation,特别是自定义注解,一般是在构建框架或者通用库时候使用的较多。下面列出了些我知道的,其他的欢迎补充。

Spring框架:解耦神器。
JUnit :测试框架
ButterKnife :在Android中使用的视图注解框架,Android的小伙伴们都知道。
Dagger2 :依赖注入框架,在Android中用的也比较多。
Retrofit :Http网络访问框架,Android网络请求标配。
Room :Google 发布的用于Android开发的本地数据库解决方案库。

参考文章:
深入理解Java注解类型(@Annotation)
《疯狂Java讲义》
《Think in java》

程序员灯塔
转载请注明原文链接:秒懂 Java之注解类型(@Annotation)
喜欢 (0)