Kotlin 中的領域特定語言">Kotlin 中的領域特定語言

分類:IT技術 時間:2017-09-28

如果你看過 我最近發表 關於  Kotlin 的文章,你可能會註意到我曾經提到過   DSL( Domain Specific Languages ,領域專用語言) Kotlin 是一門提供了強大特性支持 DSL 的編程語言。這些特性中,我曾經介紹過 具有接收者的函數字面量(Function Literals with Receiver) ,以及 調用約定 和 中綴表達式 。

這篇文章中,我們會看到 DSL 的概念,當然還有如何使用 Kotlin 創建一個相對簡單的 DSL 示例。

舉例來說,我常常在需要 HTTPS 通訊的情況下,艱難地使用 Java API 建立 SSL/TLS 連接。就在最近,我還不得不在我們的應用程序中實現了一個不同類型的 SSL/TLS。為了做這個事情,我再次想寫一個小型庫來支持類似的任務,以樣板的方式避開所有困難。

領域專用語言 (DSL)

領域專用語言 這個術語現在使用得非常廣泛,但就所要談論的情況而言,它指的是某種“微型語言”。它以半陳述的方式描述特定領域對象的構造。用於創建 XML、HTML 或 UI 數據的  Groovy builders 就是一個例子。在我看來,最好的例子是  Gradle ,它也是使用基於 Grovvy 的 DSL 來描述軟件構建自動化。(順便提一下,還有一個 Gradle-Script-Kotlin ,是針對 Gradle 的 Kotlin DSL。)

把目標簡化一下,DSL 是一種提供 API 的方式,這種 API 更清晰、更具可讀性,最重要的是,它比傳統 API 結構更明確。DSL 使用嵌入的描述而不是用一種 命令的 方式調用各個功能,這種方式會創建清晰的結構,我們甚至可以稱之為“語法”。DSL 定義可以合並不同構造,應用於各個作用域,並在其中使用不同的功能。

為什麽 Kotlin 特別適用於 DSL

大家都知道 Kotlin 是靜態類型語言,它擁有像 Groovy 這樣的動態類型語言所不具備的能力。最重要的是,靜態類型允許在編譯期檢查錯誤,而且一般情況下會得到 IDE 更好的支持。

好了,別再浪費時間在理論上,我們來感受 DSL 的樂趣吧,有很多嵌入的 Lambda 哦!因此,你最好先搞懂如何在 Kotlin 中使用 Lambda !

Kotlin DSL 的示例

本文的引言部分就說過我們會使用 Java API 建立 SSL/TLS 連接來作為示例。如果你對此並不熟悉,我們先來簡單的介紹一下。

Java 安全套接字擴展

Java 安全套接字擴展 (Java Secure Socket Extension, JSSE) 是 Java SE 1.4 就引入的庫,它提供通過 SSL/TLS 創建安全連接的功能,包括客戶端/服務器認證、數據加密以及保證消息完整性。和許多其他人一樣,我發現安全問題相當棘手,哪怕在日常工作中我們經常用到這些功能。原因之一可能就是需要組合大量 API。另一個原因建立這樣的連接非常繁瑣。來看看類層次結構:

相當多的類,不是嗎?你通常從創建一個 信息密鑰 存儲開始,然後配合一個隨機數生成器建立 SSLContext。這可以用於工廠模式,用來創建你的 Socket。老實說,聽起來並不難,不過我們來看看實現呢 —— 用 Java ...

使用 Java 設置 TLS 連接

我需要100多行代碼來做到這一點。它展示了一個函數,可用於連接到具有可選相互身份驗證的TLS服務器,如果這是雙方的需要,客戶端和服務器都需要彼此信任。

JSSE Java:

public class TLSConfiguration { ... }
public class StoreType { ... }

public void connectSSL(String host, int port,
        TLSConfiguration tlsConfiguration) throws IOException {

        String tlsversion = tlsConfiguration.getProtocol();
        StoreType keystore = tlsConfiguration.getKeystore();
        StoreType trustStore = tlsConfiguration.getTruststore();
        try {
            SSLContext ctx = SSLContext.getInstance(tlsVersion);
            TrustManager[] tm = null;
            KeyManager[] km = null;
            if (trustStore != null) {
                tm = getTrustManagers(trustStore.getFilename(), 
                        trustStore.getPassword().toCharArray(),
                        trustStore.getStoretype(), trustStore.getAlgorithm());
            }
            if (keystore != null) {
                km = createKeyManagers(keystore.getFilename(), 
                        keystore.getPassword(),
                        keystore.getStoretype(), keystore.getAlgorithm());
            }
            ctx.init(km, tm, new SecureRandom());
            SSLSocketFactory sslSocketFactory = ctx.getSocketFactory();
            SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(
                                  host, port);
            sslSocket.startHandshake();
        } catch (Exception e) {
            throw new IllegalStateException("Not working :-(", e);
        }
    }


    private static TrustManager[] getTrustManagers(
        final String path, final char[] password,
        final String storeType, final String algorithm) throws Exception {

        TrustManagerFactory fac = TrustManagerFactory.getInstance(
               algorithm == null ? "SunX509" : algorithm);
        KeyStore ks = KeyStore.getInstance(
               storeType == null ? "JKS" : storeType);
        Path storeFile = Paths.get(path);
        ks.load(new FileInputStream(storeFile.toFile()), password);
        fac.init(ks);
        return fac.getTrustManagers();
    }

    private static KeyManager[] createKeyManagers(
        final String filename, final String password,
        final String keyStoreType, final String algorithm) throws Exception {

        KeyStore ks = KeyStore.getInstance(
                keyStoreType == null ? "PKCS12" : keyStoreType);
        ks.load(new FileInputStream(filename), password.toCharArray());
        KeyManagerFactory kmf = KeyManagerFactory.getInstance(
                algorithm == null ? "SunX509" : algorithm);
        kmf.init(ks, password.toCharArray());
        return kmf.getKeyManagers();
    }

好的,這是Java,對吧?嗯,代碼相當的冗長 - 有許多被檢查的異常和資源被處理,為簡潔起見,我已經在這裏簡化了。

下一步,我們將這些代碼轉換成簡明的Kotlin代碼,然後為願意建立TLS連接的客戶端提供DSL。

使用 Kotlin 設置 TLS 連接

Kotlin 的 SSLSocketFactory:

fun connectSSL(host: String, port: Int, protocols: List<String>, kmConfig: Store?, tmConfig: Store?){
    val context = createSSLContext(protocols, kmConfig, tmConfig)
    val sslSocket = context.socketFactory.createSocket(host, port) as SSLSocket
    sslSocket.startHandshake()
}

fun createSSLContext(protocols: List<String>, kmConfig: Store?, tmConfig: Store?): SSLContext {
    if (protocols.isEmpty()) {
        throw IllegalArgumentException("At least one protocol must be provided.")
    }
    return SSLContext.getInstance(protocols[0]).apply {
        val keyManagerFactory = kmConfig?.let { conf ->
            val defaultAlgorithm = KeyManagerFactory.getDefaultAlgorithm()
            KeyManagerFactory.getInstance(conf.algorithm ?: defaultAlgorithm).apply {
                init(loadKeyStore(conf), conf.password)
            }
        }
        val trustManagerFactory = tmConfig?.let { conf ->
            val defaultAlgorithm = TrustManagerFactory.getDefaultAlgorithm()
            TrustManagerFactory.getInstance(conf.algorithm ?: defaultAlgorithm).apply {
                init(loadKeyStore(conf))
            }
        }

        init(keyManagerFactory?.keyManagers, trustManagerFactory?.trustManagers,
            SecureRandom())
    }

}

fun loadKeyStore(store: Store) = KeyStore.getInstance(store.fileType).apply {
    load(FileInputStream(store.name), store.password)
}

您可能會註意到,我沒有在這裏進行一對一轉換,這是因為在Kotlin的stdlib中提供了一些函數,這在許多情況下有很多幫助。這一小段源代碼包含四種apply的用法,一種利用 擴展函數對象 的方法。它允許我們通過在創建時傳遞上下文給它的lambda的語句塊內重用,就像DSL一樣,我們將在稍後看到。

被apply的對象成為函數的receiver,然後可以通過這個receiver在lambda中使用,即可以調用成員,而不需要任何額外的前綴。如果仍然不明白,可以看看我的博客文章關於這些擴展函數對象的部分。

我們已經看到,Kotlin可以比Java更簡潔,但這是常識。我們現在想把這個代碼包裝在一個DSL中,然後客戶端可以用它來進行TSL連接。

使用 Kotlin 創建 DSL

在創建 API 時要考慮的第一件事 —— 這也適用於 DSL,即客戶端會被問到的:我們需要哪些配置參數。

在我們的例子中,這是非常簡單的。我們需要分別為 keystore 和  truststore  提供零個或一個描述。另外,重要的是要知道接受的密碼套件和套接字鏈接超時。最後同樣重要的是,必須要為我們的連接提供一組協議,例如  TLSv1.2 。對於每一個配置的值,缺省值都是可用的,必要時將需要使用。

這可以很容易地封裝在配置類中,我們稱之為 ProviderConfiguration ,因為它稍後將會配置在我們的  TLSSocketFactoryProvider 中。

配置

DSL 配置類:

class ProviderConfiguration {

    var kmConfig: Store? = null
    var tmConfig: Store? = null
    var socketConfig: SocketConfiguration? = null

    fun open(name: String) = Store(name)

    fun sockets(configInit: SocketConfiguration.() -> Unit) {
        this.socketConfig = SocketConfiguration().apply(configInit)
    }

    fun keyManager(store: () -> Store) {
        this.kmConfig = store()
    }

    fun trustManager(store: () -> Store) {
        this.tmConfig = store()
    }
}

這裏有三個可空屬性,默認情況下,它們都為 null ,因為客戶端可能不希望配置連接的所有內容。這裏的重要方法是 socketskeyManager , 和  trustManager ,它們擁有一個帶有函數類型的參數。第一個  SocketConfiguration 是通過定義一個  receiver  的函數顯式聲明。這使得客戶端可以傳入一個 lambda 以訪問  SocketConfiguration 中的所有成員,正如我們從擴展函數知道的這一點。

socket 方法通過創建一個新的實例來提供 receiver,然後通過 apply 來調用傳遞的函數。然後將生成的配置實例用作內部屬性的值。另外兩個函數比較簡單,因為它們定義了簡單的函數類型,沒有 receiver。他們只是期望一個函數被傳遞,返回一個 Store 的一個實例,然後被置於內部屬性上。

現在再來看看 Store 和  SocketConfiguration 類。

DSL 配置類(2):

data class SocketConfiguration(
    var cipherSuites: List<String>? = null, var timeout: Int? = null,
    var clientAuth: Boolean = false)

class Store(val name: String) {
    var algorithm: String? = null
    var password: CharArray? = null
    var fileType: String = "JKS"

    infix fun withPass(pass: String) = apply {
        password = pass.toCharArray()
    }

    infix fun beingA(type: String) = apply {
        fileType = type
    }

    infix fun using(algo: String) = apply {
        algorithm = algo
    }
}

第一個類是一個簡單的數據類,而且屬性又是可空的。 Store 有點獨特,因為它只定義了三個  infix 函數,實際上這上是屬性的簡單設置器。我們在這裏使用 apply ,因為它之後會返回應用的對象。這使我們能夠輕松地鏈接到設置器。目前尚未提及的一件事是  ProviderConfiguration 中的函數 open(name: String) 。很快就會看到這可以用作  Store 的工廠。這一切都結合在一起,可以定義我們的配置。但是在這之前,可以先看看客戶端,先來看一下 TLSSocketFactoryProvider ,它需要配置我們剛剛看到的類。

DSL 核心類

TLSSocketFactoryProvider

class TLSSocketFactoryProvider(init: ProviderConfiguration.() -> Unit) {

    private val config: ProviderConfiguration = ProviderConfiguration().apply(init)

    fun createSocketFactory(protocols: List<String>)
    : SSLSocketFactory = with(createSSLContext(protocols)) {
        return ExtendedSSLSocketFactory(
            socketFactory, protocols.toTypedArray(),
            getOptionalCipherSuites() ?: socketFactory.defaultCipherSuites)
    }

    fun createServerSocketFactory(protocols: List<String>)
    : SSLServerSocketFactory = with(createSSLContext(protocols)) {
        return ExtendedSSLServerSocketFactory(
            serverSocketFactory, protocols.toTypedArray(),
            getOptionalCipherSuites() ?: serverSocketFactory.defaultCipherSuites)
    }

    private fun getOptionalCipherSuites() =
        config.socketConfig?.cipherSuites?.toTypedArray()


    private fun createSSLContext(protocols: List<String>): SSLContext {
       //... already known
    }
}

這個類也不難理解,它的大部分內容都不顯示在這裏,因為我們已經從使用 Kotlin 的 SSLSocketFactory 已獲知,特別是  createSSLContext

這個列表中最重要的是構造函數。它期望一個具有 ProviderConfiguration 的函數對象作為 receiver。在內部,它創建一個新的實例,並調用此函數來初始化配置。該配置用於  TLSSocketFactoryProvider 的其他函數,一旦調用了一個公共方法,即分別是  createSocketFactory 和  createServerSocketFactory ,就可以設置 SocketFactory

為了將這些組合在一起,必須創建一個頂級函數,這將是客戶端與 DSL 的接入點。


Tags: 領域 語言 DSL Kotlin 方式 使用

文章來源:


ads
ads

相關文章
ads

相關文章

ad