[Ktor] 實現(xiàn)動態(tài)化

眾所周知的,我們的服務(wù)器不可能因為一點小的改動就要編譯整個 jar 包,也不可能直接就進行完整的部署流程。正常的做法是把功能模塊做成插件,然后動態(tài)的去進行加載,這樣就不需要重啟服務(wù)了。

那么在 Ktor 里如何實現(xiàn)呢,其實也是很簡單的。Ktor 是基于 JVM 的,自然擁有 JVM 上可以做的各類騷操作,下面我們先來寫一個簡單的插件。


插件(1) 改造 Ktor 項目模板

經(jīng)過改造后的項目模板如下所示,可以看到它不具備 Application,也沒有 main 函數(shù),因為插件并不需要這些,它只需要確保自己可以被 Routing 找到即可。

SamplePlugin
  |-- src
  |    |-- PluginRouting.kt
  |-- build.gradle
  |-- gradle.properties
  |-- settings.gradle

對于 PluginRouting.kt 而言,內(nèi)容很簡單,只要寫出要響應(yīng)的請求等即可:

fun Routing.pluginRouting() {
    get("/plug") {
        call.respondText { """{"result":0,"from":"plugin"}""" }
    }
}

然后其他的可以什么都不修改,使用 Ktor 項目的標(biāo)準(zhǔn)模板即可。

插件(2) 編譯

通常我們使用 gradle build 來編譯一個 Ktor 項目,而對于插件則要使用 gradle jar 來編譯。

編譯完成后我們可以得到一個名為 SamplePlugin-1.0.0.jar 的文件,這即是插件了。

需要說明的是,對于 Kotlin 代碼中的擴展,其編譯后的代碼是靜態(tài)方法,并且傳入被擴展對象作為參數(shù),如上述代碼在編譯后生成的代碼是:

public class PluginRoutingKt {
    public static final void pluginRouting(Routing $routing$this);
}

到此,插件已經(jīng)開發(fā)完畢了,非常的簡單,它提供了一個 /plug 的請求響應(yīng)。


宿主(1) 編寫加載插件的代碼

既然有了插件,那就得用宿主去加載它,利用 JVM 內(nèi)的方法就可以輕松實現(xiàn)了,如上面的插件,我們可以這樣加載:

fun loadPlugin(r: Routing) {
    val jar = File("SamplePlugin-1.0.0.jar").toURI().toURL()
    val loader = URLClassLoader(arrayOf(jar))
    val clz = loader.loadClass("com.rarnu.sample.plugin.PluginRoutingKt")
    val m = clz.getDeclaredMethod("pluginRouting", Routing::class.java)
    m.invoke(null, r)
}

當(dāng)然你可以把代碼寫得更靈活一些,比如說從配置文件讀取要加載的路由:

fun loadPlugin(r: Routing, filePath: String, clsName: String, routingName: String) = 
    URLClassLoader(arrayOf(File(filePath).toURI().toURL()))
        .loadClass(clsName)
        .getDeclaredMethod(routingName, Routing::class.java)
        .invoke(null, r)

然后就可以寫一個 Routing 來實現(xiàn)加載了:

get("/load/{file}/{cls}/{routing}") {
    val f = call.parameters["file"] ?: ""
    val cls = call.parameters["cls"] ?: ""
    val routing = call.parameters["routing"] ?: ""
    loadPlugin(this@sampleRouting, f, cls, routing)
    call.respondText { "OK" }
}

宿主(2) 請求動態(tài)加載

跑起項目后,就可以在瀏覽器內(nèi)訪問以下 URL 以實現(xiàn)動態(tài)加載插件了

http://0.0.0.0/load/SamplePlugin-1.0.0.jar/com.rarnu.sample.plugin.PluginRoutingKt/pluginRouting

成功后即可在瀏覽器內(nèi)訪問以下 URL 來驗證結(jié)果:

http://0.0.0.0/plug

宿主(3) 動態(tài)卸載插件

如果只是用來測試或者要更新插件,那就必須先把插件卸載,否則會造成一個協(xié)程崩潰的異常:

io.ktor.server.engine.BaseApplicationResponse$ResponseAlreadySentException: Response has already been sent

其根本原因是一個 request 只能有一個 response,如果有兩個同名的路由,則會發(fā)送兩次 response 引起該錯誤。

Ktor 并沒有提供動態(tài)卸載路由的能力,只能研究源碼了,所幸的是這部分源碼非常容易找到:

@ContextDsl
open class Route(val parent: Route?, val selector: RouteSelector) : ApplicationCallPipeline() {

    val children: List<Route> get() = childList
    private val childList: MutableList<Route> = ArrayList()
    @Volatile private var cachedPipeline: ApplicationCallPipeline? = null
    internal val handlers = ArrayList<PipelineInterceptor<Unit, ApplicationCall>>()

    fun createChild(selector: RouteSelector): Route {
        val existingEntry = childList.firstOrNull { it.selector == selector }
        if (existingEntry == null) {
            val entry = Route(this, selector)
            childList.add(entry)
            return entry
        }
        return existingEntry
    }
  
    ... ...
}

可以清楚的看到,添加路由的過程,其實就是調(diào)用了 createChild 并且將新增的內(nèi)容放到 childList 內(nèi),所以刪去 childList 里的內(nèi)容,就可以實現(xiàn)卸載路由。由此也可以輕松的寫下代碼了:

fun unloadRoute(r: Routing, routes: Array<String>) {
    val list = r.javaClass.superclass.getDeclaredField("childList").apply { isAccessible = true }.get(r) as MutableList<*>
    list.removeIf { "$it" in routes }
}

同樣的,現(xiàn)在可以寫一個路由來完成卸載:

get("/unload/{routing}") {
    val routing = call.parameters["routing"] ?: ""
    unloadRoute(this@sampleRouting, arrayOf(routing))
    call.respondText { "OK" }
}

好了,就寫到這吧,Ktor 動態(tài)化的能力已經(jīng)具備了,如果要投入實用,還需要加入一定的配置文件與讀寫的機制。不需要使用路由請求的方式來加載,而是在監(jiān)視到文件變化時自動實現(xiàn)加載,才是最好的解決方案。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容