自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

避坑!為了性能,Spring挖了一個(gè)大坑

開(kāi)發(fā) 前端
將save方法的final去掉后,那么生成的代理類就可以重寫save方法了,最終調(diào)用save方法時(shí)先執(zhí)行增強(qiáng)部分,然后再調(diào)用真正的那個(gè)目標(biāo)類對(duì)象(真正的目標(biāo)類是并沒(méi)有通過(guò)objenesis創(chuàng)建,所以name是有值的)。

環(huán)境:SpringBoot2.7.18

1. 問(wèn)題復(fù)現(xiàn)

該問(wèn)題是在類中定義了一個(gè)實(shí)例變量并且賦了初始值,當(dāng)通過(guò)AOP代理后出現(xiàn)了NPE(空指針異常),代碼如下:

定義一個(gè)Service對(duì)象

@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修飾,方法體打印了當(dāng)前的class對(duì)象及name。

定義切面

在該切面中切入點(diǎn)明確指定處理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 ;
  }
}

該切面非常簡(jiǎn)單目標(biāo)方法前后打印日志。以上代碼就準(zhǔn)備完成;在運(yùn)行代碼前,我們先回顧下Spring的代理機(jī)制。

Spring AOP通過(guò)JDK動(dòng)態(tài)代理或CGLIB來(lái)為給定的目標(biāo)對(duì)象創(chuàng)建代理。JDK動(dòng)態(tài)代理是JDK內(nèi)置的功能,而CGLIB是一個(gè)常見(jiàn)的開(kāi)源類定義庫(kù)。

當(dāng)需要代理的目標(biāo)對(duì)象實(shí)現(xiàn)了至少一個(gè)接口時(shí),Spring AOP會(huì)使用JDK動(dòng)態(tài)代理。此時(shí),目標(biāo)類型實(shí)現(xiàn)的所有接口都會(huì)被代理。如果目標(biāo)對(duì)象沒(méi)有實(shí)現(xiàn)任何接口,則會(huì)創(chuàng)建一個(gè)CGLIB代理。

如果你想強(qiáng)制使用CGLIB代理(例如,為了代理目標(biāo)對(duì)象定義的所有方法,而不僅僅是那些由接口實(shí)現(xiàn)的方法)。

而在上面的代碼中PersonService并沒(méi)有實(shí)現(xiàn)如何接口,所以會(huì)通過(guò)CGLIB創(chuàng)建代碼(SpringBoot中默認(rèn)也使用的CGLIB)。

但是,通過(guò)CGLIB代理要注意下面這個(gè)問(wèn)題:在使用CGLIB時(shí),final方法不能被建議(即不能被AOP增強(qiáng)),因?yàn)樗鼈冊(cè)谶\(yùn)行時(shí)生成的子類中無(wú)法被覆蓋。

所以,在上面的PersonService中的save方法是不能被AOP增強(qiáng)的。了解了這么多以后我們來(lái)編寫一個(gè)測(cè)試程序來(lái)調(diào)用save方法看看執(zhí)行的結(jié)果。

@Service
public class AppRunService {


  private final PersonService personService ;
  public AppRunService(PersonService personService) {
    this.personService = personService ;
  }
  
  @PostConstruct
  public void init() {
    this.personService.save() ; 
  }
}

在該類中初始化階段會(huì)調(diào)用PersonService#save方法,輸出結(jié)果如下:

class: class com.pack.aop.PersonService$$EnhancerBySpringCGLIB$$557ca555, name: null

根據(jù)輸出結(jié)果得到,PersonService類被代理了,但是name為null,定義name屬性是明明是賦初始值Pack,為什么會(huì)出現(xiàn)null呢?

2. 原因分析

在上面已經(jīng)提到,Spring Boot中默認(rèn)會(huì)使用CGLIB創(chuàng)建代理對(duì)象。而CGLIB代理對(duì)象的創(chuàng)建會(huì)通過(guò)ObjenesisCglibAopProxy創(chuàng)建,如下源碼:

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方法會(huì)返回一個(gè)ObjenesisCglibAopProxy對(duì)象,由該對(duì)象創(chuàng)建代理。我們這里跳過(guò)中間流程,直接進(jìn)入到創(chuàng)建對(duì)象的代碼

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 通過(guò)CGLIB創(chuàng)建代碼的過(guò)程;看到這里大家可以先去搜索下    objenesis,這是一個(gè)開(kāi)源的庫(kù),該庫(kù)提供了一種機(jī)制,可以直接創(chuàng)建對(duì)象而跳過(guò)構(gòu)造函數(shù)。Spring重新打包了objenesis。下面通過(guò)代碼演示objenesis庫(kù)

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) ;
}

上通過(guò)ObjenesisStd創(chuàng)建對(duì)象,運(yùn)行結(jié)果:

Person [name=null]

name同樣為null??赡艿竭@里你還是不能理解為什么為null。這里我們需要對(duì)類的生命周期有了解才行,對(duì)于實(shí)例變量的初始化,是在構(gòu)造函數(shù)當(dāng)中,我們通過(guò)javap命令查看生成的字節(jié)碼

圖片圖片

通過(guò)反編譯知道了,實(shí)例變量的初始化是在構(gòu)造函數(shù)中。

到此,總結(jié)下為null的原因:

  • Spring通過(guò)cglib創(chuàng)建代理,但是對(duì)于final修飾的方法代理類是無(wú)法重新的;既然無(wú)法重寫,那么當(dāng)你調(diào)用的時(shí)候必然是調(diào)用父類中的方法。
  • 代理類的創(chuàng)建是通過(guò)objenesis,該庫(kù)創(chuàng)建的示例會(huì)跳過(guò)構(gòu)造函數(shù),而實(shí)例變量的最終初始化是在構(gòu)造函數(shù)中。

3. 解決辦法

上面分析了為什么為null的原因,那么該如何解決呢?我們可以通過(guò)3種辦法解決

3.1 成員變量添加final修飾符

public class PersonService {
  private final String name = "Pack" ;
}

輸出結(jié)果:

class: class com.pack.aop.PersonService$$EnhancerBySpringCGLIB$$87211922, name: Pack

正確輸出,因?yàn)閒inal修飾的實(shí)例變量在編譯為字節(jié)碼class時(shí)就已經(jīng)確定了值。

圖片圖片

3.2 將save方法的final去掉

將save方法的final去掉后,那么生成的代理類就可以重寫save方法了,最終調(diào)用save方法時(shí)先執(zhí)行增強(qiáng)部分,然后再調(diào)用真正的那個(gè)目標(biāo)類對(duì)象(真正的目標(biāo)類是并沒(méi)有通過(guò)objenesis創(chuàng)建,所以name是有值的)。

3.3 設(shè)置系統(tǒng)屬性

啟動(dòng)程序是添加如下系統(tǒng)屬性

-Dspring.objenesis.ignore=true

Spring容器在創(chuàng)建對(duì)象前會(huì)判斷,該系統(tǒng)屬性是否為true。

責(zé)任編輯:武曉燕 來(lái)源: Spring全家桶實(shí)戰(zhàn)案例源碼
相關(guān)推薦

2021-05-07 07:59:52

WebFluxSpring5系統(tǒng)

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 代碼操作系統(tǒng)

2017-12-27 14:51:12

Kotlin谷歌Java

2024-09-24 13:31:33

2015-05-11 10:39:19

2020-03-27 10:20:05

安全眾測(cè)滲透測(cè)試網(wǎng)絡(luò)安全

2022-03-15 17:35:20

電商系統(tǒng)架構(gòu)

2021-02-03 07:56:08

版本游戲邏輯

2020-05-22 10:35:07

CPU線程操作系統(tǒng)

2020-09-02 07:44:13

后端Long前端

2018-07-03 10:49:22

性能故障排查

2019-10-18 12:57:38

邊緣計(jì)算云計(jì)算安全

2016-03-09 11:19:01

2018-01-20 20:46:33

2012-05-30 09:40:55

Linux鍋爐

2023-04-28 12:01:56

Spring項(xiàng)目編譯

2020-06-12 11:03:22

Python開(kāi)發(fā)工具
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)