Create dynamic applications with javax.tools

Understanding and applying javax.tools.JavaCompiler for building dynamic applications

Many of today's applications require dynamic capabilities, such as enabling users to supply an abstract form of computation that extends an application's static capabilities. The javax.tools package, added to Java™ Platform, Standard Edition 6 (Java SE) as a standard API for compiling Java source, is a superb way to achieve this goal. This article provides an overview of the major classes in the package, demonstrates how to use them to create a facade for compiling Java source from Java Strings instead of files, and then uses this facade to build an interactive plotting application.

David J. Biesack (David.Biesack@sas.com), Principal Systems Developer, SAS Institute, Inc.

David BiesackDavid Biesack is a Principal Systems Developer in the Advanced Computing Lab at SAS Institute, Inc., where he works with advanced analytics and distributed computing. David has been designing and coding in the Java language for 12 of his 19 years at SAS. He participated in JSR 201, which added new syntax features to Java 5.



11 December 2007

Also available in Chinese Russian Japanese

Introduction

The javax.tools package, added to Java SE 6 as a standard API for compiling Java source, lets you add dynamic capabilities to extend static applications. This article provides an overview of the major classes in the package and demonstrates how to use them to create a façade for compiling Java source from Java Strings, StringBuffers, or other CharSequences, instead of files. It then uses this façade to build an interactive plotting application that lets the user express a numeric function y = f(x) using any valid numeric Java expression. Finally, it discusses the possible security risks associated with dynamic source compilation and ways to deal with those risks.

The idea of extending applications via compiling and loading Java extensions isn't new, and several existing frameworks support this capability. JavaServer Pages (JSP) technology in Java Platform, Enterprise Edition (Java EE) is a widely known example of a dynamic framework that generates and compiles Java classes. The JSP translator transforms .jsp files into Java servlets, using intermediate source-code files that the JSP engine then compiles and loads into the Java EE servlet container. The compilation is often performed by directly invoking the javac compiler, which requires an installed Java Development Kit (JDK) or by calling com.sun.tools.javac.Main, which can be found in Sun's tools.jar. Sun's licensing allows tools.jar to be redistributed with the full Java Runtime Environment (JRE). Other ways to implement such dynamic capabilities include using an existing dynamic scripting language (such as JavaScript or Groovy) that integrates with the application's implementation language (see Resources) or writing a domain-specific language and associated language interpreter or compiler.

Other frameworks (such as NetBeans and Eclipse) allow extensions that developers code directly in the Java language, but such systems require external static compilation and source and binary management of the Java code and its artifacts. Apache Commons JCI provides a mechanism to compile and load Java classes into a running application. Janino and Javassist also provide similar dynamic capabilities, although Janino is limited to pre-Java 1.4 language constructs, and Javassist works not at the source-code level but at a Java class abstraction level. (See Resources for links to these projects.) However, because Java developers are already adept at writing in the Java language, a system that lets you simply generate Java source code on the fly and then compile and load it promises the shortest learning curve and the most flexibility.

Benefits of using javax.tools

Using javax.tools has the following advantages:

  • It is an approved extension of Java SE, which means it's a standard API developed through the Java Community Process (as JSR 199). The com.sun.tools.javac.Main API is specifically not part of the documented Java platform API and isn't necessarily available in other vendors' JDKs or guaranteed to have the same API in future releases of the Sun JDK.
  • You use what you know: Java source, not bytecodes. You can create correct Java classes by generating valid Java source without needing to worry about learning the more intricate rules of valid bytecode or a new object model of classes, methods, statements, and expressions.
  • It simplifies, and standardizes on, one supported mechanism for code generation and loading without limiting you to file-based source.
  • It's portable across different vendor implementations of the JDK Version 6 and above, both current and future.
  • It uses a validated version of the Java compiler.
  • Unlike interpreter-based systems, your loaded classes benefit from all the JRE's runtime optimizations.

Java compilation: Concepts and implementation

To understand the javax.tools package, it's helpful to review Java compilation concepts and how the package implements them. The javax.tools package provides abstractions for all of these concepts in a general way that lets you provide the source code from alternate source objects rather than requiring the source to be located in the file system.

Compiling Java source requires the following components:

  • A classpath, from which the compiler can resolve library classes. The compiler classpath is typically composed of an ordered list of file system directories and archive files (JAR or ZIP files) that contain previously compiled .class files. The classpath is implemented by a JavaFileManager that manages multiple source and class JavaFileObject instances and the ClassLoader passed to the JavaFileManager constructor. A JavaFileObject is a FileObject, specialized with one of the one of the JavaFileObject.Kind enumerated types useful to the compiler:
    • SOURCE
    • CLASS
    • HTML
    • OTHER
    Each source file provides an openInputStream() method to access the source as an InputStream.
  • javac options, which are passed as an Iterable<String>
  • Source files — one or more .java source files to compile. JavaFileManager provides an abstract file system that maps source and output file names to JavaFileObject instances. (Here, file means an association between a unique name and a sequence of bytes. The client doesn't need to use an actual file system.) In this article's example, a JavaFileManager manages mappings between class names and the CharSequence instances containing the Java source to compile. A JavaFileManager.Location contains a file name and a flag that indicates if the location is a source or an output location. ForwardingJavaFileManager implements the Chain of Responsibility pattern (see Resources), allowing file managers to be chained together, just as a classpath and source paths chain JARs and directories together. If a Java class isn't found in the chain's first element, the lookup is delegated to the rest of the items in the chain.
  • Output directories, where the compiler writes generated .class files. Acting as a collection of output class files, the JavaFileManager also stores JavaFileObject instances representing compiled CLASS files.
  • The compiler itself. The JavaCompiler creates JavaCompiler.CompilationTask objects that compile source from JavaFileObject SOURCE objects in the JavaFileManager, creating new output JavaFileObject CLASS files and Diagnostics (warnings and errors). The static ToolProvider.getSystemJavaCompiler() method returns the compiler instance.
  • Compiler warnings and errors, which are implemented with Diagnostic and DiagnosticListener. A Diagnostic is a single warning or compile error emitted by the compiler. A Diagnostic specifies:
    • Kind (ERROR, WARNING, MANDATORY_WARNING, NOTE, or OTHER)
    • A source location (including a line and column number)
    • A message
    A client provides a DiagnosticListener to the compiler, through which the compiler passes diagnostics back to the client. DiagnosticCollector is a simple DiagnosticListener implementation.

Figure 1 maps the javac concepts to their implementations in javax.tools:

Figure 1. How javac concepts map to javax.tools interfaces
How javac concepts map into javax.tools interfaces.

With these concepts in mind, you'll now see how to implement a façade for compiling CharSequences.


Compiling Java source in CharSequence instances

In this section, I'll construct a façade for javax.tools.JavaCompiler. The javaxtools.compiler.CharSequenceCompiler class (see Download) can compile Java source stored in arbitrary java.lang.CharSequence objects (such as String, StringBuffer, and StringBuilder), returning a Class. CharSequenceCompiler has the following API:

  • public CharSequenceCompiler(ClassLoader loader, Iterable<String> options): This constructor accepts a ClassLoader that is passed to the Java compiler, allowing it to resolve dependent classes. The Iterable options allow the client to pass additional compiler options that correspond to the javac options.
  • public Map<String, Class<T>> compile(Map<String, CharSequence> classes, final DiagnosticCollector<JavaFileObject> diagnostics) throws CharSequenceCompilerException, ClassCastException: This is the general compilation method that supports compiling multiple sources together. Note that the Java compiler must handle cyclic graphs of classes, such as A.java depending on B.java, B.java depending on C.java, and C.java depending on A.java. The first argument to this method is a Map whose keys are fully qualified class names and whose corresponding values are CharSequences containing the source for that class. For example:
    • "mypackage.A""package mypackage; public class A { ... }";
    • "mypackage.B""package mypackage; class B extends A implements C { ... }";
    • "mypackage.C""package mypackage; interface C { ... }"
    The compiler adds Diagnostics to the DiagnosticCollector. The generic type parameter T is the primary type that you wish to cast the class to. compile() is overloaded with another method that takes a single class name and CharSequence to compile.
  • public ClassLoader getClassLoader(): This method returns the class loader that the compiler assembles when generating .class files, so that you can load other classes or resources from it.
  • public Class<T> loadClass(final String qualifiedClassName) throws ClassNotFoundException: Because the compile() method can define multiple classes (including public nested classes), this method allows these auxiliary classes to be loaded.

To support this CharSequenceCompiler API, I implement the javax.tools interfaces with the classes JavaFileObjectImpl (for storing the CharSequence sources and CLASS output emitted by the compiler) and JavaFileManagerImpl (which maps names to JavaFileObjectImpl instances to manage both the source sequences and the bytecode emitted from the compiler).

JavaFileObjectImpl

JavaFileObjectImpl, shown in Listing 1, implements JavaFileObject and holds a CharSequence source (for SOURCE) or a ByteArrayOutputStream byteCode (for CLASS files). The key method is CharSequence getCharContent(final boolean ignoreEncodingErrors), through which the compiler obtains the source text. See Download for the complete source for all the code examples.

Listing 1. JavaFileObjectImpl (partial source listing)
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 (see Listing 2) extends ForwardingJavaFileManager to map qualified class names to JavaFileObjectImpl instances:

Listing 2. FileManagerImpl (partial source listing)
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

If ToolProvider.getSystemJavaCompiler() can't create a JavaCompiler

The ToolProvider.getSystemJavaCompiler() method can return null if tools.jar is not in the application's classpath. The CharStringCompiler class detects this possible configuration problem and throws an exception with a recommendation for fixing the problem. Note that Sun's licensing allows tools.jar to be redistributed with the JRE.

With these support classes, I can now define the CharSequenceCompiler. It's constructed with a runtime ClassLoader and compiler options. It uses ToolProvider.getSystemJavaCompiler() to get the JavaCompiler instance, then instantiates a JavaFileManagerImpl that forwards to the compiler's standard file manager.

The compile() method iterates over the input map, constructing a JavaFileObjectImpl from each name/CharSequence and adding it to the JavaFileManager so the JavaCompiler finds them when calling the file manager's getFileForInput() method. The compile() method then creates a JavaCompiler.Task instance and runs it. Failures are thrown as a CharSequenceCompilerException. Then, for each source passed to the compile() method, the resulting Class is loaded and placed in the result Map.

The class loader associated with the CharSequenceCompiler (see Listing 3) is a ClassLoaderImpl instance that looks up the bytecode for a class in the JavaFileManagerImpl instance, returning the .class files created by the compiler:

Listing 3. CharSequenceCompiler (partial source listing)
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);
      }
   }
}

The Plotter application

Now that I have a simple API for compiling source, I'll put it in action by creating a function-plotting application, written in Swing. Figure 2 shows the application plotting the x * sin(x) * cos(x) function:

Figure 2. A dynamic application using the javaxtools.compiler package
Plotter Swing application screenshot

The application uses the Function interface defined in Listing 4:

Listing 4. Function interface
package javaxtools.compiler.examples.plotter;
public interface Function {
   double f(double x);
}

The application provides a text field in which the user can enter a Java expression that returns a double value based on an implicitly declared double x input parameter. The application inserts that expression text into the code template shown in Listing 5, at the location marked $expression. It also generates a unique class name each time, replacing $className in the template. The package name is also a template variable.

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

The application fills in the template via fillTemplate(packageName, className, expr), which returns a String object it then compiles using the CharSequenceCompiler. Exceptions or compiler diagnostics are passed to the log() method or written directly into the scrollable errors component in the application.

The newFunction() method shown in Listing 6 returns an object that implements the Function interface (see the source template in Listing 5):

Listing 6. Plotter's Function newFunction(String expr) method
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;
}

You'll typically generate source classes that extend a known base class or implement a specific interface, so that you can cast the instances to a known type and invoke its methods through a type-safe API. Note that the Function class is used as the generic type parameter T when instantiating the CharSequenceCompiler<T>. This allows the compiledFunction likewise to be typed as Class<Function> and compiledFunction.newInstance() to return a Function instance without requiring casts.

Once it has dynamically generated a Function instance, the application uses it to generate y values for a range of x values and then plot the (x,y) values using the open source JFreeChart API (see Resources). The full source of the Swing application is available in the downloadable source in the javaxtools.compiler.examples.plotter package.

This application's source code generation needs are quite modest. Other applications will benefit from a more sophisticated source template facility, such as Apache Velocity (see Resources).


Security risks and strategies

An application that allows arbitrary Java source code to be entered by the user has some inherent security risks. Analogous to SQL injection (see Resources), a system that allows a user or other agent to supply raw Java source for code generation can be exploited maliciously. For example, in the Plotter application presented here, a valid Java expression can contain anonymous nested classes that access system resources, spawn threads for denial-of-service attacks, or perform other exploits. This exploit can be termed Java injection. Such applications should not be deployed in an insecure location in which an untrusted user can access it (such as on a Java EE server as a servlet, or as an applet). Instead, most clients of javax.tools should restrict the user input and translate user requests into secure source code.

Strategies for preserving security when using this package include:

  • Use a custom SecurityManager or ClassLoader that prevents loading of anonymous classes or other classes not under your direct control.
  • Use a source-code scanner or other preprocessor that discards input that uses questionable code constructs. For example, the Plotter can use a java.io.StreamTokenizer and discard input that includes a { (left brace) character, effectively preventing the declaration of anonymous or nested classes.
  • Using the javax.tools API, the JavaFileManager can discard writing of any CLASS file that's unexpected. For example, when compiling a specific class, the JavaFileManager can throw a SecurityExeception for any other calls to store unexpected class files and allow only generated package and class names that the user can't guess or spoof. This is the strategy used by the Plotter's newFunction method.

Conclusion

I've explained the concepts and significant interfaces of the javax.tools package and shown a façade for compiling Java stored in Strings or other CharSequences, then used that library class to develop a sample application that plots an arbitrary f(x) function. The many other highly useful applications of this technique include:

  • Generating binary file readers/writers from a data-description language.
  • Generating format translators, similar to the Java Architecture for XML Binding (JAXB) or persistence frameworks.
  • Implementing domain-specific language interpreters by performing source-to-Java language translation followed by Java source compilation and loading, as is done for JSP.
  • Implementing rules engines.
  • Whatever your imagination calls to mind.

The next time your application-development needs call for dynamic behavior, explore the variety and flexibility that javax.tools provides.


Download

DescriptionNameSize
Sample code for this articlej-jcomp.zip166KB

Resources

Learn

Get products and technologies

  • Apache Velocity: A template processor that can be used for more flexible and complex Java source-code generation.
  • Apache Commons JCI: An existing API for providing access to the Java compiler.
  • Janino: Janino provides capabilities similar to javax.tools but is currently limited to Java 1.3 source compatibility.
  • Javassist: Javassist provides dynamic Java class file creation and loading, but does so through a bytecode model rather than via Java source.
  • JFreeChart: The charting API that this article's sample application uses.

Discuss

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Java technology on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java technology
ArticleID=275805
ArticleTitle=Create dynamic applications with javax.tools
publish-date=12112007