Skip to content

User guide

Martin Desruisseaux edited this page Apr 27, 2025 · 13 revisions

This page gives an introduction to the Maven compiler plugin 4.0. For differences compared to the previous compiler plugin, see migration from Maven 3 to Maven 4.

Declaring source files

The recommended way to declare source directories is with <source> elements. Note that these sources are declared in the <build> element and therefore can apply to all plugins. If no source is declared, the default values relevant to the compiler plugin are as below:

<build>
  <sources>
    <source>
      <lang>java</lang>
      <scope>main</scope>
      <directory>src/main/java</directory>
    </source>
    <source>
      <lang>java</lang>
      <scope>test</scope>
      <directory>src/test/java</directory>
    </source>
  </sources>
<build>

If a <sources> element is declared, its content will replace the above default values. Therefore, the above defaults may need to be copied if the developer wants to add sources instead of replacing them.

Multiple source directories

The <source> element can be repeated as many times as desired for the same language and scope. Optionally, an enabled flag allows to include or exclude the whole directory according a property value. The following example adds the extension directory only if the value of the include.extension property is set to true. For brevity, this example uses the default values of the <scope> and <lang> elements when applicable. These defaults are main and java respectively.

<build>
  <sources>
    <source>
      <directory>src/main/java</directory>
    </source>
    <source>
      <directory>src/extension/java</directory>
      <enabled>${include.extension}</enabled>
    </source>
    <source>
      <scope>test</scope>
      <directory>src/test/java</directory>
    </source>
  </sources>
<build>

Include/exclude filters

For each source directory, the list of source files to compile can be filtered. If <include> elements are specified, only the files that match at least one include filters may be compiled. If no <include> element is specified, then the default filter is glob:**/*.java. Next, if <exclude> elements are specified, all included elements matching at least one exclude filter become excluded.

The filtering implementation uses java.nio.file.PathMatcher. The syntax is described in the Javadoc of standard Java. Various syntaxes are possible, including glob and regex. If no syntax is specified, Maven defaults to the glob syntax where / is the path separator regardless the platform (including Windows), * matches any filename inside a directory and ** matches any number of directories (see above-cited Javadoc for more detailed explanation). All paths to be matched are relative to the path specified in the <directory> element. Example:

<build>
  <sources>
    <source>
      <directory>src/main/java</directory>
      <excludes>
        <exclude>**/Foo*.java</exclude>
      </excludes>
    </source>
    <source>
      <scope>test</scope>
      <directory>src/test/java</directory>
    </source>
  </sources>
</build>

Default syntax of include/exclude filters

If "glob:" or other syntax was specified as a prefix of a pattern, the pattern is used as-is without any change. If no syntax is specified, then the pattern is modified as below before to be used as a glob syntax:

  • The platform-specific separator (\ on Windows) is replaced by /.
  • Trailing / is completed as /**.
  • The ** wildcard is interpreted as "0 or more directories" instead of "1 or more directories". This is implemented by adding variants of the pattern with ** progressively removed.
  • Bracket characters [, ], { and } are escaped.
  • On Unix, the escape character \ is itself escaped (on Windows, it was the path separator converted to /).

Multi-releases project

The <source> element can be repeated many times with different Java releases. The lowest release value is given to the --release compiler option for the base classes, and the source files in all directories associated to that version are compiled together. Then, the sources of all other releases are compiled in separated javac executions, one execution for each release, in the order of increasing release values. For each new execution, the output directories of all previous executions are added to the class-path with highest releases first, and the output is written in the META-INF/versions/${release}/ sub-directory. Example:

<build>
  <sources>
    <source>
      <directory>src/main/java</directory>
      <targetVersion>17</targetVersion>
    </source>
    <source>
      <directory>src/main/java_21</directory>
      <targetVersion>21</targetVersion>
    </source>
    <source>
      <scope>test</scope>
      <directory>src/test/java</directory>
    </source>
  </sources>
</build>

Note: For targeting Java 8, the version number shall be 8, not 1.8.

Modular project

The recommended way to build a project using the Java Module System is to declare the module name(s) together with the sources, as in the example below. The name declared inside the <module> element must match the name declared inside the module-info.java file of that module. More than one module can be declared if desired. It means that for Java modular projects, there is no longer a one-to-one relationship between a Maven project or subproject and a Java module, unless the developer chooses to restrict herself to exactly one Java module per Maven subproject. Note, however, that there is still a one-to-one relationship between Java modules and Maven artifacts (the JAR files identified by Maven coordinates). See the maven-jar-plugin for more information. (TODO: maven-jar-plugin has not yet been updated for Java module support.)

<build>
  <sources>
    <source>
      <module>my.product.foo</module>
      <directory>src/java/my.product.foo/main</directory>
    </source>
    <source>
      <module>my.product.foo</module>
      <directory>src/java/my.product.foo/test</directory>
      <scope>test</scope>
    </source>
    <source>
      <module>my.product.bar</module>
      <directory>src/java/my.product.bar/main</directory>
    </source>
    <source>
      <module>my.product.bar</module>
      <directory>src/java/my.product.bar/test</directory>
      <scope>test</scope>
    </source>
  </sources>
</build>

While it is a common practice to have a sub-directory of the same name as the module, this is not mandatory. It is also a common practice to place the main and test sub-directories after the java sub-directory instead of before it (in order to group them per module), but this is not mandatory neither. It is also possible to specify multiple source directories for the same module.

Compiler output

For a modular project, the compiler writes the class files of each module in a directory of the same name as the module. For example, the classes of the my.product.foo module will be written in the target/classes/my.product.foo/ directory rather than directly in target/classes/. This is the standard javac behavior, not a Maven particularity. Technically, the behavior of the Maven compiler plugin is straightforward: if a <module> element is present, the plugin declares the source directories using the --module-source-path compiler option, which implies the above-cited new output directory. Otherwise, the plugin declares the source directories with the --source-path option. The plugin does nothing more.

If that new output directory is not desired, the Maven 3 way to do a modular project is still supported: setup the Maven project has if it was non-modular (without <module> element), but keep adding a module-info.java file in the sources. However, migration to a fully modular project is recommended when applicable.

Class-path versus module-path

By default, each dependency of a modular project is placed on the module-path if the dependency contains a module-info.class file or an Automatic-Module-Name entry in the META-INF/MANIFEST.MF file, otherwise the dependency is placed on the class-path. This heuristic rule can be overridden by specifying explicitly the type of the dependency. For example, the following snippet forces the placement of a dependency on the module-path even if that dependency has no module-info or manifest entry:

<dependencies>
  <dependency>
    <groupId>my.dependency</groupId>
    <artifactId>foo</artifactId>
    <type>modular-jar</type>
    <version>1.0</version>
  </dependency>
</dependencies>

If the version is managed by a <dependencyManagement> section, the managed dependency must contains the same <type> element, otherwise Maven will consider that this is not the same artifact. For the reverse operation (force placement on the class-path instead of the module-path even if the dependency is modular), replace modular-jar type by classpath-jar.

Compilation of tests

By default, the maven compiler plugin automatically adds the following options when compiling the tests:

  • --patch-module options for each <source> elements having a test scope.
  • --add-modules option with a value set to all dependencies having the test or test-only scope.
  • --add-reads options (repeated for each Java module to patch) for all dependencies having the test or test-only scope.

In many cases, this is sufficient and there is no need to configure the plugin with additional compiler arguments. However, sometime there is a need for more control. In particular, there is often a need to export or open more packages for whitebox testing. While it can be done by specifying options such as --add-exports in the <compilerArgs> element of the plugin configuration, this is tedious, error prone and need to be repeated in other plugins such as Surefire. The Maven compiler plugin offers a more convenient mechanism, described below.

Patching a module-info for tests

This is a variation of the deprecated practice consisting in overwriting the module-info.java of the main code with another module-info.java defined in the test directory. Instead, the module-info.java file formerly defined in the test should be replaced by a module-info-patch.maven file in the same directory. The later is Maven-specific: the content of module-info-patch.maven looks like module-info.java, but is not standard Java, hence the .maven file suffix. The principles are:

  • Everything that a developer would like to change in a module-info.java file for testing purposes is declared in module-info-patch.maven.
  • Everything that is not in module-info.java is not in module-info-patch.maven neither. In particular, everything that specify paths to JAR files stay in the pom.xml file.
  • All keywords inside the patch-module block of that file map directly to Java compiler or Java launcher options.

The syntax is:

  • The same styles of comment as Java (/**/ and //) are accepted.
  • The first tokens, after comments, shall be patch-module followed by the name of the module to patch.
  • All keywords inside patch-module are Java compiler or Java launcher options without the leading -- characters.
  • Each option value ends at the ; character, which is mandatory.

The accepted keywords are add-modules, limit-modules, add-reads, add-exports and add-opens. Note that they are options where the values are package or module names, not paths to source or binary files. Options with path values should be handled in the <sources> and <dependencies> elements of the POM instead.

Below is an example of a module-info-patch.maven file content for modifying the module-info of a module named my.product.foo:

/*
 * The same comments as in Java are allowed.
 */
patch-module my.product.foo {             // Put here the name of the module to patch.
    add-modules TEST-MODULE-PATH;         // Recommended value in the majority of cases.

    add-reads org.junit.jupiter.api,      // Frequently used dependency for tests.
              my.product.test.fixture;    // Put here any other dependency needed for tests.

    add-exports my.product.foo.internal   // Name of a package which is normally not exported.
             to org.junit.jupiter.api,    // Any module that need access to above package for testing.
                my.product.test.fixture;  // Can export to many modules, as a coma-separated list.

    add-exports my.product.foo.mock       // Another package to export. It may be a package defined in the tests.
             to my.product.biz;           // Another module of this project which may want to reuse test classes.
}

Special values

The following values have special meanings:

  • SUBPROJECT-MODULES: all other modules in the current Maven (sub)project.
    • This is Maven-specific, not a standard value recognized by Java tools.
    • Allowed in: add-exports.
  • TEST-MODULE-PATH: all dependencies having a test scope.
    • This is Maven-specific, not a standard value recognized by Java tools.
    • Allowed in: add-modules, add-reads and add-exports.
  • ALL-MODULE-PATH: everything on the module path, regardless if test or main.
    • This is a standard value accepted by the Java compiler.
    • Allowed in: add-modules.
  • ALL-UNNAMED: all non-modular dependencies.
    • This is a standard value accepted by the Java compiler.
    • Allowed in: add-exports.

Default module-info-patch

If no module-info-patch.maven file is present, the default behavior of the Maven compiler plugin is as if a file was present with the following content:

patch-module <module name> {
    add-modules TEST-MODULE-PATH;
    add-reads TEST-MODULE-PATH;
}

If a module-info-patch.maven file is present, then it should contain the above add-modules and add-reads lines if the default behavior is desired. However, while add-modules TEST-MODULE-PATH is usually fine, it is recommended to put values more specific than TEST-MODULE-PATH for the add-reads option. These values can be easily determined by compiling the tests without add-reads option. The compiler error messages are explicit about which reads are missing.

Project-wide options

All options declared in a module-info-patch.maven file apply only to the module declared after the patch-module token, except the --add-modules and --limit-modules options. These two options apply to all modules in a Maven (sub)project. Therefore, it is not necessary to repeat add-modules TEST-MODULE-PATH in all modules: declaring that particular option in only one module of a Maven (sub)project is sufficient.

If the --add-modules or --limit-modules options are declared in many module-info-patch.maven files of a Maven (sub)project, the effective value will be the union of the values declared in each file.

Runtime options

The content of all module-info-patch.maven files of a Maven sub(project) are compiled in a META-INF/maven/module-info-patch.args file in the output directory of the test classes. This file differs from javac-test.args (described in the next section below) in two ways:

  • It contains only the options declared in module-info-patch.maven, not the other options such as --module-path and --patch-module.
  • It contains the dependencies with test-runtime scope and not the dependencies with test-only scope, while javac-test.args contains the opposite.

This file is intended for use with the java launcher in plugins such as Surefire. Note that there is a single module-info-patch.args output file even if a multi-modular project contains many module-info-patch.maven input files.

Troubleshooting

If the compilation failed, the Maven compiler plugin writes the options that it used in a javac.args or javac-test.args file (for compilation of main code and test code respectively) in the target directory. The compilation can be executed on the command-line as below. Note that the paths to source files inside javac.args are relative to the project directory. Therefore, that command must be executed in the same directory as the pom.xml file, or in a directory containing a copy or a branch of that project.

javac @target/javac.args

By default, javac.args is not written if the compilation succeed. Users can force the plugin to write that file in the following ways:

  • Execute mvn with the --verbose option (more exactly: enable logging messages at the debug level).
  • Or provide the <verbose>true</verbose> option in the plugin configuration.

Common errors during compilation of main code

This section contains a few common errors that may occur during the execution of mvn compile, together with their fixes.

"module not found: org.foo"

Ensure that the dependency is declared in the pom.xml with a compile or provided scope. Check that the module name is correct with the following command:

jar --describe-module --file <path/to/the/dependency.jar>

The module name will be one of the first lines. If that line also contains "automatic", then org.foo is a filename-based automodule. In such case, ensure that the <dependency> declaration in the pom.xml file contains <type>modular-jar</type>. If a <dependencyManagement> section exists, then the dependency in that section needs the same <type> declaration.

Common errors during compilation of test code

This section contains a few common errors that may occur during the execution of mvn test-compile, together with their fixes.

"module not found: org.foo"

Ensure that the dependency is declared in the pom.xml with a compile, provided, test or test-only scope. If the Maven (sub)project contains some module-info-patch.maven files, ensure that at least one of them contains an add-modules statement like below (replace my.product.foo by the actual module name):

module-info-patch my.product.foo {
    add-modules TEST-MODULE-PATH;
}

Alternatively, TEST-MODULE-PATH can be replaced by an explicit enumeration of the missing modules. It is not necessary to repeat that declaration in all files of a Maven (sub)project, as the effective value of add-modules is the union of the values found in all these files. If the Maven (sub)project has no module-info-patch.maven file, then there is no need to add one since the above declaration is the default.

If the error continues to happen, then if a module-info-patch.maven file contains an add-reads option, check that the option value contains the module. If this is the case, see "module not found: org.foo" in the "Common errors during compilation of main code" section.

"package org.foo.bar is not visible"

Check the next line, which should look like "package org.foo.bar is declared in module org.foo, which does not export it". Add a module-info-patch.maven file in the test source files of that org.foo module, if not already present. Then add an add-exports statement like below in that file, where org.biz can usually be inferred from the path to the file where the error occurred:

module-info-patch org.foo {
    add-exports org.foo.bar to org.biz;
}