Maven与Git
Maven 与 Git:工程师的左膀右臂
写代码只是开发的一半,另一半是管好代码和依赖。Git 帮你管版本,Maven 帮你管 jar 包——两件事搞不定,团队协作就是灾难。这篇文章把两者最核心的知识点串起来,让你在实战和面试中都能游刃有余。
一、Git 核心操作
Git 和 SVN 最根本的区别就一句话:Git 是分布式的,SVN 是集中式的。 Git 每个开发者本地都有完整仓库,可以离线提交、本地回滚;SVN 必须连接中央服务器才能干活。此外 Git 用 SHA-1 哈希做版本标识(而非 SVN 的递增版本号),分支管理极其轻量,创建合并删除分支都是毫秒级操作。
理解了这个大前提,我们来看 Git 中最容易混淆的几组操作。
1.1 代码回滚:reset vs revert
一个文件从修改到最终上库,大致经历三个阶段:
本地修改 → git add → git commit → git push撤销本地修改(还没 add):
git checkout -- example.txt撤销暂存区(add 了但还没 commit):
git reset HEAD example.txt # 把文件从暂存区退回工作目录已经 commit 或 push 了怎么办? 这时候有两个选择:reset 和 revert。
| 维度 | git reset | git revert |
|---|---|---|
| 原理 | 直接把 HEAD 指针回退到指定版本,"假装后面的提交从没发生过" | 创建一条新的提交来"反向抵消"某次修改 |
| 历史记录 | 被回退的提交会从历史中消失(--hard 模式) | 所有历史完整保留,多了一条 revert 记录 |
| 对他人影响 | 如果已经 push,会导致其他人 pull 时冲突 | 安全,不影响已有历史 |
| 适用场景 | 个人分支上"悄悄"回退 | 公共分支上安全回退 |
reset 的三种模式,一张图就能理解:
--soft:只移动 HEAD 指针,改动还在暂存区,可以重新 commit。适合"我 commit 了但想改一下提交信息或合并几个 commit"。--mixed(默认):移动 HEAD + 清空暂存区,改动还在工作目录。适合"我想取消 add,但代码还要留着"。--hard:一刀切,HEAD、暂存区、工作目录全部回退,改动彻底丢失。适合"这次提交完全不要了"。
生活类比:reset --hard 就像把写错的纸撕了扔掉,revert 则是在纸上划掉错误的内容再写一行更正说明——两种做法都能"改错",但后者有迹可循。
1.2 代码合并:merge vs rebase
当你从主干拉了一个 feature 分支开始开发,同时别人也往主干提交了新代码,提交历史就出现了"分叉"。这时候要把代码合到一起,有两个策略。
merge(合并)
git checkout feature
git merge mainmerge 会在 feature 分支上创建一个新的"合并提交"(merge commit),把两个分支的改动揉在一起。
优点:保留了完整的分支历史,能清楚看到哪个时间点发生了合并。
缺点:提交记录会出现交叉线,git log 看起来比较乱。
rebase(变基)
git checkout feature
git rebase mainrebase 会把 feature 分支上的提交"搬"到 main 的最新提交之后,形成一条干净的直线历史。
优点:提交记录整洁漂亮,code review 时一目了然。
缺点:改写了提交历史,在共享分支上用会出大问题。
rebase 黄金法则:不要对已经 push 到远程仓库的公共分支执行 rebase。
原因很简单:rebase 改写了提交历史,但这个改写只发生在你本地。其他人还在用旧的历史,等他们再 push 的时候就会产生大量冲突。这不是技术问题,是团队协作的灾难。
实际工作中的建议:
- 个人分支上随便 rebase,保持提交历史干净
- 公共分支(main/master)上只用 merge
- 团队里 90% 的场景用 merge 就够了
1.3 cherry-pick:精准"摘樱桃"
有时候你只想把某个分支上的某一个提交搬到当前分支,而不是把整个分支合并过来。这时候就用 cherry-pick:
git cherry-pick abc123 # 把 commit abc123 应用到当前分支典型场景:
- 线上紧急 bugfix,只需要把修复的那一个 commit 合到 release 分支
- 某个 feature 分支上有一个公共的工具方法,其他分支也想用
cherry-pick 本质上是"复制"一个 commit,生成一个新的 commit(hash 值不同),所以同一个修改在两个分支上会有不同的 commit id。
二、Git 分支策略
团队开发不是一个人的独角戏,分支策略决定了大家怎么协同。业内最常见的模式是基于分支开发,基于主干发布:
- 仓库有一个主干分支(main/master),线上运行的就是这套代码
- 有需求就从主干拉 feature 分支,在分支上开发
- 开发完成后通过 MR/PR 把分支合并回主干
- 从主干发布上线
几个实践中的关键原则:
- feature 分支要短命:尽量不超过一周,频繁合并回主干,避免长期分支带来巨大合并冲突
- 合并前先同步主干:在提 MR 之前,先
git merge main把主干最新代码合过来,在本地解决冲突 - 保护主干:主干分支设为保护分支,不允许直接 push,必须通过 MR 经过 code review 后才能合入
- 发布用 tag:每次发布上线后打一个 tag,方便快速回滚到特定版本
三、Maven 依赖管理
3.1 Maven 解决了什么问题
没有 Maven 的年代,开发一个 Java 项目你需要:手动找 jar 包 → 手动导入项目 → 手动处理 jar 包之间的兼容性问题 → 手动编译打包部署……光是找 jar 包就能找一下午。
Maven 把这些痛苦一扫而光:
- 依赖管理:在 pom.xml 中声明式引入 jar 包,自动下载、自动处理传递依赖
- 标准化构建:compile → test → package → deploy,一条
mvn package命令搞定 - 统一项目结构:
src/main/java、src/test/java、src/main/resources,所有 Maven 项目结构一致
3.2 依赖 scope
Maven 依赖有一个 scope 属性,控制依赖在哪些阶段可用:
| scope | 编译时 | 测试时 | 运行时 | 打包 | 典型用途 |
|---|---|---|---|---|---|
| compile(默认) | 可用 | 可用 | 可用 | 打入 | 大多数依赖 |
| provided | 可用 | 可用 | 不可用 | 不打入 | servlet-api(容器提供) |
| runtime | 不可用 | 可用 | 可用 | 打入 | JDBC 驱动 |
| test | 不可用 | 可用 | 不可用 | 不打入 | JUnit、Mockito |
| system | 可用 | 可用 | 不可用 | 不打入 | 本地 jar(不推荐) |
最容易搞混的是 provided:编译时需要这个类来通过编译,但运行时由容器提供,不打进最终包里。比如写 Servlet 需要 javax.servlet-api,但 Tomcat 自带了这个包,你再打进去就冲突了。
3.3 依赖冲突怎么解决
当项目中同时依赖了 A 和 B,而 A 和 B 又分别依赖了 C 的不同版本——jar 包冲突就来了。运行时出现 NoSuchMethodError、ClassNotFoundException 就是它的典型症状。
Maven 有两条默认的依赖传递原则:
最短路径优先:
A → X → Y → Z(2.5)
B → X → Z(2.0)Z 最终用 2.0,因为 B 到 Z 的路径更短(2 跳 vs 3 跳)。
最先声明优先:路径长度一样时,pom 中先写的那个 dependency 对应的版本胜出。
A → Z(3.0) ← pom 中先声明
B → Z(2.5)Z 最终用 3.0。
这两条规则并不总能选到你真正想用的版本,所以需要人工干预。
排查工具:
mvn dependency:tree打印出整棵依赖树,找到冲突的 jar 包就能看清楚它的多个版本分别从哪条路径引入。IDEA 中装一个 Maven Helper 插件更方便,打开 pom 文件直接就能看 Conflicts。
解决方案一:排除依赖
<dependency>
<groupId>com.example</groupId>
<artifactId>X</artifactId>
<exclusions>
<exclusion>
<groupId>com.example</groupId>
<artifactId>Y</artifactId>
</exclusion>
</exclusions>
</dependency>适合单个冲突的精准排除。
解决方案二:版本锁定(推荐)
在父 POM 中用 dependencyManagement 统一锁定版本:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>Y</artifactId>
<version>3.0</version>
</dependency>
</dependencies>
</dependencyManagement>大型项目推荐这种方式,一处定义全局生效。注意:dependencyManagement 只做版本仲裁,不会真正引入依赖,子模块中还需要用 <dependency> 显式引入(但不用再写 <version>)。
3.4 Maven 生命周期
Maven 的构建过程由三套独立的生命周期组成:
default 生命周期(最常用):
validate → compile → test → package → verify → install → deploy执行后面的阶段会自动执行前面的所有阶段。比如 mvn package 会先 compile、再 test、最后 package。
clean 生命周期:清理编译产物
mvn clean # 删除 target 目录
mvn clean package # 先清理再打包(最常用组合)site 生命周期:生成项目文档站点(用的比较少)。
3.5 jar 包、war 包与 fat jar
| 格式 | 用途 | 说明 |
|---|---|---|
| JAR | 打包 Java 类和资源 | 可作为类库被其他项目引入,也可用 java -jar 直接运行 |
| WAR | 打包 Web 应用 | 包含 JSP/HTML/CSS/JS + WEB-INF/web.xml,部署到 Tomcat 等容器 |
| Fat JAR | 包含所有依赖的 JAR | Spring Boot 默认方式,一个 jar 文件搞定全部 |
Fat JAR(也叫 uber-jar)把所有依赖打进同一个 jar 文件,部署时只需要 java -jar app.jar,非常方便。Spring Boot 的 spring-boot-maven-plugin 默认就生成 Fat JAR。
缺点是文件体积大(一个 Spring Boot 应用可能 50MB+),可以通过以下方式缓解:
- 排除不必要的依赖
- 将不常变化的依赖拆到外部 lib 目录,应用 jar 只包含业务代码
- 用 Docker 分层构建,公共依赖层不变就不用重新上传
四、面试高频题
题目一:Git 和 SVN 有什么区别?
核心区别是分布式 vs 集中式。Git 每个开发者本地有完整仓库,可以离线工作;SVN 必须连接中央服务器。Git 用 SHA-1 哈希做版本标识,分支轻量灵活;SVN 用递增版本号,分支操作笨重。存储上 Git 用快照,SVN 用增量差异。
题目二:merge 和 rebase 怎么选?
公共分支用 merge(安全、保留完整历史),个人分支用 rebase(整洁、线性历史)。团队协作中 90% 用 merge。遵守 rebase 黄金法则:不要对已推送到远程的公共分支 rebase。
题目三:Maven 如何解决 jar 包冲突?
先用 mvn dependency:tree 或 Maven Helper 插件定位冲突来源,然后用 <exclusions> 排除不想要的传递依赖,或在父 POM 的 <dependencyManagement> 中统一锁定版本。Maven 默认按最短路径优先和最先声明优先两条规则仲裁,但不一定选对,所以需要人工干预。
题目四:什么是 Fat JAR?
Fat JAR 是把应用代码和所有依赖打进同一个 JAR 文件的打包方式。Spring Boot 默认生成 Fat JAR,用 java -jar 就能直接运行,极大简化了部署。缺点是文件体积较大,可通过拆分外部 lib、Docker 分层构建等方式优化。
小结
| 知识点 | 一句话记忆 |
|---|---|
| Git vs SVN | 分布式 vs 集中式 |
| reset vs revert | reset 撕纸(改历史),revert 划线更正(不改历史) |
| merge vs rebase | 公共分支 merge,个人分支 rebase |
| cherry-pick | 精准摘取单个 commit 到当前分支 |
| 依赖 scope | compile 全程可用,provided 编译可用运行时容器提供,test 仅测试 |
| 依赖冲突 | dependency:tree 定位 + exclusion 排除或 dependencyManagement 锁定 |
| Fat JAR | 所有依赖打进一个 jar,Spring Boot 默认方式 |
Git 管版本,Maven 管依赖,两者配合好了,工程效率直接拉满。下一篇我们聊聊 IDEA 的调试技巧和必装插件。
补充:Git 常用命令速查
日常开发中最高频的 Git 命令,记住这些就够应付 90% 的场景了:
基础操作
# 查看当前状态
git status
# 查看提交历史(单行模式更清晰)
git log --oneline --graph
# 查看某个文件的修改历史
git log --follow -p filename
# 暂存所有修改
git add .
# 提交(写清楚做了什么)
git commit -m "feat: 添加用户注册功能"
# 推送到远程
git push origin feature/user-register分支操作
# 查看所有分支(包括远程)
git branch -a
# 创建并切换到新分支
git checkout -b feature/new-feature
# 从远程拉取并创建本地分支
git checkout -b feature/xxx origin/feature/xxx
# 删除已合并的本地分支
git branch -d feature/old-feature
# 强制删除未合并的分支(慎用)
git branch -D feature/abandoned暂存工作区(stash)
开发到一半突然要切分支修 bug?用 stash 把当前改动暂存起来:
# 暂存当前改动
git stash
# 查看暂存列表
git stash list
# 恢复最近一次暂存
git stash pop
# 恢复指定的暂存
git stash apply stash@{2}提交规范
好的提交信息对团队协作非常重要。推荐使用 Angular 规范:
<type>: <subject>
type 可选值:
feat 新功能
fix 修复 bug
docs 文档变更
style 代码格式(不影响功能)
refactor 重构(不是新功能也不是修 bug)
test 添加测试
chore 构建/工具变更好的提交信息示例:
feat: 添加用户注册功能
fix: 修复订单金额计算精度丢失问题
refactor: 重构支付模块,提取公共支付网关差的提交信息示例:
update
fix bug
修改了一些东西补充:Maven 常见问题排查
依赖下载失败
最常见的原因是仓库配置问题。检查 ~/.m2/settings.xml 中的 mirror 配置是否正确。国内推荐使用阿里云 Maven 镜像:
<mirror>
<id>aliyunmaven</id>
<mirrorOf>*</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>如果某个 jar 包下载到一半失败了,会在本地仓库中留下一个 .lastUpdated 文件,导致后续构建也找不到这个包。解决办法:
# 找到并删除所有 .lastUpdated 文件
find ~/.m2/repository -name "*.lastUpdated" -delete
# 重新构建
mvn clean install编译版本问题
项目中不同模块使用不同的 JDK 版本编译,运行时可能出现 UnsupportedClassVersionError。在父 POM 中统一配置编译版本:
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>多模块项目的构建顺序
Maven 多模块项目按照依赖关系自动计算构建顺序。如果只想构建某个模块及其依赖:
# 只构建 user-service 及其依赖的模块
mvn install -pl user-service -am
# -pl 指定模块
# -am 同时构建依赖的模块(also-make)