Spring MVC中组件Controller参数

1、概念

HandlerMapping通过request找到了Handler,HandlerAdapter是具体用来干活的,每个HandlerAdapter封装了一种Handler的具体使用方法。

2、HandlerAdapter基本结构

image

HandlerAdapter是一个接口实现这个接口的有四个类,其中实现该接口的AbstractHandlerMethodAdapter的子类结构最为复杂,同时也是由于处理的Handler不是固定的格式。下面主要来介绍RequestMappingHandlerAdapter。

具体的请求处理过程可以分为三步

  • 准备好处理器所需要的参数
  • 使用处理器处理请求
  • 处理返回值,也就是将不同类型的返回值统一处理成ModelAndView类型

理解参数的绑定之前需要先弄清楚三个问题

  • 都有哪些参数需要绑定(请求方法时,参数的格式多种多样)
  • 参数的值的来源
  • 具体进行绑定的方法

参数的来源一共有6种

  • request中相关的参数,主要包括url中的参数、post过来的参数以及请求头所包含的值
  • cookie中的参数
  • session中的参数
  • 设置到FlashMap中的参数,这种参数主要用于redirect的参数传递
  • SessionAttribute传递的参数,这类参数通过@SessionAttribute注释传递
  • 通过相应的注释@ModelAttribute的方法进行设置的参数

参数的具体解析是使用HandlerMethodArgumentResolver类型的组件完成的,不同类型的参数使用不同的ArgumentResolver来解析。

3、RequestMappingHandlerAdapter自身结构

3.1 RequestMappingHandlerAdapter创建

RequestMappingHandlerAdapter的创建是在afterPropertiesSet方法中实现,其中主要内容就是初始化argumentResolver、initBinderArgumentResolver、returnValueHandler以及
@ControllerAdvice注释的类相关的modelAttributeAdviceCache、initBinderAdviceCache和responseBodyAdvice这6个属性

  • argumentResolver:用于给处理器方法和注释了@ModelAttribute的方法设置参数
  • initBinderArgumentResolver:用于给注释了@initBinder的方法设置参数
  • returnValueHandlers:用于将处理器的返回值处理成ModelAndView的类型
  • modelAttributeAdviceCache和initBinderAdviceCache:分别用于缓存@ControllerAdvice注释的类里面注释了@ModelAttribute和@InitBinder的方法
  • responseBodyAdvice:用来保存前面介绍过的实现了@ResponseBodyAdvice接口、可以修改返回的ResponseBody的类

public void afterPropertiesSet() {
        // Do this first, it may add ResponseBody advice beans
        //初始化注注释了@ControllerAdvice的类的相关属性
        initControllerAdviceCache();

        if (this.argumentResolvers == null) {
            List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
            this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
        }
        if (this.initBinderArgumentResolvers == null) {
            List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();
            this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
        }
        if (this.returnValueHandlers == null) {
            List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
            this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
        }
    }
    


private void initControllerAdviceCache() {
        if (getApplicationContext() == null) {
            return;
        }
        if (logger.isInfoEnabled()) {
            logger.info("Looking for @ControllerAdvice: " + getApplicationContext());
        }

        //获取到所有注释了@ControllerAdvice的bean
        List<ControllerAdviceBean> beans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
        
        //根据Order排序
        Collections.sort(beans, new OrderComparator());

        List<Object> responseBodyAdviceBeans = new ArrayList<Object>();

        for (ControllerAdviceBean bean : beans) {
            //查找了注释了@ModelAttribute而且没有注释@RequestMapping的方法
            Set<Method> attrMethods = HandlerMethodSelector.selectMethods(bean.getBeanType(), MODEL_ATTRIBUTE_METHODS);
            if (!attrMethods.isEmpty()) {
                this.modelAttributeAdviceCache.put(bean, attrMethods);
                logger.info("Detected @ModelAttribute methods in " + bean);
            }
            //查找注释了@InitBinder的方法
            Set<Method> binderMethods = HandlerMethodSelector.selectMethods(bean.getBeanType(), INIT_BINDER_METHODS);
            if (!binderMethods.isEmpty()) {
                this.initBinderAdviceCache.put(bean, binderMethods);
                logger.info("Detected @InitBinder methods in " + bean);
            }
            //查找实现了ResponseBodyAdvice接口的类
            if (ResponseBodyAdvice.class.isAssignableFrom(bean.getBeanType())) {
                responseBodyAdviceBeans.add(bean);
                logger.info("Detected ResponseBodyAdvice bean in " + bean);
            }
        }

        if (!responseBodyAdviceBeans.isEmpty()) {
            this.responseBodyAdvice.addAll(0, responseBodyAdviceBeans);
        }
    }

查看后面三个属性属性的初始化中getDefaultXXX方法

/**
     * Return the list of argument resolvers to use including built-in resolvers
     * and custom resolvers provided via {@link #setCustomArgumentResolvers}.
     */
    private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
        List<HandlerMethodArgumentResolver> resolvers = new ArrayList<HandlerMethodArgumentResolver>();

        // Annotation-based argument resolution
        resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
        resolvers.add(new RequestParamMapMethodArgumentResolver());
        resolvers.add(new PathVariableMethodArgumentResolver());
        resolvers.add(new PathVariableMapMethodArgumentResolver());
        resolvers.add(new MatrixVariableMethodArgumentResolver());
        resolvers.add(new MatrixVariableMapMethodArgumentResolver());
        resolvers.add(new ServletModelAttributeMethodProcessor(false));
        resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters()));
        resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters()));
        resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
        resolvers.add(new RequestHeaderMapMethodArgumentResolver());
        resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory()));
        resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));

        // Type-based argument resolution
        resolvers.add(new ServletRequestMethodArgumentResolver());
        resolvers.add(new ServletResponseMethodArgumentResolver());
        resolvers.add(new HttpEntityMethodProcessor(getMessageConverters()));
        resolvers.add(new RedirectAttributesMethodArgumentResolver());
        resolvers.add(new ModelMethodProcessor());
        resolvers.add(new MapMethodProcessor());
        resolvers.add(new ErrorsMethodArgumentResolver());
        resolvers.add(new SessionStatusMethodArgumentResolver());
        resolvers.add(new UriComponentsBuilderMethodArgumentResolver());

        // Custom arguments 添加自定义的参数解析器
        if (getCustomArgumentResolvers() != null) {
            resolvers.addAll(getCustomArgumentResolvers());
        }

        // Catch-all
        resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
        resolvers.add(new ServletModelAttributeMethodProcessor(true));

        return resolvers;
    }
    //方法中将默认的参数处理器全部添加进来

//注:在添加参数解析器时,顺序是无法进行改变的
3.2.RequestMappingHandlerAdapter使用

RequestMappingHandlerAdapter处理请求的入口方法是handlerInternal

@Override
    protected ModelAndView handleInternal(HttpServletRequest request,
            HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
    //判断Handler是否有@SessionAttribute注释的参数
        if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
            // Always prevent caching in case of session attribute management.
            checkAndPrepare(request, response, this.cacheSecondsForSessionAttributeHandlers, true);
        }
        else {
            // Uses configured default cacheSeconds setting.
            checkAndPrepare(request, response, true);
        }

        // Execute invokeHandlerMethod in synchronized block if required.
        if (this.synchronizeOnSession) {
            HttpSession session = request.getSession(false);
            if (session != null) {
                Object mutex = WebUtils.getSessionMutex(session);
                synchronized (mutex) {
                    return invokeHandleMethod(request, response, handlerMethod);
                }
            }
        }

        return invokeHandleMethod(request, response, handlerMethod);
    }
    
/**
     * Check and prepare the given request and response according to the settings 
     * of this generator. Checks for supported methods and a required session,(根据给定的请求属性设置,检查方法和session)
     * and applies the given number of cache seconds.(请求一定时间的缓存时间)
     * @param request current HTTP request
     * @param response current HTTP response
     * @param cacheSeconds positive number of seconds into the future that the
     * response should be cacheable for, 0 to prevent caching
     * @param lastModified if the mapped handler provides Last-Modified support
     * @throws ServletException if the request cannot be handled because a check failed
     */
    protected final void checkAndPrepare(
            HttpServletRequest request, HttpServletResponse response, int cacheSeconds, boolean lastModified)
            throws ServletException {

        // Check whether we should support the request method.
        String method = request.getMethod();
        if (this.supportedMethods != null && !this.supportedMethods.contains(method)) {
            throw new HttpRequestMethodNotSupportedException(
                    method, StringUtils.toStringArray(this.supportedMethods));
        }

        // Check whether a session is required.
        if (this.requireSession) {
            if (request.getSession(false) == null) {
                throw new HttpSessionRequiredException("Pre-existing session required but none found");
            }
        }

        // Do declarative cache control.
        // Revalidate if the controller supports last-modified.
        applyCacheSeconds(response, cacheSeconds, lastModified);
    }
/**
     * Apply the given cache seconds and generate respective HTTP headers.
     * <p>That is, allow caching for the given number of seconds in the
     * case of a positive value, prevent caching if given a 0 value, else
     * do nothing (i.e. leave caching to the client).
     * @param response the current HTTP response
     * @param seconds the (positive) number of seconds into the future that
     * the response should be cacheable for; 0 to prevent caching; and
     * a negative value to leave caching to the client.
     * @param mustRevalidate whether the client should revalidate the resource
     * (typically only necessary for controllers with last-modified support)
     */
    protected final void applyCacheSeconds(HttpServletResponse response, int seconds, boolean mustRevalidate) {
        if (seconds > 0) {
        //给缓存设置过期时间
            cacheForSeconds(response, seconds, mustRevalidate);
        }
        else if (seconds == 0) {
        //阻止使用缓存
            preventCaching(response);
        }
        // Leave caching to the client otherwise.
    }
protected final void cacheForSeconds(HttpServletResponse response, int seconds, boolean mustRevalidate) {
        if (this.useExpiresHeader) {
            // HTTP 1.0 header
            response.setDateHeader(HEADER_EXPIRES, System.currentTimeMillis() + seconds * 1000L);
        }
        if (this.useCacheControlHeader) {
            // HTTP 1.1 header
            String headerValue = "max-age=" + seconds;
            if (mustRevalidate || this.alwaysMustRevalidate) {
                headerValue += ", must-revalidate";
            }
            response.setHeader(HEADER_CACHE_CONTROL, headerValue);
        }
    }
    
protected final void preventCaching(HttpServletResponse response) {
    response.setHeader(HEADER_PRAGMA, "no-cache");
    if (this.useExpiresHeader) {
        // HTTP 1.0 header
        response.setDateHeader(HEADER_EXPIRES, 1L);
    }
    if (this.useCacheControlHeader) {
        // HTTP 1.1 header: "no-cache" is the standard value,
        // "no-store" is necessary to prevent caching on FireFox.
        response.setHeader(HEADER_CACHE_CONTROL, "no-cache");
        if (this.useCacheControlNoStore) {
            response.addHeader(HEADER_CACHE_CONTROL, "no-store");
        }
    }
}

缓存的设置其实就是对Response的Header进行了相应的设置,上面的代码实际含义是,如果有@SessionAttribute注释则阻止使用缓存,否则就什么也不做

invokeHandlerMethod方法初始化了三个类型的变量分别是

  • WebDataBinderFactory
  • ModelFactory
  • ServletInvocableHandlerMethod
/**
     * Invoke the {@link RequestMapping} handler method preparing a {@link ModelAndView}
     * if view resolution is required.
     */
    private ModelAndView invokeHandleMethod(HttpServletRequest request,
            HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

        ServletWebRequest webRequest = new ServletWebRequest(request, response);
    
        WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
        ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
        ServletInvocableHandlerMethod requestMappingMethod = createRequestMappingMethod(handlerMethod, binderFactory);

        ModelAndViewContainer mavContainer = new ModelAndViewContainer();
        mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
        modelFactory.initModel(webRequest, mavContainer, requestMappingMethod);
        mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);

        AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
        asyncWebRequest.setTimeout(this.asyncRequestTimeout);

        final WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
        asyncManager.setTaskExecutor(this.taskExecutor);
        asyncManager.setAsyncWebRequest(asyncWebRequest);
        asyncManager.registerCallableInterceptors(this.callableInterceptors);
        asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);

        if (asyncManager.hasConcurrentResult()) {
            Object result = asyncManager.getConcurrentResult();
            mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];
            asyncManager.clearConcurrentResult();

            if (logger.isDebugEnabled()) {
                logger.debug("Found concurrent result value [" + result + "]");
            }
            requestMappingMethod = requestMappingMethod.wrapConcurrentResult(result);
        }

        requestMappingMethod.invokeAndHandle(webRequest, mavContainer);

        if (asyncManager.isConcurrentHandlingStarted()) {
            return null;
        }

        return getModelAndView(mavContainer, modelFactory, webRequest);
    }

WebDataBinderFactory主要是用来创建WebDataBinder,它的功能是实现参数和String之间的类型转换,WebDataBinderFactory就是将符合条件的的注释了@InitBinder的方法找出来,这里的InitBinder方法包括两部分:1、注释了@ControllerAdvice的并且符合要求的全局处理器里面的InitBinder方法。2、处理器自身的InitBinder方法

private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) throws Exception {
        Class<?> handlerType = handlerMethod.getBeanType();
        //检查当前Handler中InitBinder方法是否已经存在缓存中
        Set<Method> methods = this.initBinderCache.get(handlerType);
        if (methods == null) {
        //如果没有则查找并且设置到缓存中
            methods = HandlerMethodSelector.selectMethods(handlerType, INIT_BINDER_METHODS);
            this.initBinderCache.put(handlerType, methods);
        }
        //定义保存InitBinder方法的临时变量
        List<InvocableHandlerMethod> initBinderMethods = new ArrayList<InvocableHandlerMethod>();
        // Global methods first(首先添加全局变量到initBinderMethods)
        for (Entry<ControllerAdviceBean, Set<Method>> entry : this.initBinderAdviceCache .entrySet()) {
            if (entry.getKey().isApplicableToBeanType(handlerType)) {
                Object bean = entry.getKey().resolveBean();
                for (Method method : entry.getValue()) {
                    initBinderMethods.add(createInitBinderMethod(bean, method));
                }
            }
        }
        //将当前Handler中的InitBinder方法添加到initBinderMethos
        for (Method method : methods) {
            Object bean = handlerMethod.getBean();
            initBinderMethods.add(createInitBinderMethod(bean, method));
        }
        return createDataBinderFactory(initBinderMethods);
    }

ModelFactoryz是用来处理Model的,主要包含两个功能:1、在处理器具体处理之前对Model进行初始化2、在处理完请求后对Model参数进行更新。
Model初始化分为三个部分:1、将原来的SessionAttribute中的值设置到Model。2、执行相应注释了@ModelAttribute的方法并将其值设置到Mode。3、处理器中注释了@ModelAttribute的参数如果同时在SessionAttribute中也配置了,而且在mavContainer中还没有值则从全部SessionAttribute中查找出来并设置进去

private ModelFactory getModelFactory(HandlerMethod handlerMethod, WebDataBinderFactory binderFactory) {
    //获取SessionAttributeHandler
        SessionAttributesHandler sessionAttrHandler = getSessionAttributesHandler(handlerMethod);
    //获取处理器的类型
        Class<?> handlerType = handlerMethod.getBeanType();
    //获取处理器类中注释了@ModelAttribute而且没有注释@RequestMapping的类型第一次获取后添加到缓存,以后直接从缓存中获取
        Set<Method> methods = this.modelAttributeCache.get(handlerType);
    //
        if (methods == null) {
            methods = HandlerMethodSelector.selectMethods(handlerType, MODEL_ATTRIBUTE_METHODS);
            this.modelAttributeCache.put(handlerType, methods);
        }
        List<InvocableHandlerMethod> attrMethods = new ArrayList<InvocableHandlerMethod>();
        // Global methods first(首先添加全局的方法)
        for (Entry<ControllerAdviceBean, Set<Method>> entry : this.modelAttributeAdviceCache.entrySet()) {
            if (entry.getKey().isApplicableToBeanType(handlerType)) {
                Object bean = entry.getKey().resolveBean();
                for (Method method : entry.getValue()) {
                    attrMethods.add(createModelAttributeMethod(binderFactory, bean, method));
                }
            }
        }
        for (Method method : methods) {
            Object bean = handlerMethod.getBean();
            attrMethods.add(createModelAttributeMethod(binderFactory, bean, method));
        }
        return new ModelFactory(attrMethods, binderFactory, sessionAttrHandler);
    }

ServletInvocableHandlerMethod继承自HandlerMethod,并且可以直接执行。实际请求的处理就是通过它来执行的,参数绑定、处理请求以及返回值处理都在它里面完成

private ServletInvocableHandlerMethod createRequestMappingMethod(
            HandlerMethod handlerMethod, WebDataBinderFactory binderFactory) {

        ServletInvocableHandlerMethod requestMethod;
        requestMethod = new ServletInvocableHandlerMethod(handlerMethod);
        requestMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
        requestMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
        requestMethod.setDataBinderFactory(binderFactory);
        requestMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
        return requestMethod;
    }

前面invokeHandlerMethod方法中的三个变量弄清楚之后就比较容易理解了,上面三个变量完了之后下面还有三个变量需要处理

  • 1、新建传递参数的ModelAndViewContainer容器,并将相应的参数设置到其Model中
  • 2、执行请求
  • 3、请求处理完后进行一些后置处理

新建ModelAndViewContainer类型的mavContainer参数,用于保存Model和View,它贯穿于整个处理过程,然后对mavContainer进行了设置,主要包括三部分内容:1、将FlashMap中的数据设置到Model;2、使用modelFactory将SessionAttribute和注释了@ModelAttribute的方法的参数设置到Model;3、根据配置对ignoreDefaultModelOnRedirect进行了参数设置,这个参数在分析ModelAndViewContainer的时候再详细讲解

    ModelAndViewContainer mavContainer = new ModelAndViewContainer();
    mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
    modelFactory.initModel(webRequest, mavContainer, requestMappingMethod);
    mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);

执行请求,具体方法是直接调用ServletInvocableHandlerMethod里的invokeAndHandle方法执行

requestMappingMethod.invokeAndHandle(webRequest, mavContainer);

处理完请求后的后置处理,这是在getModelAndView方法中处理的。一共做了三件事:1、调用ModelFactory的updateModel方法更新了Model。2、根据mavContainer创建了ModelAndView。3、如果mavContainer里的model是RedirectAttribute类型,则将其设置到FlashMap

private ModelAndView getModelAndView(ModelAndViewContainer mavContainer,
            ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception {

        modelFactory.updateModel(webRequest, mavContainer);
        if (mavContainer.isRequestHandled()) {
            return null;
        }
        ModelMap model = mavContainer.getModel();
        ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model);
        if (!mavContainer.isViewReference()) {
            mav.setView((View) mavContainer.getView());
        }
        if (model instanceof RedirectAttributes) {
            Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes();
            HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
            RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
        }
        return mav;
    }

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

推荐阅读更多精彩内容