环境:SpringBoot2.7.18
1. 问题复现
该问题是在类中定义了一个实例变量并且赋了初始值,当通过AOP代理后出现了NPE(空指针异常),代码如下:
定义一个Service对象
@Service
public class PersonService {
private String name = "Pack" ;
public final void save() {
System.err.printf("class: %s, name: %s%n", this.getClass(), this.name) ;
}
}
该类中定义的save方法使用final修饰,方法体打印了当前的class对象及name。
定义切面
在该切面中切入点明确指定处理PersonService类中的任意方法,如下代码:
@Component
@Aspect
public class PersonAspect {
@Pointcut("execution(* com.pack.aop.PersonService.*(..))")
private void log() {}
@Around("log()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("before...") ;
Object ret = pjp.proceed() ;
System.out.println("after...") ;
return ret ;
}
}
该切面非常简单目标方法前后打印日志。以上代码就准备完成;在运行代码前,我们先回顾下Spring的代理机制。
Spring AOP通过JDK动态代理或CGLIB来为给定的目标对象创建代理。JDK动态代理是JDK内置的功能,而CGLIB是一个常见的开源类定义库。
当需要代理的目标对象实现了至少一个接口时,Spring AOP会使用JDK动态代理。此时,目标类型实现的所有接口都会被代理。如果目标对象没有实现任何接口,则会创建一个CGLIB代理。
如果你想强制使用CGLIB代理(例如,为了代理目标对象定义的所有方法,而不仅仅是那些由接口实现的方法)。
而在上面的代码中PersonService并没有实现如何接口,所以会通过CGLIB创建代码(SpringBoot中默认也使用的CGLIB)。
但是,通过CGLIB代理要注意下面这个问题:在使用CGLIB时,final方法不能被建议(即不能被AOP增强),因为它们在运行时生成的子类中无法被覆盖。
所以,在上面的PersonService中的save方法是不能被AOP增强的。了解了这么多以后我们来编写一个测试程序来调用save方法看看执行的结果。
@Service
public class AppRunService {
private final PersonService personService ;
public AppRunService(PersonService personService) {
this.personService = personService ;
}
@PostConstruct
public void init() {
this.personService.save() ;
}
}
在该类中初始化阶段会调用PersonService#save方法,输出结果如下:
class: class com.pack.aop.PersonService$$EnhancerBySpringCGLIB$$557ca555, name: null
根据输出结果得到,PersonService类被代理了,但是name为null,定义name属性是明明是赋初始值Pack,为什么会出现null呢?
2. 原因分析
在上面已经提到,Spring Boot中默认会使用CGLIB创建代理对象。而CGLIB代理对象的创建会通过ObjenesisCglibAopProxy创建,如下源码:
public abstract class AbstractAutoProxyCreator {
protected Object wrapIfNecessary(...) {
// ...
Object proxy = createProxy(...) ;
return proxy ;
}
protected Object createProxy() {
ProxyFactory proxyFactory = new ProxyFactory();
// ...
return proxyFactory.getProxy(classLoader) ;
}
}
// 代理工厂
public class ProxyFactory {
public Object getProxy(@Nullable ClassLoader classLoader) {
return createAopProxy().getProxy(classLoader) ;
}
}
上面的createAopProxy方法会返回一个ObjenesisCglibAopProxy对象,由该对象创建代理。我们这里跳过中间流程,直接进入到创建对象的代码
class ObjenesisCglibAopProxy extends CglibAopProxy {
private static final SpringObjenesis objenesis = new SpringObjenesis();
protected Object createProxyClassAndInstance(Enhancer enhancer, Callback[] callbacks) {
Class<?> proxyClass = enhancer.createClass() ;
Object proxyInstance = null ;
proxyInstance = objenesis.newInstance(proxyClass, enhancer.getUseCache()) ;
((Factory) proxyInstance).setCallbacks(callbacks) ;
return proxyInstance ;
}
}
以上代码是Spring 通过CGLIB创建代码的过程;看到这里大家可以先去搜索下 objenesis,这是一个开源的库,该库提供了一种机制,可以直接创建对象而跳过构造函数。Spring重新打包了objenesis。下面通过代码演示objenesis库
public class Person {
private String name = "Pack" ;
public String toString() {
return "Person [name=" + name + "]";
}
}
public static void main(String[] args) {
Objenesis obj = new ObjenesisStd() ;
Person person = obj.newInstance(Person.class) ;
System.out.println(person) ;
}
上通过ObjenesisStd创建对象,运行结果:
Person [name=null]
name同样为null。可能到这里你还是不能理解为什么为null。这里我们需要对类的生命周期有了解才行,对于实例变量的初始化,是在构造函数当中,我们通过javap命令查看生成的字节码
图片
通过反编译知道了,实例变量的初始化是在构造函数中。
到此,总结下为null的原因:
- Spring通过cglib创建代理,但是对于final修饰的方法代理类是无法重新的;既然无法重写,那么当你调用的时候必然是调用父类中的方法。
- 代理类的创建是通过objenesis,该库创建的示例会跳过构造函数,而实例变量的最终初始化是在构造函数中。
3. 解决办法
上面分析了为什么为null的原因,那么该如何解决呢?我们可以通过3种办法解决
3.1 成员变量添加final修饰符
public class PersonService {
private final String name = "Pack" ;
}
输出结果:
class: class com.pack.aop.PersonService$$EnhancerBySpringCGLIB$$87211922, name: Pack
正确输出,因为final修饰的实例变量在编译为字节码class时就已经确定了值。
图片
3.2 将save方法的final去掉
将save方法的final去掉后,那么生成的代理类就可以重写save方法了,最终调用save方法时先执行增强部分,然后再调用真正的那个目标类对象(真正的目标类是并没有通过objenesis创建,所以name是有值的)。
3.3 设置系统属性
启动程序是添加如下系统属性
-Dspring.objenesis.ignore=true
Spring容器在创建对象前会判断,该系统属性是否为true。