Write in front
This demo explains how to troubleshoot a NullPointerException caused by @Transactional.
https://github.com/hengyunabc/spring-boot-inside/tree/master/demo-Transactional-NullPointerException
Code to locate NullPointerException
Demo is a simple spring transaction example, which provides the following StudentDao and uses @Transactional to declare the transaction:
@Component@Transactionalpublic 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); }}After the application is started, selectStudentById and finalSelectStudentById will be called in turn:
@PostConstruct public void init() { studentDao.selectStudentById(1); studentDao.finalSelectStudentById(1); }Use mvn spring-boot:run or import the project into the IDE to start. The exception information thrown is:
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)
Why is there no problem in executing selectStudentById in the application code, and executing finalSelectStudentById throws a NullPointerException?
In the same bean, SqlSession sqlSession has been injected, and in selectStudentById it is non-null. Why is null in finalSelectStudentById function?
Get the class name of the actual runtime
Of course, when we compare the two functions, we can know that it is because the modifier of finalSelectStudentById is final. But what is the specific reason?
We first set a breakpoint at the place where the exception is thrown, debug the code, and get the specific class at runtime:
System.err.println(studentDao.getClass());
The print result is:
class sample.mybatis.dao.StudentDao$$EnhancerBySpringCGLIB$$210b005d
It can be seen that it is a class processed by spring aop, but what is its specific bytecode content?
dumpclass analysis
We use the dumpclass tool to dump the class in jvm:
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
Find the java process pid:
$ jps5907 DemoNullPointerExceptionApplication
Dump all the relevant classes:
sudo java -jar dumpclass.jar 5907 'sample.mybatis.dao.StudentDao*' /tmp/dumpresult
Disassembly analysis
Use Javap or graphical tool jd-gui to decompose sample.mybatis.dao.StudentDao$$EnhancerBySpringCGLIB$$210b005d.
The result after decomposition is:
class StudentDao$$EnhancerBySpringCGLIB$$210b005d extends StudentDao
StudentDao$$EnhancerBySpringCGLIB$$210b005d does not have finalSelectStudentById related content
The actual call to selectStudentById is this.CGLIB$CALLBACK_0, that is, MethodInterceptor tmp4_1. Let's actually debug it later and see the specific type
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); } }Let’s take the actual debugging. Although the code of StudentDao$$EnhancerBySpringCGLIB$$210b005d cannot be directly seen, it can still be executed in a single step.
When debugging, you can see
1. StudentDao$$EnhancerBySpringCGLIB$$210b005d is null
2. The actual type of this.CGLIB$CALLBACK_0 is CglibAopProxy$DynamicAdvisedInterceptor, in which the original target object is actually saved
3. After CglibAopProxy$DynamicAdvisedInterceptor is processed by TransactionInterceptor, it will eventually call the original target object saved by itself with reflection.
Causes of throwing exception
So sort out the entire analysis:
1. After using @Transactional, spring aop will generate a cglib proxy class. StudentDao injected by @Autowired in the actual user code is also an instance of this proxy class.
2. The proxy class StudentDao$$EnhancerBySpringCGLIB$$210b005d generated by cglib is inherited from StudentDao
3.StudentDao$$EnhancerBySpringCGLIB$$210b005d is null
4. StudentDao$$EnhancerBySpringCGLIB$$210b005d is calling selectStudentById, and in fact, through CglibAopProxy$DynamicAdvisedInterceptor, it will eventually call the original target object saved by itself with reflection.
5. So there is no problem in calling the selectStudentById function
So why does the SqlSession in the finalSelectStudentById function null and then throw a NullPointerException?
1.StudentDao$$EnhancerBySpringCGLIB$$210b005d is null
2. The modifier of finalSelectStudentById function is final, cglib has no way to rewrite this function
3. When executed in finalSelectStudentById, the actual execution of the code in the original StudentDao
4. But the object is an instance of StudentDao$$EnhancerBySpringCGLIB$$210b005d. All fields inside it are null, so a NullPointerException will be thrown.
Solutions to the problem
1. The easiest thing is of course to remove the final modifier of the finalSelectStudentById function
2. There is another way. Do not use sqlSession directly in StudentDao, but use the getSqlSession() function. In this way, cglib will also process getSqlSession() and return the original target object
Summarize
1. Troubleshoot multiple debugging issues and see the object information in actual runtime
2. For the bytecode of the cglib generated class, you can use the dumpclass tool to dump and then decompose and analyze
Summarize
The above is the in-depth study of Spring Boot troubleshooting @Transactional. I hope it will be helpful to everyone. If you have any questions, please leave me a message and the editor will reply to everyone in time. Thank you very much for your support to Wulin.com website!