成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

避坑!為了性能,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

2025-01-16 16:16:53

2019-05-20 09:09:44

Web前端JavaScript

2020-06-09 08:05:11

Android 代碼操作系統

2017-12-27 14:51:12

Kotlin谷歌Java

2015-05-11 10:39:19

2024-09-24 13:31:33

2020-03-27 10:20:05

安全眾測滲透測試網絡安全

2022-03-15 17:35:20

電商系統架構

2020-05-22 10:35:07

CPU線程操作系統

2020-09-02 07:44:13

后端Long前端

2021-02-03 07:56:08

版本游戲邏輯

2018-07-03 10:49:22

性能故障排查

2019-10-18 12:57:38

邊緣計算云計算安全

2018-01-20 20:46:33

2012-05-30 09:40:55

Linux鍋爐

2016-03-09 11:19:01

2023-04-28 12:01:56

Spring項目編譯

2022-05-09 11:01:18

配置文件數據庫
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 久久久久亚洲精品 | www.99精品 | 国产ts人妖系列高潮 | 亚洲天堂成人在线视频 | 黄免费观看| 男人天堂av网| 久久久免费少妇高潮毛片 | 精品网| 91精品久久久久久久久99蜜臂 | 精品视频在线观看 | 欧美一级三级在线观看 | 四虎永久 | 成人国产精品久久久 | 97精品国产97久久久久久免费 | 亚洲自拍偷拍免费视频 | 综合色播 | 欧美精品一区二区在线观看 | 青青久久av北条麻妃海外网 | 91啪影院 | 无码日韩精品一区二区免费 | 天堂色区 | 亚洲视频在线观看免费 | 在线视频 亚洲 | aaaa日韩 | 毛片免费观看 | 国产视频久久久久 | 91精品国产一区二区三区 | 人人澡视频 | 亚洲乱码一区二区三区在线观看 | 91性高湖久久久久久久久_久久99 | 成人精品在线视频 | 亚洲福利精品 | 日韩欧美一区二区三区在线播放 | 成人免费视频一区 | 成年人在线视频 | 天天曰夜夜操 | 国产区一区 | 免费看欧美一级片 | 精国产品一区二区三区四季综 | 色性av | 精品久久久久久久 |