读后笔记 -- Java核心技术(第11版 卷 II) Chapter8 脚本、编译和注解处理
本章将介绍三种用于处理代码的技术:
- 脚本 API:可调用脚本语言,如 JavaScript、Groovy、Ruby等;
- 编译器 API:应用程序内部编译 Java 代码;
- 注解处理器:可在包含注解的 Java 源代码和类文件上进行操作;
8.1 Java 平台的脚本机制
8.1.1 获取脚本引擎
// 构造一个 ScriptEngineManager 对象; var manager = new ScriptEngineManager(); // 通过引擎名称获取 engine ScriptEngine engine = manager.getEngineByName("javascript");
8.1.2-8.1.3 脚本的处理
1)脚本与调用
// 直接调用 String scriptString = "var date = new Date();" + "date.getHours();"; Double hour = (Double)engine.eval(scriptString); // 脚本中定义变量 engine.eval("n = 1728"); Object result = engine.eval("n + 1"); // 获取脚本变量 result = engine.get("n");
2)重定向输入和输出
// 重定向输出,任何用 javascript 的 print 和 println 的输出都会被发送到 writer var writer = new StringWriter(); engine.getContext().setWriter(new PrintWriter(writer, true));
注意:
- (1)setWriter() 只会影响标准输入、输出源
- (2)nashorn 没有标准输入源的概念,所以调用 setReader 没有任何效果
println("hello"); // 重定向 java.lang.System.out.println("world"); // 不会重定向
8.1.4 调用脚本的函数和方法
1)调用函数,通过 invokeFunction()
// 定义函数 greet engine.eval("function greet(how, whom) { return how + ', ' + whom + '!'}"); // 用函数名来调用 invokeFunction() result = ((Invocable) engine).invokeFunction("greet", "Hello", "World"); System.out.println(result);
2)如果脚本语言是 面向对象,可通过调用 invokeMethod
engine.eval("function Greeter(how) { this.how = how}"); engine.eval("Greeter.prototype.welcome = function(whom) {return this.how + ', ' + whom +'!'}"); Object obj = engine.eval("new Greeter('Yo')"); result = ((Invocable) engine).invokeMethod(obj, "welcome", "World");
3)通过接口实现
// 定义接口,在 Greeter.java 文件中 public interface Greeter { String welcome(String whom); } // 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"); System.out.println(result);
8.1.5 编译及执行脚本
// 编译脚本,通过 Compiable 接口 var reader = new FileReader("myscript.js"); CompileScript script = null; if (engine implements Compilable) script = ((Compilable) engine).compile(reader); // 执行 if (script != null) script.eval(); else engine.eval(reader);
8.2 编译器 API
8.2.1 调用编译器
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
OutputStream outStream = ...; OutputStream errStream = ...; int result = compiler.run(null, outStream, errStream, "-sourcepath", "src", "Test.java"); // result = 0 表示编译成功
8.2.2 发起编译任务
JavaCompiler.CompilationTask task = compiler.getTask( null, // Uses System.err if null fileManager, // Uses the standard file manager if null diagnostics, // use the compiler's default method if null null, // null if no options null, // For annotation processing; no class names if null source);
Boolean result = task.call(); // 调用
8.2.3 捕获诊断消息
var collector = new DiagnosticCollector<JavaFileObject>(); JavaFileManager fileManager = compiler.getStandardFileManager(collector, null, null); // 在标准的文件管理器上安装一个 DiagnosticListener 对象,可以捕获有关文件缺失的消息
compiler.getTask(null, fileManager, collector, null, null, sources).call(); // 编译并运行,上面两个参数变量是为了可以捕获诊断信息 for (Diagnostic<? extends JavaFileObject> d : collector.getDiagnostics()) { System.out.println(d); }
8.2.4 从内存中读取源文件(内存中动态读取源代码)
// 1)从内存中读取动态生成的源代码 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; } } // 2)生成类的代码,并提交给编译器一个 StringSource 对象的列表 List<StringSource> sources = List.of(new StringSource(className1, class1CodeString), ...); task = compiler.getTask(null, fileManager, diagnostics, null, null, sources));
Note: 这要处理,就无需将源码等再保存到磁盘后再处理。
8.2.5 将字节码写出到内存(接 8.2.4,内存中动态编译类并加载)
1)有一个类持有这些字节:
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() { out = new ByteArrayOutputStream(); return out; } }
2)将文件管理器配置为使用这些类作为输出:
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 { // 这里的 Kind 需要 import javax.tools.JavaFileObject.Kind if (kind == Kind.CLASS) { ByteArrayClass outfile = new ByteArrayClass(className); classes.add(outfile); return outfile; } else return super.getJavaFileForOutput(location, className, kind, sibling); } };
3)使用类加载器:
public class ByteArrayClassLoader extends ClassLoader { private Iterable<ByteArrayClass> classes; public ByteArrayClassLoader(Iterable<ByteArrayClass> classes) { this.classes = classes; } @Override 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); } }
4)编译完成后,用上面的类加载器调用 Class.forName()
ByteArrayClassLoader loader = new ByteArrayClassLoader(classes); Class<?> cl = Class.forName(className, true, loader);
8.3 注解
注解:那些插入到源代码中使用其他工具可以对其进行处理的标签。这些工具可以在源码层次上进行操作,或可以处理编译器在其中放置了注解的类文件。
注解的一些可能的用法:
- 附属文件的自动生成,例如部署描述符或 bean 信息类;
- 测试、日志、事务语义等代码的自动生成;
8.3.1 注解简介
简单注解用例
public class MyClass { ... @Test public void checkRandomInsertions() // @Test 注解 checRandomInsertions() 方法 }
注解也可以包含元素
@Test(timeout="10000")
每个注解必须通过一个注解接口进行定义。
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Test { // Junit 的 Test 可以用下面的接口进行定义 long timeout() default 0L; ... }
8.4 注解语法
8.4.1 注解接口
public @interface BugReport { // 注解是由注解接口来实现的 String assignedTo() default "[none]"; // 元素形式一:带默认值 int servity(); // 元素形式二:不带默认值 }
注解元素的类型为下列之一:
- 基本类型(int, short, long, byte, char, double, float, boolean);
- String;
- Class(具有一个可选的类型参数),如 Class <? extends MyClass>;
- enum 类型;
- 注解类型;
- 由前面所述类型组成的数组;
// 综合的案例 public @interface BugReportFull { 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();
int servity(); }
8.4.2 注解
@BugReport(servity=10, assignedTo="Harry") // 注意:元素的顺序无关紧要,即使调换效果也一样
简化注解的两种快捷方式:
- 1)标记注解(没有指定元素):
// 没有指定元素有两种情况: // 1) 注解中没有任何元素; // 2) 所有元素都使用默认值; @BugReport // 此时为标记注解,使用的是默认值,等同于 @BugReport(assignedTo="[none]", servity=0)
- 2)单值注解(仅一个元素,且元素为 value):即可省略 "value="
// 注解接口: public @interface ActionListenerFor { String value(); } // 注解: @ActionListenerFor("yellowButton") // 此时不需要写成 @ActionListenerFor(value="yellowButton")
几个需要注意的点:
- 1)注解是由编译器计算而来的,因此,所有元素值必须是编译期常量;
- 2)一个元素不能设置为 "null";
- 3)数组元素值,要用 {} 括起来,如 @BugReportFull(..., reportedBy={"Harry", "Carl"})
- 4)如果数组元素是单值,则可以省略 {},如 @BugReportFull(..., reportedBy="Joe") // it is same as @BugReportFull(..., reportedBy={"Joe"})
- 5)因为一个注解元素可以是另一个注解,那么可以创建复杂的注解,如 @BugReportFull(ref=@Reference(id="352672"), ...)
8.4.3 注解各类声明
声明注解 和类型用法注解 可以出现在以下地方:
- 包;
- 类(包括 enum);
- 接口(包括 注解接口);
- 方法;
- 构造器;
- 实例域(包含 enum 常量);
- 局部变量;
- 参数变量;
- 类型参数;
// 1. 类和接口:放在 class 和 interface 关键词的前面 @Entity public class User {...} // 2. 变量:放在类型的前面 @SuppressWarnings("unchecked") List<User> users = ...; public User getUser (@Param("id") String userId) // 3. 泛化类或方法中的类型参数: public class Cache<@Immutable V> {...} // 4. 包:在文件 package-info.java 中注解,该文件只包含以注解先导的包语句 /** Package-level Javadoc */ @GPL(version="3") package com.hostmann.corejava; import org.gnu.GPL; // 5. 对局部变量的注解只能在源码级别上进行处理。类文件并不描述局部变量,所有局部变量的注解在编译完类时将被遗弃。同样,对包的注解不能在源码外。
8.4.4 注解类型用法
类型用法注解可出现在如下位置:
// 1. 与泛化类型参数一起使用: List<@NonNull String> Comparator.<@NonNull String> reverseOrder() // 2. 数组中的任何位置: @NonNull String[][] words : words[i][j] 不为 null String @NonNull [][] words : words 不为 null String[] @NonNull [] words : words[i] 不为 null // 3. 与超类和实现类一起使用: class Warning extends @Localized Message // 4. 与强制类型和 instanceof 一起使用: (@Localized String) text if (text instanceof @Localized String) // 5. 与异常规约一起使用 public String read() throws @Localized IOException // 6. 与通配符和类型边界一起使用 List<@Localized ? extends Message> List<? extends @Localized Message> // 7. 与方法和构造器一起使用 @Localized Message::getText
可以将注解放到如 private 和 static 这样的修饰符的前面或后面,习惯上来说:
- 1)类型用法注解:放在其他修饰符的后面,如 private @NonNull String text;
- 2)声明注解:放在其他修饰符的前面,如 @Id private String userId;
8.4.5 注解 this
public class Point { public boolean equals (@ReadOnly Point this, @ReadOnly Object other) {...} } // 通过上面的设定,将 Class.this 注解为 readonly, 实现 下面的调用时, p 和 q 都是未被修改 p.equals(q)
8.5 标准注解(注解用法 1/3:运行期注解)
表 8-2 标准注解 | |||
注解接口 | 类型 | 应用场合 | 目的 |
Deprecated | 用于编译 | 全部 | 将项标记为过时的 |
SuppressWarnings | 用于编译 | 除了包和注解之外的所有情况 | 阻止某个给定类型的警告信息 |
SafeVarargs | 方法 和 构造器 | 断言 vargars 参数可安全使用 | |
Override | 用于编译 | 方法 | 检查该方法是否覆盖了某一个超类方法 |
FunctionalInterface | 接口 | 将接口标记为只有一个抽象方法的函数式接口 | |
Generated | 用于编译 | 全部 |
提供代码工具来使用。任何生成的源代码都可被注解,从而与程序员提供的代码区分开。 如: @Generated("com.hostmann.beanproperty", "2008-01-04T12:08:56.235-0700"); |
PostConstruct、PreDestroy | 用于管理资源 | 方法 | 用于控制对象生命周期的环境中。被标记的方法应在构造后、移除前立即被调用 |
Resource | 用于管理资源 | 类、接口、方法、域 |
在类或接口上:标记为在其他地方要用到的资源; 在方法或域上:为“注入”而标记; 如: Resource(name="jdbc/mydb") private DataSource source; |
Resrouces | 用于资源管理 | 类、接口 | 一个资源数组 |
Target | Java 5,元注解 | 指明可以应用这个注解的那些项 | |
Retention | Java 5,元注解 | 指明这个注解可以保留多久 | |
Documented | Java 5,元注解 | 指明这个注解应该包含在注解项的文档中 | |
Inherited | Java 5,元注解 | 指明当这个注解应用于一个类的时候,能够自动被它的子类继承。仅用于对类的注解 | |
Repeatable | Java 8,元注解 | 指明这个注解可以在同一个项上应用多次 | |
Native(不常用) | Java 8,元注解 | 注释修饰符成员变量,表示该变量可被本地代码引用 |
8.5.3 元注解
@Inherited 仅用于对类的注解,如果一个类具有继承注解,则其所有子类也自动具有同样的注解。
// 定义注解接口 @Inherited @interface Persist {...} // 注解 类 @Persist class Employee {...} // 则 Employee 类的子类都是持久化的 Persist class Manager extends Employee {...}
8.6 源码级注解处理 (注解用法 2/3:源码注解)
8.6.1 注解处理器
// 调用注解处理器 javac -processor ProcessorClassName1, ProcessorClassName2, ... sourceFiles
注解处理器:只能产生新的源文件,无法修改已有的源文件。
注解处理器通常通过扩展 AbstractProcessor 类而实现 Processor 接口,需要指定你的处理器支持的注解。
@SupportedAnnotationTypes("com.hostmann.annotations.ToString") @SupportedSourceVersion(SourceVersion.REALSE_8) public class ToStringAnnotationProcessor extends AbstractProcessor { public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment currentRound) { ... } }
8.6.2 语言模型 API
语言模型API:分析源码级的注解,可以根据 Java 语言的规则分析 Java 程序。
编译器会产生一棵树,其节点实现了 java.lang.model.element.Element 接口及其 TypeElement、VariableElement、ExecutableElement 等子接口的类的实例。这些接口类比于编译时的 Class、Field/Param、Method/Constructor 反射类。
处理过程类似如下:
javac sourceAnnotations/ToStringAnnotationProcessor.java javac -processor sourceAnnotations.ToStringAnnotationProcessor rect/*.java java rect.SourceAnnotationDemo
8.7 字节码工程(注解用法 3/3:字节码级注解)