避坑!為了性能,Spring挖了一個大坑
環境: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。