避坑!為了性能,Spring挖了一個(gè)大坑
環(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。