备注:对于一个优秀的工程师来说,定位问题和处理问题的良好的心态和思路是必备的技能。一个优秀的工程师在以下三点上持续精进:首先,面对问题能够追根问底,不气馁;同时,找到原因后能够举一反,并不断完善自己的知识体系;第三, 总结输出经验,给团队同伴赋能。

 

谁吞了我的异常_其他

起因

线上服务突然报警,异常信息关键词是:NullPointerException,具体错误信息如下:

  •  
2019-12-22 13:13:19.715 [] [http-nio-80-exec-6] ERROR c.d.d.c.c.s.CheckConfigServiceImpl - error
org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException:
### Error updating database.  Cause: java.lang.NullPointerException
### The error may involve com.daojia.dop.checkcenter.configcheck.dao.CheckResultDao.updateByPrimaryKeySelective-Inline
### The error occurred while setting parameters
### SQL: update check_result SET fix_operator = ?, fix_comment = ? where id = ?
// 这种SQL语法错的【where前多了个逗号】也会报NPE ### SQL: update check_result SET fix_operator = ?, fix_comment = ?  , where id = ?
### Cause: java.lang.NullPointerException
    at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:77)
    at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:446)
    at com.sun.proxy.$Proxy79.update(Unknown Source)
    at org.mybatis.spring.SqlSessionTemplate.update(SqlSessionTemplate.java:294)
    at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:62)
    at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:59)
    at com.sun.proxy.$Proxy84.updateByPrimaryKeySelective(Unknown Source)
    at com.daojia.dop.*****.CheckConfigServiceImpl.updateCheckResult(CheckConfigServiceImpl.java:240)
    at com.daojia.dop.*****.CheckConfigServiceImpl$$FastClassBySpringCGLIB$$fd3a9057.invoke()
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:746)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
    at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:88)
   ........此处省略n行
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:748)
Caused by: org.apache.ibatis.exceptions.PersistenceException:
### Error updating database.  Cause: java.lang.NullPointerException
### The error may involve com.daojia.*****.CheckResultDao.updateByPrimaryKeySelective-Inline
### The error occurred while setting parameters
### SQL: update check_result          SET fix_operator = ?,                                           fix_comment = ?          where id = ?
### Cause: java.lang.NullPointerException
    at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession.java:200)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:433)
    ... 87 common frames omitted
Caused by: java.lang.NullPointerException: null
    at com.sun.proxy.$Proxy97.update(Unknown Source)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:63)
    at com.sun.proxy.$Proxy97.update(Unknown Source)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession.java:198)
    ... 92 common frames omitted


2019-12-22 13:13:19.715 [] [http-nio-80-exec-6] ERROR c.d.d.c.c.s.CheckConfigServiceImpl - error
org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException:
### Error updating database.  Cause: java.lang.NullPointerException
### The error may involve com.daojia.dop.checkcenter.configcheck.dao.CheckResultDao.updateByPrimaryKeySelective-Inline
### The error occurred while setting parameters
### SQL: update check_result SET fix_operator = ?, fix_comment = ? where id = ?


### Cause: java.lang.NullPointerException
    at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:77)
    at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:446)
    at com.sun.proxy.$Proxy79.update(Unknown Source)
    at org.mybatis.spring.SqlSessionTemplate.update(SqlSessionTemplate.java:294)
    at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:62)
    at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:59)
    at com.sun.proxy.$Proxy84.updateByPrimaryKeySelective(Unknown Source)
    at com.daojia.dop.*****.CheckConfigServiceImpl.updateCheckResult(CheckConfigServiceImpl.java:240)
    at com.daojia.dop.*****.CheckConfigServiceImpl$$FastClassBySpringCGLIB$$fd3a9057.invoke()
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:746)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
    at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:88)
   ........此处省略n行
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:748)
Caused by: org.apache.ibatis.exceptions.PersistenceException:
### Error updating database.  Cause: java.lang.NullPointerException
### The error may involve com.daojia.*****.CheckResultDao.updateByPrimaryKeySelective-Inline
### The error occurred while setting parameters
### SQL: update check_result          SET fix_operator = ?,                                           fix_comment = ?          where id = ?
### Cause: java.lang.NullPointerException
    at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession.java:200)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:433)
    ... 87 common frames omitted
Caused by: java.lang.NullPointerException: null
    at com.sun.proxy.$Proxy97.update(Unknown Source)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:63)
    at com.sun.proxy.$Proxy97.update(Unknown Source)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession.java:198)
    ... 92 common frames omitted


这种 SQL 语法错的【where 前多了个逗号】也会报 NPE

  •  
update check_result SET fix_operator = ?, fix_comment = ?  , where id = ?

业务场景

按 id 更新一条记录的处理人信息,功能比较简单。备注信息环境:springboot2.0.6 + mybatis3.4.4 + pagehelper 插件+cat 监控插件(大众点评开源的), 数据库是 mysql5.7

  •  
<dependency>
 <groupId>org.mybatis.spring.boot</groupId>
 <artifactId>mybatis-spring-boot-starter</artifactId>
 <version>1.3.0</version>
</dependency>
<dependency>
 <groupId>mysql</groupId>
 <artifactId>mysql-connector-java</artifactId>
 <version>5.1.40</version>
</dependency>
<dependency>
 <groupId>com.daojia.openfire</groupId>
 <artifactId>openfire-cat-mybatis</artifactId>
 <version>2.0.0-SNAPSHOT</version>
</dependency>
<dependency>
 <groupId>com.github.pagehelper</groupId>
 <artifactId>pagehelper</artifactId>
 <version>5.1.9</version>
</dependency>

排查过程

1、排查参数有无 null

看到空指针,直觉就是先检查一下参数有无 null 经排查,三个参数都有。下面是线下复现的 SQL:

  •  
update check_result SET fix_operator = 'xiaoyisdfsdfsdfsdffffgfhghhhgggfdgdgdfgdfgdfgfdgfdg', fix_comment = '备注' where id = 37714232434

通过本地执行 SQL 语句,直接使用命令执行和通过 DbVisualizer 客户端执行,都是报 data to long。fix_operator 表中设置的长度是 32,实际参数长度已经超出了 32。但是使用 Navicat 客户端执行的时候成功了,去表里查看修改的记录,发现是只取了前 32 个字节入库了,后面的都丢弃了。所以使用此客户端的要多加注意。个人感觉这种处理方式太暴力了,这就相当于埋了个定时炸弹。按说到此就可以宣布错误原因找到了,但细想还有很多疑问没解开。到底是执行之前对参数长度做了校验直接返回失败?还是请求发给了数据库,执行后返回的错误?为什么参数过长,到日志里就变成了空指针了呢?接下来就继续排查,寻找答案。

2、继续追查

是在 SQL 执行前校验报错?还是执行后返回的错?还需要继续追查下去,以便找到根本原因。

通过 debug,一直跟到给数据库发送 SQL 命令前都没有报错,也没有发现对参数长度校验的逻辑,这个疑问就可以排除掉了,看来刚才是想多了。

谁吞了我的异常_其他_02

直到接收到数据库返回的信息解析后才发现错误,这才是真实的错误:

谁吞了我的异常_其他_03

3、步步逼近真相

步步跟进,“Data to long”的异常输出到哪里了?继续追查:

谁吞了我的异常_其他_04

原来异常是在这里被吞了,肯定输出不到日志了:

谁吞了我的异常_其他_05

下面是此部分的代码 com.dianping.cat.message.internal.DefaultMessageProducer#logError(java.lang.String, java.lang.Throwable),具体如下:

  •  
public class De
faultMessageProducer implements MessageProducer {
    /**
    * 其他代码省略
    */
    public void logError(String message, Throwable cause) {
        if (Cat.getManager().isCatEnabled()) {
            if (this.shouldLog(cause)) {
                this.m_manager.getThreadLocalMessageTree().setDiscard(false);
                StringWriter writer = new StringWriter(2048);
                if (message != null) {
                    writer.write(message);
                    writer.write(32);
                }
 
                cause.printStackTrace(new PrintWriter(writer));
                String detailMessage = writer.toString();
                if (cause instanceof Error) {
                    this.logEvent("Error", cause.getClass().getName(), "ERROR", detailMessage);
                } else if (cause instanceof RuntimeException) {
                    this.logEvent("RuntimeException", cause.getClass().getName(), "ERROR", detailMessage);
                } else {
                    this.logEvent("Exception", cause.getClass().getName(), "ERROR", detailMessage);
                }
            }
        } else {
            // 由于种种原因,上面的条件未能满足,异常在这里被吞了。
            // 这种方式处理异常,不仅不能输出到log文件中,还存在风险
            cause.printStackTrace();
        }
     }

这种处理方式太简单粗暴了,一点也不优雅。网上有一种说法(是否正确,知识有限,还无法验证),e.printStackTrace()会产生重复的字符串,进入到字符串常量池,会撑爆的,满了之后其他的就会等待,造成死锁。

4、空指针又是怎么冒出来的

继续往下执行,发现返回了个 null,这个 null 会传给 invoke 的地方,到 InvocationTargetException 就变成了 java.lang.NullPointerException,具体如下:

谁吞了我的异常_其他_06

具体代码如下:

  •  
public Object intercept(Invocation
 invocation) throws Throwable {
    MappedStatement mappedStatement = (MappedStatement)invocation.getArgs()[0];
    String[] strArr = mappedStatement.getId().split("\\.");
    String methodName = strArr[strArr.length - 2] + "." + strArr[strArr.length - 1];
    Transaction t = Cat.newTransaction("SQL", methodName);
    Object parameter = null;
    if (invocation.getArgs().length > 1) {
        parameter = invocation.getArgs()[1];
    }
 
    BoundSql boundSql = mappedStatement.getBoundSql(parameter);
    Configuration configuration = mappedStatement.getConfiguration();
    String sql = this.showSql(configuration, boundSql);
    SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
    Cat.logEvent("SQL.Method", sqlCommandType.name().toLowerCase(), "0", sql);
    String s = this.getSQLDatabase();
    Cat.logEvent("SQL.Database", s);
    Object returnObj = null;
 
    try {
        returnObj = invocation.proceed();
        this.checkResult(returnObj);
        t.setStatus("0");
    } catch (Exception var17) {
        Cat.logError(var17);
    } finally {
        t.complete();
    }
    // 这里会返回null
    return returnObj;
}
谁吞了我的异常_其他_07

而这个异常会被 org.apache.ibatis.plugin.Plugin 拿到,通过 ExceptionUtil 处理,就能输出到日志里了。

谁吞了我的异常_其他_08

这就导致了在日志中只看到了 java.lang.NullPointerException。至此,我们的真凶才彻底找到了。

总结一下

问题终于找到了,剩下事情就是解决了。

但关于本次问题排查过程,还是需要好好总结一下的:

  • 1、排查问题的决心和思路很重要,这种场景可能很少遇到,需要正确的态度和思路
  • 2、不要以为开源的插件就没有问题,要做到知其然,不能单纯的停留在应用层面
  • 3、看到 java.lang.NullPointerException,检查参数是否为空是正常的解决思路,如果确认参数没有 null(自己不好确认就找同事确认一下,有时候自己看一会儿就看花眼了,尤其参数比较多的 SQL 语句),就要调整排查思路。
  • 4、MySQL 的客户端要充分了解,避免踩坑
  • 5、通过排查有个小收获,原来 MySQL 客户端和服务端的通信和 redis 差不多,也是建立一个 socket 连接,把要执行的命令、数据按协议打包成数据发送给客户端,还是阻塞等待返回结果的(这也印证了为什么有慢查询的时候连接池很快就会被耗尽)。
  • 6、com.mysql.jdbc.MysqlIO 这个类值得好好研究一下。
谁吞了我的异常_其他_09