读后笔记 -- 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:字节码级注解)

 

posted on 2023-05-22 13:45  bruce_he  阅读(58)  评论(0编辑  收藏  举报