Spring源碼學(xué)習(xí)(4) —— CglibAopProxy實(shí)現(xiàn)AOP之Enhancer源碼解析

上一節(jié)我們分析了cglib方式實(shí)現(xiàn)aop的基本過(guò)程莉恼,本文將繼續(xù)上一篇的內(nèi)容囚枪,具體講講代理對(duì)象的實(shí)現(xiàn)細(xì)節(jié)发笔。提到代理對(duì)象的產(chǎn)生缸榛,就不得不提Enhancer吝羞,這是cglib中的一個(gè)字節(jié)碼增強(qiáng)器,通過(guò)它我們可以對(duì)目標(biāo)類(lèi)進(jìn)行擴(kuò)展内颗。
本文主要分析以下兩個(gè)問(wèn)題:
1.cglib如何生成代理類(lèi)
2.cglib生成代理的方式中钧排,哪些方法不能被代理
3.cglib的二級(jí)緩存機(jī)制

1.源碼分析

Enhancer中產(chǎn)生代理類(lèi)的入口是幾個(gè)簽名不同的create()方法,但是這幾個(gè)create()方法最終都會(huì)調(diào)用createHelper()方法均澳,因此我們就從createHelper()方法著手:

private Object createHelper() {
    // 這里主要是一些參數(shù)校驗(yàn)恨溜,比較簡(jiǎn)單
    this.preValidate();

    // 根據(jù)代理配置生成緩存的key,作為二級(jí)緩存的key值找前,而一級(jí)緩存的key則是ClassLoader
    Object key = KEY_FACTORY.newInstance(this.superclass != null?this.superclass.getName():null, ReflectUtils.getNames(this.interfaces), this.filter == ALL_ZERO?null:new WeakCacheKey(this.filter), this.callbackTypes, this.useFactory, this.interceptDuringConstruction, this.serialVersionUID);
    this.currentKey = key;

    // 調(diào)用父類(lèi)AbstractClassGenerator的create()方法
    Object result = super.create(key);
    return result;
}

AbstractClassGenerator是cglib中一個(gè)很重要的類(lèi)糟袁,是字節(jié)碼生成過(guò)程中的一個(gè)核心調(diào)度者,包括緩存躺盛、命名策略系吭、字節(jié)碼生成策略都由它定義,Enhancer就是它的一個(gè)子類(lèi)颗品。

protected Object create(Object key) {
    try {
        // 獲取類(lèi)加載器
        ClassLoader loader = this.getClassLoader();
        Map<ClassLoader, AbstractClassGenerator.ClassLoaderData> cache = CACHE;

        // 優(yōu)先從緩存加載肯尺,根據(jù)ClassLoader區(qū)分
        AbstractClassGenerator.ClassLoaderData data = (AbstractClassGenerator.ClassLoaderData)cache.get(loader);

        // 緩存為空,則需要進(jìn)行初始化
        if(data == null) {
            Class var5 = AbstractClassGenerator.class;
            synchronized(AbstractClassGenerator.class) {
                cache = CACHE;
                data = (AbstractClassGenerator.ClassLoaderData)cache.get(loader);
                if(data == null) {
                    Map<ClassLoader, AbstractClassGenerator.ClassLoaderData> newCache = new WeakHashMap(cache);
                    data = new AbstractClassGenerator.ClassLoaderData(loader);
                    // 放入緩存
                    newCache.put(loader, data);
                    CACHE = newCache;
                }
            }
        }

        // 從緩存加載
        this.key = key;
        Object obj = data.get(this, this.getUseCache());
        return obj instanceof Class?this.firstInstance((Class)obj):this.nextInstance(obj);
    } 
... 異常處理省略
}

類(lèi)加載器是按照如下順序來(lái)獲取的:加載父類(lèi)或者接口的類(lèi)加載器 -> 自己的類(lèi)加載器 -> 線(xiàn)程上下文類(lèi)加載器躯枢,這個(gè)過(guò)程在上一篇也有提到则吟,這里不再展開(kāi)。

緩存在這個(gè)構(gòu)造函數(shù)中進(jìn)行初始化:

protected static class ClassLoaderData {
    public ClassLoaderData(ClassLoader classLoader) {
        if(classLoader == null) {
            throw new IllegalArgumentException("classLoader == null is not yet supported");
        } else {
            this.classLoader = new WeakReference(classLoader);
            Function<AbstractClassGenerator, Object> load = new Function<AbstractClassGenerator, Object>() {
                public Object apply(AbstractClassGenerator gen) {

                    // 產(chǎn)生字節(jié)碼文件
                    Class klass = gen.generate(ClassLoaderData.this);
                    return gen.wrapCachedClass(klass);
                }
            };

            // 生成二級(jí)緩存
            this.generatedClasses = new LoadingCache(GET_KEY, load);
        }
    }

    // 判斷是否使用緩存
    public Object get(AbstractClassGenerator gen, boolean useCache) {
        if(!useCache) {
            return gen.generate(this);
        } else {
            // 從二級(jí)緩存中取出代理對(duì)象
            Object cachedValue = this.generatedClasses.get(gen);
            return gen.unwrapCachedValue(cachedValue);
        }
    }
}
protected Class generate(AbstractClassGenerator.ClassLoaderData data) {
    Object save = CURRENT.get();
    CURRENT.set(this);

    try {
        ClassLoader classLoader = data.getClassLoader();
        if(classLoader == null) {
            throw new IllegalStateException("ClassLoader is null while trying to define class " + this.getClassName() + ". It seems that the loader has been expired from a weak reference somehow. Please file an issue at cglib's issue tracker.");
        } else {
            String className;
            synchronized(classLoader) {
                // 根據(jù)命名策略生成代理類(lèi)類(lèi)名
                className = this.generateClassName(data.getUniqueNamePredicate());
                data.reserveName(className);
                this.setClassName(className);
            }

            Class gen;
            if(this.attemptLoad) {
                try {
                    gen = classLoader.loadClass(this.getClassName());
                    Class var25 = gen;
                    return var25;
                } catch (ClassNotFoundException var20) {
                    ;
                }
            }

            // 生成二進(jìn)制流锄蹂,默認(rèn)實(shí)現(xiàn)DefaultGeneratorStrategy
            byte[] b = this.strategy.generate(this);
            className = ClassNameReader.getClassName(new ClassReader(b));
            ProtectionDomain protectionDomain = this.getProtectionDomain();
            synchronized(classLoader) {
                if(protectionDomain == null) {
                    // 根據(jù)二進(jìn)制流生成字節(jié)碼
                    gen = ReflectUtils.defineClass(className, b, classLoader);
                } else {
                    gen = ReflectUtils.defineClass(className, b, classLoader, protectionDomain);
                }
            }

            Class var8 = gen;
            return var8;
        }
    }
... 省略異常處理

生成代理類(lèi)類(lèi)名的具體邏輯如下:

public String getClassName(String prefix, String source, Object key, Predicate names) {
    // prefix一般就是目標(biāo)類(lèi)的全限定名
    if(prefix == null) {
        prefix = "org.springframework.cglib.empty.Object";
    } else if(prefix.startsWith("java")) {
        prefix = "$" + prefix;
    }

    // source就是Enhancer的全限定名
    // source.substring(source.lastIndexOf(46) + 1)就是去Enhancer的簡(jiǎn)稱(chēng)
    // this.getTag()返回的是"ByCGLIB"
    String base = prefix + "$$" + source.substring(source.lastIndexOf(46) + 1) + this.getTag() + "$$" + Integer.toHexString(STRESS_HASH_CODE?0:key.hashCode());
    String attempt = base;

    for(int var7 = 2; names.evaluate(attempt); attempt = base + "_" + var7++) {
        ;
    }

    return attempt;
}
public class DefaultGeneratorStrategy implements GeneratorStrategy {

    public byte[] generate(ClassGenerator cg) throws Exception {
        DebuggingClassWriter cw = this.getClassVisitor();
        this.transform(cg).generateClass(cw);
        return this.transform(cw.toByteArray());
    }
}

利用Enhancer來(lái)生成代理類(lèi)氓仲,底層也是采用了asm框架,具體實(shí)現(xiàn)如下:

public void generateClass(ClassVisitor v) throws Exception {
    Class sc = this.superclass == null?Object.class:this.superclass;
    if(TypeUtils.isFinal(sc.getModifiers())) {
        throw new IllegalArgumentException("Cannot subclass final class " + sc.getName());
    } else {
        List constructors = new ArrayList(Arrays.asList(sc.getDeclaredConstructors()));
        this.filterConstructors(sc, constructors);
        List actualMethods = new ArrayList();
        List interfaceMethods = new ArrayList();
        final Set forcePublic = new HashSet();

        // 采用遞歸的方式,找到目標(biāo)類(lèi)的父類(lèi)敬扛、接口的所有方法晰洒,并過(guò)濾掉滿(mǎn)足條件的方法,具體條件后文有詳細(xì)說(shuō)明
        getMethods(sc, this.interfaces, actualMethods, interfaceMethods, forcePublic);

        // 對(duì)方法的修飾符作了變化啥箭,保存到methods中谍珊,轉(zhuǎn)換之前的方法保存在actualMethods中
        List methods = CollectionUtils.transform(actualMethods, new Transformer() {
            public Object transform(Object value) {
                Method method = (Method)value;
                // 在Modifier類(lèi)中,1024急侥、256砌滞、32、16分別表示abstract坏怪、native贝润、synchronized、final修飾符铝宵,因此這句話(huà)的意思是將abstract打掘、native、synchronized修飾符全部去掉鹏秋,然后加上final修飾符
                int modifiers = 16 | method.getModifiers() & -1025 & -257 & -33;
                // 同理尊蚁,這里是把protected修飾符換成public
                if(forcePublic.contains(MethodWrapper.create(method))) {
                    modifiers = modifiers & -5 | 1;
                }

                return ReflectUtils.getMethodInfo(method, modifiers);
            }
        });

        // 這里有一段是根據(jù)asm框架產(chǎn)生字節(jié)碼了,還沒(méi)看懂拼岳,先跳過(guò)o(╯□╰)o
        ......

        if(this.currentData == null) {
            // 上一篇講過(guò)枝誊,CallbackFilter作為回調(diào)過(guò)濾器况芒,其核心方法為accept()惜纸,返回值為int類(lèi)型,代表了回調(diào)入口在callbacks數(shù)組中的位置绝骚,這里就是完成了CallbackFilter的處理耐版,確定每個(gè)方法分別有哪個(gè)過(guò)濾器來(lái)處理
            this.emitMethods(e, methods, actualMethods);
            this.emitConstructors(e, constructorInfo);
        } else {
            this.emitDefaultConstructor(e);
        }

        ......

        e.end_class();
    }
}

getMethods方法具體實(shí)現(xiàn)邏輯:

private static void getMethods(Class superclass, Class[] interfaces, List methods, List interfaceMethods, Set forcePublic) {
    // 遞歸添加父類(lèi)的所有方法
    ReflectUtils.addAllMethods(superclass, methods);
    List target = interfaceMethods != null?interfaceMethods:methods;

    // 遞歸添加接口的所有方法
    if(interfaces != null) {
        for(int i = 0; i < interfaces.length; ++i) {
            if(interfaces[i] != Factory.class) {
                ReflectUtils.addAllMethods(interfaces[i], target);
            }
        }
    }

    if(interfaceMethods != null) {
        if(forcePublic != null) {
            forcePublic.addAll(MethodWrapper.createSet(interfaceMethods));
        }

        methods.addAll(interfaceMethods);
    }

    // 過(guò)濾掉靜態(tài)方法,在Modifier類(lèi)中压汪,靜態(tài)方法的標(biāo)識(shí)就是0x00000008
    CollectionUtils.filter(methods, new RejectModifierPredicate(8));
    // 過(guò)濾掉不可見(jiàn)的方法(包括兩類(lèi):私有方法粪牲、默認(rèn)修飾符且與目標(biāo)類(lèi)不在同一個(gè)包中的方法)
    CollectionUtils.filter(methods, new VisibilityPredicate(superclass, true));
    // 過(guò)濾掉重復(fù)的方法(方法名、入?yún)⒑头祷仡?lèi)型都相同的方法)
    CollectionUtils.filter(methods, new DuplicatesPredicate());
    // 過(guò)濾掉final方法止剖,在Modifier類(lèi)中腺阳,靜態(tài)方法的標(biāo)識(shí)就是0x00000010
    CollectionUtils.filter(methods, new RejectModifierPredicate(16));
}

emitMethods方法具體實(shí)現(xiàn)邏輯:

private void emitMethods(ClassEmitter ce, List methods, List actualMethods) {
    CallbackGenerator[] generators = CallbackInfo.getGenerators(this.callbackTypes);
    Map groups = new HashMap();
    final Map indexes = new HashMap();
    final Map originalModifiers = new HashMap();
    final Map positions = CollectionUtils.getIndexMap(methods);
    Map declToBridge = new HashMap();
    Iterator it1 = methods.iterator();
    Iterator it2 = actualMethods != null?actualMethods.iterator():null;

    while(it1.hasNext()) {
        MethodInfo method = (MethodInfo)it1.next();
        Method actualMethod = it2 != null?(Method)it2.next():null;

        // 選擇具體的回調(diào)過(guò)濾器
        int index = this.filter.accept(actualMethod);
        if(index >= this.callbackTypes.length) {
            throw new IllegalArgumentException("Callback filter returned an index that is too large: " + index);
        }

        // 把方法和其對(duì)應(yīng)的修飾符的映射關(guān)系保存起來(lái)
        originalModifiers.put(method, new Integer(actualMethod != null?actualMethod.getModifiers():method.getModifiers()));
        // 把方法和其對(duì)應(yīng)的回調(diào)過(guò)濾器下表的映射關(guān)系保存起來(lái)
        indexes.put(method, new Integer(index));
        List group = (List)groups.get(generators[index]);
        if(group == null) {
            groups.put(generators[index], group = new ArrayList(methods.size()));
        }
        ((List)group).add(method);

        // 建立類(lèi)和橋接方法之間的映射關(guān)系
        // 橋接方法:jdk1.5引入了泛型,并且在編譯期會(huì)進(jìn)行類(lèi)型擦除穿香,因此字節(jié)碼文件中集合元素類(lèi)型都是Object亭引,同時(shí)由于對(duì)類(lèi)型的校驗(yàn)也提前到了編譯期,因此編譯期會(huì)生成一個(gè)橋接方法皮获,將Object類(lèi)型轉(zhuǎn)換為實(shí)際類(lèi)型焙蚓。
        if(TypeUtils.isBridge(actualMethod.getModifiers())) {
            Set bridges = (Set)declToBridge.get(actualMethod.getDeclaringClass());
            if(bridges == null) {
                bridges = new HashSet();
                declToBridge.put(actualMethod.getDeclaringClass(), bridges);
            }

            ((Set)bridges).add(method.getSignature());
        }
    }

    // 又是一堆根據(jù)asm生成字節(jié)碼的邏輯,暫時(shí)略過(guò)o(╯□╰)o
    ......
}

2.測(cè)試

好了,源碼總是虛無(wú)縹緲的购公,我們接下來(lái)舉一個(gè)實(shí)際的栗子來(lái)看一看萌京。

目標(biāo)類(lèi):

public class Target {

    public void exeTarget() {
        System.out.println("execute target.");
    }
}

增強(qiáng)類(lèi):

public class BeforeAdvice implements MethodBeforeAdvice {

    public void before(Method method, Object[] objects, Object o) throws Throwable {
        System.out.println("before advice.");
    }
}

配置文件:

<bean id="adviceSeq" class="org.springframework.aop.framework.ProxyFactoryBean"
      p:target-ref="target"
      p:proxyTargetClass="true"
      p:frozen="true"
      p:interceptorNames="beforeAdvice"/>

測(cè)試類(lèi):

public class AdviceSeqTest {

    public static void main(String[] args) {

        // 將產(chǎn)生的代理類(lèi)文件寫(xiě)入磁盤(pán)
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, ".//");
        String configPath = "advice/seq/beans.xml";
        ApplicationContext context = new ClassPathXmlApplicationContext(configPath);
        Target target = (Target) context.getBean("adviceSeq");
        target.exeTarget();
    }
}

運(yùn)行測(cè)試類(lèi)后,得到的代理類(lèi)class文件名為T(mén)arget$$EnhancerBySpringCGLIB$$8b53f595.class宏浩,跟我們前面的分析一致知残,目標(biāo)方法的代理方法為如下,靜態(tài)方法exeStaticTarget()绘闷、私有privateMethod()和final方法finalMethod()在生成的代理類(lèi)中都不存在:

// 跟之前分析的一樣橡庞,方法加上了final修飾符
public final void exeTarget() {
    try {
        MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
        if(this.CGLIB$CALLBACK_0 == null) {
            CGLIB$BIND_CALLBACKS(this);
            var10000 = this.CGLIB$CALLBACK_0;
        }

        // 如果回調(diào)函數(shù)不為空,執(zhí)行攔截印蔗,否則直接調(diào)用目標(biāo)方法
        if(var10000 != null) {
            var10000.intercept(this, CGLIB$exeTarget$6$Method, CGLIB$emptyArgs, CGLIB$exeTarget$6$Proxy);
        } else {
            super.exeTarget();
        }
    } catch (Error | RuntimeException var1) {
        throw var1;
    } catch (Throwable var2) {
        throw new UndeclaredThrowableException(var2);
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末扒最,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子华嘹,更是在濱河造成了極大的恐慌吧趣,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,651評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件耙厚,死亡現(xiàn)場(chǎng)離奇詭異强挫,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)薛躬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)俯渤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人型宝,你說(shuō)我怎么就攤上這事八匠。” “怎么了趴酣?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,931評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵梨树,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我岖寞,道長(zhǎng)抡四,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,218評(píng)論 1 292
  • 正文 為了忘掉前任仗谆,我火速辦了婚禮指巡,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘隶垮。我一直安慰自己藻雪,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,234評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布岁疼。 她就那樣靜靜地躺著阔涉,像睡著了一般缆娃。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上瑰排,一...
    開(kāi)封第一講書(shū)人閱讀 51,198評(píng)論 1 299
  • 那天行瑞,我揣著相機(jī)與錄音伞广,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛筋讨,可吹牛的內(nèi)容都是我干的尘执。 我是一名探鬼主播礼搁,決...
    沈念sama閱讀 40,084評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼瀑构,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了些举?” 一聲冷哼從身側(cè)響起跟狱,我...
    開(kāi)封第一講書(shū)人閱讀 38,926評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎户魏,沒(méi)想到半個(gè)月后驶臊,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,341評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡叼丑,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,563評(píng)論 2 333
  • 正文 我和宋清朗相戀三年关翎,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鸠信。...
    茶點(diǎn)故事閱讀 39,731評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡纵寝,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出星立,到底是詐尸還是另有隱情爽茴,我是刑警寧澤,帶...
    沈念sama閱讀 35,430評(píng)論 5 343
  • 正文 年R本政府宣布贞铣,位于F島的核電站闹啦,受9級(jí)特大地震影響沮明,放射性物質(zhì)發(fā)生泄漏辕坝。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,036評(píng)論 3 326
  • 文/蒙蒙 一荐健、第九天 我趴在偏房一處隱蔽的房頂上張望酱畅。 院中可真熱鬧,春花似錦江场、人聲如沸纺酸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,676評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)餐蔬。三九已至碎紊,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間樊诺,已是汗流浹背仗考。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,829評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留词爬,地道東北人秃嗜。 一個(gè)月前我還...
    沈念sama閱讀 47,743評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像顿膨,于是被迫代替她去往敵國(guó)和親锅锨。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,629評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理恋沃,服務(wù)發(fā)現(xiàn)必搞,斷路器,智...
    卡卡羅2017閱讀 134,652評(píng)論 18 139
  • 前言 什么是代理囊咏?火車(chē)站的黃牛顾画、明星的經(jīng)紀(jì)人、鏈家賣(mài)房子的小哥匆笤。這些代理都有一個(gè)共同特點(diǎn)研侣,那就是能夠做一些被他們代...
    后廠村老司機(jī)閱讀 1,058評(píng)論 0 4
  • 那天我在內(nèi)蒙古呼市遠(yuǎn)郊一個(gè)偌大的牧場(chǎng)里獨(dú)自徘徊庶诡。正是晚春時(shí)節(jié),來(lái)自草原強(qiáng)勁的風(fēng)吹亂了我的思緒咆课,陰霾的天空一如我的心...
    瑤鄉(xiāng)人家閱讀 496評(píng)論 0 0
  • 我是夏花花书蚪,今天我加入簡(jiǎn)書(shū)喇澡。希望以隨筆的形式記錄下自己的內(nèi)心。 歡迎志同道合的朋友交流和討論自己的故事殊校,你有故事那...
    空心即墨閱讀 164評(píng)論 0 0
  • 逢年過(guò)節(jié),親朋好友串門(mén)做客敬察,這原本是一件非常熱鬧又開(kāi)心的事情秀睛。可是一旦有熊孩子的闖入莲祸,就會(huì)讓人變得有些不安蹂安。 你一...
    o貓咪不吃魚(yú)o閱讀 709評(píng)論 9 17