深入spring循环依赖处理机制与源码解析

文章内容来源:拉勾教育Java高薪训练营8期

知识的获取,来源于输出!这是我在拉勾教育Java高薪训练营8期学习后的一个总结与分享,选取spring的循环依赖处理这一知识点做总结,因为我觉得它涵盖spring创建bean的流程,既梳理了循环依赖,也能对bean的创建流程进行一个回顾。

1.谈谈在拉勾教育训练营的学习感受

1.1 报名学习初衷

技术遇到瓶颈,目前的工作技术对技术提升难度大,给自己一个提升于与学习的机会

1.2 学习方式

直播+录播,有班主任负责学习事务,导师负责指导学习

1.3 学习感受

老师讲课能够由浅入深,有深度也有广度,跟着老师学下来。对相对复杂的知识也能掌握(如框架源码),最终能够手写实现核心逻辑

1.4 老师

班主任认真负责,经常我们监督学习和服务同学们,导师也是有问必答,帮助同学学习、解惑

2.spring循环依赖处理机制

2.1 什么是循环依赖

循环依赖就是指对象之间的相互引用,通俗来讲,就是两个或者两个以上的对象互相持有对方的引用,用代码表示就是:

public class LagouBean {
    private ItBean itBean;
    ...
}
public class ItBean {
    private LagouBean lagouBean;
    ...
}

在开发中使用spring时,对象一般都是交给spring来创建和管理,就像下面这样:

<bean id="lagouBean" class="com.lagou.edu.LagouBean">
    <property name="ItBean" ref="itBean"/>
</bean>
<bean id="itBean" class="com.lagou.edu.ItBean">
    <property name="LagouBean" ref="lagouBean"/>
</bean>

如上两个类,在创建LagouBean的时候发现它依赖了ItBean,于是要先去创建ItBean,这时候发现ItBean又依赖了LagouBean,此时又要去创建LagouBean,好像陷入了一种死循环...但在使用spring的时候,发现它并不会进入死循环导致程序异常,反而能够正常创建对象供程序调用,那么它是如何处理的?

2.2 spring如何处理循环依赖

经过翻阅spring创建bean部分的源码得知,spring处理循环依赖的核心思想是基于java的引用传递,获得对象的引用时,属性是可以延后设置的。利用这一特性,spring在创建对象时,先不设置其属性,实例化后提前暴露到容器中,提供给其他对象引用。

所以创建LagouBean时,实例化后立即暴露到容器中,然后填充属性,此时发现它依赖了ItBean,则先去创建ItBean,ItBean实例化后也会提前暴露,后填充属性,此时发现ItBean依赖了LagouBean,因为LagouBean提前已经暴露出来了,就可以从容器中取到完成ItBean的创建,ItBean创建完成,填充到LagouBean完成创建。

spring为了完成这一过程,采用了三级缓存机制,对象的提前暴露就是将创建的对象事先放入缓存之中。在org.springframework.beans.factory.support.DefaultSingletonBeanRegistry类中,定义了这三个缓存:

Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
Map<String, Object> earlySingletonObjects = new HashMap<>(16);
  • singletonObjects:一级缓存,存放创建完成的单例对象,是完完整整的对象,经历了实例化、属性填充、后置处理等一系列步骤后创建的对象
  • singletonFactories:三级缓存,存放的是将实例化后的bean包装成的ObjectFactory
  • earlySingletonObjects:二级缓存,存放的是提前暴露的bean,未填充属性,但是已经可以被其他对象引用了,这里bean的获取主要是调用三级缓存中存放的ObjectFactory的getObject()方法创建

综上,spring处理循环依赖的思想主要基于java引用传递,采用三级缓存机制,将对象仅实例化后提前暴露存放至缓存中,使得依赖它的对象可以获取其引用完成创建,然后进行属性填充、初始化等操作,最后完成对象的创建。附上一张学习时的图方便理解:

解决循环依赖问题图解

3.spring循环依赖处理源码解析

经过上一部分的大致分析,这部分将对源码进行一个解读,深入了解一下spring处理循环依赖的整个流程,写了如下一段测试代码:

public void testIoC() {
  ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
  LagouBean lagouBean = applicationContext.getBean(LagouBean.class);
  System.out.println(lagouBean);
}

3.1 容器初始化源码解析

如上面的测试代码,使用了ClassPathXmlApplicationContext容器作为测试对象,通过代码调试跟踪,最开始调用了ClassPathXmlApplicationContext的构造方法,方法内部调用了org.springframework.context.support.AbstractApplicationContext#refresh()来完成容器的创建

public ClassPathXmlApplicationContext(
            String[] configLocations, boolean refresh, @Nullable ApplicationContext parent)throws BeansException {
        //初始化父类
        super(parent);
        //设置配置信息
        setConfigLocations(configLocations);
        //完成容器的初始化
        if (refresh) {
            refresh();
        }
    }

refresh()方法的核心就是完成BeanFactory的创建,加载beandefition,然后进行bean的创建

public void refresh() throws BeansException, IllegalStateException {
        synchronized (this.startupShutdownMonitor) {
            //创建beanfactory,加载beandefition注册到beandefiitoRegisty
            ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
            //初始化bean,填充属性,初始化方法调用..
            finishBeanFactoryInitialization(beanFactory);
        }
    }

finishBeanFactoryInitialization()方法中,调用了org.springframework.beans.factory.support.DefaultListableBeanFactory#preInstantiateSingletons()方法

protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
        //实例化所有非延迟加载的bean
        beanFactory.preInstantiateSingletons();
    }

preInstantiateSingletons()中调用了org.springframework.beans.factory.support.AbstractBeanFactory#getBean(String name)方法,Bean的创建流程就是从这里开始的,getBean()自调用了doGetBean()方法,开始真正创建bean了

3.2 获取对象doGetBean方法解析

protected <T> T doGetBean() throws BeansException {
        Object bean;
        //标记:1
        Object sharedInstance = getSingleton(beanName);
        if (sharedInstance != null && args == null) {
            bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
        } else {
            try {
                if (mbd.isSingleton()) {
                     //标记:2
                    sharedInstance = getSingleton(beanName, () -> {
                        // 创建单例Bean的主要方法,返回的bean是完整的
                        return createBean(beanName, mbd, args);
                    });
                    bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
                }
            }

        }
        return (T) bean;
    }

doGetBean()方法的主要逻辑为:

  • 根据beanName尝试从缓存中取对象LagouBean
  • 取到就返回对应的实例
  • 未取到,则进行bean的创建

其中重点标记处:

  • 标记1: 调用org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(String beanName)方法,尝试从缓存中取对象
  • 标记2: 判断如果要创建的对象是单例对象,调用getSingleton的重载方法getSingleton(String beanName, ObjectFactory<?> singletonFactory)进行对象的创建,它的内部,有两段重点代码:
//标记:2-1
singletonObject = singletonFactory.getObject();
//标记:2-2
addSingleton(beanName, singletonObject);
  • 标记2-1: 从上面的标记2中可以看出singletonFactory.getObject()调用的是方法createBean(beanName, mbd, args)返回最终创建的对象
  • 标记2-2: 新创建的bean添加到【一级缓存】中,以后getBean的时候就可以直接获取了,源码如下:
protected void addSingleton(String beanName, Object singletonObject) {
        synchronized (this.singletonObjects) {
            // 将新创建的bean添加到一级缓存中
            this.singletonObjects.put(beanName, singletonObject);
            // 从其他缓存中移除相关的bean
            this.singletonFactories.remove(beanName);
            this.earlySingletonObjects.remove(beanName);
  }
}

3.3 创建对象doCreateBean源码解析

上面说到创建对象的方法在createBean(beanName, mbd, args),内部调用了org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean方法,其内部逻辑分为5大步,这里将其拆开来剖析:

3.3.1 实例化对象
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
            throws BeanCreationException {
          BeanWrapper instanceWrapper = null;
        
        // 调用无参构造实例化Bean
        if (instanceWrapper == null) {
            instanceWrapper = createBeanInstance(beanName, mbd, args);
        }
        // 实例化后的Bean对象,这里获取到的是一个原始对象,即没有进行属性填充的对象
        final Object bean = instanceWrapper.getWrappedInstance();
    }

这部分的主要逻辑就是调用无参构造实例化,拿到一个原始的Bean

3.3.2 将原始对象放入三级缓存
protected Object doCreateBean() {
         boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
                isSingletonCurrentlyInCreation(beanName));
        if (earlySingletonExposure) {
            addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
        }
    }
  • earlySingletonExposure:是否”提前暴露“原始对象的引用,如果需要提前暴露单例bean,则将该bean工厂放入【三级缓存】中
  • 方法addSingletonFactory则是将原始对象包装成ObjectFactory放入三级缓存,源码如下:
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
        synchronized (this.singletonObjects) {
            if (!this.singletonObjects.containsKey(beanName)) {
                //向三级缓存里添加ObjectFactory
                this.singletonFactories.put(beanName, singletonFactory);
               }
        }
    }
  • 匿名内部方法 getEarlyBeanReference是SmartInstantiationAwareBeanPostProcessor接口的的一个默认方法,实现这个方法的只有org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator这个类,实现逻辑为:
public Object getEarlyBeanReference(Object bean, String beanName) {
        Object cacheKey = getCacheKey(bean.getClass(), beanName);
        this.earlyProxyReferences.put(cacheKey, bean);
        return wrapIfNecessary(bean, beanName, cacheKey);
    }

它内部自调用了方法 wrapIfNecessary:

protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
        // Create proxy if we have advice.
        if (specificInterceptors != DO_NOT_PROXY) {
            Object proxy = createProxy(
                    bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
            return proxy;
        }
        return bean;
    }

这个方法关键点在 createProxy,顾名思义它是为bean创建代理对象的,所以它的整个逻辑就是,如果当前bean需要创建代理对象,那么它返回的就是代理对象,否则返回原始对象。

那么getEarlyBeanReference在什么时候调用的?

还记得在3.2章节讲述doGetBean方法时提到首先会从缓存中尝试获取bean吗?如下:

protected <T> T doGetBean() throws BeansException {
    Object sharedInstance = getSingleton(beanName);
}

调用的getSingleton方法就是尝试从缓存中获取bean,这里来看一下它的源码:

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
        // 从 singletonObjects 获取实例,singletonObjects 中的实例都是准备好的 bean 实例,可以直接使用
        Object singletonObject = this.singletonObjects.get(beanName);
        if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
            synchronized (this.singletonObjects) {
                // 从二级缓存中获取单例bean
                singletonObject = this.earlySingletonObjects.get(beanName);
                // allowEarlyReference :表示是否允许从singletonFactories中通过getObject拿到对象
                if (singletonObject == null && allowEarlyReference) {
                    // 从三级缓存中获取单例bean,并移动到二级缓存
                    ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                    if (singletonFactory != null) {
                        // 通过单例工厂获取单例bean
                        singletonObject = singletonFactory.getObject();
                        // 从三级缓存移动到了二级缓存,并移除singletonFactory
                        this.earlySingletonObjects.put(beanName, singletonObject);
                        this.singletonFactories.remove(beanName);
                    }
                }
            }
        }
        return singletonObject;
    }

可以看到,从三级缓存获取对象时,调用了singletonFactory.getObject(),这里最终调用的就是匿名内部方法getEarlyBeanReference

所以可以说三级缓存的功效就是,在bean放入【二级代理】之前,通过ObjectFactory为当前bean创建代理对象(如果需要的话),保证别的bean引用它时是一个代理对象。

3.3.3 属性填充

经过上一小节的分析可知:

  • 原始对象已经创建成功
  • 原始对象包装成ObjectFactory放入了三级缓存

接下来,就是对bean进行属性填充了

protected Object doCreateBean() {
       ...
      populateBean(beanName, mbd, instanceWrapper);
      ...
}

属性填充调用了populateBean方法,它最终调用了 applyPropertyValues进行属性填充:

protected void applyPropertyValues(String beanName, BeanDefinition mbd, BeanWrapper bw, PropertyValues pvs) {
        
        for (PropertyValue pv : original) {
            if (pv.isConverted()) {
                deepCopy.add(pv);
            }
            else {
                //获取依赖的bean
                String propertyName = pv.getName();
                Object originalValue = pv.getValue();
                //获取依赖的对象
                Object resolvedValue = valueResolver.resolveValueIfNecessary(pv, originalValue);
        }
    }

方法applyPropertyValues内部遍历处理依赖,获取依赖的bean,调用org.springframework.beans.factory.support.BeanDefinitionValueResolver#resolveValueIfNecessary方法去获取被依赖的对象:

public Object resolveValueIfNecessary(Object argName, @Nullable Object value) {
        if (value instanceof RuntimeBeanReference) {
            RuntimeBeanReference ref = (RuntimeBeanReference) value;
            return resolveReference(argName, ref);
        }
    }

方法内部又自调用了resolveValueIfNecessary方法:

private Object resolveReference(Object argName, RuntimeBeanReference ref) {
        //获取或创建依赖对象
        bean = this.beanFactory.getBean(refName);
        return bean;
    }

方法 resolveReference根据获取的beanName调用beanFactory的getBean方法去获取依赖的bean,此时如果bean还未被创建,则会走一遍创建bean的流程,之后拿到依赖的bean,进行属性填充。

以文章开头的LagouBean与ItBean为例,说明一下整个流程:
(1)首先尝试从缓存中获取LagouBean,发现没有,则创建LagouBean,实例化后,放入三级缓存
(2)为LagouBean填充属性,发现依赖了ItBean
(3)创建ItBean,实例化后,放入三级缓存
(4)为ItBean填充属性,发现依赖了LagouBean,于是尝试从缓存中获取LagouBean
(5)发现LagouBean在三级缓存中,则调用对应的objectFactory.getObject()拿到对象LagouBean(如果LagouBean需要创建代理,也是在这一步完成的)
(6)拿到对象LagouBean后移动到二级缓存,可供其他对象引用了,ItBean也因此可以完成属性填充,初始化,完成创建,最后放入一级缓存,返回给LagouBean
(7) LagouBean拿到ItBean,继续走填充流程,直到填充完毕,创建完成,放入一级缓存,到此LagouBean就可以使用了,解决了循环依赖问题

通过上述流程发现,如果LagouBean 与ItBean都需要创建代理对象,LagouBean的代理对象的创建在第(5)可完成,因为ItBean的创建在LagouBean的属性填充过程中,创建完直接放入了一级缓存,没有走二级缓存,ItBean的代理对象好像并没有创建的地方,那么spring如何保证LagouBean最终依赖的是ItBean的代理对象呢?其实ItBean的代理对象发生在bean初始化这一步,它在属性填充之后执行。

3.3.4 初始化

初始化就是给创建好的bean做后置处理,就像创建了一个人,populateBean就是给人填充各种五脏六腑,血肉...初始化就是给人穿上外衣,如下图源码所示,initializeBean方法就是对bean进行初始化

protected Object doCreateBean() {
        //填充属性(依赖注入)
        populateBean(beanName, mbd, instanceWrapper);
        //调用初始化方法,完成bean的初始化操作
        exposedObject = initializeBean(beanName, exposedObject, mbd);
        return exposedObject;
    }

initializeBean方法做了4个动作,如下

protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
        invokeAwareMethods(beanName, bean);
        Object wrappedBean = bean;
        //before
        wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
        //触发初始化方法的调用
        invokeInitMethods(beanName, wrappedBean, mbd);
        //after
        wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
        return wrappedBean;
    }
  • invokeAwareMethods方法主要给bean设置beanName,beanfactory,applicationContext
  • applyBeanPostProcessorsBeforeInitialization指预初始化方法,内部比较简单
  • invokeInitMethods方法做了两件事:一个是调⽤InitializingBean
    的afterPropertiesSet⽅法,一个是调用自定义的init方法
protected void invokeInitMethods() {
        //调用InitializingBean的afterPropertiesSet()
        ((InitializingBean) bean).afterPropertiesSet();
        //调用自定义的init方法
        invokeCustomInitMethod(beanName, bean, mbd);
    }
  • applyBeanPostProcessorsAfterInitialization后初始化方法,它内部调用了接口org.springframework.beans.factory.config. BeanPostProcessorpostProcessAfterInitialization方法,这个方法的实现类为 org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator,它的源码如下,有两个重要的方法:
public abstract class AbstractAutoProxyCreator{

    public Object getEarlyBeanReference(Object bean, String beanName) {
        Object cacheKey = getCacheKey(bean.getClass(), beanName);
        //放进缓存earlyProxyReferences里,bean此时是原始对象,标记该对象是否被循环依赖
        this.earlyProxyReferences.put(cacheKey, bean);
        //创建代理对象
        return wrapIfNecessary(bean, beanName, cacheKey);
    }

    public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
        if (bean != null) {
            Object cacheKey = getCacheKey(bean.getClass(), beanName);
           // remove方法返回记录的bean
        // 若被循环引用了,说明执行了getEarlyBeanReference方法,remove返回值是==bean的
// 若没被循环引用,getEarlyBeanReference没执行 remove方法返回null,所以就进入创建代理对象步骤
            if (this.earlyProxyReferences.remove(cacheKey) != bean) {
                return wrapIfNecessary(bean, beanName, cacheKey);
            }
        }
        return bean;
    }
}

在3.3.2章节中讲过,getEarlyBeanReference是处理循环依赖的关键方法,它的执行要先于postProcessAfterInitialization,功能就是创建好代理对象,最终放入二级缓存,提前暴露代理对象的引用。

postProcessAfterInitialization方法执行在处理循环依赖之后,所以先进行判断有没有被循环引用过,没有循环引用过,则会创建它的代理对象返回。

所以,前文中所说的ItBean没有被循环引用,如果它需要创建代理对象的话,在postProcessAfterInitialization这里就会去创建它的代理对象。最终保证依赖ItBean的类也能拿到它的代理对象引用。

到此,循环依赖处理也就讲述结束了。最后附上一张我自己手绘的图方便理解:

循环依赖处理流程

4.spring循环依赖常见问题总结

1.作用域为prototype的 bean对象支持循环依赖处理吗?

不支持。prototype的 bean存在循环引用的话会直接跑出异常org.springframework.beans.factory.BeanCurrentlyInCreationException,spring解决循环依赖的核心思想就是基于java的引用对bean缓存,提前曝光给其他对象引用,对于prototype类型的每次请求都会创建一个bean,循环引用太多了的话,无法进行缓存处理,也就无法提前曝光引用。

2.如果构造器注入方式也有循环依赖情况,支持吗?

不支持。spring处理循环依赖的核心思想是基于java的引用传递,提前实例化好bean进行曝光。实例化bean需要调用构造器,存在循环依赖的情况时,会无法实例化提前曝光,所以构造器的循环依赖无法解决。

3.二级缓存能解决循环依赖吗?

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