Maven Enforcer
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Apache Maven 3.10.0 and the Apache Maven Enforcer Plugin 3.6.3 (documentation published
2026-05-15). Every example was run on JDK 26 on 2026-10-16, in scratch copies of the tidepool project (a parent POM and
the tidepool-model, tidepool-engine and tidepool-cli modules, all plugin versions pinned in pluginManagement,
maven.compiler.release 21). Where behaviour differs, the same build was also run on Maven 3.9.16 and on Maven
4.0.0-rc-7 (a release candidate, not a final release). Console output was filtered with grep or sed where that is
said, and the lines Loaded N auto-discovered prefixes and elapsed times were left out.
A Maven build can do almost anything, and that is the problem: a project that only compiles with Java 21, a dependency tree
where two libraries need two versions of a third, a commons-logging that crept in through a transitive dependency, a plugin
whose version nobody pinned. None of these breaks the build on your machine today. The Enforcer plugin turns such assumptions
into checks that fail the build early, with a message that says what to fix. It is the usual way to apply the practices in
Troubleshooting & Best Practices
automatically, and it is the cheapest way to keep the dependency practices of
Dependency Analysis & Convergence
from rotting.
How the plugin is used
The plugin has one goal, enforcer:enforce, which runs a list of rules against the current project and the current Maven
runtime. A rule passes silently or throws an exception with a message, and the goal turns the exceptions into a build failure.
The goal’s default phase is validate, the first phase of the
build lifecycle, so the checks run before anything is
compiled. How a plugin is declared, pinned and bound is the subject of
Plugins & Their Configuration; here is
the tidepool parent POM with the plugin pinned in pluginManagement and a first execution in build/plugins:
<build>
<pluginManagement>
<plugins>
<plugin><artifactId>maven-enforcer-plugin</artifactId><version>3.6.3</version></plugin>
<!-- the other plugins, pinned the same way -->
</plugins>
</pluginManagement>
<plugins>
<plugin>
<artifactId>maven-enforcer-plugin</artifactId>
<executions>
<execution>
<id>enforce-environment</id>
<goals><goal>enforce</goal></goals>
<configuration>
<rules>
<requireMavenVersion>
<version>[3.9.0,)</version>
<message>Tidepool needs Maven 3.9.0 or newer.</message>
</requireMavenVersion>
<requireJavaVersion>
<version>[21,)</version>
</requireJavaVersion>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
Each child of <rules> is the name of a rule, and its own children are the rule’s parameters. The sample output of
mvn validate on Maven 3.10.0 (filtered to the lines of the first module):
$ mvn -B validate | grep -E "enforcer:|Rule"
[INFO] --- enforcer:3.6.3:enforce (enforce-environment) @ tidepool-parent ---
[INFO] Rule 0: org.apache.maven.enforcer.rules.version.RequireMavenVersion passed
[INFO] Rule 1: org.apache.maven.enforcer.rules.version.RequireJavaVersion passed
[INFO] --- enforcer:3.6.3:enforce (enforce-environment) @ tidepool-model ---
[INFO] --- enforcer:3.6.3:enforce (enforce-environment) @ tidepool-engine ---
[INFO] --- enforcer:3.6.3:enforce (enforce-environment) @ tidepool-cli ---
Source: The enforcer:enforce mojo
Two details are visible here. The rules are numbered in the order of the POM and named by their implementing class, which is
what to look for in an error. And the rules of the three child modules are not listed: a rule can declare that its result is
the same in every module (the version rules do, because the Maven and Java runtime does not change inside one build), and
the plugin then runs it once per build. The rules that look at the project, such as the dependency rules below, run again in each module. The parameter
ignoreCache (user property enforcer.ignoreCache) switches that off; with -Denforcer.ignoreCache=true the
RequireMavenVersion line appears under every module (run here: it did, for tidepool-parent, tidepool-model and
tidepool-engine).
The parameters of enforcer:enforce
The plugin’s parameters, from its goal documentation:
| Parameter | User property | Meaning |
|---|---|---|
|
none |
The rules to run, as XML: each first-level element is a rule name, its children the rule’s parameters. |
|
|
Skip all checks. Default |
|
|
Fail the build when a rule fails. Default |
|
|
Stop at the first rule that fails instead of reporting all of them. Default |
|
|
Fail when no rule is configured (since 3.2.0). Default |
|
|
Do not reuse the results of rules that say they can be cached. Default |
|
|
Run only the rules with these names (since 3.2.0). |
|
|
Do not run the rules with these names (since 3.2.0). |
When a rule fails
A failing rule stops the build in the module where it fails. To see one, the Java requirement of the parent POM was
temporarily changed to [27,), a JDK newer than the one running:
$ mvn -B validate -pl tidepool-model | grep -E "ERROR"
[ERROR] BUILD FAILURE
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-enforcer-plugin:3.6.3:enforce (enforce-environment) on project tidepool-model:
[ERROR] Rule 1: org.apache.maven.enforcer.rules.version.RequireJavaVersion failed with message:
[ERROR] Detected JDK /Library/Java/JavaVirtualMachines/jdk-26.jdk/Contents/Home is version 26 which is not in the allowed range [27,).
[ERROR] -> [Help 1]
...
(... marks the usual Maven help lines that were cut.) Without message, a rule prints its own explanation, as above; with
message, the text you give is shown instead (see the Tidepool needs Maven ... text in the extension section below). Three switches change what a failure does:
-
levelis a parameter every rule accepts (see below). A rule with<level>WARN</level>logs a warning and the build goes on. -
-Denforcer.fail=falsemakes every failure a warning. Run here with the Maven requirement unmet, the output had[WARNING] Rule 0: ...RequireMavenVersion failed with message:andBUILD SUCCESS. -
-Denforcer.skipskips the whole goal: the log still shows the--- enforcer:3.6.3:enforce (...)header and then noRulelines (run here,BUILD SUCCESS).
By default all failing rules of an execution are reported together, which is useful when you are fixing a list of problems. With
failFast the execution stops at the first failing rule (see the dependency example below).
Source: The enforcer:enforce mojo
The common parameters of every rule
Every rule accepts level, which is ERROR (the default) or WARN, and ruleName, an optional name for the rule
configuration. Individual rules add their own, such as message. The tidepool parent can warn about a convergence problem
without blocking while the team fixes it:
<dependencyConvergence>
<level>WARN</level>
</dependencyConvergence>
and the output, in a module with the conflict of the next section (filtered with sed to the rule, the other rules of the
execution passed or failed as usual):
[WARNING] Rule 0: org.apache.maven.enforcer.rules.dependency.DependencyConvergence warned with message:
Failed while enforcing releasability.
Dependency convergence error for commons-codec:commons-codec:jar:1.11. Paths to dependency are:
+-io.tidepool:tidepool-engine:jar:1.0.0-SNAPSHOT
+-org.apache.httpcomponents:httpclient:jar:4.5.14:compile
+-commons-codec:commons-codec:jar:1.11:compile
and
+-io.tidepool:tidepool-engine:jar:1.0.0-SNAPSHOT
+-org.apache.commons:commons-csv:jar:1.14.1:compile
+-commons-codec:commons-codec:jar:1.19.0:compile
Rules about the environment
requireMavenVersion and requireJavaVersion take a version in the Enforcer’s range syntax, the standard Maven range
syntax with one change: a bare 3.9.0 means at least 3.9.0 (in a dependency it would only recommend a version). So
[3.9.0,) and 3.9.0 mean the same, [17,21] is an inclusive range and (,1.0],[1.2,) is a union of ranges. The Java version
is normalised before the comparison (1.5.0_07-b03 becomes 1.5.0-7), and 8 can be written for 1.8. requireJavaVersion
accepts display to print the normalised value. The message in the failure above, version 26 which is not in the allowed
range [27,), shows the JDK home and the version it found, which is the quickest way to find out which JDK Maven is really using
(see Compiling & Toolchains for how that relates
to release and toolchains).
The requirement [3.9.0,) is a minimum, not an exact version; to pin the exact Maven that a team uses, see
Maven Wrapper. The other environment rules are
requireJavaVendor, requireOS (family, name, architecture, version), requireEnvironmentVariable and requireActiveProfile.
They follow the same pattern and were not run here.
Source: requireJavaVersion
Rules about dependencies
These are the rules that pay for the plugin. They look at the resolved dependency tree of each module (the mediation rules are in Dependencies, Scopes & Mediation). The second execution of the tidepool parent:
<execution>
<id>enforce-dependencies</id>
<goals><goal>enforce</goal></goals>
<configuration>
<rules>
<dependencyConvergence/>
<requireUpperBoundDeps/>
<bannedDependencies>
<excludes>
<exclude>commons-logging:commons-logging</exclude>
<exclude>log4j:log4j</exclude>
</excludes>
<message>Route logging through SLF4J (use jcl-over-slf4j instead of commons-logging).</message>
</bannedDependencies>
<banDuplicatePomDependencyVersions/>
<requireReleaseDeps>
<onlyWhenRelease>true</onlyWhenRelease>
<message>A release must not depend on SNAPSHOT artifacts.</message>
</requireReleaseDeps>
</rules>
</configuration>
</execution>
Source: Enforcer built-in rules
For the unmodified tidepool modules every rule passes. To see them work, tidepool-engine gets a dependency on
httpclient 4.5.14 (declared before commons-csv), which brings its own, older commons-codec and a commons-logging:
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.14</version>
</dependency>
This added dependency is this page’s own lab setup, not an example from the Maven documentation.
$ mvn -B validate -pl tidepool-engine -am | sed -n '/^\[ERROR\] Failed to execute/,/^\[ERROR\] -> \[Help/p'
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-enforcer-plugin:3.6.3:enforce (enforce-dependencies) on project tidepool-engine:
[ERROR] Rule 0: org.apache.maven.enforcer.rules.dependency.DependencyConvergence failed with message:
[ERROR] Failed while enforcing releasability.
[ERROR]
[ERROR] Dependency convergence error for commons-codec:commons-codec:jar:1.11. Paths to dependency are:
[ERROR] +-io.tidepool:tidepool-engine:jar:1.0.0-SNAPSHOT
[ERROR] +-org.apache.httpcomponents:httpclient:jar:4.5.14:compile
[ERROR] +-commons-codec:commons-codec:jar:1.11:compile
[ERROR] and
[ERROR] +-io.tidepool:tidepool-engine:jar:1.0.0-SNAPSHOT
[ERROR] +-org.apache.commons:commons-csv:jar:1.14.1:compile
[ERROR] +-commons-codec:commons-codec:jar:1.19.0:compile
[ERROR]
[ERROR] Rule 1: org.apache.maven.enforcer.rules.dependency.RequireUpperBoundDeps failed with message:
[ERROR] Failed while enforcing RequireUpperBoundDeps. The error(s) are [
[ERROR] Require upper bound dependencies error for commons-codec:commons-codec:1.11. Paths to dependency are:
[ERROR] +-io.tidepool:tidepool-engine:1.0.0-SNAPSHOT
[ERROR] +-org.apache.httpcomponents:httpclient:4.5.14
[ERROR] +-commons-codec:commons-codec:1.11
[ERROR] and
[ERROR] +-io.tidepool:tidepool-engine:1.0.0-SNAPSHOT
[ERROR] +-org.apache.commons:commons-csv:1.14.1
[ERROR] +-commons-codec:commons-codec:1.19.0
[ERROR] ]
[ERROR] Rule 2: org.apache.maven.enforcer.rules.dependency.BannedDependencies failed with message:
[ERROR] Route logging through SLF4J (use jcl-over-slf4j instead of commons-logging).
[ERROR] io.tidepool:tidepool-engine:jar:1.0.0-SNAPSHOT
[ERROR] org.apache.httpcomponents:httpclient:jar:4.5.14
[ERROR] commons-logging:commons-logging:jar:1.2 <--- banned via the exclude/include list
[ERROR] -> [Help 1]
Three rules failed in one run, and each shows the paths to the problem, which dependency:tree would only show after you knew
where to look.
-
dependencyConvergencerequires that every artifact in the tree is reached with a single version over all paths. Herecommons-codecis 1.11 throughhttpclientand 1.19.0 throughcommons-csv, and the nearest-wins mediation would hide that by silently picking one. -
requireUpperBoundDepsis stricter about the direction: the version Maven selects must not be lower than any version found in the tree. Maven picked 1.11 (the first declared at that depth), lower than the 1.19.0commons-csvwas built against, which can break at run time. It ignores thetestandprovidedscopes by default (excludedScopes) and acceptsexcludes. -
bannedDependenciesbans artifacts by patterngroupId[:artifactId[:version[:type[:scope[:classifier]]]]]with wildcards, looks at transitive dependencies by default (searchTransitive), and can carve exceptions withincludes.
Source: dependencyConvergence
Source: requireUpperBoundDeps
Source: bannedDependencies
The fix is in the POMs, not in the rule: manage the version of the conflicting artifact so that every path gets the same one
(see Dependency Management & BOMs),
exclude the unwanted dependency, and add the replacement. In tidepool-engine:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.19.0</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.14</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>2.0.17</version>
</dependency>
<!-- the other dependencies of tidepool-engine -->
</dependencies>
and all five rules pass again (run here, BUILD SUCCESS). failFast changes the report: with -Denforcer.failFast=true
on the broken POM, and the convergence rule set to WARN as in the previous section, the log showed Rule 0 ... warned and
Rule 1 ... RequireUpperBoundDeps failed, and no Rule 2.
The other dependency rules
-
banDuplicatePomDependencyVersionsfails when the same dependency is declared twice in one POM. On Maven 3.10.0 and 4.0.0-rc-7 the rule never gets a chance: the POM itself is invalid (the model error is'dependencies.dependency.(groupId:artifactId:type:classifier)' must be unique: info.picocli:picocli:jar -> version (?) vs 4.7.7, and the build stops withThe build could not read 1 project). On Maven 3.9.16 the model builder only warns, and the rule is what fails the build (Found 1 duplicate dependency declaration in this project: - dependencies.dependency[info.picocli:picocli:jar] (2 times)). Run on all three with a secondpicoclideclaration intidepool-cli. -
requireReleaseDepsfails if any dependency, transitive ones included by default (searchTransitive), and also the parent (failWhenParentIsSnapshot, defaulttrue) is a SNAPSHOT. Since the tidepool modules are themselves snapshots, the parent POM usesonlyWhenRelease, which runs the rule only when the project version is not a SNAPSHOT. Without it the build failed immediately withParent Cannot be a snapshot: io.tidepool:tidepool-parent:pom:1.0.0-SNAPSHOT, and withfailWhenParentIsSnapshotset tofalseit listedio.tidepool:tidepool-model:jar:1.0.0-SNAPSHOT <--- is not a release dependency. A release whose build still depends on a snapshot is not reproducible, see Releasing. -
banDynamicVersionsbans version ranges,LATEST,RELEASEand SNAPSHOT versions (each can be allowed withallowRanges,allowLatest,allowRelease,allowSnapshots) and takesignores. It sits in the next execution of the tidepool parent with<ignore>io.tidepool</ignore>, because the modules depend on each other’s snapshots. With thecommons-csvversion changed to[1.14,1.15)the failure wasDependency org.apache.commons:commons-csv:jar:1.14.1 (compile) is referenced with a banned dynamic version [1.14,1.15). See Versions, Snapshots & Ranges for why ranges hurt reproducibility. -
banTransitiveDependencies,requireExplicitDependencyScope(every dependency declares itsscopein its own POM) andbanDependencyManagementScopecomplete the family. They were not run here.
Source: dependencyConvergence
Source: requireUpperBoundDeps
Source: bannedDependencies
Source: requireReleaseDeps
Source: banDynamicVersions
Rules about the build itself
The last execution of the tidepool parent checks the build definition:
<execution>
<id>enforce-hygiene</id>
<goals><goal>enforce</goal></goals>
<configuration>
<rules>
<requirePluginVersions/>
<banDynamicVersions>
<ignores>
<ignore>io.tidepool</ignore>
</ignores>
</banDynamicVersions>
<requireProperty>
<property>tidepool.channel</property>
<message>Set tidepool.channel in the parent POM.</message>
<regex>stable|beta</regex>
<regexMessage>tidepool.channel must be stable or beta.</regexMessage>
</requireProperty>
<bannedRepositories>
<bannedRepositories>
<bannedRepository>http://*</bannedRepository>
</bannedRepositories>
<bannedPluginRepositories>
<bannedPluginRepository>http://*</bannedPluginRepository>
</bannedPluginRepositories>
</bannedRepositories>
</rules>
</configuration>
</execution>
-
requirePluginVersionsfails for a plugin without a version, or withLATEST,RELEASEor a SNAPSHOT version. It also bans (banMavenDefaults, defaulttrue) versions that come only from the running Maven’s default lifecycle bindings, which is the point of the rule: the build then changes when you upgrade Maven. With the pin ofmaven-resources-plugindeleted from the parent,mvn package -DskipTests -pl tidepool-modelfailed on each of the three Maven versions, and the message shows what each of them would have used:Maven Message (the Rule 0text, abbreviated)3.9.16
... depend on Maven 3.9.16 defaults ... org.apache.maven.plugins:maven-resources-plugin. The version currently in use is 3.4.0 via default lifecycle bindings3.10.0
... depend on Maven 3.10.0 defaults ... maven-resources-plugin. The version currently in use is 3.5.0 via default lifecycle bindings4.0.0-rc-7
... depend on Maven 4.0.0-rc-7 defaults ... maven-resources-plugin. The version currently in use is 3.5.0 via default lifecycle bindingsIt is the quickest way to find the plugins you forgot to pin before a Maven upgrade changes them. The
phasesparameter (defaultclean,deploy,site) says which lifecycle phases to inspect for bound plugins, andadditionalPluginsandunCheckedPluginListextend and relax the check. -
requirePropertydemands that a property exists and optionally matches aregex. Withtidepool.channelset tonightlythe failure wastidepool.channel must be stable or beta., and with the property removedSet tidepool.channel in the parent POM.(run on Maven 3.10.0). A property that Maven itself defines is always present: removingproject.build.sourceEncodingdid not fail a rule on it, because since 3.10 Maven defaults it to UTF-8. -
bannedRepositoriesand its siblingrequireNoRepositorieskeep repository declarations (and the supply-chain risk they carry, see Repositories, Mirrors & Repository Managers) out of your POMs; herehttp://*is banned.banDistributionManagementandbannedPluginsdo the same for those elements. -
reactorModuleConvergencechecks that the modules of a multi-module build have consistent versions and that every module’s parent is part of the reactor, andrequireSameVersionsthat chosen plugins or dependencies share one version (for examplemaven-surefire-pluginandmaven-failsafe-plugin, see Multi-Module Builds & the Reactor).requireFilesExist,requireFilesDontExist,requireFilesSize,requireFileChecksum,requireTextFileChecksum,requirePrerequisite,requireMatchingCoordinates,requireProfileIdsExist,requireReleaseVersion,requireSnapshotVersion,evaluateBeanshellandexternalRulesare the remaining built-in rules. They were not run here.
Source: requirePluginVersions
Source: requireProperty
Source: Enforcer built-in rules
Beyond the built-in rules, the plugin site lists two third-party rule sets: the Sonatype OSS Index rule to ban vulnerable dependencies, and MojoHaus Extra Enforcer Rules. Neither was run here (the first needs access to an external vulnerability service).
Running one rule from the command line
enforcer:enforce also works without any POM configuration, which is a way to test a rule on a project that does not use the
plugin yet. In an empty project with only coordinates:
$ mvn -B org.apache.maven.plugins:maven-enforcer-plugin:3.6.3:enforce -Denforcer.rules=alwaysPass,alwaysFail | grep -E "enforcer:|Rule|BUILD"
[INFO] --- enforcer:3.6.3:enforce (default-cli) @ cli-check ---
[INFO] Rule 0: org.apache.maven.enforcer.rules.AlwaysPass passed
[ERROR] BUILD FAILURE
[ERROR] Rule 1: org.apache.maven.enforcer.rules.AlwaysFail failed with message:
Without -Denforcer.rules and without configuration the goal refuses to run: No rules are configured. Use the skip flag if you
want to disable execution. (failIfNoRules). When the rule is configured in the POM, the same option selects it, but
read this carefully: if the configuration is inside an <execution>, you must name the execution (mvn
enforcer:enforce@enforce-hygiene), as with any goal bound with an id, because the goal run from the command line is the
default-cli execution, which only sees plugin-level configuration. Run here, mvn enforcer:enforce@enforce-hygiene
-pl tidepool-model ran the four rules of that execution.
A gotcha: -Denforcer.rules=requireJavaVersion on a normal mvn validate applies to every execution bound to the build, including
the ones that do not configure that rule, and on Maven 3.10.0 the enforce-dependencies execution failed with an internal error
(Cannot invoke "org.apache.maven.artifact.versioning.VersionRange.getRecommendedVersion()" because "versionRange" is null)
(apparently because the rule was created without its version). Select rules with enforcer:enforce@id or rulesToSkip (which worked: with
-Denforcer.skipRules=requireJavaVersion,requirePluginVersions the other rules ran and the build succeeded).
Source: Check specific rule via CLI
The Enforcer Extension
The plugin is configured in the POM, so a rule that must hold for a whole organisation has to be inherited through a parent
POM. The Enforcer Extension keeps the same rules outside the POM, in the project’s .mvn directory (see
Extensions and
Configuring Maven). Register the extension in
.mvn/extensions.xml:
<extensions>
<extension>
<groupId>org.apache.maven.extensions</groupId>
<artifactId>maven-enforcer-extension</artifactId>
<version>3.6.3</version>
</extension>
</extensions>
and put the rules into .mvn/enforcer-extension.xml, whose content is like an <execution> of the plugin (the default id
is default-extension and the default phase validate):
<extension>
<executions>
<execution>
<id>enforce-maven</id>
<configuration>
<rules>
<requireMavenVersion>
<version>[3.11.0,)</version>
<message>Tidepool needs Maven 3.11.0 or newer (this example is meant to fail).</message>
</requireMavenVersion>
</rules>
</configuration>
</execution>
</executions>
</extension>
The version requirement was deliberately set higher than the Maven used, with the enforcer section removed from the parent POM, to
see the extension work:
$ mvn -B validate | sed -n '/enforcer:/,$p' | grep -E "enforcer:|ERROR\] (Rule|Tidepool)|BUILD"
[INFO] --- enforcer:3.6.3:enforce (enforce-maven) @ tidepool-parent ---
[ERROR] BUILD FAILURE
[ERROR] Rule 0: org.apache.maven.enforcer.rules.version.RequireMavenVersion failed with message:
[ERROR] Tidepool needs Maven 3.11.0 or newer (this example is meant to fail).
That is Maven 3.10.0 and also Maven 3.9.16 (the run printed the same failure, preceded by messages about the extension jar being
verified in the local repository). On Maven 4.0.0-rc-7 the build passed, as it should: 4.0.0 satisfies [3.11.0,), and the
extension added the enforcer:3.6.3:enforce (enforce-maven) execution to all four projects. The extension adds the execution to every
project, so no module needs an enforcer section at all, and -Denforcer.skip skips it (run here: the header was printed and no
Rule line). The extension’s documentation also names a third way to register it, in the POM, and says that the plugin makes
more sense in that case.
Source: Enforcer extension: usage
Writing a custom rule
When no built-in rule fits, write one. A rule is a class in a normal JAR project that extends AbstractEnforcerRule (available
since API 3.2.1), carries a @Named("ruleName") annotation whose name starts with a lowercase letter, and implements
execute(): return when the check passes, throw EnforcerRuleException with a helpful message when it fails. Fields (or
setters) become configuration parameters, and Maven components such as MavenProject or MavenSession are injected with
@Inject. The enforcer plugin shares its class loader with the rule, so enforcer-api and Maven core are provided scope, and a
sisu-maven-plugin main-index execution generates the index that lets Maven find the @Named class. The tidepool project’s rule
checks the stations file that the StationReader reads:
@Named("requireStationsCsv")
public class RequireStationsCsv extends AbstractEnforcerRule {
private final MavenProject project;
/** Path relative to the project base directory. */
private String file = "data/stations.csv";
/** Minimum number of data rows. */
private int minStations = 1;
@Inject
public RequireStationsCsv(MavenProject project) {
this.project = project;
}
@Override
public void execute() throws EnforcerRuleException {
Path csv = project.getBasedir().toPath().resolve(file);
getLog().debug("Checking " + csv);
try {
List<String> lines = Files.readAllLines(csv);
if (lines.isEmpty() || !lines.get(0).startsWith("id,")) {
throw new EnforcerRuleException(file + " must start with a header line beginning with 'id,'.");
}
int rows = lines.size() - 1;
if (rows < minStations) {
throw new EnforcerRuleException(file + " has " + rows + " station(s), at least " + minStations + " required.");
}
} catch (IOException e) {
throw new EnforcerRuleException("Cannot read " + csv + ": " + e.getMessage(), e);
}
}
@Override
public String getCacheId() {
// The result depends on the module directory, so do not share it between modules.
return project.getBasedir() + "|" + file + "|" + minStations;
}
}
(The imports are java.io.IOException, java.nio.file.Files, java.nio.file.Path, java.util.List, javax.inject.Inject,
javax.inject.Named, the two enforcer API classes org.apache.maven.enforcer.rule.api.AbstractEnforcerRule and
EnforcerRuleException, and org.apache.maven.project.MavenProject.) Its POM, a JAR project with release 17, declares
enforcer-api 3.6.3, maven-core 3.9.15 and javax.inject 1 as provided dependencies and the plugin org.eclipse.sisu:sisu-maven-plugin:1.0.0
with the goal main-index. After mvn install the JAR contained META-INF/sisu/javax.inject.Named with the single line
io.tidepool.rules.RequireStationsCsv.
The rule is used by adding the JAR as a dependency of the enforcer plugin and writing the rule’s name in <rules>, in
tidepool-engine/pom.xml:
<plugin>
<artifactId>maven-enforcer-plugin</artifactId>
<dependencies>
<dependency>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-enforcer-rules</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
<executions>
<execution>
<id>check-stations</id>
<goals><goal>enforce</goal></goals>
<configuration>
<rules>
<requireStationsCsv>
<file>../data/stations.csv</file>
<minStations>5</minStations>
</requireStationsCsv>
</rules>
</configuration>
</execution>
</executions>
</plugin>
$ mvn -B validate -pl tidepool-engine -am | grep -E "check-stations|ERROR\] (Rule|\.\.)|BUILD"
[INFO] --- enforcer:3.6.3:enforce (check-stations) @ tidepool-engine ---
[ERROR] BUILD FAILURE
[ERROR] Rule 0: io.tidepool.rules.RequireStationsCsv failed with message:
[ERROR] ../data/stations.csv has 3 station(s), at least 5 required.
The same failure was printed on Maven 3.10.0, 3.9.16 and 4.0.0-rc-7; with minStations 3 the rule passed (Rule 0:
io.tidepool.rules.RequireStationsCsv passed). getCacheId() is there on purpose. According to the documentation a rule that is not cacheable need not override
it, and a cacheable rule returns an id that is unique for the parameters and conditions that change its result; results are
stored per id. A rule that reads the project must therefore include the module in its id, as above. The rule artifact has to be resolvable when the
build runs: here it was installed into the local repository of the scratch build; in a real organisation it is deployed
to the company’s repository (see
Install, Deploy & Distribution Management).
The classes of enforcer-rules are not API and a custom rule must not use them. To break the build immediately instead of
collecting failures, execute() can throw EnforcerRuleError.
Source: Writing a custom rule
Changes between Maven versions
| Version | What changes for the Enforcer |
|---|---|
3.9.16 |
|
3.10.0 |
Duplicate dependencies are rejected when the POM is read (stricter POM validation), before any rule runs. The default
plugin versions are those of 3.10.0 ( |
4.0.0-rc-7 (release candidate) |
Plugin 3.6.3, the extension and the custom rule ran unchanged. Duplicate dependencies are rejected when the POM is read, as in
3.10.0 (the |
Where older material is outdated
Material written for Maven 2 predates most of what is on this page. What still circulates with it, and is outdated:
-
Plugin versions were left out of POMs;
requirePluginVersionsexists to forbid that, and what Maven supplies as a default changes with every Maven release (3.4.0 in 3.9.16, 3.5.0 in 3.10.0 for the resources plugin). -
Duplicate dependency declarations were tolerated with a warning; Maven 3.10 and 4 now refuse them.
-
A Java version written as
1.8can be written8; the version strings of the JDK are normalised before the comparison.
Equivalents in Ant and Gradle
| Maven Enforcer | Ant | Gradle |
|---|---|---|
|
A |
The wrapper pins the Gradle version, and a toolchain requirement pins the JDK; there is no single "rules" plugin. |
|
Ivy conflict managers and |
|
|
An Ivy exclude. |
|
|
Not applicable: Ant has no plugins. |
Pinned plugin versions in the |
Custom rule |
A custom task or |
A task or plugin in |
Source: Ant conditions: antversion
The cross-tool picture is in Comparing Ant, Maven and Gradle.
Summary
-
enforcer:enforce(default phasevalidate) runs rules and fails the build with a message;level,fail,failFast,skip,rulesToExecuteandrulesToSkipdecide what a failure does. -
Pin the plugin in
pluginManagementand group rules into executions by purpose: environment, dependencies and build hygiene. -
The rules worth their cost:
requireMavenVersionandrequireJavaVersion,dependencyConvergence,requireUpperBoundDeps,bannedDependencies,requireReleaseDeps(withonlyWhenRelease),banDynamicVersionsandrequirePluginVersions. -
A conflict is fixed in the POMs (manage a version, exclude, replace), not by loosening the rule.
-
The rules can live outside the POM, in
.mvn/enforcer-extension.xml, through the extension; a custom rule is a@Namedclass extendingAbstractEnforcerRuleand is added as a dependency of the plugin. -
Use
enforcer:enforce@id, not-Denforcer.rules, to run one execution from the command line.