HttpMessageConverter是這樣轉(zhuǎn)換數(shù)據(jù)的

Java Web 人員經(jīng)常要設(shè)計(jì) RESTful API(如何設(shè)計(jì)好的RESTful API),通過(guò) json 數(shù)據(jù)進(jìn)行交互。那么前端傳入的 json 數(shù)據(jù)如何被解析成 Java 對(duì)象作為 API入?yún)ⅲ珹PI 返回結(jié)果又如何將 Java 對(duì)象解析成 json 格式數(shù)據(jù)返回給前端,其實(shí)在整個(gè)數(shù)據(jù)流轉(zhuǎn)過(guò)程中,HttpMessageConverter 起到了重要作用恤煞;另外在轉(zhuǎn)換的過(guò)程我們可以加入哪些定制化內(nèi)容?

HttpMessageConverter 介紹

org.springframework.http.converter.HttpMessageConverter 是一個(gè)策略接口施籍,接口說(shuō)明如下:

Strategy interface that specifies a converter that can convert from and to HTTP requests and responses. 簡(jiǎn)單說(shuō)就是 HTTP request (請(qǐng)求)和response (響應(yīng))的轉(zhuǎn)換器居扒。該接口有只有5個(gè)方法,簡(jiǎn)單來(lái)說(shuō)就是獲取支持的 MediaType(application/json之類)丑慎,接收到請(qǐng)求時(shí)判斷是否能讀(canRead)喜喂,能讀則讀(read);返回結(jié)果時(shí)判斷是否能寫(canWrite)竿裂,能寫則寫(write)玉吁。這幾個(gè)方法先有個(gè)印象即可:

boolean canRead(Class<?> clazz, MediaType mediaType);
boolean canWrite(Class<?> clazz, MediaType mediaType);
List<MediaType> getSupportedMediaTypes();
T read(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException;
void write(T t, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException;

缺省配置

我們寫 Demo 沒(méi)有配置任何 MessageConverter,但是數(shù)據(jù)前后傳遞依舊好用铛绰,是因?yàn)?SpringMVC 啟動(dòng)時(shí)會(huì)自動(dòng)配置一些HttpMessageConverter诈茧,在 WebMvcConfigurationSupport 類中添加了缺省 MessageConverter:

protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
        StringHttpMessageConverter stringConverter = new StringHttpMessageConverter();
        stringConverter.setWriteAcceptCharset(false);

        messageConverters.add(new ByteArrayHttpMessageConverter());
        messageConverters.add(stringConverter);
        messageConverters.add(new ResourceHttpMessageConverter());
        messageConverters.add(new SourceHttpMessageConverter<Source>());
        messageConverters.add(new AllEncompassingFormHttpMessageConverter());

        if (romePresent) {
            messageConverters.add(new AtomFeedHttpMessageConverter());
            messageConverters.add(new RssChannelHttpMessageConverter());
        }

        if (jackson2XmlPresent) {
            ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.xml().applicationContext(this.applicationContext).build();
            messageConverters.add(new MappingJackson2XmlHttpMessageConverter(objectMapper));
        }
        else if (jaxb2Present) {
            messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
        }

        if (jackson2Present) {
            ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().applicationContext(this.applicationContext).build();
            messageConverters.add(new MappingJackson2HttpMessageConverter(objectMapper));
        }
        else if (gsonPresent) {
            messageConverters.add(new GsonHttpMessageConverter());
        }
    }

我們看到很熟悉的 MappingJackson2HttpMessageConverter 产喉,如果我們引入 jackson 相關(guān)包捂掰,Spring 就會(huì)為我們添加該 MessageConverter,但是我們通常在搭建框架的時(shí)候還是會(huì)手動(dòng)添加配置 MappingJackson2HttpMessageConverter曾沈,為什么这嚣? 先思考一下:

當(dāng)我們配置了自己的 MessageConverter, SpringMVC 啟動(dòng)過(guò)程就不會(huì)調(diào)用 addDefaultHttpMessageConverters 方法塞俱,且看下面代碼 if 條件姐帚,這樣做也是為了定制化我們自己的 MessageConverter

protected final List<HttpMessageConverter<?>> getMessageConverters() {
        if (this.messageConverters == null) {
            this.messageConverters = new ArrayList<HttpMessageConverter<?>>();
            configureMessageConverters(this.messageConverters);
            if (this.messageConverters.isEmpty()) {
                addDefaultHttpMessageConverters(this.messageConverters);
            }
            extendMessageConverters(this.messageConverters);
        }
        return this.messageConverters;
    }

類關(guān)系圖

在此處僅列出 MappingJackson2HttpMessageConverterStringHttpMessageConverter 兩個(gè)轉(zhuǎn)換器,我們發(fā)現(xiàn)障涯, 前者實(shí)現(xiàn)了 GenericHttpMessageConverter 接口, 而后者卻沒(méi)有罐旗,留有這個(gè)關(guān)鍵印象膳汪,這是數(shù)據(jù)流轉(zhuǎn)過(guò)程中關(guān)鍵邏輯判斷

Xnip2019-05-26_11-46-37.jpg

數(shù)據(jù)流轉(zhuǎn)解析

數(shù)據(jù)的請(qǐng)求和響應(yīng)都要經(jīng)過(guò) DispatcherServlet 類的 doDispatch(HttpServletRequest request, HttpServletResponse response) 方法的處理

請(qǐng)求過(guò)程解析

看 doDispatch 方法中的關(guān)鍵代碼:

// 這里的 Adapter 實(shí)際上是 RequestMappingHandlerAdapter
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler()); 
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
    return;
}
// 實(shí)際處理的handler
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());            mappedHandler.applyPostHandle(processedRequest, response, mv);

從進(jìn)入handle之后我先將調(diào)用棧粘貼在此處,希望小伙伴可以按照調(diào)用棧路線動(dòng)手跟蹤嘗試:

readWithMessageConverters:192, AbstractMessageConverterMethodArgumentResolver (org.springframework.web.servlet.mvc.method.annotation)
readWithMessageConverters:150, RequestResponseBodyMethodProcessor (org.springframework.web.servlet.mvc.method.annotation)
resolveArgument:128, RequestResponseBodyMethodProcessor (org.springframework.web.servlet.mvc.method.annotation)
resolveArgument:121, HandlerMethodArgumentResolverComposite (org.springframework.web.method.support)
getMethodArgumentValues:158, InvocableHandlerMethod (org.springframework.web.method.support)
invokeForRequest:128, InvocableHandlerMethod (org.springframework.web.method.support)
 // 下面的調(diào)用棧重點(diǎn)關(guān)注九秀,處理請(qǐng)求和返回值的分叉口就在這里
invokeAndHandle:97, ServletInvocableHandlerMethod (org.springframework.web.servlet.mvc.method.annotation)
invokeHandlerMethod:849, RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation)
handleInternal:760, RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation)
handle:85, AbstractHandlerMethodAdapter (org.springframework.web.servlet.mvc.method)
doDispatch:967, DispatcherServlet (org.springframework.web.servlet)

這里重點(diǎn)說(shuō)明調(diào)用棧最頂層 readWithMessageConverters 方法中內(nèi)容:

// 遍歷 messageConverters
for (HttpMessageConverter<?> converter : this.messageConverters) {
    Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
        // 上文類關(guān)系圖處要重點(diǎn)記住的地方遗嗽,主要判斷 MappingJackson2HttpMessageConverter 是否是 GenericHttpMessageConverter 類型
    if (converter instanceof GenericHttpMessageConverter) {
        GenericHttpMessageConverter<?> genericConverter = (GenericHttpMessageConverter<?>) converter;
        if (genericConverter.canRead(targetType, contextClass, contentType)) {
            if (logger.isDebugEnabled()) {
                logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]");
            }
            if (inputMessage.getBody() != null) {
                inputMessage = getAdvice().beforeBodyRead(inputMessage, parameter, targetType, converterType);
                body = genericConverter.read(targetType, contextClass, inputMessage);
                body = getAdvice().afterBodyRead(body, inputMessage, parameter, targetType, converterType);
            }
            else {
                body = getAdvice().handleEmptyBody(null, inputMessage, parameter, targetType, converterType);
            }
            break;
        }
    }
    else if (targetClass != null) {
        if (converter.canRead(targetClass, contentType)) {
            if (logger.isDebugEnabled()) {
                logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]");
            }
            if (inputMessage.getBody() != null) {
                inputMessage = getAdvice().beforeBodyRead(inputMessage, parameter, targetType, converterType);
                body = ((HttpMessageConverter<T>) converter).read(targetClass, inputMessage);
                body = getAdvice().afterBodyRead(body, inputMessage, parameter, targetType, converterType);
            }
            else {
                body = getAdvice().handleEmptyBody(null, inputMessage, parameter, targetType, converterType);
            }
            break;
        }
    }
}

然后就判斷是否canRead,能讀就read鼓蜒,最終走到下面代碼處將輸入的內(nèi)容反序列化出來(lái):

protected Object _readMapAndClose(JsonParser p0, JavaType valueType)
        throws IOException
    {
        try (JsonParser p = p0) {
            Object result;
            JsonToken t = _initForReading(p);
            if (t == JsonToken.VALUE_NULL) {
                // Ask JsonDeserializer what 'null value' to use:
                DeserializationContext ctxt = createDeserializationContext(p,
                        getDeserializationConfig());
                result = _findRootDeserializer(ctxt, valueType).getNullValue(ctxt);
            } else if (t == JsonToken.END_ARRAY || t == JsonToken.END_OBJECT) {
                result = null;
            } else {
                DeserializationConfig cfg = getDeserializationConfig();
                DeserializationContext ctxt = createDeserializationContext(p, cfg);
                JsonDeserializer<Object> deser = _findRootDeserializer(ctxt, valueType);
                if (cfg.useRootWrapping()) {
                    result = _unwrapAndDeserialize(p, ctxt, cfg, valueType, deser);
                } else {
                    result = deser.deserialize(p, ctxt);
                }
                ctxt.checkUnresolvedObjectId();
            }
            // Need to consume the token too
            p.clearCurrentToken();
            return result;
        }
    }

到這里從請(qǐng)求中解析參數(shù)過(guò)程就到此結(jié)束了痹换,趁熱打鐵來(lái)看將響應(yīng)結(jié)果返回給前端的過(guò)程

返回過(guò)程解析

在上面調(diào)用棧請(qǐng)求和返回結(jié)果分叉口處同樣處理返回的內(nèi)容:

writeWithMessageConverters:224, AbstractMessageConverterMethodProcessor (org.springframework.web.servlet.mvc.method.annotation)
handleReturnValue:174, RequestResponseBodyMethodProcessor (org.springframework.web.servlet.mvc.method.annotation)
handleReturnValue:81, HandlerMethodReturnValueHandlerComposite (org.springframework.web.method.support)
// 分叉口
invokeAndHandle:113, ServletInvocableHandlerMethod (org.springframework.web.servlet.mvc.method.annotation)

重點(diǎn)關(guān)注調(diào)用棧頂層內(nèi)容,是不是很熟悉的樣子都弹,完全一樣的邏輯, 判斷是否能寫canWrite娇豫,能寫則write:

for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
    if (messageConverter instanceof GenericHttpMessageConverter) {
        if (((GenericHttpMessageConverter) messageConverter).canWrite(
                declaredType, valueType, selectedMediaType)) {
            outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType,
                    (Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(),
                    inputMessage, outputMessage);
            if (outputValue != null) {
                addContentDispositionHeader(inputMessage, outputMessage);
                ((GenericHttpMessageConverter) messageConverter).write(
                        outputValue, declaredType, selectedMediaType, outputMessage);
                if (logger.isDebugEnabled()) {
                    logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType +
                            "\" using [" + messageConverter + "]");
                }
            }
            return;
        }
    }
    else if (messageConverter.canWrite(valueType, selectedMediaType)) {
        outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType,
                (Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(),
                inputMessage, outputMessage);
        if (outputValue != null) {
            addContentDispositionHeader(inputMessage, outputMessage);
            ((HttpMessageConverter) messageConverter).write(outputValue, selectedMediaType, outputMessage);
            if (logger.isDebugEnabled()) {
                logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType +
                        "\" using [" + messageConverter + "]");
            }
        }
        return;
    }
}

我們看到有這樣一行代碼:

outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType,
                    (Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(),
                    inputMessage, outputMessage);

我們?cè)谠O(shè)計(jì) RESTful API 接口的時(shí)候通常會(huì)將返回的數(shù)據(jù)封裝成統(tǒng)一格式,通常我們會(huì)實(shí)現(xiàn) ResponseBodyAdvice<T> 接口來(lái)處理所有 API 的返回值畅厢,在真正 write 之前將數(shù)據(jù)進(jìn)行統(tǒng)一的封裝

@RestControllerAdvice()
public class CommonResultResponseAdvice implements ResponseBodyAdvice<Object> {

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
            Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
            ServerHttpResponse response) {
        if (body instanceof CommonResult) {
            return body;
        }
        return new CommonResult<Object>(body);
    }

}

整個(gè)處理流程就是這樣冯痢,整個(gè)實(shí)現(xiàn)過(guò)程細(xì)節(jié)還需小伙伴自行追蹤發(fā)現(xiàn),文章開(kāi)頭我們說(shuō)過(guò) 添加自己的 MessageConverter 能更好的滿足我們的定制化或详,都有哪些可以定制的呢系羞?

定制化

空值處理

請(qǐng)求和返回的數(shù)據(jù)有很多空值,這些值有時(shí)候并沒(méi)有實(shí)際意義霸琴,我們可以過(guò)濾掉和不返回椒振,或設(shè)置成默認(rèn)值烧给。比如通過(guò)重寫 getObjectMapper 方法糠溜,將返回結(jié)果的空值不進(jìn)行序列化:

converters.add(0, new MappingJackson2HttpMessageConverter(){
    @Override
    public ObjectMapper getObjectMapper() {
        super.getObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL);
                return super.getObjectMapper();
    }
}

XSS 腳本攻擊

為了保證輸入的數(shù)據(jù)更安全艾船,防止 XSS 腳本攻擊竭钝,我們可以添加自定義反序列化器:

//對(duì)應(yīng)無(wú)法直接返回String類型
converters.add(0, new MappingJackson2HttpMessageConverter(){
    @Override
    public ObjectMapper getObjectMapper() {
        super.getObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL);

                // XSS 腳本過(guò)濾
        SimpleModule simpleModule = new SimpleModule();
        simpleModule.addDeserializer(String.class, new StringXssDeserializer());
        super.getObjectMapper().registerModule(simpleModule);

        return super.getObjectMapper();
    }
}

細(xì)節(jié)分析

canRead 和 canWrite 的判斷邏輯是什么呢掺冠? 請(qǐng)看下圖:

Xnip2019-05-24_20-34-03.jpg

客戶端 Request Header 中設(shè)置好 Content-Type(傳入的數(shù)據(jù)格式)和Accept(接收的數(shù)據(jù)格式)骤公,根據(jù)配置好的MessageConverter來(lái)判斷是否 canRead 或 canWrite戚炫,然后決定 response.body 的 Content-Type 的第一要素是對(duì)應(yīng)的request.headers.Accept 屬性的值哀军,又叫做 MediaType仁堪。如果服務(wù)端支持這個(gè) Accept哮洽,那么應(yīng)該按照這個(gè) Accept 來(lái)確定返回response.body 對(duì)應(yīng)的格式,同時(shí)把 response.headers.Content-Type 設(shè)置成自己支持的符合那個(gè) Accept 的 MediaType

總結(jié)與思考

站在上帝視角看弦聂,整個(gè)流程可以按照下圖進(jìn)行概括鸟辅,請(qǐng)求報(bào)文先轉(zhuǎn)換成 HttpInputMessage, 然后再通過(guò) HttpMessageConverter 將其轉(zhuǎn)換成 SpringMVC 的 java 對(duì)象,反之亦然莺葫。


Xnip2019-05-26_10-24-00.jpg

將各種常用 HttpMessageConverter 支持的MediaType 和 JavaType 以及對(duì)應(yīng)關(guān)系總結(jié)在此處:

類名 支持的JavaType 支持的MediaType
ByteArrayHttpMessageConverter byte[] application/octet-stream, */*
StringHttpMessageConverter String text/plain, */*
MappingJackson2HttpMessageConverter Object application/json, application/*+json
AllEncompassingFormHttpMessageConverter Map<K, List<?>> application/x-www-form-urlencoded, multipart/form-data
SourceHttpMessageConverter Source application/xml, text/xml, application/*+xml

最后思考這樣一個(gè)問(wèn)題:為什么 HttpMessageConverter 在寫的過(guò)程中匪凉,先判斷 canWrite 后判斷是否有 responseBodyAdvice 的數(shù)據(jù)封裝呢? 如果先進(jìn)行 responseBodyAdvice 的數(shù)據(jù)封裝后判斷 canWrite 會(huì)怎樣呢捺檬?

提高效率工具

依舊介紹寫該文章用到的一些好的工具

processon

ProcessOn是一個(gè)在線作圖工具的聚合平臺(tái)再层,它可以在線畫流程圖、思維導(dǎo)圖、UI原型圖聂受、UML蒿秦、網(wǎng)絡(luò)拓?fù)鋱D、組織結(jié)構(gòu)圖等等蛋济,
您無(wú)需擔(dān)心下載和更新的問(wèn)題渤早,不管Mac還是Windows,一個(gè)瀏覽器就可以隨時(shí)隨地的發(fā)揮創(chuàng)意瘫俊,規(guī)劃工作鹊杖,同時(shí)您可以把作品分享給團(tuán)隊(duì)成員或好友,無(wú)論何時(shí)何地大家都可以對(duì)作品進(jìn)行編輯扛芽、閱讀和評(píng)論


image.png

SequenceDiagram

SequenceDiagram 是 IntelliJ IDEA 的一個(gè)插件骂蓖,有了這個(gè)插件,你可以

  1. 生成簡(jiǎn)單序列圖川尖。
  2. 單擊圖形形狀來(lái)導(dǎo)航代碼登下。
  3. 從圖中刪除類。
  4. 將圖表導(dǎo)出為圖像叮喳。
  5. 通過(guò)“設(shè)置”>“其他設(shè)置”>“序列”從圖表中排除類
    方便快速的定位方法和理解類的調(diào)用過(guò)程


    image.png

歡迎關(guān)注公眾號(hào)被芳,趣談coding那些事,你有一個(gè)思想馍悟,我有一個(gè)思想畔濒,我們交換后就都有兩個(gè)思想

image
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市锣咒,隨后出現(xiàn)的幾起案子侵状,更是在濱河造成了極大的恐慌,老刑警劉巖毅整,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件趣兄,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡悼嫉,警方通過(guò)查閱死者的電腦和手機(jī)艇潭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)戏蔑,“玉大人蹋凝,你說(shuō)我怎么就攤上這事⌒岭” “怎么了仙粱?”我有些...
    開(kāi)封第一講書人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵房交,是天一觀的道長(zhǎng)彻舰。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么刃唤? 我笑而不...
    開(kāi)封第一講書人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任隔心,我火速辦了婚禮,結(jié)果婚禮上尚胞,老公的妹妹穿的比我還像新娘硬霍。我一直安慰自己,他們只是感情好笼裳,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布唯卖。 她就那樣靜靜地躺著,像睡著了一般躬柬。 火紅的嫁衣襯著肌膚如雪拜轨。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 49,036評(píng)論 1 285
  • 那天允青,我揣著相機(jī)與錄音橄碾,去河邊找鬼。 笑死颠锉,一個(gè)胖子當(dāng)著我的面吹牛法牲,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播琼掠,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼拒垃,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了瓷蛙?” 一聲冷哼從身側(cè)響起恶复,我...
    開(kāi)封第一講書人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎速挑,沒(méi)想到半個(gè)月后谤牡,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡姥宝,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年翅萤,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片腊满。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡套么,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出碳蛋,到底是詐尸還是另有隱情胚泌,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布肃弟,位于F島的核電站玷室,受9級(jí)特大地震影響零蓉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜穷缤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一敌蜂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧津肛,春花似錦章喉、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至部蛇,卻和暖如春撞反,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背搪花。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工遏片, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人撮竿。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓吮便,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親幢踏。 傳聞我的和親對(duì)象是個(gè)殘疾皇子髓需,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容