zuul学习五:zuul 异常处理

过滤器的执行流程
  • 一般来讲,正常的流程是pre-->route-->post
  • 在pre过滤器阶段抛出异常,pre--> error -->post
  • 在route过滤器阶段抛出异常,pre-->route-->error -->post
  • 在post过滤器阶段抛出异常,pre-->route-->post--> error

通过上面请求生命周期和核心过滤器的介绍,我们发现在核心过滤器中并没有实现error阶段的过滤器,那么当过滤器出现异常的时候需要怎么处理呢?

自定义一个过滤器ThrowExceptionFilter在执行时期抛出异常

@Component
public class ThrowExceptionFilter extends ZuulFilter{

    private static Logger logger = LoggerFactory.getLogger(ThrowExceptionFilter.class);

    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        logger.info("this is a pre filter,it will throw a RuntimeException");
        doSomething();
        return null;
    }

    private void doSomething(){
        throw new RuntimeException("exist some errors....");
    }
}

启动服务,


访问user服务,

http://192.168.5.5:6069/users/user/home

我们发现api网关服务的控制台输出ThrowExceptionFilter的过滤逻辑的日志信息,但是没有输出任何异常信息,同时发起的请求也没有获得任何响应结果。

为什么会出现这样的情况?我们又该怎样处理过滤器中的一场呢?

try-catch处理?

回想一下,我们在上一节中介绍的所有核心过滤器,有一个post过滤器SendErrorFilter用来处理一场信息的?根据正常的处理流程,该过滤器会处理异常信息,那么这里没有出现任何异常信息说明很有可能就是这个过滤器没有执行。所以看看SendErrorFiltershouldFilter函数:

@Override
public boolean shouldFilter() {
    RequestContext ctx = RequestContext.getCurrentContext();
    // only forward to errorPath if it hasn't been forwarded to already
    return ctx.containsKey("error.status_code")
            && !ctx.getBoolean(SEND_ERROR_FILTER_RAN, false);
}

可以看到,该方法的返回值中有一个重要的判断依据ctx.containsKey("error.status_code"),也就是说请求上下文必须有error.status_code参数,我们实现的ThrowExceptionFilter中没有设置这个参数,所以自然不会进入SendErrorFilter过滤器的处理逻辑。那么如何使用这个参数呢?可以看看route类型的几个过滤器,由于这些过滤器会对外发起请求,所以肯定有异常需要处理,比如RibbonRoutingFilter的run方法实现如下:

@Override
public Object run() {
    RequestContext context = RequestContext.getCurrentContext();
    this.helper.addIgnoredHeaders();
    try {
        RibbonCommandContext commandContext = buildCommandContext(context);
        ClientHttpResponse response = forward(commandContext);
        setResponse(response);
        return response;
    }
    catch (ZuulException ex) {
            context.set(ERROR_STATUS_CODE, ex.nStatusCode);
            context.set("error.message", ex.errorCause);
            context.set("error.exception", ex);
    }
    catch (Exception ex) {
        context.set("error.status_code",
            HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        context.set("error.exception", ex);
    }
    return null;
}

可以看到,整个发起请求的逻辑都采用了try-catch块处理。在catch异常的处理逻辑中并没有任何输出操作,而是向请求中添加了一些error相关的参数,主要有下面的三个参数。

error.status_code:错误代码
error.exception:Exception异常信息
error.message:错误信息

error.status_code就是SendErrorFilter过滤器用来判断是否需要执行的重要参数。可以改造一下我们的ThrowExceptionFilter的run方法,

改造ThrowExceptionFilter的run方法之后:

@Override
 public Object run() {
        logger.info("this is a pre filter,it will throw a RuntimeException");
        RequestContext context = RequestContext.getCurrentContext();
        try{
            doSomething();
        }catch (Exception e){
            context.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            context.set("error.message",e.getMessage());
            context.set("error.exception", e);
      }

        return null;
}

书中有个错context.set("error.message",e);这边在SendErrorFilter的时候就会类型转换报错。

此时,异常信息已经被SendErrorFilter过滤器正常处理并返回给客户端了,同时在网关的控制台中也输出了异常信息。从返回的响应信息中,可以看到几个之前我们在请求上下文中设置的内容,

  • status:对应error.status_code参数值
  • exception:对应error.exception参数中Exception的类型
  • message:对应error.exception参数中Exception的message信息。

再去访问http://192.168.1.57:6069/user-service/user/index,页面显示错误的页面。

ErrorFilter处理

通过上面的分析与实验,我们已经知道如何在过滤器中正确的处理异常,让错误信息能够顺利地流转到SendErrorFilter过滤器来组织和输出。但是,我们可以在过滤器中使用try-catch来处理业务逻辑并向请求上下文中添加异常信息,但是不可控的人为因素,意外的程序因素等,依然会使得一些异常从过滤器中抛出,怎样处理呢?

我们使用error类型的过滤器,在请求的生命周期的pre,route,post三个阶段中有异常抛出的时候都会进入error阶段的处理,所以可以通过创建一个error类型的过滤器来捕获这些异常信息,并根据这些异常信息在请求上下文中注入需要返回给客户端的错误描述。这里我们可以直接沿用try-catch处理异常信息时用的那些error参数,这样就可以让这些信息被SendErrorFilter捕获并组织成响应消息返回给客户端。

@Component
public class ErrorFilter extends ZuulFilter{

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public String filterType() {
        return "error";
    }

    @Override
    public int filterOrder() {
        return 10;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        RequestContext context = RequestContext.getCurrentContext();
        Throwable throwable = context.getThrowable();
        logger.error("this is a ErrorFilter :{}",throwable.getCause().getMessage());
        context.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        context.set("error.message",throwable.getCause().getMessage());
        return null;
    }
}

将上面的ThrowExceptionFilter过滤器不使用try...catch来处理,还是直接throw异常出去,这样ErrorFilter过滤器就能接收到抛出的异常,并且能将其流转到SendErrorFilter进行处理。(原因在于pre类型的过滤器流转到error类型的过滤器最后还是要流转到post类型的过滤器,之后会讲到)

@Override
public Object run() {
        logger.info("this is a pre filter,it will throw a RuntimeException");
        doSomething();
        return null;
 }

访问http://192.168.5.5:6069/users/user/index还是可以将异常和状态码打印在页面上。

不足与优化

我们已经掌握了核心过滤器处理逻辑之下,对自定义过滤器中处理逻辑的两种基本解决方法:一种是通过在各个阶段的过滤器中增加try..catch块,实现过滤器的内部处理;另外一种利用error类型过滤器的生命周期特性,集中处理pre,route,post阶段抛出的异常信息。通常情况下,我们可以将这二种手段同时使用,其中第一种是对开发人员的基本要求,第二种是对第一种处理方式的补充,防止意外的异常抛出。

还是有一些不足,看看外部请求到达api网关服务之后,各个阶段的过滤器是如何进行调度的,我们看com.netflix.zuul.http.ZuulServlet的service方法实现,定义了zuul处理外部请求过程,各个类型的过滤器的执行逻辑。代码中可以看到3个try...catch块,依次代表了preroutepost三个阶段的过滤器调用。在catch的异常处理中我们可以看到它们都会被error过滤器进行处理(之前使用error过滤器来定义统一的异常处理也正是利用了这个特性);error类型的过滤器处理完毕后,处理来自post阶段的异常外,都会在被post过滤器进行处理,

@Override
    public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
        try {
            init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);

            // Marks this request as having passed through the "Zuul engine", as opposed to servlets
            // explicitly bound in web.xml, for which requests will not have the same data attached
            RequestContext context = RequestContext.getCurrentContext();
            context.setZuulEngineRan();

            try {
                preRoute();
            } catch (ZuulException e) {
                error(e);
                postRoute();
                return;
            }
            try {
                route();
            } catch (ZuulException e) {
                error(e);
                postRoute();
                return;
            }
            try {
                postRoute();
            } catch (ZuulException e) {
                error(e);
                return;
            }

        } catch (Throwable e) {
            error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
        } finally {
            RequestContext.getCurrentContext().unset();
        }
    } 

对于从post过滤器中抛出的异常的情况,在经过error过滤器之后,就没有其他类型的过滤器来接手了,回想之前实现的二种异常处理方法,其中非常核心的一点是,这两种处理方法都在异常处理时向请求上下文添加了一系列的error.*参数,而这些参数真正起作用的地方是在post阶段的SendErrorFilter,在该过滤器中会使用这些参数来组织内容返回给客户端。而对于post阶段抛出的异常的情况,由error过滤器处理之后并不会再调用post阶段的请求,自然这些error.*参数也就不会被SendErrorFilter消费输出。我们在自定义post过滤器的时候,没有正确处理异常,就依然有可能出现日志中没有异常但请求响应内容为空的问题。可以将之前的ThrowExceptionFilter的filterType改为post来验证这个问题的存在,

@Component
public class ThrowExceptionFilter extends ZuulFilter{

    private static Logger logger = LoggerFactory.getLogger(ThrowExceptionFilter.class);

    @Override
    public String filterType() {
        return "post";
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        logger.info("this is a pre filter,it will throw a RuntimeException");
        doSomething();
        return null;
    }

    private void doSomething(){
        throw new RuntimeException("exist some errors....");
    }
}

访问:http://192.168.5.5:6069/users/user/index再去访问就会出现白板不抛异常的情况。

解决这个问题的方法有很多种,最直接的我们可以在实现error过滤器的时候,直接组织结果返回就能实现效果。缺点很明显,对于错误信息组织和返回代码实现会存在多份,不利于维护,我们希望将post过滤器抛出的异常交给SendErrorFilter来处理。

我们在之前实现了一个ErrorFilter来捕获pre,route,post过滤器抛出的异常,并组织error.*参数保存到请求的上下文。由于我们的目标是沿用SendErrorFilter,这些error.*参数依然对我们有用,所以可以继续沿用该过滤器,让它在post过滤器抛出异常的时候,继续组织error.*参数,只是这里我们已经无法将这些error.*参数传递给SendErrorFilter过滤器来处理了。所以,我们需要在ErrorFilter过滤器之后再定义一个error类型的过滤器,让它来实现SendErrorFilter的功能,但是这个error过滤器并不需要处理所有出现异常的情况,它仅仅处理post过滤器抛出的异常,复用它的run方法,然后重写它的类型,顺序及执行条件,实现对原有逻辑的复用

public class ErrorExtFilter extends SendErrorFilter{

    @Override
    public String filterType() {
        return "error";
    }

    @Override
    public int filterOrder() {
        return 30; //大于ErrorFilter的值
    }


    //只处理post过滤器抛出异常的过滤器
    @Override
    public boolean shouldFilter() {
        return true;
    }
}

如何实现shouldFilter的逻辑呢?当有异常抛出的时候,记录下抛出的过滤器,这样我们就可以在ErrorExtFilter过滤器的shouldFilter方法中获取并以此判断异常是否来自于post阶段的过滤器了。

为了扩展过滤器的处理逻辑,为请求上下文增加一些自定义属性,深入了解zuul过滤器的核心处理器:com.netflix.zuul.FilterProcessor,定义了过滤器调用和处理相关的核心方法:

  • getInstance:该方法用来获取当前处理器的实例
  • setProcessor(FilterProcessor processor):该方法用来设置处理器实例,可以使用此方法来设置自定义的处理器。
  • processZuulFilter(ZuulFilter filter):该方法定义了用来执行filter的具体逻辑,包括对请求上下文的设置,判断是否应该执行,执行时一些异常处理等。
  • runFilters(String sType):该方法会根据传入的filterType来调用getFiltersByType(String filterType)获取排序后的过滤器列表,然后轮询这些过滤器,并调用processZuulFilter(ZuulFilter filter)来依次执行它们。
  • preRoute():调用runFilters("pre")来执行所有pre类型的过滤器。
  • route():调用runFilters("route")来执行所有route类型的过滤器。
  • postRoute():调用runFilters("post")来执行所有post类型的过滤器。
  • error():调用runFilters("error")来执行所有error类型的过滤器。

直接扩展processZuulFilter(ZuulFilter filter),当过滤器执行抛出异常的时候,我们来捕获它,并向请求上下文中记录一些信息,

public class DidiFilterProcessor extends FilterProcessor{

    @Override
    public Object processZuulFilter(ZuulFilter filter) throws ZuulException {
        try{
            return super.processZuulFilter(filter);
        }catch (ZuulException e){
            RequestContext requestContext = RequestContext.getCurrentContext();
            requestContext.set("failed.filter",filter);
            throw e;
        }
    }
}

在上面的代码实现中,创建了一个FilterProcessor的子类,并重写了processZuulFilter(ZuulFilter filter),虽然主逻辑依然使用了父类的实现,但是在最外层,我们为其增加了异常捕获,并在异常处理中为请求上下文添加failed.filter属性,以存储抛出异常的过滤器实例。在实现了这个扩展之后,我们可以完善之前的ErrorExtFiltershouldFilter()方法了,通过从请求上下文中获取信息作出正确的判断:

@Component
public class ErrorExtFilter extends SendErrorFilter{

    @Override
    public String filterType() {
        return "error";
    }

    @Override
    public int filterOrder() {
        return 30; //大于ErrorFilter的值
    }

    //只处理post过滤器抛出异常的过滤器
    @Override
    public boolean shouldFilter() {
        //判断,仅处理来自post过滤器引起的异常
        RequestContext context = RequestContext.getCurrentContext();
        ZuulFilter failedFilter =(ZuulFilter)context.get("failed.filter");
        if(failedFilter != null && failedFilter.filterType().equals("post")){
            return true;
        }

        return false;

    }
}

最后,我们还要调用FilterProcessor.setProcessor(new DidiFilterProcessor());方法来启动自定义的核心处理器。

@SpringBootApplication
@EnableZuulProxy
public class ZuulApplication {
    public static void main(String[] args) {
        FilterProcessor.setProcessor(new DidiFilterProcessor());
        SpringApplication.run(ZuulApplication.class,args);
}

自定义异常信息

实际应用到业务系统中,默认的错误信息并不符合系统设计的响应格式,那么我们就需要对返回的异常信息进行定制。对于如何定制这个错误信息有很多种方法可以实现。
最直接的是,可以编写一个自定义的post过滤器来组织错误结果,该方法实现起来简单粗暴,完全可以参考SendErrorFilter的实现,然后直接组织请求响应而不是forward到/error端点,只是使用该方法时需要注意:为了替代SendErrorFilter,还需要禁用SendErrorFilter过滤器(下面提到怎么禁用zuul的filter)。

demo
写的很随意的一个过滤器,参考SendErrorFilter和SendResponseFilter过滤器:

@Component
public class SendNewErrorFilter extends ZuulFilter{

    private Logger log = LoggerFactory.getLogger(getClass());

    protected static final String SEND_ERROR_FILTER_RAN = "sendErrorFilter.ran";

    @Override
    public String filterType() {
        return "post";
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        // only forward to errorPath if it hasn't been forwarded to already
        return ctx.containsKey("error.status_code")
                && !ctx.getBoolean(SEND_ERROR_FILTER_RAN, false);
    }

    @Override
    public Object run() {
        try {
            RequestContext ctx = RequestContext.getCurrentContext();
            HttpServletRequest request = ctx.getRequest();

            HttpServletResponse servletResponse = ctx.getResponse();
            servletResponse.setCharacterEncoding("UTF-8");
            OutputStream outStream = servletResponse.getOutputStream();
            String errormessage = "error,try again later!!";
            InputStream is = new ByteArrayInputStream(errormessage.getBytes(servletResponse.getCharacterEncoding()));
            writeResponse(is,outStream);
        }
        catch (Exception ex) {
            ReflectionUtils.rethrowRuntimeException(ex);
        }
        return null;
    }


    private void writeResponse(InputStream zin, OutputStream out) throws Exception {
        byte[] bytes = new byte[1024];
        int bytesRead = -1;
        while ((bytesRead = zin.read(bytes)) != -1) {
            out.write(bytes, 0, bytesRead);
        }
    }
}

然后禁用调默认的SendErrorFilter过滤器

zuul:
  SendErrorFilter:
    post:
      disable: true
  SendResponseFilter:
    post:
      disable: true

再去访问http://192.168.1.57:6069/user-service/user/index页面展示自定义的异常。

方法二
如果不采用重写过滤器的方式,依然想要使用SendErrorFilter来处理异常返回的话,我们需要如何去定制返回的结果呢?这个时候,我们的关注点就不能放在zuul的过滤器上了,因为错误信息的生成实际上并不是由spring cloud zuul完成的。我们在介绍SendErrorFilter的时候提到过,它会根据请求上下文保存的错误信息来组织一个forward到/error端点的请求来获取错误响应,所以我们的扩展目标转移到/error端点的实现。

/error端点的实现来源于Springboot的org.springframework.boot.autoconfigure.web.BasicErrorController

@RequestMapping
@ResponseBody
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
    Map<String, Object> body = getErrorAttributes(request,
            isIncludeStackTrace(request, MediaType.ALL));
    HttpStatus status = getStatus(request);
    return new ResponseEntity<Map<String, Object>>(body, status);
}
protected Map<String, Object> getErrorAttributes(HttpServletRequest request,
        boolean includeStackTrace) {
    RequestAttributes requestAttributes = new ServletRequestAttributes(request);
    return this.errorAttributes.getErrorAttributes(requestAttributes,
            includeStackTrace);
}

getErrorAttributes的实现默认的是DefaultErrorAttributes的实现。

从源码中可以看到,实现非常简单,通过getErrorAttributes方法根据请求参数组织错误信息的返回结果,而这里的getErrorAttributes方法会将具体组织逻辑委托给org.springframework.boot.autoconfigure.web.ErrorAttributes接口提供的
getErrorAttributes来实现。在spring boot的自动化配置机制中,默认会采用org.springframework.boot.autoconfigure.web.DefaultErrorAttributes作为该接口的实现。

再定义Error处理的自动化配置中,该接口的默认实现采用@ConditionalOnMissingBean修饰,说明DefaultErrorAttributes实例仅在没有ErrorAttributes接口的实例时才会被创建出来使用,所以我们只需要自己编写一个自定义的ErrorAttributes接口实现类,并创建它的实例替代这个默认实现,达到自定义错误信息的效果。

@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
// Load before the main WebMvcAutoConfiguration so that the error View is available
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties(ResourceProperties.class)
public class ErrorMvcAutoConfiguration {

    private final ApplicationContext applicationContext;

    private final ServerProperties serverProperties;

    private final ResourceProperties resourceProperties;

    @Autowired(required = false)
    private List<ErrorViewResolver> errorViewResolvers;

    public ErrorMvcAutoConfiguration(ApplicationContext applicationContext,
            ServerProperties serverProperties, ResourceProperties resourceProperties) {
        this.applicationContext = applicationContext;
        this.serverProperties = serverProperties;
        this.resourceProperties = resourceProperties;
    }

    @Bean
    @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
    public DefaultErrorAttributes errorAttributes() {
        return new DefaultErrorAttributes();
}

举个例子,我们不希望将exception属性返回给客户端,那么就可以编写一个自定义的实现,它可以基于DefaultErrorAttribute,然后重写getErrorAttributes方法,从原来的结果中将exception移除即可,具体实现如下:

public class DidiErrorAttributes extends DefaultErrorAttributes{


    @Override
    public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {
        Map<String,Object> result = super.getErrorAttributes(requestAttributes,includeStackTrace);
        result.put("error","missing error");
        return result;
    }
}

发现去掉四个属性都不行,具体的细节源码没去研究,

最后,为了让自定义的错误信息生成逻辑生效,需要在应用主类中加入如下代码,为其创建实例代替默认的实现:

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

推荐阅读更多精彩内容