疯狂 java 讲义之注解-Annotation

前言

​ 注解,其实是代码里的特殊标 记,这些标记可以在编译、类加载、运行时被读取,并执行相应的处 理。通过使用注解,程序开发人员可以在不改变原有逻辑的情况下, 在源文件中嵌入一些补充的信息。代码分析工具、开发工具和部署工具可以通过这些补充信息进行验证或者进行部署。

​ 注解提供了一种为程序元素设置元数据的方法,从某些方面来看,注解就像修饰符一样,可用于修饰包、类、构造器、方法、成员 变量、参数、局部变量的声明,这些信息被存储在注解的
“name=value”对中。

注意

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

​ 注解能被用来为程序元素(类、方法、成员变量等)设置元数 据。值得指出的是,注解不影响程序代码的执行,无论增加、删除注 解,代码都始终如一地执行。如果希望让程序中的注解在运行时起一 定的作用,只有通过某种配套的工具对注解中的信息进行访问和处 理,访问和处理注解的工具统称 APT ( Annotation Processing
Tool)。

一、基本注解

​ 注解必须使用工具来处理,工具负责提取注解里包含的元数据, 工具还会根据这些元数据增加额外的功能。在系统学习新的注解语法 之前,先看一下Java提供的5个基本注解的用法——使用注解时要在其 前面增加@符号,并把该注解当成一个修饰符使用,用于修饰它支持的 程序元素。
5个基本的注解如下:

➢ @Override

➢ @Deprecated

➢ @SuppressWarnings

➢ @SafeVarargs

➢ @FunctionalInterface

上 面 5 个 基 本 注 解 中 的 @SafeVarargs 是 Java 7 新增的、@FunctionalInterface 是Java8 新增的。这5个基本的注解都定义在 java.lang 包下

(一)、@Override 限定重写父类方法

​ @Override就是用来指定方法覆载的,它可以强制一个子类必须覆 盖父类的方法。如下程序中使用@Override指定子类Apple的info()方
法必须重写父类方法。

​ @Override的作用是告诉编译器检查这个方法,保证父类要包含一个 被该方法重写的方法,否则就会编译出错。@Override主要是帮助程序 员避免一些低级错误,例如把一个名为 info 的方法不小心写成了inf0,这样的“低级错误”可能会成为后期排错时的巨大障碍。

(二)、@Deprecated Java9 增强

​ @Deprecated用于表示某个程序元素(类、方法等)已过时,当其 他程序使用已过时的类、方法时,编译器将会给出警告。如下程序指定 Apple 类中的 info() 方法已过时,其他程序中使用 Apple 类的info() 方法时编译器将会给出警告。
​ Java 9为@Deprecated注解增加了如下两个属性。

➢ forRemoval:该boolean类型的属性指定该API在将来是否会被 删除。
➢ since:该String类型的属性指定该API从哪个版本被标记为过时。

提示

​ @Deprecated 的作用与文档注释中的 @deprecated 标记的作用基本相同,但它们的用法不同,前者是 JDK5 才支持的注解,无须放在 文档注释语法(/**…*/部分)中,而是直接用于修饰程序中的程序单元,如方法、类、接口等。

(三)、@SuppressWarnings 抑制编译器警告

​ @SuppressWarnings 指示被该注解修饰的程序元素(以及该程序元素中的所有子元素)取消显示指定的编译器警告。@SuppressWarnings 会一直作用于该程序元素的所有子元素,例如,使用@SuppressWarnings 修饰某个类取消显示某个编译器警告,同时又修饰该类里的某个方法取消显示另一个编译器警告,那么该方法将会同时取消显示这两个编译器警告。

(四)、堆污染” 警告与 Java9 增强的 @SafeVarargs

public static void main(String[] args) {
    List list = new ArrayList<Integer>();
    // 添加元素时引发 unchecked 异常
    list.add(20);
    // "未经检查的转换" 警告,编译运行正常
    List<String> ls = list; // ①
    // 访问 ls 里的元素就会引起异常,这就是堆污染 (Heappollution)
    System.out.println(ls.get(0));
}

当把一个不带泛型的对象赋给一个带泛型的变量时,往往就会发生这种“堆污染”,如上代码 ① 处。

​ 对于形参个数可变的方法,该形参的类型又是泛型,这将更容易 导致“堆污染”

public static void test(List<String>... listStrArray) {
    // Java 不允许创建泛型数组,因此 listArray 只能被当成 List[] 处理
    // 此时相当于把 List<String> 赋值给 List, 已经发生了堆污染,如下
    List[] listArray = listStrArray; // 堆污染
    List<Integer> myList = new ArrayList<>();
    myList.add(new Random().nextInt(100));
    listArray[0] = myList;
    String s = listStrArray[0].get(0);
}

​ 由于该方法有 个形参是 List<String>…类型,个数可变的形参相当于数组,但 Java 又不支持泛型数组,因此程序只能把 List<String>…当成 List[] 处 理,这里就发生了“堆污染”。 在 Java6 以及更早的版本中,Java编译器认为 test() 方法完全没有问题,既不会提示错误,也没有提示警告

​ 从Java 7开始,Java编译器将会进行更严格的检查,Java编译器 在编译阶段就会发出一个参数化 vararg 类型 List<String> 的堆可能已受污染 的警告

​ 由此可见,Java 7会在定义该方法时就发出“堆污染”警告,这样保证开发者“更早”地注意到程序中可能存在的“漏洞”。 但在有些时候,开发者不希望看到这个警告,则可以使用如下三种方式来“抑制”这个警告。 ➢ 使用 @SafeVarargs 修饰引发该警告的方法或构造器。Java 9增 强了该注解,允许使用该注解修饰私有实例方法。
➢ 使用 @SuppressWarnings(“unchecked”) 修饰。

➢ 编译时使用-Xlint:varargs选项。

很明显,第三种方式一般比较少用,通常可以选择第一种或第二种方式,尤其是使用 @SafeVarargs 修饰引发该警告的方法或构造器,它是Java 7专门为抑制“堆污染”警告提供的。

(五)、@FunctionalInterface 函数式接口

​ 从Java 8开始:如果接口中只有一个(有且只有一个)抽象方法 (可以包含多个默认方法或多个static方法),该接口就是函数式接口。@FunctionalInterface 就是用来指定某个接口必须是函数式接口。

提示

​ 函数式接口就是为 Java8 的 Lambda 表达式准备的,Java8 允许使用 Lambda 表达式创建函数式接口的实例,因此 Java8 专门增加了 @FunctionalInterface。

@FunctionalInterface
public interface test {
    static void foo() {
        System.out.println("foo");
    }

    default void bar() {
        System.out.println("bar");
    }

    // 有且只有一个抽象方法
    void test();
}

二、JDK 的元注解

JDK 除在 java.lang 下提供了 5 个基本的注解之外,还在 java.lang.annotation 包下提供了 6 个Meta注解(元注解),其中有 5 个元注解都用于修饰其他的注解定义。其中 @Repeatable 专门用于定义 Java8 新增的重复注解

(一)、使用 @Retention

​ @Retention 只能用于修饰注解定义,用于指定被修饰的注解可以保留多长时间,@Retention 包含一个 RetentionPolicy 类型的 value 成员变量,所以使用 @Retention 时必须为该 value 成员变量指定值。value 成员变量的值只能是如下三个。

➢ RetentionPolicy.CLASS:

编译器将把注解记录在class文件 中。当运行Java程序时,JVM不可获取注解信息。这是默认值。

➢ RetentionPolicy.RUNTIME:

编译器将把注解记录在class文件 中。当运行Java程序时,JVM也可获取注解信息,程序可以通过 反射获取该注解信息。

➢ RetentionPolicy.SOURCE:

注解只保留在源代码中,编译器直接丢弃这种注解。

如果需要通过反射获取注解信息,就需要使用 value 属性值为 RetentionPolicy.RUNTIME 的@Retention。使用@Retention元注解可采用如下代码为value指定值。

// @Retention(value = RetentionPolicy.RUNTIME)
// 如果使用注解时只需要为value成员变量指定值,则使用该注解 时可以直接在该注解后的括号里指定value成员变量的值,无须使用 “value=变量值”的形式。
@Retention(RetentionPolicy.RUNTIME) 
public @interface TestRetention {}

如果使用注解时只需要为 value 成员变量指定值,则使用该注解 时可以直接在该注解后的括号里指定value成员变量的值,无须使用 “value=变量值”的形式。

(二)、使用 @Target

​ @Target 也只能修饰注解定义,它用于指定被修饰的注解能用于修饰哪些程序单元。@Target元注解也包含一个名为value的成员变量,该成员变量的值只能是如下几个。

➢ ElementType.ANNOTATION_TYPE:指定该策略的注解只能修饰 注解。

➢ ElementType.CONSTRUCTOR:指定该策略的注解只能修饰 构造器。

➢ ElementType.FIELD:指定该策略的注解只能修饰 成员变量。

➢ ElementType.LOCAL_VARIABLE:指定该策略的注解只能修饰 局部变量。

➢ ElementType.METHOD:指定该策略的注解只能修饰 方法定义。

➢ ElementType.PACKAGE:指定该策略的注解只能修饰包定义。

➢ ElementType.PARAMETER:指定该策略的注解可以修饰参数。

➢ ElementType.TYPE:指定该策略的注解可以修饰类、接口(包括注解类型)或枚举定义。

(三)、使用 @Documented 注解

​ @Documented 用于指定被该元注解修饰的注解类将被 javadoc 工具提取成文档,如果定义注解类时使用了 @Documented 修饰,则所有使用该注解修饰的程序元素的API文档中将会包含该注解说明。

@Target(ElementType.METHOD)
@Documented // 被该元注解修饰的注解类将被 javadoc 工具提取成文档
public @interface TestAnnotation {}

创建 Test 类

public class Test {
    @TestAnnotation // 在方法上使用注解
    public void info() {
        System.out.println("--");
    }
}

创建 Test1 类,并使用 Test 的 info 方法

public class Test1 {
    public static void main(String[] args) {
        new Test().info();
    }
}

image-20220629001635828

将 @TestAnnotation 注解中的 @Documented 注解去掉,再次提取 javadoc 文档

image-20220629003744330

(四)、使用 @Inherited

​ @Inherited 元注解指定被它修饰的注解将具有继承性——如果某个类使用了 @Xxx 注解(定义该注解时使用了 @Inherited)修饰,则其子类将自动被 @Xxx 修饰。

定义注解 @TestAnnotation

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited // 使其修饰的注解具有继承性
public @interface TestAnnotation { }

测试继承性

// 使用被 @Inherited 修饰的注解修饰 Base 类
@TestAnnotation
class Base {
}

// Test 类只是继承了 Base 类,并没有使用 @TestAnnotation 注解修饰
public class Test extends Base {
    public static void main(String[] args) {
        // 打印 Test 类是否有 @TestAnnotation 注解修饰
        System.out.println(Test.class.isAnnotationPresent(TestAnnotation.class)); // true
    }
}

三、自定义注解

自定义注解并利用注解完成一些实际功能

(一)、定义注解

​ 定义新的注解类型使用 @interface 关键字(在原有的 interface 关键字前增加 @ 符号)定义一个新的注解类型与定义一个接口非常像。定义了注解之后,就可以在程序的任何地方使用该注解,使用 注解的语法非常类似于 public、final 这样的修饰符,通常可用于修饰程序中的类、方法、变量、接口等定义。通常会把注解放在所有修饰符之前,而且由于使用注解时可能还需要为成员变量指定值,因而注解的长度可能较长,所以通常把注解另放一行。在默认情况下,注解可用于修饰任何程序元素,包括类、接口、 方法等。

​ 注解不仅可以是简单的注解,还可以带成员变量,成员变量在注解定义中以无形参的方法形式来声明,其方法名和返回值定义了该成员变量的名字和类型。

public @interface TestAnnotation {
    // 定义了带两个成员变量的注解
    // 注解中的成员变量以方法的形式来定义
    String name();
    int age();
}

一旦在注解里定义了成员变量之后,使用该注解时就应该为它的 成员变量指定值,如下面代码所示

@TestAnnotation(name = "zuijin", age = 13)
public class Test { }

也可以在定义注解的成员变量时为其指定初始值(默认值),指定成员变量的初始值可使用 default 关键字。如果为注解的成员变量指定了默认值,使用该注解时则可以不为 这些成员变量指定值,而是直接使用默认值。当然也可以在使用 @TestAnnotation 注解时为成员变量指定值,如果为 @TestAnnotation 的成员变量指定了值,则默认值不会起作用。

public @interface TestAnnotation {
    // 定义了带两个成员变量的注解
    // 注解中的成员变量以方法的形式来定义
    String name() default "zuijin";
    int age() default 18;
}

根据注解是否可以包含成员变量,可以把注解分为如下两类。

➢ 标记注解:没有定义成员变量的注解类型被称为标记。这种注 解仅利用自身的存在与否来提供信息,如前面介绍的 @Override、@Test 等注解。
➢ 元数据注解:包含成员变量的注解,因为它们可以接受更多的元数据,所以也被称为元数据注解。

(二)、提取注解信息

​ 使用注解修饰了类、方法、成员变量等成员之后,这些注解不会自己生效,必须由开发者提供相应的工具来提取并处理注解信息。

​ Java 使用 java.lang.annotation.Annotation 接口来代表程序元素前面的注解,该接口是所有注解的父接口 。Java5 在 java.lang.reflect 包下新增了 AnnotatedElement 接口,该接口代表程序中可以接受注解的程序元素。该接口主要有如下几个实现类。

➢ Class:类定义。

➢ Constructor:构造器定义。

➢ Field:类的成员变量定义。

➢ Method:类的方法定义。

➢ Package:类的包定义。

​ java.lang.reflect 包下主要包含一些实现反射功能的工具类,从 Java5 开始,java.lang.reflect 包所提供的反射API增加了读取运行时注解的能力。只有当定义注解时使用了 @Retention(RetentionPolicy.RUNTIME)修饰,该注解才会在运行时可见,JVM才会在装载 *.class 文件时读取保存在 class 文件中的注解信息

​ AnnotatedElement 接口是所有程序元素(如 Class、Method、Constructor 等)的父接口,所以程序通过反射获取了某个类的 AnnotatedElement 对象(如 Class、Method、Constructor 等)之后,程序就可以调用该对象的如下几个方法来访问注解信息。

➢ <A extends Annotation> A getAnnotation(Class<A> annotationClass):

返回该程序元素上存在的、指定类型的注解,如果该类型的注解不存在,则返回null。

➢ <A extends Annotation> A getDeclaredAnnotation(Class<A> annotationClass) :

这是Java 8新增的方法,该方法尝试获取直接修饰该程序元素、指定类型的注解。如果该类型的注解不存在,则返回null。

➢ Annotation[] getAnnotations():

返回该程序元素上存在的 所有注解。

➢ Annotation[] getDeclaredAnnotations():

返回直接修饰该 程序元素的所有注解。

➢ boolean isAnnotationPresent(Class< ? extends Annotation> annotationClass):

判断该程序元素上是否存在 指定类型的注解,如果存在则返回true,否则返回false。

➢ <A extends Annotation> A[] getAnnotationsByType(Class<A> annotationClass):

该方法 的功能与前面介绍的 getAnnotation() 方法基本相似。但由于 Java8 增加了重复注解功能,因此需要使用该方法获取修饰该 程序元素、指定类型的多个注解。

➢ <A extends Annotation> A[] getDeclaredAnnotationsByType(Class<A> annotationClass) :

该方法的功能与前面介绍的 getDeclaredAnnotations() 方法基本相似。但由于Java8 增加了重复注解功能,因此需要使用该方法获取直接修饰该程序元素、指定类型的多个注解。

注意:

为了获得程序中的程序元素(如Class、Method等),必须使用反射知识。

eg: 获取Test类的info方法里的所有注解,并将这些注解打印出来。

1、创建注解

@Target(ElementType.METHOD)
// 只有当定义注解时使用了 @Retention(RetentionPolicy.RUNTIME)修饰,该注解才会在运行时可见,JVM才会在装载 *.class 文件时读取保存在 class 文件中的注解信息
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
    // 定义了带两个成员变量的注解
    // 注解中的成员变量以方法的形式来定义
    String name() default "zuijin";
    int age() default 18;
}

2、使用注解

public class Test {
    @TestAnnotation(name = "zuijin", age = 13)
    public void info() {}
}

3、打印出 Test 类中 info() 方法的注解

public class Test2 {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException {
        // Annotation[] aArray = Class.forName(Test.class.getName()).getMethod("info").getAnnotations();
        Annotation[] aArray = Test.class.getMethod("info").getAnnotations();
        for (Annotation annotation : aArray) {
            System.out.println(annotation);
        }
    }
}

打印结果。

image-20220629213121224

​ 如果需要获取某个注解里的元数据,则可以将注解强制类型转换成所需的注解类型,然后通过注解对象的抽象方法来访问这些元数据。

public class Test2 {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException {
        // Annotation[] aArray = Class.forName(Test.class.getName()).getMethod("info").getAnnotations();
        Annotation[] aArray = Test.class.getMethod("info").getAnnotations();
        for (Annotation annotation : aArray) {
            if (annotation instanceof TestAnnotation) {
            	// 将注解强制类型转换成所需的注解类型
                System.out.println(((TestAnnotation) annotation).name());
            }
        }
    }
}

打印结果

image-20220629214625957

(三)、使用注解的示例

1,简单的标记注解

(1)、定义一个简单的标记注解
// 使用 @Retention 指定注解保留到运行时
@Retention(RetentionPolicy.RUNTIME)
// 使用 @Target 指定被修饰的注解可用于修饰方法
@Target(ElementType.METHOD)
// 定义一个注解,仅用于标记,不包含任何成员变量
public @interface Testable { }
(2)、编写测试用例
public class MyTest {
    // 使用 @Testable 标记该方法是可测试的
    @Testable
    public static void m1() { }

    // 使用 @Testable 标记该方法是可测试的
    @Testable
    public static void m2() {
        throw new NullPointerException("这是一个空指针异常!");
    }

    // 使用 @Testable 标记该方法是可测试的
    @Testable
    public static void m3() {
        throw new RuntimeException("运行时异常!");
    }


    public static void m4() { }

    public static void m5() { }
}

根据预测结果,应该只有前三个方法是可测试的

(3)、注解处理工具
public class ProcessorTest {
    public static void main(String[] args) {
        int passed = 0;
        int failed = 0;
        Method[] methods = MyTest.class.getMethods();
        for (Method method : methods) {
            // isAnnotationPresent 如果此元素上存在指定类型的注释,则返回 true,否则返回 false。此方法主要是为了方便访问标记注释而设计的。
            // 判断方法是否被 @Testable 修饰
            if (method.isAnnotationPresent(Testable.class)) {
                try {
                    method.invoke(null);
                    passed++; // 运行成功
                } catch (Exception e) {
                    System.out.println("方法 " + method + " 运行失败,抛出 " + e.getCause() + " 异常");
                    failed++; // 运行失败
                }
            }
        }
        // 统计结果
        System.out.println("共测试了:" + (passed + failed) + " 个方法");
        System.out.println("成功运行:" + passed + " 个方法");
        System.out.println("失败运行:" + failed + " 个方法");
    }
}

运行结果

方法 public static void main.annotation.exampleuse.MyTest.m2() 运行失败,抛出 java.lang.NullPointerException: 这是一个空指针异常! 异常
方法 public static void main.annotation.exampleuse.MyTest.m3() 运行失败,抛出 java.lang.RuntimeException: 运行时异常! 异常
共测试了:3 个方法
成功运行:1 个方法
失败运行:2 个方法

2,利用注解简化事件编程

(1)、创建 @ActionListenerFor 注解

​ 通过使用注解来简化事件编程,在传统的事件编程中总是需要通过 addActionListener() 方法来为事件源绑定事件监听器,本示例程序中则通过@ActionListenerFor 来为程序中的按钮绑定事件监听器。

// 只能修饰 成员变量。
@Target(ElementType.FIELD)
// 注解保留到运行时
@Retention(RetentionPolicy.RUNTIME)
public @interface ActionListenerFor {
    // 定义一个成员变量用于设置元数据
    // 该 listener 成员变量用于保存监听器实现类
    Class<? extends ActionListener> listener();
}

定义了这个 @ActionListenerFor之后,使用该注解时需要指定一个 listener 成员变量,该成员变量用于指定监听器的实现类。

(2)、使用@ActionListenerFor 注解为按钮绑定事件监听器
public class AnnotationTest {
    private final JFrame mainWin = new JFrame("使用注解绑定事件监听器");
    // 使用 @ActionListenerFor 注解为 ok 按钮绑定事件监听器
    @ActionListenerFor(listener = OkListener.class)
    private final JButton ok = new JButton("确定");
    // 使用 @ActionListenerFor 注解为 cancel 按钮绑定事件监听器
    @ActionListenerFor(listener = CancelListener.class)
    private final JButton cancel = new JButton("取消");

    public void init() {
        JPanel jPanel = new JPanel();
        jPanel.add(ok);
        jPanel.add(cancel);
        mainWin.add(jPanel);
        // 处理本程序中的注解
        ActionListenerInstaller.processAnnotations(this); // ①
        mainWin.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        // 设置窗口居中
        mainWin.setLocationRelativeTo(null);
        mainWin.pack();
        mainWin.setVisible(true);
    }

    public static void main(String[] args) {
        new AnnotationTest().init();
    }
}

// 定义 ok 按钮的事件监听器实现类
class OkListener implements ActionListener {
    public void actionPerformed(ActionEvent event) {
        JOptionPane.showMessageDialog(null, "单击了确认按钮");
    }
}

// 定义 cancel 按钮的事件监听器实现类
class CancelListener implements ActionListener {

    @Override
    public void actionPerformed(ActionEvent e) {
        JOptionPane.showMessageDialog(null, "单击了取消按钮");
    }
}

​ 上面程序中定义了两个JButton按钮,并使用 @ActionListenerFor 注解为这两个按钮绑定了事件监听器,使用 @ActionListenerFor,注解时传入了,listener,元数据,该数据用于设定 每个按钮的监听器实现类。 正如前面提到的,如果仅在程序中使用注解是不会起任何作用的,必须使用注解处理工具来处理程序中的注解。程序中 ① 处代码使用了 ActionListenerInstaller 类来处理本程序中的注解,该处理器分 析目标对象中的所有成员变量,如果该成员变量前使用了 @ActionListenerFor 修饰,则取出该注解中的listener 元数据,并根据该数据来绑定事件监听器。

(3)、处理程序中的注解类 ActionListenerInstaller
public class ActionListenerInstaller {
    public static void processAnnotations(Object obj) {
        try {
            // 获取 obj 对象的类
            Class<?> c = obj.getClass();
            // 获取指定 obj 对象的所有成员变量,并遍历每个成员变量
            Field[] fields = c.getDeclaredFields();
            for (Field field : fields) {
                // 将该成员变量设置成可自由访问
                field.setAccessible(true);
                // 获取成员变量上的 ActionListenerFor 类型注解
                ActionListenerFor a = field.getAnnotation(ActionListenerFor.class);
                // 返回指定对象上此 Field 表示的字段的值。如果该值具有原始类型,则该值会自动包装在对象中
                Object fieldValue = field.get(obj);
                if (a != null && fieldValue instanceof AbstractButton) {
                    // 获取 a 注解里的 listener 元数据(一个监听器类)
                    Class<? extends ActionListener> listenerClazz = a.listener();
                    // 使用反射来创建 listener 类的对象
                    ActionListener actionListener = listenerClazz.getDeclaredConstructor().newInstance();
                    var ab = (AbstractButton) fieldValue;
                    // 为 ab 按钮添加监听器事件
                    ab.addActionListener(actionListener);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

测试

注解1

(四)、创建重复注解之 @Repeatable

​ 在 Java8 以前,同一个程序元素前最多只能使用一个相同类型的注解;如果需要在同一个元素前使用多个相同类型的注解,则必须使用注解“容器”。Java8 允许使用多个相同类型的注解来修饰同一个类

1、创建 @Fruit 注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
// 用于指示它(元)注释其声明的注释类型是可重复的。不指明则重复使用会报错
// @Repeatable的值表示可重复注解类型的包含注解类型。
@Repeatable(Fruits.class)
public @interface Fruit {
    String name() default "苹果";

    int price();
}

​ 为了将该注解改造成重复注解,需要使用 @Repeatable 修饰该注解,使用 @Repeatable 时必须为value 成员变量指定值,该成员变量的值应该是一个“容器”注解——该“容器”注解可包含多个 @Fruit,因此还需要定义如下的“容器”注解。

2、容器注解 @Fruits

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

​ 该代码定义了一个 Fruit[] 类型的 value 成员变量,这意味着 @Fruits 注解的 value 成员变量可接受多个 @Fruit 注解,因此 @Fruits 注解可作为 @Fruit 的容器。

​ @Retention(RetentionPolicy.RUNTIME) 指定 @Fruits 注解信息也可保留到运行时,这是必需的,因为:@Fruiit 注解信息需要保留到运行时, 如果 @Fruits 注解只能保留到源代码级别 (RetentionPolicy.SOURCE)或类文件(RetentionPolicy.CLASS),将会导致 @Fruits 的保留期小于 @Fruit 的保留期,如果程序将多个 @Fruit 注解放入 @Fruits 中,若JVM丢弃了 @Fruits 注解,自然也就丢弃了 @Fruit 的信息——而我们希望@FkTag注解可以保留到运行时,这就矛盾了。

注意

“容器”注解的保留期必须比它所包含的注解的保留期更长, 否则编译器会报错。

3、重复注解获取

// 使用重复注解
@Fruit(price = 30)
@Fruit(name = "梨子", price = 20)
public class FruitTest {
    public static void main(String[] args) {
        // 使用 Java8 新增的 getDeclaredAnnotationsByType() 方法获取
        // 修饰 FruitTest 类的多个 @Fruit 注解
        Fruit[] tags = FruitTest.class.getDeclaredAnnotationsByType(Fruit.class);
        for (Fruit tag : tags) {
            System.out.println("name:" + tag.name() + " price:" + tag.price());
        }

        /*
         * 虽然 FruitTest 代码中并未显式使用 @Fruits 注解,但是由于程序使用了两个 @Fruit 注解修饰该类,
         * 因此系统会自动将两个 @Fruit 注解作为 @Fruits 的 value 成员变量的数组元素处理。
         * 因此,下列代码将可以成功地获取到 @Fruits 注解。
         * */
        Fruits container = FruitTest.class.getDeclaredAnnotation(Fruits.class);
        System.out.println(container);
    }
}

提示:

​ 重复注解只是一种简化写法,这种简化写法是一种假象:多个重复注解其实会被作为“容器”注解的 value成员变量的数组元素。例如上面的重复的 @Fruit 注解其实会被作为 @Fruits 注解的 value 成员变量的数组元素处理。

(五)、类型注解

​ Java8 为 ElementType 枚举增加了 TYPE_PARAMETER、TYPE_USE 两个枚举值,这样就允许定义注 解时使用 @Target(ElementType.TYPE_USE)修饰,这种注解被称为类型注解(Type Annotation),类型注解可用于修饰在任何地方出现的类型。

​ 在 Java8 以前,只能在定义各种程序元素(定义类、定义接口、 定义方法、定义成员变量……)时使用注解。从 Java8 开始,类型注解可以修饰在任何地方出现的类型。比如,允许在如下位置使用类型注解。

➢ 创建对象(用new关键字创建)。

➢ 类型转换。

➢ 使用 implements 实现接口。

➢ 使用 throws声明抛出异常。

上面这些情形都会用到类型,因此都可以使用类型注解来修饰。

eg:

// 定义一个简单类型的注解,不带任何成员变量
@Target(ElementType.TYPE_USE)
@interface NotNull { }

// 定义类时使用类型注解
@NotNull
public class TypeAnTest implements @NotNull /* implements 时使用类型注解 */ Serializable {
    // 方法形参中使用类型注解,抛出异常中使用类型注解
    public static void main(@NotNull String[] args) throws @NotNull FileNotFoundException {
        Object obj = "zuijin";
        // 强制类型转换时使用类型注解
        String string = (@NotNull String) obj;
        // 创建对象时使用类型注解
        Object win = new @NotNull JFrame("醉瑾");
    }

    // 泛型中使用类型注解
    public void foo(List<@NotNull String> info) { }
}

​ 需要指出的是,上面程序虽然大量使用了 @NotNull 注解,但这些注解暂时不会起任何作用——因为并没有为这些注解提供处理工具。而且 Java8 本身并没有提供对类型注解执行检查的框架,因此如果需要让这些类型注解发挥作用,开发者需要自己实现类型注解检查框架。
​ 幸运的是,Java8 提供了类型注解之后,第三方组织在发布他们的框架时,可能会随着框架一起发布类型注解检查工具,这样普通开发者即可直接使用第三方框架提供的类型注解,从而让编译器执行更严格的检查,保证代码更加健壮。

四、编译时处理注解

​ APT(Annotation Processing Tool)是一种注解处理工具,它对源代码文件进行检测,并找出源文件所包含的注解信息,然后针对注 解信息进行额外的处理。使用 APT 工具处理注解时可以根据源文件中的注解生成额外的源文件和其他的文件(文件的具体内容由注解处理器的编写者决定),APT 还会编译生成的源代码文件和原来的源文件,将它们一起生成 class 文件。

​ 使用APT的主要目的是简化开发者的工作量,因为APT可以在编译程序源代码的同时生成一些附属文件(比如源文件、类文件、程序发 布描述文件等),这些附属文件的内容也都与源代码相关。换句话说,使用 APT 可以代替传统的对代码信息和附属文件的维护工作。

​ Java提供的 javac.exe 工具有一个 -processor 选项,该选项可指定一个注解处理器,如果在编译Java 源文件时通过该选项指定了注解处理器,那么这个注解处理器将会在编译时提取并处理Java源文件中的注解。每个注解处理器都需要实现 javax.annotation.processing 包下的 Processor 接口。不过实现该接口必须实现它里面所有的方法,因此通常会采用继承 AbstractProcessor 的方式来实现注解处理器。一个注解处理器可以处理一种或者多种注解类型。一个注解 处理器可以处理一种或者多种注解类型。

(一)、定义修饰持久化类的 @Persitent 注解

import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
@Documented
public @interface Persistent {
    String table();
}

​ 这是一个非常简单的注解,它能修饰类、接口等类型声明,这个注解使用了 @Retention 元注解指定它仅在 Java 源文件中保留,运行时不能通过反射来读取该注解信息。

(二)、修饰标识属性的 @Id 注解

import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
@Documented
public @interface Id {
    String column();
    String type();
    String generator();
}

(三)、修饰普通成员属性的注解 @Property 注解

import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
@Documented
public @interface Property {
    String column();
    String type();
}

(四)、实体类 Person

@Persistent(table = "person_inf")
public class Person {
    @Id(column = "person_id", type = "integer", generator = "identity")
    private int id;
    @Property(column = "person_name", type = "string")
    private String name;
    @Property(column = "person_age", type = "integer")
    private int age;

    public Person() {
    }

    public Person(int id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }
	// 省略 getter / setter
}

上面的 Person 类是一个非常普通的 Java 类,但这个普通的 Java 类中使用了 @Persistent、@Id 和 @Property 三个注解进行修饰。下面为这三个注解提供一个APT工具,该工具的功能是根据注解来生成一个 Hibernate映射文件(不懂Hibernate也没有关系,读者只需要明白可以根据这些注解来生成另一份XML文件即可)。

(五)、APT 工具生成额外文件

import javax.annotation.processing.*;
import javax.lang.model.element.*;
import javax.lang.model.*;
import java.io.*;
import java.util.*;
@SupportedSourceVersion(SourceVersion.RELEASE_11)
// 指定可处理@Persistent、@Id、@Property三个注解
@SupportedAnnotationTypes({"Persistent", "Id", "Property"})
public class HibernateAnnotationProcessor extends AbstractProcessor {
    // 循环处理每个需要处理的程序对象
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // 定义一个文件输出流,用于生成额外的文件
        PrintStream ps = null;
        try {
            // 遍历每个被 @Persistent 修饰的 class 文件
            for (Element t : roundEnv.getElementsAnnotatedWith(Persistent.class)) {
                // 获取正在处理的类名
                Name clazzName = t.getSimpleName();
                // 获取类定义前的@Persistent注解
                Persistent per = t.getAnnotation(Persistent.class);
                // 创建文件输出流
                ps = new PrintStream(new FileOutputStream(clazzName + ".hbm.xml"));
                // 执行输出
                ps.println("<?xml version=\"1.0\"?>");
                ps.println("<!DOCTYPE hibernate-mapping PUBLIC");
                ps.println("	\"-//Hibernate/Hibernate " + "Mapping DTD 3.0//EN\"");
                ps.println("	\"http://www.hibernate.org/dtd/" + "hibernate-mapping-3.0.dtd\">");
                ps.println("<hibernate-mapping>");
                ps.print("	<class name=\"" + t);
                // 输出 per 的 table() 的值
                ps.println("\" table=\"" + per.table() + "\">");
                for (Element f : t.getEnclosedElements()) {
                    // 只处理成员变量上的注解
                    if (f.getKind() == ElementKind.FIELD)   // ①
                    {
                        // 获取成员变量定义前的@Id注解
                        Id id = f.getAnnotation(Id.class);      // ②
                        // 当@Id注解存在时输出<id.../>元素
                        if (id != null) {
                            ps.println("		<id name=\"" + f.getSimpleName() + "\" column=\"" + id.column() + "\" type=\"" + id.type() + "\">");
                            ps.println("		<generator class=\"" + id.generator() + "\"/>");
                            ps.println("		</id>");
                        }
                        // 获取成员变量定义前的@Property注解
                        Property p = f.getAnnotation(Property.class);  // ③
                        // 当 @Property 注解存在时输出<property.../>元素
                        if (p != null) {
                            ps.println("		<property name=\"" + f.getSimpleName() + "\" column=\"" + p.column() + "\" type=\"" + p.type() + "\"/>");
                        }
                    }
                }
                ps.println("	</class>");
                ps.println("</hibernate-mapping>");
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            if (ps != null) {
                try {
                    ps.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        }
        return true;
    }
}

​ 上面的注解处理器其实非常简单,与前面通过反射来获取注解信息不同的是,这个注解处理器使用RoundEnvironment 来获取注解信息,RoundEnvironment 里包含了一个getElementsAnnotatedWith() 方法,可根据注解获取需要处理的程序单元,这个程序单元由 Element 代表。Element 里包含一个 getKind() 方法,该方法返回 Element 所代表的程序单元,返回值可以是 ElementKind.CLASS(类),ElementKind.FIELD(成员变量)等。

​ 除此之外,Element 还包含一个 getEnclosedElements() 方法,该方法可用于获取该 Element 里定义的所有程序单元,包括成员变量、方法、构造器、内部类等。

接下来程序只处理成员变量前面的注解,因此程序先判断这个 Element必须是ElementKind.FIELD(如上面程序中①号粗体字代码所 示)。 再接下来程序调用了Element提供的 getAnnotation(Class clazz) 方法来获取修饰该 Element 的注解,如上面程序中②③号粗体 字代码就是获取成员变量上注解对象的代码。获取到成员变量上的 @Id、@Property 注解之后,接下来就根据它们提供的信息执行输出

(六)、命令行运行生成 xml 文件

将以上代码放入同一目录,并命名为相应 .java 文件,在目录下进入 cmd 运行命令行,运行如下命令

javac -encoding UTF-8 *.java # 先执行此句编译所有 java 文件
javac -processor HibernateAnnotationProcessor Person.java

在相同目录下生成如下文件

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
	"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
	"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
	<class name="Person" table="person_inf">
		<id name="id" column="person_id" type="integer">
		<generator class="identity"/>
		</id>
		<property name="name" column="person_name" type="string"/>
		<property name="age" column="person_age" type="integer"/>
	</class>
</hibernate-mapping>

)。 再接下来程序调用了Element提供的 getAnnotation(Class clazz) 方法来获取修饰该 Element 的注解,如上面程序中②③号粗体 字代码就是获取成员变量上注解对象的代码。获取到成员变量上的 @Id、@Property 注解之后,接下来就根据它们提供的信息执行输出

(六)、命令行运行生成 xml 文件

将以上代码放入同一目录,并命名为相应 .java 文件,在目录下进入 cmd 运行命令行,运行如下命令

javac -encoding UTF-8 *.java # 先执行此句编译所有 java 文件
javac -processor HibernateAnnotationProcessor Person.java

在相同目录下生成如下文件

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
	"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
	"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
	<class name="Person" table="person_inf">
		<id name="id" column="person_id" type="integer">
		<generator class="identity"/>
		</id>
		<property name="name" column="person_name" type="string"/>
		<property name="age" column="person_age" type="integer"/>
	</class>
</hibernate-mapping>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夏至xz

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值