引言
Maven
应该是大家的老熟客了,身为
Java
程序员,几乎每天都会跟他打交道。
不过有趣的是:
很多伙伴对
Maven
,似乎很熟,但又好像不熟;在理解上,处于似懂非懂的「量子纠缠态」
,为什么这么说呢?原因很简单,要说不熟吧,偏偏每天都有所接触;要说熟吧,可是对许多高级功能又仅是一知半解。
正因如此,为了辅助大家从「量子纠缠态」中走出来,本文会从零开始,带着大家玩转
Maven
技术。当然,其实写这篇文章更大的目的,是为后续写
【漫谈分布式】
专栏做准备,毕竟后续会频繁用到
Maven
构建多工程项目。
一、Maven快速上手/回顾
1.1、Maven工作原理剖析
在
Maven
中,节点会分为工程、仓库两大类,工程是「依赖使用者」,仓库是「依赖提供者」,关系如下:
看着或许有点头大,要讲明白得先弄清里面三种仓库:
• 中央仓库:就是前面配置的镜像源,里面拥有海量的公共
jar
包资源;
• 远程仓库:也叫私服仓库,主要存储公司内部的
jar
包资源,这个后续会细说;
• 本地仓库:自己电脑本地的仓库,会在磁盘上存储
jar
包资源。
大致了解三种仓库的含义后,接着来梳理
Maven
的工作流程:
• ①项目通过
GAV
坐标引入依赖,首先会去本地仓库查找
jar
包;
• ②如果在本地仓库中找到了,直接把依赖载入到当前工程的
External Libraries
中;
• ③如果没找到,则去读取
settings.xml
文件,判断是否存在私服配置;
• ④如果有私服配置,根据配置的地址找到远程仓库,接着拉取依赖到本地仓库;
• ⑤如果远程仓库中没有依赖,根据私服配置去中央仓库拉取,然后放到私服、本地仓库;
• ⑥从远程或中央仓库中,把依赖下载到本地后,再重复第二步,把依赖载入到项目中。
上述六步便是
Maven
的完整工作流程,可能许多人没接触过私服,这个会放到后面聊。如果你的项目没配置
Maven
私服,那么第三步时,会直接从
settings.xml
读取镜像源配置,直接去到中央仓库拉取依赖。
不过这里有个问题,拉取/引入依赖时,
Maven
是怎么知道要找谁呢?答案是依靠
GAV
坐标,大家可以去观察一下本地仓库,当你引入一个依赖后,本地仓库中的目录,会跟你的
GAV
坐标一一对应,如:
无论是什么类型的仓库,都会遵循这个原则进行构建,所以,只要你书写了正确的
GAV
坐标,就一定能够找到所需的依赖,并将其载入到项目中。
1.2、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、聚合工程的构建
前面说到过,
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.5、聚合打包跳过测试
当大家要做项目发版时,就需要对整个聚合工程的每个工程打包(
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
环境变量
|
${
.
属性名},如
${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
这个文件,可以配置的标签有几百个,本文仅讲到了几十个罢了。
个人简介
哪吒,群友爱称:吒哥,一个靠着热情攀登至C站巅峰的中年男子,CSDN粉丝50万+,2022CSDN博客之星Top1,10年开发管理经验,目前就职于某一线大厂,专注Java硬核干货分享,立志做到Java赛道全网Top N。
喜欢的可以给个关注,关注「哪吒编程」,提高Java技能
最后欢迎大家加入哪吒的知识星球【Java学习星球】,星球中有很多独家的干货内容。比如:Java后端学习路线,10万字208道Java经典面试题,10大学习专栏,包含Java基础、数据库、SSM、SpringBoot、微服务、华为OD机试、算法、优化、中间件、设计模式等系列文章。
坚持每日学习打卡,养成持续学习、持续成长的好习惯。
成功秘诀只有一个,那就是卷,督促和鞭策自己,永不放弃。
和哪吒一起学Java,陪伴学习,共同优秀。