使用maven构建的工程的实践心得

  • Post author:
  • Post category:其他


点击上方蓝色字体,关注我们

前言

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 听说过吗


动图帮你理解【Java垃圾收集机制】


一文搞懂 JVM 架构和运行时数据区 (内存区域)


为什么你的 Spring Task 定时任务没有定时执行?


搞java开发,看懂JVM的GC日志真的很重要


一次容器化springboot程序OOM问题探险


Java 应用性能调优的一些实践


可视化界面在线生成JVM参数

我就知道你“在看”



版权声明:本文为u012811805原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。