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 parentpom.xml
.The jakarta dependency is not an APT and does not belong there
I also added
children.append
to theerror-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.