spring內嵌cglib包,這裡藏著一個大坑

子月生發表於2022-01-24

問題發現

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 來建立同一個類:

bug_analysis_006.png

接著我用程式碼試驗了一下,果然出現了同樣的報錯:

bug_analysis_007.png

問題解決

於是,我們可以給出結論:使用了spring 和原生兩個不同的 cglib 來生成同一個 class,會因為快取無法共享而出現 duplicate class attempt 的報錯

知道了原因,解決的辦法就非常簡單了。只要把 cglib 的導包改成同一個就行了。

修復後,生產再無該類報錯,基本證明我們是對的。

結語

以上就是這次生產報錯的處理過程。這裡我有幾個疑惑的地方:

  1. cglib 判斷一個 class 是否存在,為什麼不直接檢查專案裡的 class?卻要用快取這種不可靠的手段?
  2. spring 為什麼不直接依賴 cglib?而要自己內嵌一個?

最後,感謝閱讀,歡迎交流、指正。

本文為原創文章,轉載請附上原文出處連結:https://www.cnblogs.com/ZhangZiSheng001/p/15838657.html

相關文章