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

資訊專欄INFORMATION COLUMN

仿照 Spring 實現簡單的 IOC 和 AOP - 下篇

AlexTuan / 2003人閱讀

摘要:在上文中,我實現了一個很簡單的和容器。比如,我們所熟悉的就是在這里將切面邏輯織入相關中的。初始化的工作算是結束了,此時處于就緒狀態,等待外部程序的調用。其中動態代理只能代理實現了接口的對象,而動態代理則無此限制。

1. 背景

本文承接上文,來繼續說說 IOC 和 AOP 的仿寫。在上文中,我實現了一個很簡單的 IOC 和 AOP 容器。上文實現的 IOC 和 AOP 功能很單一,且 IOC 和 AOP 兩個模塊沒有整合到一起。IOC 在加載 bean 過程中,AOP 不能對 bean 織入通知。在本文中,我們詳細說一下升級版 IOC 和 AOP。這個版本的實現包含了在上篇中所說的功能,這里再重述一下,如下:

根據 xml 配置文件加載相關 bean

對 BeanPostProcessor 類型的 bean 提供支持

對 BeanFactoryAware 類型的 bean 提供支持

實現了基于 JDK 動態代理的 AOP

整合了 IOC 和 AOP,使得二者可很好的協同工作

上面羅列了5個功能點,雖然看起來不多,但是對于新手來說,實現起來還是不很容易的。所以接下來,我將圍繞上面的功能點展開詳細的描述。如果大家有興趣,我還是很建議大家跟著寫一遍,因為很多時候能看懂,但是寫的卻不一定能寫出來。仿寫一遍能夠加深對 Spring IOC 和 AOP 原理的理解,多動手是有好處的。

另外需要說明的是,黃億華前輩實現的 tiny-spring 項目時間節點是 2014.1,當時應該是參照 Spring 3.x 版本編寫的。部分類的設計思想可能會與現在最新穩定版 4.3.10 有一定的出入,由于我暫時沒有閱讀 Spring 源碼的計劃,所以這里不能告知大家 tiny-spring 哪些類與 Spring 最新的源碼有出入,見諒。

好了,本章內容先介紹到這,接下來進入正文。

2. IOC 的實現 2.1 BeanFactory 的生命流程

上面簡述了 toy-spring 項目的編碼背景,接下來,在本節中,我將向大家介紹 toy-spring 項目中 IOC 部分的實現原理。在詳細介紹 IOC 的實現原理前,這里先簡單說一下 BeanFactory 的生命流程:

BeanFactory 加載 Bean 配置文件,將讀到的 Bean 配置封裝成 BeanDefinition 對象

將封裝好的 BeanDefinition 對象注冊到 BeanDefinition 容器中

注冊 BeanPostProcessor 相關實現類到 BeanPostProcessor 容器中

BeanFactory 進入就緒狀態

外部調用 BeanFactory 的 getBean(String name) 方法,BeanFactory 著手實例化相應的 bean

重復步驟 3 和 4,直至程序退出,BeanFactory 被銷毀

上面簡單羅列了 BeanFactory 的生命流程,也就是 IOC 容器的生命流程。接下來就來圍繞上面的流程展開討論。

2.2 BeanDefinition 及其他一些類的介紹

在詳細介紹 IOC 容器的工作原理前,這里先介紹一下實現 IOC 所用到的一些輔助類,包括BeanDefinition、BeanReference、PropertyValues、PropertyValue。這些類與接下來的 2.3 節 xml 的解析緊密相關。按照順序,先從 BeanDefinition 開始介紹。

BeanDefinition,從字面意思上翻譯成中文就是 “Bean 的定義”。從翻譯結果中就可以猜出這個類的用途,即根據 Bean 配置信息生成相應的 Bean 詳情對象。舉個例子,如果把 Bean 比作是電腦 ?,那么 BeanDefinition 就是這臺電腦的配置清單。我們從外觀上無法看出這臺電腦里面都有哪些配置,也看不出電腦的性能咋樣。但是通過配置清單,我們就可了解這臺電腦的詳細配置。我們可以知道這臺電腦是不是用了牙膏廠的 CPU,BOOM 廠的固態硬盤等。透過配置清單,我們也就可大致評估出這臺電腦的性能。

圖1 電腦和配置清單

上面那個例子還是比較貼切的,但是只是個例子,和實際還是有差距的。那么在具體實現中,BeanDefinition 和 xml 是怎么對應的呢?答案在下面:

圖2 根據 bean 配置生成 BeanDefinition

看完上圖,我想大家對 BeanDefinition 的用途有了更進一步的認識。接下來我們來說說上圖中的 ref 對應的 BeanReference 對象。BeanReference 對象保存的是 bean 配置中 ref 屬性對應的值,在后續 BeanFactory 實例化 bean 時,會根據 BeanReference 保存的值去實例化 bean 所依賴的其他 bean。

接下來說說 PropertyValues 和 PropertyValue 這兩個長的比較像的類,首先是PropertyValue。PropertyValue 中有兩個字段 name 和 value,用于記錄 bean 配置中的 標簽的屬性值。然后是PropertyValues,PropertyValues 從字面意思上來看,是 PropertyValue 復數形式,在功能上等同于 List。那么為什么 Spring 不直接使用 List,而自己定義一個新類呢?答案是要獲得一定的控制權,看下面的代碼:

public class PropertyValues {

    private final List propertyValueList = new ArrayList();

    public void addPropertyValue(PropertyValue pv) {
        // 在這里可以對參數值 pv 做一些處理,如果直接使用 List,則就不行了
        this.propertyValueList.add(pv);
    }

    public List getPropertyValues() {
        return this.propertyValueList;
    }

}

好了,輔助類介紹完了,接下來我們繼續 BeanFactory 的生命流程探索。

2.3 xml 的解析

BeanFactory 初始化時,會根據傳入的 xml 配置文件路徑加載并解析配置文件。但是加載和解析 xml 配置文件這種臟活累活,BeanFactory 可不太愿意干,它只想高冷的管理容器中的 bean。于是 BeanFactory 將加載和解析配置文件的任務委托給專職人員 BeanDefinitionReader 的實現類 XmlBeanDefinitionReader 去做。那么 XmlBeanDefinitionReader 具體是怎么做的呢?XmlBeanDefinitionReader 做了如下幾件事情:

將 xml 配置文件加載到內存中

獲取根標簽 下所有的 標簽

遍歷獲取到的 標簽列表,并從標簽中讀取 id,class 屬性

創建 BeanDefinition 對象,并將剛剛讀取到的 id,class 屬性值保存到對象中

遍歷 標簽下的 標簽,從中讀取屬性值,并保持在 BeanDefinition 對象中

鍵值對緩存在 Map 中,留作后用

重復3、4、5、6步,直至解析結束

上面的解析步驟并不復雜,實現起來也不難,就是解析 xml 而已,這里就不過多敘述了。

2.4 注冊 BeanPostProcessor

BeanPostProcessor 接口是 Spring 對外拓展的接口之一,其主要用途提供一個機會,讓開發人員能夠插手 bean 的實例化過程。通過實現這個接口,我們就可在 bean 實例化時,對bean 進行一些處理。比如,我們所熟悉的 AOP 就是在這里將切面邏輯織入相關 bean 中的。正是因為有了 BeanPostProcessor 接口作為橋梁,才使得 AOP 可以和 IOC 容器產生聯系。關于這一點,我將會在后續章節詳細說明。

接下來說說 BeanFactory 是怎么注冊 BeanPostProcessor 相關實現類的。

XmlBeanDefinitionReader 在完成解析工作后,BeanFactory 會將它解析得到的 鍵值對注冊到自己的 beanDefinitionMap 中。BeanFactory 注冊好 BeanDefinition 后,就立即開始注冊 BeanPostProcessor 相關實現類。這個過程比較簡單:

根據 BeanDefinition 記錄的信息,尋找所有實現了 BeanPostProcessor 接口的類。

實例化 BeanPostProcessor 接口的實現類

將實例化好的對象放入 List

重復2、3步,直至所有的實現類完成注冊

上面簡述了 BeanPostProcessor 接口的用途以及注冊的過程。BeanPostProcessor 是一個比較常用接口,相信大家都很熟悉了,這里就不過多敘述了。

2.5 getBean 過程解析

在完成了 xml 的解析、BeanDefinition 的注冊以及 BeanPostProcessor 的注冊過程后。BeanFactory 初始化的工作算是結束了,此時 BeanFactory 處于就緒狀態,等待外部程序的調用。

外部程序一般都是通過調用 BeanFactory 的 getBean(String name) 方法來獲取容器中的 bean。BeanFactory 具有延遲實例化 bean 的特性,也就是等外部程序需要的時候,才實例化相關的 bean。這樣做的好處是比較顯而易見的,第一是提高了 BeanFactory 的初始化速度,第二是節省了內存資源。下面我們就來詳細說說 bean 的實例化過程:

圖3 Spring bean實例化過程

上圖是一個完整的 Spring bean 實例化過程圖。在我的仿寫項目中,沒有做的這么復雜,簡化了 bean 實例化的過程,如下:

圖4 toy-spring bean實例化過程

接下來我將按照簡化后的 bean 實例化過程介紹,如果想了解完整的 bean 實例化過程,可以參考我的另一篇文章:Spring bean的生命流程。簡化后的實例化流程如下:

實例化 bean 對象,類似于 new XXObject()

將配置文件中配置的屬性填充到剛剛創建的 bean 對象中

檢查 bean 對象是否實現了 Aware 一類的接口,如果實現了則把相應的依賴設置到 bean 對象中。toy-spring 目前僅對 BeanFactoryAware 接口實現類提供了支持

調用 BeanPostProcessor 前置處理方法,即 postProcessBeforeInitialization(Object bean, String beanName)

調用 BeanPostProcessor 后置處理方法,即 postProcessAfterInitialization(Object bean, String beanName)

bean 對象處于就緒狀態,可以使用了

上面 6 步流程并不復雜,源碼實現的也較為簡單,這里就不在貼代碼說明了。大家如果想了解細節,可以去 github 上下載 toy-spring 源碼閱讀。

3. 實現 AOP 3.1 AOP 原理

AOP 是基于動態代理模式實現的,具體實現上可以基于 JDK 動態代理或者 Cglib 動態代理。其中 JDK 動態代理只能代理實現了接口的對象,而 Cglib 動態代理則無此限制。所以在為沒有實現接口的對象生成代理時,只能使用 Cglib。在 toy-spring 項目中,暫時只實現了基于 JDK 動態代理的代理對象生成器。

關于 AOP 原理這里就不多說了,下面說說 toy-spring 中 AOP 的實現步驟。還是像上面一樣,先列流程:

AOP 邏輯介入 BeanFactory 實例化 bean 的過程

根據 Pointcut 定義的匹配規則,判斷當前正在實例化的 bean 是否符合規則

如果符合,代理生成器將切面邏輯 Advice 織入 bean 相關方法中,并為目標 bean 生成代理對象

將生成的 bean 的代理對象返回給 BeanFactory 容器,到此,AOP 邏輯執行結束

對于上面的4步流程,熟悉 Spring AOP 的朋友應該能很容易理解。如果有朋友不理解也沒關系,在后續章節,我會詳細介紹相關流程的具體實現。

3.2 基于 JDK 動態代理的 AOP 實現

本節說說基于 JDK 動態代理的代理對象生成器具體實現。在 toy-spring 項目中,代理對象生成器的邏輯主要寫在了 JdkDynamicAopProxy 類中,這個類的有兩個方法,其中 getProxy 方法用于生成代理對象。invoke 方法是 InvocationHandler 接口的具體實現,包含了將通知(Advice)織入相關方法中,是3.1節所列流程中第3步流程的具體實現。好了,接下來,對著源碼講解 JdkDynamicAopProxy:

JdkDynamicAopProxy 實現代碼:

public abstract class AbstractAopProxy implements AopProxy {

    protected AdvisedSupport advised;

    public AbstractAopProxy(AdvisedSupport advised) {
        this.advised = advised;
    }
}

/**
 * 基于 JDK 動態代理的代理對象生成器
 * Created by code4wt on 17/8/16.
 */
final public class JdkDynamicAopProxy extends AbstractAopProxy implements InvocationHandler {

    public JdkDynamicAopProxy(AdvisedSupport advised) {
        super(advised);
    }

    /**
     * 為目標 bean 生成代理對象
     * @return bean 的代理對象
     */
    @Override
    public Object getProxy() {
        return Proxy.newProxyInstance(getClass().getClassLoader(), advised.getTargetSource().getInterfaces(), this);
    }

    /**
     * InvocationHandler 接口中的 invoke 方法具體實現,封裝了具體的代理邏輯
     * @param proxy
     * @param method
     * @param args
     * @return 代理方法或原方法的返回值
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        MethodMatcher methodMatcher = advised.getMethodMatcher();
        
        // 1. 使用方法匹配器 methodMatcher 測試 bean 中原始方法 method 是否符合匹配規則
        if (methodMatcher != null && methodMatcher.matchers(method, advised.getTargetSource().getTargetClass())) {
            
            // 獲取 Advice。MethodInterceptor 的父接口繼承了 Advice
            MethodInterceptor methodInterceptor = advised.getMethodInterceptor();
            
           /* 
            * 2. 將 bean 的原始方法 method 封裝在 MethodInvocation 接口實現類對象中,
            * 并把生成的對象作為參數傳給 Adivce 實現類對象,執行通知邏輯
            */ 
            return methodInterceptor.invoke(
                    new ReflectiveMethodInvocation(advised.getTargetSource().getTarget(), method, args));
        } else {
            // 2. 當前 method 不符合匹配規則,直接調用 bean 的原始方法 method
            return method.invoke(advised.getTargetSource().getTarget(), args);
        }
    }
}

上面貼的代碼,已經對 JdkDynamicAopProxy 實現代碼進行了逐行介解釋,這里不再多說。下面用個流程圖對通知織入邏輯進行總結:


圖5 toy-spring AOP 通知織入流程圖

最后對 JdkDynamicAopProxy 進行簡單的測試,測試代碼及結果如下

測試類:

public class LogInterceptor implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println(invocation.getMethod().getName() + " method start");
        Object obj= invocation.proceed();
        System.out.println(invocation.getMethod().getName() + " method end");
        return obj;
    }
}

public class JdkDynamicAopProxyTest {

    @Test
    public void getProxy() throws Exception {
        System.out.println("---------- no proxy ----------");
        HelloService helloService = new HelloServiceImpl();
        helloService.sayHelloWorld();

        System.out.println("
----------- proxy -----------");
        AdvisedSupport advisedSupport = new AdvisedSupport();
        advisedSupport.setMethodInterceptor(new LogInterceptor());

        TargetSource targetSource = new TargetSource(
                helloService, HelloServiceImpl.class, HelloServiceImpl.class.getInterfaces());
        advisedSupport.setTargetSource(targetSource);
        advisedSupport.setMethodMatcher((Method method, Class beanClass) -> true);

        helloService = (HelloService) new JdkDynamicAopProxy(advisedSupport).getProxy();
        helloService.sayHelloWorld();
    }
}

測試結果:

為了控制文章篇幅,上面代碼中用到的其他輔助類,這里就不貼出來了,想看的朋友可以到 github 上下載源碼。

3.3 AOP 與 IOC 協作

上一節介紹了3.1節所列流程中第3步流程的具體實現,這一節則會介紹1、2、4步流程的具體實現。在介紹之前,還要再次提一下 BeanPostProcessor接口。在之前2.4節 注冊 BeanPostProcessor 中我已經介紹過 BeanPostProcessor 的作用,也說到了 AOP 是通過 BeanPostProcessor 接口與 IOC 產生聯系的。不過2.4節,只是蜻蜓點水提了一下,沒有詳細展開說明。在本節中,我將詳細講解 toy-spring 項目中 AOP 和 IOC 是怎樣被整合到一起的。

Spring 從2.0版本開始集成 AspectJ,通過集成 AspectJ,也使得 Spring AOP 的功能得到了很大的增強。我們在平時開發中,很多時候是使用基于 AspectJ 表達式及其他配置來實現切面功能。所以我在編寫 toy-spring 項目時,也在項目中簡單集成了 AspectJ。通過集成 AspectJ,使得 toy-spring AOP 可以基于 AspectJ 表達式完成復雜的匹配邏輯。接下來就讓我們看看袖珍版 Spring AOP 是怎樣實現的吧。

在 toy-spring 中,AOP 和 IOC 產生聯系的具體實現類是 AspectJAwareAdvisorAutoProxyCreator(下面簡稱 AutoProxyCreator),這個類實現了 BeanPostProcessor 和 BeanFactoryAware 接口。BeanFactory 在注冊 BeanPostProcessor 接口相關實現類的階段,會將其本身注入到 AutoProxyCreator 中,為后面 AOP 給 bean 生成代理對象做準備。BeanFactory 初始化結束后,AOP 與 IOC 橋梁類 AutoProxyCreator 也完成了實例化,并被緩存在 BeanFactory 中,靜待 BeanFactory 實例化 bean。當外部產生調用,BeanFactory 開始實例化 bean 時。AutoProxyCreator 就開始悄悄的工作了,工作細節如下:

從 BeanFactory 查找實現了 PointcutAdvisor 接口的切面對象,切面對象中包含了實現 Pointcut 和 Advice 接口的對象。

使用 Pointcut 中的表達式對象匹配當前 bean 對象。如果匹配成功,進行下一步。否則終止邏輯,返回 bean。

JdkDynamicAopProxy 對象為匹配到的 bean 生成代理對象,并將代理對象返回給 BeanFactory。

經過上面3步,AutoProxyCreator 就悄無聲息的把原來的 bean 替換為代理對象了,是不是有種偷天換日的感覺。最后把 toy-spring AOP 剩余的實現代碼貼出來:

public class AspectJAwareAdvisorAutoProxyCreator implements BeanPostProcessor, BeanFactoryAware {

    private XmlBeanFactory xmlBeanFactory;

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws Exception {
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws Exception {
        /* 這里兩個 if 判斷很有必要,如果刪除將會使程序進入死循環狀態,
         * 最終導致 StackOverflowError 錯誤發生
         */
        if (bean instanceof AspectJExpressionPointcutAdvisor) {
            return bean;
        }
        if (bean instanceof MethodInterceptor) {
            return bean;
        }

        // 1.  從 BeanFactory 查找 AspectJExpressionPointcutAdvisor 類型的對象
        List advisors =
                xmlBeanFactory.getBeansForType(AspectJExpressionPointcutAdvisor.class);
        for (AspectJExpressionPointcutAdvisor advisor : advisors) {

            // 2. 使用 Pointcut 對象匹配當前 bean 對象
            if (advisor.getPointcut().getClassFilter().matchers(bean.getClass())) {
                ProxyFactory advisedSupport = new ProxyFactory();
                advisedSupport.setMethodInterceptor((MethodInterceptor) advisor.getAdvice());
                advisedSupport.setMethodMatcher(advisor.getPointcut().getMethodMatcher());

                TargetSource targetSource = new TargetSource(bean, bean.getClass(), bean.getClass().getInterfaces());
                advisedSupport.setTargetSource(targetSource);
                
                // 3. 生成代理對象,并返回
                return advisedSupport.getProxy();
            }
        }

        // 2. 匹配失敗,返回 bean
        return bean;
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws Exception {
        xmlBeanFactory = (XmlBeanFactory) beanFactory;
    }
}

ProxyFactory 實現代碼:

/**
 * AopProxy 實現類的工廠類
 */
public class ProxyFactory extends AdvisedSupport implements AopProxy {
    @Override
    public Object getProxy() {
        return createAopProxy().getProxy();
    }

    private AopProxy createAopProxy() {
        return new JdkDynamicAopProxy(this);
    }
}

測試類:

public class XmlBeanFactoryTest {
    @Test
    public void getBean() throws Exception {
        System.out.println("--------- AOP test ----------");
        String location = getClass().getClassLoader().getResource("spring.xml").getFile();
        XmlBeanFactory bf = new XmlBeanFactory(location);
        HelloService helloService = (HelloService) bf.getBean("helloService");
        helloService.sayHelloWorld();
    }
}

測試結果:

4. 寫在最后

到此,本文的主要內容寫完了。如果你耐心的讀完了文章,并感覺不錯的話,歡迎猛點贊和收藏按鈕。這篇文章花了我一天的時間,寫的實在有點累,也深感認真寫博客的不易。本篇文章與 仿照 Spring 實現簡單的 IOC 和 AOP - 上篇,Spring bean的生命流程 共三篇文章,對 Spring IOC 和 AOP 的實現原理進行了較為詳細的結束。也是通過認真編寫這三篇文章,使得我對 Spring 框架原理有了更進一步的認識。當然限于我的經驗和能力,以上三篇文章中可能存在著一些錯誤。如果這些錯誤給大家造成了干擾,我表示很抱歉。所以文章若有疏漏不妥之處,還請指出來,如果能不吝賜教,那就更好了。好了,最后感謝大家耐心讀完我的文章,下次再見。

參考:

《Spring揭秘》

??tiny-spring

本文在知識共享許可協議 4.0 下發布,轉載請注明出處
作者:coolblog
為了獲得更好的分類閱讀體驗,
請移步至本人的個人博客:http://www.coolblog.xyz


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

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

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

相關文章

  • 仿照 Spring 實現簡單 IOC AOP - 上篇

    摘要:不過那個實現太過于簡單,和,相去甚遠。在接下來文章中,我也將從易到難,實現不同版本的和。切面切面包含了通知和切點,通知和切點共同定義了切面是什么,在何時,何處執行切面邏輯。 1. 背景 我在大四實習的時候開始接觸 J2EE 方面的開發工作,也是在同時期接觸并學習 Spring 框架,到現在也有快有兩年的時間了。不過之前沒有仿寫過 Spring IOC 和 AOP,只是宏觀上對 Spri...

    layman 評論0 收藏0
  • Spring IOC 容器源碼分析系列文章導讀

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

    NSFish 評論0 收藏0
  • 【好好面試】你必須要懂Spring-Aop

    摘要:干貨點此處是好好面試系列文的第篇文章。而這也是出現的原因,沒錯,就是被設計出來彌補短板的。運行結果如下運行結果可想而知,的通過驗證,的失敗。 【干貨點】此處是【好好面試】系列文的第10篇文章。看完該篇文章,你就可以了解Spring中Aop的相關使用和原理,并且能夠輕松解答Aop相關的面試問題。 在實際研發中,Spring是我們經常會使用的框架,畢竟它們太火了,也因此Spring相關的知...

    honhon 評論0 收藏0
  • Spring Boot [配置-下篇]

    摘要:進行異常的捕捉與錯誤信息頁面的定制。告訴,這是一個對象,該對象應該被注冊為在應用程序上下文中的。不同框架的不同配置這里以作為演示默認情況下,保護已啟用。你必須配置包含令牌的所有的網頁來工作。該命名結構旨在幫你找到需要的。 導讀: 在上篇文章中我們了解到Spring Boot 的一些常用的外部化配置,在本篇中我們將會繼續對類的配置進行了解 一個簡單的例子[錯誤頁面定制]: 在 Spr...

    StonePanda 評論0 收藏0
  • Spring框架學習筆記(一):官方文檔介紹,IoCAOP概念學習

    摘要:構造函數注入通過調用類的構造函數,將接口實現類通過構造函數變量傳入。而在中,其使用橫切技術,將這類代碼從原屬的封裝對象中提取出來,封裝到一個可重用模塊中,稱為。 最近實習用到Spring的開發框架,但是之前沒有接觸過,因此希望利用網上的資源來學習以下。 Spring官方給出了非常全面的介紹,非常適合我這種完全的小白……在這一系列學習中,我閱讀的主要資源是5.1.2 Reference ...

    mindwind 評論0 收藏0

發表評論

0條評論

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