Java服务端研发知识图谱
上QQ阅读APP看书,第一时间看更新

第2章 Maven

在Java的世界中,依赖管理是不得不面对的问题。无论是外部的开源类库依赖,还是项目内部的模块间依赖,都需要进行依赖管理。可以说依赖管理是持续集成的核心内容之一。Maven抽象定义了一个软件的完整生命周期,遵循这个模型,可轻松地管理自己的软件项目,避免不必要的学习成本,并促进软件项目管理的标准化、流程化。

2.1 Maven安装和配置

从本节开始实际操作Maven。首先介绍如何在Windows系统中安装Maven以及Maven的基本配置。

2.1.1 Maven环境的搭建

Maven的基础环境简单来说就是下载对应的Maven包,解压到本机,配置对应的环境 变量。

(1)检查JDK环境是否配置成功,在安装Maven之前,需要检查当前JDK基础环境是否配置正确,Maven3.3+需要JDK 7及以上版本。在Windows的命令行输入java-version,运行命令来检查JDK的版本,如图2-1所示。

978-7-111-61011-3-Part01-122.jpg

图2-1 Java版本

(2)首先通过官网https://maven.apache.org/download.cgi下载对应的Maven版本,在编写本书的时候,Maven的最新版本为3.5.2,这里下载apache-maven-3.5.2,解压到常用软件安装目录,然后进行环境变量的配置。在计算机中找到设置环境变量的地方,添加对应的变量名和值,见表2-1。

2-1 环境变量配置

978-7-111-61011-3-Part01-123.jpg

通过以上两步就配置好了Maven基础环境。在命令行输入mvn-v,观察输出结果,会看到Maven的路径等信息,表明Maven已正确安装,如图2-2所示。

978-7-111-61011-3-Part01-124.jpg

图2-2 Maven版本

2.1.2 在Eclipse中配置Maven的settings文件

打开Eclipse工具,在菜单栏选择“Window->Preferences”,在弹出的选项卡中左边选择Maven选项的“User Settings”,然后在右边的全局配置(Global Settings)中可以指定自己的settings.xml文件,如图2-3所示。

检测配置是否成功:打开Eclipse的“Window->Show View->Other”,然后选择“Maven-> Maven Repositories”,打开Maven仓库,会出现Local Repositories仓库,在此仓库下可以看到在setting.xml中配置过的仓库,至此表示Maven配置成功,如图2-4所示。

978-7-111-61011-3-Part01-125.jpg

图2-3 Maven设置

978-7-111-61011-3-Part01-126.jpg

图2-4 Maven仓库

2.2 Maven使用

Maven基础环境准备妥当后,创建一个简单的Hello World项目。本章会一步一步地编写代码并输出结果。

2.2.1 在Eclipse中创建第一个Maven项目

在Eclipse中选择“File->New->Maven Project”,在弹出的tab页面中直接点击Next按钮,在第二个tab页面中保持默认的Archetype[27](maven-archetype-quickstart)设置即可,点击Next按钮,在第三个tab页面中输入信息,见表2-2,然后点击Finish按钮即可。

2-2 项目配置

978-7-111-61011-3-Part01-127.jpg

常见的Maven项目代码结构如图2-5所示。

Maven项目的目录属性一般如下。

■pom.xml:用于Maven的配置文件。

■/src:源代码目录。

■/src/main:工程源代码目录。

■/src/main/java:放置项目Java源代码目录。

978-7-111-61011-3-Part01-128.jpg

图2-5 项目结构

■/src/main/resources[28]:放置项目的资源文件目录。

■/src/test:单元测试目录。

■/src/test/java:工程测试Java代码目录。

■/target:输出目录,项目输出存放在此目录中。

2.2.2 认识pom文件

Maven项目的核心是pom.xml文件,此文件包含项目的基本信息、包依赖、项目构建等信息。下面为常用pom文件的基本内容。

978-7-111-61011-3-Part01-129.jpg

第一行代码是XML头,然后是project元素,project是pom.xml的根元素,同时还声明了pom命名空间以及xsd元素。

pom文件里面的常见元素以及含义见表2-3。

2-3 pom元素含义

978-7-111-61011-3-Part01-130.jpg

(续)

978-7-111-61011-3-Part01-131.jpg

这里着重说明一下groupId、artifactId、version这三个标签,它们定义了一个项目的基本坐标。任何jar、pom或war都是基于这三个标签进行区分的。

groupId定义了项目属于哪个组,建议用公司名或组织名。一般来说,groupId由三个部分组成,每个部分之间以“.”分隔,第一部分是项目用途,例如用于商业的就是com,用于非营利性组织的就是org;第二部分是公司名,例如tengxun、baidu、alibaba;第三部分是项目名。

artifactId定义了当前Maven项目组中唯一的ID,这里定义此项目的artifactId为hello- world-demo。

version指项目当前的版本,默认版本为SNAPSHOT版本,SNAPSHOT意思为快照,说明当前项目处于开发迭代中,不是稳定版本。随着开发的推进,可以对版本依次递进修改,例如xx.SNAPSHOT,xx.beta,1.0,2.0等。

以上为Maven的常用标签,下面简要介绍Maven的其他标签,见表2-4,读者了解即可。

2-4 pom元素含义

978-7-111-61011-3-Part01-132.jpg

(续)

978-7-111-61011-3-Part01-133.jpg

2.2.3 运行Maven项目

当编写好业务代码后,需要构建运行项目。直接在项目的pom.xml文件上右击,选择“Run As”,就能看到常用的Maven命令,如图2-6所示。

978-7-111-61011-3-Part01-134.jpg

图2-6 运行项目

选择要执行的Maven命令就能执行相关的构建操作,同时在Eclipse的Console中就能看到执行命令的结果输出。如果想执行自定义顺序的命令,只需要点击“Maven build…”,在弹出的对话框的Goals输入中输入要执行的命令如clean install即可,如图2-7所示。

978-7-111-61011-3-Part01-135.jpg

图2-7 构建配置

点击Run按钮,可看到如图2-8所示的运行结果。

978-7-111-61011-3-Part01-136.jpg

图2-8 运行结果

Goals输入框常用的Maven命令见表2-5。

2-5 Maven命令

978-7-111-61011-3-Part01-137.jpg

常用命令的含义如下:

■compile:编译当前项目,编译后的class文件会放在项目的target/classes文件夹中,这是Maven约定的存放位置。

■test:编译当前项目并执行测试用例,这里Maven可能会下载测试所依赖的构件,并且在测试之前,Maven会编译主代码。

■package:打包当前项目,如果读者的pom文件里面的packaging元素设置的值为jar,那么执行此命令会编译当前项目生成一个jar文件存放在target目录下面,默认生成的文件名称由artifactId和version拼接组成。

■install:安装到仓库命令,可以把生成的jar文件直接安装到本地的Maven仓库(默认仓库地址在当前用户目录下面的.m2/repository文件夹)。

2.3 Maven坐标和依赖

Maven的一个重要功能是管理项目的依赖。本节讲解Maven坐标和它的作用、Maven如何在实际的项目中进行应用以及常用的Maven使用技巧和实战经验。

2.3.1 什么是坐标

坐标是Maven中任何一个依赖包的唯一标识。任何一个构件都明确地定义了自己的坐标。Maven的坐标包含以下几个元素:

■groupId定义了当前Maven项目的归属组织。

■artifactId定义了一个Maven项目或者模块的唯一名称。

■version定义了当前Maven项目所处的版本。

■packaging定义了当前Maven项目的打包方式。

■classifier定义了用来帮助定义构建输出的一些附属构建。

上述5个元素中,groupId,artifactId,version必须定义,packaging可选(默认为jar),classifier是不能直接定义的,因为附属构件不是项目直接默认生成的,而是由附加的插件帮助生成的。

Maven会根据pom文件里面配置的坐标元素,到Maven内置的中央仓库(https://repo1. maven.org/maven2/)里面寻找对应的依赖包,例如在pom文件中定义了groupId=junit、artifactId=junit、version=4.12,Maven就会检查本地仓库有没有对应文件,如果没有就会从中央仓库找到对应的文件并下载到本地的仓库,提供给工程使用。

2.3.2 什么是Maven依赖

在项目开发的时候,或多或少会依赖第三方组件或者其他工程模块,Maven依赖即指引用的第三方组件或者模块。这体现在本工程的pom文件里面project下面的dependencies标签下,此标签可以包含一个或多个dependency元素,来声明当前项目所依赖的一个或多个依赖。类似如下形式:

978-7-111-61011-3-Part01-138.jpg

每个依赖的groupId、artifactId、version这三个元素构成一个依赖的基本坐标,Maven根据这个坐标才能找到对应的依赖。

下面讲解在Maven项目中如何引入本地的包,例如在其他项目中生成一个jar包,引入到现有的项目中来。

方法一:将待引入的包放在指定目录下(如lib目录下),修改项目的pom文件,加入依赖并且将scope设置为system。pom文件配置如下所示:

978-7-111-61011-3-Part01-139.jpg

方法二:将待引入的jar包安装到本地repository中,例如将hello-world-demo-0.0.1.jar安装到本地仓库。

1)先把待引入的jar包放在一个目录下,打开命令行,进入jar包所在的目录,执行mvn install命令,具体如下:

978-7-111-61011-3-Part01-140.jpg

2)在项目的pom文件中加入包对应的依赖:

978-7-111-61011-3-Part01-141.jpg

这样就可以将本地的jar文件配置到自己的Maven项目中进行使用了。

2.3.3 Maven依赖的scope范围

Maven在不同的生命周期使用的classpath是不同的,例如执行项目的测试和Maven项目运行的时候,这两者之间的classpath是有差异的。常用的JUnit构件就是如此,在测试阶段会引入,但在运行Maven项目的时候是不需要的。

Maven的依赖范围与classpath的关系见表2-6。

2-6 scope范围

978-7-111-61011-3-Part01-142.jpg

根据上面的信息,可以轻松地引入其他构件,协助开发程序。在引用其他依赖时如果出现引用冲突,可以通过Maven的传递性依赖解决这一问题。

如果依赖没有声明依赖范围,那么其依赖范围就是默认的compile。假如A依赖B,B依赖C,那么A对B是第一直接依赖,B对C是第二直接依赖,A对C是传递性依赖。第一直接依赖范围和第二直接依赖范围决定了传递性依赖的范围。

在表2-7中,左边第一列表示第一直接依赖范围,最上面一行表示第二直接依赖的范围,中间的单元格表示传递性依赖范围,表格中的“-”表示依赖无法传递。

2-7 依赖传递

978-7-111-61011-3-Part01-143.jpg

根据上面的规则举例,例如项目Proj中,有一个直接依赖A,其依赖范围为compile,而A依赖里面又有一个B的直接依赖,其依赖范围为runtime,那么显然B是Proj项目的传递性依赖。参照表2-7,第一直接依赖为compile,第二直接依赖为runtime,因此B对项目Proj是一个范围为runtime的传递性依赖。

2.3.4 Maven的依赖调解原则

当项目里面依赖变多的时候,多个项目之间难免存在引用不同版本依赖的情况,这样容易出现依赖版本不一致,导致项目的构建出现问题。要解决此问题需要明白Maven的依赖调解原则。下面介绍这两个原则。

(1)路径最短者优先。

这里“->”符号代表依赖,“()”代表版本号。例如A->B(2.0)指的是A依赖版本号为2.0的B构件。下面是两条依赖链条。

A->B->C->X(1.0)

A->D->X(2.0)

可以发现两个依赖链条上都有版本X,而且X的版本是不一致的,根据路径最短者优先原则,X(1.0)的版本路径长度为3,而X(2.0)的版本路径长度为2,那么X(2.0)会被依赖使用。

(2)依赖路径长度相等的前提下,顺序最靠前的那个依赖优先。

A->B->X(1.0)

A->D->X(2.0)

上面两个不同版本的X的路径长度是一样的,依据Maven定义的依赖调解的第二原则:第一声明者优先。这里X(1.0)声明在前面,会被工程使用。

2.3.5 Maven仓库使用

Maven通过仓库来管理构件,仓库分为两种类型:本地仓库和远程仓库。当Maven根据坐标查找构件的时候,它首先会查看本地仓库,如果本地仓库存在此构件,直接使用。如果不存在,Maven就会去远程仓库查找,发现需要的构件后,下载到本地仓库再使用。如果在本地和远程仓库都没有找到,那么Maven就会显示找不到构件的错误提示信息。

本地仓库是在用户当前操作系统上存放构件的地方,默认在当前用户目录下面都有一个路径名称为.m2/repository/的仓库目录。

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

私服是另一种远程仓库,例如许多公司为了节省带宽和时间,会在内部搭建一个私服,也就是内部使用的Maven仓库,可以存放公司内部的构件或者其他开源构件。例如常见的Nexus服务。

众所周知,国内开发很头疼的一件事就是Maven仓库的下载速度太慢。所以一般使用国内公开仓库,常见的有阿里云仓库(http://maven.aliyun.com/nexus/content/groups/public/)。 下面介绍如何修改仓库地址。修改Maven根目录下的conf文件夹中的setting.xml文件,对应内容如下:

978-7-111-61011-3-Part01-144.jpg

978-7-111-61011-3-Part01-145.jpg

这样就把Maven的中央仓库的地址修改成阿里云仓库地址了。

当然也可以定义本地仓库的目录地址,修改settings.xml文件,设置本地仓库的实际存储路径。例如:

978-7-111-61011-3-Part01-146.jpg

2.4 Maven生命周期和插件

Maven的生命周期是对项目构建生命周期的一个抽象。在Maven出现之前,项目构建的生命周期早已存在。例如开发人员对项目的清理、编译、测试以及部署。

2.4.1 Maven生命周期

Maven有一套完善、易扩展的生命周期。包含了项目的清理、初始化、编译、打包、集成测试、验证、部署和站点生成等。可以映射到目前几乎所有软件的生命周期上。

每个生命周期包含了一系列阶段(phase),这些阶段有自己的顺序,并且前后阶段是有依赖关系的。Maven的生命周期如图2-9所示。

978-7-111-61011-3-Part01-147.jpg

图2-9 Maven生命周期

下面以常用的Maven命令为例,讲解其执行的生命周期阶段:

(1)mvn clean:该命令调用clean生命周期的clean阶段。实际执行的阶段为clean生命周期的pre-clean和clean阶段。

(2)mvn test:该命令调用default生命周期的test阶段。实际执行的阶段为default生命周期的validate、initialize等直到test的所有阶段。这也解释了为什么在执行测试的时候,项目代码能够自动得到编译。

(3)mvn clean install:调用clean生命周期的clean阶段和default生命周期的install阶段。实际执行为clean生命周期的pre-clean、clean阶段以及default生命周期的从validate至install的所有阶段。

(4)mvn clean deploy site-deploy:调用clean生命周期的clean阶段、default生命周期的deploy阶段以及site生命周期的site-deploy阶段。实际执行的阶段为clean生命周期的pre- clean、clean阶段,default生命周期的所有阶段和site生命周期的所有阶段。

2.4.2 Maven插件

Maven常用的插件见表2-8。

2-8 Maven常用插件

978-7-111-61011-3-Part01-148.jpg

Windows系统默认使用GBK编码格式,Java项目经常使用的编码为UTF-8,需要在compiler插件中进行相应设置,否则中文乱码可能会导致编译错误。

使用插件maven-compiler-plugin设定编译的JDK版本和编码格式,如下所示:

978-7-111-61011-3-Part01-149.jpg

2.4.3 生命周期与插件的关系

Maven的生命周期本身是不做任何实际工作的,实际的任务操作都交给插件来完成。生命周期抽象了构建的各个步骤,定义了步骤执行的顺序,但是没有具体实现,具体的实现由插件来完成。即Maven通过这种插件的机制,使得每个构建的步骤都可以绑定一个或者多个插件的行为,而且Maven内置了很多默认的插件。让使用者在大多数的时间里,感觉不到插件的存在。当然该机制提供了足够的扩展空间,使用者可以自己配置插件或者自定义插件来构建特定的行为。

2.5 Maven聚合和继承

Maven的聚合特性能够把一个项目的各个模块聚合在一起进行构建。Maven的继承特性能够帮助抽取相同的依赖和插件等配置,在简化pom的同时,还能够促进各个模块配置的一致性。

978-7-111-61011-3-Part01-150.jpg

图2-10 项目结构

2.5.1 聚合应用的场景

假设有两个Maven项目,分别为工程child01和工程child02,如图2-10所示。两个项目并行开发时,需要分别到两个模块目录中执行mvn命令进行构建。如果并行的项目更多会造成命令执行操作非常烦琐。而Maven聚合可以实现执行一次命令,构建多个模块。下面对child01和child02两个项目进行聚合操作。

创建另外一个项目parent,通过该项目构建项目组所有模块。parent作为一个Maven项目,必须拥有自己的pom文件。在Eclipse中创建此项目时要选择maven-archetype-site-simple。此parent项目不作为业务代码开发使用。

parent项目pom文件关键部分配置如下:

978-7-111-61011-3-Part01-151.jpg

parent项目中的packaging配置必须为pom。在parent项目中运行clean package命令,就会分别在child01/child02下生成对应的jar包,如图2-11所示。

978-7-111-61011-3-Part01-152.jpg

图2-11 运行结果

2.5.2 Maven的继承

如果子项目child01,child02需要继承项目parent中的pom配置,那么就需要使用Maven的继承。在子项目中添加配置:

978-7-111-61011-3-Part01-153.jpg

parent元素中的属性对应的都是父项目中的内容。在parent元素中还有一个属性relativePath,maven会通过这个路径去查找父项目的pom.xml文件,如果找不到会从本地仓库中查找。relativePath的默认值是../pom.xml,也就是默认父项目的pom在上一层目录。由于当前parent项目与已有项目平级,这里就需要指定pom文件的位置。可继承的pom元素见表2-9。

2-9 可继承的pom元素

978-7-111-61011-3-Part01-154.jpg

2.5.3 Maven中dependencyManagement的使用

子项目都会继承父项目的依赖关系,如果子项目不需要父项目的依赖关系,Maven提供的dependencyManagement元素能让子项目继承到父项目的依赖配置等属性(如版本信息),确保子模块的灵活性,同时dependencyManagement元素下的依赖声明不会引入实际的依赖。

父项目中使用该元素声明的依赖既不会给父项目引入依赖也不会给子项目引入依赖,但是该配置会被继承。如果子项目中不声明经过父项目dependencyManagement修饰的依赖,那么子项目就不会引入该依赖。子项目如果要使用父项目中经过dependencyManagement修饰的依赖,只需要定义groupId和artifactId即可。例如parent项目有以下配置:

978-7-111-61011-3-Part01-155.jpg

子项目要使用父项目的JUnit和Gson依赖,不需要添加版本号信息,只需向dependencies中加入如下依赖配置:

978-7-111-61011-3-Part01-156.jpg

Maven继承机制以及dependencyManagement元素能解决不同模块相同依赖构件版本不一致问题。注意,是dependencyManagement而非dependencies。也许读者已经想到在父模块中配置dependencies,那样所有子模块都自动继承,不仅达到了依赖一致的目的,还省掉了大段代码。这么做是有问题的,例如将模块child01的依赖spring-aop提取到了父模块中,但模块child02不需要spring-aop,却也直接继承了。dependencyManagement就没有这样的问题,dependency Management只会影响现有依赖的配置,但不会引入依赖。

在多模块Maven项目中,dependencyManagement几乎是必不可少的,用它能够有效地维护依赖一致性,消除多模块插件配置重复。

2.5.4 Maven中的pluginManagement的使用

与dependencyManagement类似,也可以使用pluginManagement元素管理插件。一个常见的用法就是希望项目所有模块使用Maven Compiler Plugin的时候,都使用Java 1.8,以及指定Java源文件编码为UTF-8,这时可以在父模块的pom文件中对pluginManagement进行如下 配置:

978-7-111-61011-3-Part01-157.jpg

978-7-111-61011-3-Part01-158.jpg

这段配置会被应用到所有子模块的maven-compiler-plugin中,由于Maven内置了maven-compiler-plugin与生命周期的绑定,因此子模块就不再需要任何maven-compiler-plugin的配置了。

通常所有项目对于任意一个依赖的配置都应该是统一的,但插件却不是这样,例如你希望模块A运行所有单元测试,模块B要跳过一些测试,这时就需要配置maven-surefire-plugin插件来实现,那样两个模块的插件配置就不一致了。也就是说,简单地把插件配置提取到父pom的pluginManagement中往往不适合所有情况,因此在使用的时候就需要注意了,只有那些普适的插件配置才应该使用pluginManagement提取到父pom中。

虽然Maven只是用来帮助构建项目和管理依赖的工具,pom也并不是正式产品代码的一部分,但也应该认真对待pom。随着敏捷开发和TDD[29]等方式越来越被人接受,测试代码得到了开发人员越来越多的关注。因此不能仅满足于一个能用的pom,而应该积极地修复pom中使用不当的地方。