免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 1263 | 回复: 0
打印 上一主题 下一主题

Java SE 6 新特性: 编译器 API [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2008-08-25 14:06 |只看该作者 |倒序浏览

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.
    程序运行时编译文件

    01 package compile;
    02 import java.util.Date;
    03 public class Target {
    04   public void doSomething(){
    05     Date date = new Date(10, 3, 3);
             // 这个构造函数被标记为deprecated, 编译时会
             // 向错误输出输出信息。
    06     System.out.println("Doing...");
    07   }
    08 }
    09 package compile;
    10 import javax.tools.*;
    11 import java.io.FileOutputStream;
    12 public class Compiler {
    13   public static void main(String[] args) throws Exception{
    14     String fullQuanlifiedFileName = "compile" + java.io.File.separator +
                 "Target.java";     
    15     JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
    16     FileOutputStream err = new FileOutputStream("err.txt");
    17     int compilationResult = compiler.run(null, null, err, fullQuanlifiedFileName);
    18     if(compilationResult == 0){
    19       System.out.println("Done");
    20     } else {
    21       System.out.println("Fail");
    22     }
    23   }
    24 }
       
    首先运行 \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);




    回页首
    编译非文本形式的文件
    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 diagnosticListener,
        Iterable options,
        Iterable classes,
        Iterable compilationUnits
    )
    方法得到。关于每个参数的含义,请参见 JDK 文档。传递不同的参数,会得到不同的
    CompilationTask。通过构造这个类,一个编译过程可以被分成多步。进一步,CompilationTask
    提供了 setProcessors(Iterableprocessors)
    方法,用户可以制定处理 annotation 的处理器。图 1 展示了通过 CompilationTask
    进行编译的过程:
    图 1. 使用 CompilationTask 进行编译


    下面的例子通过构造 CompilationTask 分多步编译一组 Java 源文件。
    清单 2. 构造 CompilationTask 进行编译
    01 package math;
    02 public class Calculator {
    03     public int multiply(int multiplicand, int multiplier) {
    04         return multiplicand * multiplier;
    05     }
    06 }
    07 package compile;
    08 import javax.tools.*;
    09 import java.io.FileOutputStream;
    10 import java.util.Arrays;
    11 public class Compiler {
    12   public static void main(String[] args) throws Exception{
    13     String fullQuanlifiedFileName = "math" + java.io.File.separator +"Calculator.java";
    14     JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
    15     StandardJavaFileManager fileManager  =
               compiler.getStandardFileManager(null, null, null);
    16     Iterable files =
                 fileManager.getJavaFileObjectsFromStrings(
                 Arrays.asList(fullQuanlifiedFileName));
    17     JavaCompiler.CompilationTask task = compiler.getTask(
                 null, fileManager, null, null, null, files);
    18     Boolean result = task.call();
    19     if( result == true ) {
    20       System.out.println("Succeeded");
    21     }
    22   }
    23 }
       
    以上是第一步,通过构造一个 CompilationTask 编译了一个 Java 文件。14-17 行实现了主要逻辑。第 14
    行,首先取得一个编译器对象。由于仅仅需要编译普通文件,因此第 15 行中通过编译器对象取得了一个标准文件管理器。16 行,将需要编译的文件构造成了一个
    Iterable 对象。最后将文件管理器和 Iterable 对象传递给
    JavaCompiler 的 getTask 方法,取得了
    JavaCompiler.CompilationTask 对象。
    接下来第二步,开发者希望生成 Calculator 的一个测试类,而不是手工编写。使用 compiler
    API,可以将内存中的一段字符串,编译成一个 CLASS 文件。
    清单 3. 定制 JavaFileObject
    对象

    01 package math;
    02 import java.net.URI;
    03 public class StringObject extends SimpleJavaFileObject{
    04     private String contents = null;
    05     public StringObject(String className, String contents) throws Exception{
    06         super(new URI(className), Kind.SOURCE);
    07         this.contents = contents;
    08     }
    09     public CharSequence getCharContent(boolean ignoreEncodingErrors)
                 throws IOException {
    10         return contents;
    11     }
    12 }
       
    SimpleJavaFileObject 是 JavaFileObject
    的子类,它提供了默认的实现。继承 SimpleJavaObject 之后,只需要实现
    getCharContent 方法。如
    清单 3

    中的 9-11 行所示。接下来,在内存中构造 Calculator 的测试类
    CalculatorTest,并将代表该类的字符串放置到 StringObject 中,传递给
    JavaCompiler 的 getTask 方法。
    清单 4
    展现了这些步骤。
    清单 4.
    编译非文本形式的源文件

    01 package math;
    02 import javax.tools.*;
    03 import java.io.FileOutputStream;
    04 import java.util.Arrays;
    05 public class AdvancedCompiler {
    06   public static void main(String[] args) throws Exception{
    07     // Steps used to compile Calculator
    08     // Steps used to compile StringObject
    09     // construct CalculatorTest in memory
    10     JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
    11     StandardJavaFileManager fileManager  =
               compiler.getStandardFileManager(null, null, null);
    12         JavaFileObject file = constructTestor();
    13         Iterable files = Arrays.asList(file);
    14         JavaCompiler.CompilationTask task = compiler.getTask (
                     null, fileManager, null, null, null, files);
    15         Boolean result = task.call();
    16         if( result == true ) {
    17           System.out.println("Succeeded");
    18         }
    19   }
    20   private static SimpleJavaFileObject constructTestor() {
    21     StringBuilder contents = new StringBuilder(
               "package math;" +
               "class CalculatorTest {\n" +
                     "  public void testMultiply() {\n" +
                       "    Calculator c = new Calculator();\n" +
                       "    System.out.println(c.multiply(2, 4));\n" +
                       "  }\n" +
                       "  public static void main(String[] args) {\n" +
                       "    CalculatorTest ct = new CalculatorTest();\n" +
                       "    ct.testMultiply();\n" +
                       "  }\n" +
                       "}\n");
    22      StringObject so = null;
    23      try {
    24        so = new StringObject("math.CalculatorTest", contents.toString());
    25      } catch(Exception exception) {
    26        exception.printStackTrace();
    27      }
    28      return so;
    29    }
    30 }
    实现逻辑和
    清单 2
    相似。不同的是在 20-30
    行,程序在内存中构造了 CalculatorTest 类,并且通过 StringObject
    的构造函数,将内存中的字符串,转换成了 JavaFileObject 对象。




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

    01 package math;
    02 public class Calculator {
    03   public int multiply(int multiplicand, int multiplier) {
    04     return multiplicand * multiplier
             // deliberately omit semicolon, ADiagnosticListener
             // will take effect
    05   }
    06 }
    07 package compile;
    08 import javax.tools.*;
    09 import java.io.FileOutputStream;
    10 import java.util.Arrays;
    11 public class CompilerWithListener {
    12   public static void main(String[] args) throws Exception{
    13     String fullQuanlifiedFileName = "math" +
               java.io.File.separator +"Calculator.java";
    14     JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
    15     StandardJavaFileManager fileManager  =
               compiler.getStandardFileManager(null, null, null);
    16     Iterable files =
               fileManager.getJavaFileObjectsFromStrings(
               Arrays.asList(fullQuanlifiedFileName));
    17           DiagnosticCollector collector =
               new DiagnosticCollector();
    18           JavaCompiler.CompilationTask task =
               compiler.getTask(null, fileManager, collector, null, null, files);
    19     Boolean result = task.call();
    20     List> diagnostics =
               collector.getDiagnostics();
    21     for(Diagnostic d : diagnostics){
    22         System.out.println("Line Number->" + d.getLineNumber());
    23                   System.out.println("Message->"+
                               d.getMessage(Locale.ENGLISH));
    24                   System.out.println("Source" + d.getCode());
    25                   System.out.println("\n");
    26     }
    27     if( result == true ) {
    28       System.out.println("Succeeded");
    29     }
    30   }
    31 }
    在 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

    01 class ADiagnosticListener implements DiagnosticListener{
    02          public void report(Diagnostic diagnostic) {
    03           System.out.println("Line Number->" + diagnostic.getLineNumber());
    04           System.out.println("Message->"+ diagnostic.getMessage(Locale.ENGLISH));
    05           System.out.println("Source" + diagnostic.getCode());
    06           System.out.println("\n");
    07         }
    08 }




    回页首
    总结
    JDK 6 的编译器新特性,使得开发者可以更自如的控制编译的过程,这给了工具开发者更加灵活的自由度。通过 API
    的调用完成编译操作的特性,使得开发者可以更方便、高效地将编译变为软件系统运行时的服务。而编译更广泛形式的源代码,则为整合更多的数据源及功能提供了强大的支持。相信随着
    JDK 的不断完善,更多的工具将具有 API 支持,我们拭目以待。
                   
                   
                   

    本文来自ChinaUnix博客,如果查看原文请点:http://blog.chinaunix.net/u1/37262/showart_1146556.html
  • 您需要登录后才可以回帖 登录 | 注册

    本版积分规则 发表回复

      

    北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
    未成年举报专区
    中国互联网协会会员  联系我们:huangweiwei@itpub.net
    感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

    清除 Cookies - ChinaUnix - Archiver - WAP - TOP