当前位置: 欣欣网 > 码农

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

2024-02-23码农

引言

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 的原因:

    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 会自动将后者剔除,这点从图中也可明显看出,层级为 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>

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

    创建子工程-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 不能自动剔除怎么办?这时就需要用到最开始所说的「隐藏依赖」技术了!

    修改 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,陪伴学习,共同优秀。