Maven Compiler Plugin
last modified November 8, 2026
In this article we show how to configure Java compilation with the Maven Compiler Plugin. The examples use Maven 3.10, Java 25, and version 3.16.0 of the plugin.
The Compiler plugin compiles the sources of a project. Its
compile goal is bound to the compile phase and
compiles the main sources; its testCompile goal is bound to the
test-compile phase and compiles the tests. By default it calls
the javac compiler of the JDK that runs Maven through the
javax.tools API.
Compiling without configuration
The most important setting is the Java version. If a project does not configure it, the plugin still uses its old defaults: source and target 8. Here is a project that uses records and sealed interfaces but has no compiler configuration.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>compiler-example</artifactId>
<version>1.0.0</version>
<build>
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.16.0</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
The POM only pins the plugin version in pluginManagement.
package com.example;
import java.util.List;
public class App {
sealed interface Shape permits Circle, Square {}
record Circle(double radius) implements Shape {}
record Square(double side) implements Shape {}
static double area(Shape shape) {
return switch (shape) {
case Circle(var r) -> Math.PI * r * r;
case Square(var s) -> s * s;
};
}
public static void main(String[] args) {
var shapes = List.of(new Circle(1.5), new Square(2));
for (var shape : shapes) {
System.out.printf("%s: %.2f%n", shape, area(shape));
}
var info = """
Compiled for Java %d
Running on Java %s""".formatted(
Runtime.version().feature(), System.getProperty("java.version"));
System.out.println(info);
}
}
The application uses a sealed interface, records, record patterns in a
switch expression, and a text block. All of these need a recent
Java version.
$ mvn compile [INFO] Compiling 1 source file with javac [debug target 1.8] to target/classes [WARNING] source value 8 is obsolete and will be removed in a future release [ERROR] /home/user/noconfig/src/main/java/com/example/App.java:[9,5] records are not supported in -source 8 [ERROR] BUILD FAILURE [ERROR] /home/user/noconfig/src/main/java/com/example/App.java:[9,5] records are not supported in -source 8 ...
Setting the Java release
The recommended way to set the Java version is the
maven.compiler.release property. We add it to the same project.
<properties>
<maven.compiler.release>25</maven.compiler.release>
</properties>
The property sets the release parameter of the plugin, which
becomes the --release option of javac. The same can
be written in the plugin configuration as
<release>25</release>. The properties of the form
maven.compiler.* are user properties: they can also be set on
the command line with -D, as long as the POM does not set the
parameter directly in the plugin configuration.
We do not need to set the source encoding. Since Maven 3.10, the
project.build.sourceEncoding property defaults to UTF-8, and the
plugin's encoding parameter defaults to that property.
$ mvn compile [INFO] --- compiler:3.16.0:compile (default-compile) @ compiler-example --- [INFO] Compiling 1 source file with javac [debug release 25] to target/classes [INFO] BUILD SUCCESS ... $ java -cp target/classes com.example.App Circle[radius=1.5]: 7.07 Square[side=2.0]: 4.00 Compiled for Java 25 Running on Java 25.0.3
release versus source and target
Older POMs use the maven.compiler.source and
maven.compiler.target properties instead. These only set the
language level and the bytecode version; the code is still compiled against
the class library of the JDK running Maven. The following project is compiled
for Java 21 on JDK 25.
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
The project sets source and target to 21.
package com.example;
public class App {
public static void main(String[] args) {
IO.println("Hello from " + System.getProperty("java.version"));
}
}
The java.lang.IO class was added in Java 25. Since the compiler
uses the JDK 25 class library, the code compiles. The class files are marked
as Java 21 class files, but they fail on Java 21 with a
NoClassDefFoundError. The compiler warns about exactly this
problem and recommends --release.
$ mvn compile
[INFO] Compiling 1 source file with javac [debug target 21] to target/classes
[WARNING] location of system modules is not set in conjunction with -source 21
not setting the location of system modules may lead to class files that cannot run on JDK 21
--release 21 is recommended instead of -source 21 -target 21 because it sets the location of system modules automatically
[INFO] BUILD SUCCESS
...
$ java -cp target/classes com.example.App
Hello from 25.0.3
$ ~/.sdkman/candidates/java/21.0.2-amzn/bin/java -cp target/classes com.example.App
Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/IO
at com.example.App.main(App.java:6)
Caused by: java.lang.ClassNotFoundException: java.lang.IO
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:526)
... 1 more
Recompiling after configuration changes
With the release option, the compiler checks the API as well. Note that the
plugin compiles incrementally: it only recompiles when sources or
dependencies change. Passing a different option on the command line is not
detected, so the first command reports that there is nothing to compile.
After clean, the compiler rejects the IO class,
which does not exist in the Java 21 API.
$ mvn compile -Dmaven.compiler.release=21 [INFO] Nothing to compile - all classes are up to date. [INFO] BUILD SUCCESS ... $ mvn clean compile -Dmaven.compiler.release=21 [INFO] Compiling 1 source file with javac [debug release 21] to target/classes [ERROR] /home/user/srctarget/src/main/java/com/example/App.java:[6,9] cannot find symbol symbol: variable IO [ERROR] BUILD FAILURE [ERROR] /home/user/srctarget/src/main/java/com/example/App.java:[6,9] cannot find symbol ...
Compiler warnings
By default, javac only prints a summary note about deprecated
and unchecked code. The compilerArgs parameter passes options to
the compiler; -Xlint:all turns on all lint warnings.
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.16.0</version>
<configuration>
<compilerArgs>
<arg>-Xlint:all</arg>
</compilerArgs>
</configuration>
</plugin>
The -Xlint:all option enables all warning categories. Single
categories can be disabled with a minus sign, for example
-Xlint:all,-serial.
package com.example;
import java.util.ArrayList;
import java.util.List;
public class App {
public static void main(String[] args) {
long id = Thread.currentThread().getId();
System.out.println("Thread id: " + id);
List words = new ArrayList();
words.add("falcon");
System.out.println(words);
}
}
The code calls the deprecated Thread.getId method and uses a raw
List type.
The first build prints both warnings and succeeds. The
failOnWarning parameter (the
maven.compiler.failOnWarning user property) adds the
-Werror option, which turns warnings into a build failure. A
related parameter, showDeprecation, adds the
-deprecation option; it is not needed here, because
-Xlint:all includes deprecation warnings.
$ mvn compile [INFO] Compiling 1 source file with javac [debug release 25] to target/classes [WARNING] /home/user/warnings/src/main/java/com/example/App.java:[9,41] getId() in java.lang.Thread has been deprecated [WARNING] /home/user/warnings/src/main/java/com/example/App.java:[12,9] found raw type: java.util.List [WARNING] /home/user/warnings/src/main/java/com/example/App.java:[12,26] found raw type: java.util.ArrayList [WARNING] /home/user/warnings/src/main/java/com/example/App.java:[13,18] unchecked call to add(E) as a member of the raw type java.util.List [INFO] BUILD SUCCESS ... $ mvn clean compile -Dmaven.compiler.failOnWarning=true [INFO] Compiling 1 source file with javac [debug release 25] to target/classes [ERROR] /home/user/warnings/src/main/java/com/example/App.java: warnings found and -Werror specified [ERROR] BUILD FAILURE [ERROR] /home/user/warnings/src/main/java/com/example/App.java: warnings found and -Werror specified ...
Main and test sources
The tests are compiled by the testCompile goal, which uses the
same configuration as compile. A few parameters have test
counterparts: the maven.compiler.testRelease property, for
instance, sets a different release for test code.
<properties>
<maven.compiler.release>17</maven.compiler.release>
<maven.compiler.testRelease>25</maven.compiler.testRelease>
</properties>
Here the library is compiled for Java 17, while the tests can use Java 25
features. To configure other parameters separately, we override the
default-compile or default-testCompile execution,
as shown in the Maven build phases article.
Multi-release JAR files
A multi-release JAR (JEP 238) contains different versions of some classes
for different Java versions. The base classes are in the root of the JAR;
the versioned classes are in META-INF/versions/N. A Java runtime
of version N or newer loads the versioned class instead of the base one.
<properties>
<maven.compiler.release>21</maven.compiler.release>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.16.0</version>
<executions>
<execution>
<id>compile-java25</id>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<release>25</release>
<compileSourceRoots>
<compileSourceRoot>${project.basedir}/src/main/java25</compileSourceRoot>
</compileSourceRoots>
<multiReleaseOutput>true</multiReleaseOutput>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<archive>
<manifest>
<mainClass>com.example.App</mainClass>
</manifest>
<manifestEntries>
<Multi-Release>true</Multi-Release>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
The default execution compiles src/main/java for Java 21. The
compile-java25 execution compiles src/main/java25
for Java 25. The multiReleaseOutput parameter places the classes
in META-INF/versions/25 inside the output directory. The JAR
plugin adds the Multi-Release: true manifest entry, without
which the runtime ignores the versioned classes, and sets the main class.
package com.example;
public class Greeter {
public static String greet() {
return "Base version, running on Java " + Runtime.version().feature();
}
}
The base version of the Greeter class runs on Java 21.
package com.example;
public class Greeter {
public static String greet() {
IO.println("Using the java.lang.IO class");
return "Java 25 version, running on Java " + Runtime.version().feature();
}
}
The Java 25 version of the same class uses the IO class. Both
versions must have the same public API.
package com.example;
public class App {
public static void main(String[] args) {
System.out.println(Greeter.greet());
}
}
The main class is shared by both versions.
The same JAR file prints a different message on Java 25 and on Java 21.
$ mvn package [INFO] --- compiler:3.16.0:compile (default-compile) @ multirelease --- [INFO] Compiling 2 source files with javac [debug release 21] to target/classes [INFO] --- compiler:3.16.0:compile (compile-java25) @ multirelease --- [INFO] Compiling 1 source file with javac [debug release 25] to target/classes/META-INF/versions/25 [INFO] --- jar:3.5.1:jar (default-jar) @ multirelease --- [INFO] BUILD SUCCESS ... $ jar tf target/multirelease-1.0.0.jar | grep class META-INF/versions/25/com/example/Greeter.class com/example/App.class com/example/Greeter.class $ java -jar target/multirelease-1.0.0.jar Using the java.lang.IO class Java 25 version, running on Java 25 $ ~/.sdkman/candidates/java/21.0.2-amzn/bin/java -jar target/multirelease-1.0.0.jar Base version, running on Java 21
Annotation processors
Annotation processors, such as Lombok or MapStruct, generate code during
compilation. Since JDK 23, javac no longer runs processors it
finds on the class path, unless asked to. The reliable way is to list them in
the annotationProcessorPaths parameter, which puts them on a
separate processor path.
<properties>
<maven.compiler.release>25</maven.compiler.release>
<lombok.version>1.18.48</lombok.version>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.16.0</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
Lombok is declared twice: as a provided dependency, so that its
annotations are available to the compiler but are not part of the
application, and as a processor path entry, so that its annotation processor
runs. The version is shared through a property. Without the processor path,
the build fails with cannot find symbol errors, because the
generated methods do not exist; the
<proc>full</proc> parameter would restore the old
behaviour.
package com.example;
import lombok.Builder;
import lombok.Value;
@Value
@Builder
public class User {
String name;
String occupation;
int age;
}
The @Value annotation generates a constructor, getters,
equals, hashCode, and toString for an
immutable class; @Builder generates a builder.
package com.example;
public class App {
public static void main(String[] args) {
var user = User.builder()
.name("John Doe")
.occupation("gardener")
.age(34)
.build();
System.out.println(user);
System.out.println(user.getName() + " is " + user.getAge());
}
}
The application uses the generated builder and getters.
The WARNING lines are printed by the JDK, not by Maven: Lombok
uses internal sun.misc.Unsafe methods, which JDK 24 and later
report as deprecated for removal.
$ mvn compile [INFO] Compiling 2 source files with javac [debug release 25] to target/classes WARNING: A terminally deprecated method in sun.misc.Unsafe has been called WARNING: sun.misc.Unsafe::objectFieldOffset has been called by lombok.permit.Permit WARNING: Please consider reporting this to the maintainers of class lombok.permit.Permit WARNING: sun.misc.Unsafe::objectFieldOffset will be removed in a future release [INFO] BUILD SUCCESS ... $ java -cp target/classes com.example.App User(name=John Doe, occupation=gardener, age=34) John Doe is 34
Useful parameters
These are some commonly used parameters of the compile goal
with their user properties.
| Parameter | User property | Description |
|---|---|---|
release | maven.compiler.release | Java version to compile for (--release) |
source, target | maven.compiler.source, maven.compiler.target | Language level and bytecode version; both default to 8 |
compilerArgs | — | Additional javac options |
failOnWarning | maven.compiler.failOnWarning | Fail the build on warnings (-Werror) |
showDeprecation | maven.compiler.showDeprecation | Show deprecation warnings |
parameters | maven.compiler.parameters | Store method parameter names (-parameters) |
enablePreview | maven.compiler.enablePreview | Enable preview language features |
encoding | encoding | Source file encoding; defaults to project.build.sourceEncoding |
proc | maven.compiler.proc | Annotation processing mode: none, only, or full |
The optimize parameter seen in older POMs is deprecated and has
no effect; the plugin documentation describes it as a no-op in
javac. The
complete list is printed by mvn help:describe
-Dplugin=compiler -Ddetail.
Source
In this article we have shown how to configure Java compilation with the Maven Compiler Plugin.
Author
List all Java tutorials.