走进Spring框架系列—IOC应用进阶实战

一、简介

在上一章节《走进Spring框架系列—IOC应用进阶篇》中,笔者一一介绍了几个Spring框架中比较进阶的应用,为了更加深刻体会它们的作用,本章笔者将会带大家使用它们模拟实现简易版的Spring AOP和@MapperScan。

本章的两个模拟都是参考原版的源码,整体实现思路是一致的,所以,如果你对Spring AOP或者@MapperScan的原理感兴趣的话,也请不要错过本章节。

二、AOP模拟

这里实现AOP我要用到的是ImportSelector和BeanPostProcessor,真实的Spring AOP用的是ImportBeanDefinitionRegistrar和BeanPostProcessor,因为它在注册一个BeanPostProcessor前需要动态更改bean的一些属性和参数,我这里就不做那么麻烦了,直接用ImportSelector注册就好。

由于这里只是模拟一个AOP,所以只会提供大概的实现思路,有些复杂的具体逻辑我这里会写死,比如Spring AOP让我们提供的切面,里面包含了切点,切入逻辑等等,还有JDK和CGLib的选择。

阅读本模拟案例,需要你对JDK动态代理的使用有了解

废话不多说,先定义一个代理类生成接口:

/**
 * @Author: Richard Lv
 * @Date: 2021/3/1 15:51
 * @Description: 为了规范编程,我们还是定义一个代理类生成接口,
 * 在我们模拟的AOP中,该接口只有唯一一个实现类,就是使用JDK生成代理的类,
 * 在Spring AOP中也有这样一个类似的接口,它的实现类就有JDK和CGLib两个代理生成类
 */
public interface AopProxy {
    Object createProxy(Class<?> clazz,Object target);
}

然后编写该接口唯一的实现类,由于要生成代理类,所以顺便也实现了InvocationHandler接口:

/**
 * @Author: Richard Lv
 * @Date: 2021/3/1 15:52
 * @Description: JDK生成代理工厂类
 */
public class MyJDKProxy implements AopProxy, InvocationHandler {
    //目标对象
    private Object target;

    @Override
    public Object createProxy(Class<?> clazz,Object target) {
        //获得需要代理的类的所有接口
        Class<?>[] interfaces = clazz.getInterfaces();
        //给目标对象变量赋值
        this.target=target;
        //创建JDK动态代理并返回
        return Proxy.newProxyInstance(this.getClass().getClassLoader(),interfaces,this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        /*
         * 代理逻辑,我这里就简单写了一个打印"我被代理了"字符串
         * 真实的Spring AOP会在这里利用beanFactory得到我们写的切面类
         * 获取里面的连接点,织入逻辑等等,然后在这里反射调用
         */
        System.out.println("我被代理了");
        /*
         * 反射调用目标方法
         */
        method.invoke(target,args);
        return proxy;
    }
}

接下来就开始写一个BeanPostProcessor拦截真实类,并替换成代理类

/**
 * @Author: Richard Lv
 * @Date: 2021/3/1 15:49
 * @Description: 简易版AOP后置处理器
 */
public class MyAopPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        /*
         * 这里要注意几点:
         * 1.为什么没有在MyJDKProxy类上加@Component然后在这里直接使用@Autowired注入进来?
         *   很简单,因为我这里是在模拟Spring AOP,那么在Spring框架源码中,它怎么可能使用@Component和@Autowired,这
         *   两个注解本来就是Spring框架定义的,那不就套娃了吗,在Spring框架源码中,对象要么是new出来的,要么是反射生成的
         * 2.为什么把new MyJDKProxy()写死了?
         *   再次强调,我这里是模拟简易的AOP,所以只写了这一个实现类,要完全模拟的话,还应该有一个MyCGLibProxy实现类,甚至
         *   还可以扩展其他动态代理实现逻辑类,那么,这里就会根据用户配置来判断应该用哪一个类。
         * 3. 贴出Spring AOP源码证实上面两点: 
         * public org.springframework.aop.framework.AopProxy createAopProxy(AdvisedSupport config) throws
         * AopConfigException {
         *    if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
         *        Class<?> targetClass = config.getTargetClass();
         *       if (targetClass == null) {
         *            throw new AopConfigException("TargetSource cannot determine target class: " +
         *                    "Either an interface or a target is required for proxy creation.");
         *        }
         *        if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
         *            return new JdkDynamicAopProxy(config);
         *        }
         *        return new ObjenesisCglibAopProxy(config);
         *    }
         *   else {
         *        return new JdkDynamicAopProxy(config);
         *    }
         * }
         * 这个方法只有一些if else,很容易读,可以看到,spring aop把创建代理生成类封装成了一个方法,并传了一个配置信息类作为参数
         * 然后根据配置信息,创建对应的代理生成类
         */
        AopProxy aopProxy=new MyJDKProxy();
        //这里Spring AOP会提前解析切面,用来判断当前bean是否需要代理,我这里因为没有切面,就直接对所有类进行代理
        return aopProxy.createProxy(bean.getClass(),bean);
    }
}

至此就完成了一半,然后定义一个开启AOP功能的注解开关,忘了的可以返回上一篇文章看ImportSelector相关内容:

@Retention(RetentionPolicy.RUNTIME)
@Import({MyImportSelector.class})
public @interface EnableMyAop {
}

编写MyImportSelector类实现ImportSelector接口,动态把我的MyAopPostProcessor放入spring容器:

/**
 * @Author: Richard Lv
 * @Date: 2021/2/25 16:14
 * @Description: 动态把处理AOP的BeanPostProcessor交给spring容器
 */
public class MyImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        //检查加了@Import注解的类上有没有@EnableMyAop注解
        if(annotationMetadata.hasAnnotation(EnableMyAop.class.getName())){
            return new String[]{MyAopPostProcessor.class.getName()};
        }
        return new String[0];
    }
}

最后,搞个类来测试AOP效果,因为JDK动态代理是基于接口的,所以写一个接口,让我的Richard类实现它:

/**
 * @Author: Richard Lv
 * @Date: 2021/3/1 17:36
 * @Description:
 */
public interface Human {
    void selfIntroduction();
}

改造后的Richard:

/**
 * @Author: Richard Lv
 * @Date: 2021/2/23 17:59
 * @Description:
 */
@Component("richard")
public class Richard implements Human{
    @Override
    public void selfIntroduction(){
        System.out.println("Hello everyone,My name is Richard");
    }
}

大功告成,开始测试,首先,不在配置类上加我的@EnableMyAop注解,正常运行主类:

/**
 * @Author: Richard Lv
 * @Date: 2021/1/29 10:44
 * @Description: 测试类
 */
public class Test {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext beanFactory=new AnnotationConfigApplicationContext(SpringConfig.class);
        /*
         * 一旦被动态代理,这里获取的就不是原先的Richard类,而是一个代理类,所以我用Human接口来接收,
         * 开启AOP后,这里也不用修改代码,这也是面向抽象编程的好处。
         */
        Human richard = (Human) beanFactory.getBean("richard");
        richard.selfIntroduction();
    }
}
运行结果:
Hello everyone,My name is Richard

在配置类上加上@EnableMyAop,开启我的简易版AOP:

/**
 * @Author: Richard Lv
 * @Date: 2021/2/2 18:27
 * @Description: spring 配置类
 */

@Configuration
@ComponentScan("com.richard")
@EnableMyAop
public class SpringConfig { }

再次运行主类,得到结果打印如下:

我被代理了
Hello everyone,My name is Richard

至此,一个简单的AOP功能就完成了。其实,从本案例中,对AOP概念模糊的读者也可以从中体会到AOP面向切面的真正含义,一个不影响原方法逻辑的增强功能,那么我们就可以把它提出来作为AOP切面逻辑,通过开关(注解)的开启,我们就可以让指定的类获得增强功能,在实际应用中,这样的增强功能就是日志、事务等等,那么又如何让一个类的方法获得增强?那就是使用代理,这就是AOP的本质。

三、@MapperScan模拟

不熟悉@MapperScan应用的读者也大可放心阅读,几乎不涉及其真实应用,这一个模拟用到了ImportBeanDefinitionRegistrar和FactoryBean。

与AOP模拟一样,省略了一些与接口演示无关的逻辑,比如扫描包,执行sql语句等等。

先写一个Mapper接口:

/**
 * @Author: Richard Lv
 * @Date: 2021/3/2 13:52
 * @Description: 
 */
public interface UserMapper {
   @Select("SELECT * FROM t_userinfo WHERE id = 1")
    User getUser();
}

我们知道,接口是无法被实例化的,所以我们也不可能直接把这个接口交给spring容器来管理,那么我们就需要在运行期间动态生成接口的实现类,怎么做呢?用JDK动态代理,生成一个接口对应的代理类,那么又有一个问题了,一个动态代理类能不能被spring容器实例化出来呢,目前,我们不知道,那就先试一试,写一个自定义的ImportBeanDefinitionRegistrar实现类,来注入一个对mapper接口生成的代理类:

/**
 * @Author: Richard Lv
 * @Date: 2021/2/25 17:37
 * @Description: 自定义的ImportBeanDefinitionRegistrar,可以动态注入一个bean给spring容器
 */
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        /*
         * 本来应该解析用户配置然后扫描指定包下所有的mapper接口,然后遍历循环进行下列操作
         * 但本次模拟对简化扫描操作,直接对UserMapper单个接口进行注册
         */
        //对UserMapper接口生成代理类
        Object o = Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{UserMapper.class},
                (proxy, method, args) -> {
                    System.out.println("执行sql逻辑");
                    return null;
                });
        /*
         * registerBeanDefinition(String s, BeanDefinition beanDefinition)方法是我们最终要向spring容器注册bean的方法,
         * 第一个参数是bean的名字,方便我们向容器取出bean
         * 第二个参数是一个数据结构,叫BeanDefinition,它是spring容器对bean的定义,也就是存的bean的元数据,相当于java
         * 中用来描述一个类的类Clazz
         * spring提供了方法让我们去创建一个BeanDefinition,先根据我们要创建bean的对象的类对象Clazz获取一个BeanDefinitionBuilder,
         * BeanDefinitionBuilder就能为我们创建出一个BeanDefinition
         */
        //根据生成的代理类的类对象创建BeanDefinitionBuilder
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(o.getClass());
        //BeanDefinitionBuilder为我们创建一个BeanDefinition,它返回的是一个父类对象AbstractBeanDefinition,
        //我们这里把它强转为更常用的基本BeanDefinition类型GenericBeanDefinition,当然,你要用父类对象也是可以的,只是子类对象有更丰富的API
        GenericBeanDefinition beanDefinition = (GenericBeanDefinition) beanDefinitionBuilder.getBeanDefinition();
        //向spring容器注册BeanDefinition
        registry.registerBeanDefinition("mapper",beanDefinition);
    }
}

基本完成,现在定义我自己的MapperScan注解,并import上面的MyImportBeanDefinitionRegistrar:

@Retention(RetentionPolicy.RUNTIME)
@Import({MyImportBeanDefinitionRegistrar.class})
public @interface MyMapperScan {
}

配置类加上注解:

@Configuration
@ComponentScan(value = "com.richard")
@MyMapperScan
public class SpringConfig { }

修改主类:

/**
 * @Author: Richard Lv
 * @Date: 2021/1/29 10:44
 * @Description: 测试类
 */
public class Test {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext beanFactory=new AnnotationConfigApplicationContext(SpringConfig.class);
        UserMapper mapper = (UserMapper) beanFactory.getBean("mapper");
        mapper.getUser();
    }
}

运行发现报错了:

警告: Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'mapper': Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'java.lang.reflect.InvocationHandler' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
Exception in thread "main" org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'mapper': Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'java.lang.reflect.InvocationHandler' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:797)
    at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:227)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1356)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1203)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:556)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:516)
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:324)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:322)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:897)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:879)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:551)
    at org.springframework.context.annotation.AnnotationConfigApplicationContext.<init>(AnnotationConfigApplicationContext.java:89)
    at com.richard.Test.main(Test.java:22)

很明显,我们的代理类没法被实例化,因为spring没法维护里面的依赖关系,那怎么办呢?还记得之前讲的FactoryBean吗,这时候它就能排上用场了,FactoryBean是可以被实例化的,我们在它的getObject()方法中来创建这个代理类不就可以了,说干就干,写一个自定义的FactoryBean:

/**
 * @Author: Richard Lv
 * @Date: 2021/3/2 17:09
 * @Description: 自定义的FactoryBean,顺便把代理用的InvocationHandler接口也实现了
 */
public class MyMapperFactoryBean implements FactoryBean, InvocationHandler {
    @Override
    public Object getObject() throws Exception {
        Object o = Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{UserMapper.class}, this);
        return o;
    }

    @Override
    public Class<?> getObjectType() {
        return UserMapper.class;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("执行sql逻辑");
        return null;
    }
}

修改MyImportBeanDefinitionRegistrar类:

/**
 * @Author: Richard Lv
 * @Date: 2021/2/25 17:37
 * @Description: 自定义的ImportBeanDefinitionRegistrar,可以动态注入一个bean给spring容器
 */
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(MyMapperFactoryBean.class);
        GenericBeanDefinition beanDefinition = (GenericBeanDefinition) beanDefinitionBuilder.getBeanDefinition();
        registry.registerBeanDefinition("mapper",beanDefinition);
    }
}

再次运行主类,结果打印如下:

执行sql逻辑

那么基本功能就完成了,下面进行一些优化,MyMapperFactoryBean中我们把代理接口类型写死了,所以我们要增加一个构造方法,把需要代理的接口类型给传过来,顺便,我们把执行sql逻辑那里也改造一下,让其更贴近真实应用:

/**
 * @Author: Richard Lv
 * @Date: 2021/3/2 17:09
 * @Description: 自定义的FactoryBean,顺便把代理用的InvocationHandler接口也实现了
 */
public class MyMapperFactoryBean implements FactoryBean, InvocationHandler {

    Class<?> clazz;

    /**
     * 加入该构造方法,让接口类型作为参数传进来
     * 那么这个MyMapperFactoryBean对象就可以为任何接口提供注入方式
     */
    public MyMapperFactoryBean(Class<?> clazz){
        this.clazz=clazz;
    }

    @Override
    public Object getObject() throws Exception {
        return Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{clazz}, this);
    }

    @Override
    public Class<?> getObjectType() {
        return clazz;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //获取方法上的@Select注解的值,返回的是一个数组,为了简洁,我直接取第0个
        String value = method.getDeclaredAnnotation(Select.class).value()[0];
        System.out.println("执行sql:"+value);
        return null;
    }
}

MyMapperFactoryBean就改造好了,但还没完,MyMapperFactoryBean之前只有一个默认的无参构造方法,Spring容器拿到它的Clazz类型,就可以直接通过Clazz.newInstance()反射生成实例对象,但是现在只有个带参构造方法,spring容器就没法去实例化它了,所以我们要修改它的BeanDefinition,设置参数名字让spring容器能通过byName方式把对应的接口类型注入进来,这里不懂的可以先看看有个映像,以后源码章节会讲解相关知识:

/**
 * @Author: Richard Lv
 * @Date: 2021/2/25 17:37
 * @Description: 自定义的ImportBeanDefinitionRegistrar,可以动态注入一个bean给spring容器
 */
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        //Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(MyMapperScan.class.getName());
        //假如我的@MyMapperScan中定义了一个value属性用来让用户配置需要扫描的包路径,这里我就要取出value属性的值
        //assert attributes != null;
        //String packagePath= (String) attributes.get("value");
        //假设有一个scanPackages()方法,参数就是用户配置的包路径,该方法会返回指定路径下所有的mapper接口,然后遍历接口集合
        //for (Class<?> clazz:scanPackages(packagePath)){
            BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(MyMapperFactoryBean.class);
            GenericBeanDefinition beanDefinition = (GenericBeanDefinition) beanDefinitionBuilder.getBeanDefinition();
            /*设置构造方法的参数名字,这里写死为UserMapper接口的类型名字,在真实应用中大概是注释代码中所展示的样子*/
            //beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(clazz.getTypeName());
            beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(UserMapper.class.getTypeName());
            registry.registerBeanDefinition("mapper",beanDefinition);
        //}
    }
}

再次运行主类,结果打印如下:

执行sql:SELECT * FROM t_userinfo WHERE id = 1

至此,两个模拟实战就完成了,如有任何问题都欢迎留言指出。

《走进Spring框架系列—初识IOC》
《走进Spring框架系列—IOC应用》
《走进Spring框架系列—IOC应用进阶篇》
《走进Spring框架系列—IOC应用进阶实战》

本系列文章参考-------Spring官网以及Spring框架源码

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