超级详细的 Maven 教程(基础 + 高级)
1. Maven 是什么
Maven 是 Apache 软件基金会组织维护的一款专门为 Java 项目提供构建和依赖管理支持的工具。
一个 Maven 工程有约定的目录结构,约定的目录结构对于 Maven 实现自动化构建而言是必不可少的一环,就拿自动编译来说,Maven 必须 能找到 Java 源文件,下一步才能编译,而编译之后也必须有一个准确的位置保持编译得到的字节码文件。 我们在开发中如果需要让第三方工具或框架知道我们自己创建的资源在哪,那么基本上就是两种方式:
通过配置的形式明确告诉它
基于第三方工具或框架的约定 Maven 对工程目录结构的要求
1.1 构建
Java 项目开发过程中,构建指的是使用『原材料生产产品』的过程。
构建过程主要包含以下环节:
1.2 依赖
Maven 中最关键的部分,我们使用 Maven 最主要的就是使用它的依赖管理功能。当 A jar 包用到了 B jar 包中的某些类时,A 就对 B 产生了依赖,那么我们就可以说 A 依赖 B。
依赖管理中要解决的具体问题:
jar 包的下载:使用 Maven 之后,jar 包会从规范的远程仓库下载到本地
jar 包之间的依赖:通过依赖的传递性自动完成
jar 包之间的冲突:通过对依赖的配置进行调整,让某些 jar 包不会被导入
2. Maven 开发环境配置
2.1 下载安装
首页:
Maven – Welcome to Apache Maven
下载页面:
或者你也可以选择之前的版本:
然后里面选择自己对应的版本下载即可:
下载之后解压到非中文、没有空格的目录,如下:
2.2 指定本地仓库
本地仓库默认值:用户家目录/.m2/repository。由于本地仓库的默认位置是在用户的家目录下,而家目录往往是在 C 盘,也就是系统盘。将来 Maven 仓库中 jar 包越来越多,仓库体积越来越大,可能会拖慢 C 盘运行速度,影响系统性能。所以建议将 Maven 的本地仓库放在其他盘符下。配置方式如下:
本地仓库这个目录,我们手动创建一个空的目录即可。
记住:一定要把 localRepository 标签从注释中拿出来。
注意:本地仓库本身也需要使用一个非中文、没有空格的目录。
2.3 配置阿里云提供的镜像仓库
Maven 下载 jar 包默认访问境外的中央仓库,而国外网站速度很慢。改成阿里云提供的镜像仓库,访问国内网站,可以让 Maven 下载 jar 包的时候速度更快。配置的方式是:
将原有的例子配置注释掉
加入自己的配置
2.4 配置基础 JDK 版本
如果按照默认配置运行,Java 工程使用的默认 JDK 版本是 1.5,而我们熟悉和常用的是 JDK 1.8 版本。修改配置的方式是:将 profile
标签整个复制到 settings.xml 文件的 profiles
标签内。
2.5 配置环境变量
Maven 是一个用 Java 语言开发的程序,它必须基于 JDK 来运行,需要通过 JAVA_HOME 来找到 JDK 的安装位置。
可以使用下面的命令验证:
然后新建环境变量:
配置环境变量的规律:
XXX_HOME 通常指向的是 bin 目录的上一级
PATH 指向的是 bin 目录
在配置 PATH
通过 mvn -v
验证:
3. Maven 的使用
3.1 核心概念:坐标
数学中的坐标使用 x、y、z 三个『向量』作为空间的坐标系,可以在『空间』中唯一的定位到一个『点』。
Maven 中的坐标使用三个『向量』在『Maven 的仓库』中唯一的定位到一个『jar』包。
groupId:公司或组织的 id,即公司或组织域名的倒序,通常也会加上项目名称
例如:groupId:com.javatv.maven
artifactId:一个项目或者是项目中的一个模块的 id,即模块的名称,将来作为 Maven 工程的工程名
例如:artifactId:auth
version:版本号
例如:version:1.0.0
提示:坐标和仓库中 jar 包的存储路径之间的对应关系,如下
上面坐标对应的 jar 包在 Maven 本地仓库中的位置:
3.2 pom.xml
POM:Project Object Model,项目对象模型。和 POM 类似的是:DOM(Document Object Model),文档对象模型。它们都是模型化思想的具体体现。
POM 表示将工程抽象为一个模型,再用程序中的对象来描述这个模型。这样我们就可以用程序来管理项目了。我们在开发过程中,最基本的做法就是将现实生活中的事物抽象为模型,然后封装模型相关的数据作为一个对象,这样就可以在程序中计算与现实事物相关的数据。
POM 理念集中体现在 Maven 工程根目录下 pom.xml 这个配置文件中。所以这个 pom.xml 配置文件就是 Maven 工程的核心配置文件。其实学习 Maven 就是学这个文件怎么配置,各个配置有什么用。
3.3 依赖
上面说到我们使用 Maven 最主要的就是使用它的依赖管理功能,引入依赖存在一个范围,maven 的依赖范围包括: compile
,provide
,runtime
,test
,system
。
compile:表示编译范围,指 A 在编译时依赖 B,该范围为默认依赖范围。编译范围的依赖会用在编译,测试,运行,由于运行时需要,所以编译范围的依赖会被打包。
provided:provied 依赖只有当 jdk 或者一个容器已提供该依赖之后才使用。provide 依赖在编译和测试时需要,在运行时不需要。例如:servlet api 被 Tomcat 容器提供了。
runtime:runtime 依赖在运行和测试系统时需要,但在编译时不需要。例如:jdbc 的驱动包。由于运行时需要,所以 runtime 范围的依赖会被打包。
test:test 范围依赖在编译和运行时都不需要,只在测试编译和测试运行时需要。例如:Junit。由于运行时不需要,所以 test 范围依赖不会被打包。
system:system 范围依赖与 provide 类似,但是必须显示的提供一个对于本地系统中 jar 文件的路径。一般不推荐使用。
而在实际开发中,我们常用的就是 compile
、test
、provided
。
3.4 依赖的传递
A 依赖 B,B 依赖 C,那么在 A 没有配置对 C 的依赖的情况下,A 里面能不能直接使用 C?
再以上的前提下,C 是否能够传递到 A,取决于 B 依赖 C 时使用的依赖范围。
B 依赖 C 时使用 compile 范围:可以传递
B 依赖 C 时使用 test 或 provided 范围:不能传递,所以需要这样的 jar 包时,就必须在需要的地方明确配置依赖才可以。
3.5 依赖的排除
当 A 依赖 B,B 依赖 C 而且 C 可以传递到 A 的时候,A 不想要 C,需要在 A 里面把 C 排除掉。而往往这种情况都是为了避免 jar 包之间的冲突。
所以配置依赖的排除其实就是阻止某些 jar 包的传递。因为这样的 jar 包传递过来会和其他 jar 包冲突。
一般通过使用excludes
标签配置依赖的排除:
3.6 继承
3.6.1 概念
Maven 工程之间,A 工程继承 B 工程
B 工程:父工程
A 工程:子工程
本质上是 A 工程的 pom.xml 中的配置继承了 B 工程中 pom.xml 的配置。
3.6.2 作用
在父工程中统一管理项目中的依赖信息,具体来说是管理依赖信息的版本。
它的背景是:
对一个比较大型的项目进行了模块拆分。
一个 project 下面,创建了很多个 module。
每一个 module 都需要配置自己的依赖信息。
它背后的需求是:
在每一个 module 中各自维护各自的依赖信息很容易发生出入,不易统一管理。
使用同一个框架内的不同 jar 包,它们应该是同一个版本,所以整个项目中使用的框架版本需要统一。
使用框架时所需要的 jar 包组合(或者说依赖信息组合)需要经过长期摸索和反复调试,最终确定一个可用组合。这个耗费很大精力总结出来的方案不应该在新的项目中重新摸索。
通过在父工程中为整个项目维护依赖信息的组合既保证了整个项目使用规范、准确的 jar 包;又能够将以往的经验沉淀下来,节约时间和精力。
3.6.3 一个例子
① 一般再模块化开发中一般都会创建一个父工程,如下:
父工程创建好之后,要修改它的打包方式:
只有打包方式为 pom 的 Maven 工程能够管理其他 Maven 工程。打包方式为 pom 的 Maven 工程中不写业务代码,它是专门管理其他 Maven 工程的工程,所以可以将生成的 src 目录删除。
② 创建模块工程
然后可以再父工程的 pom 文件中看到:
而子工程的 pom 如下:
③ 在父工程中配置依赖的统一管理
使用dependencyManagement
标签配置对依赖的管理,如下:
而实际上被管理的依赖并没有真正被引入到工程。
④ 子工程中引用那些被父工程管理的依赖
关键点:省略版本号
子工程引用父工程中的依赖信息时,可以把版本号去掉。把版本号去掉就表示子工程中这个依赖的版本由父工程决定,具体来说是由父工程的 dependencyManagement 来决定。
子工程 pom 如下:
此时,被管理的依赖才被引入到工程。
⑤ 修改父工程依赖信息的版本
这个修改可以是降级,也可以是升级,但一般来说都是升级。
⑥ 父工程中声明自定义属性
对同一个框架的一组 jar 包最好使用相同的版本,为了方便升级框架,可以将 jar 包的版本信息统一提取出来,统一声明版本号 :
在需要的地方使用${}
的形式来引用自定义的属性名,真正实现一处修改,处处生效。
编写一套符合要求、开发各种功能都能正常工作的依赖组合并不容易。如果公司里已经有人总结了成熟的组合方案,那么再开发新项目时,如果不使用原有的积累,而是重新摸索,会浪费大量的时间。为了提高效率,我们可以使用工程继承的机制,让成熟的依赖组合方案能够保留下来。如下:
如上图所示,公司级的父工程中管理的就是成熟的依赖组合方案,各个新项目、子系统各取所需即可。
3.7 聚合
聚合,指分散的聚集到一起,即部分组成整体。
3.7.1 Maven 中的聚合
使用一个总工程将各个模块工程汇集起来,作为一个整体对应完整的项目,实际就是 module
标签。
项目:整体
模块:部分
3.7.2 继承和聚合的对应关系
从继承关系角度来看:
父工程
子工程
从聚合关系角度来看:
总工程
模块工程
3.7.3 聚合的配置
在总工程中配置 modules 即可:
3.7.4 依赖循环问题
如果 A 工程依赖 B 工程,B 工程依赖 C 工程,C 工程又反过来依赖 A 工程,那么在执行构建操作时会报下面的错误:
这个错误的含义是:循环引用。
4. build 标签
在实际使用 Maven 的过程中,我们会发现 build 标签有时候有,有时候没,这是怎么回事呢?其实通过有效 POM 我们能够看到,build 标签的相关配置其实一直都在,只是在我们需要定制构建过程的时候才会通过配置 build 标签覆盖默认值或补充配置。这一点我们可以通过打印有效 POM 来看到。
打印有效 pom
mvn help:effective-pom
当默认配置无法满足需求的定制构建的时候,就需要使用 build 标签。
4.1 build 标签的组成
build 标签的子标签大致包含三个主体部分:
4.1.1 定义约定的目录结构
各个目录的作用如下:
4.1.2 备用插件管理
pluginManagement 标签存放着几个极少用到的插件:
maven-antrun-plugin
maven-assembly-plugin
maven-dependency-plugin
maven-release-plugin
通过 pluginManagement 标签管理起来的插件就像 dependencyManagement 一样,子工程使用时可以省略版本号,起到在父工程中统一管理版本的效果。
4.1.3 生命周期插件
plugins 标签存放的是默认生命周期中实际会用到的插件,这些插件想必大家都不陌生,所以抛开插件本身不谈,plugin 标签的结构如下:
① 坐标部分
artifactId 和 version 标签定义了插件的坐标,作为 Maven 的自带插件这里省略了 groupId。
② 执行部分
executions 标签内可以配置多个 execution 标签,execution 标签内:
id:指定唯一标识
phase:关联的生命周期阶段
goals/goal:关联指定生命周期的目标
goals 标签中可以配置多个 goal 标签,表示一个生命周期环节可以对应当前插件的多个目标。
4.2 典型应用:指定 JDK 版本
前面我们在 settings.xml 中配置了 JDK 版本,那么将来把 Maven 工程部署都服务器上,脱离了 settings.xml 配置,如何保证程序正常运行呢?思路就是我们直接把 JDK 版本信息告诉负责编译操作的 maven-compiler-plugin 插件,让它在构建过程中,按照我们指定的信息工作。如下:
settings.xml 中配置:仅在本地生效,如果脱离当前 settings.xml 能够覆盖的范围,则无法生效。
在当前 Maven 工程 pom.xml 中配置:无论在哪个环境执行编译等构建操作都有效。
4.3 典型应用:SpringBoot 定制化打包
很显然 spring-boot-maven-plugin 并不是 Maven 自带的插件,而是 SpringBoot 提供的,用来改变 Maven 默认的构建行为。具体来说是改变打包的行为。默认情况下 Maven 调用 maven-jar-plugin 插件的 jar 目标,生成普通的 jar 包。
普通 jar 包没法使用 java -jar xxx.jar 这样的命令来启动、运行,但是 SpringBoot 的设计理念就是每一个『微服务』导出为一个 jar 包,这个 jar 包可以使用 java -jar xxx.jar 这样的命令直接启动运行。
这样一来,打包的方式肯定要进行调整。所以 SpringBoot 提供了 spring-boot-maven-plugin 这个插件来定制打包行为。
5. 依赖配置补充
管理依赖最基本的办法是继承父工程,但是和 Java 类一样,Maven 也是单继承的。如果不同体系的依赖信息封装在不同 POM 中了,没办法继承多个父工程怎么办?这时就可以使用 import 依赖范围。
5.1 import
典型案例当然是在项目中引入 SpringBoot、SpringCloud 依赖:
import 依赖范围使用要求:
打包类型必须是 pom
必须放在 dependencyManagement 中
官网说明如下:
This scope is only supported on a dependency of type
pom
in the<dependencyManagement>
section. It indicates the dependency is to be replaced with the effective list of dependencies in the specified POM's<dependencyManagement>
section. Since they are replaced, dependencies with a scope ofimport
do not actually participate in limiting the transitivity of a dependency.
5.2 system
以 Windows 系统环境下开发为例,假设现在 D:\product\maven-demo-parent\demo-module\target\demo-module-1.0-SNAPSHOT.jar
想要引入到我们的项目中,此时我们就可以将依赖配置为 system 范围:
但是很明显:这样引入依赖完全不具有可移植性,所以不要使用。
5.3 runtime
专门用于编译时不需要,但是运行时需要的 jar 包。比如:编译时我们根据接口调用方法,但是实际运行时需要的是接口的实现类。典型案例是:
6. profile
6.1 profile 概述
这里我们可以对接 profile 这个单词中『侧面』这个含义:项目的每一个运行环境,相当于是项目整体的一个侧面。
通常情况下,我们项目至少有三种运行环境:
开发环境:供不同开发工程师开发的各个模块之间互相调用、访问;内部使用
测试环境:供测试工程师对项目的各个模块进行功能测试;内部使用
生产环境:供最终用户访问——所以这是正式的运行环境,对外提供服务
而我们这里的『环境』仍然只是一个笼统的说法,实际工作中一整套运行环境会包含很多种不同服务器:
MySQL
Redis
ElasticSearch
RabbitMQ
FastDFS
Nginx
Tomcat
……
就拿其中的 MySQL 来说,不同环境下的访问参数肯定完全不同,可是代码只有一套。如果在 jdbc.properties 里面来回改,那就太麻烦了,而且很容易遗漏或写错,增加调试的难度和工作量。所以最好的办法就是把适用于各种不同环境的配置信息分别准备好,部署哪个环境就激活哪个配置。
在 Maven 中,使用 profile 机制来管理不同环境下的配置信息。但是解决同类问题的类似机制在其他框架中也有,而且从模块划分的角度来说,持久化层的信息放在构建工具中配置也违反了『高内聚,低耦合』的原则。
实际上,即使我们在 pom.xml 中不配置 profile 标签,也已经用到 profile 了。为什么呢?因为根标签 project 下所有标签相当于都是在设定默认的 profile。这样一来我们也就很容易理解下面这句话:project 标签下除了 modelVersion 和坐标标签之外,其它标签都可以配置到 profile 中。
6.2 profile 配置
6.2.1 外部视角:配置文件
从外部视角来看,profile 可以在下面两种配置文件中配置:
settings.xml:全局生效。其中我们最熟悉的就是配置 JDK 1.8。
pom.xml:当前 POM 生效
6.2.2 内部实现:具体标签
从内部视角来看,配置 profile 有如下语法要求:
① profiles/profile 标签
由于 profile 天然代表众多可选配置中的一个所以由复数形式的 profiles 标签统一管理。
由于 profile 标签覆盖了 pom.xml 中的默认配置,所以 profiles 标签通常是 pom.xml 中的最后一个标签。
② id 标签
每个 profile 都必须有一个 id 标签,指定该 profile 的唯一标识。这个 id 标签的值会在命令行调用 profile 时被用到。这个命令格式是:
③ 其它允许出现的标签
一个 profile 可以覆盖项目的最终名称、项目依赖、插件配置等各个方面以影响构建行为。
build
defaultGoal
finalName
resources
testResources
plugins
reporting
modules
dependencies
dependencyManagement
repositories
pluginRepositories
properties
6.3 激活 profile
① 默认配置默认被激活
前面提到了,POM 中没有在 profile 标签里的就是默认的 profile,当然默认被激活。
② 基于环境信息激活
环境信息包含:JDK 版本、操作系统参数、文件、属性等各个方面。一个 profile 一旦被激活,那么它定义的所有配置都会覆盖原来 POM 中对应层次的元素。可参考下面的标签结构:
这里有个问题是:多个激活条件之间是什么关系呢?
Maven 3.2.2 之前:遇到第一个满足的条件即可激活——或的关系。
Maven 3.2.2 开始:各条件均需满足——且的关系。
下面我们来看一个具体例子。假设有如下 profile 配置,在 JDK 版本为 1.6 时被激活:
这里需要指出的是:Maven 会自动检测当前环境安装的 JDK 版本,只要 JDK 版本是以 1.6 开头都算符合条件。下面几个例子都符合:
1.6.0_03
1.6.0_02
……
6.4 Maven profile 多环境管理
在开发过程中,我们的软件会面对不同的运行环境,比如开发环境、测试环境、生产环境,而我们的软件在不同的环境中,有的配置可能会不一样,比如数据源配置、日志文件配置、以及一些软件运行过程中的基本配置,那每次我们将软件部署到不同的环境时,都需要修改相应的配置文件,这样来回修改,很容易出错,而且浪费劳动力。
因此我们可以利用 Maven 的 profile 来进行定义多个 profile,然后每个 profile 对应不同的激活条件和配置信息,从而达到不同环境使用不同配置信息的效果。
在 idea 中可以看到,因此,当你需要打包哪一个环境的就勾选即可:
同时,SpringBoot 天然支持多环境配置,一般来说,application.yml
存放公共的配置,application-dev.yml
、application-test.yml
、application.prod.yml
分别存放三个环境的配置。如下:
application.yml
中配置spring.profiles.active=prod
(或者 dev、test)指定使用的配置文件,如下:
注:profileActive
,就是上面我们自定义的标签。
然后当我们勾选哪一个环境,打包的配置文件就是那一个环境:
同时我们再在 resource 标签下看到 includes 和 excludes 标签。它们的作用是:
includes:指定执行 resource 阶段时要包含到目标位置的资源
excludes:指定执行 resource 阶段时要排除的资源
7. 搭建 Maven 私服:Nexus
很多公司都是搭建自己的 Maven 私有仓库,主要用于项目的公共模块的迭代更新等。
7.1 Nexus 下载安装
下载地址:https://download.sonatype.com/nexus/3/latest-unix.tar.gz
百度网盘:https://pan.baidu.com/s/12IjpSSUSZa6wHZoQ8wHsxg (提取码:5bu6)
然后将下载的文件上传到 Linux 系统,解压后即可使用,不需要安装。但是需要注意:必须提前安装 JDK。(我这里放在 /root/nexus 下)
解压后如下:
通过以下命令启动:
如果显示nexus is stopped.
则说明启动失败,通过命令查看端口占用情况:
可以看到 8081 端口被占用,而 nexus 的默认端口为 8081,我们可以修改其默认端口号,其配置文件在 etc
目录下的nexus-default.properties
,如下:
打开后修改为自己需要设置的端口,注意开启对外防火墙:
提示:
bin 目录下 nexus.vmoptions 文件,可调整内存参数,防止占用内存太大。
etc 目录下 nexus-default.properties 文件可配置默认端口和 host 及访问根目录。
然后访问 http://[Linux 服务器地址]:8081/
进入首页:
7.2 初始设置
点击右上角的登录:
这里参考提示:
用户名:admin
密码:查看 /opt/sonatype-work/nexus3/admin.password 文件
然后输入密码进行下一步:
匿名登录,启用还是禁用?由于启用匿名登录后,后续操作比较简单,这里我们演示禁用匿名登录的操作方式:
除了默认账号 admin,admin 具有全部权限,还有 anonymous,anonymous 作为匿名用户,只具有查看权限,但可以查看仓库并下载依赖。
完成:
7.3 Nexus Repository
nexus 默认创建了几个仓库,如下:
其中仓库 Type 类型为:
仓库名称:
其中 maven-public 相当于仓库总和,默认把其他 3 个仓库加进来一起对外提供服务了,另外,如果有自己建的仓库,也要加进该仓库才有用。
初始状态下,这几个仓库都没有内容:
7.4 创建 Nexus Repository
除了自带的仓库,有时候我们需要单独创建自己的仓库,按照默认创建的仓库类型来创建我们自己的仓库。
7.4.1 创建 Nexus 宿主仓库
点击左边导航栏中的 Repositories,如下图:
然后创建仓库,如下:
同理创建 releases 仓库,然后查看列表:
宿主仓库配置如下:
7.4.2 创建 Nexus 代理仓库
然后建一个代理仓库,用来下载和缓存中央仓库(或者阿里云仓库)的构件,这里选择 proxy:
然后创建:
代理仓库配置中,仓库 ID、仓库名称、Provider、Policy 以及 Default Local Storage Location 等配置的含义与宿主仓库相同,不再赘述。需要注意的是,代理仓库的 Repository Type 的取值是 proxy。
代理仓库配置如下表:
7.4.3 创建 Nexus 仓库组
下面我们将创建一个仓库组,并将刚刚创建的 3 个仓库都聚合起来,这里选择 group,如下:
查看 Nexus 仓库列表,可以看到创建的仓库组已经创建完成,如下图:
7.5 通过 Nexus 下载 jar 包
由于初始状态下都没有内容,所以我们需要进行配置,我们先在本地的 Maven 的配置文件中新建一个空的本地仓库作为测试。
然后,把我们原来配置阿里云仓库地址的 mirror 标签改成下面这样:
这里的 url 标签是这么来的:
把上图中看到的地址复制出来即可。如果我们在前面允许了匿名访问,到这里就够了。但如果我们禁用了匿名访问,那么接下来我们还要继续配置 settings.xml:
注意:server 标签内的 id 标签值必须和 mirror 标签中的 id 值一样。
然后找一个用到框架的 Maven 工程,编译 compile,下载过程日志:
下载后,Nexus 服务器上就有了 jar 包:
7.6 将 jar 包部署到 Nexus
这一步的作用是将通用的模块打成 jar 包,发布到 Nexus 私服,让其他的项目来引用,以更简洁高效的方式来实现复用和管理。
需要配置 server:
然后在我们需要上传的 maven 项目中的pom.xml
添加如下配置:
7.6.1 上传到 maven-snapshots
执行命令 mvn deploy
将当前 SNAPSHOT(快照版)上传到私服 maven-snapshots。
7.6.2 上传到 maven-releases
修改项目的版本,如下:
执行命令 mvn deploy
:
查看:
package 命令完成了项目编译、单元测试、打包功能。
install 命令完成了项目编译、单元测试、打包功能,同时把打好的可执行 jar 包(war 包或其它形式的包)布署到本地 maven 仓库。
deploy 命令完成了项目编译、单元测试、打包功能,同时把打好的可执行 jar 包(war 包或其它形式的包)布署到本地 maven 仓库和远程 maven 私服仓库。
8. jar 包冲突问题
先给结论:编订依赖列表的程序员。初次设定一组依赖,因为尚未经过验证,所以确实有可能存在各种问题,需要做有针对性的调整。那么谁来做这件事呢?我们最不希望看到的就是:团队中每个程序员都需要自己去找依赖,即使是做同一个项目,每个模块也各加各的依赖,没有统一管理。那前人踩过的坑,后人还要再踩一遍。而且大家用的依赖有很多细节都不一样,版本更是五花八门,这就让事情变得更加复杂。
所以虽然初期需要根据项目开发和实际运行情况对依赖配置不断调整,最终确定一个各方面都 OK 的版本。但是一旦确定下来,放在父工程中做依赖管理,各个子模块各取所需,这样基本上就能很好的避免问题的扩散。
即使开发中遇到了新问题,也可以回到源头检查、调整 dependencyManagement 配置的列表——而不是每个模块都要改。
8.1 表现形式
由于实际开发时我们往往都会整合使用很多大型框架,所以一个项目中哪怕只是一个模块也会涉及到大量 jar 包。数以百计的 jar 包要彼此协调、精密配合才能保证程序正常运行。而规模如此庞大的 jar 包组合在一起难免会有磕磕碰碰。最关键的是由于 jar 包冲突所导致的问题非常诡异,这里我们只能罗列较为典型的问题,而没法保证穷举。
但是我们仍然能够指出一点:一般来说,由于我们自己编写代码、配置文件写错所导致的问题通常能够在异常信息中看到我们自己类的全类名或配置文件的所在路径。如果整个错误信息中完全没有我们负责的部分,全部是框架、第三方工具包里面的类报错,这往往就是 jar 包的问题所引起的。
而具体的表现形式中,主要体现为找不到类或找不到方法。
8.1.1 抛异常:找不到类
此时抛出的常见的异常类型:
java.lang.ClassNotFoundException:编译过程中找不到类
java.lang.NoClassDefFoundError:运行过程中找不到类
java.lang.LinkageError:不同类加载器分别加载的多个类有相同的全限定名
我们来举个例子:
httpclient 这个 jar 包中有一个类:org.apache.http.conn.ssl.NoopHostnameVerifier。这个类在较低版本中没有,但在较高版本存在。比如:
那当我们确实需要用到 NoopHostnameVerifier 这个类,我们看到 Maven 通过依赖传递机制引入了这个 jar 包,所以没有明确地显式声明对这个 jar 包的依赖。可是 Maven 传递过来的 jar 包是 4.3.6 版本,里面没有包含我们需要的类,就会抛出异常。
而『冲突』体现在:4.3.6 和 4.4 这两个版本的 jar 包都被框架所依赖的 jar 包给传递进来了,但是假设 Maven 根据『版本仲裁』规则实际采纳的是 4.3.6。
版本仲裁
Maven 的版本仲裁机制只是在没有人为干预的情况下,自主决定 jar 包版本的一个办法。而实际上我们要使用具体的哪一个版本,还要取决于项目中的实际情况。所以在项目正常运行的情况下,jar 包版本可以由 Maven 仲裁,不必我们操心;而发生冲突时 Maven 仲裁决定的版本无法满足要求,此时就应该由程序员明确指定 jar 包版本。
版本仲裁遵循以下规则:
最短路径优先
在下图的例子中,对模块 pro25-module-a 来说,Maven 会采纳 1.2.12 版本。
路径相同时先声明者优先
此时 Maven 采纳哪个版本,取决于在 pro29-module-x 中,对 pro30-module-y 和 pro31-module-z 两个模块的依赖哪一个先声明。
8.1.2 抛异常:找不到方法
程序找不到符合预期的方法。这种情况多见于通过反射调用方法,所以经常会导致:java.lang.NoSuchMethodError。
8.1.3 没报错但结果不对
发生这种情况比较典型的原因是:两个 jar 包中的类分别实现了同一个接口,这本来是很正常的。但是问题在于:由于没有注意命名规范,两个不同实现类恰巧是同一个名字。
具体例子是实际工作中遇到过:项目中部分模块使用 log4j 打印日志;其它模块使用 logback,编译运行都不会冲突,但是会引起日志服务降级,让你的 log 配置文件失效。比如:你指定了 error 级别输出,但是冲突就会导致 info、debug 都在输出。
8.2 本质
以上表现形式归根到底是两种基本情况导致的:
同一 jar 包的不同版本
不同 jar 包中包含同名类
这里我们拿 netty 来举个例子,netty 是一个类似 Tomcat 的 Servlet 容器。通常我们不会直接依赖它,所以基本上都是框架传递进来的。那么当我们用到的框架很多时,就会有不同的框架用不同的坐标导入 netty。可以参照下表对比一下两组坐标:
但是偏偏这两个『不同的包』里面又有很多『全限定名相同』的类。例如:
8.3 解决办法
很多情况下常用框架之间的整合容易出现的冲突问题都有人总结过了,拿抛出的异常搜索一下基本上就可以直接找到对应的 jar 包。我们接下来要说的是通用方法。
不管具体使用的是什么工具,基本思路无非是这么两步:
第一步:把彼此冲突的 jar 包找到
第二步:在冲突的 jar 包中选定一个。具体做法无非是通过 exclusions 排除依赖,或是明确声明依赖。
8.3.1 IDEA 的 Maven Helper 插件
这个插件是 IDEA 中安装的插件,不是 Maven 插件。它能够给我们罗列出来同一个 jar 包的不同版本,以及它们的来源。但是对不同 jar 包中同名的类没有办法。
然后基于 pom.xml 的依赖冲突分析,如下:
查看冲突分析结果:
8.3.2 Maven 的 enforcer 插件
使用 Maven 的 enforcer 插件既可以检测同一个 jar 包的不同版本,又可以检测不同 jar 包中同名的类。
这里我们引入两个对 netty 的依赖,展示不同 jar 包中有同名类的情况作为例子。
然后配置 enforcer 插件:
执行如下 Maven 命令:
部分运行结果:
版权声明: 本文为 InfoQ 作者【Ayue、】的原创文章。
原文链接:【http://xie.infoq.cn/article/77c75784ede87b54d30c2dde8】。文章转载请联系作者。
评论