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

文摘   2024-12-09 08:01   新疆  

环境:SpringBoot2.7.18



1. 问题复现

该问题是在类中定义了一个实例变量并且赋了初始值,当通过AOP代理后出现了NPE(空指针异常),代码如下:

定义一个Service对象

@Servicepublic 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@Aspectpublic 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方法看看执行的结果。

@Servicepublic 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的原因:

  1. Spring通过cglib创建代理,但是对于final修饰的方法代理类是无法重新的;既然无法重写,那么当你调用的时候必然是调用父类中的方法。

  2. 代理类的创建是通过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。

以上是本篇文章的全部内容,如对你有帮助帮忙点赞+转发+收藏

推荐文章

SpringBoot这些异常你知道原因吗?你遇过到几个?

强大!基于Spring Boot HTTP请求响应日志记录组件,值得一用

优雅!基于Spring Boot字段加密后的模糊查询,支持MyBatis, JPA

Spring Boot 一个注解防止重复请求

手写Spring MVC核心组件,底层原理如此简单

强大!SpringBoot这个注解你知道干什么的吗?

神器!API接口限流就是这么简单

掌握Spring Boot最佳途径!就该如此做

高性能缓存神器Caffeine

我给Spring提交一个Bug,已在最新版修复!请这样修复!

SpringBoot @Value注解这些高级玩法用过吗?

总结7种JVM出现OOM时的原因及解决方案

强大!SpringBoot通过这3个注解监测Controller接口

SpringBoot这几个工具类太有用了

SpringBoot强大的数据格式化功能

基于SpringBoot通过3种方式轻松搞定敏感字段加密处理

两种方式实现SpringBoot外部配置实时刷新最佳实践

实战案例SpringBoot整合Seata AT模式实现分布式事务【超详细】

请一定牢记SpringBoot项目开发中的8个扩展接口

Spring全家桶实战案例源码
spring, springboot, springcloud 案例开发详解
 最新文章