maven-compiler-plugin
The maven-compiler-plugin is a fundamental part of building Java projects with Apache Maven. Its primary job is to invoke the Java compiler and turn source files under src/main/java and src/test/java into .class files. Although Maven can compile a simple project without explicit plugin configuration, declaring the plugin gives the build a predictable compiler version, Java compatibility level, source encoding, and set of compiler options.
The plugin’s compile goal is bound to Maven’s compile phase by default, while testCompile is associated with the test-compile phase. Pinning the plugin version is important because different plugin releases may support different JDKs, compilers, and configuration options.
A typical Maven build can configure the compiler plugin as follows:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>17</release>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
This configuration compiles the project as Java 17 and ensures that source files are read consistently as UTF-8. The version of the plugin is separate from the Java version: one controls Maven Compiler Plugin behavior, while the other controls the language and platform APIs available to the project.
For centralized version management, the same values can be defined as Maven properties:
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.plugin.version>3.13.0</maven.compiler.plugin.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven.compiler.plugin.version}</version>
</plugin>
</plugins>
</build>
Using properties makes it easier to upgrade the project or its Java baseline without searching through multiple sections of the POM.
Choosing release, source, or target
The compiler plugin exposes three closely related settings:
<source>selects the Java language level accepted byjavac.<target>selects the bytecode version generated for the compiled classes.<release>defines one Java platform against which both language features and standard-library APIs are checked.
For modern builds, <release> is the preferred option. It corresponds to the javac --release flag and provides stronger compatibility guarantees than setting source and target independently.
For example, consider a project compiled on JDK 17 with this configuration:
<configuration>
<source>8</source>
<target>8</target>
</configuration>
The compiler may accept calls to APIs introduced after Java 8 because source and target do not fully restrict access to the JDK’s standard library. The resulting application can therefore compile successfully but fail later with errors such as NoSuchMethodError when it runs on Java 8.
Using the following configuration avoids that problem:
<configuration>
<release>8</release>
</configuration>
The compiler verifies that the code uses only Java 8 language features and Java 8 platform APIs. It also produces class files compatible with Java 8.
The release option requires a compatible JDK, generally Java 9 or newer. It should not be confused with Maven’s maven.compiler.release property, which simply supplies the default value when the plugin configuration does not override it.
Passing compiler options
Additional javac options can be supplied through <compilerArgs>:
<configuration>
<compilerArgs>
<arg>-Xlint:all</arg>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
This example enables detailed lint warnings and preserves parameter names in compiled class metadata. The -parameters option is particularly useful for frameworks and tools that rely on reflective access to constructor and method parameter names.
Compiler arguments should be used selectively. Enabling every diagnostic may produce a large number of warnings, while disabling warnings globally can hide real compatibility or maintenance problems. A shared parent POM can define a sensible baseline, allowing individual modules to extend it when necessary.
Configuring annotation processors
Annotation processors execute during compilation and can generate source code, validate annotations, or create implementations at build time. Common examples include Lombok and MapStruct.
Processor artifacts can be isolated from the project’s regular dependency classpath with <annotationProcessorPaths>:
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.38</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.6.3</version>
</path>
</annotationProcessorPaths>
</configuration>
Keeping processors in a dedicated path prevents them from being bundled into the application’s runtime dependencies when they are needed only at compile time. If the project uses processors that depend on additional libraries, those libraries must also be available to the processor path.
Practical recommendations
A maintainable compiler setup should follow several guidelines:
- Pin the plugin version. Avoid relying on the version inherited from a super POM or Maven installation.
- Use
<release>for Java 9 and newer. It aligns language syntax, bytecode, and platform APIs. - Build with an appropriate JDK. The JDK running Maven must support the selected release level.
- Set UTF-8 explicitly. A fixed encoding prevents source files from being interpreted differently across operating systems.
- Use Maven Toolchains for multiple JDKs. Toolchains allow different modules to compile with different JDK installations while keeping the build reproducible.
- Keep compiler arguments minimal. Prefer focused options over broad warning suppression.
Maven normally uses the JDK that launches the build. If the selected release is newer than that JDK, the build may fail with an error such as invalid target release. In that case, either run Maven with the required JDK or configure a Maven Toolchain.
Troubleshooting common problems
New language features are rejected
Errors involving var, switch expressions, records, sealed classes, or other recent syntax usually indicate that the configured release level is too low. Verify both the plugin configuration and the JDK used by Maven.
The build compiles but fails on an older runtime
This often happens when source and target are used without release. The code may reference a newer standard-library API even though the generated bytecode is intended for an older JVM. Replacing those settings with <release> usually resolves the mismatch.
Lombok-generated members are missing
Check that Lombok is listed under <annotationProcessorPaths>, confirm that its version supports the JDK in use, and ensure that the IDE has Lombok support enabled. Maven and IDE compilation settings should also agree.
The IDE and Maven report different Java versions
IntelliJ IDEA, Eclipse, and other IDEs may maintain their own compiler settings independently of Maven. Make the IDE delegate compilation to Maven or align its project SDK and language level with the POM.
Source characters are decoded incorrectly
An explicit UTF-8 encoding should be configured both in the compiler plugin and through project.build.sourceEncoding. This reduces failures caused by differences between operating-system defaults.
Overall, the compiler plugin should be treated as a controlled part of the build contract. A clear Java release level, a pinned plugin version, deterministic encoding, and explicit processor paths help teams avoid environment-dependent builds and produce artifacts that behave consistently across development machines and deployment targets.