dcsimg
 

An Introduction to the Java Compiler API

by Manoj Debnath
An Introduction to the Java Compiler API

Dig into the concept of Java Compiler APIs and learn what they are all about.

Java Compiler API

The Java Compiler APIs are part of a Java module called java.compiler. This module includes the language model and annotation processing, along with compiler APIs. It defines the type and model declaration of the Java programming language and compiler tools that can be invoked from an application code during execution. The annotation processing facilitates access to annotation processor which can be thought of as a plug-in to the Java compiler. It enables communication between the annotation processor and annotation processing tools environment. The model, element, and type packages deal with the elements of Java programming language whereas the util package assists in the processing of program elements and types.

Compiler Tools

The javax.tools package provides interfaces and classes to work with the Java compiler and can be invoked from a program during execution. It provides a framework that allow clients to locate and run compilers from their own application code. It also provides a Service Provider Interface (SPI) for structured access to diagnostics and file abstraction for overriding file access. The ToolProvider class provides the entry point of the compiler APIs. This class provides methods to locate tool providers of compilers. For example, we can easily find out the list of Java source version supported by the compiler installed in your system.

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
for(SourceVersion sv:compiler.getSourceVersions()){
   System.out.println(sv);
}

And the output is as follows (as per the version installed in the system).

RELEASE_3
RELEASE_4
RELEASE_5
RELEASE_6
RELEASE_7
RELEASE_8
RELEASE_9
RELEASE_10
RELEASE_11

The ToolProvider locates the default compiler in this case. It is also possible to locate alternative compilers or tools by using service provider mechanism. If some vendor provides the Java compiler, the jar file would contain the file META-INF/service/javax.tool.JavaCompiler and would contain a single line: com.vendor.VendorJavaCompiler. We can put the jar file into the class path and locate it as follows:

JavaCompiler vendorJavaCompiler =
   ServiceLoader.load(JavaCompiler.class).iterator().next();

The ServiceProvider is one of Java's util classes that locates and loads a service provider deployed in the execution environment.

Once we locate the JavaCompiler, we can do various compilation diagnostic tasks over the Java source. To illustrate the idea, let us create a simple class first as follows:

package com.mano.jcapidemo;
import java.util.Random;
public class MyClass {
   public static void main(String[] args){
      Random r = new Random();
      System.out.println("Today your Lucky Number is:
         "+r.nextInt(10));
   }
}

Now, with the Java source file created, we can use the diagnostic collector class called DiagnosticCollector to collect diagnostics in a list.

Create another class from which we will invoke the compiler to compile the above class, MyClass, and report diagnostic information to this class. In other words, we'll create an application that loads Java source files, gets it compiled by the Java compiler, and, if there is any error in the source code, make sure it is reported to the host application.

package com.mano.jcapidemo;

import javax.tools.*;
import java.io.File;
import java.util.Arrays;
public class Main {
   public static void main(String[] args) throws Exception{
      JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
      DiagnosticCollector< JavaFileObject > ds = new
         DiagnosticCollector<>();
      try( StandardJavaFileManager mgr =
         compiler.getStandardFileManager( ds, null, null ) ) {
         File file =
            new File( Main.class.getResource("MyClass.java")
               .toURI() );
         Iterable<? extends JavaFileObject> sources =
            mgr.getJavaFileObjectsFromFiles( Arrays.
               asList( file ) );
         JavaCompiler.CompilationTask task =
            compiler.getTask( null, mgr, ds, null,
               null, sources );
         task.call();
      }
      for( Diagnostic < ? extends JavaFileObject >
            d: ds.getDiagnostics() ) {
         System.out.format("Line: %d, %s in %s",
            d.getLineNumber(), d.getMessage( null ),
            d.getSource().getName() );
      }
   }
}

The compiler relies on two services: diagnostic listener and file manager. If listeners are provided, the diagnostics would be supplied to the listener; otherwise, the diagnostics are formatted in an unspecified format and directed to the default error output system (System.err). The compiler tool, by default, associates with the standard file manager and can work as well with any other file manager that meets its requirement.

Annotation Processor

The compilation process also includes an annotation processor. It performs the additional process of compiling code driven by annotations. The processing occurs in a sequence of rounds where each round processes a subset of annotations produced by its previous round. The interface for implementing annotation process is javax.annotation.processin.Processor. The implementing class must provide a no-argument constructor to use by the tools to instantiate the processor. The processing infrastructure should adhere to certain protocols such as:

  • The annotation processor is instantiated by using the no-argument constructor of the processor class.
  • The init method is called by the tools by passing the appropriate ProcessingEnvironment instance.
  • The tools call methods such as getSupportedAnnotationTypes(), getSupportedOptions(), and getSupportedSourceVersion(), as defined by the Processor interface. These methods are called once per run and not on each round.
  • Finally, the process () method on the Processor object is called.

For example, a simple annotation may define as follows.

package com.mano.jcapidemo;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
public@interface CustomAnnotation {
}

A very simple annotation processor to warn that the annotation is applied to any other element except field is as follows:

package com.mano.jcapidemo;

import com.mano.annotation.CustomAnnotation;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
@SupportedAnnotationTypes("com.mano.annotation.CustomAnnotation")
@SupportedSourceVersion(SourceVersion.RELEASE_10)
public class CustomAnnotationProcessor extends
      AbstractProcessor {
   public CustomAnnotationProcessor() {
   }   public Boolean process(Set<? extends
          TypeElement> annotations,
          RoundEnvironment roundEnv) {
      for (Element e : roundEnv.getElementsAnnotatedWith
            (CustomAnnotation.class)) {
         if (e.getKind() != ElementKind.FIELD) {
            processingEnv.getMessager().printMessage(
               Diagnostic.Kind.WARNING,
               "Not a field", e);
            continue;
         }
      }
      return true;
   }
}

The SupportedAnnotationTypes defines what kind of annotations the annotation processor will process and the SupportedSourceVersion defines the version it supports. We begin by extending the AbstractProcessor abstract class which lets us to override the process method. The logic written inside the process method does the all the tricks about what criteria we opt to set to process the annotation. This ultimately determines the meaning of the annotation.

Element Scanners

The element scanners perform analysis across all language elements during the compilation process. It is built around a visitor pattern to scan program elements with default behavior, as appropriate according to the release of the source version. For example, the ElementScanner9 scans according to the source version RELEASE_9 and RELEASE_10, whereas ElementScanner8 scans according to the source version RELEASE_8, respectively. Both these classes are found in the javax.lang.model.util package.

Compiler Tree API

Sometimes. it is necessary to parse the entire Java source file into an abstract syntax tree especially for the purpose of doing deeper analysis. The Java Compiler Tree API adheres to that requirement and closely associates with the javax.lang.model package. It is built around the same patterns as the element scanner and works in a similar fashion. The key class is called TreePathScanner. It visits all the child tree nodes and helps to maintain the path to the parent node. To visit a particular node, we can simply override the corresponding visitorXYZ method.

Conclusion

The Java Compiler APIs provide programmatic access to the Java Compiler from within the Java Applications. As should be obvious, there are deeper implications of this API, of which here we only have scratched the surface. Nonetheless, this quick introduction perhaps would provide clues as to what to look for while getting started with the Java Compiler APIs.

Reference

Java API Documentation

This article was originally published on Tuesday Dec 10th 2019
Home
Mobile Site | Full Site