☰
Maven资源处理实战:build、resources、testResources与过滤配置全解析
2026/10/19 3:40:16 网站建设 项目流程

构建完依赖树,接下来绕不开的就是资源文件。很多项目跑着跑着突然报找不到某个XML配置、某个SQL脚本,或者打包出来的JAR里少了一整个资源目录,十有八九都是pom.xml里的build、resources、testResources这段没写对。这篇就专门把这几个标签从头到尾拆开讲清楚,包括它们各自的默认行为、完整配置项、资源过滤原理,以及我这一路踩过的坑和排查套路。适合刚把Maven生命周期搞明白、开始在真实项目里折腾打包配置的人,也适合配置了resources但一直没搞懂为啥这样写的老手随手翻翻。

1. build标签与资源目录的默认约定

1.1 先理解Maven的“约定优于配置”到底约定了什么

大多数时候你新建一个Maven项目,什么都不配置,直接执行mvn clean package,src/main/resources下的文件会被自动复制到target/classes,src/test/resources下的文件会被自动复制到target/test-classes。你既没写build也没写resources,它照样工作,这就让很多人误以为资源处理是“开箱即用、不用管”的。

其实这里隐藏了一个完整的机制链条。Maven的构建生命周期里有一个阶段叫process-resources,它专门负责处理主资源;还有一个阶段叫process-test-resources,专门处理测试资源。这两个阶段分别绑定在编译之前,也就是说在compile和test-compile之前,资源文件必须先就位。

默认情况下,负责执行这些阶段的是maven-resources-plugin。它遵循一个约定:主资源目录固定是${basedir}/src/main/resources,测试资源目录固定是${basedir}/src/test/resources。只要你不打破这个约定,就感知不到build标签的存在。

但约定只是“默认值”,不是“魔法”。当你开始接触多模块项目、需要资源目录改名、需要打包含特定路径的资源、需要根据环境替换配置里的占位符的时候,默认约定就撑不住了。这时候build标签就该上场了。

1.2 build标签在pom.xml里的层级位置

很多人会在<project>根节点下找<build>,这个方向没问题,但要注意,build标签本身也有两种形态:一种是<project><build>,叫项目级build,作用于当前模块;另一种是<project><profiles><profile><build>,叫profile级build,只在激活对应profile时生效。后一种在多环境构建时非常有用,后面讲过滤时会用到。

build标签内部的基本结构是这样:

<build> <finalName>myapp</finalName> <sourceDirectory>src/main/java</sourceDirectory> <resources> <resource>...</resource> </resources> <testResources> <testResource>...</testResource> </testResources> <plugins> <plugin>...</plugin> </plugins> </build>

常见的子标签包括:finalName控制最终构件名,sourceDirectory控制源码目录,resources和testResources就是本篇的主角,plugins用来配置插件。

如果你只想配资源文件,那么build这一层只需要写resources和testResources两个子标签就行,其他全都可以不写。这大概是很多人第一次看到resources时最大的疑问:build里面那么多标签,我到底要用哪几个?答案是——按需。

1.3 资源复制与编译的生命周期关系

资源处理发生在编译之前,这不是随便排的顺序,而是有原因的。编译需要类路径上有必要的配置文件,比如Spring的applicationContext.xml、MyBatis的mapper文件、日志框架的logback.xml,如果这些文件还没到target/classes里,编译器可能在构建期就找不到资源。

整理一下相关生命周期阶段的顺序:

  1. process-resources:复制主资源到target/classes
  2. compile:编译Java源码
  3. process-test-resources:复制测试资源到target/test-classes
  4. test-compile:编译测试源码
  5. test:跑测试
  6. package:打包

搞清楚这个顺序,你就明白了为什么“打出来的包没有配置文件”这种问题,往往不是打包那一步出的问题,而是更早的process-resources阶段就出问题了。

2. resources标签的核心配置要素

2.1 directory——资源目录到底在哪

<resource>标签下的最核心子标签是<directory>,它指定资源所在目录。默认值是src/main/resources,但你可以改成任何相对路径,比如src/main/resources-custom、config,甚至可以用绝对路径指向项目外(不过我自己强烈不建议这么做,依赖外部环境会导致构建不可复现)。

一个简短的例子:

<resources> <resource> <directory>conf</directory> </resource> </resources>

这个配置的意思是:把项目根目录下conf目录里的所有文件,当作主资源复制到target/classes根目录。这样做的好处是,比如你的项目里有一堆运维侧配置、环境初始化SQL,不想塞到标准的src/main/resources里,用独立目录更容易管理。

需要注意,directory写的是conf,那么conf目录下的结构会原样映射到target/classes下。比如conf/scripts/init.sql复制后就是target/classes/scripts/init.sql。

2.2 includes与excludes——精确控制要复制的文件

大部分人在实际项目里会遇到这种需求:某个资源目录下有上百个文件,但打包时只需要其中一部分;又或者需要把某个子目录整个排除掉。这就是includes和excludes的工作。

它们的规则是逗号分隔的Ant风格通配符:

  • *:匹配零个或多个字符
  • ?:匹配单个字符
  • **:匹配任意层级的目录

示例,只打包mappers目录下的所有XML,同时排除其中的legacy子目录:

<resources> <resource> <directory>src/main/resources</directory> <includes> <include>mappers/**/*.xml</include> </includes> <excludes> <exclude>mappers/legacy/**</exclude> </excludes> </resource> </resources>

一个容易忽视的细节是:一旦你写了includes,排除逻辑就要特别注意。includes只管“要哪些”,excludes的作用是“在已选中的基础上再排除哪些”。顺序上Maven先应用includes,再应用excludes。所以如果includes里压根没包含某个文件,那excludes里写不写它都无所谓。

我见过一个翻车案例:某同学在resource里只include了**/*.xml,但项目里的messages.properties就丢了,因为他漏掉了properties文件。查了半天才反应过来,includes是白名单机制,不是黑名单。

2.3 filtering到底做了什么

这是resources标签里最容易被误解的配置,也是最容易出坑的。filtering的值是true或false,默认是false。当它为true时,Maven会在复制资源的过程中扫描文件内容,找到${...}形式的占位符,然后用当前构建时可用的属性值去替换它们。

举个最简单的例子。假设src/main/resources/application.properties里有一行:

app.version=${project.version}

如果不开启filtering,打包出来这个文件还是原样,${project.version}一点都没变。如果在该resource上配置了<filtering>true</filtering>,构建时Maven会去上下文里找project.version这个属性。project.version是Maven内置属性,表示当前项目的版本号,所以打包出来就变成了:

app.version=1.0.0

这个机制的本质是把“配置文件的内容”和“构建时的环境信息”解耦。环境相关的值(数据库地址、中间件端口、密钥别名等)在构建时动态注入,既不用改源码,也不用维护一堆环境分支。

暂缓一下,filtering的具体实战用法我在第4节会详细展开,先记住一个核心原则:只对文本文件开启filtering,二进制文件绝对不要开。

2.4 targetPath——改变文件输出路径

<targetPath>用来指定资源复制后的目标路径。默认情况下,资源会直接复制到target/classes对应的相对位置。一旦指定了targetPath,就可以改变这个对应关系。

比如你希望conf/env.properties最终被放到target/classes/META-INF/conf/env.properties:

<resources> <resource> <directory>conf</directory> <targetPath>META-INF/conf</targetPath> </resource> </resources>

这个功能在需要“把配置文件藏进JAR内部特定路径”时很有用。比如某些框架要求配置必须从META-INF下的固定位置读取,你就必须靠targetPath把文件挪过去。

不过我要泼一盆冷水:targetPath能不用尽量别用。它会打破你对输出目录结构的直觉,排查问题时很容易忽略“文件其实被复制到了子目录里”这个事实。尤其是多人协作的项目,别人看到你用法了,跑起来没问题,但他改代码时完全不会意识到资源路径已经被改了,一旦他要用相对路径读文件,就会有一堆奇怪问题冒出来。我经历过的几次资源丢失事故里,有一半和targetPath有关。

2.5 一个生产级完整配置示例

把上面零散的配置拼起来,一个我比较推荐的“兜底型”resources配置长这样:

<build> <resources> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> <excludes> <exclude>**/*.xlsx</exclude> <exclude>**/*.jpg</exclude> <exclude>**/*.png</exclude> <exclude>**/*.class</exclude> </excludes> </resource> <resource> <directory>src/main/resources</directory> <filtering>false</filtering> <includes> <include>**/*.xlsx</include> <include>**/*.jpg</include> <include>**/*.png</include> <include>**/*.class</include> </includes> </resource> </resources> </build>

这里用了两个resource块专门处理同一目录下的不同类型文件。第一个块开启过滤,但把二进制文件排除;第二个块不开启过滤,只包含这些二进制文件。这样既保留了文本文件的占位符替换能力,又避免二进制文件被过滤搞坏。这种“文本归文本、二进制归二进制”的分流写法,我在项目里用了很多年,几乎没再犯过资源文件损坏的毛病。

为什么这样做?因为Maven的过滤机制会以文本方式读取文件内容,如果遇到一个含有某些特殊字节的二进制文件,比如写了一半的UTF-8序列、压缩流、加密密钥文件,过滤器在替换过程中极大概率会把字节搞坏,最终产物直接不可用。二进制文件老老实实走filtering=false,这应该是所有Maven项目的底线。

3. testResources——测试资源的独立战场

3.1 测试资源与主资源的区别

<testResources>里面的每个子项叫<testResource>,配置字段和<resource>几乎一一对应,也是directory、includes、excludes、filtering、targetPath这几个。

但它们的目的地完全不同:主资源去target/classes,测试资源去target/test-classes。为什么要把测试资源单独拎出来一套?因为测试代码经常需要一些“只在测试期存在”的文件,比如造数据用的SQL、Mock接口返回的JSON、临时的日志配置。这些东西如果混进主资源目录,就会被一起打包进生产JAR,毫无意义还容易泄漏测试数据。

所以Maven默认约定src/test/resources是测试资源目录,即使你什么都不配,它也不会进主流程的package产物。这个隔离机制是Maven设计里很巧妙的一部分,省去了你手动“打包时排除测试文件”的麻烦。

3.2 testResources的配置示例

比如下面是只把测试用的testdata目录复制过去,并且排除其中big子目录的配置:

<testResources> <testResource> <directory>src/test/resources</directory> <includes> <include>testdata/**/*</include> </includes> <excludes> <exclude>testdata/big/**</exclude> </excludes> <filtering>true</filtering> </testResource> </testResources>

这里需要提醒一个容易踩的坑:Maven关于testResources同样存在“默认目录”的概念。如果src/test/resources目录存在,那么即使你完全不写testResources,它也会被默认处理。而如果你写了testResources,等于把默认设置覆盖掉了,之后src/test/resources里的文件就不再自动复制了,除非你像上面这样显式指向它。

这个cover行为在搭建新项目时坑了不少人:本来测试好好的,后来为了调整测试配置加了testResources块,结果忘了把src/test/resources这个默认目录也纳入进来,导致一批测试数据文件突然消失,测试咔咔报错。所以原则是:testResources这个标签,要么彻底不写,要写就必须把默认逻辑想清楚。

3.3 同名资源文件的优先级陷阱

测试资源和主资源有一个隐蔽的优先级关系:当测试阶段运行时,target/test-classes在类路径上的顺序排在target/classes之前。也就是说,如果测试资源目录和主资源目录里存在同名同相对路径的文件,测试时加载到的是测试资源里的那个版本。

这个特性有时候极好用。比如正式主资源里配置了一个真实的数据库连接,测试资源里放一个指向本地测试库的相同配置文件,那么跑测试时用测试配置,打包时自动包正式配置,两不干扰。这其实是一种很朴素的“环境隔离”玩法。

但反过来的坑是:你测试资源里某个文件忘了更新,而它恰好遮蔽了主资源的最新版本,那跑测试时会用旧内容,结果测试现象和真实环境完全对不上。我在多个项目里都见过这种“本地测试通过、一上线就炸”的经典事故,排查到最后往往是src/test/resources下有个陈年老文件在捣鬼。建议团队约好:测试资源目录里只放测试特有文件,不放同名覆盖文件;如果非放不可,审核时重点盯一下版本同步问题。

3.4 关于targetPath在testResources中的应用

targetPath在testResources里同样存在,但它更少用到,而且风险更大。因为测试资源路径一旦写偏,可能会把文件复制到一个非预期的classpath位置,导致测试时找不到配置。

如果确实要用,比如需要把测试配置放到META-INF/spring/下,可以这样写:

<testResources> <testResource> <directory>src/test/resources</directory> <targetPath>META-INF/spring</targetPath> </testResource> </testResources>

但我强烈建议测试资源保持默认路径即可。测试场景本来就追求简单可复现,targetPath这种魔法少用为妙。真遇到框架强制要求路径,那也得先确认框架的测试态加载路径和JAR运行态加载路径是一致的。

4. 资源过滤与多环境打包的完整玩法

4.1 占位符到底从哪里取值

前面说过,filtering是Maven扫描文件并替换${...}占位符的动作。真正要搞懂,还得知道这些值能来自哪些渠道。

Maven内置了一些常用属性,比如${project.version}、${project.basedir}、${project.build.outputDirectory}等。这些是最简单的来源,但因为它们是构建参数而不是业务参数,通常只做版本号、构建时间这类信息注入。

更常见的是<properties>标签里自定义的属性。比如:

<properties> <db.url>jdbc:mysql://localhost:3306/dev_db</db.url> <db.username>dev_user</db.username> </properties>

然后src/main/resources/jdbc.properties里写:

db.url=${db.url} db.username=${db.username}

开启filtering后,构建产物就变成了实际的数据库连接串。

4.2 用profile组合不同环境

一套代码要部署到开发、测试、生产环境,最朴素的需求就是数据库地址、日志级别、中间件IP不一样。用filtering恰好可以优雅地解决。

在pom里定义几个profile,每个profile里带不同的properties和filter占位符,构建时用-P参数激活即可:

<profiles> <profile> <id>dev</id> <properties> <env.name>dev</env.name> <db.url>jdbc:mysql://local-dev/db</db.url> </properties> </profile> <profile> <id>prod</id> <properties> <env.name>prod</env.name> <db.url>jdbc:mysql://prod-server/db</db.url> </properties> </profile> </profiles>

资源文件里写:

env.name=${env.name} db.url=${db.url}

构建时执行:

mvn clean package -Pdev mvn clean package -Pprod

同一个工程,两个环境,两套配置。这样做的核心价值是把“环境差异”从代码里抽离出来,放到构建参数层。代码仓库里不藏生产密钥,生产配置只存在于构建机器的profile或者外部配置中心,安全性高不少。

4.3 定界符:为什么有时用@...@不用${...}

filtering默认的占位符定界符就是${...},但直接用它有一个很大的问题:你的配置文本里如果本身就有类似${...}的内容,比如Spring的@Value("${server.port}")、Shell脚本里的${PATH},就会被错误替换或者打包时报“找不到属性”的异常。

解决办法是修改maven-resources-plugin的useDefaultDelimiters和delimiters配置,把占位符改成@...@。这是Maven一个很经典的技巧,网上俗称“@一大法”。

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <version>3.3.1</version> <configuration> <useDefaultDelimiters>false</useDefaultDelimiters> <delimiters> <delimiter>@</delimiter> </delimiters> </configuration> </plugin>

这样资源文件里就用@db.url@这种写法了。配置文件里正常出现的${...}不再被Maven碰,而@...@则被替换成实际值。我强烈建议所有做资源过滤的项目都切换到@...@定界符,尤其是SpringBoot项目——Spring本身大量使用${...}做占位符,如果用默认过滤规则,你的配置会被Maven和Spring双重处理,那场面想一想都觉得头大。

4.4 二进制文件与过滤的兼容性

前文反复强调二进制文件不能开过滤。这里再补充一个容易被阴的场景:某些扩展名是文本、但内容带有二进制特征的文件,比如Excel文件(xlsx本质是个zip包)或带有签名的JAR。

我调试过一个线上怪病,某个包含.xlsx模板的资源文件在本地works,打出来的生产JAR里就损坏了,Excel打不开。排查时发现是resources里全局开了<filtering>true</filtering>,Maven把xlsx当成文本处理,在替换占位符的过程中破坏了压缩包结构。

站在事后视角,这个错误很蠢;但站在当时的排查现场,就是差点把整个项目拆了才定位到。所以再次强调:生产环境只要涉及resources,一定要先把二进制扩展名列进黑名单,再考虑开filtering。还是那句话,文本白名单模式更稳。

4.5 多环境过滤的实战组合:配置文件与profiles结合

除了在pom里写死properties,还有一种主流方案是把不同环境的差异配置放在独立的property文件里,比如env-dev.properties、env-prod.properties,然后通过<filters>标签引入。

<build> <filters> <filter>env-${env.name}.properties</filter> </filters> <resources> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> </resource> </resources> </build> <profiles> <profile> <id>dev</id> <properties> <env.name>dev</env.name> </properties> </profile> <profile> <id>prod</id> <properties> <env.name>prod</env.name> </properties> </profile> </profiles>

这里<filters>不是resources里的子标签,而是build直接下的标签。它的作用是告诉Maven“去读哪些外部属性文件,作为过滤时的属性来源”。配合profile里定义的env.name动态选择读哪个文件,就能实现完全外部化的环境配置管理。

用-Pdev构建时,env.name为dev,filter读取env-dev.properties;用-Pprod构建时,filter读取env-prod.properties。要新增一个环境,只需新增一个properties文件和一个profile,代码零改动。项目从两三套环境扩张到七八套环境时,这种模式能省下大量重复维护成本。

5. 常见问题与排查技巧实录

5.1 明明有资源文件,打包后却没有

症状:本地IDEA里能跑,从target/classes里看文件还在,但打成JAR后文件不见了。

排查套路:先看是不是resources配置覆盖了默认目录。一旦写了resources且没包含src/main/resources,默认目录就失效了。比如只写了<directory>conf</directory>,那你原来的src/main/resources路径就不在构建里。

另外,如果resources里写了includes,也容易漏文件。比如你只想包含**/*.xml,那其他properties就会消失。最快捷的排错方法是执行:

mvn clean package -X

看process-resources阶段的复制日志,Maven会列出哪些文件被复制、哪些被排除。日志里找不到就说明这个阶段就没处理它,问题在resources配置而不是后续打包。

5.2 配置了testResources后,测试数据丢失

症状:加了testResources配置之后,原本在src/test/resources下生效的测试文件找不到了。

原因就是前面说的覆盖默认目录。testResources一旦存在,默认的src/test/resources就不自动生效。你需要在这个testResource的directory里显式补上src/test/resources。

我自己写项目时的处理习惯是:

<testResources> <testResource> <directory>src/test/resources</directory> </testResource> <!-- 再追加额外的测试资源目录 --> <testResource> <directory>testdata</directory> </testResource> </testResources>

多个testResource可以共存,Maven会按声明顺序逐一处理。如果多个目录下有同名文件,后复制的那个会覆盖先复制的,这个覆盖顺序也得心里有数。

5.3 开启filtering后文件乱码或内容异常

症状:配置文件里中文注释变成乱码,或者一个${...}没被替换反而原样保留。

排查要点:

  1. 检查project.build.sourceEncoding是否设置了UTF-8。资源过滤读取文件时会依赖项目编码设置,如果编码不对,中文就崩了。
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties>
  1. 检查占位符的定界符。如果还用的默认${...},而要替换的内容带嵌套,就容易被误判。

  2. 确认属性来源是否可达。如果${db.url}对应的properties或<properties>里根本没有这个key,Maven会保留原样或者抛异常,具体行为取决于插件的failOnMissingWebXml这类历史遗留开关,不过现代版本默认会让它原样输出还是让你失败,不同版本行为不一样。

建议显式加一段配置,让缺失属性时构建直接失败,避免“生产环境悄悄带着没替换的占位符上线”这种极端危险的场景:

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <configuration> <useDefaultDelimiters>false</useDefaultDelimiters> <delimiters> <delimiter>@</delimiter> </delimiters> <strictFiltering>true</strictFiltering> </configuration> </plugin>

strictFiltering为true时,遇到无法解析的过滤器引用就直接报错,而不是悄悄放过。这个开关我建议打开,宁可构建期暴露问题,也别让问题跑到线上。

5.4 修改了resources目录下的文件,打包却没变化

症状:改了某个配置文件,执行mvn package后target里还是旧内容。

大多数时候是没做clean。Maven的dependency和compile层面有增量机制,但资源复制这方面,很多时候是全量覆盖,所以不clean的话target/classes里残留旧文件也是常有的事。执行mvn clean package基本能解决八成这种问题。

另外一个隐蔽原因:你修改的是src/main/java下的文件(有些人图省事把配置放在Java源码目录下),这些文件在compile阶段会被当作classpath资源带过去,但它们的处理逻辑跟resources完全不同。把配置文件放java目录下不是一个好习惯,它会让你搞不清到底是谁把它复制的。正确做法是只把src/main/resources当资源目录。

5.5 多模块项目里资源和依赖模块的路径坑

在多模块聚合项目中,模块A把资源输出到了target/classes的某个子目录,模块B依赖模块A并以相对路径读这个文件。很多人会想当然地以为“依赖模块的资源会直接出现在我的classpath”,这个思路在大多数框架的默认配置下是成立的,因为依赖模块的JAR被解包后,它内部的资源路径会被合入当前classpath。

但如果模块A的resources里使用了targetPath把文件放到了一个特殊位置,这个位置不一定是B模块classpath的根,于是B运行时就出现“明明A里的包有文件,我却读不到”的现象。

这类问题的排查诀窍就一句话:先看JAR包内部结构,再谈代码路径。

jar tf 模块A-1.0.0.jar

先搞清楚文件在JAR里的实际路径,再去代码里看读取路径是否一致。我调试过几次跨模块资源问题,最终都靠这个命令定位。

5.6 一份速查表收藏

症状大概率原因核心排查点
打出的包里缺文件resources覆盖默认目录 / includes漏配执行-X看process-resources日志
测试数据消失testResources覆盖默认目录显式补上src/test/resources
过滤后中文乱码未设置sourceEncoding设置UTF-8编码
二进制文件损坏对二进制开了filtering文件按类型分流
占位符没替换定界符不一致 / 属性缺失启用strictFiltering
修改后不生效没有clean执行mvn clean package
跨模块读不到资源targetPath导致实际路径变化用jar tf查看内部路径

5.7 我最常用的排查三板斧

遇到资源相关的诡异问题,我的固定操作顺序是:

先跑mvn help:effective-pom看当前生效的pom配置。因为实际构建时用的是有效POM,不是你自己写的那个。如果说你写的配置和实际生效的不一致,那一切都白谈。

再翻target/classes或target/test-classes看文件是否真的到了位。这个比直接打开JAR快,能区分是复制阶段失败还是打包阶段出问题。

最后一步才动代码层排查。如果文件在target里存在,但运行时空指针或file not found,那多半是代码里的读取路径有误。把文件存在性和路径对齐搞定,绝大多数资源问题都能收工。

写在最后的经验

如果只让我留一条建议,那就是:资源文件目录保持默认,filtering保持克制,能不换路径就不换路径,能不开过滤就不开过滤。默认约定虽然平淡,但意味着团队里每个人看一眼目录结构就能预测构建结果,这对协作来说有时候比所谓“灵活配置”值钱得多。真需要多环境配置时,优先用@...@定界符 + profile + 外部properties的组合,而不是在一个巨大的resources配置里塞十几行魔法路径。我见过的线上构建事故,多数不是Maven功能不够,而是配置写得太过“聪明”,聪明到没人能一眼看懂它最终会把文件复制到哪里去。少一些奇技淫巧,多一份可预测性,这大概就是Maven这十几年沉淀下来最值得尊重的哲学。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询