引言

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

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

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

一、Maven快速上手/回顾

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

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

├─ProjectName                              

│  ├─src                      

│  │   ├─main                  

│  │   │  ├─java             

│  │   │  ├─resources      

│  │   │  └─webapp                            

│  │   ├─test                  

│  │   │  ├─java             

│  │   │  └─resources                            

│  └─pom.xml                      

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

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

并且最重要的一点,它还提供了依赖(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,然后点击编辑,找到标签,将其挪动到注释区域外,然后配置本地仓库位置:

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

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

  

    alimaven  

    aliyun maven  

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

    central          

到这里,整个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中,先写一个标签; ②在标签中,使用标签来导入依赖; ③在标签中,通过GAV坐标来导入依赖。

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

    

        org.springframework

        spring-web

        5.1.8.RELEASE

    

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

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

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

1.2.3、依赖范围管理

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

    org.springframework.boot

    spring-boot-starter-test

    2.1.8.RELEASE

    test

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

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

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

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

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

    some.group

    some-artifact

    1.0

    plugin

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

最后,标签还有一类特殊、但很常用的取值范围,即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的标签中,依靠插件来导入。

二、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无法自动解决的冲突,就可以基于这种手段进行处理,例如:

    org.springframework

    spring-web

    5.1.8.RELEASE

    

        

        

            org.springframework

            spring-beans

        

    

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

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

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:

pom

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

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

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

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

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

2.3.2、聚合工程的依赖管理

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

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

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

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

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

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

相信这样解释后,大家对于两个标签的区别,就能一清二楚了!同时注意,子工程在使用中已有的依赖项时,不需要写版本号,版本号在父工程中统一管理,这就满足了前面的需求。这样做的好处在于:以后为项目的技术栈升级版本时,不需要单独修改每个子工程的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,如下:

    org.springframework

    spring-aop

    5.1.8.RELEASE

    true

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

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

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

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

2.3.4、父工程的依赖传递

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

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

    com.alibaba.cloud

    spring-cloud-alibaba-dependencies

    ${spring-cloud-alibaba.version}

    pom

    import

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

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

    com.alibaba.cloud

    spring-cloud-starter-alibaba-sentinel

    com.alibaba.cloud

    spring-cloud-starter-alibaba-nacos-config

2.3.5、聚合工程的构建

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

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

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

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

2.3.6、聚合打包跳过测试

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

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

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

mvn package –D skipTests

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

    

        

            maven-surefire-plugin

            2.22.1

            

                true

                

                    

                    **/XXX*Test.java

                

                

                    

                    **/XXX*Test.java

                

            

        

    

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

2.4、Maven属性

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

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

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

    5.2.0.RELEASE

而在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版聚合项目,需稍微改造:

    org.springframework.boot

    spring-boot-starter-parent

    2.1.5.RELEASE

    

4.0.0

com.zhuzi

maven_zhuzi

1.0-SNAPSHOT

pom

    8

    

    

        org.springframework.boot

        spring-boot-starter-web

    

    

    

        org.springframework.boot

        spring-boot-maven-plugin

    

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

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

    maven_zhuzi

    com.zhuzi

    1.0-SNAPSHOT

4.0.0

boot_zhuzi_001

boot_zhuzi_001

Demo project for Spring Boot

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

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

    

    

        dev

        

            dev

        

    

    

    

    

        prod

        

            prod

        

        

        

            true

        

    

    

    

    

        test

        

            test

        

    

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

默认停留在prod上,这是因为POM中用标签指定了,接着去到子工程的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中,加一个依赖和插件:

    org.springframework.boot

    spring-boot-configuration-processor

    2.1.5.RELEASE

    true

    org.apache.maven.plugins

    maven-resources-plugin

    3.2.0

    

        UTF-8

        true

    

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

①在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官网[6]下载一下安装包,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:

访问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里的镜像源配置,之前配的阿里云镜像不能用了,改成:

    nexus-zhuzi

    *

    http://localhost:8081/repository/maven-public/

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

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

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

http:

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

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

  zhuzi-release

  admin

  你的私服账号密码

  zhuzi-snapshot

  admin

  你的私服账号密码

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

3.4、项目配置私服

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

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

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

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

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

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

    

        

        zhuzi-release

        

        http://localhost:8081/repository/zhuzi-release/

    

    

        zhuzi-snapshot

        http://localhost:8081/repository/zhuzi-snapshot/

    

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

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

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

3.5、Nexus配置仓库组

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

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

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

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

    

        zhuzi-group

        

        http://localhost:8081/repository/zhuzi-group/

        

        

            true

        

        

        

            true

        

    

    

        plugin-group

        http://localhost:8081/repository/zhuzi-group/

        

            true

        

        

            true

        

    

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

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

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

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

 zhuzi-group

 

  

   nexus-maven

   http://localhost:8081/repository/zhuzi-group/

   

    true

    always

   

   

    true

    always

   

  

 

 

 

  

   nexus-maven

   http://localhost:8081/repository/zhuzi-group/

   

    true

    always

   

   

    true

    always

   

  

 

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

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

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

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

    

 zhuzi-group

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

四、Maven总结

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

到这里,对于Maven常用的功能已经讲完了,掌握这些知识后,玩转Maven的难度应该不大,不过Maven的功能远不仅如此,就光说pom.xml这个文件,可以配置的标签有几百个,本文仅讲到了几十个罢了。

好文阅读

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: