AOP基础之动态代理

AOP即面向切面编程,实现的方式有很多,这篇文章主要介绍一下动态代理实现AOP的方式。主要从动态代理的原理进行分析。

1. jdk自带动态代理

代理分为静态代理和动态代理,静态代理这里就不多说了,直接看动态代理,下面看个小例子。
假设有个Login接口和LoginImpl实现类

public interface Login {
   boolean login(String userName,String passwd);
}
public class LoginImpl implements Login{
    @Override
    public boolean login(String userName, String passwd) {
        System.out.println("userName:"+userName+"passwd:"+passwd);
        return true;
    }
}

现在我们对login方法进行动态代理。动态代理实现方式有两种,jdk自带的动态代理和cglib方式。jdk自带的动态代理方式要求被代理对象必须实现至少一个接口,cglib则没有这个限制。但是cglib也有其自身的限制,就是被代理对象不能是final修饰的,同时final修饰的方法也是不能被代理的。看到这里,可能有些读者已经明白了其中的道理,其实试想一下,我们如果要实现对某个对象的代理,就要能拿到被代理对象的方法,大致有以下两种思路:

  • 通过接口,被代理类和代理类均实现相同的接口,代理类通过接口可以很轻松的拿到被代理类的方法。
  • 继承的方式,如果代理类继承了被代理类,那么很明显,通过子类进行方法增强,可以达到aop的目的,但是final类不能被继承。

猜测jdk自带的动态代理应该采用的是思路一,cglib应该采用的是思路二,是不是这样呢,我们去探索一下答案,先来看jdk自带的动态代理模式。实现起来很简单,首先需要实现InvocationHandler接口

public class ProxyHandler implements InvocationHandler{
    private Object target;
    public ProxyHandler(Object obj){
        this.target=obj;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object ret=null;
        System.out.println("before->"+method.getName());
        ret=method.invoke(target, args);
        System.out.println("after->"+method.getName());
        return ret;
    }
}

接着通过Proxy的newProxyInstance方法创建Proxy对象

public class Main {
     public static void main(String[] args)throws Exception 
        {  
            LoginImpl imp=new LoginImpl();
            Login login=(Login)Proxy.newProxyInstance(imp.getClass().getClassLoader(), imp.getClass().getInterfaces(), new ProxyHandler(imp));
            boolean ret=login.login("lanjunjian", "1234");
            System.out.println(ret);
      }
}

结果如下:可见我们成功的实现了动态代理。

before->login
userName:lanjunjian    passwd:1234
after->login
true

下面我们看一下动态代理是怎么实现的。动态代理其实就涉及到两个类,InvocationHandler和Proxy。InvocationHandler是个接口,只包含invoke方法,这里没什么可看的,直接查看下Proxy的newProxyInstance方法。

public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
    {
        if (h == null) {
            throw new NullPointerException();
        }
        Class cl = getProxyClass(loader, interfaces);
        try {
            Constructor cons = cl.getConstructor(constructorParams);
            return (Object) cons.newInstance(new Object[] { h });
        } catch (NoSuchMethodException e) {
            throw new InternalError(e.toString());
        } catch (IllegalAccessException e) {
            throw new InternalError(e.toString());
        } catch (InstantiationException e) {
            throw new InternalError(e.toString());
        } catch (InvocationTargetException e) {
            throw new InternalError(e.toString());
        }
    }

看起来很清晰,通过getProxyClass方法生成了代理对象的Class,然后调用代理对象的只含InvocationHandler的构造函数生成实例。接着看一下getProxyClass方法。

private final static String proxyClassNamePrefix = "$Proxy";
String proxyName = proxyPkg + proxyClassNamePrefix + num;
public static Class<?> getProxyClass(ClassLoader loader,
                                       Class<?>... interfaces)
      throws IllegalArgumentException
  {
      if (interfaces.length > 65535) {
          throw new IllegalArgumentException("interface limit exceeded");
      }
      Class proxyClass = null;
      Set interfaceSet = new HashSet();       // for detecting duplicates
      for (int i = 0; i < interfaces.length; i++) {
          String interfaceName = interfaces[i].getName();
          Class interfaceClass = null;
          try {
              interfaceClass = Class.forName(interfaceName, false, loader);
          } catch (ClassNotFoundException e) {
          }
          if (interfaceClass != interfaces[i]) {
              throw new IllegalArgumentException(
                  interfaces[i] + " is not visible from class loader");
          }
          interfaceSet.add(interfaceClass);
          interfaceNames[i] = interfaceName;
      }
            ...
            ...
          byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
                  proxyName, interfaces);
           proxyClass = defineClass0(loader, proxyName,
                      proxyClassFile, 0, proxyClassFile.length);
          proxyClasses.put(proxyClass, null);
          ...
          ...
      return proxyClass;
  }

篇幅原因,省略了部分代码,代码很清晰,没有太多可以解释的,主要是进行收集接口,然后转交给ProxyGenerator的generateProxyClass生成字节码的byte数组。ProxyGenerator类并不属于J2SE规范,代码位于sun.misc包下。我们大致看一下ProxyGenerator源码,字节码生成的过程是在generateClassFile中完成的。

  private final static boolean saveGeneratedFiles =
        java.security.AccessController.doPrivileged(
            new GetBooleanAction(
                "sun.misc.ProxyGenerator.saveGeneratedFiles")).booleanValue();

    /**
     * Generate a proxy class given a name and a list of proxy interfaces.
     */
    public static byte[] generateProxyClass(final String name,
                                            Class[] interfaces)
    {
        ProxyGenerator gen = new ProxyGenerator(name, interfaces);
        final byte[] classFile = gen.generateClassFile();

        if (saveGeneratedFiles) {
            java.security.AccessController.doPrivileged(
            new java.security.PrivilegedAction() {
                public Object run() {
                    try {
                        FileOutputStream file =
                            new FileOutputStream(dotToSlash(name) + ".class");
                        file.write(classFile);
                        file.close();
                        return null;
                    } catch (IOException e) {
                        throw new InternalError(
                            "I/O exception saving generated file: " + e);
                    }
                }
            });
        }
        return classFile;
    }
 private byte[] generateClassFile() {
        addProxyMethod(hashCodeMethod, Object.class);
        addProxyMethod(equalsMethod, Object.class);
        addProxyMethod(toStringMethod, Object.class);
        for (int i = 0; i < interfaces.length; i++) {
            Method[] methods = interfaces[i].getMethods();
            for (int j = 0; j < methods.length; j++) {
                addProxyMethod(methods[j], interfaces[i]);
            }
        }
        for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
            checkReturnTypes(sigmethods);
        }
        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));
                    methods.add(pm.generateMethod());
                }
            }
            methods.add(generateStaticInitializer());
        } catch (IOException e) {
            throw new InternalError("unexpected I/O Exception");
        }

        if (methods.size() > 65535) {
            throw new IllegalArgumentException("method limit exceeded");
        }
        if (fields.size() > 65535) {
            throw new IllegalArgumentException("field limit exceeded");
        }
        cp.getClass(dotToSlash(className));
        cp.getClass(superclassName);
        for (int i = 0; i < interfaces.length; i++) {
            cp.getClass(dotToSlash(interfaces[i].getName()));
        }
        cp.setReadOnly();
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        DataOutputStream dout = new DataOutputStream(bout);
        try {
            dout.writeInt(0xCAFEBABE);
            dout.writeShort(CLASSFILE_MINOR_VERSION);
            dout.writeShort(CLASSFILE_MAJOR_VERSION);
            cp.write(dout);  
            dout.writeShort(ACC_PUBLIC | ACC_FINAL | ACC_SUPER);
            dout.writeShort(cp.getClass(dotToSlash(className)));
            dout.writeShort(cp.getClass(superclassName));
            dout.writeShort(interfaces.length);
                                        // u2 interfaces[interfaces_count];
            for (int i = 0; i < interfaces.length; i++) {
                dout.writeShort(cp.getClass(
                    dotToSlash(interfaces[i].getName())));
            }
            dout.writeShort(fields.size());
            for (FieldInfo f : fields) {
                f.write(dout);
            }
            dout.writeShort(methods.size());
            for (MethodInfo m : methods) {
                m.write(dout);
            }
            dout.writeShort(0); // (no ClassFile attributes for proxy classes)
        } catch (IOException e) {
            throw new InternalError("unexpected I/O Exception");
        }
        return bout.toByteArray();
    }

generateClassFile展示了Proxy类生成的全过程,包括了接口声明的所有方法,此外还包括Object类中的三个方法hashCode,equals,toString方法。具体的addProxyMethod这里就不分析了,我们可以想办法拿到动态代理生成的字节码。动态代理是在运行期进行的,默认是不保存字节码文件的,怎么保存字节码呢?我们看到上文中generateProxyClass方法会判断saveGeneratedFiles是否为true。ok,很明显我们在代理前将这个变量设置为true即可。我们运行前加入如下代码,可在工程根目录下得到包名为com.sun.proxy的Proxy[num].class文件,num一般情况下为0。

public static void saveGeneratedJdkProxyFiles() throws Exception {
            Field field = System.class.getDeclaredField("props");
            field.setAccessible(true);
            Properties props = (Properties) field.get(null);
        props.put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
}

下面我们反编译一下$Proxy0.class。

public final class $Proxy0 extends Proxy implements Login
{
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m0;
    public $Proxy0(final InvocationHandler invocationHandler) {
        super(invocationHandler);
    }
    public final boolean equals(final Object o) {
        try {
            return (boolean)super.h.invoke(this, $Proxy0.m1, new Object[] { o });
        }
        catch (Error | RuntimeException t) {
            throw t;
        }
        catch (Throwable t2) {
            throw new UndeclaredThrowableException(t2);
        }
    }
    public final String toString() {
        try {
            return (String)super.h.invoke(this, $Proxy0.m2, null);
        }
        catch (Error | RuntimeException t) {
            throw t;
        }
        catch (Throwable t2) {
            throw new UndeclaredThrowableException(t2);
        }
    }
    public final boolean login(final String s, final String s2) {
        try {
            return (boolean)super.h.invoke(this, $Proxy0.m3, new Object[] { s, s2 });
        }
        catch (Error | RuntimeException t) {
            throw t;
        }
        catch (Throwable t2) {
            throw new UndeclaredThrowableException(t2);
        }
    }
    public final int hashCode() {
        try {
            return (int)super.h.invoke(this, $Proxy0.m0, null);
        }
        catch (Error | RuntimeException t) {
            throw t;
        }
        catch (Throwable t2) {
            throw new UndeclaredThrowableException(t2);
        }
    }
    static {
        try {
            $Proxy0.m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            $Proxy0.m2 = Class.forName("java.lang.Object").getMethod("toString", (Class<?>[])new Class[0]);
            $Proxy0.m3 = Class.forName("com.ljj.Login").getMethod("login", Class.forName("java.lang.String"), Class.forName("java.lang.String"));
            $Proxy0.m0 = Class.forName("java.lang.Object").getMethod("hashCode", (Class<?>[])new Class[0]);
        }
        catch (NoSuchMethodException ex) {
            throw new NoSuchMethodError(ex.getMessage());
        }
        catch (ClassNotFoundException ex2) {
            throw new NoClassDefFoundError(ex2.getMessage());
        }
    }
}

可以看到$Proxy继承了Proxy类并实现了Login接口,在静态代码块中加载了所有需要代理的方法,方法的调用都是通过InvocationHandler的invoke方法转发的。这里也看出来了由于java的单继承的限制,jdk自带的动态代理是无法代理普通类的,换句话说即使某个类实现了某个接口,但主要不是接口内定义的方法都是无法进行代理的。此外,动态代理后方法的调用只能通过反射来进行,性能上会有一些开销。

2. cglib动态代理

为了弥补jdk自带动态代理的限制,出现了cglib,可以实现类的动态代理,像spring框架就是jdk动态代理和cglib结合进行的,jdk动态代理搞不定的利用cglib进行,cglib引入了ASM库来进行底层字节码生成。这里简单的介绍一下。
同样定义一个Login类,不实现任何接口

public class Login {
    public boolean login(String name,String passwd){
        System.out.println("name->:"+name+"passwd->:"+passwd);
        return false;
    }
}

第一步实现一个MethodInterceptor类似于InvocationHandler

public class Hacker implements MethodInterceptor{
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        // TODO Auto-generated method stub
        System.out.println("xxxxxxbefore");
        Object ret=proxy.invokeSuper(obj, args);
        System.out.println("xxxxxxafter");
        return ret;
    }
}

第二步直接调用

 public static void main(String[] args)throws Exception 
        {    System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "/Users/ljj/Documents/workspace/HookTest"); 
         Test1 test=new Test1();
         Hacker hacker=new Hacker();
         Enhancer enhancer=new Enhancer();
         enhancer.setSuperclass(test.getClass());
         enhancer.setCallback(hacker);
         Test1 proxy=(Test1)enhancer.create();
         proxy.login("lanjunjian", "12346");
        }

由于android中加载的是dex文件,不是class文件,cglib不支持android系统,所以cglib的具体实现过程就不详细说了。具体原理可以参考这篇文章说说cglib动态代理。简单理解cglib是采用继承的方式进行代理。生成的代理类是继承自被代理类的。

public class Test1$$EnhancerByCGLIB$$d42d7d8c extends Test1 implements Factory

此外,cglib采用了FastClass机制,FastClass就是根据方法签名保存了代理类和被代理类的索引信息,然后为每个方法生成一个MethodProxy,proxy中有Invoke和invokeSuper两个方法,当我们调用invokeSuper时,根据方法签名去FastClass可以找到被代理类的方法,然后直接进行调用。所以cglib和动态代理很大的区别是 cglib使用的是直接调用,jdk是利用的反射。也有人利用dexmaker实现了android上的cglib,项目地址:MethodInterceptProxy

3.动态代理在android上的简单应用

代理可以理解为是hook的一种手段,例如插件框架中替换Instrumention,实际上采用的是静态代理的方式,但是很多情况下,接口或类可能是hide的,我们无法通过继承或者接口实现等方式构造代理类,这种情况下我们就没法使用静态代理,可以酌情考虑动态代理。
我认为进行动态代理最大的难点在于hook的点很难找,主要能找到hook点,一切也好办。一般情况下,hook点最好是静态或者是单例,有些时候很难找到实例对象,而且往往我们都需要借助反射来获取被代理对象。下面就以发送通知为例,假设我要拦截每次发送通知的内容该怎么做呢?

 Intent intent=new Intent();
 Notification build = new Notification.Builder(this)
     .setContentTitle("hook")
     .setContentText("拦截通知")
     .setAutoCancel(true)
     .setSmallIcon(R.mipmap.ic_launcher)
     .setWhen(System.currentTimeMillis())
     .setContentIntent(PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
     .build();
 NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
 manager.notify((int) (System.currentTimeMillis()/1000L), build);

上一段是我们典型的发送通知的代码。整个发送的过程是由NotificationManager来控制的,我们知道通知的发送是一个跨进程的操作,这里由于篇幅原因,不去详细谈Binder相关的内容,只是为了从主观上感受下动态代理在android方面怎么使用。

 public void notifyAsUser(String tag, int id, Notification notification, UserHandle user)
    {
        int[] idOut = new int[1];
        INotificationManager service = getService();
        String pkg = mContext.getPackageName();
        // Fix the notification as best we can.
        Notification.addFieldsFromContext(mContext, notification);
        if (notification.sound != null) {
            notification.sound = notification.sound.getCanonicalUri();
            if (StrictMode.vmFileUriExposureEnabled()) {
                notification.sound.checkFileUriExposed("Notification.sound");
            }
        }
        fixLegacySmallIcon(notification, pkg);
        if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1) {
            if (notification.getSmallIcon() == null) {
                throw new IllegalArgumentException("Invalid notification (no valid small icon): "
                        + notification);
            }
        }
        if (localLOGV) Log.v(TAG, pkg + ": notify(" + id + ", " + notification + ")");
        final Notification copy = Builder.maybeCloneStrippedForDelivery(notification);
        try {
            service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id,
                    copy, idOut, user.getIdentifier());
            if (localLOGV && id != idOut[0]) {
                Log.v(TAG, "notify: id corrupted: sent " + id + ", got back " + idOut[0]);
            }
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

当调用manager.notify时,会调用到NotificationManager的notifyAsUser方法,可以看到整个发送流程都是通过INotificationManager接口进行的。一看到接口感觉应该可以做点什么,我们进一步看一下getService方法。

private static INotificationManager sService;
    /** @hide */
    static public INotificationManager getService()
    {
        if (sService != null) {
            return sService;
        }
        IBinder b = ServiceManager.getService("notification");
        sService = INotificationManager.Stub.asInterface(b);
        return sService;
    }

首先在ServiceManager通过getService方法获取到了一个原声的IBinder对象,然后通过AIDL机制由asInterface方法转换成了本地的代理对象,INotificationManager是一个由AIDL接口生成的本地代理对象,正好sService是一个static变量,我们通过反射获取到该对象然后替换成我们的Proxy是不是就能实现通知的拦截了呢?想一下我们动态代理的实现过程,操作一下。我们需要三个要素,接口的Class对象,获取被代理对象和一个InvocationHandler的子类。

  • 获取接口class对象很简单,反射即可。
Class<?> INotificationManagerClazz = Class.forName("android.app.INotificationManager");
  • 获取被代理对象,也就是要拿到sService,可以通过反射拿到,我们可以通过发射sService变量拿到,也可以通过反射调用getService方法获取,如果直接发射sService变量,此时有可能获取到的为null,所以采用反射getService方法获取。
 NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
      Method method = notificationManager.getClass().getDeclaredMethod("getService");
      method.setAccessible(true);
      final Object sService = method.invoke(notificationManager);
  • InvocationHandler的子类直接实现一个即可。
    准备工作做完了,我们直接进行代理的生成,同时要记得用proxy替换原来的sService,所有的工作就完成了。
 Object proxy = Proxy.newProxyInstance(INotificationManagerClazz.getClassLoader(),
          new Class[]{INotificationManagerClazz},new ProxyHandler(sService));
      Field target = notificationManager.getClass().getDeclaredField("sService");
      target.setAccessible(true);
      target.set(notificationManager, proxy);

我们怎么拦截内容呢,观察notifyAsUser方法中会调用enqueueNotificationWithTag方法,我们只需要拦截这个方法即可。

class ProxyHandler implements InvocationHandler {
    private Object mObject;
    public ProxyHandler(Object mObject) {
      this.mObject = mObject;
    }
    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      if (method.getName().equals("enqueueNotificationWithTag")) {
        for (int i = 0; i < args.length; i++) {
          if(args[i] instanceof Notification){
            Notification notification=(Notification)(args[i]);
            String content=notification.extras.getString(Notification.EXTRA_TEXT);
            Log.i("ljj", "invoke: "+content);
          }
        }
        return method.invoke(mObject, args);
      }
      return null;
    }
  }

这里说明一下,通知的内容在不同版本里获取方式不太一样,这里只是为了直观的体现动态代理的作用,没有进行适配,下面给出完整的hook代码。

  public  void hookNotificationContent(Context context) {
    try {
      NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
      Method method = notificationManager.getClass().getDeclaredMethod("getService");
      method.setAccessible(true);
      final Object sService = method.invoke(notificationManager);//获取到Nofificiaton原来的sService对象
      Class<?> INotificationManagerClazz = Class.forName("android.app.INotificationManager");
      Object proxy = Proxy.newProxyInstance(INotificationManagerClazz.getClassLoader(),
          new Class[]{INotificationManagerClazz},new ProxyHandler(sService));
      Field target = notificationManager.getClass().getDeclaredField("sService");
      target.setAccessible(true);
    target.set(notificationManager, proxy);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

最后总结一下,这篇文章比较基础,主要想搞明白以下三个知识点,希望对大家有所帮助。

  • jdk动态代理的原理以及为什么只能对接口做代理
  • cglib与jdk动态代理的区别
  • 在android的应用场景

参考文献

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

推荐阅读更多精彩内容