動態(tài)代理

一制市、代理模式

代理模式會給某個對象提供一個代理對象些膨,并由代理對象控制原對象的引用湖笨。通俗的來講代理模式就是我們生活中常見的中介黄刚。代理模式一般會有三個角色:

代理模式結(jié)構(gòu)圖

抽象接口:代理角色和真實(shí)角色對外提供的公共方法黔漂,一般為一個接口诫尽。
真實(shí)角色:需要實(shí)現(xiàn)抽象接口,定義了真實(shí)角色所要實(shí)現(xiàn)的業(yè)務(wù)邏輯炬守,以便供代理角色調(diào)用牧嫉。真正的業(yè)務(wù)邏輯在此。
代理角色:需要實(shí)現(xiàn)抽象接口,是真實(shí)角色的代理酣藻,通過真實(shí)角色的業(yè)務(wù)邏輯方法來實(shí)現(xiàn)抽象方法曹洽,并可以附加自己的操作。將統(tǒng)一的流程控制都放到代理角色中處理辽剧。

一個簡單的例子就是送淆,業(yè)務(wù)中需要統(tǒng)計某些方法的執(zhí)行時間。由于這是和業(yè)務(wù)邏輯不相干的功能怕轿,我們不想把它放在真實(shí)角色中偷崩,而是通過代理角色在調(diào)用執(zhí)行真實(shí)角色方法的前后插入代碼來統(tǒng)計執(zhí)行時間。那么就可以把被統(tǒng)計的方法抽取到接口中:

public interface IFunction {
    void action();
}

public class RealSubject implements ITime {
    @Override
    public void action() {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("RealSubject action() execute!");
    }
}

public class ProxySubject implements IFunction {

    private IFunction iFunction;

    public ProxySubject(IFunction iTime) {
        this.iFunction = iTime;
    }

    @Override
    public void action() {
        // 計算時間的操作是對真實(shí)對象方法的增強(qiáng)
        long startTime = System.currentTimeMillis();
        // 實(shí)際執(zhí)行還是執(zhí)行真實(shí)對象的方法
        iFunction.action();
        long endTime = System.currentTimeMillis();
        System.out.println("Run time:" + (endTime - startTime));
    }
}

public class Test {

    public static void main(String[] args) {
        RealSubject realSubject = new RealSubject();
        ProxySubject proxySubject = new ProxySubject(realSubject);
        proxySubject.action();
    }
}

輸出結(jié)果為:

RealSubject action() execute!
Run time:500

可以看到代理起到隔離調(diào)用者與真實(shí)對象的作用撞羽,不會使二者產(chǎn)生直接聯(lián)系阐斜。

上邊這種實(shí)現(xiàn)模式是靜態(tài)代理,靜態(tài)代理類需要持有真實(shí)對象的引用诀紊,一個代理可以代理一個或多個被代理對象谒出。

二、靜態(tài)代理的弊端

一對一的問題是時靜態(tài)代理對象量多渡紫、代碼量大到推,從而導(dǎo)致代碼復(fù)雜,可維護(hù)性差惕澎;而一對多代理對象會出現(xiàn)擴(kuò)展能力差的問題(在一對多時會出現(xiàn)100個真實(shí)對象共用一個代理莉测,需要1個代理和100個真實(shí)對象同時產(chǎn)生關(guān)系,從而因?yàn)殛P(guān)系復(fù)雜而很難擴(kuò)展)唧喉。

比如消費(fèi)者 Consumer 想要買房捣卤,那么他就要去找房產(chǎn)中介 HouseAgent,二者可以抽象出 IHouse 接口:

public interface IHouse {
    void buyHouse();
}

public class Consumer implements IHouse {
    @Override
    public void buyHouse() {
        System.out.println("Consumer buyHouse()");
    }
}

public class HouseAgent implements IHouse {

    private IHouse iHouse;

    public HouseAgent(IHouse iHouse) {
        this.iHouse = iHouse;
    }

    @Override
    public void buyHouse() {
        before();
        iHouse.buyHouse();
        after();
    }

    private void before() {
        System.out.println("Before buying house");
    }

    private void after() {
        System.out.println("After buying house");
    }
}

public class Test {

    public static void main(String[] args) {
        HouseAgent houseAgent = new HouseAgent(new Consumer());
        houseAgent.buyHouse();
    }
}

接下來 Consumer 又想買車八孝,那么他就又要去找一個買車的代理 CarAgent(根據(jù)軟件設(shè)計的單一職責(zé)原則和開閉原則董朝,不應(yīng)該在 IHouse 接口中增加買車的方法 buyCar(),而是新增 ICar 接口)干跛,這樣的話就新增如下代碼:

public interface ICar {
    void buyCar();
}

public class CarAgent implements ICar {

    private ICar iCar;

    public CarAgent(ICar iCar) {
        this.iCar = iCar;
    }

    @Override
    public void buyCar() {
        before();
        iCar.buyCar();
        after();
    }

    private void before() {
        System.out.println("Before buying car");
    }

    private void after() {
        System.out.println("After buying car");
    }
}


public class Consumer implements IHouse, ICar {
    @Override
    public void buyHouse() {
        System.out.println("Consumer buyHouse()");
    }

    @Override
    public void buyCar() {
        System.out.println("Consumer buyCar()");
    }
}

public class Test {

    public static void main(String[] args) {

        Consumer consumer = new Consumer();

        HouseAgent houseAgent = new HouseAgent(consumer);
        houseAgent.buyHouse();

        CarAgent carAgent = new CarAgent(consumer);
        carAgent.buyCar();
    }
}

在一個代理只維護(hù)一個接口的情況下子姜,每新增一個接口就要增加一個代理類,接口數(shù)量增多時也會造成代理類數(shù)量增多楼入。

三哥捕、JDK 動態(tài)代理

JDK 動態(tài)代理可以有效的彌補(bǔ)靜態(tài)代理的不足,可以通過一個代理類實(shí)現(xiàn)全部的代理功能嘉熊,但是 JDK 動態(tài)代理只能代理接口遥赚,不能代理一個類。

動態(tài)代理使用方式為:

public class Test {

    public static void main(String[] args) {

        final Consumer consumer = new Consumer();

        // 生成動態(tài)代理對象
        Object object = Proxy.newProxyInstance(Test.class.getClassLoader(), new Class[]{IHouse.class, ICar.class},
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object object, Method method, Object[] args) throws Throwable {
                        return method.invoke(consumer, args);
                    }
                });

        // 代理對象轉(zhuǎn)換成真實(shí)對象實(shí)例并執(zhí)行業(yè)務(wù)方法
        IHouse iHouse = (IHouse) object;
        iHouse.buyHouse();
    }
}

使用 Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler invocationHandler) 可以創(chuàng)建一個動態(tài)代理對象阐肤,需要傳入三個參數(shù):

  1. loader:加載生成的代理類到 JVM 中的 ClassLoader凫佛。
  2. interfaces:動態(tài)代理類要代理(實(shí)現(xiàn))的接口讲坎。
  3. invocationHandler:一個接口對象,動態(tài)代理對象在執(zhí)行方法時愧薛,會回調(diào)到接口中唯一的方法 invoke()晨炕。

invoke(Object object, Method method, Object[] args) 有三個參數(shù),它們的含義分別是:

  1. object:動態(tài)代理對象厚满,其實(shí)就是通過 Proxy.newProxyInstance() 生成的那個代理對象府瞄。
  2. method:動態(tài)代理調(diào)用的方法對象碧磅。
  3. args:method 方法的參數(shù)碘箍。

使用動態(tài)代理時注意不要犯這種錯誤:

    public static void main(String[] args) {

        Object object = Proxy.newProxyInstance(Test.class.getClassLoader(), new Class[]{IHouse.class, ICar.class},
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object object, Method method, Object[] args) throws Throwable {
                        System.out.println(object);
                        return null;
                    }
                });

        IHouse iHouse = (IHouse) object;
        iHouse.buyHouse();
    }

這樣會造成堆溢出。原因是動態(tài)代理對象執(zhí)行了方法之后回調(diào) invoke()鲸郊,在 invoke() 中輸出時相當(dāng)于調(diào)用了 object.toString() 方法丰榴,因?yàn)橹灰{(diào)用動態(tài)代理的方法就會先回調(diào)到 invoke(),這樣就形成了死循環(huán)似的調(diào)用秆撮,造成堆溢出四濒。

再來看下 Proxy 的源碼:

    public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
    {
        Objects.requireNonNull(h);

        final Class<?>[] intfs = interfaces.clone();
        
        Class<?> cl = getProxyClass0(loader, intfs);

        try {

            final Constructor<?> cons = cl.getConstructor(constructorParams);
            final InvocationHandler ih = h;
            if (!Modifier.isPublic(cl.getModifiers())) {
                cons.setAccessible(true);
            }
            return cons.newInstance(new Object[]{h});
        } // catch ....
    }
    
    private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
        proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());
    
    private static Class<?> getProxyClass0(ClassLoader loader,
                                           Class<?>... interfaces) {
        if (interfaces.length > 65535) {
            throw new IllegalArgumentException("interface limit exceeded");
        }

        // If the proxy class defined by the given loader implementing
        // the given interfaces exists, this will simply return the cached copy;
        // otherwise, it will create the proxy class via the ProxyClassFactory
        return proxyClassCache.get(loader, interfaces);
    }

先克隆一份 interfaces,如果 <loader,interfaces> 所對應(yīng)的 Class 對象 已經(jīng)被緩存职辨,那么就拿緩存數(shù)據(jù)的拷貝盗蟆,否則通過 ProxyClassFactory 創(chuàng)建出一個 Class。拿到 Class 對象后舒裤,通過構(gòu)造方法和傳進(jìn)來的 InvocationHandler 創(chuàng)建出動態(tài)代理對象喳资。

那么動態(tài)代理的 Class 是如何生成的呢?它與一般的 Class 生成方式不同:

  • 一般的 Class 都是由 Java 源文件經(jīng)過編譯先在硬盤上生成字節(jié)碼 .class 文件腾供,再通過 ClassLoader 進(jìn)行類加載仆邓,加載進(jìn)內(nèi)存稱為 Class 對象。
  • 動態(tài)代理的 Class 不會在硬盤上生成伴鳖,而是直接在內(nèi)存中產(chǎn)生节值。

生成動態(tài)代理 Class 類及其對象的正是前面提到過的,Proxy 的靜態(tài)內(nèi)部類 ProxyClassFactory榜聂,其代碼如下:

    /**
     * A factory function that generates, defines and returns the proxy class given
     * the ClassLoader and array of interfaces.
     */
    private static final class ProxyClassFactory
        implements BiFunction<ClassLoader, Class<?>[], Class<?>> {
        
        // prefix for all proxy class names
        private static final String proxyClassNamePrefix = "$Proxy";

        // next number to use for generation of unique proxy class names
        private static final AtomicLong nextUniqueNumber = new AtomicLong();
        
        @Override
        public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {

            Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
            for (Class<?> intf : interfaces) {
                /*
                 * Verify that the class loader resolves the name of this
                 * interface to the same Class object.
                 */
                Class<?> interfaceClass = null;
                try {
                    interfaceClass = Class.forName(intf.getName(), false, loader);
                } catch (ClassNotFoundException e) {
                }
                if (interfaceClass != intf) {
                    throw new IllegalArgumentException(
                        intf + " is not visible from class loader");
                }
                /*
                 * Verify that the Class object actually represents an
                 * interface.
                 */
                if (!interfaceClass.isInterface()) {
                    throw new IllegalArgumentException(
                        interfaceClass.getName() + " is not an interface");
                }
                /*
                 * Verify that this interface is not a duplicate.
                 */
                if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
                    throw new IllegalArgumentException(
                        "repeated interface: " + interfaceClass.getName());
                }
            }

            String proxyPkg = null;     // package to define proxy class in
            int accessFlags = Modifier.PUBLIC | Modifier.FINAL;

            /*
             * Record the package of a non-public proxy interface so that the
             * proxy class will be defined in the same package.  Verify that
             * all non-public proxy interfaces are in the same package.
             */
            for (Class<?> intf : interfaces) {
                int flags = intf.getModifiers();
                if (!Modifier.isPublic(flags)) {
                    accessFlags = Modifier.FINAL;
                    String name = intf.getName();
                    int n = name.lastIndexOf('.');
                    String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
                    if (proxyPkg == null) {
                        proxyPkg = pkg;
                    } else if (!pkg.equals(proxyPkg)) {
                        throw new IllegalArgumentException(
                            "non-public interfaces from different packages");
                    }
                }
            }

            if (proxyPkg == null) {
                // if no non-public proxy interfaces, use the default package.
                proxyPkg = "";
            }

            {
                // Android-changed: Generate the proxy directly instead of calling
                // through to ProxyGenerator.
                List<Method> methods = getMethods(interfaces);
                Collections.sort(methods, ORDER_BY_SIGNATURE_AND_SUBTYPE);
                validateReturnTypes(methods);
                List<Class<?>[]> exceptions = deduplicateAndGetExceptions(methods);

                Method[] methodsArray = methods.toArray(new Method[methods.size()]);
                Class<?>[][] exceptionsArray = exceptions.toArray(new Class<?>[exceptions.size()][]);

                /*
                 * Choose a name for the proxy class to generate.
                 */
                long num = nextUniqueNumber.getAndIncrement();
                String proxyName = proxyPkg + proxyClassNamePrefix + num;

                return generateProxy(proxyName, interfaces, loader, methodsArray,
                                     exceptionsArray);
            }
        }
    }

ProxyClassFactory 實(shí)現(xiàn)了函數(shù)式接口 BiFunction<T, U, R>搞疗,其中泛型 T,U 是其接口方法 apply(T,U) 的參數(shù)類型须肆,R 是 apply(T,U) 的返回值類型匿乃。在這里其實(shí)就是傳入 ClassLoader 和接口的 Class 數(shù)組,得到動態(tài)代理的 Class 對象休吠。

ProxyClassFactory 類中定義的兩個常量 proxyClassNamePrefix 和 nextUniqueNumber 是用來定義生成的動態(tài)代理類文件的名稱的扳埂,從倒數(shù)第二行代碼來看,命名規(guī)則為:代理所在的包名 + proxyClassNamePrefix + nextUniqueNumber瘤礁,即形如 com.frank.agent$0阳懂。

apply() 方法內(nèi),先對我們傳入的代理接口進(jìn)行檢查。先通過接口名和 loader 反射拿到 interfaceClass 對象岩调,并檢查這個 interfaceClass 對象是否和提供名字的接口 intf 相同巷燥;再看 interfaceClass 是否是一個接口;最后檢查 interfaceClass 是否已經(jīng)被添加到 interfaceSet 中了(interfaceSet 是一個 IdentityHashMap号枕,即不以 equals() 而是以 == 的結(jié)果作為比較標(biāo)準(zhǔn))缰揪。

接口篩選完畢后,開始構(gòu)造要生成的動態(tài)代理類的包名葱淳,這個包名由 interfaces 中接口的訪問控制符決定:

  • 如果所有接口都由 public 修飾钝腺,那么就使用默認(rèn)的包名"";
  • 如果存在非 public 屬性的接口赞厕,那么所有非 public 的接口包名必須相同艳狐,動態(tài)代理類也就使用這個包名。

最后就是生成代理類了皿桑。API 29 不再使用 ProxyGenerator 而是直接生成毫目。先獲取到所有接口中的所有方法,然后再收集所有方法聲明的異常诲侮,把它們轉(zhuǎn)換成數(shù)組傳遞給最終生成動態(tài)代理 Class 對象的 generateProxy()镀虐。

我們?nèi)匀豢梢允褂?ProxyGenerator 生成動態(tài)代理的 .class 文件:

    private static void proxy() {
        String name = IHouse.class.getName() + "$Proxy0";
        byte[] bytes = ProxyGenerator.generateProxyClass(name, new Class[]{IHouse.class});
        File file = new File(name+".class");

        try {
            if (!file.exists() && file.createNewFile()) {
                FileOutputStream fos = new FileOutputStream(file);
                fos.write(bytes);
                fos.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

這樣就會生成一個名字為 com.frank.agent2.IHouse$Proxy0.class 的 .class 文件:

public final class IHouse$Proxy0 extends Proxy implements IHouse {
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m0;

    public IHouse$Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final void buyHouse() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m3 = Class.forName("com.frank.agent2.IHouse").getMethod("buyHouse");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

動態(tài)代理類 IHouse$Proxy0 繼承 Proxy 實(shí)現(xiàn)了 IHouse 接口,有4個 Method 類型成員沟绪,從 m0~m3 依次對應(yīng) hashCode()刮便、equals()、toString() 和 IHouse 接口中的 buyHouse()近零。

當(dāng)動態(tài)代理對象調(diào)用 buyHouse() 時诺核,會執(zhí)行到 super.h.invoke(),就是調(diào)用創(chuàng)建動態(tài)代理對象時創(chuàng)建的那個 InvocationHandler 的 invoke()久信。invoke() 的參數(shù)依次傳遞的是動態(tài)代理對象窖杀、動態(tài)代理對象調(diào)用方法的 Method 對象以及調(diào)用方法的參數(shù)(方法沒參數(shù)就傳 null)。這與前邊介紹的 invoke() 方法參數(shù)含義是一致的裙士。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末入客,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子腿椎,更是在濱河造成了極大的恐慌桌硫,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件啃炸,死亡現(xiàn)場離奇詭異铆隘,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)南用,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進(jìn)店門膀钠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來掏湾,“玉大人,你說我怎么就攤上這事肿嘲∪诨鳎” “怎么了?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵雳窟,是天一觀的道長尊浪。 經(jīng)常有香客問我,道長封救,這世上最難降的妖魔是什么拇涤? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮兴泥,結(jié)果婚禮上工育,老公的妹妹穿的比我還像新娘虾宇。我一直安慰自己搓彻,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布嘱朽。 她就那樣靜靜地躺著旭贬,像睡著了一般。 火紅的嫁衣襯著肌膚如雪搪泳。 梳的紋絲不亂的頭發(fā)上稀轨,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天,我揣著相機(jī)與錄音岸军,去河邊找鬼奋刽。 笑死,一個胖子當(dāng)著我的面吹牛艰赞,可吹牛的內(nèi)容都是我干的佣谐。 我是一名探鬼主播,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼方妖,長吁一口氣:“原來是場噩夢啊……” “哼狭魂!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起党觅,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤雌澄,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后杯瞻,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體镐牺,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年魁莉,在試婚紗的時候發(fā)現(xiàn)自己被綠了睬涧。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片卒废。...
    茶點(diǎn)故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖宙地,靈堂內(nèi)的尸體忽然破棺而出摔认,到底是詐尸還是另有隱情,我是刑警寧澤宅粥,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布参袱,位于F島的核電站,受9級特大地震影響秽梅,放射性物質(zhì)發(fā)生泄漏抹蚀。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一企垦、第九天 我趴在偏房一處隱蔽的房頂上張望环壤。 院中可真熱鬧,春花似錦钞诡、人聲如沸郑现。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽接箫。三九已至,卻和暖如春朵诫,著一層夾襖步出監(jiān)牢的瞬間辛友,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工剪返, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留废累,地道東北人。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓脱盲,卻偏偏與公主長得像邑滨,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子宾毒,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,435評論 2 359

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