我刚开始用 Java 做项目那会儿,最头疼的不是业务代码怎么写,而是怎么把项目跑起来。那时候没有统一的构建工具,每人一套 jar 包,A 同事拷给你一个 lib 目录,B 同事发你一个压缩包,版本对不上就当场爆炸。后来接触了 Maven,再配合 Spring Boot 这套组合拳,才真正体会到什么叫"工程化开发"。这篇文章不打算做百科式的科普,我想从一个实际做项目的角度,把 Maven 怎么装、怎么配、怎么和 Spring Boot 集成、日常怎么操作,包括那些文档里不会写但你一定会遇到的坑,一次性说清楚。
这篇文章适合三类人:刚接触 Java 生态、被各种依赖搞到怀疑人生的新手;会用 Maven 但说不清原理、遇到报错只能瞎搜的半熟手;以及想带着团队规范构建流程、减少无意义争吵的技术负责人。我会尽量把"为什么这样做"也讲透,而不是只给步骤。
1. Maven到底解决了什么问题:先建立整体认知框架
很多教程上来就让你装 Maven、配环境变量,结果你装完了也不知道它在项目里扮演什么角色。我先花点篇幅把 Maven 的核心价值讲清楚,因为只有理解了它解决什么问题,后面遇到坑你才知道往哪个方向排查。
1.1 依赖管理:从"手动拷贝 jar 包"到"声明式拉取"
在 Maven 出现之前,Java 项目的依赖管理基本靠人工。你从网上下载一个 jar 包,扔进 lib 目录,再右键项目把 jar 包加入 Build Path。这个过程有三个致命问题:第一,jar 包本身没有版本约束,A 同事用的 2.0,B 同事用的 2.1,合并代码后行为不一致;第二,jar 包之间的传递依赖你根本管不了,比如你引了 HttpClient,它内部依赖了某个版本的 commons-logging,这个 jar 你往往意识不到它是存在的;第三,换一台机器或者换一个人,整个环境的搭建成本极高。
Maven 的做法是引入"坐标"概念。每个依赖都会声明三要素:groupId、artifactId、version。你在 pom.xml 里声明我需要什么库、什么版本,Maven 会根据坐标自动去本地仓库找,找不到就去远程仓库下载,同时会把依赖的依赖(传递依赖)一并拉下来。这就是为什么你用 Maven 引入一个 Spring Boot 相关的 starter 之后,项目中会多出几十个 jar 包——那些都是传递依赖。
1.2 标准化的构建生命周期:让"构建"这件事变得可预期
Maven 把项目的构建过程抽象成了一套标准化的生命周期(Lifecycle),核心包含:validate、compile、test、package、verify、install、deploy 这几个阶段。当你执行某一个阶段时,它前面的所有阶段都会自动执行。比如你运行 mvn install,Maven 会先帮你编译源码,再运行测试,再打包成 jar/war,最后把构建产物安装到本地仓库。
这个机制的价值在于标准化。不管项目是谁写的,只要它是 Maven 工程,新接手的人看几个核心命令就明白怎么构建:mvn clean 清掉旧产物,mvn test 跑单元测试,mvn package 出包,mvn install 供本机其他模块引用,mvn deploy 发布到远程仓库。对一个团队来说,统一构建方式比统一代码风格更能减少内耗。
1.3 约定优于配置:目录结构本身就是规范
Maven 规定了一套标准的工程目录结构:src/main/java 放业务代码,src/main/resources 放配置文件,src/test/java 放单元测试。这套约定让所有 Maven 工程看起来都是"同构"的。你用 IDEA 打开任何一个规范的 Maven 项目,不用看文档就能猜到哪里找代码、哪里改配置,这种一致性是 Maven 对团队协作最大的贡献之一。
Spring Boot 官方推荐的项目结构也完全符合 Maven 约定,所以两者天然契合。你创建一个 Spring Boot 项目后,看到的目录骨架其实就是 Maven 约定结构的标准形态。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 安装与环境配置:这一步最容易埋雷,踩过的人不在少数
环境配置这块我见过太多人出问题,大多是版本不匹配和镜像源没配好导致的。我把自己验证过的方案写出来,你照着操作基本能一次过。
2.1 版本选择和 JDK 的兼容性
先说结论:如果你用的是 JDK 8,装 Maven 3.6.3 或 3.8.x 都行;如果你用的是 JDK 11 及以上,建议装 Maven 3.8.5 以上版本。
Maven 和 JDK 之间存在兼容关系。Maven 3.3 之前的版本对 JDK 9+ 的支持很差,会出现反射访问报错之类的问题。Maven 3.9.x 虽然也常见,但在一些旧项目的 maven-compiler-plugin 版本较低时会出现兼容性问题,所以我个人在中间件环境里更倾向于 3.8.x 产线版本。你现在去 Maven 官网下载 apache-maven-3.8.8,是目前社区验证比较充分的版本,Windows、Linux、macOS 三个平台都有对应压缩包。
下载之后解压到一个纯英文路径,比如 Windows 下放在 D:\dev\apache-maven-3.8.8,macOS 下放在 /usr/local/apache-maven-3.8.8。路径里有中文或者空格,后面执行 mvn 命令时会有各种莫名其妙的问题,别给自己找不痛快。
2.2 环境变量的配置细节
Windows 系统下,你需要在系统环境变量里新增一个 MAVEN_HOME,值指向你的 Maven 解压目录。然后在 Path 变量里追加 %MAVEN_HOME%\bin。配完之后打开命令行执行 mvn -v,如果能看到版本号、Java 版本信息,说明配置成功。
macOS 和 Linux 下,编辑 ~/.bash_profile 或 ~/.zshrc,写入:
bash复制export MAVEN_HOME=/usr/local/apache-maven-3.8.8
export PATH=$MAVEN_HOME/bin:$PATH
然后执行 source ~/.zshrc 让配置生效。有一个细节容易被忽略:mvn -v 输出的 Java 版本决定了 Maven 运行时的 JDK,你项目实际用哪个 JDK 编译,是由 pom.xml 里配置的 maven.compiler.source/target 或 java.version 属性控制的。两者可以不一致,但最好保持一致,否则会出现"本地编译能过、服务器上跑不起来"的问题。
2.3 仓库配置:本地仓库、中央仓库和镜像仓库的关系
Maven 下载依赖时遵循一个查找顺序:先查本地仓库,本地没有就去中央仓库或镜像仓库下。本地仓库默认位置在用户目录下的 .m2/repository,Windows 在 C:\Users\你的用户名\.m2\repository,macOS 在 /Users/你的用户名/.m2/repository。
默认的中央仓库服务器在国外,国内网络环境下下载依赖经常慢到怀疑人生。解决办法是修改 settings.xml 文件,配置阿里云镜像。这个文件在 Maven 安装目录的 conf 文件夹下,修改它会对本机所有 Maven 工程生效。
xml复制<mirrors>
<mirror>
<id>aliyunmaven</id>
<mirrorOf>central</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
</mirrors>
注意 mirrorOf 的取值:配成 central 表示只拦截中央仓库的请求;如果你的公司有私服,想全部走私服,可以配成 *。我不建议上来就配 *,因为你可能在本地依赖了一些第三方公开仓库的构件,全部拦到私服反而会拉不到。
这里也顺便回答一个经常被问到的问题:为什么我改了 settings.xml 但 IDEA 里下载依赖还是走的老地址?因为你需要检查 IDEA 里 Maven 设置中的 User settings file 是否指向了你改的那个 settings.xml。IDEA 默认会使用自己内置的 Maven 配置路径,你要在 Settings -> Build, Execution, Deployment -> Build Tools -> Maven 里手动指定。
2.4 关于仓库路径迁移的建议
很多开发者的 C 盘空间告急,而 Maven 本地仓库默认就在 C 盘用户目录下,下载的依赖越多,C 盘占用越大。你可以把本地仓库迁到其他盘:在 settings.xml 里找到 localRepository 标签,改成你要的路径,比如:
xml复制<localRepository>D:/dev/maven-repository</localRepository>
改完之后记得把原来 ~/.m2/repository 里的内容复制过去,否则之前下载的依赖全要重新下。这个操作是纯配置修改,不涉及任何代码逻辑,但确实能让你少骂几次电脑。
3. 打通 Spring Boot:从零创建一个可运行的工程
Maven 环境就绪之后,真正的重头戏是和 Spring Boot 集成。我要先解释一个让很多人困惑的问题:为什么 Spring Boot 项目在 pom.xml 里引入一个 spring-boot-starter-parent 之后,很多东西都不用配了?
3.1 理解 spring-boot-starter-parent:它不是普通依赖
spring-boot-starter-parent 是一个 Maven Parent POM。它本身不是一个具体功能库,而是一个"依赖管理中枢"。它内部通过 dependencyManagement 声明了大量常用依赖的版本号。比如你引入 spring-boot-starter-web 时不需要写版本号,就是因为版本已经被这个 Parent POM 托管了。
这样设计的核心好处是版本统一。Spring Boot 团队对一个版本组合做过完整测试,哪些库和哪些库兼容、各自用什么版本,他们已经在 Parent POM 里锁好了。你不需要自己操心这些匹配关系,也避免了团队里每个人各自引版本导致的三方库冲突。这个特性叫"依赖版本仲裁"。
一个标准 Spring Boot 项目的 pom.xml 核心结构长这样:
xml复制<?xml version="1.0" encoding="UTF-8"?>
<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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>1.0.0</version>
<name>demo</name>
<properties>
<java.version>8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
你注意到 spring-boot-starter-web 没有写 <version>,这就是 Parent POM 在发挥作用。java.version 这个属性同时控制编译级别和 Spring Boot 框架内部的依赖版本选择,比如你写 8,很多组件会选用兼容 JDK8 的版本。
3.2 版本号选择的一个实用建议
Spring Boot 3.x 是当前主线版本,但它要求 JDK 17 以上。如果你公司的服务器还停留在 JDK8,那就老老实实选 2.7.x 的最后一个版本(目前是 2.7.18)。我自己在维护老项目时始终遵循一个原则:生产环境的 Spring Boot 版本只选奇数小版本之后的修订版,不追新。比如 2.7.x 系列选 2.7.18,3.2.x 系列选 3.2.5 之后,等社区跑一段时间确认没坑了再升。新版本发布初期经常会出现各种兼容性疏漏,尤其是第三方 starter 没有跟上 Spring Boot 版本节奏的时候,换版本踩坑的成本远大于那点新特性带来的收益。
3.3 手动创建和骨架生成的取舍
很多人创建 Spring Boot 项目会直接去 IDEA 里选 Spring Initializr,联网让官方帮你生成工程,然后用 IDEA 打开。这种方式没问题,但我建议新手至少手动创建一次。你手动建出 src/main/java、src/main/resources 目录,手动写出第一份 pom.xml,你会真正理解这个工程的骨架规则,之后遇到 IDEA 自动生成的项目报错时,你才有排查的线索。
我当时手动创建时花了一晚上搞清楚三件事:第一,为什么 src/main/resources 下的 application.yml 会被自动加载——因为 Maven 约定将该目录作为资源根目录,构建时会把它复制到 classes 目录,Spring Boot 启动时会从 classpath 根路径读取配置文件;第二,为什么启动类要放在包的根目录——因为 @SpringBootApplication 默认扫描的是启动类所在包及其子包;第三,为什么改了代码需要重启才能生效——因为 Spring Boot 的 spring-boot-devtools 依赖提供了热重启功能,但没有依赖它时你手动改完代码必须重启应用。这些理解比单纯点下一步生成一个工程要有用得多。
3.4 一个必须掌握的构建命令组合
在 Spring Boot 项目根目录执行:
bash复制mvn clean package
这条命令做了四件事:清空 target 目录、编译主代码、执行单元测试、把项目打包成可执行的 jar 包。打包完成的 jar 在 target 目录下,名字遵循 <artifactId>-<version>.jar 的规则。Spring Boot 的可执行 jar 是一个特殊的 fat jar,它会将依赖的三方库嵌入 jar 内部,所以这个 jar 可以直接用 java -jar 运行,不需要额外配置 classpath。
如果你只想快速跑起来不想打包,执行:
bash复制mvn spring-boot:run
这个命令会直接启动 Spring Boot 应用。它和 java -jar 启动的区别在于:前者会使用你当前工作区的代码即时编译启动,适合开发调试;后者使用打包产物,更贴近生产环境的行为。
4. 高频 Maven 操作背后的原理与注意事项
接下来这部分是日常开发中你反复要用的操作。我不只告诉你命令是什么,更想说明白每条命令背后的细节。
4.1 clean、compile、test、install、deploy:一条条拆解
-
mvn clean:清理target目录,把之前的构建产物全部删掉。很多人觉得这不是可有可无的操作,但实际上了target目录里残留的旧 class 文件会导致你的修改没有生效,代码改了但行为没变化,排查半天发现是构建缓存问题。我在改动依赖版本或改动代码结构时,习惯先clean再package。 -
mvn compile:只编译src/main/java下的代码到target/classes。这个命令平时用得少,但排查编译报错时很好用,因为它的输出日志更聚焦,不会被测试和打包的信息干扰。 -
mvn test:运行src/test/java下的单元测试。如果你使用mvn package打包,测试阶段会自动执行,如果测试失败则打包中断。这对保证交付质量很有帮助,但有一个常见矛盾点:当数据库或中间件依赖不可用导致测试环境不稳定时,你并不想让测试阻塞打包。这种情况下你可以在打包时加上-Dmaven.test.skip=true跳过测试,注意-DskipTests只是不执行测试但仍会编译测试代码,-Dmaven.test.skip=true连测试代码都不编译。两者的差异在大型项目中影响显著,跳过编译测试类能省不少时间。 -
mvn install:把当前模块的构建结果安装到本地 Maven 仓库。这个操作是多模块项目的基石。比如你的工程有common模块和web模块,web依赖common,你必须先在common模块下执行mvn install,web模块才能通过本地仓库找到common的最新版本。如果web模块一直用的旧的common代码,往往是因为你改了common却没有重新install。 -
mvn deploy:把构建产物发布到远程仓库(公司私服或中央仓库发布服务器)。这个命令通常只有负责发布的人执行,但团队协作时每个人都应该理解,因为你依赖的某个内部工具包就是这么通过deploy进入公司仓库的。
4.2 依赖下载慢和失败的重试思路
网络环境差的时候,mvn package 可能会卡在下载依赖的环节,甚至出现 Could not transfer artifact 的报错。大部分情况都是网络中断或镜像源不稳定造成的,解决办法第一步不是重试,而是先确认你的依赖到底卡在哪个仓库上。执行:
bash复制mvn dependency:resolve -X
用 -X 参数开启调试日志,日志会打印每次下载请求的实际 URL。如果发现走了 repo.maven.apache.org,说明你的镜像配置没生效,回到 settings.xml 重新检查 mirrorOf 配置。如果确认走的是阿里云,但依然超时,可以临时切换其他镜像,比如华为云:
xml复制<mirror>
<id>huaweicloud</id>
<mirrorOf>central</mirrorOf>
<url>https://repo.huaweicloud.com/repository/maven/</url>
</mirror>
还有一种常见情况:jar 包下载了一半导致本地仓库出现 .lastUpdated 后缀的残留文件,之后 Maven 再也不会重新下载这个 jar,一直报同一个错。这是 Maven 的失败缓存机制在起作用。解决办法是找到本地仓库里对应的目录,把 .lastUpdated 文件删掉,再重新执行构建。
4.3 IDEA 中 Maven 面板的操作逻辑
IDEA 右侧的 Maven 工具窗口不只是让你点几个按钮用的。双击某个依赖可以直接看到它的传递依赖树;点击 Download Sources 按钮可以下载这个 jar 的源码包,想看三方库内部实现时就靠这个功能。
同时我强烈建议你注意 IDEA 的一个默认行为:使用 IDEA 自身内置的 Maven 还是你命令行用的那个 Maven。如果你在命令行配了阿里云镜像、换了本地仓库位置,但 IDEA 里用的是内置 Maven 和内置配置,那么两边行为会不一致。最典型的表现是 IDEA 里能编译,命令行一编译就报依赖缺失。你需要在 Settings -> Build Tools -> Maven 里把 Maven home path 指向你自己安装的目录,User settings file 指向你的 settings.xml。这一步很多人忽略,但它是所有 IDEA/Maven 联动的坑中概率最高的一个。
4.4 一个实操命令:如何快速查看依赖冲突
依赖冲突是我处理过最多的 Maven 问题,后面会有专门章节细讲。这里先给一条高频命令:
bash复制mvn dependency:tree -Dverbose
dependency:tree 会以树形结构打印当前项目的完整依赖关系,-Dverbose 会额外显示那些被省略的传递依赖以及仲裁结果。当你怀疑某个 jar 版本不对时,先跑这条命令看实际的版本是什么,再倒推是谁把它带进来的。这是排查依赖问题最基本的操作路径。
5. 依赖冲突与版本仲裁:每个 Java 开发者都会遇到的硬仗
依赖冲突这个坑,理论上任何一个有一定规模的 Spring Boot 项目都躲不过。我先讲清楚 Maven 是怎么从一堆冲突的版本中选出最终版本的,然后再讲遇到问题时的排查手法。
5.1 Maven 的版本仲裁策略
当同一个构件出现多个版本时,Maven 采用"最短路径优先"原则。比如你的项目直接依赖了 A 库 1.0 和 B 库 2.0,而 A 库内部传递依赖了 C 库 1.0,B 库内部传递依赖了 C 库 2.0。此时 C 库离你的项目有两条路径:A->C(长度为2)和 B->C(长度为2),路径一样长,那就按照声明顺序,谁先声明谁生效。
如果路径长度不一样,短路径的版本获胜。这就导致了一个很隐蔽的问题:你的项目用的 C 库版本可能既不是最新的,也不是你想要的,而是那个"依赖路径更短的"。
5.2 最常见的冲突案例:NoSuchMethodError 和 ClassNotFoundException
Spring Boot 项目中最经典的冲突案例是 org.apache.commons 的 commons-lang3 版本冲突,或者 Fastjson 的多个版本共存。当你启动应用时,某一处代码调用了新版本才有的方法,但实际加载到 classpath 上的是旧版本,JVM 直接抛 NoSuchMethodError。这种错误非常误导人,因为它不在编译期暴露,而是在运行时才爆发。
排查过程我总结为三步:
- 先跑
mvn dependency:tree -Dverbose找出当前生效的版本。 - 看这个版本是被哪条路径带进来的,用
-Dincludes=groupId:artifactId过滤只查冲突的构件。 - 在
pom.xml中对不适用的依赖用<exclusion>排除,或直接用<dependencyManagement>锁定你期望的版本。
5.3 用 exclusion 和 dependencyManagement 管控版本
排除依赖的写法要稍微小心,坐标必须写完整:
xml复制<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
如果你要排除某个间接依赖,比如你引入了 com.foo:some-lib,它传递依赖了一个老版本的 commons-io,但你项目其他地方已经用了新版本,你就在这个依赖里排除它:
xml复制<dependency>
<groupId>com.foo</groupId>
<artifactId>some-lib</artifactId>
<version>1.0.0</version>
<exclusions>
<exclusion>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</exclusion>
</exclusions>
</dependency>
而 dependencyManagement 的使用场景是强制统一某个依赖的版本。在 <dependencyManagement> 里声明版本之后,后续所有 <dependencies> 中该构件如果不写版本,就会统一使用你声明的版本:
xml复制<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
</dependencies>
</dependencyManagement>
这里有个细节:dependencyManagement 只是统一版本,并不会真的引入依赖。真正要引入还是要写在 <dependencies> 里。不要在 dependencyManagement 里声明了一堆依赖就以为它们都被引入了,这是很多新人的误区。
5.4 一个排查思路的实战演绎
我遇到过一次非常折磨人的问题:项目发布后在某些环境启动正常,在另一些环境启动直接报 ClassNotFoundException: org.apache.ibatis.session.Configuration。我的第一反应是 MyBatis 的版本问题,于是跑 mvn dependency:tree,发现存在两条 MyBatis 的依赖路径:一条是直接依赖的 mybatis-spring-boot-starter 带进来的 3.5.x,另一条是某个内部工具包传递依赖的 3.4.x。由于传递依赖的路径更短,Maven 选择了 3.4.x,导致某些新 API 在某环境编译时能解析、运行时却找不到类。
解决办法是直接在 pom.xml 中引入 mybatis 依赖并指定正确版本,利用最短路径让显式声明的版本覆盖传递依赖的版本。这个案例说明:很多冲突问题跟代码无关,就是依赖树里的版本选择在作祟。排查思路远比记住某个具体报错更重要。
6. 踩坑实录:Maven 操作中的高频报错与修复链路
这一节我把自己实际踩过、也帮别人排查过的问题整理成清单,每个问题都按"现象-原因-解决"的结构来讲。这些问题在社区里反复出现,你照着排查大概率能命中。
6.1 编译报错"程序包不存在"或"找不到符号"
这是最常见的一个报错,新手遇到基本懵圈。现象是 IDEA 里可能还能正常跑,但用 mvn package 打包时直接报找不到某个包或类。
排查链路按顺序走:先确认你依赖的坐标是否写对,包括 groupId、artifactId、version 三要素是不是齐全,有没有漏版本号导致依赖无法解析;再用 mvn dependency:tree 确认这个依赖是否真的存在;如果依赖存在但就是找不到类,那大概率是两个可能:一是你依赖的那个 jar 包是旧版本,里面根本没有当前引用的类;二是依赖被系统排除了,看看有没有 <optional> 或 <scope>provided</scope> 导致依赖没有进入编译 classpath。
另外,在多模块项目中还有一个特殊原因:你改动了某个模块的代码,但其他模块依赖的还是旧版本的该模块。解决方式是到被依赖的模块目录下执行 mvn install,把它安装到本地仓库。
6.2 "Invalid LOC header (bad signature)" 报错
这个报错看起来非常吓人,但实际上是本地仓库的 jar 包损坏导致的。原因通常是你之前用 IDEA 强行中断过 Maven 下载,或者其他工具篡改了本地仓库的文件。解决方式:
- 根据报错信息找到具体是哪个 jar 包出了问题。
- 在本地仓库中删除对应的目录。
- 重新执行
mvn package,让 Maven 重新下载这个 jar。
不要试图手工替换一个 jar 文件进去,因为签名校验会再次失败,删掉让它重新下是最干净的。
6.3 "Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin" 测试执行失败
Surefire 是 Maven 默认的测试执行插件。出现这个报错时,通常是你的单元测试里有些测试用例跑挂了。数据源、网络、Mock 不完整,都会导致测试失败。此时项目会终止打包。
开发阶段如果想快速构建,可以用 -DskipTests 跳过,但这不是长久之计。正确做法是打开 target/surefire-reports 目录下的测试报告,找到具体失败的用例,把测试代码修好。把测试挂了就手动跳过是一种很危险的习惯,它会让回归问题在不知不觉中积累。
6.4 Spring Boot 项目打包成普通 jar 而非可执行 jar
我见过这样的场景:编写了标准的 Spring Boot 项目,执行 mvn package 后生成一个挺小的 jar,用 java -jar 启动却报"没有主清单属性"(no main manifest attribute)。原因很明确:pom.xml 里没有配置 spring-boot-maven-plugin。
这个插件做的事情是在打包阶段对 jar 进行重新整理,把所有依赖 lib 放到 jar 内部,并生成 Main-Class 和 Start-Class 清单信息。如果你的 pom 里没有这个插件,打出来的就是一个普通 jar,当然无法直接运行。确认 build 节点下是否包含:
xml复制<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
6.5 IDEA 中 Spring Boot 项目启动超时
有些人在 IDEA 里运行 Spring Boot 主类时,长时间停留在构建阶段,然后报超时。这种问题通常是 IDEA 在启动时执行 Maven 配置更新或依赖下载阻塞了。排查手段:
- 看 IDEA 右下角是否在下载依赖,如果在下载就耐心等待
- 检查
Settings -> Build, Execution, Deployment -> Build Tools -> Maven -> Importing里的VM options for importer是否设置过限制,内存太小会导致解析大项目卡住 - 如果超时频繁,试着手动在命令行执行一次
mvn clean package,排除 Maven 层面的阻塞
6.6 Maven 命令卡住不动但 CPU 没占用
控制台卡在 Downloading... 且长时间没有进展,多半是网络原因。特别是一些公司内网环境需要代理访问公网,但代理设置没有同步到 Maven。如果你在公司内网,可以在 settings.xml 中配置代理:
xml复制<proxies>
<proxy>
<id>company-proxy</id>
<active>true</active>
<protocol>http</protocol>
<host>proxy.company.com</host>
<port>8080</port>
</proxy>
</proxies>
这时也可以优先考虑用公司私服仓库替换公网镜像,既快又稳定。
7. 进阶场景:私服部署、多模块管理、Spring Boot 的 Maven 专属玩法
当你跨过基础使用阶段,会面临一些更复杂的工程化场景。这部分内容取决于你的团队规模和项目复杂度,但了解它们能在正确的时机帮你做出正确的架构决策。
7.1 公司私服(Nexus)的配置思路
当团队规模变大,每个人都直接连公网镜像下载依赖,一方面带宽压力大,另一方面代码中引用的内部产物无法通过公网仓库共享。公司内搭 Nexus 私服是主流做法。
从开发者的视角看,私服引入后的变化只有一处:settings.xml 中需要配置私服地址的 mirror 和 profile,让所有依赖先走私服,私服没有时自动从公网拉取。Nexus 本身可以作为中央仓库的代理缓存,这意味着团队所有人第一次下载某个依赖时,Nexus 会去公网拉,之后大家就都走 Nexus 本地缓存,速度快很多。
内部工具包通过部署到私服实现共享。执行 mvn deploy 前,需要在 pom.xml 里配置 distributionManagement:
xml复制<distributionManagement>
<repository>
<id>releases</id>
<url>http://nexus.company.com/repository/maven-releases/</url>
</repository>
<snapshotRepository>
<id>snapshots</id>
<url>http://nexus.company.com/repository/maven-snapshots/</url>
</snapshotRepository>
</distributionManagement>
注意:id 必须和 settings.xml 中 <servers> 配置的 id 对应,否则发布时会因为找不到认证信息报 401。
7.2 多模块项目的依赖管理实践
Spring Boot 项目做大之后,单模块结构会变得很臃肿。通常的做法是拆分成 parent、common、service、web 等模块。父模块用 packaging 类型为 pom,专门做依赖版本管理,子模块继承父模块后只写自己的业务依赖。
多模块项目里有一个容易出问题的点:子模块对兄弟模块的依赖。比如 web 模块依赖 service 模块,必须使用 ${project.version} 这种占位方式,而不是写死版本号,否则每次版本变更都要改多处:
xml复制<dependency>
<groupId>com.example</groupId>
<artifactId>service</artifactId>
<version>${project.version}</version>
</dependency>
多模块项目根目录执行 mvn clean package 时,Maven 会根据模块间的依赖关系自动排序构建顺序,但前提是模块间的依赖描述要正确。如果写错了循环依赖,Maven 会在解析依赖图时直接报 Cycle detected。
7.3 spring-boot-maven-plugin 的三个实用配置
除了打包可执行 jar,这个插件还提供一些日常有用的功能。
第一个是 repackage 的 exclude 配置。有时候你不想把某些依赖打进 fat jar(比如单元测试用的依赖或系统已提供的库),可以通过配置排除:
xml复制<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
第二个是 build-info 目标。执行 mvn package 时自动生成 build-info.properties 文件,里面包含构建时间、版本号等信息。配合 Spring Boot Actuator 的 /actuator/info 端点,可以很方便地暴露当前应用的构建信息和版本。
xml复制<executions>
<execution>
<goals>
<goal>build-info</goal>
</goals>
</execution>
</executions>
第三个是默认的 repackage goal,它绑定在 package 阶段,对正常的 jar 进行"二次加工"。如果你在排查打包后的 jar 和本地运行行为不一致的问题,可以考虑反编译看下 target 下的 jar 中的 class 文件是不是最新的,这通常能定位到增量编译缓存造成的诡异问题。
7.4 用 profile 实现多环境差异化打包
Spring Boot 项目通常需要区分 dev、test、prod 环境,除了在 application.yml 中用 spring.profiles.active 切换配置,Maven 层面也可以用 profile 联动不同环境的资源文件。比如在 pom.xml 中:
xml复制<profiles>
<profile>
<id>dev</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<env>dev</env>
</properties>
</profile>
<profile>
<id>prod</id>
<properties>
<env>prod</env>
</properties>
</profile>
</profiles>
配合 Maven 的 maven-resources-plugin,可以做到不同环境打包时选用 src/main/resources-{env} 目录下的配置文件。执行时用 mvn clean package -Pprod 指定 profile。这个方案相对简单直接,但要注意一点:如果 application.yml 中本身就有大量环境差异配置,用 Spring Boot 原生的 spring.config.activate.on-profile 更合理,Maven profile 只适合处理需要在构建期就确定的内容,比如三方库版本和包含的资源文件。
8. 日常工作中的几个高效操作习惯
最后分享一些我认为每个 Maven 使用者都该养成的操作习惯,它们不一定在官方文档里被强调,但实际工作中特别能节省时间、减少焦虑。
第一,保持本地仓库的"干净"。不要动不动就删掉整个 .m2/repository。正确的做法是精准删除有问题的 artifact 目录,或者使用 mvn dependency:purge-local-repository(不过这个命令会把项目所有依赖都清空重下,代价比较大,除非你想彻底排除本地缓存问题才建议用)。
第二,善用 -pl 和 -am 参数。多模块项目里,如果你只想构建某个模块及其依赖模块,不用每次都在根目录全量构建。-pl 表示指定构建某个模块(用 :模块名 方式),-am 表示同时构建该模块依赖的其他模块。比如:
bash复制mvn clean install -pl web -am
这条命令只构建 web 模块以及它所依赖的模块,比全量构建省时间得多。
第三,留意 Maven 的增量编译机制。Maven 默认是增量编译的,只重编译改动的文件。这加快了构建速度,但有时也会因为时间戳判断异常导致编译产物不一致。当你遇到诡异的运行问题且怀疑构建缓存在捣乱时,先执行 mvn clean 再打包,这会排除绝大多数缓存干扰。
第四,定期检查依赖更新。项目用久了,依赖会积累老版本,其中可能包含安全漏洞。你可以偶尔执行 mvn versions:display-dependency-updates 查看依赖是否有新版本可用,但要理性升级,不要无脑 update,参考 Spring Boot 版本兼容关系和你自己的测试用例决定是否升级。
第五,写清楚 pom.xml 的注释。很多人认为 pom.xml 是配置文件不需要注释,但大型项目中的依赖选择往往充满了历史原因。为什么排除某个依赖、为什么锁定某个版本,这些信息不写下来,三个月后你自己都会忘,更不要说后来接手的同事。我维护过一个老项目,里面有个依赖冲突的修复是通过在 pom 里排除一个核心库解决大问题的,但没有任何注释,后来的同事不理解为什么不能用新版本,又给加回来,结果线上出事故。所以,在 pom.xml 中标注关键决策的原因是成本最低的团队协作方式。
从实际项目经验看,Maven 和 Spring Boot 的组合之所以成为 Java 开发的主流标配,核心原因不是某一个命令多么好用,而是它把"依赖管理""构建过程""工程规范"三个层面统一了起来。你前期花一点时间把这些机制理解透,后面省下的是数不清的排查时间。而且 Maven 的很多概念——坐标、仓库、生命周期、依赖仲裁——理解之后完全能平移到其他语言的包管理工具上,本质都是同一套建模思想。
我最后想说的还是那句话:不要满足于项目能跑起来。花一个下午把 mvn dependency:tree 的输出看一遍,把自己项目里每一个依赖是谁带进来的弄清楚,顺着这个思路走一遍,你对 Java 工程化的理解会上一个台阶。
