环境:SpringBoot2.7.18
该问题是在类中定义了一个实例变量并且赋了初始值,当通过AOP代理后出现了NPE(空指针异常),代码如下:
@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呢?
在上面已经提到,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,该库创建的示例会跳过构造函数,而实例变量的最终初始化是在构造函数中。
上面分析了为什么为null的原因,那么该如何解决呢?我们可以通过3种办法解决
public class PersonService { private final String name = "Pack" ; }
输出结果:
class: class com.pack.aop.PersonService$$EnhancerBySpringCGLIB$$87211922, name: Pack
正确输出,因为final修饰的实例变量在编译为字节码class时就已经确定了值。
图片
将save方法的final去掉后,那么生成的代理类就可以重写save方法了,最终调用save方法时先执行增强部分,然后再调用真正的那个目标类对象(真正的目标类是并没有通过objenesis创建,所以name是有值的)。
启动程序是添加如下系统属性
-Dspring.objenesis.ignore=true
Spring容器在创建对象前会判断,该系统属性是否为true。