Maven 干货 全篇共:28232 字 预计阅读时间:110 分钟 建议收藏!

文章结构图

Maven 简介

Maven 这个词可以翻译为“知识的积累”,也可以翻译为“专家”或“内行”。Maven 是一个跨平台的项目管理工具。主要服务于基于 Java 平台的项目构建、依赖管理和项目信息管理。

仔细想想,作为程序猿,除了编写源代码,我们每天有相当一部分时间花在了编译、运行单元测试、生成文档、打包和部署等繁琐且不起眼的工作上,这就是构建。

如果我们现在还手工这样做,那成本也太高了,Maven 是一款优秀的构建工具,让这一系列工作完全自动化,使得软件的构建可以像全自动流水线一样,只需要一条简单的命令,所有繁琐的步骤都能够自动完成,很快就能得到最终结果。

Maven 不仅是构建工具,还是一个依赖管理工具和项目信息管理工具。

它提供了中央仓库,能帮我们自动下载构件。几乎任何 Java 应用都会借用一些第三方的开源类库,这些类库都可通过依赖的方式引入到项目中来。Maven 还能帮助我们管理原本分散在项目中各个角落的项目信息,包括项目描述、开发者列表、版本控制系统地址、许可证、缺陷管理系统地址等。这些微小的变化看起来很琐碎,并不起眼,但却在不知不觉中为我们节省了大量寻找信息的时间。

使用 Maven 还能享受一个额外的好处,即 Maven 对于项目目录结构、测试用例命名方式等内容都有既定的规整,只要遵循了这些成熟的规则,用户在项目间切换的时候就免去了额外的学习成本,可以说是约定优于配置。

Maven 入门

编写POM

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

现在先为 Hello World 项目编写一个最简单的 pom.xml。首先创建一个名为 hello-world 的文件夹,打开文件夹,新建一个名为 pom.xml 的文件,其内容如下:

<?xml version = "1.0" encoding = "UTF-8"?>

  
    4.0.0

    com.mk.study
    hello-word
    1.0-SNAPSHOT
    Maven Hello Word Project

第一行是 XML 头,指定了该 xml 文档的版本和编码方式。紧接着是 project 元素,project 是所有 pom.xml 的根元素,它还声明了一些 POM 相关的命名空间及 xsd 元素,虽然这些属性不是必须的,但使用这些属性能够让第三方工具(如 IDE 中的 XML 编辑器)帮助我们快速编辑 POM。

根元素下的第一个元素 modelVersion 指定了当前 POM 模型的版本,对于 Maven 2 及 Maven 3 来说, 它只能是 4.0.0。

groupId 定义了项目属于哪个组,这个组往往和项目所在的组织或公司存在关联。譬如在 googlecode 上建立了一个名为 myapp 的项目,那么 groupId 就应该是 com.googlecode.myapp。

artifactId 定义了当前 Maven 项目在组中唯一的 ID。

顾名思义,version 指定了 Hello World 项目当前的版本——1.0-SNAPSHOT。SNAPSHOT 意为快照,说明项目还处于开发中,是不稳定的版本。

最后一个 name 元素声明了一个对于用户更为友好的项目名称,虽然这不是必须的,但还是推荐为每个 POM 声明 name,以方便信息交流。

没有任何实际的 Java 代码,我们就能够定义一个 Maven 项目的 POM,这体现了 Maven 的一大优点,它能让项目对象模型最大程度地与实际代码相独立,我们可以称之为解耦,或者正交性。这在很大程度上避免了 Java 代码和 POM 代码的相互影响。比如当项目需要升级版本时,只需要修改 POM,而不需要更改 Java 代码;而在 POM 稳定之后,日常的 Java 代码开发工作基本不涉及 POM 的修改。

编写主代码

默认情况,Maven 假设项目主代码位于 src/main/java 目录,我们遵循 Mavan 的约定,创建该目录,然后在该目录下创建文件 com/mk/study/helloworld/HelloWorld.java,其内容如下:

package com.mk.study.helloworld;

public class HelloWorld{
 
 public String sayHello(){
  return "Hello World";
 }
 
 public static void main(String[] args){
  System.out.print(new HelloWorld().sayHello());
 }
}

首先,在绝大多数情况下,应该把项目主代码放到 src/main/java 目录下(遵循 Maven 的约定),而无须额外的配置,Maven 会自动搜该目录找到项目主代码。其次,该 Java 类的包名是 com.mk.study.helloworld ,这与之前在 POM 中定义的 groupId 和 artifactId 相吻合。一般来说,项目中 Java 类的包名都应该基于项目的 groupId 和 artifactId,这样更加清晰,更加符合逻辑,也方便搜索构件或者 Java 类。

代码编写完毕后,使用 Maven 进行编译,在项目根目录下运行 mvn clean compile 会得到如下输出:

INFO] Scanning for projects...
[INFO]
[INFO] ----------------------< com.mk.study:hello-word >-----------------------
[INFO] Building Maven Hello Word Project 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ hello-word ---
[INFO] Deleting C:~Desktophello-word	arget
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ hello-word ---
[WARNING] Using platform encoding (GBK actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory C:~Desktophello-wordsrcmainresources
[INFO]
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ hello-word ---
[INFO] Changes detected - recompiling the module!
[WARNING] File encoding has not been set, using platform encoding GBK, i.e. build is platform dependent!
[INFO] Compiling 1 source file to C:~Desktophello-word	argetclasses
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  0.936 s
[INFO] Finished at: 2022-12-31T21:45:37+08:00
[INFO] ------------------------------------------------------------------------

clean 告诉 Maven 清理输出目录 target/,compile 告诉 Maven 编译项目主代码,

从输出中看到 Maven 首先执行了 clean 任务,删除 target/ 目录,默认情况下,Maven 构建的所有输出都在 target/ 目录中;接着执行 resources 任务(未定义项目资源, 暂且略过);最后执行 compile 任务,将项目主代码编译至 target/classes 目录(编译好的类为 com/mk/study/helloworld/HelloWorld.class)。

编写测试代码

测试代码与主代码不同,项目的主代码会被打包到最终的构件中(如 jar),而测试代码只在运行测试时用到,不会被打包。默认情况下,测试代码目录是 src/test/java。因此,在编写测试用例之前,应当先创建该目录。

我们这里使用 JUnit,首先需要为 Hello World 项目添加一个 JUnit 依赖:

<?xml version = "1.0" encoding = "UTF-8"?>

  
 4.0.0
 
 com.mk.study
 hello-word
 1.0-SNAPSHOT
 Maven Hello Word Project
 
  
   junit
   junit
   4.10
   test
  
 

代码中添加了 dependencies 元素,该元素下可以包含多个 dependency 元素以声明项目的依赖。Maven 会自动访问中央仓库(http://repo1.maven.org/maven2/),下载需要的文件。

上诉 POM 代码中还有一个值为 test 的元素 scope,scope 为依赖范围,若依赖范围为 test 则表示该依赖只对测试有效。测试代码中的 import JUnit 代码是没有问题的,但是如果在主代码中用 import JUnit 代码,就会造成编译错误。如果不声明依赖范围,那么默认值就是 compile,表示该依赖对主代码和测试代码都有效。

接下来编写测试类,在 src/test/java 目录下创建 HelloWorldTest.java 文件,内容如下:

package com.mk.study.helloworld;

import static org.junit.Assert.assertEquals;
import org.junit.Test;

public class HelloWorldTest{
 
 @Test
 public void testSayHello(){
  HelloWorld helloWorld = new HelloWorld();
  String result = helloWorld.sayHello();
  assertEquals("Hello Maven",result);
 }
 
}

一个典型的单元测试包含三个步骤:准备测试类及数据、执行要测试的行为、检查结果。

上述样例首先初始化了一个要测试的 HelloWorld 实例,接着执行该实例的 sayHello() 方法并保存结果到 result 变量中 ,最后使用 JUnit 框架的 Assert 类检查结果是否为我们期望的 “Hello Maven”。在 JUnit 3 中,约定所有需要执行测试的方法都以 test 开头,这里使用了 JUnit 4,但仍然遵循这一约定。在 JUnit 4 中,需要执行的测试方法都应该以 @Test 进行标注。

测试用例编写完毕之后就可以调用 Maven 执行测试,运行 mvn clean test :

[INFO] Scanning for projects...
[INFO]
[INFO] ----------------------< com.mk.study:hello-word >-----------------------
[INFO] Building Maven Hello Word Project 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ hello-word ---
[INFO] Deleting C:~Desktophello-word	arget
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ hello-word ---
[WARNING] Using platform encoding (GBK actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory C:~Desktophello-wordsrcmainresources
[INFO]
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ hello-word ---
[INFO] Changes detected - recompiling the module!
[WARNING] File encoding has not been set, using platform encoding GBK, i.e. build is platform dependent!
[INFO] Compiling 1 source file to C:~Desktophello-word	argetclasses
[INFO]
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ hello-word ---
[WARNING] Using platform encoding (GBK actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory C:~Desktophello-wordsrc	estresources
[INFO]
[INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ hello-word ---
[INFO] Changes detected - recompiling the module!
[WARNING] File encoding has not been set, using platform encoding GBK, i.e. build is platform dependent!
[INFO] Compiling 1 source file to C:~Desktophello-word	arget	est-classes
[INFO]
[INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ hello-word ---
[INFO] Surefire report directory: C:~Desktophello-word	argetsurefire-reports

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running com.mk.study.helloworld.HelloWorldTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.053 sec

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  3.061 s
[INFO] Finished at: 2022-12-31T23:40:02+08:00
[INFO] ------------------------------------------------------------------------

我们看到 compile:testCompile 任务执行成功了,测试代码通过编译后在 target/test-classes 下生成了二进制文件,紧接着 surefire:test 任务运行测试,surefire 是 Maven 中负责执行测试的插件,这里它运行测试用例 HelloWorldTest,并输出测试报告,显示一共运行了多少测试 ,失败了多少,出错了多少,跳过了多少。显然,我们是测试通过了。

这里记录一个问题

当所有代码编写完成,执行 mvn clean test 命令的时候,并不是那么的顺序,maven 报出了如下问题:

[INFO] Scanning for projects...
……
[INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ hello-word ---
[INFO] Surefire report directory: C:~Desktophello-word argetsurefire-reports
Downloading from aliyunmaven: https://maven.aliyun.com/repository/spring/org/apache/maven/surefire/surefire-junit4/2.12.4/surefire-junit4-2.12.4.pom
[INFO] Failure detected.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.973 s
[INFO] Finished at: 2022-12-31T23:52:37+08:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:2.12.4:test (default-test) on project hello-word: Unable to generate classpath: org.apache.maven.artifact.resolver.ArtifactResolutionException: Unable to get dependency information for org.apache.maven.surefire:surefire-junit4:jar:2.12.4: Failed to retrieve POM for org.apache.maven.surefire:surefire-junit4:jar:2.12.4: Could not transfer artifact org.apache.maven.surefire:surefire-junit4:pom:2.12.4 from/to aliyunmaven (https://maven.aliyun.com/repository/spring): Transfer failed for https://maven.aliyun.com/repository/spring/org/apache/maven/surefire/surefire-junit4/2.12.4/surefire-junit4-2.12.4.pom
[ERROR] org.apache.maven.surefire:surefire-junit4:jar:2.12.4
[ERROR]
[ERROR] from the specified remote repositories:
[ERROR] aliyunmaven (https://maven.aliyun.com/repository/spring, releases=true, snapshots=false)
[ERROR] Path to dependency:
[ERROR] 1) dummy:dummy:jar:1.0
[ERROR]
[ERROR] : java.lang.RuntimeException: Unexpected error: java.security.InvalidAlgorithmParameterException: the trustAnchors parameter must be non-empty
[ERROR] -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionException

从 [ERROR] 信息中可以看出来,原因是因为无法下载 org.apache.maven.surefire:surefire-junit4:jar、org.apache.maven.surefire:surefire-junit4:pom 两个依赖文件。但是在 阿里云仓库 中可以搜到,阿里云有多个仓库:central、public、spring,但无论换成哪一个,即使仓库中有,也都无法下载。最后灵机一动,将下载不下来的 jar、pom 从网页中下载下来,然后 copy 到本地对应目录,再次编译,执行成功。

对于以后遇到的一些无论是网络原因还是其他原因,重复几次 Maven 无法下载的文件,也可以通过直接到 阿里云仓库 中去下载,然后将其 copy 到本地仓库中相应的目录来解决。

打包和运行

jar 包

将项目进行编译、测试之后,下一个重要步骤就是打包(package)。在没有指定打包类型时,默认使用打包类型是 jar。可以简单执行命令 mvn clean package 进行打包,之后看到如下输出:

[INFO] Scanning for projects...
[INFO]
[INFO] ----------------------< com.mk.study:hello-word >-----------------------
[INFO] Building Maven Hello Word Project 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ hello-word ---
[INFO] Deleting C:~Desktophello-word	arget
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ hello-word ---
[WARNING] Using platform encoding (GBK actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory C:~Desktophello-wordsrcmainresources
[INFO]
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ hello-word ---
[INFO] Changes detected - recompiling the module!
[WARNING] File encoding has not been set, using platform encoding GBK, i.e. build is platform dependent!
[INFO] Compiling 1 source file to C:~Desktophello-word	argetclasses
[INFO]
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ hello-word ---
[WARNING] Using platform encoding (GBK actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory C:~Desktophello-wordsrc	estresources
[INFO]
[INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ hello-word ---
[INFO] Changes detected - recompiling the module!
[WARNING] File encoding has not been set, using platform encoding GBK, i.e. build is platform dependent!
[INFO] Compiling 1 source file to C:~Desktophello-word	arget	est-classes
[INFO]
[INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ hello-word ---
[INFO] Surefire report directory: C:~Desktophello-word	argetsurefire-reports

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running com.mk.study.helloworld.HelloWorldTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.067 sec

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

[INFO]
[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ hello-word ---
[INFO] Building jar: C:~Desktophello-word	argethello-word-1.0-SNAPSHOT.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  2.673 s
[INFO] Finished at: 2023-01-01T00:12:07+08:00
[INFO] ------------------------------------------------------------------------

类似地,Maven 会在打包之前执行编译、测试等操作。这里看到 jar:jar 任务负责打包,实际上就是 jar 插件的 jar 目标将项目主代码打包成一个名为 hello-world-1.0-SNAPSHOT.jar 的文件。该文件也位于 target/ 输出目录中,它是根据 artifact-version.jar 规整进行命名的,如有需要,还可以使用 finalName 来自定义该文件的名称。

如何才能让其他的 Maven 项目直接引用这个 jar 呢?还需要一个安装的步骤,执行 mvn clean install:

[INFO] Scanning for projects...
[INFO]
[INFO] ----------------------< com.mk.study:hello-word >-----------------------
[INFO] Building Maven Hello Word Project 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ hello-word ---
[INFO] Deleting C:~Desktophello-word	arget
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ hello-word ---
[WARNING] Using platform encoding (GBK actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory C:~Desktophello-wordsrcmainresources
[INFO]
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ hello-word ---
[INFO] Changes detected - recompiling the module!
[WARNING] File encoding has not been set, using platform encoding GBK, i.e. build is platform dependent!
[INFO] Compiling 1 source file to C:~Desktophello-word	argetclasses
[INFO]
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ hello-word ---
[WARNING] Using platform encoding (GBK actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory C:~Desktophello-wordsrc	estresources
[INFO]
[INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ hello-word ---
[INFO] Changes detected - recompiling the module!
[WARNING] File encoding has not been set, using platform encoding GBK, i.e. build is platform dependent!
[INFO] Compiling 1 source file to C:~Desktophello-word	arget	est-classes
[INFO]
[INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ hello-word ---
[INFO] Surefire report directory: C:~Desktophello-word	argetsurefire-reports

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running com.mk.study.helloworld.HelloWorldTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.042 sec

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

[INFO]
[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ hello-word ---
[INFO] Building jar: C:~Desktophello-word	argethello-word-1.0-SNAPSHOT.jar
[INFO]
[INFO] --- maven-install-plugin:2.4:install (default-install) @ hello-word ---
[INFO] Installing C:~Desktophello-word	argethello-word-1.0-SNAPSHOT.jar to C:~.m2repositorycommkstudyhello-word1.0-SNAPSHOThello-word-1.0-SNAPSHOT.jar
[INFO] Installing C:~Desktophello-wordpom.xml to C:~.m2repositorycommkstudyhello-word1.0-SNAPSHOThello-word-1.0-SNAPSHOT.pom
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  2.660 s
[INFO] Finished at: 2023-01-01T10:28:23+08:00
[INFO] ------------------------------------------------------------------------

打包之后又执行了安装任务 install:install 。从输出可以看到该任务将项目输出的 jar 安装到了 Maven 本地仓库。

至此,我们已经体验了 Maven 最主要的命令:mvn clean compile、mvn clean test、mvn clean package、mvn clean install。执行 test 之前是会先执行 compile 的,执行 package 之前是会先执行 test 的,而类似地,install 之前会执行 package。

运行 jar 包

到目前为止,我们还没有运行过 Hello World 项目,别忘了,在 HelloWorld.java 类中是有一个 main 方法的。默认打包生成的 jar是不能够直接运行的,因为带有 main 方法的信息不会添加到 manifest 中(打开 jar 文件中的 META-INF/MANIFEST 文件,将无法看到 Main-Class 一行)。

Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Built-By: mk
Created-By: Apache Maven 3.6.3
Build-Jdk: 1.8.0_41

为了生成可执行的 jar 文件,需要借助 maven-shade-plugin,修改 POM 文件,配置该插件如下:

<?xml version = "1.0" encoding = "UTF-8"?>

  
 4.0.0
 
 com.mk.study
 hello-world
 1.0-SNAPSHOT
 Maven Hello World Project
 
  
   junit
   junit
   4.10
   test
  
 
 
  
   
    org.apache.maven.plugins
    maven-shade-plugin
    3.2.1
    
     
      package
      
       shade
      
      
       
        
         com.mk.study.helloworld.HelloWorld
        
       
      
     
    
   
  
 

我们配置了mainClass 为 com.mk.study.helloworld.HelloWorld,项目在打包时会将该信息放到 MANIFEST 中。现在执行 mvn clean install ,待构建完成之后打开 target/ 目录,可以看到 hello-world-1.0-SNAPSHOT.jar 和 original-hello-world-1.0-SNAPSHOT.jar,前者是带有 Main-Class 信息的可运行 jar,后者是原始的 jar,在 hello-world-1.0-SNAPSHOT.jar 的 MANIFEST.MF 文件中可以看到,它包含了 Main-Class: com.mk.study.helloworld.HelloWorld 一行信息:

Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Built-By: mk
Created-By: Apache Maven 3.6.3
Build-Jdk: 1.8.0_41
Main-Class: com.mk.study.helloworld.HelloWorld

进入 target 目录中,通过命令 java -jar hello-world-1.0-SNAPSHOT.jar 执行 jar 文件,可以看到控制台输出:

~hello-word	arget> java -jar hello-world-1.0-SNAPSHOT.jar
Hello Maven
~hello-word	arget>

控制台输出 Hello Maven ,这正是我们所期望的。

war 包

基于 Java 的 Web 应用,其标准的打包方式是 war。war 与 jar 类似,只不过它可以包含更多的内容,如 JSP 文件、Servlet、Java 类、web.xml配置文件、依赖 jar 包、静态 web 资源(如 HTML、CSS、JavaScript 文件)等。

其实对于 Maven 来说,jar 与 war 只是打包方式的不同,只需要调整 packaging 元素值为 war 即可。我们主要是介绍 Maven 的用法,并不会创建一个 web 工程,主要介绍下 war 包的组成结构。


    ……
 com.mk.study
 hello-world
    war
    1.0-SNAPSHOT
 Maven Hello World Project
    ……

一个典型的 war 文件会有如下目录结构:

-war/
 + META-INF/
 + WEB-INF/
 | + classes/
 | | + ServletA.class
 | | + config.properties
 | | + ……
 | |
 | + lib/
 | | + dom4j-1.4.1.jar
 | | + main-1.4.1.jar
 | | + ……
 | |
 | + web.xml
 |
 + img/
 |
 + css
 |
 + js
 |
 + index.html
 + sample.jsp

一个 war 包下至少包含两个子目录:META-INF 和 WEB-INF。前者包含了一些打包元数据信息,后者是 war 包的核心,WEB-INF 下必须包含一个 web 资源表述文件 web.xml,它的子目录 classes 包含所有该 web 项目的类,而另一个子目录 lib 则包含所有该 web 项目的依赖 jar 包,classes 和 lib 目录都会在运行的时候被加入到 Classpath 中。除了 META-INF 和 WEB-INF 外,一般的 war 包都会包含很多 web 资源,比如 html 或者 jsp 文件,还有一些文件夹比如 img、css 和 js。

web 项目的类及资源文件同一般 jar 项目一样,默认位置都是 src/main/java/ 和 src/main/resources/,测试类及测试资源文件的默认位置是 src/test/java/ 和 src/test/resources/。web 项目比较特殊的地方在于:它还有一个 web 资源目录,其默认位置是 src/main/webapp/。一个典型的 web 项目的 Maven 目录结构如下:

+ project
  |
  + pom.xml
  |
  + src/
    + main/
    | + java/
    | | + ServletA.java
    | | + ……
    | |
    | + resources/
    | | + config.properties
    | | + ……
    | |
    | + webapp/
    |    + WEB-INF/
    |    | + web.xml
    |    |
    |    + img/
    |    |
    |    + css/
    |    |
    |    + js/
    |    +
    |    + index.html
    |    + sample.jsp
    | 
    + test/
      + java/
      + resources/

在 src/main/webapp/ 目录下,必须包含一个子目录 WEB-INF,该子目录还必须要包含 web.xml 文件。src/main/webapp 目录下的其他文件和目录包括 html、jsp、css、JavaScript等,它们与 war 包中的 web 资源完全一致。

war 包中有一个 lib 目录包含所有依赖 jar 包,但 Maven 项目结构中没有这样一个目录,这是因为依赖都配置在 POM 中,Maven 在用 war 方式打包的时候会根据 POM 的配置从本地仓库复制相应的 jar 文件。

坐标和依赖

依赖详解

Maven 定义了这样一组规整:世界上任何一个构件都可以使用 Maven 坐标唯一标识,我们只要提供正确的坐标元素,Maven 就能找到对应的构件。而一些 Maven 坐标是通过一些元素定义的,他们是 groupId、artifactId、version、packaging、classifier。如下:


    ……
 com.mk.study
 hello-world
    war
    1.0-SNAPSHOT
 Maven Hello World Project
    ……

下面详细解释下各个坐标元素:

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

依赖配置

上面罗列了一些基本的依赖配置,其实一个依赖声明可以包含如下一些元素:


 ……
    
     
         ……
            ……
            ……
            ……
            ……
            ……
            
             ……
                ……
            
        
        ……
    
    ……

依赖范围

Maven 在编译项目主代码的时候需要使用一套 classpath,在编译和执行测试代码的时候会使用另外一套 classpath,最后,实际运行 Maven 项目的时候,又会使用一套 classpath。依赖范围就是用来控制依赖于三种 classpath 的关系,Maven 有以下几种依赖范围:

上述除 import 以外的各种依赖范围与三种 classpath 的关系如下表:

Scope

编译 classpath

测试 classpath

运行时 classpath

例子

compile

Y

Y

Y

spring-ore

test

Y

JUnit

provided

Y

Y

servlet-api

runtime

Y

Y

JDBC 驱动实现

system

Y

Y

本地的,Maven 仓库之外的类库文件

传递性依赖

考虑一个基于 Spring Framework 的 项目,如果不使用 Maven ,那么在项目中就需要手动下载相关依赖。由于 Spring Framework 又会依赖于其他开源类库,因此实际项目中往往会下载一个很大的如 spring-framework-2.5.6-with-dependencies.zip 的包,这里包含了所有 Spring Framework 的 jar 包,以及所有它依赖的其他 jar 包。这么做往往就引入了很多不必要的依赖。另一种做法是只下载 spring-framework-2.5.6.zip 这样一个包,这里不包含其他相关依赖,到实际使用的时候,根据出错信息,或者查询相关文档,加入需要的其他依赖。很显然,这也是一件非常麻烦的事情。

Maven 的传递性依赖机制可以很好地解决这一问题。假设我们有一个项目 myApp,该项目有一个 org.springframework:spring-core:2.5.6 的依赖,而实际上 spring-core 也有它自己的依赖,我们可以直接访问位于中央仓库的该构件的 POM:http://repo1.maven.org/maven2/org/springframework/spring-core/2.5.6/spring-core-2.5.6.pom。该文件包含了一个 commons-logging 依赖,如下:


 commons-logging
    commons-logging
    1.1.1

该依赖没有声明依赖范围,那么其依赖范围就是默认的 compile。myApp 有一个 compile 范围的 spring-core 依赖,spring-core 有一个 compile 范围的 commons-logging 依赖,那么 commons-logging 就会称为 myApp 的 compile 范围依赖,commons-logging 是 myApp 的一个传递性依赖,如下所示:

传递性依赖

有了传递性依赖机制,不用担心引入多于的依赖。Maven 会解析各个直接依赖的 POM,将那些必要的间接依赖,以传递性依赖的形式引入到当前项目中。

以来范围不仅可以控制依赖于三种 classpath 的关系,还对传递性依赖产生影响。假设 A 依赖于 B,B 依赖于 C,我们说 A 对于 B 是第一直接依赖,B 对于 C 是第二直接依赖, A 对于 C 是传递性依赖。第一直接依赖的范围和第二直接依赖的范围决定了传递性依赖的范围,如下表,最左边一列表示第一直接依赖范围,最上面一行表示第二直接依赖范围,中间的交叉单元格则表示传递性依赖范围:


compile

test

provided

runtime

mpile

compile

runtime

test

test

test

provided

provided

provided

provided

runtime

runtime

runtime

依赖调解

传递性依赖机制,一方面大大简化和方便了依赖声明,另一方面,大部分情况下我们只需要关心项目的直接依赖是什么,而不用考虑这些直接依赖会引入什么传递性依赖。

但是有时候,传递性依赖也会造成一些问题,例如,项目 A 有这样的依赖关系:A -> B -> C -> X(1.0)、A -> D -> X(2.0),X 是 A 的传递性依赖,但是两条依赖路径上有两个版本的 X,那么哪个 X 会被 Maven 解析使用呢?Maven 依赖调解的第一原则是:路径最近者优先。因此,X(2.0) 会被解析使用。

依赖调解第一原则不能解决所有问题,比如这样的依赖关系:A -> B ->Y(1.0)、A -> C -> Y(2.0)。从 Maven 2.0.9 开始,为了尽可能避免构建的不确定性,Maven 定义了依赖调解的第二原则:第一声明者优先。在依赖路径长度相等的前提下,在 POM 中依赖声明的顺序决定了谁会被解析使用,顺序最靠前的那个依赖优胜。

可选依赖

假设有这样一个依赖关系:A -> B、B -> X(可选)、B -> Y(可选)。根据传递性依赖定义,由于这里 X、Y 是可选依赖,依赖将不会得以传递。如下图所示:

可选依赖

为什么要是用可选依赖这一特性呢?假如项目 B 是一个持久层隔离工具包,它支持多种数据库,包括 MySQL、PostgreSQL等,在构件这个工具包的时候,需要这两种数据库的驱动程序,但在使用这个工具包的时候,只会依赖一种数据库。项目 B 的依赖声明代码清单如下:


 4.0.0
    com.mk.b
    project-b
    1.0.0
    
     
         mysql
            mysql-connector-java
            5.1.10
            true
        
        
         postgresql
            postgresql
            8.4-701.jdbc3
            true
        
    

当其他项目依赖于 B 的时候,这两个依赖不会被传递。因此, 当项目 A 依赖于项目 B 的时候,如果其实际使用基于 MySQL 数据库,那么在项目 A 中就需要显示地声明 mysql-connector-java 这一依赖。

在理想情况下,是不应该使用可选依赖的。更好的做法是为 MySQL 和 PostgreSQL 分别创建一个 Maven 项目,基于同样的 groupId 分配不同的 artifactId,如 com.mk.b:project-b-mysql 和 com.mk.b:project-b-postgresql,在各自的 POM 中声明对应的 JDBC 驱动依赖 ,而且不使用可选依赖,用户则根据需要选择使用。

排除依赖

传递性依赖会给项目隐士地引入很多依赖 ,极大地简化了项目依赖的管理,但是有些时候这种特性也会带来问题。例如,当前项目有一个第三方依赖,而这个第三方依赖由于某些原因依赖了另外一个类库的 SNAPSHOT 版本,那么这个 SNAPSHOT 就会成为当前项目的传递性依赖,而 SNAPSHOT 的不稳定性会直接影响当前的项目。这时就需要排除掉该 SNAPSHOT,并且在当前项目中声明该类库的某个正式发布的版本。或者还有一些情况,你想要的替换某个传递性依赖。


 4.0.0
    com.mk.study
    study-a
    1.0.0
    
     
         com.mk.study
            project-b
            1.0.0
            
             
                 com.mk.study
                    project-c
                
            
        
        
            com.mk.study
            project-c
            1.1.0
        
    

上述代码中,项目 A 依赖于项目 B,但是由于一些原因,不想引入传递性依赖 C,而是自己显式地声明对于项目 C 1.1.0 版本的依赖。代码中使用 exclusions 元素声明排除依赖,exclusions 可以包含一个或多个 exclusion 子元素,因此可以排除一个或者多个传递性依赖。需要注意的是,声明 exclusion 的时候只需要 groupId 和 artifactId,而不需要 version 元素,这是因为只需要 groupId 和 artifactId 就能唯一定位依赖图中的某个依赖。换句话说,Maven 解析后的依赖中,不可能出现 groupId 和 artifactId 相同,但是 version 不同的两个依赖。该例的依赖解析逻辑如下图:

排除依赖

归类依赖(Maven 属性)

假如项目 A 有很多关于 Spring Framework 的依赖,分别是 org.springframework:spring-core:2.5.6、org.springframework:spring-beans:2.5.6、org.springframework:spring-context:2.5.6 和 org.springframework:spring-core-support:2.5.6,他们是来自同一个项目的不同模块。可以预见,如果将来需要升级 Spring Framewor,这些依赖的版本会一起升级。

应该有一个唯一的地方定义版本,并且在 dependency 声明中引用这一版本。这样,在升级 Spring Framework 的时候就只需要修改一处,实现代码如下:


 4.0.0
    com.mk.study
    project-a
    Project A
    1.0.0-SNAPSHOT
    
    
     2.5.6
    
    
    
     
         org.springframework
            spring-core
            ${springframework.version}
        
        
         org.springframework
            spring-beans
            ${springframework.version}
        
        
         org.springframework
            spring-coontext
            ${springframework.version}
        
        
         org.springframework
            spring-context-support
            ${springframework.version}
        
    

这里简单用到了 Maven 属性,首先使用 properties 元素定义 Maven 属性,有了这个属性定义之后,就可以使用美元符号和大括弧环绕的方式引用 Maven 属性。

优化依赖

Maven 会自动解析所有项目的直接依赖和传递性依赖,并且根据规则正确判断每个依赖的范围,对于一些依赖冲突,也能进行调解,以确保任何一个构件只有唯一的版本在依赖中存在。最后得到的那些依赖被称为已解析依赖。

我们以上面 Maven 入门 中的案例为例,可以运行 mvn dependency:list 命令查看当前项目的已解析依赖:

[INFO] Scanning for projects...
[INFO]
[INFO] ----------------------< com.mk.study:hello-world >----------------------
[INFO] Building Maven Hello World Project 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-dependency-plugin:2.8:list (default-cli) @ hello-world ---
[INFO]
[INFO] The following files have been resolved:
[INFO]    junit:junit:jar:4.10:test
[INFO]    org.hamcrest:hamcrest-core:jar:1.1:test
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  0.990 s
[INFO] Finished at: 2023-01-01T16:05:34+08:00
[INFO] ------------------------------------------------------------------------

将直接在当前项目 POM 声明的依赖定义为顶层依赖,而这些顶层依赖的依赖则定义为第二层依赖,以此类推,有第三、四层依赖。当这些依赖经 Maven 解析后,就会构成一个依赖树,可以运行 mvn dependency:tree 命令查看当前项目的依赖树:

[INFO] Scanning for projects...
[INFO]
[INFO] ----------------------< com.mk.study:hello-world >----------------------
[INFO] Building Maven Hello World Project 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ hello-world ---
[INFO] com.mk.study:hello-world:jar:1.0-SNAPSHOT
[INFO] - junit:junit:jar:4.10:test
[INFO]    - org.hamcrest:hamcrest-core:jar:1.1:test
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  1.242 s
[INFO] Finished at: 2023-01-01T23:50:11+08:00
[INFO] ------------------------------------------------------------------------

在此基础上,还有 dependency:analyze 工具可以帮助分析当前项目的依赖。运行命令 mvn dependency:analyze 输出如下:

[INFO] Scanning for projects...
[INFO]
[INFO] ----------------------< com.mk.study:hello-world >----------------------
[INFO] Building Maven Hello World Project 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] >>> maven-dependency-plugin:2.8:analyze (default-cli) > test-compile @ hello-world >>>
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ hello-world ---
[WARNING] Using platform encoding (GBK actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory C:~Desktophello-wordsrcmainresources
[INFO]
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ hello-world ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ hello-world ---
[WARNING] Using platform encoding (GBK actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory C:~Desktophello-wordsrc	estresources
[INFO]
[INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ hello-world ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] <<< maven-dependency-plugin:2.8:analyze (default-cli) < test-compile @ hello-world <<<
[INFO]
[INFO]
[INFO] --- maven-dependency-plugin:2.8:analyze (default-cli) @ hello-world ---
[WARNING] Unused declared dependencies found:
[WARNING]    org.springframework:spring-aop:jar:5.3.16:compile
[WARNING]    org.springframework:spring-beans:jar:5.3.16:compile
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  1.279 s
[INFO] Finished at: 2023-01-01T16:20:28+08:00
[INFO] ------------------------------------------------------------------------

从输出中可以看到,Unused declared dependencies,意指项目中未使用的,但显示声明的依赖。对于这一类依赖,我们不应该简单地直接删除其声明,而是应该仔细分析。由于 dependency:analyze 只会分析编译主代码和测试代码需要用到的依赖,一些执行测试和运行时需要的依赖它就发现不了。

对应的,还应该有 Used declared dependencies,由于示例项目比较简单, 此处未打印。Used declared dependencies指项目中使用到的,但是没有显示声明的依赖。这种依赖意味着潜在风险,当前项目在直接使用它们,而这种依赖是通过直接依赖传递进来的,当升级直接依赖的时候,相关传递性依赖的版本也可能发生变化 ,这种变化不易察觉,但是有可能导致当前项目出错 。例如由于接口的改变,当前项目中的相关代码无法编译。这种隐藏的、潜在的威胁一旦出现,就往往需要耗费大量的时间来查明原因。因此,显式声明任何项目中直接用到的依赖。

生命周期

何为生命周期

Maven 的生命周期就是为了对所有的构建过程进行抽象和统一。这个声命周期包含了项目的清理、初始化、编译、测试、打包、集成测试、验证、部署和站点生成等几乎所有构件步骤。也就是说,几乎所有项目的构建,都能映射到这样一个生命周期上。

Maven 的生命周期是抽象的,这意味着生命周期本身不做任何实际的工作,在 Maven 的设计中,实际的任务(如编译源代码)都交由插件来完成。这种思想与设计模式中的模板方法非常相似。

生命周期抽象了构建的各个步骤,定义了它们的次序,但没有提供具体实现。每个构建步骤都可以绑定一个或者多个插件行为,而且 Maven 为大多数构建步骤编写并绑定来了默认插件。虽然大多数时间里, 用户几乎都不会察觉到插件的存在,但实际上编译是由 maven-compile-plugin 完成的,而测试是由 maven-surefire-plugin 完成的。当用户有特殊需要的时候,也可以配置插件定制构建行为,甚至自己编写插件。

Maven 定义的生命周期和插件机制一方面保证了所有 Maven 项目有一致的构件标准,另一方面又通过默认插件简化和稳定了实际项目的构建。

生命周期详解

Maven 拥有三套相互独立的生命周期,它们分别为 clean、default 和 site。clean 生命周期的目的是清理项目,default 生命周期的目的是构建项目,而 site 生命周期的目的是建立项目站点。

每个生命周期包含一些阶段,这些阶段是有顺序的,并且后面的阶段依赖于前面的阶段,用户和 Maven 最直接的交互方式就是调用这些生命周期阶段。

较之于生命周期阶段的前后依赖关系,三套生命周期本身是相互独立的,用户可以仅仅调用 clean 生命周期的某个阶段,或者仅仅调用 default 生命周期的某个阶段,而不会对其他生命周期产生任何影响。

从命令行执行 Maven 任务的最主要方式就是调用 Maven 的生命周期阶段。各个生命周期是相互独立的,而一个生命周期的阶段是右前后依赖关系的。下面以一些常见的 Maven 命令为例,解释器执行的生命周期阶段:

Maven 中主要的生命周期阶段并不多,而常用的 Maven 命令实际都是基于这些阶段简单组合而成的,因此只要对 Maven 生命周期有一个基本的理解,就可以正确而熟练地使用 Maven 命令。

聚合和继承

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

聚合

当想要一次构建两个项目,而不是待两个模块的目录下分别执行 mvn 命令时,可以使用 Maven 聚合这一特性。

为了能够使用一条命令就能构建 project-a 和project-b 两个模块,我们需要创建一个额外的名为 project-parent 的模块,然后通过该模块构建整个项目的所有模块,project-parent 本身作为一个 Maven 项目,它必须要有自己的 POM,不过,同时作为一个聚合项目,其 POM 又有特殊的地方,如下:


 4.0.0
    com.mk.project
    project-parent
    1.0.0-SNAPSHOT
    pom
    Project Parent
    
     project-a
        project-b
    

上述 POM 使用了与 project-a 和 project-b 相同的 groupId,artifactId 为独立的 project-parent,版本也与其他两个模块一致。一个特殊的地方是 packaging,其值为 pom。对于聚合模块来说,其打包方式 packaging 的值必须为 pom,否则就无法构建。

元素 modules 是实现聚合的最核心的配置。可以通过在一个打包方式为 pom 的 Maven 项目中声明任意数量的 module元素来实现模块的聚合。这里每个 module 的值都是一个当前 POM 的相对目录。

一般来说,为了方便快速定位内容,模块所处的目录名称应当与其 artifactId 一致。为了方便用户构建项目,通常将聚合模块放在项目目录的最顶层,其他模块则作为聚合模块的子目录存在,这样当用户得到源码的时候,第一眼发现的就是聚合模块的 POM,不用从多个模块中去寻找聚合模块来构建整个项目。

在聚合模块下运行 mvn clean install 命令,Maven 会首先解析聚合模块的 POM、分析要构建的模块、并计算出一个反应堆构建顺序,然后根据这个顺序依次构建各个模块。反应堆是所有模块组成的一个构件结构。

继承

面向对象设计中,程序猿可以建立一种类的父子结构,然后在父类中声明一些字段和方法供子类继承,这样就可以做到“一处声明,多处使用”。类似地,我们可以创建 POM 的父子结构,然后在父 POM 中声明一些配置供子 POM 继承,以实现“一处声明,多处使用”的目的。

父模块只是为了帮助消除配置的重复,因此它本身不包含除 POM 之外的项目文件。我们还以聚合中的示例为例,父模块为 project-parent,另外还有 project-a 和 project-b 两个子模块。父模块的 POM 内容如下:


 4.0.0
    com.mk.project
    project-parent
    1.0.0-SNAPSHOT
    pom
    Project Parent
   
    
     project-a
        project-b
    
    

有了父模块,对应的 project-a 和 project-b 模块的 POM 修改如下(以 project-a 为例):


    
 
     com.mk.project
        project-parent
        1.0.0-SNAPSHOT
        ../pom.xml
    
    
    project-a
    Project A
    
    
    ……
    
    
     
        ……
        
    

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

这个更新过的 POM 没有为 project-a 声明 groupId 和 version,实际上,这个子模块隐式地从父模块继承了这两个元素,这也就消除了一些不必要的配置。

可继承的 POM 元素

上面看到,groupId 和 version 是可以被继承的,那么还有哪些 POM 元素可以被继承呢?下面是一个完整的列表:

依赖管理

假设子模块 project-a 和 project-b 同时依赖了 org.springframework:spring-core:2.5.6、org.springframework:spring-beans:2.5.6、org.springframework:spring-context:2.5.6、org.springframework:spring-context-support:2.5.6 和 junit:junit:4.10,既然 dependencies 元素可以被继承,因此,可以将这些依赖配置放到父模块 project-parent 中,两个子模块就可以移除这些依赖,简化配置。

上述做法是可行的,但却存在问题。到目前为止,能够确定这两个子模块都包含那四个依赖,不过无法确定将来添加的子模块就一定需要这四个依赖。假设将来项目中需要加入一个 project-util 模块,该模块只是提供一些简单的帮助工具,与 springframework 完全无关。那上面的方法就有些不太合适了。

dependencyManagement

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


 4.0.0
    com.mk.project
    project-parent
    1.0.0-SNAPSHOT
    pom
    Project Parent
    
     2.5.6
        4.10
    
    
        
            
                org.springframework
                spring-core
                ${springframework.version}
            
            
                org.springframework
                spring-beans
                ${springframework.version}
            
            
                org.springframework
                spring-context
                ${springframework.version}
            
            
                org.springframework
                spring-context-support
                ${springframework.version}
            
            
                junit
                junit
                ${junit.version}
                test
            
        
    

这里使用 dependencyManagement 声明的依赖既不会给 project-parent 引入依赖,也不会给其子模块引入依赖,不过这段配置会被继承。

现在修改 project-parent 的子模块的 POM 修改如下:

……

 1.4.1
    1.3.1b


 
     org.springframework
        spring-core
    
    
     org.springframework
        spring-beans
    
    
     org.springframework
        spring-context
    
    
     org.springframework
        spring-context-support
    
    
     junit
        junit
    
    
     javax.mail
        mail
        ${javax.mail.version}
    
    
     com.icegreen
        greenmail
        ${greenmail.version}
        test
    

……

所有 springframework 依赖只配置了 groupId 和 artifactId,省去了 version,而 junit 依赖不仅省去了 version,还省去了依赖范围 scope。这些信息可以省略是因为继承了project-parent 中的 dependencyManagement 配置,完整的依赖声明已经包含在父 POM 中,子模块只需要配置简单的 groupId 和 artifactId 就能获得对应的依赖信息,从而引入正确的依赖。

使用这种依赖管理机制似乎不能减少太多的 POM 配置,不过还是强烈建议推荐采用这种方法。原因在于父 POM 中使用 dependencyManagement 声明依赖能够统一项目范围中依赖的版本,当依赖版本在父 POM 中声明之后,子模块在使用依赖的时候就无须声明版本,也就不会发生多个子模块使用依赖版本不一致的情况。这可以帮助降低依赖冲突的几率。

如果子模块不声明依赖的使用,即使该依赖已经在父 POM 的 dependencyManagement 中声明了,也不会产生任何实际的效果。

import

import 以来范围只在 dependencyManagement 元素下才有效果,使用该范围的依赖通常指向一个 POM,作用是将目标 POM 中的 dependencyManagement 配置导入合并到当前 POPM 的 dependencyManagement 元素中。例如,想要在另一个模块中使用与上面完全一样的 dependencyManagemnt 配置,除了复制配置或者继承这两种方式之外,还可以使用 import 范围依赖这一配置导入,如下:

……

 
     
         com.mk.project
            project-parent
            1.0.0-SNAPSHOT
            pom
            import
        
    

……

上述代码中依赖的 type 值为 pom,import 范围依赖由于其特殊性,一般都是指向打包类型为 pom 的模块。如果有多个项目,他们使用的依赖版本都是一致的,则就可以定义一个使用 dependencyManagement 专门管理的 POM,然后在各个项目中导入这些依赖管理配置。

插件管理

Maven 提供了 dependencyManagement 元素帮助管理依赖, 类似地,Maven 也提供了 pluginManagement 元素帮助管理插件。在该元素中配置的依赖不会造成实际的插件调用行为,当 POM 中配置了真正的 plugin 元素,并且其 groupId 和 artifactId 与 pluginMangement 中配置的插件匹配时,pluginManagement 的配置才会影响实际的插件行为。

与上面的 dependencyManagement 元素使用方式类似,这里不再举例。

聚合与继承的关系

多模块 Maven 项目中的聚合与继承其实是两个概念,其目的完全是不同的。前者主要是为了方便快速构建项目,后者主要是为了消除重复配置。

对于聚合模块来说,它知道有哪些被聚合的模块,但那些被聚合的模块不知道这个聚合模块的存在。

对于继承关系的父 POM 来说,它不知道有哪些子模块继承于它,但那些子模块都必须知道自己的父 POM 是什么。

如果非要说这两个特性的共同点,那么就是,聚合 POM 与继承关系中的父 POM 的 packaging 都必须是 pom,同时, 聚合模块与继承关系中的父模块除了 POM 之外都没有实际的内容。

在现有的实际项目中,往往会发现一个 POM 即是聚合 POM,又是父 POM,这么做主要是为了方便。其实,对于上面的案例,我们也是这么做的。

约定优于配置

Maven 提倡“约定优于配置”,这是 Maven 最核心的设计理念之一,使用约定可以大量减少配置。

Maven 会假设用户的项目是这样的:

遵循约定虽然损失了一定的灵活性,用户不能随意安排目录结构,但是却能减少配置。更重要的是,遵循约定能够帮助用户遵守构建标准。

Maven 允许自定义源码目录,如下:


 4.0.0
    com.mk.project
    project-a
    1.0
    
     src/java
    

该例中源码目录就成了 src/java 而不是默认的 src/main/java。但这往往会造成交流问题,习惯 Maven 的人会奇怪,源代码去哪里了?当这种定义大量存在的时候,交流成本就会大大提高。

任何一个 Maven 项目都隐式地继承自超级 POM,这有点类似于任何一个 Java 类都隐式地继承于 Object 类。因此,大量超级 POM 的配置都会被所有 Maven 项目继承,这些配置也就成了 Maven 所提倡的约定。

对于 Maven 3,超级 POM 在文件 中的路径下。对于,超级在文件MAVEN_HOME/lib/maven-x.x.x-uber.jar 中的 org/apache/maven/project/pom-4.0.0.xml 目录下。这里的 x.x.x 表示 Maven 的具体版本。

超级 POM 的内容在 Maven 2 和 Maven 3 中基本一致。首先超级 POM 定义了仓库及插件仓库,两者的地址都为中央仓库 http://repo1.maven.org/maven2,并且都关闭了 SNAPSHOT 的支持。

超级 POM 实际上很简单,但从这个 POM 我们就能够知晓 Maven 约定的由来,不仅可以理解什么是约定,为什么要遵守约定,还能明白约定是如何实现的。

反应堆

在一个多模块的 Maven 项目中,反应堆(Reactor)是指所有模块组成的一个构建结构。对于单模块的项目,反应堆就是该模块本身,但对于多模块项目来说,反应堆就包含了各模块之间的依赖关系,从而能够自动计算出合理的模块构建顺序。

反应堆的构件顺序

为了能更能清楚反应堆的构件数信,将 project-aggregator 的聚合配置修改如下:


 project-a
    project-b
    project-parent

构建 project-aggregator 会看到如下输出:

[INFO] ------------------------------------------------------------------------
[INFO] Reactor Build Order:
[INFO] 
[INFO] Project Aggregator
[INFO] Project Parent
[INFO] Project A
[INFO] Project B
[INFO]
[INFO] ------------------------------------------------------------------------

上述输出告诉了我们反应堆的构建顺序。如果按顺序读取 POM 文件,首先应该读到的是 project-aggregator 的 POM,实际情况与预料的一致,但是接下来的几个模块的构件次序与声明的顺序就不一致了。

实际的构件顺序是这样形成的:Maven 按序读取 POM,如果该 POM 没有依赖模块,那么就构件该模块,否则就先构建其依赖模块,如果该依赖还依赖于其他模块,则进一步先构建依赖的依赖。模块间的依赖关系将反应堆构建成一个有向非循环图,各个模块是该图的节点,依赖关系构成了有向边。

裁剪反应堆

一般来说,用户会选择构建整个项目或者选择构建单个模块,但有些时候,会想要仅仅构建完整反应堆中的某些模块。换句话说,用户需要实时地裁剪反应堆。

Maven 提供很多命令行选项支持裁剪反应堆,输入 mvn -h 可以看到这些选项:

默认情况下,从 project-aggregator 执行 mvn clean install 会得到如下完整的反应堆:

[INFO] ------------------------------------------------------------------------
[INFO] Reactor Build Order:
[INFO] 
[INFO] Project Aggregator
[INFO] Project Parent
[INFO] Project A
[INFO] Project B
[INFO]
[INFO] ------------------------------------------------------------------------

可以使用 -pl 选项指定构建某几个模块,如运行如下命令:

mvn clean install-pl project-b

得到的反映堆为:

[INFO] ------------------------------------------------------------------------
[INFO] Reactor Build Order:
[INFO] 
[INFO] Project Aggregator
[INFO] Project Parent
[INFO] Project B
[INFO]
[INFO] ------------------------------------------------------------------------

版本管理

何为版本管理

一个健康的项目通常有一个长期、合理的版本演变过程。例如 JUnit 有 3.7、3.8、3.8.1、3.8.2、4.0、4.1等版本。还有 Maven 特有的快照版本。

为了方便团队的合作,在项目开发的过程中,都应该使用快照版本,Maven 能够很智能地处理这种特殊的版本,解析项目各个模块最小的“快照”。当项目需要对外发布的时候,显然需要提供非常稳定的版本,使用该版本应当永远只能定位到唯一的构件,而不是像快照版本那样,定位的构件随时可能发生变化。对应地,我们称这类稳定的版本为发布版本。

版本管理关系的问题之一就是这种快照版本和发布版本之间的转换。

如下如图,项目经过了一段时间的 1.0-SNAPSHOT 的开发之后, 在某个时刻发不了 1.0 正式版,然后项目又进入了 1.1-SNAPSHOT 的开发,这个版本可能添加了一些有趣的特性,然后在某个时刻发布 1.1正式版。项目紧接着进入 1.2-SNAPSHOT的开发。

快照版和发布版之间的转换

由于快照对应了项目的开发过程,因此往往对应了很长的时间,而正式版本对应了项目的发布,因此仅仅代表某个时刻项目的状态。

版本管理和版本控制

版本管理是指项目整体版本的演变过程管理,如从 1.0-SNAPSHOT 到 1.0,再到 1.1-SNAPSHOT。版本控制是指借助版本控制工具(如 Git)追踪代码的每一个变更。

Maven 的版本号定义约定

Maven 的版本号定义约定是这样的:

<主版本>.<次版本>.<增量版本>-<里程碑版本>

主版本和次版本之间,以及次版本和增量版本之间用点号分割,里程碑版本之前用连字号分隔。

需要注意的是,不是每个版本号都必须拥有这四个部分。一般来说, 主版本和次版本都会声明,但增量版本和里程碑就不一定了。

展开阅读全文

页面更新:2024-04-28

标签:干货   全篇   构件   生命周期   模块   元素   声明   阶段   版本   代码   建议   时间   测试   收藏   项目

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top