Java Compiler API and Maven integration

Java Compiler API and Maven integration

Suppose that we need to make some changes to our source code during a Maven build, e.g., we want to add a special method to a class for a testing Maven profile, but not for other Maven profiles. This post shows a method to achieve this by calling the Java Compiler API from a Maven plugin. The method does not require the knowledge of annotation processing or bytecode manipulation.

The idea is that we load the source files we want to modify into Strings, modify the Strings, and feed the Strings to the Java Compiler API to get the corresponding class files. The modification could be anything you want, e.g., replacing a special token with some value, as long as the Strings still compile.

We need to make a Maven plugin, which extracts properties, such as classpath, source directory, and output directory, from a Maven build, and passes to the Java Compiler API, as well as the modified Strings. We call the plugin SourceProcessingMojo. We bind it to the process-classes phase so that the classes generated by the Java Compiler API can overwrite classes with the same names generated in the compile phase.

  1. /**
  2.  * @goal source-processing
  3.  * @phase process-classes
  4.  * @requiresDependencyResolution compile
  5.  */
  6. public class SourceProcessingMojo extends AbstractMojo {
  7.  
  8.   /**
  9.    * @required
  10.    * @readonly
  11.    * @parameter expression="${project.compileClasspathElements}"
  12.    */
  13.   protected List<String> compileClasspathElements;
  14.  
  15.   /**
  16.    * @required
  17.    * @parameter expression="${project.build.sourceDirectory}"
  18.    */
  19.   protected String sourceDirectory;
  20.  
  21.   /**
  22.    * @required
  23.    * @parameter expression="${project.build.outputDirectory}"
  24.    */
  25.   protected String outputDirectory;
  26.  
  27.   /**
  28.    * e.g.,
  29.    * me/shanhe/Foo;me/shanhe/Bar
  30.    * for
  31.    * me.shanhe.Foo.java and me.shanhe.Bar.java, respectively.
  32.    * @required
  33.    * @parameter expression="${filenames}"
  34.    */
  35.   protected String filenames;
  36.  
  37.   @Override
  38.   public void execute() throws MojoExecutionException, MojoFailureException {
  39.     final String classpath = StringUtility.implode(
  40.         File.pathSeparatorChar, compileClasspathElements);
  41.     final List<JavaFileObject> units = new LinkedList<JavaFileObject>();
  42.     for(String filename : StringUtility.explode(';', filenames)) {
  43.       units.add(new SourceProcessingJavaFileObject(sourceDirectory, filename));
  44.     }
  45.     final List<String> options = new LinkedList<String>();
  46.     options.add("-classpath");
  47.     options.add(classpath);
  48.     options.add("-d");
  49.     options.add(outputDirectory);
  50.     options.add("-encoding");
  51.     options.add("UTF-8");
  52.     options.add("-source");
  53.     options.add("1.6");
  54.     options.add("-target");
  55.     options.add("1.6");
  56.     options.add("-verbose");
  57.     if(false == ToolProvider
  58.         .getSystemJavaCompiler()
  59.         .getTask(null, null, null, options, null, units)
  60.         .call()
  61.         .booleanValue()) {
  62.       throw new MojoFailureException("source processing failure.");
  63.     }
  64.   }
  65. }

The class SourceProcessingJavaFileObject supplies the Java Compiler API with modified Strings.

  1. public class SourceProcessingJavaFileObject extends SimpleJavaFileObject {
  2.  
  3.   private final String filename;
  4.  
  5.   public SourceProcessingJavaFileObject(
  6.       final String directory, final String filename) {
  7.     super(URI.create(String.format("string:///%s%s"
  8.         , filename, Kind.SOURCE.extension)), Kind.SOURCE);
  9.     this.filename = String.format("%s/%s%s"
  10.         , directory, filename, Kind.SOURCE.extension);
  11.   }
  12.  
  13.   @Override
  14.   public CharSequence getCharContent(boolean unused) throws IOException {
  15.     // load the content of filename into a String.
  16.     // modify the String as needed.
  17.     // return the modified String.
  18.   }
  19. }

In a Maven profile, we reference the plugin using a snippet like

  1. <plugin>
  2.   <groupId>me.shanhe</groupId>
  3.   <artifactId>source-processing-mojo</artifactId>
  4.   <version>0.0.1-SNAPSHOT</version>
  5.   <executions>
  6.     <execution>
  7.       <id>source-processing</id>
  8.       <goals>
  9.         <goal>source-processing</goal>
  10.       </goals>
  11.       <configuration>
  12.         <filenames>me/shanhe/Foo;me/shanhe/Bar</filenames>
  13.       </configuration>
  14.     </execution>
  15.   </executions>
  16. </plugin>

Assume that there are /src/main/java/me/shanhe/Foo.java and /src/main/java/me/shanhe/Bar.java.

Basically, we just called javac through the Java Compiler API. You can also do annotation processing, static analysis, and more. Refer to http://docs.oracle.com/javase/6/docs/api/javax/tools/package-summary.html.

Post new comment

The content of this field is kept private and will not be shown publicly.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.

More information about formatting options

To prevent automated spam submissions leave this field empty.