Rxjava2 + Retrofit +DBflow + 自定义缓存框架搭建

最喜欢干的事,莫过于拿着工资搭框架了。
其实这个框架已经出来很久了,并不是什么新鲜玩意儿了,只不过我一直没有尝试着去写一篇内容比较大的文章来分享,这次就卖弄一下,希望各种大神轻喷,有什么问题也希望各位大神不吝赐教。

Retrofit的接入

ApiService

首先Retrofit的框架架构搭建其实比较简单,因为Retrofit本身已经极致简单了。

/**
 * Author       : yizhihao (Merlin)
 * Create time  : 2017-08-23 15:48
 * contact      :
 * 562536056@qq.com || yizhihao.hut@gmail.com
 */
public interface ApiService {

    @GET("{url}")
    Observable<ResponseBody> executeGet(
            @Path("url") String url,
            @QueryMap Map<String, String> maps);


    @POST("{url}")
    Observable<ResponseBody> executePost(
            @Path("url") String url,
            @QueryMap Map<String, String> maps);

    @POST("{url}")
    Observable<ResponseBody> executeCachePost(
            @Path("url") String url,
            @QueryMap Map<String, String> maps);

    @POST("{url}")
    Observable<ResponseBody> uploadFiles(
            @Path("url") String url,
            @Path("headers") Map<String, String> headers,
            @Part("filename") String description,
            @PartMap()  Map<String, RequestBody> maps);

    @Streaming
    @GET
    Observable<ResponseBody> downloadFile(@Url String fileUrl);
}

上面的代码通过将接口返回类型通用化返回结合rxjava的Observable这样我们就可以愉快的用rxjava来处理线程切换了。

Retrofit接口对象
public static Retrofit retrofit() {
        return retrofit(sBaseUrl);
    }

    public static Retrofit retrofit(String baseUrl) {
        return new Retrofit.Builder()
                .baseUrl(baseUrl)
                .client(getInstance().getHttpClient())//添加自定义OkHttpClient
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create(GsonUtils.getInstance().getGson()))
                .build();
    }

    public OkHttpClient getHttpClient() {
        if (client == null) {
            client = new OkHttpClient.Builder()
                 //.addNetworkInterceptor(newCacheNetworkInterceptor())
                    //日志,可以配置 level 为 BASIC / HEADERS / BODY
                    .addInterceptor(new LoggingInterceptor()) 
                    .connectTimeout(DEFAULT_TIME_OUT, TimeUnit.SECONDS)
                    //.cache(provideCache())
                    .retryOnConnectionFailure(true)
                    .build();
        }
        return client;
    }

Retrofit管理类主要是整合okhttp进行必要的配置

缓存拦截器

细心的读者可能发现了CacheNetworkInterceptor这个注释的拦截器,它的职责本来要添加的NetworkInterceptor是为了做缓存Hook的。
但是查阅了一些资料,还有okhttp源码,其实okhttp本身是自带缓存逻辑的,这套逻辑完全遵守RFC协议进行缓存控制的。很多人都去hook掉了这步。其实查阅源码可以看到


image.png

从源码中不难看出,我们在自定义cache的时候,okhttp会把自己的internalCache给废弃掉,而我们在okhttp的内部拦截器中也会看到CacheInterceptor,这个类其实就是实现了okhttp的Cache-control。所以我并没有选择去拦截Response手动添加Cache-control进行缓存处理。当然大家要用我也拦不住,毕竟也挺方便的。

日志拦截器

LoggingInterceptor拦截器主要是为了打印请求发送和收到请求的Log.

public class LoggingInterceptor implements Interceptor {
    private boolean debugMode = DebugConstant.isDebug;

    @Override
    public Response intercept(Chain chain) throws IOException {
        if(!debugMode){
            return chain.proceed(chain.request());
        }
        //这个chain里面包含了request和response,所以你要什么都可以从这里拿
        Request request = chain.request();

        long t1 = System.nanoTime();//请求发起的时间
        LogUtils.e(String.format("发送请求 %s on %s%n%s", request.url(), chain.connection(), request.headers()));

        Response response = chain.proceed(request);

        long t2 = System.nanoTime();//收到响应的时间

        //这里不能直接使用response.body().string()的方式输出日志
        //因为response.body().string()之后,response中的流会被关闭,程序会报错,我们需要创建出一
        //个新的response给应用层处理
        ResponseBody responseBody = response.peekBody(1024 * 1024);

        LogUtils.d(String.format("接收响应: [%s]" +
                        "\n %n返回json:【%100s】 " +
                        "\n请求执行时间%.1fms" +
                        "\n%n%s",
                response.request().url(),
                responseBody.string(),
                (t2 - t1) / 1e6d,
                response.headers()));
        return response;
    }
}

加上日志拦截器之后log如下图


image.png

看到打印出来的详细的log有木有感觉很酸爽。

Rxjava的封装

绑定Activity生命周期

对rxjava中的subcriber的封装,这里主要是将activity的生命周期和subcriber绑定联系起来,当activity被finish的时候我们的subcriber也应该dispose取消掉。

   private CompositeDisposable disposables2Stop;// 管理Stop取消订阅者者
    private CompositeDisposable disposables2Destroy;// 管理Destroy取消订阅者者

在baseaActivity中通过CompositeDisposable组合管理添加进来的Disposable。然后在ondestroy中进行统一取消,防止内存泄漏。

@Override
    protected void onDestroy() {
        super.onDestroy();
        if (disposables2Destroy == null) {
            throw new IllegalStateException(
                    "onDestroy called multiple times or onCreate not called");
        }
        disposables2Destroy.dispose();
        disposables2Destroy = null;
        if (mDelegate != null) {
            mDelegate.ondestroy();
            mDelegate = null;
        }
    }
基类订阅者BaseObserver

通用BaseObserver是继承于rxjava的Observer,在错误回调中的代码,前半部分是获取错误的堆栈进行打印的逻辑,后面是对各类错误的通用处理。

public void onError(Throwable e) {
        if (BuildConfig.DEBUG) {
            StringBuilder sb = new StringBuilder();
            StackTraceElement[] stacks = e.getStackTrace();
            sb.append(e.getMessage());
            sb.append("\n");
            for (StackTraceElement stack : stacks) {
                sb.append(stack.getMethodName());
                sb.append("(");
                sb.append(stack.getClassName());
                sb.append(".java:");
                sb.append(stack.getLineNumber());
                sb.append(")");
                sb.append("\n");
            }
            LogUtils.e("Retrofit", sb.toString());
        }
        mBaseImpl.dismissProgress();
        if (e instanceof HttpException) {                 //   HTTP错误
            onException(ExceptionReason.BAD_NETWORK);
        } else if (e instanceof ConnectException
                || e instanceof UnknownHostException) {   //   连接错误
            onException(ExceptionReason.CONNECT_ERROR);
        } else if (e instanceof InterruptedIOException) { //  连接超时
            onException(ExceptionReason.CONNECT_TIMEOUT);
        } else if (e instanceof JsonParseException
                || e instanceof JSONException
                || e instanceof ParseException) {         //  解析错误
            onException(ExceptionReason.PARSE_ERROR);
        } else {
            onException(ExceptionReason.UNKNOWN_ERROR);
        }
    }

Observer中另一个最重要的结果回调onNext中对errcode进行过滤,因为我自己封装的model层返回的BaseResponce是没有errorCode的,这个model后面会讲到,当然我也可以自己给通用BaseResponse加上200的code但是总感觉这两个逻辑还是不要耦合的好,万一code变了我model也要改,所以我在我在BaseResponce中设置了一个变量fromCache用于标记返回结果为缓存。代码如下:

@Override
    public void onNext(@NonNull T tBaseResponce) {
        LogUtils.d(tBaseResponce.errCode + " || from cache : " + tBaseResponce.fromCache);
        if (tBaseResponce.errCode == 200 || tBaseResponce.fromCache) {
            onSuccess(tBaseResponce);
        } else {
            onFail(tBaseResponce);
        }
    }
public class BaseResponse<T>{

    @SerializedName("code")
    public int errCode;

    @SerializedName("msg")
    public String errMsg;

    @SerializedName("data")
    public T realData;

    /**
     * 請求結果是否來自緩存
     */
    public boolean fromCache = false;

    public BaseResponse<T> setData(T data){
        realData = data;
        return this;
    }

    @Override
    public String toString() {
        return "BaseResponse{" +
                "errCode='" + errCode + '\'' +
                ", errMsg='" + errMsg + '\'' +
                ", data=" + realData +
                '}';
    }
}

另外BaseObserver引用的BaseImpl是activity的抽象接口,托管了进度条和绑定了activity的生命周期的逻辑。

public abstract class BaseObserver<T extends BaseResponse> implements Observer<T> {

    private BaseImpl mBaseImpl;
    //  Activity 是否在执行onStop()时取消订阅
    private boolean isAddInStop = false;
    private boolean needProgress = false;

    public BaseObserver(BaseImpl mBaseImpl,boolean needProgress) {
        this.needProgress = needProgress;
        this.mBaseImpl = mBaseImpl;
    }

    @Override
    public void onSubscribe(@NonNull Disposable d) {
        if(needProgress) mBaseImpl.showProgress("加载中");
        if (isAddInStop) {    //  在onStop中取消订阅
            mBaseImpl.addRxStop(d);
        } else { //  在onDestroy中取消订阅
            mBaseImpl.addRxDestroy(d);
        }
    }

    @Override
    public void onNext(@NonNull T tBaseResponce) {
        LogUtils.d(tBaseResponce.errCode + " || from cache : " + tBaseResponce.fromCache);
        if (tBaseResponce.errCode == 200 || tBaseResponce.fromCache) {
            onSuccess(tBaseResponce);
        } else {
            onFail(tBaseResponce);
        }
    }

    @Override
    public void onError(Throwable e) {
        if (BuildConfig.DEBUG) {
            StringBuilder sb = new StringBuilder();
            StackTraceElement[] stacks = e.getStackTrace();
            sb.append(e.getMessage());
            sb.append("\n");
            for (StackTraceElement stack : stacks) {
                sb.append(stack.getMethodName());
                sb.append("(");
                sb.append(stack.getClassName());
                sb.append(".java:");
                sb.append(stack.getLineNumber());
                sb.append(")");
                sb.append("\n");
            }
            LogUtils.e("Retrofit", sb.toString());
        }
        mBaseImpl.dismissProgress();
        if (e instanceof HttpException) {                 //   HTTP错误
            onException(ExceptionReason.BAD_NETWORK);
        } else if (e instanceof ConnectException
                || e instanceof UnknownHostException) {   //   连接错误
            onException(ExceptionReason.CONNECT_ERROR);
        } else if (e instanceof InterruptedIOException) { //  连接超时
            onException(ExceptionReason.CONNECT_TIMEOUT);
        } else if (e instanceof JsonParseException
                || e instanceof JSONException
                || e instanceof ParseException) {         //  解析错误
            onException(ExceptionReason.PARSE_ERROR);
        } else {
            onException(ExceptionReason.UNKNOWN_ERROR);
        }
    }

    @Override
    public void onComplete() {
        if(needProgress) mBaseImpl.dismissProgress();
    }

    /**
     * 请求成功
     *
     * @param response 服务器返回的数据
     */
    abstract public void onSuccess(T response);

    /**
     * 服务器返回数据,但响应码不为200
     *
     * @param response 服务器返回的数据
     */
    public void onFail(T response) {
        String message = response.errMsg;
        if (TextUtils.isEmpty(message)) {
            ToastUtils.showShort(R.string.response_return_error);
        } else {
            ToastUtils.showShort(message);
        }
    }

    /**
     * 请求异常
     *
     * @param reason
     */
    public void onException(ExceptionReason reason) {
        switch (reason) {
            case CONNECT_ERROR:
                ToastUtils.showShort(R.string.connect_error, Toast.LENGTH_SHORT);
                break;

            case CONNECT_TIMEOUT:
                ToastUtils.showShort(R.string.connect_timeout, Toast.LENGTH_SHORT);
                break;

            case BAD_NETWORK:
                ToastUtils.showShort(R.string.bad_network, Toast.LENGTH_SHORT);
                break;

            case PARSE_ERROR:
                ToastUtils.showShort(R.string.parse_error, Toast.LENGTH_SHORT);
                break;

            case UNKNOWN_ERROR:
            default:
                ToastUtils.showShort(R.string.unknown_error, Toast.LENGTH_SHORT);
                break;
        }
    }

    /**
     * 请求网络失败原因
     */
    public enum ExceptionReason {
        /**
         * 解析数据失败
         */
        PARSE_ERROR,
        /**
         * 网络问题
         */
        BAD_NETWORK,
        /**
         * 连接错误
         */
        CONNECT_ERROR,
        /**
         * 连接超时
         */
        CONNECT_TIMEOUT,
        /**
         * 未知错误
         */
        UNKNOWN_ERROR,
    }
}

Model层的封装

逻辑流程图

然后说下上面提到的model层,我定义了接口IRepository。
这个model的主要逻辑是 :


image.png

首先判断是否需要强制刷新,如果不需要强制刷新则去数据库缓存中查看是否含有对象的缓存,如果是网络获取判断是否需要缓存。这里的逻辑主要由客户端控制。

public interface IRepository<T> {

    /**
     * 用于gson解析,以及一些Logname的打印。
     * @return
     */
    Class getTClass();

    Observable<T> getEntry(final String url, Map<String, String> queryMap, final boolean needCache, boolean forceRefresh);

    Observable<T> getEntry(final String url, Map<String, String> queryMap);

    T getCache(String url) throws Exception;

    Observable<T> getEntryFromNet(String url, Map<String, String> queryMap, boolean needCache);

    void saveCache(String url, T baseBeanList);

    String getCacheKey(String url, Map<String, String> queryMap);

    void clearCache();

}
model的实现

拿目前公司的restful接口数据格式类型举例:

{
    "code":200,
    "msg":"请求成功",
    "data":{
        "count":10,
        "game_list":[
            {
                "gameid":362938
            }
        ]
    }
}

可以看出BaseResponce返回的泛型T对应的data数据还需要继续解析。所以以目前的IRepository<T>

public abstract class IDBFlowRespository<BeanContainer,DBBean> implements IRepository<BaseResponse<BeanContainer>>{

是代码是不能很好的封装满足需求的,所以我定义了抽象类继承IRepository。
定义了2个泛型BeanContainer和DBBean,数据库的相关操作基本由DBBean泛型实例完成,网络层的解析由BeanContainer完成。
各司其职。GameContainer对应的是上图json的data,gameList对应的是上图json的game_list。当然如果有其他类型的restful结构,我只需要在定义对应类型的repository抽象类就好了,毕竟现在返回的restful接口的json格式非常局限满世界也就那么几种,所以不用担心repository的扩展类太多的问题。

而真正的Repository实例代码非常少,只需要继承4个接口就能满足上述定义的model接口的功能,如下:

public class GameBeanRespository extends DBListRepository<GameContainerBean,GameContainerBean.GameListBean> {
    //用于Gson对泛型的解析
    @Override
    public Class getTClass() {
        return GameContainerBean.class;
    }

    //用于DB抽象类获取对数据库的引用
    @Override
    public Class getTableClass() {
        return GameContainerBean.GameListBean.class;
    }

    @Override
    public List<GameContainerBean.GameListBean> mapContainer(GameContainerBean beanContainer) {
        return beanContainer.gameList;
    }

    @Override
    public GameContainerBean mapTableBean(List<GameContainerBean.GameListBean> gameListBeen) {
        return new GameContainerBean(gameListBeen);
    }
}

BaseModel是我对实体的抽象继承的是DBflow的BaseModel可以进行数据库的增删改,很方便。

public abstract class BaseModel extends com.raizlabs.android.dbflow.structure.BaseModel{
    public static final String KEY = "keyUrl";
    @Column(name = KEY)
    public String keyUrl;
}

其中key是对每个bean对应的数据库增加的字段主要是用来根据url进行缓存查询的。
其中key是由Url拼接上queryMap的参数组成,逻辑如下:

public String getCacheKey(String url, Map<String, String> queryMap) {
        StringBuilder sb = new StringBuilder();
        sb.append(url);
        if (queryMap != null && !queryMap.isEmpty()) {
            Set<String> keys = queryMap.keySet();
            sb.append("?");
            for (String key : keys) {
                sb.append(key).append("=").append(queryMap.get(key));
            }
        }
        return sb.toString();
    }

有个小问题,因为网络数据获取是从我们定义的retrofit通用接口中返回,返回的对象是Obserable<ResponseBody>而我们的model接受的参数是Observable<BaseResponce<T>>,等于是承包了GsonConvertFactory的工作,我们把返回的Observer通过rxjava的map转成我们的Model对应的的Observer类型就行了。

@Override
    public Observable<BaseResponse<Container>> getEntryFromNet(String url, Map<String, String> queryMap, boolean needCache) {
        return HttpRequestFactory.retrofit().create(ApiService.class)
                .executeGet(url,queryMap).map(new Function<ResponseBody, BaseResponse<Container>>() {
                    @Override
                    public BaseResponse<Container> apply(@NonNull ResponseBody responseBody) throws Exception {
                        return GsonUtils.getInstance().fromJson(responseBody.string(), GsonUtils.type(BaseResponse.class,getTClass()));
                    }
                });
    }

获取model集合的的主要逻辑代码块如下:

@Override
    public Observable<BaseResponse<Container>> getEntry(final String url, Map<String, String> queryMap, final boolean needCache, boolean forceRefresh) {

        final String key = getCacheKey(url, queryMap);

        //get cache
        Observable<BaseResponse<Container>> fromCache = Observable.create(new ObservableOnSubscribe<BaseResponse<Container>>() {
            @Override
            public void subscribe(@NonNull ObservableEmitter<BaseResponse<Container>> e) throws Exception {
                final BaseResponse<Container> cacheResponce = getCache(key);
                if (cacheResponce != null) {
                    LogUtils.e("Cache hint  | key = " + key);
                    cacheResponce.fromCache = true;
                    e.onNext(cacheResponce);
                } else {
                    e.onComplete();
                }
            }
        });

        //save cache
        Observable<BaseResponse<Container>> fromNet = getEntryFromNet(url, queryMap ,needCache).map(new Function<BaseResponse<Container>, BaseResponse<Container>>() {
            @Override
            public BaseResponse<Container> apply(@NonNull BaseResponse<Container> tBaseResponse) throws Exception {
                if (needCache) saveCache(key, tBaseResponse);
                return tBaseResponse;
            }
        });

        if (forceRefresh) {
            return fromNet;
        }
        return Observable.concat(fromCache, fromNet)
                .subscribeOn(Schedulers.io())
                .unsubscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread());
    }

可以看到,Cache命中的时候会将里面BaseResponce的fromCache标记为true。这样就能和上面的提到的

BaseObserver对应的onNext逻辑相吻合了。

测试实例

new GameBeanRespository()
                .getEntry(new UrlConstant.Builder(false).shuffix().game().list().build()//参数url
                        ,new RxMap()
                                .put("page","2")
                                .put("offset","10")
                                .build())//参数query maps
                .subscribe(new BaseObserver<BaseResponse<GameContainerBean>>(this,false) {
                    @Override
                    public void onSuccess(BaseResponse<GameContainerBean> response) {
                        LogUtils.d(response.realData);
                    }
                });

GameRepository继承于DBlistRepository需要做的事情很少

public class GameBeanRespository extends DBListRepository<GameContainerBean,GameContainerBean.GameListBean> {

    @Override
    public Class getTClass() {
        return GameContainerBean.class;
    }

    @Override
    public Class getTableClass() {
        return GameContainerBean.GameListBean.class;
    }

    @Override
    public List<GameContainerBean.GameListBean> mapContainer(GameContainerBean beanContainer) {
        return beanContainer.gameList;
    }

    @Override
    public GameContainerBean mapTableBean(List<GameContainerBean.GameListBean> gameListBeen) {
        return new GameContainerBean(gameListBeen);
    }

}

上面看到的rxMap只是我写的一个链式调用的Map包装类,链式调用编写的效率和心情大家应该都理解 -3-

有兴趣的可以拿去用,也就是个小玩意儿。

public class RxMap<T,R>{

    Map<T,R> map;

    public static <T,R> RxMap<T,R> newInstance(){
        return new RxMap<>();
    }

    public RxMap() {
        this.map = new HashMap<>();
    }

    public RxMap(Map<T,R> map) {
        this.map = map;
    }

    public RxMap<T,R> put(T t, R r){
        map.put(t,r);
        return this;
    }

    public Map<T,R> build(){
        return map;
    }
}
DBflow

简单的说下DBflow,可能你直接看到了bean的实例进行了数据库的save操作,觉得很酸爽,确实很酸爽,而且DBflow继承了GreenDao和OrmLite各自的优点,简单易用上无可挑剔,自动生成数据Dao类,只需要类似于OrmLite利用注解声明各个bean之间的关系,另外继承BaseModel就让bean自己具备了增删改的能力了。

关于DBflow这个数据库的使用我就不多说了,因为太简单,学习成本低,推荐大家去用,用了感觉不爽来打我 - -!!,当然我不会告诉你我在哪里上班的。

后续我会抽出一个框架的demo的github地址补充在文章下面。

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

推荐阅读更多精彩内容