Maven自动化构建工具

1,077 阅读18分钟

1. Maven简介

1.1. 为什么使用Maven

  • 项目中手动管理jar包和jar包之间的依赖关系很麻烦;
  • 项目不断的变大,导致维护变得麻烦;
  • 项目的构建过程中存在大量重复步骤;

1.2. Maven是什么

Maven 翻译为"专家"、"内行",是 Apache 下的一个纯 Java 开发的开源项目。基于项目对象模型(缩写:POM)概念,Maven利用一个中央信息片断能管理一个项目的构建、报告和文档等步骤。

Maven 是一个项目管理工具,可以对 Java 项目进行构建、依赖管理。

Maven 也可被用于构建和管理各种项目,例如 C#,Ruby,Scala 和其他语言编写的项目。Maven 曾是 Jakarta 项目的子项目,现为由 Apache 软件基金会主持的独立 Apache 项目。Maven官网Maven官网中文翻译

Maven解决的问题:

  • 项目jar包管理
  • 项目自动化构建
  • 项目模块化管理

Maven解决以上问题的方式:

jar包管理:通过在POM对象中书写坐标引用的方式确定jar包的版本信息;

自动化构建:通过Maven定义生命周期,通过插件的调用实现自动化项目构建;

项目模块化管理:通过定义父级Maven项目,配置子项目模块的方式进行管理;

1.3. 配置Maven

关于Maven的配置网上有很多的教程,此处不再赘述,这里放一个配置教程:IDEA和Eclipse中配置Maven

2. Maven核心概念

2.1. 约定的目录结构

当我们使用Maven进行自动编译时,Maven需要首先找到Java源文件,下一步才能进行自动编译,自动编译后的字节码文件也需要存放在准确的位置,我们可以采用两种方式让开发工具IDE和框架知道我们的项目资源文件位置:

① 通过配置的形式明确指明文件位置

② 通过三方工具和框架的约定

JavaEE开发领域普遍认同的观点,约定 > 配置 > 编码。基本意思是,能通过配置解决的问题就不必要进行编码,能基于约定解决的问题就不进行配置。Maven就是通过约定项目目录的方式对我们的Maven项目进行自动化构建。

约定的项目目录结构对于Maven的自动化构建而言很重要,Maven可以到约定的目录结构下查找到项目的资源文件,测试文件,对项目进行自动化构建。如果不按照约定的目录结构创建的项目会导致Maven无法识别项目内容,也就没办法进行自动化项目构建。

Maven中约定的项目目录结构:

demo                                项目文件目录
    |---src                         项目的源文件目录
    |---|---main                    项目主文件目录
    |---|---|---java                项目java文件目录
    |---|---|---resources           项目资源文件目录
    |---|---test                    测试文件目录
    |---|---|---java                测试java文件目录
    |---|---|---resources           测试资源文件目录
    |---target                      打包后的项目输出目录(此目录会在执行Maven打包命令后生成)
    |---|---classes                 编译输出目录
    |---|---test-classes            测试编译输出目录
    |---pom.xml                     Maven构建文件

2.2.POM

POM:Project Object Model(项目对象模型)是 Maven 工程的基本工作单元,是一个XML文件,也就是我们定义在项目路径下的 pom.xml 文件,其中包含了项目的基本信息,用于描述项目如何构建,项目中所需的依赖,等等。

Maven在执行任务或目标时会首先在当前的项目目录中查找POM,并读取POM中的配置,依照其中的配置对项目进行构建。

典型的POM文件:

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <!-- 模型版本 -->
    <modelVersion>4.0.0</modelVersion>
	
    <!-- 公司或组织的唯一标志 -->
    <groupId>cn.bruce</groupId>
    <!-- 项目的唯一ID,一个groupId下可能有多个项目,通过artifactId进行区分 -->
    <artifactId>maven-demo</artifactId>
    <!-- 此项目的版本号 -->
    <version>1.0-SNAPSHOT</version>


</project>

我们可以通过配置POM文件实现对工程的管理,POM文件中还有很多可以配置的地方,下面内容中还会提及到。

完整的POM文件信息:Maven POM

2.3. 坐标

和数学中使用 (x,y,z) 坐标来表示三维空间内的某一点相似,Maven使用三个向量(GAV模型)来定位仓库中的一个maven工程,这三个向量分别是:

  • gropId:组织id
  • artifactId:模块名
  • version:版本

在POM文件中使用 <dependency></dependency> 标签来描述项目中jar包依赖关系

示例:

<dependencies>
    <dependency>
        <!-- 依赖的组织名 -->
        <groupId>mysql</groupId>
        <!-- 依赖的模块名 -->
        <artifactId>mysql-connector-java</artifactId>
        <!-- 依赖的版本 -->
        <version>8.0.17</version>
    </dependency>

    ......

</dependencies>

版本中常见的单词:

  • SNAPSHOT:快照版本,不稳定的版本
  • RELEASE/GA:稳定版本

坐标和本地仓库中jar包的对应关系:

pom文件中的坐标:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <version>2.3.4.RELEASE</version>
    </dependency>
    
    ......
    
</dependencies>

实际仓库中的路径:

image-20201210221843508

Maven工程不再将jar包放在项目目录中,而是在POM中指出仓库中的引用位置,具体的依赖的jar包存放在本地仓库中。

2.4. 依赖

依赖的作用:管理项目中需要用到的jar包,换句话说依赖就是项目中使用的jar包,我们使用Maven最主要的就是使用它的依赖管理功能。针对依赖的管理就是对项目中的jar包进行管理,也可以管理依赖的使用范围,依赖之间的传递关系,统一依赖的版本等问题。我们使用前面提及到的坐标功能来导入项目中的jar包,如:在项目中导入MySQL的驱动。

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>cn.bruce</groupId>
    <artifactId>maven-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.12</version>
        </dependency>
    </dependencies>

</project>

2.4.1. 依赖的范围:

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

  • complie:编译依赖范围。如果没有指定,就会默认使用该依赖范围。使用此依赖范围的maven依赖,对于编译 测试 运行三种的classpath都有效。
  • test:测试依赖范围。使用此依赖范围的Maven依赖,只对于测试的classpath有效,在编译主代码或者运行主代码的时候都无法依赖此类依赖。典型的例子是jUnit,它只有在编译测试代码及运行测试代码的时候才有效。
  • provided:已提供依赖范围。使用此依赖范围的maven依赖,对于编译和测试classpath有效,但在运行时无效。典型的例子是servlet-api,编译和测试项目的时候需要该依赖,但在运行的时候,由于容器已经提供,就不需要maven重复地引入一遍。打包的时候可以不用包进去,别的设施会提供。事实上该依赖理论上可以参与编译,测试,运行等周期。相当于compile,但是打包阶段做了exclude操作。
  • runtime:运行时依赖范围。使用此依赖范围的maven依赖,对于测试和运行classpath有效,但在编译主代码时无效。典型的例子是JDBC驱动实现,项目主代码的编译只需要jdk提供的jdbc的接口,只有在执行测试或者运行测试的时候才需要实现上述接口的jdbc的驱动
  • system:系统依赖范围。从参与度来说,和provided相同,不过被依赖项不会从maven仓库下载,而是从本地文件系统拿。需要添加systemPath的属性来定义路径,该依赖与三种范围的classpath和provided依赖范围完全一致。可能造成不可移植,谨慎使用。
  • import:导入依赖范围。该依赖范围不会对三种classpath产生实际的影响。只有在dependencyManagement下才有效果。

常用的声明周期范围对比:

complietestprovided
是否对主程序生效
是否参与测试阶段
是否参与部署

在POM文件中使用 <scop></scop> 标签设置jar包的依赖范围,示例:

<dependencies>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.12</version>
    </dependency>

    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13.1</version>
        <!-- 在测试阶段生效 -->
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>servlet-api</artifactId>
        <version>2.5</version>
        <!-- 在部署时排除此依赖 -->
        <scope>provided</scope>
    </dependency>

</dependencies>

2.4.2. 依赖的传递

依赖具有传递性,我们导入的jar包依赖其他jar包,Maven会将jar包所依赖的jar包同时传递过来。

例如:项目中依赖spring-boot-stater:2.1.8.RELEASE jar包,但是此jar包同时依赖其他的jar包,我们在引入此依赖的同时Maven会将 spring-boot-stater:2.1.8.RELEASE 所依赖的jar包也传递到项目中,通过IDEA提供的Maven管理工具我们可以看到如下图所示:

image-20201212164332791

好处:可传递的依赖不必要在每个项目中书写,在公共项目中配置好,其他项目依赖即可。

例如:我们在 maven-demo 项目中配置好所需的依赖,在 maven-model-1 项目中引入 maven-demo 依赖,即可在 maven-model-1 项目中使用 maven-demo 所依赖的jar包。

maven-demo 项目的POM结构

<dependencies>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.12</version>
    </dependency>

    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13.1</version>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>servlet-api</artifactId>
        <version>2.5</version>
        <scope>provided</scope>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
        <version>2.1.8.RELEASE</version>
    </dependency>

</dependencies>

maven-model-1 项目的POM结构:

<dependencies>
    <dependency>
        <groupId>cn.bruce</groupId>
        <artifactId>maven-demo</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>

通过IDEA的Maven管理工具可以查看到 maven-model-1 项目中已经导入了 maven-demo 项目所需的依赖。

image-20201212164111080

我们注意到 maven-demo 中有一些依赖并没有被传递到 maven-model-1 中,这是因为非 compile 范围依赖不能被传递。

2.4.3. 依赖的排除

我们在当前的工程中引入了依赖A,而A又依赖了B,此时Maven会将A和B同时引入当前工程,但是我们在工程中已经引入了B,此时我们希望A直接依赖项目中的B而不是原本依赖的B,因此我们需要对A中的B进行依赖排除。

依赖排除的主要目的也是为了对项目中的依赖版本进行统一的管理,在POM文件中 <exclusion></exclusion> 标签进行依赖排除操作。

示例代码:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>

未排除前:

image-20201214114131660

排除后:

image-20201214114211679

2.4.4. 依赖的原则

我们可以通过上面的依赖排除解决工程中依赖冲突的问题,同时Maven也提供了解决依赖冲突的方案,即我们不解决冲突问题,Maven也是会帮我们解决的,但是不建议这样,出现依赖冲突最好还是解决避免造成项目的不稳定。

Maven解决冲突的原则:

① 路径最短优先

image-20201214101233994

如图所示,项目A依赖项目B,项目B同时依赖项目C,且项目A、项目B、项目C同时依赖 slf4j-api 但是依赖的版本不同,项目A,B,C对 slf4j-api 的依赖路径分别为1,2,3。根据路径最短原则,项目A中最终引入的 slf4j-api 的版本为 1.7.30

路径:当前项目和依赖的maven工程之间的模块层级。

② 先声明者优先

image-20201214103016602

项目A中的POM文件:

<dependencies>
	<dependency>
    	<groupId>cn.bruce</groupId>
        <artifactId>projectB</artifactId>
        <version>1.0.1.SNAPSHOT</version>
    </dependency>
    
    <dependency>
    	<groupId>cn.bruce</groupId>
        <artifactId>projectC</artifactId>
        <version>1.0.1.SNAPSHOT</version>
    </dependency>
</dependencies>

如图所示项目,A同时依赖项目B和项目C,且项目A、项目B、项目C同时依赖 slf4j-api 但是依赖的版本不同,此时项目中依赖的 slf4j-api 的版本取决于项目A中,对项目B和项目C依赖的声明位置。在项目A的POM文件中,projectB的声明在projectC的声明前,因此项目中使用projectB中指定的 slf4j-api:1.7.23

先声明指的是 <dependency></dependency> 标签声明的顺序

2.4.5. 统一管理依赖的版本

我们在项目中可能依赖同一个框架的一组jar包,我们在使用的时候,最好使用统一的版本,如果采用我们上面讲到的内容,我们需要对框架内的每个依赖包逐一进行版本的修改和设置,Maven提供了让我们统一修改版本

项目中的POM文件

<properties>
    <spring.version>5.0.0</spring.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>${spring.version}</version>
    </dependency>
    
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-beans</artifactId>
        <version>${spring.version}</version>
    </dependency>
    
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aop</artifactId>
        <version>${spring.version}</version>
    </dependency>
    
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
        <version>${spring.version}</version>
    </dependency>
</dependencies>

此时项目中Spring各个版本jar包依赖都是5.0.0,如果想统一升级为5.0.1时,仅需修改 <properties> 标签中的版本号即可。因此建议项目中依赖jar包使用 <properties>自定义标签名</properties> 标签进行统一的版本管理,在需要用的的地方使用 ${自定义标签名}

<properties>自定义标签名</properties> 标签不仅仅用来进行版本号管理,凡是需要统一声明后再引用的场合都可以使用此标签。如:声明项目的编译编码和Java的版本

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
</properties>

2.4.6. 查找Maven工程版本

image-20201214111002965

当我们在POM文件中指定好依赖的相关信息(即定位到指定的jar包版本后)Maven的查找顺序:

① Maven会首先到我们的本地仓库中查找jar包;

② 本地仓库中查找不到会到Maven私服中查找;

③ Maven私服中还未查到则会到远程仓库中查找jar包,找到后下载到本地;

⑤ 下次在再使用时直接引用本地仓库中的jar包;

查找过程:本地-->私服--> 远程仓库

2.5. 仓库

2.5.1. 仓库的分类

① 本地仓库:本机中的仓库,为本机上的所有Maven工程服务

② 远程仓库

  • Maven私服:部署在局域网环境下,为当前局域网范围内的Maven工程服务;
  • Maven中央仓库:部署再Internet上,为全世界的Maven工程服务;
  • Maven中央仓库镜像:为中央仓库分担压力,提供更快的依赖请求服务,如:阿里云镜像仓库

2.5.2. 仓库中的文件

仓库中的内容:Maven工程

  • Maven自身所需的插件
  • 第三方框架或工具jar包
  • 我们自己开发的Maven工程

2.6. 生命周期/插件/目标

2.6.1. 生命周期

Maven的生命周期是指Maven在执行项目构建时的各个环节,Maven的生命周期定义了各个构建环节的执行顺序,有了执行顺序清单,Maven就可以自动化的执行构建命令。

Maven包含三套生命周期:

① Clean Lifecycle 在进行真正的项目构建之前进行清理工作;

② Default Lifecyle 构建的核心部分,编译、测试、打包、安装、部署等等;

③ Site Lifecycle 生成项目报告、站点、发布站点;

2.6.1.1 Clean 生命周期

阶段描述
pre-clean在实际项目清理之前执行所需的过程
clean删除上一版本生成的所有文件
post-clean执行完成项目清理所需的过程

6.1.2 Default 生命周期

Default生命周期是Maven生命周期中最重要的一个,绝大部分工作都发生在这个生命周期中。

阶段描述
validate验证项目正确无误,并提供所有必要的信息。
initialize初始化构建状态,例如设置属性或创建目录。
generate-sources生成任何要包含在编译中的源代码。
process-sources处理源代码,例如过滤任何值。
generate-resources生成资源以包含在包中。
process-resources将资源复制并处理到目标目录中,以备打包。
compile编译项目的源代码。
process-classes对编译后生成的文件进行后处理,例如对Java类进行字节码增强。
generate-test-sources生成任何测试源代码以包含在编译中。
process-test-sources处理测试源代码,例如过滤所有值。
generate-test-resources创建测试资源。
process-test-resources将资源复制并处理到测试目标目录中。
test-compile将测试源代码编译到测试目标目录中
process-test-classes从测试编译中对生成的文件进行后处理,例如对Java类进行字节码增强。
test使用合适的单元测试框架运行测试。这些测试不应要求将代码打包或部署。
prepare-package在实际包装之前执行准备包装所需的任何操作。这通常会导致包装的未包装,已处理版本。
package获取编译后的代码,并将其打包为可分发格式,例如JAR。
pre-integration-test在执行集成测试之前执行所需的操作。这可能涉及诸如设置所需环境的事情。
integration-test处理该程序包并将其部署到可以运行集成测试的环境中(如有必要)。
post-integration-test在执行集成测试后执行所需的操作。这可能包括清理环境。
verify运行任何检查以确认包装有效并符合质量标准。
install将软件包安装到本地存储库中,以作为本地其他项目中的依赖项。
deploy在集成或发布环境中完成后,将最终程序包复制到远程存储库,以便与其他开发人员和项目共享。

6.1.3 Site 生命周期

一般用于创建报告文档、部署站点等。

阶段描述
pre-site在实际项目站点生成之前执行所需的过程
site生成项目的站点文档
post-site执行完成站点生成并为站点部署做准备所需的过程
site-deploy将生成的站点文档部署到指定的Web服务器

Maven构建生命周期的特点:

  • 各个构建环节的执行顺序:不能打乱顺序,必须按照正确的顺序执行;
  • maven核心程序定义了抽象的生命周期,生命周期中的具体任务由插件完成;
  • Maven会按照顺序执行,执行生命周期中的任何阶段,都会从最初的位置开始执行;

2.6.2. 插件和插件目标

maven的核心程序中仅仅定义了抽象的生命周期,具体的工作需要通过特定的插件完成,插件本身并不包含在Maven核心程序中。当项目构建时,maven会首先到本地仓库中查找插件,找不到会到远程仓库中下载 。

每个插件都能实现多个功能,每个功能就是一个插件目标;

Maven生命周期与插件的目标相互绑定,来完成某个具体的构建任务;

插件目标:

  1. 生命周期的各个阶段仅仅定义了要执行什么任务
  2. 各个阶段和插件的目标是对应的
  3. 相似的目标由插件完成
  4. 可以将目标看作调用功能的命令
生命周期阶段插件目标插件
compilecompilemaven-compiler-plugin
test-compiletest-compilemaven-compiler-plugin
..................

2.7. 继承

我们想对项目中的junit依赖版本进行统一的管理,由于test范围的依赖不能传递,容易造成版本不一致。Maven为我们提供了继承的方式来解决这些问题。

解决方式:在父工程中指定junit的完整依赖信息,子工程中声明junit依赖时不指定版本,以父工程中统一设定为准

操作步骤:

① 创建父级Maven工程,打包方式为pom

② 在子工程中声明父工程的应用

③ 在父工程中对依赖的版本进行管理

④ 在子工程中使用父工程中管理的版本不需要书写版本信息

代码示例:

① 创建父级Maven工程,打包方式为pom

<groupId>cn.bruce</groupId>
<artifactId>maven-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<name>maven-demo</name>
<packaging>pom</packaging>

② 在子工程中声明父工程的应用

使用 <parent></parent> 标签指明子模块的父级模块

<parent>
    <artifactId>maven-demo</artifactId>
    <groupId>cn.bruce</groupId>
    <version>1.0-SNAPSHOT</version>
    <relativePath>../maven-demo/pom.xml</relativePath>
</parent>

③ 在父工程中对依赖的版本进行管理

使用 <dependencies></dependencies> 标签进行依赖管理

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.1</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

④ 在子工程中使用父工程中管理的版本不需要书写版本信息

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <scope>test</scope>
</dependency>

2.8. 聚合

项目会伴随着业务的发展变得越来越大,涉及到的模块也会变得更多,如果我们对每个模块都执行声明周期会花费很多时间和精力。Maven提供了聚合的功能,方便我们进行统一的模块管理。

聚合的目的:在一个“总的聚合工程”中管理各个参与的模块

聚合模块中,使用标签:<module></module> 进行模块管理,配置模块聚合后,在聚合模块执行生命周期操作,聚合模块中的子模会执行和聚合模块相同的生命周期操作,Maven会自动识别子模块间的执行顺序。

具体操作:

① 聚合模块的配置

使用 <modules></modules> 标签配置需要聚合的模块

<modules>
    <module>../maven-model-1</module>
    <module>../maven-model-2</module>
</modules>

聚合模块中配置好子模块后,使用IDEA中的Maven工具可以查看到,聚合模块中有【root】的标识

image-20201214151918185

在聚合模块执行生命周期后可以看到Maven执行的结果


......

[INFO] maven-parent ....................................... SUCCESS [  1.361 s]
[INFO] maven-model-1 ...................................... SUCCESS [  0.003 s]
[INFO] maven-model-2 ...................................... SUCCESS [  0.002 s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  1.710 s
[INFO] Finished at: 2020-12-14T15:22:16+08:00
[INFO] ------------------------------------------------------------------------

在实际的项目搭建中继承、聚合和依赖传递通常一同使用,来进行项目依赖和生命周期的统一管理。

  • 依赖统一管理:在父级Maven工程中配置各个子模块中公共依赖的相关版本信息,子模块继承父级模块方便进行依赖的统一管理;
  • 生命周期统一管理:将子模块在父级模块中配置完成后,即可在父级模块上进行统一的生命周期操作;