面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

服务端阅读 05月31日 23:58

什么是 Maven?它解决了 Java 项目构建里的哪些问题?

Maven 是 Java 生态里最常用的项目构建和依赖管理工具之一。它用一个 pom.xml 描述项目坐标、依赖、插件和构建流程,然后按约定完成编译、测试、打包、安装和发布。以前手工下载 jar、手写编译脚本、每个项目目录结构都不一样,Maven 解决的正是这些重复又容易出错的问题。Maven 的核心概念有四个:POM、坐标、仓库和生命周期。POM 是项目对象模型,坐标由 groupId、artifactId、version 标识一个构件。仓库分本地仓库和远程仓库,依赖会先查本地,找不到再从远程下载。生命周期则规定构建顺序,插件负责在具体阶段执行真正的任务。<project> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>hello-maven</artifactId> <version>1.0.0</version> <dependencies> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.14.0</version> </dependency> </dependencies></project>Maven 的工作方式Maven 的一个重要价值是让项目可以被重复构建。只要源码、POM、仓库和 JDK 环境一致,团队成员和 CI 理论上应该得到相同产物。它不会要求每个人手动下载同一批 jar,也不会让编译命令散落在各个脚本里。对团队协作来说,这种确定性比少写几行 XML 更重要。依赖解析是 Maven 最常被使用、也最容易被误解的能力。Maven 会根据坐标下载直接依赖,也会解析这些依赖背后的传递依赖,并按规则选择最终版本。好处是接入一个框架很快,代价是你必须会看依赖树,否则遇到版本冲突时只能靠猜。Maven 也不是只适合简单项目。多模块、父子 POM、BOM、插件管理和 profile 能支撑很复杂的工程,但复杂度应该逐步引入。一个新项目先保持标准目录和少量依赖,等确实出现版本统一、模块拆分或多环境构建需求时,再把高级配置加进去,维护成本会低很多。学习 Maven 时不要只背命令,更要理解它把项目描述、依赖解析和构建流程拆开了。POM 负责描述项目是什么,仓库负责找到需要的构件,生命周期负责规定什么时候做什么。这个模型一旦理解,后面看多模块、scope 或插件配置就不会觉得零散。实际排错时也按这三块拆开看,定位会快很多。这也是 Maven 面试题常从概念追到依赖、生命周期和插件的原因。追问Maven 和手动管理 jar 最大区别是什么?手动管理 jar 看似直接,但版本来源、传递依赖、冲突排查都靠人记,很容易失控。Maven 把依赖声明成坐标,并通过仓库自动解析传递依赖,团队成员拿到代码后可以用同一套方式构建。它的取舍是引入了 POM、生命周期、插件这些学习成本。项目越大、协作越多,Maven 的标准化收益越明显。POM 里最重要的配置有哪些?最基础的是 groupId、artifactId、version,它们决定项目产物的唯一坐标。然后是 <dependencies>,用于声明项目真正需要的依赖。复杂项目还会用 <dependencyManagement> 管版本,用 <build> 和插件配置编译参数、打包方式和测试行为。踩坑点是把所有配置都塞进一个 POM,短期省事,后期多模块复用和排查会很痛苦。Maven 的“约定优于配置”有什么边界?Maven 默认假设源码在 src/main/java,测试在 src/test/java,资源在 src/main/resources,产物放到 target。遵守这些约定时,POM 可以很短,团队也容易理解项目结构。边界是老项目或特殊生成代码项目可能不符合默认目录,这时可以配置插件,但配置越多,项目越不容易被新人接手。除非确实有历史包袱,否则不要轻易挑战 Maven 的默认目录。mvn compilemvn testmvn clean packagemvn installMaven 插件到底负责什么?生命周期只是流程名,插件才是真正执行编译、测试、打包、复制资源的组件。比如编译由 compiler 插件完成,测试常由 surefire 插件执行,打包 jar 由 jar 插件完成。你可以通过插件配置 Java 版本、编码、测试包含规则等细节。常见坑是 Maven 版本、插件版本和 JDK 版本不匹配,导致本地和 CI 的构建结果不同。Maven 有哪些优势,又有哪些不适合的地方?它的优势是标准项目结构、成熟仓库生态、自动依赖解析、多模块构建和 CI 友好。对于绝大多数 Java 后端项目,这些能力已经足够稳定。它的不足是 XML 配置较啰嗦,复杂插件调试不够直观,极端定制构建可能没有 Gradle 灵活。取舍上,如果团队更看重稳定、统一和生态兼容,Maven 仍然是很稳的选择;如果构建逻辑高度动态,才需要认真比较其他工具。
服务端阅读 05月31日 23:58

Maven scope 有哪些类型?什么时候该用 compile、provided 或 runtime?

Maven 的 scope 决定一个依赖在哪些阶段可见:编译、测试、运行、打包,以及是否会传递给下游项目。它不是简单的分类标签,而是在控制 classpath 边界。scope 配错时,轻则最终包变大,重则本地能跑、服务器启动就报类找不到。最常见的是 compile,它是默认范围,编译、测试、运行都可用,也会传递。provided 表示编译和测试需要,但运行时由 JDK、容器或平台提供。runtime 表示编译时不需要,运行和测试时需要。test 只在测试代码中可见,不会进入正式产物。system 需要指定本地 jar 路径,几乎不推荐。import 只能用于 dependencyManagement,常用来导入 BOM。<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.34</version> <scope>provided</scope></dependency><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> <scope>runtime</scope></dependency>选择 scope 的判断方法判断 scope 时可以先问三个问题:编译主代码是否需要它,运行产物是否需要它,下游项目是否应该感知它。三个答案都是“是”,通常就是 compile;只在测试里用,就是 test;编译要用但运行环境提供,就是 provided;编译不直接用但运行要加载,就是 runtime。这个判断比死记六种 scope 更可靠。打包方式也会影响 scope 的效果。普通 jar 项目不会把依赖直接塞进 jar,Spring Boot 这类可执行包会重新打包依赖,Web 项目生成 war 时又要考虑容器提供的库。比如 servlet-api 在传统 war 中适合 provided,但在嵌入式容器的可执行 jar 场景下,相关依赖可能由 starter 间接带入。边界不是某个依赖永远用某个 scope,而是看运行环境谁负责提供它。排查 scope 问题时,除了看 POM,还要看最终产物。可以用 mvn dependency:tree 看解析结果,用 jar tf target/app.jar 或查看 WEB-INF/lib 确认依赖是否被打包。很多线上问题不是依赖没声明,而是 scope 让它没有进入最终运行路径。在多模块项目里,scope 还会影响下游模块的 classpath。一个公共 api 模块如果把实现库设为 compile,下游会被迫继承这个选择;如果设成 provided 或 optional,又可能需要下游自己补依赖。这个边界要在模块设计时想清楚,不能等打包失败后再临时改。对库作者来说,scope 既是构建配置,也是对使用方的一种契约。因此评审 POM 时,scope 应该和依赖版本一样被认真检查。尤其是从 war 迁移到可执行 jar 时,原来合理的 provided 可能就需要重新评估。否则 scope 看起来只是一个小字段,实际却会改变整个运行包的内容。追问compile scope 为什么是默认值?大多数业务代码依赖的库既要参与编译,也要在运行时使用,所以 Maven 把 compile 设为默认值。比如工具类库、核心框架 API、业务 SDK 通常都适合 compile。它的代价是会向下游传递,库项目如果滥用 compile,会把很多不必要的依赖带给使用方。取舍原则是:只有公共 API 暴露出去的类型,才更有理由使用 compile。provided 适合哪些场景?provided 适合运行环境已经提供该依赖的情况,例如 Servlet API 由 Tomcat 提供,Lombok 只在编译期生成代码。这样可以避免把容器已有的 jar 再打进应用,减少冲突。边界是你必须确定运行环境真的有这个依赖,而且版本兼容。最常见的坑是本地测试靠 Maven classpath 能跑,部署到精简容器后缺包。<dependency> <groupId>jakarta.servlet</groupId> <artifactId>jakarta.servlet-api</artifactId> <version>6.0.0</version> <scope>provided</scope></dependency>runtime 和 compile 怎么区分?如果代码编译时只依赖标准接口,具体实现到运行时才需要,就适合 runtime。JDBC 驱动是典型例子,业务代码引用的是 java.sql 接口,不需要在编译期直接使用 MySQL 驱动类。这样做能让编译 classpath 更干净,也避免把实现细节误暴露给上游模块。踩坑点是某些框架启动时会反射加载驱动,scope 写成 test 或漏配时,编译没问题但启动失败。test scope 会不会传递给正式代码?不会,test 只在测试编译和测试运行阶段可见,不会参与正式打包,也不会传递给依赖你的项目。JUnit、Mockito、AssertJ 这类测试库都应该放 test。取舍点是测试工具不要污染生产运行环境,尤其是 mock、内嵌服务器和测试容器库。常见坑是把测试辅助类放进 src/main/java,导致主代码反过来依赖 test scope,编译直接失败。import scope 为什么只能配合 dependencyManagement?import 的作用是导入另一个 POM 的依赖管理配置,本质上是把一组版本约束合并进当前项目。它不是普通 jar 依赖,所以不能放在常规 <dependencies> 中使用。Spring Boot 项目常通过导入 BOM 统一 Spring、日志、Jackson 等版本,避免手动拼版本矩阵。边界是 BOM 只管版本,不保证你的代码兼容升级后的行为,升级前仍然要跑测试和依赖树检查。
服务端阅读 05月31日 23:58

Maven 依赖传递如何工作?冲突版本该怎么排查?

Maven 依赖传递的意思是:你的项目依赖 A,而 A 又依赖 B,那么 B 通常会自动进入你的项目 classpath。这个机制省掉了大量手工拷 jar 的工作,但也会带来版本冲突、重复依赖和运行时类不兼容。排查 Maven 依赖问题时,不要先猜哪个 jar 错了,先看依赖树。Maven 决定冲突版本时主要看两个规则:路径近的优先,路径一样近时先声明的优先。比如项目同时通过两条路径引入 guava:30 和 guava:32,Maven 只会选一个版本进入最终依赖。这个选择不一定符合业务预期,所以大型项目通常会用父 POM 或 BOM 统一锁版本。mvn dependency:treemvn dependency:tree -Dincludes=com.google.guava:guavamvn dependency:analyze排查依赖冲突的顺序依赖冲突不要从删除 jar 开始,应该先确定最终 classpath 里到底用了哪个版本。第一步看 mvn dependency:tree,确认冲突依赖来自哪条路径;第二步看业务报错,是编译期缺类、运行时缺方法,还是类重复导致的加载问题;第三步再决定是直接声明版本、使用 dependencyManagement,还是对某个上游依赖做 exclusion。版本统一也不是越新越好。安全漏洞修复通常要求升级,但框架生态会对版本组合有约束,例如 Spring Boot 管理的 Jackson、Netty、Logback 版本最好优先跟随官方 BOM。强行把其中一个依赖单独升到很新的版本,短期能过扫描,长期可能引入二进制兼容问题。在库项目里尤其要注意依赖暴露。你的公共 API 如果返回了某个第三方类型,下游就被迫感知这个依赖版本;如果只是内部实现使用,尽量不要让它泄漏到 API 签名中。这个边界处理不好,依赖冲突会从一个项目扩散到所有使用方。还有一种隐蔽问题是依赖范围带来的传递差异。上游把某个库标成 optional 或 provided,下游可能并不会自动拿到它。此时不要盲目责怪 Maven,先确认这个依赖到底是不是上游希望暴露给你的运行时依赖。这类依赖最好在模块说明里写清楚,否则使用方很难判断该不该自己声明版本。追问Maven 解决依赖冲突时为什么是“最近优先”?最近优先可以让直接依赖更容易表达项目意图,因为离当前项目越近,通常越能代表当前项目真正需要的版本。它的好处是规则简单、可预测,坏处是版本选择可能被某个中间依赖无意改变。比如新增一个 starter 后,依赖路径变短,最终使用的日志库版本就可能变了。踩坑时不要只看 pom.xml 的直接依赖,要用依赖树确认最终生效版本。dependencyManagement 能不能直接引入依赖?不能,它只负责管理版本和范围,不会把依赖自动加入项目。子模块仍然需要在 <dependencies> 中声明 groupId 和 artifactId,才会真正使用这个依赖。这个设计的取舍是牺牲一点书写便利,换来模块依赖边界清晰。常见错误是只在父 POM 的 dependencyManagement 里加了版本,然后发现代码里还是找不到类。<dependencyManagement> <dependencies> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>33.2.1-jre</version> </dependency> </dependencies></dependencyManagement>什么时候该用 exclusions 排除传递依赖?当某个传递依赖明确不应该进入当前项目,或者它引入了冲突版本时,可以用 <exclusions>。但排除依赖不是越多越好,因为你可能把上游库运行时真正需要的类排掉,最后变成 ClassNotFoundException。更稳的做法是先用依赖树确认来源,再排除具体坐标,而不是大面积排掉一组库。边界是安全类、日志实现、旧版 JSON 库这类常见冲突点适合谨慎排除。<dependency> <groupId>com.example</groupId> <artifactId>legacy-sdk</artifactId> <version>1.0.0</version> <exclusions> <exclusion> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> </exclusion> </exclusions></dependency>BOM 和父 POM 管版本有什么区别?父 POM 可以管理依赖版本、插件版本、属性和构建配置,适合公司内部统一工程规范。BOM 通常只通过 import scope 导入一批依赖版本,适合 Spring Boot、Spring Cloud 这类生态统一版本矩阵。取舍上,如果你只想复用版本,不想继承别人的插件和构建约定,BOM 更轻。踩坑点是多个 BOM 同时导入时,顺序会影响相同依赖的最终版本。依赖冲突一定会在编译期暴露吗?不一定,很多冲突只会在运行时出现,例如 NoSuchMethodError、NoClassDefFoundError 或序列化行为异常。编译期只说明当前源码能找到方法,不代表运行时加载的 jar 版本完全匹配。边界在于 Java classpath 最终只加载一个同名类,多个版本并存时谁先生效会影响结果。生产事故里最怕的就是测试环境和线上依赖树不同,所以发布前最好在 CI 中固定 Maven 版本、仓库源和构建参数。
服务端阅读 05月31日 23:58

Maven 生命周期有哪些阶段?执行命令时到底跑了什么?

Maven 生命周期可以理解为一条已经排好顺序的构建流水线。你输入的不是“执行某个脚本”,而是告诉 Maven 要跑到哪个阶段,Maven 会把这个阶段之前的阶段一起执行。最常用的是 default 生命周期,另外还有 clean 生命周期和 site 生命周期,它们彼此独立。clean 生命周期负责清理构建产物,典型命令是 mvn clean,会删除 target 目录。default 生命周期负责从校验、编译、测试到打包、安装、发布的主流程。site 生命周期负责生成项目站点文档,业务项目里用得少,但开源库发布文档时仍然有价值。default 生命周期里高频阶段包括 validate、compile、test、package、verify、install、deploy。执行 mvn package 会先跑到测试再打包,执行 mvn install 会在打包后把产物安装到本地仓库,执行 mvn deploy 则继续上传到远程仓库。mvn cleanmvn testmvn clean packagemvn clean install -DskipTestsmvn clean deploy -Prelease命令执行时要看清边界Maven 命令由生命周期阶段、插件目标、参数和 profile 共同决定。mvn clean package 里既有 clean 生命周期,也有 default 生命周期的 package 阶段;mvn dependency:tree 则是直接执行插件目标,不会自动跑完整编译流程。理解这个区别后,很多“为什么这个命令没有编译代码”的疑问就能解释清楚。CI 里通常不会只跑 package,而会选择 verify 或 deploy。verify 更适合质量门禁,因为它给集成测试、静态检查、覆盖率校验留了位置;deploy 则适合发布流水线,前提是仓库凭证和版本策略已经配置好。取舍点是本地越快越好,CI 越确定越好,所以两边命令不必完全一样。还有一个容易忽略的点是 profile。相同的 mvn clean package,加上 -Pprod 后可能启用不同资源、插件或依赖,最终产物也会不同。排查构建差异时,要同时记录 Maven 版本、JDK 版本、命令参数和激活的 profile,不要只说“我也是 package”。如果项目里有集成测试,还要区分 surefire 和 failsafe。前者通常跑单元测试,后者更适合绑定到 integration-test 和 verify。这样可以把快速反馈和完整验证拆开,避免每次本地小改动都等待慢测试。本地调试时可以先用较短阶段确认问题,合并前再交给 CI 跑完整验证。追问mvn package、install 和 deploy 有什么区别?package 只是在当前项目里生成 jar、war 等产物,通常放在 target 目录下。install 会把产物安装到本机 Maven 仓库,供本机其他项目依赖,所以多模块以外的本地联调经常需要它。deploy 会把产物发布到远程仓库,影响团队其他人和 CI 环境,边界更重。踩坑点是把 install 当成发布,结果别人机器上根本拿不到你的包。为什么执行某个阶段会连前面的阶段一起跑?生命周期的设计就是保证构建状态可靠,不能在没有编译的情况下直接测试,也不能在没测试的情况下默认打包。比如 mvn verify 会跑过 validate、compile、test、package 等前置阶段。这个规则减少了遗漏步骤,但也带来耗时问题,所以本地开发会用 -DskipTests 或 -DskipITs 做取舍。注意跳过测试只适合本地快速验证,CI 主线不建议长期这么做。生命周期阶段和 Maven 插件是什么关系?阶段本身只是一个抽象节点,真正干活的是绑定到阶段上的插件 goal。比如 jar 项目的 compile 阶段通常由 maven-compiler-plugin:compile 执行,test 阶段由 maven-surefire-plugin:test 执行。你也可以直接运行插件目标,例如 mvn dependency:tree,它不一定属于完整生命周期。常见踩坑是以为改了生命周期就等于改了插件行为,实际上很多细节要在插件配置里改。<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.13.0</version> <configuration> <release>17</release> </configuration> </plugin> </plugins></build>clean 和 package 为什么经常一起用?clean 会删除旧的 target,可以避免历史产物、生成代码或资源文件残留影响结果。package 则负责重新编译测试并打包,两者组合能得到更干净的构建。取舍在于速度:本地频繁改代码时不一定每次都 clean,否则增量编译优势会被浪费。遇到“本地能跑、CI 不能跑”或资源文件莫名不更新时,再用 mvn clean package 排查更合适。多模块项目执行生命周期有什么边界?在多模块项目根目录执行命令时,Maven reactor 会按模块依赖顺序跑同一个阶段。用 -pl 可以限制模块范围,用 -am 可以补上被依赖模块,这对大仓库节省时间很有用。边界是 Maven 只理解 POM 里的模块和依赖关系,不会自动知道你运行时通过反射、脚本或配置文件间接依赖了哪个模块。遇到这种隐式依赖,最好把模块依赖显式化,否则局部构建很容易漏东西。
服务端阅读 05月31日 23:58

Maven 多模块项目如何管理?聚合和继承该怎么取舍?

Maven 多模块项目通常要同时解决两件事:一次构建多个模块,以及让多个模块共享同一套版本、插件和属性配置。聚合解决的是“构建顺序和构建入口”,继承解决的是“公共配置从哪里来”。两者经常放在同一个父 POM 里,但它们不是一回事,也不是必须绑定使用。一个典型结构会把根目录作为 parent,打包类型设为 pom,再通过 <modules> 声明子模块。执行根目录的 mvn clean install 时,Maven 会读取各模块之间的依赖关系,按 reactor 顺序编译,而不是简单按目录顺序跑。<project> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>demo-parent</artifactId> <version>1.0.0</version> <packaging>pom</packaging> <modules> <module>demo-api</module> <module>demo-service</module> <module>demo-web</module> </modules></project>继承则写在子模块的 pom.xml 中。子模块通过 <parent> 继承父 POM 里的 properties、dependencyManagement、pluginManagement 等配置,但是否被父项目聚合构建,要看父 POM 有没有把它放进 <modules>。<parent> <groupId>com.example</groupId> <artifactId>demo-parent</artifactId> <version>1.0.0</version> <relativePath>../pom.xml</relativePath></parent><artifactId>demo-service</artifactId>实际管理思路在真实项目里,根 POM 最好只承担“统一入口”和“统一约束”两类职责。版本号、编码、Java release、插件版本适合放在父 POM,因为这些配置越分散,越容易出现本地和 CI 不一致。业务模块自己的依赖、打包方式和资源配置,应该留在模块内部,否则父 POM 会慢慢变成谁也不敢改的公共垃圾箱。聚合模块的边界也要控制清楚。一个仓库里可以有多个聚合入口,例如 pom.xml 构建全量模块,pom-service.xml 只聚合后端服务模块,但不要让开发者必须记住十几个入口。模块之间依赖要保持单向,常见方向是 web 依赖 service,service 依赖 api 或 domain,底层模块不要反过来引用上层模块。版本管理上,团队通常会把第三方依赖放进父 POM 的 dependencyManagement,把内部模块版本统一成 ${project.version}。这样发布时能保证同一批模块版本一致,也能减少“api 是 1.0.1、service 还是 1.0.0”的联调问题。边界是公共 parent 如果被很多仓库继承,升级就要更谨慎,最好配合 release notes 和兼容性测试。另外,父 POM 不一定要发布业务代码,但它本身也要有清晰版本。团队里如果把 parent 当成随手可改的配置文件,子模块升级时就会出现不可追踪的差异。更稳的做法是把公共约束当成产品维护,每次改动都说明影响范围。追问聚合和继承最大的区别是什么?聚合关心“从哪个入口构建哪些模块”,继承关心“子模块复用哪些配置”。一个模块可以继承某个公司级 parent,但不出现在当前仓库的 <modules> 中,这在公共 parent 单独发布时很常见。反过来,一个聚合 POM 也可以只负责组装构建,不给子模块继承配置。踩坑点是把两者混为一谈,结果改了父 POM 版本却发现某些模块根本没有继承到。父 POM 里应该放 dependencies 还是 dependencyManagement?公共依赖确实可以放进 <dependencies>,但这样所有子模块都会自动引入,容易让模块边界变脏。更稳妥的做法是把版本放在 <dependencyManagement>,子模块需要时再显式声明依赖。取舍标准很简单:所有模块都必需的基础依赖可以直接放,业务库、驱动、Web 框架这类只给部分模块用的依赖应只管理版本。这样排查冲突时也能看清依赖到底是谁主动引入的。多模块构建时只想编译一个模块怎么办?可以用 -pl 指定模块,用 -am 顺带构建它依赖的本仓库模块。例如 mvn clean install -pl demo-web -am 会构建 demo-web 以及它依赖的 demo-api、demo-service。如果还想构建依赖它的模块,可以用 -amd,但在大仓库里会拉起很多模块,耗时可能反而更长。常见踩坑是只跑 -pl demo-web,本地仓库里的上游模块还是旧版本,导致问题看起来像代码没生效。mvn clean install -pl demo-web -ammvn test -pl demo-servicemvn clean install -pl '!demo-web'模块应该按业务拆还是按技术层拆?小项目按技术层拆成 api、service、web 很直观,团队沟通成本低。业务复杂后,更推荐按领域或边界上下文拆模块,否则所有业务都挤在一个 service 模块里,最后还是一个大泥球。取舍点在于依赖方向是否稳定:底层公共模块不应该反向依赖上层业务模块。模块拆得太细也会让构建、发布和版本管理变复杂,所以不要为了“看起来架构好”硬拆。多模块项目最容易遇到什么坑?第一个坑是循环依赖,例如 demo-api 依赖 demo-service,demo-service 又依赖 demo-api,Maven reactor 会直接报错。第二个坑是父 POM 的 <relativePath> 写错,IDE 里可能还能识别,命令行构建却解析到仓库里的旧 parent。第三个坑是插件版本没有放进 <pluginManagement>,不同模块各跑各的插件版本,CI 上才暴露差异。处理这类问题时,先用 mvn -q help:effective-pom 和 mvn dependency:tree 看最终配置,比盯着源码猜更可靠。
服务端阅读 05月31日 23:47

Maven 仓库和镜像该怎么配置才不会拉错依赖?

Maven 仓库看似只是下载依赖的地方,实际会影响构建速度、依赖安全和产物可追溯性。一个项目通常会同时接触本地仓库、中央仓库、公司私服和镜像;配置不当时,最常见的问题不是“下载不到”,而是下载到了不该用的版本。理解仓库配置时要分清三个概念:repository 是依赖来源,mirror 是替代某个来源的镜像,server 是认证信息。三者 id 对不上,Maven 不会好心提醒你设计错了,只会在构建时用各种 401、404 或 checksum 错误折磨你。Maven 会按什么顺序找依赖?Maven 先查本地仓库,默认在 ~/.m2/repository。本地没有时,再根据有效 POM 和 settings 中的远程仓库配置去下载;中央仓库是默认存在的远程仓库。公司项目一般会让所有请求先走 Nexus 或 Artifactory,由私服代理中央仓库并托管内部包,这样既能加速,也能保留依赖审计记录。<settings> <localRepository>/data/maven-repository</localRepository></settings>自定义本地仓库适合 CI 缓存或磁盘隔离,但不建议团队成员随意改路径。路径太分散会导致“我本地有、你本地没有”的错觉,尤其是内部 SNAPSHOT 包没有正确发布时。私有仓库应该写在 POM 还是 settings?项目必须知道的仓库地址可以放 POM,但账号密码应该放 settings.xml。更常见的企业做法是 POM 只声明公司仓库 id,settings 里放认证信息和镜像规则。注意 <server><id> 必须和仓库 id 对应,否则认证不会套上去。<repositories> <repository> <id>company-releases</id> <url>https://repo.example.com/repository/maven-releases/</url> <releases><enabled>true</enabled></releases> <snapshots><enabled>false</enabled></snapshots> </repository> <repository> <id>company-snapshots</id> <url>https://repo.example.com/repository/maven-snapshots/</url> <releases><enabled>false</enabled></releases> <snapshots><enabled>true</enabled></snapshots> </repository></repositories><settings> <servers> <server> <id>company-releases</id> <username>${env.MAVEN_REPO_USER}</username> <password>${env.MAVEN_REPO_PASSWORD}</password> </server> <server> <id>company-snapshots</id> <username>${env.MAVEN_REPO_USER}</username> <password>${env.MAVEN_REPO_PASSWORD}</password> </server> </servers></settings>mirrorOf 怎么配置才安全?镜像会替代匹配到的仓库,最容易踩坑的是把 <mirrorOf>*</mirrorOf> 指向一个不完整的公共镜像。这样内部私服、插件仓库甚至快照仓库都可能被错误替代。企业内网可以用 *,前提是私服已经代理了所有需要的远程仓库;个人开发更稳妥的是只镜像 central。<mirrors> <mirror> <id>central-mirror</id> <mirrorOf>central</mirrorOf> <url>https://maven.aliyun.com/repository/public</url> </mirror></mirrors>企业私服配置的安全边界公司内网通常会把中央仓库、第三方仓库和内部发布仓库统一代理到一个私服入口,这样便于缓存、审计和阻断高风险依赖。问题是私服不是万能兜底,如果没有同步插件仓库或禁用了 snapshot,Maven 仍然会失败。更稳的做法是在 settings 中配置统一 mirror,在 POM 中只声明项目确实需要的仓库,并把发布地址放到 distributionManagement。账号密码不要写入 POM,也不要写进团队文档截图里;CI 使用环境变量注入,个人电脑可以用 Maven 的密码加密机制或系统凭据管理。这样配置之后,依赖从哪里来、谁有权限发布、出了问题该查哪一层,都会清楚很多。追问repository 和 mirror 到底有什么区别?repository 是项目声明“我可以从哪里找依赖”,mirror 是 settings 声明“访问某些仓库时改走这个地址”。mirror 的优先级很高,一旦匹配,原仓库 URL 会被替换。取舍是镜像能统一加速和管控,但配置过宽会导致依赖来源被悄悄改掉。边界是 POM 表达项目需求,settings 表达当前机器或组织的访问策略。releases 和 snapshots 为什么要分开?release 应该稳定、不可变,snapshot 天生表示还在变化。两者混在一个仓库里,缓存策略、清理策略和审计都会变得混乱。分开后的代价是配置多一点,但能避免生产构建误拉 SNAPSHOT。踩坑最多的是依赖版本写了 1.0-SNAPSHOT,仓库却禁用了 snapshots。私服认证明明配了,为什么还是 401?先检查 <server><id> 是否和 repository 或 distributionManagement 里的 id 完全一致。Maven 不是按 URL 匹配认证,而是按 id 匹配。另一个坑是密码里有特殊字符,手写 XML 时转义不正确,或者 CI 环境变量没有注入。排查时加 -X 可以看到 Maven 选择了哪个仓库,但不要把带密码的日志贴到公共渠道。为什么本地仓库里删了依赖还是拉不到新版本?可能远程仓库本身没有新版本,也可能 mirror 把请求转到了另一个代理仓库。SNAPSHOT 还受 updatePolicy 影响,Maven 不一定每次都检查远端。取舍上,频繁检查能拿到最新包,但会拖慢构建并增加私服压力。临时排查可以用 mvn -U clean package,长期还是要规范发布版本。插件仓库需要单独配置吗?需要时可以配置 <pluginRepositories>,因为插件解析和普通依赖解析是两条配置。很多公司只代理了依赖仓库,却忘了代理 Maven 插件仓库,结果 maven-compiler-plugin 或第三方插件下载失败。边界是常用官方插件通常中央仓库能解决,内部自研插件或受限网络环境才需要显式配置。配置后同样要注意 mirror 是否把它覆盖掉。
服务端阅读 05月31日 23:47

Maven 插件是怎样绑定生命周期并执行构建任务的?

Maven 真正干活的是插件。mvn package 看起来只是一条生命周期命令,背后会按阶段触发一串插件目标,比如编译、复制资源、运行测试、打包。理解插件时不要只背“plugin、goal、phase”三个词,关键是看它什么时候执行、执行几次、配置从哪里来。很多构建问题并不是 Maven 神秘,而是某个插件目标绑定错阶段,或者父 POM 里的默认配置被子模块悄悄继承了。插件、目标和生命周期是什么关系?插件是一组能力,目标是插件里的一个具体动作,生命周期阶段只是 Maven 预设的一条时间线。比如 maven-compiler-plugin 提供 compile 目标,通常绑定在 compile 阶段;maven-surefire-plugin 负责单元测试,通常在 test 阶段运行。你可以直接执行 mvn compiler:compile,也可以执行 mvn test 让 Maven 按生命周期自动调用绑定好的目标。<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.13.0</version> <configuration> <release>17</release> <encoding>UTF-8</encoding> </configuration> </plugin> </plugins></build>source、target 和 release 的选择也有边界。JDK 9 以后更推荐 release,它会同时约束语言级别和可用 API;只配 source/target 时,代码仍可能误用高版本 JDK 的类库,运行到低版本环境才炸。pluginManagement 和 plugins 不要混用错父 POM 里常用 pluginManagement 统一版本,但它只提供默认配置,不会让插件自动执行。真正生效还需要在子模块的 <plugins> 中声明,或者由打包类型的默认生命周期绑定触发。这个边界很重要:pluginManagement 像菜单,plugins 才是点菜。大型项目推荐父 POM 锁版本,子模块按需启用,避免每个模块复制一堆插件配置。<build> <pluginManagement> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>3.2.5</version> <configuration> <includes> <include>**/*Test.java</include> </includes> </configuration> </plugin> </plugins> </pluginManagement></build>如何把插件目标绑定到阶段?需要额外产物时,用 executions 明确绑定目标。例如发布 SDK 时经常附带源码包,这可以让 source:jar-no-fork 在 verify 阶段执行。坑在于同一个插件可以有多个 execution,如果 id、phase、goal 写得含糊,最后可能重复打包或漏执行。<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-source-plugin</artifactId> <version>3.3.1</version> <executions> <execution> <id>attach-sources</id> <phase>verify</phase> <goals> <goal>jar-no-fork</goal> </goals> </execution> </executions></plugin>常用插件怎么取舍?compiler、surefire、jar、resources 属于大多数 Java 项目的基础插件,应该在父 POM 里锁定版本。shade、assembly、spring-boot-maven-plugin 这类会重组产物的插件要谨慎使用,因为它们不仅影响构建过程,还会改变最终包的结构。比如 shade 能把依赖合进一个 fat jar,部署方便,但也可能带来类冲突、资源文件覆盖和许可证合规问题。团队里最好把“产物形态”写清楚:普通库只发布 jar,应用服务才打可执行包,不要每个模块都复制同一套打包插件。这类约定看起来保守,却能减少很多“本地能跑、发布包不能跑”的隐性问题。追问为什么有些插件不用配置也会执行?因为 Maven 对不同 packaging 有默认生命周期绑定,例如 jar 项目默认会编译、测试、打包。这个默认行为降低了入门成本,但也让人误以为所有插件都是“自动发现”的。边界是默认绑定只覆盖常见任务,像生成源码包、Docker 镜像、代码覆盖率通常需要自己声明。排查时可以用 mvn help:effective-pom 看最终绑定。pluginManagement 里配置了插件,为什么没有生效?pluginManagement 只是管理版本和默认配置,不等于启用插件。子模块没有在 <plugins> 声明时,它通常不会凭空执行。这样设计的取舍是父 POM 可以统一标准,又不会强迫每个模块跑不需要的插件。踩坑时先看插件到底出现在 effective POM 的哪个位置。跳过测试应该用 skipTests 还是 maven.test.skip?skipTests 通常只跳过测试执行,测试代码仍可能编译;maven.test.skip=true 连测试编译也跳过。前者适合临时加快打包,后者更激进,可能掩盖测试代码长期编译失败的问题。CI 主线不建议长期使用任何跳过测试的开关,除非有单独阶段补回。边界是本地调试可以求快,发布流水线要保守。Maven 插件版本要不要每次都写?建议写,至少在父 POM 里统一锁定。完全依赖 Maven 或超级 POM 的默认版本,会让不同 Maven 版本、不同时间创建的项目表现不一致。取舍是维护版本矩阵多一点工作,但构建可复现性更好。插件升级也要看 release note,特别是 surefire、compiler、shade 这类会影响产物的插件。直接执行 mvn plugin:goal 和跑生命周期有什么区别?直接执行目标只跑你点名的动作,生命周期命令会从前置阶段一路执行到指定阶段。比如 mvn jar:jar 可能不会先编译最新代码,而 mvn package 会按顺序处理资源、编译、测试、打包。前者适合诊断单个插件,后者适合日常构建。踩坑点是手动执行目标得到一个包,并不代表完整构建流程是健康的。
服务端阅读 05月31日 23:47

Maven Profile 如何管理多环境配置才不容易出错?

Maven Profile 适合解决“同一套项目,在不同构建场景下需要少量差异配置”的问题,比如 dev、test、prod 使用不同资源过滤值,或者 CI 构建时额外打开跳过集成测试的开关。它不适合承载所有运行时配置,更不应该把数据库密码、生产密钥直接写进 pom.xml。判断边界很简单:影响构建产物、依赖、插件行为的配置可以放 Profile;应用启动后才读取的配置,优先交给配置中心、环境变量或部署平台。Profile 放在哪里更合适?项目级 Profile 通常写在 pom.xml,便于团队共享,适合资源过滤、插件参数、可选依赖这类和项目强相关的内容。用户级 Profile 写在 ~/.m2/settings.xml,适合仓库、账号、个人本地路径,不建议提交到仓库。全局 Maven 配置也能写 Profile,但团队协作里很少依赖它,因为不同机器的 Maven 安装目录不一致,排查起来很费劲。<profiles> <profile> <id>dev</id> <properties> <app.env>dev</app.env> <db.url>jdbc:mysql://localhost:3306/demo_dev</db.url> </properties> </profile> <profile> <id>prod</id> <properties> <app.env>prod</app.env> <db.url>${env.PROD_DB_URL}</db.url> </properties> </profile></profiles>如果要让这些属性进入资源文件,需要显式开启 filtering。踩坑最多的是以为定义了 Profile 属性,application.properties 就会自动替换;实际上没有资源过滤时,${app.env} 会原样留在文件里。<build> <resources> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> </resource> </resources></build>常见激活方式怎么选?命令行 -Pdev 最直观,也最适合 CI/CD,因为构建脚本里能清楚看到使用了哪个环境。自动激活适合本地便利配置,比如根据 JDK 或操作系统调整插件参数,但它的代价是隐蔽:同一条命令在两台机器上可能得到不同结果。多人项目里,生产 Profile 最好显式激活,不要依赖文件存在、环境变量这种容易被忽略的条件。mvn clean package -Pdevmvn clean package -Pdev,skip-itmvn help:active-profiles<profile> <id>jdk17</id> <activation> <jdk>[17,)</jdk> </activation> <properties> <maven.compiler.release>17</maven.compiler.release> </properties></profile>实战里怎样控制边界?Profile 最好只处理“构建时必须确定”的差异,例如资源过滤值、是否附加源码包、是否启用某个测试阶段。像连接池大小、灰度开关、第三方接口地址这类运行期变量,放在应用配置或部署系统里更合适。否则一次重新打包就可能被误认为一次配置变更,回滚时也分不清到底是代码问题还是环境值问题。团队里可以约定只有 dev、test、prod 这类环境 Profile 允许改产物内容,skip-test、local 这类辅助 Profile 只能影响本地效率,不参与正式发布。因此,Profile 的价值不是把所有环境差异都塞进 Maven,而是让构建结果可解释、可复现。追问Profile 能不能直接管理生产数据库密码?不建议,尤其不要把密码明文提交到 pom.xml。Profile 可以引用环境变量,例如 ${env.PROD_DB_URL},但真正的密钥仍应由 CI 密文变量、部署平台或配置中心管理。这样做的取舍是本地复现稍麻烦,但换来的是仓库泄露时不会连带泄露生产凭据。边界在于 Maven 只负责构建,不应该变成运行时密钥仓库。pom.xml 里的 Profile 和 settings.xml 里的 Profile 有什么区别?pom.xml 更适合项目共享配置,任何开发者拉下代码都能得到一致构建规则。settings.xml 更适合个人或公司内部环境,比如私服地址、认证账号、本机路径。踩坑点是 settings.xml 不随代码提交,CI 机器如果没配置对应 Profile,本地能过、流水线会失败。一般做法是项目必需配置放 POM,机器差异配置放 settings。多个 Profile 同时激活时谁覆盖谁?同名属性后生效的配置可能覆盖先生效的配置,但不要把业务正确性建立在这种隐式顺序上。多个 Profile 同时改同一个属性,很容易让排查变成猜谜。更稳妥的方式是拆清职责,比如 dev 管环境,skip-it 管测试开关,避免两个 Profile 都写 db.url。如果确实要覆盖,至少用 mvn help:effective-pom 看最终结果。为什么资源文件里的占位符没有被替换?最常见原因是忘了给 resource 开启 <filtering>true</filtering>。另一个坑是占位符语法和框架自己的变量冲突,例如 Spring 配置里也常用 ${},Maven 过滤可能提前替换掉本该运行时解析的值。取舍上,构建期固定的值可以过滤,运行期变化的值不要过滤。遇到问题先看打包后的 target/classes 文件,而不是只看源码。CI/CD 里应该怎么用 Maven Profile?CI 里推荐显式写出 mvn clean package -Pprod,不要依赖某个文件是否存在来自动激活。这样日志可追溯,回滚或复现构建时也更清楚。边界是 Profile 只决定构建差异,不应该决定部署到哪台机器;部署目标应该由流水线参数或发布系统控制。踩坑最多的是 dev Profile 被默认激活,结果生产包里混入了测试配置。
服务端阅读 05月31日 23:47

Maven 和 Gradle 有什么区别?项目该怎么选?

Maven 和 Gradle 都能完成 Java 项目的依赖管理、编译、测试和打包,但它们解决问题的方式不一样。Maven 更像一套稳定的约定:目录怎么放、生命周期怎么走、插件怎么绑定,都有明确规则。Gradle 更像可编程的构建平台:你可以用 Groovy 或 Kotlin DSL 写任务、组合逻辑、做缓存和增量构建。选择哪一个,不是看谁更“先进”,而是看项目需要稳定约定,还是需要更强的构建表达能力。配置方式有什么差别?Maven 使用 XML 写 pom.xml,结构清晰,但配置稍显冗长。Gradle 使用 build.gradle 或 build.gradle.kts,表达更短,也更容易写条件逻辑。XML 的好处是团队成员一眼能看出依赖和插件,DSL 的好处是复杂构建可以少写重复配置。<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency>dependencies { implementation("org.springframework.boot:spring-boot-starter-web")}取舍点在于可读性和自由度。Maven 的限制让项目更标准,Gradle 的自由让构建更灵活,但也可能让构建脚本变成另一套业务代码。团队如果没有人维护 Gradle 脚本,灵活性很快会变成隐性成本。性能和生态有什么区别?Gradle 在增量构建、构建缓存、配置缓存和多模块构建上通常更有优势,Android 生态也基本围绕 Gradle 展开。Maven 的优势是生态成熟、企业项目存量大、插件行为稳定,出问题时更容易搜索到解决方案。Maven 的生命周期固定:validate、compile、test、package、verify、install、deploy。Gradle 是任务图模型,任务之间可以声明依赖,构建时按图执行。前者适合标准 Java 服务,后者适合需要自定义代码生成、多语言混编、复杂打包流程的项目。依赖管理谁更好?Maven 的传递依赖规则比较固定,冲突时遵循“最短路径优先、路径相同则先声明优先”。Gradle 的依赖能力更丰富,支持版本约束、能力冲突处理、平台依赖和更细的配置范围。能力越强,规则也越多;如果团队只需要稳定引入依赖,Maven 已经足够。mvn dependency:tree./gradlew dependencies --configuration runtimeClasspath排查依赖时,Maven 的树更直观;Gradle 的配置维度更细,需要知道 implementation、api、runtimeOnly 等作用范围。迁移时最容易踩坑的是把 Maven 的 scope 机械翻译成 Gradle 配置,结果编译期或运行时 classpath 发生变化。追问新项目默认应该选 Maven 还是 Gradle?普通 Spring 后端服务、团队偏稳定交付、构建逻辑不复杂时,Maven 是省心选择。Android、多语言仓库、大型多模块、需要构建缓存时,Gradle 更值得考虑。边界是团队经验:一个理论上更快的 Gradle 项目,如果没人懂脚本维护,长期成本可能高于 Maven。工具选择首先要服务团队,而不是服务技术偏好。Gradle 一定比 Maven 快吗?不一定。Gradle 的增量构建和缓存配置得当时确实很快,但冷启动、首次下载依赖、脚本配置阶段也会消耗时间。小型项目里 Maven 和 Gradle 的差距可能不明显,甚至 Maven 更简单直接。踩坑点是只看一次本地构建耗时就做结论,应该比较 CI、全量构建、增量构建和失败重试的整体成本。Maven 项目有必要迁移到 Gradle 吗?只有当现有构建真的成为瓶颈时才值得迁移,例如多模块构建太慢、代码生成逻辑复杂、Android 或 Kotlin 生态强依赖 Gradle。迁移会带来插件替换、依赖范围变化、CI 脚本重写和团队学习成本。比较稳妥的做法是先选一个边界清楚的子模块试点,而不是一次性改完整仓库。否则构建工具迁移很容易变成长期悬空的技术债。Maven 的 XML 冗长是不是明显缺点?冗长是缺点,但也带来确定性。很多企业项目宁愿接受重复 XML,也不愿让构建脚本出现太多条件分支和动态逻辑。Gradle DSL 写得好会很优雅,写得差会比 XML 更难读。判断边界很简单:如果构建规则主要是声明依赖和插件,Maven 的冗长通常可以接受;如果规则本身需要编程,Gradle 更合适。两个工具能在同一个团队里并存吗?可以,但要有清楚边界。比如后端服务统一 Maven,Android 或构建平台模块使用 Gradle,这样大家知道去哪套规范里找答案。最怕的是同类项目一半 Maven、一半 Gradle,CI、依赖升级、安全扫描都要维护两套。并存不是问题,无规则并存才是问题。如果项目追求标准化、稳定、低学习成本,Maven 仍然很合适;如果项目需要更快的增量构建、更灵活的任务编排和跨语言支持,Gradle 更有空间。真正的选择标准不是工具名,而是构建复杂度、团队经验和未来维护成本。
服务端阅读 05月31日 23:47

Maven 常用命令怎么用?哪些参数能提高构建效率?

Maven 命令看起来很多,其实大部分都围绕三件事:按生命周期构建项目、查看依赖问题、控制构建范围。日常开发不需要背完整手册,但要知道每个命令会跑到哪个阶段、会不会执行测试、会不会把产物写进本地仓库或远程仓库。命令用错,轻则构建慢,重则把不该发布的版本推到仓库。生命周期命令怎么选?最常用的是 clean、compile、test、package、install 和 deploy。Maven 会从生命周期起点一直执行到你指定的阶段,所以 mvn package 不只是打包,它会先编译并运行测试;mvn install 会在打包后把产物安装到本地仓库;mvn deploy 则会发布到远程仓库,通常只应由 CI 执行。mvn clean # 删除 target 目录mvn compile # 编译主代码mvn test # 运行单元测试mvn package # 生成 jar/warmvn install # 安装到本地仓库mvn deploy # 发布到远程仓库开发阶段想快速验证语法和依赖,mvn test 往往比 clean install 更合适。clean install 很完整,但它会清理增量结果并写入本地仓库,在大项目中成本不低。只有需要验证全量构建、给其他本地模块引用产物,或 CI 做最终检查时,才更适合用它。依赖排查命令怎么用?依赖冲突、版本不一致、包重复,是 Maven 项目里最常见的构建问题。dependency:tree 用来查看依赖来源,dependency:analyze 用来发现声明了但没用、用了却没声明的依赖。前者适合排查版本冲突,后者适合清理 POM。mvn dependency:treemvn dependency:tree -Dincludes=com.fasterxml.jackson.coremvn dependency:analyzemvn dependency:resolvemvn dependency:sources如果本地依赖损坏,可以用 dependency:purge-local-repository 清理后重新下载,但不要把它当成日常加速手段。它会让下一次构建重新拉依赖,网络慢时反而更耗时。遇到 SNAPSHOT 依赖没更新,可以加 -U 强制检查远程更新。mvn clean test -Dtest=OrderServiceTestmvn test -DfailIfNoTests=falsemvn help:effective-pommvn help:active-profiles定位问题时,help:effective-pom 很有用。它会把父 POM、当前 POM、Profile 和默认配置合并后展示出来,比直接盯着一个 pom.xml 更接近 Maven 实际看到的内容。边界是输出很长,适合排查“配置到底从哪来”,不适合每次构建都跑。多模块项目怎么少构建一点?多模块工程最有用的参数是 -pl、-am 和 -rf。-pl 指定构建哪些模块,-am 会顺带构建这些模块依赖的上游模块,-rf 可以从失败模块继续构建。它们的价值不在“命令高级”,而在少跑无关模块。mvn test -pl order-service -ammvn package -pl '!legacy-module'mvn install -rf payment-servicemvn clean install -T 1C-T 可以并行构建,例如 -T 1C 表示每个 CPU 核心一个线程。边界是并行只对模块间依赖清楚、插件线程安全的项目效果好。如果某些插件会写同一个文件,或者测试依赖共享端口,并行构建可能把偶发失败放大。追问-DskipTests 和 -Dmaven.test.skip=true 有什么区别?-DskipTests 会跳过测试执行,但通常仍会编译测试代码。-Dmaven.test.skip=true 会跳过测试编译和测试执行,速度更快,但也更容易掩盖测试代码编译失败。日常临时打包可以用前者,CI 主流程不建议长期跳测试。踩坑点是测试工具类被主代码误引用时,跳过测试编译可能让问题延后暴露。什么时候用 mvn install,什么时候只用 mvn package?如果只是看当前项目能不能打包,package 就够了。install 会把产物写入 ~/.m2/repository,适合本地另一个项目要引用这个 SNAPSHOT 包的场景。它的副作用是本地仓库可能残留旧快照,让你误以为代码已经同步。多人协作时,真正共享的版本应该走远程仓库,而不是靠各自本地 install。为什么 dependency:tree 查到了冲突,运行时还是报错?dependency:tree 只能告诉你 Maven 解析出的依赖路径,不保证运行环境和它完全一致。应用服务器、自带 lib、Spring Boot 打包插件、shade 插件都可能改变最终 classpath。排查时要结合最终包内容,例如查看 BOOT-INF/lib 或运行时启动日志。边界是:构建期依赖正确,不等于部署形态一定正确。-o 离线模式适合所有场景吗?不适合。mvn -o 只使用本地仓库,网络不可用或 CI 缓存充分时很有用。它的前提是所有依赖、插件和父 POM 都已经存在本地,否则构建会直接失败。常见踩坑是只缓存了依赖,没缓存插件,结果离线构建卡在插件解析。命令参数写得越多越专业吗?不是。参数越多,构建语义越难复现,尤其是 -D 覆盖属性、-P 激活 profile、跳过插件这些参数。团队里最好把稳定规则写进 POM 或 CI 脚本,把临时参数留给本地排查。否则同一个项目会出现“我的命令能过、你的命令不过”的情况。把 Maven 命令分成构建、依赖排查、多模块加速三类,基本就能覆盖日常 80% 的问题。真正值得记住的不是命令数量,而是每个命令会改变哪些边界:是否清理、是否测试、是否安装、是否发布、是否只构建局部模块。
服务端阅读 05月31日 23:47

Maven POM 文件怎么配置才清晰?核心元素有哪些?

POM 是 Maven 项目的说明书,也是构建时最先被读取的配置入口。一个项目能不能稳定编译、依赖会不会乱、插件版本是否可控,很多时候不取决于命令写得多熟,而取决于 pom.xml 有没有把边界写清楚。POM 不需要堆满配置,真正重要的是坐标、依赖、版本管理和构建插件这几块各司其职。POM 的核心元素怎么分工?最基础的是项目坐标:groupId、artifactId、version 和 packaging。groupId 通常用组织域名倒序,artifactId 是模块名,version 表示当前产物版本,packaging 决定最终打成 jar、war 还是只作为父工程的 pom。坐标一旦发布到仓库,就会被其他项目当作依赖引用,所以不要随意改名。<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>order-service</artifactId> <version>1.0.0-SNAPSHOT</version> <packaging>jar</packaging></project>properties 适合放 Java 版本、编码、依赖版本这类会被复用的值。它的好处是集中修改,代价是过度抽象后不容易追踪真实版本。团队项目里可以把 Spring、MyBatis、JUnit 等版本写成属性,但不要把每个只用一次的小依赖都抽成变量。dependencies 声明当前模块真的要用的依赖;dependencyManagement 只管理版本,不会自动引入依赖。这是很多人踩坑的地方:父 POM 里写了 dependencyManagement,子模块仍然需要在 dependencies 中声明依赖,只是可以省略版本。<properties> <java.version>17</java.version> <spring.boot.version>3.2.5</spring.boot.version></properties><dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring.boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies></dependencyManagement><dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency></dependencies>build 和插件应该怎么写?build 负责资源处理、编译、测试、打包等构建行为。插件版本最好固定在父 POM 的 pluginManagement 中,子模块按需启用。这样既能避免不同机器拉到不同插件版本,也不会让每个模块都复制一大段配置。<build> <pluginManagement> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.13.0</version> <configuration> <release>${java.version}</release> <encoding>UTF-8</encoding> </configuration> </plugin> </plugins> </pluginManagement></build>父子 POM 适合多模块工程。父 POM 用 packaging=pom,统一管理依赖版本、插件版本和公共属性;子模块通过 parent 继承。取舍点在于:公共配置越集中越容易维护,但父 POM 过重会让所有模块被迫背上无关配置。<parent> <groupId>com.example</groupId> <artifactId>platform-parent</artifactId> <version>1.0.0</version> <relativePath>../pom.xml</relativePath></parent>profiles 可以处理环境差异,例如本地、测试、生产使用不同资源过滤或插件开关。不过它不适合承载业务配置,更不应该把数据库密码这类敏感信息直接写进 POM。更稳妥的做法是让 POM 只决定构建差异,运行时配置交给环境变量、配置中心或部署平台。这样构建产物更容易复现,也能减少“换个环境就重新打包”的问题。追问dependencies 和 dependencyManagement 有什么区别?dependencies 会真正把依赖加入当前模块的 classpath,编译和运行都可能用到。dependencyManagement 只是提供版本约束,子模块没有声明依赖时不会自动生效。这个设计的好处是父 POM 能统一版本,又不会强行污染每个模块。踩坑最多的是以为导入 BOM 后依赖就存在,结果编译时报类找不到。parent、modules 和 dependencyManagement 能互相替代吗?不能。parent 是继承配置,modules 是聚合构建,dependencyManagement 是版本管理。一个父工程可以同时做继承和聚合,但大型仓库里也常把它们拆开,避免构建范围过大。边界是:如果只是统一版本,不一定要把所有项目都放进同一个聚合工程。POM 里应该直接写仓库 repositories 吗?一般不建议在业务项目里随意写公共仓库地址,企业项目更适合在 settings.xml 或私服里统一配置镜像。POM 中写仓库会影响所有使用该项目的人,甚至让构建依赖某个不稳定的外部源。只有项目确实依赖特殊仓库,且团队接受这个约束时,才应该写进 POM。否则后续排查“为什么我机器能构建、CI 不行”会很麻烦。版本号放 properties 还是直接写在依赖上?被多个依赖或多个模块共享的版本适合放进 properties,例如 Spring Boot、Jackson、JUnit。只出现一次的小依赖直接写版本反而更清楚。过度属性化会让读 POM 的人频繁跳转,维护成本上升。一个实用边界是:能被统一升级的版本集中管理,临时依赖不要为了“整齐”硬抽象。POM 文件越完整越好吗?不是。Maven 的默认约定已经覆盖了目录结构、编译阶段和基础生命周期,很多配置不写反而更稳定。真正需要显式写的是版本、插件行为差异、资源过滤和发布规则。把网上模板整段复制进来,常见后果是插件互相覆盖、构建阶段重复执行,最后没人敢改。POM 写得好不是 XML 多,而是项目坐标稳定、依赖边界清楚、版本来源可追踪。先让默认约定发挥作用,再把团队确实需要统一的内容放进父 POM,通常比一开始就写“大而全模板”更可靠。
服务端阅读 05月31日 23:47

Maven 资源过滤怎么管理多环境配置?

Maven 资源过滤是在构建时把资源文件里的占位符替换成真实值,例如把 ${env}、${jdbc.url} 写进配置文件,再由 Maven 根据 profile 或属性替换。它适合处理少量构建期差异,比如应用名、版本号、环境标识、默认地址。它不适合管理密码、密钥和频繁变化的运行时配置,因为这些内容一旦打进包里,修改就要重新构建。基本配置在 POM 里开启过滤时,最好只针对明确的文本文件:<build> <resources> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> <includes> <include>**/*.properties</include> <include>**/*.yml</include> </includes> </resource> <resource> <directory>src/main/resources</directory> <filtering>false</filtering> <excludes> <exclude>**/*.properties</exclude> <exclude>**/*.yml</exclude> </excludes> </resource> </resources></build>资源文件中可以这样写:app.name=${project.artifactId}app.version=${project.version}app.env=${deploy.env}多环境值可以放在 profile 里:<profiles> <profile> <id>prod</id> <properties> <deploy.env>prod</deploy.env> <api.baseUrl>https://api.example.com</api.baseUrl> </properties> </profile></profiles>构建时指定环境:mvn clean package -Pprod追问Maven 资源过滤和 Spring Profile 有什么区别?Maven 资源过滤发生在构建期,打包后内容已经被替换。Spring Profile 发生在运行期,同一个包可以通过启动参数切换环境。取舍上,如果配置跟构建产物强相关,例如版本号、构建时间,可以用 Maven;如果配置跟部署环境相关,例如数据库地址、开关状态,更适合运行期 Profile 或配置中心。把所有环境都打成不同包会增加发布复杂度,也容易出现“测试包和生产包不是同一个产物”的问题。为什么不建议过滤整个 resources 目录?因为 resources 里不一定全是文本文件。图片、字体、证书、Excel 模板等二进制文件如果被过滤,可能会被改坏,错误还不一定在构建阶段暴露。更稳妥的方式是只 include properties、yml、xml 这类确实需要替换的文件。边界是 XML 里如果包含 ${} 但不是 Maven 变量,也要小心误替换。占位符和业务框架的 ${} 冲突怎么办?这是资源过滤最常见的坑。Spring、Logback、Shell 模板里也大量使用 ${},Maven 过滤可能提前把它们处理掉。可以改用自定义分隔符,或者只对少数配置文件开启过滤。示例做法是在 resources 插件中配置 delimiter,让 Maven 只替换 @name@ 形式的变量。<plugin> <artifactId>maven-resources-plugin</artifactId> <version>3.3.1</version> <configuration> <delimiters> <delimiter>@</delimiter> </delimiters> <useDefaultDelimiters>false</useDefaultDelimiters> </configuration></plugin>密码和 Token 能不能放进过滤文件?不建议。资源过滤会把值写进最终 jar、war 或镜像里,任何能拿到产物的人都可能反编译或解压看到配置。密码、Token、证书路径这类敏感信息应该来自环境变量、密钥服务、Kubernetes Secret 或配置中心。Maven 可以写默认占位符,但不要把真实密钥放进 POM、filter 文件或 Git 仓库。多环境配置应该打多个包还是一个包多环境运行?如果是传统部署,多个 profile 打多个包看起来简单,但会增加制品数量,也让“哪个包上线了”变得难追踪。现代 CI/CD 更推荐一次构建生成一个不可变制品,部署时通过环境变量或配置中心注入差异。Maven 资源过滤适合补充构建元信息,不适合作为完整环境管理系统。除非你的发布流程明确要求按环境出包,否则一个包多环境运行更稳。资源过滤很好用,但它解决的是构建期替换,不是配置治理。把它用在版本号、环境名、少量默认地址上,会让包更清晰;把它用来塞满所有环境变量和密钥,后面维护成本会很高。
服务端阅读 05月31日 23:47

Maven Archetype 怎么用,什么时候该自定义模板?

Maven Archetype 可以理解成 Maven 的项目模板:它把目录结构、POM、示例代码和默认配置打包起来,让新项目不用从空文件夹开始搭。它适合重复创建结构相似的项目,比如公司内部的 Spring Boot 服务、SDK 工程、插件工程。真正要注意的是,Archetype 不是越复杂越好,模板太重会把过时配置复制到每个新项目里。快速生成项目使用官方 quickstart 模板可以快速创建一个普通 Java 项目:mvn archetype:generate -DgroupId=com.example -DartifactId=demo-app -Dversion=1.0.0-SNAPSHOT -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=falseWeb 项目也可以使用 maven-archetype-webapp,但现在很多团队更倾向用 Spring Initializr 创建 Spring Boot 项目。Archetype 的价值更多体现在公司内部模板:统一包名、日志配置、基础依赖、CI 文件和代码规范。自定义 Archetype 的关键文件模板工程通常包含 archetype-metadata.xml,用来声明哪些文件参与生成、哪些文件需要变量替换。<archetype-descriptor name="service-template"> <fileSets> <fileSet filtered="true" packaged="true"> <directory>src/main/java</directory> <includes> <include>**/*.java</include> </includes> </fileSet> <fileSet filtered="true"> <directory>src/main/resources</directory> <includes> <include>**/*.yml</include> </includes> </fileSet> </fileSets></archetype-descriptor>生成和安装模板常用命令如下:mvn archetype:create-from-projectcd target/generated-sources/archetypemvn install追问Maven Archetype 和直接复制项目模板有什么区别?直接复制最快,但容易把旧包名、旧 artifactId、无用配置一起复制过去。Archetype 会在生成时替换 groupId、artifactId、package 等变量,更适合标准化创建。它的边界是模板维护成本更高,尤其当基础依赖升级时,需要同步更新模板。小团队项目少时复制模板够用,项目数量多或规范要求强时再上 Archetype 更划算。自定义 Archetype 里哪些文件应该做过滤?需要替换变量的文本文件适合过滤,比如 Java 类、POM、YAML、properties。二进制文件、图片、证书和压缩包不要开启过滤,否则可能被 Maven 当文本处理后损坏。一个常见坑是对所有 resources 都开 filtered=true,结果字体、图片或 keystore 构建后不可用。更稳的做法是只 include 明确需要替换的文件类型。Archetype 适合生成 Spring Boot 项目吗?可以,但不一定是首选。Spring Boot 项目如果只是选择依赖和版本,Spring Initializr 更方便,生态支持也更及时。Archetype 更适合加入公司内部规范,例如统一异常结构、日志埋点、健康检查、Dockerfile 和 CI 模板。取舍点是你要通用生态模板,还是要强绑定企业工程规范。模板里要不要放很多示例业务代码?不建议放太多。示例代码越多,新项目删除成本越高,也更容易留下没人维护的假业务逻辑。可以保留一个最小可运行的 Controller、配置类或单元测试,用来证明生成项目能启动。边界是模板应表达工程骨架,而不是替业务团队提前设计业务分层。自定义 Archetype 发布后怎么让团队使用?可以先把模板安装到本地仓库验证,再发布到公司 Nexus 或 Artifactory。使用时指定 archetype 坐标即可:mvn archetype:generate -DarchetypeGroupId=com.company -DarchetypeArtifactId=service-archetype -DarchetypeVersion=1.2.0 -DgroupId=com.company.demo -DartifactId=order-service -DinteractiveMode=false模板版本也要像普通依赖一样管理。每次更新基础依赖或目录规范,都应发布新版本,而不是覆盖旧版本;否则老项目排查生成来源时会很痛苦。
服务端阅读 05月31日 23:47

Maven 依赖版本到底该怎么管理?

Maven 依赖版本管理的核心目标不是“写得越新越好”,而是让团队能稳定复现同一次构建。一个项目里常见的问题是:直接依赖写了版本,传递依赖又带来另一个版本,父 POM 或 BOM 还会覆盖一部分版本。最后代码能不能跑,往往取决于 Maven 的冲突调解规则,而不是你以为的那个版本。推荐的版本管理方式普通业务项目建议把版本集中放到 dependencyManagement,业务模块只声明依赖,不重复写版本。<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>3.3.5</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies></dependencyManagement><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency>排查版本时,先看依赖树,不要靠猜:mvn dependency:tree -Dincludes=org.slf4jmvn help:effective-pomMaven 遇到版本冲突时通常采用“路径最近者优先”,路径一样近时再看声明顺序。这意味着传递依赖的版本可能被更近的直接依赖覆盖,也可能因为父 POM 的管理而变化。追问dependencyManagement 和 dependencies 有什么区别?dependencyManagement 只负责规定版本和范围,本身不会把依赖加入项目。真正引入依赖的是 dependencies,只是它可以省略已经被管理的版本。这样做的好处是多模块项目版本统一,坏处是新同事容易误以为写进 dependencyManagement 就已经生效。边界很清楚:要用就必须在模块的 dependencies 里声明。Maven 版本范围能不能用于生产项目?技术上可以,比如 [1.0,2.0) 表示使用 1.x 范围内的版本。但生产项目一般不建议这么做,因为同一份代码在不同时间可能解析出不同依赖,构建不可复现。它更适合内部库探索或临时验证,不适合发布链路。真正上线时应固定版本,并把升级动作变成一次明确的代码变更。SNAPSHOT 版本适合长期依赖吗?不适合。SNAPSHOT 的语义是“还在变化”,远程仓库里的内容可能被重新发布,同一个版本号对应的二进制不一定相同。团队内部联调可以短期使用,但发布分支和生产构建应切到 release 版本。常见踩坑是 CI 缓存了旧 SNAPSHOT,本地又拉到了新 SNAPSHOT,问题很难复现。BOM 和父 POM 该怎么取舍?父 POM 能继承插件、属性、依赖管理和构建配置,适合公司内部统一工程规范。BOM 只导入依赖版本管理,不强迫项目继承某个父结构,更适合库或多技术栈项目。Spring Boot 项目如果已经继承了公司父 POM,可以用 import BOM 的方式管理 Spring 生态版本。取舍点在于你要的是“全套构建规范”,还是“只统一依赖版本”。发现依赖冲突时怎么处理更稳?先用 mvn dependency:tree 找到冲突来源,再决定是显式声明直接依赖、在 dependencyManagement 里统一版本,还是排除某个传递依赖。不要一上来就到处写 <exclusions>,排除太多会让依赖图变得难维护。示例:<exclusions> <exclusion> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> </exclusion></exclusions>版本管理做得好,升级会变成可审查、可回滚的小变更;做得差,项目会被传递依赖牵着走。Maven 的规则并不复杂,关键是把版本来源集中起来,并让每次升级都有明确理由。
服务端阅读 05月31日 23:47

Maven 构建速度慢该怎么优化?

Maven 构建慢,通常不是某一个开关没打开,而是依赖下载、测试执行、插件配置、多模块顺序和 CI 缓存一起拖慢了速度。优化时不要先急着跳过所有检查,先用 mvn -X、CI 日志或构建耗时统计看瓶颈在哪:是下载依赖慢、单元测试慢、编译慢,还是打包插件慢。真正稳妥的做法,是把“本地开发更快”和“流水线发布可靠”分开配置。常用优化配置本地开发可以优先使用并行构建和跳过非必要步骤:mvn clean install -T 1Cmvn test -pl user-service -ammvn package -DskipTests-T 1C 表示按 CPU 核心数并行构建,适合多模块项目;-pl 指定模块,-am 自动构建它依赖的上游模块。CI 中更推荐缓存本地仓库,并固定 Maven、JDK 和插件版本,避免每次构建都重新解析依赖。<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>3.2.5</version> <configuration> <reuseForks>true</reuseForks> <forkCount>1C</forkCount> </configuration></plugin>测试很慢时,不要只知道 -DskipTests。它会编译测试但不执行;-Dmaven.test.skip=true 连测试编译也跳过,速度更快,但更容易把测试代码编译错误带到后面才暴露。追问Maven 并行构建一定会更快吗?不一定。多模块项目之间如果依赖链很长,Maven 仍然要按依赖顺序执行,能并行的部分有限。并行构建还会放大线程安全问题,一些老插件或自定义插件没有标注 thread-safe,可能出现偶发失败。取舍上,本地开发可以大胆用 -T 1C,发布流水线则要先跑几轮稳定性验证,再决定是否启用。-DskipTests 和 -Dmaven.test.skip=true 怎么选?-DskipTests 只跳过测试执行,测试源码仍会编译,因此能发现测试代码里的编译错误。-Dmaven.test.skip=true 会跳过测试编译和执行,适合临时打包或验证非测试相关改动。坑在于它可能掩盖测试代码与主代码 API 不兼容的问题,等到合并或发布阶段才炸。团队里一般把前者用于普通开发,后者限制在明确知道风险的临时场景。多模块项目只构建一个模块会不会漏东西?如果只写 -pl order-service,Maven 只构建这个模块,依赖的本地模块可能不会自动更新。通常要配合 -am,让 Maven 把它依赖的上游模块一起构建。反过来,-amd 会构建依赖当前模块的下游模块,更适合底层公共模块改动后的影响面验证。边界是模块关系必须在 reactor 里可见,跨仓库依赖仍然需要先安装或发布到仓库。CI 里优化 Maven 构建最值得做什么?最先做缓存,而不是先砍测试。缓存 ~/.m2/repository 能减少大量依赖下载时间,尤其是依赖多、网络不稳定的流水线。其次固定插件版本,否则 Maven 可能解析到不同插件版本,构建时间和结果都不稳定。需要注意的是,缓存也可能带来脏依赖问题,遇到无法解释的构建错误时要支持手动清缓存重跑。Maven 编译插件有哪些容易忽略的配置?最常见的是没有固定 maven-compiler-plugin 版本和 Java release,导致本地和 CI 使用不同 JDK 时行为不一致。推荐用 release 而不是只写 source、target,它能同时限制可用 API。示例配置如下:<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.13.0</version> <configuration> <release>17</release> </configuration></plugin>速度优化最终要服务于稳定交付。开发机上可以用并行、局部构建和跳过测试节省时间;主干和发布流水线则要保留必要校验,只把依赖缓存、插件固定和模块拆分做到位。
服务端阅读 05月31日 22:22

Maven 项目如何接入 CI 才能稳定构建和发布?

Maven 接入 CI 的目标不是把本地命令搬到流水线上,而是让每次提交都在干净、可重复、可追踪的环境里完成编译、测试、打包和必要的检查。好的 Maven CI 会固定 JDK 和 Maven 版本,缓存依赖但不污染构建,上传测试报告和构建产物,并把 deploy 限制在受保护分支或手动发布阶段。做不到这些,流水线就只是另一台“同事电脑”。最小可用的 Maven CIGitHub Actions 可以从这份配置开始:name: Maven CIon: push: branches: [ main, master, develop ] pull_request: branches: [ main, master ]jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: distribution: temurin java-version: '17' cache: maven - name: Build and test run: ./mvnw -B --no-transfer-progress clean verify - name: Upload test reports if: always() uses: actions/upload-artifact@v4 with: name: surefire-reports path: '**/target/surefire-reports/*.xml'如果项目还没加 Maven Wrapper,也可以用 mvn -B clean verify,但长期建议改成 ./mvnw。verify 比 test 更适合作为默认阶段,因为它会覆盖集成测试、打包前检查和部分插件绑定目标。GitLab CI 里要注意本地仓库路径和缓存:stages: [build, test, package]variables: MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"cache: key: maven-${CI_COMMIT_REF_SLUG} paths: - .m2/repositorybuild: image: maven:3.9.6-eclipse-temurin-17 stage: build script: - mvn -B --no-transfer-progress clean verify artifacts: when: always reports: junit: '**/target/surefire-reports/TEST-*.xml' paths: - target/*.jarJenkins 用 Docker agent 时,最好挂载 Maven 仓库缓存,但不要让多个项目共享到互相污染:pipeline { agent { docker { image 'maven:3.9.6-eclipse-temurin-17' args '-v $WORKSPACE/.m2:/root/.m2' } } stages { stage('Verify') { steps { sh 'mvn -B --no-transfer-progress clean verify' } } } post { always { junit '**/target/surefire-reports/*.xml' } }}发布阶段要和普通 CI 分开。PR 只跑 verify,主分支可以打包并上传 artifact,deploy 应只在 tag、release 分支或人工审批后执行。涉及私服发布时,用 CI Secret 生成临时 settings.xml,不要把账号密码写进仓库。质量检查可以按成本分层。每个 PR 都跑单元测试、编译和基础静态检查;依赖漏洞扫描、集成测试、性能测试可以放在主分支或夜间任务。这样反馈速度和质量保障能兼顾,不会因为流水线太慢让开发者绕开它。Maven 里可以把慢测试绑定到 Failsafe,再通过 profile 控制是否执行,例如 PR 跳过外部依赖集成测试,合并后再跑完整验证。发布制品时还要区分 snapshot 和 release。普通分支可以部署到 snapshot 仓库,正式 tag 才允许部署到 releases 仓库。distributionManagement 负责声明地址,CI 条件负责限制何时执行 mvn deploy。如果公司要求制品可追溯,还可以在构建时写入 Git commit、构建号和时间,但这些信息最好进入 manifest 或 build-info 文件,不要影响依赖坐标本身。流水线还应该保留足够的失败现场。测试报告、构建日志、关键产物和覆盖率报告最好在失败时也上传,否则排查只能重新跑。对于偶发失败,保留 surefire dump、failsafe report 和容器日志尤其重要。CI 的价值不只是挡住坏代码,也要让失败原因尽快暴露。追问CI 中应该用 clean install 还是 clean verify?多数项目默认用 clean verify 更合适。install 会把产物写进本地仓库,可能掩盖模块依赖声明不完整的问题,也会污染缓存。只有后续步骤确实需要从本地仓库解析当前项目产物时,才考虑 install。常见坑是上一次流水线留下的本地 SNAPSHOT 被复用,本次代码其实没构建对也能通过。Maven 依赖缓存会不会带来风险?会,但可以控制。缓存能显著减少下载时间,尤其是多模块项目和频繁 PR 构建;风险是缓存损坏、SNAPSHOT 过期或不同分支互相影响。取舍上,release 构建应更保守,必要时清理 SNAPSHOT 或使用独立 cache key。踩坑点是缓存整个 .m2 后私服里已删除的坏包仍被 CI 使用,导致问题复现不了。多模块项目如何加快 CI?可以用 Maven 并行构建,比如 mvn -T 1C clean verify,也可以根据变更模块做增量构建。并行适合测试隔离做得好的项目,增量适合模块边界清晰的大仓。边界是不要为了速度牺牲可信度,公共模块、父 POM 或依赖管理变化时必须扩大构建范围。常见坑是测试共用端口、临时目录或数据库,并行后偶发失败,看起来像代码不稳定,其实是测试隔离差。CI 里如何处理私服账号和 settings.xml?账号密码应放在 CI Secret,通过模板生成 settings.xml,或者使用平台提供的 Maven server 配置。POM 只写 repository id 和 distributionManagement,不写凭据。取舍是配置稍麻烦,但安全边界清楚,离职和轮换密钥也容易处理。坑在于 settings 里的 server id 和 POM 里的 repository id 不一致,deploy 阶段才报 401。测试、质量检查和发布应该放在同一条流水线吗?可以在同一个 workflow 里分 job,但触发条件要分开。PR 阶段跑编译、单测、静态检查;主分支可以产出制品;发布需要 tag、手动触发或审批。这样既不拖慢每个 PR,也能保证发布入口受控。常见坑是 push 到任意分支都执行 mvn deploy,轻则私服堆满无意义版本,重则把未审核代码发布出去。小结Maven CI 的稳定性来自几条基本规则:固定 JDK 和 Maven,优先跑 clean verify,缓存依赖但隔离发布,报告测试结果,凭据只走 Secret。流水线越复杂,越要把“验证”和“发布”分开,否则出问题时很难判断是代码失败、环境失败,还是发布策略本身有漏洞。
服务端阅读 05月31日 22:22

Maven Wrapper 如何保证团队和 CI 构建一致?

Maven Wrapper 的作用很直接:项目自己带一套启动脚本,第一次执行时下载指定版本的 Maven,以后团队成员和 CI 都用同一个 Maven 版本构建。它解决的不是依赖版本问题,而是构建工具版本问题。很多“我本地能构建、你那里不行”的问题,根源不是代码,而是 Maven 3.6、3.8、3.9 对插件解析、HTTP 仓库拦截、传输协议的行为不同。如何添加 Maven Wrapper在已有 Maven 的机器上执行一次:mvn -N wrapper:wrapper -Dmaven=3.9.6生成后的文件应提交到版本库:.mvn/wrapper/maven-wrapper.properties.mvn/wrapper/maven-wrapper.jarmvnwmvnw.cmdmaven-wrapper.properties 里最关键的是 distributionUrl:distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zipwrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar之后本地和 CI 都不要再直接写 mvn clean install,而是写:./mvnw -B clean verifyWindows 使用:mvnw.cmd -B clean verify如果项目需要固定 JVM 参数,可以放在 .mvn/jvm.config;如果需要默认 Maven 参数,可以放在 .mvn/maven.config。例如:文件:.mvn/maven.config--no-transfer-progress-Dstyle.color=alwaysCI 配置也很简单:name: Maven Wrapper CIon: [push, pull_request]jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: distribution: temurin java-version: '17' cache: maven - run: chmod +x mvnw - run: ./mvnw -B clean verify企业内网环境常见问题是无法访问 Maven 官方下载地址。可以把 distributionUrl 改成内部 Nexus 或 Artifactory 的 Maven zip 地址,但要保证这个地址长期可用。不要把下载后的 Maven 目录提交进仓库,.mvn/wrapper/dists/ 应该忽略。Wrapper 还应该和 JDK 版本一起管理。Maven 固定为 3.9.6,但 CI 用 JDK 11、本地用 JDK 17,构建结果仍可能不同。可以在 README、CI 配置、Dockerfile 或 .java-version 中明确 JDK,必要时用 Maven Enforcer Plugin 检查 Java 和 Maven 版本。这样失败会发生在构建早期,而不是编译一半后报出一串难懂的插件错误。安全方面也不要完全忽略。distributionUrl 指向的是构建工具下载地址,如果被改到不可信源,等于所有构建机器都会执行来路不明的工具链。企业项目可以使用内部制品库代理 Maven distribution,并配合代码评审关注 wrapper 文件变更。升级 Wrapper 时不要只看能不能构建,还要确认下载地址、版本号和脚本内容都符合团队规范。在多人协作里,README 也要同步改成 Wrapper 命令。文档写 mvn test,CI 写 ./mvnw verify,新人通常会照文档执行,最后又回到工具版本不一致的问题。更好的做法是所有示例命令都用 ./mvnw,Windows 用户旁边补一行 mvnw.cmd,避免靠口头提醒。如果仓库启用了 Dependabot 或 Renovate,也要把 Wrapper 升级纳入规则。依赖升级只改 POM,工具链升级改 wrapper,两类变更最好分开提交,回滚时才清楚影响范围。 这样 CI 失败时,定位会更快。评审时看到 wrapper 文件变化,不要只看版本号,还要确认 distributionUrl 没被改到临时地址。追问Maven Wrapper 和固定 pom 里的插件版本有什么区别?Wrapper 固定的是 Maven 运行器版本,POM 固定的是项目依赖和插件版本。两者解决的问题不同,不能互相替代。取舍上,小项目只固定插件版本也能跑,但团队协作和 CI 最好连 Maven 本身一起固定。坑在于 Maven 版本升级后传输策略变了,比如旧 HTTP 仓库被拦截,POM 没动但构建突然失败。maven-wrapper.jar 要不要提交到 Git?多数团队会提交,这样首次构建不依赖额外下载 wrapper jar,只下载 Maven distribution。也可以不提交 jar,使用下载器模式,但这增加了首次构建的不确定性。取舍点是仓库洁癖和构建稳定性,企业项目通常更偏向稳定。踩坑最多的是只提交 mvnw,没提交 .mvn/wrapper,新人执行脚本时才发现缺文件。Maven Wrapper 能不能解决依赖下载慢?不能直接解决。它只保证用哪个 Maven 版本,依赖下载速度仍取决于仓库镜像、网络和缓存。可以配合 settings.xml 的 mirror、CI cache、公司制品仓库来加速。边界要分清:Wrapper 管工具,settings 管仓库;把镜像地址硬塞进 wrapper properties 只能影响 Maven 本体下载,不会影响项目依赖。什么时候需要升级 Maven Wrapper?当 Maven 版本存在安全修复、公司统一升级、插件要求更高 Maven 版本,或者旧版本和当前 JDK 不兼容时需要升级。升级命令可以用 ./mvnw -N wrapper:wrapper -Dmaven=3.9.6,然后检查 properties 和脚本变化。取舍是新版本带来修复,也可能带来更严格的校验。常见坑是只改 distributionUrl,没有更新 wrapper 相关脚本,导致不同系统上的行为不一致。CI 里已经装了 Maven,还需要 Wrapper 吗?仍然建议用 Wrapper。CI 镜像会升级,托管平台也可能调整预装工具版本,用 Wrapper 可以让构建入口由项目控制。代价是第一次下载 Maven 会多一点时间,但缓存后影响很小。坑在于 CI 脚本一部分用 mvn,一部分用 ./mvnw,排查问题时很难确认到底是哪套 Maven 在执行。小结Maven Wrapper 是构建一致性的低成本保险。把 wrapper 文件提交进仓库,本地和 CI 统一使用 ./mvnw,再配合固定 JDK、Maven cache 和公司 settings,构建环境就不会靠口头约定维持。它不解决所有 Maven 问题,但能先把“工具版本不一致”这类噪音降下来。
服务端阅读 05月31日 22:22

Maven Assembly Plugin 如何打出可交付分发包?

Maven Assembly Plugin 解决的是“交付物不只是一个 JAR”的问题。很多命令行工具、离线部署包、传统服务程序需要 bin/、conf/、lib/、README、启动脚本一起交付,这时单纯的 mvn package 不够,Assembly 可以按描述符把文件和依赖组织成 zip、tar.gz 或 jar-with-dependencies。它不负责解决依赖冲突,也不负责容器化部署;它负责把已经构建好的东西按你指定的目录结构装箱。什么时候该用 Assembly如果你只是做 Spring Boot 应用,优先使用 spring-boot-maven-plugin 打可执行 JAR。Assembly 更适合通用 Java 程序、批处理工具、带脚本和配置文件的服务包,或者需要同时产出二进制包和源码包的项目。最常见目录是:demo-1.0.0/ bin/start.sh bin/stop.sh conf/application.yml lib/demo-1.0.0.jar lib/*.jar README.mdPOM 中引用自定义描述符:<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>3.7.1</version> <configuration> <descriptors> <descriptor>src/assembly/distribution.xml</descriptor> </descriptors> <appendAssemblyId>true</appendAssemblyId> </configuration> <executions> <execution> <id>make-dist</id> <phase>package</phase> <goals><goal>single</goal></goals> </execution> </executions></plugin>描述符决定分发包长什么样:<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.1.0 https://maven.apache.org/xsd/assembly-2.1.0.xsd"> <id>dist</id> <formats> <format>zip</format> <format>tar.gz</format> </formats> <includeBaseDirectory>true</includeBaseDirectory> <baseDirectory>${project.artifactId}-${project.version}</baseDirectory> <fileSets> <fileSet> <directory>src/main/scripts</directory> <outputDirectory>bin</outputDirectory> <fileMode>0755</fileMode> </fileSet> <fileSet> <directory>src/main/config</directory> <outputDirectory>conf</outputDirectory> <filtered>true</filtered> </fileSet> </fileSets> <dependencySets> <dependencySet> <outputDirectory>lib</outputDirectory> <useProjectArtifact>true</useProjectArtifact> <scope>runtime</scope> </dependencySet> </dependencySets></assembly>构建命令是 mvn clean package,产物会出现在 target/demo-1.0.0-dist.zip 一类文件中。CI 里可以把这个 zip 作为 artifact 上传,或者在 release 阶段发布到制品仓库。- name: Build distribution run: mvn -B clean package- name: Upload dist uses: actions/upload-artifact@v4 with: name: demo-dist path: target/*-dist.*分发包最好在构建后做一次“解压即运行”检查。比如 CI 里执行 unzip target/*-dist.zip -d target/check,再检查 bin/start.sh、conf/、lib/ 是否存在,必要时启动 java -cp 'lib/*' com.example.Main --version。这一步看似简单,却能提前发现脚本权限、路径拼接、依赖遗漏等问题。很多团队只验证 Maven 构建成功,真正交付给运维后才发现包结构不符合部署脚本预期。文件命名也要保持稳定。appendAssemblyId=true 会生成带 dist 后缀的包,适合同时产出多个 assembly;如果只需要一个主分发包,可以根据仓库规范调整。不要让 CI 用模糊路径上传一堆临时包,否则发布页面里会出现 sources、javadoc、dist 混在一起的情况。正式发布时最好对分发包生成校验和,至少保留 SHA-256,方便部署端确认下载完整性。如果分发包要给客户或离线环境使用,还要提前处理许可证和第三方依赖清单。Assembly 可以把 LICENSE、NOTICE、THIRD-PARTY.txt 放进包里,但清单内容通常要由 license 插件或内部合规流程生成。别等交付前一天再补这些文件,依赖一多,许可证核对会非常耗时。追问Assembly 和 Shade Plugin 应该怎么选?Assembly 擅长做分发包,能保留目录结构,把脚本、配置、依赖分开放。Shade Plugin 擅长做 uber-jar,并且可以重定位包名,缓解依赖冲突。取舍点是交付形式:要 zip/tar.gz 选 Assembly,要一个可运行胖 JAR 且担心依赖冲突选 Shade。常见坑是用 jar-with-dependencies 打包复杂项目,多个依赖的同名资源被覆盖,运行时才发现配置或 SPI 丢失。dependencySet 的 scope 应该怎么写?大多数运行包使用 runtime,这样会包含运行必需依赖,不会把 test 依赖打进去。如果是开发调试包,可能需要额外包含源码或文档,但正式分发包应尽量干净。边界在于 provided 依赖是否由目标环境提供,比如 Servlet 容器或应用服务器。坑在于把 provided 也打进 lib,线上类加载顺序变化后出现版本冲突。配置文件要不要 filtered?可以过滤构建号、版本号这类非敏感值,但不要把数据库密码、生产密钥过滤进包里。分发包通常应该携带模板配置,真正的环境值由部署系统注入。取舍是开箱即用和安全隔离之间的平衡:内部工具可以多给默认值,生产服务应少塞秘密。常见坑是开启 filtering 后,YAML 里的 ${...} 被 Maven 当占位符处理,Spring 运行时反而拿不到原本想保留的变量。fileMode 在 Windows 上还有意义吗?对 zip 来说意义有限,对 tar.gz 在 Linux 部署场景很重要。启动脚本如果没有 0755 权限,解压后第一步就是 chmod,自动化部署会多一个失败点。边界是目标平台全是 Windows 时可以不强调 fileMode,但跨平台包最好同时提供 .sh 和 .bat。坑在于脚本在 Git 里有执行权限,本地测试没问题,CI 重新打包后权限丢了。多模块项目里 Assembly 应该放在哪个模块?通常放在专门的 distribution 模块,而不是塞进业务模块。这样它可以依赖多个子模块产物,再统一组装成一个包。取舍是结构稍微多一层,但职责更清楚,避免每个模块都知道最终分发目录。常见坑是在父 POM 里绑定 assembly,结果所有子模块都执行一次,产物重复、路径混乱,还拖慢构建。小结Assembly Plugin 的关键是先想清楚交付目录,再写描述符。应用 JAR、脚本、配置、依赖各归其位,CI 只负责稳定产出和上传。不要把它当依赖冲突修复工具,也不要把环境秘密打进包里,这两个边界守住,分发包会好维护很多。
服务端阅读 05月31日 22:22

Maven Release Plugin 如何安全完成版本发布?

Maven Release Plugin 用来把“准备版本、打标签、发布制品、推进下一个快照版本”这组动作自动化。它适合仍以 Maven 仓库发布 JAR、WAR 或内部 SDK 的团队,尤其是多模块项目,不适合把所有发布逻辑都塞进一个临时脚本。它的价值在于可追溯:哪个 Git tag 对应哪个 Maven version,哪个构建产物进入了 releases 仓库,都能对上。发布前要先把 POM 配完整Release Plugin 依赖 SCM 和 distributionManagement。没有 SCM,它不知道标签打到哪里;没有发布仓库,它只能改版本和打 tag,不能把制品 deploy 出去。<scm> <connection>scm:git:https://github.com/acme/demo.git</connection> <developerConnection>scm:git:git@github.com:acme/demo.git</developerConnection> <url>https://github.com/acme/demo</url> <tag>HEAD</tag></scm><distributionManagement> <repository> <id>company-releases</id> <url>https://repo.example.com/repository/maven-releases/</url> </repository> <snapshotRepository> <id>company-snapshots</id> <url>https://repo.example.com/repository/maven-snapshots/</url> </snapshotRepository></distributionManagement>插件配置可以从保守开始:<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-release-plugin</artifactId> <version>3.0.1</version> <configuration> <tagNameFormat>v@{project.version}</tagNameFormat> <autoVersionSubmodules>true</autoVersionSubmodules> <releaseProfiles>release</releaseProfiles> <goals>clean deploy</goals> </configuration></plugin>仓库认证不要写进 POM,应放到 ~/.m2/settings.xml 或 CI Secret 注入的 settings 文件里:<servers> <server> <id>company-releases</id> <username>${env.MAVEN_REPO_USER}</username> <password>${env.MAVEN_REPO_PASSWORD}</password> </server></servers>发布命令通常分两步。先 mvn -B release:prepare,它会检查工作区、确认没有 SNAPSHOT 依赖、把版本从 1.2.0-SNAPSHOT 改成 1.2.0、提交、打 tag,再改成下一个快照版本。再执行 mvn -B release:perform,它会检出 tag 并执行 deploy。第一次接入一定先跑 mvn -B release:prepare -DdryRun=true,确认版本号、tag 名称和提交内容没问题。CI 示例可以这样写:name: Maven Releaseon: workflow_dispatch: inputs: releaseVersion: required: true developmentVersion: required: truejobs: release: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-java@v4 with: distribution: temurin java-version: '17' cache: maven - run: mvn -B release:prepare release:perform -DreleaseVersion=${{ inputs.releaseVersion }} -DdevelopmentVersion=${{ inputs.developmentVersion }}发布流程还要考虑分支模型。主干开发团队通常在 main 上保持可发布状态,releaseVersion 由人工输入或从 tag 推导;Git Flow 团队可能从 release 分支执行 prepare,再把版本提交合回主干。两种方式都能用,但不要让插件一边改版本一边跨分支合并,这会让冲突难以理解。比较稳的做法是发布分支只做版本冻结和缺陷修复,功能变更不要混进来。多模块项目更要提前决定版本策略。autoVersionSubmodules=true 会让所有子模块使用同一个版本,简单、清晰,也适合一起发布的服务组。如果子模块各自独立发布,Release Plugin 的交互式流程会变得很重,甚至不如使用 flatten-maven-plugin、versions-maven-plugin 或专门的发布脚本。边界在于模块之间是否必须同进同出,不能只因为“这是多模块项目”就默认全部一起发。还有一个现实问题是发布说明。Release Plugin 不会自动替你判断哪些变更值得写进 changelog,它最多保证版本和 tag 可追溯。团队如果需要面向用户的发布说明,最好在 CI 中根据 PR 标题或 conventional commits 生成草稿,再由负责人确认。不要把“构建成功”等同于“发布准备充分”。追问release:prepare 和 release:perform 到底差在哪?prepare 改的是源码仓库状态:检查、改版本、提交和打标签。perform 面向发布仓库:从标签检出干净代码,然后构建并 deploy。边界很清楚,prepare 成功不代表制品已经发布,perform 失败也不应该随手删 tag。常见坑是把两步放在本地执行,中途网络失败后工作区、tag 和远程仓库状态不一致,恢复成本很高。为什么发布前不能有 SNAPSHOT 依赖?正式版本需要可重复构建,而 SNAPSHOT 依赖随时可能变化。今天同一个 tag 构建出的包和明天不一致,发布审计就失去意义。取舍是内部快速迭代阶段可以用 SNAPSHOT,但进入 release 分支前必须替换为正式版本。踩坑点是传递依赖里藏着 SNAPSHOT,表面 POM 看不出来,需要 mvn dependency:tree | grep SNAPSHOT 检查。Maven Release Plugin 适合所有项目发布吗?不适合。它适合 Maven 制品发布和版本号强绑定的项目,比如 Java SDK、内部 starter、多模块服务。容器镜像发布、Helm Chart 发布或前后端混合发布,通常需要 CI 编排更多步骤,Release Plugin 只能负责 Maven 这部分。边界在于“最终交付物是不是 Maven artifact”,如果不是,就不要强行让它接管整个发布流程。发布失败后应该 rollback 还是手动修?如果失败发生在 prepare 阶段且还没推送远程,mvn release:rollback 通常够用。如果 tag 已推送或制品已 deploy,就要按团队发布规范处理,不能简单回滚本地文件。取舍点是保持历史真实还是追求清爽,正式发布场景通常宁可追加修复版本,也不要改写已公开的 tag。常见坑是删除远程 tag 后重新发布同版本,结果仓库里已有不可覆盖的 release artifact。在 CI 中运行 Release Plugin 要注意什么?CI 必须能推送 tag,也必须能访问 Maven 发布仓库,所以 Git 凭据和 Maven settings 都要提前准备。actions/checkout 要设置 fetch-depth: 0,否则插件可能拿不到完整 SCM 信息。为了减少误发布,建议只允许手动触发或受保护分支触发,并把 releaseVersion、developmentVersion 显式输入。坑在于使用默认 token 权限不足,prepare 到 push tag 时才失败,前面版本文件已经被改过。小结Maven Release Plugin 的重点不是命令多熟,而是发布边界清楚:POM 里声明 SCM 和仓库,settings 里放凭据,CI 中固定触发入口。先 dryRun,再 prepare,最后 perform;失败后先判断已经影响到本地、远程 tag 还是制品仓库,再决定回滚方式。
服务端阅读 05月31日 22:22

Spring Boot 项目如何配置 Maven 才不容易踩坑?

Spring Boot 项目里,Maven 的核心作用不是“把依赖写进 pom.xml”这么简单,而是把 Java 版本、依赖版本、打包方式、测试插件和运行命令统一到一套可重复的构建规则里。最省心的做法是使用 spring-boot-starter-parent,让 Spring Boot 帮你管理常见依赖和插件版本;如果公司已有统一父 POM,则用 spring-boot-dependencies 做 dependencyManagement。两种方式都能用,区别在于谁来当父 POM,这一点在企业项目里经常被忽略。Spring Boot 和 Maven 集成的关键配置典型单体服务可以直接继承 Spring Boot 父 POM:<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.5</version> <relativePath/></parent><properties> <java.version>17</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency></dependencies>如果不能继承它,比如公司已经有 company-parent,就导入 BOM:<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>3.2.5</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies></dependencyManagement>打包可执行 JAR 时必须关注 spring-boot-maven-plugin。它会把普通 JAR 重新打成包含启动器和依赖布局的 Boot JAR,否则 java -jar 可能找不到主类或依赖。<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <mainClass>com.example.Application</mainClass> </configuration> <executions> <execution> <goals><goal>repackage</goal></goals> </execution> </executions> </plugin> </plugins></build>常用命令保持简单即可:mvn clean package 打包,mvn spring-boot:run 本地启动,java -jar target/app.jar --spring.profiles.active=prod 按环境运行。Profile 可以放在 Maven 里控制构建参数,但业务环境更推荐由 Spring 的 application-dev.yml、环境变量或启动参数控制,避免“构建一次只能用于一个环境”。还有一个容易被忽略的点是测试插件。Spring Boot 父 POM 会管理 Surefire 和 Failsafe 的常见版本,但如果企业父 POM 覆盖过这些插件,JUnit 5 测试可能出现本地能跑、CI 不识别测试用例的情况。排查时不要只看依赖,先执行 mvn -X test 或检查 target/surefire-reports 是否生成。对于多模块项目,建议在父 POM 统一插件版本,在应用模块只保留差异化配置。资源过滤也要克制使用。src/main/resources 全量开启 filtering 后,某些二进制资源、证书文件或包含 ${} 的配置文件可能被 Maven 意外处理。更稳妥的方式是只对 application-build.properties 这类构建信息文件开启过滤,把运行环境变量留给 Spring Boot 自己解析。这样构建产物更通用,也更适合 Docker 镜像和多环境部署。版本升级时也要先读 Spring Boot 的 release notes。比如从 2.x 升到 3.x,Java 基线、Jakarta 包名、部分自动配置条件都会变化,单靠 Maven 改版本并不能完成迁移。稳妥做法是先在分支里升级父 POM 或 BOM,再跑完整测试和依赖树检查,最后处理运行时配置差异。追问starter-parent 和 spring-boot-dependencies 应该选哪个?starter-parent 适合新项目或没有统一父 POM 的项目,因为它连插件默认配置也一起给了。spring-boot-dependencies 更适合企业项目,它只导入依赖版本,不抢父 POM 位置。取舍点在于控制权:前者省配置,后者更容易接入公司构建规范。常见坑是导入 BOM 后以为插件版本也被完整管理了,结果编译器插件、surefire 插件仍受公司父 POM 或 Maven 默认值影响。为什么不建议给每个 starter 手动写 version?Spring Boot 的 BOM 已经验证过一组依赖组合,手动写版本会破坏这套兼容矩阵。偶尔覆盖版本可以解决安全漏洞,但要配合 mvn dependency:tree 看传递依赖有没有被带歪。边界是底层库存在紧急 CVE 或公司必须统一版本时才覆盖,而不是为了“用最新版”。踩坑最多的是 Jackson、Tomcat、Hibernate 这类深度集成依赖,单独升级后编译通过,运行时才暴露方法签名不兼容。Maven Profile 能不能直接管理 Spring 的 dev、prod 环境?可以,但不建议把它当唯一方案。Maven Profile 更适合控制构建时差异,比如是否开启资源过滤、是否打包前端资源、是否替换构建号。Spring Profile 更适合控制运行时差异,比如数据库、缓存、日志级别。坑在于把生产配置过滤进 JAR,后面同一个包就没法在不同环境复用,也容易把敏感信息带进构建产物。spring-boot-maven-plugin 和 maven-jar-plugin 有什么区别?maven-jar-plugin 生成普通 JAR,主要负责 manifest、包含文件等基础打包。spring-boot-maven-plugin 的 repackage 会把普通 JAR 改造成可执行 Boot JAR,并处理依赖嵌套和启动类。取舍上,库项目不应该打 Boot JAR,应用项目才需要。常见坑是多模块项目在公共模块也启用 repackage,导致公共模块无法作为普通依赖被其他模块正确引用。CI 里构建 Spring Boot 项目有哪些必要检查?至少要跑 mvn -B clean verify,让测试、编译和打包在非交互模式下完成。可以加 mvn dependency:tree 或 OWASP Dependency Check 做依赖排查,但安全扫描会增加耗时,适合按分支或定时任务分层执行。CI 中最好固定 JDK 和 Maven 版本,否则本地能过、流水线失败很难排查。一个常见坑是 CI 使用 JDK 21,本地使用 JDK 17,Spring Boot 版本、插件版本和字节码目标不一致时会出现莫名其妙的编译或测试问题。小结Spring Boot 与 Maven 集成的重点是版本统一、插件正确、环境边界清晰。新项目优先用 starter-parent,企业项目用 BOM 接入现有父 POM;应用模块启用 spring-boot-maven-plugin,公共模块保持普通 JAR。只要这几条边界守住,后面的依赖升级、打包和 CI 才不会变成反复试错。