Tomcat请求响应处理(二)

前言
在上半部分我们分析了Tomcat请求响应的生成过程,以及对应请求容器的映射过程,就像客人去朋友家小聚,首先肯定要知道朋友的地址和门牌号码,知道之后当然就要敲门进去,一阵吃喝吹吹牛逼,再从朋友家出来返回。地址和门牌号码对应请求映射关系,吃吃喝喝对应Servlet处理业务逻辑,回家对应返回response。本文就对下半部分:根据门牌号码找到朋友家再返回的过程进行解析

Tomcat请求响应处理(一)的最后,我们看到一个很长的链式调用connector.getService().getContainer().getPipeline().getFirst().invoke(request, response),我们一点点来分析一下。connector.getService()得到Connector的父容器StandardService,由于StandardService是连接Tomcat两大组件的桥梁,自然getContainer()又可以得到Container顶层容器StandardEngine,在Tomcat架构中各个组件及组件间关系(二)中说过每个Container都存在一个StandardPipeline管道,每个管道中存在一个或者多个Valve阀门。当请求来时会按照容器的父子关系依次流入一个个管道,遇到管道中一个个阀门,既然管道有顺序,里面的阀门也有顺序,getPipeline().getFirst()就对应着第一个阀门StandardEngineValve,进而调用其invoke(Request, Response)

图1. StandardEngineValve的invoke(Request, Response)

由于参数request中已经保存了正确的“门牌号码”,自然能得到请求对应的虚拟主机StandardHost,如果此时该对象为空自然有问题,将错误码塞入response中返回,最后责任链模式再次出现,调用StandardHost中管道的第一个阀门,默认情况下在server.xml中存在一个Valve,对应的实体为AccessLogValve,主要用来记录该虚拟主机的访问情况
图2. AccessLogValve的invoke(Request, Response)

其中并没有做其他的操作,仅仅调用了管道中下一个阀门,下一个阀门依然不是基础阀门,在StandardHost启动时Tomcat又为其添加了另一个“错误上报阀门”
图3. StandardHost的startInternal()

getErrorReportValveClass()返回该阀门对应全路径字符串org.apache.catalina.valves.ErrorReportValve,当管道中不存在对应名称的阀门就将该阀门加入管道中
图4. ErrorReportValve的invoke()

该阀门的第一句直接调用了下一个阀门,我们可以把该阀门的功能理解为spring中的后置增强,即在响应之后再进行某些操作,因为该阀门是用来记录处理请求中产生错误的,而上面说过,当流程中发生错误会存在一个对应的错误码,而该错误码又封装在response中,那这里就不难理解为什么要在调用链返回过程中再做处理。下一个阀门就是StandardHost的基础阀门StandardHostValve代码清单1

@Override
public final void invoke(Request request, Response response)
    throws IOException, ServletException {

    // Select the Context to be used for this Request
    Context context = request.getContext();
    if (context == null) {
        response.sendError
            (HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
             sm.getString("standardHost.noContext"));
        return;
    }

    // Bind the context CL to the current thread
    if( context.getLoader() != null ) {
        // Not started - it should check for availability first
        // This should eventually move to Engine, it's generic.
        if (Globals.IS_SECURITY_ENABLED) {
            PrivilegedAction<Void> pa = new PrivilegedSetTccl(
                    context.getLoader().getClassLoader());
            AccessController.doPrivileged(pa);                
        } else {
            Thread.currentThread().setContextClassLoader
                    (context.getLoader().getClassLoader());
        }
    }
    if (request.isAsyncSupported()) {
        request.setAsyncSupported(context.getPipeline().isAsyncSupported());
    }
    //    (1)
    boolean asyncAtStart = request.isAsync();
    boolean asyncDispatching = request.isAsyncDispatching();
    if (asyncAtStart || context.fireRequestInitEvent(request)) {

        // Ask this Context to process this request. Requests that are in
        // async mode and are not being dispatched to this resource must be
        // in error and have been routed here to check for application
        // defined error pages.
        try {
            if (!asyncAtStart || asyncDispatching) {
                //    (2)
                context.getPipeline().getFirst().invoke(request, response);
            } else {
                // Make sure this request/response is here because an error
                // report is required.
                if (!response.isErrorReportRequired()) {
                    throw new IllegalStateException(sm.getString("standardHost.asyncStateError"));
                }
            }
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            container.getLogger().error("Exception Processing " + request.getRequestURI(), t);
            // If a new error occurred while trying to report a previous
            // error allow the original error to be reported.
            if (!response.isErrorReportRequired()) {
                request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t);
                throwable(request, response, t);
            }
        }

        // Now that the request/response pair is back under container
        // control lift the suspension so that the error handling can
        // complete and/or the container can flush any remaining data
        response.setSuspended(false);

        Throwable t = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);

        // Protect against NPEs if the context was destroyed during a
        // long running request.
        if (!context.getState().isAvailable()) {
            return;
        }

        // Look for (and render if found) an application level error page
        if (response.isErrorReportRequired()) {
            if (t != null) {
                throwable(request, response, t);
            } else {
                status(request, response);
            }
        }

        if (!request.isAsync() && (!asyncAtStart || !response.isErrorReportRequired())) {
            context.fireRequestDestroyEvent(request);
        }
    }

    // Access a session (if present) to update last accessed time, based on a
    // strict interpretation of the specification
    if (ACCESS_SESSION) {
        request.getSession(false);
    }

    // Restore the context classloader
    if (Globals.IS_SECURITY_ENABLED) {
        PrivilegedAction<Void> pa = new PrivilegedSetTccl(
                StandardHostValve.class.getClassLoader());
        AccessController.doPrivileged(pa);                
    } else {
        Thread.currentThread().setContextClassLoader
                (StandardHostValve.class.getClassLoader());
    }
}

标注(1)下的两行代码判断该请求是否异步,默认为false,Context.fireRequestInitEvent(Request),该方法内封装了ServletRequestEvent事件,并由一系列的应用事件监听器applicationEventListenersObjects负责处理,同样默认不存在具体的监听器,返回true,导致代码走到标注(2)处,再一次责任链调用StandardContext内的第一个阀门

图5. StandardContextValve的invoke()

看到第一个if判断对/META-INF//WEB-INF目录下的资源进行了过滤,记得我刚学web编程时老师让我们把自定义的Servlet放在/WEB-INF下,说是不让直接访问只能内部跳转从而保证安全,那不能访问的秘密就是这段代码了。之后调用请求映射的Wrapper,进而invoke管道中的第一个阀门,对应的类为StandardWrapperValve
图6. StandardWrapperValve的invoke()

图中我删除了很多非重点代码,并将主要流程分成两部分,第一红框域具体的Servlet有关,第二个与该Servlet相关的过滤器有关。我们从第一个开始分析,wrapper.allocate()最终会调用StandardWrapper.loadServlet()代码清单2

public synchronized Servlet loadServlet() throws ServletException {

    if (unloading) {
        throw new ServletException(
                sm.getString("standardWrapper.unloading", getName()));
    }
    //    (1)
    // Nothing to do if we already have an instance or an instance pool
    if (!singleThreadModel && (instance != null))
        return instance;

    PrintStream out = System.out;
    if (swallowOutput) {
        SystemLogHandler.startCapture();
    }

    Servlet servlet;
    try {
        long t1=System.currentTimeMillis();
        // Complain if no servlet class has been specified
        if (servletClass == null) {
            unavailable(null);
            throw new ServletException
                (sm.getString("standardWrapper.notClass", getName()));
        }

        InstanceManager instanceManager = ((StandardContext)getParent()).getInstanceManager();
        try {
            //    (2)
            servlet = (Servlet) instanceManager.newInstance(servletClass);

        } catch (ClassCastException e) {
            unavailable(null);
            // Restore the context ClassLoader
            throw new ServletException
                (sm.getString("standardWrapper.notServlet", servletClass), e);
        } catch (Throwable e) {
            e = ExceptionUtils.unwrapInvocationTargetException(e);
            ExceptionUtils.handleThrowable(e);
            unavailable(null);

            // Added extra log statement for Bugzilla 36630:
            // http://bz.apache.org/bugzilla/show_bug.cgi?id=36630
            if(log.isDebugEnabled()) {
                log.debug(sm.getString("standardWrapper.instantiate", servletClass), e);
            }

            // Restore the context ClassLoader
            throw new ServletException
                (sm.getString("standardWrapper.instantiate", servletClass), e);
        }
        //    (3)
        if (multipartConfigElement == null) {
            MultipartConfig annotation =
                    servlet.getClass().getAnnotation(MultipartConfig.class);
            if (annotation != null) {
                multipartConfigElement =
                        new MultipartConfigElement(annotation);
            }
        }

        processServletSecurityAnnotation(servlet.getClass());

        // Special handling for ContainerServlet instances
        if ((servlet instanceof ContainerServlet) &&
                (isContainerProvidedServlet(servletClass) ||
                        ((Context) getParent()).getPrivileged() )) {
            ((ContainerServlet) servlet).setWrapper(this);
        }

        classLoadTime=(int) (System.currentTimeMillis() -t1);

        if (servlet instanceof SingleThreadModel) {
            if (instancePool == null) {
                instancePool = new Stack<Servlet>();
            }
            singleThreadModel = true;
        }
        //    (4)
        initServlet(servlet);

        fireContainerEvent("load", this);

        loadTime=System.currentTimeMillis() -t1;
    } finally {
        if (swallowOutput) {
            String log = SystemLogHandler.stopCapture();
            if (log != null && log.length() > 0) {
                if (getServletContext() != null) {
                    getServletContext().log(log);
                } else {
                    out.println(log);
                }
            }
        }
    }
    return servlet;

}

标注(1)判断该Servlet是否为单例,默认Servlet是多例的,如果实现一个过时的SingleThreadModel标记接口,Tomcat就会将标识singleThreadModel置为true,而这里就会直接返回;否则进入标注(2)根据解析的servletClass反射创建出用户自己配置的Servlet。标注(3)是在Servlet3.0中引入的注解形式的文件上传方式校验,标注(4)最终会调用Servletinit(ServletConfig),该方法GenericServlet.init(ServletConfig),内部又调用了一个无参的init(),当我们创建Servlet时可以覆写该方法,从而在第一次调用Servlet时进行一些初始化的操作
我们回到图6中的第二个红框,代码使用工厂创建了一个过滤器链filterChain,具体创建代码如 代码清单3

public ApplicationFilterChain createFilterChain
    (ServletRequest request, Wrapper wrapper, Servlet servlet) {

    // get the dispatcher type
    DispatcherType dispatcher = null;
    if (request.getAttribute(Globals.DISPATCHER_TYPE_ATTR) != null) {
        dispatcher = (DispatcherType) request.getAttribute(
                Globals.DISPATCHER_TYPE_ATTR);
    }
    String requestPath = null;
    Object attribute = request.getAttribute(
            Globals.DISPATCHER_REQUEST_PATH_ATTR);

    if (attribute != null){
        requestPath = attribute.toString();
    }

    // If there is no servlet to execute, return null
    if (servlet == null)
        return (null);

    boolean comet = false;

    // Create and initialize a filter chain object
    ApplicationFilterChain filterChain = null;
    if (request instanceof Request) {
        Request req = (Request) request;
        comet = req.isComet();
        if (Globals.IS_SECURITY_ENABLED) {
            // Security: Do not recycle
            filterChain = new ApplicationFilterChain();
            if (comet) {
                req.setFilterChain(filterChain);
            }
        } else {
            //    (1)
            filterChain = (ApplicationFilterChain) req.getFilterChain();
            if (filterChain == null) {
                filterChain = new ApplicationFilterChain();
                req.setFilterChain(filterChain);
            }
        }
    } else {
        // Request dispatcher in use
        filterChain = new ApplicationFilterChain();
    }
    //    (2)
    filterChain.setServlet(servlet);

    filterChain.setSupport
        (((StandardWrapper)wrapper).getInstanceSupport());

    // Acquire the filter mappings for this Context
    StandardContext context = (StandardContext) wrapper.getParent();
    FilterMap filterMaps[] = context.findFilterMaps();

    // If there are no filter mappings, we are done
    if ((filterMaps == null) || (filterMaps.length == 0))
        return (filterChain);

    // Acquire the information we will need to match filter mappings
    String servletName = wrapper.getName();

    // Add the relevant path-mapped filters to this filter chain
    //    (3)
    for (int i = 0; i < filterMaps.length; i++) {
        if (!matchDispatcher(filterMaps[i] ,dispatcher)) {
            continue;
        }
        if (!matchFiltersURL(filterMaps[i], requestPath))
            continue;
        ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
            context.findFilterConfig(filterMaps[i].getFilterName());
        if (filterConfig == null) {
            continue;
        }
        boolean isCometFilter = false;
        if (comet) {
            try {
                isCometFilter = filterConfig.getFilter() instanceof CometFilter;
            } catch (Exception e) {
                // Note: The try catch is there because getFilter has a lot of
                // declared exceptions. However, the filter is allocated much
                // earlier
                Throwable t = ExceptionUtils.unwrapInvocationTargetException(e);
                ExceptionUtils.handleThrowable(t);
            }
            if (isCometFilter) {
                filterChain.addFilter(filterConfig);
            }
        } else {
            filterChain.addFilter(filterConfig);
        }
    }

    // Add filters that match on servlet name second
    //    (4)
    for (int i = 0; i < filterMaps.length; i++) {
        if (!matchDispatcher(filterMaps[i] ,dispatcher)) {
            continue;
        }
        if (!matchFiltersServlet(filterMaps[i], servletName))
            continue;
        ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
            context.findFilterConfig(filterMaps[i].getFilterName());
        if (filterConfig == null) {
            continue;
        }
        boolean isCometFilter = false;
        if (comet) {
            try {
                isCometFilter = filterConfig.getFilter() instanceof CometFilter;
            } catch (Exception e) {
                // Note: The try catch is there because getFilter has a lot of
                // declared exceptions. However, the filter is allocated much
                // earlier
            }
            if (isCometFilter) {
                filterChain.addFilter(filterConfig);
            }
        } else {
            filterChain.addFilter(filterConfig);
        }
    }

    // Return the completed filter chain
    return (filterChain);

}

标注(1)和(2)创建出过滤器链并将请求与之对应的Servlet与之关联,通过StandardContext得到web.xml中配置的所有<filter-mapping>对应的实体FilterMap数组,该数组中的数据来源逆序调用为StandardContext -> addFilterMap(FilterMap) -> WebXml.configureContext(Context) -> ContextConfig.webConfig() -> ContextConfig.configureStart() -> ContextConfig检测到Lifecycle.CONFIGURE_START_EVENT事件,具体的流程分析可以参考Tomcat架构中各个组件及组件间关系(二)。还有一点需要注意的是,每一个过滤器都存在一个类型为DispatcherType的调度类型dispatcher,表示该过滤器对哪一种请求类型进行拦截,共有FORWORDINCLUDEREQUESTASYNCERROR五种类型,默认为REQUEST
标注(3)遍历所有的过滤器数组,进行两轮匹配判断,第一轮matchDispatcher(FilterMap, DispatcherType)判断是否filter配置的调度类型在上述五种类型之中;第二轮matchFiltersURL(FilterMap, String)判断请求URL是否命中一个filter,如若存在一个匹配过滤器,那么根据对应filter名称从StandardContext中的成员变量HashMap<String, ApplicationFilterConfig> filterConfigs中得到对应的实例ApplicationFilterConfig。之前的文章曾经分析过在解析web.xml时,过滤器对应的对象为FilterDef,但在StandardContext启动时中间有一步是启动所有的过滤器,此时会将所有的FilterDef转成ApplicationFilterConfig放入该Map
要理解标注(4)必须先回忆一下<filter-mapping>配置的方式,要拦截请求其实有两种方式:1.配置<url-pattern>过滤请求路径;2.配置<servlet-name>过滤特定Servlet,那代码中的两个for循环就对应两种方式了。最后将请求路径或者请求对应Servlet的过滤器通过addFilter(filterConfig)加入到ApplicationFilterChain中成员变量filters数组中
回到图6第二个红框中最后一句,终于见到了我们熟悉的doFilter(Request, Response),内部最终走到ApplicationFilterChain.internalDoFilter(ServletRequest, ServletResponse),见代码清单4

private void internalDoFilter(ServletRequest request,
                              ServletResponse response)
        throws IOException, ServletException {

    // Call the next filter if there is one
    if (pos < n) {
        ApplicationFilterConfig filterConfig = filters[pos++];
        Filter filter = null;

        try {
            filter = filterConfig.getFilter();
            support.fireInstanceEvent(InstanceEvent.BEFORE_FILTER_EVENT,
                    filter, request, response);

            if (request.isAsyncSupported() && "false".equalsIgnoreCase(
                    filterConfig.getFilterDef().getAsyncSupported())) {
                request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR,
                        Boolean.FALSE);
            }
            if (Globals.IS_SECURITY_ENABLED) {
                final ServletRequest req = request;
                final ServletResponse res = response;
                Principal principal =
                        ((HttpServletRequest) req).getUserPrincipal();

                Object[] args = new Object[] {req, res, this};
                SecurityUtil.doAsPrivilege
                        ("doFilter", filter, classType, args, principal);

            } else {
                filter.doFilter(request, response, this);
            }

            support.fireInstanceEvent(InstanceEvent.AFTER_FILTER_EVENT,
                    filter, request, response);
        } catch (IOException e) {
            if (filter != null) {
                support.fireInstanceEvent(InstanceEvent.AFTER_FILTER_EVENT,
                        filter, request, response, e);
            }
            throw e;
        } catch (ServletException e) {
            if (filter != null) {
                support.fireInstanceEvent(InstanceEvent.AFTER_FILTER_EVENT,
                        filter, request, response, e);
            }
            throw e;
        } catch (RuntimeException e) {
            if (filter != null) {
                support.fireInstanceEvent(InstanceEvent.AFTER_FILTER_EVENT,
                        filter, request, response, e);
            }
            throw e;
        } catch (Throwable e) {
            e = ExceptionUtils.unwrapInvocationTargetException(e);
            ExceptionUtils.handleThrowable(e);
            if (filter != null) {
                support.fireInstanceEvent(InstanceEvent.AFTER_FILTER_EVENT,
                        filter, request, response, e);
            }
            throw new ServletException
                    (sm.getString("filterChain.filter"), e);
        }
        return;
    }

    // We fell off the end of the chain -- call the servlet instance
    try {
        if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
            lastServicedRequest.set(request);
            lastServicedResponse.set(response);
        }

        support.fireInstanceEvent(InstanceEvent.BEFORE_SERVICE_EVENT,
                servlet, request, response);
        if (request.isAsyncSupported()
                && !support.getWrapper().isAsyncSupported()) {
            request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR,
                    Boolean.FALSE);
        }

        // Use potentially wrapped request from this point
        if ((request instanceof HttpServletRequest) &&
                (response instanceof HttpServletResponse)) {

            if (Globals.IS_SECURITY_ENABLED) {
                final ServletRequest req = request;
                final ServletResponse res = response;
                Principal principal =
                        ((HttpServletRequest) req).getUserPrincipal();
                Object[] args = new Object[] {req, res};
                SecurityUtil.doAsPrivilege("service",
                        servlet,
                        classTypeUsedInService,
                        args,
                        principal);
            } else {
                servlet.service(request, response);
            }
        } else {
            servlet.service(request, response);
        }
        support.fireInstanceEvent(InstanceEvent.AFTER_SERVICE_EVENT,
                servlet, request, response);
    } catch (IOException e) {
        support.fireInstanceEvent(InstanceEvent.AFTER_SERVICE_EVENT,
                servlet, request, response, e);
        throw e;
    } catch (ServletException e) {
        support.fireInstanceEvent(InstanceEvent.AFTER_SERVICE_EVENT,
                servlet, request, response, e);
        throw e;
    } catch (RuntimeException e) {
        support.fireInstanceEvent(InstanceEvent.AFTER_SERVICE_EVENT,
                servlet, request, response, e);
        throw e;
    } catch (Throwable e) {
        ExceptionUtils.handleThrowable(e);
        support.fireInstanceEvent(InstanceEvent.AFTER_SERVICE_EVENT,
                servlet, request, response, e);
        throw new ServletException
                (sm.getString("filterChain.servlet"), e);
    } finally {
        if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
            lastServicedRequest.set(null);
            lastServicedResponse.set(null);
        }
    }

}

虽然代码比较长但是结构还是很清晰的,主要分为上下两部分。第一部分由整个if块包裹,pos是当前遍历ApplicationFilter元素对应的数组下标,n为整个数组长度,总的意思就是遍历过滤器数组中每一个filter,并依次调用它的doFilter(ServletRequest, ServletResponse),该方法就由我们自己实现了。当所有的filter处理完毕走到第二部分,就调用serlvet.service(request, response)。至此Tomcat整个请求响应处理的过程分析完毕

后记
对于Tomcat源码的解析暂时告一段落了,我从整个过程中学到了很多。好的代码就像好的作文一样,对程序员的影响是潜移默化的,很难想象一个从来不看范文的人能写出多好的文章

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