一、maven的两个作用
- 项目自动化构建,通过命令行就可以完成整个项目构建过程,不需要我们手动地进行项目构建
- 管理项目依赖和jar包
以上两个就是maven的主要功能:项目依赖管理和项目构建管理。
所以Maven是一个功能强大的构建工具,可以帮助我们自动化构成过程,从清理、编译、测试到生成报告,再到打包部署。同时Maven也是一个依赖管理和项目信息管理的工具,它能够将我们需要依赖的包通过pom.xml来配置对应信息就可以进行统一的帮我们管理整个项目的依赖关系和jar包。
二、项目构建
有人认为;从建立项目、编写代码到测试、打包、再到部署的整个流程及时项目构建的过程。
自动化构建:
举个例子,我今天从git上pull下了代码,开始编码工作,解决bug,编写测试代码,跑测试用例,解决问题,打包,部署。发现问题,重新修改代码,测试,打包,部署。
项目构建的整一个过程是非常繁琐的,可能一天下来,我们很多时间都花在打包,测试,部署身上了。真正专注在解决业务代码需求的时候就少了很多。所以我们此时就会想有没有东西可以帮我们一条龙的解决掉,而不需要程序员自己总是手动的去构建项目。
所谓自动化构建就是一个脚本或一个工具,帮你解决总是要手动构建的问题。
三、maven的坐标和依赖
3.1 坐标
maven中的坐标是用来描述一个构件存在maven仓库中的地址,也就是maven是通过pom.xml文件中构件的坐标去仓库中寻找的。只有我们提供了正确的坐标元素,maven才能在仓库中找到对应的构件,所以我们在定义一个maven项目时,就必须为该maven项目定义一个项目坐标,去对外暴露地址。
3.2、maven坐标的5个重要元素
maven坐标含有5个重要的属性:
- groupid:定义当前maven项目隶属的实际项目
- artifactid:该元素定义该groupid项目的实际maven项目时什么,我们也可以简单的当成是当前项目的项目名称。
- version:即当前maven项目的版本号,一个项目可能存在很多个版本,需要为其定义一个版本号,同时也可以为了让其他人可以找到或者特定使用这个版本。
- packing:该元素定义了maven项目的打包方式,比如jar,pom,war等
- classifier:该元素帮助定义构建输出的一些附属构件,比如一些主构件可能带有附属构件,如javadoc,source等附属构件
注:上述5个元素中,groupid、artifactid、version是必须定义的,packaging是可选的,classifier是不可直接定义的
3.3、maven的依赖
在没有maven的阶段,我们的java项目如果需要用到第三方的jar包,我们必须去其官网下载,打包到我们项目目录中。而有了maven,我们就不需要这么做了。而是通过在maven项目的pom.xml文件中配置需要的第三方jar的坐标信息就可以了。而在pom.xml文件中配置了第三方jar的坐标信息的过程,我们就称为配置依赖,所以我们也可以简单的把该jar的配置信息或者说所需的第三方jar包就称为当前maven项目的依赖。
3.4maven的依赖配置和七个元素
那我们要怎么配置项目的依赖呢?
<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:用来排除传递性依赖
3.5依赖范围
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标签内某个依赖导入进本项目中
3.6、传递性依赖
为什么要有传递性依赖?
当我们需要一个第三方jar包时,我们引入了它的依赖,但这个第三方jar又依赖其他jar包怎么办,我们就要手动为它也依赖其依赖的jar包,这个操作非常的繁琐,甚至我还要查它依赖那些jar包。所以就引入了传递性依赖机制。我依赖什么,就直接导入什么,而不关系它又依赖什么。也可以称为是一种一站式服务吧。
传递性依赖的依赖范围会受影响:
如果直接依赖的依赖范围时runtime时,但间接依赖的范围时compile时,依赖范围有点冲突,那怎么办?窄化依赖范围,取交集,没有交集则该传递性依赖无效。
3.7依赖调节
这是因为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谁先声明,我们就用谁的传递依赖。
3.8可选依赖
简单点说的话,就是我有两个项目A和B,项目A依赖项目B,项目B中有slf4j的依赖包,是可选的依赖,即<optional>true</optional>。那么这样的话,slf4j是不会成为项目A的传递性依赖的。因为slf4j是可选依赖。如果你想让它成为项目A的传递依赖。只需要将optional项目去掉或改为false
3.9排除依赖
排除依赖也很好理解,平时我们看已有的Maven项目时,也常常能看到<exclusion>,他就是排除依赖的标签。
他的作用是什么呢?就比如说我们有一个项目X,它有两个依赖A和B,A和B都有一个slf4j的依赖,版本是SNAPSHOT不稳定版。所以我不想用他们的传递性依赖,想自己引用一个稳定版本。所以我们就需要在项目X的pom.xml文件中引用A和B依赖时,添加<exclusion>标签。总之就是用来排除不想引用的传递性依赖
四、maven的仓库
4.1什么是仓库
maven仓库就是一个存放jar文件的地方,所有的maven项目可以从同一个maven仓库中获取自己所需要的依赖jar包,这节省了磁盘资源也方便了管理。每一个jar包文件都有自己的坐标,我们在pom根据坐标就可以从仓库中下载对应的jar包了。
4,2仓库的分类
- 中央仓库:就是maven为全世界程序员免费提供的公共远程仓库,是默认的仓库,不需要配置,坐标id为central。
- 本地仓库,可以在setting.xml中配置。
五、maven的生命周期和插件
5.1什么是maven的生命周期
在maven之前,项目构建的生命周期就存在了。例如软件开发人员每天在对项目进行清理,编译,测试,部署就是一个项目构建的过程,也就是项目构建的生命周期。那么什么是maven的生命周期呢?maven的生命周期也类似,有清理、测试、编译、打包、部署等等阶段,是一个抽象的概念。
简而言之,maven的生命周期就是对项目进行构建过程,从生命阶段开始,到什么时候结束。
实际上maven有三套完整而相互独立的生命周期:
clean生命周期,default生命周期,site生命周期。
5.2clean生命周期
clean生命周期的目的是清理项目,它包括三个阶段
- pre-clean 执行一些清理前需要完成的工作
- clean 清理上一次构建生成的文件
- post-clean 执行一些清理后需要完成的工作
5.3 default生命周期
default生命周期定义了真正项目构建时所需要执行的所有步骤,它是三套生命周期中的核心。我们常用的有complile,test-complile,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 //将包复制远程仓库中
5.4site生命周期
site生命周期的目的是建立和发布项目的站点,Maven能够基于POM所包含的信息,自动生成一个友好的站点,方便团队交流和发布项目信息。该生命周期包含如下阶段:
- pre-site //执行一项在生成项目站点之前需要完成的工作
- site //生成项目站点文档
- post-site //执行一些在生成项目站点之后需要完成的工作
- site-deploy //将生成的站点发布到服务器上
5.5生命周期和命令行的关系
从命令行执行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命令空开的参数代表一个生命周期的一个阶段
5.6插件目标goal
maven的核心仅仅定义了抽象的生命周期,具体的任务是交给插件完成的,插件以独立的构件形式存在,因此maven核心的分发包只有不要3mb的大小,maven会在需要的时候下载并使用插件。
每个插件都有很多个目标,每个目标对应一个功能,就比如说maven-dependency-plugin有十几个目标,分别对应十几个该插件的功能,比如dependency:analyze,dependency:tree等等。当我们直接执行mvn dependency:tree就是执行的是某个插件具体的目标,也就是功能。
通用表达方式
mvn dependency:tree命令执行时的maven-dependency-plugin的tree目标。插件名前缀:目标是一个通用的表达方式。
5.7生命周期和插件的关系
当我们了解了生命周期之后,我们就会知道生命周期只是maven提供的一个抽象的概念,它实际上不会做些什么,那么谁去做生命周期代表的事情呢?就是插件。
比如我们执行mvn clean命令时,他代表着clean生命周期的clean阶段。具体做这个clean阶段的事情的工具就是maven-clean-plugin插件。当然还有很多插件maven-jar-plugin等等。默认情况下,maven生命周期的阶段都有内置绑定,即都有对应一个或多个插件去实现(也或者是一个插件的多个goal)。我们可以把生命周期和插件的关系比喻成,将某个插件的某个行为绑定到Maven的某个生命周期中,当执行maven的某个生命周期时,被绑定在该阶段的行为就会被触发。
插件绑定
Maven的生命周期和插件互相绑定,用以完成实际的构建任务。具体而言,是生命周期的阶段和插件的目标相互绑定,以完成某个具体的任务构建。
例如编译这项任务,它对应了default生命周期的compile阶段,而maven-compiler-plugin这一插件的compile目标能够完成该任务。因为将他们绑定就能完成该任务。
六、maven的聚合和继承关系
6.1继承
为何需要继承?
我们知道Maven工程之间可以完成依赖的传递性,实际上就是各个jar包和war包之间存在依赖的传递性,但是必须是compile范围的依赖才具有传递性,才可以根据传递性统一的管理一个依赖的版本。而对于test范围的依赖,只是孤零零的存在于某个项目中,各个项目中的依赖版本可能不同,容易造成问题,所以test范围的依赖的统一版本的问题依靠依赖的传递性是无法解决的。所以我们使用继承这个概念来解决。
我们在上一章 Maven 依赖 中提到了依赖的范围概念,了解到junit 依赖是test范围的依赖,是不可以传递的,因此在多模块项目中我们在每个模块都需要依赖。那么问题来了既然是多模块那就是不同的组甚至是不同的部门来开发,junit依赖的version 很有可能就会不一样,但是为了项目后期维护以及项目人员调度尽可能的方便,我们需要将junit的版本设为统一的。所以就需要有一个机制来给统一这个标准,maven提供了一种机制就是 继承。
如何实现继承?
继承,顾名思义,存在着父子关系。我们需要定义一个统一管理某些test范围依赖的父工程,它打包的方式是pom。在它的pom文件中声明test范围的依赖的坐标。然后在子工程中第一以<parent>坐标</parent>的形式声明父工程的坐标并且声明test范围的依赖坐标,子工程中的也要声明test范围的依赖的坐标,但是需要注意的是必须将坐标中的版本号给去掉,才可以完成统一管理test范围依赖的版本。
62.聚合
为何需要聚合
我们最终都要讲各个Maven工程安装到仓库中,但是由于存在继承关系使得我们必选先安装父工程才可以安装子工程,否则会报错。而且必须一个一个的install。那么能不能有一种更好的方式完成一键安装呢?聚合工程就可以完成。
如何实现聚合?
我们首先要定义一个打包方式为pom的工程当做聚合工程,并且在其中用<models><model></model></models>标签的形式将一个一个Maven工程聚合进来,不必在意在model中的顺序,它会自动识别父工程来先完成安装。然后只有将这个聚合工程install那么其中聚合进来的工程就都可以顺利的install了。
需要注意的是:在实际项目开发工程中,我们可以使用同一个pom打包方式的工程来充当父工程和聚合工程。即效果是其中的pom.xml文件包含test范围的依赖和models标签将各个Maven工程聚合进来。