MENU

自动构建工具 -- Maven

June 4, 2018 • Code

前言:项目依赖管理与构建工具 Maven 学习记录

Maven 初步了解

对 Maven 有初步的认识,同时学会使用 Maven。

什么是 Maven

来自维基:

Apache Maven,是一个软件(特别是 Java 软件)项目管理及自动构建工具,由 Apache 软件基金会所提供。基于项目对象模型(缩写:POM)概念,Maven 利用一个中央信息片断能管理一个项目的构建、报告和文档等步骤。

简单来说,Maven 就是一个提供依赖管理、项目自动构建功能的工具。

  • 依赖管理:对项目依赖库(如 Jar 包)的关系进行管理。

  • 自动构建:自动生成组建工件,包括将计算机源代码编译成二进制码、将二进制码包装成软件包以及运行自动化测试。

Maven 的使用

主要包括新建 Maven 项目及引入依赖。

1. 新建 Maven 项目

创建 Maven 项目的方法有许多,这里以 Intellij IDEA 2018 创建 Maven web 工程为栗子:

  • 新建项目

  • 选择从模板创建 Maven 项目

  • 填写 Maven 坐标信息

  • 完成创建

  • 补全目录

2. 引入依赖

pom.xml 作为 Maven 项目的配置文件,以 XML 的形式包含了项目的所有信息,是 Maven 的核心。

以引入 SpringMVC 为栗:

Maven 进阶

进一步认识 Maven,理解 Maven 中的坐标系统、依赖管理、自动构建、生命周期等。

约定大于配置

Maven 在目录设置方面采用的是「约定大于配置」,即多数人习惯将源码目录命名为 src,那就约定好用 src 目录存放源码,不用专门配置。同样的 clean,compile,package 等也约定好,不需要专门定义。这样既简化了配置文件,同时也降低了学习成本。

标准目录

仓库

Maven 中的仓库是提供依赖构件及插件的存储库,主要有两种类型:本地仓库和远程仓库。

  • 远程仓库:指存放于远程服务器上的依赖存储库,可通过传输协议 file://http:// 将依赖组件下载到本地使用。默认会从 Maven 中央仓库下载,可在 setting.xml配置中央仓库镜像,如将镜像设置为阿里 Maven 公开仓库。
<mirror>
    <id>nexus-aliyun</id>
    <mirrorOf>*</mirrorOf>
    <name>Nexus aliyun</name>
    <url>http://maven.aliyun.com/nexus/content/groups/public</url>
</mirror> 
  • 本地仓库:包含从远程仓库下载的依赖包的缓存和本地的临时构件。每次寻找依赖构件先在本地仓库中寻找,寻不到时再从配置的远程仓库中下载,并缓存到本地仓库重复使用。如要修改本地仓库的位置,可在 ~/.m2/settings.xml 中增加下列配置项(没有该文件则新建):
<settings>  
    <localRepository>新本地仓库的位置</localRepository>  
</settings>  

坐标系统

在 Maven 的世界中,依赖构件和插件都有坐标,Maven 依靠坐标进行准确定位。

坐标主要包括:

  • groupId(必须):对应实际的项目,一般同 jar 包的写法,采用域名倒写的形式。

  • artifactId(必须):对应项目中具体的 Maven 模块名称,在实际的工程中,一个工程中往往包含多个 Maven 模块。

  • version(必须):定义 Maven 项目当前版本。

  • packaging(可选):定义 Maven 打包方式,默认为 jar。打包方式不同会影响生命周期。

  • classifier(不可直接定义):定义输出的附属构件坐标。

以上面创建的工程为栗子:

生成项目后对应的 pom.xml 如下:

其中 SNAPSHOT 后缀代表这是一个快照版本。它与正式版的区别在于当依赖一个快照版本的库,Maven 会首先去远程仓库寻找该库最新的快照版本,而不是先在本地仓库寻找。主要用于开发时的依赖管理,在团队内开发时,如果正在开发的库设置为正式版,那么依赖该库的成员就没有办法获取到最新的库文件,因为 Maven 会先去找缓存在本地仓库的旧版本。SNAPSHOT 版本可以很好解决这一问题。

生命周期

Maven 提供了三套生命周期:clean、default、site,不同生命周期中 phases 相互独立,但同一生命周期中 phases 具体前后依赖关系,即后面的 phases 依赖于前面的 phases,故执行某一 phases 时,同一生命周期中该 phases 前面的 phases 会依次执行。

举个栗子:当执行 post-clean 时,pre-clean 和 clean 会被自动执行,但其他生命周期的 phases 并不受此影响。

生命周期与插件的关系

Maven 中生命周期抽象定义了每个阶段的任务,具体实施者为 Maven 中的插件。每一个阶段可以绑定一个或多个插件。

而插件本身往往又细分为多个目标(goal),以便更加有效地复用代码。故插件的调用采用 插件名称:目标名称 的形式。

内置绑定

Maven 为了使用方便,提供了一些常用生命周期阶段的内置绑定,当调用这些生命周期时直接使用对应插件中的目标。

更多内置绑定如下:

自定义绑定

在 pom.xml 文件中,通过下列代码可将 maven-source-plugin 插件的 jar-no-fork 目标绑定到 verify 阶段中,其它形式可以类推。

<build>  
    <plugins>  
        <groupId>org.apache.maven.plugins</groupId>  
        <artifactId>maven-source-plugin</artifactId>  
        <version>2.1.1</version>  
        <execution>  
            <id>attach-sources</id>   
            <phase>verify</phase>  
            <goals>  
                <goal>jar-no-fork</goal>      
            </goals>  
        </execution>  
    </plugins>  
</build>

pom.xml

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>
 
  <!-- The Basics -->
  <groupId>...</groupId>
  <artifactId>...</artifactId>
  <version>...</version>
  <packaging>...</packaging>
  <dependencies>...</dependencies>
  <parent>...</parent>
  <dependencyManagement>...</dependencyManagement>
  <modules>...</modules>
  <properties>...</properties>
 
  <!-- Build Settings -->
  <build>...</build>
  <reporting>...</reporting>
 
  <!-- More Project Information -->
  <name>...</name>
  <description>...</description>
  <url>...</url>
  <inceptionYear>...</inceptionYear>
  <licenses>...</licenses>
  <organization>...</organization>
  <developers>...</developers>
  <contributors>...</contributors>
 
  <!-- Environment Settings -->
  <issueManagement>...</issueManagement>
  <ciManagement>...</ciManagement>
  <mailingLists>...</mailingLists>
  <scm>...</scm>
  <prerequisites>...</prerequisites>
  <repositories>...</repositories>
  <pluginRepositories>...</pluginRepositories>
  <distributionManagement>...</distributionManagement>
  <profiles>...</profiles>
</project>

主要配置项

pom.xml 文件主要分几大部分:

必须部分
  • modelVersion、groupId、artifactId、version
<?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>xxx.xxx</groupId>
    <artifactId>demo</artifactId>
    <version>1.0-SNAPSHOT</version>
</project>
  • 打包方式:
<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">
  ...
  <!-- 可选值:pom, jar, maven-plugin, ejb, war, ear, rar, par -->
  <packaging>war</packaging>
  ...
</project>
  • 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
                      https://maven.apache.org/xsd/maven-4.0.0.xsd">
  ...
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.0</version>
      <type>jar</type>
    </dependency>
    ...
  </dependencies>
  ...
</project>
Build Settings

According to the POM 4.0.0 XSD, the build element is conceptually divided into two parts: there is a BaseBuild type which contains the set of elements common to both build elements (the top-level build element under project and the build element under profiles, covered below); and there is the Build type, which contains the BaseBuild set as well as more elements for the top level definition. Let us begin with an analysis of the common elements between the two.

build 的概念结构如下

其中 project build 与 profile build 如下面的例子:

<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">
  ...
  <!-- "Project Build" contains more elements than just the BaseBuild set -->
  <build>...</build>
 
  <profiles>
    <profile>
      <!-- "Profile Build" contains a subset of "Project Build"s elements -->
      <build>...</build>
    </profile>
  </profiles>
</project>

关于 POM 更多内容参见 官网-POM

超级 POM

超级 POM 可以看作是所有 POM 的父类,所有 POM 都隐式继承这一文件。它规定了 Maven 中的默认设置,包含 Maven 中的所谓「约定」也是在这一文件中规定了。
文件内容如下:

<project>
  <modelVersion>4.0.0</modelVersion>
 
  <repositories>
    <repository>
      <id>central</id>
      <name>Central Repository</name>
      <url>http://repo.maven.apache.org/maven2</url>
      <layout>default</layout>
      <snapshots>
        <enabled>false</enabled>
      </snapshots>
    </repository>
  </repositories>
 
  <pluginRepositories>
    <pluginRepository>
      <id>central</id>
      <name>Central Repository</name>
      <url>http://repo.maven.apache.org/maven2</url>
      <layout>default</layout>
      <snapshots>
        <enabled>false</enabled>
      </snapshots>
      <releases>
        <updatePolicy>never</updatePolicy>
      </releases>
    </pluginRepository>
  </pluginRepositories>
 
  <build>
    <directory>${project.basedir}/target</directory>
    <outputDirectory>${project.build.directory}/classes</outputDirectory>
    <finalName>${project.artifactId}-${project.version}</finalName>
    <testOutputDirectory>${project.build.directory}/test-classes</testOutputDirectory>
    <sourceDirectory>${project.basedir}/src/main/java</sourceDirectory>
    <scriptSourceDirectory>src/main/scripts</scriptSourceDirectory>
    <testSourceDirectory>${project.basedir}/src/test/java</testSourceDirectory>
    <resources>
      <resource>
        <directory>${project.basedir}/src/main/resources</directory>
      </resource>
    </resources>
    <testResources>
      <testResource>
        <directory>${project.basedir}/src/test/resources</directory>
      </testResource>
    </testResources>
    <pluginManagement>
      <!-- NOTE: These plugins will be removed from future versions of the super POM -->
      <!-- They are kept for the moment as they are very unlikely to conflict with lifecycle mappings (MNG-4453) -->
      <plugins>
        <plugin>
          <artifactId>maven-antrun-plugin</artifactId>
          <version>1.3</version>
        </plugin>
        <plugin>
          <artifactId>maven-assembly-plugin</artifactId>
          <version>2.2-beta-5</version>
        </plugin>
        <plugin>
          <artifactId>maven-dependency-plugin</artifactId>
          <version>2.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-release-plugin</artifactId>
          <version>2.0</version>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>
 
  <reporting>
    <outputDirectory>${project.build.directory}/site</outputDirectory>
  </reporting>
 
  <profiles>
    <!-- NOTE: The release profile will be removed from future versions of the super POM -->
    <profile>
      <id>release-profile</id>
 
      <activation>
        <property>
          <name>performRelease</name>
          <value>true</value>
        </property>
      </activation>
 
      <build>
        <plugins>
          <plugin>
            <inherited>true</inherited>
            <artifactId>maven-source-plugin</artifactId>
            <executions>
              <execution>
                <id>attach-sources</id>
                <goals>
                  <goal>jar</goal>
                </goals>
              </execution>
            </executions>
          </plugin>
          <plugin>
            <inherited>true</inherited>
            <artifactId>maven-javadoc-plugin</artifactId>
            <executions>
              <execution>
                <id>attach-javadocs</id>
                <goals>
                  <goal>jar</goal>
                </goals>
              </execution>
            </executions>
          </plugin>
          <plugin>
            <inherited>true</inherited>
            <artifactId>maven-deploy-plugin</artifactId>
            <configuration>
              <updateReleaseInfo>true</updateReleaseInfo>
            </configuration>
          </plugin>
        </plugins>
      </build>
    </profile>
  </profiles>
 
</project>

settings.xml

settings.xml 为 Maven 的配置文件,可在两个目录下找到:

// 全局配置
${maven.home}/conf/settings.xml 

// 当前用户配置
${user.home}/.m2/settings.xml

主要框架如下:

<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
                      https://maven.apache.org/xsd/settings-1.0.0.xsd">
  <localRepository/>
  <interactiveMode/>
  <usePluginRegistry/>
  <offline/>
  <pluginGroups/>
  <servers/>
  <mirrors/>
  <proxies/>
  <profiles/>
  <activeProfiles/>
</settings>

简单属性

<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
                      https://maven.apache.org/xsd/settings-1.0.0.xsd">
  <!-- 本地仓库位置,默认值 ${user.home}/.m2/repository -->
  <localRepository>${user.home}/.m2/repository</localRepository>
  
  <!-- Maven 是否接受用户输入,true 表示接受,默认true -->
  <interactiveMode>true</interactiveMode>
  
  <!-- Maven 是否使用 ${user.home}/.m2/plugin-registry.xml 管理插件版本,true 表示使用,默认为false -->
  <usePluginRegistry>false</usePluginRegistry>
  
  <!-- 构建系统是否在离线模式下执行,true 表示使用,默认为false -->
  <offline>false</offline>
  ...
</settings>

Plugin Groups

指定插件的默认 groupid,当使用插件没有加上 groupid 时,则通过这一属性自动加上,该属性默认包含 org.apache.maven.pluginsorg.codehaus.mojo

举个栗子:

<pluginGroups>  
    <pluginGroup>org.mortbay.jetty</pluginGroup>  
</pluginGroups>  

则在使用时可直接用 mvn jetty:run

Servers

前面提到,在 POM 中可配置远程仓库。访问远程仓库更加详细的信息如端口、登陆密码可在此配置。

<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
                      https://maven.apache.org/xsd/settings-1.0.0.xsd">
  ...
  <servers>
    <server>
      <!-- id 需与远程仓库配置的 id 对应 -->
      <id>server001</id>
      <username>my_login</username>
      <password>my_password</password>
      <privateKey>${user.home}/.ssh/id_dsa</privateKey>
      <passphrase>some_passphrase</passphrase>
      <filePermissions>664</filePermissions>
      <directoryPermissions>775</directoryPermissions>
      <configuration></configuration>
    </server>
  </servers>
  ...
</settings>

Mirrors

配置远程仓库镜像,上面有提及。

详情可参见 Using Mirrors for Repositories

Proxies

配置代理服务器

Profiles

包含四个元素:

  • Activation:通过 Activation 来指定 profile 生效的环境

  • Properties:定义属性(即K-V对),通过 ${x} 引入

  • Repositories:定义该 Profiles 中使用到的远程依赖仓库

  • pluginRepositories:定义该 Profiles 中使用到的远程插件仓库

如果 Profiles 属性被激活,那么会覆盖定义在 POM 中相同 ID 的属性

Active Profiles

这一属性定义总是被激活的 Profiles,无视环境要求,即屏蔽 Profiles 属性中的 Activation。

关于 Settings 更多内容参见 官网-Settings

依赖管理

依赖管理是 Maven 的一大功能。随着现代项目的规模越来越庞大,项目的依赖及依赖间的关系也愈发复杂,因此引入依赖管理工具是十分必要的。

Maven 中的依赖关系可以从以下几个方面理解:

  • 依赖范围
  • 依赖传递
  • 依赖可选
  • 依赖冲突
  • 依赖排除

下面就这几个方面进行阐述。

依赖范围

依赖范围指引入的依赖在什么时候发挥作用,用 <scope> 标签指定。

依赖范围 说明
compile(默认) 编译、运行、测试均有效
provided 编译、测试有效
runtime 运行有效
test 测试有效
system 类似 provided,区别在于需要用 systemPath 标签明确 Jar 包路径,Maven 不会去仓库中寻找
import (only available in Maven 2.0.9 or later) 可导入定义在 dependencyManagement 元素中的依赖

依赖传递

举个栗子:当项目 B 依赖于项目 A,那么项目 C 依赖于项目 B,即 C -> B -> A,对项目 C 而言,依赖项目 B 称为第一依赖,依赖项目 A 称为第二依赖。

上图说明了:

  • 当 B 引入依赖 A 的范围为 provided 及 test 时,C 引入依赖 B 并不会引入 A

  • 当 B 引入依赖 A 的范围为 compile 时,C 引入依赖 B 会引入 A,A 在 C 中的范围与 B 在 C 中的范围相同。

  • 当 B 引入依赖 A 的范围为 runtime 时,情况与上一种相同,除了一种情况: B 在 C 中范围为 compiler,那 A 在 C 中的范围为 runtime。

依赖可选

pom.xml 文件中, Optional 元素表示该依赖是否可选,默认是false。如果为true,则表示该依赖不会传递下去,如果为false,则会传递下去。

依赖冲突

依赖冲突指引入依赖于版本上产生冲突。

  • 栗子一:项目 A 引入 JUnit 3.8.1,项目 B 同时依赖于 A 和 JUnit 4.12,在 JUnit 版本上就产生冲突。

  • 栗子二:项目 A 同时依赖于 JUnit 3.8.1 和 JUnit 4.12,也会发生冲突。

上面两个栗子都会发生冲突,但是默认情况下 Maven 还是可以正常工作,因为当发生冲突时 Maven 会遵循就近原则自动处理冲突。

  • 针对栗子一:在项目 B 中,直接引入的 JUnit 3.8.1 比间接引入 JUnit 4.12 路径短,故选用 JUnit 3.8.1,A 和 B 同时依赖 JUnit 3.8.1。

  • 针对栗子二:越下面越近。如果 JUnit 3.8.1 在 JUnit 4.12 之后声明,则使用 JUnit 3.8.1,反之使用 JUnit 4.12。

依赖排除

当依赖发生冲突,并且无法自动解决时,需要手动进行依赖排除。

  • Exclusions 标签手动排除
<properties>
    <unitils.version>3.4.2</unitils.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.unitils</groupId>
        <artifactId>unitils-testng</artifactId>
        <version>${unitils.version}</version>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <artifactId>junit</artifactId>
                <groupId>junit</groupId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.unitils</groupId>
        <artifactId>unitils-spring</artifactId>
        <version>${unitils.version}</version>
        <scope>test</scope>
    </dependency>
</dependencies>
  • Intellij IDEA 图形界面排除

查看 POM 文件结构图

显示如下,点击被红线关联的 Jar 包,会出现红色虚线指明冲突包

在想排除的 Jar 包上右键点击 Exclude 即可自动在 pom.xml 文件中增加 exclusions 标签。

如果想进一步分析依赖关系,可通过在工程根目录输入命令 mvn dependency:tree -Dverbose 查看依赖树 及 mvn dependency:analyze 分析依赖关系。

Maven 高级进阶

针对更高级的 Maven 用法,如 Maven 多模块继承和插件,可以参考《Maven 实战》和 Maven提高篇系列

参考

Last Modified: July 5, 2018
Archives QR Code
QR Code for this page
Tipping QR Code