入木三分:从设计者角度看Retrofit原理

前言

通常我不喜欢去写分析源码类的文章,流水线式的分析 枯燥乏味,但读完Retrofit源码后让我有了改变这种想法的冲动~~

一般来讲读源码的好处有两点:

  • 熟悉代码设计流程,使用过程碰到问题可以更快速解决。说实话仅这一点无法激起我读源码的兴趣,毕竟以正确的姿态使用一个优秀的框架不应该出现这种问题。
  • 一个优秀的框架必须要保证易用性、扩展性,所以作者定会引入大量的思考进行设计,如若我们能吸收一二,那何尝不是与作者进行了一次心灵交互呢!

今天我将带着我的理解,尝试从设计者的角度分析Retrofit原理,相信你认真读完再加以思考,当再被面试官问Retrofit时你的答复或许会让他眼前一亮

提示:Retrofit基于2.9.0。文中贴的源码可能会有部分缺失,这是我刻意为之,目的在于筛选掉无用信息增强可读性

目录

  • 1. 什么是REST ful API?
  • 2. 为什么将请求设置为(接口+注解)形式?
    • 2.1 迪米特法则和门面模式
    • 2.2 为什么通过门面模式设计ApiService?
  • 3. 动态代理其实不是工具
    • 3.1 Retrofit构建
    • 3.2 何为动态代理?
    • 3.3 动态代理获取ApiService并发起请求
  • 4. ReturnT、ResponseT做一次适配的意义何在?
    • 4.1 创建HttpServiceMethod
    • 4.2 创建callAdapter、responseConverter
    • 4.3 发起请求

1. 什么是REST ful API?

一句话概括REST ful API:在我们使用HTTP协议做数据传输时应当遵守HTTP的规矩,包括请求方法资源类型Uri格式等等..

不久在再群里看到某小伙伴提出一个问题:“应后端要求需要在GET请求加入BodyRetrofitGET 请求添加Body会报错,如何解决?” 一时间讨论的好不热闹,有让把Body塞到Header里的,有让自定义拦截器、也有人直接怂恿改源码...但问题的本质不是后端先违反规则在先吗?两个人打架总不能把挨打的抓起来吧。

俗话说无规矩不成方圆,面对以上这种情况应当让错误方去修改,因为所有人都知道GET没有Body,否则一旦其他人接手你的代码很容易被搞懵。

RetrofitREST ful API的兼容做的很优秀,不符合规范直接给你报错,强行规范你的代码。所以你们公司正在使用REST ful APIRetrofit将是你的不二选择

2. 为什么将请求设置为(接口+注解)形式?

该小节为前置知识

2.1 迪米特法则和门面模式

  • 迪米特法则:也称之为最小知道原则,即模块之间尽量减少不必要的依赖,即降低模块间的耦合性。
  • 门面模式:基于迪米特法则拓展出来的一种设计模式,旨在将复杂的模块/系统访问入口控制的更加单一。 举个例子:现要做一个获取图片功能,优先从本地缓存获取,没有缓存从网络获取随后再加入到本地缓存,假如不做任何处理,那每获取一张图片都要写一遍缓存逻辑,写的越多出错的可能就越高,其实调用者只是想获取一张图片而已,具体如何获取他不需要关心。此时可以通过门面模式将缓存功能做一个封装,只暴露出一个获取图片入口,这样调用者使用起来更加方便而且安全性更高。其实函数式编程也是门面模式的产物

2.2 为什么通过门面模式设计ApiService?

用Retrofit做一次请求大致流程如下:

interface ApiService {
    /**
     * 获取首页数据
     */
    @GET("/article/list/{page}/json")
    suspend fun getHomeList(@Path("page") pageNo: Int)
    : ApiResponse<ArticleBean>
}

//构建Retrofit
val retrofit = Retrofit.Builder().build()

//创建ApiService实例
val apiService =retrofit.create(ApiService::class.java)

//发起请求(这里用的是suspend会自动发起请求,Java中可通过返回的call请求)
apiService.getHomeList(1)

然后通过Retrofit创建ApiService类型实例调用对应方法即可发起请求。乍一看感觉很普通,但实际上Retrofit通过这种模式(门面模式)帮我们过滤掉了很多无用信息

如果直接使用OkHttp,当在构造Request时要做很多繁琐的工作,最要命的是Request可能在多处被构造(ViewModel、Repository...),写的越分散出错时排查的难度就越高。而Retrofit通过注解的形式将Request需要的必要信息全依附在方法上(还是个抽象方法,尽量撇除一切多余信息),作为使用者只需要调用对应方法即可实现请求。至于如何解析、构造、发起请求 Retrofit内部会做处理,调用者不想也不需要知道,

所以Retrofit通过门面模式帮调用者屏蔽了一些无用信息,只暴露出唯一入口,让调用者更专注于业务开发。像我们常用的RoomGreenDao也使用了这种模式

3. 动态代理其实不是工具

看过很多Retrofit相关的文章,都喜欢上来就抛动态代理,关于为什么用只字不提,搞的Retrofit动态代理像是一个工具(框架)一样,殊不知它只是代理模式思想层面的一个产物而已。本小结会透过Retrofit看动态代理本质,帮你解除对它的误解

3.1 Retrofit构建

Retrofit构建如下所示:

Retrofit.Builder()
    .client(okHttpClient)
    .addConverterFactory(GsonConverterFactory.create())
    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
    .baseUrl(ApiConstants.BASE_URL)
    .build()

很典型的构建者模式,可以配置OkHttpGsonRxJava等等,最后通过build()做构建操作,跟一下build()代码:

#Retrofit.class

public Retrofit build() {

        //1.CallAdapter工厂集合
        List<CallAdapter.Factory> callAdapterFactories = new ArrayList<>(this.callAdapterFactories);
        callAdapterFactories.addAll(platform.defaultCallAdapterFactories(callbackExecutor));

        //2.Converter工厂集合
        List<Converter.Factory> converterFactories =
                new ArrayList<>(
                        1 + this.converterFactories.size() + platform.defaultConverterFactoriesSize());
        converterFactories.add(new BuiltInConverters());
        converterFactories.addAll(this.converterFactories);
        converterFactories.addAll(platform.defaultConverterFactories());

        return new Retrofit(
              callFactory,
                baseUrl,
                unmodifiableList(converterFactories),
                unmodifiableList(callAdapterFactories),
                callbackExecutor,
                validateEagerly);
    }

将一些必要信息注入到Retrofit并创建返回。注释1、2处两个集合非常重要,这里先埋个伏笔后面我们再回来看

3.2 何为动态代理?

什么是代理模式?

代理模式概念非常简单,比如A想做一件事可以让B帮他做,这样做的好处是什么?
下面通过一个例子简要说明。需求:每一次本地数据库CRUD都要做一次上报

最简单粗暴的方式就是每次CRUD时都单独做一次记录,代码如下

//业务层方法test1
fun test1{
    //数据库插入操作
    dao.insert()
    //上报
    post()
}
//业务层方法test2
fun test2(){
    //数据库更新操作
    dao.update()
    //上报
    post()
}

以上这种方式存在一个问题:

  • 上报操作本身与具体业务无关,一旦需要对上报进行修改,那就可能影响到业务,进而可能造成不可预期的问题产生

面对以上问题可以通过代理模式完美规避,改造后的代码如下:

class DaoProxy(){
    //数据库插入操作
    fun insert(){
        dao.insert()
        //上报
        post()
    }

    //数据库更新操作
    fun update(){
        dao.update()
        //上报
        post()
    }
}

//业务层方法test1
fun test1{
    //数据库插入操作
    daoProxy.insert()
}
//业务层方法test2
fun test2(){
    //数据库更新操作
    daoProxy.update()
}

新增一个代理类DaoProxy,将dao以及上报操作在代理类中执行,业务层直接操作代理对象,这样就将上报从业务层抽离出来,从而避免业务层改动带来的问题。实际使用代理模式时应遵守基于接口而非实现编程思想,但文章侧重于传授思想,规范上可能欠缺

此时还有一个问题,每次CRUD都会手动做一次上报操作,这显然是模版代码,如何解决?下面来看动态代理:

什么是动态代理?

java中的动态代理就是在运行时通过反射为目标对象做一些附加操作,代码如下:

class DaoProxy() {
    //创建代理类
    fun createProxy(): Any {
        //创建dao
        val proxyAny = Dao()
        val interfaces = proxyAny.javaClass.interfaces
        val handler = ProxyHandler(proxyAny)
        return Proxy.newProxyInstance(proxyAny::class.java.classLoader, interfaces, handler)
    }

    //代理委托类
    class ProxyHandler(private val proxyObject:Any): InvocationHandler {
        //代理方法,p1为目标类方法、p2为目标类参数。调用proxyObject任一方法时都会执行invoke
        override fun invoke(p0: Any, p1: Method, p2: Array<out Any>): Any {
            //执行Dao各个方法(CRUD)
            val result = p1.invoke(proxyObject,p2)
            //上报
            post()
            return result
        }
    }
}
//此处规范上应该使用基于接口而非实现编程。如果要替换Dao通过接口编程可提高扩展性
val dao:Dao = DaoProxy().createProxy() as Dao
dao.insert()
dao.update()

其中ProxyJDK中用于创建动态代理的类,InvocationHandler是一个委托类, 内部的invoke(代理方法)方法会随着目标类(Dao)任一方法的调用而调用,所以在其内部实现上报操作即可消除大量模版代码。

动态代理静态代理核心思想一致,区别是动态代理可以在运行时通过反射动态创建一个切面(InvocationHandler#invoke),用来消除模板代码。喜欢思考的同学其实已经发现,代理模式符合面向切面编程(AOP)思想,而代理类就是切面

3.3 动态代理获取ApiService并发起请求

2.2小节有提到可以通过retrofit.create()创建ApiService,跟一下retrofitcreate()

#Retrofit.class

public <T> T create(final Class<T> service) {
        //第一处
        validateServiceInterface(service);
        return (T) Proxy.newProxyInstance(
                        service.getClassLoader(),
                        new Class<?>[] {service},
                        new InvocationHandler() {
                            //第二处
                            @Override
                            public @Nullable Object invoke(Object proxy, Method method, @Nullable Object[] args)
                                    throws Throwable {
                                ...
                                return platform.isDefaultMethod(method)
                                        ? platform.invokeDefaultMethod(method, service, proxy, args)
                                        : loadServiceMethod(method).invoke(args);
                            }
                        });
    }

create()大致可以分为两部分:

  • 第一部分为validateServiceInterface()内容,用来验证ApiService合法性,比较简单就不多描述,感兴趣的同学可自行查看。
  • 第二部分就是invoke(),通过3.2小节可知这是一个代理方法,可通过调用ApiService中的任一方法执行,其中参数methodargs代表ApiService对应的方法和参数。返回值中有一个isDefaultMethod,这里如果是Java8的默认方法直接执行,毕竟我们只需要代理ApiService中方法即可。经过反复筛选最后重任落在了loadServiceMethod,这也是Retrofit中最核心的一个方法,下面我们来跟一下
#Retrofit.class

ServiceMethod<?> loadServiceMethod(Method method) {
    ServiceMethod<?> result = serviceMethodCache.get(method);
    if (result != null) return result;
    synchronized (serviceMethodCache) {
      result = serviceMethodCache.get(method);
      if (result == null) {
        result = ServiceMethod.parseAnnotations(this, method);
        serviceMethodCache.put(method, result);
      }
    }
    return result;
  }

大致就是对ServiceMethod做一个很常见的缓存操作,这样做的目的是为了提升运行效率,毕竟创建一个ServiceMethod会用到大量反射。创建ServiceMethod对象是通过其静态方法parseAnnotations实现的,再跟一下这个方法:

#ServiceMethod.class

  static <T> ServiceMethod<T> parseAnnotations(Retrofit retrofit, Method method) {
        //第一步
        RequestFactory requestFactory =
            RequestFactory.parseAnnotations(retrofit, method);
        Type returnType = method.getGenericReturnType();
        ...
        //第二步
        return HttpServiceMethod.parseAnnotations(retrofit,
                method, requestFactory);
    }

第一步:

通过RequestFactoryparseAnnotations()解析method(ApiService的method)中的注解信息,具体代码很简单就不再贴了。不过需要注意这一步只是解析注解并保存在RequestFactory工厂中,会在请求时再通过RequestFactory将请求信息做拼装。

第二步:

调用HttpServiceMethodparseAnnotations创建ServiceMethod,这个方法很长并且信息量很大,下一小节我再详细描述,此处你只需知道它做了什么即可。其实到这方法调用链已经很绕了,我先帮大家捋一下 HttpServiceMethod其实是ServiceMethod的子类,Retrofit动态代理里面的loadServiceMethod就是HttpServiceMethod类型对象,最后来看一下它的invoke()方法。

#HttpServiceMethod.class

@Override
  final @Nullable ReturnT invoke(Object[] args) {
    Call<ResponseT> call = new OkHttpCall<>(requestFactory, args, callFactory, responseConverter);
    return adapt(call, args);
  }

创建了一个OkHttpCall实例,它内部其实就是对OkHttp的一系列操作,这里先按住不表后面我会再提到。把关注点切到返回值,返回的Call对象没做任何操作,而是传入到adapter()方法一并返回来,字面意思应该是一个适配操作,那究竟如何适配?这里再埋一个伏笔与3.1结尾相呼应,下一小节我们再一一揭开。

动态代理讲完了,那么它解决了什么问题?

  • 假如不使用代理模式,那关于ApiService中方法注解解析的操作势必会浸入到业务当中,一旦对其修改就有可能影响到业务,其实也就是也违背了我们前面所说的门面模式迪米特法则,通过代理模式做一个切面操作(AOP)可以完美规避了这一问题。可见这里的门面模式代理模式是相辅相成的

  • Retrofit事先都不知道ApiService方法数量,就算知道也避免不了逐一解析而产生大量的模版代码,此时可通过引入动态代理在运行时动态解析 从而解决这一问题。

4. ReturnT、ResponseT做一次适配的意义何在?

ResponseT、ReturnT是 Retrofit 对响应数据类型和返回值类型的简称

4.1 创建HttpServiceMethod

上一小节我们跟到了adapter(),这是一个抽象方法,其实现类是通过HttpServiceMethodparseAnnotations创建的,继续跟下去:

#HttpServiceMethod.class

static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotations(
            Retrofit retrofit, Method method, RequestFactory requestFactory) {
        boolean isKotlinSuspendFunction = requestFactory.isKotlinSuspendFunction;
        boolean continuationWantsResponse = false;
        boolean continuationBodyNullable = false;

        Annotation[] annotations = method.getAnnotations();
        Type adapterType;
        //1.获取adapterType,默认为method返回值类型
        if (isKotlinSuspendFunction) {
            Type[] parameterTypes = method.getGenericParameterTypes();
            Type responseType =
                    Utils.getParameterLowerBound(
                            0, (ParameterizedType) parameterTypes[parameterTypes.length - 1]);
            if (getRawType(responseType) == Response.class && responseType instanceof ParameterizedType) {
                // Unwrap the actual body type from Response<T>.
                responseType = Utils.getParameterUpperBound(0, (ParameterizedType) responseType);
                continuationWantsResponse = true;
            } else {
            }
            adapterType = new Utils.ParameterizedTypeImpl(null, Call.class, responseType);
            annotations = SkipCallbackExecutorImpl.ensurePresent(annotations);
        } else {
            adapterType = method.getGenericReturnType();
        }
        //2.创建CallAdapter
        CallAdapter<ResponseT, ReturnT> callAdapter =
                createCallAdapter(retrofit, method, adapterType, annotations);
        Type responseType = callAdapter.responseType();
        //3.创建responseConverter
        Converter<ResponseBody, ResponseT> responseConverter =
                createResponseConverter(retrofit, method, responseType);

        okhttp3.Call.Factory callFactory = retrofit.callFactory;
        //4.创建HttpServiceMethod类型具体实例
        if (!isKotlinSuspendFunction) {
            return new HttpServiceMethod.CallAdapted<>(requestFactory, callFactory, responseConverter, callAdapter);
        }
        //兼容kotlin suspend方法
        else if (continuationWantsResponse) {
            //noinspection unchecked Kotlin compiler guarantees ReturnT to be Object.
            return (HttpServiceMethod<ResponseT, ReturnT>)
                    new HttpServiceMethod.SuspendForResponse<>(
                            requestFactory,
                            callFactory,
                            responseConverter,
                            (CallAdapter<ResponseT, Call<ResponseT>>) callAdapter);
        } else {
            //noinspection unchecked Kotlin compiler guarantees ReturnT to be Object.
            return (HttpServiceMethod<ResponseT, ReturnT>)
                    new HttpServiceMethod.SuspendForBody<>(
                            requestFactory,
                            callFactory,
                            responseConverter,
                            (CallAdapter<ResponseT, Call<ResponseT>>) callAdapter,
                            continuationBodyNullable);
        }
    }
  • 注释1:获取adapterType,这里的adapter指的是Retrofit构建时通过addCallAdapterFactory()添加的类型,如果添加的是RxJavaadapterType便是Observable。默认是method返回值,同时也会做kotlin suspend适配
  • 注释2:创建callAdapter,暂时掠过,下面详细描述
  • 注释3:创建responseConverter,暂时掠过,下面详细描述
  • 注释4:这里会创建具体的HttpServiceMethod类型实例,总共有三种类型CallAdaptedSuspendForResponseSuspendForBody,第一种为默认类型,后两种可兼容kotlin suspend。内部主要做的事情其实很简单,就是通过内部的adapter()调用callAdapter``adapter(),具体代码就不贴了,感兴趣的自行查看

4.2 创建callAdapter、responseConverter

创建创建callAdapter

#HttpServiceMethod.class

 private static <ResponseT, ReturnT> CallAdapter<ResponseT, ReturnT> createCallAdapter(
            Retrofit retrofit, Method method, Type returnType, Annotation[] annotations) {
        return (CallAdapter<ResponseT, ReturnT>) retrofit.callAdapter(returnType, annotations);
        ...
    }

通过retrofit#callAdapter()获取CallAdapter,继续跟

#Retrofit.class

public CallAdapter<?, ?> callAdapter(Type returnType, Annotation[] annotations) {
        return nextCallAdapter(null, returnType, annotations);
}

public CallAdapter<?, ?> nextCallAdapter(
            @Nullable CallAdapter.Factory skipPast, Type returnType, Annotation[] annotations) {
        int start = callAdapterFactories.indexOf(skipPast) + 1;
        for (int i = start, count = callAdapterFactories.size(); i < count; i++) {
            //通过returnType在callAdapterFactories获取adapter工厂,再get adapter
            CallAdapter<?, ?> adapter = callAdapterFactories.get(i).get(returnType, annotations, this);
            if (adapter != null) {
                return adapter;
            }
        }
        ...
    }

先通过returnTypecallAdapterFactories获取adapter工厂,再通过工厂get()获取CallAdapter实例。callAdapterFactories3.1结尾build()中初始化的,通过platform添加默认类型,也可以通过addCallAdapterFactory()添加RxJava之类的适配器类型。

这里用到了两个设计模式适配器策略

适配器模式
返回的CallAdapter其实就是对Call<T>的适配器,假如你想让Retrofit配合RxJava使用,常规方式只能在业务中单独创建Observable并与Call融合,关于ObservableCall融合(适配)其实是与业务无关的,此时可以引入适配器模式将Call适配成Observable,将适配细节从业务层挪到Retrofit内部,符合迪米特法则

策略模式
通过ReturnT获取对应的CallAdapter,如果ReturnTCall<T>那获取的是DefaultCallAdapterFactory创建的实例,如果如果ReturnTObservable<T>则获取的是RxJava2CallAdapterFactory创建的实例。假如想新增一种适配器只需明确ReturnT,创建对应工厂再通过addCallAdapterFactory添加即可,Retrofit会通过ReturnT自动寻找对应CallAdapter,符合开闭原则(扩展开放)

创建responseConverter

关于responseConverter其实是做数据转换的,可以将ResponseT适配成我们想要的数据类型,比如Gson解析只需通过addConverterFactory添加GsonConverterFactory创建的Converter实例即可
具体添加、获取流程与CallAdapter基本一致,感兴趣的同学可自行查看

4.3 发起请求

到上一小结我们已经创建了所有需要的内容,再回到HttpServiceMethodinvoke,这里会将OkHttpCall传入到adapt执行并返回,HttpServiceMethod的实现类的adapter会执行对应CallAdapteradapter
我们就取默认的CallAdapterDefaultCallAdapterFactory通过get获取的CallAdapter,代码如下:

DefaultCallAdapterFactory.class

public @Nullable CallAdapter<?, ?> get(
        return new CallAdapter<Object, Call<?>>() {
            @Override
            public Type responseType() {
                return responseType;
            }

            @Override
            public Call<Object> adapt(Call<Object> call) {
                return executor == null ? call : new DefaultCallAdapterFactory.ExecutorCallbackCall<>(executor, call);
            }
        };
    }

内部adaptApiService method最终返回的ExecutorCallbackCallOkHttpCall装饰类,最后可通过OkHttpCallexecute发起请求,代码如下:

#OkHttpCall.class

public Response<T> execute() throws IOException {
        okhttp3.Call call;
        ...
        return parseResponse(call.execute());
    }

OkHttp常规操作,再把关注点放到onResponseparseResponse

#OkHttpCall.class

Response<T> parseResponse(okhttp3.Response rawResponse) throws IOException {
        ...
        T body = responseConverter.convert(catchingBody);
        ...
        return Response.success(body, rawResponse);
    }

responseConverter会对Body做一个适配,如果addConverterFactory添加了GsonConvert那解析操作就会在此处进行

至此Retrofit全部流程分析完毕

综上所述

  • Retrofit通过REST ful API从范式层面约束代码
  • 通过门面模式设计ApiService可以让开发者更专注于业务
  • 动态代理只是将功能代码从业务剥离,并解决了模板代码问题
  • ReturnT、ResponseT引入适配器模式可以让结果更加灵活
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容