深入学习Maven构建工具
该笔记主要是学习了许晓斌大佬的 《Maven实战》 之后的笔记。虽然书已经老了,但是精髓还是在的。共勉
- 前提概念
- 什么是构建?
- 什么是自动化构建?
- Maven的概念
- 什么是Maven?
- 为什么要用Maven?
- Maven的作用?
- Maven中的坐标和依赖
- Maven中的坐标
- Maven坐标的5个重要元素
- Maven的依赖
- Maven的依赖配置和7个元素
- 依赖范围
- 传递性依赖
- 传递调解
- 可选依赖
- 排除依赖
- Maven中的仓库
- Maven的生命周期和插件
- 什么是Maven的生命周期
- Maven的三套生命周期
- 生命周期和命令行的关系
- 插件目标Goal
- 生命周期和插件的关系
- 插件绑定
- Maven中的聚合和继承
前提概念
什么是构建?
什么是构建呢?我个人感觉这是一个较为笼统,又没有一个标准的概念。所以我根据书上的知识加上自己的理解,我认为构建就是: 从建立项目,编写代码,到测试,到打包,到部署的整个流程就是一个项目构建的过程。
什么是自动化构建?
举个例子,我今天从git上pull下了代码,开始编码工作,解决bug,编写测试代码,跑测试用例,解决问题,打包,部署。发现问题,重新修改代码,测试,打包,部署。
项目构建的整一个过程是非常繁琐的,可能一天下来,我们很多时间都花在打包,测试,部署身上了。真正专注在解决业务代码需求的时候就少了很多。所以我们此时就会想有没有东西可以帮我们一条龙的解决掉,而不需要程序员自己总是手动的去构建项目。
所谓自动化构建就是一个脚本或一个工具,帮你解决总是要手动构建的问题
Maven的概念
什么是Maven?
引用百度百科上的话:
Maven项目对象模型(POM),可以通过一小段描述信息来管理项目的构建,报告和文档的软件项目管理工具。
Maven - 百度百科
引用书上的话:
Maven这个词可以翻译为“知识的积累”,也可以翻译成“专家”或“内行”。
总之我们可以知道,Maven是一个专业且非常好用的项目构建管理工具,在国内有非常广泛的应用。
为什么用Maven?
两个好处,也是Maven的两个作用
- 项目自动化构建,通过命令行就可以完成整个项目构建过程,终于不再需要我们总是手动的进行项目构建
- 管理项目依赖和Jar包,终于不用担心项目庞大起来,出现的各种jar管理,版本冲突,依赖臃肿等等问题
Maven的作用?
据我了解,Maven的主要功能可以笼统的归纳为两项:
- 项目依赖管理
- 项目构建管理
所以Maven是一个功能强大的构建工具,可以帮助我们自动化构成过程,从清理、编译、测试到生成报告,再到打包部署。同时Maven也是一个依赖管理和项目信息管理的工具,它能够将我们需要依赖的包通过pom.xml来配置对应信息就可以进行统一的帮我们管理整个项目的依赖关系和jar包。
Maven的坐标和依赖
Maven中的坐标
我们都知道生活意义上的坐标是什么?那么Maven中的坐标也是我们生活中的含义吗?答案是当然。
什么是Maven的坐标呢?
maven中的坐标是用来描述一个构件存在Maven仓库中的地址,也就是Maven是通过pom.xml文件中构件的坐标来去仓库中寻找的。只有我们提供了正确的坐标元素,Maven才能在仓库中找到对应的构件,所以我们在定义一个Maven项目时,就必须为该Maven项目定义一个项目坐标,去对外暴露地址。
Maven坐标的5个重要元素
maven坐标含有5个重要的属性:
- groudId
定义当前Maven项目隶属的实际项目,比如org.springframework.boot
,所以当一个项目的groudId是org.springframework.boot
,那么可以说明该项目可能隶属于SpringBoot项目,同时所下的构件肯定在org.springframework.boot
路径下 - artifactId
该元素定义该groupId项目的实际Maven项目是什么,我们也可以简单的当成是当前项目的项目名称,代号或是简称,比如spring-boot-starter-web
项目是org.springframework.boot
项目下的实际Maven项目 - version
version其实没什么好说的,就是当前Maven项目的版本号,毕竟一个项目可能存在很多个版本,你需要为其定义一个版本号,同时也为了让其他人可以找到或特定使用这个版本 - packaging
该元素定义了Maven项目的打包方式,比如jar,pom,war等 - classifier
该元素用帮助定义构建输出的一些附属构件,比如一些主构件可能带有附属构件,如javadoc,source等附属构件
注意:
上述5个元素中,groudId、artifactId、version是必须定义的,packaging是可选的,classfier是不可直接定义的
Maven的依赖
什么是Maven的依赖呢? 其实我们所说的依赖就是说,在没有Maven的阶段,我们的Java项目如果需要用到第三方的Jar包,我们必须去其官网下载,打包到我们项目目录中。而有了Maven,我们就不需要这么做了。而是通过在Maven项目的pom.xml
文件中配置需要的第三方Jar的坐标信息就可以了。而在pom.xml
文件中配置了第三方Jar的坐标信息的过程,我们就称为配置依赖,所以我们也可以简单的把该jar的配置信息或者说所需的第三方Jar包就称为当前Maven项目的依赖
简而言之,所谓依赖,就是我们依赖你嘛,我们的Maven项目需要某个第三方Jar包的支持,不就等于我们的项目依赖该第三方Jar包,Maven的依赖也就是这个一个意思
Maven的依赖配置和7个元素
那我们要怎么配置项目的依赖呢?这里举个例子:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.0.4.RELEASE</version>
</dependency>
</dependencies>
这就是一个Maven依赖,依赖所指向的Jar包就是spring-boot-starter-web-2.0.4.RELEASE.jar
我们从标签意思也能很容易的看出来,dependencies标签就是依赖集合的意思,该标签包的就是一个个的依赖
依赖配置的7的元素:
- groupId
没啥好说,所属项目,你也可以简单的当做是该项目的存放路径或是包 - artifactId
也没啥好说的,某项目的唯一标识,项目名&代号&简称等 - version
依赖的版本 - type
依赖的类型,对应项目坐标定义的packaging,一般不需要声明,默认为jar - scope
依赖范围 - optional
标记该依赖是否可选 - exclusions
用来排除传递性依赖
依赖范围
我们知道当我们定义依赖的的时候,有一个<scope>
标签,该标签的作用就是用来定义依赖范围。比如:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.0.4.RELEASE</version>
<scope>test</scope>
</dependency>
spring-boot-starter-test构件的依赖范围就是test,意思就是该依赖只有在项目进行测试的时候才会被依赖
这里又涉及到三种classpath
- 编译classpath
- 测试classpath
- 运行classpath
对于以上三种classpth,我其实也不是非常的理解,所以我个人认为这是maven提供了三种虚拟的classpath分别对应编译,测试,运行三个阶段。不同的scope会影响依赖在三个阶段的关系
Maven的6种依赖范围:
- compile
编译依赖范围,是默认的依赖范围,如果此依赖范围,该依赖对编译,测试,运行三种classpath都有效果,也及时该依赖在三个阶段都会被导入 - test
测试依赖范围,使用该依赖范围的依赖,只对测试classpath才会有效,就比如上面的spring-boot-starter-test
- provided
已提供依赖范围,使用该依赖范围则代表该依赖只对编译和测试两种classpath有效,在运行时无效,这是什么意思的。比如说servlet-api
,编译和测试项目时都需要该依赖,但是运行项目时则不需要了,因为运行容器已经提供有该依赖了,就不需要Maven重复引用了,所以称为已提供依赖范围 - runtime
运行时依赖范围,使用此依赖范围的依赖,对于测试和运行classpath才有效,在编译主代码时是无效的。比如JDBC的驱动,编译时只需要JDK提供的驱动接口,只要在执行测试和运行时才会用上具体的JDBC驱动 - system
和provide的功能一致,但是有区别,使用system依赖范围时,依赖必须通过systemPath元素显示地依赖文件的路径,具有本地局限性,会造成构建的不可移植性,谨慎使用 - import
导入依赖范围,这个依赖范围与其他依赖范围有些不一样,需要用在dependencyManagement
标签中,说明当前含有dependencyManagement的Maven项目将dependencyManagement标签内某个依赖导入进本项目中
传递性依赖
什么是传递性依赖?
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
就比如说假如我们有个Maven项目叫spring-demo
,它依赖spring-boot-starter-web
这个包,但我们打开spring-boot-starter-web
项目的pom文件,可以看到它还依赖其他的jar包,就如spring-boot-starter
和spring-boot-starter-json
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.0.4.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
<version>2.0.4.RELEASE</version>
<scope>compile</scope>
</dependency>
...
...
<dependencies>
所以我们就能说我们的项目spring-demo
直接依赖spring-boot-starter-web
。而spring-boot-starter
和spring-boot-starter-json
是spring-demo
的传递性依赖,也称间接依赖
为什么要有传递性依赖?
当我们需要一个第三方jar包时,我们引入了它的依赖,但这个第三方jar又依赖其他jar包怎么办,我们就要手动为它也依赖其依赖的jar包,这个操作非常的繁琐,甚至我还要查它依赖那些jar包。所以就引入了传递性依赖机制。我依赖什么,就直接导入什么,而不关系它又依赖什么。也可以称为是一种一站式服务吧。
传递性依赖的依赖范围会受影响:
如果直接依赖的依赖范围时runtime时,但间接依赖的范围时compile时,依赖范围有点冲突,那怎么办?窄化依赖范围,取交集,没有交集则该传递性依赖无效
属性 | compile | test | provided | runtime |
---|---|---|---|---|
compile | compile | 空 | 空 | runtime |
test | test | 空 | 空 | test |
provided | provided | 空 | provided | provided |
runtime | runtime | 空 | 空 | runtime |
传递调解
什么是传递调解?这是因为Maven引用了传递性依赖的机制,这虽然大大的化简了我们的依赖声明,但是也会造成一个依赖冲突。就比如说我有个项目X,这个项目依赖了A和B
- 但A和B都同时依赖了一个C。
- A依赖了C,B依赖了D,D又依赖了C,也就是说A直接依赖C,B间接依赖C。
对于上面两种情况,都说明因为传递性依赖机制,所以我的项目会间接依赖同样的jar包,但是这个jar包的依赖信息可能会不一致,比如版本不一致,依赖范围不同等等。那我的项目间接依赖C到底是根据A来选呢?还是根据B来选呢?
所以Maven又提供了传递调解机制,这样我们就能清楚的知道项目该引用那个传递性依赖:
- 路径最近者优先
- 路径相同,声明优先
比如说项目X的依赖关系是 :(1)X -> A -> C ,(2)X -> B -> D -> C。 我们知道(1)的依赖路劲为2,(2)的依赖路劲为3,所以根据依赖调解原则,路劲最近者优先,所以我们的项目X对C的依赖取用(1)方案。
又比如说项目X的依赖关系是:(1)X -> A -> C ,(2)X -> B -> C,此时的(1)和(2)对C的依赖路劲大小都是2,那么怎么办呢?此时根据依赖调解原则,路径相同,看谁先声明,所以我们就看直接依赖A和B谁先声明,我们就用谁的传递依赖。
可选依赖
简单点说的话,就是我有两个项目A和B,项目A依赖项目B,项目B中有slf4j
的依赖包,是可选的依赖,即<optional>true</optional>
。那么这样的话,slf4j是不会成为项目A的传递性依赖的。因为slf4j是可选依赖。如果你想让它成为项目A的传递依赖。只需要将optional项目去掉或改为false
排除依赖
排除依赖也很好理解,平时我们看已有的Maven项目时,也常常能看到<exclusion>
,他就是排除依赖的标签。
他的作用是什么呢 ?就比如说我们有一个项目X,它有两个依赖A和B,A和B都有一个slf4j的依赖,版本是SNAPSHOT不稳定版。所以我不想用他们的传递性依赖,想自己引用一个稳定版本。所以我们就需要在项目X的pom.xml文件中引用A和B依赖时,添加<exclusion>
标签。总之就是用来排除不想引用的传递性依赖
使用示例:
<dependencies>
<dependency>
<groudId>com.test.A</groudId>
<artifactId>project-A</artifactId>
<version>1.0.0</version>
<exclusions>
<exclusion>
<groudId>com.test.slf4j</groudId>
<artifactId>slf4j</artifactId>
</exclusion>
</exclusions>
</dependencies>
需要注意的是: 声明<exclusion>
的时候,记得先声明<exclusions>
.同时声明<exclusion>
是不需要声明<version>
的,只需要声明groudId和artifactId即可
Maven的仓库
Maven的生命周期和插件
除了坐标、依赖和仓库之外,Maven还有两个重要的概念就是生命周期和插件。
什么是Maven的生命周期
在Maven出行之前,项目构建的生命周期就存在了。例如软件开发人员每天在对项目进行清理,编译,测试,部署就是一个项目构建的过程,也就是项目构建的生命周期。那么什么是Maven的生命周期呢? Maven的生命周期也类似,有清理,测试,编译,打包,部署等等阶段,是一个抽象的概念。
简而言之,Maven的生命周期就是对项目进行构建过程,从什么阶段开始,到什么时候结束。
Maven的三套生命周期
Maven的生命周期不是一个完整的整体,在没看书之前,我也以为是一个整体,但并不是,实际上Maven有三套完整而互相独立的生命周期
- clean生命周期
- default生命周期
- site生命周期
clean生命周期
clean生命周期的目的是清理项目,它包括三个阶段
- pre-clean //执行一项清理前需要完成的工作
- clean //清理上一次构建生成的文件
- post-clean //执行一些清理后需要完成的工作
default生命周期
default生命周期就复杂了,它定义了真正项目构建时所需要执行的所有步骤,它是三套生命周期中的核心
我们常用的有compile->test-compile->test->package->install->deploy
- validate
- initialize
- generate-sources
- process-sources //处理项目主资源文件,对src/main/resources目录的内容复制到项目输出的目录下
- generate-resources
- process-resources
- compile //编译项目的主源码,编译src/main/jaav目录下的Java文件到项目输出目录下
- process-classes
- generate-test-sources
- process-test-sources //处理项目测试资源文件
- generate-test-resources
- process-test-resources
- test-compile
- process-test-classes
- test //使用单元测试框架运行测试,测试代码不会被打包或部署
- prepare-package
- package //接受编译好的代码,打包成可发布的格式,如JAR
- pre-integration-test
- integration-test
- post-integration-test
- verify
- install //将包安装到Maven本地仓库中
- deploy //将包复制远程仓库中
site生命周期
site生命周期,其实我实际上没接触到。site生命周期的目的是建立和发布项目的站点,Maven能够基于POM所包含的信息,自动生成一个友好的站点,方便团队交流和发布项目信息。该生命周期包含如下阶段:
- pre-site //执行一项在生成项目站点之前需要完成的工作
- site //生成项目站点文档
- post-site //执行一些在生成项目站点之后需要完成的工作
- site-deploy //将生成的站点发布到服务器上
生命周期和命令行的关系
从命令行执行Maven任务的最主要的方式就是调用Maven的生命周期阶段。也就是说,命令行调用的是抽象的maven生命周期阶段。还有一个重要的特性是,虽然各个生命周期是相互独立的,但每个生命周期的阶段是有前后依赖关系的。比如
- mvn clean
该命令调用的是clean生命周期的clean阶段。实际执行的阶段是pre-clean->clean - mvn test
该命令调用的是default生命周期的test阶段,实际执行的是default生命周期从validate,initialize到test的所有阶段 - mvn clean deploy site-deploy
该命令调用的是三个生命周期的三个阶段,实际执行的是clean生命周期的pre-clean到clean阶段,default生命周期的所有阶段,site生命周期的所有阶段
小结一下,就是说
- mvn命令调用的是抽象的生命周期阶段
- 只要你执行某个生命周期的某个阶段,那么它就必须从哪个生命周期的起始阶段执行到要执行的哪个阶段
- mvn命令空开的参数代表一个生命周期的一个阶段
插件目标Goal
前面一直在讲生命周期,现在我们来讲一个插件,尤其是插件的goal
在进一步讲插件和生命周期的绑定关系之前,必须先了解一下插件的目标(plugin goal)的概念 ; 我们知道,Maven的核心仅仅定义了抽象的生命周期,具体的任务是交给插件完成的,插件以独立的构件形式存在,因此Maven核心的分发包只有不要3mb的大小,maven会在需要的时候下载并使用插件。可以这么说maven就是一个插件集合
什么是目标(goal)?
每个插件都有很多个目标,每个目标对应一个功能,就比如说maven-dependency-plugin
有十几个目标,分别对应十几个该插件的功能,比如dependency:analyze
,dependency:tree
等等。当我们直接执行mvn dependency:tree
就是执行的是某个插件具体的目标,也就是功能。
通用表达方式:
mvn dependency:tree
命令执行时的maven-dependency-plugin
的tree
目标。插件名前缀:目标是一个通用的表达方式。如果你问什么是插件前缀,那你就自己度娘吧。
小结:
- 当我们执行mvn clean时,调用的是mvn的clean生命周期clean阶段。当我们执行的是mvn clean:clean时,执行时是具体的插件目标
- goal就是插件的具体的某个功能
- maven就是将插件的goal一一对应到maven的生命周期中,来完成生命周期的实现的
生命周期和插件的关系
当我们了解了生命周期之后,我们就会知道生命周期只是Maven提供的一个抽象的概念,它实际上不会做些什么。那么谁去做生命周期代表的事情呢?那就是插件
比如我们执行mvn clean
命令时,他代表着clean生命周期的clean阶段。具体做这个clean阶段的事情的工具就是maven-clean-plugin
插件。当然还有很多插件maven-jar-plugin
等等。默认情况下,maven生命周期的阶段都有内置绑定,即都有对应一个或多个插件去实现(也或者是一个插件的多个goal)。我们可以把生命周期和插件的关系比喻成,将某个插件的某个行为绑定到Maven的某个生命周期中,当执行maven的某个生命周期时,被绑定在该阶段的行为就会被触发
插件绑定
Maven的生命周期和插件互相绑定,用以完成实际的构建任务。具体而言,是生命周期的阶段和插件的目标相互绑定,以完成某个具体的任务构建。
例如编译这项任务,它对应了default生命周期的compile阶段,而maven-compiler-plugin这一插件的compile目标能够完成该任务。因为将他们绑定就能完成该任务。
Maven的聚合和继承关系
<modules>
聚合,用于项目构建
<parent>
继承,用于继承依赖
//TODO 有时间补上
参考资料
- 《Maven实战》