1. 项目由来:为什么“打包前先删文件”会成为一个必须解决的问题
先交代一下背景,这是我最近在维护一个遗留项目时踩到的一个真坑。项目本身不算复杂,一个基于 Spring Boot 的常规 web 应用,用 Maven 管理依赖和构建,前端资源由另外的同事维护,最终产物是一个可执行的 jar 包。按理说,“mvn clean package”一把梭,不该有什么幺蛾子,但问题恰恰出在“clean”这件事上。
项目里有一批需要打包但不需要被 Maven 重新编译和拷贝的静态资源,它们散落在 src/main/resources 下面,比如一些业务方临时投放的 H5 活动页面、几份季度运营报告生成的 PDF、还有一批带时间戳的旧版本配置文件模板。这些文件有个共同点:内容会不定期更新,但更新动作不通过代码仓库,而是直接在服务器或本地目录里替换。于是每次发布前,我都得手动确认这些文件是不是最新版,再决定要不要覆盖。
更麻烦的是,某些环境下构建时会自动生成一些临时产物,比如 velocity 模板编译后的中间文件、日志框架在不同环境初始化时产生的调试配置文件,这些文件一旦混进最终的 jar 包,轻则导致启动时读取到错误的配置,重则让整个应用在特定环境下行为异常。最开始我尝试靠 .gitignore 和 maven-resources-plugin 的 excludes 来过滤,但效果不稳定,有时候新加的文件没在 exclude 列表里,就会混进去。
后来项目引入了新的构建流程,要求在正式打包前必须把指定目录下的“过时或不需要的文件”清掉,然后再执行编译和打包。于是“Maven 打包前先删除不需要的文件”这个需求,就从“洁癖”变成了“刚需”。
如果你也遇到过类似情况:mvn clean package 打出来的 jar 里总有一些不该出现的东西,或者干脆编译错误是因为残留的旧文件导致资源重复定义,那这篇文章应该能帮你省下不少排查时间。我会从原理讲起,把常见的几种删除方案、他们的适用场景、还有我实际用下来踩过的坑,一次说清楚。
2. 先搞清楚 Maven 打包的生命周期:clean 和 package 到底各自干了什么
2.1 Maven 生命周期和阶段的关系
在 Maven 里,生命周期是一个个阶段(phase)串起来的,默认有三个生命周期:clean、default 和 site。其中clean 生命周期负责清理,default 生命周期从 validate 开始,依次经过 compile、test、package、install、deploy 等阶段。
大部分人习惯执行mvn clean package,这里的 clean 是一个生命周期,package 是 default 生命周期中的一个阶段。Maven 会把这两个生命周期的阶段按顺序组合执行:先执行 clean 生命周期里的全部阶段(实际上 clean 生命周期就三个阶段:pre-clean、clean、post-clean),再执行 default 生命周期中直到 package 为止的所有阶段。
所以mvn clean package的执行顺序大致是:
- pre-clean:执行一些清理前要做的准备,实际上很少配置
- clean:删除 target 目录
- validate:校验项目信息是否完整
- initialize:初始化构建状态
- generate-sources:生成源码
- process-sources:处理源码
- generate-resources:生成资源文件
- process-resources:把 src/main/resources 下的资源拷贝到 target/classes
- compile:编译 src/main/java 下的 Java 源码
- process-test-resources
- test-compile
- test
- prepare-package
- package:打 jar 或 war
关键点来了:clean 只删除 target 目录。如果你有一些“不需要的文件”存放在 src/main/resources 或 src/main/webapp 下,clean 根本不会碰它们。所以单纯执行mvn clean package,并不能保证最终产物里只有你希望的文件。
2.2 为什么不能只靠 clean 解决
有人会想:那我每次打包前手动把 target 删掉,再把不需要的文件从源码目录移走,不就行了吗?
短期可以,但长期不可行:
- 手动操作容易漏,特别是文件多的时候,你根本记不住哪些该留哪些该删
- 如果文件是通过某种自动化脚本或 CI/CD 流程生成的,手动删除就没法保证可重复性
- 团队协作时,每个开发者的本地环境可能残留不同状态的文件,手动清理只能救一时,不能根治
所以我们需要的是把“删除不需要的文件”这件事固化到 Maven 的构建流程里,让它在 package 之前自动执行,并且可配置、可维护。
3. 删除方案全景对比:maven-clean-plugin 插件、antrun 插件、maven-resources-plugin 过滤
3.1 方案 A:用 maven-clean-plugin 的 clean 目标配置 fileset
这个方案的核心思路是:复用 clean 生命周期的 clean 目标,但通过配置 fileset,让它除了删除 target 目录,还删除你指定的文件。
原理其实很简单。maven-clean-plugin 在执行 clean 时,默认会把项目构建输出目录(一般是 target)删掉。但它的<filesets>配置允许你指定多个<fileset>,每个 fileset 可以定义一个目录和一组包含/排除规则,Maven 会按照这些规则删除匹配到的文件。
具体配置长这样:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-clean-plugin</artifactId> <version>3.2.0</version> <configuration> <filesets> <fileset> <directory>src/main/resources/static/tmp</directory> <includes> <include>**/*.tmp</include> <include>**/*.bak</include> </includes> <followSymlinks>false</followSymlinks> </fileset> <fileset> <directory>src/main/webapp/old-pages</directory> <includes> <include>**/*</include> </includes> </fileset> </filesets> </configuration> </plugin>这个方案的优点:
- 语义非常清晰,看到 fileset 就知道是清理文件
- 和 clean 生命周期天然绑定,不需要额外指定执行阶段
- 支持 includes/excludes 规则,可以精确控制哪些文件删、哪些保留
缺点和坑:
- 它在 pre-clean 阶段执行,而不是在 package 之前执行。如果你希望在 process-resources 之后、compile 之前删除某些文件,这个方案就不行了
- 如果文件是构建过程中生成的(比如某些代码生成插件先产生了文件,然后你想删掉其中一部分),fileset 就无从下手
- 对 Windows 环境下的文件锁定、权限问题比较敏感,删除失败会直接中断构建
我的实际体验:如果你只是想“每次构建前清掉一两个已知的坏文件”,这个方案最直接。但如果删除逻辑需要依赖构建过程中的状态,就得用方案 B。
3.2 方案 B:用 maven-antrun-plugin 执行 delete 任务
maven-antrun-plugin 的作用是在 Maven 构建中运行 Ant 的任务。Ant 本身就是一个以任务为中心的构建工具,它的 delete 任务非常灵活,支持文件集、目录、正则匹配等方式删除文件。
配置示例:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-antrun-plugin</artifactId> <version>3.1.0</version> <executions> <execution> <id>delete-unwanted-files</id> <phase>process-resources</phase> <goals> <goal>run</goal> </goals> <configuration> <target> <delete verbose="true"> <fileset dir="${project.build.outputDirectory}" includes="**/old-config/**/*.xml" /> <fileset dir="${project.basedir}/src/main/resources/static" includes="**/*.orig" /> </delete> </target> </configuration> </execution> </executions> </plugin>这个方案之所以是我最终的“主力方案”,因为它有两个 maven-clean-plugin 不具备的优点:
- 可以绑定到任意 phase。比如绑定到 process-resources,就能保证在资源拷贝完成之后、编译开始之前删除文件。这样你可以先让某些插件生成文件,再针对生成结果做清理。
- 可以使用 ${project.build.outputDirectory}、${project.basedir} 等 Maven 属性,配合 Ant 的文件集语法,删除目标很精确。
Ant 的 fileset 语法和 maven-clean-plugin 略有不同,它用的是<fileset dir="..." includes="..."/>,includes 里用逗号分隔多个模式,也支持**/*.xml这种通配。
实际踩坑提醒:
- Ant 的 delete 任务默认不删除空目录,如果你想连目录一起删,要加
<includeEmptyDirs="true"/>或者用delete dir="..."直接删目录 <delete verbose="true"/>会在构建日志里列出每个被删除的文件,调试时非常有用,建议加上- 如果文件是只读的,Ant 默认删除会失败,需要设置
<delete failonerror="false"/>或者先把只读属性去掉
3.3 方案 C:用 maven-resources-plugin 的 excludes 过滤资源
这个方案不是“删除”,而是在拷贝资源时把不需要的文件过滤掉,效果上等价于删除,但实现思路完全不同。
process-resources阶段会把src/main/resources下的资源拷贝到target/classes,如果你在 maven-resources-plugin 里配置<excludes>,那么匹配的文件根本不会进入输出目录。
配置示例:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <version>3.3.1</version> <configuration> <excludes> <exclude>**/*.orig</exclude> <exclude>**/unused/**/*</exclude> </excludes> </configuration> </plugin>这个方案的优势:
- 不需要额外执行删除动作,性能好
- 不容易受文件权限影响
- 过滤规则集中管理,一目了然
但它有个致命的局限:只对通过 process-resources 拷贝的资源生效。如果文件是构建过程中由别的插件直接生成到输出目录的,或者你希望打包时某些文件保留源文件、又要删除某部分,excludes 就无能为力了。另外,如果你不是把文件放在src/main/resources,而是直接放在项目根目录或者 webapp 目录,这个方案也不适用。
3.4 三种方案怎么选
我做了个对比表,方便你按项目情况选:
| 维度 | maven-clean-plugin fileset | maven-antrun-plugin delete | maven-resources-plugin excludes |
|---|---|---|---|
| 执行时机 | pre-clean 阶段 | 可绑定任意 phase | process-resources 阶段 |
| 能否删除生成的文件 | 否 | 能 | 否 |
| 对只读文件的处理 | 容易失败 | 可配 failonerror | 不需要删,无影响 |
| 配置复杂度 | 低 | 中 | 低 |
| 适用场景 | 固定目录清理 | 复杂删除逻辑、动态生成后的清理 | 过滤拷贝资源 |
| 日志可观测性 | 一般 | verbose 模式强 | 一般 |
我的选择逻辑很简单:如果只是固定删几个目录,优先用方案 A;如果删除逻辑跟构建过程有交互,比如某个插件先生成文件、你又想删掉其中一部分,或者你想在 process-resources 之后对 target/classes 里的内容做清理,用方案 B;如果只是不希望某些资源被打进去,根本不用删除,用方案 C 就够了。
4. 实操笔记:我用 maven-antrun-plugin 在 process-resources 阶段删除特定文件的全过程
4.1 场景复现
我遇到的实际场景是这样的:
业务侧每周会在src/main/resources/pages下投放一批新的活动 H5 页面,但目录里会残留上上周的旧页面,只有总监口头通知“这次不要那些旧的”。同时,静态资源目录下有个config/version.json,是运维在服务器上手动更新的版本号文件,这个文件每次打包都不该出现在 jar 里,但它在源码目录里确实存在,只是会被其他脚本替换。
也就是说,我需要:
- 删除
src/main/resources/pages下所有非本周投放的 HTML 文件 - 删除
src/main/resources/config/version.json - 这些操作必须在 process-resources 之后执行,因为有些文件可能是资源插件在 process-resources 阶段从其他目录拷贝进来的
4.2 配置过程
第一步,在 pom.xml 的<build><plugins>里加上 maven-antrun-plugin:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-antrun-plugin</artifactId> <version>3.1.0</version> <executions> <execution> <id>cleanup-before-package</id> <phase>process-resources</phase> <goals> <goal>run</goal> </goals> <configuration> <target> <!-- 删除旧活动页面,保留本周的 --> <delete verbose="true"> <fileset dir="${project.basedir}/src/main/resources/pages"> <include name="*.html"/> <exclude name="2025-03-*.html"/> </fileset> </delete> <!-- 删除不想打进 jar 的版本文件 --> <delete file="${project.basedir}/src/main/resources/config/version.json" failonerror="false"/> </target> </configuration> </execution> </executions> </plugin>第二步,执行打包:
mvn clean process-resources注意我这里没有直接用mvn clean package,而是先单独跑 process-resources 验证删除逻辑,因为一旦 package,整个构建流程很长,出了问题不好定位。先确认删除日志里列出了预期的文件,再跑全量打包。
第三步,全量打包:
mvn clean package -DskipTests执行后从日志里能看到类似这样的输出:
[INFO] --- antrun:3.1.0:run (cleanup-before-package) @ my-project --- [INFO] Deleting: /Users/me/work/my-project/src/main/resources/pages/2025-02-28-activity.html [INFO] Deleting: /Users/me/work/my-project/src/main/resources/pages/2025-03-01-activity.html [INFO] Deleting: /Users/me/work/my-project/src/main/resources/config/version.json看到这几行,说明删除成功了。最后我去target/classes里确认,不再有这些文件,jar 包里的结构也干净了。
4.3 过程中踩过的坑和解决方案
坑一:删除源文件后,后续反复打包会出问题
因为process-resources阶段会先把src/main/resources下的文件拷贝到target/classes,如果你在 process-resources 阶段删除了源文件,那么下次构建时,源文件已经不在了,拷贝的时候自然也就没有。这本身不是问题,问题在于如果删除失败,比如文件被占用,整个构建会中断。
我的解决办法:把failonerror设置为 false,这样删除失败时只记录警告,不会让构建中断。但要注意,如果删除失败而文件还在,可能打包出错误的内容,所以日志要认真看。
坑二:删除源目录文件会让 IDE 里的文件消失,影响他人
如果你是在本地开发环境执行这个删除,那么源文件确实会被删掉。即使 Git 里还有备份,同事的 IDE 里也会出现文件缺失提示。我在踩过这个坑之后,改用了一种更安全的方式:只删除输出目录里的文件,不动源目录。
具体的做法是,在 process-resources 阶段,先让资源插件正常拷贝,然后用 antrun 删除target/classes里对应的文件。这样源码目录始终保留完整,只是打包内容里没有这些文件。
对应的配置改为:
<fileset dir="${project.build.outputDirectory}/pages"> <include name="*.html"/> <exclude name="2025-03-*.html"/> </fileset>这样构建结束之后,target/classes/pages里是干净的内容,但src/main/resources/pages里所有文件都还在。如果运维需要更新某个文件,直接更新源目录,下次打包时自动生效。这是我最推荐的模式。
坑三:Ant 的 fileset 默认区分大小写,过滤器要写准确
Ant 的 includes/excludes 模式是区分大小写的,Windows 上常见的问题是文件名大小写不一致,导致排除规则不生效。建议在编写规则时统一用小写,并在测试阶段故意放几个不同命名方式的文件来验证。
4.4 为什么选择 process-resources 而不是其他阶段
我把绑定阶段选为process-resources,理由如下:
- 如果绑定到 prepare-package,那么 process-resources 之后,compile 阶段可能会重新生成某些文件,你删掉的又回来了
- 如果绑定到 compile 之后,某些插件会在 process-resources 和 compile 之间把文件重新写进输出目录,这时候删除可能滞后
process-resources是资源处理的最后一个官方阶段,删除操作可以覆盖所有经过资源处理的文件,同时不会影响后续编译(编译阶段一般只关心 class 文件)
简单说,这是一个“刚刚好”的位置:资源拷贝完成,编译尚未开始,这时候清理是最干净的。
5. 进阶玩法:结合 profile 实现“测试包保留、正式包删除”
5.1 需求来源
有一次,测试环境反馈说正式包和测试包行为不一致,查了半天发现是某个配置文件导致。我们的做法是:测试包必须保留config/debug.yaml,方便测试人员调试;但正式包绝对不能出现这个文件,否则会有安全隐患。
这种需求用单一配置很难兼顾,最合理的做法是引入 Maven profile,让不同环境下的构建执行不同的删除逻辑。
5.2 配置示例
<profiles> <profile> <id>release</id> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-antrun-plugin</artifactId> <version>3.1.0</version> <executions> <execution> <id>remove-debug-config</id> <phase>process-resources</phase> <goals> <goal>run</goal> </goals> <configuration> <target> <delete file="${project.build.outputDirectory}/config/debug.yaml" failonerror="false"/> </target> </configuration> </execution> </executions> </plugin> </plugins> </build> </profile> </profiles>打包命令:
# 测试包,不启用 release profile mvn clean package -DskipTests # 正式包,启用 release profile mvn clean package -DskipTests -Prelease执行效果:
- 不启用 release profile 时,
debug.yaml会打进包 - 启用 release profile 时,
process-resources阶段会把target/classes/config/debug.yaml删掉,正式包就不含这个文件
这个方法非常实用,而且不破坏源码目录,也不会影响开发时本地调试。
5.3 另一个进阶思路:在 package 阶段直接修改 jar 内容
如果你希望删除动作发生在 jar 已经打好之后,也可以用maven-antrun-plugin在 package 阶段执行 jar 操作,但这种方式更复杂,而且容易因为 jar 被占用而失败。我的建议是:尽量在打包之前把文件从输出目录里清掉,而不是打完之后再去解包删文件。
这也是为什么我推荐在 process-resources 阶段做删除的根本原因:Maven 的 war 或 jar 插件在 package 阶段打包时,使用的是target/classes或target/${project.build.finalName}等目录下的内容,只要这些目录里没有你不要的文件,最终产物自然就是干净的。
6. 关于删除操作对开发环境和 CI/CD 的影响
6.1 本地开发:删除源文件的后果
这一点值得单独拿出来说。许多新手习惯在src/main/resources下直接配置删除文件,结果运行mvn package时,自己代码里引用的资源被删了,编译直接失败,或者在 IDE 里突然发现一堆文件“消失”了。
我遇到过最典型的情况:一位同事配置了删除src/main/resources/application.yml的规则,结果每次构建都会把配置文件删掉,然后本地启动 Spring Boot 应用时因为找不到配置文件直接崩溃。他把问题归咎于“Maven 打包有问题”,实际上完全是删除规则写错了。
所以我再次强调:除非你有非常特殊的需求,否则永远不要删除源码目录下的文件。要删,就删输出目录里的那份,源码目录保持完整。
6.2 CI/CD 环境下的注意点
CI/CD 环境一般是从代码仓库拉取最新代码,然后执行构建。这时候源码目录是全新的,删除源文件的风险相对较小,但仍需注意:
- 构建服务器上可能有缓存目录,如果删除的是相对路径的源文件,可能影响缓存一致性
- 多模块项目里,父模块的删除配置会影响子模块,要注意模块之间的文件引用关系
- 如果是并行构建多个分支,删除共享目录下的文件可能导致构建冲突
这些问题排查起来成本很高,强烈建议在 CI 流程里加上构建日志的删除记录项,比如开启 antrun 的 verbose,方便在出问题的时候快速定位。
7. 常见问题排查清单与速查表
7.1 删除不生效怎么办
这是最高频的问题,一般有三种原因:
原因一:phase 绑定不对
删除操作绑定的阶段在打包阶段之后,比如你绑定到 install,但执行的是mvn package,那删除根本不会执行。请确认自己执行的命令覆盖了你绑定的阶段,比如绑定到process-resources,至少执行mvn process-resources或更后面的阶段。
原因二:路径写错了
Ant fileset 的 dir 是相对 Maven 项目根目录还是相对于模块目录,容易搞混。在多模块项目里,execution 默认在模块目录下执行,如果你写了src/main/resources,实际上可能指向子模块的路径,与预期不一致。
原因三:includes 模式不匹配
Ant 的 includes 模式和纯正则不一样,*.html只匹配当前目录下的文件,不递归子目录;如果想匹配任意子目录下的 html,要写**/*.html。我见过不少把*.html写成**/*.html之后反而一个都没匹配到的例子。
7.2 删除文件时报权限不足
Windows 下比较常见,尤其当文件被 IDE 或其他进程占用时。解决思路:
- 关闭占用文件的程序
- 给 pom.xml 里的删除配置加上
failonerror="false",让构建继续,但要在日志里确认文件是否真的删掉了 - 或者在构建前用系统命令清理,但不推荐,因为不够自动化
7.3 删除后文件又变回来了
原因是:删除操作之后,又有某个插件把文件重新写入输出目录。例如有些代码生成插件在 generate-resources 阶段生成文件,而你把删除操作绑定在 process-resources 之前,删除的优先级不够,导致文件“复活”。
解决方案:把删除操作绑定到prepare-package阶段,虽然不如 process-resources 干净,但可以确保在所有 generate 之后执行。当然,前提是编译期不会再生成这些文件。
7.4 删除整个目录而不是文件
Ant delete 支持删除目录:
<delete dir="${project.build.outputDirectory}/old-static" failonerror="false"/>注意,这个操作会删除整个目录,包括目录内所有文件。使用前请确认这个目录确实是目标目录。
7.5 常见问题速查表
| 现象 | 可能原因 | 解决措施 |
|---|---|---|
| 删除不生效 | phase 绑定错误 | 调整 execution 的 phase |
| 删除不生效 | 路径错误 | 用${project.basedir}或${project.build.outputDirectory}明确路径 |
| 删除不生效 | includes 写错 | 用**/*.xxx代替*.xxx |
| 构建失败 | 文件被占用 | 关闭占用程序或加 failοnerrοr=false |
| 删除后又出现 | 阶段顺序不对 | 绑定到 prepare-package 或调整插件顺序 |
| 源文件被误删 | 配置了源目录删除 | 改为删除输出目录文件 |
8. 最终总结:Maven 打包前删除文件的完整方法论
整个过程做下来,其实核心就是一句话:把“删除”从人工操作变成构建流程的一部分,并且尽量只动输出目录,不动源码目录。
对我来说,最顺手的组合是 maven-antrun-plugin + process-resources 阶段,理由是它的修改方式最灵活、执行时机最合适、调试也方便。如果项目相对简单,固定清理几个目录,可以直接用 maven-clean-plugin 的 fileset 省事。如果只是不想让某些资源进包,那 maven-resources-plugin 的 excludes 更干净。
最后再补充三个我踩坑后总结的小技巧:
- 配置删除规则时,永远先跑
mvn process-resources而不是直接mvn package,看到日志里打印了预期删除的文件名再继续 - 删除规则尽量用输出目录变量
${project.build.outputDirectory},避免手动拼接完整路径导致模块迁移后失效 - 不要在一个 pom 里堆太多删除规则,如果文件特别多,建议用 profile 或者外部脚本管理,降低维护成本
希望这篇笔记能帮大家少走弯路。如果你也在为“Maven 打包时混入多余文件”头疼,建议先检查自己项目里用的是什么资源目录、哪些文件是动态生成的,再决定用哪种删除策略,基本上就不再需要手动删文件了。