深入Spring Boot:排查@Transactional引起的NullPointerException

标签: spring boot transactional | 发表时间:2018-04-27 14:29 | 作者:weiboxie
出处:http://www.iteye.com

写在前面

这个demo来说明怎么排查一个 @Transactional引起的 NullPointerException

https://github.com/hengyunabc/spring-boot-inside/tree/master/demo-Transactional-NullPointerException

定位 NullPointerException 的代码

Demo是一个简单的spring事务例子,提供了下面一个 StudentDao,并用 @Transactional来声明事务:

 

@Component
@Transactional
public class StudentDao {

    @Autowired
    private SqlSession sqlSession;

    public Student selectStudentById(long id) {
        return sqlSession.selectOne("selectStudentById", id);
    }

    public final Student finalSelectStudentById(long id) {
        return sqlSession.selectOne("selectStudentById", id);
    }
}
 

 

 

应用启动后,会依次调用 selectStudentByIdfinalSelectStudentById

 

 @PostConstruct
    public void init() {
        studentDao.selectStudentById(1);
        studentDao.finalSelectStudentById(1);
    }
 

 

      

mvn spring-boot:run 或者把工程导入IDE里启动,抛出来的异常信息是:

Caused by: java.lang.NullPointerException
    at sample.mybatis.dao.StudentDao.finalSelectStudentById(StudentDao.java:27)
    at com.example.demo.transactional.nullpointerexception.DemoNullPointerExceptionApplication.init(DemoNullPointerExceptionApplication.java:30)
    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.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleElement.invoke(InitDestroyAnnotationBeanPostProcessor.java:366)
    at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleMetadata.invokeInitMethods(InitDestroyAnnotationBeanPostProcessor.java:311)

 

为什么应用代码里执行 selectStudentById没有问题,而执行 finalSelectStudentById就抛出 NullPointerException?

同一个bean里,明明 SqlSession sqlSession已经被注入了,在 selectStudentById里它是非null的。为什么 finalSelectStudentById函数里是null?

获取实际运行时的类名

当然,我们对比两个函数,可以知道是因为 finalSelectStudentById的修饰符是 final。但是具体原因是什么呢?

我们先在抛出异常的地方打上断点,调试代码,获取到具体运行时的 class是什么:

System.err.println(studentDao.getClass());

 

打印的结果是:

class sample.mybatis.dao.StudentDao$$EnhancerBySpringCGLIB$$210b005d

 

可以看出是一个被spring aop处理过的类,但是它的具体字节码内容是什么呢?

dumpclass分析

我们使用dumpclass工具来把jvm里的类dump出来:

https://github.com/hengyunabc/dumpclass

写道
wget http://search.maven.org/remotecontent?filepath=io/github/hengyunabc/dumpclass/0.0.1/dumpclass-0.0.1.jar -O dumpclass.jar

 

找到java进程pid:

$ jps 5907 DemoNullPointerExceptionApplication

把相关的类都dump下来:

sudo java -jar dumpclass.jar 5907 'sample.mybatis.dao.StudentDao*' /tmp/dumpresult

反汇编分析

用javap或者图形化工具jd-gui来反编绎 sample.mybatis.dao.StudentDao$$EnhancerBySpringCGLIB$$210b005d

反编绎后的结果是:

  1. class StudentDao$$EnhancerBySpringCGLIB$$210b005d extends StudentDao
  2. StudentDao$$EnhancerBySpringCGLIB$$210b005d里没有 finalSelectStudentById相关的内容

  3. selectStudentById实际调用的是 this.CGLIB$CALLBACK_0,即 MethodInterceptor tmp4_1,等下我们实际debug,看具体的类型

    public final Student selectStudentById(long paramLong)
      {
        try
        {
          MethodInterceptor tmp4_1 = this.CGLIB$CALLBACK_0;
          if (tmp4_1 == null)
          {
            tmp4_1;
            CGLIB$BIND_CALLBACKS(this);
          }
          MethodInterceptor tmp17_14 = this.CGLIB$CALLBACK_0;
          if (tmp17_14 != null)
          {
            Object[] tmp29_26 = new Object[1];
            Long tmp35_32 = new java/lang/Long;
            Long tmp36_35 = tmp35_32;
            tmp36_35;
            tmp36_35.<init>(paramLong);
            tmp29_26[0] = tmp35_32;
            return (Student)tmp17_14.intercept(this, CGLIB$selectStudentById$0$Method, tmp29_26, CGLIB$selectStudentById$0$Proxy);
          }
          return super.selectStudentById(paramLong);
        }
        catch (RuntimeException|Error localRuntimeException)
        {
          throw localRuntimeException;
        }
        catch (Throwable localThrowable)
        {
          throw new UndeclaredThrowableException(localThrowable);
        }
      }

     

  4. 再来实际debug,尽管 StudentDao$$EnhancerBySpringCGLIB$$210b005d的代码不能直接看到,但是还是可以单步执行的。

在debug时,可以看到

  1. StudentDao$$EnhancerBySpringCGLIB$$210b005d里的所有field都是null

     

  2. this.CGLIB$CALLBACK_0的实际类型是 CglibAopProxy$DynamicAdvisedInterceptor,在这个Interceptor里实际保存了原始的target对象

    cglib-target

  3. CglibAopProxy$DynamicAdvisedInterceptor在经过 TransactionInterceptor处理之后,最终会用反射调用自己保存的原始target对象

抛出异常的原因

所以整理下整个分析:

  1. 在使用了 @Transactional之后,spring aop会生成一个cglib代理类,实际用户代码里 @Autowired注入的 StudentDao也是这个代理类的实例
  2. cglib生成的代理类 StudentDao$$EnhancerBySpringCGLIB$$210b005d继承自 StudentDao
  3. StudentDao$$EnhancerBySpringCGLIB$$210b005d里的所有field都是null
  4. StudentDao$$EnhancerBySpringCGLIB$$210b005d在调用 selectStudentById,实际上通过 CglibAopProxy$DynamicAdvisedInterceptor,最终会用反射调用自己保存的原始target对象
  5. 所以 selectStudentById函数的调用没有问题

那么为什么 finalSelectStudentById函数里的 SqlSession sqlSession会是null,然后抛出 NullPointerException

  1. StudentDao$$EnhancerBySpringCGLIB$$210b005d里的所有field都是null
  2. finalSelectStudentById函数的修饰符是 final,cglib没有办法重写这个函数
  3. 当执行到 finalSelectStudentById里,实际执行的是原始的 StudentDao里的代码
  4. 但是对象是 StudentDao$$EnhancerBySpringCGLIB$$210b005d的实例,它里面的所有field都是null,所以会抛出 NullPointerException

解决问题办法

  1. 最简单的当然是把 finalSelectStudentById函数的 final修饰符去掉
  2. 还有一种办法,在 StudentDao里不要直接使用 sqlSession,而通过 getSqlSession()函数,这样cglib也会处理 getSqlSession(),返回原始的target对象

总结

  • 排查问题多debug,看实际运行时的对象信息
  • 对于cglib生成类的字节码,可以用dumpclass工具来dump,再反编绎分析
 

 



已有 0 人发表留言,猛击->> 这里<<-参与讨论


ITeye推荐



相关 [spring boot transactional] 推荐:

深入Spring Boot:排查@Transactional引起的NullPointerException

- - 开源软件 - ITeye博客
这个demo来说明怎么排查一个. @Transactional引起的. 定位 NullPointerException 的代码. Demo是一个简单的spring事务例子,提供了下面一个. @Transactional来声明事务:. selectStudentById和. mvn spring-boot:run 或者把工程导入IDE里启动,抛出来的异常信息是:.

Spring中@Transactional与读写分离

- - 编程语言 - ITeye博客
    本文主要介绍如何使用Spring @Transactional基于JDBC Replication协议便捷的实现数据库的读写分离.     1)Spring 4.x + 环境.     3)tomcat-jdbc-pool连接池.     4)spring @Transaction使用与JDBC Replcation协议.

Spring boot传统部署

- - 企业架构 - ITeye博客
使用spring boot很方便,一个jar包就可以启动了,因为它里面内嵌了tomcat等服务器. 但是spring boot也提供了部署到独立服务器的方法. 如果你看文档的话,从jar转换为war包很简单,pom.xml的配置修改略去不讲. 只看source的修改,很简单,只要一个配置类,继承自SpringBootServletInitializer, 并覆盖configure方法.

值得使用的Spring Boot

- - ImportNew
2013年12月12日,Spring发布了4.0版本. 这个本来只是作为Java平台上的控制反转容器的库,经过将近10年的发展已经成为了一个巨无霸产品. 不过其依靠良好的分层设计,每个功能模块都能保持较好的独立性,是Java平台不可多得的好用的开源应用程序框架. Spring的4.0版本可以说是一个重大的更新,其全面支持Java8,并且对Groovy语言也有良好的支持.

Spring Boot配置多个DataSource

- - 廖雪峰的官方网站
使用Spring Boot时,默认情况下,配置 DataSource非常容易. Spring Boot会自动为我们配置好一个 DataSource. 如果在 application.yml中指定了 spring.datasource的相关配置,Spring Boot就会使用该配置创建一个 DataSource.

Spring boot executable jar/war 原理

- - ImportNew
spring boot里其实不仅可以直接以 Java -jar demo.jar的方式启动,还可以把jar/war变为一个可以执行的脚本来启动,比如./demo.jar. 把这个executable jar/war 链接到/etc/init.d下面,还可以变为Linux下的一个service. 只要在spring boot maven plugin里配置:.

Spring Boot Starter是什么?

- - 技术,永无止境
在工作中我们经常能看到各种各样的springboot starter,如spring-cloud-netflix、spring-cloud-alibaba等等. 这些starter究竟有什么作用呢. 在了解这些starter之前,我们需要先大概知道Spring MVC与Spring Boot的关系.

SPRING BOOT OAUTH2 + KEYCLOAK - service to service call

- - BlogJava-首页技术区
employee-service调用department-service,如果要按OAUTH2.0流程,只需要提供client-id和client-secrect即可. 在KEYCLOAK中引入service-account,即配置该employee-service时,取消standard-flow,同时激活service-account.

spring boot与spring batch、postgres及elasticsearch整合

- - 互联网 - ITeye博客
当系统有大量数据需要从数据库导入elasticsearch时,使用sping batch可以提高导入的效率. 这篇文章使用spring batch将数据从postgres导入elasticsearch. 本文使用spring data jest连接ES(也可以使用spring data elasticsearch连接ES),ES版本为5.5.3.

Spring Boot使用redis做数据缓存

- - ITeye博客
SysUser.class)); //请注意这里. 3 redis服务器配置. /** *此处的dao操作使用的是spring data jpa,使用@Cacheable可以在任意方法上,*比如@Service或者@Controller的方法上 */ public interface SysUserRepo1 extends CustomRepository {.