Skip to main content

Maven3 in Action

Maven实战

部分内容源自小山code

本书的书稿使用Git和Unfuddle(https://unfuddle.com/)进行管理的,书中的大量截图是使用Jing(https://www.techsmith.com/screen-capture.html)制作的。

image-20240204142252333

Tasks

image-20240204143125491

Maven简介

何为Maven

Apache组织中的一个颇为成功的开源项目,主要服务于基于Java平台的项目构建、依赖管理和项目信息管理。

何为构建

早上来到公司,我们做的第一件事情就是从源码库签出最新的源码,然后进行单元测试,如果发现失败的测试,会找相关的同事一起调试,修复错误代码。接着回到自己的工作上来,编写自己的单元测试及产品代码。

​ 忙到午饭时间,代码编写的差不多了,测试也通过了,开心的享用午餐,然后休息。下午先在昏昏沉沉中开了个例会,会议结束后喝杯咖啡继续工作。会上经理要求看的测试报告,于是找到了相关工具集成到IDE,生成了像模像样的测试覆盖率报告,接着邮箱发给经理,松了口气。谁料QA小组又发过来了几个bug,没办法,先在本地重现了再说,于是熟练地用IDE生成了一个WAR包,部署到Web容器下,启动容器。看到熟悉的界面了,遵循bug报告,一步步重现了bug……快下班的时候,bug改好了,提交代码,通知QA小组,在愉快中结束了一天的工作。

​ 编译、运行单元测试、生成文档、打包和部署等烦琐且不起眼的工作上,这就是构建。

Maven:作为构建工具,不仅能帮我们自动化构建,还能够抽象构建过程,提供构建任务实现;他跨平台,对外提供了一致的操作接口,这一切足以使他称为优秀的、流行的构建工具。

不仅仅是构建工具

Maven帮助管理的同时,也为全世界提供了一个免费的中央仓库。Maven的衍生工具(如Nexus)。约定优于配置(Convention Over Configuration)。

为什么需要Maven

组装PC和品牌PC

IDE不是万能的

Make:不支持跨平台。

Ant(Another Neat Tool),最早用来构建Tomcat,创作动机是受不了Makefile的语法格式。我们可以将Ant看成是一个Java版本的Make,也正因为使用了Java,所以Ant跨平台。

不重复发明轮子

Maven与极限编程

极限编程(XP)强调拥抱变化。

  • 测试驱动开发(TDD)
  • 持续集成(CI):Hudson和CruiseControl、Jenkins、GitLab CI

被误解的Maven

使用Maven最高效的方式永远是命令行。

Maven社区提倡为你使用的任何插件设定稳定的版本。

Maven仓库确实不完美,由于许可证等因素,需要做的是建立一个组织内部的仓库服务器。

安装与配置

1.检查JDK安装

echo %JAVA_HOME%   # win
echo $JAVA_HOME # linux

java -version

2.下载Maven

Maven3对Maven2完全兼容,可以直接升级。

3.本地安装

D:\bin> jar xvf "3.0 bin.zip"

jar命令就相当于tar。

添加环境变量M2_HOME 变量值为maven目录;在Path的变量中添加%M2_HOME%\bin

echo %M2_HOME%
mvn -v

在Linux中推荐使用符号链接(软链接)

ln -s apache-maven-3.0 apache-maven
export M2_HOME = /home/jihuaixi/bin/apache-maven

4.升级Maven

​ 在基于UNIX的系统上,可以利用符号链接这一工具来简化Maven的升级,不必像Windows上那样,每次升级都必须更新环境变量。

安装目录分析

M2_HOME

bin
-mvn
-mvnDebug: MAVEN_DEBUG_OPTS参数
-m2.conf: classworlds的配置文件
boot
-plexus-classworlds-2.6.0.jar:类加载器框架
conf
-settings.xml
lib
-Maven运行时需要的Java类库
# 打印出所有的Java系统属性和环境变量
mvn help:system

~/.m2

推荐将M2_HOME/conf/settings.xml文件复制到~/.m2/settings.xml。这是一条最佳实践。

设置HTTP代理

​ 公司处于安全因素考虑,要求你使用通过安全认证的代理访问因特网(使用ping repo1.maven.org检查网络)。这种情况下就需要为Maven配置HTTP代理,才能让它正常访问外部仓库。

​ 检测代理服务器使用telnet ip port命令,如果连接正确,则输入Ctrl + ],然后q,回车,退出即可。

<proxies>
<proxy>
<id>my-proxy</id>
<active>true</active>
<protocol>http</protocol>
<host>39.106.154.119</host>
<port>3128</port>
<!--
<username>***</username>
<password>***</password>
<nonProxyHosts>repository.mycom.com|*.google.com</nonProxyHosts>
-->
</proxy>
</proxies>

proxies下可以有多个proxy元素,如果声明了多个,则默认情况下第一个被激活的proxy会生效。

Maven安装最佳实践

  1. 设置MAVEN_OPTS环境变量:通常设置为- Xms128m -Xmx512m,因为Java默认的最大可用内存往往不能够满足Maven运行的需要,比如在项目较大时,使用Maven生成项目站点需要占用大量的内存,如果么有配置,容易导致java.lang.OutOfMemeoryError。推荐设置环境变量,文不是修改mvn脚本。
  2. 配置用户范围settings.xml:$M2_HOME/conf/settings.xml为全局范围,~/.m2/settings.xml为用户范围。便于升级,不需要修改用户目录下的配置。

使用入门

编写POM

就像Make的Makefile、Ant的build.xml一样,pom.xml是Maven的核心。POM(Project Object Model)。

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>top.aerion</groupId>
<artifactId>tacocloud</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>tacocloud</name>
<description>tacocloud</description>
</project>

第一行是xml版本和编码方式。

紧接着是project元素,还声明了命名空间及xsd元素。虽然这些属性不是必须的,但使用这些属性能够让第三方工具帮助我们快速便捷POM。

对于Maven2和Maven3来说,modelVersion只能是4.0.0。

最重要的是groupId、artifactId和version三行,组往往和项目所在的组织或公司存在关联。版本号中的SNAPSHOT意为快照,说明该项目还处于开发中,是不稳定版本。

POM文件实现了与Java代码的解耦。

编写主代码

项目主代码会被最终打包,而测试代码不会。

约定主代码位于src/main/java目录,然后在该目录下创建top/aerion/mvnbook/helloworld/HelloWorld.java。

package top.aerion.mvnbook.helloworld

public class HelloWorld {
public String sayHello() {
return "Hello Maven";
}

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

编译:

mvn clean compile

clean为清理target目录。默认情况下,构建的所有输出都在target目录中,接着执行resources:resources任务(未定义项目资源,暂且略过),最后执行compile编译将主代码编译到target/classes目录。

不再支持源选项 5。请使用 7 或更高版本。,修改pom.xml或者settings.xml。这是由于历史原因,核心插件之一compile插件默认只支持编译Java1.3,一次需要配置Java17.

<project>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compile-plugin</artifactId>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

编写测试代码

目录src/test/java。

JUnit是事实上的单元测试标准。

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.7</version>
<scope>test</scope>
</dependency>
</dependencies>

对应junit-4.7.jar。自动从中央仓库(http://repo1.maven.org/maven2/)下载。也可以直接访问仓库(https://repo1.maven.org/maven2/junit/junit/4.7/)。

scope为test,则在主代码中使用import JUnit报错,在测试代码中使用是没问题的。不声明则默认compile,标识该依赖对主代码和测试代码都有效。

package top.aerion.mvnbook.helloworld;


import org.junit.Test;

import static org.junit.Assert.assertEquals;

public class HelloWorldTest {

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

典型的单元测试包含三个步骤:

① 准备测试类及数据:HelloWorldTest类

② 执行要测试的行为:测试sayHello方法

③ 检查结果:Assert类检查结果是否为期望的“Hello Maven”

在Maven3中约定测试方法都以test开头。需要执行的测试方法都应该以@Test进行标注。

jihuaixi@jihuaixi-800G5M-800G5W:~/code/mvnbook$ mvn clean test
[INFO] Scanning for projects...
[INFO]
[INFO] -------------------------< top.aerion:mvnbook >-------------------------
[INFO] Building mvnbook 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ mvnbook ---
[INFO] Deleting /home/jihuaixi/code/mvnbook/target
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ mvnbook ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /home/jihuaixi/code/mvnbook/src/main/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ mvnbook ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to /home/jihuaixi/code/mvnbook/target/classes
[INFO]
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ mvnbook ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /home/jihuaixi/code/mvnbook/src/test/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ mvnbook ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to /home/jihuaixi/code/mvnbook/target/test-classes
[INFO]
[INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ mvnbook ---
[INFO] Surefire report directory: /home/jihuaixi/code/mvnbook/target/surefire-reports

-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running top.aerion.mvnbook.helloworld.HelloWorldTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.066 sec

Results :

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

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.529 s
[INFO] Finished at: 2024-02-14T00:18:29+08:00
[INFO] ------------------------------------------------------------------------

clean:clean任务、resource:resources任务、compile:compile任务、resource:testResources任务、 compile:testCompile任务、surefire:test任务

这是Maven生命周期的一个特性。

surefire是Maven中负责执行测试的插件,输出测试报告,显示一共执行了多个测试,失败了多少,出错了多少,跳过多少。

打包和运行

将项目编译、测试之后,下一个重要步骤就是打包(package)。默认为jar包。jar:jar任务

mvn clean package

如果需要在其他Maven项目中引用这个jar包:

mvn clean install

install:install任务

[INFO] --- maven-install-plugin:2.4:install (default-install) @ mvnbook ---
[INFO] Installing /home/jihuaixi/code/mvnbook/target/mvnbook-0.0.1-SNAPSHOT.jar to /home/jihuaixi/maven/apache-maven-3.8.3/repo/top/aerion/mvnbook/0.0.1-SNAPSHOT/mvnbook-0.0.1-SNAPSHOT.jar
[INFO] Installing /home/jihuaixi/code/mvnbook/pom.xml to /home/jihuaixi/maven/apache-maven-3.8.3/repo/top/aerion/mvnbook/0.0.1-SNAPSHOT/mvnbook-0.0.1-SNAPSHOT.pom

test之前会先执行compile,package之前会先执行test,install之前会先执行package。

默认打包的jar不能直接运行。因为jar文件中的META-INF/MANIFEST.MF文件没有Main-Class配置。需要借助maven-shade-plugin插件,配置如下

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.0</version>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Main-Class>top.aerion.mvnbook.helloworld.HelloWorld</Main-Class>
</manifestEntries>
</transformer>
</transformers>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

参考官网shade插件地址

生成两个文件mvnbook-0.0.1-SNAPSHOT.jar和original-mvnbook-0.0.1-SNAPSHOT.jar。前者带Main-Class信息可运行,后者是原始的jar。

使用Archetype生成项目骨架

使用maven archetype来创建项目的骨架。

如果是Maven3,简单的运行:

mvn archetype:generate

如果是Maven2则需要指定具体的版本,推荐使用稳定版本,Maven3自动解析最新的稳定版本,因此是安全的。

Choose archetype:
1: internal -> org.apache.maven.archetypes:maven-archetype-archetype (An archetype which contains a sample archetype.)
2: internal -> org.apache.maven.archetypes:maven-archetype-j2ee-simple (An archetype which contains a simplifed sample J2EE application.)
3: internal -> org.apache.maven.archetypes:maven-archetype-plugin (An archetype which contains a sample Maven plugin.)
4: internal -> org.apache.maven.archetypes:maven-archetype-plugin-site (An archetype which contains a sample Maven plugin site.
This archetype can be layered upon an existing Maven plugin project.)
5: internal -> org.apache.maven.archetypes:maven-archetype-portlet (An archetype which contains a sample JSR-268 Portlet.)
6: internal -> org.apache.maven.archetypes:maven-archetype-profiles ()
7: internal -> org.apache.maven.archetypes:maven-archetype-quickstart (An archetype which contains a sample Maven project.)
8: internal -> org.apache.maven.archetypes:maven-archetype-site (An archetype which contains a sample Maven site which demonstrates
some of the supported document types like APT, XDoc, and FML and demonstrates how
to i18n your site. This archetype can be layered upon an existing Maven project.)
9: internal -> org.apache.maven.archetypes:maven-archetype-site-simple (An archetype which contains a sample Maven site.)
10: internal -> org.apache.maven.archetypes:maven-archetype-webapp (An archetype which contains a sample Maven Webapp project.)
Choose a number or apply filter (format: [groupId:]artifactId, case sensitive contains): 7: 7
Define value for property 'groupId': top.aerion.mvntest
Define value for property 'artifactId': hello-world
Define value for property 'version' 1.0-SNAPSHOT: :
Define value for property 'package' top.aerion.mvntest: : top.aerion.mvntest.helloworld
Confirm properties configuration:
groupId: top.aerion.mvntest
artifactId: hello-world
version: 1.0-SNAPSHOT
package: top.aerion.mvntest.helloworld
Y: : Y

生成的目录如下

hello-world
├── pom.xml
└── src
├── main
│   └── java
│   └── top
│   └── aerion
│   └── mvntest
│   └── helloworld
│   └── App.java
└── test
└── java
└── top
└── aerion
└── mvntest
└── helloworld
└── AppTest.java

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>top.aerion.mvntest</groupId>
<artifactId>hello-world</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>

<name>hello-world</name>
<url>http://maven.apache.org</url>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

App.java

package top.aerion.mvntest.helloworld;

/**
* Hello world!
*
*/
public class App
{
public static void main( String[] args )
{
System.out.println( "Hello World!" );
}
}

AppTest.java

package top.aerion.mvntest.helloworld;

import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;

/**
* Unit test for simple App.
*/
public class AppTest
extends TestCase
{
/**
* Create the test case
*
* @param testName name of the test case
*/
public AppTest( String testName )
{
super( testName );
}

/**
* @return the suite of tests being tested
*/
public static Test suite()
{
return new TestSuite( AppTest.class );
}

/**
* Rigourous Test :-)
*/
public void testApp()
{
assertTrue( true );
}
}

背景案例

简单的账户注册服务

  • 提供一个未被使用的账号ID
  • 提供一个未被使用的email地址
  • 提供一个任意的显示名称
  • 设置安全密码,并重复输入以确认
  • 输入验证码
  • 前往邮箱查收激活链接并单击激活账号
  • 登录

需求阐述

需求用例

包含了一个主要场景和几个扩展场景。连个角色:用户和系统。

界面原型

简要设计

接口

模块结构:web、service、persist(持久化)、captcha(key生成)、email

坐标和依赖

Maven的一大功能就是管理项目依赖,就必须将它们唯一标识,这就是依赖管理的底层基础——坐标。

何为Maven坐标

Maven坐标(Coordinate)的元素包括groupId、artifactId、version、packaging、classifier。

中央仓库地址(http://repo1.maven.org/maven2

坐标详解

示例

<dependency>
<groupId>org.sonatype.nexus</groupId>
<artifactId>nexus-indexer</artifactId>
<version>3.0.4</version>
<packaging>jar</packaging>
</dependency>
  • groupId:定义当前Maven项目隶属的实际项目。Maven项目和实际项目不一定一一对应,比如SpringFramework项目,其对应的Maven项目有spring-core、spring-context等等,这是由于模块的概念。org.sonatype对应公司域名,nexus是项目名称
  • artifactId:对应实际项目中的一个Maven项目(模块)。项目名做前缀,这样方便寻找实际构件。
  • version:
  • packaging:jar 和 war打包会使用不同的命令。缺省值为jar。
  • classifier:该元素用来帮助定义构建输出的一些附属构件。附属构件与主构件对应,例如主构件nexus-indexer-2.0.0.jar,通过使用插件生成nexus-indexer-2.0.0-javadoc.jar、nexus-indexer-2.0.0-resources.jar这样的文档和源代码附属构件

前三个是必须的,packaging可选,classifier是不能直接定义的。

文件名规则:artifaId-version [-classifier] .packaging

account-email

依赖的配置

<dependency>
<groupId>...</groupId>
<artifactId>...</artifactId>
<version>...</version>
<type>...</type>
<scope>...</scope>
<optional>...</optional>
<exclusions>
<exclusion>
...
</exclusion>
...
</exclusions>
</dependency>
  • type:依赖的类型,对应于packaging,大部分情况下,不必声明,默认值为jar。
  • scope:依赖的范围
  • optional:标记依赖是否可选。
  • exclusions:用来排除传递性依赖。

依赖范围

Maven在编译主代码时需要使用一套classpath。在测试的时候用的是另外一套classpath,例如junit,该文件以依赖的方式引入到测试使用的classpath中。

依赖范围就是用来控制与这三种classpath(编译classpath、测试classpath、运行classpath)的关系:

  • compile:编译依赖范围。如果没有指定,这也是默认值。对于编译、测试和运行三种都有效,例如spring-core。
  • test:测试依赖范围。只对测试classpath有用,例如junit。
  • provided:已提供依赖范围。对于编译和测试有效,但是在运行时无效。典型的就是servlet-api,由于JDK或容器已经提供,就不需要重新引入一遍。
  • runtime:运行时依赖范围。对于测试和运行classpath有效,编译主代码的时候无效,例如JDBC驱动实现,编译只需要JDK提供的JDBC接口,只有在测试和运行时才需要实现上述接口的JDBC驱动。
  • system:系统依赖范围。和provided依赖范围完全一致。但是使用system依赖必须通过systemPath元素显式地指定依赖文件的路径。由于使用的是本地路径,注意移植性,谨慎使用。
  • import:导入依赖范围。不会对三种classpath产生实际的影响。
<dependency>
<groupId>javax.sql</groupId>
<artifactId>jdbc-stdext</artifactId>
<version>2.0</version>
<scop>system</scop>
<systemPath>${java.home}/lib/rt.jar</systemPath>
</dependency>

传递性依赖

account-email依赖于spring-core,spring-core依赖于commons-logging,那么commons-logging就会为account-email的依赖范围,即具有传递性。

假设A依赖于B,B依赖于C,我们说A对B是第一直接依赖,B对C是第二直接依赖,A对C是传递性依赖。

image-20240216120203675

例如account-email对com.icegreen:greenmail是第一直接依赖,依赖范围是test,com.icegreen:greenmail对javax.mail:mail是第二直接依赖,依赖范围是compile,所以传递依赖范围是test。

上表规律:当第二直接依赖范围是compile时,传递性依赖的范围与第一直接依赖的范围一致;当第二直接依赖范围是test时,依赖不会传递;当依赖的范围是provided时,相同才会传递;当第二直接依赖范围为runtime时与第一直接依赖范围一致,但compile除外,此时传递性依赖为runtime。

依赖调解

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

A->D->X(2.0)

依赖调解的第一原则是路径最近者优先

依赖调解的第二原则是第一声明者优先

依赖长度相同的前提下,声明顺序决定了谁会被解析使用.

可选依赖

A->B、B->X(可选)、B->Y(可选),则

由于X、Y是可选依赖,依赖将不会传递,即X、Y不会对A有任何影响。

这一特性由来:项目B实现了两个特性,特性一依赖于X,特性二依赖于Y,而两者是互斥的,用户不可能同时使用两个特性。比如B是一个持久层隔离工具包,它支持多种数据库,包括MySQL、PostgreSQL等,构建时只能使用一种数据库。

<dependencies>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<version>3.3.2</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.3.0</version>
<optional>true</optional>
</dependency>
</dependencies>

在理想状态下,不应该使用可选依赖。因为某个项目最好只有一个特性,上面的项目可分成两个Maven项目,一个是mysql另一个是mariadb。

最佳实践

排除依赖

<dependency>
<groupId>...</groupId>
<artifactId>...</artifactId>
<version>...</version>
<exclusions>
<exclusion>
<groupId>...</groupId>
<artifactId>...</artifactId>
</exclusion>
</exclusions>
</dependency>

归类依赖

<project>
...
<properties>
<springframework.version>2.5.6</springframework.version>
</properties>
<dependencies>
<dependency>
<groupId>...</groupId>
<artifactId>A</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>...</groupId>
<artifactId>B</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>...</groupId>
<artifactId>C</artifactId>
<version>${springframework.version}</version>
</dependency>
</dependencies>
</project>

优化依赖

程序员除了通过重构对代码进行优化,也应该对Maven项目的依赖了然于胸,并对其进行优化,如去除多余的依赖,显式地声明某些必要的依赖。

通过自动解析和调节,最后得到的哪些依赖被称为已解析依赖(Resolved Dependency)。

mvn dependency:list
mvn dependency:tree
mvn dependency:analyze

Used undeclared dependencies found是项目中使用到的,但是没有显式声明的依赖。这里的spring-context依赖意味着潜在的风险,当前项目直接在使用他们,例如有许多相关的Java import声明,而这种依赖是通过直接依赖传递进来的,当升级直接依赖时,相关的传递性依赖的版本可能发生变化,这种变化不易察觉,但有可能导致当前项目出错。例如接口改变。

Unused declared dependencies found项目中未使用,但显示声明的依赖,这里有spring-boot-starter-web、spring-boot-starter-test。有时候确实能根据该信息删除一些没有的依赖,但一定要小心测试。

仓库

坐标和依赖是任何一个构件的逻辑表示方式,而物理表示方式是文件,通过仓库来统一管理这些文件。

何为Maven仓库

没有Maven的时候,依赖包放到lib文件夹下,得益于坐标机制,Maven在某个统一的存储所有共享构件的地方称为仓库。

为了实现重用,仙姑构建完毕后生成的构件也可以安装或者部署到仓库中,供其他项目使用。

仓库的布局

路径大致为:

groupId/artifactId/version/artifactId-version.packaging

private static final char PATH_SEPARATOR='/';
private static final char GROUP_SEPARATOR='.';
private static final char ARTIFACT_SEPARATOR='-';

public String pathOf(Artifact artifact){
ArtifactHandler artifactHandler = artifact.getArtifactHandler();
StringBuilder path = new StringBuilder(128);
path.append(formatAsDirectory(artifact.getGroupId())).append(PATH_SEPARATOR );
path,append(artifact.getArtifactId()).append(PATH_SEPARATOR);
path.append(artifact.getBaseVersion()).append(PATH_SEPARATOR);
path.append(artifact.getArtifactId()).append(ARTIFACT_SEPARATOR)
.append(artifact.getVersion());

if(artifact.hasClassifier()){
path.append(ARTIFACT_SEPARATOR).append(artifact.getClassifier());
}
if(artifactHandler.getExtension() != null && artifactHandler.getExtension().length() >0)
{
path.append(GROUP_SEPARATOR).append(artifactHandler.getExtension());
}
return path.toString();
}

private String formatAsDirectory(String directory){
return directory.replace(GROUP_SEPARATOR,PATH_SEPARATOR);
}
  1. groupId的点./
  2. 添加artifactId
  3. version
  4. artifactId-version:testng-5
  5. 如果有Classifier就添加上:testng-5.8
  6. 检查extension,有就加上:testng-5.8-jdk5
  7. 扩展名为packaging:testng-5.8-jdk5.jar

仓库的分类

本地仓库 和远程仓库(中央仓库、私服、其他公共库)

先检查本地仓库,再检查远程仓库。

私服是一种特殊的远程仓库,为了节省带宽和时间,应该在局域网内架设一个私有的仓库服务器,用其代理所有外部的远程仓库。内部的项目还能部署到私服上供其他项目使用。

其他公共库有Java.net Maven库和JBoss Maven库。

本地仓库

<settings>
<localRepository>/home/jihuaixi/maven/apache-maven-3.8.3/repo</localRepository>
</settings>

mvn clean install或者从远程仓库下载到本地仓库才能使用。

远程仓库

每个用户只有一个本地仓库,但可以配置访问很多远程仓库。

中央仓库

Maven必须知道一个远程仓库才能在执行Maven命令的时候下载需要的构件。中央仓库就是这样的一个默认远程仓库。

maven-model-builder-3.9.4.jar解压后的pom-4.0.0.xml有如下的配置

<project>
<modelVersion>4.0.0</modelVersion>

<repositories>
<repository>
<id>central</id>
<name>Central Repository</name>
<url>https://repo.maven.apache.org/maven2</url>
<layout>default</layout>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
</project>

这段配置是所有Maven项目都会继承的超级POM。

使用default布局,snapshots下的enabled是false说明不从该中央仓库下载快照版本的构件。

私服

架设在局域网内的仓库服务,私服代理广域网上的远程仓库。

image-20240218200100267

即使在一台直接接入Internet的个人机器上使用Maven,也因该在本地建立私服。因为私服可以帮助你:

  • 节省自己的外网带宽。大量的对于外部仓库的重复请求会消耗很大的带宽。
  • 加速Maven的构建。不停地连接请求外部仓库是十分耗时的,但是Maven的额一些内部机制(如快照更新检查)要求Maven在执行构建的时候不停地检查远程仓库数据。因此当配置了很多外部仓库时,构建速度大大降低。使用私服可以解决这一问题。
  • 部署第三方构件。构建无法从任何一个外部远程仓库获得。例如组织内部生成的私有构建无法从外部获取、Oracle的JDBC驱动因版权不能发布到公共仓库中。
  • 提高稳定性,增强控制。Internet不稳定时,私服中可以有缓存保证正常运行,此外还有一些权限管理、RELEASE/SNAPSHOT区分等,管理员可以对仓库进行一些更高级的控制。
  • 降低中央仓库的负荷。

后面介绍私服软件Nexus。

远程仓库的配置

<project>
...
<repositories>
<repository>
<id>jboss</id>
<name>JBoss Repository</name>
<url>http://repository.jboss.com/maven2/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
<layout>default</layout>
</repository>
</repositories>
</project>

使用repository可以声明一个或多个远程仓库。Maven自带的中央仓库使用的id为central。

这里只下载发布版本而不下载快照版本。

layout为default说明的是Maven2及Maven3的默认布局,而不是Maven1。

<snapshots>
<enabled>true</enabled>
<updatePolicy>daily</updatePolicy>
<checksumPolicy>ignore</checksumPolicy>
</snapshots>

updatePolicy表示更新频率,默认值为daily,其他可用值:never、always(每次构建都检查)、interval:X(每隔X分钟检查一次更细)

checksumPolicy表示检查检验和文件的策略。构件上传到仓库时,会同时部署对应的校验和文件。下载构件的时候Maven会验证校验和文件,如果校验失败,当checksumPolicy值为warn时输出警告信息,fail会构建失败,ignore会忽略。

远程仓库的认证

例如组织内的一个Maven仓库服务器,该服务器为每个项目都提供独立的Maven仓库,为了防止非法的仓库访问,管理员为每个仓库提供了一组用户名及密码。

<settings>
<servers>
<server>
<id>my-proj</id>
<username>repo-user</username>
<password>repo-pwd</password>
</server>
</servers>
</settings>

这里关键是id元素,必须要和项目POM中需要认证的distributionManagement中的repository元素的id完全一致。

部署到远程仓库

在POM文件中添加下面配置。

<project>
<distributionManagement>
<repository>
<id>proj-releases</id>
<name>Proj Release Repository</name>
<url>http://192.168.1.100/content/repositories/proj-releases</url>
</repository>
<snapshotRepository>
<id>proj-snapshots</id>
<name>Proj Snapshot Repository</name>
<url>http://192.168.1.100/content/repositories/proj-snapshots</url>
</snapshotRepository>
</distributionManagement>
</project>

前者为发布版本仓库,后者为快照版本仓库。

mvn clean deploy

就会将项目部署到配置对应的远程仓库。

快照版本

1.0.0、1.3-alpha-4、2.0为稳定的发布版

2.1-SNAPSHOT、2.1-20231214.221414-13为不稳定的快照版本。

场景:模块A和B正在开发中,B依赖于A。

方案一:B每次签出A的代码,进行编译,问题是多出了版本控制和Maven操作,如果出现了编译错误还需要找A解决。

方案二:不停地更新版本号2.1.1、2.1.2、2.1.3。首先两个模块都需要频繁更改,其次大量版本其实仅仅包含了微笑的差异,有时候是对版本号的滥用。

Maven的快照版本就是为了解决上述问题。

将版本号设置为2.1-SNAPSHOT,然后发布到私服,发布的过程中,Maven会自动地为构件打上时间戳,比如2.1-20231214.221414-13就是2023年12月14日22点14分14秒的第13次快照。有了改时间戳,Maven就会从仓库中寻找最新的文件。默认情况下,Maven每天检查一次更新(updatePolicy控制),用户也可是强制没检查更新

mvn clean install-U

基于快照版本,A需要构建后才能部署到仓库,而B也不需要考虑编译的问题。

当项目经过完善的测试后需要发布的时候,就应该更改为发布版本,2.1表示稳定版本。

快照版本只应该在组织内部的项目或模块间依赖使用,因为这时组织对这些快照版本的额依赖具有完全的理解及控制权。不应该依赖于任何组织外部的快照版本依赖,由于快照的不稳定性,这样的依赖会造成潜在的危险。就是说项目今天构建是成功的,明天就可能因为外部不可控因素而失败。

从仓库解析依赖的机制

仓库与依赖的关系:

当本地没有依赖构件的时候,会从远程仓库下载;当依赖版本为快照版本的时候,会自动找到最新的快照。

  1. 当依赖的范围是system的时候,Maven直接从本地文件系统解析构件。
  2. 根据坐标计算仓库路径后,尝试从本地仓库寻找,如果发现相应的构件则解析成功。
  3. 本地仓库找不到的时候,如果依赖的版本是显式的发布版本构件,则遍历所有的远程仓库,发现后下载并解析使用。
  4. 如果依赖的版本是RELEASE或者LATEST,则基于更新策略读取所有远程仓库的元数据groupId/artifactId/maven-metadata.xml,将其与本地仓库的对应元数据合并后,得到最新版本的值,然后基于这个值,检查本地或从远程下载,如步骤2、3。
  5. 如果依赖的版本是SNAPSHOT,则基于更新策略读取所有远程仓库的元数据groupId/artifactId/version/maven-metadata.xml,将其与本地仓库的对应元数据合并后,得到最新快照版本的值,然后基于该值,检查本地仓库或者从远程下载。
  6. 如果最后解析得到的构建版本是时间戳格式的快照,则复制其时间戳格式的文件至非时间戳格式,如SNAPSHOT,并使用该非时间戳格式的构件。

当版本不清晰时,如RELEASE、LATEST和SNAPSHOT,就需要基于更新远程仓库的更新策略来检查更新

groupId/artifactId/maven-metadata.xml

<?xml version="1.0"encoding="UTP-8"?>
<metadata>
<groupId>org.sonatype.nexus</groupId>
<artifactId>nexus</artifactId>
<versioning>
<latest>1.4.2-SNAPSHOT</latest>
<release>1.4.0</release>
<versions>
<version>1.3.5</version>
<version>1.3.6</version>
<version>1.4.0-SNAPSHOT</version>
<version>1.4.0</version>
<version> 1.4.0.1-SNAPSHOT</version>
<version>1.4.1-SNAPSHOT</version>
<version>1.4.2-SNAPSHOT</version>
</versions>
<lastUpdated>20091214221557</lastUpdated>
</versioning>
</metadata>

展示了所有版本以及latest和release版本。

在依赖中声明LATEST和RELEASE是不推荐的做法,因为Maven随时可能解析到不同的构件。

Maven3中已不支持使用LATEST和RELEASE。如果不设置插件版本,其效果跟RELEASE一样。

当依赖的版本为快照版本时,会检查

groupId/artifactId/version/maven-metadata.xml

<?xml version="1.0"encoding="UTF-8*?>
<metadata>
<groupId>org.sonatype.nexus</groupId>
<artifactId>nexus</artifactId>
<version>1.4.2-SNAPSHOT</version>
<versioning>
<snapshot>
<timestamp>20091214.221414</timestamp>
<buildNumber>13</buildNumber>
</snapshot>
<lastUpdated>20091214221558</lastUpdated>
</versioning>
</metadata>

​ 仓库的元数据并不是永远正确的,有时候当用户发现无法解析某些构件,或者解析到错误构件时,就有可能是出现了仓库元数据错误,这就需要手动的,或者使用工具(例如Nexus)对其进行修复。

镜像

如果仓库X提供仓库Y存储的所有内容,那么认为X是Y的一个镜像。

例如:http://maven.net.cn/content/groups/public/是中央仓库http://repo1.maven.org/maven2/在中国的镜像,由于地理位置因素,该镜像比中央仓库有更快的服务。

<settings>
<mirrors>
<mirror>
<id>maven.net.cn</id>
<name>one of the central mirrors in China</name>
<url>http://maven.net.cn/content/groups/public/</url>
<mirrorof>central</mirrorof>
</mirror>
</mirrors>
</settings>

镜像的另一个常用方法四结合私服。

<settings>
<mirrors>
<mirror>
<id>internalrepository</id>
<name>Internal Repository Manager</name>
<url>http:// 192.168.1.100/maven2/</url>
<mirrorof>*</mirrorof>
</mirror>
</mirrors>
</settings>

mirrorOf元素的值为星号,表示该配置是所有Maven仓库的镜像,任何对于远程仓库的请求都会转至http:// 192.168.1.100/maven2/

  • <mirrorOf>*</mirrorOf>:匹配所有远程仓库。
  • <mirrorOf>external:*</mirrorOf>:匹配所有远程仓库,使用localhost的除外,使用file://协议的除外,即不在本机的远程仓库。
  • <mirrorOf>repo1,repo2</mirrorOf>:匹配仓库repo1和repo2。
  • <mirrorOf>*,!repo1</mirrorOf>:匹配所有远程仓库,repo1除外。

仓库搜索服务

Sonatype Nexus

https://repository.sonatype.org/

当前最流行的开源Maven仓库管理软件,这是Sonatype架设的一个公共Nexus仓库实例。

在这里插入图片描述

自测不好用

Jarvana

MVNbrowser

MVNrepository

https://mvnrepository.com/

网络有时候不稳定

image-20240219145737598

SONATYPE

https://central.sonatype.com/

自己发现的一个感觉还不错,速度块,也很稳定。

image-20240219145833575

阿里也有一个,不过内容比较杂乱。

生命周期和插件

除了坐标、依赖以及仓库之外,Maven的另外两个核心概念是生命周期和插件。

命令行的输入往往就对应了生命周期,如mvn package。生命周期与插件两者协同工作,密不可分。

何为生命周期

Maven的生命周期就是为了对所有的构建过程进行抽象和统一。

生命周期是抽象的,不做实际工作,实际工作交给插件来完成。这种思想与设计模式中的模板方法(Template Method)非常相似。模板方法模式在父类中定义算法的整体结构,子类通过实现或重写父类的方法来控制实际的行为,这样既保证了算法有足够的可扩展性,又能够严格控制算法的整体结构。

public abstract class AbstractBuild
{
public void build()
{
initialize();
compile();
test();
packagee();
integrationTest();
deploy();
}
// 初始化
protected abstract void initialize();
// 编译
protected abstract void compile():
// 测试
protected abstract void test();
// 打包 package是关键字,这里采用的是packagee
protected abstract void packagee();
// 集成测试
protected abstract void integrationTest():
// 部署
protected abstract void deploy();
}

这里只定义了构建过程,但是没有实现,行为的实现都交给了子类。

为了重复造轮子,Maven设计了插件机制。每个步骤都可以绑定一个或者多个插件行为,而且Maven为大多数构建步骤编写并绑定了默认插件。例如,针对编译的插件有maven-compiler-plugin, 针对测试的插件有 maven-surefire-plugin 等。当用户有特殊需要的时候,也可以配置插件定制构建行为,甚至自己写插件。

image-20240219160832747

生命周期详解

三套生命周期

  • clean:清理项目。
  • default:构建项目。
  • site:建立项目站点。

每个生命周期包含一些阶段(phase),这些阶段是有顺序的,而且后面的阶段依赖前面的阶段。Maven和用户最直接的交互方式就是调用这些生命周期阶段。以clean生命周期为例,包含的阶段有pre-clean、clean和post-clean。当用户调用pre-clean时,只有pre-clean这一个阶段执行,当调用post-clean时,三个阶段都会执行。

clean生命周期

  • pre-clean 执行一些清理前需要完成的工作。
  • clean 清理上一次构建生成的文件。
  • post-clean 执行一些清理后需要完成的工作。

default生命周期

default生命周期定义了真正构建时所需要执行的所有步骤,它是所有生命周期中最核心的部分,其包含的阶段如下,这里只对重要的阶段进行解释:

  • validate
  • initialize
  • generate-sources
  • process-sources 处理项目主资源文件。 一般来说,是对 src/main/resources 目录的内 容进行变量替换等工作后,复制到项目输出的主 classpath 目录中。
  • generate-resources
  • process-resources
  • compile 编译项目的主源码。 一般来说,是编译 src/main/java 目录下的Java 文件至项 目输出的主 classpath 目录中。
  • process-classes
  • generate-test-sources
  • process-test-sources 处理项目测试资源文件。 一般来说,是对 src/test/resources 目录 的内容进行变量替换等工作后,复制到项目输出的测试 classpath 目录中。
  • generate-test-resources
  • process-test-resources
  • test-compile 编译项目的测试代码。 一般来说,是编译 src/test/java 目录下的 Java 文 件至项目输出的测试 classpath 目录中。
  • process-test-classes
  • test 使用单元测试框架运行测试,测试代码不会被打包或部署。
  • prepare-package
  • package 接受编译好的代码,打包成可发布的格式,如JAR。
  • pre-integration-test
  • integration-test
  • post-integration-test
  • verify
  • install 将包安装到Maven 本地仓库,供本地其他Maven项目使用。
  • deploy 将最终的包复制到远程仓库,供其他开发人员和 Maven项目使用。

对于上述未加解释的阶段,读者也应该能够根据名字大概猜到其用途,若想了解进一 步的这些阶段的详细信息,可以参阅官方的解释: https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html

site生命周期

site生命周期的目的是建立和发布项目站点, Maven 能够基于POM 所包含的信息,自 动生成一个友好的站点,方便团队交流和发布项目信息。该生命周期包含如下阶段:

  • pre-site 执行一些在生成项目站点之前需要完成的工作。
  • site 生成项目站点文档。
  • post-site 执行一些在生成项目站点之后需要完成的工作。
  • site-deploy 将生成的项目站点发布到服务器上。

命令行与生命周期

  • mvn clean:clean生命周期的clean阶段。实际执行的为clean生命周期的pre-clean和clean阶段。
  • mvn clean install:该命令调用clean生命周期的clean阶段,default生命周期的install阶段。
  • mvn clean deploy site-deploy:clean:clean、default:deploy、site:site-deploy

插件目标

Maven的核心是定义了抽象的生命周期,具体的实现交给插件完成,所以Maven核心分发包就比较小,在需要的时候进行下载插件。

对于插件本身,为了能够复用代码,一个插件可以完成多个任务。每个功能就是一个插件目标(Plugin Goal)。

例如:maven-dependency-plugin有十多个目标,每个目标对应一个功能,例如dependency:list、dependency:tree、dependency:analyze,类似的还有compile:compile

插件绑定

Maven 的生命周期与插件相互绑定,用以完成实际的构建任务。具体而言,是生命周期的阶段与插件的目标相互绑定,以完成某个具体的构建任务。

image-20240219193042174

内置绑定

为了让用户不用配置就能构建Maven项目,Maven在核心为一些主要的生命周期阶段绑定了很多插件的目标。

image-20240219195510318

image-20240219195535225

default生命周期

生命周期阶段插件目标执行任务
process-resourcesmaven-resources-plugin:resources复制主资源文件至主输出目录
compilemaven-compiler-plugin:compile编译主代码至主输出目录
process-test-resourcesmaven-resources-plugin:testResources复制测试资源文件至测试输出目录
test-compilemaven-compiler-plugin:testCompile编译测试代码至测试输出目录
testmaven-surefire-plugin:test执行测试用例
packagemaven-jar-plugin:jar创建项目jar包
installmaven-install-plugin:install将项目输出构件安装到本地仓库
deploymaven-deploy-plugin:deploy将项目输出构件部署到远程仓库

除了默认的打包类型jar之外,常见的打包类型还有 warpommaven-pluginear等。 它们的default生命周期与插件目标的绑定关系可参阅Maven官方文档:https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html

自定义绑定

用户可以自己选择将某个插件目标绑定到生命周期的某个阶段上。

一个常见的例子是创建项目的源码jar包 , 内置的插件绑定关系中并没有涉及这一任务,因此需要用户自行配置。maven-source-plugin可以帮助我们完成该任务,它的jar-no-fork目标能够将项目的主代码打包成 jar文件,可以将其绑定到 default 生命周期的verify 阶段上, 在执行完集成测试后和安装构件之前创建源码jar包。

https://maven.apache.org/plugins/maven-source-plugin/jar-no-fork-mojo.html

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

executions下每个 execution 子元素可以用来配置执行一个任务。该例中配置了一个id为 attach-sources的任务,通过phrase配置,将其绑定到 verify生命周期阶段上,再通过 goals配置指定要执行的插件目标。至此,自定义插件绑定完成。

mvn verify
[INFO] --- maven-source-plugin:3.3.0:jar-no-fork (attach-sources) @ mvnbook ---
[INFO] Building jar: /home/jihuaixi/code/mvnbook/target/mvnbook-0.0.1-SNAPSHOT-sources.jar

phase不是必须的,插件默认绑定阶段package。

可以使用maven-help-plugin查看插件详细信息,了解插件目标的默认绑定阶段

mvn help:describe -Dplugin=org.apache.maven.plugins:maven-source-plugin:3.3.0 -Ddetail
source:test-jar-no-fork
Description: This goal bundles all the test sources into a jar archive.
This goal functions the same as the test-jar goal but does not fork the
build, and is suitable for attaching to the build lifecycle.
Implementation: org.apache.maven.plugins.source.TestSourceJarNoForkMojo
Language: java
Bound to phase: package

插件配置

几乎所有Maven 插件的目标都有一些可配置的参数,用户可以通过命令行和POM 配置等方式来配置这些参数。

命令行插件配置

maven-surefire-plugin 提供了一个 maven.test.skip 参数,当其值为 true 的时候,就会跳过执行测试。

mvn install -Dmaven.test.skip=true

POM中插件全局配置

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.12.1</version>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
</plugins>
</build>

这样,不管绑定到 compile 阶段的maven-compiler-plugin:compile任务,还是绑定到 test-compiler 阶段的maven-compiler-plugin:testCompiler任务,就都能够使用该配置,基于 Java17版本进行编译。

POM中插件任务配置

除了为插件配置全局的参数,用户还可以为某个插件任务配置特定的参数。以maven-antrun-plugin为例,它有一个目标 run,可以用来在Maven中调用Ant任务。用户将maven-antrun-plugin:run绑定到多个生命周期阶段上,再加以不同的配置,就可以让Maven在不同的生命阶段执行不同的任务

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>ant-validate</id>
<phase>validate</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<tasks>
<echo>I'm bound to validate phase.</echo>
</tasks>
</configuration>
</execution>
<execution>
<id>ant-verify</id>
<phase>verify</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<tasks>
<echo>I'm bound to verify phase.</echo>
</tasks>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

获取插件信息

在线插件信息

基本上所有主要的 Maven 插件都来自 Apache 和 Codehaus。

https://maven.apache.org/plugins/index.html

托管于Codehaus 上 的Mojo 项目也提供了大量了Maven插件

https://www.mojohaus.org/plugins.html

命令行参数是由该插件参数的表达式(Expression)决定的。surefire:test skip参数的表达式为${maven.test.skip},它表示可以在命令行以 -Dmaven.test.skip=true 的方式配置该目标。并不是所有插件目标参数都有表达式,也就是说,一些插件目标参数只能在POM中配置。

使用maven-help-plugin描述插件

mvn help:describe -Dplugin=org.apache.maven.plugins:maven-compiler-plugin:2.1

感觉不如直接浏览在线信息。

从命令行调用插件

usage: mvn [options] [<goal(s)>] [<phase(s)>]
Options:
mvn help:describe-Dplugin=compiler
mvn dependency:tree

不过,这里还有一个疑问,describemaven-help-plugin的目标没错,但冒号前面的 help是什么呢? 它既不是 groupId, 也不是 artifactId, Maven 是如何根据该信息找到对应版本插件的呢? 同理,为什么不是 maven-dependeney-plugin:tree, 而是 dependency:tree?

mvn org.apache.maven.plugins:maven-help-plugin:2.1:describe-Dplugin=compiler
mvn org.apache.maven.plugins:maven-dependency-plugin:2.1:tree

效果一致,为了达到该目的,Maven 引入了目标前缀的概念, help 是maven-help-plugin的目 标前缀, dependeney 是 maven-dependeney-plugin 的前缀,有了插件前缀, Maven 就能找到对应的 arifactId。 不过,除了 artifactId, Maven 还需要得到 groupId 和 version 才能精确定位到某个插件。

插件解析机制

为了方便用户使用和配置插件,Maven不需要用户提供完整的插件坐标信息,就可以解析得到正确的插件,Maven的这一特性是一把双刃剑,虽然它简化了插件的使用和配置,可一旦插件的行为出现异常,用户就很难快速定位到出问题的插件构件。例如 mvn help:system这样一条命令,它到底执行了什么插件? 该插件的 groupId、artifactId 和 version分别是什么? 这个构件是从哪里来的? 本节就详细介绍Maven的运行机制。

插件仓库

与依赖构件一样,插件构件同样基于坐标存储在Maven仓库中。在需要的时候,Maven会从本地仓库寻找插件,如果不存在,则从远程仓库查找。找到插件之后,再下载到本地仓库使用。

值得一提的是,Maven会区别对待依赖的远程仓库与插件的远程仓库,前面介绍了如何配置远程仓库,但那种配置只对一般依赖有效果。当Maven需要的依赖在本地仓库不存在时,它会去所配置的远程仓库查找,可是当Maven需要的插件在本地仓库不存在时,它就不会去这些远程仓库查找。(有自己的插件仓库)

不同于repositories及其repository子元素,插件的远程仓库使用pluginRepositoriespluginRepository配置。例如,Maven内置了如下的插件远程仓库配置:

<pluginRepositories>
<pluginRepository>
<id>central</id>
<name>Maven Plugin Repository</name>
<url>http://repo1.maven.org/maven2</url>
<layout>default</layout>
<snapshots>
<enabled>false</enabled>
</snapshots>
<releases>
<updatePolicy>never</updatePolicy>
</releases>
</pluginRepository>
</pluginRepositories>

这个默认插件仓库的地址就是中央仓库,它关闭了对 SNAPSHOT的支持,以防止引入 SNAPSHOT版本的插件而导致不稳定的构建。

一般来说,中央仓库所包含的插件完全能够满足我们的需要,因此也不需要配置其他的插件仓库。只有在很少的情况下,项目使用的插件无法在中央仓库找到,或者自己编写了插件,这个时候可以参考上述的配置,在POM或者settings.xml中加入其他的插件仓库配置。

插件的默认groupId

在POM中配置插件的时候,如果该插件是Maven的官方插件(即如果其 groupIdorg.apache.maven.plugins), 就可以省略 groupId配置:

<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.1</version>
<configuration>
<source>1.5</source>
<target>1.5</target
</configuration>
</plugin>
</plugins>
</build>

上述配置中省略了 maven-compiler-plugingroupId, Maven 在解析该插件的时候,会自动用默认 groupId org.apache.maven.plugins 补齐。

但不推荐使用 Maven 的这一机制,虽然这么做可以省略一些配置,但这样的配置会让团队中不熟悉 Maven 的成员感到费解,况且能省略的配置也就仅仅一行而已。

解析插件版本

同样是为了简化插件的配置和使用,在用户没有提供插件版本的情况下,Maven会自动解析插件版本。

首先,Maven在超级POM中为所有核心插件设定了版本,超级POM是所有Maven项目的父POM,所有项目都继承这个超级POM的配置,因此,即使用户不加任何配置,Maven使用核心插件的时候,它们的版本就已经确定了。这些插件包括maven-clean-plugin、maven-compiler-plugin、maven-surefire-plugin等。 如果用户使用某个插件时没有设定版本,而这个插件又不属于核心插件的范畴,Maven就会去检查所有仓库中可用的版本,然后做出选择。

https://repo1.maven.org/maven2/org/apache/maven/plugins/maven-compiler-plugin/maven-metadata.xml

<?xml version="1.0"encoding=*UTP-8"?>
<metadata>
<groupId>org.apache.maven.plugins</groupld>
<artifactId>maven-compiler-plugin</artifactId>
<versioning>
<latest>2.1</latest>
<release>2.1</release>
<versions>
<version>2,0-beta-1</version>
<version>2.0</version>
<version>2.0.1</version>
<version>2.0.2</version>
<version>2.1</version
</versions>
<lastUpdated>20100102092331</lastUpdated>
</versioning>
</metadata>

Maven遍历本地仓库和所有远程插件仓库,将该路径下的仓库元数据归并后,就能计算出latestrelease 的值。latest 表示所有仓库中该构件的最新版本,而 release表示最新的非快照版本。

依赖Maven解析插件版本其实是不推荐的做法,即使Maven3将版本解析到最新的非快照版,也还是会有潜在的不稳定性。

解析插件前缀

前面讲到mvn命令行支持使用插件前缀来简化插件的调用,现在解释Maven如何根据插件前缀解析得到插件的坐标。

插件前缀与 groupId:artifactId是一一对应的,这种匹配关系存储在仓库元数据中。与之前提到的 goupId/artifactId/maven-metadata.xml不同,这里的仓库元数据为 groupId/maven-metadata.xml, 那么这里的groupId是什么呢? 前面提到主要的插件都位于http://repol.maven.org/maven2/org/apache/maven/plugins/ http://repository.codehaus.org/org/code-haus/mojo/, 相应地,Maven在解析插件仓库元数据的时候,会默认使用org.apache.maven.plugins 和org.codehaus.mojo 两个 groupId。也可以通过配置 settings.xml让Maven检查其他 groupId上的插件仓库元数据:

<settings>
<pluginGroups>
<pluginGroup>com.your.plugins</pluginGroup>
</pluginGroups>
</settings>

基于该配置,Maven就不仅仅会检查 org/apache/maven/plugins/maven-metadata.xml 和org/codehaus/mojo/maven-metadata.xml, 还会检查 com/your/plugins/maven-metadata.xml。下面看一下插件仓库元数据的内容:

<metadata>
<plugins>
<plugin>
<name>Maven Clean Plugin</name>
<prefix>clean</prefix>
<artifactId>maven-clean-plugin</artifactId>
</plugin>
<plugin>
<name>Maven Compiler Plugin</name>
<prefix>compiler</prefix>
<artifactId>maven-compiler-plugin</artifactid>
</plugin>
<plugin>
<name>Maven Dependency Plugin</name>
<prefix>dependency</prefix>
<artifactId>maven-dependency-plugin</artifactId>
</plugin>
</plugins>
</metadata>

上述内容是从中央仓库的 org.apache.maven.plugins groupId下插件仓库元数据中截取的一些片段,从这段数据中就能看到 maven-clean-plugin 的前缀为 clean, maven-compiler-plugin的前缀为compiler, maven-dependency-plugin的前缀为dependency。

当Maven解析到dependency:tree这样的命令后,它首先基于默认的 groupId归并所有插件仓库的元数据org/apache/maven/plugins/maven-metadata.xml; 其次检查归并后的元数据,找到对应的 arifactId为 maven-dependeney-plugin; 然后结合当前元数据的groupId org.apache.maven.plugins; 最后使用方法解析得到version, 这时就得到了完整的插件坐标。

如果 org/apache/maven/plugins/maven-metadata.xml 没有记录该插件前缀,则接着检查其他groupId下的元数据,如 org/codehaus/mojo/maven-metadata.xml, 以及用户自定义的插件组。如果所有元数据中都不包含该前缀,则报错。

聚合与继承

一个账户注册服务可被划分成 account-emailaccount-persist等五个模块。Maven的聚合特性能够把项目的各个模块聚合在一起构建,而Maven的继承特性则能帮助抽取各模块相同的依赖和插件等配置,在简化POM的同时,还能促进各个模块配置的一致性。

account-persist

聚合

为了能够使用一条命令就能构建account-email和account-persist两个模块,我们需要创建一个额外的名为accounl-aggregator的模块,然后通过该模块构建整个项目的所有模块。account-aggregator本身作为一个Maven项目,它必须要有自己的POM,不过,同时作为一个聚合项目,其POM又有特殊的地方。如下为account-aggregator的 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/maven-v4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>top.aerion.mvnbook.account</groupId>
<artifactId>account-aggregator</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<ename>Account Aggregator</name>
<modules>
<module>account-email</module>
<module>account-persist</module>
</modules>
</project>

这里的第一个特殊的地方为packaging,其值为POM。回顾account-emailaccount-persist,它们都没有声明packaging, 即使用了默认值jar。对于聚合模块来说,其打包方式packaging的值必须为pom, 否则就无法构建。

image-20240222202014001

这里的聚合模块可以是父子结构,也可以是平行结构。

<modules>
<module>../account-email</module>
<module>../account-persist</module>
</modules>
[INFO] Reactor Summary:
[INFO]
[INFO] account-email 1.0-SNAPSHOT ......................... SUCCESS [ 3.084 s]
[INFO] account-persist 1.0-SNAPSHOT ....................... SUCCESS [ 0.229 s]
[INFO] Account Aggregator 1.0.0-SNAPSHOT .................. SUCCESS [ 0.002 s]
[INFO] ------------------------------------------------------------------------

继承

细心的朋友可能已经比较过这两个模块的POM,这两个 POM 有着很多相同的配置,如它们有相同的 groupId 和 version, 有相同的 spring-core、spring-beans、spring-context 和junit 依赖,还有相同的 maven-compiler-plugin 与 maven- resources-plugin 配置。程序员的嗅觉对这种现象比较敏感,没错,这是重复!

account-parent

account-aggregator

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<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/maven-v4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>top.aerion.mvnbook</groupId>
<artifactId>account-aggregator</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Account Aggregator</name>
<modules>
<module>account-parent</module>
<module>account-email</module>
<module>account-persist</module>
</modules>
</project>

account-parent

<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>top.aerion.mvnbook</groupId>
<artifactId>account-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Account Parent</name>
</project>

account-email

<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>top.aerion.mvnbook</groupId>
<artifactId>account-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../account-parent/pom.xml</relativePath>
</parent>
<artifactId>account-email</artifactId>
<name>Account Email</name>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

可继承的pom元素

  • groupId: 项目组ID, 项目坐标的核心元素。
  • version: 项目版本,项目坐标的核心元素。
  • description: 项目的描述信息。
  • organization: 项目的组织信息。
  • inceptionYear: 项目的创始年份。
  • url: 项目的URL地址。
  • developers: 项目的开发者信息。
  • contributors: 项目的贡献者信息。
  • distributionManagement: 项目的部署配置。
  • issueManagement: 项目的缺陷跟踪系统信息。
  • ciManagement: 项目的持续集成系统信息。
  • scm: 项目的版本控制系统信息。
  • mailingLists: 项目的邮件列表信息。
  • properties: 自定义的Maven属性。
  • dependencies: 项目的依赖配置。
  • dependencyManagement: 项目的依赖管理配置。
  • repositories: 项目的仓库配置。
  • build:包括项目的源码目录配置、输出目录配置、插件配置、插件管理配置等。
  • reporting: 包括项目的报告输出目录配置、报告插件配置等。

依赖管理

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

account-parent

<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>top.aerion.mvnbook</groupId>
<artifactId>account-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Account Parent</name>
<properties>
<springframework.version>6.1.4</springframework.version>
<junit.version>4.13.2</junit.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>

account-email

<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>top.aerion.mvnbook</groupId>
<artifactId>account-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../account-parent/pom.xml</relativePath>
</parent>
<artifactId>account-email</artifactId>
<name>Account Email</name>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
</dependencies>
</project>

前面在介绍依赖范围的时候提到了名为import的依赖范围,推迟到现在介绍是因为该范围的依赖只在dependencyManagement元素下才有效果,使用该范围的依赖通常指向一个POM, 作用是将目标POM中的dependencyManagement配置导入并合并到当前POM的dependencyManagement元素中。例如想要在另外一个模块中使用与某个代码完全一样的dependencyManagement配置,除了复制配置或者继承这两种方式之外,还可以使用import 范围依赖将这一配置导入,见代码:

<dependencyManagement>
<dependencies>
<dependency>
<groupId>top.aerion.mvnbook</groupId>
<artifactId>account-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

插件管理

父模块

<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.12.1</version>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.3.1</version>
<configuration>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>

子模块

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
</plugin>
</plugins>
</build>

聚合与继承的关系

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

聚合POM与继承关系中的父POM的 packaging 都必须是pom, 同时,聚合模块与继承关系中的父模块除了POM之外都没有实际的内容

image-20240223133632365

在现有的实际项目中,往往会发现一个POM既是聚合POM, 又是父POM, 这么做主要是为了方便。一般来说,融合使用聚合与继承也没有什么问题,例如可以将 account-aggregatoraccount-parent 合并成一个新的 account-parent:

<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>top.aerion.mvnbook</groupId>
<artifactId>account-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Account Parent</name>
<modules>
<module>account-email</module>
<module>account-persist</module>
</modules>
<properties>
<springframework.version>6.1.4</springframework.version>
<junit.version>4.13.2</junit.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.12.1</version>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.3.1</version>
<configuration>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>

现在新的 account-parent在上一层目录,这是Maven默认能识别的父模块位置,因此不再需要配置 relativePath

<parent>
<groupId>top.aerion.mvnbook</groupId>
<artifactId>account-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>

约定优于配置

Maven提倡 “约定优于配置”(Convention Over Configuration), 这是Maven最核心的设计理念之一。

也许这时候有读者会问,如果我不想遵守约定该怎么办? 这时,请首先问自己三遍,你真的需要这么做吗?如果仅仅是因为喜好,就不要耍个性,个性往往意味着牺牲通用性,意味着增加无谓的复杂度。

对于Maven3, 超级POM在文件 $MAVEN_HOME/lib/maven-model-builder-3.8.3.jar 中的org/apache/maven/model/pom-4.0.0.xml路径下。

首先超级POM定义了仓库及插件仓库两者的地址都为中央仓库 http://repol.maven.org/maven2, 并且都关闭了SNAPSHOT的支持。这也就解释了为什么Maven默认就可以按需要从中央仓库下载构件。

<repositories>
<repository>
<id>central</id>
<name>Central Repository</name>
<url>https://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>https://repo.maven.apache.org/maven2</url>
<layout>default</layout>
<snapshots>
<enabled>false</enabled>
</snapshots>
<releases>
<updatePolicy>never</updatePolicy>
</releases>
</pluginRepository>
</pluginRepositories>

这里依次定义了项目的主输出目录、主代码输出目录、最终构件的名称格式、测试代码输出目录、主源码目录、脚本源码目录、测试源码目录、主资源目录和测试资源目录。 这就是Maven 项目结构的约定。

<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>${project.basedir}/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>
</build>

紧接着超级 POM 为核心插件设定版本

<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.8</version>
</plugin>
<plugin>
<artifactId>maven-release-plugin</artifactId>
<version>2.5.3</version>
</plugin>
</plugins>
</pluginManagement>

反应堆

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

反应堆的构建顺序

<modules>
<module>account-email</module>
<module>account-persist</module>
<module>account-parent</module>
</modules>

修改完毕之后构建account-aggregator会看到如下的输出:

[INFO]-----
[INFO] Reactor Build Order:
[INFO]
[INFO] Account Aggregator
[INFO] Account Parent
[INFO] Account Emall
[INFO] Account Persist
[INFO]
[INFO]

​ 上述输出告诉了我们反应堆的构建顺序,它们依次为account-aggregator、account-parent、account-email和account-persist。我们知道,如果按顺序读取POM文件,首先应该读到的是 account-aggregator的POM , 实际情况与预料的一致,可是接下来几个模块的构建次序显然与它们在聚合模块中的声明顺序不一致,account-parent跑到了 account-email前面,这是为什么呢?

image-20240223154308714

​ 图中从上至下的箭头表示POM的读取次序,但这不足以决定反应堆的构建顺序,Maven还需要考虑模块之间的继承和依赖关系,图中的有向虚线表示模块之间的继承或者依赖,该例中account-email和 account-persist 依赖于 account-parent, 那么account-parent就必须先于另外两个模块构建。也就是说,这里还有一个从右向左的箭头。实际的构建顺序是这样形成的:Maven按序读取POM, 如果该POM没有依赖模块,那么就构建该模块,否则就先构建其依赖模块,如果该依赖还依赖于其他模块,则进一步先构建依赖的依赖。

​ 模块间的依赖关系会将反应堆构成一个有向非循环图(Directed Acyclic Graph,DAG),各个模块是该图的节点,依赖关系构成了有向边。这个图不允许出现循环,因此,当出现模块A依赖于B, 而B又依赖于A的情况时,Maven就会报错。

裁剪反应堆

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

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

  • -am, --also-make:同时构建所列模块的依赖模块;
  • -amd, -also-make-dependents:同时构建依赖于所列模块的模块;
  • -pl, --projects <arg>:构建指定的模块,模块间用逗号分隔;
  • -rf, -resume-from <arg>:从指定的模块回复反应堆。

​ 执行 mvn clean install 会得到如下完整的反应堆:

[INFO] --------------------------------
[INFO] Reactor Build Order:
[INFO]
[INFO] Account Aggregator
[INFO] Account Parent
[INFO] Account Email
[INFO] Account Persist
[INFO]
[INFO]---------------------------------

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

mvn clean install -pl account-email,account-persist
[INFO]---------------------------------
[INFO] Reactor Build Order:
[INFO]
[INFO] Account Email
[INFO] Account Persist
[INFO]
[INFO]---------------------------------
# 使用 -amd选项可以同时构建依赖于所列模块的模块。
mvn clean install -pl account-parent -amd
[INPO]-------------------------------
[INFO] Reactor Build Order:
[INFO]
[INFO] Account Parent
[INFO] Account Email
[INFO] Account Persist
[INFO]
[INFO]------------------------------
# 使用 -rf选项可以在完整的反应堆构建顺序基础上指定从哪个模块开始构建。
mvn clean install -rf account-email
[INFO]----------------------------------
[INFO] Reactor Build Order:
[INFO]
[INFO] Account Email
[INFO] Account Persist
[INFO]
[INFO]----------------------------------
# 最后,在-pl -am或者 -pl -amd的基础上,还能应用-f参数,以对裁剪后的反应堆再次裁剪。
mvn clean install -pl account-parent -amd -rf account-email
[INFO]---------------------------------
[INFO] Reactor Build Order:
[INFO]
[INFO] Account-Email
[INFO] Account-Persist
[INFO]
[INFO]---------------------------------

使用Nexus创建私服

私服不是Maven 的核心概念,它仅仅是一种衍生出来的特殊的Maven仓库。

有三种专门的Maven 仓库管理软件可以用来帮助大家建立私服: Apache 基金会的Archiva、JFrog 的 Artifactory 和 Sonatype 的 Nexus。 其中, Archiva 是开源的,而 Artifactory 和 Nexus 的核心也是开源的,因此大家可以自由选择使用。个人比较推崇 Nexus。事实上,Nexus 也是当前最流行的 Maven 仓库管理软件。

Nexus简介

2005年12月,Tamas Cservenak 由于受不了匈牙利电信ADSL的低速度,开始着手开发 Proximity—一个很简单的 Web 应用。它可以代理并缓存 Maven 构件,当 Maven 需要下载构件的时候,就不需要反复依赖于ADSL。 到2007年, Sonatype 邀请 Tamas 参与创建一个更酷的 Maven 仓库管理软件,这就是后来的 Nexus。

安装Nexus

Nexus是典型的Java Web应用,它有两种安装包,一种是包含 Jetty 容器的 Bundle 包 , 另一种是不包含 Web 容器的 war包。

下载Nexus

https://help.sonatype.com/en/download.html

官网下载地址不能直接访问,这里要感谢我是个假程序员网盘地址

3.53.0 CSDN地址,不需要积分,但需要下载码

Nexus的仓库与仓库组

部署构件至Nexus

Nexus的权限管理

使用Maven进行测试

account-captcha

maven-surefire-plugin简介

Maven所做的只是在构建执行到特定生命周期阶段的时候,通过插件来执行JUnit或者TestNG的测试用例。这一插件就是maven-surefire-plugin,可以称之为测试运行器(TestRuner), 它能很好地兼容JUnit3、JUnit4以及TestNG。

跳过测试

动态指定要运行的测试用例

包含和排除测试用例

测试报告

运行TestNG测试

重用测试代码

使用Hudson进行持续集成

Hudson是Jenkins的前身

使用Maven构建Web应用

版本管理

灵活的构建

生成项目站点

编写Maven插件

Archetype