1.1 从 jar 地狱到 Maven 的诞生


文档摘要

1.1 从 jar 地狱到 Maven 的诞生 本节摘要:jar 地狱指手工管理 classpath 与第三方库版本导致的重复类、版本错配与构建不可复现问题。Maven 用"坐标定位构件、仓库集中存放、POM 声明依赖"三件套根治了它。本节从一次 2002 年前后的典型事故出发,讲清 Maven 的设计动机与"约定优于配置"哲学。 事故还原:没有构建工具的年代 时间拨回 Maven 诞生之前。一个十人团队维护的电商后台,部署脚本长这样: 这套流程有三个致命伤。第一,jar 是靠"拷贝到 lib 目录"传播的,谁改了版本、谁删了一个包,没有任何记录。

1.1 从 jar 地狱到 Maven 的诞生

本节摘要:jar 地狱指手工管理 classpath 与第三方库版本导致的重复类、版本错配与构建不可复现问题。Maven 用"坐标定位构件、仓库集中存放、POM 声明依赖"三件套根治了它。本节从一次 2002 年前后的典型事故出发,讲清 Maven 的设计动机与"约定优于配置"哲学。

事故还原:没有构建工具的年代

时间拨回 Maven 诞生之前。一个十人团队维护的电商后台,部署脚本长这样:

# 部署脚本片段:手工拼装 classpath java -cp lib/commons-logging-1.0.4.jar:lib/log4j-1.2.13.jar:lib/commons-collections-3.1.jar:lib/hibernate-3.2.jar:lib/dom4j-1.6.1.jar:build/classes com.shop.Main # 张三机器上跑得起来,李四机器上报 ClassNotFoundError # 原因:李四的 lib 目录里少了一个 dom4j,没人知道该放哪些 jar

这套流程有三个致命伤。第一,jar 是靠"拷贝到 lib 目录"传播的,谁改了版本、谁删了一个包,没有任何记录。第二,同一个类可能出现在两个 jar 里(比如 commons-logging 同时出现在两个传递引入的包里),JVM 加载哪个取决于 classpath 顺序,结果就不可预测。第三,新人入职要花两天凑齐环境,凑不齐就只能借用别人的机器调试。

2001 年,Jakarta Turbine 项目的开发者们被同样的问题折磨够了。他们尝试用脚本来描述项目结构,很快发现每个项目的脚本都在重复描述"源码在哪、怎么编译、怎么打包"。2002 年,Lucene 的贡献者 Jason van Zyl 基于"项目其实长得都差不多"的洞察发起了 Maven,2003 年成为 Apache 顶级项目。名字取自意第绪语,意思是"知识的积累者"——它的野心从一开始就不只是编译工具,而是项目知识的载体。

Maven 的解法:坐标、仓库、声明

Maven 对上述三个致命伤分别给出解法。

版本混乱靠坐标解决。 每个构件用三元组定位:groupId(组织,如 com.shop)、artifactId(构件名,如 risk-engine)、version(版本,如 1.0.0)。加上 packaging(打包形态,默认 jar)与 classifier(同版本下的细分,如 jdk11),一个 jar 在全世界的 Maven 体系里就有了唯一地址。

拷贝传播靠仓库解决。 构件不再随邮件与优盘流动,而是集中存放在仓库里,本地的、公司私有的、全球中央的,按坐标分目录检索。谁要用,写坐标即可,文件由 Maven 统一搬运。

环境不可复现靠声明解决。 项目的依赖、插件、构建配置全部写进一个 POM 文件,它是唯一的真相源。新人 clone 之后一条命令即可还原环境。risk-engine 的第一份 POM 长这样:

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0"> <modelVersion>4.0.0</modelVersion> <!-- 坐标:这个项目在 Maven 世界里的身份证 --> <groupId>com.shop.risk</groupId> <artifactId>risk-engine</artifactId> <version>1.0.0</version> <packaging>jar</packaging> <properties> <!-- 属性区:统一版本号,改一处生效全局 --> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!-- 声明式依赖:只要坐标,不管文件从哪来 --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.13.4</version> </dependency> </dependencies> </project>

执行 mvn package,Maven 会发现本地仓库没有 jackson-databind,自动从中央仓库下载到用户目录下的 repository 缓存,然后编译、测试、打包,全程无需人工干预。对比开头那个部署脚本,差异一目了然。

约定优于配置:双刃剑怎么用

Maven 最有争议也最有影响的决策是"约定优于配置"。它规定了一套标准目录布局:主源码、测试源码、资源文件、打包输出各有固定位置。不写任何配置,Maven 也知道去哪找代码。

risk-engine/ pom.xml 项目对象模型,唯一真相源 src/main/java 主源码根目录 src/main/resources 主资源,如配置文件 src/test/java 测试源码根目录 src/test/resources 测试资源 target/ 构建输出:classes、jar、测试报告

这套约定的价值在团队作战时最明显:任何人接手任何 Maven 项目,目录结构零学习成本;持续集成服务器不需要为每个项目定制脚本。代价是灵活性受让——如果你的源码非要放在奇怪的目录,就得写配置去覆盖约定,而覆盖约定的人多了,"零配置"的优势也就消失了。我的建议是:除非有历史包袱,否则别跟约定较劲。目录布局这点"自由"换不回什么收益,却会让每个新人都多问一遍"代码在哪"。

⚠️ 常见坑:把 target 目录提交进版本库。它是构建产物,随时可重建,提交只会制造无意义的合并冲突与仓库膨胀。同理,任何时候看到有人在 target 里手工改文件,都要拦下来——那是在易失的沙地上写字。

对比一下同时代的竞争者,能更清楚 Maven 的位置:Ant 灵活但要为每个项目写完整构建脚本;Make 与平台绑定过深;Ivy 给 Ant 补上了依赖管理但没有标准布局;Gradle 后来用 Groovy 或 Kotlin DSL 换来了更强的表达能力,代价是学习曲线与"每个项目一个 DSL"的碎片化。Maven 用 XML 的冗长换来了"全团队乃至全行业同一套结构",这份可预测性正是作战室最需要的属性——排查问题时,你不需要先问"这个项目的构建脚本是谁写的"。

本节要点回顾

  • jar 地狱的三个症状:版本靠拷贝传播无记录、同 classpath 重复类加载顺序不可控、环境还原依赖特定机器;
  • Maven 三件套:GAV 坐标给构件唯一地址,仓库集中存放与分发,POM 声明式描述一切构建事实;
  • 约定优于配置:标准目录布局与默认生命周期阶段换来了零学习成本与可预测性,覆盖约定要有充分理由;
  • POM 是唯一真相源:版本管理只提交 POM 与源码,构建产物与本地缓存永远可重建;
  • 历史视角:Maven 生于 2002 年 Jakarta 项目的工程痛点,解决的是协作规模下的依赖治理,而不是"编译比 javac 快"。

下一节进入坐标系统的细节:GAV 三元组如何拼出仓库里的物理路径,本地、远程、中央仓库又是按什么顺序接力的——这是诊断一切下载问题的地形基础。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U