1.1 从 jar 地狱到 Maven 的诞生 本节摘要:jar 地狱指手工管理 classpath 与第三方库版本导致的重复类、版本错配与构建不可复现问题。Maven 用"坐标定位构件、仓库集中存放、POM 声明依赖"三件套根治了它。本节从一次 2002 年前后的典型事故出发,讲清 Maven 的设计动机与"约定优于配置"哲学。 事故还原:没有构建工具的年代 时间拨回 Maven 诞生之前。一个十人团队维护的电商后台,部署脚本长这样: 这套流程有三个致命伤。第一,jar 是靠"拷贝到 lib 目录"传播的,谁改了版本、谁删了一个包,没有任何记录。
本节摘要: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 对上述三个致命伤分别给出解法。
版本混乱靠坐标解决。 每个构件用三元组定位: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 的冗长换来了"全团队乃至全行业同一套结构",这份可预测性正是作战室最需要的属性——排查问题时,你不需要先问"这个项目的构建脚本是谁写的"。
下一节进入坐标系统的细节:GAV 三元组如何拼出仓库里的物理路径,本地、远程、中央仓库又是按什么顺序接力的——这是诊断一切下载问题的地形基础。