
上周我接手一个老项目同事用 Maven 打了一个 release 包装到客户环境上结果系统一直连不上数据库。后来一查发现 JAR 包里面竟然带着一份 application-dev.yml里面数据库地址还指向本地。说白了就是打包之前没把不需要的文件先删干净。这类问题在 Maven 项目里特别常见。开发机上改过的本地配置、IDE 生成的临时文件、测试用的静态页面只要没在打包前做“清理”就会混进最终产物。而很多人的第一反应是加个 .gitignore或者每次手动删可要么没用要么删完忘记恢复。本文就把“Maven 打包前删除不需要文件”这件事的常见做法、完整配置和坑都过一遍适合刚接触 Maven 的新手也适合正在为打包产物里多出文件而头疼的老手。1. 为什么打包前要先做“减法”漏进 JAR 里的文件都有哪些1.1 四种最常见的不该打包文件我见过的项目里被打进产物的“垃圾文件”基本可以分成四类每一种的处理难度都不一样。第一类是环境相关的配置文件这是危害最大的一类。比如application-dev.yml、application-local.properties、config.local.xml这类东西。它们往往带着本地数据库地址、测试账号、甚至内部接口的访问密钥。一旦打进 JAR线上环境如果激活了错误的 profile可能导致系统直接连到开发库轻则数据混乱重则泄露敏感信息。更麻烦的是这种文件在开发机上每天都用你不会想到要删它但打包时它就成了隐患。第二类是 IDE 和操作系统产生的垃圾文件。.idea目录、*.iml、.DS_Store、Thumbs.db这些平时在 Git 里可能早就被 ignore 了但 Maven 打包并不会看 .gitignore 的脸色。尤其是有些项目把.idea目录不小心放进了src/main/resources打包后整个 IDE 配置都进了 JAR。虽然不影响运行但很脏而且容易在不同环境下引起诡异问题。第三类是构建过程的残留物。如果你打的是增量包或者构建脚本里没有正确执行 cleantarget目录里上一次构建留下的旧 class、旧资源、慢日志文件都会被带进新产物。很多奇怪 bug 就是这种“旧文件覆盖新文件”导致的。第四类是临时资源和演示页面。测试用的test-page.html、demo 图片、内部说明文档这些文件开发阶段有用生产环境完全不需要。放进产物里除了占空间还可能泄露内部项目结构信息。1.2 一个真实事故dev 配置跟着 JAR 进了生产环境前面说的数据库连不上的事故具体过程是这样的。同事本地改了application-dev.yml把数据库地址指向自己的开发库项目提交到了 Git。CI 上执行的是mvn clean package注意这里的clean只删target目录源码src/main/resources里的 yml 文件是源文件根本不在 clean 的管辖范围内。Maven 在process-resources阶段会原样复制这个目录于是application-dev.yml先进了target/classes再被spring-boot-maven-plugin打进了 fat JAR 的BOOT-INF/classes下。线上环境一启动Spring Boot 激活了默认 profile读到了这份 dev 配置于是去连一个根本不存在的本地数据库应用起不来。排查的时候把 fat JAR 解压出来那个application-dev.yml就明晃晃躺在里面。这个案例很典型也说明了一个关键点打包前的清理不是可选项而是必要项。它直接决定产物干不干净、线上会不会踩雷。2. 第一板斧maven-clean-plugin 的定向删除2.1 默认只删 target源码目录从来不是它的地盘很多新手以为mvn clean会清理整个项目其实不是。maven-clean-plugin 默认的fileset只有${project.build.directory}也就是通常说的target目录。它打扫的是构建输出不是输入。这一点很符合 Maven 的构建哲学Maven 把源码当作只读输入不让你在构建过程中随意修改src目录。普通项目里所有编译、复制、打包操作产出的东西都在target下所以默认只删target就够了。但当我们想让src/main/resources下某个文件不进产物时默认的 clean 就无能为力了——它根本不会去扫描你的源码目录。这也解释了为什么很多人给 Git 加了.gitignore还是没用.gitignore只管版本控制Maven 压根不认识它。只要文件还在磁盘上在src/main/resources里Maven 就会一视同仁地复制、打包。2.2 filesets 精确指哪打哪maven-clean-plugin 其实提供了扩展能力通过filesets可以指定额外的清理目录。下面这个配置可以让mvn clean在清完target后顺手清掉源码里的开发配置。plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-clean-plugin/artifactId version3.2.0/version configuration filesets fileset directorysrc/main/resources/directory includes include**/application-dev.yml/include include**/application-local.*/include /includes excludes exclude**/application-prod.yml/exclude /excludes /fileset /filesets /configuration /plugin这里directory是清理的目标根目录includes里写要清理哪些文件excludes表示即使命中了includes也不要动。比如清理src/main/resources下的所有application-dev.yml和application-local.*但保留application-prod.yml。需要注意这个 filesets 配置默认只在执行cleangoal 时生效。如果你构建命令写的是mvn package没带clean那这段配置不会执行。所以我建议打包命令统一用mvn clean package一方面保证输出目录干净另一方面让自定义 filesets 有执行机会。2.3 一个安全原则优先删编译产物少动源码用 clean 插件删源码目录里的文件功能上没问题但它有个副作用文件是真的被删除了。开发机上如果做了这次 clean本地再启动项目时就会缺配置你还得手动恢复或者靠 Git 找回。我后来养成了一个习惯能删target/classes里的文件就尽量别删src源码。构建过程已经把源码复制到了target/classes这时候只需要在打包前把目标文件从那里拿走源码原封不动本地开发完全不受影响产物也干净。但这里有个问题maven-clean-plugin 的cleangoal 生来就是做整目录清理的绑到prepare-package阶段执行clean会把整个target清空编译好的 class 全部没了打包肯定失败。它确实不适合用来做“打包前精确删除某几个文件”这种精细操作。这个需求要交给下一把更灵活的板斧。3. 第二板斧maven-antrun-plugin 定点清理3.1 为什么需要 antrunclean 插件的边界在哪maven-antrun-plugin 允许在任意 Maven 生命周期阶段执行 Apache Ant 的任务。Ant 的文件操作能力非常成熟delete任务既支持删单个文件也支持按目录、按通配符批量删除。相比 clean 插件它灵活得多。举个例子我想在打包前把target/classes里的application-dev.yml和static/test-page.html删掉。antrun 插件可以这样写plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-antrun-plugin/artifactId version3.1.0/version executions execution idremove-dev-files-from-output/id phaseprepare-package/phase goals goalrun/goal /goals configuration target delete file${project.build.outputDirectory}/application-dev.yml/ delete quiettrue fileset dir${project.build.outputDirectory}/static includestest-page.html/ /delete /target /configuration /execution /executions /plugin这段配置绑定在prepare-package阶段也就是测试跑完之后、正式打成 JAR/WAR 之前。删除的目标直接指向${project.build.outputDirectory}默认就是target/classes。这样操作的是构建产物不碰源码。3.2 执行时机怎么选prepare-package 最推荐很多人第一次写 antrun 删除任务喜欢把执行阶段写成validate或者process-resources我觉得这需要谨慎。validate阶段是整个生命周期最早的阶段这时候连资源都没复制你只能去删源码目录里的文件。前面说过这样会影响本地开发环境而且如果构建过程中单元测试还需要这些文件直接就挂掉了。process-resources阶段已经完成了资源复制可以删target/classes里的文件了。但这个阶段在compile之前后续单元测试阶段如果代码要读取这些配置会找不到文件。比如 Spring Boot 的SpringBootTest启动时会加载target/classes下的application.yml你要是把基础配置删了测试直接失败。prepare-package则是最稳的位置。它位于test阶段之后、package阶段之前。单元测试已经全部跑完了此时删除多余文件不会影响任何测试行为而删除结果的直接影响范围正好是后续要进入压缩包的内容。这也是我目前最推荐的执行阶段。提示如果你用spring-boot-maven-plugin打 fat JAR它的 repackage goal 默认绑定在package阶段。在prepare-package删除target/classes里的文件能确保 fat JAR 里的BOOT-INF/classes同样不含这些文件。3.3 用 profile 区分环境别一把梭删文件的配置如果写在主 pom 里那意味着任何一次mvn package都会执行删除。开发机上你只是想本地打个包结果application-dev.yml被删了下次启动项目就蒙圈。解决办法是把 antrun 插件放进 Maven profile。只有特定场景才激活这个 profile比如 CI 发布、手动 release或者显式传一个参数进去。我比较喜欢用属性激活的方式因为可控性最强不会因为当前机器上某个环境变量对不对就被意外激活。profiles profile idclean-dev-config/id activation property namecleanDevConfig/name valuetrue/value /property /activation build plugins !-- 把上面的 antrun 插件配置挪到这里 -- /plugins /build /profile /profiles构建命令是这样mvn clean package -DcleanDevConfigtrue不加那个参数的时候mvn clean package一切照旧不删任何东西。加了参数才执行清理逻辑。这套思路在 CI 上尤其好用本地开发随便玩CI 发布走专门参数避免两边行为不一样。3.4 路径、通配符与 Windows 下的坑配置删除路径时有几个细节容易翻车。路径要尽量用 Maven 内置变量不要手写绝对路径。${project.basedir}代表项目根目录${project.build.outputDirectory}代表target/classes。一旦你写了类似D:/workspace/xxx/target/classes这种绝对路径换一台机器、换一个 CI 环境就废了。Ant 通配符规则要分清*匹配当前目录下的文件名**匹配任意层级目录。比如**/*.local.*能匹配a/b/c/x.local.yml也能匹配根目录下的x.local.properties。很多人把*和**混用配置出来明明觉得该删的文件没删就是这个原因。路径分隔符统一用正斜杠/。虽然 Windows 系统习惯反斜杠但 Ant 和 Maven 的路径解析都会把/当成路径分隔符这样写才能做到跨平台一致。还有个小细节是quiet属性。默认情况下Ant 的delete在文件不存在时不会报错这是好事保证任务幂等——重复执行不会有问题。但如果你路径写错了本来想删static/test-page.html结果目录名写成statics删除不会报错文件也没删掉最终产物里那个文件还在。所以我建议在删除任务前面加一行echo把准备删除的文件先打印出来构建日志里能看到真实执行情况。target echo messageDeleting dev files: ${project.build.outputDirectory}/application-dev.yml/ delete file${project.build.outputDirectory}/application-dev.yml/ /target4. 第三板斧打包时排除而不是删除4.1 resources 插件排除最优雅的做法删除的思路是“文件我先拿掉再打包”。其实还有一种更干净的做法在资源复制阶段就告诉 Maven这个文件我不想让它进target/classes连复制都不要复制。这样源文件始终在src/main/resources里开发环境、单元测试都能正常读唯独打包产物里不会有。实现方式是配置buildresources节点build resources resource directorysrc/main/resources/directory excludes exclude**/application-dev.yml/exclude exclude**/test-page.html/exclude /excludes /resource /resources /build这个配置的意思很直白复制src/main/resources下的资源时跳过application-dev.yml和test-page.html。它们不进target/classes后续 packaging 自然也不会把它们收进去。这种方式最大的优点是不破坏任何东西。源码目录一个字节都没动本地开发继续用application-dev.yml单元测试照常加载构建产物干净利落。而且配置可读性很强一眼就能看出哪些文件被排除了方便后面维护。这里有个坑我需要提醒一下。如果你在resources节点显式声明了 resource 配置Maven 默认的资源目录行为会被你的配置覆盖。什么意思呢默认情况下 Maven 会自动把src/main/resources当作资源目录但你一旦自己写了resource标签就必须把src/main/resources也声明进来否则它可能反而不复制这个目录了。上面那段配置里我已经把directory写出来了这是最稳妥的写法。4.2 jar/war 插件的 excludes 配置除了资源阶段排除Maven 的打包插件本身也提供了排除能力。maven-jar-plugin 可以这样配plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-jar-plugin/artifactId configuration excludes exclude**/application-dev.yml/exclude /excludes /configuration /pluginmaven-war-plugin 的手法是packagingExcludesplugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-war-plugin/artifactId configuration packagingExcludes packagingExcludeWEB-INF/classes/**/application-dev.yml/packagingExclude /packagingExcludes /configuration /plugin不过对于 Spring Boot 项目我的建议是不要只依赖这一层。因为spring-boot-maven-plugin的 repackage 过程会重新组织 JAR 结构如果之前文件已经进了target/classes仅仅在 maven-jar-plugin 层做 exclude能不能在 fat JAR 里保持排除效果踩过的坑不少。保险的做法还是优先用 4.1 的资源阶段排除或者用 3.1 的 antrun 在prepare-package阶段直接清target/classes。4.3 “删除”和“排除”到底怎么选这两类思路各有适用场景我整理了一个对比表方式文件是否还存在适合场景主要风险删除源码文件不存在了确定不该出现的开发残留想彻底清掉本地启动缺文件需要配合 profile删除 target/classes 文件源码还在产物里没有打包阶段清理不想影响本地开发阶段选错会影响单元测试资源阶段排除源码还在进不了 target/classes环境差异配置、演示页面、临时资源配置时需完整声明 resources 目录jar/war 插件排除文件和 target/classes 里都还在临时微调打包内容Spring Boot repackage 时效果不稳定我的习惯是资源排除为主prepare-package 删除为辅。对于应用配置这种明确不该上线的文件在resources里排除最省心。对于历史遗留项目里已经混进很多目录、不好逐个排除的情况就上 antrun 在打包前清一把简单粗暴效果也直观。5. 完整实操配置一套可直接抄的 pom.xml5.1 场景设定假设现在有一个 Spring Boot 多环境项目目录结构如下src/main/resources/ ├── application.yml ├── application-dev.yml ├── application-prod.yml └── static/ ├── app.js └── test-page.html目标很明确打 release 包的时候产物里不要出现application-dev.yml和static/test-page.html并且本地开发打包不受影响。5.2 pom 配置分步拆解第一步先用 resources 排除保证它们根本不进target/classes。第二步再配一个 antrun 清理任务放在 profile 里通过-DcleanDevConfigtrue激活作为双重保险。万一有人改了 resources 配置或者资源阶段出了问题打包前还能再兜一层底。完整配置片段我贴在这里插件版本建议固定不要用空版本号。project !-- ... groupId、artifactId 等基础信息省略 ... -- build resources resource directorysrc/main/resources/directory excludes exclude**/application-dev.yml/exclude exclude**/test-page.html/exclude /excludes /resource /resources /build profiles profile idclean-dev-config/id activation property namecleanDevConfig/name valuetrue/value /property /activation build plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-antrun-plugin/artifactId version3.1.0/version executions execution idremove-dev-files-from-output/id phaseprepare-package/phase goals goalrun/goal /goals configuration target echo messageRemoving dev config from target/classes.../ delete file${project.build.outputDirectory}/application-dev.yml/ delete fileset dir${project.build.outputDirectory}/static includestest-page.html/ /delete /target /configuration /execution /executions /plugin /plugins /build /profile /profiles /project这里resources配置保证源文件层面就已经不进产物antrun作为最后一层兜底也方便你看到构建日志里有没有执行删除。5.3 执行与验证jar tf 才是照妖镜构建命令mvn clean package -DcleanDevConfigtrue构建完成后的验证环节特别重要。不要看完 BUILD SUCCESS 就完事一定要确认产物里真的没有那些文件。直接解压 JAR 或者 WAR 查一下是最实在的jar tf target/your-app.jar | grep -E application-dev|test-page如果输出为空说明清理成功。对于 Spring Boot fat JAR路径通常长这样BOOT-INF/classes/application-dev.ymlgrep 的时候一样能搜出来。如果机器上没装 JDK 的jar命令用unzip -l也可以unzip -l target/your-app.jar | grep -E application-dev|test-page再进一步验证产物能不能正常启动。我一般会切到 prod 配置试一下java -jar target/your-app.jar --spring.profiles.activeprod如果应用能正常起来再确认日志里加载的确实是application-prod.yml这就算是把删除工作闭环了。6. 实战排坑几个经常翻车的地方6.1 JAR 里还是出现目标文件这是最常见的排查场景明明配置了删除或者排除解压 JAR 一看文件还在。我自己的排查顺序是固定的。先检查删除动作发生在什么生命周期。如果你配置在validate阶段去删src/main/resources而 Maven 在process-resources阶段复制资源时文件可能还在那它照样进产物。这种情况下即使文件后来被删了也已经复制到target/classes一次了效果为零。正确做法是删除target/classes里的文件绑定prepare-package或者干脆在资源阶段做到不进target/classes。再检查路径和通配符。比如文件在target/classes/static/test-page.html你配置删除${project.build.outputDirectory}/test-page.html目录层级没对上肯定删不到。建议先把目录结构列出来对照着写路径。最后检查构建命令有没有激活对应的 profile。如果你把 antrun 放进了 profile 里但执行mvn clean package时没传激活参数整个删除逻辑压根不会跑JAR 里自然有文件。6.2 把源码目录删没了这个是血泪教训。不要轻易在删除任务里写大范围的目录匹配尤其是对src目录。比如这样一段配置delete fileset dir${project.basedir}/src/main includes**/*.java/ /delete看起来只是想清掉一部分 Java 文件一旦includes写得比预期宽或者dir指到了src根目录整棵树都会被删掉。我见过有人把src/main整个目录删了之后Git 回滚花了大半天。安全起见删除目标尽量精确到文件不要用宽泛目录。如果非要删一批文件先加一段echo打印出 fileset 实际匹配到了哪些文件确认无误再执行。另外平时养成勤提交的习惯git status干净了再跑构建出问题也容易恢复。6.3 Windows 文件占用与只读Windows 下执行构建最经常遇到的就是mvn clean报文件删不掉错误信息里带着 “Access Denied” 或者 “Unable to delete”。原因通常是 target 目录下的某个文件被 IDE 的进程、spring-boot-devtools 的自动重启进程或者javaw.exe占用着。排查方法关掉 IDE 后台缓存和正在运行的java进程确认没有多个构建同时跑。如果是只读文件导致删除失败antrun 里可以设置failonerrorfalse但我不建议这么做。删不掉说明有问题让它报错反而能逼你把原因找出来。真要绕过去手动去文件管理器里把 target 目录删一遍比硬扛配置省心。6.4 本地和 CI 行为不一致本地构建一切正常CI 上产物却不一样这类问题一般出在三个地方。第一CI 工作区是全新克隆的本地那些“需要删除的配置文件”可能根本没被提交到 Git。本地删除任务能找到文件所以删了CI 上文件不存在删除任务什么都不做结果两边看起来行为一致但实际状态不同。我的建议是删除逻辑要幂等文件存在就删不存在就安静跳过不要对未提交文件产生依赖。第二profile 激活条件不一致。如果 profile 是通过环境变量或者本地系统属性激活的CI 上没那个条件整个删除就不会执行。解决办法是统一用显式参数激活像-DcleanDevConfigtrue让本地和 CI 执行同一条命令。第三路径问题。配置里如果写了${user.home}或者本机绝对路径CI 上是另外的环境路径不存在找不到文件。统一改用${project.basedir}、${project.build.outputDirectory}这种 Maven 内置变量从根本上杜绝路径漂移。这篇文章写到这里我回头看了一下自己的习惯其实我现在已经很少在 pom 里用“删除”去处理开发配置了除非是历史遗留项目不好动源码目录否则优先用 resources 排除把规则写在构建脚本里。删除这招更像是在打补丁能解决问题但同时也要接受它带来的副作用文件是真没了想找回来只能靠 Git。可如果项目特别乱Git 都没提交过那些文件那删之前记得先备份一份。我的经验就一句话打包产物里出现的每一个文件都应该有人为它负责要么是显式声明要带的要么就该在构建阶段把它“按下去”。具体用哪招根据自己的项目挑。