Maven 实战(一)——坐标规划

  • 许晓斌

2010 年 12 月 12 日

话题:JavaDevOps语言 & 开发

坐标是什么?为什么要规划?

坐标是 Maven 最基本的概念,它就像每个构件的身份证号码,有了它我们就可以在数以千万计的构件中定位任何一个我们感兴趣的构件。举个最简单的例子,如果没有坐标,使用 JUnit 的时候,用户就需要去下载依赖 jar 包,用依赖的方式,简单配置使用如 junit:junit:4.8.2 就可以了。这里第一个 junit 是 groupId,第二个 junit 是 artifactId,4.8.2 是 version。

Maven 的很多其他核心机制都依赖于坐标,其中最显著的就是仓库和依赖管理。对于仓库来说,有了坐标就知道在什么位置存储构件的内容,例如 junit:junit:4.8.2 就对应仓库中的路径/junit/junit/4.8.2/junit-4.8.2.pom/junit/junit/4.8.2/junit-4.8.2.jar这样的文件,读者可以直接访问中央仓库地址看到这样的仓库布局,或者浏览本地仓库目录~/.m2/repository/以获得直观的体验。

依赖的配置也是完全基于坐标的,例如:

<dependency>
  <groupId>junit</groupId>
  <artifactId>junit</artifactId>
  <version>4.8.2</version>
  <scope>test</scope>
</dependency>

有了正确的坐标,Maven 才能够在正确的位置找到依赖文件并使用,这里值为 test 的 scope 是用来控制该依赖只在测试时可用,与坐标无关。

正因为坐标是 Maven 核心的核心,因此规划正确的坐标至关重要,如果你使用了模糊不清的坐标,那么你的用户就很难找到你的构件,或者即使找到了,也容易写错。错误的使用坐标,还会造成冲突,如果你也使用 junit 这样的 groupId,那会发生什么?下面先看一些不是很规范的坐标使用方式。

坐标规划的原则

滥用坐标、错用坐标的样例比比皆是,在中央仓库中我们能看到 SpringFramework 有两种坐标,其一是直接使用 springframework 作为 groupId,如 springframework:spring-beans:1.2.6,另一种是用 org.springframework 作为 groupId,如 org.springframework:spring-beans:2.5。细心看看,前一种方式显得比较随意,后一种方式则是基于域名衍生出来的,显然后者更合理,因为用户能一眼根据域名联想到其 Maven 坐标,方便寻找。因此新版本的 SpringFramework 构件都使用 org.springframework 作为 groupId。由这个例子我们可以看到坐标规划一个原则是基于项目域名衍生。其实很多流行的开源项目都破坏了这个原则,例如 JUnit,这是因为 Maven 社区在最开始接受构件并部署到中央仓库的时候,没有很很严格的限制,而对于这些流行的项目来说,一时间更改坐标会影响大量用户,因此也算是个历史遗留问题了。

还有一个常见的问题是将 groupId 直接匹配到公司或者组织名称,因为乍一看这是显而易见的。例如组织是 zoo.com,有个项目是 dog,那有些人就直接使用 groupId com.zoo 了。如果项目只有一个模块,这是没有什么问题的,但现实世界的项目往往会有很多模块,Maven 的一大长处就是通过多模块的方式管理项目。那 dog 项目可能会有很多模块,我们用坐标的哪个部分来定义模块呢?groupId 显然不对,version 也不可能是,那只有 artifactId。因此要这里有了另外一个原则,用 artifactId 来定义模块,而不是定义项目。接下来,很显然的,项目就必须用 groupId 来定义。因此对于 dog 项目来说,应该使用 groupId com.zoo.dog,不仅体现出这是 zoo.com 下的一个项目,而且可以与该组织下的其他项目如 com.zoo.cat 区分开来。

除此之外,artifactId 的定义也有最佳实践,我们常常可以看到一个项目有很多的模块,例如 api,dao,service,web 等等。Maven 项目在默认情况下生成的构件,其名称不会是基于 artifactId,version 和 packaging 生成的,例如api-1.0.jardao-1.0.jar等等,他们不会带有 groupId 的信息,这会造成一个问题,例如当我们把所有这些构件放到 Web 容器下的时候,你会发现项目 dog 有 api-1.0.jar,项目 cat 也有 api-1.0.jar,这就造成了冲突。更坏的情况是,dog 项目有 api-1.0.jar,cat 项目有 api-2.0.jar,其实两者没什么关系,可当放在一起的时候,却很容易让人混淆。为了让坐标更加清晰,又出现了一个原则,即在定义 artiafctId 时也加入项目的信息,例如 dog 项目的 api 模块,那就使用 artifactId dog-api,其他就是 dog-dao,dao-service 等等。虽然连字号是不允许出现在 Java 的包名中的,但 Maven 没这个限制。现在 dog-api-1.0.jar,cat-2.0.jar 被放在一起时,就不容易混淆了。

关于坐标,我们还没谈到 version,这里不再详述因为读者可以从Maven: The Complete Guide中找到详细的解释,简言之就是使用这样一个格式:

< 主版本 >.< 次版本 >.< 增量版本 >-< 限定符 >

其中主版本主要表示大型架构变更,次版本主要表示特性的增加,增量版本主要服务于 bug 修复,而限定符如 alpha、beta 等等是用来表示里程碑。当然不是每个项目的版本都要用到这些 4 个部分,根据需要选择性的使用即可。在此基础上 Maven 还引入了 SNAPSHOT 的概念,用来表示活动的开发状态,由于不涉及坐标规划,这里不进行详述。不过有点要提醒的是,由于 SNAPSHOT 的存在,自己显式地在 version 中使用时间戳字符串其实没有必要。

Classifier

Classifier 可能是最容易被忽略的 Maven 特性,但它确实非常重要,我们也需要它来帮助规划坐标。设想这样一个情况,有一个 jar 项目,就说是 dog-cli-1.0.jar 吧,运行它用户就能在命令行上画一只小狗出来。现在用户的要求是希望你能提供一个 zip 包,里面不仅包含这个可运行的 jar,还得包含源代码和文档,换句话说,这是比较正式的分发包。这个文件名应该是怎样的呢?dog-cli-1.0.zip?不够清楚,仅仅从扩展名很难分辨什么是 Maven 默认生成的构件,什么是额外配置生成分发包。如果能是dog-cli-1.0-dist.zip就最好了。这里的 dist 就是 classifier,默认 Maven 只生成一个构件,我们称之为主构件,那当我们希望 Maven 生成其他附属构件的时候,就能用上 classifier。常见的 classifier 还有如dog-cli-1.0-sources.jar表示源码包,dog-cli-1.0-javadoc.jar表示 JavaDoc 包等等。制作 classifier 的方式多种多样,其中最重要的一种是使用Maven Assembly Plugin,感兴趣的读者可以进一步研究。

小结

本文是 InfoQ Maven 专栏的第一篇,讨论的是 Maven 坐标的规划,包括如何正确的使用 groupId、artifactId、version,以及 classfier。笔者在维护 Maven 中央仓库的工作过程中遇到过各种各样模糊的甚至是错误的坐标,它们的存在给广大 Maven 用户带来的极大的不便。本文抛出一些较好的实践,帮助大家更好的使用 Maven。如果读者有相关的经验总结,也请不吝分享。

关于作者

许晓斌(Juven Xu),国内社区公认的 Maven 技术专家、Maven 中文用户组创始人、Maven 技术的先驱和积极推动者。对 Maven 有深刻的认识,实战经验丰富,不仅撰写了大量关于 Maven 的技术文章,而且还翻译了开源书籍《Maven 权威指南》,对 Maven 技术在国内的普及和发展做出了很大的贡献。就职于 Maven 之父的公司,负责维护 Maven 中央仓库,是 Maven 仓库管理器 Nexus(著名开源软件)的核心开发者之一,曾多次受邀到淘宝等大型企业开展 Maven 方面的培训。此外,他还是开源技术的积极倡导者和推动者,擅长 Java 开发和敏捷开发实践。他的个人网站是:http://www.juvenxu.com

【编者按】InfoQ 中文站有幸邀请到 Sonotype(Maven 背后的公司)在中国唯一的员工、《Maven 实战》的作者许晓斌先生,在 InfoQ 中文站开辟 Maven 的专栏,为开发者带来一些 Maven 的高级话题,以及他积累多年的 Maven 经验分享。

JavaDevOps语言 & 开发