To main content

Properly nesting application processors in Apache Maven projects

Published by Benjamin Marwell on

I recently encountered a Maven multi module (reactor) project where each module had one annotation processor defined, but the database project had two defined. This can be done easier using the lesser known combine.children="append" attribute in a pom.xml. This article will give you a hint how to do this.

Example project: Immutables.org and one module with a JPA metamodel generator

Consider this Maven project structure:

❯ tree -P "pom*"
.
├── bot
│   └── pom.xml
├── bsky-client
│   └── pom.xml
├── common
│   └── pom.xml
├── conversion
│   └── pom.xml
├── db
│   └── pom.xml
├── mastodon
│   └── pom.xml
└── pom.xml

For the sake of this blog article: even with Java Records available, there are cases where I prefer immutables over records. They have a staged builder available, checks are easy to include, they have nice Jackson support, etc.

Since I use it almost everywhere, let’s put it into our root pom.xml:

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-compiler-plugin</artifactId>
  <configuration>
    <annotationProcessorPaths>
      <path>
        <groupId>org.immutables</groupId>
        <artifactId>value</artifactId>
        <version>${dependency.immutables.version}</version>
      </path>
    </annotationProcessorPaths>
  </configuration>
</plugin>

Bad idea: Adding a second annotation processor by repeating all others

So, you may have noticed something. There is one module which cries "create a static metamodel"! That is the db Maven Module, which uses JakartaEE Persistence in this example.

Now, we could just add a static metamodel generator (in this example from EclipseLink) like so:

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-compiler-plugin</artifactId>
  <configuration>
    <annotationProcessorPaths>
      <path>
        <groupId>org.immutables</groupId>
        <artifactId>value</artifactId>
        <version>${dependency.immutables.version}</version>
      </path>
      <path>
        <groupId>org.eclipse.persistence</groupId>
        <artifactId>org.eclipse.persistence.jpa.modelgen.processor</artifactId>
        <version>${dependency.eclipselink.version}</version>
      </path>
    </annotationProcessorPaths>
  </configuration>
</plugin>

However, for this to work, we would have to repeat all globally-defined annotation processors every time. If we add or remove one of them in our root pom.xml, this module will get not notice about that change.

Using children.append in pom.xml

Now, of course we are not adding the JPA modelgen to our root pom.xml. Otherwise, it would run on all other processes, wasting CPU cycles.

Instead, let’s make the <annotationProcessorPaths> configuration appendable:

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-compiler-plugin</artifactId>
  <configuration>
    <annotationProcessorPaths combine.children="append">
      <path>
        <groupId>org.eclipse.persistence</groupId>
        <artifactId>org.eclipse.persistence.jpa.modelgen.processor</artifactId>
        <version>${dependency.eclipselink.version}</version>
      </path>
    </annotationProcessorPaths>
    <compilerArgs combine.children="append">
      <arg>-Aopenjpa.log=INFO</arg>
    </compilerArgs>
  </configuration>
</plugin>

Take a look at the rarely used XML tag: combine.children=append. It means that the inner contents of this tag will be appended to the parent’s contents. The default behaviour would be to replace all contents, but this way, we can preserve the parent’s pom.xml contents.

Example PR on GitHub

Here is an example: social-metricbot#269.

Notice the following things:

  • The children.append was a no-op in the parent pom.xml.

  • The jakarta dependency is not an APT and does not belong there

  • I also added children.append to the error-prone profile. This works, because now three APTs get appended one after another!

Conclusion

The above example shows a common use case where I often encounter duplicate configuration items in projects.

After applying this technique, if you ever change the root pom.xml 's <annotationProcessorPaths> or <compilerArgs>, the update will not be missed in the db module. This neat little trick helps to prevent you from adding duplicate configuration blocks. This proves: Maven supports the DRY principle.