ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Maven pom.xml资源配置:resources与filtering详解

Maven pom.xml资源配置:resources与filtering详解

1. 一次让我彻底重视pom.xml资源的线上事故

1.1 一个jar包引发的深夜排查

我在一家做企业服务系统的公司时,经历过一次很丢人的上线事故:功能测试全部通过,代码评审也没问题,大家觉得稳了,结果部署到生产环境,应用启动直接失败。报错信息里只有一个“缺少配置文件”的提示。当时排查了好几个小时,从代码到数据库连接挨个查,最后才发现,问题出在Maven打包这一环。

那个缺失的配置文件是一个环境校验规则文件,放在项目根目录下一个名叫conf的文件夹里。开发环境启动时,IDE会自动把这个目录加入classpath,所以一切正常。但是用Maven打包时,Maven根本不知道这个目录存在,它默认只会处理src/main/resources里的内容。于是生成的jar里压根没有这个文件,生产环境自然起不来。

1.2 Maven里的“资源”到底指什么

那次事故之后,我把pom.xml里的资源相关配置从头到尾研究了一遍。Maven项目里有两类东西:一类是Java源码,经过编译变成class文件;另一类是资源文件,包括properties配置、XML配置、YAML配置、SQL脚本、模板文件、证书文件等。这些资源文件虽然不参与编译,但运行时必须能读到它们。Maven的默认行为,是把src/main/resources下的文件原样复制到classpath,把src/test/resources下的文件复制到测试classpath。

、 、 这三个标签,就是控制“哪些目录的资源会被复制、复制到哪里、复制前要不要做处理”的关键。理解它们,才算真正理解了Maven的资源管理机制。下面我按我自己的使用顺序,一个一个拆开讲,顺便把我踩过的坑也一并说出来。

2. build标签:控制构建行为的核心枢纽

2.1 约定优于配置:默认目录结构先记牢

先看pom.xml的整体结构。 是project下最核心的标签之一,负责构建过程的全部配置。默认情况下,Maven已经给你设好了目录约定:

  • src/main/java:主Java源码,编译后进入classes
  • src/main/resources:主资源文件,最终进入classpath和jar包
  • src/test/java:测试Java源码,只参与测试编译
  • src/test/resources:测试资源文件,只出现在测试classpath

这种“约定优于配置”的设计,让你不写任何配置就能正常构建。但约定不是万能的,项目一旦复杂起来就会发现:资源文件不止放在src/main/resources里。比如我见过有的团队把前端模板放在src/main/webapp下,把公共配置放在shared-config目录下,把部署脚本放在scripts目录下。Maven不会自动认识这些目录,所以必须通过build标签告诉它。

2.2 build里能改什么:常用子标签盘点

build下面有不少子标签,我列一个常用清单。

子标签作用我实际用到的场景
finalName指定构建产物的最终文件名生成固定名字的jar/war,方便脚本引用
sourceDirectory指定Java源码目录源码不在标准src/main/java下时
outputDirectory指定class输出目录临时改target目录名
resources配置主资源目录的处理规则增加额外资源目录、过滤配置文件
testResources配置测试资源目录的处理规则测试环境专属配置、测试数据
plugins引入构建插件编译、打包、资源处理都靠它

finalName这个标签我几乎每个项目都会用。默认打出来的jar叫xxx-1.0-SNAPSHOT.jar,带版本号看起来正规,但脚本里写死文件名就尴尬了。我通常会把finalName设成一个不随版本变化的名字,比如app-prod,这样后续脚本和Dockerfile不用每次跟着版本号改。sourceDirectory我唯一一次用到,是接手一个老项目,它的Java代码放在src/java目录下,不写这个标签根本编译不了。这类项目不多,但存在。

2.3 有些项目就是不走默认目录,怎么办

如果你的项目里有一部分资源放在其他目录,解决办法是在resources下面新增一个resource节点,把directory指到那个目录。比如我有一个公共模块,配置中心放在项目根目录的config-center目录下:

<build> <resources> <resource> <directory>config-center</directory> </resource> </resources> </build>

注意,默认的src/main/resources仍然会被处理。一个项目可以有多个resource节点,每个对应一个额外目录,Maven会把它们的内容合并复制到classpath。这就是自定义资源目录的核心逻辑。

为什么不直接把文件拷贝进src/main/resources?因为把第三方配置混在项目源码里,不利于维护、容易误提交,多环境部署时也更容易冲突。用resources声明目录,配置路径一目了然,换环境改一处就行。

3. resources:主资源目录的精细化控制

3.1 resource的五个基础配置项

resources下面每个resource节点都可以配置五个子元素:directory、targetPath、filtering、includes、excludes。先记住一句话:这个节点表达的完整语义是“把directory目录下、符合includes和excludes条件、经过filtering处理之后的文件,复制到targetPath指定的目录”。

配置项作用默认值
directory资源目录路径无
targetPath复制到classpath下的目标路径直接复制到classpath根目录
filtering是否对文件内容做占位符替换false
includes包含哪些文件,用通配符表达所有文件
excludes排除哪些文件,用通配符表达无

directory是相对于项目根目录的路径,最常见的是src/main/resources,但你可以写config、scripts、lib等。targetPath是一个容易忽视的选项,它决定了文件进入classpath之后的相对路径。如果你设了targetPath为config,那么代码里读取文件时就要用classpath:config/xxx,而不是classpath:xxx。

3.2 includes/excludes:过滤规则的两个必知细节

includes和excludes的取值支持两种通配符:*匹配任意字符,但不跨目录;**可以跨目录。例如:

<includes> <include>**/*.xml</include> <include>**/*.properties</include> </includes>

第一个细节,一旦写了includes,默认的“复制全部文件”行为就失效了。也就是说,如果你只想复制xml,includes里只写**/*.xml,那么目录下的sql、yml、jks等文件都不会被复制。很多人第一次配置这里时,明明想排除某些文件,结果却把其他需要的文件也弄丢了,多半就是这个原因。

第二个细节,excludes的优先级高于includes。即使你include了**/,也可以用excludes把**/.jks这类文件去掉。两个标签同时存在时,Maven先按includes筛选,再按excludes排除。这个顺序很重要,能帮你避免很多“怎么全被复制了”的困惑。

3.3 filtering:把配置文件里的占位符变成环境值

filtering是最容易被忽略,也最实用的配置。设成true以后,Maven会在复制资源文件时解析文件里的${...}占位符,使用pom.xml里properties定义的值或者外部profile传入的值进行替换。

比如application.properties里写:

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

pom.xml里:

<properties> <db.url>jdbc:mysql://localhost:3306/app</db.url> <db.username>dev</db.username> </properties>

只要这个properties文件对应的resource节点filtering设为true,打包后classpath里的application.properties就会变成实际的值。

这个机制在开发、测试、生产环境配置差异明显时特别有用。我通常配合profile使用,在打包命令里传入-Pprod,让不同的profile定义不同的db.url,不需要维护三个完全不同的application文件。

3.4 一个能直接抄的resources完整配置

下面是我常用的一份多环境资源配置,注释写在代码里:

<build> <resources> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> <includes> <include>**/*.properties</include> <include>**/*.yml</include> <include>**/*.xml</include> </includes> </resource> <resource> <directory>config</directory> <targetPath>config</targetPath> <filtering>false</filtering> </resource> </resources> </build>

这份配置前半段把主资源目录里常见的文本类配置做占位符替换,后半段把我单独管理的config目录原样复制到classpath的config子目录下。过滤只针对文本文件,二进制文件我全部设成false,避免图片、证书之类被改写坏。

这份配置直接抄就能用。要注意的是targetPath目录叫config后,代码里读取路径就得写classpath:config/xxx,别按根目录找。

4. testResources:测试资源的独立王国

4.1 它和resources到底差在哪

testResources的作用对象是src/test/resources。它的配置语法和resources几乎一样,区别在于复制时机和去向。主资源在process-resources阶段复制,进入主classpath,随应用一起打包;测试资源在process-test-resources阶段复制,只进入测试classpath,不会打到jar或war里。

这个隔离非常关键。测试环境经常需要一些主环境没有的东西:mock数据、测试专用配置、本地数据库初始化脚本。如果把这些放进src/main/resources,它们就会被带到生产包。我在很多项目里见过生产包里有test目录、mock配置文件,就是因为有人懒得区分,全塞进了主资源目录。测试资源天生就带有“不进生产包”的隔离属性。

4.2 测试资源的两个实用玩法:同名覆盖与独立数据

同名覆盖:Maven构建测试classpath时,src/test/resources排在src/main/resources前面,所以测试资源里放一个同名文件,可以覆盖主资源里的那个。比如主资源里application.properties指向真实数据库,测试时用一个同样路径的application.properties指向H2内存库,不需要改动主文件。这个特性比用profile切换配置还干净,因为测试代码读到的路径完全一致。

独立数据:测试资源也经常用来放测试数据集。比如单元测试要读一批固定json作为输入,放在src/test/resources/testdata/xx.json,无论本地还是CI,永远能拿到同一份数据。不会因为某人的工作目录不同而读取失败,也不会因为误改了主资源数据而污染其他环境。

4.3 testResources配置示例

testResources的配置方式与主资源几乎一致,也可以使用filtering。下面是我做服务集成测试时常用的一段:

<testResources> <testResource> <directory>src/test/resources</directory> <filtering>true</filtering> <includes> <include>**/*.properties</include> </includes> </testResource> <testResource> <directory>test-config</directory> <targetPath>testdata</targetPath> </testResource> </testResources>

第一个节点处理标准的测试资源配置,把占位符替换成测试值;第二个节点把工程根目录下一个叫test-config的外部目录整体复制到测试classpath的testdata子目录,用来统一维护较大的测试数据文件。两份资源会在测试classpath下合并,测试代码可以直接按classpath:testdata/xxx访问。

5. 藏在生命周期里的资源处理逻辑

5.1 资源复制在哪个阶段发生,谁在执行

Maven的生命周期默认有三个循环:clean、default、site。default生命周期里,process-resources阶段负责主资源复制,process-test-resources阶段负责测试资源复制。resources里的配置,本质上是给maven-resources-plugin提供参数,这个插件默认绑定到了上面两个阶段。

这意味着,运行mvn compile时,process-resources阶段会先把主资源复制好,然后Java编译才拿到完整的classpath。运行mvn test-compile时,process-test-resources再把测试资源复制到target/test-classes。理解这个顺序,你就能明白为什么有时候改了一个资源文件,必须重新执行mvn compile才能生效,因为IDE只编译代码,不会帮你走Maven的资源复制流程。

资源复制后的实际位置也有规律:src/main/resources下的文件会进入target/classes,src/test/resources下的文件会进入target/test-classes。这两个目录就是主classpath和测试classpath对应的物理位置。所以排查资源问题时,不用先解压jar,直接看target/classes就能判断资源是否被正确处理了。

5.2 filtering是如何解析占位符的

filtering=true时,maven-resources-plugin会在文件复制过程中读取文件内容,匹配默认的${...}定界符,然后从Maven项目的属性集合中查值替换。属性来源包括:pom.xml里properties、settings.xml里配置的属性、命令行-D参数,以及pom里profile下的properties。

常用的写法是在pom里定义profile:

<profiles> <profile> <id>prod</id> <properties> <db.url>jdbc:oracle:thin:@prod-db:1521:app</db.url> </properties> </profile> </profiles>

打包时执行mvn clean package -Pprod,资源里的${db.url}就会被替换成prod值。不带-P时,使用pom里默认的properties,或者找不到对应属性就不替换,占位符原样保留。

细节:有些配置值里本身含有$字符,比如shell脚本、正则表达式、某些模板语言。filtering会尝试替换所有${...},所以把filtering误开到脚本目录,很可能把好好的脚本改坏。我的经验是:过滤范围尽量收窄,能用includes限定文件类型就限定,不要对整个目录一刀切打开filtering。

5.3 编码问题:为什么配置文件中文会乱码

资源文件复制是一个IO过程,必然涉及编码。maven-resources-plugin默认按项目构建的编码读取文件,如果工程里没设置project.build.sourceEncoding,不同机器、不同系统默认编码不一样,中文配置很容易变成乱码。出现“开发机正常,打包服务器乱码”的情况,十有八九是这个编码没锁定。

我一般在pom.xml的properties里加:

<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties>

这两行看起来不起眼,但能解决大量filtering和资源复制时的编码烂账。加了以后,资源文件按UTF-8读、按UTF-8写,不会再因为打包服务器默认编码是GBK而乱成一团。

6. 三个高频场景的配置模板

6.1 多环境部署:dev/test/prod配置自动切换

这是resources加filtering最典型的应用。我习惯这样组织多环境配置:不同环境用不同的profile承载不同的属性值,资源文件里只需要写占位符。

<profiles> <profile> <id>dev</id> <properties> <db.url>jdbc:mysql://localhost:3306/app_dev</db.url> <redis.host>localhost</redis.host> </properties> </profile> <profile> <id>prod</id> <properties> <db.url>jdbc:mysql://prod-db.internal:3306/app</db.url> <redis.host>redis-prod.internal</redis.host> </properties> </profile> </profiles>

然后把application.properties里的数据库地址写成:

db.url=${db.url} redis.host=${redis.host}

配合前面讲的filtering,打包时执行mvn clean package -Pdev或-Pprod,配置文件会自动变成对应环境的值。对中小型项目,这种方式最直观,也不容易出错。

6.2 不想进jar包的文件:证书、密钥、本地环境配置

有些资源文件只应该存在于特定环境,比如生产服务器上的jks证书、密钥文件、.env本地配置。把它们打包进jar会有泄露风险,还会导致容器镜像里出现不必要的文件。这时候用excludes把它们挡在构建产物之外:

<resource> <directory>src/main/resources</directory> <excludes> <exclude>**/*.jks</exclude> <exclude>**/*.p12</exclude> <exclude>**/.env</exclude> <exclude>**/application-local.yaml</exclude> </excludes> </resource>

注意,如果某个资源节点没有任何includes,excludes会基于“全部文件”来排除,所以这条规则能正常生效。我在项目的持续集成环境里还会用同样的思路排除.DS_Store、*.log这类临时文件,避免构建产物体积膨胀。

6.3 把src/main/java里的xml一并打包

有一个出现频率极高的场景:MyBatis的mapper XML不想单独放在resources目录,而是放在Java包路径下,和DAO接口放一起。默认情况下,src/main/java下只有.java文件会被编译,xml不会被复制到classpath,运行时Mapper接口找不到XML语句,直接报绑定异常。

解决方法是新增一个resource节点指向src/main/java,并限定只复制xml:

<resources> <resource> <directory>src/main/resources</directory> </resource> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> </resource> </resources>

这样Java源码正常编译,xml被当作资源复制到对应的包路径下。这里有个常见误区:直接写 src/main/java 且不带includes,会把java文件也复制一份到classpath,虽然通常不影响启动,但会造成classpath里出现冗余的.java文件。一定要用includes把范围锁死。

7. 资源相关问题的排查速查表

7.1 打包后找不到文件或路径不对

现象:jar里没有某配置文件,或者文件在,但路径和代码里写的不一样。

排查方向:先确认文件所在目录是否被某个resource节点覆盖;再看targetPath有没有改变classpath结构;最后确认includes/excludes有没有误伤。

我一般的排查顺序是:

  1. 执行mvn clean package,直接看target/classes目录,资源文件复制之后会先落在这里。
  2. 在pom中临时把resource配置注释掉,看默认行为是否正常。
  3. 确认代码里的读取路径和targetPath匹配。

target/classes里的文件结构,就是最终jar包里的classpath结构。这一步通常一分钟就能定位问题。

7.2 二进制文件被过滤搞坏

现象:图片、证书、字体文件在打包后打不开,或者报格式错误。原因几乎都是filtering被应用到了非文本文件上。maven-resources-plugin在替换占位符时是按文本读取的,二进制文件被转码后字节就变了。

处理方式:把二进制文件所在目录或文件从filtering中摘出去,单独配置一个filtering=false的resource节点。一句话原则:filtering只开给文本配置文件,其他一律false。

7.3 占位符没替换或替换成了错误值

现象:打包后的配置文件里还是${db.url},或者替换成了意料之外的值。

排查方向:确认资源节点filtering是否为true;确认占位符写法和pom里properties的key完全一致,包括大小写;确认是否存在多个profile同时被激活,不同profile的properties会合并,靠后的覆盖靠前的;确认文件是否被多个resource节点重复处理,重复处理时后一次可能拿到已经被替换过的内容。

占位符冲突还有一个经典场景:配置文件里本身有正则表达式,比如${...}形式的变量也会被误判。遇到这种情况,我建议把这个文件排除出过滤目录,别和filtering较劲,直接在代码里读取原始文件更省事。

7.4 测试资源覆盖主资源失效

现象:src/test/resources里放了同名properties,但测试加载到的还是主资源里的值。

多数情况是因为主资源和测试资源都配置了targetPath,而且targetPath不一致,导致两个文件没有落在classpath的同一个相对路径上,覆盖关系自然失效。我习惯让测试资源保持默认targetPath;如果主资源配置了targetPath=config,那么测试资源也要配上targetPath=config,才能在同名覆盖时生效。

另外,测试资源覆盖只在测试classpath内生效,它不会改变主jar里的文件。这不算问题,但很多人误以为改了测试资源就能影响生产包,必须记清楚:testResources永远不影响主构建产物。

最后分享一个我从那次事故之后养成的习惯:每次执行mvn clean package后,不急着部署,先打开target/classes目录看一眼里面有什么,再用解压命令看一眼最终jar的完整文件列表。一般耗时不到半分钟,却能省下深夜排查的力气。资源文件这种事,配置错了不报错,但运行期一定会找上门。把pom.xml里这三个标签读透,比临时上网查一百次都好使。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进