【Maven专栏系列】重新认识一下Maven这款工具

1,313 阅读10分钟

这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战

前言

在我最开始学习Java时,经常会遇到需要一些第三方Jar包,比如学习JDBC时,需要MySQL的驱动包,还有一些其他的工具包,例如GuavaApache Common等等,最开始我都是去官网或者网上搜索下载,比如CS某N,还经常因为没有积分下不了,体验特别差,有时候还会因为各种版本问题,遗漏某个Jar,导致代码执行时报错,然后丈二的和尚摸不着脑袋,走了很多的弯路。

直到后来学习了Maven这款构建工具,再也不用到处下载了,需要的Jar包在pom.xml文件中添加依赖,然后mvn install一下就好了,是真香呀,后悔没有早点遇见,如果刚开始有人能告诉我有Maven这款工具,那我肯定非常感谢他/她/它。当然Maven不仅仅可以管理Jar包依赖,还能做很多项目构建的事情。

所以这就是我写这个专栏的原因。我希望能够让看到这篇专栏的人能少走一点我走过的弯路,学习路上更容易一点点。

扯了这么多,回归今天的正题。

Maven是一款Java软件项目的强大构建工具。本期内容的目的是让你了解Maven是如何工作的。因此,会重点介绍Maven的核心概念。一旦理解了核心概念,在使用时遇到问题再去网上查找文档就容易得多了。一旦理解Maven并开始使用它,你就会发现它到底是什么。

什么是构建工具?

首先说一下什么是构建工具,构建工具是一种自动化构建软件项目的工具。构建一个软件项目通常包括以下一个或多个步骤:

  • 生成源代码(如在项目中使用自动生成的代码);
  • 将源代码生成文档;
  • 编译源代码;
  • 将编译后的代码打包成JAR文件或ZIP文件;
  • 将打包的代码安装到服务器、存储库或其他地方;

自动化构建过程的好处是,可以在手工构建软件时将人为出错的风险降到最低。此外,自动化构建工具通常比人工手动执行相同步骤要快。

Maven安装

要在自己的系统(计算机)上安装Maven,首先需要去官网下载安装包然后进行安装,安装步骤如下:

Maven官网下载地址

  1. 设置好JAVA_HOME环境变量;
  2. 下载并解压Maven;
  3. 设置MAVEN_HOME环境变量为解压Maven的目录;
  4. MAVEN_HOME添加到PATH环境变量(Windows上为%MAVEN_HOME%\bin,Linux上为$MAVEN_HOME/bin);
  5. 打开命令提示符并输入mvn -version验证是否安装成功。

如果命令行显示结果如下则表示安装成功。

image-20211104233852537

注意:Maven在执行时需要使用Java,因此在安装Maven之前需要安装Java环境(以及如上所述设置JAVA_HOME环境变量)。Maven 3.0.5需要Java版本1.5或更高版本。

Maven核心概念

在Maven中以POM文件(项目对象模型)为中心。POM文件是管理项目资源的XML,如源代码、测试代码、依赖项(外部jar)等。

POM包含对所有这些资源的引用。POM文件应该位于它所属项目的根目录中。

下面的图表说明了Maven如何使用POM文件,以及POM文件主要包含什么内容:

image-20211104235128310

pom文件

当执行Maven命令时,需要向Maven提供一个POM文件,以便在其上执行命令。然后,Maven将按照对POM中配置的资源执行命令。

Build Life Cycles, Phases和Goals

Maven的构建过程被划分为Build Life Cycles, Phases和Goals。Build Life Cycles由一系列Phases组成,每个Phases由一系列Goals组成。

当运行Maven时,将一个命令传递给Maven。这个命令是Build Life Cycles, Phases或Goals的名称。

如果请求执行一个Life Cycles,则执行该Life Cycles中的所有Phases。

如果一个Phases被请求执行,那么在它之前的预定义Phases中的所有Phases也将被执行。

依赖和仓库

Maven执行的第一个目标是检查项目所需的依赖项。

依赖项是项目使用的外部JAR包。如果在本地Maven仓库中没有找到依赖项,Maven将从中央仓库下载,并将它们放到本地存储库中。

本地仓库只是计算机硬盘上的一个目录,可以指定本地仓库位置。还可以指定使用哪个远程仓库来下载依赖项。

构建插件Plugins

构建插件用于在构建Phases时插入额外的Goals。如果需要为项目执行一组标准Maven构建Phases和Goals没有涵盖的操作,可以向POM文件添加一个插件。Maven有一些已有的标准插件,如果需要,还可以用Java实现自定义插件。

Profiles文件

如果需要以不同的方式构建项目,则需要使用Profiles文件。例如,可能需要在本地构建项目进行开发和测试。还需要用不同的方式构建项目用于生产环境部署。这两个版本可能是有差异的,可以向POM文件添加不同的Profiles文件,在执行Maven时,选择使用哪一个。

Maven的POM文件

Maven的POM文件(项目对象模型)是配置项目资源的XML文件,包括源代码、测试源代码等所在的目录,项目的外部依赖等等。

POM文件配置要构建什么,但不需要配置如何构建。如何构建它取决于Maven构建Phases和Goals。

但是,如果需要,也可以将自定义操作(Goals)插入到Maven构建Phases。

每个项目都有一个POM文件。POM文件名为pom.xml,应该位于项目的根目录中。

一个被划分为子项目的项目通常有一个归属于父项目的POM文件,这种结构既允许在一个步骤中构建整个项目,也允许单独构建任何子项目。

下面是一个最简单的pom.xml示例。

<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>com.heiz</groupId>
    <artifactId>java-web-demo</artifactId>
    <version>1.0.0</version>
</project>

modelVersion元素设置正在使用的POM模型的版本。要使用与正在使用的Maven版本匹配的版本。版本4.0.0匹配Maven版本2和3。

groupId元素是一个组织或项目(例如,一个开源项目)的唯一ID。最常见的情况是,将使用与项目的根Java包名称类似的groupId。例如,对于我的java-web-demo项目,我可以选择组ID com.heiz。如果这个项目是一个有许多独立贡献者的开源项目,那么使用与这个项目相关的groupId可能比使用与我的公司相关的groupId更有意义。

groupId不一定要是Java包名。符号(点符号)用于分隔ID中的单词。groupId中的每一个.会在项目构建后替换为存放在本地仓库的目录分割符,每个单词对应一个文件夹,所以groupId com.heiz将位于名为MAVEN_REPO/com/heiz的目录中。目录的MAVEN_REPO部分将被Maven存储库的目录路径替换。

artifactId元素代表要构建的项目的名称。在我的java-web-demo项目中,artifactId将是java-web-demo。artifactId用作Maven存储库中groupId目录下的子目录的名称。artifactId也用作在构建项目时生成的JAR文件的名称的一部分,当然结果也可以是WAR文件或者其他格式。

version元素表示项目的版本号。如果项目已经以不同的版本发布,例如一个开源API,对构建版本进行版本控制是很常见的。这样项目的用户就可以引用项目的指定版本。版本号用作artifactId目录下的子目录的名称。版本号也用作所构建JAR的名称的一部分。

上面的groupId、artifactId和version将生成一个JAR文件,并将其放入本地Maven存储库中,路径如下(目录和文件名):

MAVEN_REPO/com/heiz/java-web-demo/1.0.0/java-web-deom-1.0.0.jar

父级POM

所有的Maven POM文件都继承自一个父级POM。如果没有指定父级POM,则POM文件继承自基本POM。如下图所示:

可以在POM文件显式地继承另一个POM文件。通过这种方式,可以通过所有继承POM的通用POM来更改设置。

<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>
    
        <parent>
        <groupId>org.heiz.mojo</groupId>
        <artifactId>my-parent</artifactId>
        <version>2.0</version>
        <relativePath>../my-parent</relativePath>
        </parent>
    
    <artifactId>my-project</artifactId>
    ...
</project>

继承的POM文件在自己的文件中也可以用新的配置覆盖父级POM的配置。

有效的POM

如果需要确认POM文件是否在一个项目中是有效的POM,可以使用下面的命令进行验证:

mvn help:effective-pom

会在命令行中将有效的POM打印出来。

运行Maven

当安装了Maven并创建了一个Maven项目,增加了POM文件配置后,可以在项目上运行Maven。

运行Maven可以通过在命令行执行mvn命令来完成的。在执行mvn命令时,将Build Life Cycles, Phases和Goals的名称作为参数。

如:

mvn install

该命令执行称为install的构建阶段,该阶段构建项目并将打包的JAR文件复制到本地Maven存储库中。

也可以指定多个参数,比如:

 mvn clean install

该命令首先执行clean构建生命周期,从Maven输出目录中删除已编译的类,然后执行install构建阶段。

还可以通过将构建阶段和目标作为参数来执行Maven目标。下面是一个例子:

mvn dependency:copy-dependencies

此命令执行dependency构建阶段的copy-dependencies目标。

Maven目录结构

Maven有一个标准的目录结构。如果项目遵循这个目录结构,那么就不需要在POM文件中指定源代码、测试代码的目录。

以下是最重要的目录:

- src
  - main
    - java
    - resources
    - webapp
  - test
    - java
    - resources

- target

src目录是源代码和测试代码的根目录。

main目录是与应用程序本身相关的源代码的根目录(不是测试代码)。

test目录包含测试源代码。

main和test下的java目录包含应用程序本身的java代码(在main下)和测试的java代码(在test下)。

resources目录包含项目所需的其他资源,比如一些配置文件。

如果是一个web应用程序,webapp目录包含Java web应用程序。webapp目录将成为web应用程序的根目录。因此,webapp目录包含WEB-INF目录等。

target目录是由Maven创建的。它包含Maven生成的所有已编译类、JAR文件等。在执行清理构建阶段时,要清理的是target目录。

项目依赖

除非你的项目很小,否则可能会需要引入外部框架,这些框架打包在它们自己的JAR文件中。在编译项目代码时,classpath上需要这些JAR文件。

引入外部JAR文件的正确版本是一项重要的任务。每个外部JAR可能还需要其他外部JAR文件。尤其是当项目越来越大的时候确保下载正确版本的JAR是很麻烦的。

而Maven内置的依赖项管理就是为了解决这个事情。可以在POM文件中指定项目依赖的外部依赖以及版本,然后Maven会下载它们并将它们放入本地Maven存储库中。如果这些外部依赖中的任何一个需要其他依赖,那么这些其他依赖也会下载到本地Maven存储库中。

以在POM文件中的dependencies元素中指定项目依赖项。下面是一个例子:

<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>com.heiz.demo</groupId>
    <artifactId>java-web-demo</artifactId>
    <version>1.0.0</version>

    <dependencies>

        <dependency>
            <groupId>org.jsoup</groupId>
            <artifactId>jsoup</artifactId>
            <version>1.7.1</version>
        </dependency>

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

    </dependencies>

    <build>
    </build>

</project>

请注意dependencies元素,它里面有两个dependency元素。每个dependency项描述一个外部依赖项。

每个依赖项由其groupId、artifactId和version描述。

当这个POM文件被Maven执行时,这两个依赖项将从中央仓库下载并放到本地仓库中。如果已经在本地仓库中找到了依赖项,将不会下载。

有时候中央仓库中的依赖不可用,你可以从官网或其他地方下载需要的Jar放入到本地Maven仓库中使用。

外部依赖

Maven中的外部依赖项是不在Maven仓库(既不是本地、中央或远程仓库)中的依赖项(JAR文件)。它可能位于本地的某个地方,例如在webapp的lib目录中,或者其他地方。因此,“外部”一词意味着Maven存储库系统的外部——而不仅仅是项目的外部。大多数依赖项都在项目的外部,但很不在仓库中。如果有这种情况,你可以按下面的方式配置外部依赖:

<dependency>
    <groupId>mydependency</groupId>
    <artifactId>mydependency</artifactId>
    <scope>system</scope>
    <version>1.0</version>
    <systemPath>${basedir}\war\WEB-INF\lib\mydependency.jar</systemPath>
</dependency>

scope元素值被设置为systemsystemePath元素被设置为指向包含依赖项的JAR文件的位置。${basedir}指向POM所在的目录。

快照依赖(Snapshot)

快照依赖关系是正在开发的依赖关系(JAR文件)。您可以依赖项目的快照版本,而不是不断地更新版本号以获得最新版本。

每次构建,即使匹配的快照版本已经位于本地存储库中,也会重新下载到本地存储库中。这可以确保本地存储库中始终拥有针对每个构建的最新版本。

只需在POM开头的版本号后面加上-SNAPSHOT即可表示你的项目是快照版本。

<version>1.0-SNAPSHOT</version>

在配置依赖项时,指定版本号为快照版本,便可以实现对快照版本的依赖。

<dependency>
    <groupId>com.jenkov</groupId>
    <artifactId>java-web-crawler</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

可以在Maven的settings.xml文件中配置下载快照依赖项的频率。

传递依赖

如果你的项目依赖于一个依赖项,比如依赖项ABC,而依赖项ABC本身又依赖于另一个依赖项,比如依赖项XYZ,那么你的项目就依赖于XYZ。

排除依赖

有时候,项目的直接依赖可能会与直接依赖的传递依赖发生冲突。

例如,你可能正在使用JAX-RS实现,该实现在内部使用较旧版本的Jackson包。但是,你的应用程序可能正在使用更新版本的Jackson包。您如何知道将使用这两个版本中的哪一个?

一个解决方案是为JAX-RS依赖项指定应该排除它对Jackson包的旧版本的依赖项。这也被称为依赖排除。

<dependency>
  <groupId>example.jaxrs</groupId>
  <artifactId>JAX-RS-TOOLKIT</artifactId>
  <version>1.0</version>
  <scope>compile</scope>
  <exclusions>
    <exclusion>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-core</artifactId>
    </exclusion>
  </exclusions>
</dependency>

有了这个依赖项排除声明,在Maven编译项目时,被排除依赖项将被忽略。

Maven仓库

Maven仓库是一个包含特殊元数据的打包JAR文件的目录。

元数据是指每个打包JAR文件所属的项目的POM文件,包括每个JAR具有的外部依赖项。正是这个元数据使Maven能够递归地下载依赖项的依赖项,直到整个依赖项树被下载并放到本地仓库中。

Maven有三种类型的仓库:

  • 本地仓库
  • 中央仓库
  • 远程仓库

Maven会按照上述顺序在这些仓库中搜索依赖项。首先在本地仓库中,然后在中央仓库中,如果在POM中指定,则在远程仓库中搜索。

本地仓库

默认情况下,Maven将本地仓库放在本地计算机上的用户主目录中。但是,可以通过在Maven的settings.xml文件中来更改本地仓库的位置。

Maven的settings.xml文件位于Maven安装目录的conf/目录下。以下是为本地仓库指定位置的方法:

<settings>
    <localRepository>
        d:\data\java\products\maven\repository
    </localRepository>
</settings>

中央仓库

中央仓库库是Maven社区提供的。默认情况下,Maven会在这个中央仓库库中查找任何需要但在本地仓库库中没有找到的依赖项。然后将这些依赖项下载到本地仓库库中。

远程仓库

远程仓库通常用于承载组织内部的项目,这些项目由多个项目共享。例如,一个公共安全项目可以跨多个内部项目使用。外部世界不应访问此安全项目,因此中央Maven仓库不应暴露在公网中。可以在POM文件中配置远程存储库:

<repositories>
   <repository>
       <id>heiz.code</id>
       <url>http://maven.heiz.com/maven2/lib</url>
   </repository>
</repositories>

Build Life Cycles、Phases和goals

Build Life Cycles

Maven有3个内置的构建生命周期:

  1. default
  2. clean
  3. site

这些构建生命周期中的每一个都负责构建软件项目的不同情况。因此,每一个构建生命周期都是独立地执行的。可以让Maven执行多个构建生命周期,但它们将依次执行,彼此分开,就像执行了两个独立的Maven命令一样。

default处理编译和打包项目相关的所有事情。

clean处理输出目录删除临时文件相关的所有事情,包括生成的源文件、编译的类、以前的JAR文件等。

site处理与为项目生成文档相关的所有事情。事实上,site可以为项目生成一个包含文档的完整网站。

Build Phases

你可以执行像clean或site这样的整个构建生命周期,像install这样的构建阶段(这是default构建生命周期的一部分),或者像dependency:copy-dependencies这样的构建目标。

注意:不能直接执行default的生命周期。您必须在default的生命周期内指定构建阶段或目标。

当执行一个构建阶段时,将执行该标准阶段序列中该构建阶段之前的所有构建阶段。因此,执行install构建阶段实际上意味着在install阶段之前执行所有构建阶段,然后执行install阶段。

default的生命周期是最重要的,因为它是构建代码的部分。因为不能直接执行default的生命周期,所以需要从default的生命周期执行构建阶段或目标。默认的生命周期有一个构建阶段和目标的顺序,最常用的构建阶段是:

Build Phase描述
validate校验项目是正确的,所有必要的信息都是可用的,确保依赖项成功下载。
compile编译项目的源代码。
test使用合适的单元测试框架对编译后的源代码运行测试。这些测试不应该被package或deploy。
package将编译后的代码打包成可发布的格式,例如JAR。
install将包安装到本地仓库中,作为本地其他项目的依赖项使用。
deploy将最终包复制到远程存储库,以便与其他开发人员和项目共享。

可以通过mvn命令来执行其中一个构建阶段。下面是一个例子:

mvn package

此示例执行package构建阶段,也会执行Maven预定义的构建阶段序列中package之前的所有构建阶段。

Build Goals

构建目标是Maven构建过程中最好的步骤。一个目标可以绑定到一个或多个构建阶段,也可以不绑定。如果目标没有绑定到任何构建阶段,则只能通过将目标名称传递给mvn命令来执行它。如果一个目标被绑定到多个构建阶段,那么该目标将在它所绑定的每个构建阶段中执行。

Maven构建Profiles

profiles使能够使用不同的配置来构建项目。不需要创建两个单独的POM文件,只需使用不同的构建配置指定一个profiles,然后在需要时使用这个profiles构建项目。

profiles在POM文件的profiles元素中指定。每个profile都嵌套在profiles元素中。下面是一个例子:

<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>com.jenkov.crawler</groupId>
  <artifactId>java-web-crawler</artifactId>
  <version>1.0.0</version>

  <profiles>
      <profile>
          <id>test</id>
          <activation>...</activation>
          <build>...</build>
          <modules>...</modules>
          <repositories>...</repositories>
          <pluginRepositories>...</pluginRepositories>
          <dependencies>...</dependencies>
          <reporting>...</reporting>
          <dependencyManagement>...</dependencyManagement>
          <distributionManagement>...</distributionManagement>
      </profile>
  </profiles>

</project>

profile中配置在构建时应该对POM文件进行哪些更改。profile元素中的元素将覆盖POM中同名元素的值。

在profile元素中,可以看到一个activation元素。此元素描述触发要使用的构建概要文件的条件。选择profiles的一种方法是在settings.xml文件中设置使用的配置文件。另一种方法是在Maven命令行中添加-P profile-name。

插件Plugins

Maven插件允许将自己的操作添加到构建过程中。可以创建一个简单的Java类,它扩展了一个特殊的Maven类,然后为项目创建一个POM。关于插件的开发将会在本专栏后面几期的内容中涉及。

小结

今天的内容主要和大家重点介绍Maven的核心概念,有POM文件,构建过程、阶段和目标,依赖,仓库、Profiles和插件等。

希望通过本期内容你能够对Maven有一个更清晰的认识,下期内容将会用一个项目来对今天的内容进行展开阐述。

推荐与下面的文章连续阅读,效果更佳。

【Maven专栏系列】Maven项目从0到1

点个关注可以及时收到后续文章的更新,如果觉得对你有所帮助,点个赞是对我最大的鼓励!