Maven大家都很熟悉,但是我们很多人,对它其实都是似乎很熟,但是又好像不熟悉的感觉,包括我,今天咱们就一起来彻底了解Maven的所有功能,我们从入门,到原理剖析,再到实践操作,最后是私服的搭建以及配置,整体并彻底了解一下Maven。
1. Maven的安装以及配置
1.1 Maven的下载
下载路径:Maven官网
上面一个是Linux环境,一个Windows环境,大家依照自己的环境下载解压即可(后面我会以Windows环境为例)。
1.2 环境变量的配置
在系统环境中,新建一个MAVEN_HOME或M2_HOME的环境变量,值写成解压路径。
找到Path变量并编辑,在其中新增一行,配置一下bin目录:
%M2_HOME%\bin
之所以要配置这一步,主要是因为软件的bin目录,通常会存放一些可执行的脚本/工具,如JDK的bin目录中,就存放着javac、javap、jstack……一系列工具。如果不在Path中配置bin,那想要使用这些工具,只能去到JDK安装目录下的bin目录,然后才能使用。
配置完毕以后就会全局生效。
1.3 指定Maven本地仓库位置
找到根目录下的conf/settings.xml
,然后点击编辑,找到<localRepository>
标签,将其挪动到注释区域外,然后配置本地仓库位置:
<localRepository>空的本地目录(最好别带中文)</localRepository>
1.4 配置阿里镜像仓库
由于Apache的官方镜像位于国外,平时拉取依赖比较慢,我们一般配置阿里的镜像仓库。搜索<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命令,就可以看到Maven的版本信息。
1.5 Maven的IDEA配置
安装好Maven完毕后,通过IDEA工具来创建Maven项目,我们需要配置本地Maven及仓库位置:
1.6 Maven依赖的查询网站
Maven仓库官网
1.7 依赖的范围管理
有时候我们需要对引入的依赖进行范围管理,比如测试依赖包JUnit,我们只需要在我们进行单元测试的时候有效,其他环境不需要。这时候我们就可以通过<score>
标签来控制。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.1.8.RELEASE</version>
<scope>test</scope>
</dependency>
作用范围有三种:
- 主代码范围有效。
- 测试代码范围有效。
- 是否参与打包(package命令范围内)
同时,<scope>
标签还可以通过自定义的方式来添加其他的scope范围,例如Maven插件中使用的scope值:
<dependency>
<groupId>some.group</groupId>
<artifactId>some-artifact</artifactId>
<version>1.0</version>
<scope>plugin</scope>
</dependency>
这里的plugin就是自定义的scope,表示该依赖只在Maven插件中生效。
2. Maven的工作原理
在Maven中,整体会分为工程、仓库两大类,工程是“依赖使用者”,仓库是“依赖提供者”,关系如下:
这里面:
中央仓库:指的是镜像源,里面拥有海量的公共jar包资源;
远程仓库:也就是私服仓库,主要存储公司内部的jar包资源;
本地仓库:自己电脑本地的仓库,会在磁盘上存储jar包资源。
Maven的工作流程如下:
- 项目通过GAV坐标引入依赖,首先会去本地仓库查找jar包;
- 如果在本地仓库中找到了,直接把依赖载入到当前工程的External Libraries中;
- 如果没找到,则去读取settings.xml文件,判断是否存在私服配置;
- 如果有私服配置,根据配置的地址找到远程仓库,接着拉取依赖到本地仓库;
- 如果远程仓库中没有依赖,根据私服配置去中央仓库拉取,然后放到私服、本地仓库;
3. Maven的生命周期
在IDEA中我们可以看到Maven的九种Lifecycle命令,如下:
双击其中任何一个,都会执行相应的Maven构建动作,因为里面封装了Maven提供的命令。下面解释一下每个命令的作用:
- 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
命令,它会先执行validate、compile这两个阶段,然后才会真正执行test阶段。
多命令同时执行:
mvn clean install
执行过程:先执行clean周期里的pre-clean、clean,再执行default周期中,validate~install这个闭区间内的所有阶段。
还有个小问题点:
就是我们的IDEA为什么还会有个Plugins的,如下:
这是因为:Maven插件会实现生命周期中的每个阶段,而每个阶段具体执行的操作,这会交给插件去干。当你双击Lifecycle中的某个生命周期阶段,实际会调用Plugins中对应的插件。
4. Maven的高级操作
4.1 依赖冲突
依赖冲突是指:在Maven项目中,当多个依赖包,引入了同一份类库的不同版本时,可能会导致编译错误或运行时异常。这种情况下,想要解决依赖冲突,可以靠升级/降级某些依赖项的版本,从而让不同依赖引入的同一类库,保持一致的版本号,还可以通过隐藏依赖、或者排除特定的依赖项来解决问题。
在这个之前我们得要了解一下什么是依赖传递性。
1. 依赖的传递性
依赖具有传递性,当我们引入了一个依赖的时候,就会自动引入该依赖引入的所有依赖,依次往下引入所有依赖。比如我引入spring-boot-starter-web,会自动引入:
总之就是当引入的一个包,如果依赖于其他包(类库),当前的工程就必须再把其他包引入进来。
2. 自动解决冲突
在绝对大多数情况下,依赖冲突问题并不需要我们考虑,Maven工具会自动解决,根据如下几个原则:
- 层级优先原则:Maven会根据依赖树的层级,来自动剔除相同的包,层级越浅,优先级越高,无效的包会自动变成灰色。
- 声明优先原则:上条原则是基于层级深度,来自动剔除冲突的依赖,假设同级出现两个相同的依赖,这时候同层级出现包冲突时,先声明的会覆盖后声明的,为此后者会被剔除。
- 配置优先原则:同级出现不同版本的相同类库时,后配置的会覆盖先配置的。
Maven会依据上述三条原则,帮我们智能化自动剔除冲突的依赖。
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>
4.2 Maven的分模块开发
分模块开发,即是指创建多个Maven工程,组成一个完整项目。通常会先按某个维度划分出多个模块,接着为每个模块创建一个Maven工程,比较常见的拆分维度有:
- 接入维度:按不同的接入端,将项目划分为多个模块,如APP、WEB、小程序等;
- 业务维度:根据业务性质,将项目划分为多个业务模块,如前台、后台、用户等;
- 功能维度:共用代码做成基础模块,业务做成一个模块、API做成一个模块……。
多模块开发的好处有:
- 简化项目管理,拆成多个模块后,每个模块可以独立编译、打包、发布等;
- 提高代码复用性,不同模块间可以相互引用,可以建立公共模块,减少代码冗余度;
- 方便团队协作,多人各司其职,负责不同的模块,Git管理时也能减少交叉冲突;
- 构建管理度更高,更方便做持续集成,可以根据需要灵活配置整个项目的构建流程;
4.3 Maven聚合工程
聚合工程,即是指:一个项目允许创建多个子模块,多个子模块组成一个整体,可以统一进行项目的构建。
在此之前我们先理解一下父子工程:
父工程:不具备任何代码、仅有pom.xml的空项目,用来定义公共依赖、插件和配置;
子工程:编写具体代码的子项目,可以继承父工程的配置、依赖项,还可以独立拓展。
而Maven聚合工程,就是基于父子工程结构,来将一个完整项目,划分出不同的层次。
1. 聚合工程的创建
首先要创建一个空的Maven项目,作为父工程,这时可以在IDEA创建Maven项目时,把打包方式选成POM,也可以创建一个普通的Maven项目,然后把src目录删掉,再修改一下pom.xml:
<!-- 写在当前项目GAV坐标下面 -->
<packaging>pom</packaging>
然后创建子工程
继续往下走即可。
这时候我们看父工程的pom.xml中,会用一个<modules>
标签,来记录自己名下的子工程列表,而子工程的pom头,也多了一个<parent>
标签包裹,如下:
注意:子工程下面也可以继续创建子工程,但是不建议,这样会比较混乱。
2. 聚合工程的依赖管理
一般情况下,为了防止依赖冗余,我们会将公共的依赖、配置、插件等,都可以配置在父工程里。然后会发现子工程都会继承了父依赖。
为了防止不同子工程引入不同版本的依赖,最好的做法是在父工程中,统一对依赖的版本进行控制,规定所有子工程都使用同一版本的依赖。可以使用<dependencyManagement>
标签来管理。
要注意如下两个的区别:
<dependencies>
:定义强制性依赖,写在该标签里的依赖项,子工程必须强制继承;
<dependencyManagement>
:定义可选性依赖,该标签里的依赖项,子工程可选择使用。
注意:子工程在使用<dependencyManagement>
中已有的依赖项时,不需要写<version>
版本号,版本号在父工程中统一管理,这就满足了前面的需求。好处在于:以后为项目的技术栈升级版本时,不需要修改每个子工程的POM,只需要修改父POM文件即可,提高了维护性。
3. 聚合工程处理依赖冲突
大多数情况下,Maven会基于那三条原则,自动帮你剔除重复的依赖,如果无法剔除,就得使用“隐藏依赖”技术了,如下:
假如现有两个子工程001,002。
修改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:默认值,表示当前依赖会保持传递性,其他引入当前工程的项目会间接依赖。
当开启隐藏后,其他工程引入当前工程时,就不会再间接引入当前工程的隐藏依赖,因此来手动排除聚合工程中的依赖冲突问题。
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时,也会将这些可选依赖继承过来。
5. 聚合工程的构建
Maven聚合工程可以对所有子工程进行统一构建。
尾巴上带有root标识的工程,意味这是一个父工程,当你双击父工程的某个Lifecycle命令,它找到父POM的<modules>
标签,再根据其中的子工程列表,完成对整个聚合工程的构建工作。
这里就有一个问题,我们怎么解决maven子工程之间的相互依赖,这个随着项目的复杂度提升,引用不当,就会出现,最好的办法就是解决这种问题,因为这种本质是不合理的,有如下解决方式:
- 将相互依赖的部分抽出来,单独形成一个模块,让这两个来分别依赖这个模块。
- 使用插件build-helper-maven-plugin。
6. 聚合打包跳过测试
有时候我们不想执行测试用例,但是如果直接双击父工程里的package命令,但test命令在package之前,按照之前聊的生命周期原则,就会先执行test,再进行打包。
解决办法:
先选中test命令,接着点击上面的闪电图标,这时test就会画上横线,表示该阶段会跳过。
同时还可以在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>
4.4 Maven属性
我们可以通过在POM的<properties>
标签中,自定义属性,来进行版本管理,如下:
4.5 Maven的多环境配置
实际工作会分为开发、测试、生产等环境,不同环境的配置信息也略有不同,而大家都知道,我们可以通过spring.profiles.active属性,来动态使用不同环境的配置,而Maven为何又整出一个多环境配置出来呢?
带着问题我们先搭建一个SpringBoot版的Maven聚合工程。
首先创建一个只有POM的父工程。
<!-- 先把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>
接着来创建子工程。
<!-- 声明父工程 -->
<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>
然后再做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
---
或者采用不同环境的yml来做区分配置信息。这时候会发现spring.profiles.active
属性以前我们会写上固定的值,而现在写的是${profile.active}
。这表示会从pom.xml中,读取profile.active
属性值,而父POM中配了三组值:dev、prod、test
,所以当前子工程的POM,也会继承这组配置,而目前默认勾选在prod上,所以最终表达的含义是spring.profiles.active=prod
。
要注意的是,要想yml读取到pom文件的值,我们还得在父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端口启动。
有人可能就要问,既然都是修改文件启动,这不就是从修改yml改成了修改pom呗,这样想就错了,因为你们肯定也发现了,我从头到尾一直是在父工程上的pom,修改环境配置,只需要修改一次,假如现在有20个微服务,如果去修改yml,就得每个都得修改,那还不把人累死,而修改父工程pom,只需要修改一次。
今天和大家就分享到这里吧,明天给大家分享下Maven私服的搭建过程,内容有点多,分两次写吧。