3K整合系列(二) Ktor + Ktorm

3K = Kotlin + Ktor + Ktorm,不要記錯(cuò)了哦

在上一篇里鞍匾,我們成功完成了對(duì) Ktorm 框架的引入交洗,并且也留了一個(gè)懸念,即 Ktorm 的實(shí)例可以用 jackson 來(lái)進(jìn)行序列化橡淑。其實(shí)說(shuō)到序列化构拳,我們最常用的場(chǎng)景也就是對(duì)于服務(wù)接口的輸入輸出了,這將引出我們今天想要講的東西梁棠,即 Ktor置森。

Ktor 是由 JetBrains 官方推出的一款服務(wù)端的框架,我們可以輕松的用它來(lái)實(shí)現(xiàn)一個(gè)服務(wù)端的應(yīng)用符糊,一個(gè)最簡(jiǎn)單的 Demo 如下:

fun main() {
    embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
        routing {
            get("/") {
                call.respondText("Hello World!")
            }
        }
    }.start(wait = true)
}

運(yùn)行它并且在 Terminal 輸入 curl 請(qǐng)求凫海,就可以看到這個(gè)接口已經(jīng)正常工作了。

$ curl http://0.0.0.0:8080/
Hello World!

當(dāng)然了男娄,我們不可能如此簡(jiǎn)單的去應(yīng)用一個(gè)框架行贪,但是這段代碼已經(jīng)向我們展示了 Ktor 的一個(gè)重要特性漾稀,就是它的簡(jiǎn)單。

作為 Ktor 本身來(lái)說(shuō)建瘫,它給我們提供了相當(dāng)多的特性崭捍,許多已經(jīng)制作好的插件,來(lái)方便我們做到開箱即用啰脚,這個(gè)所謂的簡(jiǎn)單殷蛇,比 Springboot 來(lái)得更為簡(jiǎn)單。雖然 Springboot 已是開箱即用橄浓,但是頻繁使用注解粒梦,反射的效率不佳,項(xiàng)目啟動(dòng)慢贮配,占用內(nèi)存高谍倦,也始終為我們所詬病。而 Ktor 由于使用 Kotlin 本身的異步函數(shù)式特性作為語(yǔ)言基礎(chǔ)泪勒,就如上面的 Demo 代碼中所寫的那樣昼蛀,一切皆是函數(shù),這就對(duì)“注解”很不友好了圆存。當(dāng)然了叼旋,Ktor 本身也不推薦使用注解,而是推薦使用插件沦辙,這是一種比注解更友好的 AOP 解決方案(當(dāng)然插件的編寫有一定的難度夫植,但是收獲大于付出),同時(shí) Ktor 的內(nèi)存占用比較小油讯,啟動(dòng)也很快详民,我當(dāng)前正在改造的一個(gè)項(xiàng)目,原本是 Springboot 所寫陌兑,啟動(dòng)需要 20 秒沈跨,啟動(dòng)后占用內(nèi)存 340M,而改成 Ktor 后兔综,啟動(dòng)僅需 2 秒饿凛,占用內(nèi)存 97M,只能說(shuō)這是一個(gè)巨大的進(jìn)步了软驰。更何況 Kotlin 還有一個(gè)眾所周知的特點(diǎn)涧窒,那就是寫起來(lái)也快。


下面是正題锭亏,為了順利的使用 Ktor 框架纠吴,來(lái)實(shí)現(xiàn)一些常規(guī)的服務(wù)端應(yīng)用所需要的內(nèi)容,我們還是有必要對(duì)它的插件作出一定的了解慧瘤,以下的官方插件都非常的有用(且易用)呜象。

一膳凝、跨域請(qǐng)求

對(duì)于現(xiàn)代的前后端分離的架構(gòu),支持跨域請(qǐng)求是非常有必要的恭陡,Ktor 里通過(guò) CORS 插件來(lái)實(shí)現(xiàn)這一能力:

install(CORS) {
    allowMethod(HttpMethod.Get)
    allowMethod(HttpMethod.Post)
    allowMethod(HttpMethod.Put)
    allowMethod(HttpMethod.Patch)
    allowMethod(HttpMethod.Delete)
    allowMethod(HttpMethod.Head)
    allowMethod(HttpMethod.Options)
    allowHeader(HttpHeaders.Authorization)
    anyHost()
    allowCredentials = true
    allowNonSimpleContentTypes = true
    maxAgeInSeconds = 1000L * 60 * 60 * 24
}

二、用戶會(huì)話

這是用來(lái)實(shí)現(xiàn)一個(gè)有狀態(tài)的服務(wù)所必備的條件上煤,Sessions 插件將會(huì)把用戶的會(huì)話注入在框架里休玩,使得每一個(gè)請(qǐng)求都可以拿到會(huì)話信息。

data class SessionUser(val userId: Long = -1, val userName: String = "") : Principal

install(Sessions) {
     cookie<SessionUser>("Session") {
        cookie.extensions["SameSite"] = "lax"
        cookie.httpOnly = true
        cookie.path = "/"
        transform(SessionTransportTransformerEncrypt(hex(secretEncryptKey), hex(secretSignKey)))
    }
}

在這里可以發(fā)現(xiàn) Ktor 對(duì)于開發(fā)者友好的地方劫狠,由于明文保存 Cookie 會(huì)有安全上的風(fēng)險(xiǎn)拴疤,因此 Session 插件允許你對(duì) Cookie 進(jìn)行加密,你只需要提供 32 位長(zhǎng)度的字符串作為加密密鑰独泞,再提供一個(gè)大于 8 位長(zhǎng)度的字符串作為簽名密鑰就可以了呐矾,Ktor 采用的是簽名驗(yàn)證的方式來(lái)驗(yàn)證 Cookie。

當(dāng)你使用了 Sessions 插件后懦砂,就可以在接到請(qǐng)求時(shí)獲取用戶的會(huì)話信息了:

get("/sample") {
    val user = call.sessions.get<SessionUser>()  // 獲取用戶會(huì)話
    ... ...
    call.sessions.set(user)  // 設(shè)置用戶會(huì)話
}

當(dāng)然了蜒犯,這里還有一些小技巧,如果你不喜歡每次都用這么復(fù)雜的代碼去獲取或設(shè)置用戶會(huì)話荞膘,你可以增加一個(gè)擴(kuò)展:

inline var PipelineContext<*, ApplicationCall>.user: SessionUser?
    get() = context.sessions.get()
    set(value) = context.sessions.set(value)

這樣你就可以在接到請(qǐng)求時(shí)使用這樣的代碼來(lái)獲得/設(shè)置用戶會(huì)話了:

get("/sample") {
    val u = user  // 獲取用戶會(huì)話
    ... ...
    user = u  // 設(shè)置用戶會(huì)話
}

在這里還有一個(gè)用戶會(huì)話對(duì)象序列化的問(wèn)題,下面將會(huì)講到。

三椰于、身份認(rèn)證

最常見的場(chǎng)景就是很多情況下梭姓,接口需要用戶登錄后才能請(qǐng)求,這里的登錄就是一種身份認(rèn)證的方式屠升,Ktor 提供了 Authentication 插件來(lái)實(shí)現(xiàn)身份認(rèn)證

install(Authentication) {
    session<SessionUser>("auth.session") {
        validate {
            if (it.userId == -1L) null else it
        }
        challenge {
            call.respond(AjaxResult.error("必須先登錄才能訪問(wèn)此接口"))
        }
    }
}

四潮改、序列化

序列化作為輸入輸出中最重要的方式,自然也被 Ktor 所支持腹暖,Ktor 目前支持三種序列化框架汇在,分別是 kotlinx.serialization,gson 和 jackson微服。除去官方框架稍有些難用外(需要大量注解以及使用官方的序列化編譯插件)趾疚,另兩個(gè)框架都是行業(yè)里面大量使用并且已得到大量驗(yàn)證的。上面說(shuō)過(guò)以蕴,Ktorm 是支持使用 jackson 來(lái)進(jìn)行序列化的糙麦,因此在 3K 整合的場(chǎng)景下,jackson 就是唯一的選擇丛肮,下面來(lái)看一下如何配置這樣的序列化:

install(ContentNegotiation) {
    jackson { }
}

最簡(jiǎn)單的配置赡磅,只需要一句話就可以完成了,但是通常情況下宝与,只有默認(rèn)配置還是不夠的焚廊,當(dāng)我們需要對(duì)一些自定義的類型進(jìn)行序列化時(shí)冶匹,jackson 默認(rèn)的配置就不夠用了,我們有必要自己寫一些配置:

fun ObjectMapper.config(localDatePattern: String, localTimePattern: String, localDateTimePattern: String): ObjectMapper {
    registerModule(KtormModule())
    registerModule(JavaTimeModule().apply {
        addDeserializer(LocalDate::class.java, LocalDateDeserializer(DateTimeFormatter.ofPattern(localDatePattern)))
        addSerializer(LocalDate::class.java, LocalDateSerializer(DateTimeFormatter.ofPattern(localDatePattern)))
        addDeserializer(LocalTime::class.java, LocalTimeDeserializer(DateTimeFormatter.ofPattern(localTimePattern)))
        addSerializer(LocalTime::class.java, LocalTimeSerializer(DateTimeFormatter.ofPattern(localTimePattern)))
        addDeserializer(LocalDateTime::class.java, LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(localDateTimePattern)))
        addSerializer(LocalDateTime::class.java, LocalDateTimeSerializer(DateTimeFormatter.ofPattern(localDateTimePattern)))
    })
    configure(SerializationFeature.INDENT_OUTPUT, true)
    setDefaultLeniency(true)
    setDefaultPrettyPrinter(DefaultPrettyPrinter().apply {
        indentArraysWith(DefaultPrettyPrinter.FixedSpaceIndenter.instance)
        indentObjectsWith(DefaultIndenter("  ", "\n"))
    })
    return this
}

install(ContentNegotiation) {
    jackson {
        config("yyyy-MM-dd", "hh:mm:ss", "yyyy-MM-dd hh:mm:ss")
    }
}

經(jīng)過(guò)了這樣的配置后咆瘟,就可以用 jackson 來(lái)完成更多類型的序列化了嚼隘。需要注意的是,在 ContentNegotiation 插件內(nèi)配置的序列化工具袒餐,僅對(duì)“請(qǐng)求”的輸入?yún)?shù)飞蛹,以及輸出參數(shù)有效,但是對(duì)用戶會(huì)話的序列化無(wú)效(不知 JetBrains 那群人怎么想的灸眼,這也能區(qū)別對(duì)待卧檐?),所以在需要對(duì)用戶會(huì)話對(duì)象進(jìn)行序列化的場(chǎng)景下焰宣,我們需要對(duì) Sessions 插件里的序列化器進(jìn)行設(shè)置霉囚。

install(Sessions) {
    cookie<SessionUser>(sessionIdentifier) {
        cookie.extensions["SameSite"] = "lax"
        cookie.httpOnly = true
        cookie.path = "/"
        serializer = generateSerializer(localDatePattern, localTimePattern, localDateTimePattern)
        transform(SessionTransportTransformerEncrypt(hex(secretEncryptKey), hex(secretSignKey)))
    }
}

好了,那么這里的 generateSerializer 應(yīng)該怎么寫呢匕积,我們需要的是一些 Kotlin 的小技巧:

inline fun <reified T : Any> generateSerializer(
        localDatePattern: String, localTimePattern: String, localDateTimePattern: String
): SessionSerializer<T> = object : SessionSerializer<T> {
    private val om = ObjectMapper().config(localDatePattern, localTimePattern, localDateTimePattern)
    override fun deserialize(text: String): T = om.readValue(text, T::class.java)
    override fun serialize(session: T): String = om.writeValueAsString(session)
}

由于我們?cè)谏舷挛睦镆呀?jīng)帶上了泛型類型為 SessionUser盈罐,因此這里是可以自動(dòng)推定的,無(wú)須再寫一次 T闸天。另外暖呕,在函數(shù)中使得泛型保有其實(shí)際類型的做法也是相當(dāng)有用,它可以使得一個(gè)泛型類型 T 可以被實(shí)例化苞氮,可以在傳遞中不發(fā)生類型丟失湾揽。

還有一些插件,也是我們很常用的笼吟,出于篇幅關(guān)系不詳細(xì)寫了库物,大家可以看個(gè)表:

插件 作用
HttpsRedirect 允許請(qǐng)求為https時(shí)進(jìn)行重定向
Compression 要求請(qǐng)求/響應(yīng)內(nèi)容進(jìn)行 gzip 壓縮
DefaultHeaders 為響應(yīng)增加默認(rèn)頭部
PartialContent 對(duì)響應(yīng)內(nèi)容進(jìn)行分片(特別是針對(duì)Safari瀏覽器播放視頻流時(shí),必須使用)
Resources 允許從服務(wù)端本地加載靜態(tài)頁(yè)面資源
WebSockets 允許使用 WebSocket
CallLogging 允許對(duì)請(qǐng)求進(jìn)行鏈路跟蹤

下面來(lái)整體做一個(gè)基本整合吧贷帮,把 Ktor 和 Ktorm 整合到一起去使用戚揭。同樣出于篇幅,這里只完成一個(gè)簡(jiǎn)單的用戶登錄撵枢,登出和獲取用戶信息的功能民晒。

首先用官方工程向?qū)Ы?Ktor 工程,刪掉除了 Main.kt 和配置文件以外的文件锄禽,那些東西我們都不需要潜必。然后編寫 Main.kt 的代碼:

fun main(args: Array<String>) {
    io.ktor.server.netty.EngineMain.main(args)
}

fun Application.module() {

    val dbCfg = loadDatabaseConfig()
    WHDatabase.initDatabase(dbCfg)
    
    pluginCORS()
    pluginSession<SessionUser>(
        isSecret = true,
        secretEncryptKey = "00112233445566778899aabbccddeeff",
        secretSignKey = "12345678"
    )

    pluginContentNegotiation()
    pluginAuthSession<SessionUser>(AUTH_SESSION) {
        validate { it }
        challenge {
            call.respond(AjaxResult.error("必須先登錄才能訪問(wèn)此接口"))
        }
    }

    routing {   // 注冊(cè)路由
        user()
    }
}

好的,目前這代碼里肯定會(huì)有一些劃紅線的地方的沃但,我們一點(diǎn)一點(diǎn)來(lái)填磁滚。

先把用戶會(huì)話類型給填了,這個(gè)類型相當(dāng)?shù)闹匾卿涍^(guò)程也會(huì)用到它:

data class SessionUser(val userId: Long = -1, val userName: String = ""): Principal

當(dāng)然了垂攘,只有這個(gè)類维雇,是無(wú)法完成用戶登錄的,真實(shí)的登錄需要數(shù)據(jù)庫(kù)的支持晒他,這里就要拿出上一篇中講過(guò)的 Ktorm 了:

interface SysUser : Entity<SysUser> {
    companion object : Entity.Factory<SysUser>()
    var userId: Long
    var userName: String
    @get:JsonIgnore
    val password: String
}

object SysUsers : Table<SysUser>("sys_user") {
    var userId = long("user_id").primaryKey().bindTo { it.userId }
    var userName = varchar("user_name").bindTo { it.userName }
    var password = varchar("password").bindTo { it.password }
}

好了吱型,我們可以看到 SessionUser 和 SysUser,是兩個(gè)不同的類型仪芒,要將 SysUser 轉(zhuǎn)換為 SessionUser唁影,還需要多做一個(gè)步驟:

data class SessionUser(val userId: Long = -1, val userName: String = ""): Principal {
    companion object {
        fun fromSysUser(u: SysUser?): SessionUser = if (u == null) {
            SessionUser()
        } else {
            SessionUser(u.userId, u.userName)
        }
    }
}

現(xiàn)在有了用戶會(huì)話類型,也有了數(shù)據(jù)庫(kù)類型掂名,下面把路由的空給填了,也就是要寫那個(gè) routing 下面的 user() 方法:

data class ReqLogin(val userName: String, val password: String)

data class AjaxResult <T>(val code: Int, val msg: String, val data: T?) {
    companion object {
        fun success(): AjaxResult<*> = AjaxResult(200, "操作成功", null)
        fun success(msg: String): AjaxResult<*> = AjaxResult(200, msg, null)
        fun<T> success(obj: T?): AjaxResult<T> = AjaxResult(200, "操作成功", obj)
        fun<T> success(msg: String, obj: T?): AjaxResult<T> = AjaxResult(200, msg, obj)
        fun error(): AjaxResult<*> = AjaxResult(500, "操作失敗", null)
        fun error(msg: String): AjaxResult<*> = AjaxResult(500, msg, null)
        fun<T> error(obj: T?): AjaxResult<T> = AjaxResult(500, "操作失敗", obj)
        fun<T> error(msg: String, obj: T?): AjaxResult<T> = AjaxResult(500, msg, obj)
    }
}

fun Routing.user() = route("/user") {
    post<ReqLogin>("/login") {
        val (u, err) = UserMapper.login(it)
        if (u == null) {
            call.respond(AjaxResult.error(err))
            return@post
        }
        val su = SessionUser.fromSysUser(u)
        call.sessions.set(su)
        call.respond(AjaxResult.success(su))
    }
    authenticate(AUTH_SESSION) {
        get("/info") {
            val u = UserMapper.getInfo(user?.userId ?: -1)
            call.respond(AjaxResult.success(u))
        }
        get("/logout") {
            call.sessions.set(null)
            call.respond(AjaxResult.success())
        }
    }
}

好了哟沫,到這里我們又發(fā)現(xiàn)缺了個(gè) UserMapper饺蔑,在我們以往的經(jīng)驗(yàn)中,Mapper 就是要操作數(shù)據(jù)庫(kù)了嗜诀,在 Ktor 里面也不例外猾警,最后把這個(gè) Mapper 補(bǔ)上:

object UserMapper {
    fun login(req: ReqLogin): Pair<SysUser?, String> {
        val user = WHDatabase.database.sysUsers.find { it.userName eq req.userName } ?: return null to "用戶不存在"
        val match = BCryptPasswordEncoder().matches(req.password, user.password)
        if (!match) {
            return null to "密碼錯(cuò)誤"
        }
        return user to ""
    }

    fun getInfo(userId: Long): SysUser? =
        WHDatabase.database.sysUsers.find { it.userId eq userId }
}

這樣的 Mapper 是不是也很簡(jiǎn)單,不需要寫任何的 xml隆敢,也沒有 SQL 注入的風(fēng)險(xiǎn)发皿。

最后,我們來(lái)嘗試一下拂蝎,把項(xiàng)目跑起來(lái)穴墅,然后同樣用 curl 進(jìn)行試驗(yàn)吧:

$ curl -d '{"userName": "admin", "password": "12345678"}' http://0.0.0.0:8080/user/login -c cookie
{
  "code" : 200,
  "msg" : "操作成功",
  "data" : {
    "userId" : 1,
    "userName" : "admin"
  }
}

$ curl http://0.0.0.0:8080/user/info
{
  "code" : 500,
  "msg" : "必須先登錄才能訪問(wèn)此接口",
  "data" : null
}

$ curl http://0.0.0.0:8080/user/info -b cookie
{
  "code" : 200,
  "msg" : "操作成功",
  "data" : {
    "userId" : 1,
    "userName" : "admin"
  }
}

好了,目前我們已經(jīng)完成了 Ktor + Ktorm 的整合了温自,代碼量很少玄货,寫起來(lái)很方便也很直觀,你所需要的東西悼泌,都有約定俗成的實(shí)現(xiàn)方法松捉,也完全不需要關(guān)注序列化相關(guān)的內(nèi)容」堇铮基于上面的這個(gè)案例隘世,你甚至可以直接拿來(lái)擴(kuò)充功能。

從另一方面來(lái)說(shuō)鸠踪,目前這份代碼也著實(shí)有些簡(jiǎn)陋丙者,舉個(gè)例子,假設(shè)我們的用戶擁有不同權(quán)限慢哈,對(duì)于某些接口的訪問(wèn)需要先驗(yàn)證權(quán)限蔓钟,是不是需要每次在接到請(qǐng)求時(shí)都查一次用戶權(quán)限呢?Springboot 有很好用的權(quán)限組件卵贱,Ktor 有類似的東西嗎滥沫?這些問(wèn)題將在下一篇為各位解答侣集。

順便說(shuō)一句,上面的代碼兰绣,使用了封裝得更優(yōu)雅的世分,由指令集自研的 Ktor 庫(kù),我們致力于讓代碼變得更簡(jiǎn)單缀辩,讓開發(fā)的同學(xué)可以更多的心思放在業(yè)務(wù)邏輯上臭埋。如何引用我們的庫(kù),也很簡(jiǎn)單:

implementation("com.github.isyscore:common-ktor:2.2.1")
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末臀玄,一起剝皮案震驚了整個(gè)濱河市瓢阴,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌健无,老刑警劉巖荣恐,帶你破解...
    沈念sama閱讀 219,366評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異累贤,居然都是意外死亡叠穆,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門臼膏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)硼被,“玉大人,你說(shuō)我怎么就攤上這事渗磅∪铝颍” “怎么了?”我有些...
    開封第一講書人閱讀 165,689評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵夺溢,是天一觀的道長(zhǎng)论巍。 經(jīng)常有香客問(wèn)我,道長(zhǎng)风响,這世上最難降的妖魔是什么嘉汰? 我笑而不...
    開封第一講書人閱讀 58,925評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮状勤,結(jié)果婚禮上鞋怀,老公的妹妹穿的比我還像新娘。我一直安慰自己持搜,他們只是感情好密似,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著葫盼,像睡著了一般残腌。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,727評(píng)論 1 305
  • 那天抛猫,我揣著相機(jī)與錄音蟆盹,去河邊找鬼。 笑死闺金,一個(gè)胖子當(dāng)著我的面吹牛逾滥,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播败匹,決...
    沈念sama閱讀 40,447評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼寨昙,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了掀亩?” 一聲冷哼從身側(cè)響起舔哪,我...
    開封第一講書人閱讀 39,349評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎槽棍,沒想到半個(gè)月后尸红,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,820評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡刹泄,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了怎爵。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片特石。...
    茶點(diǎn)故事閱讀 40,127評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖鳖链,靈堂內(nèi)的尸體忽然破棺而出姆蘸,到底是詐尸還是另有隱情,我是刑警寧澤芙委,帶...
    沈念sama閱讀 35,812評(píng)論 5 346
  • 正文 年R本政府宣布逞敷,位于F島的核電站,受9級(jí)特大地震影響灌侣,放射性物質(zhì)發(fā)生泄漏推捐。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評(píng)論 3 331
  • 文/蒙蒙 一侧啼、第九天 我趴在偏房一處隱蔽的房頂上張望牛柒。 院中可真熱鬧,春花似錦痊乾、人聲如沸皮壁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)蛾魄。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間滴须,已是汗流浹背舌狗。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留描馅,地道東北人把夸。 一個(gè)月前我還...
    沈念sama閱讀 48,388評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像铭污,于是被迫代替她去往敵國(guó)和親恋日。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評(píng)論 2 355

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