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 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 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 annotations,  RoundEnvironment roundEnv) {      if (!roundEnv.processingOver()) {         Set 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);  List args = 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插件的方法是什么
URL标题:
http://hbruida.cn/article/gssspg.html