四十五图,一万五千字!一文让你走出迷雾玩转Maven!

22,422 阅读31分钟

引言

Maven应该是大家的老熟客了,身为Java程序员,几乎每天都会跟他打交道。

不过有趣的是:很多伙伴对Maven,似乎很熟,但又好像不熟;在理解上,处于似懂非懂的“量子纠缠态”,为什么这么说呢?原因很简单,要说不熟吧,偏偏每天都有所接触;要说熟吧,可是对许多高级功能又仅是一知半解。

正因如此,为了辅助大家从“量子纠缠态”中走出来,本文会从零开始,带着大家玩转Maven技术。当然,其实写这篇文章更大的目的,是为后续写《漫谈分布式》专栏做准备,毕竟后续会频繁用到Maven构建多工程项目。

PS:个人编写的《技术人求职指南》小册已完结,其中从技术总结开始,到制定期望、技术突击、简历优化、面试准备、面试技巧、谈薪技巧、面试复盘、选Offer方法、新人入职、进阶提升、职业规划、技术管理、涨薪跳槽、仲裁赔偿、副业兼职……,为大家打造了一套“从求职到跳槽”的一条龙服务,同时也为诸位准备了七折优惠码:3DoleNaE,感兴趣的小伙伴可以点击:s.juejin.cn/ds/USoa2R3/了解详情!

一、Maven快速上手/回顾

声明:如果基础够扎实的小伙伴,可以跳到1.3阶段(快速刷一遍当复习也行)。

Maven

Maven是专门用于构建、管理Java项目的工具,它为我们提供了标准化的项目结构,如下:

├─ProjectName                              // 项目名称
│  ├─src                      // 根目录
│  │   ├─main                  // 主目录
│  │   │  ├─java             // Java源码目录
│  │   │  ├─resources      //配置文件目录
│  │   │  └─webapp                            // Web文件目录
│  │   ├─test                  // 测试目录
│  │   │  ├─java             // Java测试代码目录
│  │   │  └─resources                            // 测试资源目录
│  └─pom.xml                      // Maven项目核心配置文件

同时也提供了一套标准的构建流程:

构建流程

从编译,到测试、打包、发布……,涵盖整个项目开发的全流程。

并且最重要的一点,它还提供了依赖(Jar包)管理功能,回想大家最初学JavaEE时,想要用到一个外部的工具包,必须先从网上找到对应的Jar文件,接着将其手动丢到项目的lib目录下,当项目需要依赖的外部包达到几十个、每个外部包还依赖其他包时,这个过程无疑很痛苦。

而这一切的一切,随着Maven的出现,从此不复存在。

1.1、Maven安装指南

使用Maven前,必须先安装它,这时可以先去到Maven官网下载自己所需的版本:

下载

下载进行解压后(不过解压的目录最好别带中文,否则后续会碰到一些问题),接着需要配置一下,总共分为四步。

①在系统环境中,新建一个MAVEN_HOMEM2_HOME的环境变量,值写成解压路径。

②找到Path变量并编辑,在其中新增一行,配置一下bin目录:

%M2_HOME%\bin

其实安装许多软件,都要配置这一步,到底是为啥呢?因为任何软件的bin目录,通常会存放一些可执行的脚本/工具,如JDKbin目录中,就存放着javac、javap、jstack……一系列工具。如果不在Path中配置bin,那想要使用这些工具,只能去到JDK安装目录下的bin目录,然后才能使用。

不过当大家在Path中配置了bin之后,这个配置就会对全局生效,任何位置执行javac这类指令,都可以从Path中,找到对应的bin目录位置,然后调用其中提供的工具。

③找到Maven解压目录下的conf/settings.xml,然后点击编辑,找到<localRepository>标签,将其挪动到注释区域外,然后配置本地仓库位置:

<localRepository>自己选择一个空的本地目录(最好别带中文)</localRepository>

④由于Apache的官方镜像位于国外,平时拉取依赖比较慢,所以还需配置Maven国内的镜像源,这时在settings.xml文件中,先搜索<mirrors>标签,接着在其中配置阿里云的镜像地址:

<mirror>  
    <id>alimaven</id>  
    <name>aliyun maven</name>  
    <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
    <mirrorOf>central</mirrorOf>          
</mirror>

到这里,整个Maven安装流程全部结束,最后也可以在终端工具,执行mvn -v命令检测一下。

1.2、Maven入门指南

安装好Maven后,接着可以通过IDEA工具来创建Maven项目,不过要记得配置一下本地Maven及仓库位置:

IDEA

在这里配置,是对全局生效,后续创建的所有Maven项目,都会使用这组配置。

1.2.1、IDEA创建Maven项目

接着就可以创建Maven项目,这个过程特别简单,先选择New Project

创建项目-1

这里选创建Maven项目,接着指定一下JDK,还可以选择是否使用骨架,选好后直接Next下一步:

创建项目-2

这里需要写一下GAV坐标,稍微解释一下三个选项的含义:

  • GroupID:组织ID,一般写公司的名称缩写;
  • ArtifactID:当前Maven工程的项目名字;
  • Version:当前Maven工程的版本。

接着点下一步,然后选择一下项目的存储位置,最后点击Finish创建即可:

创建项目-3

这一步结束后,就得到了一个纯净版的Maven项目,然后可以基于Maven实现依赖管理。

1.2.2、Maven依赖管理

最简单的依赖管理,总共就只有三步,如下:

  • ①在pom.xml中,先写一个<dependencies>标签;
  • ②在<dependencies>标签中,使用<dependency>标签来导入依赖;
  • ③在<dependency>标签中,通过GAV坐标来导入依赖。

如果你不知道一个依赖的GAV该怎么写,可以去仓库索引中搜索,现在写个坐标来感受一下:

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
        <version>5.1.8.RELEASE</version>
    </dependency>
</dependencies>

引入GAV坐标后,依赖不会立马生效,需要手动刷新一下项目:

刷新依赖

可以借助IDEA自带的Maven项目工具来进行刷新;也可以安装Maven-Helper插件,在项目上右键,然后通过Run Maven里的指令刷新。至此,大家就掌握了Maven的基本使用。

PS:如果你本地仓库中有依赖,但忘了GAV坐标怎么写,通过IDEA工具,在pom.xml文件中按下alt+insert快捷键,接着点击Dependency,可以做到可视化快捷导入。

1.2.3、依赖范围管理

有时候,有些依赖我们并不希望一直有效,比如典型的JUnit测试包,对于这类jar包而言,最好只针对测试环境有效,而编译环境、运行环境中,因为用不到单元测试,所以有没有办法移除呢?这时可以通过<scope>标签来控制生效范围:例如:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <version>2.1.8.RELEASE</version>
    <scope>test</scope>
</dependency>

该标签共有五种取值方式,每种取值对应着一种依赖范围,而不同的依赖范围,生效的环境(classpath)也并不同,如下表所示:

依赖范围编译环境测试环境运行环境
compile生效生效生效
provided生效生效不生效
system生效生效不生效
runtime不生效生效生效
test不生效生效不生效

项目引入的所有依赖,如果不显式指定依赖范围,默认是compile,意味着所有环境下都生效,而一般的依赖包也无需更改,只有某些特殊的依赖,才需要手动配置一下。如:

  • JUnit、spring-test这类包,只在测试环境使用,所以配成test
  • Tomcat内置servlet-api包,为了避免在运行环境冲突,应该配成provided
  • ……

同时,<scope>标签还可以通过自定义的方式来添加其他的scope范围,例如Maven插件中使用的scope值:

<dependency>
    <groupId>some.group</groupId>
    <artifactId>some-artifact</artifactId>
    <version>1.0</version>
    <scope>plugin</scope>
</dependency>

这里的plugin就是自定义的scope,表示该依赖只在Maven插件中生效。

最后,<scope>标签还有一类特殊、但很常用的取值范围,即import,但这个放在后面去讲。

1.3、Maven工作原理剖析

Maven中,节点会分为工程、仓库两大类,工程是“依赖使用者”,仓库是“依赖提供者”,关系如下:

仓库/工程关系

看着或许有点头大,要讲明白得先弄清里面三种仓库:

  • 中央仓库:就是前面配置的镜像源,里面拥有海量的公共jar包资源;
  • 远程仓库:也叫私服仓库,主要存储公司内部的jar包资源,这个后续会细说;
  • 本地仓库:自己电脑本地的仓库,会在磁盘上存储jar包资源。

大致了解三种仓库的含义后,接着来梳理Maven的工作流程:

  • ①项目通过GAV坐标引入依赖,首先会去本地仓库查找jar包;
  • ②如果在本地仓库中找到了,直接把依赖载入到当前工程的External Libraries中;
  • ③如果没找到,则去读取settings.xml文件,判断是否存在私服配置;
  • ④如果有私服配置,根据配置的地址找到远程仓库,接着拉取依赖到本地仓库;
  • ⑤如果远程仓库中没有依赖,根据私服配置去中央仓库拉取,然后放到私服、本地仓库;
  • ⑥从远程或中央仓库中,把依赖下载到本地后,再重复第二步,把依赖载入到项目中。

上述六步便是Maven的完整工作流程,可能许多人没接触过私服,这个会放到后面聊。如果你的项目没配置Maven私服,那么第三步时,会直接从settings.xml读取镜像源配置,直接去到中央仓库拉取依赖。

不过这里有个问题,拉取/引入依赖时,Maven是怎么知道要找谁呢?答案是依靠GAV坐标,大家可以去观察一下本地仓库,当你引入一个依赖后,本地仓库中的目录,会跟你的GAV坐标一一对应,如:

仓库结构

无论是什么类型的仓库,都会遵循这个原则进行构建,所以,只要你书写了正确的GAV坐标,就一定能够找到所需的依赖,并将其载入到项目中。

1.4、Maven生命周期

通过IDEA工具的辅助,能很轻易看见Maven的九种Lifecycle命令,如下:

构建流程

双击其中任何一个,都会执行相应的Maven构建动作,为啥IDEA能实现这个功能呢?道理很简单,因为IDEA封装了Maven提供的命令,如:点击图中的clean,本质是在当前目录中,执行了mvn clean命令,下面解释一下每个命令的作用:

  • clean:清除当前工程编译后生成的文件(即删除target整个目录);
  • validate:对工程进行基础验证,如工程结构、pom、资源文件等是否正确;
  • compile:对src/main/java目录下的源码进行编译(会生成target目录);
  • test:编译并执行src/test/java/目录下的所有测试用例;
  • package:将当前项目打包,普通项目打jar包,webapp项目打war包;
  • verify:验证工程所有代码、配置进行是否正确,如类中代码的语法检测等;
  • install:将当前工程打包,然后安装到本地仓库,别人可通过GAV导入;
  • site:生成项目的概述、源码测试覆盖率、开发者列表等站点文档(需要额外配置);
  • deploy:将当前工程对应的包,上传到远程仓库,提供给他人使用(私服会用)。

上述便是九个周期阶段命令的释义,而Maven总共划分了三套生命周期:

生命周期

主要看default这套,该生命周期涵盖了构建过程中的检测、编译、测试、打包、验证、安装、部署每个阶段。注意一点:同一生命周期内,执行后面的命令,前面的所有命令会自动执行!比如现在执行一条命令:

mvn test

test命令位于default这个生命周期内,所以它会先执行validate、compile这两个阶段,然后才会真正执行test阶段。同时,还可以一起执行多个命令,如:

mvn clean install

这两个命令隶属于不同的周期,所以会这样执行:先执行clean周期里的pre-clean、clean,再执行default周期中,validate~install这个闭区间内的所有阶段。

从上面不难发现,defaultMaven的核心周期,但其实上面并没有给完整,因为官方定义的default一共包含23个小阶段,上面的图只列出了七个核心周期,对详细阶段感兴趣的可以自行了解。

Maven中只定义了三套生命周期,以及每套周期会包含哪些阶段,而每个阶段具体执行的操作,这会交给插件去干,也就是说:Maven插件会实现生命周期中的每个阶段,这也是大家为什么看到IDEALifecycle下面,还会有个Plugins的原因:

Maven插件

当你双击Lifecycle中的某个生命周期阶段,实际会调用Plugins中对应的插件。在Shell窗口执行mvn命令时,亦是如此,因为插件对应的实现包,都会以jar包形式存储在本地仓库里。

你有特殊的需求,也可以在pom.xml<build>标签中,依靠<plugins>插件来导入。

二、Maven进阶操作

上面所说到的一些知识,仅仅只是Maven的基本操作,而它作为Java项目管理占有率最高的工具,还提供了一系列高阶功能,例如属性管理、多模块开发、聚合工程等,不过这里先来说说依赖冲突。

2.1、依赖冲突

依赖冲突是指:Maven项目中,当多个依赖包,引入了同一份类库的不同版本时,可能会导致编译错误或运行时异常。这种情况下,想要解决依赖冲突,可以靠升级/降级某些依赖项的版本,从而让不同依赖引入的同一类库,保持一致的版本号。

另外,还可以通过隐藏依赖、或者排除特定的依赖项来解决问题。但是想搞明白这些,首先得理解Maven中的依赖传递性,一起来看看。

2.1.1、依赖的传递性

先来看个例子:

依赖层级

目前的工程中,仅导入了一个spring-web依赖,可是从下面的依赖树来看,web还间接依赖于beans、core包,而core包又依赖于jcl包,此时就出现了依赖传递,所谓的依赖传递是指:当引入的一个包,如果依赖于其他包(类库),当前的工程就必须再把其他包引入进来

这相当于无限套娃,而这类“套娃”引入的包,被称为间接性依赖。与之对应的是直接性依赖,即:当前工程的pom.xml中,直接通过GAV坐标引入的包。既然如此,那么一个工程内的依赖包,就必然会出现层级,如:

boot-test依赖

在这里我们仅引入了一个boot-test坐标,但当打开依赖树时,会发现这一个包,依赖于其他许多包,而它所依赖的包又依赖于其他包……,如此不断套娃,最深套到了五层。而不同的包,根据自己所处的层级不同,会被划分为1、2、3、4……级。

2.1.2、自动解决冲突问题

Maven作为Apache旗下的产品,而且还经过这么多个版本迭代,对于依赖冲突问题,难道官方想不到吗?必然想到了,所以在绝对大多数情况下,依赖冲突问题并不需要我们考虑,Maven工具会自动解决,怎么解决的呢?就是基于前面所说的依赖层级,下面来详细说说。

①层级优先原则Maven会根据依赖树的层级,来自动剔除相同的包,层级越浅,优先级越高。这是啥意思呢?同样来看个例子:

层级优先

我们又通过GAV坐标导入了spring-web包,根据前面所说,web依赖于beans、core包,而beans包又依赖于core包,此时注意,这里出现了两个core包,前者的层级为2,后者的层级为3,所以Maven会自动将后者剔除,这点从图中也可明显看出,层级为3core直接变灰了。

②声明优先原则,上条原则是基于层级深度,来自动剔除冲突的依赖,那假设同级出现两个相同的依赖怎么办?来看例子:

声明优先

此时用GAV引入了web、jdbc两个包,来看右边的依赖树,web依赖于beans、core包,jdbc也依赖于这两个包,此时相同层级出现了依赖冲突,可从结果上来看,后面jdbc所依赖的两个包被剔除了,能明显看到一句:omitted for duplicate,这又是为啥呢?因为根据声明优先原则,同层级出现包冲突时,先声明的会覆盖后声明的,为此后者会被剔除

③配置优先原则,此时问题又又来了,既然相同层级出现同版本的类库,前面的会覆盖后面的,可是当相同层级,出现不同版本的包呢?依旧来看例子:

配置优先

此时pom引入了两个web包,前者版本为5.1.8,后者为5.1.2,这两个包的层级都是1,可是看右边的依赖树,此时会发现,5.1.8压根没引进来啊!为啥?这就是配置优先原则,同级出现不同版本的相同类库时,后配置的会覆盖先配置的

所以大家发现了嘛?在很多时候,并不需要我们考虑依赖冲突问题,Maven会依据上述三条原则,帮我们智能化自动剔除冲突的依赖,其他包都会共享留下来的类库,只有当出现无法解决的冲突时,这才需要咱们手动介入。

通常来说,Maven如果无法自动解决冲突问题,会在构建过程中抛出异常并提供相关信息,这时大家可以根据给出的信息,手动排除指定依赖。

2.1.3、主动排除依赖

所谓的排除依赖,即是指从一个依赖包中,排除掉它依赖的其他包,如果出现了Maven无法自动解决的冲突,就可以基于这种手段进行处理,例如:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
    <version>5.1.8.RELEASE</version>
    <exclusions>
        <!-- 排除web包依赖的beans包 -->
        <exclusion>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
        </exclusion>
    </exclusions>
</dependency>

依赖排除

从图中结果可以明显看出,通过这种方式,可以手动移除包所依赖的其他包。当出现冲突时,通过这种方式将冲突的两个包,移除掉其中一个即可。

其实还有种叫做“隐藏依赖”的手段,不过这种手段是用于多工程聚合项目,所以先讲清楚“多模块/工程”项目,接着再讲“隐藏依赖”。

2.2、Maven分模块开发

现如今,一个稍具规模的完整项目,通常都要考虑接入多端,如PC、WEB、APP端等,那此时问题来了,每个端之间的逻辑,多少会存在细微差异,如果将所有代码融入在一个Maven工程里,这无疑会显得十分臃肿!为了解决这个问题,Maven推出了分模块开发技术。

所谓的分模块开发,即是指创建多个Maven工程,组成一个完整项目。通常会先按某个维度划分出多个模块,接着为每个模块创建一个Maven工程,典型的拆分维度有三个:

  • ①接入维度:按不同的接入端,将项目划分为多个模块,如APP、WEB、小程序等;
  • ②业务维度:根据业务性质,将项目划分为一个个业务模块,如前台、后台、用户等;
  • ③功能维度:共用代码做成基础模块,业务做成一个模块、API做成一个模块……。

当然,通常①、②会和③混合起来用,比如典型的“先根据代码功能拆分,再根据业务维度拆分”。

相较于把所有代码揉在一起的“大锅饭”,多模块开发的好处特别明显:

  • ①简化项目管理,拆成多个模块/子系统后,每个模块可以独立编译、打包、发布等;
  • ②提高代码复用性,不同模块间可以相互引用,可以建立公共模块,减少代码冗余度;
  • ③方便团队协作,多人各司其职,负责不同的模块,Git管理时也能减少交叉冲突;
  • ④构建管理度更高,更方便做持续集成,可以根据需要灵活配置整个项目的构建流程;
  • ……

不过Maven2.0.9才开始支持聚合工程,在最初的时期里,想要实现分模块开发,需要手动先建立一个空的Java项目(Empty Project):

空项目

接着再在其中建立多个Maven Project

创建子项目

然后再通过mvn install命令,将不同的Maven项目安装到本地仓库,其他工程才能通过GAV坐标引入。

这种传统方式特别吃力,尤其是多人开发时,另一个模块的代码更新了,必须手动去更新本地仓库的jar包;而且多个模块之间相互依赖时,构建起来额外的麻烦!正因如此,官方在后面推出了“聚合工程”,下面聊聊这个。

2.3、Maven聚合工程

所谓的聚合工程,即是指:一个项目允许创建多个子模块,多个子模块组成一个整体,可以统一进行项目的构建。不过想要弄明白聚合工程,得先清楚“父子工程”的概念:

  • 父工程:不具备任何代码、仅有pom.xml的空项目,用来定义公共依赖、插件和配置;
  • 子工程:编写具体代码的子项目,可以继承父工程的配置、依赖项,还可以独立拓展。

Maven聚合工程,就是基于父子工程结构,来将一个完整项目,划分出不同的层次,这种方式可以很好的管理多模块之间的依赖关系,以及构建顺序,大大提高了开发效率、维护性。并且当一个子工程更新时,聚合工程可以保障同步更新其他存在关联的子工程!

2.3.1、聚合工程入门指南

理解聚合工程是个什么东东之后,接着来聊聊如何创建聚合工程,首先要创建一个空的Maven项目,作为父工程,这时可以在IDEA创建Maven项目时,把打包方式选成POM,也可以创建一个普通的Maven项目,然后把src目录删掉,再修改一下pom.xml

<!-- 写在当前项目GAV坐标下面 -->
<packaging>pom</packaging>

这样就得到了一个父工程,接着可以在此基础上,继续创建子工程:

创建子工程-1

当点击Next后,大家会发现:

创建子工程-2

这时无法手动指定G、V了,而是会从父工程中继承,最终效果如下:

聚合工程

这里我创建了两个子工程,所以父工程的pom.xml中,会用一个<modules>标签,来记录自己名下的子工程列表,而子工程的pom头,也多了一个<parent>标签包裹!大家看这个标签有没有眼熟感?大家可以去看一下SpringBoot项目,每个pom.xml文件的头,都是这样的。

这里提个问题:子工程下面能不能继续创建子工程?答案Yes,你可以无限套娃下去,不过我的建议是:一个聚合项目,最多只能有三层,路径太深反而会出现稀奇古怪的问题。

2.3.2、聚合工程的依赖管理

前面搭建好了聚合工程,接着来看个问题:

依赖冗余

zhuzi_001、002两个子工程中,各自引入了三个依赖,可观察上图会发现,两者引入的依赖仅有一个不同,其余全部一模一样!所以这时,就出现了“依赖冗余”问题,那有没有好的方式解决呢?答案是有的,前面说过:公共的依赖、配置、插件等,都可以配置在父工程里,如下:

继承依赖

当把公共的依赖定义在父工程中,此时观察图中右侧的依赖树,会发现两个子工程都继承了父依赖。

不过此时问题又来了!为了防止不同子工程引入不同版本的依赖,最好的做法是在父工程中,统一对依赖的版本进行控制,规定所有子工程都使用同一版本的依赖,怎么做到这点呢?可以使用<dependencyManagement>标签来管理,例如:

可选依赖

在父工程中,<dependencies>里只定义了一个webmvc依赖,而<dependencyManagement>中定义了druid、test、jdbc三个依赖,这两个标签有何区别呢?

  • <dependencies>:定义强制性依赖,写在该标签里的依赖项,子工程必须强制继承;
  • <dependencyManagement>:定义可选性依赖,该标签里的依赖项,子工程可选择使用。

相信这样解释后,大家对于两个标签的区别,就能一清二楚了!同时注意,子工程在使用<dependencyManagement>中已有的依赖项时,不需要写<version>版本号,版本号在父工程中统一管理,这就满足了前面的需求。这样做的好处在于:以后为项目的技术栈升级版本时,不需要单独修改每个子工程的POM,只需要修改父POM文件即可,大大提高了维护性

2.3.3、聚合工程解决依赖冲突

之前传统的Maven项目会存在依赖冲突问题,那聚合工程中存不存在呢?当然存在,比如001中引入了jdbc、test这两个包,而002中也引入了,这时假设把001工程打包到本地仓库,在002工程中引入时,此时依赖是不是又冲突了?Yes,怎么处理呢?先看例子:

聚合依赖传递

在上图中,001引入了aop包,接着通过install操作,把001工程打到了本地仓库。于是,在002工程中,引入了web、zhuzi_001这两个包。根据前面所说的依赖传递原则,002在引入001时,由于001引用了别的包,所以002被迫也引入了其他包。

还是那句话,大多数情况下,Maven会基于那三条原则,自动帮你剔除重复的依赖,如上图右边的依赖树所示,Maven自动剔除了重复依赖。这种结果显然是好现象,可是万一Maven不能自动剔除怎么办?这时就需要用到最开始所说的“隐藏依赖”技术了!

修改001pom.xml,如下:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.1.8.RELEASE</version>
    <optional>true</optional>
</dependency>

眼尖的小伙应该能发现,此时多了一个<optional>标签,该标签即是“隐藏依赖”的开关:

  • true:开启隐藏,当前依赖不会向其他工程传递,只保留给自己用;
  • false:默认值,表示当前依赖会保持传递性,其他引入当前工程的项目会间接依赖。

此时重新把001打到本地仓库,再来看看依赖树关系:

隐藏依赖

当开启隐藏后,其他工程引入当前工程时,就不会再间接引入当前工程的隐藏依赖,因此来手动排除聚合工程中的依赖冲突问题。其他许多资料里,讲这块时,多少讲的有点令人迷糊,而相信看到这里,大家就一定理解了Maven依赖管理。

2.3.4、父工程的依赖传递

来思考一个问题,现在项目需要用到Spring-Cloud-Alibaba的多个依赖项,如Nacos、Sentinel……等,根据前面所说的原则,由于这些依赖项可能会在多个子工程用到,最好的方式是定义在父POM<dependencyManagement>标签里,可是CloudAlibaba依赖这么多,一个个写未免太繁杂、冗余了吧?

面对上述问题时,该如何处理呢?如下:

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-dependencies</artifactId>
    <version>${spring-cloud-alibaba.version}</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>

<scope>标签取值为import的方式,通常会用在聚合工程的父工程中,不过必须配合<type>pom</type>使用,这是啥意思呢?这代表着:spring-cloud-alibaba-dependencies的所有子依赖,作为当前项目的可选依赖向下传递

而当前父工程下的所有子工程,在继承父POM时,也会将这些可选依赖继承过来,这时就可以直接选择使用某些依赖项啦,如:

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

2.3.5、聚合工程的构建

前面说到过,Maven聚合工程可以对所有子工程进行统一构建,这是啥意思呢?如果是传统的分模块项目,需要挨个进行打包、测试、安装……等工作,而聚合工程则不同,来看IDEA提供的Maven辅助工具:

项目构建

尾巴上带有root标识的工程,意味着这是一个父工程,在我们的案例中,有一个父、两个子,来看IDEA的工具,除开给两个子工程提供了Lifecycle命令外,还给父工程提供了一套Lifecycle命令,这两者的区别在哪儿呢?当你双击父工程的某个Lifecycle命令,它找到父POM<modules>标签,再根据其中的子工程列表,完成对整个聚合工程的构建工作。

大家可以去试一下,当你双击父工程Lifecycle下的clean,它会把你所有子工程的target目录删除。同理,执行其他命令时也一样,比如install命令,双击后它会把你所有的子工程,打包并安装到本地仓库,不过问题又又又来了!

假设这里001引用了002002又引用了001,两者相互引用,Maven会如何构建啊?到底该先打包001,还是该先打包002?我没去看过Lifecycle插件的源码,不过相信背后的逻辑,应该跟Spring解决依赖循环类似,感兴趣的小伙伴可以自行去研究。不过我这里声明一点:Maven聚合工程的构建流程,跟<modules>标签里的书写顺序无关,它会自行去推断依赖关系,从而完成整个项目的构建

2.3.6、聚合打包跳过测试

当大家要做项目发版时,就需要对整个聚合工程的每个工程打包(jarwar包),此时可以直接双击父工程里的package命令,但test命令在package之前,按照之前聊的生命周期原则,就会先执行test,再进行打包。

test阶段,会去找到所有子工程的src/test/java目录,并执行里面的测试用例,如果其中任何一个报错,就无法完成打包工作。而且就算不报错,执行所有测试用例也会特别耗时,这时该怎么办呢?可以选择跳过test阶段,在IDEA工具里的操作如下:

跳过测试

先选中test命令,接着点击上面的闪电图标,这时test就会画上横线,表示该阶段会跳过。如果你是在用mvn命令,那么打包跳过测试的命令如下:

mvn package –D skipTests

同时大家还可以在pom.xml里,配置插件来精准控制,比如跳过某个测试类不执行,配置规则如下:

<build>
    <plugins>
        <plugin>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>2.22.1</version>
            <configuration>
                <skipTests>true</skipTests>
                <includes>
                    <!-- 指定要执行的测试用例 -->
                    <include>**/XXX*Test.java</include>
                </includes>
                <excludes>
                    <!-- 执行要跳过的测试用例 -->
                    <exclude>**/XXX*Test.java</exclude>
                </excludes>
            </configuration>
        </plugin>
    </plugins>
</build>

不过这个功能有点鸡肋,了解即可,通常不需要用到。

2.4、Maven属性

回到之前案例的父工程POM中,此时来思考一个问题:

版本冗余

虽然我们通过<dependencyManagement>标签,来控制了子工程中的依赖版本,可目前还有一个小问题:版本冗余!比如现在我想把Spring版本从5.1.8升级到5.2.0,虽然不需要去修改子工程的POM文件,可从上图中大家会发现,想升级Spring的版本,还需要修改多处地方!

咋办?总不能只升级其中一个依赖的版本吧?可如果全部都改一遍,无疑就太累了……,所以,这里我们可以通过Maven属性来做管理,我们可以在POM<properties>标签中,自定义属性,如:

<properties>
    <spring.version>5.2.0.RELEASE</spring.version>
</properties>

而在POM的其他位置中,可以通过${}来引用该属性,例如:

属性管理

这样做的好处特别明显,现在我想升级Spring版本,只需要修改一处地方即可!

除开可以自定义属性外,Maven也会有很多内置属性,大体可分为四类:

类型使用方式
Maven内置属性${属性名},如${version}
项目环境属性${setting.属性名},如${settings.localRepository}
Java环境变量${xxx.属性名},如${java.class.path}
系统环境变量${env.属性名},如${env.USERNAME}

不过这些用的也不多,同时不需要记,要用的时候,IDEA工具会有提示:

提示

2.5、Maven多环境配置

实际工作会分为开发、测试、生产等环境,不同环境的配置信息也略有不同,而大家都知道,我们可以通过spring.profiles.active属性,来动态使用不同环境的配置,而Maven为何又整出一个多环境配置出来呢?想要搞清楚,得先搭建一个SpringBoot版的Maven聚合工程。

首先创建一个只有POM的父工程,但要注意,这里是SpringBoot版聚合项目,需稍微改造:

<!-- 先把Spring Boot Starter声明为父工程 -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.5.RELEASE</version>
    <relativePath/>
</parent>

<!-- 当前父工程的GAV坐标 -->
<modelVersion>4.0.0</modelVersion>
<groupId>com.zhuzi</groupId>
<artifactId>maven_zhuzi</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>

<!-- 配置JDK版本 -->
<properties>
    <java.version>8</java.version>
</properties>

<dependencies>
    <!-- 引入SpringBootWeb的Starter依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

<build>
<plugins>
    <!-- 引入SpringBoot整合Maven的插件 -->
    <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
</plugins>
</build>

对比普通聚合工程的父POM来说,SpringBoot版的聚合工程,需要先把spring-boot-starter声明成自己的“爹”,同时需要引入SpringBoot相关的插件,并且我在这里还引入了一个boot-web依赖。

接着来创建子工程,在创建时记得选SpringBoot模板来创建,不过创建后记得改造POM

<!-- 声明父工程 -->
<parent>
    <artifactId>maven_zhuzi</artifactId>
    <groupId>com.zhuzi</groupId>
    <version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<!-- 子工程的描述信息 -->
<artifactId>boot_zhuzi_001</artifactId>
<name>boot_zhuzi_001</name>
<description>Demo project for Spring Boot</description>

就只需要这么多,因为SpringBoot的插件、依赖包,在父工程中已经声明了,这里会继承过来。

接着来做Maven多环境配置,找到父工程的POM进行修改,如下:

<profiles>
    <!-- 开发环境 -->
    <profile>
        <id>dev</id>
        <properties>
            <profile.active>dev</profile.active>
        </properties>
    </profile>
    
    <!-- 生产环境 -->
    <profile>
        <id>prod</id>
        <properties>
            <profile.active>prod</profile.active>
        </properties>
        <!-- activeByDefault=true,表示打包时,默认使用这个环境 -->
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
    </profile>
    
    <!-- 测试环境 -->
    <profile>
        <id>test</id>
        <properties>
            <profile.active>test</profile.active>
        </properties>
    </profile>
</profiles>

配置完这个后,刷新当前Maven工程,IDEA中就会出现这个:

多环境

默认停留在prod上,这是因为POM中用<activeByDefault>标签指定了,接着去到子工程的application.yml中,完成Spring的多环境配置,如下:

# 设置启用的环境
spring:
  profiles:
    active: ${profile.active}

---
# 开发环境
spring:
  profiles: dev
server:
  port: 80
---
# 生产环境
spring:
  profiles: prod
server:
  port: 81
---
# 测试环境
spring:
  profiles: test
server:
  port: 82
---

这里可以通过文件来区分不同环境的配置信息,但我这里为了简单,就直接用---进行区分,这组配置大家应该很熟悉,也就是不同的环境中,使用不同的端口号,但唯一不同的是:以前spring.profiles.active属性会写上固定的值,而现在写的是${profile.active},这是为什么呢?

这代表从pom.xml中,读取profile.active属性值的意思,而父POM中配了三组值:dev、prod、test,所以当前子工程的POM,也会继承这组配置,而目前默认勾选在prod上,所以最终spring.profiles.active=prod,不过想要在application.yml读到pom.xml的值,还需在父POM中,加一个依赖和插件:

<!-- 开启 yml 文件的 ${} 取值支持 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <version>2.1.5.RELEASE</version>
    <optional>true</optional>
</dependency>

<!-- 添加插件,将项目的资源文件复制到输出目录中 -->
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-resources-plugin</artifactId>
    <version>3.2.0</version>
    <configuration>
        <encoding>UTF-8</encoding>
        <useDefaultDelimiters>true</useDefaultDelimiters>
    </configuration>
</plugin>

最后来尝试启动子工程,操作流程如下:

  • ①在Maven工具的Profiles中勾选dev,并刷新当前项目;
  • ②接着找到子工程的启动类,并右键选择Run ……启动子项目。

多环境整合

先仔细看执行的结果,我来解释一下执行流程:

  • ①启动时,pom.xml根据勾选的Profiles,使用相应的dev环境配置;
  • yml${profile.active}会读到profile.active=dev,使用dev配置组;
  • application.yml中的dev配置组,server.port=80,所以最终通过80端口启动。

看完这个流程,大家明白最开始那个问题了吗?Maven为何还整了一个多环境配置?

大家可能有种似懂非懂的感觉,这里来说明一下,先把环境换到微服务项目中,假设有20个微服务,此时项目要上线或测试,所以需要更改配置信息,比如把数据库地址换成测试、线上地址等,而不同环境的配置,相信大家一定用application-dev.yml、application-prod.yml……做好了区分。

但就算提前准备了不同环境的配置,可到了切换环境时,还需要挨个服务修改spring.profiles.active这个值,从dev改成prod、test,然后才能使用对应的配置进行打包,可这里有20个微服务啊,难道要手动改20次吗?

而在父POM中配置了Maven多环境后,这时yml会读取pom.xml中的值,来使用不同的配置文件,此时大家就只需要在IDEA工具的Profiles中,把钩子从dev换到test、prod,然后刷新一下MavenSpringBoot就能动态的切换配置文件,这是不是妙极了?因此,这才是Maven多环境的正确使用姿势!

三、Maven私服搭建

前面叨叨絮絮说了一大堆,最后就来聊聊Maven私服配置,为啥需要私服呢?

大家来设想这么个场景,假设你身在基建团队,主要负责研发各个业务开发组的公用组件,那么当你写完一个组件后,为了能让别的业务开发组用上,难道是先把代码打包,接着用U盘拷出来,给别人送过去嘛?有人说不至于,难道我不会直接发过去啊……

的确,用通讯软件发过去也行,但问题依旧在,假设你的组件升级了,又发一遍吗?所以,为了便于团队协作,搭建一个远程仓库很有必要,写完公用代码后,直接发布到远程仓库,别人需要用到时,直接从远程仓库拉取即可,而你升级组件后,只需要再发布一个新版本即可!

那远程仓库该怎么搭建呀?这就得用到Maven私服技术,最常用的就是基于Nexus来搭建。

3.1、Nexus私服搭建指南

NexusSonatype公司开源的一款私服产品,大家可以先去到Nexus官网下载一下安装包,Nexus同样是一款解压即用的工具,不过也要注意:解压的目录中不能存在中文,否则后面启动不起来!

解压完成后,会看到两个目录:

  • nexus-x.x.x-xx:里面会放Nexus启动时所需要的依赖、环境配置;
  • sonatype-work:存放Nexus运行时的工作数据,如存储上传的jar包等。

接着可以去到:

解压目录/etc/nexus-default.properties

这个文件修改默认配置,默认端口号是8081,如果你这个端口已被使用,就可以修改一下,否则通常不需要更改。接着可以去到解压目录的bin文件夹中,打开cmd终端,执行启动命令:

nexus.exe /run nexus

初次启动的过程会额外的慢,因为它需要初始化环境,创建工作空间、内嵌数据库等,直到看见这句提示:

启动成功

此时才算启动成功,Nexus初次启动后,会在sonatype-work目录中生成一个/nexus3/admin.password文件,这里面存放着你的初始密码,默认账号就是admin,在浏览器输入:

http://localhost:8081

访问Nexus界面,接着可以在网页上通过初始密码登录,登录后就会让你修改密码,改完后就代表Nexus搭建成功(不过要记住,改完密码记得重新登录一次,否则后面的操作会没有权限)。

3.2、Nexus私服仓库

默认仓库

登录成功后,点击Browse会看到一些默认仓库,这里稍微解释一下每个字段的含义。

  • Name:仓库的名字;
  • Type:仓库的类型;
  • Format:仓库的格式;
  • Status:仓库的状态;
  • URL:仓库的网络地址。

重点来说说仓库的分类,总共有四种类型:

类型释义作用
hosted宿主仓库保存中央仓库中没有的资源,如自研组件
proxy代理仓库配置中央仓库,即镜像源,私服中没有时会去这个地址拉取
group仓库组用来对宿主、代理仓库分组,将多个仓库组合成一个对外服务
virtual虚拟仓库并非真实存在的仓库,类似于MySQL中的视图

仓库的关系如下:

仓库关系

本地的Maven需要配置私服地址,当项目需要的依赖,在本地仓库没有时,就会去到相应的宿主/远程仓库拉取;如果宿主仓库也没有,就会根据配置的代理仓库地址,去到中央仓库拉取,最后依次返回……。

3.3、Maven配置私服

Maven想要使用私服,需要先修改settings.xml文件,我的建议是别直接改,先拷贝一份出来,接着来讲讲配置步骤。

①修改settings.xml里的镜像源配置,之前配的阿里云镜像不能用了,改成:

<mirror>
    <id>nexus-zhuzi</id>
    <mirrorOf>*</mirrorOf>
    <url>http://localhost:8081/repository/maven-public/</url>
</mirror>

②在私服中修改访问权限,允许匿名用户访问,如下:

开启权限

③在Nexus私服中配置一下代理仓库地址,即配置镜像源:

配置镜像

将这个默认的中央仓库地址,改为国内的阿里云镜像:

http://maven.aliyun.com/nexus/content/groups/public/

改完后记得拖动到最下方,点击Save保存一下即可。

④在Mavensettings.xml中,配置私服的账号密码:

<server>
  <id>zhuzi-release</id>
  <username>admin</username>
  <password>你的私服账号密码</password>
</server>

<server>
  <id>zhuzi-snapshot</id>
  <username>admin</username>
  <password>你的私服账号密码</password>
</server>

这两组配置,放到<servers>标签中的任何一处即可,这里可以先这样配置,看不懂没关系。

3.4、项目配置私服

前面配置好了本地Maven与私服的映射关系,接着要配置项目和私服的连接,说下流程。

①为项目创建对应的私服仓库,如果已有仓库,可以直接复用,创建步骤如下:

创建仓库

其中唯一值得一提的就是仓库格式,这里有三个可选项:

  • Release:稳定版,表示存放可以稳定使用的版本仓库;
  • Snapshot:快照版,代表存储开发阶段的版本仓库;
  • Mixed:混合版,不区分格式,表示混合存储代码的仓库。

为了规范性,我的建议是Release、Snapshot格式的仓库,各自都创建一个。

②在Maven工程的pom.xml文件中,配置对应的私服仓库地址,如下:

<!-- 配置当前工程,在私服中保存的具体位置 -->
<distributionManagement>
    <repository>
        <!-- 这里对应之前 settings.xml 里配置的server-id -->
        <id>zhuzi-release</id>
        <!-- 这里代表私服仓库的地址,大家只需要把后面的名字换掉即可 -->
        <url>http://localhost:8081/repository/zhuzi-release/</url>
    </repository>
    <snapshotRepository>
        <id>zhuzi-snapshot</id>
        <url>http://localhost:8081/repository/zhuzi-snapshot/</url>
    </snapshotRepository>
</distributionManagement>

③将当前项目发布到私服仓库,这里可以执行mvn clean deploy命令,也可以通过IDEA工具完成:

发布

不过这里有一个细节要注意,由于配置了私服上的两个宿主仓库,一个为稳定仓库,另一个为快照仓库,所以发布时,默认会根据当前项目的<version>版本结尾,来选择上传到相应的仓库,例如上图中的结尾是SNAPSHOT,所以会被发布到快照仓库,如果结尾不是这个后缀时,就会被发布到Release仓库。

当发布完成后,大家就可以登录Nexus界面,找到对应的宿主仓库,查看相应的jar包信息啦!不过还有一点要注意:你要发布的包不能带有上级,即不能有parent依赖,否则在其他人在拉取该项目时,会找不到其父项目而构建失败。要解决这个问题,可以先将parent项目打包并上传至远程仓库,然后再发布依赖于该parent项目的子模块。

3.5、Nexus配置仓库组

前面在说仓库类型时,还提到过一个“仓库组”的概念,如果你目前所处的公司,是一个大型企业,不同团队都有着各自的宿主仓库,而你恰恰又需要用到其他团队的组件,这时难道需要在pom.xml中,将远程仓库地址先改为其他团队的地址吗?答案是不需要的,这时可以创建一个仓库组。

创建仓库组

大家可以看到,图中的Members区域代表当前仓库组的成员,而这些成员会按照你排列的顺序,具备不同的优先级,越靠前的优先级越高。创建好仓库组后,接着可以去配置一下仓库组,这里有两种方式。

3.5.1、配置单个工程与仓库组的映射

这种方式只需修改pom.xml即可:

<repositories>
    <repository>
        <id>zhuzi-group</id>
        <!-- 配置仓库组的地址 -->
        <url>http://localhost:8081/repository/zhuzi-group/</url>
        <!-- 允许从中拉取稳定版的依赖 -->
        <releases>
            <enabled>true</enabled>
        </releases>
        <!-- 也允许从中拉取快照版的依赖 -->
        <snapshots>
            <enabled>true</enabled>
        </snapshots>
    </repository>
</repositories>

<pluginRepositories>
    <pluginRepository>
        <id>plugin-group</id>
        <url>http://localhost:8081/repository/zhuzi-group/</url>
        <releases>
            <enabled>true</enabled>
        </releases>
        <snapshots>
            <enabled>true</enabled>
        </snapshots>
    </pluginRepository>
</pluginRepositories>

在上述这组配置中,配置了<repositories>、<pluginRepositories>两个标签,分别是啥意思呢?很简单,第一个是普通依赖的仓库组地址,第二个是插件依赖的仓库组地址,前者针对于pom.xml中的<dependency>标签生效,后者针对<plugin>标签生效。

当你通过GAV坐标,引入一个依赖时,如果本地仓库中没找到,则会根据配置的仓库组地址,去到Nexus私服上拉取依赖。不过因为仓库组是由多个仓库组成的,所以拉取时,会根据仓库的优先级,依次搜索相应的依赖,第一个仓库将是最优先搜索的仓库。

3.5.2、配置本地Maven与仓库组的映射

上一种配置方式,只针对于单个Maven工程生效,如果你所有的Maven工程,都需要与Nexus私服上的仓库组绑定,这时就可以直接修改settings.xml文件,如下:

<profile>
	<id>zhuzi-group</id>
	<repositories>
		<repository>
			<id>nexus-maven</id>
			<url>http://localhost:8081/repository/zhuzi-group/</url>
			<releases>
				<enabled>true</enabled>
				<updatePolicy>always</updatePolicy>
			</releases>
			<snapshots>
				<enabled>true</enabled>
				<updatePolicy>always</updatePolicy>
			</snapshots>
		</repository>
	</repositories>
 
	<pluginRepositories>
		<pluginRepository>
			<id>nexus-maven</id>
			<url>http://localhost:8081/repository/zhuzi-group/</url>
			<releases>
				<enabled>true</enabled>
				<updatePolicy>always</updatePolicy>
			</releases>
			<snapshots>
				<enabled>true</enabled>
				<updatePolicy>always</updatePolicy>
			</snapshots>
		</pluginRepository>
	</pluginRepositories>
</profile>

这组配置要写在<profiles>标签里面,其他的与前一种方式没太大区别,唯一不同的是多了一个<updatePolicy>标签,该标签的作用是指定仓库镜像的更新策略,可选项如下:

  • always:每次需要Maven依赖时,都先尝试从远程仓库下载最新的依赖项;
  • daily:每天首次使用某个依赖时,从远程仓库中下载一次依赖项;
  • interval:X:每隔X个小时,下载一次远程仓库的依赖,X只能是整数;
  • never:仅使用本地仓库中已经存在的依赖项,不尝试从远程仓库中拉取。

Maven工程使用依赖时,首先会从本地仓库中查找所需的依赖项,如果本地仓库没有,则从配置的远程仓库下载这时会根据<updatePolicy>策略来决定是否需要从远程仓库下载依赖。

不过上述这样配置后,还无法让配置生效,如果想要生效,还得激活一下上述配置:

<activeProfiles>
    <!-- 这里写前面配置的ID -->
	<activeProfile>zhuzi-group</activeProfile>
</activeProfiles>

不过要记住,无论两种方式内的哪一种,都只允许从私服上拉取依赖,如果你的某个工程,想要打包发布到私服上,还是需要配置3.4阶段的<distributionManagement>标签。

四、Maven总结

最后,对于Maven项目的命名,不同单词最好用-减号分割,而不是_下划线,毕竟Spring、Apache……的开源项目,都采用这种命名方式。不过,如果你要问我:“你为啥用_不用-啊”?别问,问就是我控几不住我寄几啊……,更何况有句话说的好:知错不改,善莫大焉!

到这里,对于Maven常用的功能已经讲完了,掌握这些知识后,玩转Maven的难度应该不大,不过Maven的功能远不仅如此,就光说pom.xml这个文件,可以配置的标签有几百个,本文仅讲到了几十个罢了。如果你对其他不常用的标签感兴趣,我整了一份POM帮助文档,和Nexus安装包一起放到了网盘里,有需要可以关注微信公众号:竹子爱熊猫,回复POM领取。