IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> PHP知识库 -> Java编程笔记20:注解 -> 正文阅读

[PHP知识库]Java编程笔记20:注解

Java编程笔记20:注解

5c9c3b3b392ac581.jpg

图源:PHP中文网

注解(annotation)不同于可有可无的注释(comment),其同样是编程语言的重要组成部分。不同语言的注解其作用和风格也是不同的。

对于Python而言,因为它是一种强类型的动态语言,所以早期的Python缺乏在静态编译期的类型检查能力,因此后续PEP-484等PEP主键推出和完善了注解语法,通过注解可以帮助Python实现一部分的静态期类型检查能力。不过Python本质上依然是一种动态语言,注解被设置为非强制性的,也就是说有没有注解都不会影响程序运行。

对Python注解感兴趣的可以阅读PEP 484 – Type Hints - 魔芋红茶’s blog (icexmoon.cn)

对于Go语言来说,原生并不支持注解,其社区讨论的结果也并不打算支持这一特性,但一些开发者通过其它的途径和工具添加了对注解的支持,感兴趣的可以阅读Go:我有注解,Java:不,你没有! - 技术颜良 - 博客园 (cnblogs.com)

有意思的是PHP之前并不支持注解,在最新的PHP8中引入了注解,其用途和语法都和Java颇为类似,实际上这两种语言也都是借鉴于C/C++发展而来。对PHP8注解语法感兴趣的可以阅读下边两篇文章:

最后还是回到我们今天的主要话题——Java中的注解。

Java作为一种静态的强类型语言,本身有着完备的静态类型检查能力,因此并没有类似Python之类的需求。Java注解的主要工作是对源码进行一些额外“标注”,以可以用其它工具或程序来在编译期或运行时对源码进行处理。下面会用一些例子展示一些注解在Java中的用途。

基本语法

标准注解

实际上我们已经多次使用或见到过注解了,比如:

enum Color {
	...
    @Override
    public String toString() {
        return Fmt.sprintf("%s(%s)", name(), des);
    }
}

这里的@Override就是一个注解,准确的说它是一个标准注解,也就是Java标准库预定义的注解。

Java的标准注解包含:

  • @Override,方法覆盖(重写),如果有不正确的方法覆盖,编译器会报错。
  • @Deprecated,将方法、接口、类、属性等标记为“过期”,使用相应的元素将被编译器警告。
  • @SuppressWarnings,压制(屏蔽)错误。
  • @SafeVarargs,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。
  • @FunctionalInterface,标识一个匿名函数或函数式接口。

关于标准注解的更多说明可以阅读官方教程Predefined Annotation Types (The Java? Tutorials > Learning the Java Language > Annotations) (oracle.com)

元注解

除了标准注解,我们还可以自行定义注解,而定义注解就需要使用元注解,元注解分为以下几种:

  • @Target,限制注解可以用于什么地方。可以使用的值包括:
    • ElementType.ANNOTATION_TYPE,注解
    • ElementType.CONSTRUCTOR,构造器声明
    • ElementType.FIELD,域声明
    • ElementType.LOCAL_VARIABLE,局部变量声明
    • ElementType.METHOD,方法声明
    • ElementType.PACKAGE,包声明
    • ElementType.PARAMETED,参数声明
    • ElementType.TYPE,类、接口或enum声明
  • @Retention,表示需要在什么级别保存注解信息,具体包含:
    • RetentionPolicy.SOURCE,源码时,编译为字节码后被抛弃。
    • RetentionPolicy.CLAS,字节码时,JVM运行时被抛弃。
    • RetentionPolicy.RUNTIME,运行时,JVM运行时可以使用。
  • @Documented,将注解包含在Javadoc中。
  • @Inherited,允许子类继承父类中的注解。
  • @Repeatable,标识某注解可以在同一个声明上使用多次。

经常使用的是前两个,后两个并不常用。

定义注解

假设我们有这样的源码:

package ch20.define;

// Author: John Doe
// Date: 3/17/2002
// Current revision: 6
// Last modified: 4/12/2004
// By: Jane Doe
// Reviewers: Alice, Bill, Cindy
public class Main {
    public static void main(String[] args) {

    }
}

源码中的注释是很常见的做法——使用注释来标注作者、修改时间等常见的代码维护信息。

这样做的缺点在于这些信息只能用于源码查看,你很难对其进行统计分析等再次利用,因为它们只是普通注释,是非结构化的数据。即使你可以编写一个简单的文字处理程序进行分析,你也很难保证每个类似的注释都编写的很规范。

如果用注解来进行标注就不会产生类似的问题:

package ch20.define2;

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

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface Description {
    String autor();

    String date();

    int version() default 1;

    String lastModified();

    String lastModifiedBy();

    String[] reviewer();
}

这里使用元注解@Target@Retention对注解进行限定,如果不使用@Target,注解就可以被用于任意地方。

可以看出注解本身的语法类似于接口定义,不同的是,注解中的String autor();并不是在定义方法,而是在定义注解类型元素(annotation type element),也可以认为是“注解类型属性”。

注解类型元素可以使用的类型有:

  • intfloat等基础类型
  • String
  • Class
  • enum
  • Annotation
  • 以上类型的数组

并且可以使用default关键字给注解类型元素一个默认值。

使用注解也很简单:

package ch20.define2;

@Description(autor = "John Doe", date = "3/17/2002", version = 6, lastModified = "4/12/2004", lastModifiedBy = "Jane Doe", reviewer = {
        "Alice", "Bill", "Cindy" })
public class Main {
    public static void main(String[] args) {

    }
}

可以看到,注解类型元素需要像键值对那样被赋值,如果某个注解类型元素没有被赋值,就会被用默认值赋值。

需要注意的是,注解类型元素赋值不能被赋值为null,如果要表示一个空数据,可以用空字符串或者-1

此外,如果注解有一个名为value的注解类型元素,并且在赋值时仅对value赋值,可以进行简化,比如@Des(1)实际上就相当于@Des(value=1)

类型注解和可插式类型系统

在Java SE 8之前,注解只能用于声明,在Java SE 8中,你可以在任何使用类型的地方使用注解。这种注解被称作“类型注解”。

类型注解的目的是提供更强的类型检查,比如你可以为代码中的一个变量添加一个类型注解,以确保其不会被赋值为null

@NonNull String str;

当然这需要类型处理器的支持,不过类似的处理器不需要你自己实现,有很多成熟的可插式类型系统可以使用。

更多的相关内容可以阅读官方文档Type Annotations and Pluggable Type Systems (The Java? Tutorials > Learning the Java Language > Annotations) (oracle.com)

重复注解

在Java SE 8之前,你不能对一个元素重复使用相同的注解,但在Java SE 8之后,你就可以这样做了。

比如,你可能希望通过注解为方法设置一个“定时器”,即在指定的时间点调用该方法:

@Schedule(dayOfMonth="last")
@Schedule(dayOfWeek="Fri", hour="23")
public void doPeriodicCleanup() { ... }

在上面这个示例中,重复使用Schedule注解的目的是为doPeriodicCleanup方法调用设置两组定时器,一组用于在每个月最后一天调用方法,而领走一组用于在每周五的23点调用方法。

如果在Java SE 8之前,这样使用会产生一个编译错误,但现在,你如果将@Schedule以合适的方式定义为一个重复注解,就可以在同一个元素上重复使用它。

要想定义重复注解,需要先为注解定义一个容器注解(container annotation),所谓的容器注解就是可以包含一组重复注解的注解:

public @interface Schedules {
    Schedule[] value();
}

因为前面说了,在注解元素类型中唯一能使用的包含多个元素的类型就只能是数组,所以这里使用Schedule[],并且需要将其名称设置为value

然后需要在目标注解的定义中加上元注解@Repeatable

import java.lang.annotation.Repeatable;

@Repeatable(Schedules.class)
public @interface Schedule {
  String dayOfMonth() default "first";
  String dayOfWeek() default "Mon";
  int hour() default 12;
}

@Repeatablevalue内容为容器注解的Class对象。

之所以要用这么复杂的方式实现重复注解,是因为向后兼容的考虑。为了让旧的注解相关代码能正常运行,不得不这么做。对于非重复注解,依然可以使用以前的AnnotatedElement.getAnnotation(Class)方法获取,如果是重复注解,可以通过新的 AnnotatedElement.getAnnotationsByType(Class)方法获取多个重复注解。

重复注解的相关示例代码摘抄自官方文档,详细内容见Repeating Annotations (The Java? Tutorials > Learning the Java Language > Annotations) (oracle.com)

注解处理器

单一的注解用途有限,往往需要搭配注解处理器才能发挥应有的作用。下面通过一系列例子来说明注解的用途以及如何编写注解处理器。

假设我们有一个项目,是关于学生管理系统的,其中有一部分代码负责构建班级和学生的关系,其中涉及两个主要用例:

  1. 将学生添加到班级中。
  2. 将学生移除班级。

如果我们需要通过注解来帮助实现”用例驱动开发“,可能会创建如下的注解:

package ch20.usecase;

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

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
    int id();

    String description() default "";
}

接下来就可以在代码中使用该注解标注已经完成的用例:

package ch20.usecase;

import java.util.ArrayList;
import java.util.List;

import util.Fmt;

public class ClassRoom {
	...
    @UseCase(id = 1, description = "add student use case.")
    public boolean addStudent(Student s) {
        if (students.size() >= LIMIT) {
            return false;
        }
        students.add(s);
        return true;
    }

    @UseCase(id = 2, description = "remove student from classroom use case.")
    public boolean removeStudent(Student s) {
        return students.remove(s);
    }
	...
}

这里只展示关键代码,完整代码见java-notebook(github.com)

我们可以利用反射编写一个UseCase注解的处理器:

package ch20.usecase;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import util.Fmt;

public class UseCaseAnalysis {
    public static void main(String[] args) {
        List<Integer> allUseCaseIds = new ArrayList<>();
        allUseCaseIds.addAll(Arrays.asList(1, 2, 3, 4, 5));
        List<Class<?>> allCls = new ArrayList<>();
        allCls.addAll(Arrays.asList(Student.class, ClassRoom.class));
        int totalCase = allUseCaseIds.size();
        int downCase = 0;
        for (Class<?> cls : allCls) {
            Method[] methods = cls.getDeclaredMethods();
            for (Method method : methods) {
                UseCase useCase = method.getAnnotation(UseCase.class);
                if (useCase != null) {
                    int id = useCase.id();
                    String description = useCase.description();
                    Fmt.printf("usecase #%d is done, description is %s\n", id, description);
                    allUseCaseIds.remove(Integer.valueOf(id));
                    downCase++;
                }
            }
        }
        int leftCase = allUseCaseIds.size();
        Fmt.printf("test %d use case, %d is down, %d is not.\n", totalCase, downCase, leftCase);
    }
}
// usecase #2 is done, description is remove student from classroom use case.
// usecase #1 is done, description is add student use case.
// test 5 use case, 2 is down, 3 is not.

这里allUseCaseIds中存放的是全部的用例ID,示例中假设有五个。allCls中存放的是用例相关的类的Class对象。然后就可以遍历allCls并通过Class.getDeclaredMethods方法获取Method对象列表,然后通过Method.getAnnotation获取我们想要的注解对象UseCase

实际上getAnnotation方法来源于AnnotatedElement接口:

public interface AnnotatedElement {
	...
	<T extends Annotation> T getAnnotation(Class<T> annotationClass);
	Annotation[] getAnnotations();
	default <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass) {
		...
	}
	...
}

该接口有一些注解相关的方法,而MethodClass等反射相关的类都实现了该接口。

我们需要判断getAnnotation方法的返回值是否为null(并不是所有相关Class对象的方法都有一个UseCase注解),如果不为null,就可以通过UseCase定义中的注解类型元素获取相应的信息。

最终,我们可以通过打印和汇总相关信息明确有哪些用例已经完成,哪些用例还没有完成,以及用例覆盖率等。

需要注意的是,这个注解处理器是利用反射机制实现的,而反射是在运行时起作用的,所以这里的注解信息必须在JVM运行时有效,因此注解@UseCase的元注解@Retention必须指定为RetentionPolicy.RUNTIME

ORM

ORM全称为Object Relational Mapping(对象关系映射),其目的是将编程语言中的对象与数据库表结构进行映射,然后实现通过DB层的对象自动生成数据库表,或者根据数据库表自动生成DB层代码,甚至是互相更新。

通常成熟的编程语言都有很多配套的ORM框架,之前我在学习Go时就使用过Go语言的一些相关ORM框架,具体可以阅读Go语言编程笔记16:存储数据 - 魔芋红茶’s blog (icexmoon.cn)

当然这里要讨论的并不是ORM框架本身,而是注解在ORM框架中的应用。实际上利用注解可以很轻松的实现一个简单的ORM框架。

Go社区中提议加入注解功能的一个理由就是实现ORM框架,但显然没有被采纳。而目前Go语言的ORM框架使用特殊的注释来实现。

我们先看一个用注解实现ORM框架后的可能的代码是什么样的:

package ch20.orm;

@Table("student")
public class Student {
    @ColumnInt(name = "id", pk = true, isUnique = true, isNotNull = true)
    private int id;
    @ColumnStr(name = "name", size = 10, pk = false, isUnique = true, isNotNull = true)
    private String name;
}

我们用@Table注解来标注类对应的表结构的相关信息,比如表名、数据库引擎、字符集等。示例中为了简化,只包含一个表名。用@ColumnInt@ColumnStr分别代表整型的表字段和字符串形式的表字段,当然也可以用一个枚举类型来表示字段类型,这个在后边会说明。字段中包含一些数据库字段常见的定义,比如字段名称、是否主键、是否唯一性、是否可以为null等。而整数字段和字符串字段的区别在于后者需要指定宽度,而前者不需要。

下面来尝试创建相应的注解:

package ch20.orm;

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

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Table {
    String value() default "";
}

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface ColumnInt {
    String name() default "";

    boolean pk() default false;

    boolean isUnique() default false;

    boolean isNotNull() default true;
}

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface ColumnStr {
    String name() default "";

    boolean pk() default false;

    boolean isUnique() default false;

    boolean isNotNull() default true;

    int size();
}

实际上ColumnStrColumnInt有相当一部分注解元素是相同的,我们可以通过包含一个“共有注解”的方式进行简化:

package ch20.orm2;

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

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Table {
    String value() default "";
}

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface ColumnCommon {
    String name() default "";

    boolean pk() default false;

    boolean isUnique() default false;

    boolean isNotNull() default true;
}

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface ColumnInt {
    ColumnCommon common() default @ColumnCommon;
}

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface ColumnStr {
    ColumnCommon common() default @ColumnCommon;

    int size();
}

相应的,使用注解的源码也要做出修改:

package ch20.orm2;

@Table("student")
public class Student {
    @ColumnInt(common = @ColumnCommon(name = "id", pk = true, isUnique = true, isNotNull = true))
    private int id;
    @ColumnStr(size = 10, common = @ColumnCommon(name = "name", pk = false, isUnique = true, isNotNull = true))
    private String name;
}

现在我们看如何为这个注解类型编写处理器以生成SQL语句:

package ch20.orm2;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class SQLBuilder {
    public static void main(String[] args) {
        List<Class<?>> allCls = new ArrayList<>();
        allCls.addAll(Arrays.asList(Student.class));
        StringBuilder SQLsb = new StringBuilder();
        for (Class<?> cls : allCls) {
            Table table = cls.getAnnotation(Table.class);
            if (table != null) {
                String tableName = table.value();
                if (tableName.length() == 0) {
                    tableName = cls.getSimpleName().toLowerCase();
                }
                SQLsb.append("CREATE TABLE `test`.`");
                SQLsb.append(tableName);
                SQLsb.append("`(");
                String primaryKey = "";
                for (Field field : cls.getDeclaredFields()) {
                    Annotation[] annotations = field.getDeclaredAnnotations();
                    if (annotations.length != 0) {
                        for (Annotation annotation : annotations) {
                            if (annotation != null) {
                                if (annotation instanceof ColumnInt) {
                                    ColumnInt columnInt = (ColumnInt) annotation;
                                    SQLsb.append(columnIntSQL(columnInt));
                                    SQLsb.append(",");
                                    if (columnInt.common().pk()) {
                                        primaryKey = columnInt.common().name();
                                    }
                                } else if (annotation instanceof ColumnStr) {
                                    ColumnStr columnStr = (ColumnStr) annotation;
                                    SQLsb.append(columnStrSQL(columnStr));
                                    SQLsb.append(",");
                                    if (columnStr.common().pk()) {
                                        primaryKey = columnStr.common().name();
                                    }
                                } else {
                                    ;
                                }
                            }
                        }
                    }
                }
                SQLsb.append(" PRIMARY KEY (`");
                SQLsb.append(primaryKey);
                SQLsb.append("`) );");
            }

        }
        System.out.println(SQLsb.toString());
    }

    private static String columnStrSQL(ColumnStr columnStr) {
        StringBuilder sb = new StringBuilder();
        sb.append(" `");
        sb.append(columnStr.common().name());
        sb.append("` VARCHAR(");
        sb.append(columnStr.size());
        sb.append(")");
        if (columnStr.common().isNotNull()) {
            sb.append(" NOT NULL");
        }
        return sb.toString();
    }

    private static String columnIntSQL(ColumnInt columnInt) {
        StringBuilder sb = new StringBuilder();
        sb.append(" `");
        sb.append(columnInt.common().name());
        sb.append("` INT UNSIGNED");
        if (columnInt.common().isNotNull()) {
            sb.append(" NOT NULL");
        }
        return sb.toString();
    }
}
// CREATE TABLE `test`.`student`( `id` INT UNSIGNED NOT NULL, `name` VARCHAR(10) NOT NULL, PRIMARY KEY (`id`) );

我们可以将生成的SQL保存到文件,或者连接数据库后直接执行。

  • 这里有一个编写SQL的小技巧,可以使用SQLyog等数据库连接工具创建目标表结构,然后拷贝生成表结构时产生的SQL,然后按需要进行修改。
  • 不同的数据库,SQL也不尽相同,这里使用的是MySQL数据库。

需要提醒的是,这里仅是一个简单示例,这个示例作为一个ORM显然是不合格的,真正的ORM功能比这要丰富和完善的多,但是这个示例用于学习和说明注解在实现ORM时的作用已经足够了。

这里的设计存在一些问题,比如用不同的注解类型来对应不同类型的数据库字段,这样就要构造很多个注解类型。其实也可以用一个注解类型,并结合枚举来替代:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface Column {
    String name() default "";

    boolean pk() default false;

    boolean isUnique() default false;

    boolean isNotNull() default true;

    int size() default -1;

    ColumnType columnType();
}

当然,要对处理器和源码做出相应修改,完整代码见java-notebook(github.com)

使用apt处理注解

虽然利用反射在运行时处理注解可以实现一些强大的功能,但是它也存在一些难以克服的缺点:

  • 反射作为一种动态机制,不可避免地性能不佳。
  • 因为反射基于JVM运行时,所以无法在此时利用注解来生成并注入新的代码(那需要重新编译和运行)。

但这些缺点可以通过apt以及相关的API来解决。

apt全称Annotation Processing Tool(注解处理工具),其作用是帮助调用注解处理程序。

我们上面创建的注解处理器,必须要手动执行,通过使用apt就可以“自动化”地调用注解处理器并指向相应的处理工作。

我们先创建一个注解:

package ch20.extract_interface;

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

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface ExtractInterface {
    String value();
}

这个注解将用于标注类,之后我们会创建一个处理器,该处理器将提取这个类中的公有方法,并生成相应的接口文件。

创建一个测试类并进行标注:

package ch20.extract_interface;

@ExtractInterface("Actionable")
public class Student {
    public void action() {
        System.out.println("student start doing something.");
    }
}

下面创建注解处理程序。因为要使用apt进行调用而非自己运行,所以注解处理器也必须遵循apt规定的接口。

在Java SE 7之前,apt使用com.sun.mirror.apt包的相关接口,但在那之后,改为使用javax.annotation.processing以及 javax.lang.model等相关接口,具体新旧接口对照及相关说明可以阅读Getting Started with the Annotation Processing Tool, apt (oracle.com)

这里直接给出完整代码:

package ch20.extract_interface;

import java.io.IOException;
import java.io.Writer;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.tools.JavaFileObject;

public class ExtractInterfaceProcessor extends AbstractProcessor {
    private ProcessingEnvironment processingEnv;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        this.processingEnv = processingEnv;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        StringBuilder sb = new StringBuilder();
        boolean handled = false;
        for (Element classElement : roundEnv.getElementsAnnotatedWith(ExtractInterface.class)) {
            if (classElement.getKind() != ElementKind.CLASS) {
                throw new RuntimeException("Annotation @ExtractInterface only be used in class.");
            }
            ExtractInterface extractInterface = classElement.getAnnotation(ExtractInterface.class);
            String interfaceName = extractInterface.value();
            sb.append("package ch20.extract_interface;\n\n");
            sb.append("public interface ");
            sb.append(interfaceName);
            sb.append("{\n");
            for (Element element : classElement.getEnclosedElements()) {
                if (element.getKind() == ElementKind.METHOD) {
                    if (!element.getModifiers().contains(Modifier.PUBLIC)) {
                        continue;
                    }
                    ExecutableElement executableElement = (ExecutableElement) element;
                    sb.append(executableElementStr(executableElement));
                }
            }
            sb.append("}");
            try {
                JavaFileObject file = processingEnv.getFiler().createSourceFile(interfaceName);
                Writer writer = file.openWriter();
                writer.write(sb.toString());
                writer.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            handled = true;
        }
        return handled;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annocationTypes = new HashSet<>();
        annocationTypes.add(ExtractInterface.class.getCanonicalName());
        return annocationTypes;
    }

    private static String executableElementStr(ExecutableElement element) {
        StringBuilder sb = new StringBuilder();
        sb.append("    public ");
        sb.append(element.getReturnType());
        sb.append(" ");
        sb.append(element.getSimpleName());
        sb.append(" (");
        List<? extends VariableElement> parameters = element.getParameters();
        if (parameters.size() > 0) {
            for (VariableElement parameter : parameters) {
                sb.append(parameter.asType());
                sb.append(" ");
                sb.append(parameter.getSimpleName());
                sb.append(",");
            }
            sb.delete(sb.length() - 1, sb.length());
        }
        sb.append(");\n");
        return sb.toString();
    }
}

使用apt的处理器必须实现javax.annotation.processing.Processor接口,该接口有这几个方法比较重要:

  • void init(ProcessingEnvironment processingEnv);,对处理器初始化,这里传入的ProcessingEnvironment类型的参数可以用于获取处理器的相关工具,包括通过ProcessingEnvironment.getFiler获取一个文件处理工具,可以创建源码。
  • boolean process(Set<? extends TypeElement> annotations,RoundEnvironment roundEnv);,这个是最重要的方法,处理器的主要逻辑就放在这个方法中。可以通过RoundEnvironment类型的参数获取注解元素,返回值表示当前处理器是否已经完成处理,如果是true,就不会再分配给后续的处理器处理了。
  • Set<String> getSupportedAnnotationTypes();,这个方法表明当前处理器支持处理的注解类型有哪些,由一个字符串Set构成,需要注意的是是包含完整包名的注解类型名称。
  • SourceVersion getSupportedSourceVersion();,这个方法返回处理器支持的源码版本。

一般我们不需要自己实现Processor接口,因为标准库已经定义了一个AbstractProcessor类,我们直接继承即可。

apt具有一个特性,如果相关的处理器调用后生成了新的源代码,它就会加载源代码。而这里我们正要利用这一点生成接口代码并加载,因此覆盖了init方法,并保存了一个ProcessingEnvironment对象引用。

getSupportedAnnotationTypesgetSupportedSourceVersion方法同样覆盖,内容相对简单,这里不做赘述。

process方法用于主要的处理流程,因为apt是作用于将源码编译为字节码的期间,因此这里注解的@Retention也被设置为RetentionPolicy.SOURC。相应的,我们也无法使用反射来获取相关信息,只能通过apt配套的javax.lang.model.element包相关的类来获取。

具体是先通过RoundEnvironment.getElementsAnnotatedWith获取注解标注的元素。这里获取到的是Element对象,Element可以代表源码文件中的任何“节点”,这有点像是XML或HTML解析时的“DOM节点”概念。

我们可以通过Element.getKind方法获取节点的类型,并进行判断。

可以通过Element.getAnnotation从节点获取标注的注解。

通过Element.getEnclosedElements()获取节点的“子节点”,这里因为我们的父节点已经明确是类,因此子节点就是属性或方法。同样的,可以利用子节点的getKind方法判断其类型,这里我们只需要处理方法。

Element.getModifiers可以返回节点的相关修饰符组成的Set,可以使用Set.contains来判断具体是否包含需要的修饰符,这里用于判断方法是否为公有方法。

Element有很多子类型,表示不同类型的节点。因为这里已经确定是方法,所以可以将其向下转型为ExecutableElement,这样我们就可以通过ExecutableElement.getParameters获取方法参数的相关信息。

最后,在构造好接口的源码内容后,我们通过processingEnv.getFiler().createSourceFile(interfaceName)获取一个JavaFileObject对象,可以用它打开输出流写入源码文件,并且写入后apt工具会自动加载(如果语法没有错误的话)。

最后我们可以用命令行进行测试。

需要先编译处理器:

javac -cp D:\workspace\java\java-notebook\xyz\icexmoon\java_notes ExtractInterfaceProcessor.java

然后编译目标代码,并用-processor参数指定处理器:

javac -processor ch20.extract_interface.ExtractInterfaceProcessor -cp D:\workspace\java\java-notebook\xyz\icexmoon\java_notes Student.java

可以使用javac --help查看相关参数的解释。或者阅读Java知识点:javac命令 - xiazdong - 博客园 (cnblogs.com)

如果你查看当前目录,就会发现生成了Actionable.javaActionable.class文件,这说明我们的处理器正确创建了源码并被apt编译和加载。

当然,这里生成源码的方式相当“原始”,有很多现成的工具可以帮助你生成源码,这里不做赘述。

网上关于注解和apt的90%都是Android开发的内容,纯Java的几乎没有,为了跑通上面的示例我查了很多资料…

使用观察者模式

如果将所有的处理器逻辑都写在process方法中,代码会变得很难维护,尤其是处理逻辑变得相当复杂时。

因此apt的相关API提供一种观察者模式的应用方式,可以将“节点遍历”与“业务逻辑”进行解耦:

package ch20.extract_interface2;

import java.io.IOException;
import java.io.Writer;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.util.SimpleElementVisitor14;
import javax.tools.JavaFileObject;

public class ExtractInterfaceProcessor extends AbstractProcessor {
    private ProcessingEnvironment processingEnv;
    private boolean handled = false;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        this.processingEnv = processingEnv;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getRootElements()) {
            element.accept(new ExtractInterfaceVisitor(), null);
        }
        return handled;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annocationTypes = new HashSet<>();
        annocationTypes.add(ExtractInterface.class.getCanonicalName());
        return annocationTypes;
    }

    private static String executableElementStr(ExecutableElement element) {
        StringBuilder sb = new StringBuilder();
        sb.append("    public ");
        sb.append(element.getReturnType());
        sb.append(" ");
        sb.append(element.getSimpleName());
        sb.append(" (");
        List<? extends VariableElement> parameters = element.getParameters();
        if (parameters.size() > 0) {
            for (VariableElement parameter : parameters) {
                sb.append(parameter.asType());
                sb.append(" ");
                sb.append(parameter.getSimpleName());
                sb.append(",");
            }
            sb.delete(sb.length() - 1, sb.length());
        }
        sb.append(");\n");
        return sb.toString();
    }

    private class ExtractInterfaceVisitor extends SimpleElementVisitor14<Object, Object> {

        @Override
        public Object visitExecutable(ExecutableElement e, Object p) {
            return super.visitExecutable(e, p);
        }

        @Override
        public Object visitType(TypeElement classElement, Object p) {
            StringBuilder sb = new StringBuilder();
            ExtractInterface extractInterface = classElement.getAnnotation(ExtractInterface.class);
            if (extractInterface != null) {
                String interfaceName = extractInterface.value();
                sb.append("package ch20.extract_interface2;\n\n");
                sb.append("public interface ");
                sb.append(interfaceName);
                sb.append("{\n");
                for (Element element : classElement.getEnclosedElements()) {
                    if (element.getKind() == ElementKind.METHOD) {
                        if (!element.getModifiers().contains(Modifier.PUBLIC)) {
                            continue;
                        }
                        ExecutableElement executableElement = (ExecutableElement) element;
                        sb.append(executableElementStr(executableElement));
                    }
                }
                sb.append("}");
                try {
                    JavaFileObject file = processingEnv.getFiler().createSourceFile(interfaceName);
                    Writer writer = file.openWriter();
                    writer.write(sb.toString());
                    writer.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
                handled = true;
            }
            return null;
        }

    }
}

这里的关键在于可以通过Element.accept方法为Element添加一个ElementVisitor。具体的注解处理由ElementVisitor对象完成。ElementVisitor可以看做是观察者模式中的Observer,在apt执行处理器时,会调用Element上绑定的ElementVisitor进行处理。

更多观察者模式的介绍可以阅读设计模式 with Python2:观察者模式 - 魔芋红茶’s blog (icexmoon.cn)

我们不需要从头实现ElementVisitor接口,可以从SimpleElementVisitor14<R,P>类继承。和ElementVisitor接口一样,这是一个泛型类,需要指定两个泛型参数,其中R表示返回值类型,P表示传递给ElementVisitor的参数类型。一般来说我们是不需要传入参数和返回值的,可以将其设置为Object或者Void

SimpleElementVisitorXX有很多版本,比如SimpleElementVisitor13等,最后的数字表明新旧,目前最新的是SimpleElementVisitor14

ElementVisitor接口按照Element类型的不同,划分为不同的方法,按照你需要处理的类型覆盖不同的方法即可:

  • R visitExecutable(ExecutableElement e, P p);,处理可执行对象(包括方法等)。
  • R visitTypeParameter(TypeParameterElement e, P p);,处理泛型参数。
  • R visitType(TypeElement e, P p);处理类、接口等。
  • R visitVariable(VariableElement e, P p);,处理字段、方法参数等。

关于Element类型对应的语言结构,可以参考这张图:

img

图源:jianshu.com

更多Element相关内容可以阅读java-apt的实现之Element详解 - 简书 (jianshu.com)

最后的运行测试依然可以使用之前介绍的javac工具。

当然,正式的Java项目并不需要这么麻烦,在构建Jar包时使用的构建工具本身会支持配置注解处理器,然后每次构建都会调用相关的注解处理器进行项目构建工作,例如Android项目所使用的Gradle构建工具。

单元测试

可以利用注解实现一个单元测试框架,这类似于前面介绍的ORM,不过其目的在于帮助你实现单元测试。

事实上Java上已经有一个成熟的单元测试框架JUnit,关于它的使用和介绍可以阅读JUnit - 概述_w3cschool或者前往其官网JUnit 5

因为单元测试本身是一个相对独立的内容,且撰写本篇文章时已经在apt上花费了较长时间和精力,所以这里不再赘述。

关于注解的内容就到这里了,谢谢阅读。

参考资料

  PHP知识库 最新文章
Laravel 下实现 Google 2fa 验证
UUCTF WP
DASCTF10月 web
XAMPP任意命令执行提升权限漏洞(CVE-2020-
[GYCTF2020]Easyphp
iwebsec靶场 代码执行关卡通关笔记
多个线程同步执行,多个线程依次执行,多个
php 没事记录下常用方法 (TP5.1)
php之jwt
2021-09-18
上一篇文章      下一篇文章      查看所有文章
加:2022-04-18 17:20:14  更:2022-04-18 17:21:14 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/18 12:06:04-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码