Maven Wrapper深入实战

概述

官网GitHub

Maven Wrapper,缩写为mvnw,是一个受Gradle Wrapper和Takari Wrapper启发而产生的Maven子项目,主要有以下三个用途:

  • 让开发者电脑上无需安装Maven,也不用配置环境变量,即可使用Maven构建项目;
  • 团队开发时,可让每个开发人员都保持一致的Maven版本;
  • 极个别情况下,某些特殊项目,必须使用某个指定的Maven版本;其他项目使用安装配置指定的Maven版本,不互相干扰。

另外还有一款mvnd工具和命令行,参考比Maven快2~10倍的编译工具mvnd简介与实战

安装

安装Maven Wrapper最简单的方式是在项目(注意,可以不是Maven项目,跟Maven Wrapper版本号有关,至少需要是3.3.0;下面截图可看到并没有pom.xml文件)的根目录下运行安装命令mvn wrapper:wrapper

两个mvnw脚本文件的日期都是2024年5月21日,是Maven Wrapper 3.3.2版本的发布日期。

默认使用最新版本的Maven Wrapper,当前最新版是3.3.2

另外Maven Wrapper默认下载配置好的Maven,也就是mvn -v命令依赖的版本:

如果想指定Maven版本,则追加版本号,即使用命令mvn wrapper:wrapper -Dmaven=3.9.9指定下载3.9.9版本。

另,Maven马上迎来4这个大版本,拭目以待吧。

根据Maven Wrapper版本的不同,会在当前目录下生成3或4个文件:

  • .mvn\wrapper\maven-wrapper.jar.mvn\wrapper隐藏目录下的JAR包,没有显式告知版本号。通过JD-GUI等反编译工具当然可以查找到版本号。3.3.2版本(自什么版本开始,暂时未知)的Maven Wrapper不再生成此文件(下文有原因分析)。
  • .mvn\wrapper\maven-wrapper.properties:配置文件,内容如下,据此可知使用的Maven和Maven Wrapper版本,可解答上面的问题。注:Maven Wrapper有些版本此文件只有一个distributionUrl,无法一眼看出Wrapper版本,这是Takari Maven Wrapper。
sh 复制代码
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar

另外,如果不再生成JAR包,则maven-wrapper.properties配置文件也会不一样:

sh 复制代码
wrapperVersion=3.3.2
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
  • mvnw:用于Linux、Mac等系统的脚本;
  • mvnw.cmd:用于Windows系统的脚本;

下载安装

因为执行mvn wrapper:wrapper命令安装Maven Wrapper需提前下载并配置好Maven。如果开发者电脑里没有Maven,咋办呢。

参考stackoverflow,从这里选择一个版本,推荐最新版。一般直接下载第一个文件即可,形如maven-wrapper-distribution-x.x.x-bin.zip的压缩文件。解压缩,然后手动添加.mvn/wrapper/maven-wrapper.properties配置文件并指定想要使用的Maven版本(参考上文);提交到Git Server前,最好删除压缩包里的JAR文件。

确实有点麻烦。

Takari

上面提到JD-GUI可反编译maven-wrapper.jar文件。值得注意的是,除了Apache Maven Wrapper外,还有个Takari Maven Wrapper:

上面截图,Takari Maven Wrapper使用的包路径是apache。

在Maven仓库里搜索,最后一个GAV如下:

xml 复制代码
<dependency>
	<groupId>io.takari</groupId>
	<artifactId>maven-wrapper</artifactId>
	<version>0.5.6</version>
</dependency>

时间停留在2019年12月4日。

这两者的关系是:Maven Wrapper,最初由Takari团队开发。后来这个工具被广泛采纳和使用,成为Maven官方推荐的工具。因此之前的GAV不再发布新版本,最新的GAV如下:

xml 复制代码
<dependency>
	<groupId>org.apache.maven.wrapper</groupId>
	<artifactId>maven-wrapper</artifactId>
	<version>3.3.2</version>
</dependency>

版本号和上面提到的3.3.2对应得上。

版本控制系统

可能有些朋友会有疑问,使用Maven Wrapper无需下载安装Maven,更无需配置环境变量,但上面却使用mvn wrapper:wrapper命令来安装Maven Wrapper,mvn命令的使用不是得提前安装并配置Maven么。

是不是有种鸡生蛋,还是蛋生鸡的既视感?

实际上,执行mvn wrapper:wrapper命令生成的几个文件,是需要一并提交到Git版本管理系统里。也就是说,从GitHub或GitLab上clone下来的某个项目。如果有这几个文件,则不管你是否安装并配置过Maven,都会使用此项目指定的Maven版本,也就是Maven Wrapper通过配置文件maven-wrapper.properties指定的版本:

从截图可知,默认在C盘用户目录下安装zip文件并解压,然后设置MAVEN_HOME指向解压缩后的目录。另外,最新版Maven Wrapper,即3.3.2版本会删除压缩包,毕竟留着也没用。

反过来,正是因为这几个文件需要提交到Git Server,如Maven Wrapper早期版本会生成JAR包,此文件说大不大,说小也并不小,40~50k左右。

Git版本管理系统有一个最佳实践是:提交代码、配置等文本文件,尽可能不要提交图片、JAR包等二进制文件。

有些朋友可能会说,就那么50k,也没有什么大碍吧。编码洁癖强迫症,像我就是,真受不了。

或者,有其他朋友会说,这些二进制文件并不会更新,一次性提交,后期不会更新,不要紧的。

等等,真的不会更新吗?

指定Maven Wrapper版本

不管是使用Takari Maven Wrapper,还是使用Apache Maven Wrapper,就和使用其他三方依赖,如Spring Boot版本号,肯定是尽可能使用次新版,毕竟新版本修复若干问题和优化若干性能等。

然后上面提到的命令行追加的版本号针对的是Maven的版本。如果想要使用某个版本的Maven Wrapper,而不是最新版的Maven Wrapper,该怎么做呢。

还是用命令行,此处对一个空的文件夹执行命令mvn wrapper:wrapper:3.1.0

执行失败,报错如下:

No plugin descriptor found at META-INF/maven/plugin.xml。什么鬼,我想下载的是3.1.0版本,怎么会是\wrapper\wrapper\3.0.3\wrapper-3.0.3.jar这个JAR,去D盘一看,确实有这个JAR,Maven仓库也有这个JAR:

这里给出一个未加证实的猜测:Maven命令执行时从Maven仓库下载指定版本的JAR,如果没有指定版本的JAR,会做降级处理,下载低版本的JAR。比如我想要的是3.1.0版本,结果会下载3.0.3,毕竟Maven仓库里只有这个版本。至于那个报错,No plugin descriptor是Maven Wrapper的插件扫描机制的报错。

稍加分析,不难得知,mvn wrapper:wrapper是经过缩写的命令行,如果需要指定版本,必须写全GAV信息,即groupId、artifactId、version。但是很不幸,还是相同的报错:No plugin descriptor found at META-INF/maven/plugin.xml

经过各种反复试错,以及对报错的分析plugin.xml,加上之前经常看到maven-xxx-plugin

于是想到执行命令:mvn org.apache.maven.plugins:maven-wrapper-plugin:wrapper,终于执行成功:

可以看到命令执行成功,虽然还是使用最新版Maven Wrapper;但是,至少我们知道mvn wrapper:wrappermvn org.apache.maven.plugins:maven-wrapper-plugin:wrapper命令的缩写。

现在需要做的就是加上版本号。

姿势:mvn org.apache.maven.plugins:maven-wrapper-plugin:3.1.0:wrapper

经过试错,发现早期版本的Apache Maven Wrapper(区别于Takari Maven Wrapper)不支持在非Maven工程目录下执行mvn wrapper:wrapper

直到3.3.0版本,才开始支持对非Maven项目(没有pom.xml文件)执行mvn wrapper:wrapper

经过验证,Apache Maven Wrapper第一个版本,即3.1.0,可从Maven仓库搜索得知,还是存在maven-wrapper.jar这个JAR文件:

后面Apache Maven团队优化这个问题,即执行mvn wrapper:wrapper命令时不生成JAR文件,当然就不用提交到Git Server。JAR文件也参与到版本控制系统里,确实很不合适。

org.apache.maven.plugins:maven-wrapper-plugin和org.apache.maven.wrapper:maven-wrapper

上面在诸般尝试切换Maven Wrapper版本时,发现两个很类似的GAV:

xml 复制代码
<dependency>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-wrapper-plugin</artifactId>
	<version>3.3.2</version>
</dependency>

以及

xml 复制代码
<dependency>
	<groupId>org.apache.maven.wrapper</groupId>
	<artifactId>maven-wrapper</artifactId>
	<version>3.3.2</version>
</dependency>

两者都可在Maven参考搜索,但版本号有些许不同。那有什么区别呢?

org.apache.maven.plugins:maven-wrapper-plugin:如命名所示,这是一个Maven插件,用于帮助项目创建Maven Wrapper;通过Maven插件的方式执行,可生成mvnw脚本和.mvn目录。

org.apache.maven.wrapper:maven-wrapper:是Maven Wrapper的实际实现,包含Maven Wrapper的核心逻辑和脚本文件,用于包装和管理Maven的版本。maven-wrapper是在mvnw脚本中调用的库,负责下载和执行指定版本的Maven。它确保即使在没有预先安装Maven的情况下,项目也能使用指定的Maven版本来进行构建。

前者是插件,后者是具体的实现类库。

其他诸多Maven插件和实现库,原理类似。

升级Maven Wrapper

上面也提到更高版本更加合理,对一个传统的非Maven项目执行mvn wrapper:wrapper变成Maven项目,是很合理的需求及业务场景。

所以,升级Maven Wrapper也是理所当然的。

对一个已经使用Maven Wrapper的Maven项目,执行命令mvn wrapper:wrapper会升级Maven Wrapper版本到最新,升级还是涉及那几个文件:两个脚本文件、一个配置文件、一个可选的JAR文件。如果指定版本号,则升级到指定的版本号。

也就是说,使用早期版本的Maven Wrapper(所有Takari版本的Maven Wrapper,以及早期的Apache Maven Wrapper),执行命令会更新JAR,这个JAR是需要提交到Git Server的。Maven团队也意识到这个问题,在某个版本后,执行命令不再生成JAR文件,只剩下3个文件。

不同版本的潜在问题

上面提到Maven Wrapper可以使得同一个开发团队里的不同开发者使用相同版本的Maven。不过,鄙人用了8年多Maven,也没怎么去关注过CI/CD系统使用的Maven版本,倒一直没有遇到Maven版本不同带来的任何问题。

只能说,使用不同的大版本(如Maven 3.3和Maven 3.6)的Maven,可能存在如下潜在的问题:

  • 构建结果不一致:不同的 Maven 版本可能对插件、依赖解析、配置处理等有不同的行为。如果开发者使用的 Maven 版本不一致,可能会导致在不同机器上构建相同项目时产生不同的结果。这种不一致会在 CI/CD 流水线中表现得更加明显,特别是在构建成功与失败的标准上;
  • 插件兼容性问题:Maven 插件可能针对特定的 Maven 版本进行开发和优化。使用较新的 Maven 版本可能会默认启用或弃用一些功能,而较旧的 Maven 版本则可能不支持这些功能,导致插件无法正常运行或行为不一致;
  • 性能差异:Maven 的不同版本在性能上可能有显著差异。新版本通常会修复一些性能问题或引入更高效的依赖管理机制,但旧版本可能仍然存在这些性能问题,这会导致构建时间上的差异,进而影响开发效率;
  • Bug 和功能特性:新版本的 Maven 通常修复了一些已知的 bug,并引入了新的功能。如果团队成员使用的版本不一致,可能会导致部分成员无法使用新功能,或者某些问题在新版本中已被修复但在旧版本中仍然存在,这会导致团队协作中的额外沟通和问题排查成本;
  • 配置文件解析:Maven 的某些配置文件解析逻辑可能在不同版本中有所不同,尤其是在处理复杂的继承结构、Profile、插件配置等方面。这会导致同一套配置文件在不同的 Maven 版本中产生不同的解析结果,从而影响构建过程;
  • 依赖解析问题:Maven 版本之间的依赖解析算法可能会有所不同。新版本可能会更智能地处理冲突和版本解析,而旧版本可能存在解析错误或不完善的地方。这种差异会导致开发者在解析依赖时遇到不同的问题,甚至导致构建失败。

推荐和CI系统使用的Maven保持一致,或引入Maven Wrapper。

使用脚本

以Windows操作系统为例,双击mvnw.cmd文件,会根据.mvn\wrapper\maven-wrapper.properties文件里的distributionUrl,去指定的地方(https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/,这个地址基本上固定不变)下载指定版本的Maven压缩包(如上是`3.8.6`版本),存储到C盘用户目录下,如`C:\Users\johnny\.m2\wrapper\dists\apache-maven-3.8.6-bin\1ks0nkde5v1pk9vtc31i9d0lcd`。随机生成的文件夹。

国内网络因为一些原因,下载速度可能会非常慢(不使用VPN,浏览器打开上述下载地址,速度还行;命令行会慢很多)。下载成功后,解压缩zip包,解析项目pom.xml文件,开启新一轮的漫长等待:

默认情况下,三方依赖JAR会下载到C:\Users\johnny\.m2\repository下。

切换源

上面提到Maven Wrapper速度慢(应该是仅大陆用户吧)的问题:

  • 下载Maven压缩zip包
  • 下载pom.xml文件引入的依赖

针对第一个问题,可考虑修改配置文件maven-wrapper.properties里的distributionUrl指向阿里云或别的镜像仓库:https://mirrors.aliyun.com/apache/maven/maven-3/

阿里云只维护这么几个Maven版本,少得有点过分:

针对第二个问题,

一般本机安装Maven时,都会在conf/settings.xml中配置maven的国内源,但使用Maven Wrapper时并没有看到类似setting.xml的配置文件。此时可在项目pom.xml中直接指定国内源。在公司开发多个项目时,则可考虑在parent父工程里的pom.xml文件里添加类似配置:

xml 复制代码
<repositories>
	<repository>
		<id>aliyun</id>
		<url>https://maven.aliyun.com/repository/maven-public/</url>
	</repository>
</repositories>
<pluginRepositories>
	<pluginRepository>
		<id>aliyun</id>
		<url>https://maven.aliyun.com/repository/maven-public/</url>
	</pluginRepository>
</pluginRepositories>

mvnw.cmd

还是以Windows系统为例,最新版(3.3.2版本)的mvnw.cmd文件,去掉License相关注释(@REM开头的行就是注释)后如下:

sh 复制代码
<# : batch portion
@REM Optional ENV vars
@REM   MVNW_REPOURL - repo url base for downloading maven distribution
@REM   MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
@REM   MVNW_VERBOSE - true: enable verbose log; others: silence the output
@REM ----------------------------------------------------------------------------

@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
@SET __MVNW_CMD__=
@SET __MVNW_ERROR__=
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
@SET PSModulePath=
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
  IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
)
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
@SET __MVNW_PSMODULEP_SAVE=
@SET __MVNW_ARG0_NAME__=
@SET MVNW_USERNAME=
@SET MVNW_PASSWORD=
@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
@echo Cannot start maven from wrapper >&2 && exit /b 1
@GOTO :EOF
: end batch / begin powershell #>

$ErrorActionPreference = "Stop"
if ($env:MVNW_VERBOSE -eq "true") {
  $VerbosePreference = "Continue"
}

# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
if (!$distributionUrl) {
  Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
}

switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
  "maven-mvnd-*" {
    $USE_MVND = $true
    $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
    $MVN_CMD = "mvnd.cmd"
    break
  }
  default {
    $USE_MVND = $false
    $MVN_CMD = $script -replace '^mvnw','mvn'
    break
  }
}

# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
if ($env:MVNW_REPOURL) {
  $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
  $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
}
$distributionUrlName = $distributionUrl -replace '^.*/',''
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
if ($env:MAVEN_USER_HOME) {
  $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
}
$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"

if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
  Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
  Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
  exit $?
}

if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
  Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
}

# prepare tmp dir
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
trap {
  if ($TMP_DOWNLOAD_DIR.Exists) {
    try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
    catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
  }
}

New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null

# Download and Install Apache Maven
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
Write-Verbose "Downloading from: $distributionUrl"
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"

$webclient = New-Object System.Net.WebClient
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
  $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
}
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null

# If specified, validate the SHA-256 sum of the Maven distribution zip file
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
if ($distributionSha256Sum) {
  if ($USE_MVND) {
    Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
  }
  Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
  if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
    Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
  }
}

# unzip and move
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
try {
  Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
} catch {
  if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
    Write-Error "fail to move MAVEN_HOME"
  }
} finally {
  try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
  catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}

Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"

脚本解读:

参考

相关推荐
摆烂且佛系19 小时前
IDEA Maven 仓库配置优先级
github·maven·intellij-idea
momo_via1 天前
maven下载与安装及在IDEA中配置maven
java·maven·intellij-idea
李贺梖梖1 天前
Maven 设置项目编码,防止编译打包出现编码错误
java·maven
洛克大航海1 天前
Ubuntu安装JDK与Maven和IntelliJ IDEA
ubuntu·jdk·maven·intellij idea
假客套1 天前
2025 FastExcel在Java的Maven项目的导出和导入,简单易上手,以下为完整示例
java·maven·fastexcel
有梦想的攻城狮1 天前
Maven中的settings.xml文件配置详解
xml·java·maven·settings.xml
李贺梖梖2 天前
maven本地仓库有相应的依赖,依旧会从远程仓库拉取问题的原因及解决
maven
纳于大麓2 天前
Android Maven私服搭建(Windows)
java·maven
诸神缄默不语3 天前
Maven用户设置文件(settings.xml)配置指南
xml·java·maven
安逸sgr3 天前
SpringMVC启动流程
java·jvm·spring·spring cloud·eclipse·tomcat·maven