点击上方蓝色字体,关注我们
前言
Maven 是众多 Java 开发者所青睐的构建工具之一。还记得未接触Maven之前,所有的依赖包都是从网站找的,找了某个jar包后,又要去搜索它的依赖,如此循环往复。耗费大量的时间和精力,很多初识Maven的开发者可能会存在很多疑惑,没有系统的摸清Maven的用法,所以本篇文章想要介绍在Maven领域的一切实践思路,让读者更好的领略 Maven 的精髓。
maven的兼容性矩阵
maven的兼容性可能一直被大多数人忽视,确实属于比较小众的知识点,可偏偏就被笔者遇到了。事情起源于一次系统重装,重装后高高兴兴的下载的开发工具idea和maven。但是由于两者的兼容性问题,导致一直打包报错。整个经历过程如下:
高高兴兴的下载Idea 2019.2.2 和 Maven 3.6.2。
安装配置,一气呵成!
打包报错????
配置没问题,环境变量没问题,到底是哪里出了问题?
崩溃!换3.6.0版本的Maven
再次打包成功!
看来是Idea 和 maven的兼容性问题导致的。果然,每次下载最新版本,总能能遇到一些坑。后续通过 idea官网 也印证了这个兼容性的bug。
官方已经在IntelliJ IDEA 2019.2.3 及 2019.2.2.3版本后修复了这个问题。
Maven 和 JDK 的兼容性,这个只需要注意满足最低版本即可,由于现在普遍使用的是Java 8,所以一般不会有什么问题,参考整理的兼容性矩阵如下:
认识 maven 的生命周期
相信一提到Maven,大家脑海里浮现出来它的第一个作用,就是用来打包,但是经常“用完即走”的我们真的到了需要认认真真梳理一下Maven的相关概念了。
Maven为我们提供了一套自动化的构建方案,从最基础的清理、编译代码、运行测试到打包和部署。这一系列过程称之为它的生命周期。
默认情况下,Maven有三套相互独立的生命周期,分别是 clean、default 和site。每个生命周期包含一些阶段(phase),阶段是有顺序的,后面的阶段依赖于前面的阶段。每个阶段的内部由 插件的 Goals(目标)组成。
可以这样理解,其实phase就是goal的容器。真正被执行的都是goal (目标)。
phase (阶段)被执行时,实际执行的都是被绑定到该 phase (阶段)的一系列goal。
而 goal 与 goal 之间是独立的。单独执行一个goal不会导致其他goal被执行。
深入理解 maven 的生命周期
maven使用一个名为 components.xml 的配置文件来描述其架构的组织结构,以我本地的maven 3.6.0版本为例。不同版本的maven, 配置文件的路径相似。
apache-maven-${version}\lib\maven-core-${version}.jar
\META-INFO\plexus\conponents.xml
这里我们针对default生命周期来作详细的了解:
下图是我整理出default生命周期的所有 phase (阶段)。
mvn <phase> { 例如: mvn package }
可以看到 当我们执行一个 简单的 mvn package 时,maven其实内部按照顺序流转了很多的phase (阶段)。
那如何来理解 Goal (目标)呢?
我们前面说过,一系列的 Goal (目标) 组成 了 phase (阶段)。
相信你用过maven的很多插件吧?这里我以 打 jar包的插件为例来介绍:
聪明的你看到这个图可能已经猜到了,jar:后面的一系列单词 就是Goal (目标)。
解释一下,goal 其实是由存在于Maven的插件plugin提供的一个个小的功能程序。你可以根据自己的实际需求,灵活的来组装他们,实现各种定制的功能。执行格式为:
mvn [插件名称]:[goal的名称]
再举一个实际案例,在打包时,分离出三方的依赖包。我们会使用到一个叫maven-dependency-plugin的插件。
上图表明,我制定了一个copy-dependencies 的目标,在 prepare-package 阶段执行。也就是在执行package打包之前,拷贝依赖到当前工程的lib文件夹下面。
直接通过执行命令也能达到上述的效果:
mvn clean dependency:copy-dependencies package
Maven 管理项目的依赖
Maven提供的依赖管理,是其中的一大核心功能。就像文章开始所说的,我们需要一个jar包,这个jar包又依赖于另外的jar包,这种依赖关系会不断的传递,直至最后一个没有任何其他依赖包。
Maven就是通过自动去发现和包含这些传递的依赖,避免了我们人工的去搜索下载。
这个传递依赖的层级是没有限制的,在使用过程中需要注意的一点是,循环依赖的问题。
遭遇到循环依赖如何解决呢?Maven 提供了一个 build-helper-maven-plugin 插件。其原理是:将三个模块合并成一个中间的工程,临时编译出模块D,然后三个模块再分别依赖D 进行编译。
不推荐使用这种办法,这会导致我们的项目工程结构很混乱,只能作为临时规避的一个措施来使用。
还有一种办法是通过分析依赖,从根本上消除掉循环依赖。常用的分析依赖的命令有:
mvn dependency:help
mvn dependency:analyze
mvn dependency:tree
mvn dependency:tree -Dverbose
除了使用命令查看依赖树,常见的办法还有:
-
在项目启动时把所有的加载的 jar 包都打印出来,添加 VM 参数 -verbose:class 。通过打印的信息确认是否正确的 jar 包被依赖。
-
有的时候仅仅通过 mvn dependency:tree 可能无法快速的发现冲突,这个时候就可以尝试使用 Enforcer 插件,这个插件也可以自定义许多规则,我们可以使用 dependencyConvergence 规则。来保证所有的依赖都使用相同的版本。
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>1.3.1</version>
<executions>
<execution>
<id>enforce</id>
<configuration>
<rules><DependencyConvergence /></rules>
</configuration>
<goals>
<goal>enforce</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
Maven 管理的依赖范围
依赖范围主要是用来限制各个包的传递性。并且影响我们最终的构建结果。在引入依赖时,通过标签来指定依赖的范围。
Maven 内部为我们提供了 6种依赖的范围:
-
compile:缺省值,编译、测试都有效。
-
provided:编译,测试都有效,但是在运行时并不会加入。例如Servlet API,因为Web容器本身有,如果加入就会出现冲突。
-
runtime:测试、运行有效,表明这个依赖项不是编译所必须的,但是是执行时需要的。需要位于运行时的类路径中。
-
test:仅测试时有效,这个范围不是可传递的。
-
system:与本机相关,可移植性差。
-
import:导入的依赖范围,仅适用在dependencyManager中,表示从其他pom导入dependency配置。
dependencyManagement 作用
上面介绍的依赖范围都比较好理解,最后一个import范围涉及到一个dependencyManagement,这里我们具体解释一下他和dependencies 的区别。
dependencyManagement 主要有两个作用:
-
一是集中管理项目的依赖项
-
二是控制使用的依赖项的版本
如果有多个子模块依赖相同的包,那么放在 中来管理。
使用 dependencyManagement 声明依赖,在项目中并不会实际引入,因此子项目需要显示声明需要用的依赖。
当不在子项目中声明依赖的时候,是不会从父项目中继承下来的。
只有在子项目中写了该依赖项,并且没有指定具体版本,才会从父项目中继承该项,并且 version 和 scope 都读取自父 pom
如果子项目中指定了版本号,那么会使用子项目中指定的 jar 版本
而对于 dependencies :
dependencies 即使在子项目中不写该依赖项,那么子项目仍然会从父项目中继承该依赖项。父工程使用 dependencyManagement 假引用,目的是管理版本号。dependencies 用于实际上需要引入的工程,这些工程如果继承于父工程会找到对应的版本号。
Maven 的仓库分类
从大类来看,Maven仓库主要分为两大类,一是本地仓库,二是远程仓库。
本地仓库,主要是用来存储从远程仓库下载的插件和 jar 包,当项目需要使用 插件 或 jar 包时,会优先从本地仓库中查找。
远程仓库,是当无法在本地仓库中查找到包时,会默认去远程仓库下载。
远程仓库包括了,中央仓库、镜像仓库和私服。
而在配置了多个 Maven 仓库查找依赖的顺序大致如下:
-
1) 在本地仓库中寻找,如果没有则进入下一步。
-
2) 在全局配置的私服仓库(settings.xml中配置的并有激活)中寻找,如果没有则进入下一步。
-
3) 在项目自身配置的私服仓库(pom.xml)中寻找,如果没有则进入下一步。
-
4) 在中央仓库中寻找,如果没有则终止寻找。
Maven 内置的变量
${basedir} 表示项目根目录(即pom.xml文件所在目录)
${project.build.directory} 构建目录,缺省为target目录
${project.build.outputDirectory} 构建过程输出目录,缺省为target/classes
${project.build.finalName} 产出物名称比如jar包,缺省为${project.artifactId}-${project.version}
${project.packaging} 打包类型,缺省为jar
${project.xxx} 当前pom文件的任意节点的内容
${env.xxx} 获取系统环境变量。例如,"env.PATH"指代了$path环境变量(在Windows上是%PATH%)。
${settings.xxx} 指代了settings.xml中对应元素的值。
Java System Properties: 所有可通过java.lang.System.getProperties()访问的属性都能在POM中使用
比较实用的,例如 ${JAVA_HOME}。
总结
本文主要以一个兼容性问题为引,介绍了maven的兼容性矩阵,及其三大默认的生命周期,了解了阶段(phase) 和 Goals(目标)的概念。对比了dependencyManagement和dependencies的区别。以及Maven仓库的分类、依赖管理的范围及内置实用变量的学习。希望能帮助到大家。
Java垃圾收集器一网打尽, ZGC 和 Shenandoah 听说过吗
我就知道你“在看”