在日常的开发中,排查问题是一个合格Java开发者的的基本能力。对于常见的NullPointerException,NoClassDefFoundError等问题一般通过google直接就能找到答案。
不过还有一些异常情况不是那么直观,google一般搜不到有效的信息,就需要深入研究排查。新人遇到这类问题,往往一脸懵逼,不知如何下手,请教高手,高手如果只是简单指导一个方向,新人踩过几个坑没解决后会更加沮丧。甚至怀疑自己遇到一个神秘的无法解决的bug。有经验的开发往往也不能一眼就看出问题的修复方案,但是排查问题多了会有大概的思路,需要沉下心来不断的踩坑、爬坑,最终才能定位解决问题。
本文以Maven构建工具为例,从原理、思路、工具、实践几方面分享Java中复杂jar包依赖问题排查经验。
二、Maven依赖问题的表现形式
1、老项目
-
一般是新引入了一个jar包,导致项目启动不起来
-
原来的服务好好的,新的改动不涉及这块,但是新改动代码后突然报错了
2、新项目
-
日志问题:众所周知,jar生态的日志包及其混乱,同一套日志组件,不同版本之间也可能会带来问题
-
组件整合问题:各组件底层依赖jar包版本不一致,导致问题。最麻烦的是不兼容的依赖。
三、问题排查策略
1、基础知识:Maven依赖传递策略
compile (编译范围):默认策略;编译,测试,运行,打包都能用
provided (已提供范围):编译,测试可用,不会打包到package包中。大部分情况下需要底层环境提供,比如tomcat服务中已经内置了servlet包,代码写代码时需要调用servlet的类
runtime (运行时范围):只有运行时才可用,但在编译的时候不需要。比如,你可能在编译的时候只需要JDBC API JAR,而只有在运行的时候才需要JDBC驱动实现。
test (测试范围):test范围依赖在编译和运行时都不需要,它们只有在测试编译和测试运行阶段可用。
system (系统范围):system范围依赖与provided 类似,但是你必须显式的提供一个对于本地系统中JAR 文件的路径。适合maven中央仓库中不存在jar包引用。
2、基础知识:Maven依赖树的解析规则
(1)深入优先原则
深度优先遍历依赖,并缓存节点剪枝。比如下图:
-
A→B→D→E/F
-
A→C→D
在第二步A→C→D时,由于节点D已经被缓存,所以会立即返回,不必再次遍历E/F,避免重复搜索。
(2)短路径优先原则
比如下图 A 通过 B 和 D 引入了 1.0 版本的 E,同时 A 通过 C 引入了 2.0 版本的 E。针对这种多个版本构建依赖时,Maven 采用短路径优先原则,即 A 会依赖 2.0 版本的 E。如果想引入 1.0 版本的 E,需要直接在 A 的 pom 中声明 E 的版本。
(3)依赖循环
maven中禁止有循环依赖。比如A 依赖了 B,同时 B 又依赖了 A。这种循环依赖可能不会直接显现,但是会在一个很长的调用关系显现出来
(4)Maven多模块问题
B模块依赖A模块,A模块的依赖修改了,需要install一下,B模块才能感知到最新的依赖。
3、工具
(1)日志
开发环境,建议默认日志级别设置为info,对需要关注的模块,建议设置为debug模式,比如当前工程目录,正在联调的依赖jar包。
如果异常无法定位,第一反应应该是添加日志。
(2)maven命令行
添加以下有空的命令行参数:
-
-e 出错日志
-
-X 打印debug日志
-
-q 只打印错误
(3)IDEA插件:Maven Helper
安装插件后左下角会出现个新标签页,点击后搜所有此模块依赖的jar包及冲突情况
4、新项目
新项目碰到错误,复杂的问题,先不要深入研究,先看下是否有以下情况:
-
新项目推荐Log4j2,代码里统一用slf4j打日志
-
新项目推荐spring boot,它们内部把依赖问题都修复了,可以避免很多问题
5、从下往上递归根因
java的错误栈是从报错的最底层逐步往上打,这样能很方便的定位,一般的问题通过此手段定位问题后即可很快修复。
复杂点的异常,比如jar包冲突异常,最底层的异常一般都是基础组件,比如类加载器、日志组件等,这种情况就要多看几层。
6、从上往下查看变动
大部分问题都是新引入依赖导致,从上往下查看变动,配合从下往上定位信息,定位出问题的组件。
四、案例分享
1、Tomcat项目启动报错
背景:新需求接入舆情SDK,测试环境测试通过,线上发布有大量机器tomcat启动失败。
(1)查找错误日志
异常日志:
(2)尝试修复:第一次
通过module-info.class,和 tag in constant pool: 19分析,直观感受是tomcat不支持java9中的模块策略
机器上使用的是tomcat6, 还不支持java9的新模块策略,遂升级tomcat6到tomcat8,启动错误还有
(3)尝试修复:第二次
此异常是lombok-1.18.10引起的,这个版本为了支持java9添加了module-info.class,而lombok包是java源代码增强的一个工具,运行时,并不需要,遂把依赖的scope改成provided。
重新发布,又报了以下异常:
(4)尝试修复:第三次
报错Not running on Jetty,而我们的服务是运行在tomcat上,配合从上到下排查的思路,在新引入的maven依赖项打开pom.xml,查看里面的依赖项还有parent里的依赖项,找到了以下元凶:parent里会依赖***-boot-starter-web,而这里默认使用jetty作为引擎,遂排除掉,并验证业务逻辑是否正常。
重新发布,问题解决。
(5)经验总结
- 排查jar包也有风险,很可能导致业务功能受影响,排除后要经过充分验证
2、新加依赖后服务启动失败
背景:新需求接入用户个性化api jar包,使用MDP的thrift注解注册了 thrift client bean,引入后项目启动失败。
(1)异常日志