工具——Maven

331 阅读14分钟

Maven 是最流行的 Java 项目构建系统,Maven项目对象模型(POM),可以通过一小段描述信息来管理项目的构建,报告和文档的软件项目管理工具。

Maven简介

        Maven项目的核心是pom. xmI。POM( Project Object Model,项目对象模型)定义了项目的基本信息,用于描述项目如何构建,声明项目依赖,等等。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.3.6.RELEASE</version>

     代码的第一行是xml头,指定了该xml文档的版本和编码方式。紧接着是 project元素, project是所有 pom. xml的根元素,它还声明了一些 POM 相关的命名空间及 xsd 元素。 根元素下的第一个子元素 modelversion 指定了当前 POM 模型的版本,对于 Maven2 及 Maven3来说,它只能是4.0.0。

         最重要的是包含 grouped、 artifact 和 version 的三行。这三个元素定义了一个项目基本的坐标,在 Maven 的世界,任何的 jar、pom 或者war 都是以基于这些基本的坐标进行区分的。

坐标

      在一个平面坐标系中,坐标(x,y)表示该平面上与x轴距离为y,与y轴距离为x的一点.任何一个坐标都能够唯一标识该平面中的一点。 在实际生活中,我们也可以将地址看成是一种坐标。省、市、区、街道等一系列信息同样可以唯一标识城市中的任一居住地址和工作地址。

        Maven坐标是通过一些元素定义的,它们是 groupId、 artifactId、 version、 packaging、classifier。先看一组坐标定义,如下:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
	<version>2.3.6.RELEASE</version>
</dependency>
  • groupId:定义当前 Maven项目隶属的实际项目。
  • artifactId:该元素定义实际项目中的一个 Maven项目(模块),推荐的做法是使用实际项目名称作为 artifact 的前缀。
  • version:该元素定义 Maven项目当前所处的版本。Maven 定义了一套完成的版本规范,以及快照( SNAPSHOT)的概念。
  • packaging:该元素定义 Maven项目的打包方式。打包方式通常与所生成构件的文件扩展名对应,当不定义 packaging 的时候,Maven 会使用默认值jar。
  • classifier:该元素用来帮助定义构建输出的一些附属构件。如 spring-boot-starter-web-2.3.6.RELEASE-javadoc.jar、 spring-boot-starter-web-2.3.6.RELEASE-sources.jar 这样一些附属构件,其包含了文档和源代码。 javadoc和 sources就是这两个附属构件的classifier。

      上述5个元素中, groupId、 artifact、 version是必须定义的,packaging 是可选的(默认为jar),而 classifier 是不能直接定义的。 

       项目构件的文件名是与坐标相对应的,一般的规则为 artifactId-version [ -classifier]. packaging,[- classifier]表示可选。

依赖

       根元素 project 下的 dependencies 可以包含一个或者多个 dependency 元素,以声明一个或者多个项目依赖。每个依赖可以包含的元素有:

  • groupId、 artifactId、 version :依赖的基本坐标,Maven根据坐标才能找到需要的依赖。 
  • type:依赖的类型,对应于项目坐标定义的 packaging。大部分情况下,该元素不必声明,其默认值为jar
  • scope:依赖的范围。 
  • optional:标记依赖是否可选
  • exclusions:用来排除传递性依赖

依赖范围

       Maven在编译项目主代码的时候需要使用一套 classpath。其次, Maven在编译和执行测试的时候会使用另外一套 classpath 。比如 JUnit,该文件也以依赖的方式引入到测试使用的 classpath 中,不同的是这里的依赖范围是 test 。最后,实际运行 Maven 项目的时候,又会使用一套  classpath。

       依赖范围就是用来控制依赖与这三种  classpath (编译 classpath、测试 classpath、运行classpath )的关系, Maven有以下几种依赖范围:

compile:编译依赖范围。如果没有指定,就会默认使用该依赖范围。使用此依赖范围的 Maven依赖,对于编译、测试、运行三种 classpath都有效。

test:测试依赖范围。使用此依赖范围的 Maven依赖,只对于测试 classpath有效,在编译主代码或者运行项目的使用时将无法使用此类依赖。典型的例子是 JUnit,它只有在编译测试代码及运行测试的时候才需要。 

provided:已提供依赖范围。使用此依赖范围的maven依赖,,对于编译和测试classpath有效,但在运行时无效。典型的例子是 servlet-api,编译和测试项目的时候需要该依赖,但在运行项目的时候,由于容器已经提供,就不需要 Maven重复地引入遍。 

runtime:运行时依赖范围。使用此依赖范围的 Maven依赖,对于测试和运行classpath有效,但在编译主代码时无效。典型的例子是JDBC驱动实现,项目主代码的编译只需要JDK提供的JDBC接口,只有在执行测试或者运行项目的时候才需要实现上述接口的具体JDBC驱动。

system:系统依赖范围。该依赖与三种 classpath的关系,和 provided依赖范围完全一致。但是,使用 system 范围的依赖时必须通过 systemPath 元素显式地指定依赖文件的路径。由于此类依赖不是通过 Maven 仓库解析的,而且往往与本机系统绑定,可能造成构建的不可移植,因此应该谨慎使用。 systemPath元素可以引用环境变量。

import:导入依赖范围。该依赖范围不会对三种 classpath产生实际的影响。

传递性依赖

        传递性依赖机制就是Maven会解析各个直接依赖的POM,将那些必要的间接依赖,以传递性依赖的形式引人到当前的项目中。

       依赖范围不仅可以控制依赖与三种  classpath 的关系,还对传递性依赖产生影响。假设A依赖于B,B依赖于C,我们说A对于B是第一直接依赖,B对于C是第二直接依赖,A对于C是传递性依赖。

       第一直接依赖的范围和第二直接依赖的范围决定了传递性依赖的范围,如下所示,最左边一行表示第一直接依赖范围,最上面一行表示第二直接依赖范围,中间的交叉单元格则表示传递性依赖范围。

        当第二直接依赖的范围是 compile的时候,传递性依赖的范围与第一直接依赖的范围一致;当第二直接依赖的范围是test的时候依赖不会得以传递;当第二直接依赖的范围是 provided 的时候,只传递第一直接依赖范围也为 provided的依赖,且传递性依赖的范围同样为 provided;当第二直接依赖的范围是runtine的时候,传递性依赖的范围与第一直接依赖的范围一致,但 compile例外,此时传递性依赖的范围为 runtime。

依赖调解

       项目A有这样的依赖关系:A->B->C->X(1.0)、A->D->X(2.0),X是A的传递性依赖,但是两条依赖路径上有两个版本的X,那么哪个X会被 Maven解析使用呢?

        Maven依赖调解( Dependency Mediation)的第一原则是:路径最近者优先。该例中X(1.0)的路径长度为3,而X(2.0)的路径长度为2,因此X(2.0)会被解析使用。 

       Maven定义了依赖调解的第二原则:第一声明者优先。在依赖路径长度相等的前提下,在POM中依赖声明的顺序决定了谁会被解析使用,顺序最靠前的那个依赖优胜。

可选依赖

      假设有这样一个依赖关系,项目A依赖于项目B,项目B依赖于项目Ⅹ和Y,B对于Ⅹ和Y的依赖都是可选依赖:A->B、B->(可选)、B->Y(可选)。根据传递性依赖的定义,如果所有这三个依赖的范围都是 compile,那么X、Y就是A的 compile范围传递性依赖。然而,由于这里Ⅹ、Y是可选依赖,依赖将不会得以传递,Ⅹ、Y将不会有任何影响。 

        在理想的情况下,是不应该使用可选依赖的。 使用可选依赖的原因是某一个项目实现了多个特性,在面向对象设计中,有个单一职责性原则,意指一个类应该只有一项职责,而不是糅合太多的功能。这个原则在规划 Maven项目的时候也同样适用。

排除依赖

      项目A依赖于项目B,但是由于一些原因,不想引人传递性依赖C,而是自己显式地声明对于项目C版本的依赖。代码中使用 exclusions元素声明排除依赖,exclusions 可以包含一个或者多个 exclusion子元素,因此可以排除一个或者多个传递性依赖。声明 exclusion 的时候只需要 groupId 和 artifactId 能唯一定位依赖图中的某个依赖。

Maven仓库

       在 Maven中,任何一个依赖、插件一组坐标唯一标识。在此基础上, Maven可以在某个位置统一存储所有 Maven项目共享的构件,这个统一的位置就是仓库。在Maven项目种只需要声明这些依赖的坐标,在需要的时候(例如,编译项目的时候需要将依赖加入到 classpath中), Maven会自动根据坐标找到仓库中的构件,并使用它们。 为了实现重用,项目构建完毕后生成的构件也可以安装或者部署到仓库中,供其他项目使用。

      任何一个构件都有其唯一的坐标,根据这个坐标可以定义其在仓库中的唯一存储路径,这便是 Maven的仓库布局方式。

仓库的分类

       对于 Maven来说,仓库只分为两类:本地仓库和远程仓库。当maven根据坐标寻找构件的时候,它首先会查看本地仓库,如果本地仓库存在此构件,则直接使用;如果本地仓库不存在此构件,或者需要査看是否有更新的构件版本,Maven就会去远程仓库査找,发现需要的构件之后,下载到本地仓库再使用。如果本地仓库和远程仓库都没有需要的构件,Maven就会报错。 

       中央仓库是 Maven核心自带的远程仓库,它包含了绝大部分开源的构件。在默认配置下,当本地仓库没有Maven需要的构件的时候,它就会尝试从中央仓库下载。

       私服是另一种特殊的远程仓库,在局域网内架设一个私有的仓库服务器,用其代理所有外部的远程仓库。内部的项日还能部署到私服上供其他项日使用。

远程仓库的配置

        在 repositories 元素下,可以使用 repository 子元素声明一个或者多个远程仓库。任何一个仓库声明的id必须是唯一的,Maven自带的中央仓库使用的id为 central,如果其他的仓库声明也使用该id,就会覆盖中央仓库的配置。该配置中的ur值指向了仓库的地址,一般来说,该地址都基于 http 协议, Maven 用户都可以在浏览器中打开仓库地址浏览构件。 

      配置中的 releases和 snapshots元素用来控制 Maven对于发布版构件和快照版构件的下载。

     元素 updatePolicy 用来配置 Maven从远程仓库检查更新的频率,默认的值是 daily,表示Maven每天检查一次。其他可用的值包括:never一从不检查更新; always一每次构建都检查更新; interval:X一每隔X分钟检查一次更新(X为任意整数)。

<repositories>
    <repository>
        <id>company</id>
        <name>Company Repository</name>
        <url>https://repository.jboss.org/nexus/content/groups/public/</url>
        <releases>
		 <enabled>true</enabled>
		 <updatePolicy>always</updatePolicy>
	 </releases>
	 <snapshots>
		 <enabled>false</enabled>
		 <updatePolicy>always</updatePolicy>
	 </snapshots>
    </repository>
</repositories>

镜像

       如果仓库Ⅹ可以提供仓库Y存储的所有内容,那么就可以认为X是Y的一个镜像。换句话说,任何一个可以从仓库Y获得的构件,都能够从它的镜像中获取。可以配置 Maven使用该镜像来替代中央仓库。

<mirror>
	<id>alimaven</id>
	<name>aliyun maven</name>
	<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
	<mirrorOf>central</mirrorOf>
</mirror>

      < mirrorOf>的值为 central ,表示该配置为中央仓库的镜像,任何对于中央仓库的请求都会转至该镜像,,用户也可以使用同样的方法配置其他仓库的镜像。另外三个元素id、name、url与一般仓库配置无异,表示该镜像仓库的唯一标识符、名称以及地址。

       为了满足一些复杂的需求, Maven还支持更高级的镜像配置: 

  • < mirrorOf>*</ mirrorOf>:匹配所有远程仓库。 
  • < mirrorOf> external:*</ mirrorOf>:匹配所有远程仓库,匹配所有不在本机上的远程仓库。 
  • < mirrorOf> repo1,repo2</ mirrorOf>:匹配仓库 repo1和repo2,使用逗号分隔多个远程仓库。 
  • < mirrorOf>*,! repo1</ mirrorOf>:匹配所有远程仓库, repo1除外,使用感叹号将仓库从匹配中排除。 

      由于镜像仓库完全屏蔽了被镜像仓库,当镜像仓库不稳定或者停止服务的时候, Maven仍将无法访问被镜像仓库,因而将无法下载构件。

聚合与继承

聚合

      一个简单的需求:我们会想要一次构建两个项目,而不是到两个模块的日录下分别执行mvn命令。 Maven聚合(或者称为多模块)这一特性就是为该需求服务的。

<modules>
    <module>A</module>
    <module>B</module>
</modules>

       对于聚合模块来说,其打包方式 packaging的值必须为pom,否则就无法构建。     

<packaging>pom</packaging>

        Maven会首先解析聚合模块的POM、分析要构建的模块、并计算出一个反应堆构建顺序( Reactor build order),然后根据这个顺序依次构建各个模块。反应堆是所有模块组成的一个构建结构。

继承

        使用 Maven的聚合特性通过一条命令同时构建 A 和 B 两个模块,不过这仅仅解决了多模块 Maven项目的一个问题。在这两个POM有着很多相同的配置,例如它们有相同的 groupId和 version,有相同的 SpringBoot,还有相同的 spring-boot-maven-plugin 插件配置。在Java中,可以使用类继承在一定程度上消除重复,在 Maven的中,也有类似的机制抽取出重复的配置,这就是POM的继承。

      与聚合模块一样,作为父模块的POM,其打包类型也必须为pom。 由于父模块只是为了帮助消除配置的重复,因此它本身不包含除POM之外的项目文件,也就不需要src/main/java/之类的文件夹了有了父模块,就需要让其他模块来继承它。

<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>2.3.6.RELEASE</version>
        <relativePath>../B/pom.xml</relativePath>
</parent>

        POM中使用 parent元素声明父模块,parent下的子元素 artifactId、 artifact和version指定了父模块的坐标,这三个元素是必须的。元素 relativePath 表示父模块POM的相对路径,当项目构建时, Maven会首先根据 relativePath 检查父POM,如果找不到,再从本地仓库査找。 relativePath 的默认值是 ./pom.xml,也就是说, Maven默认父POM在上一层目录下。

依赖管理

       Maven提供的    元素既能让子模块继承到父模块的依赖配置,又能保证子模块依赖使用的灵活性。在 dependencyManagement 元素下的依赖声明不会引人实际的依赖,不过它能够约束 dependencies下的依赖使用。

<dependencyManagement>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-dependencies</artifactId>
			<version>2.3.6.RELEASE</version>
			<type>pom</type>
			<scope>import</scope>
		</dependency>
</dependencyManagement>		

       依赖范围 Import ,只在 dependency Management元素下才有效果,使用该范围的依赖通常指向一个POM,作用是将目标POM中的 dependency Management配置导入并合并到当前POM的 dependency Management元素中。       

       依赖的type值为pom,一般都是指向打包类型为pom的模块。如果有多个项目,它们使用的依赖版本都是一致的,则就可以定义一个使用 dependency Management专门管理依赖的POM,然后在各个项目中导人这些依赖管理配置。

参考

Maven实战