Feign源码解析

Feign源码解析

gitHub地址


从两年前开始接手一个接口网关项目开始,对于第三方接口的请求对接,在对于HTTP请求的工具类选型以及使用上,我已经成为了feign重度使用患者。Feign makes writing java http clients easier,中文译为Feign使得写java http客户端更容易。选择feign原因如下:

  • Feign允许我们通过几个注解的方式实现http的第三方请求
  • 能够让我们以面向对象化的形式简单快速的完成第三方请求的开放,解决了代码的冗余
  • Feign插件化的开放定时允许我们自定义编码器解码器错误处理器拦截器等等

测试代码

public class TestFeign {

    //定一个请求接口
    public interface PersonRequest {
        //添加相关的注解类对象
        @RequestLine("GET /person/getPersonById?id={id}")
        Person getPersonById(@Param("id") String id);

        @RequestLine("POST /person/getPerson")
        Person getPerson(PersonRequestData person);
    }

    @Data
    public static class PersonRequestData {
        private String id;
    }

    @Data
    public static class Person {
        private String id; 
        private String name;
    }

    public static void main(String[] args) {
        //通过Feign构建一个代理进行请求
        PersonRequest personRequest = Feign.builder()
                //添加json编码器,用于请求前将对象序列化json请求体
                .encoder(new JacksonEncoder())
                //添加josn接码器,将响应反序列化为json对象
                .decoder(new JacksonDecoder())
                .options(new Request.Options(5000, 5000))
                //重试机制
                .retryer(new Retryer.Default(5000, 5000, 3))
                //请求地址以及需要生成代理对象的接口
                .target(PersonRequest.class, "http://127.0.0.1:8080");
        System.out.println("user: " + personRequest.getPersonById("123456"));
    }
}

  • 整个流程如下:
    1. 定义一个接口,
    2. 在接口的类型声明方法声明方法参数上面定义相关的注解,其中包含请求地址请求头请求方式等信息,具体参考官方文档
    3. 通过Feign.builder().{xx...}.target()方法生成一个接口代理对象,最后对代理对象实施方法调用。

Feign整体架构

该图片借用了Spring Cloud Feign设计原理

image.png

代理对象生成分析

从上文可知道,我们在需要发起一个http请求的时候,定义了一个接口,在接口中通过方法定义了需要发起的请求,但是单单从一个接口上来看,是不可能实现请求的发送,为此,通过Feign.BuilderFeign通过动态代理模式生成代理对对象,该步骤如下:

  • 通过建造者模式动态的将各个组件进行配置
  • 通过动态代理模式 最终调用target()方法来生成实际发起请求的代理对象

1. Feign.Builder

public static class Builder {

    private final List<RequestInterceptor> requestInterceptors =
        new ArrayList<RequestInterceptor>();
    private Logger.Level logLevel = Logger.Level.NONE;
    private Contract contract = new Contract.Default();
    private Client client = new Client.Default(null, null);
    private Retryer retryer = new Retryer.Default();
    private Logger logger = new NoOpLogger();
    private Encoder encoder = new Encoder.Default();
    private Decoder decoder = new Decoder.Default();
    private ErrorDecoder errorDecoder = new ErrorDecoder.Default();
    private Options options = new Options();
    private InvocationHandlerFactory invocationHandlerFactory =
        new InvocationHandlerFactory.Default();
    private boolean decode404;

    public Builder logLevel(Logger.Level logLevel) {
      this.logLevel = logLevel;
      return this;
    }

    public Builder contract(Contract contract) {
      this.contract = contract;
      return this;
    }

    public Builder client(Client client) {
      this.client = client;
      return this;
    }

    public Builder retryer(Retryer retryer) {
      this.retryer = retryer;
      return this;
    }

    public Builder logger(Logger logger) {
      this.logger = logger;
      return this;
    }

    public Builder encoder(Encoder encoder) {
      this.encoder = encoder;
      return this;
    }

    public Builder decoder(Decoder decoder) {
      this.decoder = decoder;
      return this;
    }

    
    public Builder decode404() {
      this.decode404 = true;
      return this;
    }

    public Builder errorDecoder(ErrorDecoder errorDecoder) {
      this.errorDecoder = errorDecoder;
      return this;
    }

    public Builder options(Options options) {
      this.options = options;
      return this;
    }

    /**
     * Adds a single request interceptor to the builder.
     */
    public Builder requestInterceptor(RequestInterceptor requestInterceptor) {
      this.requestInterceptors.add(requestInterceptor);
      return this;
    }

    /**
     * Sets the full set of request interceptors for the builder, overwriting any previous
     * interceptors.
     */
    public Builder requestInterceptors(Iterable<RequestInterceptor> requestInterceptors) {
      this.requestInterceptors.clear();
      for (RequestInterceptor requestInterceptor : requestInterceptors) {
        this.requestInterceptors.add(requestInterceptor);
      }
      return this;
    }

    /**
     * Allows you to override how reflective dispatch works inside of Feign.
     */
    public Builder invocationHandlerFactory(InvocationHandlerFactory invocationHandlerFactory) {
      this.invocationHandlerFactory = invocationHandlerFactory;
      return this;
    }

    public <T> T target(Class<T> apiType, String url) {
      return target(new HardCodedTarget<T>(apiType, url));
    }

    public <T> T target(Target<T> target) {
      return build().newInstance(target);
    }

    public Feign build() {
      //获取MethodHandler工厂类对象
      SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
          new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
                                               logLevel, decode404);
      //构建生成
      ParseHandlersByName handlersByName =
          new ParseHandlersByName(contract, options, encoder, decoder,
                                  errorDecoder, synchronousMethodHandlerFactory);
      //
      return new ReflectiveFeign(handlersByName, invocationHandlerFactory);
    }
  }
  • 通过源码可知道:
  1. Feign通过 Feign.Builder建造者模式方式对各个开放组件进行配置
  2. target()最终会调用new ReflectiveFeign(...)来生成Feign实例
  3. SynchronousMethodHandler.Factory用于创建一个SynchronousMethodHandler对象
  4. ParseHandlersByNameTarget的所有接口方法转换为Map<String, MethodHandler>对象
  5. ReflectiveFeignFeign的具体实现类,最终会调用newInstance方法通过原生的动态代理框架生成最终的动态代理对象

2. ReflectiveFeign.newInstance()

public class ReflectiveFeign extends Feign {
    //省略部分无关紧要的代码

    /**
     * 最终调用newInstance返回动态的代理对象
     * @param target
     * @param <T>
     * @return
     */
    public <T> T newInstance(Target<T> target) {
        //通过ParseHandlersByName对象apply方法生成方法名与MethodHandler的映射关系,见图1
        Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
        //构建Method与其对应的MethodHandler的关系存放入methodToHandler,主要用于在实际调用的时候,能够根据方法名获取对应的MethodHandler发起请求,见图2
        Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
        List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();
        //通过循环构建方法与其对应的MethodHandler的关系存放入methodToHandler
        for (Method method : target.type().getMethods()) {
            if (method.getDeclaringClass() == Object.class) {
                continue;
            } else if(Util.isDefault(method)) {
                DefaultMethodHandler handler = new DefaultMethodHandler(method);
                defaultMethodHandlers.add(handler);
                methodToHandler.put(method, handler);
            } else {
                methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
            }
        }
        //通过InvocationHandlerFactory factory创建代理对象
        InvocationHandler handler = factory.create(target, methodToHandler);
        //生成代理对象
        T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[]{target.type()}, handler);

        for(DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
            defaultMethodHandler.bindTo(proxy);
        }
        return proxy;
    }

    static class FeignInvocationHandler implements InvocationHandler {

        private final Target target;
        private final Map<Method, MethodHandler> dispatch;

        FeignInvocationHandler(Target target, Map<Method, MethodHandler> dispatch) {
            this.target = checkNotNull(target, "target");
            this.dispatch = checkNotNull(dispatch, "dispatch for %s", target);
        }

        /**
         * 请求接口的动态代理,在调用方法的过程中,实际是执行该方法进行调用
         * @param proxy
         * @param method
         * @param args
         * @return
         * @throws Throwable
         */
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if ("equals".equals(method.getName())) {
                try {
                    Object
                            otherHandler =
                            args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
                    return equals(otherHandler);
                } catch (IllegalArgumentException e) {
                    return false;
                }
            } else if ("hashCode".equals(method.getName())) {
                return hashCode();
            } else if ("toString".equals(method.getName())) {
                return toString();
            }
            //根据方法名获取对应的MethodHandler发起请求
            return dispatch.get(method).invoke(args);
        }

        @Override
        public boolean equals(Object obj) {
            if (obj instanceof FeignInvocationHandler) {
                FeignInvocationHandler other = (FeignInvocationHandler) obj;
                return target.equals(other.target);
            }
            return false;
        }

        @Override
        public int hashCode() {
            return target.hashCode();
        }

        @Override
        public String toString() {
            return target.toString();
        }
    }
}

1599408941972.png

​ 图1

1599409555723.png

​ 图2

  • 具体步骤如下:
  1. 根据target,解析生成MethodHandler对象
  2. 构建methodMethodHandler的关系
  3. 通过jdk动态代理生成代理对象
  4. DefaultMethodHandler绑定到代理对象

3. ParseHandlersByName.apply()

static final class ParseHandlersByName {
    private final Contract contract;
    private final Options options;
    private final Encoder encoder;
    private final Decoder decoder;
    private final ErrorDecoder errorDecoder;
    private final SynchronousMethodHandler.Factory factory;

    ParseHandlersByName(Contract contract, Options options, Encoder encoder, Decoder decoder,
                        ErrorDecoder errorDecoder, SynchronousMethodHandler.Factory factory) {
        this.contract = contract;
        this.options = options;
        this.factory = factory;
        this.errorDecoder = errorDecoder;
        this.encoder = checkNotNull(encoder, "encoder");
        this.decoder = checkNotNull(decoder, "decoder");
    }

    /**
     * 构建方法与具体处理对象的关系,用于后续构建动态代理对象时构建Method与MethodHandler
     * @param key
     * @return
     */
    public Map<String, MethodHandler> apply(Target key) {
        //从key中获实际的方法对象,生成方法元数据,见图3
        List<MethodMetadata> metadata = contract.parseAndValidatateMetadata(key.type());
        Map<String, MethodHandler> result = new LinkedHashMap<String, MethodHandler>();
        //根据方法元数据实例化生成MethodHandler
        for (MethodMetadata md : metadata) {
            BuildTemplateByResolvingArgs buildTemplate;
            if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) {
                buildTemplate = new BuildFormEncodedTemplateFromArgs(md, encoder);
            } else if (md.bodyIndex() != null) {
                buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder);
            } else {
                buildTemplate = new BuildTemplateByResolvingArgs(md);
            }
            result.put(md.configKey(),
                    factory.create(key, md, buildTemplate, options, decoder, errorDecoder));
        }
        return result;
    }
}
1599410025540.png

​ 图3

调用过程分析

通过Feign.Builder,我们已经将接口生成了具体的代理对象,在调用过程中,实际上就是调用代理对象InvocationHandlerinvoke方法来实现http调用。

  • 实际上,从上文的源码中可以发现,feign关键的地方在于FeignInvocationHandler.invoke()方法中的这一行代码return dispatch.get(method).invoke(args)
  • 其中dispatch就是上文中提到的Map<Method, MethodHandler> methodToHandler

1.FeignInvocationHandler.invoke()

public Object invoke(Object[] argv) throws Throwable {
    RequestTemplate template = buildTemplateFromArgs.create(argv);
    Retryer retryer = this.retryer.clone();
    while (true) {
      try {
        return executeAndDecode(template);
      } catch (RetryableException e) {
        retryer.continueOrPropagate(e);
        if (logLevel != Logger.Level.NONE) {
          logger.logRetry(metadata.configKey(), logLevel);
        }
        continue;
      }
    }
}
  • 从源码可知,该方法进行了以下步骤
  1. 根据请求参数解析生成RequestTemplate用于请求

  2. 构建Retryer重试机制

  3. 在一个循环体中执行executeAndDecode(template),这里主要是如果存在重试机制的话,在异常情况下会重复请求,默认的重试如下图

1599411038892.png

2. RequestTemplate

  • 通过RequestTemplate template = buildTemplateFromArgs.create(argv);得到一个RequestTemplate template,用于后续请求时成功具体的Request对象,但实际的生成并不在下面这个方法,从这个方法可以看出,在这里只是对参数的填充
public RequestTemplate create(Object[] argv)
{
    //
    RequestTemplate mutable = new RequestTemplate(metadata.template());
    if(metadata.urlIndex() != null)
    {
        int urlIndex = metadata.urlIndex();
        checkArgument(argv[urlIndex] != null, "URI parameter %s was null", urlIndex);
        mutable.insert(0, String.valueOf(argv[urlIndex]));
    }
    Map < String, Object > varBuilder = new LinkedHashMap < String, Object > ();
    for(Entry < Integer, Collection < String >> entry: metadata.indexToName().entrySet())
    {
        int i = entry.getKey();
        Object value = argv[entry.getKey()];
        if(value != null)
        { // Null values are skipped.
            if(indexToExpander.containsKey(i))
            {
                value = expandElements(indexToExpander.get(i), value);
            }
            for(String name: entry.getValue())
            {
                varBuilder.put(name, value);
            }
        }
    }
    RequestTemplate template = resolve(argv, mutable, varBuilder);
    if(metadata.queryMapIndex() != null)
    {
        // add query map parameters after initial resolve so that they take
        // precedence over any predefined values
        template = addQueryMapQueryParameters(argv, template);
    }
    if(metadata.headerMapIndex() != null)
    {
        template = addHeaderMapHeaders(argv, template);
    }
    return template;
}
  • 具体的RequestTemplate早在MethodMetadata创建时就存在,在构建过程中陆续的填充构建相关的请求信息

    1599412965939.png
```java
protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method)
{
    Class <? extends Annotation > annotationType = methodAnnotation.annotationType();
    //解析RequestLine注解填充相关的信息
    if(annotationType == RequestLine.class)
    {
        String requestLine = RequestLine.class.cast(methodAnnotation).value();
        checkState(emptyToNull(requestLine) != null, "RequestLine annotation was empty on method %s.", method.getName());
        //填充请求方式GET或者PORT
        if(requestLine.indexOf(' ') == -1)
        {
            checkState(requestLine.indexOf('/') == -1, "RequestLine annotation didn't start with an HTTP verb on method %s.", method.getName());
            data.template().method(requestLine);
            return;
        }
        data.template().method(requestLine.substring(0, requestLine.indexOf(' ')));
        //填充请求地址
        if(requestLine.indexOf(' ') == requestLine.lastIndexOf(' '))
        {
            // no HTTP version is ok
            data.template().append(requestLine.substring(requestLine.indexOf(' ') + 1));
        }
        else
        {
            // skip HTTP version
            data.template().append(requestLine.substring(requestLine.indexOf(' ') + 1, requestLine.lastIndexOf(' ')));
        }
        data.template().decodeSlash(RequestLine.class.cast(methodAnnotation).decodeSlash());
    }
    else if(annotationType == Body.class)
    {   
        //请求体数据填充
        String body = Body.class.cast(methodAnnotation).value();
        checkState(emptyToNull(body) != null, "Body annotation was empty on method %s.", method.getName());
        if(body.indexOf('{') == -1)
        {
            data.template().body(body);
        }
        else
        {
            data.template().bodyTemplate(body);
        }
    }
    else if(annotationType == Headers.class)
    {
        String[] headersOnMethod = Headers.class.cast(methodAnnotation).value();
        checkState(headersOnMethod.length > 0, "Headers annotation was empty on method %s.", method.getName());
        data.template().headers(toMap(headersOnMethod));
    }
}
```

*   数据填充的地方还有很多,这里就不一一例句,在这里,可以理解`Feign`通过对类注解的解析,将请求数据填充进`RequestTemplate`,在最后的请求中,将最终的请求参数填充到`RequestTemplate`中生成`Request`对象,在这,可以理解其实还是`建造者模式`的思想

3. Retryer

Retryer不是线程安全的对象,所以每一次方法调用我们都需要借助于原型模式来生成一个新的对象,这就是上文中为什么会有Retryer retryer = this.retryer.clone();的原因

/**
 * 如果重试允许的话,直接返回(可能在休眠一定时间之后)。其他情况,需要对外抛出异常对请求进行终止
 */
void continueOrPropagate(RetryableException e);

4. executeAndDecode(template)

Object executeAndDecode(RequestTemplate template) throws Throwable
{
    //根据RequestTemplate生成request对象
    Request request = targetRequest(template);
    //判断日志等级是否输出日志,下同
    if(logLevel != Logger.Level.NONE)
    {
        logger.logRequest(metadata.configKey(), logLevel, request);
    }
    Response response;
    //获取开始时间
    long start = System.nanoTime();
    try
    {
         //调用client对象发起请求,这里client也是一个外部组件,默认使用feign自带的client对象执行http调用逻辑。在工作中,我们用了okhttp3
        response = client.execute(request, options);
    }
    catch(IOException e)
    {
        if(logLevel != Logger.Level.NONE)
        {
            logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
        }
        throw errorExecuting(request, e);
    }
    // 统计请求调用花费的时间
    long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
    boolean shouldClose = true;
    try
    {
        if(logLevel != Logger.Level.NONE)
        {
            response = logger.logAndRebufferResponse(metadata.configKey(), logLevel, response, elapsedTime);
        }
        //如果元数据返回类型是Response,直接返回,不调用decode()进行解码
        if(Response.class == metadata.returnType())
        {
            if(response.body() == null)
            {
                return response;
            }
            if(response.body().length() == null || response.body().length() > MAX_RESPONSE_BUFFER_SIZE)
            {
                shouldClose = false;
                return response;
            }
            // Ensure the response body is disconnected
            byte[] bodyData = Util.toByteArray(response.body().asInputStream());
            return Response.create(response.status(), response.reason(), response.headers(), bodyData);
        }
          //调用decode()进行解码,其中会对200,404等情况使用不同的接码器
        if(response.status() >= 200 && response.status() < 300)
        {
            if(void.class == metadata.returnType())
            {
                return null;
            }
            else
            {
                return decode(response);
            }
        }
        //如果有配置404接码,会对404进行针对性的处理
        else if(decode404 && response.status() == 404)
        {
            return decoder.decode(response, metadata.returnType());
        }
        else
        {
            //可配置errorDecoder进行解码
            throw errorDecoder.decode(metadata.configKey(), response);
        }
    }
    catch(IOException e)
    {
        if(logLevel != Logger.Level.NONE)
        {
            logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime);
        }
        throw errorReading(request, response, e);
    }
    finally
    {
        //关闭response,因为response可能的个流操作,比如下载
        if(shouldClose)
        {
            ensureClosed(response.body());
        }
    }
}

该方法做的事情,我们罗列一下:

  1. 根据RequestTemplate生成Request对象

    • targetRequest(),是先调用的拦截器方法,再生成Request对象,拦截器我一般是用于鉴权时,在这通过拦截器填充token数据

      Request targetRequest(RequestTemplate template) {
        for (RequestInterceptor interceptor : requestInterceptors) {
          interceptor.apply(template);
        }
        return target.apply(new RequestTemplate(template));
      }
      
  2. 调用client对象发起请求,

    • client也是一个外部组件,默认使用feign自带的client对象执行http调用逻辑。
    • feign.Client.Default.execute()方法使用了HttpURLConnection的方式来请求web服务器,并没有使用对象池技术,所以性能较低,在实际工作中我使用了Okhttp3作为请求连接池,要注意的时候Okhttp3的连接初始化只有5个,我们需要按需设置,之前在性能调优时发现并发下出现连接池不够用的情况
  3. 如果元数据返回类型是Response,直接返回,不调用decode()解码

  4. 调用decode()进行解码,其中会对200,404等情况使用不同的接码器

  5. 在对Response使用完成之后,需要关闭Response,因为Response可能有对输入流的操作

总结

​ Feign 的英文表意为“假装,伪装,变形”,通过它可以以轻量级的面向对象的方式调用Http请求,而不用像Java中通过封装HTTP请求报文的方式直接调用,增强了代码的可维护性,提升了代码的质量,让调用者无需关心具体的调用过程,这也是为什么我喜欢Feign ,Feign包罗万象,而又不失开放,以插件的形式让我们需求得以个性化的落地,这一点值得我们在编码设计过程中拿来借鉴

参考

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