国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

Spring IOC 容器源碼分析 - 循環依賴的解決辦法

aikin / 1313人閱讀

摘要:實例化時,發現又依賴于。一些緩存的介紹在進行源碼分析前,我們先來看一組緩存的定義。可是看完源碼后,我們似乎仍然不知道這些源碼是如何解決循環依賴問題的。

1. 簡介

本文,我們來看一下 Spring 是如何解決循環依賴問題的。在本篇文章中,我會首先向大家介紹一下什么是循環依賴。然后,進入源碼分析階段。為了更好的說明 Spring 解決循環依賴的辦法,我將會從獲取 bean 的方法getBean(String)開始,把整個調用過程梳理一遍。梳理完后,再來詳細分析源碼。通過這幾步的講解,希望讓大家能夠弄懂什么是循環依賴,以及如何解循環依賴。

循環依賴相關的源碼本身不是很復雜,不過這里要先介紹大量的前置知識。不然這些源碼看起來很簡單,但讀起來可能卻也不知所云。那下面我們先來了解一下什么是循環依賴。

2. 背景知識 2.1 什么是循環依賴

所謂的循環依賴是指,A 依賴 B,B 又依賴 A,它們之間形成了循環依賴。或者是 A 依賴 B,B 依賴 C,C 又依賴 A。它們之間的依賴關系如下:

這里以兩個類直接相互依賴為例,他們的實現代碼可能如下:

public class BeanB {
    private BeanA beanA;
    // 省略 getter/setter
}

public class BeanA {
    private BeanB beanB;
}

配置信息如下:


    


    

IOC 容器在讀到上面的配置時,會按照順序,先去實例化 beanA。然后發現 beanA 依賴于 beanB,接在又去實例化 beanB。實例化 beanB 時,發現 beanB 又依賴于 beanA。如果容器不處理循環依賴的話,容器會無限執行上面的流程,直到內存溢出,程序崩潰。當然,Spring 是不會讓這種情況發生的。在容器再次發現 beanB 依賴于 beanA 時,容器會獲取 beanA 對象的一個早期的引用(early reference),并把這個早期引用注入到 beanB 中,讓 beanB 先完成實例化。beanB 完成實例化,beanA 就可以獲取到 beanB 的引用,beanA 隨之完成實例化。這里大家可能不知道“早期引用”是什么意思,這里先別著急,我會在下一章進行說明。

好了,本章先到這里,我們繼續往下看。

2.2 一些緩存的介紹

在進行源碼分析前,我們先來看一組緩存的定義。如下:

/** Cache of singleton objects: bean name --> bean instance */
private final Map singletonObjects = new ConcurrentHashMap(256);

/** Cache of singleton factories: bean name --> ObjectFactory */
private final Map> singletonFactories = new HashMap>(16);

/** Cache of early singleton objects: bean name --> bean instance */
private final Map earlySingletonObjects = new HashMap(16);

根據緩存變量上面的注釋,大家應該能大致了解他們的用途。我這里簡單說明一下吧:

緩存 用途
singletonObjects 用于存放完全初始化好的 bean,從該緩存中取出的 bean 可以直接使用
earlySingletonObjects 存放原始的 bean 對象(尚未填充屬性),用于解決循環依賴
singletonFactories 存放 bean 工廠對象,用于解決循環依賴

上一章提到了”早期引用“,所謂的”早期引用“是指向原始對象的引用。所謂的原始對象是指剛創建好的對象,但還未填充屬性。這樣講大家不知道大家聽明白了沒,不過沒聽明白也不要緊。簡單做個實驗就知道了,這里我們先定義一個對象 Room:

/** Room 包含了一些電器 */
public class Room {
    private String television;
    private String airConditioner;
    private String refrigerator;
    private String washer;
    // 省略 getter/setter
}

配置如下:


    
    
    
    

我們先看一下完全實例化好后的 bean 長什么樣的。如下:

從調試信息中可以看得出,Room 的每個成員變量都被賦上值了。然后我們再來看一下“原始的 bean 對象”長的是什么樣的,如下:

結果比較明顯了,所有字段都是 null。這里的 bean 和上面的 bean 指向的是同一個對象Room@1567,但現在這個對象所有字段都是 null,我們把這種對象成為原始的對象。形象點說,上面的 bean 對象是一個裝修好的房子,可以拎包入住了。而這里的 bean 對象還是個毛坯房,還要裝修一下(填充屬性)才行。

2.3 回顧獲取 bean 的過程

本節,我們來了解從 Spring IOC 容器中獲取 bean 實例的流程(簡化版),這對我們后續的源碼分析會有比較大的幫助。先看圖:

先來簡單介紹一下這張圖,這張圖是一個簡化后的流程圖。開始流程圖中只有一條執行路徑,在條件 sharedInstance != null 這里出現了岔路,形成了綠色和紅色兩條路徑。在上圖中,讀取/添加緩存的方法我用藍色的框和☆標注了出來。至于虛線的箭頭,和虛線框里的路徑,這個下面會說到。

我來按照上面的圖,分析一下整個流程的執行順序。這個流程從 getBean 方法開始,getBean 是個空殼方法,所有邏輯都在 doGetBean 方法中。doGetBean 首先會調用 getSingleton(beanName) 方法獲取 sharedInstance,sharedInstance 可能是完全實例化好的 bean,也可能是一個原始的 bean,當然也有可能是 null。如果不為 null,則走綠色的那條路徑。再經 getObjectForBeanInstance 這一步處理后,綠色的這條執行路徑就結束了。

我們再來看一下紅色的那條執行路徑,也就是 sharedInstance = null 的情況。在第一次獲取某個 bean 的時候,緩存中是沒有記錄的,所以這個時候要走創建邏輯。上圖中的 getSingleton(beanName,
new ObjectFactory() {...}) 方法會創建一個 bean 實例,上圖虛線路徑指的是 getSingleton 方法內部調用的兩個方法,其邏輯如下:

public Object getSingleton(String beanName, ObjectFactory singletonFactory) {
    // 省略部分代碼
    singletonObject = singletonFactory.getObject();
    // ...
    addSingleton(beanName, singletonObject);
}

如上所示,getSingleton 會在內部先調用 getObject 方法創建 singletonObject,然后再調用 addSingleton 將 singletonObject 放入緩存中。getObject 在內部代用了 createBean 方法,createBean 方法基本上也屬于空殼方法,更多的邏輯是寫在 doCreateBean 方法中的。doCreateBean 方法中的邏輯很多,其首先調用了 createBeanInstance 方法創建了一個原始的 bean 對象,隨后調用 addSingletonFactory 方法向緩存中添加單例 bean 工廠,從該工廠可以獲取原始對象的引用,也就是所謂的“早期引用”。再之后,繼續調用 populateBean 方法向原始 bean 對象中填充屬性,并解析依賴。getObject 執行完成后,會返回完全實例化好的 bean。緊接著再調用 addSingleton 把完全實例化好的 bean 對象放入緩存中。到這里,紅色執行路徑差不多也就要結束的。

我這里沒有把 getObject、addSingleton 方法和 getSingleton(String, ObjectFactory) 并列畫在紅色的路徑里,目的是想簡化一下方法的調用棧(都畫進來有點復雜)。我們可以進一步簡化上面的調用流程,比如下面:

這個流程看起來是不是簡單多了,命中緩存走綠色路徑,未命中走紅色的創建路徑。好了,本節先到這。

3. 源碼分析

好了,經過前面的鋪墊,現在我們終于可以深入源碼一探究竟了,想必大家已等不及了。那我不賣關子了,下面我們按照方法的調用順序,依次來看一下循環依賴相關的代碼。如下:

protected  T doGetBean(
            final String name, final Class requiredType, final Object[] args, boolean typeCheckOnly)
            throws BeansException {

    // ...... 
    
    // 從緩存中獲取 bean 實例
    Object sharedInstance = getSingleton(beanName);

    // ......
}

public Object getSingleton(String beanName) {
    return getSingleton(beanName, true);
}

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 從 singletonObjects 獲取實例,singletonObjects 中的實例都是準備好的 bean 實例,可以直接使用
    Object singletonObject = this.singletonObjects.get(beanName);
    // 判斷 beanName 對應的 bean 是否正在創建中
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            // 從 earlySingletonObjects 中獲取提前曝光的 bean
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null && allowEarlyReference) {
                // 獲取相應的 bean 工廠
                ObjectFactory singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    // 提前曝光 bean 實例(raw bean),用于解決循環依賴
                    singletonObject = singletonFactory.getObject();
                    
                    // 將 singletonObject 放入緩存中,并將 singletonFactory 從緩存中移除
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return (singletonObject != NULL_OBJECT ? singletonObject : null);
}

上面的源碼中,doGetBean 所調用的方法 getSingleton(String) 是一個空殼方法,其主要邏輯在 getSingleton(String, boolean) 中。該方法邏輯比較簡單,首先從 singletonObjects 緩存中獲取 bean 實例。若未命中,再去 earlySingletonObjects 緩存中獲取原始 bean 實例。如果仍未命中,則從 singletonFactory 緩存中獲取 ObjectFactory 對象,然后再調用 getObject 方法獲取原始 bean 實例的應用,也就是早期引用。獲取成功后,將該實例放入 earlySingletonObjects 緩存中,并將 ObjectFactory 對象從 singletonFactories 移除。看完這個方法,我們再來看看 getSingleton(String, ObjectFactory) 方法,這個方法也是在 doGetBean 中被調用的。這次我會把 doGetBean 的代碼多貼一點出來,如下:

protected  T doGetBean(
        final String name, final Class requiredType, final Object[] args, boolean typeCheckOnly)
        throws BeansException {

    // ...... 
    Object bean;

    // 從緩存中獲取 bean 實例
    Object sharedInstance = getSingleton(beanName);

    // 這里先忽略 args == null 這個條件
    if (sharedInstance != null && args == null) {
        // 進行后續的處理
        bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
    } else {
        // ......

        // mbd.isSingleton() 用于判斷 bean 是否是單例模式
        if (mbd.isSingleton()) {
            // 再次獲取 bean 實例
            sharedInstance = getSingleton(beanName, new ObjectFactory() {
                @Override
                public Object getObject() throws BeansException {
                    try {
                        // 創建 bean 實例,createBean 返回的 bean 是完全實例化好的
                        return createBean(beanName, mbd, args);
                    } catch (BeansException ex) {
                        destroySingleton(beanName);
                        throw ex;
                    }
                }
            });
            // 進行后續的處理
            bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
        }

        // ......
    }

    // ......

    // 返回 bean
    return (T) bean;
}

這里的代碼邏輯和我在 2.3 回顧獲取 bean 的過程 一節的最后貼的主流程圖已經很接近了,對照那張圖和代碼中的注釋,大家應該可以理解 doGetBean 方法了。繼續往下看:

public Object getSingleton(String beanName, ObjectFactory singletonFactory) {
    synchronized (this.singletonObjects) {

        // ......
        
        // 調用 getObject 方法創建 bean 實例
        singletonObject = singletonFactory.getObject();
        newSingleton = true;

        if (newSingleton) {
            // 添加 bean 到 singletonObjects 緩存中,并從其他集合中將 bean 相關記錄移除
            addSingleton(beanName, singletonObject);
        }

        // ......
        
        // 返回 singletonObject
        return (singletonObject != NULL_OBJECT ? singletonObject : null);
    }
}

protected void addSingleton(String beanName, Object singletonObject) {
    synchronized (this.singletonObjects) {
        // 將  映射存入 singletonObjects 中
        this.singletonObjects.put(beanName, (singletonObject != null ? singletonObject : NULL_OBJECT));

        // 從其他緩存中移除 beanName 相關映射
        this.singletonFactories.remove(beanName);
        this.earlySingletonObjects.remove(beanName);
        this.registeredSingletons.add(beanName);
    }
}

上面的代碼中包含兩步操作,第一步操作是調用 getObject 創建 bean 實例,第二步是調用 addSingleton 方法將創建好的 bean 放入緩存中。代碼邏輯并不復雜,相信大家都能看懂。那么接下來我們繼續往下看,這次分析的是 doCreateBean 中的一些邏輯。如下:

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args)
        throws BeanCreationException {

    BeanWrapper instanceWrapper = null;

    // ......

    // ☆ 創建 bean 對象,并將 bean 對象包裹在 BeanWrapper 對象中返回
    instanceWrapper = createBeanInstance(beanName, mbd, args);
    
    // 從 BeanWrapper 對象中獲取 bean 對象,這里的 bean 指向的是一個原始的對象
    final Object bean = (instanceWrapper != null ? instanceWrapper.getWrappedInstance() : null);

    /*
     * earlySingletonExposure 用于表示是否”提前暴露“原始對象的引用,用于解決循環依賴。
     * 對于單例 bean,該變量一般為 true。更詳細的解釋可以參考我之前的文章
     */ 
    boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
            isSingletonCurrentlyInCreation(beanName));
    if (earlySingletonExposure) {
        // ☆ 添加 bean 工廠對象到 singletonFactories 緩存中
        addSingletonFactory(beanName, new ObjectFactory() {
            @Override
            public Object getObject() throws BeansException {
                /* 
                 * 獲取原始對象的早期引用,在 getEarlyBeanReference 方法中,會執行 AOP 
                 * 相關邏輯。若 bean 未被 AOP 攔截,getEarlyBeanReference 原樣返回 
                 * bean,所以大家可以把 
                 *      return getEarlyBeanReference(beanName, mbd, bean) 
                 * 等價于:
                 *      return bean;
                 */
                return getEarlyBeanReference(beanName, mbd, bean);
            }
        });
    }

    Object exposedObject = bean;

    // ......
    
    // ☆ 填充屬性,解析依賴
    populateBean(beanName, mbd, instanceWrapper);

    // ......

    // 返回 bean 實例
    return exposedObject;
}

protected void addSingletonFactory(String beanName, ObjectFactory singletonFactory) {
    synchronized (this.singletonObjects) {
        if (!this.singletonObjects.containsKey(beanName)) {
            // 將 singletonFactory 添加到 singletonFactories 緩存中
            this.singletonFactories.put(beanName, singletonFactory);

            // 從其他緩存中移除相關記錄,即使沒有
            this.earlySingletonObjects.remove(beanName);
            this.registeredSingletons.add(beanName);
        }
    }
}

上面的代碼簡化了不少,不過看起來仍有點復雜。好在,上面代碼的主線邏輯比較簡單,由三個方法組成。如下:

1. 創建原始 bean 實例 → createBeanInstance(beanName, mbd, args)
2. 添加原始對象工廠對象到 singletonFactories 緩存中 
        → addSingletonFactory(beanName, new ObjectFactory{...})
3. 填充屬性,解析依賴 → populateBean(beanName, mbd, instanceWrapper)

到這里,本節涉及到的源碼就分析完了。可是看完源碼后,我們似乎仍然不知道這些源碼是如何解決循環依賴問題的。難道本篇文章就到這里了嗎?答案是否。下面我來解答這個問題,這里我還是以 BeanA 和 BeanB 兩個類相互依賴為例。在上面的方法調用中,有幾個關鍵的地方,下面一一列舉出來:

1. 創建原始 bean 對象

instanceWrapper = createBeanInstance(beanName, mbd, args);
final Object bean = (instanceWrapper != null ? instanceWrapper.getWrappedInstance() : null);

假設 beanA 先被創建,創建后的原始對象為 BeanA@1234,上面代碼中的 bean 變量指向就是這個對象。

2. 暴露早期引用

addSingletonFactory(beanName, new ObjectFactory() {
    @Override
    public Object getObject() throws BeansException {
        return getEarlyBeanReference(beanName, mbd, bean);
    }
});

beanA 指向的原始對象創建好后,就開始把指向原始對象的引用通過 ObjectFactory 暴露出去。getEarlyBeanReference 方法的第三個參數 bean 指向的正是 createBeanInstance 方法創建出原始 bean 對象 BeanA@1234。

3. 解析依賴

populateBean(beanName, mbd, instanceWrapper);

populateBean 用于向 beanA 這個原始對象中填充屬性,當它檢測到 beanA 依賴于 beanB 時,會首先去實例化 beanB。beanB 在此方法處也會解析自己的依賴,當它檢測到 beanA 這個依賴,于是調用 BeanFactry.getBean("beanA") 這個方法,從容器中獲取 beanA。

4. 獲取早期引用

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            // ☆ 從緩存中獲取早期引用
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null && allowEarlyReference) {
                ObjectFactory singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    // ☆ 從 SingletonFactory 中獲取早期引用
                    singletonObject = singletonFactory.getObject();
                    
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return (singletonObject != NULL_OBJECT ? singletonObject : null);
}

接著上面的步驟講,populateBean 調用 BeanFactry.getBean("beanA") 以獲取 beanB 的依賴。getBean("beanA") 會先調用 getSingleton("beanA"),嘗試從緩存中獲取 beanA。此時由于 beanA 還沒完全實例化好,于是 this.singletonObjects.get("beanA") 返回 null。接著 this.earlySingletonObjects.get("beanA") 也返回空,因為 beanA 早期引用還沒放入到這個緩存中。最后調用 singletonFactory.getObject() 返回 singletonObject,此時 singletonObject != null。singletonObject 指向 BeanA@1234,也就是 createBeanInstance 創建的原始對象。此時 beanB 獲取到了這個原始對象的引用,beanB 就能順利完成實例化。beanB 完成實例化后,beanA 就能獲取到 beanB 所指向的實例,beanA 隨之也完成了實例化工作。由于 beanB.beanA 和 beanA 指向的是同一個對象 BeanA@1234,所以 beanB 中的 beanA 此時也處于可用狀態了。

以上的過程對應下面的流程圖:

4. 總結

到這里,本篇文章差不多就快寫完了,不知道大家看懂了沒。這篇文章在前面做了大量的鋪墊,然后再進行源碼分析。相比于我之前寫的幾篇文章,本篇文章所對應的源碼難度上比之前簡單一些。但說實話也不好寫,我本來只想簡單介紹一下背景知識,然后直接進行源碼分析。但是又怕有的朋友看不懂,所以還是用了大篇幅介紹的背景知識。這樣寫,可能有的朋友覺得比較啰嗦。但是考慮到大家的水平不一,為了保證讓大家能夠更好的理解,所以還是盡量寫的詳細一點。本篇文章總的來說寫的還是有點累的,花了一些心思思考怎么安排章節順序,怎么簡化代碼和畫圖。如果大家看完這篇文章,覺得還不錯的話,不妨給個贊吧,也算是對我的鼓勵吧。

由于個人的技術能力有限,若文章有錯誤不妥之處,歡迎大家指出來。好了,本篇文章到此結束,謝謝大家的閱讀。

參考:

《Spring 源碼深度解析》- 郝佳

附錄:Spring 源碼分析文章列表 Ⅰ. IOC
更新時間 標題
2018-05-30 Spring IOC 容器源碼分析系列文章導讀
2018-06-01 Spring IOC 容器源碼分析 - 獲取單例 bean
2018-06-04 Spring IOC 容器源碼分析 - 創建單例 bean 的過程
2018-06-06 Spring IOC 容器源碼分析 - 創建原始 bean 對象
2018-06-08 Spring IOC 容器源碼分析 - 循環依賴的解決辦法
2018-06-11 Spring IOC 容器源碼分析 - 填充屬性到 bean 原始對象
2018-06-11 Spring IOC 容器源碼分析 - 余下的初始化工作
Ⅱ. AOP
更新時間 標題
2018-06-17 Spring AOP 源碼分析系列文章導讀
2018-06-20 Spring AOP 源碼分析 - 篩選合適的通知器
2018-06-20 Spring AOP 源碼分析 - 創建代理對象
2018-06-22 Spring AOP 源碼分析 - 攔截器鏈的執行過程
Ⅲ. MVC
更新時間 標題
2018-06-29 Spring MVC 原理探秘 - 一個請求的旅行過程
2018-06-30 Spring MVC 原理探秘 - 容器的創建過程
本文在知識共享許可協議 4.0 下發布,轉載需在明顯位置處注明出處
作者:coolblog.xyz
本文同步發布在我的個人博客:http://www.coolblog.xyz


本作品采用知識共享署名-非商業性使用-禁止演繹 4.0 國際許可協議進行許可。

文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。

轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/69693.html

相關文章

  • Spring IOC 容器源碼分析系列文章導讀

    摘要:本文是容器源碼分析系列文章的第一篇文章,將會著重介紹的一些使用方法和特性,為后續的源碼分析文章做鋪墊。我們可以通過這兩個別名獲取到這個實例,比如下面的測試代碼測試結果如下本小節,我們來了解一下這個特性。 1. 簡介 Spring 是一個輕量級的企業級應用開發框架,于 2004 年由 Rod Johnson 發布了 1.0 版本。經過十幾年的迭代,現在的 Spring 框架已經非常成熟了...

    NSFish 評論0 收藏0
  • Spring IOC 容器源碼分析 - 獲取單例 bean

    摘要:簡介為了寫容器源碼分析系列的文章,我特地寫了一篇容器的導讀文章。在做完必要的準備工作后,從本文開始,正式開始進入源碼分析的階段。從緩存中獲取單例。返回以上就是和兩個方法的分析。 1. 簡介 為了寫 Spring IOC 容器源碼分析系列的文章,我特地寫了一篇 Spring IOC 容器的導讀文章。在導讀一文中,我介紹了 Spring 的一些特性以及閱讀 Spring 源碼的一些建議。在...

    lufficc 評論0 收藏0
  • Spring IOC 容器源碼分析 - 創建單例 bean 過程

    摘要:關于創建實例的過程,我將會分幾篇文章進行分析。源碼分析創建實例的入口在正式分析方法前,我們先來看看方法是在哪里被調用的。時,表明方法不存在,此時拋出異常。該變量用于表示是否提前暴露單例,用于解決循環依賴。 1. 簡介 在上一篇文章中,我比較詳細的分析了獲取 bean 的方法,也就是getBean(String)的實現邏輯。對于已實例化好的單例 bean,getBean(String) ...

    mochixuan 評論0 收藏0
  • Spring IOC 容器源碼分析 - 余下初始化工作

    摘要:簡介本篇文章是容器源碼分析系列文章的最后一篇文章,本篇文章所分析的對象是方法,該方法用于對已完成屬性填充的做最后的初始化工作。后置處理器是拓展點之一,通過實現后置處理器接口,我們就可以插手的初始化過程。 1. 簡介 本篇文章是Spring IOC 容器源碼分析系列文章的最后一篇文章,本篇文章所分析的對象是 initializeBean 方法,該方法用于對已完成屬性填充的 bean 做最...

    Alfred 評論0 收藏0
  • Spring AOP 源碼分析系列文章導讀

    摘要:在寫完容器源碼分析系列文章中的最后一篇后,沒敢懈怠,趁熱打鐵,花了天時間閱讀了方面的源碼。從今天開始,我將對部分的源碼分析系列文章進行更新。全稱是,即面向切面的編程,是一種開發理念。在中,切面只是一個概念,并沒有一個具體的接口或類與此對應。 1. 簡介 前一段時間,我學習了 Spring IOC 容器方面的源碼,并寫了數篇文章對此進行講解。在寫完 Spring IOC 容器源碼分析系列...

    張春雷 評論0 收藏0

發表評論

0條評論

aikin

|高級講師

TA的文章

閱讀更多
最新活動
閱讀需要支付1元查看
<