寫在前面
這個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);
}
}
應用啟動後,會依次調用
selectStudentById
和
finalSelectStudentById
:
@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/dumpclasswget 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
反編繹後的結果是:
-
class StudentDao$$EnhancerBySpringCGLIB$$210b005d extends StudentDao
-
裡沒有StudentDao$$EnhancerBySpringCGLIB$$210b005d
相關的内容finalSelectStudentById
-
實際調用的是selectStudentById
,即this.CGLIB$CALLBACK_0
,等下我們實際debug,看具體的類型MethodInterceptor tmp4_1
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); } }
再來實際debug,盡管
StudentDao$$EnhancerBySpringCGLIB$$210b005d
的代碼不能直接看到,但是還是可以單步執行的。
在debug時,可以看到
-
裡的所有field都是nullStudentDao$$EnhancerBySpringCGLIB$$210b005d
-
的實際類型是this.CGLIB$CALLBACK_0
,在這個Interceptor裡實際儲存了原始的target對象CglibAopProxy$DynamicAdvisedInterceptor
-
在經過CglibAopProxy$DynamicAdvisedInterceptor
處理之後,最終會用反射調用自己儲存的原始target對象TransactionInterceptor
抛出異常的原因
是以整理下整個分析:
- 在使用了
之後,spring aop會生成一個cglib代理類,實際使用者代碼裡@Transactional
注入的@Autowired
也是這個代理類的執行個體StudentDao
- cglib生成的代理類
繼承自StudentDao$$EnhancerBySpringCGLIB$$210b005d
StudentDao
-
StudentDao$$EnhancerBySpringCGLIB$$210b005d
-
在調用StudentDao$$EnhancerBySpringCGLIB$$210b005d
,實際上通過selectStudentById
,最終會用反射調用自己儲存的原始target對象CglibAopProxy$DynamicAdvisedInterceptor
- 是以
函數的調用沒有問題selectStudentById
那麼為什麼
finalSelectStudentById
函數裡的
SqlSession sqlSession
會是null,然後抛出
NullPointerException
?
-
StudentDao$$EnhancerBySpringCGLIB$$210b005d
-
函數的修飾符是finalSelectStudentById
,cglib沒有辦法重寫這個函數final
- 當執行到
裡,實際執行的是原始的finalSelectStudentById
裡的代碼StudentDao
- 但是對象是
的執行個體,它裡面的所有field都是null,是以會抛出StudentDao$$EnhancerBySpringCGLIB$$210b005d
NullPointerException
解決問題辦法
- 最簡單的當然是把
函數的finalSelectStudentById
修飾符去掉final
- 還有一種辦法,在
裡不要直接使用StudentDao
,而通過sqlSession
函數,這樣cglib也會處理getSqlSession()
,傳回原始的target對象getSqlSession()
總結
- 排查問題多debug,看實際運作時的對象資訊
- 對于cglib生成類的位元組碼,可以用dumpclass工具來dump,再反編繹分析