Java编程笔记20:注解
图源: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),也可以认为是“注解类型属性”。
注解类型元素可以使用的类型有:
int
、float
等基础类型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;
}
@Repeatable
的value
内容为容器注解的Class
对象。
之所以要用这么复杂的方式实现重复注解,是因为向后兼容的考虑。为了让旧的注解相关代码能正常运行,不得不这么做。对于非重复注解,依然可以使用以前的AnnotatedElement.getAnnotation(Class)方法获取,如果是重复注解,可以通过新的 AnnotatedElement.getAnnotationsByType(Class)方法获取多个重复注解。
重复注解的相关示例代码摘抄自官方文档,详细内容见Repeating Annotations (The Java™ Tutorials > Learning the Java Language > Annotations) (oracle.com)。
注解处理器
单一的注解用途有限,往往需要搭配注解处理器才能发挥应有的作用。下面通过一系列例子来说明注解的用途以及如何编写注解处理器。
假设我们有一个项目,是关于学生管理系统的,其中有一部分代码负责构建班级和学生的关系,其中涉及两个主要用例:
- 将学生添加到班级中。
- 将学生移除班级。
如果我们需要通过注解来帮助实现”用例驱动开发“,可能会创建如下的注解:
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) {
...
}
...
}
该接口有一些注解相关的方法,而Method
、Class
等反射相关的类都实现了该接口。
我们需要判断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();
}
实际上ColumnStr
和ColumnInt
有相当一部分注解元素是相同的,我们可以通过包含一个“共有注解”的方式进行简化:
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
对象引用。
getSupportedAnnotationTypes
和getSupportedSourceVersion
方法同样覆盖,内容相对简单,这里不做赘述。
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.java
和Actionable.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
类型对应的语言结构,可以参考这张图:
图源:jianshu.com
更多Element
相关内容可以阅读java-apt的实现之Element详解 - 简书 (jianshu.com)。
最后的运行测试依然可以使用之前介绍的javac
工具。
当然,正式的Java项目并不需要这么麻烦,在构建Jar包时使用的构建工具本身会支持配置注解处理器,然后每次构建都会调用相关的注解处理器进行项目构建工作,例如Android项目所使用的Gradle构建工具。
单元测试
可以利用注解实现一个单元测试框架,这类似于前面介绍的ORM,不过其目的在于帮助你实现单元测试。
事实上Java上已经有一个成熟的单元测试框架JUnit,关于它的使用和介绍可以阅读JUnit - 概述_w3cschool或者前往其官网JUnit 5。
因为单元测试本身是一个相对独立的内容,且撰写本篇文章时已经在apt上花费了较长时间和精力,所以这里不再赘述。
关于注解的内容就到这里了,谢谢阅读。
参考资料
- Java @Deprecated注解 (biancheng.net)
- Predefined Annotation Types (The Java™ Tutorials > Learning the Java Language > Annotations) (oracle.com)
- Java 注解(Annotation) | 菜鸟教程 (runoob.com)
- java注解与APT技术_fengxingzhe001的博客-CSDN博客_apt 注解
- Processor (Java Platform SE 7 ) (oracle.com)
- AnnotationProcessor (Mirror API ) (oracle.com)
- [AbstractProcessor (Java Platform SE 7 ) (oracle.com)](https://docs.oracle.com/javase/7/docs/api/javax/annotation/processing/AbstractProcessor.html#process(java.util.Set, javax.annotation.processing.RoundEnvironment))
- 编译时注解(一)AbstractProcessor实战 - 云+社区 - 腾讯云 (tencent.com)
- Java进阶–编译时注解处理器(APT)详解 - 掘金 (juejin.cn)
- Getting Started with the Annotation Processing Tool, apt (oracle.com)
- Java知识点:javac命令 - xiazdong - 博客园 (cnblogs.com)