spring-beans深入源码之Bean Definition(Bean的装载过程)

我们在使用一个bean得时候,在不用任何框架的情况下都是需要自己new的,spring框架既然为我们提供bean容器使得我们把bean得管理权交给它,那么我们看看spring的bean是怎么装载的,先说xml,注解的方式其实是一样的。
   接着上篇的test例子<b>CollectionMergingTests</b>其中最开始有个<b>setUp</b>方法,这是junit方法运行前的装配方法

public class CollectionMergingTests extends TestCase {

    private DefaultListableBeanFactory beanFactory;
        @Override
    protected void setUp() throws Exception {
        this.beanFactory = new DefaultListableBeanFactory();
        BeanDefinitionReader reader = new XmlBeanDefinitionReader(this.beanFactory);
        reader.loadBeanDefinitions(new ClassPathResource("collectionMerging.xml", getClass()));
    }
………………
}

这个方法就是在装配bean到容器中。
  <b>DefaultListableBeanFactory </b>是spring 真正可以独立使用IOC容器的<b>BeanFactory</b>。继承和实现的传递性可知<b>DefaultListableBeanFactory </b>默认实现了<b>BeanFactory</b>和<b>BeanDefinitionRegistry</b>,<b>DefaultListableBeanFactory </b>类继承图(引用自software-architecture-design

DefaultListableBeanFactory继承关系

直接debug看下其初始化过程便基本知道继承关系。

DefaultListableBeanFactory初始化第一步

可见先是到static初始化块

static {
        try {
            javaUtilOptionalClass =
                    ClassUtils.forName("java.util.Optional", DefaultListableBeanFactory.class.getClassLoader());
        }
        catch (ClassNotFoundException ex) {
            // Java 8 not available - Optional references simply not supported then.
        }
        try {
            javaxInjectProviderClass =
                    ClassUtils.forName("javax.inject.Provider", DefaultListableBeanFactory.class.getClassLoader());
        }
        catch (ClassNotFoundException ex) {
            // JSR-330 API not available - Provider interface simply not supported then.
        }
    }

加载class java.util.Optional(JAVA8新加入的)和interface javax.inject.Provider
<b>java.util.Optional</b>是JAVA8新加入的类,这个类专门用来解决空引用的问题,同时这个类和lambda表达式和函数式编程也可以比较好的整合在一起使用
java.util.Optional doc
<b>javax.inject.Provider</b>提供了一个 T的实例。 通常作为一个依赖注入容器器的父接口. 可以注入任何类型的 T, 当然也可以入 Provider<T>
相对于直接注入 T ,注入 Provider<T> 有如下作用(doc的英文大概翻译):

  • 检索多个实例
  • 延迟或者选择性的检索一个实例
  • 打破循环依赖
  • 抽象的scope,可以从一个包含scope的更小的scope中检索一个实例
    javax.inject.Provider doc

在debug该类初始化的过程中我在类的属性初始化上打断点无意间发现了eclipse的断点类型

breakpoint

第一处的是<b>Watchpoint</b>这个主要是关注变量的值得变化过程,第二处是<b>Line Breakpoint</b>最简单的断点,即执行到此处就断点,还有。eclipse 还有条件断点,方法断点 异常断点 远程调试等 这个完了再细化记录博客吧。
这里想说的是为什么 第一行是<b>Watchpoint</b>二处是<b>Line Breakpoint</b> 同样是属性是为什么? 其实是final关键字的作用,<b>Watchpoint</b>是观察值的变化情况 final的变量是不可变的 所以final修饰的参数断点都是<b>Line Breakpoint</b> 类似于方法断点,所以有时候要分清你可以在属性上设置的断点类型。

接下来到了属性的初始化
属性初始化

继续往下走的时候可以看到 类初始化的关系 先是<b>DefaultListableBeanFactory</b>static静态初始化块和static属性,然后是SimpleAliasRegistry的属性


SimpleAliasRegistry初始化

再是SimpleAliasRegistry的子类<b>DefaultSingletonBeanRegistry</b>


DefaultSingletonBeanRegistry初始化

接着是<b>DefaultSingletonBeanRegistry</b>的子类<b>FactoryBeanRegistrySupport</b>
Paste_Image.png

接着<b>FactoryBeanRegistrySupport</b>子类<b>AbstractBeanFactory</b>


AbstractBeanFactory初始化

接着是<b>AbstractBeanFactory</b>子类<b>AbstractAutowireCapableBeanFactory</b>

AbstractAutowireCapableBeanFactory初始化

最后到<b>DefaultListableBeanFactory</b>自己

DefaultListableBeanFactory初始化
由此我们基本可以得出类初始化的顺序(为什么要说这个 是因为我看到网上好多说法和我运行的得到的结果不一致):

先找到根类,根类static静态块和static属性的初始化 接着根类的子类static静态块和static属性初始化 最后回到类本身的类static静态块和static属性初始化然后再到根类 其他属性的初始化,接着根类子类的其他属性初始化 最后回到自己的其他属性初始化。
<b>DefaultListableBeanFactory</b>初始化完毕以后接着是<b>BeanDefinitionReader</b>的初始化 选择初始化的类实例是<b>XmlBeanDefinitionReader</b>初始化完毕后调用

reader.loadBeanDefinitions(new ClassPathResource("collectionMerging.xml", getClass()));

方法去加载collectionMerging.xml配置文件。
ClassPathResource类是Resource接口实现类的子类,如果没有指定相对的类名,该类将从类的根路径开始寻找某个resource,如果指定了相对的类名,则根据指定类的相对路径来查找某个resource。上面的还可以写成:

reader.loadBeanDefinitions(new ClassPathResource(
"org/springframework/beans/factory/xml/collectionMerging.xml"));

<b>XmlBeanDefinitionReader</b>初始化的时候先找到根类<b>AbstractBeanDefinitionReader</b>
其构造方法

protected AbstractBeanDefinitionReader(BeanDefinitionRegistry registry) {
        Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
        this.registry = registry;

        // Determine ResourceLoader to use.
        if (this.registry instanceof ResourceLoader) {
            this.resourceLoader = (ResourceLoader) this.registry;
        }
        else {
            this.resourceLoader = new PathMatchingResourcePatternResolver();
        }

        // Inherit Environment if possible
        if (this.registry instanceof EnvironmentCapable) {
            this.environment = ((EnvironmentCapable) this.registry).getEnvironment();
        }
        else {
            this.environment = new StandardEnvironment();
        }
    }

在构造方法中有instanceof ResourceLoader 判断 显然传入的registry 是<b>DefaultListableBeanFactory</b>的实例 没有实现ResourceLoader接口 所以走else 逻辑创建<b>PathMatchingResourcePatternResolver</b>类,在<b>PathMatchingResourcePatternResolver</b>初始化的过程中

public PathMatchingResourcePatternResolver() {
        this.resourceLoader = new DefaultResourceLoader();
    }

其创建的是<b>DefaultResourceLoader</b>类 该类实现了<b>ResourceLoader</b>接口。所以最后还是创建的是<b>ResourceLoader</b>的实例

public DefaultResourceLoader() {
        this.classLoader = ClassUtils.getDefaultClassLoader();
    }

这个构造方法就是为了初始化private ClassLoader classLoader;属性用于load xml资源。
好了<b>Resource</b>参数构造完毕后进入

@Override
    public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
        return loadBeanDefinitions(new EncodedResource(resource));
    }

方法,这里将<b>Resource</b>包装成了<b>EncodedResource</b> 其实EncodedResource是对Resource的包装 增加了encoding和charset属性而已 这里都为null 相当于默认的null。接着进入方法

public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
        Assert.notNull(encodedResource, "EncodedResource must not be null");
        if (logger.isInfoEnabled()) {
            logger.info("Loading XML bean definitions from " + encodedResource.getResource());
        }

        Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
        if (currentResources == null) {
            currentResources = new HashSet<EncodedResource>(4);
            this.resourcesCurrentlyBeingLoaded.set(currentResources);
        }
        if (!currentResources.add(encodedResource)) {
            throw new BeanDefinitionStoreException(
                    "Detected cyclic loading of " + encodedResource + " - check your import definitions!");
        }
        try {
            InputStream inputStream = encodedResource.getResource().getInputStream();
            try {
                InputSource inputSource = new InputSource(inputStream);
                if (encodedResource.getEncoding() != null) {
                    inputSource.setEncoding(encodedResource.getEncoding());
                }
                return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
            }
            finally {
                inputStream.close();
            }
        }
        catch (IOException ex) {
            throw new BeanDefinitionStoreException(
                    "IOException parsing XML document from " + encodedResource.getResource(), ex);
        }
        finally {
            currentResources.remove(encodedResource);
            if (currentResources.isEmpty()) {
                this.resourcesCurrentlyBeingLoaded.remove();
            }
        }
    }

这里会有一个比较经典的同步缓存的方式

    Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();

this.resourcesCurrentlyBeingLoaded是

private final ThreadLocal<Set<EncodedResource>> resourcesCurrentlyBeingLoaded =
            new NamedThreadLocal<Set<EncodedResource>>("XML bean definition resources currently being loaded");

可以看到采用了<b>ThreadLocal</b>的存贮方式。在JDK1.2的时候就提供了<b>java.lang.ThreadLocal</b><b>ThreadLocal</b>为解决多线程问题提供了非常非常大帮助,当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。这样就完美的解决了多线程的问题,不过对于计数的情况不适用 建议使用ATMOIC的变量。
在没有取到的情况下 将新的<b>EncodedResource</b>加入ThreadLocal变量的缓存中。
接着从<b>EncodedResource</b>获取输入流<b>InputStream</b>构造 <b>InputSource</b>,<b>InputSource</b>比较简单的一个类,封装了一下<b>InputStream</b>最后调用 doLoadBeanDefinitions方法。部分源码:

try {
            InputStream inputStream = encodedResource.getResource().getInputStream();
            try {
                InputSource inputSource = new InputSource(inputStream);
                if (encodedResource.getEncoding() != null) {
                    inputSource.setEncoding(encodedResource.getEncoding());
                }
                return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
            }
            finally {
                inputStream.close();
            }
        }
        catch (IOException ex) {
            throw new BeanDefinitionStoreException(
                    "IOException parsing XML document from " + encodedResource.getResource(), ex);
        }
        finally {
            currentResources.remove(encodedResource);
            if (currentResources.isEmpty()) {
                this.resourcesCurrentlyBeingLoaded.remove();
            }
        }

这里的<b>finally</b>方法最后将<b>ThreadLocal</b>变量<b>resourcesCurrentlyBeingLoaded</b> remove掉了。看了好半天才看懂其中的奇妙之处:<b>这个方法本身存在多线程问题,最简单的做法就是同步语句块,但是大家知道这会影响性能所以借助<b>ThreadLocal</b>变量做了一个过程同步 在使用完即remove掉</b>。
<b>doLoadBeanDefinitions</b>是java dom加载xml的过程,大家都知道java解析xml的几种方式,基本的解析方式有两种,一种叫SAX,另一种叫DOM,再细一点有

  • DOM生成和解析XML文档
    为 XML 文档的已解析版本定义了一组接口。解析器读入整个文档,然后构建一个驻留内存的树结构,然后代码就可以使用 DOM 接口来操作这个树结构。
    优点:整个文档树在内存中,便于操作;支持删除、修改、重新排列等多种功能;
    缺点:将整个文档调入内存(包括无用的节点),浪费时间和空间;使用场合:一旦解析了文档还需多次访问这些数据;硬件资源充足(内存、CPU)。
  • SAX生成和解析XML文档
    为解决DOM的问题,出现了SAX。SAX ,事件驱动。当解析器发现元素开始、元素结束、文本、文档的开始或结束等时,发送事件,程序员编写响应这些事件的代码,保存数据。
    优点:不用事先调入整个文档,占用资源少;SAX解析器代码比DOM解析器代码小,适于Applet,下载。
    缺点:不是持久的;事件过后,若没保存数据,那么数据就丢了;无状态性;从事件中只能得到文本,但不知该文本属于哪个元素;使用场合:Applet;只需XML文档的少量内容,很少回头访问;机器内存少;
  • DOM4J生成和解析XML文档
    DOM4J 是一个非常非常优秀的Java XML API,具有性能优异、功能强大和极端易用使用的特点,同时它也是一个开放源代码的软件。如今你可以看到越来越多的 Java 软件都在使用 DOM4J 来读写 XML,特别值得一提的是连 Sun 的 JAXM 也在用 DOM4J。
  • JDOM生成和解析XML
    为减少DOM、SAX的编码量,出现了JDOM;优点:20-80原则,极大减少了代码量。使用场合:要实现的功能简单,如解析、创建等,但在底层,JDOM还是使用SAX(最常用)、DOM、Xanan文档。

这里spring用java dom解析xml 我想原因就是需要加载的xml配置文件都不是大型的xml文件,都是比较小的 所以使用java dom反而效果会好一些。
这里定义了<b>DocumentLoader</b>接口,提供了默认的实现类<b>DefaultDocumentLoader</b>其中的方法:

public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,
            ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {

        DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);
        if (logger.isDebugEnabled()) {
            logger.debug("Using JAXP provider [" + factory.getClass().getName() + "]");
        }
        DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);
        return builder.parse(inputSource);
    }

解析过程可以看oracle jom的api文档
最后返回<b>Document</b>对象。

Documen return

进入很重要的方法<b>registerBeanDefinitions</b>该方法将读取的dom转换成bean definition先进入方法

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
        BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
        int countBefore = getRegistry().getBeanDefinitionCount();
        documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
        return getRegistry().getBeanDefinitionCount() - countBefore;
    }

第一行BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader(); 创建了<b>BeanDefinitionDocumentReader</b>对象 该对象是将document中包含的所偶bean定义解析出来。

protected BeanDefinitionDocumentReader createBeanDefinitionDocumentReader() {
        return BeanDefinitionDocumentReader.class.cast(BeanUtils.instantiateClass(this.documentReaderClass));
    }

第二行int countBefore = getRegistry().getBeanDefinitionCount(); 获取到解析前的bean的数量
第三行documentReader.registerBeanDefinitions(doc, createReaderContext(resource));这里先根据传入的<b>Resource</b>参数创建<b>XmlReaderContext</b>对象,官方给出的解释是

Extension of {@link org.springframework.beans.factory.parsing.ReaderContext},
 * specific to use with an {@link XmlBeanDefinitionReader}. Provides access to the
 * {@link NamespaceHandlerResolver} configured in the {@link XmlBeanDefinitionReader}.

大概意思就是配合XmlBeanDefinitionReader使用的类。继续往下走到:

@Override
    public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
        this.readerContext = readerContext;
        logger.debug("Loading bean definitions");
        Element root = doc.getDocumentElement();
        doRegisterBeanDefinitions(root);
    }

到方法<b>doRegisterBeanDefinitions(root)</b> 进入:

protected void doRegisterBeanDefinitions(Element root) {
        // Any nested <beans> elements will cause recursion in this method. In
        // order to propagate and preserve <beans> default-* attributes correctly,
        // keep track of the current (parent) delegate, which may be null. Create
        // the new (child) delegate with a reference to the parent for fallback purposes,
        // then ultimately reset this.delegate back to its original (parent) reference.
        // this behavior emulates a stack of delegates without actually necessitating one.
        BeanDefinitionParserDelegate parent = this.delegate;
        this.delegate = createDelegate(getReaderContext(), root, parent);

        if (this.delegate.isDefaultNamespace(root)) {
            String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
            if (StringUtils.hasText(profileSpec)) {
                String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
                        profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
                if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
                    return;
                }
            }
        }

        preProcessXml(root);
        parseBeanDefinitions(root, this.delegate);
        postProcessXml(root);

        this.delegate = parent;
    }

先创建<b>BeanDefinitionParserDelegate</b>

BeanDefinitionParserDelegate create

开始解析的三部曲

解析三部曲
  • 先看<b>preProcessXml</b>
    哈哈这真是个好方法 空实现 我喜欢,不过这算是解析的可扩展性 三部曲的步骤必需保证全 以免扩展需要
  • 在看<b>parseBeanDefinitions</b>
    protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
        if (delegate.isDefaultNamespace(root)) {
            NodeList nl = root.getChildNodes();
            for (int i = 0; i < nl.getLength(); i++) {
                Node node = nl.item(i);
                if (node instanceof Element) {
                    Element ele = (Element) node;
                    if (delegate.isDefaultNamespace(ele)) {
                        parseDefaultElement(ele, delegate);
                    }
                    else {
                        delegate.parseCustomElement(ele);
                    }
                }
            }
        }
        else {
            delegate.parseCustomElement(root);
        }
    }
    
    这个方法就是最重要的啦,可以看到在遍历节点,其中方法<b>parseDefaultElement</b>

private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
importBeanDefinitionResource(ele);
}
else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
processAliasRegistration(ele);
}
else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
processBeanDefinition(ele, delegate);
}
else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
// recurse
doRegisterBeanDefinitions(ele);
}
}
```
分了好几种情况去解析 看开头是import、alias、bean、beans这四种开头的 标签去解析 ,我们就找个bean开头的跟踪下情况 其它的标签也是一样的道理。


bean开头标签的解析

进入方法

protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
        BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
        if (bdHolder != null) {
            bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
            try {
                // Register the final decorated instance.
                BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
            }
            catch (BeanDefinitionStoreException ex) {
                getReaderContext().error("Failed to register bean definition with name '" +
                        bdHolder.getBeanName() + "'", ele, ex);
            }
            // Send registration event.
            getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
        }
    }

其中<b>parseBeanDefinitionElement</b>解析后获得<b>BeanDefinitionHolder</b>其中过程中比较中要的一个类<b>GenericBeanDefinition</b>每个bean标签都是解析成这样的beandefinition了。获得这个bean后对应解析各种

GenericBeanDefinition 各个部分的解析

对应的bean部件解析parseMetaElements、parseLookupOverrideSubElements、parseReplacedMethodSubElements、parseConstructorArgElements、parsePropertyElements、parseQualifierElements 见名知意的方法没有多大难度就是对应的解析xml即可,解析完成 bean definition也就构造完成了。

  • 最后看<b>postProcessXml</b>
    哈哈这也真是个好方法 空实现 我喜欢,不过还是那句话这算是解析的可扩展性 三部曲的步骤必需保证全 以免扩展需要。

BeanDefinition装配过程基本完成 最后是装配到了最先初始化的<b>DefaultListableBeanFactory</b>的各个map属性中了。 好多东西很粗率的过了 后续补进。
不早了 go home!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,376评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,126评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,966评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,432评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,519评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,792评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,933评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,701评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,143评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,488评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,626评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,292评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,896评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,742评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,324评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,494评论 2 348

推荐阅读更多精彩内容