問題發現
2022-01-21 早上 9 點,訂單系統出現大面積的“系統未知錯誤”報錯,導致部分使用者無法正常下單。查詢後臺日誌,可以看到大量的 duplicate class attempt。
java.lang.LinkageError-->loader (instance of org/springframework/boot/loader/LaunchedURLClassLoader): attempted duplicate class definition for name: "com/order/vo/OrderAndExtendVO$$BeanMapByCGLIB$$e8178b2a"
StackTrace:
org.springframework.cglib.core.CodeGenerationException: java.lang.LinkageError-->loader (instance of org/springframework/boot/loader/LaunchedURLClassLoader): attempted duplicate class definition for name: "com/order/vo/OrderAndExtendVO$$BeanMapByCGLIB$$e8178b2a"
at org.springframework.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:538)
at org.springframework.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:363)
at org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData$3.apply(AbstractClassGenerator.java:110)
at org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData$3.apply(AbstractClassGenerator.java:108)
at org.springframework.cglib.core.internal.LoadingCache$2.call(LoadingCache.java:54)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at org.springframework.cglib.core.internal.LoadingCache.createEntry(LoadingCache.java:61)
at org.springframework.cglib.core.internal.LoadingCache.get(LoadingCache.java:34)
at org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:134)
at org.springframework.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:319)
at org.springframework.cglib.beans.BeanMap$Generator.create(BeanMap.java:127)
at org.springframework.cglib.beans.BeanMap.create(BeanMap.java:59)
····省略其他堆疊
問題分析
首先,通過堆疊,可以初步判斷,報錯是 cglib 嘗試生成一個已經存在的 class 導致的。
程式碼中呼叫了BeanMap.create(Object)
方法,這個方法會生成動態代理類。我們直接進入到AbstractClassGenerator.create(Object)
的原始碼,可以看到,全域性快取裡已經有了就不會再次生成,按理來說,代理類並不會重複生成,難道快取失效了嗎?
一開始我懷疑是因為快取被禁用了。但是吧,這個 useCache 欄位只能通過AbstractClassGenerator.setUseCache(boolean)
方法設定,而整個專案並沒有任何地方引用到這個方法,所以,這個假設並不成立。
abstract public class AbstractClassGenerator<T> implements ClassGenerator {
// 是否使用快取
private boolean useCache = true;
private static final Object NAME_KEY = new Object();
// 快取是全域性的
private static final Source SOURCE = new Source(BeanMap.class.getName());
protected static class Source {
String name;
/*
* 全域性快取,格式為:
* <blockquote><pre>
* {
* "classLoader1":{
* NAME_KEY:["className1","className2"],
* "className1":class1,
* "className2":class2
* },
* "classLoader2":{
* NAME_KEY:["className2","className3"],
* "className3":class3,
* "className2":class2
* }
* }
* zzs001
* </pre></blockquote>
* @author zzs
* @date 2022年1月24日 上午9:51:41
* @param args void
*/
Map cache = new WeakHashMap();
public Source(String name) {
this.name = name;
}
}
protected Object create(Object key) {
try {
Class gen = null;
synchronized (source) {
ClassLoader loader = getClassLoader();
Map cache2 = null;
cache2 = (Map)source.cache.get(loader);
if (cache2 == null) {
cache2 = new HashMap();
cache2.put(NAME_KEY, new HashSet());
source.cache.put(loader, cache2);
} else if (useCache) {
Reference ref = (Reference)cache2.get(key);
gen = (Class) (( ref == null ) ? null : ref.get());
}
if (gen == null) {
Object save = CURRENT.get();
CURRENT.set(this);
try {
this.key = key;
if (attemptLoad) {
try {
gen = loader.loadClass(getClassName());
} catch (ClassNotFoundException e) {
// ignore
}
}
if (gen == null) {
byte[] b = strategy.generate(this);
String className = ClassNameReader.getClassName(new ClassReader(b));
getClassNameCache(loader).add(className);
gen = ReflectUtils.defineClass(className, b, loader);
}
if (useCache) {
cache2.put(key, new WeakReference(gen));
}
return firstInstance(gen);
} finally {
CURRENT.set(save);
}
}
}
return firstInstance(gen);
} catch (RuntimeException e) {
throw e;
} catch (Error e) {
throw e;
} catch (Exception e) {
throw new CodeGenerationException(e);
}
}
}
我突然想到,這個 cglib 只是 spring 內嵌的 cglib,並不是”真正的 cglib“。快取只是在當前的這個 cglib 生效,如果原生的 cglib 也要建立這個類,是不是就會報錯了呢?
通過檢視引用,專案裡確實存在這種情況:使用了不同的 cglib 來建立同一個類:
接著我用程式碼試驗了一下,果然出現了同樣的報錯:
問題解決
於是,我們可以給出結論:使用了spring 和原生兩個不同的 cglib 來生成同一個 class,會因為快取無法共享而出現 duplicate class attempt 的報錯。
知道了原因,解決的辦法就非常簡單了。只要把 cglib 的導包改成同一個就行了。
修復後,生產再無該類報錯,基本證明我們是對的。
結語
以上就是這次生產報錯的處理過程。這裡我有幾個疑惑的地方:
- cglib 判斷一個 class 是否存在,為什麼不直接檢查專案裡的 class?卻要用快取這種不可靠的手段?
- spring 為什麼不直接依賴 cglib?而要自己內嵌一個?
最後,感謝閱讀,歡迎交流、指正。
本文為原創文章,轉載請附上原文出處連結:https://www.cnblogs.com/ZhangZiSheng001/p/15838657.html