<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          一直在用的 Spring,你知道它的加載原理嗎?

          共 14239字,需瀏覽 29分鐘

           ·

          2020-08-04 12:16

          ??Java大聯(lián)盟

          ? 幫助萬千Java學習者持續(xù)成長

          關注



          作者|擁抱心中的夢想

          juejin.im/post/5ab30714f265da237b21fbcc


          B 站搜索:楠哥教你學Java

          獲取更多優(yōu)質視頻教程


          一、前言

          作為一個經常使用 Spring 的后端程序員,小編很早就想徹底弄懂整個 Spring 框架了!但它整體是非常大的,所有繼承圖非常復雜,加上小編修行尚淺,顯得力不從心。不過,男兒在世當立志,今天就先從 Spring IOC 容器的初始化開始說起,即使完成不了對整個 Spring 框架的完全掌握,也不丟人,因為小編動手了,穩(wěn)住,咱能贏!

          下面說一些閱讀前的建議:

          1、閱讀源碼分析是非常無聊的,但既然你進來了,肯定也是對這個東西進行了解,也希望這篇總結能對你有所啟發(fā)。

          2、前方高能,文章可能會非常的長,圖文并茂。

          3、閱讀前建議你對相關設計模式、軟件設計 6 大原則有所了解,小編會在行文中進行穿插。

          4、如果你發(fā)現(xiàn)文章觀點有所錯誤或者與你見解有差異,歡迎指出和交流!

          5、建議你邊看文章的時候可以邊在 IDE 中進行調試跟蹤

          6、文章所有 UML 圖利用 idea 自動生成,具體生成方法為:選中一個類名,先ctrl+shift+alt+U,再ctrl+alt+B,然后回車即可


          二、文章將圍繞什么來進行展開?

          不多,就一行代碼,如下圖:

          這句是 Spring 初始化的代碼,雖然只有一句代碼,但內容賊多!


          三、Spring 容器 IOC 有哪些東西組成?

          這樣子,小編先理清下思路,一步一步來:

          1、上面那句代碼有個文件叫applicationContext.xml, 這是個資源文件,由于我們的bean都在里邊進行配置定義,那 Spring 總得對這個文件進行讀取并解析吧!所以 Spring 中有個模塊叫Resource模塊,顧名思義,就是資源嘛!用于對所有資源xml、txt、property等文件資源的抽象。

          下面先貼一張小編生成的類圖(圖片有點大,不知道會不會不清晰,如果不清晰可以按照上面說的idea生成方法去生成即可)


          可以看到Resource是整個體系的根接口,點進源碼可以看到它定義了許多的策略方法,因為它是用了策略模式這種設計模式,運用的好處就是策略接口/類定義了同一的策略,不同的子類有不同的具體策略實現(xiàn),客戶端調用時傳入一個具體的實現(xiàn)對象比如UrlResource或者FileSystemResource給策略接口/類Resource即可!

          所有策略如下:



          2、上面講了 Spring 框架對各種資源的抽象采用了策略模式,那么問題來了,現(xiàn)在表示資源的東西有了,那么是怎么把該資源加載進來呢?于是就有了下面的ResourceLoader組件,該組件負責對 Spring 資源的加載,資源指的是xml、properties等文件資源,返回一個對應類型的Resource對象。。UML 圖如下:



          從上面的 UML 圖可以看出,ResourceLoader組件其實跟Resource組件差不多,都是一個根接口,對應有不同的子類實現(xiàn),比如加載來自文件系統(tǒng)的資源,則可以使用FileSystemResourceLoader, 加載來自ServletContext上下文的資源,則可以使用ServletContextResourceLoader。還有最重要的一點。


          從上圖看出,ApplicationContext,AbstractApplication是實現(xiàn)了ResourceLoader的,這說明什么呢?說明我們的應用上下文ApplicationContext擁有加載資源的能力,這也說明了為什么可以通過傳入一個String resource path給ClassPathXmlApplicationContext("applicationContext.xml")就能獲得 xml 文件資源的原因了!清晰了嗎?nice!


          3、上面兩點講到了,好!既然我們擁有了加載器ResourceLoader,也擁有了對資源的描述Resource, 但是我們在 xml 文件中聲明的標簽在 Spring 又是怎么表示的呢?


          注意這里只是說對bean的定義,而不是說如何將轉換為bean對象。我想應該不難理解吧!就像你想表示一個學生Student,那么你在程序中肯定要聲明一個類Student吧!


          至于學生數據是從excel導入,或者程序運行時new出來,或者從xml中加載進來這些都不重要,重要的是你要有一個將現(xiàn)實中的實體表示為程序中的對象的東西,所以也需要在 Spring 中做一個定義!于是就引入一個叫BeanDefinition的組件,UML 圖如下:



          下面講解下 UML 圖:


          首先配置文件中的標簽跟我們的BeanDefinition是一一對應的,元素標簽擁有class、scope、lazy-init等配置屬性,BeanDefinition則提供了相應的beanClass、scope、lazyInit屬性。


          4、有了加載器ResourceLoader,也擁有了對資源的描述Resource,也有了對bean的定義,我們不禁要問,我們的Resource資源是怎么轉成我們的BeanDefinition的呢? 因此就引入了BeanDefinitionReader組件, Reader 嘛!就是一種讀取機制,UML 圖如下:



          從上面可以看出,Spring 對 reader 進行了抽象,具體的功能交給其子類去實現(xiàn),不同的實現(xiàn)對應不同的類,如PropertiedBeanDefinitionReader,XmlBeanDefinitionReader對應從 Property 和 xml 的 Resource 解析成BeanDefinition。


          5、好了!基本上所有組件都快齊全了!對了,還有一個組件,你有了BeanDefinition后,你還必須將它們注冊到工廠中去,所以當你使用getBean()方法時工廠才知道返回什么給你。


          還有一個問題,既然要保存注冊這些bean, 那肯定要有個數據結構充當容器吧!沒錯,就是一個Map, 下面貼出BeanDefinitionRegistry的一個實現(xiàn),叫SimpleBeanDefinitionRegistry的源碼圖:



          BeanDefinitionRegistry的 UML 圖如下:



          從圖中可以看出,BeanDefinitionRegistry有三個默認實現(xiàn),分別是SimpleBeanDefinitionRegistry,DefaultListableBeanFactory,GenericApplicationContext, 其中SimpleBeanDefinitionRegistry,DefaultListableBeanFactory都持有一個 Map。


          也就是說這兩個實現(xiàn)類把保存了 bean。而GenericApplicationContext則持有一個DefaultListableBeanFactory對象引用用于獲取里邊對應的 Map。在DefaultListableBeanFactory中



          在GenericApplicationContext中



          6、前面說的 5 個點基本上可以看出ApplicationContext上下文基本直接或間接貫穿所有的部分,因此我們一般稱之為容器,除此之外,ApplicationContext還擁有除了bean容器這種角色外,還包括了獲取整個程序運行的環(huán)境參數等信息(比如 JDK 版本,jre 等),其實這部分 Spring 也做了對應的封裝,稱之為Enviroment, 下面就跟著小編的 eclipse, 一起 debug 下容器的初始化工程吧!



          四、實踐是檢驗真理的唯一標準

          學生類Student.java如下:

          package com.wokao666;
          public class Student {
          private int id; private String name; private int age;
          public int getId() { return id; }
          public void setId(int id) { this.id = id; }
          public String getName() { return name; }
          public void setName(String name) { this.name = name; }
          public int getAge() { return age; }
          public void setAge(int age) { this.age = age; }
          public Student(int id, String name, int age) { super(); this.id = id; this.name = name; this.age = age; }
          public Student() { super(); }
          @Override public String toString() { return "Student [id=" + id + ", ]"; }
          }

          在application.xml中進行配置,兩個bean:

          <bean id="stu1" class="com.wokao666.Student">   <property >property>   <property >property>   <property >property> bean>  <bean id="stu2" class="com.wokao666.Student">   <property >property>   <property >property>   <property >property> bean>

          好了,接下來給最開頭那段代碼打個斷點 (Breakpoint):


          第一步:急切地加載ContextClosedEvent類,以避免在WebLogic 8.1中的應用程序關閉時出現(xiàn)奇怪的類加載器問題。




          這一步無需太過在意!


          第二步:既然是new ClassPathXmlApplicationContext()?那么就調用構造器嘛!


          第三步:


          第四步:

          好,我們跟著第三步中的super(parent),再結合上面第三節(jié)的第 6 小點 UML 圖一步一步跟蹤,然后我們來到AbstractApplicationContext的這個方法:


          那么里邊的resourcePatternResolver的類型是什么呢?屬于第三節(jié)說的 6 大步驟的哪個部分呢?通過跟蹤可以看到它的類型是ResourcePatternResolver類型的,而ResourcePatternResolver又是繼承了ResourceLoader接口,因此屬于加載資源模塊,如果還不清晰,咱們再看看ResourcePatternResolver的源碼即可,如下圖:


          對吧!不僅繼承ResourceLoader接口,而且只定義一個getResources()方法用于返回Resource[]資源集合。再者,這個接口還使用了策略模式,其具體的實現(xiàn)都在實現(xiàn)類當中,好吧!來看看 UML 圖就知道了!


          PathMatchingResourcePatternResolver這個實現(xiàn)類呢!它就是用來解釋不同路徑資源的,比如你傳入的資源路徑有可能是一個常規(guī)的url, 又或者有可能是以classpath*前綴,都交給它處理。


          ServletContextResourcePatternResolver這個實現(xiàn)類顧名思義就是用來加載Servlet上下文的,通常用在 web 中。


          第五步:

          接著第四步的方法,我們在未進入第四步的方法時,此時會對AbstractApplicationContext進行實例化,此時this對象的某些屬性被初始化了(如日志對象),如下圖:


          接著進入getResourcePatternResolver()方法:


          第四步說了,PathMatchingResourcePatternResolver用來處理不同的資源路徑的,怎么處理,我們先進去看看!


          如果找到,此時控制臺會打印找到用于OSGi包URL解析的Equinox FileLocator日志。沒打印很明顯找不到!

          運行完成返回setParent()方法。


          第六步:


          如果父代是非null,,則該父代與當前this應用上下文環(huán)境合并。顯然這一步并沒有做什么事!parent顯然是null的,那么就不合并嘛!還是使用當前this的環(huán)境。

          做個總結:前六步基本上做了兩件事:

          • 1、初始化相關上下文環(huán)境,也就是初始化ClassPathXmlApplicationContext實例

          • 2、獲得一個resourcePatternResolver對象,方便第七步的資源解析成Resource對象


          第七步:



          第七步又回到剛開始第三步的代碼,因為我們前面 6 步已經完成對super(parent)的追蹤。讓我們看看setConfigLocation()方法是怎么一回事~

          /** * Set the config locations for this application context.//未應用上下文設置資源路徑 * 

          If not set, the implementation may use a default as appropriate.//如果未設置,則實現(xiàn)可以根據需要使用默認值。 */public void setConfigLocations(String... locations) { if (locations != null) {//非空 Assert.noNullElements(locations, "Config locations must not be null");//斷言保證locations的每個元素都不為null this.configLocations = new String[locations.length]; for (int i = 0; i < locations.length; i++) { this.configLocations[i] = resolvePath(locations[i]).trim();//去空格,很好奇resolvePath做了什么事情? } } else { this.configLocations = null; }}


          進入resolvePath()方法看看:

          /** * 解析給定的資源路徑,必要時用相應的環(huán)境屬性值替換占位符,應用于資源路徑配置。 * Resolve the given path, replacing placeholders with corresponding * environment property values if necessary. Applied to config locations. * @param path the original file path * @return the resolved file path * @see org.springframework.core.env.Environment#resolveRequiredPlaceholders(String) */protected String resolvePath(String path) {  return getEnvironment().resolveRequiredPlaceholders(path);}
          進入getEnvironment()看看:/** * {@inheritDoc} *

          If {@code null}, a new environment will be initialized via * {@link #createEnvironment()}. */@Overridepublic ConfigurableEnvironment getEnvironment() { if (this.environment == null) { this.environment = createEnvironment(); } return this.environment;}


          進入createEnvironment(), 方法,我們看到在這里創(chuàng)建了一個新的StandardEnviroment對象,它是Environment的實現(xiàn)類,表示容器運行的環(huán)境,比如 JDK 環(huán)境,Servlet 環(huán)境,Spring 環(huán)境等等。


          每個環(huán)境都有自己的配置數據,如System.getProperties()、System.getenv()等可以拿到 JDK 環(huán)境數據;ServletContext.getInitParameter()可以拿到 Servlet 環(huán)境配置數據等等, 也就是說 Spring 抽象了一個Environment來表示環(huán)境配置。


          生成的StandardEnviroment對象并沒有包含什么內容,只是一個標準的環(huán)境,所有的屬性都是默認值。


          第八步:這一步是重頭戲

          先做個小結:到現(xiàn)在為止,我們擁有了以下實例:


          現(xiàn)在代碼運行到如下圖的refresh()方法:



          看一下這個方法的內容是什么?

          @Overridepublic void refresh() throws BeansException, IllegalStateException {  synchronized (this.startupShutdownMonitor) {    // 刷新前準備工作,包括設置啟動時間,是否激活標識位,初始化屬性源(property source)配置    prepareRefresh();
          // 創(chuàng)建beanFactory(過程是根據xml為每個bean生成BeanDefinition并注冊到生成的beanFactory ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
          //準備創(chuàng)建好的beanFactory(給beanFactory設置ClassLoader,設置SpEL表達式解析器,設置類型轉化器【能將xml String類型轉成相應對象】, //增加內置ApplicationContextAwareProcessor對象,忽略各種Aware對象,注冊各種內置的對賬對象【BeanFactory,ApplicationContext】等, //注冊AOP相關的一些東西,注冊環(huán)境相關的一些bean prepareBeanFactory(beanFactory);
          try { // 模板方法,為容器某些子類擴展功能所用(工廠后處理器)這里可以參考BeanFactoryPostProcessor接口的postProcessBeanFactory方法 postProcessBeanFactory(beanFactory);
          // 調用所有BeanFactoryPostProcessor注冊為Bean invokeBeanFactoryPostProcessors(beanFactory);
          // 注冊所有實現(xiàn)了BeanPostProcessor接口的Bean registerBeanPostProcessors(beanFactory);
          // 初始化MessageSource,和國際化相關 initMessageSource();
          // 初始化容器事件傳播器 initApplicationEventMulticaster();
          // 調用容器子類某些特殊Bean的初始化,模板方法 onRefresh();
          // 為事件傳播器注冊監(jiān)聽器 registerListeners();
          // 初始化所有剩余的bean(普通bean) finishBeanFactoryInitialization(beanFactory);
          // 初始化容器的生命周期事件處理器,并發(fā)布容器的生命周期事件 finishRefresh(); } catch (BeansException ex) { if (logger.isWarnEnabled()) { logger.warn("Exception encountered during context initialization - " + "cancelling refresh attempt: " + ex); } // 銷毀已創(chuàng)建的bean destroyBeans(); // 重置`active`標志 cancelRefresh(ex); throw ex; } finally { //重置一些緩存 resetCommonCaches(); } }}

          在這里我想說一下,這個refresh()方法其實是一個模板方法, 很多方法都讓不同的實現(xiàn)類去實現(xiàn),但該類本身也實現(xiàn)了其中一些方法,并且這些已經實現(xiàn)的方法是不允許子類重寫的,比如:prepareRefresh()方法。更多模板方法設計模式,可看我之前的文章?談一談我對‘模板方法’設計模式的理解(Template)。

          先進入prepareRefresh()方法:


          /** * Prepare this context for refreshing, setting its startup date and * active flag as well as performing any initialization of property sources. */protected void prepareRefresh() {  this.startupDate = System.currentTimeMillis();//設置容器啟動時間  this.closed.set(false);//容器關閉標志,是否關閉?  this.active.set(true);//容器激活標志,是否激活?    if (logger.isInfoEnabled()) {//運行到這里,控制臺就會打印當前容器的信息    logger.info("Refreshing " + this);  }
          // 空方法,由子類覆蓋實現(xiàn),初始化容器上下文中的property文件 initPropertySources();
          //驗證標記為必需的所有屬性均可解析,請參閱ConfigurablePropertyResolver#setRequiredProperties getEnvironment().validateRequiredProperties();
          //允許收集早期的ApplicationEvents,一旦多播器可用,即可發(fā)布... this.earlyApplicationEvents = new LinkedHashSet();}

          控制臺輸出:


          三月 22, 2018 4:21:13 下午 org.springframework.context.support.ClassPathXmlApplicationContext prepareRefresh信息: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@96532d6: startup date [Thu Mar 22 16:21:09 CST 2018]; root of context hierarchy

          第九步:

          進入obtainFreshBeanFactory()方法:

          /** * 告訴子類刷新內部bean工廠(子類是指AbstractApplicationContext的子類,我們使用的是ClassPathXmlApplicationContext) * Tell the subclass to refresh the internal bean factory. */protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {  refreshBeanFactory();//刷新Bean工廠,如果已經存在Bean工廠,那就關閉并銷毀,再創(chuàng)建一個新的bean工廠  ConfigurableListableBeanFactory beanFactory = getBeanFactory();//獲取新創(chuàng)建的Bean工廠  if (logger.isDebugEnabled()) {    logger.debug("Bean factory for " + getDisplayName() + ": " + beanFactory);//控制臺打印  }  return beanFactory;}
          進入refreshBeanFactory()方法:/** * 該實現(xiàn)執(zhí)行該上下文的基礎Bean工廠的實際刷新,關閉以前的Bean工廠(如果有的話)以及為該上下文的生命周期的下一階段初始化新鮮的Bean工廠。 * This implementation performs an actual refresh of this context's underlying * bean factory, shutting down the previous bean factory (if any) and * initializing a fresh bean factory for the next phase of the context's lifecycle. */@Overrideprotected final void refreshBeanFactory() throws BeansException { if (hasBeanFactory()) {//如果已有bean工廠 destroyBeans();//銷毀 closeBeanFactory();//關閉 } try { DefaultListableBeanFactory beanFactory = createBeanFactory();//創(chuàng)建一個新的bean工廠 beanFactory.setSerializationId(getId());//為序列化目的指定一個id,如果需要,可以將此BeanFactory從此id反序列化回BeanFactory對象。 //定制容器,設置啟動參數(bean可覆蓋、循環(huán)引用),開啟注解自動裝配 customizeBeanFactory(beanFactory); ////將所有BeanDefinition載入beanFactory中,此處依舊是模板方法,具體由子類實現(xiàn) loadBeanDefinitions(beanFactory); //beanFactory同步賦值 synchronized (this.beanFactoryMonitor) { this.beanFactory = beanFactory; } } catch (IOException ex) { throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex); }}

          總結:這一步主要的工作就是判斷刷新容器前是否已經有 beanfactory 存在,如果有,那么就銷毀舊的 beanfactory, 那么就銷毀掉并且創(chuàng)建一個新的 beanfactory 返回給容器,同時將 xml 文件的BeanDefinition注冊到 beanfactory 中。如果不太清楚可以回過頭看看我們的第三節(jié)第5點內容


          第十步:

          進入第九步的loadBeanDefinitions(beanFactory)方法中去take a look:

          /** * 使用XmlBeanDefinitionReader來加載beandefnition,之前說過使用reader機制加載Resource資源變?yōu)锽eanDefinition對象 * Loads the bean definitions via an XmlBeanDefinitionReader. * @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader * @see #initBeanDefinitionReader * @see #loadBeanDefinitions */@Overrideprotected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {  // 創(chuàng)建XmlBeanDefinitionReader對象  XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);
          // 使用當前上下文Enviroment中的Resource配置beanDefinitionReader,因為beanDefinitionReader要將Resource解析成BeanDefinition嘛! beanDefinitionReader.setEnvironment(this.getEnvironment()); beanDefinitionReader.setResourceLoader(this); beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));
          //初始化這個reader initBeanDefinitionReader(beanDefinitionReader); //將beandefinition注冊到工廠中(這一步就是將bean保存到Map中) loadBeanDefinitions(beanDefinitionReader);}

          控制臺輸出:

          三月 22, 2018 5:09:40 下午 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions信息: Loading XML bean definitions from class path resource [applicationContext.xml]

          第十一步:

          進入prepareBeanFactory(beanFactory)方法:

          //設置bean類加載器//設置Spring語言表達式(SpEL)解析器//掃描ApplicationContextAware bean//注冊類加載期類型切面織入(AOP)LoadTimeWeaver//為各種加載進入beanFactory的bean配置默認環(huán)境

          第十二步:

          postProcessBeanFactory(beanFactory)方法:

          postProcessBeanFactory同樣作為一個模板方法,由子類來提供具體的實現(xiàn),子類可以有自己的特殊對BeanDefinition后處理方法,即子類可以在這對前面生成的BeanDefinition,即bean的元數據再處理。比如修改某個bean的id/name屬性、scope屬性、lazy-init屬性等。


          第十三步:

          invokeBeanFactoryPostProcessors(beanFactory)方法:

          該方法調用所有的BeanFactoryPostProcessor,它是一個接口,實現(xiàn)了此接口的類需重寫postProcessBeanFactory()這個方法,可以看出該方法跟第十二步的方法是一樣的,只不過作為接口,更多的是提供給開發(fā)者來對生成的BeanDefinition做處理,由開發(fā)者提供處理邏輯。


          第十四步:

          其余剩下的方法基本都是像初始化消息處理源,初始化容器事件,注冊bean監(jiān)聽器到事件傳播器上,最后完成容器刷新。


          五、總結

          恭喜我,我終于寫完了,同樣也恭喜你,你也閱讀完了。

          我很佩服我自己能花這么長時間進行總結發(fā)布,之所以要進行總結,那是因為小編還是贊同好記性不如爛筆頭的說法。

          你不記,你過陣子就會忘記,你若記錄,你過陣子也會忘記!區(qū)別在于忘記了,可以回過頭在很短的時間內進行回憶,查漏補缺,減少學習成本。

          再者,我認為我分析的還不是完美的,缺陷很多,因此我將我寫的所有文章發(fā)布出來和大家探討交流,汕頭大學有校訓說得非常地好,那就是說知識是用來共享的,因為共享了,知識才能承前啟后。


          現(xiàn)在再梳理一下 Spring 初始化過程:


          1、首先初始化上下文,生成ClassPathXmlApplicationContext對象,在獲取resourcePatternResolver對象將xml解析成Resource對象。


          2、利用 1 生成的 context、resource 初始化工廠,并將 resource 解析成 beandefinition, 再將 beandefinition 注冊到 beanfactory 中。


          推薦閱讀

          1、Spring Boot+Vue項目實戰(zhàn)

          2、B站:4小時上手MyBatis Plus

          3、一文搞懂前后端分離

          4、快速上手Spring Boot+Vue前后端分離


          楠哥簡介

          資深 Java 工程師,微信號?southwindss

          《Java零基礎實戰(zhàn)》一書作者

          騰訊課程官方 Java 面試官今日頭條認證大V

          GitChat認證作者,B站認證UP主(楠哥教你學Java)

          致力于幫助萬千 Java 學習者持續(xù)成長。




          有收獲,就在看?
          瀏覽 67
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国内精品国产成人国产三级 | 日韩小电影 | 躁BBB躁BBB躁BBBBBB日 | 天天综合视频入口 | 无码爱爱网站 |