这篇文章英文的原文在:http://scg.unibe.ch/archive/projects/Erni08b.pdf. 做毕业设计报告时,老师要求必须翻译一篇外文,于是很认真的翻译了一下,也算是为开源做一点小贡献。翻译如下:
Javac骇客指南
David Erni and Adrian Kuhn
University of Bern, March 2008
0. 摘要:
这篇文章是介绍修改java编译器的。其中包含对Java编译器的介绍,以及两个例子的实现:一个简单的hello world 和一个重写AST(抽象语法树)的插件。
1. 介绍
随着Java 6的发布,java编译器已经有了开源的版本了。开源的编译器是OpenJDK项目的一部分,可以从Java编译器小组的网站下载 http://www.openjdk.org/groups/compiler/ 。然而就这篇文档的例子来说,任何Java 6的版本都是可用的,因为这些例子并不会重新编译编译器,他们只是扩展编译器的功能。
这篇文章介绍了Java编译器的内在实现。首先我们给出java编译器所包含的编译步骤,然后我们在编写两个例子。两个例子都使用到了编译器里面的插件机制,也就是JSR269所描述的机制。然而,这两个例子却超出了JSR269的范围。把JSR对象和编译器对接,我们实现了AST的重写。我们的例子里面,没有使用assertion(断言)语句,而使用了if-throw语句。
2. Java编译器的内核
这个部分概括了OpenJDK里面的java编译器的编译步骤和对应的注释。这个小节包含了一个简短的介绍。
编译的过程是由定义在com.sun.tools.javac.main里面的Java Compiler类来决定的。当编译器以默认的编译参数编译时,它会执行以下步骤:
a) Parse: 读入一堆*.java源代码,并且把读进来的符号(Token)映射到AST节点上去。
b) Enter: 把类的定义放到符号表(Symbol Table)中去。
c) Process annotations: 可选的。处理编译单元(compilation units)里面所找到的标记(annotation)。
d) Attribute: 为AST添加属性。这一步包含名字解析(name resolution),类型检测(type checking)和常数折叠(constant fold)。
e) Flow: 为前面得到的AST执行流分析(Flow analysis)操作。这个步骤包含赋值(assignment)的检查和可执行性(reachability)的检查。
f) Desugar: 重写AST, 并且把一些复杂的语法转化成一般的语法。
g) Generate: 生成源文件或者类文件。
2.1 Parse
想要Parse文件,编译器要用到com.sun.tools.javac.parser.*里面的类。作为第一步,词法分析器(lexical analyzer)把输入的字符流(character sequence)映射成一个符号流(token sequence)。然后Parser再把生成的符号流映射成一个抽象语法树(AST)
2.2 Enter
在这个步骤中,编译器会找到当前范围(enclosing scope)中发现的所有的定义(definitions),并且把这些定义注册成符号(symbols)。Enter这个步骤又分为以下两个阶段:
在第一个阶段,编译器会注册所有类的符号,并且把这写符号和相应的范围(scope)联系在一起。实现方法是使用一个Visitor(访问者)类,由上而下的遍历AST,访问所有的类,包括类里面的内部类。Enter给每一个类的符号都添加了一个MemberEnter对象,这个对象是由第二个阶段来调用的
在第二个阶段中,这些类被MemberEnter对象所完成(completed,即完成类的成员变量的Enter)。首先,MemberEnter决定一个类的参数,父类和接口。然后这些符号被添加进了类的范围中。不像前一个步骤,这个步骤是懒惰执行的。类的成员只有在被访问时,才加入类的定义中的。这里的实现,是通过安装一个完成对象(member object)到类的符号中。这些对象可以在需要时调用member-enter
最后,enter把所有的顶层类(top-level classes)放到一个todo-queue中,
2.3 Process Annotations
如果存在标记处理器,并且编译参数里面指定要处理标记,那么这个过程就会处理在某个编译单元里面的标记。JSR269定义了一个接口,可以用来写这种Annotation处理插件。然而,这个接口的功能非常有限,并且不能用Collective Behavior扩展这种语言。主要的限制是JSR269不提供子方法的反射调用。
2.4 Attribute
为Enter阶段生成的所有AST添加属性。应当注意,Attribte可能会需要额外的文件被解析(Parse),通过SourceCompleter加入到符号表中。
大多数的环境相关的分析都是发生在这个阶段的。这些分析包括名称解析,类型检查,常数折叠。这些都是子任务。有些子任务调用下列的一些类,但也可能调用其他的。
l Check:这是用于类型检查的类。当有完成错误(completion error)或者类型错误时,它就会报错。
l Resovle: 这是名字解析的类。如果解析失败,就会报错。
l ConstFold: 这是参数折叠类。常数折叠用于简化在编译时的常数表达式。
l Infer:类参数引用的类。
2.5 Flow
这个阶段会对添加属性后的类,执行数据流的检查。存活性分析(liveness analysis) 检查是否每个语句都可以被执行到。异常分析(Excepetion analysis) 检查是豆每个被抛出的异常都是声明过的,并且这些异常是否都会被捕获。确定行赋值(definite assignment)分析保证每个变量在使用时已经被赋值。而确定性不赋值(definite unassignment)分析保证final变量不会被多次赋值。
2.6 Desugar
除去多余的语法,像内部类,类的常数,assertion断言语句,foreach循环等。
2.7 Generate
这是最终的阶段。这个阶段生成许多源文件或者类文件。到底是生成源文件还是类文件取决于编译选项。
3. 什么是JSR 269
Annotation(标记)是java 5里面引进来的,用于在源代码里面附加元信息(meta-information).Java 6则进一步加强了标记的处理功能,即JSR269. JSR269,即插入式标记处理API,为java编译器添加了一个插件机制。有了JSR269,就有能力为java编译器写一个特定的标记处理器了。
JSR269有两组基本API,一组用于对java语言的建模,一组用于编写标记处理器。这两组API分别存在于javax.lang.model.* 和 javax.annotation.processing里面。JSR269的功能是通过以下的java编译选项来调用的。
-proc:{none,only} 是否执行Annotation处理或者编译
-processor
-processorpath
标记处理在javac中时默认开启的。如果要是只想处理标记,而不想编译生成类文件的话,用 –proc:only 选项既即可。
4. 如何用Javac打印出“Hello World!”
在这第一个例子里面,我们些一个简单的标记处理器,用于在编译的时候打印“Hello World!”.我们用编译器的内部消息机制来打印“hello world”。
首先,我们定义如下HelloWorld标记。
public @interface HelloWorld{
} |
添加一个Dummy类使用以上的标记
@HelloWorld public class Dummy{
} |
标记处理可能会发生很轮。每一轮处理器只处理特定的一些标记,并且生成的源文件或者类文件,交给下一轮来处理。如果处理器被要求只处理特定的某一轮,那么他也会处理后续的那些次,包括最后一轮,就算最后一轮没有可以处理的标记。处理器可能也会去处理被这个工具生成的文件。
后一个方法处理前一轮生成的标记类型,并且返回是否这些标记会声明。如果返回是True,那么后续的处理器就不会去处理它们。如果返回是false,那么后续处理器会继续处理它们。一个处理器可能总是返回同样的逻辑值,或者是根据选项改变结果。为了要写一个标记处理器,我们用一个子类来继承AbstractProcessor,并且用SupportedAnnotationTyps 和SupportedSourceVersion标记这个子类。这个子类必须要复写这两个方法:
l public synchronized void init(ProcessingEnvironment processingEnv)
l public boolean process(Set extends TypeElement> annotations,
RoundEnvironment roundEnv)
这两个方法都是在标记处理过程中被java编译器调用的。第一个方法用来初始化插件,只被调用一次。而第二个方法每一轮标记处理都会被调用,并且在所有处理都结束后还会调用一次。
我们的简单的HelloWorldProcessors是这样生成的:
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; } }
|
第八行注册了HelloWorld的标记处理器。也就是说,当标记出现是,就会有一系列的程序被自动调用。第九行设置了标记所支持的源代码版本。
第12到15行复写了初始化方法, 目前为止,我们只是调用父类的方法。
第17到24行复写了处理方法。这个方法是由一些列被标记的程序元素来调用的。这个方法在每一轮处理时,都会调用,并且在最后会多出一轮,用于对空集合的元素的处理。这样,我们可以由一个简单的if语句,使得最后多出的那一轮什么事情都不做。在其他轮中,我们只打印一个hello world消息。我们不用System.out.print,二十使用编译器的消息框架来打印一个消息(note类型的)。其他可能的类型是警告(warning)或者错误(error)。
这个方法返回true,如果你想要声明元素已经被处理过了。
要运行这个例子,执行:
javac HelloWorldProcessor.java
javac -processor HelloWorldProcessor *.java
这个应该会输出:
Note: Hello World!
5. 如何巧妙利用JSR269来重写AST
在这个例子中,我们深入到编译器自身的实现细节中去。我们利用JSR269做一些超出它本身的事情—重写AST。这个处理器会把每一个Assertion语句替换成一个throw语句。也就是说,每当有以下语句出现时
assert cond: detail;
会被替换成:
If(!cond) throw new AssertionError(detail);
后面的这个语句不会生成assert的字节码,而是生成一个普通的if语句,带有一个throw重句。结果就算你的虚拟机没有激活assertions功能时,assertions的检查还是会被执行。这个功能对各种库是非常有用的,因为你写库的时候,是没有办法控制用户的VM设置的。
再次,我们还是先继承AbstractProcessor。然而,这次我们不会针对某一个特殊的标记,而是用“*”这个符号来表示对所有的源代码都调用处理器。
@SupportedAnnotationTypes("*") @SupportedSourceVersion(SourceVersion.RELEASE_6) public class ForceAssertions extends AbstractProcessor { } |
初始化方法如下:
30 private int tally; 31 private Trees trees; 32 private TreeMaker make; 33 private Name.Table names; 34 35 @Override 36 public synchronized void init(ProcessingEnvironment env) { 37 super.init(env); 38 trees = Trees.instance(env); 39 Context context = ((JavacProcessingEnvironment) env).getContext(); 40 make = TreeMaker.instance(context); 41 names = Name.Table.instance(context); 42 tally = 0; 43 } |
我们使用处理环境(ProcessingEnvironment)来获得对编译器一些组件的引用。在编译器里面,在每次调用编译器时都会有一个处理环境(ProcessingEnvironment)。在编译器中,我们使用Component.instance(context)来获得对组件的引用。
我们使用的组件如下:
l Trees – JSR269的一个工具类,用于联系程序元素和树节点。比如,对于一个方法元素,我们可以获得这个元素对应的AST树节点。
l TreeMaker – 编译器的内部组件,是用于创建树节点的工厂类。工厂类里面方法的命名方式跟Javac源代码里面的方法是统一的。
l Name.Table – 另一个编译器的内部组件。Name类是编译器内部字符串的一个抽象。为了提高效率,Javac使用了哈希字符串。
请注意,在第39行,我们把处理环境(ProcessingEnvironment)强制转换成了编译器的内部类型。
最后,我们把一个计数器初始化成0.这个计数器是用来记录发生替换的数量。
处理方法如下:
45 @Override 46 public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) { 47 if (!roundEnv.processingOver()) { 48 Set extends Element> elements = roundEnv.getRootElements(); 49 for (Element each : elements) { 50 if (each.getKind() == ElementKind.CLASS) { 51 JCTree tree = (JCTree) trees.getTree(each); 52 TreeTranslator visitor = new Inliner(); 53 tree.accept(visitor); 54 } 55 } 56 } else 57 processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, tally + " assertions inlined."); 58 return false; 59 } |
我们遍历所有的程序元素,为每一个类都重写AST。在第51行,我们把JSR269的树节点转换成编译器内部的树节点。这两种树节点的不同之处在于,JSR269节点是停留在方法层的(即方法method是最基本的元素,不会再细分下去),而内部的AST节点,是所有元素(包括方法以下的)都可以访问的。我们要访问每一个语句,所以需要访问到AST的所有节点。
树的转换是通过继承TreeTranslator来完成的,TreeTranslator本身是继承自TreeVisitor的。这些类都不是JSR269的一部分。所以,从这里开始,我们所写的所有代码都是在编译器内部工作的。
在第57行,是else部分,用于报告处理过的assertion语句数量。这个语句只有在最后一轮处理才会执行。
Inliner这个类实现了AST重写。Inliner继承了TreeTranslator,并且是标记处理器的一个内部类。注意,TreeTranslator本身是不会转换任何节点的。
private class Inliner extends TreeTranslator {
}
为了转换assertion语句,我们需要复写默认的TreeTranslator.visitAssert (JCAssert) 方法,如下所示:
63 @Override 64 public void visitAssert(JCAssert tree) { 65 super.visitAssert(tree); 66 JCStatement newNode = makeIfThrowException(tree); 67 result = newNode; 68 tally++; 69 } |
正在转换的节点会被当做参数传入到方法中。在第67行,转换的结果,通过赋值给变量TreeTranslator.result而返回。
按照惯例,一个转换方法应该这样生成:
l 调用父类的转换方法,以确保转换可以被应用到自己点上面去。
l 执行真正的转换
l 把转换结果赋值给TreeTranslator.result。结果的类型不一定要和传进来的参数的类型一样。相反,只要java编译器允许,我们可以返回任何类型的节点。这里TreeTranslator本身没有限制类型,但是如果返回了错误的类型,那么就很有在后续过程中产生灾难性后果。
我们写一个私有函数来实现转换,makeIfThrowException:
71 private JCStatement makeIfThrowException(JCAssert node) { 72 // make: if (!(condition) throw new AssertionError(detail); 73 List 74 ? List. 75 : List.of(node.detail); 76 JCExpression expr = make.NewClass( 77 null, 78 null, 79 make.Ident(names.fromString("AssertionError")), 80 args, 81 null); 82 return make.If( 83 make.Unary(JCTree.NOT, node.cond), 84 make.Throw(expr), 85 null); 86 } |
这个方法传入一个assertion语句,返回一个if语句。我们可以这样做,事因为不管是assertion还是if,他们都是语句(statement),所以在java的语法中是等价的。Java中没有明文规定,禁止用if语句来代替assertion语句。
makeIfThrowException是用于AST重写的方法。我们使用TreeMaker来创建新的树节点。如果有这样的一个表达式:
assert cond:detail;
我们就可以替换成下面的形式:
If(!cond) throw new AssertionErrror(detal);
在第73到75行,我们考虑到了detail被省略的情况。在76到81行,我们创建了一个AST节点,这个节点的作用是创建AssertionError。在第79行,我们使用Name.Table来把字符串“AssertionError”变成编译器内部的字符串。在80行,我们再传入73到75行创建的参数args。第77,78和81行传入了null值,因为这个节点既没有外部实例,也没有类型参数,也不是在匿名类内部。
在第83行,我们对assertion的条件做了一个Not操作。84行,我们创建了一个throw表达式,最后,在82到85行,我们把所有的东西都放到了if语句中。
注意:List类是java编译器中另外一个令人印象深刻的实现。编译器用了它自己的数据类型来实现List,而不是使用java集合框架(Java Collection Framework)。List和Pair数据类的实现,都用到了Lisp语言里面所谓的cons。Pairs是这样实现的:
public class Pair { public final A fst; public final B snd; public Pair(A fst, B snd) { this.fst = fst; this.snd = snd; } ... }
|
而List是这样实现的:
public class List extends AbstractCollection implements java.util.List { public A head; public List tail; public List(A head, List tail) { this.tail = tail; this.head = head; } ... } |
并且有许多静态的方法,可以很方便的创建List:
l List.nil()
l List.of(A)
l List.of(A,A)
l List.of(A,A,A)
l List.of(A,A,A,A...)
Pair也是一样:
l Pair.of(A,B)
同样,非传统的命名方式也带来了更漂亮的代码
不像传统java中用的代码:
List list = new List();
list.add(a);
list.add(b);
list.add(c);
而现在只需要写:
List.of(a, b, c);
5.1 运行AST重写
为了展示AST重写,我们使用:
1 public class Example { 2 3 public static void main(String[] args) { 4 String str = null; 5 assert str != null : "Must not be null"; 6 } 7 8 } |
并且执行:
javac ForceAssertions.java
javac -processor ForceAssertions Example.java
就会产生这样的输出:
Note: 1 assertions inlined
现在,我们我们我们禁用assertion,再执行例子:
java -disableassertions Example
得到:
Exception in thread "main" java.lang.AssertionError: Must not be null at Example.main(Example.java:1)
利用编译器的选项 –printsource,我们甚至可以得到重写过后的AST,并且以Java源代码的方式显示出来。要注意的是,我们必须重定向输出,否者原来的源文件会被覆盖了。
执行:
javac -processor ForceAssertions -printsource -d gen Example.java
产生结果:
1 public class Example { 2 3 public Example() { 4 super(); 5 } 6 7 public static void main(String[] args) { 8 String str = null; 9 if (!(str != null)) throw new AssertionError("Must not be null"); 10 } 11 }
|
可以发现,第9行已经被重写过了,第3到5行加入了一个默认的构造函数。
5.2 如何把标记处理器注册成服务
Java提供了一个注册服务的机制。如果一个标记处理器被注册成了一个服务,编译器就会自动的去找到这个标记处理器。注册的方法是,在classpath中找到一个叫META-INF/services的文件夹,然后放入一个javax.annotation.processing.Processor的文件。文件格式是很明显的,就是要包含要注册的标记处理器的完整名称。每个名字都要占单独的一行。
5.3 进一步的阅读
Erni在他的本科毕业设计中描述了一个更复杂的编译器修改。他不是依赖JSR269,而是直接在编译过程中的几个点进行直接修改。
参考
[1] David Erni. JAG - a Prototype for Collective Behavior in Java. Bachelors
Thesis, University of Bern. March 2008.
[2] Joseph D. Darcy. JSR-000269 Pluggable Annotation Processing API,
December 2006. http://jcp.org/en/jsr/detail?id=269.
A 在OSX下面安装Java 6
默认情况下,当前的OSX只是内置 了Java 5.0,为了安装Java6,从以下地址下载安装文件http://www.apple.com/support/downloads/javaformacosx105update1.html. 这个更新需要Mac OS X 10.5.2或者是更新的,而且要64位基于Intel的Mac。这个更新不会替换已经存在的J2SE 5.0安装,或者是改变Java的默认版本。
B ForceAssertions.java的全部源代码
1 import java.util.Set; 2 3 import javax.annotation.processing.AbstractProcessor; 4 import javax.annotation.processing.ProcessingEnvironment; 5 import javax.annotation.processing.RoundEnvironment; 6 import javax.annotation.processing.SupportedAnnotationTypes; 7 import javax.annotation.processing.SupportedSourceVersion; 8 import javax.lang.model.SourceVersion; 9 import javax.lang.model.element.Element; 10 import javax.lang.model.element.ElementKind; 11 import javax.lang.model.element.TypeElement; 12 import javax.tools.Diagnostic; 13 14 import com.sun.source.util.Trees; 15 import com.sun.tools.javac.processing.JavacProcessingEnvironment; 16 import com.sun.tools.javac.tree.JCTree; 17 import com.sun.tools.javac.tree.TreeMaker; 18 import com.sun.tools.javac.tree.TreeTranslator; 19 import com.sun.tools.javac.tree.JCTree.JCAssert; 20 import com.sun.tools.javac.tree.JCTree.JCExpression; 21 import com.sun.tools.javac.tree.JCTree.JCStatement; 22 import com.sun.tools.javac.util.Context; 23 import com.sun.tools.javac.util.List; 24 import com.sun.tools.javac.util.Name; 25 26 @SupportedAnnotationTypes("*") 27 @SupportedSourceVersion(SourceVersion.RELEASE_6) 28 public class ForceAssertions extends AbstractProcessor { 29 30 private int tally; 31 private Trees trees; 32 private TreeMaker make; 33 private Name.Table names; 34 35 @Override 36 public synchronized void init(ProcessingEnvironment env) { 37 super.init(env); 38 trees = Trees.instance(env); 39 Context context = ((JavacProcessingEnvironment) env).getContext(); 40 make = TreeMaker.instance(context); 41 names = Name.Table.instance(context); 42 tally = 0; 43 } 44 45 @Override 46 public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) { 47 if (!roundEnv.processingOver()) { 48 Set extends Element> elements = roundEnv.getRootElements(); 49 for (Element each : elements) { 50 if (each.getKind() == ElementKind.CLASS) { 51 JCTree tree = (JCTree) trees.getTree(each); 52 TreeTranslator visitor = new Inliner(); 53 tree.accept(visitor); 54 } 55 } 56 } else 57 processingEnv.getMessager().printMessage( Diagnostic.Kind.NOTE, tally + " assertions inlined."); 58 return false; 59 } 60 61 private class Inliner extends TreeTranslator { 62 63 @Override 64 public void visitAssert(JCAssert tree) { 65 super.visitAssert(tree); 66 JCStatement newNode = makeIfThrowException(tree); 67 result = newNode; 68 tally++; 69 } 70 71 private JCStatement makeIfThrowException(JCAssert node) { 72 // make: if (!(condition) throw new AssertionError(detail); 73 List 74 ? List. 75 : List.of(node.detail); 76 JCExpression expr = make.NewClass( 77 null, 78 null, 79 make.Ident(names.fromString("AssertionError")), 80 args, 81 null); 82 return make.If( 83 make.Unary(JCTree.NOT, node.cond), 84 make.Throw(expr), 85 null); 86 } 87 88 } 89 90 } 91 |