使用 javax.tools 创建动态应用程序

理解并使用 javax.tools.JavaCompiler 构建动态应用程序

如今,很多应用程序都需要实现动态功能,例如,能够为用户提供抽象形式的计算来扩展应用程序的静态功能。作为 Java™ Platform, Standard Edition 6 (Java SE) 中新添加的标准 API(实现 Java 源代码编译),javax.tools 包可以很好地满足这一需求。本文将探查 javax.tools 包中提供的主要类,并演示如何使用它们创建一个 façade,以从 Java String 而不是从文件中编译 Java 源代码,并使用这个 façade 构建交互式绘图应用程序。

David J. Biesack (David.Biesack@sas.com), 首席系统开发人员, SAS Institute, Inc.

David BiesackDavid Biesack 是 SAS Institute, Inc. 高级计算实验室的首席系统开发人员,主要研究高级分析学和分布式计算。在 SAS 的 19 年职业生涯中,David 利用 Java 语言进行设计和编码的时间长达 12 年。他参与制定了 JSR 201 规范,该规范向 Java 5 添加新的语法特性。



2007 年 12 月 24 日

简介

javax.tools 包是一种添加到 Java SE 6 的标准 API,可以实现 Java 源代码编译,使您能够添加动态功能来扩展静态应用程序。本文将探查 javax.tools 包中提供的主要类,并演示如何使用它们创建一个 façade,以从 Java StringStringBuffer 或其他 CharSequence 中编译 Java 源代码,而不是从文件中编译。之后,使用这个 façade 构建一个交互式绘图应用程序,通过该应用程序,用户可以使用任何有效的数值 Java 表达式表示一个数值函数 y = f(x)。最后,本文将讨论与动态源代码编译相关的安全风险以及应对方法。

通过编译和加载 Java 扩展来对应用程序进行扩展,这种想法并不新鲜,并且一些现有框架也支持这一功能。Java Platform, Enterprise Edition (Java EE) 中的 JavaServer Pages (JSP) 技术就是一种广为人知的动态框架,能够生成并编译 Java 类。JSP 转换器通过中间产物即源代码文件将 .jsp 文件转换为 Java servlet,JSP 引擎随后将源代码文件编译并加载到 Java EE servlet 容器中。编译过程通常是通过直接调用 javac 编译器完成的,这需要安装 Java Development Kit (JDK) 或者调用 com.sun.tools.javac.Main(可通过 Sun 的 tools.jar 获得)。Sun 的许可证允许跟随完整的 Java 运行时环境(Java Runtime Environment,JRE)一起重新分发 tools.jar。其他实现动态功能的方法包括使用可与应用程序实现语言(参见 参考资料)集成的现有动态脚本编制语言(例如 JavaScript 或 Groovy),或者编写特定于域的语言和相关的语言解释器和编译器。

其他框架(例如 NetBeans 和 Eclipse)支持开发人员使用 Java 语言直接编写扩展,但是这些框架需要外部静态编译,并需要管理 Java 代码及其工件的源代码和二进制文件。Apache Commons JCI 提供了一种机制可以将 Java 类编译并加载到运行的应用程序中。Janino 和 Javassist 也提供了类似的动态功能,但是 Janino 只限于 Java 1.4 之前的语言,而 Javassist 只能工作在 Java 类抽象级别,而不能在源代码级别工作(参见 参考资料 中有关这些项目的链接)。然而,Java 开发人员已经熟悉如何使用 Java 语言编写程序,如果一种系统能够动态生成 Java 源代码并进行编译和加载,那么它可以保证最短的学习曲线并提供最大程度的灵活性。

使用 javax.tools 的优点

使用 javax.tools 具有以下好处:

  • 它是经过认可的 Java SE 扩展,这意味着它是 Java Community Process(按照 JSR 199 规范)开发的标准 API。com.sun.tools.javac.Main API 不属于 经过文件归档的 Java 平台 API,因此没有必要在其他供应商的 JDK 中提供或保证在未来版本的 Sun JDK 中提供该 API。
  • 您可以应用已经掌握的知识:Java 源代码,而不是字节码。不需要学习生成有效字节码的复杂规则或者新的类对象模型、方法、语句和表达式,通过生成有效的 Java 源代码,您就可以创建正确的 Java 类。
  • 它简化了一种受支持机制,并进行了标准化,使您不用局限于基于文件的源代码就可生成并加载代码。
  • 它可以在 JDK Version 6 和更高版本的各种供应商实现之间移植,并且将来也支持这种移植性。
  • 它使用经过验证的 Java 编译器。
  • 与基于解释器的系统不同,所加载的类可以从 JRE 的运行时优化中受益。

Java 编译:概念和实现

要理解 javax.tools 包,回顾 Java 编译概念以及如何使用包实现编译将非常有帮助。javax.tools 包以一种通用的方式对这些概念进行了抽象化,使您能够从备用的源代码对象提供源代码,而不要求源代码必须位于文件系统中。

编译 Java 源代码需要用到以下组件:

  • 类路径,编译器从其中解析库类。编译器类路径通常由一个有序的文件系统目录列表和一些归档文件(JAR 或 ZIP 文件)组成,归档文件中包含先前编译过的 .class 文件。类路径由一个 JavaFileManager 实现,后者管理多个源代码和类 JavaFileObject 实例以及传递给 JavaFileManager 构造函数的 ClassLoaderJavaFileObject 是一个 FileObject,专门处理以下任一种由编译器使用的 JavaFileObject.Kind 枚举类型:
    • SOURCE
    • CLASS
    • HTML
    • OTHER
    每个源文件提供一个 openInputStream() 方法,可以作为 InputStream访问源代码。
  • javac 选项,以 Iterable<String> 的形式传递
  • 源文件 — 待编译的一个或多个 .java 源文件。JavaFileManager 提供了一个抽象的文件系统,可以将源文件和输出文件的文件名映射到 JavaFileObject 实例(其中,文件 表示一个惟一名称和一串字节之间的关联。客户机不需要使用实际的文件系统)。在本文的示例中,JavaFileManager 管理类名与 CharSequence 实例之间的映射,后者包含待编译的 Java 源代码。JavaFileManager.Location 包含一个文件名和一个标记,该标记可以表明该位置是源代码还是一个输出位置。 ForwardingJavaFileManager 实现 Chain of Responsibility 模式(参见 参考资料),允许将文件管理器链接在一起,就像类路径和源路径将 JAR 和目录链接起来一样。如果在这条链的第一个元素中没有发现 Java 类,那么将对链中的其他元素进行查找。
  • 输出目录,编译器在其中编写生成的 .class 文件。作为输出类文件的集合,JavaFileManager 也保存表示编译过的 CLASS 文件的 JavaFileObject 实例。
  • 编译器JavaCompiler 创建 JavaCompiler.CompilationTask 对象,后者从 JavaFileManager 中的 JavaFileObject SOURCE 对象编译源代码,创建新的输出 JavaFileObject CLASS 文件和 Diagnostic(警告和错误)。静态 ToolProvider.getSystemJavaCompiler() 方法返回编译器实例。
  • 编译器警告和错误,这些内容通过 DiagnosticDiagnosticListener 实现。Diagnostic 是编译器发出的警告或编译错误。Diagnostic 指定以下内容:
    • KindERRORWARNINGMANDATORY_WARNINGNOTEOTHER
    • 源代码中的位置(包括行号和列号)
    • 消息
    客户机向编译器提供一个 DiagnosticListener,编译器可通过它向客户机发回诊断信息。DiagnosticCollector 是一个简单的 DiagnosticListener 实现。

图 1 展示了 javax.tools 中的 javac 概念与其实现之间的映射:

图 1. javac 概念如何映射到 javax.tools 接口
javac 概念如何映射到 javax.tools 接口。

了解了这些概念,我们现在看一下如何实现一个 façade 来编译 CharSequence


编译 CharSequence 实例中的 Java 源代码

在本节中,我将为 javax.tools.JavaCompiler 构造一个 façade。javaxtools.compiler.CharSequenceCompiler 类(参见 下载)可以编译保存在任何 java.lang.CharSequence 对象(例如 StringStringBufferStringBuilder)中的 Java 源代码,并返回一个 ClassCharSequenceCompiler 提供了以下 API:

  • public CharSequenceCompiler(ClassLoader loader, Iterable<String> options):该构造函数接收传递给 Java 编译器的 ClassLoader,允许它解析相关类。Iterable options 允许客户机传递额外的编译器选项,这些选项均对应于 javac 选项。
  • public Map<String, Class<T>> compile(Map<String, CharSequence> classes, final DiagnosticCollector<JavaFileObject> diagnostics) throws CharSequenceCompilerException, ClassCastException:这是常用的编译方法,支持同时编译多个源代码。注意,Java 编译器必须处理类的循环依赖性,例如 A.java 依赖 B.java,B.java 依赖 C.java,而 C.java 又依赖 A.java。该方法的第一个参数是 Map,它的键为完全限定类名,而相应的值为包含该类源代码的 CharSequence。例如:
    • "mypackage.A""package mypackage; public class A { ... }";
    • "mypackage.B""package mypackage; class B extends A implements C { ... }";
    • "mypackage.C""package mypackage; interface C { ... }"
    编译器将 Diagnostic 添加到 DiagnosticCollector。您希望对类进行强制转换的主要类型是泛型类型参数 Tcompile() 被另一个方法重载,其参数为一个类名和待编译的 CharSequence
  • public ClassLoader getClassLoader():该方法返回编译器在生成 .class 文件时组装的类加载器,因此,可以从其中加载其他类或资源。
  • public Class<T> loadClass(final String qualifiedClassName) throws ClassNotFoundException:由于 compile() 方法可以定义多个类(包括公共的嵌套类),因此允许加载辅助类。

为了支持 CharSequenceCompiler API,我使用 JavaFileObjectImpl 类和 JavaFileManagerImpl 实现了 javax.tools 接口,其中,JavaFileObjectImpl 类用于保存 CharSequence 源代码和编译器产生的 CLASS 输出,而 JavaFileManagerImpl 用于将名称映射到 JavaFileObjectImpl 实例,从而管理源代码和编译器产生的字节码。

JavaFileObjectImpl

清单 1 中的 JavaFileObjectImpl 实现 JavaFileObject 并保存 CharSequence source(用于 SOURCE)或一个 ByteArrayOutputStream byteCode(用于 CLASS 文件)。关键方法是 CharSequence getCharContent(final boolean ignoreEncodingErrors),编译器通过它获得源代码文本。参见 下载,获取所有代码示例的源代码。

清单 1. JavaFileObjectImpl(只显示部分源代码)
final class JavaFileObjectImpl extends SimpleJavaFileObject {
   private final CharSequence source;

   JavaFileObjectImpl(final String baseName, final CharSequence source) {
      super(CharSequenceCompiler.toURI(baseName + ".java"), Kind.SOURCE);
      this.source = source;
   }
   @Override
   public CharSequence getCharContent(final boolean ignoreEncodingErrors)
         throws UnsupportedOperationException {
      if (source == null)
         throw new UnsupportedOperationException("getCharContent()");
      return source;
   }
}

FileManagerImpl

FileManagerImpl(参见清单 2)对 ForwardingJavaFileManager 进行了扩展,将限定的类名映射到 JavaFileObjectImpl 实例:

清单 2. FileManagerImpl(只显示部分源代码)
final class FileManagerImpl extends ForwardingJavaFileManager<JavaFileManager> {
   private final ClassLoaderImpl classLoader;
   private final Map<URI, JavaFileObject> fileObjects 
           = new HashMap<URI, JavaFileObject>();

   public FileManagerImpl(JavaFileManager fileManager, ClassLoaderImpl classLoader) {
      super(fileManager);
      this.classLoader = classLoader;
   }

   @Override
   public FileObject getFileForInput(Location location, String packageName,
         String relativeName) throws IOException {
      FileObject o = fileObjects.get(uri(location, packageName, relativeName));
      if (o != null)
         return o;
      return super.getFileForInput(location, packageName, relativeName);
   }

   public void putFileForInput(StandardLocation location, String packageName,
         String relativeName, JavaFileObject file) {
      fileObjects.put(uri(location, packageName, relativeName), file);
   }
}

CharSequenceCompiler

如果 ToolProvider.getSystemJavaCompiler() 不能创建 JavaCompiler

如果 tools.jar 不在应用程序类路径中,ToolProvider.getSystemJavaCompiler() 方法可以返回 nullCharStringCompiler 类检测到这一配置问题后将抛出一个异常,并给出修复建议。注意,Sun 许可证允许跟随 JRE 一起重新分发 tools.jar。

通过这些支持类,现在可以定义 CharSequenceCompiler,可使用运行时 ClassLoader 和编译器选项构建。它使用 ToolProvider.getSystemJavaCompiler() 获得 JavaCompiler 实例,然后对转发给编译器标准文件管理器的 JavaFileManagerImpl 进行实例化。

compile() 方法对输入映射进行迭代,从每个名称/CharSequence 中构建一个 JavaFileObjectImpl,并将其添加到 JavaFileManager 中,因此,在调用文件管理器的 getFileForInput() 方法时,JavaCompiler 将找到它们。compile() 方法随后将创建一个 JavaCompiler.Task 实例并运行该实例。故障被作为 CharSequenceCompilerException 抛出。然后,对于传递给 compile() 方法的所有源代码,加载产生的 Class 类并放在结果 Map 中。

CharSequenceCompiler (参见清单 3)相关的类加载器是一个 ClassLoaderImpl 实例,它在 JavaFileManagerImpl 实例中查找类的字节码,返回编译器创建的 .class 文件:

清单 3. CharSequenceCompiler(只显示部分源代码)
public class CharSequenceCompiler<T> {
   private final ClassLoaderImpl classLoader;
   private final JavaCompiler compiler;
   private final List<String> options;
   private DiagnosticCollector<JavaFileObject> diagnostics;
   private final FileManagerImpl javaFileManager;

   public CharSequenceCompiler(ClassLoader loader, Iterable<String> options) {
      compiler = ToolProvider.getSystemJavaCompiler();
      if (compiler == null) {
         throw new IllegalStateException(
               "Cannot find the system Java compiler. "
               + "Check that your class path includes tools.jar");
      }
      classLoader = new ClassLoaderImpl(loader);
      diagnostics = new DiagnosticCollector<JavaFileObject>();
      final JavaFileManager fileManager = compiler.getStandardFileManager(diagnostics,
            null, null);
      javaFileManager = new FileManagerImpl(fileManager, classLoader);
      this.options = new ArrayList<String>();
      if (options != null) {
         for (String option : options) {
            this.options.add(option);
         }
      }
   }

   public synchronized Map<String, Class<T>> 
	      compile(final Map<String, CharSequence> classes,
                  final DiagnosticCollector<JavaFileObject> diagnosticsList)
          throws CharSequenceCompilerException, ClassCastException {
      List<JavaFileObject> sources = new ArrayList<JavaFileObject>();
      for (Entry<String, CharSequence> entry : classes.entrySet()) {
         String qualifiedClassName = entry.getKey();
         CharSequence javaSource = entry.getValue();
         if (javaSource != null) {
            final int dotPos = qualifiedClassName.lastIndexOf('.');
            final String className = dotPos == -1 
	              ? qualifiedClassName
                  : qualifiedClassName.substring(dotPos + 1);
            final String packageName = dotPos == -1 
	              ? "" 
                  : qualifiedClassName .substring(0, dotPos);
            final JavaFileObjectImpl source = 
	              new JavaFileObjectImpl(className, javaSource);
            sources.add(source);
            javaFileManager.putFileForInput(StandardLocation.SOURCE_PATH, packageName,
                  className + ".java", source);
         }
      }
      final CompilationTask task = compiler.getTask(null, javaFileManager, diagnostics,
                                                    options, null, sources);
      final Boolean result = task.call();
      if (result == null || !result.booleanValue()) {
         throw new CharSequenceCompilerException("Compilation failed.", 
                                                 classes.keySet(), diagnostics);
      }
      try {
         Map<String, Class<T>> compiled = 
	                    new HashMap<String, Class<T>>();
         for (Entry<String, CharSequence> entry : classes.entrySet()) {
            String qualifiedClassName = entry.getKey();
            final Class<T> newClass = loadClass(qualifiedClassName);
            compiled.put(qualifiedClassName, newClass);
         }
         return compiled;
      } catch (ClassNotFoundException e) {
         throw new CharSequenceCompilerException(classes.keySet(), e, diagnostics);
      } catch (IllegalArgumentException e) {
         throw new CharSequenceCompilerException(classes.keySet(), e, diagnostics);
      } catch (SecurityException e) {
         throw new CharSequenceCompilerException(classes.keySet(), e, diagnostics);
      }
   }
}

Plotter 应用程序

现在,我有了一个可以编译源代码的简单 API,我将通过创建函数绘制应用程序(使用 Swing 编写)来发挥其功用。图 2 展示了该应用程序使用图形表示 x * sin(x) * cos(x) 函数:

图 2. 使用 javaxtools.compiler 包的动态应用程序
Plotter Swing 应用程序屏幕截图

该应用程序使用清单 4 中定义的 Function 接口:

清单 4. Function 接口
package javaxtools.compiler.examples.plotter;
public interface Function {
   double f(double x);
}

应用程序提供了一个文本字段,用户可以向其中输入一个 Java 表达式,后者根据隐式声明的 double x 输入参数返回一个 double 值。在清单 5 所示的代码模板中,应用程序将表达式文本插入到以 $expression 标记的位置。并且每次生成一个惟一的类名,替代模板中的 $className。包名也是一个模板变量。

清单 5. Function 模板
package $packageName;
import static java.lang.Math.*;
public class $className
             implements javaxtools.compiler.examples.plotter.Function {
  public double f(double x) { 
    return ($expression) ; 
  }
}

应用程序使用 fillTemplate(packageName, className, expr) 函数填充模板,它返回一个 String 对象,然后应用程序使用 CharSequenceCompiler 进行编译。异常或编译器诊断信息被传递给 log() 方法或直接写入到应用程序中可滚动的 errors 组件。

清单 6 显示的 newFunction() 方法将返回一个对象,它将实现 Function 接口(参见 清单 5 中的源代码模板):

清单 6. PlotterFunction newFunction(String expr) 方法
Function newFunction(final String expr) {
   errors.setText("");
   try {
      // generate semi-secure unique package and class names
      final String packageName = PACKAGE_NAME + digits();
      final String className = "Fx_" + (classNameSuffix++) + digits();
      final String qName = packageName + '.' + className;
      // generate the source class as String
      final String source = fillTemplate(packageName, className, expr);
      // compile the generated Java source
      final DiagnosticCollector<JavaFileObject> errs =
            new DiagnosticCollector<JavaFileObject>();
      Class<Function> compiledFunction = stringCompiler.compile(qName, source, errs,
            new Class<?>[] { Function.class });
      log(errs);
      return compiledFunction.newInstance();
   } catch (CharSequenceCompilerException e) {
      log(e.getDiagnostics());
   } catch (InstantiationException e) {
      errors.setText(e.getMessage());
   } catch (IllegalAccessException e) {
      errors.setText(e.getMessage());
   } catch (IOException e) {
      errors.setText(e.getMessage());
   }
   return NULL_FUNCTION;
}

您通常会生成一些源类,使用它们扩展已有的基类或实现特定接口,从而可以将实例转换为已知的类型并通过一个类型安全 API 调用其方法。注意,在实例化 CharSequenceCompiler<T> 时,Function 类被作为泛型类型参数 T 使用。因此,也可以将 compiledFunction 输入作为 Class<Function>compiledFunction.newInstance(),以返回 Function 实例,而不需要进行强制转换。

动态生成一个 Function 实例后,应用程序使用它针对一系列 x 值生成 y 值,然后使用开源的 JFreeChart API(参见 参考资料)描绘(x,y)值。Swing 应用程序的完整源代码可以通过 javaxtools.compiler.examples.plotter 包的 可下载源代码 部分中获得。

这个应用程序的源代码生成需求非常普通。更为复杂的源代码模板工具可以更好地满足其他应用程序的需求,例如 Apache Velocity (参见 参考资料)。


安全风险和策略

如果应用程序允许用户随意输入 Java 源代码,那么会存在一些内在的安全风险。类似 SQL 注入(参见 参考资料),如果系统允许用户或其他代理提供原始的 Java 源代码来生成代码,那么恶意用户可能会利用这一点。例如,在本文的 Plotter 应用程序中,一个有效的 Java 表达式可能包含匿名的嵌套类,它可以访问系统资源、在受到拒绝服务攻击时产生大量线程或者执行其他行为。这些行为被称为 Java 注入。这种应用程序不应该部署在非信任用户可以随意访问的不可靠位置,例如作为 servlet 或 applet 的 Java EE 服务器。相反,javax.tools 的大多数客户机应该限制用户输入并将用户请求转换为安全的源代码。

使用这种包时可以采用的安全策略包括:

  • 使用定制的 SecurityManagerClassLoader 阻止加载匿名类或其他无法直接控制的类。
  • 使用源代码扫描程序或其他预处理程序,删除含有可疑代码构造的输入。例如,Plotter 可以使用 java.io.StreamTokenizer 并删除含有 {(左侧大括号)字符的输入,从而有效阻止了匿名或嵌套类的声明。
  • 使用 javax.tools API,JavaFileManager 可以删除任何预料之外的 CLASS 文件的写入。例如,当编译某个特定类时,对于要求保存预料之外的类文件的任何调用,JavaFileManager 将抛出一个 SecurityExeception 异常,并只允许生成用户无法猜测或欺骗的包名或类名。PlotternewFunction 方法使用的就是这种策略。

结束语

在本文中,我解释了 javax.tools 包的概念和重要接口,并展示了一个 façade,使用它编译保存在 String 或其他 CharSequence 中的 Java 源代码,然后使用这个库类开发可以描绘任意 f(x) 函数的样例应用程序。可以使用这种技术创建其他有用的应用程序:

  • 根据数据描述语言生成二进制文件阅读程序/写入程序。
  • 生成类似于 Java Architecture for XML Binding (JAXB) 或持久性框架的格式转换程序。
  • 通过执行源代码与 Java 语言的转换、Java 源代码编译和加载(类似于 JSP 技术),实现特定于域的语言解释器。
  • 实现规则引擎。
  • 您可以想像得到的任何内容。

下一次开发应用程序时,如果需要使用动态行为,请尝试 javax.tools 提供的多样性和灵活性。


下载

描述名字大小
本文的样例代码j-jcomp.zip166KB

参考资料

学习

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文
  • javax.tools:javax.tools 包 API 的 Javadoc 文档。
  • JSR 199: Java Compiler API:最早的 Java Specification Request,Java Community Process 根据该规范开发了 javax.tools 包。
  • Java design patterns 101”(David Gallardo,developerWorks,2002 年 1 月):该教程介绍了 Façade 和 Chain of Responsibility 模式,这些内容是在 Design Patterns: Elements of Reusable Object-Oriented Design(Erich Gamma et al.,Addison-Wesley,1994 年)中首次提出的。
  • 安全编程: 安全地调用组件”(David Wheeler,developerWorks,2004 年 12 月):本文介绍了 SQL 注入,这种安全漏洞允许用户指定插入到 SQL 语句中的原始文本,这些 SQL 语句随后会被提交到数据库。
  • 动态调用动态语言”(Tom McQueeney,developerWorks,2007 年 9月):这个包含两部分的系列文章描述了另一种使用 Java SE 6 轻松实现动态应用程序功能的方法。
  • 访问 developerWorks Java 技术专区,获得所需的资源提高 Java 技能。
  • 浏览 技术书店,查找有关本文所述主题和其他技术主题的图书。

获得产品和技术

  • Apache Velocity:该模板处理程序可以实现更灵活和更复杂的 Java 源代码生成。
  • Apache Commons JCI:这个现有 API 可以提供对 Java 编译器的访问。
  • Janino:Janino 提供了与 javax.tools 类似的功能,但是只与 Java 1.3 源代码兼容。
  • Javassist:Javassist 提供了动态的 Java 文件创建和加载功能,但是是通过字节码模型而不是 Java 源代码完成的。
  • JFreeChart:本文的样例应用程序使用了这个绘图 API。

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=278924
ArticleTitle=使用 javax.tools 创建动态应用程序
publish-date=12242007