0%

引言

本文将介绍一款用于迁移 Elasticsearch 数据的实用工具 —— elasticdump。它专为在不同 Elasticsearch 实例之间导出和导入数据而设计,非常适合小规模的数据迁移、备份以及测试等场景。

elasticdump

elasticdump简介

elasticdump 是一个命令行工具,主要用于将 Elasticsearch 的索引数据导出(dump)为 JSON 文件或从 JSON 文件导入到 Elasticsearch。它支持迁移 Mapping(映射)、Data(数据)。

GitHub 地址(官方文档):
https://github.com/elasticsearch-dump/elasticsearch-dump

安装方法

elasticdump 是一个基于 Node.js 的命令行工具,因此在安装时需要使用 npm 命令。

1
npm install elasticdump -g

具体使用

本节将通过一个具体示例,演示如何从 es1 实例中导出数据,并将其导入到 es2 实例中。

定义变量

1
2
es1_input="http://IP:PORT/index"
es2_output="http://IP:PORT/index"

步骤1: 从 es1 实例导出数据

导出列类型

1
2
3
4
elasticdump \
--input="${es1_input}" \
--output=./my_index_mapping.json \
--type=mapping

说明:在 Elasticsearch 中,导出”列类型”相当于导出数据库中的表结构。如果不迁移这些字段映射(mapping),Elasticsearch 会自动推断字段类型,这可能导致类型不准确,进而引发查询或索引异常。因此,建议在迁移数据时一并迁移字段映射,以确保数据结构的一致性。

导出数据

1
2
3
4
elasticdump \
--input="${es1_input}" \
--output=./my_index.json \
--type=data

说明1: 导出的数据是 jsonline 格式,一行一条,每条是一个 json 格式的数据。

说明2: 上述命令适用于 Linux 或 macOS 系统,使用 \ 作为续行符。如果你使用的是 Windows 操作系统,则在命令提示符(CMD)中执行命令时需要将续行符改为 ^

1
2
3
4
elasticdump ^
--input="${es1_input}" ^
--output=my_index.json ^
--type=data

步骤2: 将数据导入 es2 实例

导入列类型

1
2
3
4
elasticdump \
--input=my_index_mapping.json \
--output="${es2_output}" \
--type=mapping

细心的读者可能已经注意到,这里的 input 和 output 与步骤1中正好相反:此处的 input 是本地的 JSON 文件,而 output 则是 Elasticsearch 实例。

导入数据

1
2
3
4
elasticdump \
--input=my_index.json \
--output="${es2_output}" \
--type=data

总结

Elasticdump 是一款简单却功能强大的 Elasticsearch 数据迁移工具。通过几条简洁的命令,就可以轻松实现对 Elasticsearch 数据的导出与导入。无论是数据迁移、备份,还是测试,它都能成为你的得力助手。


微信端的朋友也可关注我的公众号

qrcode-12cm

引言

对于开发人员来说树形结构是一种非线性结构,与数组、栈、队列等线性结构相比,复杂度更高。要深入理解和掌握树形结构,亲自绘制树形结构示意图是一种有效的方法,正所谓想要做到心中有“树”,需要自己动手实践。本文介绍一款高效的绘图工具Graphviz,并演示2个使用 Graphviz 绘制树形结构的例子,同时文章还会推荐一些相关的工具,帮助读者更容易地入门。

Graphviz简介

Graphviz ,是Graph Visualization Software的缩写,是由AT&T实验室精心打造的开源工具包,它在生成各种图形表示方面应用广泛。Graphviz采用DOT语言描述图表,并通过布局引擎解析脚本,自动完成脚本的布局, 此外,它还支持多种丰富的导出格式,包括SVG和PDF等。

我对Graphviz的喜爱源于其自动布局的特性,它真正实现了“Graph as code”的理念,即像编写代码一样绘制图形。Graphviz的强大之处在于其“所思即所得”(WYTIWYG,what you think is what you get)的工作方式,这与传统的“所见即所得”(WYSIWYG,what you see is what you get)工具截然不同。普通的所见即所得工具依赖于鼠标拖拽操作,而使用Graphviz时,我的主要职责是编写DOT脚本,只需关注图形中各元素之间的关系,无需分心于布局问题,这让我从排版的繁琐中解放出来。在日常开发工作中,这一点尤为重要,我们希望在构思代码的同时,能够专注于数据结构本身,快速根据我们的思路生成示意图,而不必时刻考虑排版问题。这样的工作方式让我们能够更加专注于工作本身,将注意力集中在问题的核心上。

我的实践

本节演示2个用Graphviz画树形结构的例子,让大家对Graphviz先有个直观的认识。

实践1 树形结构示意图

我接到了一个关于树形结构的开发任务,需要将数据按照省,市,区县的结构组织成树形结构, 借助Grapihviz可以很方便的绘制一个树形结构示意图,方便开发者沟通交流,效果图如下。

OrgTree-preview

树形结构图对应的源码如下,可以看到只需要定义节点及节点之间的关系,不需要关心布局。

OrgTree-codesnippet

实践2 双向链接树形结构示意图

一般的树形结构从父节点可以找到子节点,但是从子节点找不到父节点,我需要构造一个双向链接的树形结构,也即从子节点也可以找到父节点,例如我想知道市辖区属于哪个地市,则需要通过子节点不断向上遍历直到找到合适的父节点。 下图是用Graphivz绘制的示意图,可以看到父子节点之间的箭头是双向的。

OrgMultiLinkTree-preview

如下是对应的源码,这里箭头的指向为双向。

OrgMultiLineTree-codesnippet

工具推荐

前几节介绍了Graphviz以及使用它的原因, 并且展示了我用它画树形结构的实例,本节介绍一些具体的工具,方便大家入门。

推荐1 Graphviz 命令行工具

Graphviz其实是一个命令行工具,使用它之前需要先安装,不同的操作系统安装方式不同,具体可查看官网 https://graphviz.org/

  • 如果是mac可通过homebrew或者port安装
  • 如果是windows可直接下载安装包
  • 如果是linux根据不同的发行版用不同的包管理软件安装 如aptyum

如下是用Graphviz命令将dot源文件生成图片的命令示例

1
dot -Tpng input.dot -o output.png

推荐2 vscode插件

Graphviz 是一个强大的命令行工具,但使用起来可能不太方便。幸运的是,VSCode 提供了一些插件,可以显著改善用户体验,使使用 Graphviz 更加直观和友好,下面介绍一些好用的插件。

  1. Graphviz language support for visual studio code

提供语法高亮

vscode-Graphviz-language-support-for-visual-studio-code

  1. Graphviz Preview

可以快速预览或者导出graphviz文件,这样就不需要记忆具体的命令了。

vscode-Graphviz-Preview

推荐3 python接口

很多编程语言会集成 Graphviz 绘图工具,PyGraphviz是Graphviz的Python接口,允许开发者使用Python代码与Graphviz进行交互,实现图形的创建、布局、可视化和分析

官网 https://github.com/pygraphviz/pygraphviz

总结

通过掌握基本的 DOT 语言和 Graphviz 工具,用户可以轻松创建和自定义自己的图形,满足常见的可视化需求。正所谓一图胜千言,不论是在学术研究、软件开发还是日常工作中,Graphviz 都能够提供极大的便利和帮助,赶快实践起来,体验它的强大功能吧!


微信端的朋友也可关注我的公众号

qrcode-12cm

引言

我们知道shell中的命令都是串行执行的,如果想要充分利用服务器资源,减小等待时间, 让程序并行运行就需要些技巧了, 本文分享一些shell并行运行程序的技巧,下面会用代码演示并行执行3个任务,并等待所有任务都执行完成后再执行后续的逻辑。

代码演示

准备工作

在做整体的演示之前,先写一个程序模拟任务运行的过程,之后演示时会用到该程序。

code-task

该程序名叫task.sh,其会休眠几秒模拟任务运行的过程,其有2个参数

  • 第一个参数是任务名称
  • 第二个参数是要休眠的时长(单位秒),相当于模拟程序运行了多少秒

调用方式如下

1
2
# 启动一个名为task1的任务, 运行10秒后结束
./task.sh task1 10

串行执行

shell中的命令都是串行执行的,下面这段代码比较典型,在启动程序的脚本中非常常见,大家应该比较熟悉。

如下依次启动3个任务, task1 运行10秒, task2 运行20秒,task3运行30秒

code-test1

test1执行结果如下

run-test1

可以看到任务是串行执行的, task1运行了10秒,task2运行了20秒,task3运行了30秒,总共运行了60秒,这个符合我们的预期。

并行执行

之前讲了shell中的命令都是串行执行的,如果想要充分利用服务器资源,减小等待时间, 让程序并行运行该如何做呢?

这里并不希望大量改造程序,希望用最小的代价,让程序并行运行, 下面提供一种思路 使用&和wait改造启动脚本即可,不需要大量改造程序。

基础知识铺垫

知识点1:**& 让程序后台运行**

在shell脚本中当我们需要把一个任务放在后台运行时,通常我们会使用&符号, 此时主进程会继续往下执行,而子进程会在后台启动运行。

知识点2:wait命令 在shell脚本“多进程”执行模式下,起到一些特殊控制的作用

× wait命令用来阻塞当前进程的执行,直至指定的子进程执行结束后,才继续执行。如果wait后面不带任何的进程号或作业号,那么wait会阻塞当前进程的执行,直至当前进程的所有子进程都执行结束后,才继续执行

× wait命令一个很重要用途就是用在shell并行编程中,可以在shell脚本中启动多个后台进程(使用&),然后调用wait命令,等待所有后台进程都运行完毕后,主进程再继续向下执行。

并行改造1

有了上面的知识铺垫,下面再看一段代码(参见 test2),其和test1的区别是增加了&, 让任务后台运行。

code-test2

执行结果如下

run-test2

可以看到3个任务虽然是并行执行的,但总运行时长是0秒, 也即主进程并没有等待3个任务运行完就结束了,相当于失去了对子进程的控制。

并行改造2

下面再看一段代码(参见test3), 其在test2的基础上更进一步,在末尾加了wait命令,其会等待上面3个任务都执行完才会执行后面的命令

code-test3

test3的运行结果如下,可以看到总运行时长是30秒,从时间上可以看出其是等运行时间最长的任务结束后才执行后面的逻辑,也即等所有任务都运行结束后才会执行后面的逻辑。

run-test3

这就是我想要的结果,比较贴合实际情况,在子任务运行完成后,主程序还要做一些收尾工作,所以要等所有子任务都执行完才能执行后续的逻辑。

结语

通过上述的改造, 可以大大的提高程序运行效率,在完成业务需求的同时,还可以充分利用主机资源,减少等待时间。

该技巧抽取自笔者真实项目中的实践,在几乎没有改造主体代码的情况下就能让程序并行运行,也不失为一种好办法。笔者就是用这些朴素的技巧,硬是将运行时长从十几小时降到十分钟内,达到了优化目标,小伙伴们快点实践起来吧!


微信端的朋友也可关注我的公众号

qrcode-12cm

引出问题

做数据加工的同学有时需要通过shell脚本执行sql语句操作MySQL, 对于下面的命令应该并不陌生。

1
mysql -h${mysql_host}  -P${mysql_port} -u${mysql_user} -p${mysql_password} ${mysql_dbname}  -e "source ./sql/sql1.sql"

如果直接将密码写到命令行将出现如下的警告

Warning: Using a password on the command line interface can be insecure.

warning-insecure

那么用shell脚本操作MySQL时如何避免将密码明文写到shell脚本中呢? 本文提供2种思路

解决思路

思路1 defaults-extra-file 选项

defaults-extra-file 是 mysql 命令的一个选项,它允许你指定一个包含额外配置信息的配置文件,对于在执行mysql命令时提供一些额外的配置非常有用。

具体命令

1
mysql --defaults-extra-file=./my.cnf  -e "source ./sql/sql1.sql"

可以看到这里并没有在命令行中指定数据库连接信息,而是将这些信息配置在配置文件./my.cnf中

my.cnf

1
2
3
4
5
6
[client]
host=YOUR_HOST
port=YOUR_PORT
user=YOUR_USER
password=YOUR_PASSWORD
database=YOUR_DATABASE

思路2 MySQL 安全登陆工具 mysql_config_editor

mysql_config_editor 是MySQL自带的一款用于安全加密登录的工具,可以在一些场合避免使用明文密码;另外如果使用mysql命令登录数据库也可以避免每次都输入一堆参数。

具体使用方式

步骤1: 先用 mysql_config_editor 添加一个login-path

1
mysql_config_editor set -G test-login-path1 -h ${mysql_host} -P ${mysql_port} -u ${mysql_user} -p
  1. 该命令用mysql_config_editor创建一个名为test-login-path1的login-path
  2. 其会将登录MySQL的 username、password、port等信息加密存入一个隐藏文件 ~/.mylogin.cnf
  3. 该文件可作为连接MySQL服务器的认证凭证,以后在命令行或者脚本中可免于输入明文密码,安全方便

步骤2: 连接数据库时指定login-path即可,不需要再指定数据库信息

如下演示2个使用 –login-path 参数的命令

登录mysql的场景

1
mysql --login-path=test-login-path1

连接mysql 并执行sql语句的场景

1
mysql --login-path=test-login-path1 -e "source ./sql/sql1.sql"

是不是挺方便? 这样既避免了敏感信息的暴露,也避免了重复输入登录信息,方便了运维人员的工作。

总结

本文详细探讨了两种连接 MySQL 时避免将密码以明文形式写入shell 脚本的策略。通过采纳这些最佳实践,我们在完成工作的基础上,进一步加强了对敏感信息的保护。这些技巧的应用不仅提升了系统的安全水平,还展现了对隐私和敏感信息负责任的态度, 快点实践起来吧!


微信端的朋友也可关注我的公众号

qrcode-12cm

引言

本文分享一个shell脚本中处理命令行选项的小技巧,我看到项目中许多shell脚本没有处理命令行参数这部分内容,这也是导致我们写的shell脚本不够灵活,不能抽取成可复用脚本的原因之一,所以分享下自己在这方面的实践。

引出问题

在命令行中通常有两种类型的选项:短命令行选项(short options)和长命令行选项(long options)

  • 短命令行选项通常用于提供快速且紧凑的命令行选项 如 -s csv
  • 长命令行选项通常用于提供更具可读性和描述性的选项 如 --file-suffix=csv

现在需要写一个shell脚本, 可同时支持短选项和长选项,写好后使用方式如下:

  • 短选项方式 x.sh -s csv
  • 长选项方式 x.sh --file-suffix=csv

在shell中getopts可以很好的处理短选项,那如何处理长选项呢?

基础知识铺垫

在解决这个问题之前先做一个基础知识的铺垫

如下的代码,只要写过shell脚本的同学都知道,是引用变量的值。

1
${value}

再进一步理解如下的代码,其使用了一种叫变量扩展的知识点

1
${value#pattern}

其返回的不是value变量的值,而是一个替换后的值。那又是如何替换的呢?删除value中与pattern相匹配的部分,从左向右匹配,举个例子

1
2
3
value=www.skygroup.com
echo ${value#*.}
输出 skygroup.com

解决办法

再回到最开始的问题,我们要写一个shell脚本可同时支持短选项和长选项两种风格的命令行参数,这里介绍其中一种思路,通过getopts命令结合上面讲的变量扩展的知识点解决解析命令行长选项的问题

有了刚才基础知识的铺垫,下面看一段更完整的代码片断

getopts-long-options

假设执行的命令是 x.sh --file-suffix=csv,我们需要从命令行选项--file-suffix=csv中解析出具体的值csv,这里就用到了${value#pattern}的知识点,关键代码如下

1
file_suffix="${OPTARG#*=}"

结语

本文从实际工作场景出发介绍了笔者在写shell脚本时处理命令行选项,特别是长命令行选项的思路。能够处理命令行参数才能使我们的脚本更灵活,在工作中也更容易抽取出可复用的核心脚本。

其中涉及到变量扩展的知识点,因为篇幅限制没有展开细讲,笔者在博客中专门写了一篇关于shell变量扩展的文章,有兴趣的读者可参考。


微信端的朋友也可关注我的公众号

qrcode-12cm

1. 基础知识

1.1 变量引用

shell引用变量的基本格式 ${parameter}

  • 如果parameter是数字,则是参数扩展
  • 如果parameter是字符串,则是变量扩展
  • 如果parameter是数组,遵循数组的扩展规则;
  • parameter还可以是@ * # ? -等特殊参数,参考特殊参数的引用

1.2 变量扩展

1.2.1 模式扩展

1.2.1.1 ${value#pattern}

删除value中与pattern相匹配的部分,从左向右匹配

1
2
3
value=www.skygroup.com
echo ${value#*.}
输出 skygroup.com

1.2.1.2 ${value##pattern}

也是删除value中与pattern相匹配的部分,从左向右匹配,但是#与##的区别在于贪婪模式上

  • #是非贪婪模式,也即最短匹配模式 lazy
  • ##是贪婪模式,也即最长匹配模式 greedy
1
2
3
value=www.skygroup.com
echo ${value##*.}
输出 com

1.2.1.3 ${value%pattern}

删除value中与pattern相匹配的部分,但是是从右向左匹配

1
2
3
4
value=www.skygroup.com
echo ${value%.*}
输出 www.skygroup.
注意因为是%是从右向左匹配,所以要写成.* 而不是*.

1.2.1.4 ${value%%pattern}

也是删除value中与pattern相匹配的部分,从右向左匹配,%与%%的区别在于贪婪模式上

  • %是非贪婪模式,也即最短匹配模式 lazy
  • %%是贪婪模式,也即最长匹配模式 greedy
1
2
3
value=www.skygroup.com
echo ${value%%.*}
输出 www

1.2.1.5 总结

  • #表示从左向右匹配,##表示从左向右贪婪匹配,删除位于#右侧通配符匹配的字符串
  • %表示从右向左匹配,%%表示从右向左贪婪匹配,删除位于%右侧通配符匹配的字符串

记忆的方法为

  • #是去掉左边(键盘上#在$的左边)
  • %是去掉右边(键盘上%在$的右边)

2. 案例

2.1 长命令行选项

背景: 在命令行中通常有两种类型的选项:短命令行选项(short options)和长命令行选项(long options)。短选项通常用于提供快速且紧凑的命令行选项,长命令行选项通常用于提供更具可读性和描述性的选项

现在需要写一个shell脚本, 可同时支持短选项和长选项,写好后使用方式如下。

  1. 短选项方式 x.sh -s csv
  2. 长选项方式 x.sh --file-suffix=csv

在shell中getopts可以很好的处理短选项,那如何处理长选项呢?

这里介绍其中一种思路,需要用到上面讲的变量扩展中关于模式扩展的知识点 ${value#pattern}

完整代码片断如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 解析命令行选项
while getopts "s:vh-:" opt; do
case $opt in
s)
file_suffix="$OPTARG"
;;
-)
case "${OPTARG}" in
file-suffix=*)
file_suffix="${OPTARG#*=}"
;;
*)
echo "unknown option: --$OPTARG" >&2
exit 1
;;
esac
;;
\?)
echo "unknown option: -$OPTARG" >&2
exit 1
;;
esac
done

假设执行的命令是 x.sh --file-suffix=csv,我们需要从命令行选项--file-suffix=csv中解析出具体的值csv,这里就用到了${value#pattern}的知识点,关键代码如下

1
file_suffix="${OPTARG#*=}"

2.2 获取不带后缀的文件名

假设获取到的文件名是 x.sh,我希望从x.sh中提取出不带后缀名的文件名,这时就用到了${value%pattern}的知识点

1
2
3
4
5
6
SHELL_NAME=x.sh
SHELL_NAME0=${SHELL_NAME%.*}
echo ${SHELL_NAME0}

输出
x

微信端的朋友也可关注我的公众号

qrcode-12cm

引言

Vim是笔者最常用的一款文本编辑软件,几乎伴随着我学习linux的全过程, 是一款经过时间考验的工具

本文介绍一个vim小技巧,临时加载配置

当时也是让笔者眼前一亮的技巧,因为笔者在实际工作中定义了很多快捷键,快捷键多了之后就会出现难以记忆而且相互冲突的问题,实际上这些快捷键不需要立即加载,只在某些场景下临时用一下即可,该技巧正好解决了笔者的痛点。

背景

笔者在用vim处理文本时会临时定义一些快捷键,但不希望将这些快捷键定义在全局的配置文件.vimrc中,只是希望在某些场景临时用一下,但是因为命令多又不想一个一个执行,有没有办法一次执行呢?

1
2
3
:map ,y :Bill<CR>
:map ,l A 漏记<ESC>
:map ,c :s/\s*漏记.*$//<CR>

解决办法

步骤1: 创建一个名为 commands.vim 的脚本文件,将你的命令逐行写入

1
2
3
4
" commands.vim
map ,y :Bill<CR>
map ,l A 漏记<ESC>
map ,c :s/\s*漏记.*$//<CR>

步骤2: 打开 Vim 执行以下命令加载脚本文件中的命令

1
:source /path/to/commands.vim

是不是挺高效? 比起很多“开箱即用”的编辑器,Vim 有一定的学习曲线,但是依然觉得值得。


微信端的朋友也可关注我的公众号

qrcode-12cm

简述

在拼接字符串的过程中有时需要拼接分隔符, 如果你只会用 + 或者 StringBuilder/ StringBuffer 拼接字符串?那你就 OUT 了,建议使用 Java 8 中的这款字符串拼接神器:StringJoiner,你值得拥有

代码片断

原来的字符串拼接代码

1
2
3
4
5
6
7
8
String logStr="\n---------------------------------------------------------------------" +
"\n请求ip:" + clientIp + ":" + clientPort +
"\n请求url:"+clientUrl+
"\n请求方式:"+requestMethod+
"\n执行时长:" + stopwatch.elapsed(TimeUnit.MILLISECONDS) + "ms" +
"\n入参:" + (args == null ? "[]" : Arrays.toString(args)) +
"\n出参:" + (result == null ? "[]" : objectMapper.writeValueAsString(result))+
"\n----------------------------------------------------------------------";

使用 StringJoiner 之后

1
2
3
4
5
6
7
8
9
10
11
StringJoiner stringJoiner = new StringJoiner("\n");
stringJoiner.add("");
stringJoiner.add("---------------------------------------------------------------------");
stringJoiner.add("请求ip:" + clientIp + ":" + clientPort);
stringJoiner.add("请求url:"+clientUrl);
stringJoiner.add("请求方式:"+requestMethod);
stringJoiner.add("执行时长:"+stopwatch.elapsed(TimeUnit.MILLISECONDS) + "ms");
stringJoiner.add("入参:" + (args == null ? "[]" : Arrays.toString(args)));
stringJoiner.add("出参:" + (result == null ? "[]" : objectMapper.writeValueAsString(result)));
stringJoiner.add("----------------------------------------------------------------------");
String logStr=stringJoiner.toString();

不需要自己处理分隔符了,是不是轻闲了不少?

进一步研究源码就会发现,其实际上是对 StringBuilder 做了进一步的封装

stringJoiner-01


微信端的朋友也可关注我的公众号

qrcode-12cm

背景说明

如果你的工程中使用maven, 你是否亲自处理过jar包冲突?

jar包冲突一般是间接依赖引起的,举个例子,假如你在项目中使用了2个jar包,分别是A和B, 现在A依赖C, B也依赖C, 但是A依赖的C的版本是1.0, B依赖的C的版本是2.0。

这时候你的项目中的C就会有2个不同的版本,这时maven会依据自己的原则,来决定使用哪个版本的jar包, 而另一个无用的的jar包则未被使用,这就是所谓的依赖冲突

在大多数情况下,依赖冲突并不会对系统造成什么异常,但是在某些情况下,有可能会出现找不到类的异常, 因此理解冲突导致的原因并且快速定位到冲突源,是每个程序员的必修课

这里列出一些常见的由于jar包冲突导致的异常

  • 程序抛出java.lang.ClassNotFoundException异常
  • 程序抛出java.lang.NoSuchMethodError异常
  • 程序抛出java.lang.NoClassDefFoundError异常
  • 程序抛出java.lang.LinkageError异常等

这些是能够直观呈现的,当然还有隐性的异常,比如程序执行结果与预期不符等。

原理介绍

上面介绍了什么是jar包冲突以及jar包冲突有可能产生的错误, 本节讲一些maven jar包管理原则,了解这些背景知识会帮助我们解决jar包冲突。

依赖传递

当在maven项目中引入A依赖,A依赖通常又会引入B的jar包,B可能还会引入C的jar包。这样,当你在pom.xml文件中添加了A依赖,maven会自动帮你把所有相关的依赖都添加进来. 这就叫做依赖传递. transitive dependencies

maven引入依赖传递机制,一方面大大简化和方便了依赖声明,另一方面,大部分情况下我们只需要关心项目的直接依赖是什么,而不用考虑这些间接依赖。但有时候,当依赖传递造成问题的时候,我们就需要清楚地知道该传递性依赖是从哪条依赖路径引入的。

依赖调解

上一节讲了maven的依赖传递, 其是jar包冲突产生的原因, 而当maven中出现依赖冲突时,其又是如何解决的呢? 这就涉及到依赖调解,maven中有2条处理依赖的原则

  1. 第一原则:最短路径优先原则
  2. 第二原则:最先声明优先原则

最短路径优先原则

主要根据依赖的路径长短来决定引入哪个依赖(两个冲突的依赖)

示例:

maven-helper-27267

项目中同时引入了A和B两个依赖,它们都间接引入了Z依赖,但由于B的依赖链路比较短,因此最终生效的是Z(20.0)版本。这就是最短路径优先原则。

此时如果Z的21.0版本和20.0版本区别较大,那么就会发生jar包冲突的表现

最先声明优先原则

如果两个依赖的路径一样,最短路径优先原则是无法进行判断的,此时需要使用最先声明优先原则,也就是说,谁的声明在前则优先选择谁。

示例:

maven-helper-20082

A和B最终都依赖Z,此时A的声明(pom中引入的顺序)优先于B,则针对冲突的Z会优先引入Z(21.0)。

如果Z(21.0)向下兼容Z(20.0),则不会出现Jar包冲突问题。但如果将B声明放前面,则有可能会发生Jar包冲突

工程问题总结

上面说的都是原理,可能大家更关心的是如下2个问题

  1. 我的项目中到底有没有依赖冲突?
  2. 冲突是从哪里引入的?

解决思路

方法1 maven-dependency-plugin 插件

maven-dependency-plugin插件可以打印依赖树,如果加上-Dverbose参数可以查看更详细的日志,当然也包括冲突的jar包

1
mvn dependency:tree -Dverbose

但是很少有人告诉你该插件从3.0开始不再支持 -Dverbose 参数,所以即使加上该参数其也不会输出冲突的jar包,如果使用该参数会看到如下的日志

1
Verbose not supported since maven-dependency-plugin 3.0

所以正确的命令是指定一个支持-Dverbose参数的版本,例如2.10版本

1
mvn org.apache.maven.plugins:maven-dependency-plugin:2.10:tree -Dverbose > tree.txt

方法2 IDEA 插件 Maven Helper

在这里向大家推荐IDEA 解决Maven依赖冲突的高能神器 Maven Helper。

maven-helper-7082

当你安装完该插件后,打开pom.xml 会看到一个 Dependency Analyzer 的标签页

maven-helper-7069

其有三大功能,而且还提供搜索功能方便使用

  1. Conflicts(查看冲突):可以很方便的看出哪些jar包冲突,其冲突是从哪些jar包引入的, 最终用的是哪个版本
  2. All Dependencies as List(列表形式查看所有依赖)
  3. All Dependencies as Tree(树形式查看所有依赖)

实践案例

案例1 log4j漏洞爆发后快速定位log4j-2.X的引入源头

2021年12月7日 log4j爆发了史诗级漏洞,log4j这个再平常不过的jar包,如今却变成了洪水猛兽,只要工程中有log4j-2.X相关版本的jar包,都要立即清除,当时需要确认工程中是否引入了log4j-2.X版本, 我们确实没有在工程中显式引入log4j-2.X,但是最终的lib中确实有log4j-2.X相关的jar包。

maven-helper-26949

我们希望能快速定位到是哪个依赖间接引入了log4j-2.X, 当时就借助了Maven Helper插件,很快定位到问题。

如下可以看到是mybatis-spring-boot-starter这个依赖间接引入了log4j-2.X 相关的jar包

maven-helper-24139

案例2: 引入 EasyExcel 引发 asm依赖冲突

项目中有excel导出功能,所以引入了EasyExcel, 当时希望将导出excel的功能封装到一个jar包里,但是在使用的过程中有报错,后来定位到问题与依赖冲突有关系。

当时就借助Maven Helper插件, 可以看到spring-boot-starter-test 依赖asm 5.0.3, blue-web中通过easyexcel依赖asm 7.1, 而最终使用的asm的版本是通过spring-boot-starter-test 引入的, 定位到冲突的产生的根源后,我们很快解决了问题。

maven-helper-30182

结语

理解冲突导致的原因并且快速定位到冲突源,是每个程序员的必修课,希望通过该文档的讲解能帮助到大家。


微信端的朋友也可关注我的公众号

小马向前走

qrcode-12cm

引言

接口调试是每个软件开发从业者必不可少的一项技能,一个项目的完成,可能接口调试的时间比真正开发写代码的时间还要多,几乎是每个开发者的日常工作项。之前写的几篇文章都是从HOW的角度讲如何使用 REST Client 插件, 没有从为什么的角度去讲, 今天将从WHY的角度讲为什么使用该插件。

实际上有很多接口测试工具 如postman, swagger, 他们的使用体验都非常好,那为什么我们会选择REST Client 插件呢?

其除了能满足基本的接口测试功能外, 最大的好处就在于分享, 因为接口文件是纯文本,我们可以把HTTP文件放到GitLab上, 这样既便于管理也便于分享。 所有开发者或者使用项目的人都能复用这个HTTP文件,也极大的方便管理所有REST API, 满足自己的同时也方便了他人

结语

当你跳出个人的视角,从团队的角度出发, 拉通各个环节之间的壁垒, 提升协作的效率 , 也许比单纯的局部优化要好


微信端的朋友也可关注我的公众号

小马向前走

qrcode-12cm