Java中的Pluggable Annotation Processing API

什么是Java Annotation

注解(Annotation)机制从Java 5开始被引入,它能够向源代码中添加句法(syntactic)元数据。类、方法、变量(variables)、参数(parameters)、包(packages)都可以被注解。与Javadoc标签不同,Java注解(Annotation)能够被反射,因为它们能够被嵌入在编译器生成的class文件中,并且可以被Java虚拟机保留使得在运行时也可获取。

Java定义了一系列内建的注解,分为两类:

①用于Java代码的注解

  • @Override
  • @Deprecated
  • @SuppressWarnings
  • @SafeVarargs
  • @FunctionalInterface

②用于其他注解的注解(也就是“元注解”(Meta Annotation))

  • @Retention
  • @Documented
  • @Target
  • @Inherited
  • @Repeatable

如何实现自定义注解

自定义注解的实现非常简单,以下是一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Same as: @Edible(value = true)
@Edible(true)
Item item = new Carrot();

public @interface Edible {
boolean value() default false;
}

@Author(first = "Oompah", last = "Loompah")
Book book = new Book();

public @interface Author {
String first();
String last();
}

上面的代码定义了2个注解,并使用了它们。当注解中使用的属性名为value时,对其赋值时可以不指定属性的名称而直接写上属性值接口;除了value以外的变量名都需要使用name=value的方式赋值。

另外,可以用@Retention和@Target元注解来分别制定自定义注解的保留策略和作用范围。示例如下:

1
2
3
4
5
@Retention(RetentionPolicy.RUNTIME) // Make this annotation accessible at runtime via reflection.
@Target({ElementType.METHOD}) // This annotation can only be applied to class methods.
public @interface Tweezable {

}

@Retention元注解的可以取以下值:

CLASS:
编译器将把注释记录在类文件中,但在运行时 VM 不需要保留注释。
RUNTIME:
编译器将把注释记录在类文件中,在运行时 VM 将保留注释,因此可以反射性地读取。
SOURCE:
编译器要丢弃的注释。

@Target可以取以下值:

TYPE:Class, interface or enum declaration.
FIELD:Field declaration.
METHOD:Method declaration.
PARAMETER:Parameter declaration.
CONSTRUCTOR:Constructor declaration.
LOCAL_VARIABLE:Local variable declaration.
ANNOTATION_TYPE:Annotation type declaration.
PACKAGE:Package declaration.

定义并使用了注解后,我们要取到注解中的值并处理它,这个自定义注解才有意义。

反射的方式使用注解

这是最简单最常见的使用自定义注解的方式,具体方法是把自定义注解的@Retention定义为RUNTIME,然后在代码中利用java反射机制读取注解的属性值。

下面是一个完整的例子:
①定义@Test和@TesterInfo这2个注解,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//Test.java
package com.mkyong.test.core;

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

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD) //can use in method only.
public @interface Test {

//should ignore this test?
public boolean enabled() default true;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//TesterInfo.java
package com.mkyong.test.core;

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

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE) //on class level
public @interface TesterInfo {

public enum Priority {
LOW, MEDIUM, HIGH
}

Priority priority() default Priority.MEDIUM;

String[] tags() default "";

String createdBy() default "Mkyong";

String lastModified() default "03/01/2014";

}

②创建一个简单的单元测试TestExample.java,使用上面定义的两个注解,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//TestExample.java
package com.mkyong.test;

import com.mkyong.test.core.Test;
import com.mkyong.test.core.TesterInfo;
import com.mkyong.test.core.TesterInfo.Priority;

@TesterInfo(
priority = Priority.HIGH,
createdBy = "mkyong.com",
tags = {"sales","test" }
)
public class TestExample {

@Test
void testA() {
if (true)
throw new RuntimeException("This test always failed");
}

@Test(enabled = false)
void testB() {
if (false)
throw new RuntimeException("This test always passed");
}

@Test(enabled = true)
void testC() {
if (10 > 1) {
// do nothing, this test always passed.
}
}

}

③使用Java反射API读取并处理第②步中使用的自定义注解,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
//RunTest.java
package com.mkyong.test;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;

import com.mkyong.test.core.Test;
import com.mkyong.test.core.TesterInfo;

public class RunTest {

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

System.out.println("Testing...");

int passed = 0, failed = 0, count = 0, ignore = 0;

Class<TestExample> obj = TestExample.class;

// Process @TesterInfo
if (obj.isAnnotationPresent(TesterInfo.class)) {

Annotation annotation = obj.getAnnotation(TesterInfo.class);
TesterInfo testerInfo = (TesterInfo) annotation;

System.out.printf("%nPriority :%s", testerInfo.priority());
System.out.printf("%nCreatedBy :%s", testerInfo.createdBy());
System.out.printf("%nTags :");

int tagLength = testerInfo.tags().length;
for (String tag : testerInfo.tags()) {
if (tagLength > 1) {
System.out.print(tag + ", ");
} else {
System.out.print(tag);
}
tagLength--;
}

System.out.printf("%nLastModified :%s%n%n", testerInfo.lastModified());

}

// Process @Test
for (Method method : obj.getDeclaredMethods()) {

// if method is annotated with @Test
if (method.isAnnotationPresent(Test.class)) {

Annotation annotation = method.getAnnotation(Test.class);
Test test = (Test) annotation;

// if enabled = true (default)
if (test.enabled()) {

try {
method.invoke(obj.newInstance());
System.out.printf("%s - Test '%s' - passed %n", ++count, method.getName());
passed++;
} catch (Throwable ex) {
System.out.printf("%s - Test '%s' - failed: %s %n", ++count, method.getName(), ex.getCause());
failed++;
}

} else {
System.out.printf("%s - Test '%s' - ignored%n", ++count, method.getName());
ignore++;
}

}

}
System.out.printf("%nResult : Total : %d, Passed: %d, Failed %d, Ignore %d%n", count, passed, failed, ignore);

}
}

运行上面的示例代码,结果如下:

Testing…

Priority :HIGH
CreatedBy :mkyong.com
Tags :sales, test
LastModified :03/01/2014

1 - Test ‘testA’ - failed: java.lang.RuntimeException: This test always failed
2 - Test ‘testC’ - passed
3 - Test ‘testB’ - ignored

Result : Total : 3, Passed: 1, Failed 1, Ignore 1

与编译器结合的方法来使用注解

反射的方法局限性较大。首先,它必须定义@Retention为RetentionPolicy.RUNTIME,只能在运行时通过反射来获取注解值,使得运行时代码效率降低。其次,如果想在编译阶段利用注解来进行一些检查,对用户的某些不合理代码给出错误报告,反射的使用方法就无能为力了。

要把注解与Java编译器结合使用,Java也给出了解决方案。Java 5在引入annotation的同时,也引入了Annotation Processing Tool (apt),这是一个命令行工具,能够提供构建时(build-time)、基于源代码(source-based)对程序结构的读取功能,能够通过运行注解处理器来生成新的中间文件进而影响编译进程。不过,apt在Java 8中被移除了。

作为替代方案,可以使用JSR 269 annotation processing facility,这个机制能够在编译阶段使用注解信息,该机制是从JDK 6开始被引入的。JSR 269的标题是Pluggable Annotation Processing API,定义了一套可插拔的接口用来开发构建时注解处理器,用插件机制来扩展Java编译器。

JSR 269分为两部分:
①模拟Java编程语言的API
②编写注解处理器的API

这2部分分别位于包javax.lang.model.*和javax.annotation.processing中。

JSR 269的功能通过javac命令行的新选项暴露出来:

选项 说明
-proc:{none,only} 控制是否执行注释处理和/或编译。
-processor [,,…] 要运行的注释处理程序的名称; 绕过默认的搜索进程
-processorpath <路径> 指定查找注释处理程序的位置

javac默认启用了注解处理。如果需要仅运行注解处理而不把源代码编译成class文件,可以使用-proc:only选项。

下面是一个简单的例子:

①定义一个名为HelloWorld的注解:

1
2
3
public @interface HelloWorld {

}

②在类Dummy中使用上面定义的注解:

1
2
3
4
@HelloWorld                                                                                                                                  
public class Dummy {

}

③继承AbstractProcessor实现自定义Processor,并使用@SupportedAnnotationTypes和@SupportedSourceVersion来标识该Processor要处理的注解和支持的java版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.util.Set;                                                                                                                        
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;

@SupportedAnnotationTypes("HelloWorld")
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public class HelloWorldProcessor extends AbstractProcessor{

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

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (!roundEnv.processingOver()) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Hello Worlds!");
}
return true;
}
}

④运行上面的例子:

1
javac HelloWorldProcessor.java
javac -processor HelloWorldProcessor *.java

得到的输出为:

Note: Hello Worlds!

JSR 269定义的API的详细信息,可以在Java Community Process网站查看。

文章目录
  1. 1. 什么是Java Annotation
  2. 2. 如何实现自定义注解
  3. 3. 反射的方式使用注解
  4. 4. 与编译器结合的方法来使用注解