揭开动态代理的面纱: Jdk/Cglib动态代理

写在前面
  1. 代理模式
    代理模式,指的是给目标对象提供一个代理对象,并由代理对象控制对目标对象的引用。
    为什么要引入这个代理对象呢,两个目的:
  • 通过代理对象来间接访问目标对象,这样能防止直接访问目标对象带来的一些不必要的复杂性。
    (什么不必要的麻烦呢?例如如果客户端需要访问的对象在服务端,客户端直接访问需要处理网络等一系列复杂的问题,如果使用代理模式那么客户端只需要和代理打交道,客户端不需要知道代理怎么和服务端交互。)
  • 通过代理对象来访问目标对象,能通过代理对象对原有业务进行增强。


  1. 代理模式有三种实现方式:
    静态代理,Jdk动态代理,Cglib动态代理。
    其中,静态代理和动态代理的区别在于代理类的生成时间不同。
静态代理

静态代理三大要素:

  • 抽象对象(接口,约定了服务能够提供的功能)
  • 真实对象(实现类)
  • 代理对象

真实对象及代理对象都必须实现这个接口
代理对象必须包含真实对象。(它不提供服务,只是服务的搬运工)

面向对象设计开发的几个原则之一:
开闭原则(开放-封闭原则):程序对外扩展开放,对修改关闭。换句话说,当需求发生变化时,我们可以通过添加新模块来满足新需求,而不是通过修改原来的实现代码来满足新需求。

静态代理作为代理模式的第一个实现版本,违反了开闭原则:

  • 让这个类的可扩展性大打折扣。
  • 可维护性下降,牵一发,动全身。

而动态代理,很好的解决了这个缺点。

动态代理

何谓动态?动态指的是代理对象不是固定对象,而是一个动态对象。
动态代理没有源文件,直接在内存生成类的字节码文件。

  1. Jdk动态代理
*Tips*

在研究Jdk动态代理之前,先提出几点疑问:
a. 为什么Jdk动态代理一定要基于接口?
b. 怎么在内存生成字节码?
c. 生成的字节码长什么样?
d. 多个对象,实现同一个接口,若对这多个对象分别进行代理,会生成多个代理对象吗?
e. 多个对象,实现不同接口,若对这多个对象分别进行代理,是否会生成多个代理对象呢?

接下来,带着这几个问题来研究Jdk动态代理的底层源码。

1.1 Jdk动态代理的两大关键点:Proxy类和InvocationHandler接口。
Proxy - 调度器,生成代理对象

        JdkProxy dynamicProxy=new JdkProxy(hello);
        Greeting target1=(Greeting) Proxy.newProxyInstance(hello.getClass().getClassLoader(),
                hello.getClass().getInterfaces(), dynamicProxy);

InvocationHandler - 代理对象到底有什么业务能力

public class JdkProxy implements InvocationHandler {

    private Object target;
    
    public JdkProxy(Object obj){
        this.target=obj;
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        
        Object result=null;
        before();
        result=method.invoke(target, args);
        after();
        return result;
    }
    public void before(){
        System.out.println("[JdkProxy] Come to someone.");
    }
    public void after(){
        System.out.println("[JdkProxy] Back to his own corner");
    }
}

1.2 探究Proxy类
Proxy类位于java.lang.reflect包中,追踪Proxy类的newProxyInstance方法,会定位到ProxyBuilder的如下方法,截取关键部分:

    private static final class ProxyBuilder {
        private static final Unsafe UNSAFE = Unsafe.getUnsafe();

        // prefix for all proxy class names
        private static final String proxyClassNamePrefix = "$Proxy";

        private static Class<?> defineProxyClass(Module m, List<Class<?>> interfaces) {
            //......
            /*
             * Choose a name for the proxy class to generate.
             */
            long num = nextUniqueNumber.getAndIncrement();
            String proxyName = proxyPkg.isEmpty()
                                    ? proxyClassNamePrefix + num
                                    : proxyPkg + "." + proxyClassNamePrefix + num;
            //......
            /*
             * Generate the specified proxy class.
             */
            byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
                    proxyName, interfaces.toArray(EMPTY_CLASS_ARRAY), accessFlags);
            try {
                Class<?> pc = UNSAFE.defineClass(proxyName, proxyClassFile,
                                                 0, proxyClassFile.length,
                                                 loader, null);
                reverseProxyCache.sub(pc).putIfAbsent(loader, Boolean.TRUE);
                return pc;
            } catch (ClassFormatError e) {
                throw new IllegalArgumentException(e.toString());
            }
        }
}

其中,generateProxyClass就是在内存里直接生成字节码的地方!它内部调用了generateClassFile方法(参见附录),这个方法就会根据Class文件的结构,来动态拼接出代理对象的字节码。
而UNSAFE.defineClass,就是直接根据刚才生成的字节码,生成Class对象。

这个defineClass方法,最终调用的是一个native方法。native方法是用C写的本地方法,直接调用操作系统的类库,来进行生成Class对象。

    public native Class<?> defineClass0(String name, byte[] b, int off, int len,
                                        ClassLoader loader,
                                        ProtectionDomain protectionDomain);

1.3 看一看动态生成的字节码文件
既然已经在内存中生成了字节码文件,那么就把它保存到硬盘上,满足一下好奇心,看看里面是个什么玩意儿吧。
在ProxyGenerator中,有这样一个属性,它询问是否需要将动态代理的对象存到硬盘。(这里是基于jdk9的demo,如果用的是其它版本的jdk,那么属性可以会略有不同)

    /** debugging flag for saving generated class files */
    private static final boolean saveGeneratedFiles =
        java.security.AccessController.doPrivileged(
            new GetBooleanAction(
      "jdk.proxy.ProxyGenerator.saveGeneratedFiles")).booleanValue();

既然如此,那么只要将这个属性设为true,就能直接查看硬盘上的Class文件了。

System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");

果不其然,在项目目录下,生成了com/sun/proxy子目录,下面有$Proxy0.class字节码文件。这个类继承了Proxy类,并且实现了Greeting接口。

public final class $Proxy0 extends Proxy  implements Greeting  {
}
  //接口代理方法
    public final void doGreet()
    {
        try
        {
            // invocation handler的 invoke方法在这里被调用
            super.h.invoke(this, m3, null);
            return;
        }
        catch (Error ) { }
        catch (Throwable throwable)
        {
            throw new UndeclaredThrowableException(throwable);
        }
    }

1.4 关于InvocationHandler
1.4.1 InvocationHandler是一个接口。

public interface InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

它被作为参数传给底层方法newProxyInstance(caller, cons, h)。这个方法里有一行注释:

    private static Object newProxyInstance(Class<?> caller, // null if no SecurityManager
                                           Constructor<?> cons,
                                           InvocationHandler h) {
        /*
         * Invoke its constructor with the designated invocation handler.
         */
        //......
    }

1.4.2 追踪这个方法,会发现最终调用了一个native方法,InvocationHandler被作为参数传过去了。

    private static native Object newInstance0(Constructor<?> c, Object[] args)
        throws InstantiationException,
               IllegalArgumentException,
               InvocationTargetException;

这个类实际做的事情就是:

  • 根据前面生成的类$Proxy0的字节码,来实例化这个类
  • 并且,将invocationHandler传递给$Proxy0的父类Proxy的构造函数,初始化Proxy的属性h
    /**
     * Constructs a new {@code Proxy} instance from a subclass
     * (typically, a dynamic proxy class) with the specified value
     * for its invocation handler.
     *
     * @param  h the invocation handler for this proxy instance
     *
     * @throws NullPointerException if the given invocation handler, {@code h},
     *         is {@code null}.
     */
    protected Proxy(InvocationHandler h) {
        Objects.requireNonNull(h);
        this.h = h;
    }

1.4.3 invoke方法的参数

  • InvocationHandler接口定义了一个invoke方法,它接收三个参数。
public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;

第二个参数method,和第三个参数args,指的是代理类被调用的方法和参数。
而第一个参数proxy,很少会被用到,其实它指的就是代理对象。将代理对象传递进来有两个用处:
可以通过反射获取代理对象的信息。
可以将代理对象返回进行连续调用。

  • 用一个例子感受一下。(截取部分代码,完整代码见附录)
    实现InvocationHandler的invoke方法。
    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        before();
        method.invoke(target, args);
        after();
        System.out.println("JdkProxy.this is : " + this.getClass());
        System.out.println("proxy is : " + proxy.getClass());
        
        return proxy;
    }

通过代理类调用目标方法。

        Meeting videoMeeting = new VideoMeeting();
        dynamicProxy = new JdkProxy(videoMeeting);
        Meeting target4 = (Meeting) Proxy.newProxyInstance(videoMeeting.getClass().getClassLoader(), 
                videoMeeting.getClass().getInterfaces(), dynamicProxy);
        target4.haveMeeting().haveMeeting();

执行,通过输出,可以发现proxy就是动态生成的代理类$Proxy1,而目标方法通过连续调用,被代理类调用了两次。

[JdkProxy] Come to someone.
Meeting by video .
[JdkProxy] Back to his own corner
JdkProxy.this is : class spring.core.aop.jdk.JdkProxy
proxy is : class com.sun.proxy.$Proxy1
[JdkProxy] Come to someone.
Meeting by video .
[JdkProxy] Back to his own corner
JdkProxy.this is : class spring.core.aop.jdk.JdkProxy
proxy is : class com.sun.proxy.$Proxy1

1.5 为什么Jdk动态代理必须基于接口

  • 这是由jdk动态代理的设计决定的
    通过查看动态代理类的代码,发现它继承了Proxy类。而Java是单继承,不能同时继承两个类,所以我们需要和想要代理的类建立联系,那就只能通过接口来实现。

  • 那么问题来了,jdk动态代理为什么要设计成只允许动态代理接口呢?
    在代理模式中,代理类只做一些额外的拦截处理,实际处理是转发到原始类做的。
    如果允许动态代理一个类,那么代理对象也会继承类的字段,而这些字段实际上是没有使用的,因为代理对象只做转发处理,对象的字段存取都是在原始对象上处理,所以如果代理一个类,对内存空间是一种浪费。

1.6 缓存机制
调用过程中发现,对于不同的类做动态代理,生成的代理对象,有时候是同一个对象。这是因为当需要生成代理对象字节码之前,会先查看缓存,是否已经存在同样classLoader&同样interface的代理对象,如果存在,则直接复用缓存中的class字节码。

  1. Cglib动态代理
    (Code Generator Library)
*Tips*

同样,在研究Cglib动态代理之前,同样提出几个问题:
a. 为什么Cglib动态代理不用基于接口?
b. 怎么在内存生成字节码?
c. 生成的字节码长什么样?

2.1 Cglib动态代理的两大关键点

  • Enhancer类
    类似于jdk动态代理中的Proxy类,只不过,Enhancer类既能代理普通的类,也能够代理接口。
    public <T> T getProxy(Class<T> cls){
        return (T) Enhancer.create(cls, this);
    }
  • MethodInterceptor接口
    重写接口中的intercept方法对目标方法进行拦截。
    @Override
    public Object intercept(Object obj, Method method, Object[] arg,
            MethodProxy proxy) throws Throwable {
        Object result=null;
        try {
            before();
            result= proxy.invokeSuper(obj, arg);
            after();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

2.2 追踪Enhancer.create()方法
会发现Cglib的底层是调用了ASM开源包,将代理对象类的class文件加载进来,通过修改其字节码生成子类来进行处理。

    private Object createHelper() {
        preValidate();
        Object key = KEY_FACTORY.newInstance((superclass != null) ? superclass.getName() : null,
                ReflectUtils.getNames(interfaces),
                filter == ALL_ZERO ? null : new WeakCacheKey<CallbackFilter>(filter),
                callbackTypes,
                useFactory,
                interceptDuringConstruction,
                serialVersionUID);
        this.currentKey = key;
        Object result = super.create(key);
        return result;
    }

2.3 关于MethodInterceptor
方法拦截器

2.4 动态代理对象的字节码文件

public class SayHello$$EnhancerByCGLIB$$4da4ebaf extends SayHello
    implements Factory
{
 }

2.5 当被代理方法或者对象被final修饰

  • 被代理方法被final修饰时,若进行代理,则增强无效。
  • 被代理对象被final修饰时,若进行代理,则运行时报错,如下。
Exception in thread "main" java.lang.IllegalArgumentException: Cannot subclass final class spring.core.aop.SayHello
  1. 比较jdk动态代理与cglib动态代理
    3.1 各自局限
    JDK动态代理:只能代理实现了接口的类。
    Cglib动态代理:由于它的原理是对指定的目标类生成一个子类,并覆盖其中方法实现增强,采用了继承,所以不能对final修饰的类进行代理。

3.2 各自优势
JDK动态代理:最小化依赖关系,减少依赖意味着简化开发和维护,由于JDK本身的支持,可能比Cglib更可靠。
Cglib动态代理:可以代理没有接口的类。并且性能相对更高。

3.3 性能比较(未考证)
jdk8之前,cglib效率更高。
jdk8及之后,jdk效率更高。

附上部分底层代码及示例代码
底层方法

generateClassFile

    private byte[] generateClassFile() {

        /* ============================================================
         * Step 1: Assemble ProxyMethod objects for all methods to
         * generate proxy dispatching code for.
         */

        /*
         * Record that proxy methods are needed for the hashCode, equals,
         * and toString methods of java.lang.Object.  This is done before
         * the methods from the proxy interfaces so that the methods from
         * java.lang.Object take precedence over duplicate methods in the
         * proxy interfaces.
         */
        addProxyMethod(hashCodeMethod, Object.class);
        addProxyMethod(equalsMethod, Object.class);
        addProxyMethod(toStringMethod, Object.class);

        /*
         * Now record all of the methods from the proxy interfaces, giving
         * earlier interfaces precedence over later ones with duplicate
         * methods.
         */
        for (Class<?> intf : interfaces) {
            for (Method m : intf.getMethods()) {
                addProxyMethod(m, intf);
            }
        }

        /*
         * For each set of proxy methods with the same signature,
         * verify that the methods' return types are compatible.
         */
        for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
            checkReturnTypes(sigmethods);
        }

        /* ============================================================
         * Step 2: Assemble FieldInfo and MethodInfo structs for all of
         * fields and methods in the class we are generating.
         */
        try {
            methods.add(generateConstructor());

            for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
                for (ProxyMethod pm : sigmethods) {

                    // add static field for method's Method object
                    fields.add(new FieldInfo(pm.methodFieldName,
                        "Ljava/lang/reflect/Method;",
                         ACC_PRIVATE | ACC_STATIC));

                    // generate code for proxy method and add it
                    methods.add(pm.generateMethod());
                }
            }

            methods.add(generateStaticInitializer());

        } catch (IOException e) {
            throw new InternalError("unexpected I/O Exception", e);
        }

        if (methods.size() > 65535) {
            throw new IllegalArgumentException("method limit exceeded");
        }
        if (fields.size() > 65535) {
            throw new IllegalArgumentException("field limit exceeded");
        }

        /* ============================================================
         * Step 3: Write the final class file.
         */

        /*
         * Make sure that constant pool indexes are reserved for the
         * following items before starting to write the final class file.
         */
        cp.getClass(dotToSlash(className));
        cp.getClass(superclassName);
        for (Class<?> intf: interfaces) {
            cp.getClass(dotToSlash(intf.getName()));
        }

        /*
         * Disallow new constant pool additions beyond this point, since
         * we are about to write the final constant pool table.
         */
        cp.setReadOnly();

        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        DataOutputStream dout = new DataOutputStream(bout);

        try {
            /*
             * Write all the items of the "ClassFile" structure.
             * See JVMS section 4.1.
             */
                                        // u4 magic;
            dout.writeInt(0xCAFEBABE);
                                        // u2 minor_version;
            dout.writeShort(CLASSFILE_MINOR_VERSION);
                                        // u2 major_version;
            dout.writeShort(CLASSFILE_MAJOR_VERSION);

            cp.write(dout);             // (write constant pool)

                                        // u2 access_flags;
            dout.writeShort(accessFlags);
                                        // u2 this_class;
            dout.writeShort(cp.getClass(dotToSlash(className)));
                                        // u2 super_class;
            dout.writeShort(cp.getClass(superclassName));

                                        // u2 interfaces_count;
            dout.writeShort(interfaces.length);
                                        // u2 interfaces[interfaces_count];
            for (Class<?> intf : interfaces) {
                dout.writeShort(cp.getClass(
                    dotToSlash(intf.getName())));
            }

                                        // u2 fields_count;
            dout.writeShort(fields.size());
                                        // field_info fields[fields_count];
            for (FieldInfo f : fields) {
                f.write(dout);
            }

                                        // u2 methods_count;
            dout.writeShort(methods.size());
                                        // method_info methods[methods_count];
            for (MethodInfo m : methods) {
                m.write(dout);
            }

                                         // u2 attributes_count;
            dout.writeShort(0); // (no ClassFile attributes for proxy classes)

        } catch (IOException e) {
            throw new InternalError("unexpected I/O Exception", e);
        }

        return bout.toByteArray();
    }
完整示例代码-静态代理
public interface Greeting {
    public void doGreet();
}
public class SayHello implements Greeting {
    @Override
    public void doGreet() {
        System.out.println("Greeting by say 'hello' .");
    }
}
public class GreetStaticProxy implements Greeting {

    private Greeting hello;//被代理对象
    public GreetStaticProxy(Greeting hello){
        this.hello=hello;
    }
    
    @Override
    public void doGreet() {
        before();//执行其他操作
        this.hello.doGreet();//调用目标方法
        after();//执行其他操作
    }

    public void before(){
        System.out.println("[StaticProxy] Come to someone.");
    }
    public void after(){
        System.out.println("[StaticProxy] Back to his own corner");
    }
}
public class Main {

    public static void main(String[] args) {
        Greeting hello=new SayHello();
        
        //静态代理
        GreetStaticProxy staticHelloProxy=new GreetStaticProxy(hello);
        staticHelloProxy.doGreet();
        System.out.println();
    }    

}
完整示例代码-JDK动态代理
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class JdkProxy implements InvocationHandler {

    private Object target;
    
    public JdkProxy(Object obj){
        this.target=obj;
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        before();
        method.invoke(target, args);
        after();
        System.out.println("JdkProxy.this is : " + this.getClass());
        System.out.println("proxy is : " + proxy.getClass());
        
        return proxy;
    }

    
    public Object getTarget() {
        return target;
    }

    public void setTarget(Object target) {
        this.target = target;
    }

    public void before(){
        System.out.println("[JdkProxy] Come to someone.");
    }
    public void after(){
        System.out.println("[JdkProxy] Back to his own corner");
    }
}
import java.lang.reflect.Proxy;

public class Main {
    public static void main(String[] args) {
            System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");
        
        Greeting hello=new SayHello();
        Greeting shakeHands=new ShakeHands();
        
        //jdk动态代理
        JdkProxy dynamicProxy=new JdkProxy(hello);
        Greeting target1=(Greeting) Proxy.newProxyInstance(hello.getClass().getClassLoader(),
                hello.getClass().getInterfaces(), dynamicProxy);
        target1.doGreet();
        System.out.println();          
    }    
}
完整示例代码-CGLIB动态代理
public final class SayHello {
    public final void doGreet() {
        System.out.println("Greeting by say 'hello' .");
    }
}
import java.lang.reflect.Method;

import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

public class CglibProxy implements MethodInterceptor {

    public static CglibProxy proxy=new CglibProxy();
    private CglibProxy(){}
    
    public static CglibProxy getInstance(){
        return proxy;
    }
    
    public <T> T getProxy(Class<T> cls){
        return (T) Enhancer.create(cls, this);
    }
    
    @Override
    public Object intercept(Object obj, Method method, Object[] arg,
            MethodProxy proxy) throws Throwable {
        Object result=null;
        try {
            before();
            result= proxy.invokeSuper(obj, arg);
            after();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    public void before(){
        System.out.println("[cglib] Come to someone.");
    }
    public void after(){
        System.out.println("[cglib] Back to his own corner.");
    }
}
public class Main {

    public static void main(String[] args) {
        
        //cglib代理
        SayHello targetProxy=CglibProxy.getInstance().getProxy(SayHello.class);
        targetProxy.doGreet();
        System.out.println();
        
CglibProxy.getInstance().getInstance().getProxy(KissHello.class).doGreet();    
    }    

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

推荐阅读更多精彩内容