Spring AOP的实现方式

一、AOP是什么?AOP与拦截器的区别?

太抽象的不说,如果你知道Struts2的拦截器,拦截器就是应用的AOP的思想,它用于拦截Action以进行一些预处理或结果处理。而Spring的AOP是一种更通用的模式,可以拦截Spring管理的Bean,功能更强大,适用范围也更广,它是通过动态代理与反射机制实现的。

二、AOP一些概念

1.通知(Advice)

通知定义了在切入点代码执行时间点附近需要做的工作。

Spring支持五种类型的通知:

Before(前)  org.apringframework.aop.MethodBeforeAdvice

After-returning(返回后) org.springframework.aop.AfterReturningAdvice

After-throwing(抛出后) org.springframework.aop.ThrowsAdvice

Arround(周围) org.aopaliance.intercept.MethodInterceptor

Introduction(引入) org.springframework.aop.IntroductionInterceptor

2.连接点(Joinpoint)

程序能够应用通知的一个“时机”,这些“时机”就是连接点,例如方法调用时、异常抛出时、方法返回后等等。

3.切入点(Pointcut)

通知定义了切面要发生的“故事”,连接点定义了“故事”发生的时机,那么切入点就定义了“故事”发生的地点,例如某个类或方法的名称,Spring中允许我们方便的用正则表达式来指定。

4.切面(Aspect)

通知、连接点、切入点共同组成了切面:时间、地点和要发生的“故事”。

5.引入(Introduction)

引入允许我们向现有的类添加新的方法和属性(Spring提供了一个方法注入的功能)。

6.目标(Target)

即被通知的对象,如果没有AOP,那么通知的逻辑就要写在目标对象中,有了AOP之后它可以只关注自己要做的事,解耦合!

7.代理(proxy)

应用通知的对象,详细内容参见设计模式里面的动态代理模式。

8.织入(Weaving)

把切面应用到目标对象来创建新的代理对象的过程,织入一般发生在如下几个时机:

(1)编译时:当一个类文件被编译时进行织入,这需要特殊的编译器才可以做的到,例如AspectJ的织入编译器;

(2)类加载时:使用特殊的ClassLoader在目标类被加载到程序之前增强类的字节代码;

(3)运行时:切面在运行的某个时刻被织入,SpringAOP就是以这种方式织入切面的,原理应该是使用了JDK的动态代理技术。

三、使用AOP的方式

1.经典的基于代理的AOP

2.@AspectJ注解驱动的切面

3.纯POJO切面

4.注入式AspectJ切面

四、如何实现AOP

 1.代理方式实现AOP

(1)可睡觉的接口,任何可以睡觉的人或机器都可以实现它。

   public interface Sleepable {

    public void sleep();

    }

(2)接口实现类,“Me”可以睡觉,“Me”就实现可以睡觉的接口。

   public class Me implements Sleepable{

         public void sleep() {

         System.out.println("\n睡觉!不休息哪里有力气学习!\n");

}

}

(3)Me关注于睡觉的逻辑,但是睡觉需要其他功能辅助,比如睡前脱衣服,起床脱衣服,这里开始就需要AOP替“Me”完成!解耦!首先需要一个SleepHelper类。因为一个是切入点前执行、一个是切入点之后执行,所以实现对应接口。

public class SleepHelper implements MethodBeforeAdvice, AfterReturningAdvice {

public void before(Method arg0, Object[] arg1, Object arg2) throws Throwable {

System.out.println("睡觉前要脱衣服!");

}

public void afterReturning(Object arg0, Method arg1, Object[] arg2, Object arg3) throws Throwable {

System.out.println("起床后要穿衣服!");

}

}

(4)最关键的来了,Spring核心配置文件application.xml配置AOP。

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

<span style="white-space:pre"> </span>xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

<span style="white-space:pre"> </span>xmlns:aop="http://www.springframework.org/schema/aop"

<span style="white-space:pre"> </span>xsi:schemaLocation="http://www.springframework.org/schema/beans

<span style="white-space:pre"> </span>http://www.springframework.org/schema/beans/spring-beans-3.0.xsd

<span style="white-space:pre"> </span>http://www.springframework.org/schema/aop

<span style="white-space:pre"> </span>http://www.springframework.org/schema/aop/spring-aop-3.0.xsd ">

  <!-- 定义被代理者 -->

  <bean id="me" class="com.springAOP.bean.Me"></bean>

  <!-- 定义通知内容,也就是切入点执行前后需要做的事情 -->

  <bean id="sleepHelper" class="com.springAOP.bean.SleepHelper"></bean>

  <!-- 定义切入点位置 -->

  <bean id="sleepPointcut" class="org.springframework.aop.support.JdkRegexpMethodPointcut">

<property name="pattern" value=".*sleep"></property>

  </bean>

  <!-- 使切入点与通知相关联,完成切面配置 -->

  <bean id="sleepHelperAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">

<property name="advice" ref="sleepHelper"></property> 

  <property name="pointcut" ref="sleepPointcut"></property>

  </bean>

  <!-- 设置代理 -->

  <bean id="proxy" class="org.springframework.aop.framework.ProxyFactoryBean">

<!-- 代理的对象,有睡觉能力 -->

<property name="target" ref="me"></property>

<!-- 使用切面 -->

<property name="interceptorNames" value="sleepHelperAdvisor"></property>

<!-- 代理接口,睡觉接口 -->

<property name="proxyInterfaces" value="com.springAOP.bean.Sleepable"></property>

  </bean>

</beans>

其中:

<beans>是Spring的配置标签,beans里面几个重要的属性:

xmlns:

是默认的xml文档解析格式,即spring的beans。地址是http://www.springframework.org/schema/beans;通过设置这个属性,所有在beans里面声明的属性,可以直接通过<>来使用,比如<bean>等等。一个XML文件,只能声明一个默认的语义解析的规范。例如上面的xml中就只有beans一个是默认的,其他的都需要通过特定的标签来使用,比如aop,它自己有很多的属性,如果要使用,前面就必须加上aop:xxx才可以。类似的,如果默认的xmlns配置的是aop相关的语义解析规范,那么在xml中就可以直接写config这种标签了。

xmlns:xsi:

是xml需要遵守的规范,通过URL可以看到,是w3的统一规范,后面通过xsi:schemaLocation来定位所有的解析文件。

xmlns:aop:

这个是重点,是我们这里需要使用到的一些语义规范,与面向切面AOP相关。

xmlns:tx:

Spring中与事务相关的配置内容。

(5)测试类,Test,其中,通过AOP代理的方式执行Me的sleep()方法,会把执行前、执行后的操作执行,实现了AOP的效果!

public class Test {

public static void main(String[] args){

@SuppressWarnings("resource")

//如果是web项目,则使用注释的代码加载配置文件,这里是一般的Java项目,所以使用下面的方式

    //ApplicationContext appCtx = new ClassPathXmlApplicationContext("application.xml");

ApplicationContext appCtx = new FileSystemXmlApplicationContext("application.xml");

Sleepable me = (Sleepable)appCtx.getBean("proxy");

me.sleep();

}

}

执行结果:

(6)通过org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator简化配置。

将配置文件中设置代理的代码去掉,加上:

<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>

然后,在Test中,直接获取me对象,执行sleep方法,就可以实现同样的功能!

通过自动匹配,切面会自动匹配符合切入点的bean,会被自动代理,实现功能!

————————————————

2.更简单的方式,通过AspectJ提供的注解实现AOP

1)同样的例子,修改后的SleepHelper:

@Aspect

public class SleepHelper{

    public SleepHelper(){


    }


    @Pointcut("execution(* *.sleep())")

    public void sleeppoint(){}


    @Before("sleeppoint()")

    public void beforeSleep(){

        System.out.println("睡觉前要脱衣服!");

    }


    @AfterReturning("sleeppoint()")

    public void afterSleep(){

        System.out.println("睡醒了要穿衣服!");

    }


}

(2)在方法中,可以加上JoinPoint参数以进行相关操作,如:

//当抛出异常时被调用

    public void doThrowing(JoinPoint point, Throwable ex)

    {

        System.out.println("doThrowing::method "

                + point.getTarget().getClass().getName() + "."

                + point.getSignature().getName() + " throw exception");

        System.out.println(ex.getMessage());

    }

(3)然后修改配置为:

        <aop:aspectj-autoproxy />

<!-- 定义通知内容,也就是切入点执行前后需要做的事情 -->

<bean id="sleepHelper" class="com.springAOP.bean.SleepHelper"></bean>

<!-- 定义被代理者 -->

<bean id="me" class="com.springAOP.bean.Me"></bean>

(4)最后测试,一样的结果!

public class Test {

public static void main(String[] args){

@SuppressWarnings("resource")

//如果是web项目,则使用注释的代码加载配置文件,这里是一般的Java项目,所以使用下面的方式

    //ApplicationContext appCtx = new ClassPathXmlApplicationContext("application.xml");

ApplicationContext appCtx = new FileSystemXmlApplicationContext("application.xml");

Sleepable me = (Sleepable)appCtx.getBean("me");

me.sleep();

}

}

3.使用Spring来定义纯粹的POJO切面(名字很绕口,其实就是纯粹通过<aop:fonfig>标签配置,也是一种比较简单的方式)

1)修改后的SleepHelper类,很正常的类,所以这种方式的优点就是在代码中不体现任何AOP相关配置,纯粹使用xml配置。

public class SleepHelper{

    public void beforeSleep(){

        System.out.println("睡觉前要脱衣服!");

    }


    public void afterSleep(){

        System.out.println("睡醒了要穿衣服!");

    }


}

(2)配置文件:

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"

xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-3.0.xsd

http://www.springframework.org/schema/aop

http://www.springframework.org/schema/aop/spring-aop-3.0.xsd ">

<!-- 定义通知内容,也就是切入点执行前后需要做的事情 -->

<bean id="sleepHelper" class="com.springAOP.bean.SleepHelper"></bean>

<!-- 定义被代理者 -->

<bean id="me" class="com.springAOP.bean.Me"></bean>

<aop:config>

<aop:aspect ref="sleepHelper">

<aop:before method="beforeSleep" pointcut="execution(* *.sleep(..))" />

<aop:after method="afterSleep" pointcut="execution(* *.sleep(..))" />

</aop:aspect>

</aop:config>

</beans>

(3)配置的另一种写法

<aop:config>

<aop:aspect ref="sleepHelper">

            <aop:pointcut id="sleepHelpers" expression="execution(* *.sleep(..))" />

            <aop:before pointcut-ref="sleepHelpers" method="beforeSleep" />

            <aop:after pointcut-ref="sleepHelpers" method="afterSleep" />     

        </aop:aspect>

</aop:config>

五、AspectJ与AOP与CGLIB

AspectJ与AOP

1.spectJ是一套独立的面向切面编程的解决方案。

1.1AspectJ 安装

      AspectJ 下载地址(http://www.eclipse.org/aspectj/downloads.php)。

1.2下载AspectJ  jar包,然后双击安装。安装好的目录结构为:

bin:存放了 aj、aj5、ajc、ajdoc、ajbrowser 等命令,其中 ajc 命令最常用,它的作用类似于 javac

doc:存放了 AspectJ 的使用说明、参考手册、API 文档等文档

lib:该路径下的 4 个 JAR 文件是 AspectJ 的核心类库

1.3AspectJ HelloWorld 实现

业务组件SayHelloServicepackagecom.ywsc.fenfenzhong.aspectj.learn;publicclassSayHelloService{publicvoidsay(){System.out.print("Hello  AspectJ");}}需要来了,在需要在调用say()方法之后,需要记录日志。那就是通过AspectJ的后置增强吧。

LogAspect日志记录组件,实现对com.ywsc.fenfenzhong.aspectj.learn.SayHelloService后置增强packagecom.ywsc.fenfenzhong.aspectj.learn;publicaspectLogAspect{pointcutlogPointcut():execution(voidSayHelloService.say());after():logPointcut(){System.out.println("记录日志 ...");}}

编译SayHelloService

执行命令  ajc-d.SayHelloService.javaLogAspect.java生成SayHelloService.class执行命令    javaSayHelloService输出HelloAspectJ记录日志

ajc.exe 可以理解为 javac.exe 命令,都用于编译 Java 程序,区别是 ajc.exe 命令可识别 AspectJ 的语法;我们可以将 ajc.exe 当成一个增强版的 javac.exe 命令.执行ajc命令后的 SayHelloService.class 文件不是由原来的 SayHelloService.java 文件编译得到的,该 SayHelloService.class 里新增了打印日志的内容——这表明 AspectJ 在编译时“自动”编译得到了一个新类,这个新类增强了原有的 SayHelloService.java 类的功能,因此 AspectJ 通常被称为编译时增强的 AOP 框架。

与 AspectJ 相对的还有另外一种 AOP 框架,它不需要在编译时对目标类进行增强,而是运行时生成目标类的代理类,该代理类要么与目标类实现相同的接口,要么是目标类的子类——总之,代理类的实例可作为目标类的实例来使用。一般来说,编译时增强的 AOP 框架在性能上更有优势——因为运行时动态增强的 AOP 框架需要每次运行时都进行动态增强。

2.Spring AOP也是对目标类增强,生成代理类。但是与AspectJ的最大区别在于---Spring AOP的运行时增强,而AspectJ是编译时增强。

      曾经以为AspectJ是Spring AOP一部分,是因为Spring AOP使用了AspectJ的Annotation。使用了Aspect来定义切面,使用Pointcut来定义切入点,使用Advice来定义增强处理。虽然使用了Aspect的Annotation,但是并没有使用它的编译器和织入器。其实现原理是JDK 动态代理,在运行时生成代理类。

CGLIB与JDK动态代理

1、JDK动态代理

利用拦截器(拦截器必须实现InvocationHanlder)加上反射机制生成一个实现代理接口的匿名类,

在调用具体方法前调用InvokeHandler来处理。

2、CGLIB动态代理

利用ASM开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。

3、何时使用JDK还是CGLIB?

1)如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP。

2)如果目标对象实现了接口,可以强制使用CGLIB实现AOP。

3)如果目标对象没有实现了接口,必须采用CGLIB库,Spring会自动在JDK动态代理和CGLIB之间转换。

4、如何强制使用CGLIB实现AOP?

1)添加CGLIB库(aspectjrt-xxx.jar、aspectjweaver-xxx.jar、cglib-nodep-xxx.jar)

2)在Spring配置文件中加入<aop:aspectj-autoproxy proxy-target-class="true"/>

4.4具体实现

接口:

package com.jpeony.spring.proxy.compare;

/**

* 用户管理接口(真实主题和代理主题的共同接口,这样在任何可以使用真实主题的地方都可以使用代理主题代理。)

* --被代理接口定义

*/

public interface IUserManager {

    void addUser(String id, String password);

}

实现类:

package com.jpeony.spring.proxy.compare;

/**

* 用户管理接口实现(被代理的实现类)

*/

public class UserManagerImpl implements IUserManager {

    @Override

    public void addUser(String id, String password) {

        System.out.println("======调用了UserManagerImpl.addUser()方法======");

    }

}

JDK代理实现:

package com.jpeony.spring.proxy.compare;

import java.lang.reflect.InvocationHandler;

import java.lang.reflect.Method;

import java.lang.reflect.Proxy;

/**

* JDK动态代理类

*/

public class JDKProxy implements InvocationHandler {

    /** 需要代理的目标对象 */

    private Object targetObject;

    /**

    * 将目标对象传入进行代理

    */

    public Object newProxy(Object targetObject) {

        this.targetObject = targetObject;

        //返回代理对象

        return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(),

                targetObject.getClass().getInterfaces(), this);

    }

    /**

    * invoke方法

    */

    @Override

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        // 一般我们进行逻辑处理的函数比如这个地方是模拟检查权限

        checkPopedom();

        // 设置方法的返回值

        Object ret = null;

        // 调用invoke方法,ret存储该方法的返回值

        ret  = method.invoke(targetObject, args);

        return ret;

    }

    /**

    * 模拟检查权限的例子

    */

    private void checkPopedom() {

        System.out.println("======检查权限checkPopedom()======");

    }

}

CGLIB代理实现:

package com.jpeony.spring.proxy.compare;

import net.sf.cglib.proxy.Enhancer;

import net.sf.cglib.proxy.MethodInterceptor;

import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

/**

* CGLibProxy动态代理类

*/

public class CGLibProxy implements MethodInterceptor {

    /** CGLib需要代理的目标对象 */

    private Object targetObject;

    public Object createProxyObject(Object obj) {

        this.targetObject = obj;

        Enhancer enhancer = new Enhancer();

        enhancer.setSuperclass(obj.getClass());

        enhancer.setCallback(this);

        Object proxyObj = enhancer.create();

        // 返回代理对象

        return proxyObj;

    }

    @Override

    public Object intercept(Object proxy, Method method, Object[] args,

                            MethodProxy methodProxy) throws Throwable {

        Object obj = null;

        // 过滤方法

        if ("addUser".equals(method.getName())) {

            // 检查权限

            checkPopedom();

        }

        obj = method.invoke(targetObject, args);

        return obj;

    }

    private void checkPopedom() {

        System.out.println("======检查权限checkPopedom()======");

    }

}

客户端测试类:

package com.jpeony.spring.proxy.compare;

/**

* 代理模式[[ 客户端--》代理对象--》目标对象 ]]

*/

public class Client {

    public static void main(String[] args) {

        System.out.println("**********************CGLibProxy**********************");

        CGLibProxy cgLibProxy = new CGLibProxy();

        IUserManager userManager = (IUserManager) cgLibProxy.createProxyObject(new UserManagerImpl());

        userManager.addUser("jpeony", "123456");

        System.out.println("**********************JDKProxy**********************");

        JDKProxy jdkPrpxy = new JDKProxy();

        IUserManager userManagerJDK = (IUserManager) jdkPrpxy.newProxy(new UserManagerImpl());

        userManagerJDK.addUser("jpeony", "123456");

    }

}

程序运行结果:


5、JDK动态代理和CGLIB字节码生成的区别?

1)JDK动态代理只能对实现了接口的类生成代理,而不能针对类。

2)CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,

     并覆盖其中方法实现增强,但是因为采用的是继承,所以该类或方法最好不要声明成final,

     对于final类或方法,是无法继承的。

6、CGlib比JDK快?

1)使用CGLib实现动态代理,CGLib底层采用ASM字节码生成框架,使用字节码技术生成代理类,

在jdk6之前比使用Java反射效率要高。唯一需要注意的是,CGLib不能对声明为final的方法进行代理,

因为CGLib原理是动态生成被代理类的子类。

2)在jdk6、jdk7、jdk8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于CGLIB代理效率,

只有当进行大量调用的时候,jdk6和jdk7比CGLIB代理效率低一点,但是到jdk8的时候,jdk代理效率高于CGLIB代理,

总之,每一次jdk版本升级,jdk代理效率都得到提升,而CGLIB代理消息确有点跟不上步伐。

7、Spring如何选择用JDK还是CGLIB?

1)当Bean实现接口时,Spring就会用JDK的动态代理。

2)当Bean没有实现接口时,Spring使用CGlib是实现。

3)可以强制使用CGlib(在spring配置中加入<aop:aspectj-autoproxy proxy-target-class="true"/>)。

8.总结JDK动态代理与CGLIB

JDK代理是不需要第三方库支持,只需要JDK环境就可以进行代理,使用条件:

1)实现InvocationHandler 

2)使用Proxy.newProxyInstance产生代理对象

3)被代理的对象必须要实现接口

CGLib必须依赖于CGLib的类库,但是它需要类来实现任何接口代理的是指定的类生成一个子类,

覆盖其中的方法,是一种继承但是针对接口编程的环境下推荐使用JDK的代理


原文链接:https://blog.csdn.net/yhl_jxy/java/article/details/80635012

原文链接:https://www.jianshu.com/p/fe8d1e8bd63e

原文链接:https://blog.csdn.net/zhangliangzi/java/article/details/52334964

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