Java SE 6d新特性: 编译器 API

2006 年底,sun 公司发布了 java standard edition 6(java se 6)的最终正式版,代号 mustang(野马)。跟 tiger(java se 5)相比,mustang 在性能方面有了不错的提升。与 tiger 在 api 库方面的大幅度加强相比,虽然 mustang 在 api 库方面的新特性显得不太多,但是也提供了许多实用和方便的功能:在脚本,webservice,xml,编译器 api,数据库,jmx,网络和 instrumentation 方面都有不错的新特性和功能加强。 本系列 文章主要介绍 java se 6 在 api 库方面的部分新特性,通过一些例子和讲解,帮助开发者在编程实践当中更好的运用 java se 6,提高开发效率。

本文是其中的第四篇,介绍了 jdk 6 中为在运行时操纵编译器所增加的编译器 api(jsr 199)。您将了解到,利用此 api 开发人员可以在运行时调用 java 编译器,还可以编译非文本形式的 java 源代码,最后还能够采集编译器的诊断信息。本文将展开描述这些功能,并使用这些功能构造一个简单的应用 —— 在内存中,直接为一个类生成测试用例。

新 api 功能简介

jdk 6 提供了在运行时调用编译器的 api,后面我们将假设把此 api 应用在 jsp 技术中。在传统的 jsp 技术中,服务器处理 jsp 通常需要进行下面 6 个步骤:

分析 jsp 代码;

生成 java 代码;

将 java 代码写入存储器;

启动另外一个进程并运行编译器编译 java 代码;

将类文件写入存储器;

服务器读入类文件并运行;

但如果采用运行时编译,可以同时简化步骤 4 和 5,节约新进程的开销和写入存储器的输出开销,提高系统效率。实际上,在 jdk 5 中,sun 也提供了调用编译器的编程接口。然而不同的是,老版本的编程接口并不是标准 api 的一部分,而是作为 sun 的专有实现提供的,而新版则带来了标准化的优点。

新 api 的第二个新特性是可以编译抽象文件,理论上是任何形式的对象 —— 只要该对象实现了特定的接口。有了这个特性,上述例子中的步骤 3 也可以省略。整个 jsp 的编译运行在一个进程中完成,同时消除额外的输入输出操作。

第三个新特性是可以收集编译时的诊断信息。作为对前两个新特性的补充,它可以使开发人员轻松的输出必要的编译错误或者是警告信息,从而省去了很多重定向的麻烦。

运行时编译 java 文件

在 jdk 6 中,类库通过 javax.tools 包提供了程序运行时调用编译器的 api。从这个包的名字 tools 可以看出,这个开发包提供的功能并不仅仅限于编译器。工具还包括 javah、jar、pack200 等,它们都是 jdk 提供的命令行工具。这个开发包希望通过实现一个统一的接口,可以在运行时调用这些工具。在 jdk 6 中,编译器被给予了特别的重视。针对编译器,jdk 设计了两个接口,分别是 javacompiler 和 javacompiler.compilationtask。

下面给出一个例子,展示如何在运行时调用编译器。

指定编译文件名称(该文件必须在 classpath 中可以找到):string fullquanlifiedfilename = "compile" + java.io.file.separator +"target.java";

获得编译器对象: javacompiler compiler = toolprovider.getsystemjavacompiler();

通过调用 toolprovider的 getsystemjavacompiler 方法,jdk 提供了将当前平台的编译器映射到内存中的一个对象。这样使用者可以在运行时操纵编译器。javacompiler 是一个接口,它继承了 javax.tools.tool 接口。因此,第三方实现的编译器,只要符合规范就能通过统一的接口调用。同时,tools 开发包希望对所有的工具提供统一的运行时调用接口。相信将来,toolprovider 类将会为更多地工具提供 getsystemxxxtool 方法。tools 开发包实际为多种不同工具、不同实现的共存提供了框架。

编译文件:int result = compiler.run(null, null, null, filetocompile);

获得编译器对象之后,可以调用 tool.run 方法对源文件进行编译。run 方法的前三个参数,分别可以用来重定向标准输入、标准输出和标准错误输出,null 值表示使用默认值。清单 1 给出了一个完整的例子:

清单 1. 程序运行时编译文件

view code

1 package compile;

2 import java.util.date;

3 public class target {

4public void dosomething(){

5date date = new date(10, 3, 3);

6// 这个构造函数被标记为deprecated, 编译时会

7// 向错误输出输出信息。

8system.out.println("doing...");

9}

10 }

11

12 package compile;

13 import javax.tools.*;

14 import java.io.fileoutputstream;

15 public class compiler {

16public static void main(string[] args) throws exception{

17string fullquanlifiedfilename = "compile" + java.io.file.separator +

18"target.java";

19javacompiler compiler = toolprovider.getsystemjavacompiler();

20

21fileoutputstream err = new fileoutputstream("err.txt");

22

23int compilationresult = compiler.run(null, null, err, fullquanlifiedfilename);

24

25if(compilationresult == 0){

26system.out.println("done");

27} else {

28system.out.println("fail");

29}

30}

31 }

首先运行 \bin\javac compiler.java,然后运行 \jdk1.6.0\bin\java compile.compiler。屏幕上将输出 done ,并会在当前目录生成一个 err.txt 文件,文件内容如下:

note: compile/target.java uses or overrides a deprecated api.

note: recompile with -xlint:deprecation for details.

仔细观察 run 方法,可以发现最后一个参数是 string...arguments,是一个变长的字符串数组。它的实际作用是接受传递给 javac 的参数。假设要编译 target.java 文件,并显示编译过程中的详细信息。命令行为:javac target.java -verbose。相应的可以将 17 句改为:

int compilationresult = compiler.run(null, null, err, “-verbose”,fullquanlifiedfilename);

实现逻辑和 清单 2 相似。不同的是在 20-30 行,程序在内存中构造了 calculatortest 类,并且通过 stringobject 的构造函数,将内存中的字符串,转换成了 javafileobject 对象。

编译非文本形式的文件

jdk 6 的编译器 api 的另外一个强大之处在于,它可以编译的源文件的形式并不局限于文本文件。javacompiler 类依靠文件管理服务可以编译多种形式的源文件。比如直接由内存中的字符串构造的文件,或者是从数据库中取出的文件。这种服务是由

javafilemanager 类提供的。通常的编译过程分为以下几个步骤:

解析 javac 的参数;

在 source path 和/或 classpath 中查找源文件或者 jar 包;

处理输入,输出文件;

在这个过程中,javafilemanager 类可以起到创建输出文件,读入并缓存输出文件的作用。由于它可以读入并缓存输入文件,这就使得读入各种形式的输入文件成为可能。jdk 提供的命令行工具,处理机制也大致相似,在未来的版本中,其它的工具处理各种形式的源文件也成为可能。为此,新的 jdk 定义了 javax.tools.fileobject 和 javax.tools.javafileobject 接口。任何类,只要实现了这个接口,就可以被 javafilemanager 识别。

如果要使用 javafilemanager,就必须构造 compilationtask。jdk 6 提供了 javacompiler.compilationtask 类来封装一个编译操作。这个类可以通过:

javacompiler.gettask (

writer out,

javafilemanager filemanager,

diagnosticlistener super javafileobject> diagnosticlistener,

iterablestring> options,

iterablestring> classes,

iterable extends javafileobject> compilationunits

)

方法得到。关于每个参数的含义,请参见 jdk 文档。传递不同的参数,会得到不同的 compilationtask。通过构造这个类,一个编译过程可以被分成多步。进一步,compilationtask 提供了 setprocessors(iterableprocessors) 方法,用户可以制定处理 annotation 的处理器。图 1 展示了通过 compilationtask 进行编译的过程:

图 1. 使用 compilationtask 进行编译

下面的例子通过构造 compilationtask 分多步编译一组 java 源文件。

清单 2. 构造 compilationtask 进行编译

view code

1 package math;

2

3 public class calculator {

4public int multiply(int multiplicand, int multiplier) {

5return multiplicand * multiplier;

6}

7 }

8

9 package compile;

10 import javax.tools.*;

11 import java.io.fileoutputstream;

12 import java.util.arrays;

13 public class compiler {

14public static void main(string[] args) throws exception{

15string fullquanlifiedfilename = "math" + java.io.file.separator +"calculator.java";

16javacompiler compiler = toolprovider.getsystemjavacompiler();

17standardjavafilemanager filemanager=

18compiler.getstandardfilemanager(null, null, null);

19

20iterable extends javafileobject> files =

21filemanager.getjavafileobjectsfromstrings(

22arrays.aslist(fullquanlifiedfilename));

23javacompiler.compilationtask task = compiler.gettask(

24null, filemanager, null, null, null, files);

25

26boolean result = task.call();

27if( result == true ) {

28system.out.println("succeeded");

29}

30}

31 }

以上是第一步,通过构造一个 compilationtask 编译了一个 java 文件。14-17 行实现了主要逻辑。第 14 行,首先取得一个编译器对象。由于仅仅需要编译普通文件,因此第 15 行中通过编译器对象取得了一个标准文件管理器。16 行,将需要编译的文件构造成了一个 iterable 对象。最后将文件管理器和 iterable 对象传递给 javacompiler 的 gettask 方法,取得了 javacompiler.compilationtask 对象。

接下来第二步,开发者希望生成 calculator 的一个测试类,而不是手工编写。使用 compiler api,可以将内存中的一段字符串,编译成一个 class 文件。

清单 3. 定制 javafileobject 对象

view code

1 package math;

2 import java.net.uri;

3 public class stringobject extends simplejavafileobject{

4private string contents = null;

5public stringobject(string classname, string contents) throws exception{

6super(new uri(classname), kind.source);

7this.contents = contents;

8}

9

10public charsequence getcharcontent(boolean ignoreencodingerrors)

11throws ioexception {

12return contents;

13}

14 }

simplejavafileobject 是 javafileobject 的子类,它提供了默认的实现。继承 simplejavaobject 之后,只需要实现 getcharcontent 方法。如 清单 3 中的 9-11 行所示。接下来,在内存中构造 calculator 的测试类 calculatortest,并将代表该类的字符串放置到 stringobject 中,传递给 javacompiler 的 gettask 方法。清单 4 展现了这些步骤。

清单 4. 编译非文本形式的源文件

view code

1 package math;

2 import javax.tools.*;

3 import java.io.fileoutputstream;

4 import java.util.arrays;

5 public class advancedcompiler {

6public static void main(string[] args) throws exception{

7

8// steps used to compile calculator

9// steps used to compile stringobject

10

11// construct calculatortest in memory

12javacompiler compiler = toolprovider.getsystemjavacompiler();

13standardjavafilemanager filemanager=

14compiler.getstandardfilemanager(null, null, null);

15javafileobject file = constructtestor();

16iterable extends javafileobject> files = arrays.aslist(file);

17javacompiler.compilationtask task = compiler.gettask (

18null, filemanager, null, null, null, files);

19

20boolean result = task.call();

21if( result == true ) {

22system.out.println("succeeded");

23}

24}

25

26private static simplejavafileobject constructtestor() {

27stringbuilder contents = new stringbuilder(

28"package math;" +

29"class calculatortest {\n" +

30"public void testmultiply() {\n" +

31"calculator c = new calculator();\n" +

32"system.out.println(c.multiply(2, 4));\n" +

33"}\n" +

34"public static void main(string[] args) {\n" +

35"calculatortest ct = new calculatortest();\n" +

36"ct.testmultiply();\n" +

37"}\n" +

38"}\n");

39stringobject so = null;

40try {

41so = new stringobject("math.calculatortest", contents.tostring());

42} catch(exception exception) {

43exception.printstacktrace();

44}

45return so;

46}

47 }

采集编译器的诊断信息

第三个新增加的功能,是收集编译过程中的诊断信息。诊断信息,通常指错误、警告或是编译过程中的详尽输出。jdk 6 通过 listener 机制,获取这些信息。如果要注册一个 diagnosticlistener,必须使用 compilationtask 来进行编译,因为 tool 的 run 方法没有办法注册 listener。步骤很简单,先构造一个 listener,然后传递给 javafilemanager 的构造函数。清单 5 对 清单 2 进行了改动,展示了如何注册一个 diagnosticlistener。

清单 5. 注册一个 diagnosticlistener 收集编译信息

view code

1 package math;

2

3 public class calculator {

4public int multiply(int multiplicand, int multiplier) {

5return multiplicand * multiplier

6// deliberately omit semicolon, adiagnosticlistener

7// will take effect

8}

9 }

10

11 package compile;

12 import javax.tools.*;

13 import java.io.fileoutputstream;

14 import java.util.arrays;

15 public class compilerwithlistener {

16public static void main(string[] args) throws exception{

17string fullquanlifiedfilename = "math" +

18java.io.file.separator +"calculator.java";

19javacompiler compiler = toolprovider.getsystemjavacompiler();

20standardjavafilemanager filemanager=

21compiler.getstandardfilemanager(null, null, null);

22

23iterable extends javafileobject> files =

24filemanager.getjavafileobjectsfromstrings(

25arrays.aslist(fullquanlifiedfilename));

26diagnosticcollectorjavafileobject> collector =

27new diagnosticcollectorjavafileobject>();

28javacompiler.compilationtask task =

29compiler.gettask(null, filemanager, collector, null, null, files);

30

31boolean result = task.call();

32listdiagnostic extends javafileobject>> diagnostics =

33collector.getdiagnostics();

34for(diagnostic extends javafileobject> d : diagnostics){

35system.out.println("line number->" + d.getlinenumber());

36system.out.println("message->"+

37d.getmessage(locale.english));

38system.out.println("source" + d.getcode());

39system.out.println("\n");

40}

41

42if( result == true ) {

43system.out.println("succeeded");

44}

45}

46 }

在 17 行,构造了一个 diagnosticcollector 对象,这个对象由 jdk 提供,它实现了 diagnosticlistener接口。18 行将它注册到 compilationtask 中去。一个编译过程可能有多个诊断信息。每一个诊断信息,被抽象为一个 diagnostic。20-26 行,将所有的诊断信息逐个输出。编译并运行 compiler,得到以下输出:

清单 6. diagnosticcollector 收集的编译信息

line number->5

message->math/calculator.java:5: ';' expected

source->compiler.err.expected

实际上,也可以由用户自己定制。清单 7 给出了一个定制的 listener。

清单 7. 自定义的 diagnosticlistener

view code

1 class adiagnosticlistener implements diagnosticlistenerjavafileobject>{

2public void report(diagnostic extends javafileobject> diagnostic) {

3system.out.println("line number->" + diagnostic.getlinenumber());

4system.out.println("message->"+ diagnostic.getmessage(locale.english));

5system.out.println("source" + diagnostic.getcode());

6system.out.println("\n");

7}

8 }


======================================================
在最后,我邀请大家参加新浪APP,就是新浪免费送大家的一个空间,支持PHP+MySql,免费二级域名,免费域名绑定 这个是我邀请的地址,您通过这个链接注册即为我的好友,并获赠云豆500个,价值5元哦!短网址是http://t.cn/SXOiLh我创建的小站每天访客已经达到2000+了,每天挂广告赚50+元哦,呵呵,饭钱不愁了,\(^o^)/
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值