谁再悄咪咪的吃掉异常,我上去就是一 JIO
又到周末了,周更选手申请出站~
这次分享一下上个月碰到的离奇的问题。一个简单的问题,硬是因为异常被悄咪咪吃掉,过关难度直线提升,导致小黑哥排查一个晚上。
这个美好的晚上,本想着开两把 LOL 无限火力,在召唤师峡谷来个五杀的~
哎,就这样没了啊!我知道,你们一定能理解这种五杀被抢的感觉~
下次,真的,谁再让我看到悄咪咪的吃掉异常,我真的要上去一 Jio 了!
好了,本文可不是水文,看完本篇文章,你可以学到以下知识点:
- Arthas 排查技巧
- 啥是 NoClassDefFoundError
- Dubbo 异常内部处理方式
好了,同学们,打开小本子,准备记好知识点~
先赞后看养成习惯,微信搜索「程序通事」,关注就完事了
起因
我们有个业务系统,应用之间调用链如下所示:
A 应用是业务发生起始应用,在这个应用中将会根据一定规则选择最后的通讯渠道 C,然后将这个渠道标识传递给 B 应用。
B 应用的功能类似网关,这个应用将会根据 A 应用传递过来的渠道标识,将会请求路由下发到具体的 C 应用,起到服务路由的功能。
C 应用是与外部应用交互的应用,我们将其称为渠道通讯机。
假设一次业务中,A 应用根据规则选择 C2 的渠道标识,然后传递给 B 应用。B 应用根据这个标识选择使用 C2 进行通讯,最后 C2 调用外部应用完成一次业务调用。
上述所有应用都基于 Dubbo 进行远程通讯,B 应用实现原理在小黑哥之前文章「支付路由系统演进史」中有写过,感兴趣的同学可以查看一下。
介绍完业务的基本情况,现在我们来看下到底发生了啥事。
一次业务需求中,需要改动 C2 应用,这次改动功能点真的很小,很快就完成了。小黑哥想着闲着也是闲着,于是就把之前 C2 应用中打印的日志中一些没有脱敏的信息,进行脱敏处理。
由于之前日志框架脱敏处理存在一些问题,于是就将日志框架从 Log4j 升级为 LogBack。升级之后,为了防止不同日志框架中之间的产生冲突,于是使用 IDEA Maven Helper 插件,统一将应用中所有的 Log4j 相关依赖都给排除了。
改动完成之后,将 C2 应用发布到测试环境,再次从 A 应用发起测试, B 应用返回异常提示未找到 C2 应用。
B 应用业务代码类似如下:
public Response pay(Request req) {
try {
if (!isSupport(req.getChnlCode())) {
return new Response("ERROR", "未找到相关渠道应用");
}
return doPay(req);
} catch (Exception e) {
return new Response("ERROR", "未找到相关渠道应用");
}
}
正常情况下,若是配置存在问题,B 应用将会返回未找到具体渠道,请求也会在 B 应用结束,不会调用到 C2 应用(也没办法调用)。
然而此次配置什么都没问题, 而且最诡异的是 C2 应用居然收到了请求,并且成功处理了业务请求。
排查问题
由于 B 应用异常处理时,将异常吃掉了,我们没办法得知这个过程到底发生了啥事,所以第一要紧的事获取异常信息。
最简单的办法就是,将 B 应用改造一下,加入打印异常日志。不过当时比较懒,不想改造应用,就想获取异常信息,于是想到使用 Arthas。
Arthas 排错技巧
Arthas
是Alibaba开源的Java诊断工具,这里就不再详细介绍这个工具,主要讲下这次排错用到的命令-watch。
watch 命令可以方便观察指定方的调用情况,可以具体观察方法的返回值
、抛出异常
、入参
,另外还可以通过 OGNL表达式查看对应的变量。
这里我们主要为了查看方法抛出的异常信息,执行命令如下:
watch com.dubbo.example.DemoService doPay -e -x 2 '{params,throwExp}'
上述命令将会在方法异常之后观察方法的入参以及异常信息。
注意,我们需要查看
doPay
方法,而不是pay
方法。这是因为pay
方法中我们将异常捕获,不太可能会抛出异常哦~
异常信息如下所示:
真正引起此次错误的异常信息为:
java.lang.NoClassDefFoundError: Could not initialize class xx.xxx.xx.GELogger
由于此次 B 应用不存在改动,所以推测这个异常实际发生在 C2 应用,于是在 C2 应用处再次使用 Arthas watch 命令,同样观察到相同的错误信息。
NoClassDefFoundError
NoClassDefFound,从名字上我们可以推测是因为类不存在,从而引发的这个错误。按照这个思路,我们首先可以简单查看一下 B 应用中是否存在 GELogger
相关类。
查看 B 应用相关依赖包,从中发现了这个类文件,这说明这个类确实存在。
在 IDEA 反编译查看 GELogger
类相关源码,从中发现了问题。
private static Logger logger;
static {
System.out.println("static init");
logger = Logger.getLogger(NoClassDefFoundErrorTestService.class);
System.out.println("Logger init success");
}
GELogger
存在一个静态代码块,用于初始化一个 org.apache.log4j.Logger
日志类。
然后在上面改动中,全部的 Log4j
依赖都被排除了,所以这里运行时应该会抛出另外一个找到 org.apache.log4j.Logger
错误。
执行以下代码,模拟抛错过程。
System.out.println("模拟第一次 Error");
try {
NoClassDefFoundErrorTestService noClassDefFoundErrorTestService=new NoClassDefFoundErrorTestService();
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println("模拟第二次 Error");
try {
NoClassDefFoundErrorTestService noClassDefFoundErrorTestService=new NoClassDefFoundErrorTestService();
} catch (Throwable e) {
e.printStackTrace();
}
异常信息如下所示:
第一次创建 NoClassDefFoundErrorTestService
实例时,Java 虚拟机读取加载时,将会初始化静态代码块时。由于 org.apache.log4j.Logger
类不存在,静态代码块执行异常,从而导致类加载失败。
第二次再创建 NoClassDefFoundErrorTestService
实例时,Java 虚拟机不会再次读取加载,所以直接返回了以下异常。
java.lang.NoClassDefFoundError: Could not initialize class com.dubbo.example.NoClassDefFoundErrorTestService
找到问题真正原因,解决办法也很简单,直接排除 GELogger
所在依赖包。
Dubbo 内部异常处理
虽然问题到此解决了,但是这里还有一个疑问,为何 C2 应用发生了异常,却没有相关错误日志,并且 C2 业务逻辑也正常处理完成。
这就要说到 Dubbo 内部异常错误处理方式,上面 GELogger
其实作用在一个 Dubbo 自定义 Filter 中,用来记录结果,模拟代码如下:
@Activate(
group = {"provider", "consumer"}
)
public class ErrorFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
Result result = invoker.invoke(invocation);
NoClassDefFoundErrorTestService noClassDefFoundErrorTestService=new NoClassDefFoundErrorTestService();
// 处理业务逻辑
return result;
}
}
这个自定义 Filter 中首先执行 invoker
方法,这个方法将会调用真正的业务方法,这就是为什么 C2 应用逻辑是正常处理完成。
业务方法处理完成之后,然后执行后续逻辑。由于 NoClassDefFoundErrorTestService
将会抛出 Error
,最终这个 Error
,将会在 HeaderExchangeHandler#handleRequest
被捕获,然后将会把相关异常信息返回给调用 Dubbo 消费者。
而在 Dubbo 消费者接受到服务提供者返回信息之后,将会在 DefaultFuture#doReceived
转化成 RemotingException
。
而 RemotingException
最终将会在 FailoverClusterInvoker#doInvoke
转换成 RpcException
返回给业务代码。
总结
好了,说了这么多,总结一下本文知识点
- 异常捕获之后,一定要记得打印日志,并且要记得输出堆栈信息。
- 运行时类不存在,将会导致
NoClassDefFoundError
,类加载过程失败,也会导致NoClassDefFoundError
。 - 对外提供的二方包,最好不要依赖特定日志框架,如 Log4j,Logback 等,应该使用 Slf4j 框架。
帮助
2、java.lang.NoClassDefFoundError 的解决方法一例
3、noclassdeffounderror-could-not-initialize-class-error
欢迎关注我的公众号:程序通事,获得日常干货推送。如果您对我的专题内容感兴趣,也可以关注我的博客:studyidea.cn
谁再悄咪咪的吃掉异常,我上去就是一 JIO的更多相关文章
- 悄咪咪提高团队幸福感 & Surprise!
前言 本文的灵感是在几个月以前工作不忙(摸鱼)时想到的,老是自己一个人往前冲冲冲也没啥意思,需要想一点办法,来提高团队的效率,提高团队的幸福感(效率起来了,单位时间内代码写的更多,那不就幸福啦 ),经 ...
- 编写高质量代码改善C#程序的157个建议[避免finaly内的无效代码、避免嵌套异常、避免吃掉异常、注意循环异常处理]
前言 本文已同步到http://www.cnblogs.com/aehyok/p/3624579.html.本文主要来学习以下几点建议 建议61.避免在finally内撰写无效代码 建议62.避免嵌套 ...
- ASP.NET Core中,UseDeveloperExceptionPage扩展方法会吃掉异常
在ASP.NET Core中Startup类的Configure方法中,有一个扩展方法叫UseDeveloperExceptionPage,如下所示: // This method gets call ...
- 编写高质量代码改善C#程序的157个建议——建议63:避免“吃掉”异常
建议63:避免“吃掉”异常 嵌套异常是很危险的行为,一不小心就就会将异常堆栈信息,也就是真正的Bug出处隐藏起来.这还不是最严重的,最严重的就是“吃掉”异常,即捕获,然后不向上层throw. 避免“吃 ...
- 代码review还是需要再仔细点-一次crash异常分析的总结
版本发布之后,外网新增了一些crash, 从mini dump看,的确是有异常的,619行crash了. 代码如下: 奔溃的地方如下 可以看到是Zip add的时候指针空了, 为什么呢? 结合代码,可 ...
- T-SQL编程中的异常处理-异常捕获(catch)与抛出异常(throw)
本文出处: http://www.cnblogs.com/wy123/p/6743515.html T-SQL编程与应用程序一样,都有异常处理机制,比如异常的捕获与异常的抛出,本文简单介绍异常捕获与异 ...
- T-SQL编程中的异常处理-异常捕获(try catch)与抛出异常(throw)
本文出处: http://www.cnblogs.com/wy123/p/6743515.html T-SQL编程与应用程序一样,都有异常处理机制,比如异常的捕获与异常的抛出(try catch th ...
- Java异常控制机制和异常处理原则【转】
原文:https://www.jianshu.com/p/15872cba211d Java异常控制机制又被称为“违例控制机制”. 捕获程序错误最理想的时机是在编译阶段,这样可以彻底避免错误的代码运行 ...
- C#不用union,而是有更好的方式实现 .net自定义错误页面实现 .net自定义错误页面实现升级篇 .net捕捉全局未处理异常的3种方式 一款很不错的FLASH时种插件 关于c#中委托使用小结 WEB网站常见受攻击方式及解决办法 判断URL是否存在 提升高并发量服务器性能解决思路
C#不用union,而是有更好的方式实现 用过C/C++的人都知道有个union,特别好用,似乎char数组到short,int,float等的转换无所不能,也确实是能,并且用起来十分方便.那C# ...
随机推荐
- 设计Person类 代码参考
#include <iostream> using namespace std; class Trapezium { private: int x1,y1,x2,y2,x3,y3,x4,y ...
- Cypress系列(6)- Cypress 的重试机制
如果想从头学起Cypress,可以看下面的系列文章哦 https://www.cnblogs.com/poloyy/category/1768839.html 前言 重试(Retry-ability) ...
- 「MoreThanJava」一文了解二进制和CPU工作原理
「MoreThanJava」 宣扬的是 「学习,不止 CODE」,本系列 Java 基础教程是自己在结合各方面的知识之后,对 Java 基础的一个总回顾,旨在 「帮助新朋友快速高质量的学习」. 当然 ...
- 最好用的FTP工具,最好用的FTP工具推荐!
IIS7服务器管理工具是一个良好的FTP的客户端,可以进行FTP的操作!同时,还可以作为VNC的客户端进行VNC的相关操作!它还能连接Windows和Linux的服务器和PC,并对他们的连接状态进行实 ...
- pycharm关联git
一.先创建SSH Key 给github设置SSH-KEY !!! 这一步算是连接GitHub的最基本的一步了,git是分布式的代码管理工具,远程的代码管理是基于ssh的,所以得先配好SSH key. ...
- ActiveMQ 笔记(二)部署和DEMO(队列、主题)
个人博客网:https://wushaopei.github.io/ (你想要这里多有) 一.部署操作 1. 部署在linux 上的acvtiveMQ 要可以通过前台windows 的页面访问, ...
- Java实现 LeetCode 775 全局倒置与局部倒置(分析题)
775. 全局倒置与局部倒置 数组 A 是 [0, 1, -, N - 1] 的一种排列,N 是数组 A 的长度.全局倒置指的是 i,j 满足 0 <= i < j < N 并且 A ...
- Java实现 LeetCode 688 “马”在棋盘上的概率(DFS+记忆化搜索)
688. "马"在棋盘上的概率 已知一个 NxN 的国际象棋棋盘,棋盘的行号和列号都是从 0 开始.即最左上角的格子记为 (0, 0),最右下角的记为 (N-1, N-1). 现有 ...
- Java实现 洛谷 P6183 [USACO10MAR]The Rock Game S(DFS)
P6183 [USACO10MAR]The Rock Game S 输入输出样例 输入 3 输出 OOO OXO OXX OOX XOX XXX XXO XOO OOO PS: 因为每一位只有两种可能 ...
- Java实现 LeetCode 331 验证二叉树的前序序列化
331. 验证二叉树的前序序列化 序列化二叉树的一种方法是使用前序遍历.当我们遇到一个非空节点时,我们可以记录下这个节点的值.如果它是一个空节点,我们可以使用一个标记值记录,例如 #. _9_ / \ ...