cglib實(shí)現(xiàn)原理

cglib簡介

CGLIB(Code Generation Library)是一個(gè)開源項(xiàng)目慢哈!是一個(gè)強(qiáng)大的,高性能永票,高質(zhì)量的Code生成類庫卵贱,它廣泛的被許多AOP的框架使用滥沫,例如Spring AOP為他們提供方法的interception(攔截)。CGLIB包的底層是通過使用一個(gè)小而快的字節(jié)碼處理框架ASM键俱,來轉(zhuǎn)換字節(jié)碼并生成新的類兰绣。除了CGLIB包,腳本語言例如Groovy和BeanShell编振,也是使用ASM來生成java的字節(jié)碼缀辩。當(dāng)然不鼓勵(lì)直接使用ASM,因?yàn)樗竽惚仨殞?duì)JVM內(nèi)部結(jié)構(gòu)包括class文件的格式和指令集都很熟悉党觅。


image.png

CGLIB動(dòng)態(tài)代理實(shí)例

實(shí)現(xiàn)一個(gè)業(yè)務(wù)類雌澄,注意,這個(gè)業(yè)務(wù)類并沒有實(shí)現(xiàn)任何接口:

package com.jpeony.spring.proxy.cglib;
 
public class HelloService {
 
    public HelloService() {
        System.out.println("HelloService構(gòu)造");
    }
 
    /**
     * 該方法不能被子類覆蓋,Cglib是無法代理final修飾的方法的
     */
    final public String sayOthers(String name) {
        System.out.println("HelloService:sayOthers>>"+name);
        return null;
    }
 
    public void sayHello() {
        System.out.println("HelloService:sayHello");
    }
}

自定義MethodInterceptor:

package com.jpeony.spring.proxy.cglib;
 
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
 
import java.lang.reflect.Method;
 
/**
 * 自定義MethodInterceptor
 */
public class MyMethodInterceptor implements MethodInterceptor{
 
    /**
     * sub:cglib生成的代理對(duì)象
     * method:被代理對(duì)象方法
     * objects:方法入?yún)?     * methodProxy: 代理方法
     */
    @Override
    public Object intercept(Object sub, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("======插入前置通知======");
        Object object = methodProxy.invokeSuper(sub, objects);
        System.out.println("======插入后者通知======");
        return object;
    }
}

生成CGLIB代理對(duì)象調(diào)用目標(biāo)方法:

package com.jpeony.spring.proxy.cglib;
 
import net.sf.cglib.core.DebuggingClassWriter;
import net.sf.cglib.proxy.Enhancer;
 
public class Client {
    public static void main(String[] args) {
        // 代理類class文件存入本地磁盤方便我們反編譯查看源碼
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "D:\\code");
        // 通過CGLIB動(dòng)態(tài)代理獲取代理對(duì)象的過程
        Enhancer enhancer = new Enhancer();
        // 設(shè)置enhancer對(duì)象的父類
        enhancer.setSuperclass(HelloService.class);
        // 設(shè)置enhancer的回調(diào)對(duì)象
        enhancer.setCallback(new MyMethodInterceptor());
        // 創(chuàng)建代理對(duì)象
        HelloService proxy= (HelloService)enhancer.create();
        // 通過代理對(duì)象調(diào)用目標(biāo)方法
        proxy.sayHello();
    }
}
Connected to the target VM, address: '127.0.0.1:65382', transport: 'socket'
CGLIB debugging enabled, writing to 'code'
HelloService構(gòu)造
======插入前置通知======
HelloService:sayHello
======插入后者通知======
Disconnected from the target VM, address: '127.0.0.1:65382', transport: 'socket'

Process finished with exit code 0

CGLIB動(dòng)態(tài)代理源碼分析

實(shí)現(xiàn)CGLIB動(dòng)態(tài)代理必須實(shí)現(xiàn)MethodInterceptor(方法攔截器)接口杯瞻,源碼如下:

/*
 * Copyright 2002,2003 The Apache Software Foundation
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.sf.cglib.proxy;
 
/**
 * General-purpose {@link Enhancer} callback which provides for "around advice".
 * @author Juozas Baliuka <a href="mailto:baliuka@mwm.lt">baliuka@mwm.lt</a>
 * @version $Id: MethodInterceptor.java,v 1.8 2004/06/24 21:15:20 herbyderby Exp $
 */
public interface MethodInterceptor
extends Callback
{
    /**
     * All generated proxied methods call this method instead of the original method.
     * The original method may either be invoked by normal reflection using the Method object,
     * or by using the MethodProxy (faster).
     * @param obj "this", the enhanced object
     * @param method intercepted Method
     * @param args argument array; primitive types are wrapped
     * @param proxy used to invoke super (non-intercepted method); may be called
     * as many times as needed
     * @throws Throwable any exception may be thrown; if so, super method will not be invoked
     * @return any value compatible with the signature of the proxied method. Method returning void will ignore this value.
     * @see MethodProxy
     */    
    public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args,
                               MethodProxy proxy) throws Throwable;
 
}

這個(gè)接口只有一個(gè)intercept()方法镐牺,這個(gè)方法有4個(gè)參數(shù):

1)obj表示增強(qiáng)的對(duì)象,即實(shí)現(xiàn)這個(gè)接口類的一個(gè)對(duì)象魁莉;
2)method表示要被攔截的方法睬涧;
3)args表示要被攔截方法的參數(shù);
4)proxy表示要觸發(fā)父類的方法對(duì)象旗唁;

在上面的Client代碼中畦浓,通過Enhancer.create()方法創(chuàng)建代理對(duì)象,create()方法的源碼:

/**
     * Generate a new class if necessary and uses the specified
     * callbacks (if any) to create a new object instance.
     * Uses the no-arg constructor of the superclass.
     * @return a new instance
     */
    public Object create() {
        classOnly = false;
        argumentTypes = null;
        return createHelper();
    }

該方法含義就是如果有必要就創(chuàng)建一個(gè)新類检疫,并且用指定的回調(diào)對(duì)象創(chuàng)建一個(gè)新的對(duì)象實(shí)例讶请,

使用的父類的參數(shù)的構(gòu)造方法來實(shí)例化父類的部分。核心內(nèi)容在createHelper()中屎媳,源碼如下:

private Object createHelper() {
        preValidate();
        Object key = KEY_FACTORY.newInstance((superclass != null) ? superclass.getName() : null,
                ReflectUtils.getNames(interfaces),
                filter == ALL_ZERO ? null : new WeakCacheKey<CallbackFilter>(filter),
                callbackTypes,
                useFactory,
                interceptDuringConstruction,
                serialVersionUID);
        this.currentKey = key;
        Object result = super.create(key);
        return result;
    }

通過newInstance()方法創(chuàng)建EnhancerKey對(duì)象夺溢,作為Enhancer父類AbstractClassGenerator.create()方法創(chuàng)建代理對(duì)象的參數(shù)。

protected Object create(Object key) {
        try {
            ClassLoader loader = getClassLoader();
            Map<ClassLoader, ClassLoaderData> cache = CACHE;
            ClassLoaderData data = cache.get(loader);
            if (data == null) {
                synchronized (AbstractClassGenerator.class) {
                    cache = CACHE;
                    data = cache.get(loader);
                    if (data == null) {
                        Map<ClassLoader, ClassLoaderData> newCache = new WeakHashMap<ClassLoader, ClassLoaderData>(cache);
                        data = new ClassLoaderData(loader);
                        newCache.put(loader, data);
                        CACHE = newCache;
                    }
                }
            }
            this.key = key;
            Object obj = data.get(this, getUseCache());
            if (obj instanceof Class) {
                return firstInstance((Class) obj);
            }
            return nextInstance(obj);
        } catch (RuntimeException e) {
            throw e;
        } catch (Error e) {
            throw e;
        } catch (Exception e) {
            throw new CodeGenerationException(e);
        }
    }

真正創(chuàng)建代理對(duì)象方法在nextInstance()方法中烛谊,該方法為抽象類AbstractClassGenerator的一個(gè)方法风响,簽名如下:
abstract protected Object nextInstance(Object instance) throws Exception;
在子類Enhancer中實(shí)現(xiàn),實(shí)現(xiàn)源碼如下:

protected Object nextInstance(Object instance) {
        EnhancerFactoryData data = (EnhancerFactoryData) instance;
 
        if (classOnly) {
            return data.generatedClass;
        }
 
        Class[] argumentTypes = this.argumentTypes;
        Object[] arguments = this.arguments;
        if (argumentTypes == null) {
            argumentTypes = Constants.EMPTY_CLASS_ARRAY;
            arguments = null;
        }
        return data.newInstance(argumentTypes, arguments, callbacks);
    }

看看data.newInstance(argumentTypes, arguments, callbacks)方法丹禀,

第一個(gè)參數(shù)為代理對(duì)象的構(gòu)成器類型状勤,第二個(gè)為代理對(duì)象構(gòu)造方法參數(shù),第三個(gè)為對(duì)應(yīng)回調(diào)對(duì)象双泪。最后根據(jù)這些參數(shù)持搜,通過反射生成代理對(duì)象,源碼如下:

/**
         * Creates proxy instance for given argument types, and assigns the callbacks.
         * Ideally, for each proxy class, just one set of argument types should be used,
         * otherwise it would have to spend time on constructor lookup.
         * Technically, it is a re-implementation of {@link Enhancer#createUsingReflection(Class)},
         * with "cache {@link #setThreadCallbacks} and {@link #primaryConstructor}"
         *
         * @see #createUsingReflection(Class)
         * @param argumentTypes constructor argument types
         * @param arguments constructor arguments
         * @param callbacks callbacks to set for the new instance
         * @return newly created proxy
         */
        public Object newInstance(Class[] argumentTypes, Object[] arguments, Callback[] callbacks) {
            setThreadCallbacks(callbacks);
            try {
                // Explicit reference equality is added here just in case Arrays.equals does not have one
                if (primaryConstructorArgTypes == argumentTypes ||
                        Arrays.equals(primaryConstructorArgTypes, argumentTypes)) {
                    // If we have relevant Constructor instance at hand, just call it
                    // This skips "get constructors" machinery
                    return ReflectUtils.newInstance(primaryConstructor, arguments);
                }
                // Take a slow path if observing unexpected argument types
                return ReflectUtils.newInstance(generatedClass, argumentTypes, arguments);
            } finally {
                // clear thread callbacks to allow them to be gc'd
                setThreadCallbacks(null);
            }
 
        }
image.png

將其反編譯后代碼如下:

package com.jpeony.spring.proxy.cglib;
 
import java.lang.reflect.Method;
import net.sf.cglib.core.ReflectUtils;
import net.sf.cglib.core.Signature;
import net.sf.cglib.proxy.*;
 
public class HelloService$$EnhancerByCGLIB$$4da4ebaf extends HelloService
    implements Factory
{
 
    private boolean CGLIB$BOUND;
    public static Object CGLIB$FACTORY_DATA;
    private static final ThreadLocal CGLIB$THREAD_CALLBACKS;
    private static final Callback CGLIB$STATIC_CALLBACKS[];
    private MethodInterceptor CGLIB$CALLBACK_0; // 攔截器
    private static Object CGLIB$CALLBACK_FILTER;
    private static final Method CGLIB$sayHello$0$Method; // 被代理方法
    private static final MethodProxy CGLIB$sayHello$0$Proxy; // 代理方法
    private static final Object CGLIB$emptyArgs[];
    private static final Method CGLIB$equals$1$Method;
    private static final MethodProxy CGLIB$equals$1$Proxy;
    private static final Method CGLIB$toString$2$Method;
    private static final MethodProxy CGLIB$toString$2$Proxy;
    private static final Method CGLIB$hashCode$3$Method;
    private static final MethodProxy CGLIB$hashCode$3$Proxy;
    private static final Method CGLIB$clone$4$Method;
    private static final MethodProxy CGLIB$clone$4$Proxy;
 
    static void CGLIB$STATICHOOK1()
    {
        Method amethod[];
        Method amethod1[];
        CGLIB$THREAD_CALLBACKS = new ThreadLocal();
        CGLIB$emptyArgs = new Object[0];
        // 代理類
        Class class1 = Class.forName("com.jpeony.spring.proxy.cglib.HelloService$$EnhancerByCGLIB$$4da4ebaf");
        // 被代理類
        Class class2;
        amethod = ReflectUtils.findMethods(new String[] {
            "equals", "(Ljava/lang/Object;)Z", "toString", "()Ljava/lang/String;", "hashCode", "()I", "clone", "()Ljava/lang/Object;"
        }, (class2 = Class.forName("java.lang.Object")).getDeclaredMethods());
        Method[]  = amethod;
        CGLIB$equals$1$Method = amethod[0];
        CGLIB$equals$1$Proxy = MethodProxy.create(class2, class1, "(Ljava/lang/Object;)Z", "equals", "CGLIB$equals$1");
        CGLIB$toString$2$Method = amethod[1];
        CGLIB$toString$2$Proxy = MethodProxy.create(class2, class1, "()Ljava/lang/String;", "toString", "CGLIB$toString$2");
        CGLIB$hashCode$3$Method = amethod[2];
        CGLIB$hashCode$3$Proxy = MethodProxy.create(class2, class1, "()I", "hashCode", "CGLIB$hashCode$3");
        CGLIB$clone$4$Method = amethod[3];
        CGLIB$clone$4$Proxy = MethodProxy.create(class2, class1, "()Ljava/lang/Object;", "clone", "CGLIB$clone$4");
        amethod1 = ReflectUtils.findMethods(new String[] {
            "sayHello", "()V"
        }, (class2 = Class.forName("com.jpeony.spring.proxy.cglib.HelloService")).getDeclaredMethods());
        Method[] 1 = amethod1;
        CGLIB$sayHello$0$Method = amethod1[0];
        CGLIB$sayHello$0$Proxy = MethodProxy.create(class2, class1, "()V", "sayHello", "CGLIB$sayHello$0");
    }
 
    final void CGLIB$sayHello$0()
    {
        super.sayHello();
    }
 
    public final void sayHello()
    {
      MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
      if(this.CGLIB$CALLBACK_0 == null) {
         CGLIB$BIND_CALLBACKS(this);
         var10000 = this.CGLIB$CALLBACK_0;
      }
 
      if(var10000 != null) {
         // 調(diào)用攔截器
         var10000.intercept(this, CGLIB$setPerson$0$Method, CGLIB$emptyArgs, CGLIB$setPerson$0$Proxy);
      } else {
         super.sayHello();
      }
    }
    ......
    ......
}

從代理對(duì)象反編譯源碼可以知道焙矛,代理對(duì)象繼承于HelloService朵诫,攔截器調(diào)用intercept()方法,

intercept()方法由自定義MyMethodInterceptor實(shí)現(xiàn)薄扁,所以剪返,最后調(diào)用MyMethodInterceptor中的intercept()方法废累,從而完成了由代理對(duì)象訪問到目標(biāo)對(duì)象的動(dòng)態(tài)代理實(shí)現(xiàn)。

常用的API

Enhancer

Enhancer可能是CGLIB中最常用的一個(gè)類脱盲,和Java1.3動(dòng)態(tài)代理中引入的Proxy類差不多邑滨,和Proxy不同的是,Enhancer既能夠代理普通的class钱反,也能夠代理接口掖看。Enhancer創(chuàng)建一個(gè)被代理對(duì)象的子類并且攔截所有的方法調(diào)用(包括從Object中繼承的toString和hashCode方法)Enhancer不能夠攔截final方法,例如Object.getClass()方法面哥,這是由于Java final方法語義決定的哎壳。基于同樣的道理尚卫,Enhancer也不能對(duì)fianl類進(jìn)行代理操作归榕。

public class SampleClass {
    public String test(String input){
        return "hello world";
    }
}

下面我們將以這個(gè)類作為主要的測(cè)試類,來測(cè)試調(diào)用各種方法

@Test
public void testFixedValue(){
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(SampleClass.class);
    enhancer.setCallback(new FixedValue() {
        @Override
        public Object loadObject() throws Exception {
            return "Hello cglib";
        }
    });
    SampleClass proxy = (SampleClass) enhancer.create();
    System.out.println(proxy.test(null)); //攔截test吱涉,輸出Hello cglib
    System.out.println(proxy.toString()); 
    System.out.println(proxy.getClass());
    System.out.println(proxy.hashCode());
}

程序的輸出為:

Hello cglib
Hello cglib
class com.zeus.cglib.SampleClass$$EnhancerByCGLIB$$e3ea9b7

java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Number

    at com.zeus.cglib.SampleClass$$EnhancerByCGLIB$$e3ea9b7.hashCode(<generated>)
    ...

上述代碼中刹泄,F(xiàn)ixedValue用來對(duì)所有攔截的方法返回相同的值,從輸出我們可以看出來怎爵,Enhancer對(duì)非final方法test()特石、toString()、hashCode()進(jìn)行了攔截鳖链,沒有對(duì)getClass進(jìn)行攔截姆蘸。由于hashCode()方法需要返回一個(gè)Number,但是我們返回的是一個(gè)String芙委,這解釋了上面的程序中為什么會(huì)拋出異常逞敷。
Enhancer.setSuperclass用來設(shè)置父類型,從toString方法可以看出题山,使用CGLIB生成的類為被代理類的一個(gè)子類兰粉,形如SampleClassEnhancerByCGLIBe3ea9b7

有些時(shí)候我們可能只想對(duì)特定的方法進(jìn)行攔截故痊,對(duì)其他的方法直接放行顶瞳,不做任何操作,使用Enhancer處理這種需求同樣很簡單,只需要一個(gè)CallbackFilter即可:

@Test
public void testCallbackFilter() throws Exception{
    Enhancer enhancer = new Enhancer();
    CallbackHelper callbackHelper = new CallbackHelper(SampleClass.class, new Class[0]) {
        @Override
        protected Object getCallback(Method method) {
            if(method.getDeclaringClass() != Object.class && method.getReturnType() == String.class){
                return new FixedValue() {
                    @Override
                    public Object loadObject() throws Exception {
                        return "Hello cglib";
                    }
                };
            }else{
                return NoOp.INSTANCE;
            }
        }
    };
    enhancer.setSuperclass(SampleClass.class);
    enhancer.setCallbackFilter(callbackHelper);
    enhancer.setCallbacks(callbackHelper.getCallbacks());
    SampleClass proxy = (SampleClass) enhancer.create();
    Assert.assertEquals("Hello cglib", proxy.test(null));
    Assert.assertNotEquals("Hello cglib",proxy.toString());
    System.out.println(proxy.hashCode());
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末愕秫,一起剝皮案震驚了整個(gè)濱河市慨菱,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌戴甩,老刑警劉巖符喝,帶你破解...
    沈念sama閱讀 219,427評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異甜孤,居然都是意外死亡协饲,警方通過查閱死者的電腦和手機(jī)畏腕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來茉稠,“玉大人描馅,你說我怎么就攤上這事《撸” “怎么了铭污?”我有些...
    開封第一講書人閱讀 165,747評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長膀篮。 經(jīng)常有香客問我嘹狞,道長,這世上最難降的妖魔是什么誓竿? 我笑而不...
    開封第一講書人閱讀 58,939評(píng)論 1 295
  • 正文 為了忘掉前任磅网,我火速辦了婚禮,結(jié)果婚禮上烤黍,老公的妹妹穿的比我還像新娘知市。我一直安慰自己,他們只是感情好速蕊,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評(píng)論 6 392
  • 文/花漫 我一把揭開白布嫂丙。 她就那樣靜靜地躺著,像睡著了一般规哲。 火紅的嫁衣襯著肌膚如雪跟啤。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,737評(píng)論 1 305
  • 那天唉锌,我揣著相機(jī)與錄音隅肥,去河邊找鬼。 笑死袄简,一個(gè)胖子當(dāng)著我的面吹牛腥放,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播绿语,決...
    沈念sama閱讀 40,448評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼秃症,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了吕粹?” 一聲冷哼從身側(cè)響起种柑,我...
    開封第一講書人閱讀 39,352評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎匹耕,沒想到半個(gè)月后聚请,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,834評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡稳其,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評(píng)論 3 338
  • 正文 我和宋清朗相戀三年驶赏,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了炸卑。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,133評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡煤傍,死狀恐怖矾兜,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情患久,我是刑警寧澤椅寺,帶...
    沈念sama閱讀 35,815評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站蒋失,受9級(jí)特大地震影響返帕,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜篙挽,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評(píng)論 3 331
  • 文/蒙蒙 一荆萤、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧铣卡,春花似錦链韭、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蝉仇,卻和暖如春旋讹,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背轿衔。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評(píng)論 1 272
  • 我被黑心中介騙來泰國打工沉迹, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人害驹。 一個(gè)月前我還...
    沈念sama閱讀 48,398評(píng)論 3 373
  • 正文 我出身青樓鞭呕,卻偏偏與公主長得像,于是被迫代替她去往敵國和親宛官。 傳聞我的和親對(duì)象是個(gè)殘疾皇子葫松,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評(píng)論 2 355

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