专注于 JetBrains IDEA 全家桶,永久激活,教程
持续更新 PyCharm,IDEA,WebStorm,PhpStorm,DataGrip,RubyMine,CLion,AppCode 永久激活教程

简单回顾Java注解

Java 注解

Java注解(Annotation),是在Java 5.0之后引入的一种机制。它和注释的区别是什么呢?注释是为了给程序员留下信息,告知其代码块的功能和参数意义等等。而注解则可以为JVM留下重要的配置信息,以便JVM在运行时(RUNTIME)通过反射机制获取它。

注解的作用

1、 在编译时进行格式检查,比如使用@Override检测该方法是否属于重写方法,使用@FunctionalInterface来检测该接口是否满足函数式接口的规范……等等。
2、 在运行时为JVM提供参数信息,替代原本配置文件的功能。

> Spring 3.0+ 开始已经逐渐放弃了`XML`来配置`bean`。尤其是后续的SpringBoot微服务框架则完全采用了**约定大于配置**的思想,通过大量的注解来替代了原本在`XML`中需要配置的大量标签内容。
> 
> 总体来说,通过注解的方式,牺牲了代码耦合性,但是却大大提高了开发效率。

定义一个Java 注解

首先来定义一个最简单(且不那么完整)的注解:

public @interface PlainAnnotation{}

从形式上来看,有点像是定义了一个接口,只不过在关键字上有些区别:是@interface而非interface。它表示实现了java.lang.annotation.Annotation接口。

此注解的类型即它的名字:PlainAnnotation。IntelliJ IDEA会对注解性质的类型进行黄色高亮字标注。

我们不需要显示声明extends关系,因为Annotation接口的实现细节在底层由编译器完成。

目前的注解没有任何内容,它目前只属于标记注解。而我们希望在注解内留下一些内容。

在注解中留下信息

注解内可以声明变量以保存一些信息,形式如下:

public @interface PlainAnotation {
    int value() default 2;
    String name() default "annoy";
}

我们在这个注解中留下了一个value值和一个name值,并默认初始值为2。注意:从形式上来看,这些属性值是通过方法来获取的,因此一定要带上();另外,这些值一定要有默认值,通过default关键字来实现。

注解内部元素的数据类型是受限的

注解内部的元素类型支持以下类型:

  • byte,short,Int等八种基本数据类型
  • String
  • Class
  • enum
  • Annotation
  • 以上类型的数组类型:xxx[]

这份白名单里没有包装类:也就是无法使用Integer,Double等类型。下面用代码来表示:

//基本数据类型
int INT() default 0;

//String类型
String STRING() default "";

//定义一个内部枚举类
enum Status {OK,ERROR}

//Status枚举类型
Status STATUS() default Status.OK;

//嵌套注解类型。
//注:该注解是笔者自定义的注解,主要是为了展示写法格式。
IntegerValue INTEGER_VALUE() default @IntegerValue(100);

//Class类型
Class<?> CLASS() default Void.class;

//数组类型,不需要额外的default关键字。
int[] INTS();

注解的简单使用

我们用上一节中声明的PlainAnnotation,试着用在我们的程式中的任意一处使用此注解(现在没有对这个注解做出任何限制,在下一节的元注解中就会详细介绍),并为valuename元素赋值:

@PlainAnotation(value = 2,name = "author")

注意,当注解只存在一个名字为value的元素时,注解赋值可简化如下:

@PlainAnotation(100)

再或者,这个注解内没有任何元素(即只作为一个单纯的标记注解),则可以省去参数。

@PlainAnotation

元注解

声明一个自定义注解时,我们通常还要定义这个注解修饰的类型,以及这个注解的生命周期(或者说策略属性),以此来声明一个规范,功能明确的注解。而定义这些规范我们同样使用注解去完成。这些用于定义注解的注解,我们又称为元注解。

@Target(ElementType._)

此元注解决定了自定义注解能够修饰哪些内容,通过ElementType枚举类来表示。这个枚举类的值包括了以下内容:

含义
ElementType.CONSTRUCTOR 这个注解用于修饰类构造器
ElementType.FIELD 这个注解用于修饰类的成员变量(域)
ElementType.LOCAL_VARIABLE 这个注解用于修饰某个方法的局部变量
ElementType.METHOD 这个注解用于修饰某个类的方法
ElementType.PACKAGE 这个注解用于修饰包声明
ElementType.PARAMETER 这个注解用于修饰参数声明
ElementType.TYPE 这个注解用于修饰类,接口(包含注解类),枚举类声明

如果没有显示声明此元注解,则表示注解可以存在于任何地方。注解可以作用在多处,写法如下:

@Target(value = {ElementType.TYPE,ElementType.FIELD})

@Retention(RetentionPolicy._)

这个元注解决定了注解的生存周期,通过RententionPolicy枚举类表示,这个枚举类的值包含了以下内容:

含义
RetentionPolicy.SOURCE 注解只在.java源代码中保留
RetentionPolicy.CLASS 注解在.class文件中保留
RetentionPolicy.RUNTIME 注解在运行期间保留

其中,只有生存周期为RUNTIME的注解所保存的信息在运行时可以被VM通过反射机制捕获。如果没有显示声明此注解,则默认此注解的生存周期为CLASS

@Inherited

此元注解表示:当这个注解所修饰某个类时,它的子类能否获得此注解。注意,注解本身是不可以继承的。使用@interface修饰的接口在编译时继承唯一java.lang.annotation.Annotation接口,这个工作由编译器执行。

用文字叙述比较费解,因此用代码举一个实例:

@Inherited
public @interface PlainAnnotation {}

@PlainAnnotation
class Parent {}

class Son extends Parent {
    //由于@PlainAnnotation是可继承的,因此子类也获得了来自父类的@PlainAnnotation接口。
}

稍后介绍如何通过反射的方式来获取一个类内的注解。

@Repeatable

在Java 1.8版本之前,是不允许在同一处地方出现相同的注解的。而@Repeatable注解的加入解决了这个问题。下面用一个实例来说明。作为一个Person,我希望TA同时具备两个(或以上)的身份,并且每一个身份通过一个@Identify注解来标注出来。

public @interface Identify {
    enum CAREER{TEACHER,STUDENT,FATHER,PERSON}
    CAREER value() default CAREER.STUDENT;
}

而为了能让一个Person同时具备两个相同注解,我们还需要另外声明一个**“容器注解”**@Identifies

public @interface Identifies {
    Identify[] value();
}

它内部的元素只有一个@Identify注解数组,并声明为value()。同时,我们还需要在原先的@Identify注解中添加@Repetable元注解,表示使用@Identifies作为装入重复@Identify的容器。

//补充后的Identify注解
@Repeatable(Identifies.class)
public @interface Identify {

    enum CAREER{TEACHER,STUDENT,FATHER,SPIDER_MAN}
    CAREER value() default CAREER.STUDENT;

}

随后我们就可以在一个Person类中做如下标注:

//这个Person的身份即是父亲,又是老师。
@Identify(Identify.CAREER.TEACHER)
@Identify(Identify.CAREER.FATHER)
public class Person { }

两点约束

注意,@Identify和其容器注解@Identifies相比:

1、 前者的元注解@Target所表示的范围要比后者(后者适用更多的ElementType值,且包含后者所有的ElementType值)或者相同
2、 后者(容器注解)的元注解@Rentention所标记的生命周期要比前者长或者相同

> 声明周期:SOURCE < CLASS < RUNTIME

如果不满足以上任何一个条件,编译器则会提示两种编译错误。详情可以参考下方链接:

java中@Repeatable的理解

注意

实际上,后续通过反射来验证,编译器认为该Person类仍然只持有一个注解。因为所有重复的@Identify都装入到了一个@Identify[]类型的value数组中。(实际是在给@Identifies赋值)

@Documented

若一个类使用了被元注解@Documented标记的注解,在生成javadoc文档时会保留该注解。比如我们将上文的@Identifies注解添加该元注解:

@Documented
public @interface Identifies {
    Identify[] value();
}

在命令行中使用javadoc命令为Person.java生成注释:

javadoc -d <docPath> <javaPath> -version -author

在文档中,Person类的注释将被标记出来。

104_1.png

Java内部提供的实用注解

除了元注解以外,Java还提供了许多其它的实用注解:

注解 功能
@Deprecated 该注解标记的内容表示“过时”,编译器会发出警告,并标记为删除线。
@Override 只能标注方法,编译器检测该方法是否是覆盖了父类中的方法。
@SuppressWarnings 编译器忽略指定类型的警告,不再弹出warnings.

这些注解的生存周期为CLASS,即只用于编译器对代码的检查,而不会影响运行期间程序的执行。

通过反射获取注解

java.lang.Reflect反射包下新增了AnnotatedElement接口,它表示JVM在运行期间已载入的注解元素。反射包的Construtor类,Field类,Method类,Package类和Class类都实现了AnnotationElement接口。

AnnotationElement接口在1.5版本提供了前4个实用方法。在1.8版本后又额外提供了后2个方法,用于获取被@Repetable标注的可重复注解(下文会介绍两个方法的细节)。

返回值 方法名称 作用
<A extends Annotation> getAnnotation(Class<A> AnoClass) 如果该元素上存在指定的注解,则返回,反之为null。
Annotation[] getAnnotations() 获取此元素上的所有注解,如果该元素属于Class,则还包括其父类中标记为@Inherited的注解。
boolean isAnnotationPresent(Class<? extends Annotation> AnoClass) 如果该元素上存在指定的注解,则返回true
Annotation[] getDaclaredAnnotations() 返回直接存在于此元素上的注解。若没有注解可返回,则返回长度为0的数组。
<A extends Annotation> A[] getAnnotationsByType(Class<A> annotationClass) [Java 8] 返回此元素上的注解,对@Repetable注解同样生效。可能会返回父类的注解。
<A extends Annotation> A[] getDeclaredAnnotationsByType(Class<A> annotationClass) [Java 8] 返回此元素本身的注解,对@Repetable注解同样生效。

通过一个完整实例尝试捕获注解

假设Student目前有以下注解(这些注解的生存周期都是RUNTIME):

/**
 * I'm spider man!
 */
@DBTable(TableName = "students")
@Identify(Identify.CAREER.SPIDER_MAN)
public class Student extends Person {

    @StringValue("Peter")
    private String name;

    @IntegerValue(22)
    private int age;

}

为了获取所有的注解,我们需要从Student类的Class以及Fields中通过上述方法反射注解。完整逻辑代码如下:

Class<?> clazz = Student.class;

//获取Student类中的注解
for (Annotation annotation : clazz.getAnnotations()) {
    System.out.println(annotation);
}

//获取Student每个私有域的所有注解
for (Field field : clazz.getDeclaredFields()) {
    field.setAccessible(true);
    for (Annotation annotation : field.getAnnotations()) {
        System.out.println(annotation);
    }
}

//需要 throws NoSuchFieldException
StringValue stringValue = clazz.getDeclaredField("name").getAnnotation(StringValue.class);
if (stringValue != null) System.out.println("通过getAnnotation获取了name域的注解:" + stringValue);

if (clazz.getDeclaredField("age").isAnnotationPresent(IntegerValue.class))
{
    System.out.println("通过isAnnotationPresent方法检测age域是否存在IntegerValue注解:true");
}

最后屏幕的打印结果:

@ano.DBTable(TableName=students)
@ano.Identify(value=SPIDER_MAN)
@ano.StringValue(value=Peter)
@ano.IntegerValue(value=22)
通过getAnnotation获取了name域的注解:@ano.StringValue(value=Peter)
通过isAnnotationPresent方法检测age域是否存在IntegerValue注解:true

Process finished with exit code 0

获取@Repetable标记的重复注解

在Java 1.8版本之后,主要是通过getAnnotationsByTypegetDeclaredAnnotationsByType来获取重复的标签。注意,之

假设现在有如下的继承关系:

/**
 * He is a Teacher and father.
 */
@Identify(Identify.CAREER.TEACHER)
@Identify(Identify.CAREER.FATHER)
public class Person { }

/**
 * I'm spider man!
 */
@DBTable(TableName = "students")
@Identify(Identify.CAREER.SPIDER_MAN)
public class Student extends Person {}

补充@Identify@Identifies的相关细节。注意,一定要显式将@Retention补充为运行时,否则程序无法通过反射捕获;另外,这两个注解都是可以在子类中被捕获的:

@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Identifies {
    Identify[] value();
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Identifies.class)
@Inherited
public @interface Identify {

    enum CAREER{TEACHER,STUDENT,FATHER,SPIDER_MAN}
    CAREER value() default CAREER.STUDENT;

}

通过getAnnotationsByType来获取Student上所有保留的可重复@Identify标签:

Class<?> clazz = Student.class;

//只获取Student上的注解。
Identify[] identifies = clazz.getDeclaredAnnotationsByType(Identify.class);

for (Identify identify : identifies) {
    System.out.println(identify.value());
}
System.out.println("length:" + identifies.length);

打印结果是:SPIDER_MAN,length:1。那我们使用了getDeclaredAnnotationsByType方法,是不是就能获取StudentPerson类中所有的Identify注解了呢?

我们修改注解的获取方法,重新执行一次这个方法。

//试图获取Student和Person所有的Identify注解。可行吗?
Identify[] identifies = clazz.getDeclaredAnnotationsByType(Identify.class);

但是结果并没有变化。我们进入到AnnotatedElement中查看getAnnotationsByType相关的源码:

default <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass) {
    /*
          * Definition of associated: directly or indirectly present OR
          * neither directly nor indirectly present AND the element is
          * a Class, the annotation type is inheritable, and the
          * annotation type is associated with the superclass of the
          * element.
          */
    T[] result = getDeclaredAnnotationsByType(annotationClass);

    if (result.length == 0 && // Neither directly nor indirectly present
        this instanceof Class && // the element is a class
        AnnotationType.getInstance(annotationClass).isInherited()) { // Inheritable
        Class<?> superClass = ((Class<?>) this).getSuperclass();
        if (superClass != null) {
            // Determine if the annotation is associated with the
            // superclass
            result = superClass.getAnnotationsByType(annotationClass);
        }
    }
    return result;
}

也就是说,getAnnotationsByType在执行之前,首先会尝试getDeclaredAnnotationsByType方法获取类自身的注解,只有在当前类没有找到任何有关的注解时,才会尝试去父类当中寻找注解,并且前提是这个注解是允许子类继承的。

一言以蔽之,我们要么只能获取Student类的@Identify注解,要么只能获取Person类的@Idenfity注解,且需要保证:@Identify@Identifies注解是可继承的;Student类中没有Identify注解。

为了证明这个结论,我们使用代码来验证。我们将Student类中的@Identify注解抹掉,然后重新运行主程序:

/**
 * I'm spider man!
 */
@DBTable(TableName = "students")
public class Student extends Person {}

这次,主程序中打印的是:TEACHER,FATHER;length:2。此刻我们再将@Identify@Identifies@Inherited元注解也取消掉,再次运行主程序:

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

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Identifies.class)
public @interface Identify {

    enum CAREER{TEACHER,STUDENT,FATHER,SPIDER_MAN}
    CAREER value() default CAREER.STUDENT;

}

主程序显示:length:0

参数行注解

参数行注解的形式如下。Spring为我们提供的@PathVariable,@RequestBody等都属于参数行注解。

//@NotNull就是Java为我们提供的参数行注解。
public static void PrintString(@NotNull String s)
{
    System.out.println(s);
}

使用参数行注解来实现参数为空的默认赋值操作

我们完全可以使用Optional[T]来快速实现功能,在这里只是为了对参数行注解的定义和使用进行介绍。

我们定义一个新的注解@DefaultInt,并为它的value赋值为1。我们希望它提供一个保障机制:当外部传入的参数为null时,则使用@DefaultInt提供的value做为默认值。

另外,将@Target元注解的值声明为ElementType.PARAMETER,来表示它是一个参数行注解。

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

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface DefaultInt {
    int value() default 1;
}

随后我们定义一个安全的add方法,并通过注解为参数ab赋值。这里由我们自己手写检查机制:

public static int safeAdd(@DefaultInt(1) Integer a,@DefaultInt(2) Integer b) throws Exception {
    //--首先通过反射获取方法--------//
    Method safeAdd = Main.class.getDeclaredMethod("safeAdd", Integer.class, Integer.class);

    //每个参数可能又包含多个注解,因此返回值是一个二维数组。
    Annotation[][] parameterAnnotations = safeAdd.getParameterAnnotations();

    List<Integer> DEFAULTINTS = new LinkedList<>();
    //遍历每个参数的(多个)注解。
    for (Annotation[] p: parameterAnnotations) {

        DefaultInt defaultInt = null;

        //检查是否存在DefaultInt注解
        for (Annotation e : p){
            if (e instanceof DefaultInt) {defaultInt = (DefaultInt) e;break;}
        }

        if (defaultInt!=null) {
            DEFAULTINTS.add(defaultInt.value());
        }
    }
    //-----在这里其实忽略了很多对DEFAULTINTS的检查细节---------------//
    a = a==null ? DEFAULTINTS.get(0) : a;
    b = b==null ? DEFAULTINTS.get(1) : b;
    return a + b;
}

随后,我们在主程序中故意声明两个为空的Integer,并将它们传入到safeAdd方法中:

Integer A = null;
Integer B = null;

try {
    System.out.println(safeAdd(A,B));
} catch (Exception e) {
    e.printStackTrace();
}

由于safeAdd方法在真正进行计算之前,会首先通过注解获取默认值,因此即便ABnull,程序也不会报错。

简单的示例:根据注解拼装SQL语句

假设我们已经拥有了数据库连接,并且希望能够利用注解配置的方式,自动组合SQL,从而来实现一个最简易的ORM框架

/**
 * I'm spider man!
 */
@DBTable(TableName = "students")
public class Student extends Person {

    @StringValue("Peter")
    private String name;

    @IntegerValue(22)
    private int age;

}

@DBTable为配置的数据库表名,而@StringValue@IntValue为默认的配置值。

希望程序能够根据这些注解信息就可以组装成SQL语句:

insert into students(name,age) values('Peter',22)

接下来使用注解配合反射的形式来实现这个功能:

public static String SQL(Class<?> c, db.Method method) {

    StringBuilder SQLBuilder = new StringBuilder();

    if (method == Method.INSERT) {
        SQLBuilder.append("insert into");
    }

    DBTable dbTable = c.getAnnotation(DBTable.class);

    //insert into student() values()
    if (dbTable != null) {
        SQLBuilder.append(" ");
        SQLBuilder.append(dbTable.TableName());
    }

    Field[] declaredFields = c.getDeclaredFields();

    StringBuilder columnBuilder = new StringBuilder("(");
    StringBuilder valueBuilder =new StringBuilder(" values(");

    for (Field f:
         declaredFields) {
        columnBuilder.append(f.getName());
        columnBuilder.append(",");

        Annotation[] annotations = f.getAnnotations();
        for (Annotation a:
             annotations) {

            if(a instanceof IntegerValue){
                valueBuilder.append(((IntegerValue) a).value());
                valueBuilder.append(",");
            }

            if(a instanceof StringValue){
                valueBuilder.append("'");
                valueBuilder.append(((StringValue) a).value());
                valueBuilder.append("',");
            }
        }
    }

    columnBuilder.deleteCharAt(columnBuilder.lastIndexOf(","));
    columnBuilder.append(")");

    valueBuilder.deleteCharAt(valueBuilder.lastIndexOf(","));
    valueBuilder.append(")");

    return SQLBuilder.append(columnBuilder.toString()).append(valueBuilder.toString()).toString();

}

//----通过枚举类表示数据库操作----------//
public enum Method {
    //笔者只定义了一种。
    INSERT
}

我们在主函数中只需要传入”POJO”对象,并表示要执行Insert方法,程序就会自动生成SQL语句了。

public static void main(String[] args) throws IllegalAccessException {

    System.out.println(SQL(Student.class, Method.INSERT));
}

这种操作方式,是否想起了Hibernate框架?

参考文献

1、 深入理解Java注解类型
2、 菜鸟教程: Java 注解
3、 java注解-最通俗易懂的讲解

文章永久链接:https://tech.souyunku.com/41990

未经允许不得转载:搜云库技术团队 » 简单回顾Java注解

JetBrains 全家桶,激活、破解、教程

提供 JetBrains 全家桶激活码、注册码、破解补丁下载及详细激活教程,支持 IntelliJ IDEA、PyCharm、WebStorm 等工具的永久激活。无论是破解教程,还是最新激活码,均可免费获得,帮助开发者解决常见激活问题,确保轻松破解并快速使用 JetBrains 软件。获取免费的破解补丁和激活码,快速解决激活难题,全面覆盖 2024/2025 版本!

联系我们联系我们