引申:从一次parseInt导致的异常排查经历

2019-07-20 by Victor Lv Filed under 技术博客

引申:从一次parseInt导致的异常排查经历

系统所用框架描述: JSF + EJB + JBOSS 中间件(银行老系统用的老技术……)

一、现象描述:

页面报系统异常,观察日志只有一句相关报错:

2019-07-20 15:54:44,021 INFO  [stdout] (default task-14) 2019-07-20 15:54:44,021 [default task-14] DEBUG xxxxxxxxxxx.SessionValidateFilter : 非第一次登录检查用户信息是否继续有效
2019-07-20 15:54:44,026 INFO  [stdout] (default task-14) 2019-07-20 15:54:44,025 [default task-14] ERROR xxxxxxxxxxx.exception.ExceptionMBean - javax.servlet.ServletException: For input string: ""

但是同样的代码包在另一台服务器的应用上跑一切正常。

二、分析

刚开始,根据日志的上下文,Debug 调试跟踪 SessionValidateFilter 进去,发现是 chain.dofilter() 里面抛出的异常,具体的异常是:Integer.parseInt NumberFormatException, For input string: ""(也就是拿一个空字符串进行数字转换时导致的 NumberFormatException )。一度以为是Session拦截校验器(自己系统编写的)出的啥问题,但是 chain.dofilter() 几乎无法调试,我们知道 filter 作为 JavaWeb 三大组件(另外两个是 listener 和 servlet ),它的作用是在执行 servlet 之前先执行一道拦截过滤,然后再把 httpRequest 往下转发给 servlet,所以 chain.dofilter() 干的事不仅是往下传递给下一个拦截器,还会把 httpRequest转发给 servlet,这种 JavaWeb 框架里面抛出的异常怎么调试?一度还在各个 filter 里面打断点观察,结果自然是一无所获。浏览器 F12 调试也只是看到个 httpCode-302 的信息,不足以辅助分析。

后来转换了下思维方式,我直接从抛出来的这个异常出发,进行排查。于是找到了 Integer.parseInt(),在这里打断点拦截观察,一堆字符串传进来进行转换,发现中间确实会有一个空字符串""被传进来,parseInt() 里面抛出了 NumberFormatException 这个异常。但这个空串是谁传进来的呢?是不是框架或者过滤器传进来的(因此还一度怀疑是 session 的问题,或者是浏览器兼容性问题)?根这个 parserInt 这么多地方(大部分都是引的 jar 里面引用的)都调用了,当然无法call hierarchy 来逐一排查。

在同事的启发下,我全局搜索了我们系统自己写的代码中使用 Integer.parseInt()的地方,于是发现了该异常的真正出处,这是某外包同事对一个从配置文件读取出来的字符串进行Integer.parseInt()转换。 What?从配置文件读取的内容啥判断也没有直接传进去各种转换?编码习惯坑爹啊。

三、排查为何如此苦逼

幸亏这也是在开发环境发现,还能进行 debug 排查,如果是在生产环境,日志里面就那句报错,岂不是要命?至于为什么日志里面没有打印更详细的错误信息?这是因为我们系统使用的 JSF + EJB 的组合中,对于各种异常的捕获及日志打印,绝大多数都是在 EJB 层,以及 JSF + EJB 接口交互层,但是 JSF 里面的 ManageBean 层作为 Java 程序的最外层,如果这里面出了异常,会导致页面直接报系统错误未知错误,同时因为缺少 try-catch块(因为程序员认为自己的代码就几行不肯定不会导致异常),日志也几乎不会打印这种 MBean 层出现的错误。

四、痛定思痛

总结经验教训:

1、加强代码习惯培养:

尤其是要改掉导致 Bug 的坏习惯,在本人两年的工作经验,在我所在的团队里,最常见的就有三种坏习惯:

(1)永远不要相信从外界(非程序定义常量)读取到的东西:

一种就是上述所提的,没有对外界输入的参数持有充分的怀疑心,拿到外界(比如配置文件,或者页面)输入参数后,没有任何判断,就往下进行各种转换,要是环境一切正常还好,但实际上,测试环境和生产环境各类因素极其复杂,很容易导致外界输入参数不符合预期,进而导致后续转换错误。

(2)强制类型转换:

一个父类(假设是A)衍生有两个子类(假设是 A1、A2),A1 、A2各自的属性有所差异,方法(假设是handle())传入的参数是父类 A,但方法中要对 A2 的特定属性(假设是 A2.username)做校验或其他处理,于是将 A 对象强制转换成了 A2,以便通过(A2)A.getUserName这样的代码取出 A2.username 属性进行处理。问题在于,如果 handle() 传进来的子类是 A2,程序并不会有什么问题,因为这样的 A 可以强制转换成 A2,但是万一传进来的是一个 A1 对象,那强制转换就会出现异常。

(3)日志捕获后不做任何处理:

这在身边同事中就出现过很多这样的代码,因为我们习惯了使用 IDE 软件的自动补全,编写try - catch代码块时,会使用 IDE 的自动生成 try-catch 代码块,但自动生成的 catch 函数体中,一般是没有任何代码,或者只有简单的 e.printStack ,比较好的 IDE 比如 idea intelli会有//TODO提醒,但开发人员往往着重于 try{}里面的业务实现,而忽略了 catch{}代码块里面的完善,甚至连日志也不打。这就会导致两个及其严重的问题,万一 try{}代码块中抛出了异常,虽然catch到了但任何错误日志也没有打印,导致排查问题困难;更重要的问题是,如果catch{}里面既不往外层throw Exception,也不return;,那么代码还会继续往下执行!也就是说虽然 try{}代码块中业务逻辑已经出错抛出了异常,但程序还是会认为是正常的而继续往下执行,这就导致了业务执行错误的严重问题。

(4)题外话:

关于“catch{}方法体里面如果不做任何事代码还会不会继续往下执行”这个 Java 语法基本功,我竟然用这个问题考倒了一大批工作2~5年工作经验的外包,只有一位开发能力和经验都比较强的人答对了,其他一概面试者或外包同事,都回答错误,所以后来我在面试外包公司的员工时,会以这道题目作为考察面试者 Java 基本功以及项目问题解决实战的必备考题。


本文作者为 Victor Lv ,原出处为Victor Lv's Blog(http://langlv.me),转载请保留此句。


标签 编程
Fork me on GitHub