引言
Maven
应该是大家的老熟客了,身为Java
程序员,几乎每天都会跟他打交道。
不过有趣的是:很多伙伴对Maven
,似乎很熟,但又好像不熟;在理解上,处于似懂非懂的“量子纠缠态”,为什么这么说呢?原因很简单,要说不熟吧,偏偏每天都有所接触;要说熟吧,可是对许多高级功能又仅是一知半解。
正因如此,为了辅助大家从“量子纠缠态”中走出来,本文会从零开始,带着大家玩转Maven
技术。当然,其实写这篇文章更大的目的,是为后续写《漫谈分布式》专栏做准备,毕竟后续会频繁用到Maven
构建多工程项目。
一、Maven快速上手/回顾
声明:如果基础够扎实的小伙伴,可以跳到1.3
阶段(快速刷一遍当复习也行)。
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_HOME
或M2_HOME
的环境变量,值写成解压路径。
②找到Path
变量并编辑,在其中新增一行,配置一下bin
目录:
%M2_HOME%\bin
其实安装许多软件,都要配置这一步,到底是为啥呢?因为任何软件的bin
目录,通常会存放一些可执行的脚本/工具,如JDK
的bin
目录中,就存放着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
及仓库位置:
在这里配置,是对全局生效,后续创建的所有Maven
项目,都会使用这组配置。
1.2.1、IDEA创建Maven项目
接着就可以创建Maven
项目,这个过程特别简单,先选择New Project
:
这里选创建Maven
项目,接着指定一下JDK
,还可以选择是否使用骨架,选好后直接Next
下一步:
这里需要写一下GAV
坐标,稍微解释一下三个选项的含义:
GroupID
:组织ID
,一般写公司的名称缩写;ArtifactID
:当前Maven
工程的项目名字;Version
:当前Maven
工程的版本。
接着点下一步,然后选择一下项目的存储位置,最后点击Finish
创建即可:
这一步结束后,就得到了一个纯净版的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
这个闭区间内的所有阶段。
从上面不难发现,default
是Maven
的核心周期,但其实上面并没有给完整,因为官方定义的default
一共包含23
个小阶段,上面的图只列出了七个核心周期,对详细阶段感兴趣的可以自行了解。
>
Maven
中只定义了三套生命周期,以及每套周期会包含哪些阶段,而每个阶段具体执行的操作,这会交给插件去干,也就是说:Maven
插件会实现生命周期中的每个阶段,这也是大家为什么看到IDEA
的Lifecycle
下面,还会有个Plugins
的原因:
当你双击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
坐标,但当打开依赖树时,会发现这一个包,依赖于其他许多包,而它所依赖的包又依赖于其他包……,如此不断套娃,最深套到了五层。而不同的包,根据自己所处的层级不同,会被划分为1、2、3、4……
级。
2.1.2、自动解决冲突问题
Maven
作为Apache
旗下的产品,而且还经过这么多个版本迭代,对于依赖冲突问题,难道官方想不到吗?必然想到了,所以在绝对大多数情况下,依赖冲突问题并不需要我们考虑,Maven
工具会自动解决,怎么解决的呢?就是基于前面所说的依赖层级,下面来详细说说。
①层级优先原则,Maven
会根据依赖树的层级,来自动剔除相同的包,层级越浅,优先级越高。这是啥意思呢?同样来看个例子:
我们又通过GAV
坐标导入了spring-web
包,根据前面所说,web
依赖于beans、core
包,而beans
包又依赖于core
包,此时注意,这里出现了两个core
包,前者的层级为2
,后者的层级为3
,所以Maven
会自动将后者剔除,这点从图中也可明显看出,层级为3
的core
直接变灰了。
②声明优先原则,上条原则是基于层级深度,来自动剔除冲突的依赖,那假设同级出现两个相同的依赖怎么办?来看例子:
此时用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>
这样就得到了一个父工程,接着可以在此基础上,继续创建子工程:
当点击Next
后,大家会发现:
这时无法手动指定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
不能自动剔除怎么办?这时就需要用到最开始所说的“隐藏依赖”技术了!
修改001
的pom.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
引用了002
,002
又引用了001
,两者相互引用,Maven
会如何构建啊?到底该先打包001
,还是该先打包002
?我没去看过Lifecycle
插件的源码,不过相信背后的逻辑,应该跟Spring
解决依赖循环类似,感兴趣的小伙伴可以自行去研究。不过我这里声明一点:Maven
聚合工程的构建流程,跟<modules>
标签里的书写顺序无关,它会自行去推断依赖关系,从而完成整个项目的构建。
2.3.6、聚合打包跳过测试
当大家要做项目发版时,就需要对整个聚合工程的每个工程打包(jar
或war
包),此时可以直接双击父工程里的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
,然后刷新一下Maven
,SpringBoot
就能动态的切换配置文件,这是不是妙极了?因此,这才是Maven
多环境的正确使用姿势!
三、Maven私服搭建
前面叨叨絮絮说了一大堆,最后就来聊聊Maven
私服配置,为啥需要私服呢?
大家来设想这么个场景,假设你身在基建团队,主要负责研发各个业务开发组的公用组件,那么当你写完一个组件后,为了能让别的业务开发组用上,难道是先把代码打包,接着用U
盘拷出来,给别人送过去嘛?有人说不至于,难道我不会直接发过去啊……
的确,用通讯软件发过去也行,但问题依旧在,假设你的组件升级了,又发一遍吗?所以,为了便于团队协作,搭建一个远程仓库很有必要,写完公用代码后,直接发布到远程仓库,别人需要用到时,直接从远程仓库拉取即可,而你升级组件后,只需要再发布一个新版本即可!
那远程仓库该怎么搭建呀?这就得用到Maven
私服技术,最常用的就是基于Nexus
来搭建。
3.1、Nexus私服搭建指南
Nexus
是Sonatype
公司开源的一款私服产品,大家可以先去到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
保存一下即可。
④在Maven
的settings.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
领取。