Java重写AST插件的方法是什么
本篇内容主要讲解“Java重写AST插件的方法是什么”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Java重写AST插件的方法是什么”吧!
创新互联建站是一家专注于网站制作、网站设计与策划设计,普洱网站建设哪家好?创新互联建站做网站,专注于网站建设10年,网设计领域的专业建站公司;建站业务涵盖:普洱等地区。普洱做网站价格咨询:13518219792
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: 生成源文件或者类文件。
wps_clip_image-12054_thumb
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 { }
初始化方法如下:
private int tally; private Trees trees; private TreeMaker make; private Name.Table names; @Override public synchronized void init(ProcessingEnvironment env) { super.init(env); trees = Trees.instance(env); Context context = ((JavacProcessingEnvironment) env).getContext(); make = TreeMaker.instance(context); names = Name.Table.instance(context); tally = 0; }
我们使用处理环境(ProcessingEnvironment)来获得对编译器一些组件的引用。在编译器里面,在每次调用编译器时都会有一个处理环境(ProcessingEnvironment)。在编译器中,我们使用Component.instance(context)来获得对组件的引用。
我们使用的组件如下:
l Trees – JSR269的一个工具类,用于联系程序元素和树节点。比如,对于一个方法元素,我们可以获得这个元素对应的AST树节点。
l TreeMaker – 编译器的内部组件,是用于创建树节点的工厂类。工厂类里面方法的命名方式跟Javac源代码里面的方法是统一的。
l Name.Table – 另一个编译器的内部组件。Name类是编译器内部字符串的一个抽象。为了提高效率,Javac使用了哈希字符串。
请注意,在第39行,我们把处理环境(ProcessingEnvironment)强制转换成了编译器的内部类型。
***,我们把一个计数器初始化成0.这个计数器是用来记录发生替换的数量。
处理方法如下:
@Override 46 public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) { if (!roundEnv.processingOver()) { Set extends Element> elements = roundEnv.getRootElements(); for (Element each : elements) { if (each.getKind() == ElementKind.CLASS) { JCTree tree = (JCTree) trees.getTree(each); TreeTranslator visitor = new Inliner(); tree.accept(visitor); } } } else processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, tally + " assertions inlined."); return false; }
我们遍历所有的程序元素,为每一个类都重写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) 方法,如下所示:
@Override public void visitAssert(JCAssert tree) { super.visitAssert(tree); JCStatement newNode = makeIfThrowException(tree); result = newNode; tally++; }
正在转换的节点会被当做参数传入到方法中。在第67行,转换的结果,通过赋值给变量TreeTranslator.result而返回。
按照惯例,一个转换方法应该这样生成:
l 调用父类的转换方法,以确保转换可以被应用到自己点上面去。
l 执行真正的转换
l 把转换结果赋值给TreeTranslator.result。结果的类型不一定要和传进来的参数的类型一样。相反,只要java编译器允许,我们可以返回任何类型的节点。这里TreeTranslator本身没有限制类型,但是如果返回了错误的类型,那么就很有在后续过程中产生灾难性后果。
我们写一个私有函数来实现转换,makeIfThrowException:
private JCStatement makeIfThrowException(JCAssert node) { // make: if (!(condition) throw new AssertionError(detail); Listargs = node.getDetail() == null ? List. nil() : List.of(node.detail); JCExpression expr = make.NewClass( null, null, make.Ident(names.fromString("AssertionError")), args, null); return make.If( make.Unary(JCTree.NOT, node.cond), make.Throw(expr), null); }
这个方法传入一个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重写,我们使用:
public class Example { public static void main(String[] args) { String str = null; assert str != null : "Must not be null"; } }
并且执行:
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
产生结果:
public class Example { public Example() { super(); } public static void main(String[] args) { String str = null; if (!(str != null)) throw new AssertionError("Must not be null"); } }
可以发现,第9行已经被重写过了,第3到5行加入了一个默认的构造函数。
5.2 如何把标记处理器注册成服务
Java提供了一个注册服务的机制。如果一个标记处理器被注册成了一个服务,编译器就会自动的去找到这个标记处理器。注册的方法是,在classpath中找到一个叫META-INF/services的文件夹,然后放入一个javax.annotation.processing.Processor的文件。文件格式是很明显的,就是要包含要注册的标记处理器的完整名称。每个名字都要占单独的一行。
5.3 进一步的阅读
Erni在他的本科毕业设计中描述了一个更复杂的编译器修改。他不是依赖JSR269,而是直接在编译过程中的几个点进行直接修改。
到此,相信大家对“Java重写AST插件的方法是什么”有了更深的了解,不妨来实际操作一番吧!这里是创新互联网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!
新闻标题:Java重写AST插件的方法是什么
标题链接:http://pwwzsj.com/article/gssspg.html