Appearance
Java 脚本、编译与注解
1. Java 平台的脚本机制
1.1. 获取脚本引擎
脚本引擎是一个可以执行用某种特定语言编写的脚本的类库。当虚拟机启动时,它会发现可用的脚本引擎。为了枚举这些引擎,需要构造一个 ScriptEngineManager 并调用 getEngineFactories 方法。可以向每个引擎工厂询问它们所支持的引擎名、MIME 类型和文件扩展名。表 1.1 显示了这些内容的典型值。
| 引擎 | 名字 | MIME 类型 | 文件扩展 |
|---|---|---|---|
| Nashorn(包含在 JDK 中) | nashorn、Nashorn、js、JS、JavaScript、javascript、ECMAScript、ecmascript | application/javascript、application/ecmascript、text/javascript、text/ecmascript | js |
| Groovy | groovy | - | groovy |
| Renjin | Renjin | text/x-R | R、r、S、s |
通常,你知道所需要的引擎,因此可以直接通过名字、MIME 类型或文件扩展来请求它,例如:
Java
ScriptEngine engine = manager.getEngineByName("nashorn");Java 8 引入了 Nashorn,这是由 Oracle 开发的一个 JavaScript 解释器。可以通过在类路径中提供必要的 JAR 文件来添加对更多语言的支持。
1.2. 脚本计算与绑定
一旦拥有了引擎,就可以通过下面的调用来直接调用脚本:
Java
Object result = engine.eval(scriptString);如果脚本存储在文件中,那么需要先打开一个 Reader, 然后调用:
Java
Object result = engine.eval(reader);可以在同一个引擎上调用多个脚本。如果一个脚本定义了变量、函数或类,那么大多数引擎都会保留这些定义,以供将来使用。例如:
Java
engine.eval("n = 1728");
Object result = engine.eval("n + 1");1
2
2
将返回 1729。
Note
要想知道在多个线程中并发执行脚本是否安全,可以调用:
JavaObject param = factory.getParameter("THREADING");其返回的是下列值之一:
null:并发执行不安全;"MULTITHREADED":并发执行安全。一个线程的执行效果对另外的线程有可能是可视的;"THREAD-ISOLATED":除了"MULTITHREADED",还会为每个线程维护不同的变量绑定;"STATELESS":除了"THREAD-ISOLATED", 脚本还不会改变变量绑定;
我们经常希望能够向引擎中添加新的变量绑定。绑定由名字及其关联的 Java 对象构成。例如,考虑下面的语句:
Java
engine.put("k", 1728);
Object result = engine.eval("k + 1");1
2
2
脚本代码从 “引擎作用域” 中的绑定里读取 k 的定义。这一点非常重要,因为大多数脚本语言都可以访问 Java 对象,通常使用的是比 Java 语法更简单的语法。例如:
Java
engine.put("b", new JButton());
engine.eval("b.text = 'Ok'");1
2
2
反过来,也可以获取由脚本语句绑定的变量:
Java
engine.eval("n = 1728");
Object result = engine.get("n");1
2
2
除了引擎作用域之外,还有全局作用域。任何添加到 ScriptEngineManager 中的绑定对所有引擎都是可视的。
除了向引擎或全局作用域添加绑定之外,还可以将绑定收集到一个类型为 Bindings 的对象中,然后将其传递给 eval 方法:
Java
Bindings scope = engine.createBindings();
scope.put("b", new JButton());
engine.eval(scriptString, scope);1
2
3
2
3
如果绑定集不应该为了将来对 eval 方法的调用持久化,那么这么做就很有用。
Note:你可能希望除了引擎作用域和全局作用域之外还有其他的作用域。例如,Web 容器可能需要请求作用域或会话作用域。但是,这需要你自己去解决。你需要实现一个类,它实现了
ScriptContext接口,并管理着一个作用域集合。每个作用域都是由一个整数标识的,而且越小的数字应该越先被搜索。(标准类库提供了SimpleScriptContext类,但是它只能持有全局作用域和引擎作用域。)
1.3. 重定向输入和输出
可以通过调用脚本上下文的 setReader 和 setWriter 方法来重定向脚本的标准输入和输出。例如:
Java
var writer = new StringWriter();
engine.getContext().setWriter(new PrintWriter(writer, true));1
2
2
在上例中,任何用 JavaScript 的 print 和 println 函数产生的输出都会被发送到 writer。
setReader 和 setWriter 方法只会影响脚本引擎的标准输入和输出源。例如,如果执行下面的 JavaScript 代码:
JavaScript
println("Hello");
java.lang.System.out.println("World");1
2
2
则只有第一个输出会被重定向。
Note:Nashorn 引擎没有标准输入源的概念,因此调用
setReader没有任何效果。
1.4. 调用脚本的函数和方法
对于许多脚本引擎而言,我们都可以调用脚本语言的函数,而不必对实际的脚本代码进行计算。如果允许用户用他们所选择的脚本语言来实现服务,那么这种机制就很有用了。
提供这种功能的脚本引擎实现了 Invocable 接口。特别是,Nashorn 引擎就是实现了 Invocable 接口。
要调用一个函数,需要用函数名来调用 invokeFunction 方法,函数名后面是函数的参数:
Java
// Define greet function in JavaScript
engine.eval("function greet(how, whom) { return how + ', ' + whom + '!' }");
// Call the function with arguments "Hello", "World"
result = ((Invocable) engine).invokeFunction("greet", "Hello", "World");1
2
3
4
5
2
3
4
5
如果脚本语言是面向对象的,那就可以调用 invokeMethod:
Java
// Define Greeter class in JavaScript
engine.eval("function Greeter(how) { this.how = how }");
engine.eval("Greeter.prototype.welcome = function(whom) { return this.how + ', ' + whom + '!' }");
// Construct an instance
Object yo = engine.eval("new Greeter('Yo')");
// Call the welcome method on the instance
result = ((Invocable) engine).invokeMethod(yo, "welcome", "World");1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
Note:即使脚本引擎没有实现
Invocable接口,你也可能仍旧可以以一种独立于语言的方式来调用某个方法。ScriptEngineFactory类的getMethodCallSyntax方法可以产生一个字符串,你可以将其传递给eval方法。但是,所有的方法参数必须都与名字绑定,而invokeMethod方法是可以用任意值调用的。
我们可以更进一步,让脚本引擎去实现一个 Java 接口,然后就可以用 Java 方法调用的语法来调用脚本函数。
其细节依赖于脚本引擎,但是典型情况是我们需要为该接口中的每个方法都提供一个函数。例如,考虑下面的 Java 接口:
Java
public interface Greeter {
String welcome(String whom);
}1
2
3
2
3
如果在 Nashorn 中定义了具有相同名字的函数,那么可通过这个接口来调用它:
Java
// Define welcome function in JavaScript
engine.eval("function welcome(whom) { return 'Hello, ' + whom + '!' }");
// Get a Java object and call a Java method
Greeter g = ((Invocable) engine).getInterface(Greeter.class);
result = g.welcome("World");1
2
3
4
5
6
2
3
4
5
6
在面向对象的脚本语言中,可以通过相匹配的 Java 接口来访问一个脚本类。例如,下面的代码展示了如何使用 Java 的语法来调用 JavaScript 的 SimpleGreeter 类:
Java
Greeter g = ((Invocable) engine).getInterface(yo, Greeter.class);
result = g.welcome("World");1
2
2
总之,如果你希望从 Java 中调用脚本代码,同时又不想因这种脚本语言的语法而受到困扰,那么 Invocable 接口就很有用。
1.5. 编译脚本
某些脚本引擎出于对执行效率的考虑,可以将脚本代码编译为某种中间格式。这些引擎实现了 Compilable 接口。下面的示例展示了如何编译和计算包含在脚本文件中的代码:
Java
var reader = new FileReader("myscript.js");
CompiledScript script = null;
if (engine implements Compilable)
script = ((Compilable) engine).compile(reader);1
2
3
4
2
3
4
一旦该脚本被编译,就可以执行它。下面的代码将会在编译成功的情况下执行编译后的脚本,如果引擎不支持编译,则执行原始的脚本。
Java
if (script != null)
script.eval();
else
engine.eval(reader);1
2
3
4
2
3
4
当然,只有需要重复执行时,我们才希望编译脚本。
1.6. 示例:用脚本处理 GUI 事件
为了演示脚本 API,我们将开发一个样例程序,它允许用户指定使用他们所选择的脚本语言编写的事件处理器。
script/ScriptTest.java:
Java
package script;
import java.awt.*;
import java.beans.*;
import java.io.*;
import java.lang.reflect.*;
import java.util.*;
import javax.script.*;
import javax.swing.*;
/**
* @author Cay Horstmann
* @version 1.03 2018-05-01
*/
public class ScriptTest {
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
try {
var manager = new ScriptEngineManager();
String language;
if (args.length == 0) {
System.out.println("Available factories: ");
for (ScriptEngineFactory factory : manager.getEngineFactories())
System.out.println(factory.getEngineName());
language = "nashorn";
} else language = args[0];
final ScriptEngine engine = manager.getEngineByName(language);
if (engine == null) {
System.err.println("No engine for" + language);
System.exit(1);
}
final String frameClassName = args.length < 2 ? "buttons1.ButtonFrame" : args[1];
var frame = (JFrame) Class.forName(frameClassName).getConstructor().newInstance();
InputStream in = frame.getClass().getResourceAsStream("init." + language);
if (in != null) engine.eval(new InputStreamReader(in));
var components = new HashMap<String, Component>();
getComponentBindings(frame, components);
components.forEach((name, c) -> engine.put(name, c));
var events = new Properties();
in = frame.getClass().getResourceAsStream(language + ".properties");
events.load(in);
for (Object e : events.keySet()) {
String[] s = ((String) e).split("\\.");
addListener(s[0], s[1], (String) events.get(e), engine, components);
}
frame.setTitle("ScriptTest");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
} catch (ReflectiveOperationException | IOException | ScriptException | IntrospectionException ex) {
ex.printStackTrace();
}
});
}
/**
* Gathers all named components in a container.
*
* @param c the component
* @param namedComponents a map into which to enter the component names and components
*/
private static void getComponentBindings(Component c, Map<String, Component> namedComponents) {
String name = c.getName();
if (name != null) {
namedComponents.put(name, c);
}
if (c instanceof Container) {
for (Component child : ((Container) c).getComponents())
getComponentBindings(child, namedComponents);
}
}
/**
* Adds a listener to an object whose listener method executes a script.
*
* @param beanName the name of the bean to which the listener should be added
* @param eventName the name of the listener type, such as "action" or "change"
* @param scriptCode the script code to be executed
* @param engine the engine that executes the code
* @param bindings the bindings for the execution
* @throws IntrospectionException
*/
private static void addListener(String beanName, String eventName, final String scriptCode,
ScriptEngine engine, Map<String, Component> components)
throws ReflectiveOperationException, IntrospectionException {
Object bean = components.get(beanName);
EventSetDescriptor descriptor = getEventSetDescriptor(bean, eventName);
if (descriptor == null) return;
descriptor.getAddListenerMethod().invoke(
bean,
Proxy.newProxyInstance(
null,
new Class[]{descriptor.getListenerType()},
(proxy, method, args) -> {
engine.eval(scriptCode);
return null;
}
)
);
}
private static EventSetDescriptor getEventSetDescriptor(Object bean, String eventName)
throws IntrospectionException {
for (EventSetDescriptor descriptor : Introspector.getBeanInfo(bean.getClass()).getEventSetDescriptors())
if (descriptor.getName().equals(eventName)) return descriptor;
return null;
}
}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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
buttons1/ButtonFrame.java:
Java
package buttons1;
import javax.swing.*;
/**
* A frame with a button panel.
*
* @author Cay Horstmann
* @version 1.00 2007-11-02
*/
public class ButtonFrame extends JFrame {
private static final int DEFAULT_WIDTH = 300;
private static final int DEFAULT_HEIGHT = 200;
private JPanel panel;
private JButton yellowButton;
private JButton blueButton;
private JButton redButton;
public ButtonFrame() {
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
panel = new JPanel();
panel.setName("panel");
add(panel);
yellowButton = new JButton("Yellow");
yellowButton.setName("yellowButton");
blueButton = new JButton("Blue");
blueButton.setName("blueButton");
redButton = new JButton("Red");
redButton.setName("redButton");
panel.add(yellowButton);
panel.add(blueButton);
panel.add(redButton);
}
}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
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
事件处理器是在属性文件中定义的。每个属性定义都具有下面的形式:
ABNF
component-name "." event-name "=" script-code例如,如果选择使用 JavaScript,那就要在 js.properties 文件中提供事件处理器:
Properties
yellowButton.action=panel.background = java.awt.Color.YELLOW
blueButton.action=panel.background = java.awt.Color.BLUE
redButton.action=panel.background = java.awt.Color.RED1
2
3
2
3
该程序以加载在命令行中指定的语言所需的引擎开始,如果未指定语言,则使用 JavaScript。然后,我们处理 init.language 脚本(如果该文件存在的话)。这对 R 语言和 Scheme 语言而言很有用,因为这些语言需要某些麻烦的初始化工作,我们不希望在每个事件处理器的脚本中都包括这部分工作。
接下来,我们递归地遍历所有的子构件,并在构件映射表中添加(名字,对象)绑定,然后,将它们添加到引擎中。
然后,我们读入 language.properties 文件。对于每一个属性,都合成其事件处理器代理,使得脚本代码得以执行。每个事件处理器都会调用下面的方法:
Java
engine.eval(scriptCode);让我们详细看看 yellowButton。当下面一行被处理时:
Properties
yellowButton.action=panel.background = java.awt.Color.YELLOW我们找到了具有 yellowButton 名字的 JButton 构件,然后附着一个 ActionListener,它拥有 actionPerformed 方法,该方法将执行下面的脚本,如果该脚本是用 Nashorn 执行的:
JavaScript
panel.background = java.awt.Color.YELLOW引擎包含一个将名字 panel 与这个 JPanel 对象绑定在一起的绑定。当事件发生时,该面板的 setBackground 方法就会执行,并且其颜色也会改变。
只需要执行下面的命令,就可以运行这个带有 JavaScript 事件处理器的程序:
Bash
$ java ScriptTest对于 Groovy 处理器,需要使用:
Bash
$ java -classpath .:groovy/lib/\* ScriptTest groovy这里,groovy 是 Groovy 的安装目录。
对于 R 的 Renjin 实现,要在类路径中包含 Renjin Studio 的 JAR 文件以及 Renjin 脚本引擎。它们都可以在 https://www.renjin.org/downloads.html 处获得。
这个应用演示了如何在 Java GUI 编程中使用脚本机制。大家可以更进一步,用 XML 文件来描述 GUI。然后我们的程序就会变成解释器,去解释那些由 XML 文件定义可视化表示以及用脚本语言定义行为的 GUI。请注意这与动态 HTML 页面或动态服务器端脚本环境之间的相似性。
2. 编译器 API
2.1. 调用编译器
调用编译器非常简单,下面是一个示范调用:
Java
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
OutputStream outStream = ...;
OutputStream errStream = ...;
int result = compiler.run(null, outStream, errStream, "-sourcepath", "src", "Test.java");1
2
3
4
2
3
4
返回值为 0 表示编译成功。
编译器会向提供给它的流发送输出和错误消息。如果将这些参数设置为 null,编译器就会使用 System.out 和 System.err。run 方法的第一个参数是输入流,由于编译器不会接受任何控制台输入,因此总是应该让其保持为 null。(run 方法是从泛化的 Tool 接口继承而来的,它考虑到某些工具需要读取输入。)
如果在命令行调用 javac,那么 run 方法其余的参数就会作为变量传递给 javac。这些变量是一些选项或文件名。
2.2. 发起编译任务
可以通过使用 CompilationTask 对象来对编译过程进行更多的控制。如果要从字符串中提供源码,在内存中捕获类文件,或者处理错误和警告消息,这样做就会显得很有用。
可以从 JavaCompiler 的 getTask 方法获取 CompilationTask 对象:
Java
JavaCompiler.CompilationTask task = compiler.getTask(
errorWriter, // Uses System.err if null
fileManager, // Uses the standard file manager if null
diagnostics, // Uses System.err if null
options, // null if no options
classes, // For annotation processing; null if none
sources
);1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
最后三个参数是 Iterable 的实例。例如,选项序列可以像下面这样指定:
Java
Iterable<String> options = List.of("-d", "bin");sources 参数是 JavaFileObject 实例的 Iterable。如果想要编译磁盘文件,需要获取一个 StandardJavaFileManager 对象,并调用其 getJavaFileObjects 方法:
Java
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
Iterable<JavaFileObject> sources = fileManager.getJavaFileObjectsFromStrings(List.of("File1.java", "File2.java"));
JavaCompiler.CompilationTask task = compiler.getTask(null, null, null, options, null, sources);1
2
3
2
3
Note:
classes参数只用于注解处理。在这种情况下,还需要用一个Processor对象的列表来调用task.processors(annotationProcessors)。请参见源码级注解处理小节中有关注解处理的示例。
getTask 方法会返回任务对象,但是并不会启动编译过程。CompilationTask 类扩展了 Callable<Boolean>,我们可以将其对象传递给 ExecutorService 以并行执行,或者只是做出如下的同步调用:
Java
Boolean success = task.call();2.3. 捕获诊断消息
为了监听错误消息,需要安装一个 DiagnosticListener。这个监听器在编译器报告警告或错误消息时会收到一个 Diagnostic 对象。DiagnosticCollector 类实现了这个接口,它将收集所有的诊断信息,使得你可以在编译完成之后遍历这些信息。
Java
DiagnosticCollector<JavaFileObject> collector = new DiagnosticCollector<>();
compiler.getTask(null, fileManager, collector, null, null, sources).call();
for (Diagnostic<? extends JavaFileObject> d : collector.getDiagnostics()) {
System.out.println(d);
}1
2
3
4
5
2
3
4
5
Diagnostic 对象包含有关问题位置的信息(包括文件名、行号和列号)以及人类可阅读的描述。
还可以在标准的文件管理器上安装一个 DiagnosticListener 对象,这样就可以捕获到有关文件缺失的消息:
Java
StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null);2.4. 从内存中读取源文件
如果动态地生成了源代码,那么就可以从内存中获取它来进行编译,而无须在磁盘上保存文件。可以使用下面的类来持有代码:
Java
public class StringSource extends SimpleJavaFileObject {
private String code;
StringSource(String name, String code) {
super(URI.create("string:///" + name.replace(’.’, ’/’) + ".java"), Kind.SOURCE);
this.code = code;
}
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return code;
}
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
然后,生成类的代码,并提交给编译器一个 StringSource 对象的列表:
Java
List<StringSource> sources = List.of(new StringSource(className1, class1CodeString), ...);
task = compiler.getTask(null, fileManager, diagnostics, null, null, sources);1
2
2
2.5. 将字节码写出到内存中
如果动态地编译类,那么就无须将类文件写出到硬盘上。可以将它们存储在内存中,并立即加载它们。
首先,要有一个类来持有这些字节:
Java
public class ByteArrayClass extends SimpleJavaFileObject {
private ByteArrayOutputStream out;
ByteArrayClass(String name) {
super(URI.create("bytes:///" + name.replace(’.’, ’/’) + ".class"), Kind.CLASS);
}
public byte[] getCode() {
return out.toByteArray();
}
public OutputStream openOutputStream() throws IOException {
out = new ByteArrayOutputStream();
return out;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
接下来,需要将文件管理器配置为使用这些类作为输出:
Java
List<ByteArrayClass> classes = new ArrayList<>();
StandardJavaFileManager stdFileManager = compiler.getStandardFileManager(null, null, null);
JavaFileManager fileManager = new ForwardingJavaFileManager<JavaFileManager>(stdFileManager) {
public JavaFileObject getJavaFileForOutput(Location location, String className, Kind kind, FileObject sibling)
throws IOException {
if (kind == Kind.CLASS) {
ByteArrayClass outfile = new ByteArrayClass(className);
classes.add(outfile);
return outfile;
} else return super.getJavaFileForOutput(location, className, kind, sibling);
}
};1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
为了加载这些类,需要使用类加载器:
Java
public class ByteArrayClassLoader extends ClassLoader {
private Iterable<ByteArrayClass> classes;
public ByteArrayClassLoader(Iterable<ByteArrayClass> classes) {
this.classes = classes;
}
public Class<?> findClass(String name) throws ClassNotFoundException {
for (ByteArrayClass cl : classes) {
if (cl.getName().equals("/" + name.replace(’.’, ’/’) + ".class")) {
byte[] bytes = cl.getCode();
return defineClass(name, bytes, 0, bytes.length);
}
}
throw new ClassNotFoundException(name);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
编译完成后,用上面的类加载器调用 Class.forName 方法:
Java
ByteArrayClassLoader loader = new ByteArrayClassLoader(classes);
Class<?> cl = Class.forName(className, true, loader);1
2
2
2.6. 示例:动态 Java 代码生成
在用于动态 Web 页面的 JSP 技术中,可以在 HTML 中混杂 Java 代码,例如:
Java
<p>The current date and time is <b><%= new java.util.Date() %></b>.</p>JSP 引擎动态地将 Java 代码编译到 Servlet 中。在示例应用中,我们使用了一个更简单的示例,它可以动态生成 Swing 代码。其基本思想是使用 GUI 构建器在窗体中放置构件,并在一个外部文件中指定构件的行为。buttons2/ButtonFrame.java 展示了一个非常简单的窗体类实例,而 buttons2/action.properties 展示了按钮动作的代码。请注意,窗体类的构造器调用了抽象方法 addEventHandlers。我们的代码生成器将产生一个实现了 addEventHandlers 方法的子类,并且对 buttons2/action.properties 文件中的每一行都添加了动作监听器。
我们将这个子类置于名字为 x 的包中,因为我们不希望在程序的其他地方用到它。所生成的代码有如下形式:
ABNF
generated = "package x;"
"public class Frame extends" super-class-name "{"
"protected void addEventHandlers() {"
*event-handler
"}"
"}"
event-handler = component-name ".addActionListener(event -> {"
code-for-event-handler
"});"1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
compiler/CompilerTest.java 的程序中的 buildSource 方法构建了这些代码,并将它们放到了 StringSource 对象中。该对象会传递给 Java 编译器。
如前一节所述,我们使用了一个 ForwardingJavaFileManager 对象,它会为每一个编译过的类都构造一个 ByteArrayClass 对象,这些对象会捕获 x.Frame 类被编译时所生成的类文件。该方法将每个文件对象都添加到了一个列表中,然后将其返回,以使得我们稍后可以定位这些字节码。
编译完成后,我们使用前一节中描述的类加载器来加载存储在这个列表中的所有类。然后,我们构造并显示应用程序的窗体类。
Java
var loader = new ByteArrayClassLoader(classFileObjects);
var frame = (JFrame) loader.loadClass("x.Frame").getConstructor().newInstance();
frame.setVisible(true);1
2
3
2
3
当点击按钮时,背景色会按照常规方式进行修改。为了查看这些动作是动态编译的,可以更改 buttons2/action.properties 文件中一行,例如,修改成下面这样:
Properties
yellowButton=panel.setBackground(java.awt.Color.YELLOW); yellowButton.setEnabled(false);再次运行这个程序,现在,黄色按钮在点击之后就变得禁用了。再看看代码目录,你不会发现 x 包中的类的任何源文件和类文件。这个示例向你演示了如何通过内存中的源文件和类文件来使用动态编译。
compiler/CompilerTest.java:
Java
package compiler;
import java.awt.*;
import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.List;
import javax.swing.*;
import javax.tools.*;
import javax.tools.JavaFileObject.*;
/**
* @author Cay Horstmann
* @version 1.10 2018-05-01
*/
public class CompilerTest {
public static void main(final String[] args) throws IOException, ReflectiveOperationException {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
var classFileObjects = new ArrayList<ByteArrayClass>();
var diagnostics = new DiagnosticCollector<JavaFileObject>();
JavaFileManager fileManager1 = compiler.getStandardFileManager(diagnostics, null, null);
fileManager1 = new ForwardingJavaFileManager<JavaFileManager>(fileManager1) {
public JavaFileObject getJavaFileForOutput(Location location, String className, Kind kind, FileObject sibling)
throws IOException {
if (kind == Kind.CLASS) {
var fileObject = new ByteArrayClass(className);
classFileObjects.add(fileObject);
return fileObject;
} else return super.getJavaFileForOutput(location, className, kind, sibling);
}
};
String frameClassName = args.length == 0 ? "buttons2.ButtonFrame" : args[0];
// compiler.run(null, null, null, frameClassName.replace(".", "/") + ".java");
StandardJavaFileManager fileManager2 = compiler.getStandardFileManager(null, null, null);
var sources = new ArrayList<JavaFileObject>();
for (JavaFileObject o : fileManager2.getJavaFileObjectsFromStrings(List.of(frameClassName.replace(".", "/") + ".java")))
sources.add(o);
JavaFileObject source = buildSource(frameClassName);
JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager1, diagnostics, null, null, List.of(source));
Boolean result = task.call();
for (Diagnostic<? extends JavaFileObject> d : diagnostics.getDiagnostics())
System.out.println(d.getKind() + ": " + d.getMessage(null));
fileManager1.close();
if (!result) {
System.out.println("Compilation failed.");
System.exit(1);
}
var loader = new ByteArrayClassLoader(classFileObjects);
var frame = (JFrame) loader.loadClass("x.Frame").getConstructor().newInstance();
EventQueue.invokeLater(() -> {
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setTitle("CompilerTest");
frame.setVisible(true);
});
}
/*
* Builds the source for the subclass that implements the addEventHandlers method.
* @return a file object containing the source in a string builder
*/
static JavaFileObject buildSource(String superclassName) throws IOException, ClassNotFoundException {
var builder = new StringBuilder();
builder.append("package x;\n\n");
builder.append("public class Frame extends " + superclassName + " {\n");
builder.append("protected void addEventHandlers() {\n");
var props = new Properties();
props.load(Files.newInputStream(Paths.get(superclassName.replace(".", "/")).getParent().resolve("action.properties")));
for (Map.Entry<Object, Object> e : props.entrySet()) {
var beanName = (String) e.getKey();
var eventCode = (String) e.getValue();
builder.append(beanName + ".addActionListener(event -> {\n");
builder.append(eventCode);
builder.append("\n} );\n");
}
builder.append("} }\n");
return new StringSource("x.Frame", builder.toString());
}
}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
77
78
79
80
81
82
83
84
85
86
87
88
89
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
77
78
79
80
81
82
83
84
85
86
87
88
89
buttons2/ButtonFrame.java:
Java
package buttons2;
import javax.swing.*;
/**
* A frame with a button panel.
*
* @author Cay Horstmann
* @version 1.00 2007-11-02
*/
public abstract class ButtonFrame extends JFrame {
public static final int DEFAULT_WIDTH = 300;
public static final int DEFAULT_HEIGHT = 200;
protected JPanel panel;
protected JButton yellowButton;
protected JButton blueButton;
protected JButton redButton;
protected abstract void addEventHandlers();
public ButtonFrame() {
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
panel = new JPanel();
add(panel);
yellowButton = new JButton("Yellow");
blueButton = new JButton("Blue");
redButton = new JButton("Red");
panel.add(yellowButton);
panel.add(blueButton);
panel.add(redButton);
addEventHandlers();
}
}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
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
buttons2/action.properties:
Properties
yellowButton=panel.setBackground(java.awt.Color.YELLOW);
blueButton=panel.setBackground(java.awt.Color.BLUE);1
2
2
3. 使用注解
注解是那些插入到源代码中使用其他工具可以对其进行处理的标签。这些工具可以在源码层次上进行操作,或者可以处理编译器在其中放置了注解的类文件。
注解不会改变程序的编译方式。Java 编译器对于包含注解和不包含注解的代码会生成相同的虚拟机指令。
为了能够受益于注解,你需要选择一个处理工具,然后向你的处理工具可以理解的代码中插入注解,之后运用该处理工具处理代码。
注解的使用范围还是很广泛的,并且这种广泛性让人乍一看会觉得有些杂乱无章。下面是关于注解的一些可能的用法:
- 附属文件的自动生成,例如部署描述符或者 bean 信息类;
- 测试、日志、事务语义等代码的自动生成;
3.1. 注解简介
我们首先介绍基本概念,然后将这些概念运用到一个具体示例中:我们将某些方法标注为 AWT 构件的事件监听器,然后向你展示一个能够分析注解和连接监听器的注解处理器。然后,我们对其语法规则进行详细讨论。最后我们以两个注解处理的高级示例结束本章。其中一个可以处理源代码级别的注解。另外一个使用了 Apache 的字节码工程类库,可以向注解过的方法中添加额外的字节码。
下面是一个简单注解的示例:
Java
public class MyClass {
...
@Test public void checkRandomInsertions()
}1
2
3
4
2
3
4
注解 @Test 用于注解 checkRandomInsertions 方法。
在 Java 中,注解是当作一个修饰符(修诸如 public 和 static 之类的关键词)来使用的,它被置于被注解项之前,中间没有分号。每一个注解的名称前面都加上了 @ 符号,这有点类似于 Javadoc 的注释。然而,Javadoc 注释出现在 /**...*/ 定界符的内部,而注解是代码的一部分。
@Test 注解自身并不会做任何事情,它需要工具支持才会有用。例如,当测试一个类的时候,JUnit4 测试工具(可以从 http://junit.org 处获得)可能会调用所有标识为 @Test 的方法。另一个工具可能会删除一个类文件中的所有测试方法,以便在对这个类测试完毕后,不会将这些测试方法与程序装载在一起。
注解可以定义成包含元素的形式,例如:
Java
@Test(timeout="10000")这些元素可以被读取这些注解的工具去处理。其他形式的元素也是有可能的;我们将会在本章的随后部分进行讨论。
除了方法外,还可以注解类、成员以及局部变量,这些注解可以存在于任何可以放置一个像 public 或者 static 这样的修饰符的地方。另外,正如在注解语法小节中看到的,你还可以注解包、参数变量、类型参数和类型用法。
每个注解都必须通过一个注解接口进行定义。这些接口中的方法与注解中的元素相对应。例如,JUnit 的注解 Test 可以用下面这个接口进行定义:
Java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
long timeout() default 0L;
...
}1
2
3
4
5
6
2
3
4
5
6
@interface 声明创建了一个真正的 Java 接口。处理注解的工具将接收那些实现了这个注解接口的对象。这类工具可以调用 timeout 方法来获取某个特定 Test 注解的 timeout 元素。
注解 Target 和 Retention 是元注解。它们注解了 Test 注解,即将 Test 注解标识成一个只能运用到方法上的注解,并且当类文件载入到虚拟机的时候,它仍可以保留下来。我们将在元注解小节详细讨论这些元注解。
你现在已经清楚了程序的元数据和注解这两个概念。在接下来的小节中,我们将深入到一个注解处理的具体示例中继续探讨。
Note:对于注解引人入胜的用法,可以查看 JCommander 和 picocli。这些类库将注解用于命令行参数的处理。
3.2. 示例:注解事件处理器
在用户界面编程中,一件更令人讨厌的事情就是组装事件源上的监听器。很多监听器是下面这种形式的:
Java
myButton.addActionListener(() -> doSomething());在本节,我们设计了一个注解来免除这种苦差事。该注解接口是在 runtimeAnnotations/ActionListenerFor.java 中定义的,其使用方式如下:
Java
@ActionListenerFor(source="myButton") void doSomething() { ... }程序员不再需要去调用 addActionListener 了。相反地,每个方法直接用一个注解标记起来。
runtimeAnnotations/ActionListenerInstaller.java:
Java
package runtimeAnnotations;
import java.awt.event.*;
import java.lang.reflect.*;
/**
* @author Cay Horstmann
* @version 1.00 2004-08-17
*/
public class ActionListenerInstaller {
/**
* Processes all ActionListenerFor annotations in the given object.
*
* @param obj an object whose methods may have ActionListenerFor annotations
*/
public static void processAnnotations(Object obj) {
try {
Class<?> cl = obj.getClass();
for (Method m : cl.getDeclaredMethods()) {
ActionListenerFor a = m.getAnnotation(ActionListenerFor.class);
if (a != null) {
Field f = cl.getDeclaredField(a.source());
f.setAccessible(true);
addListener(f.get(obj), obj, m);
}
}
} catch (ReflectiveOperationException e) {
e.printStackTrace();
}
}
/**
* Adds an action listener that calls a given method.
*
* @param source the event source to which an action listener is added
* @param param the implicit parameter of the method that the listener calls
* @param m the method that the listener calls
*/
public static void addListener(Object source, final Object param, final Method m)
throws ReflectiveOperationException {
var handler = new InvocationHandler() {
public Object invoke(Object proxy, Method mm, Object[] args) throws Throwable {
return m.invoke(param);
}
};
Object listener = Proxy.newProxyInstance(null, new Class[]{java.awt.event.ActionListener.class}, handler);
Method adder = source.getClass().getMethod("addActionListener", ActionListener.class);
adder.invoke(source, listener);
}
}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
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
buttons3/ButtonFrame.java:
Java
package buttons3;
import java.awt.*;
import javax.swing.*;
import runtimeAnnotations.*;
/**
* A frame with a button panel.
*
* @author Cay Horstmann
* @version 1.00 2004-08-17
*/
public class ButtonFrame extends JFrame {
private static final int DEFAULT_WIDTH = 300;
private static final int DEFAULT_HEIGHT = 200;
private JPanel panel;
private JButton yellowButton;
private JButton blueButton;
private JButton redButton;
public ButtonFrame() {
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
panel = new JPanel();
add(panel);
yellowButton = new JButton("Yellow");
blueButton = new JButton("Blue");
redButton = new JButton("Red");
panel.add(yellowButton);
panel.add(blueButton);
panel.add(redButton);
ActionListenerInstaller.processAnnotations(this);
}
@ActionListenerFor(source = "yellowButton")
public void yellowBackground() {
panel.setBackground(Color.YELLOW);
}
@ActionListenerFor(source = "blueButton")
public void blueBackground() {
panel.setBackground(Color.BLUE);
}
@ActionListenerFor(source = "redButton")
public void redBackground() {
panel.setBackground(Color.RED);
}
}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
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
runtimeAnnotations/ActionListenerFor.java:
Java
package runtimeAnnotations;
import java.lang.annotation.*;
/**
* @author Cay Horstmann
* @version 1.00 2004-08-17
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ActionListenerFor {
String source();
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
当然,这些注解本身不会做任何事情,它们只是存在于源文件中。编译器将它们置于类文件中,并且虚拟机会将它们载入。我们现在需要的是一个分析注解以及安装行为监听器的机制。这也是类 ActionListenerInstaller 的职责所在。ButtonFrame 构造器将调用下面的方法:
Java
ActionListenerInstaller.processAnnotations(this);静态的 processAnnotations 方法可以枚举出某个对象接收到的所有方法。对于每一个方法,它先获取 ActionListenerFor 注解对象,然后再对它进行处理。
Java
Class<?> cl = obj.getClass();
for (Method m : cl.getDeclaredMethods()) {
ActionListenerFor a = m.getAnnotation(ActionListenerFor.class);
if (a != null) ...
}1
2
3
4
5
2
3
4
5
这里,我们使用了定义在 AnnotatedElement 接口中的 getAnnotation 方法。Method、Constructor、Field、Class 和 Package 这些类都实现了这个接口。
源成员域的名字是存储在注解对象中的。我们可以通过调用 source 方法对它进行检索,然后查找匹配的成员域。
Java
String fieldName = a.source();
Field f = cl.getDeclaredField(fieldName);1
2
2
这表明我们的注解有点局限。源元素必须是一个成员域的名字,而不能是局部变量。
代码的剩余部分相当具有技术性。对于每一个被注解的方法,我们构造了一个实现了 ActionListener 接口的代理对象,其 actionPerformed 方法将调用这个被注解过的方法。细节并不重要,关键要知道注解的功能是通过 processAnnotations 方法建立起来的。
图 3.1 展示了在本例中注解是如何被处理的。
在这个示例中,注解是在运行时进行处理的。另外也可以在源码级别上对它们进行处理,这样,源代码生成器将产生用于添加监听器的代码。注解也可以在字节码级别上进行处理,字节码编辑器可以将对 addActionListener 的调用注入框体构造器中。听起来似乎很复杂,不过可以利用一些类库相对直截了当地实现这项任务。
对于用户界面程序员来说,我们这个示例并不能看作是一个严格意义上的工具。因为,用于添加监听器的实用方法对于程序员来说和添加一条注解一样方便。(实际上,java.beans.EventHandler 类试图实现的就是这样。通过在这个类中提供一个可以添加事件处理器的方法,而不只是构建它,就可以很容易地对它进行改进。)

不过,这个示例展示了对一个程序进行注解以及对这些注解进行分析的机制。既然你已经领会了这个具体示例,那么,现在可能已经为后续小节详述注解语法做好了更充分的准备(这也是我们所希望的)。
4. 注解语法
4.1. 注解接口
注解是由注解接口来定义的:
ABNF
annotation-interface = modifiers "@interface" annotation-name "{"
*element-declaration
"}"
element-declaration = type element-name "()" ["default" value] ";"1
2
3
4
5
2
3
4
5
例如,下面这个注解具有两个元素:assignedTo 和 severity。
Java
public @interface BugReport {
String assignedTo() default "[none]";
int severity();
}1
2
3
4
2
3
4
所有的注解接口都隐式地扩展自 java.lang.annotation.Annotation 接口。这个接口是一个常规接口,不是一个注解接口。
你无法扩展注解接口。换句话说,所有的注解接口都直接扩展自 java.lang.annotation.Annotation。
你从来不用为注解接口提供实现类。
注解元素的类型为下列之一:
- 基本类型(
int、short、long、byte、char、double、float或boolean); String;Class(具有一个可选的类型参数,例如Class<? extends MyClass>);enum类型;- 注解类型;
- 由前面所述类型组成的数组(由数组组成的数组不是合法的元素类型);
下面是一些合法的元素声明的例子:
Java
public @interface BugReport {
enum Status { UNCONFIRMED, CONFIRMED, FIXED, NOTABUG };
boolean showStopper() default false;
String assignedTo() default "[none]";
Class<?> testCase() default Void.class;
Status status() default Status.UNCONFIRMED;
Reference ref() default @Reference(); // an annotation type
String[] reportedBy();
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
4.2. 注解
每个注解都具有下面这种格式:
ABNF
annotation = "@" annotation-name ["(" [element-assignments] ")"]
element-assignments = element-assignment *("," element-assignment)
element-assignment = element-name "=" value1
2
3
4
5
2
3
4
5
例如:
Java
@BugReport(assignedTo="Harry", severity=10)元素的顺序无关紧要。下面这个注解和前面那个一样。
Java
@BugReport(severity=10, assignedTo="Harry")如果某个元素的值并未指定,那么就使用声明的默认值。例如,考虑一下下面这个注解:
Java
@BugReport(severity=10)元素 assignedTo 的值是字符串 "[none]"。
Note:默认值并不是和注解存储在一起的;相反地,它们是动态计算而来的。例如,如果你将元素
assignedTo的默认值更改为"[]",然后重新编译BugReport接口,那么注解@BugReport(severity=10)将使用这个新的默认值,甚至在那些在默认值修改之前就已经编译过的类文件中也是如此。
有两个特殊的快捷方式可以用来简化注解。
如果没有指定元素,要么是因为注解中没有任何元素,要么是因为所有元素都使用默认值,那么你就不需要使用圆括号了。例如:
Java
@BugReport和下面这个注解是一样的:
Java
@BugReport(assignedTo="[none]", severity=0)这样的注解又称为标记注解。
另外一种快捷方式是单值注解。如果一个元素具有特殊的名字 value, 并且没有指定其他元素,那么你就可以忽略掉这个元素名以及等号。例如,既然我们已经在前面将 ActionListenerFor 注解接口定义为如下形式:
Java
public @interface ActionListenerFor {
String value();
}1
2
3
2
3
那么,我们可以将这个注解书写成如下形式:
Java
@ActionListenerFor("yellowButton")而不是
Java
@ActionListenerFor(value="yellowButton")一个项可以有多个注解:
Java
@Test
@BugReport(showStopper=true, reportedBy="Joe")
public void checkRandomInsertions()1
2
3
2
3
如果注解的作者将其声明为可重复的,那么你就可以多次重复使用同一个注解:
Java
@BugReport(showStopper=true, reportedBy="Joe")
@BugReport(reportedBy={"Harry", "Carl"})
public void checkRandomInsertions()1
2
3
2
3
Note:因为注解是由编译器计算而来的,因此,所有元素值必须是编译期常量。例如:
Java@BugReport(showStopper=true, assignedTo="Harry", testCase=MyTestCase.class, status=BugReport.Status.CONFIRMED, ...)
Warning:一个注解元素永远不能设置为
null,甚至不允许其默认值为null。这样在实际应用中会相当不方便。你必须使用其他的默认值,例如""或者Void.class。
如果元素值是一个数组,那么要将它的值用括号括起来,像下面这样:
Java
@BugReport(..., reportedBy={"Harry", "Carl"})如果该元素具有单值,那么可以忽略这些括号:
Java
@BugReport(..., reportedBy="Joe") // OK, same as {"Joe"}既然一个注解元素可以是另一个注解,那么就可以创建出任意复杂的注解。例如:
Java
@BugReport(ref=@Reference(id="3352627"), ...)Tip:在注解中引入循环依赖是一种错误。例如,因为
BugReport具有一个注解类型为Reference的元素,所以Reference就不能再拥有一个类型为BugReport的元素。
4.3. 声明注解
注解可以出现在许多地方,这些地方可以分为两类:声明注解(declarations)和类型用法注解(type uses)。
声明注解可以出现在下列声明处:
- 包;
- 类(包括
enum); - 接口(包括注解接口);
- 方法;
- 构造器;
- 实例域(包含
enum常量); - 局部变量;
- 参数变量;
- 类型参数;
对于类和接口,需要将注解放置在 class 和 interface 关键词的前面:
Java
@Entity public class User { ... }对于变量,需要将它们放置在类型的前面:
Java
@SuppressWarnings("unchecked") List<User> users = ...;
public User getUser(@Param("id") String userId)1
2
2
泛化类或方法中的类型参数可以像下面这样被注解:
Java
public class Cache<@Immutable V> { ... }包是在文件 package-info.java 中注解的,该文件只包含以注解先导的包语句。
Java
/**
* Package-level Javadoc
*/
@GPL(version="3")
package com.horstmann.corejava;
import org.gnu.GPL;1
2
3
4
5
6
2
3
4
5
6
Warning:对局部变量的注解只能在源码级别上进行处理。类文件并不描述局部变量。因此,所有的局部变量注解在编译完一个类的时候就会被遗弃掉。同样地,对包的注解不能在源码级别之外存在。
4.4. 类型用法注解
声明注解提供了正在被声明的项的相关信息。例如,在下面的声明中:
Java
public User getUser(@NonNull String userId)就断言 userId 参数不为空。
Note:
@NonNull注解是 Checker Framework 的一部分。通过使用这个框架,可以在程序中包含断言,例如某个参数不为空,或者某个String包含一个正则表达式。然后,静态分析工具将检查在给定的源代码段中这些断言是否有效。
现在,假设我们有一个类型为 List<String> 的参数,并且想要表示其中所有的字符串都不为 null。这就是类型用法注解大显身手之处,可以将该注解放置到类型参数之前:List<@NonNull String>。
类型用法注解可以出现在下面的位置:
- 与泛化类型参数一起使用:
List<@NonNull String>、Comparator.<@NonNull String> reverseOrder(); - 数组中的任何位置:
@NonNull String[][] words:words[i][j]不为null;String @NonNull [][] words:words不为null;String[] @NonNull [] words:words[i]不为null;
- 与超类和实现接口一起使用:
class Warning extends @Localized Message; - 与构造器调用一起使用:
new @Localized String(...); - 与强制转型和
instanceof检查一起使用:(@Localized String) text、if (text instanceof @Localized String); - 与异常规约一起使用:
public String read() throws @Localized IOException; - 与通配符和类型边界一起使用:
List<@Localized ? extends Message>、List<? extends @Localized Message>; - 与方法和构造器引用一起使用:
@Localized Message::getText;
有多种类型位置是不能被注解的:
Java
@NonNull String.class // ERROR: Cannot annotate class literal
import java.lang.@NonNull String; // ERROR: Cannot annotate import1
2
2
可以将注解放置到诸如 private 和 static 这样的其他修饰符的前面或后面。习惯(但不是必需)的做法,是将类型用法注解放置到其他修饰符的后面和将声明注解放置到其他修饰符的前面。例如:
Java
private @NonNull String text; // Annotates the type use
@Id private String userId; // Annotates the variable1
2
2
Note
注解的作者需要指定特定的注解可以出现在哪里。如果一个注解可以同时应用于变量和类型用法,并且它确实被应用到了某个变量声明上,那么该变量和类型用法就都被注解了。例如,请考虑:
Javapublic User getUser(@NonNull String userId)如果
@NonNull可以同时应用于参数和类型用法,那么userId参数就被注解了,而其参数类型是@NonNull String。
4.5. 注解 this
假设想要将参数注解为在方法中不会被修改:
Java
public class Point {
public boolean equals(@ReadOnly Object other) { ... }
}1
2
3
2
3
那么,处理这个注解的工具在看到下面的调用时:
Java
p.equals(q)就会推理出 q 没有被修改过。
但是 p 呢?
当该方法被调用时,this 变量是绑定到 p 的。但是 this 从来都没有被声明过,因此你无法注解它。
实际上,你可以用一种很少使用的语法变体来声明它,这样你就可以添加注解了:
Java
public class Point {
public boolean equals(@ReadOnly Point this, @ReadOnly Object other) { ... }
}1
2
3
2
3
第一个参数被称为接收器参数,它必须被命名为 this,而它的类型就是要构建的类。
Note:你只能为方法而不能为构造器提供接收器参数。从概念上讲,构造器中的
this引用在构造器没有执行完之前还不是给定类型的对象。所以,放置在构造器上的注解描述的是被构建的对象的属性。
传递给内部类构造器的是另一个不同的隐藏参数,即对其外围类对象的引用。你也可以让这个参数显式化:
Java
public class Sequence {
private int from;
private int to;
class Iterator implements java.util.Iterator<Integer> {
private int current;
public Iterator(@ReadOnly Sequence Sequence.this) {
this.current = Sequence.this.from;
}
...
}
...
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
这个参数的名字必须像引用它时那样,叫做 EnclosingClass.this, 其类型为外围类。
5. 标准注解
Java SE 在 java.lang、java.lang.annotation 以及 javax.annotation 包中定义了大量的注解接口。其中有四个是元注解,用于描述注解接口的行为。其他的是常规注解,可以用它们来注解你的源代码中的项。表 5.1 列出了这些注解。我们将在接下来的两个小节详细讨论它们。
| 注解接口 | 应用场合 | 目的 |
|---|---|---|
Deprecated | 全部 | 将项标记为过时的 |
SuppressWarnings | 除了包和注解之外的所有情况 | 阻止某个给定类型的警告信息 |
SafeVarargs | 方法和构造器 | 断言 varargs 参数可安全使用 |
Override | 方法 | 检查该方法是否覆盖了某一个超类方法 |
FunctionalInterface | 接口 | 将接口标记为只有一个抽象方法的函数式接口 |
PostConstruct PreDestroy | 方法 | 被标记的方法应该在构造之后或移除之前立即被调用 |
Resource | 类、接口、方法、域 | 在类或接口上:标记为在其他地方要用到的资源; 在方法或域上:为 “注入” 而标记; |
Resources | 类、接口 | 一个资源数组 |
Generated | 全部 | |
Target | 注解 | 指明可以应用这个注解的的那些项 |
Retention | 注解 | 指明这个注解可以保留多久 |
Documented | 注解 | 指明这个注解应该包含在注解项的文档中 |
Inherited | 注解 | 指明当这个注解应用于一个类的时候,能够自动被它的子类继承 |
Repeatable | 注解 | 指明这个注解可以在同一个项上应用多次 |
5.1. 用于编译的注解
@Deprecated 注解可以被添加到任何不再鼓励使用的项上。所以,当你使用一个已过时的项时,编译器将会发出警告。这个注解与 Javadoc 标签 @deprecated 具有同等功效。但是,该注解会一直持久化到运行时。
Note:jdeprscan 工具可以扫描 JAR 文件集中的过时元素,它是 JDK 的组成部分。
@SuppressWarnings 注解会告知编译器阻止特定类型的警告信息,例如:
Java
@SuppressWarnings("unchecked")@Override 这种注解只能应用到方法上。编译器会检查具有这种注解的方法是否真正覆盖了一个来自于超类的方法。例如,如果你声明:
Java
public MyClass {
@Override public boolean equals(MyClass other);
...
}1
2
3
4
2
3
4
那么编译器会报告一个错误。毕竟,这个方法没有覆盖类的 equals 方法。因为那个方法有一个类型为 Object 而不是 MyClass 的参数。
@Generated 注解的目的是供代码生成工具来使用。任何生成的源代码都可以被注解,从而与程序员提供的代码区分开。例如,代码编辑器可以隐藏生成的代码,或者代码生成器可以移除生成代码的旧版本。每个注解都必须包含一个表示代码生成器的唯一标识符,而日期字符串(ISO8601 格式)和注释字符串是可选的。例如:
Java
@Generated("com.horstmann.beanproperty", "2008-01-04T12:08:56.235-0700");5.2. 用于管理资源的注解
@PostConstruct 和 @PreDestroy 注解用于控制对象生命周期的环境中,例如 Web 容器和应用服务器。标记了这些注解的方法应该在对象被构建之后,或者在对象被移除之前,紧接着调用。
@Resource 注解用于资源注入。例如,考虑一下访问数据库的 Web 应用。当然,数据库访问信息不应该被硬编码到 Web 应用中。而是应该让 Web 容器提供某种用户接口,以便设置连接参数和数据库资源的 JNDI 名字。在这个 Web 应用中,可以像下面这样引用数据源:
Java
@Resource(name="jdbc/mydb")
private DataSource source;1
2
2
当包含这个域的对象被构造时,容器会 “注入” 一个对该数据源的引用。
5.3. 元注解
@Target 元注解可以应用于一个注解,以限制该注解可以应用到哪些项上。例如:
Java
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface BugReport1
2
2
表 5.2 显示了所有可能的取值情况,它们属于枚举类型 ElementType。可以指定任意数量的元素类型,用括号括起来。
| 元素类型 | 注解适用场合 |
|---|---|
ANNOTATION_TYPE | 注解类型声明 |
PACKAGE | 包 |
TYPE | 类(包括 enum)及接口(包括注解类型) |
METHOD | 方法 |
CONSTRUCTOR | 构造器 |
FIELD | 成员域(包括 enum 常量) |
PARAMETER | 方法或构造器参数 |
LOCAL_VARIABLE | 局部变量 |
TYPE_PARAMETER | 类型参数 |
TYPE_USE | 类型用法 |
一条没有 @Target 限制的注解可以应用于任何项上。编译器将检查你是否将一条注解只应用到了某个允许的项上。例如,如果将 @BugReport 应用于一个成员域上,则会导致一个编译器错误。
@Retention 元注解用于指定一条注解应该保留多长时间。只能将其指定为表 5.3 中的任意值,其默认值是 RetentionPolicy.CLASS。
| 保留规则 | 描述 |
|---|---|
SOURCE | 不包括在类文件中的注解 |
CLASS | 包括在类文件中的注解,但是虚拟机不需要将它们载入 |
RUNTIME | 包括在类文件中的注解,并由虚拟机载入。通过反射 API 可获得它们 |
在 runtimeAnnotations/ActionListenerFor.java 中,@ActionListenerFor 注解声明为具有 RetentionPolicy.RUNTIME,因为我们是使用反射机制进行注解处理的。在随后的两个小节里,你将会看到一些在源码级别和类文件级别上怎样对注解进行处理的示例。
@Documented 元注解为像 Javadoc 这样的归档工具提供了一些提示。应该像处理其他修饰符(例如 protected 或 static)一样来处理归档注解,以实现其归档目的。其他注解的使用并不会纳入归档的范畴。例如,假定我们将 @ActionListenerFor 作为一个归档注解来声明:
Java
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ActionListenerFor1
2
3
4
2
3
4
现在每一个被该注解标注过的方法的归档就会含有这条注解,如图 5.1 所示:

如果某个注解是暂时性的(例如 @BugReport),那么就不应该对它们的用法进行归档。
Note:将一个注解应用到它自身上是合法的。例如,
@Documented注解被它自身注解为@Documented因此,针对注解的 Javadoc 文档可以表明它们是否可被归档。
@Inherited 元注解只能应用于对类的注解。如果一个类具有继承注解,那么它的所有子类都自动具有同样的注解。这使得创建一个与 Serializable 这样的标记接口具有相同运行方式的注解变得很容易。
实际上,@Serializable 注解应该比没有任何方法的 Serializable 标记接口更适用。一个类之所以可以被序列化,是因为存在着对它的成员域进行读写的运行期支持,而不是因为任何面向对象的设计原则。注解比接口继承更擅长描述这一事实。当然,可序列化接口是在 JDK1.1 中产生的,远比注解出现得早。
假设定义了一个继承注解 @Persistent 来指明一个类的对象可以存储到数据库中,那么该持久类的子类就会自动被注解为是持久性的。
Java
@Inherited @interface Persistent { }
@Persistent class Employee { ... }
class Manager extends Employee { ... } // also @Persistent1
2
3
2
3
在持久化机制去查找存储在数据库中的对象时,它就会同时探测到 Employee 对象以及 Manager 对象。
对于 Java SE 8 来说,将同种类型的注解多次应用于某一项是合法的。为了向后兼容,可重复注解的实现者需要提供一个容器注解,它可以将这些重复注解存储到一个数组中。
下面是如何定义 @TestCase 注解以及它的容器的代码:
Java
@Repeatable(TestCases.class)
@interface TestCase {
String params();
String expected();
}
@interface TestCases {
TestCase[] value();
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
无论何时,只要用户提供了两个或更多个 @TestCase 注解,那么它们就会自动地被包装到一个 @TestCases 注解中。
Warning:在处理可重复注解时必须非常仔细。如果调用
getAnnotation来查找某个可重复注解,而该注解又确实重复了,那么就会得到null。这是因为重复注解被包装到了容器注解中。在这种情况下,应该调用getAnnotationsByType。这个调用会 “遍历” 容器,并给出一个重复注解的数组。如果只有一条注解,那么该数组的长度就为1。通过使用这个方法,你就不用操心如何处理容器注解了。
6. 源码级注解处理
注解的另一种用法是自动处理源代码以产生更多的源代码、配置文件、脚本或其他任何我们想要生成的东西。
6.1. 注解处理器
注解处理已经被集成到了 Java 编译器中。在编译过程中,你可以通过运行下面的命令来调用注解处理器。
ABNF
cmd = "javac" "-processor" processor-classes source-files
processor-classes = processor-class-name *("," processor-class-name)1
2
3
2
3
编译器会定位源文件中的注解。每个注解处理器会依次执行,并得到它表示感兴趣的注解。如果某个注解处理器创建了一个新的源文件,那么上述过程将重复执行。如果某次处理循环没有再产生任何新的源文件,那么就编译所有的源文件。
Note:注解处理器只能产生新的源文件,它无法修改已有的源文件。
注解处理器通常通过扩展 AbstractProcessor 类而实现 Processor 接口。你需要指定你的处理器支持的注解,我们的案例如下:
Java
@SupportedAnnotationTypes("com.horstmann.annotations.ToString")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class ToStringAnnotationProcessor extends AbstractProcessor {
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment currentRound) {
...
}
}1
2
3
4
5
6
7
2
3
4
5
6
7
处理器可以声明具体的注解类型或诸如 com.horstmann.* 这样的通配符(com.horstmann 包及其所有子包中的注解),甚至是 * 表示所有注解。
在每一轮中,process 方法都会被调用一次,调用时会传递给由这一轮在所有文件中发现的所有注解构成的集,以及包含了有关当前处理轮次的信息的 RoundEnvironment 引用。
6.2. 语言模型 API
应该使用语言模型 API 来分析源码级的注解。与用来呈现类和方法的虚拟机表示形式的反射 API 不同,语言模型 API 让我们可以根据 Java 语言的规则去分析 Java 程序。
编译器会产生一棵树,其节点是实现了 javax.lang.model.element.Element 接口及其 TypeElement、VariableElement、ExecutableElement 等子接口的类的实例。这些节点可以类比于编译时的 Class、Field/Parameter 和 Method/Constructor 反射类。
在此我们并不会详细讨论该 API,但我们要强调的是,你需要知道它是如何处理注解的:
RoundEnvironment通过调用下面的方法交给你一个由特定注解标注过的所有元素构成的集:JavaSet<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> a)在源码级别上等价于
AnnotateElement接口的是AnnotatedConstruct。使用下面的方法就可以获得属于给定注解类的单条注解或重复的注解:JavaA getAnnotation(Class<A> annotationType) A[] getAnnotationsByType(Class<A> annotationType)1
2TypeElement表示一个类或接口,而getEnclosedElements方法会产生一个由它的域和方法构成的列表;在
Element上调用getSimpleName或在TypeElement上调用getQualifiedName会产生一个Name对象,它可以用toString方法转换为一个字符串;
6.3. 使用注解来生成源码
作为示例,我们将使用注解来减少实现 toString 方法时枯燥的编程工作量。我们不能将这些方法放到原来的类中,因为注解处理器只能产生新的类,而不能修改已有的类。
因此,我们将所有方法添加到工具类 ToStrings 中:
Java
public class ToStrings {
public static String toString(Point obj) {
// Generated code
}
public static String toString(Rectangle obj) {
// Generated code
}
...
public static String toString(Object obj) {
return Objects.toString(obj);
}
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
我们不想使用反射,因此对访问器方法而不是域进行注解:
Java
@ToString
public class Rectangle {
...
@ToString(includeName=false) public Point getTopLeft() { return topLeft; }
@ToString public int getWidth() { return width; }
@ToString public int getHeight() { return height; }
}1
2
3
4
5
6
7
2
3
4
5
6
7
然后,注解处理器应该生成下面的源码:
Java
public static String toString(Rectangle obj) {
var result = new StringBuilder();
result.append("Rectangle");
result.append("[");
result.append(toString(obj.getTopLeft()));
result.append(",");
result.append("width=");
result.append(toString(obj.getWidth()));
result.append(",");
result.append("height=");
result.append(toString(obj.getHeight()));
result.append("]");
return result.toString();
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
下面的框架所描述的方法可以为具有给定的 TypeElement 的类产生 toString 方法:
Java
private void writeToStringMethod(PrintWriter out, TypeElement te) {
String className = te.getQualifiedName().toString();
// Print method header and declaration of string builder
ToString ann = te.getAnnotation(ToString.class);
if (ann.includeName())
// Print code to add class name
for (Element c : te.getEnclosedElements()) {
ann = c.getAnnotation(ToString.class);
if (ann != null) {
if (ann.includeName()) // Print code to add field name
// Print code to append toString(obj.methodName())
}
}
// Print code to return string
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
而下面给出的是注解处理器的 process 方法的框架。它会创建助手类的源文件,并为每个被注解标注的类编写类头和一个 toString 方法。
Java
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment currentRound) {
if (annotations.size() == 0) return true;
try {
JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile("com.horstmann.annotations.ToStrings");
try (var out = new PrintWriter(sourceFile.openWriter())) {
// Print code for package and class
for (Element e : currentRound.getElementsAnnotatedWith(ToString.class)) {
if (e instanceof TypeElement) {
TypeElement te = (TypeElement) e;
writeToStringMethod(out, te);
}
}
// Print code for toString(Object)
} catch (IOException ex) {
processingEnv.getMessager().printMessage(Kind.ERROR, ex.getMessage());
}
}
return true;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
注意,process 方法在后续轮次中是用空的注解列表调用的,然后,它会立即返回,因此它并不会多次创建源文件。
首先,编译注解处理器,然后编译并运行测试程序,就像下面这样:
Bash
$ javac sourceAnnotations/ToStringAnnotationProcessor.java
$ javac -processor sourceAnnotations.ToStringAnnotationProcessor rect/*.java
$ java rect.SourceLevelAnnotationDemo1
2
3
2
3
Note
要想查看轮次,可以用
-XprintRounds标记来运行javac命令:TextRound 1: input files: {rect.Point, rect.Rectangle, rect.SourceLevelAnnotationDemo} annotations: [sourceAnnotations.ToString] last round: false Round 2: input files: {sourceAnnotations.ToStrings} annotations: [] last round: false Round 3: input files: {} annotations: [] last round: true1
2
3
4
5
6
7
8
9
10
11
12
这个示例演示了工具可以如何获取源文件注解以产生其他文件。生成的文件并非一定要是源文件。注解处理器可以选择生成 XML 描述符、属性文件、Shell 脚本、HTML 文档等。
Note
有些人建议使用注解来完成一项更繁重的体力活。如果琐碎的获取器和设置器可以自动生成,那岂不是很好?例如,用下面的注解:
Java@Property private String title;来产生下面的方法:
Javapublic String getTitle() { return title; } public void setTitle(String title) { this = title; }1
2但是,这些方法需要被添加到同一个类中。这需要编辑源文件而不是产生另一个文件,而这超出了注解处理器的能力范围。我们可以为实现此目的而构建另一个工具,但是这种工具超出了注解的职责范围。注解被设计为对代码项的描述,而不是添加或修改代码的指令。
7. 字节码工程
7.1. 修改类文件
在本小节,我们使用 ASM 向已注解方法中添加日志信息。如果一个方法被这样注解过:
Java
@LogEntry(logger=loggerName)那么,在方法的开头部分,我们将添加下面这条语句的字节码:
Java
Logger.getLogger(loggerName).entering(className, methodName);例如,如果对 Item 类的 hashCode 方法做了如下注解:
Java
@LogEntry(logger="global") public int hashCode()那么,在任何时候调用该方法,都会报告一条与下面打印出来的消息相似的消息:
Text
May 17, 2016 10:57:59 AM Item hashCode
FINER: ENTRY1
2
2
为了实现这项任务,我们需要遵循下面几点:
加载类文件中的字节码;
定位所有的方法;
对于每个方法,检查它是不是有一个
LogEntry注解;如果有,在方法开头部分添加下面所列指令的字节码:
Javaldc loggerName invokestatic java/util/logging/Logger.getLogger:(Ljava/lang/String;)Ljava/util/logging/Logger; ldc className ldc methodName invokevirtual java/util/logging/Logger.entering:(Ljava/lang/String;Ljava/lang/String;)V1
2
3
4
5
6
7
插入这些字节码看起来相当棘手,不过 ASM 却使它变得相当简单。我们不会详细描述和分析插入字节码的过程。关键之处是 bytecodeAnnotations/EntryLogger.java 中的程序可以编辑一个类文件,并且在已经用 LogEntry 注解标注过的方法的开头部分插入日志调用。
例如,下面展示了应该怎样向 set/Item.java 文件添加记录日志指令,其中 asm 是安装 ASM 库的目录。
Bash
$ javac set/Item.java
$ javac -classpath .:asm/lib/\* bytecodeAnnotations/EntryLogger.java
$ java -classpath .:asm/lib/\* bytecodeAnnotations.EntryLogger set.Item1
2
3
2
3
在对 Item 类文件进行修改之前和之后分别试运行一下:
Bash
$ javap -c set.Item就可以看到在 hashCode、equals 以及 compareTo 方法的开头部分插入的那些指令。
Text
public int hashCode();
Code:
0: ldc #85; // String global
2: invokestatic #80;
// Method
// java/util/logging/Logger.getLogger:(Ljava/lang/String;)Ljava/util/logging/Logger;
5: ldc #86; //String Item
7: ldc #88; //String hashCode
9: invokevirtual #84;
// Method java/util/logging/Logger.entering:(Ljava/lang/String;Ljava/lang/String;)V
12: bipush 13
14: aload_0
15: getfield #2; // Field description:Ljava/lang/String;
18: invokevirtual #15; // Method java/lang/String.hashCode:()I
21: imul
22: bipush 17
24: aload_0
25: getfield #3; // Field partNumber:I
28: imul
29: iadd
30: ireturn1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
set/SetTest.java 中的 SetTest 程序会将 Item 对象插入到一个散列集中。当你用修改过的类文件来运行该程序时,会看到下面的日志记录信息:
Text
May 17, 2016 10:57:59 AM Item hashCode
FINER: ENTRY
May 17, 2016 10:57:59 AM Item hashCode
FINER: ENTRY
May 17, 2016 10:57:59 AM Item hashCode
FINER: ENTRY
May 17, 2016 10:57:59 AM Item equals
FINER: ENTRY
[[description=Toaster, partNumber=1729], [description=Microwave, partNumber=4104]]1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
当将同一项插入两次时,请注意一下对 equals 的调用。
这个示例显示了字节码工程的强大之处:注解可以用来向程序中添加一些指示,而字节码编辑工具则可以提取这些指示,然后修改虚拟机指令。
bytecodeAnnotations/EntryLogger.java:
Java
package bytecodeAnnotations;
import java.io.*;
import java.nio.file.*;
import org.objectweb.asm.*;
import org.objectweb.asm.commons.*;
/**
* Adds "entering" logs to all methods of a class that have the LogEntry annotation.
*
* @author Cay Horstmann
* @version 1.21 2018-05-01
*/
public class EntryLogger extends ClassVisitor {
private String className;
/**
* Constructs an EntryLogger that inserts logging into annotated methods of a given class.
*
* @param cg the class
*/
public EntryLogger(ClassWriter writer, String className) {
super(Opcodes.ASM5, writer);
this.className = className;
}
public MethodVisitor visitMethod(int access, String methodName, String desc,
String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, methodName, desc, signature, exceptions);
return new AdviceAdapter(Opcodes.ASM5, mv, access, methodName, desc) {
private String loggerName;
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
return new AnnotationVisitor(Opcodes.ASM5) {
public void visit(String name, Object value) {
if (desc.equals("LbytecodeAnnotations/LogEntry;") && name.equals("logger"))
loggerName = value.toString();
}
};
}
public void onMethodEnter() {
if (loggerName != null) {
visitLdcInsn(loggerName);
visitMethodInsn(
INVOKESTATIC,
"java/util/logging/Logger",
"getLogger",
"(Ljava/lang/String;)Ljava/util/logging/Logger;",
false
);
visitLdcInsn(className);
visitLdcInsn(methodName);
visitMethodInsn(
INVOKEVIRTUAL,
"java/util/logging/Logger",
"entering",
"(Ljava/lang/String;Ljava/lang/String;)V",
false
);
loggerName = null;
}
}
};
}
/**
* Adds entry logging code to the given class.
*
* @param args the name of the class file to patch
*/
public static void main(String[] args) throws IOException {
if (args.length == 0) {
System.out.println("USAGE: java bytecodeAnnotations.EntryLogger classfile");
System.exit(1);
}
Path path = Paths.get(args[0]);
var reader = new ClassReader(Files.newInputStream(path));
var writer = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
var entryLogger = new EntryLogger(writer, path.toString().replace(".class", "").replaceAll("[/\\\\]", "."));
reader.accept(entryLogger, ClassReader.EXPAND_FRAMES);
Files.write(Paths.get(args[0]), writer.toByteArray());
}
}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
77
78
79
80
81
82
83
84
85
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
77
78
79
80
81
82
83
84
85
set/Item.java:
Java
package set;
import java.util.*;
import bytecodeAnnotations.*;
/**
* An item with a description and a part number.
*
* @author Cay Horstmann
* @version 1.01 2012-01-26
*/
public class Item {
private String description;
private int partNumber;
/**
* Constructs an item.
*
* @param aDescription the item’s description
* @param aPartNumber the item’s part number
*/
public Item(String aDescription, int aPartNumber) {
description = aDescription;
partNumber = aPartNumber;
}
/**
* Gets the description of this item.
*
* @return the description
*/
public String getDescription() {
return description;
}
public String toString() {
return "[description=" + description + ", partNumber=" + partNumber + "]";
}
@LogEntry(logger = "com.horstmann")
public boolean equals(Object otherObject) {
if (this == otherObject) return true;
if (otherObject == null) return false;
if (getClass() != otherObject.getClass()) return false;
var other = (Item) otherObject;
return Objects.equals(description, other.description) && partNumber == other.partNumber;
}
@LogEntry(logger = "com.horstmann")
public int hashCode() {
return Objects.hash(description, partNumber);
}
}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
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
set/SetTest.java:
Java
package set;
import java.util.*;
import java.util.logging.*;
/**
* @author Cay Horstmann
* @version 1.03 2018-05-01
*/
public class SetTest {
public static void main(String[] args) {
Logger.getLogger("com.horstmann").setLevel(Level.FINEST);
var handler = new ConsoleHandler();
handler.setLevel(Level.FINEST);
Logger.getLogger("com.horstmann").addHandler(handler);
var parts = new HashSet<Item>();
parts.add(new Item("Toaster", 1279));
parts.add(new Item("Microwave", 4104));
parts.add(new Item("Toaster", 1279));
System.out.println(parts);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
7.2. 在加载时修改字节码
在前一节中,已经看到了一个用于编辑类文件的工具。但是,在把另一个工具添加到程序的构建过程中时,会显得笨重不堪。更吸引人的做法是将字节码工程延迟到载入时,即类加载器加载类的时候。
设备(instrumentation)API 提供了一个安装字节码转换器的挂钩。不过,必须在程序的 main 方法调用之前安装这个转换器。通过定义一个代理,即被加载用来按照某种方式监视程序的一个类库,就可以处理这个需求。代理代码可以在 premain 方法中执行初始化。
下面是构建一个代理所需的步骤:
实现一个具有下面这个方法的类:
Javapublic static void premain(String arg, Instrumentation instr)当加载代理时,此方法会被调用。代理可以获取一个单一的命令行参数,该参数是通过
arg参数传递进来的。instr参数可以用来安装各种各样的挂钩。制作一个清单文件
EntryLoggingAgent.mf来设置Premain-Class属性。例如:PropertiesPremain-Class: bytecodeAnnotations.EntryLoggingAgent1
2将代理代码打包,并生成一个 JAR 文件,例如:
Bash$ javac -classpath .:asm/lib/\* bytecodeAnnotations/EntryLoggingAgent.java $ jar cvfm EntryLoggingAgent.jar bytecodeAnnotations/EntryLoggingAgent.mf \ bytecodeAnnotations/Entry*.class1
2
3
为了运行一个具有该代理的 Java 程序,需要使用下面这个命令行选项:
ABNF
"java" "-javaagent:" agent-jar-file "=" agent-arguments例如,运行具有日志代理的 SetTest 程序需调用:
Bash
$ javac set/SetTest.java
$ java -javaagent:EntryLoggingAgent.jar=set.Item -classpath .:asm/lib/\* set.SetTest1
2
2
Item 参数是代理应该修改的类的名称。
bytecodeAnnotations/EntryLoggingAgent.java 展示了这个代理的代码。该代理安装了一个类文件转换器,这个转换器首先检验类名是否与代理参数相匹配。如果匹配,那么它会利用上一节那个 EntryLogger 类修改字节码。不过,修改过的字节码并不保存成文件。相反地,转换器只是将它们返回,以加载到虚拟机中(参见图 7.1)。换句话说,这项技术实现的是 “即时” (just in time) 字节码修改。
bytecodeAnnotations/EntryLoggingAgent.java:
Java
package bytecodeAnnotations;
import java.lang.instrument.*;
import org.objectweb.asm.*;
/**
* @author Cay Horstmann
* @version 1.11 2018-05-01
*/
public class EntryLoggingAgent {
public static void premain(final String arg, Instrumentation instr) {
instr.addTransformer((loader, className, cl, pd, data) -> {
if (!className.replace("/", ".").equals(arg)) return null;
var reader = new ClassReader(data);
var writer = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
var el = new EntryLogger(writer, className);
reader.accept(el, ClassReader.EXPAND_FRAMES);
return writer.toByteArray();
});
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

在本章,你已经学习到了以下的知识:
- 怎样向 Java 程序中添加注解;
- 怎样设计你自己的注解接口;
- 怎样实现可以利用注解的工具;
你已经看到了三种处理代码的技术:编写脚本、编译 Java 程序和处理注解。前两种技术十分简单。而另一方面,构建注解工具可能会很复杂,但这并非是大多数开发者都需要解决的问题。本章向你介绍了一些背景知识,有助于你去理解可能会碰到的注解工具内部工作机制,但这些背景知识可能会挫伤你自行开发工具的积极性。