
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源码编译后进入classessrc/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 directoryconfig-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是否对文件内容做占位符替换falseincludes包含哪些文件用通配符表达所有文件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默认的“复制全部文件”行为就失效了。也就是说如果你只想复制xmlincludes里只写**/*.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.urljdbc:mysql://localhost:3306/app/db.url db.usernamedev/db.username /properties只要这个properties文件对应的resource节点filtering设为true打包后classpath里的application.properties就会变成实际的值。这个机制在开发、测试、生产环境配置差异明显时特别有用。我通常配合profile使用在打包命令里传入-Pprod让不同的profile定义不同的db.url不需要维护三个完全不同的application文件。3.4 一个能直接抄的resources完整配置下面是我常用的一份多环境资源配置注释写在代码里build resources resource directorysrc/main/resources/directory filteringtrue/filtering includes include**/*.properties/include include**/*.yml/include include**/*.xml/include /includes /resource resource directoryconfig/directory targetPathconfig/targetPath filteringfalse/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 directorysrc/test/resources/directory filteringtrue/filtering includes include**/*.properties/include /includes /testResource testResource directorytest-config/directory targetPathtestdata/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/classessrc/test/resources下的文件会进入target/test-classes。这两个目录就是主classpath和测试classpath对应的物理位置。所以排查资源问题时不用先解压jar直接看target/classes就能判断资源是否被正确处理了。5.2 filtering是如何解析占位符的filteringtrue时maven-resources-plugin会在文件复制过程中读取文件内容匹配默认的${...}定界符然后从Maven项目的属性集合中查值替换。属性来源包括pom.xml里properties、settings.xml里配置的属性、命令行-D参数以及pom里profile下的properties。常用的写法是在pom里定义profileprofiles profile idprod/id properties db.urljdbc: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.sourceEncodingUTF-8/project.build.sourceEncoding project.reporting.outputEncodingUTF-8/project.reporting.outputEncoding /properties这两行看起来不起眼但能解决大量filtering和资源复制时的编码烂账。加了以后资源文件按UTF-8读、按UTF-8写不会再因为打包服务器默认编码是GBK而乱成一团。6. 三个高频场景的配置模板6.1 多环境部署dev/test/prod配置自动切换这是resources加filtering最典型的应用。我习惯这样组织多环境配置不同环境用不同的profile承载不同的属性值资源文件里只需要写占位符。profiles profile iddev/id properties db.urljdbc:mysql://localhost:3306/app_dev/db.url redis.hostlocalhost/redis.host /properties /profile profile idprod/id properties db.urljdbc:mysql://prod-db.internal:3306/app/db.url redis.hostredis-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 directorysrc/main/resources/directory excludes exclude**/*.jks/exclude exclude**/*.p12/exclude exclude**/.env/exclude exclude**/application-local.yaml/exclude /excludes /resource注意如果某个资源节点没有任何includesexcludes会基于“全部文件”来排除所以这条规则能正常生效。我在项目的持续集成环境里还会用同样的思路排除.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并限定只复制xmlresources resource directorysrc/main/resources/directory /resource resource directorysrc/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有没有误伤。我一般的排查顺序是执行mvn clean package直接看target/classes目录资源文件复制之后会先落在这里。在pom中临时把resource配置注释掉看默认行为是否正常。确认代码里的读取路径和targetPath匹配。target/classes里的文件结构就是最终jar包里的classpath结构。这一步通常一分钟就能定位问题。7.2 二进制文件被过滤搞坏现象图片、证书、字体文件在打包后打不开或者报格式错误。原因几乎都是filtering被应用到了非文本文件上。maven-resources-plugin在替换占位符时是按文本读取的二进制文件被转码后字节就变了。处理方式把二进制文件所在目录或文件从filtering中摘出去单独配置一个filteringfalse的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如果主资源配置了targetPathconfig那么测试资源也要配上targetPathconfig才能在同名覆盖时生效。另外测试资源覆盖只在测试classpath内生效它不会改变主jar里的文件。这不算问题但很多人误以为改了测试资源就能影响生产包必须记清楚testResources永远不影响主构建产物。最后分享一个我从那次事故之后养成的习惯每次执行mvn clean package后不急着部署先打开target/classes目录看一眼里面有什么再用解压命令看一眼最终jar的完整文件列表。一般耗时不到半分钟却能省下深夜排查的力气。资源文件这种事配置错了不报错但运行期一定会找上门。把pom.xml里这三个标签读透比临时上网查一百次都好使。