避坑!为了性能,Spring挖了一个大坑

开发 前端
将save方法的final去掉后,那么生成的代理类就可以重写save方法了,最终调用save方法时先执行增强部分,然后再调用真正的那个目标类对象(真正的目标类是并没有通过objenesis创建,所以name是有值的)。

环境: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。

责任编辑:武晓燕 来源: Spring全家桶实战案例源码
相关推荐

2021-05-07 07:59:52

WebFluxSpring5系统

2024-08-30 11:40:19

2019-05-20 09:09:44

Web前端JavaScript

2020-06-09 08:05:11

Android 代码操作系统

2017-12-27 14:51:12

Kotlin谷歌Java

2020-03-27 10:20:05

安全众测渗透测试网络安全

2015-05-11 10:39:19

2024-09-24 13:31:33

2022-03-15 17:35:20

电商系统架构

2021-02-03 07:56:08

版本游戏逻辑

2020-05-22 10:35:07

CPU线程操作系统

2020-09-02 07:44:13

后端Long前端

2018-07-03 10:49:22

性能故障排查

2019-10-18 12:57:38

边缘计算云计算安全

2018-01-20 20:46:33

2023-04-28 12:01:56

Spring项目编译

2022-05-09 11:01:18

配置文件数据库

2016-03-09 11:19:01

2012-05-30 09:40:55

Linux锅炉

2020-06-12 11:03:22

Python开发工具
点赞
收藏

51CTO技术栈公众号