Maven Resources & Filtering
|
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 with the Resources Plugin 3.5.0. Every example was run on JDK 26 on 2026-10-16, in a scratch copy of
the tidepool project reduced to one module (tidepool-app, with every build plugin pinned in pluginManagement). The runs that could differ between
releases were repeated on Maven 3.9.16 and on Maven 4.0.0-rc-7 (a release candidate, not a final release); the result was the same unless the text says
otherwise. Paths in the console output were shortened to /work/..., my home directory to /home/dev and my user name to dev; long Maven output was filtered
down to the lines of the plugins involved, and BUILD SUCCESS lines were kept where they matter. Anything that was not run is labelled "not run here".
A resource is a file that is not compiled but is needed at run time: a properties file, an SQL script, a template, a certificate, an image. Maven copies
resources into target/classes (and test resources into target/test-classes) so that they end up on the class path and inside the JAR. Filtering is an
optional step of that copy that replaces placeholders such as ${project.version} in the files with real values. This page explains both, and what goes
wrong. For the surrounding pieces see
Build Lifecycle,
Plugins & Their Configuration,
POM Fundamentals (properties) and
Profiles.
How resources are processed
The Resources Plugin (maven-resources-plugin) has three goals that matter, and two of them are bound to the default lifecycle of a jar project:
| Goal | Default phase | What it does |
|---|---|---|
|
|
Copies the |
|
|
Copies the |
|
none |
Copies any resources to any directory. It has no default phase: you bind it yourself, or run it from the command line. |
The phases come right after generate-sources and process-sources in the lifecycle (see
Build Lifecycle), and process-resources runs before compile. So a
class that reads a resource at compile time through an annotation processor sees the copied files, and a source generator bound to
generate-sources can produce sources that the compiler picks up (see Generated sources).
The default resource directories are src/main/resources and src/test/resources, and the default is no filtering. Start with the smallest possible
case: the project below has one plain resource and one that will be filtered, kept in two separate directories (the plugin documentation recommends
this split, and the reason is in Binary files).
tidepool-app/
|-- pom.xml
`-- src/main
|-- resources/static/readme.txt
`-- resources-filtered/application.properties
<properties>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<station.default>SF</station.default>
</properties>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
</resource>
<resource>
<directory>src/main/resources-filtered</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
Declaring <resources> in the POM replaces the default list (it does not add to it), so the first entry is needed to keep src/main/resources.
The source files:
app.name=${project.name}
app.artifact=${project.artifactId}
app.version=${project.version}
app.station=${station.default}
app.java.release=${maven.compiler.release}
app.built=${maven.build.timestamp}
app.unknown=${no.such.property}
app.at-style=@project.groupId@:@project.artifactId@
readme.txt contains the single line Plain file: version is ${project.version}. Run the phase:
mvn process-resources
[INFO] --- resources:3.5.0:resources (default-resources) @ tidepool-app ---
[INFO] Copying 1 resource from src/main/resources to target/classes
[INFO] Copying 1 resource from src/main/resources-filtered to target/classes
[INFO] The encoding used to copy filtered properties files has not been set. This means that the same encoding will be used to copy filtered properties files as when copying other filtered resources. This might not be what you want! Run your build with --debug to see which files might be affected. Read more at https://maven.apache.org/plugins/maven-resources-plugin/examples/filtering-properties-files.html
[INFO] BUILD SUCCESS
The long INFO line is the plugin asking you to decide how properties files are encoded; Encoding shows the setting. Only the lines of the
plugin were kept above. The result in target/classes:
$ cat target/classes/application.properties
app.name=Tidepool App
app.artifact=tidepool-app
app.version=1.4.0-SNAPSHOT
app.station=SF
app.java.release=21
app.built=2026-10-16T13:06:17Z
app.unknown=${no.such.property}
app.at-style=io.tidepool:tidepool-app
$ cat target/classes/static/readme.txt
Plain file: version is ${project.version}
Three things to notice: the unfiltered file kept its placeholder; a placeholder that Maven cannot resolve (app.unknown) is left as it is without any
warning, so a misspelt name silently survives into the JAR; and @...@ works as well as ${...} with the default settings.
Source: Resources Plugin: Filtering
Choosing which files are copied
A <resource> has these elements:
| Element | Meaning |
|---|---|
|
The directory to copy from, relative to the POM. Required. |
|
The sub-directory of |
|
|
|
Ant-style patterns, relative to |
The next example uses four directories: one with an excluded test script, one filtered, one with a <targetPath> and an <includes> list, and the test
resources (which have their own <testResources> list and are copied by resources:testResources in the process-test-resources phase).
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<excludes>
<exclude>**/*-test.sql</exclude>
</excludes>
</resource>
<resource>
<directory>src/main/resources-filtered</directory>
<filtering>true</filtering>
</resource>
<resource>
<directory>src/main/docs</directory>
<targetPath>META-INF/docs</targetPath>
<filtering>true</filtering>
<includes>
<include>**/*.txt</include>
</includes>
</resource>
</resources>
<testResources>
<testResource>
<directory>src/test/resources</directory>
</testResource>
<testResource>
<directory>src/test/resources-filtered</directory>
<filtering>true</filtering>
</testResource>
</testResources>
src/main/resources held db/schema.sql, db/schema-test.sql, a backup file db/schema.sql~, a .DS_Store file, a .gitignore file and the
static/readme.txt file; src/main/docs held guide.md and a .txt file; src/test/resources and src/test/resources-filtered held one properties
file each. Running mvn process-test-resources copies the main resources first, because process-resources is an earlier phase:
[INFO] --- resources:3.5.0:resources (default-resources) @ tidepool-app ---
[INFO] Copying 3 resources from src/main/resources to target/classes
[INFO] Copying 0 resource from src/main/resources-filtered to target/classes
[INFO] Copying 1 resource from src/main/docs to target/classes/META-INF/docs
[INFO] --- compiler:3.16.0:compile (default-compile) @ tidepool-app ---
[INFO] --- resources:3.5.0:testResources (default-testResources) @ tidepool-app ---
[INFO] Copying 1 resource from src/test/resources to target/test-classes
[INFO] Copying 1 resource from src/test/resources-filtered to target/test-classes
$ find target/classes target/test-classes -type f | sort
target/classes/.gitignore
target/classes/META-INF/docs/tidepool-app.txt
target/classes/db/schema.sql
target/classes/static/readme.txt
target/test-classes/plain.properties
target/test-classes/test.properties
What this shows:
-
The
**/*-test.sqlexclude removedschema-test.sql, andguide.mdwas left out because only**/*.txtwas included. -
Default excludes: the editor backup
schema.sql~and.DS_Storewere not copied, as with other files such as**/.git/**,**/.svn/**and**/#*#. TheaddDefaultExcludesparameter (since 3.0.0, defaulttrue) switches that off. The parameter text mentions.gitignore, but in this run.gitignorewas copied. Do not rely on the default excludes to keep it out of a JAR; exclude it explicitly. -
src/test/resourceswas not filtered (plain.propertieskept its placeholder) andsrc/test/resources-filteredwas. -
The directory with no copyable files printed
Copying 0 resourceand nothing was created. Empty directories are not copied unlessincludeEmptyDirsistrue(defaultfalse). -
The name of a file inside
src/main/docswas${project.artifactId}.txt. It was copied astidepool-app.txtbecause the plugin configuration had<fileNameFiltering>true</fileNameFiltering>(since 3.0.0, defaultfalse), which filters the names of files and directories as well as their contents.
Everything above is the same on Maven 3.9.16 and 4.0.0-rc-7: the resulting files had identical checksums.
Filtering: values and their precedence
Filtering replaces ${name} and @name@ (with name a property) by its value. The value is looked up in this order of precedence, which I
established by defining the same name in several places and reading the result:
-
-Doptions on the command line (and Java system properties in general); -
<properties>of the POM (and of its parents and profiles); -
the filter files (see below);
-
the model of the project itself, such as
${project.version},${project.build.directory}and${basedir}.
In addition the environment variables are available with the prefix env. (${env.TIDEPOOL_HOME}), the JVM system properties directly
(${java.version}, ${user.home}), and settings values with settings. (${settings.localRepository} gave /home/dev/.m2/repository, or the
path given with -Dmaven.repo.local). A bare ${HOME} does not resolve (environment variables need env.),
which is also why shell scripts containing ${HOME} survive filtering (see Delimiters and escapes).
A filter file is a Java properties file listed in the <filters> of the build. The next example defines station.default in the POM and in the filter
file, db.url only in the filter file, and project.version in the filter file as well:
station.default=BOS
db.url=jdbc:h2:mem:tidepool
project.version=9.9.9
<filters>
<filter>filters/dev.properties</filter>
</filters>
The POM still has <station.default>SF</station.default>. The resource is:
station=${station.default}
db=${db.url}
version=${project.version}
java=${java.version}
user.home=${user.home}
tools=${env.TIDEPOOL_HOME}
target=${project.build.directory}
TIDEPOOL_HOME=/opt/tidepool mvn -q resources:resources
station=SF
db=jdbc:h2:mem:tidepool
version=9.9.9
java=26
user.home=/home/dev
tools=/opt/tidepool
target=/work/exp10/target
The POM property (SF) beat the filter file (BOS); the filter file supplied db.url; and, surprisingly, a filter file also beat the project.version
of the model (9.9.9 instead of 1.4.0-SNAPSHOT), so a filter file must not define project.* names unless you mean it. Now the command line:
TIDEPOOL_HOME=/opt/tidepool mvn -q resources:resources -Dstation.default=NYC -Ddb.url=jdbc:x
station=NYC
db=jdbc:x
version=9.9.9
The -D values won over both the POM and the filter file. The same three results were obtained on Maven 3.9.16 and 4.0.0-rc-7.
Source: Resources Plugin: Filtering
One filter file per environment
A filter file name can itself contain a property, so a profile or a -D option can select the file. The POM below defaults env to dev, and a
prod profile changes it:
<properties>
<env>dev</env>
</properties>
<profiles>
<profile>
<id>prod</id>
<properties>
<env>prod</env>
</properties>
</profile>
</profiles>
<build>
<filters>
<filter>filters/${env}.properties</filter>
</filters>
...
</build>
filters/dev.properties and filters/prod.properties define db.url and log.level, and the resource is db.url=${db.url} and
log.level=${log.level}:
$ mvn -q resources:resources
db.url=jdbc:h2:mem:tidepool
log.level=DEBUG
$ mvn -q resources:resources -Pprod
db.url=jdbc:postgresql://db.example.internal:5432/tidepool
log.level=WARN
$ mvn -q resources:resources -Denv=qa
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.5.0:resources (default-cli) on project tidepool-app: Error loading property file '/work/exp9/filters/qa.properties' -> [Help 1]
A missing filter file is an error, not a warning, which is useful: a typo in the environment name fails the build. Keep secrets out of filter files that
are committed; the values belong in settings.xml or in the environment, passed as ${env.NAME} (see
settings.xml & Password Encryption). The useBuildFilters
and filters parameters of the plugin (since 2.4) let one execution use different filter files than the build-wide list. More on profiles is in
Profiles.
Delimiters and escapes
By default the plugin recognises two delimiter styles: ${*} (begin ${, end }) and @ (the same token at both ends). Two parameters change that:
<delimiters> lists delimiters in the form begin*end (or just @ when both are equal) and <useDefaultDelimiters> says whether the default pair stays
in force alongside them (default true). <escapeString> (since 2.3) defines a prefix that protects a placeholder.
The test file below mixes the cases:
version : ${project.version}
at-style : @project.version@
escaped : \${project.version} and \@project.version\@
double : \\${project.version}
mail : support@tidepool.example and admin@tidepool.example
shell : echo ${HOME} ${BASH_VERSION}
Filtered with the defaults, then with <escapeString>\</escapeString>, then with <useDefaultDelimiters>false</useDefaultDelimiters> and a single
@ delimiter:
<configuration>
<escapeString>\</escapeString>
</configuration>
<configuration>
<useDefaultDelimiters>false</useDefaultDelimiters>
<delimiters>
<delimiter>@</delimiter>
</delimiters>
</configuration>
--- defaults
version : 1.4.0-SNAPSHOT
at-style : 1.4.0-SNAPSHOT
escaped : \1.4.0-SNAPSHOT and \@project.version\@
double : \\1.4.0-SNAPSHOT
mail : support@tidepool.example and admin@tidepool.example
shell : echo ${HOME} ${BASH_VERSION}
--- escapeString = backslash
version : 1.4.0-SNAPSHOT
at-style : 1.4.0-SNAPSHOT
escaped : ${project.version} and @project.version@
double : \\1.4.0-SNAPSHOT
mail : support@tidepool.example and admin@tidepool.example
shell : echo ${HOME} ${BASH_VERSION}
--- only @ delimiters
version : ${project.version}
at-style : 1.4.0-SNAPSHOT
escaped : \${project.version} and \@project.version\@
double : \\${project.version}
mail : support@tidepool.example and admin@tidepool.example
shell : echo ${HOME} ${BASH_VERSION}
What follows from it:
-
Without an escape string a backslash does nothing:
\${project.version}becomes a backslash followed by the version. With<escapeString>\</escapeString>the backslash is removed and the placeholder is shown literally. A doubled backslash is not an escape. -
The e-mail addresses were not touched, although two
@appear in the line: text between two@is only replaced when it is the name of a known property. A line such asa@version@bwould be replaced, so be careful in text that mixes@and property-like words. -
Only the
@delimiter makes${...}literal. That is what the Spring Boot starter parent (checked inspring-boot-starter-parent3.5.6) does: it sets the propertyresource.delimiterto@, passes it to the Resources Plugin with<useDefaultDelimiters>false</useDefaultDelimiters>, and filters onlyapplication*.properties,application*.ymlandapplication*.yaml. The point is that Spring’s own${...}placeholders in those files then reach the application intact, and Maven properties are written@project.version@. See Spring Boot: Configuration & Profiles. -
The supported way to keep
${...}in a file that is filtered is therefore either the@-only mode or the escape string. The alternative is to split files: keep the ones with shell or template syntax in a non-filtered directory.
<supportMultiLineFiltering> (since 2.5, default false) controls whether the search for the end token stops at the end of the line, and
<escapeWindowsPaths> (since 2.4, default true) escapes backslashes and colons in Windows-style path values so that they survive a properties file.
Binary files
Never filter binary files. The reader decodes the file to text using the configured encoding, replaces placeholders, and encodes it again, which breaks any byte sequence that is not valid text. The plugin documentation recommends two directories, one for filtered and one for unfiltered files, and that is the structure used in this page.
As a safety net the plugin never filters files with the extensions jpg, jpeg, gif, bmp and png; more can be added with
nonFilteredFileExtensions (since 2.3). The test used a file with PNG-like bytes, invalid UTF-8 bytes and the text build=${project.version}, saved
three times as tide.png, tide.dat and tide.bin in a filtered directory:
python3 - <<'EOF'
data = bytes([0x89,0x50,0x4e,0x47,0x0d,0x0a,0x1a,0x0a,0xff,0xfe,0x00,0x80]) + b"build=${project.version}" + bytes([0xc3,0x28,0xa0,0xa1,0x00,0xff])
for n in ("tide.png", "tide.dat", "tide.bin"):
open("src/main/resources-filtered/assets/" + n, "wb").write(data)
EOF
mvn -q resources:resources
This script is this page’s own test setup, not an example from the Maven documentation.
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.5.0:resources (default-cli) on project tidepool-app: filtering /work/exp2/src/main/resources-filtered/assets/tide.dat to /work/exp2/target/classes/assets/tide.dat failed with MalformedInputException: Input length = 1 -> [Help 1]
MalformedInputException during filtering is the classic sign of a binary file in a filtered directory. The .png was skipped, the .dat was not.
Adding the extensions fixes it:
<configuration>
<nonFilteredFileExtensions>
<nonFilteredFileExtension>dat</nonFilteredFileExtension>
<nonFilteredFileExtension>bin</nonFilteredFileExtension>
</nonFilteredFileExtensions>
</configuration>
$ shasum src/main/resources-filtered/assets/tide.*
cac92bb80d21e93ae3fbfc67beaf1491cebcabbe tide.bin
cac92bb80d21e93ae3fbfc67beaf1491cebcabbe tide.dat
cac92bb80d21e93ae3fbfc67beaf1491cebcabbe tide.png
$ mvn resources:resources
[INFO] Copying 4 resources from src/main/resources-filtered to target/classes
$ shasum target/classes/assets/tide.*
cac92bb80d21e93ae3fbfc67beaf1491cebcabbe tide.bin
cac92bb80d21e93ae3fbfc67beaf1491cebcabbe tide.dat
cac92bb80d21e93ae3fbfc67beaf1491cebcabbe tide.png
The checksums are the same before and after: the files were copied byte for byte. The failure above is the good outcome. With
-Dproject.build.sourceEncoding=ISO-8859-1 (an encoding that can decode any byte) and no nonFilteredFileExtensions, the same files are accepted and
silently damaged:
$ shasum target/classes/assets/tide.*
4f4cdeff39ac2483ab24f9487e4d14e9851840d8 tide.bin
4f4cdeff39ac2483ab24f9487e4d14e9851840d8 tide.dat
cac92bb80d21e93ae3fbfc67beaf1491cebcabbe tide.png
$ ls -l target/classes/assets/tide.*
-rw-r--r-- 1 dev wheel 38 Oct 16 15:02 tide.bin
-rw-r--r-- 1 dev wheel 38 Oct 16 15:02 tide.dat
-rw-r--r-- 1 dev wheel 42 Oct 16 15:02 tide.png
The .dat and .bin copies are 4 bytes shorter (the placeholder was replaced inside the binary data) and have a different checksum; only the .png
survived. The same results were seen on Maven 3.9.16 and 4.0.0-rc-7. A binary file that is corrupted this way (a keystore, a font, a Word document, a ZIP
file) usually fails far from the build, at run time.
Encoding
The encoding parameter of the plugin defaults to ${project.build.sourceEncoding}. Set that property in every POM (the examples on this page set it to
UTF-8). Without it the plugin uses the platform default, which makes the result depend on the machine; since JDK 18 the default is UTF-8, but
MAVEN_OPTS=-Dfile.encoding=ISO-8859-1 was enough to change it in the test. In the test, a filtered file saved in ISO-8859-1 (latin1.properties, with
the byte E9 for the letter e with an acute accent) failed under UTF-8:
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.5.0:resources (default-cli) on project tidepool-app: filtering /work/exp4/src/main/resources-filtered/latin1.properties to /work/exp4/target/classes/latin1.properties failed with MalformedInputException: Input length = 1 -> [Help 1]
Setting -Dproject.build.sourceEncoding=ISO-8859-1 made both latin1.properties and a UTF-8 file utf8.properties pass, and both kept their original
bytes (decoding and encoding with the same single-byte encoding round-trips every byte). It is a way to get a build to pass; it is not a fix.
The information message seen in the first output is about properties files, which have a history of their own with encodings (the
Properties.load method for a byte stream still assumes ISO-8859-1). The propertiesEncoding parameter (since 3.2.0) sets the encoding for filtered *.properties files separately and, when absent, follows
encoding, which is what the message says. With <propertiesEncoding>ISO-8859-1</propertiesEncoding> in the plugin configuration and the project
encoding at UTF-8, the message disappeared and the two test files were copied with their bytes unchanged:
$ xxd target/classes/latin1.properties
00000000: 7469 6465 3d4d 6172 e965 2031 2e34 2e30 tide=Mar.e 1.4.0
00000010: 2d53 4e41 5053 484f 540a -SNAPSHOT.
$ xxd target/classes/utf8.properties
00000000: 7469 6465 3d4d 6172 c3a9 6520 312e 342e tide=Mar..e 1.4.
00000010: 302d 534e 4150 5348 4f54 0a 0-SNAPSHOT.
Choose one encoding for the whole repository, normally UTF-8, convert the odd file once, and set propertiesEncoding to the same value so that the
message goes away. The Spring Boot starter parent does this (<propertiesEncoding>${project.build.sourceEncoding}</propertiesEncoding>).
Build timestamps and reproducible builds
${maven.build.timestamp} is the time at which Maven started the build, in UTC, formatted by the maven.build.timestamp.format property (default
yyyy-MM-dd'T'HH:mm:ss'Z'). It is always "now", even when project.build.outputTimestamp is set (see
Security & Reproducible Builds): in the first example
app.built=2026-10-16T13:06:17Z was the wall-clock time of the run on Maven 3.9.16, 3.10.0 and 4.0.0-rc-7. Using it in a filtered file therefore makes the
output of every build different, and it is not reproducible. The Resources Plugin 3.4.0 and later only rewrite a target file when its content differs
(the changeDetection parameter, since 3.5.0, defaults to content; the older default was timestamp). A file with a build time is rewritten at every run,
and one without is left alone:
$ mvn -q resources:resources # application.properties: version only; stamped.properties: version + build time
15:05:19 target/classes/application.properties
15:05:19 target/classes/stamped.properties
$ sleep 2; mvn -q resources:resources
15:05:19 target/classes/application.properties
15:05:23 target/classes/stamped.properties
For a stable value use the fixed ${project.build.outputTimestamp} property (an ISO-8601 instant that you set in the POM, usually by the release process)
and nothing that reads the clock. A build number or a commit id is a different matter: they come from plugins such as the MojoHaus buildnumber-maven-plugin
(3.3.1 on Central) or the commit id of a Git plugin, which put the value in a property that you then filter; I did not run those plugins here.
Generated sources
Filtering is not limited to properties. A common use is a small Java class that carries the version and the coordinates, produced from a template by
filtering. The template lives in src/main/java-templates (not in src/main/java, so that IDEs do not try to compile the placeholders):
package io.tidepool.app;
/** Generated from a template: the placeholders are filled in at build time. */
public final class BuildInfo {
public static final String GROUP = "${project.groupId}";
public static final String ARTIFACT = "${project.artifactId}";
public static final String VERSION = "${project.version}";
private BuildInfo() {
}
}
Two executions do the work in the generate-sources phase. resources:copy-resources filters the template into target/generated-sources/java-templates,
and the MojoHaus build-helper:add-source goal adds that directory to the compile source roots. Both lines are needed: the Resources Plugin does
not know about source roots, and Build Helper does not filter.
<plugins>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<id>filter-java-templates</id>
<phase>generate-sources</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/generated-sources/java-templates</outputDirectory>
<resources>
<resource>
<directory>src/main/java-templates</directory>
<filtering>true</filtering>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.6.2</version>
<executions>
<execution>
<id>add-java-templates</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>${project.build.directory}/generated-sources/java-templates</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
Main.java prints BuildInfo.GROUP + ":" + BuildInfo.ARTIFACT + ":" + BuildInfo.VERSION.
mvn compile
java -cp target/classes io.tidepool.app.Main
[INFO] --- resources:3.5.0:copy-resources (filter-java-templates) @ tidepool-app ---
[INFO] Copying 1 resource from src/main/java-templates to target/generated-sources/java-templates
[INFO] --- build-helper:3.6.2:add-source (add-java-templates) @ tidepool-app ---
[INFO] Source directory: /work/exp6/target/generated-sources/java-templates added.
[INFO] --- resources:3.5.0:resources (default-resources) @ tidepool-app ---
[INFO] --- compiler:3.16.0:compile (default-compile) @ tidepool-app ---
[INFO] Compiling 2 source files with javac [debug release 21] to target/classes
[INFO] BUILD SUCCESS
io.tidepool:tidepool-app:1.4.0-SNAPSHOT
The executions run in the order in which they appear in the POM because both are in the same phase; the output above is from Maven 3.10.0 and the same lines
(and the same program output) appeared on 3.9.16 and 4.0.0-rc-7. Maven 4.0.0-rc-7 also printed a warning about a missing root directory because the scratch
project had no .mvn directory (see
Maven 4: What’s New).
The generated directory must be a compile source root, which is why add-source is needed; adding it as a <resource> instead would copy the .java
file into the JAR and never compile it. The other common sources of generated code (ANTLR, JAXB, protobuf, OpenAPI) are plugins that add their own source
root, so they do not need Build Helper. The "Guide to generating sources" shows the pattern with the ANTLR 4 plugin: bind a plugin to generate-sources
and the compiler finds what it wrote. The MojoHaus templating-maven-plugin (3.1.1 on Central) is a ready-made version of the template pattern
shown here (a filter-sources goal); it was not run here. Similarly the Apache maven-remote-resources-plugin (3.3.0 on Central) bundles resources
(licence and notice files, for example) from other artifacts into a build; it is not run here either.
Source: Guide to generating sources
Source: MojoHaus Templating Maven Plugin
Source: Resources Plugin: Copy resources
Running, skipping and the stale-output trap
-
mvn resources:resources(andresources:testResources) runs a single goal from the command line without compiling anything; the execution id isdefault-cli. In a lifecycle run it isdefault-resources, so-Dmaven.resources.skip=trueor theskipparameter (since 3.0.0) disables it. The log saysSkipping the execution.and nothing is copied. Do not skip the plugin to save time: Maven already skips files that are unchanged. -
The plugin only adds to
target/classes. If you delete or rename a source resource, the old copy stays intarget/classesand in the JAR until the nextmvn clean. In the test, after removingreadme.txtfromsrc/main/resources,find target/classes -type fstill listedtarget/classes/static/readme.txt. This is the usual cause of "I deleted the file but it is still packaged". -
The standard layout (
src/main/resources) needs no configuration; a<resources>element replaces the default list entirely, so repeat the default directory when you add another.
Maven 4 (release candidate)
On Maven 4.0.0-rc-7 the Resources Plugin 3.5.0 and the model version 4.0.0 behave exactly as above; everything on this page was repeated there. The new
model version 4.1.0 adds a generic <sources> element to <build> that replaces the separate <resources> and source directory settings. A
<source> has a <lang> (java or resources), a <scope> (main or test), a <directory>, <includes>, <excludes>, <targetPath> and the
boolean <stringFiltering> that corresponds to <filtering>. This was run on 4.0.0-rc-7 with a POM in namespace http://maven.apache.org/POM/4.1.0:
<build>
<sources>
<source>
<lang>resources</lang>
</source>
<source>
<lang>resources</lang>
<directory>src/main/resources-filtered</directory>
<stringFiltering>true</stringFiltering>
</source>
</sources>
</build>
[INFO] --- resources:3.5.0:resources (default-cli) @ tidepool-app41 ---
[INFO] Copying 1 resource from src/main/resources to target/classes
[INFO] Copying 1 resource from src/main/resources-filtered to target/classes
$ cat target/classes/a.txt target/classes/b.txt
plain ${project.version}
filtered 1.4.0-SNAPSHOT
Writing <filtering> in a <source> is rejected with Unrecognised tag: '{http://maven.apache.org/POM/4.1.0}filtering'. A POM with model version
4.1.0 cannot be built by Maven 3 (see
Maven 4: POM Model & Consumer POM), and the 4.1.0 model is part of
a release candidate, so keep the 4.0.0 model and <resources> for builds that must also run on Maven 3.
The same ideas in Ant and Gradle
| Concern | Maven | Ant | Gradle |
|---|---|---|---|
copy resources |
|
the |
the |
replace placeholders |
|
a |
|
values and filter files |
|
|
|
binary files |
|
restrict the filterset to the text files in the fileset |
restrict |
Gradle has no default filtering and no default delimiter, so there is no equivalent of the Spring Boot @-only trick. For converting a whole Maven build see
Migrating Between Tools. These are descriptions; no Ant
or Gradle build was run for this page.
Source: Apache Ant: FilterSet
Classic mistakes
-
Filtering a directory that also holds binary files:
MalformedInputExceptionif the bytes are not valid text, silent corruption if they are. -
A misspelt property: the placeholder is left in the output with no warning. Grep
target/classesfor$and@after a build, or add a test that reads the resource. -
A filter file that defines
project.version(or anyproject.*) overrides the model. A POM property overrides a filter file, and a-Doption overrides both. -
${maven.build.timestamp}in a resource makes the build non-reproducible and the file different at every build. -
Using the default delimiters for Spring or shell files that contain
${...}text. Use@only, an escape string, or a non-filtered directory. -
A
<resources>element in a child POM replaces the parent’s list; it is not merged. With a parent declaringsrc/main/resourcesand a child declaringsrc/extra, only the child’s directory was copied in the test. -
Deleting a resource from the source tree without
mvn clean: the stale copy is still packaged. -
Putting generated Java in
src/main/resources: it is copied, not compiled. Use a source root. -
An unset encoding: the result depends on the platform. Set
project.build.sourceEncodingandpropertiesEncoding.
Where the books are outdated
The Maven 2 book that informed these pages describes filtering and the Resources Plugin at the 2.x level. Differences with current releases:
-
"Filtering uses the
${...}delimiters only": since plugin 2.4@...@, custom delimiters anduseDefaultDelimitersexist, and since 2.3 theescapeString. A modern build chooses the delimiters deliberately (Spring Boot does). -
The default of
changeDetection(the oldoverwrite) moved from timestamps to content in 3.4.0; the book’s "touch the file to force a re-copy" advice is not needed any more. Theoverwriteparameter is deprecated. -
The book relies on the plugin version that came with Maven 2 or 3.x; that version comes from the running Maven’s lifecycle bindings (not the super POM) and changes with each Maven release, so pin
maven-resources-plugin(see Plugins & Their Configuration). -
The book’s generated-sources examples use the Codehaus Maven plugins; the groupId
org.codehaus.mojois still used by MojoHaus, but the Codehaus sites are gone.
Where to go next
-
Profiles for selecting filter files and values per environment.
-
Compiling & Toolchains for what happens to the generated sources.
-
Packaging JARs, WARs & Manifests for resources in the JAR and manifest entries.
-
Security & Reproducible Builds for
project.build.outputTimestamp. -
Plugins & Their Configuration for executions and phases.