在Kotlin中创建DSL

科特林 作为一种编程语言提供了一些非常强大的功能,这些功能允许创建自定义内部 领域特定语言(DSL)我在此博客上也写到了其中一项功能,称为 带有接收器的功能文字,其他人是 调用约定 要么 中缀符号。在本文中,我将介绍如何通过引入一个将DSL作为其API公开的库来创建Kotlin DSL。当我不得不设置Java的API时,我经常在挣扎 SSL / TLS连接 在我例如需要实施 HTTPS 通讯。我一直感觉很想编写一个可以为我完成此任务的小程序库,隐藏所有困难,当然也隐藏了所需的样板。

领域特定语言

期限 领域特定语言 如今已广泛使用,但在我要谈论的情况下,它是指某种 “迷你语言”。 用来以半声明方式描述特定域对象的构造。 DSL的示例是 Groovy建设者 用于创建XML,HTML或UI数据。我认为,最好的例子是构建工具 摇篮 本身,它也使用基于Groovy的DSL来描述软件构建自动化 [1].
简而言之,DSL是一种提供 API 与传统的API相比,它更清洁,更易读,而且最重要的是结构更合理。不必在 势在必行 DSL使用嵌套的描述方式,从而创建干净的结构。我们甚至可以称其为“语法”。 DSL定义了将不同的构造相互组合的可能性,并且充分利用了可以使用不同功能的范围。

为什么Kotlin特别适合DSL

众所周知,Kotlin是一种静态类型的语言,它启用了动态类型的语言(例如Groovy * duck away *)中不可用的功能。最重要的是,静态类型允许在编译时检测错误,并且通常提供更好的IDE支持。

好的,我们不要浪费时间在理论上,而是开始使用DSL,这意味着很多嵌套的lambda!所以,您最好知道如何使用 Lambdas 在KotlinðŸ™,

以Kotlin DSL为例

如本文开头部分所述,我们以Java API 设置SSL / TLS连接为例。如果您不熟悉它,下面将提供简短的介绍。

Java安全套接字扩展

Java安全套接字扩展(JSSE) 是从1.4开始成为Java SE一部分的库。它提供了用于通过SSL / TLS创建安全连接的功能,包括客户端/服务器身份验证,数据加密, 和消息的完整性。与许多其他应用程序一样,尽管我在日常工作中经常使用这些功能,但我发现安全性主题比较棘手。一个原因可能是大量可能的API组合,另一个原因是其冗长需要建立这样的连接。看一看类的层次结构:

 jsse _classes

我们可以看到很多类,它们需要以某种有意义的方式进行组合。您通常从一开始就创建一个 相信 商店 store 和 use in combination with a random generator for setting up the SSLContext. The SSLContext is used for creating a SSLSocketFactory 要么  SSLServerSocketFactory, which then 提供 the Socket instances. This sounds pretty easy but let's observe how it looks when expressed in Java code.

在Java中设置TLS连接

The abstractly described task of assembling the bits 和 pieces together took me little more than 100 lines of code. The following snippet shows a function that can be used to connect to a TLS server with optional mutual authentication, which is needed if both parties, client 和 server, need to 相信 each other. The classes can be found in the 爪哇 x.net.ssl package.

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

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

        String  tls Version =  tls Configuration.getProtocol();
        StoreType 密钥库 =  tls Configuration.getKeystore();
        StoreType 相信Store =  tls Configuration.getTruststore();
        try {
            SSLContext ctx = SSLContext.getInstance(tlsVersion);
            TrustManager[] tm = null;
            KeyManager[] km = null;
            if (trustStore != null) {
                tm = getTrustManagers(trustStore.getFilename(), 
                        相信Store.getPassword().toCharArray(),
                        相信Store.getStoretype(), 相信Store.getAlgorithm());
            }
            if (keystore != null) {
                km = createKeyManagers(keystore.getFilename(), 
                        密钥库.getPassword(),
                        密钥库.getStoretype(), 密钥库.getAlgorithm());
            }
            ctx.init(km, tm, new SecureRandom());
            SSLSocketFactory  ssl SocketFactory = ctx.getSocketFactory();
            SSLSocket  ssl Socket = (SSLSocket)  ssl SocketFactory.createSocket(
                                  host, port);
             ssl Socket.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  键 StoreType, final String algorithm) throws Exception {

        KeyStore ks = KeyStore.getInstance(
                 键 StoreType == null ? "PKCS12" :  键 StoreType);
        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代码:

使用Kotlin设置TLS连接

 fun connectSSL(host: String, port: Int, protocols: List<String>, kmConfig: Store?, tmConfig: Store?){
    val context = createSSLContext(protocols, kmConfig, tmConfig)
    val  ssl Socket = context.socketFactory.createSocket(host, port) as SSLSocket
     ssl Socket.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  键 ManagerFactory = kmConfig?.let { conf ->
            val defaultAlgorithm = KeyManagerFactory.getDefaultAlgorithm()
            KeyManagerFactory.getInstance(conf.algorithm ?: defaultAlgorithm).apply {
                init(loadKeyStore(conf), conf.password)
            }
        }
        val 相信ManagerFactory = tmConfig?.let { conf ->
            val defaultAlgorithm = TrustManagerFactory.getDefaultAlgorithm()
            TrustManagerFactory.getInstance(conf.algorithm ?: defaultAlgorithm).apply {
                init(loadKeyStore(conf))
            }
        }

        init(keyManagerFactory?.keyManagers, 相信ManagerFactory?.trustManagers,
            SecureRandom())
    }
}

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

You might notice that the shown code is not a one-to-one conversion, which is because 科特林 提供 a set of very useful functions in its standard 图书馆 that often help with writing smarter code. This small piece of source code contains four usages of apply, a method that makes use of 带有接收器的功能文字。这是科特林著名的博物馆之一 范围函数,这将在任意上下文对象上创建一个范围,在其中我们无需附加限定符即可访问该上下文对象的成员。

到目前为止,我们已经看到Kotlin可以比Java简洁得多,但这是昨天的新闻。在下一节中,我们将最终看到如何将代码包装在DSL中,然后将DSL作为API公开给客户端。

在Kotlin中创建DSL

创建API时首先要考虑的事情(这当然也适用于DSL)是我们如何使客户端变得容易。我们需要定义某些需要由用户提供的配置参数。
就我们而言,这很简单。我们需要一个零或一个描述 密钥库 和一个 信任库 分别。另外,重要的是要接受 密码套件 和插座 连接超时 众所周知。最后但并非最不重要的一点是,必须为我们的连接提供一组协议,就像 TLSv1.2 例如。对于每种配置,默认值均可用,并将在需要时使用。
The described values can easily be wrapped into a configuration class, which we’ll call ProviderConfiguration since it will be used for configuring a TLSSocketFactoryProvider later on.

配置

class ProviderConfiguration {

    var kmConfig: Store? = null
    var tmConfig: Store? = null
    var 插座Config: SocketConfiguration? = null

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

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

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

    fun 相信Manager(store: () -> Store) {
        this.tmConfig = store()
    }
}

We can see three nullable properties here, each of which is null 通过 default because the clients might not want to configure everything for their connection. The relevant methods in this class are 插座s(), 键 Manager()相信Manager(), all of which have a single parameter of a function type. The 插座s() method goes even a step further 通过 defining a function literal 带接收器, which is SocketConfiguration here. This enables the client to pass in a 拉姆达 that has access to all members of SocketConfiguration as we know it from extension functions 和 shown with apply in earlier examples.
The 插座() method 提供 the receiver 通过 creating a new instance 和 then invoking the passed function on it with apply. The resulting configured instance is then used as a value for the internal property. Both other functions are a bit easier as they define simple functions types, without a receiver, as their parameters. They simply expect a provider of an instance of Store, which then is set on the internal property.

StoreSocketConfiguration

Here you can observce the classes StoreSocketConfiguration:

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"

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

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

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

The first one is as easy as it could get, a simple data class with, once again, nullable properties. Store is a bit unique though as it, in addition to its properties, defines three  中缀 functions, which are acting as setters for the properties basically. We again make use of apply here because it returns its context object after invocation 和 is used as a tool for providing a fluent API here; the methods can be chained later on. One thing I haven’t mentioned so far is the open(name: String) function defined in ProviderConfiguration. This one is supposed to be used as a factory for instances ofStore 和 we are about to see this in action soon. All of this in combination creates a neat way for defining the necessary configuration data. Before we can have a look at the client side, it's necessary to observe theTLSSocketFactoryProvider, which has to be configured with the classes I just introduced.

科特林 DSL核心

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

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

    fun createSocketFactory(protocols: List): SSLSocketFactory =
        with(createSSLContext(protocols)) {
            return ExtendedSSLSocketFactory(
                插座Factory, protocols.toTypedArray(),
                getOptionalCipherSuites() ?: 插座Factory.defaultCipherSuites
            )
        }

    fun createServerSocketFactory(protocols: List): 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 shown earlier
    }
}

This one isn’t hard to understand either. Most of the DSL's content has already been shown in createSSLContext() earlier.
The most important thing in this listing is the constructor. It expects a function with a ProviderConfiguration as a receiver. Internally it creates a new instance of it 和 calls this function in 要么 der to initialize the configuration. The configuration is used in TLSSocketFactoryProvider's other functions for setting up a SocketFactory as soon as one of the public methods createSocketFactory 要么 createServerSocketFactory is being called.

客户端API和DSL的用法

val defaultTLSProtocols = listOf("TLSv1.2")

fun serverSocketFactory(
    protocols: List<String> = defaultTLSProtocols,
    configuration: ProviderConfiguration.() -> Unit = {}) =
        with(TLSSocketFactoryProvider(configuration)) {
            this.createServerSocketFactory(protocols)
        }

fun 插座Factory(
    protocols: List<String> = defaultTLSProtocols,
    configuration: ProviderConfiguration.() -> Unit = {}) =
        with(TLSSocketFactoryProvider(configuration)) {
            this.createSocketFactory(protocols)
        }

In 要么 der to assemble all of this DSL together, simple top-level functions were created, which represent the client’s entry point to this DSL. These two functions only delegate a function literal with ProviderConfiguration receiver to a created instance of TLSSocketFactoryProvider, which is used to create corresponding 插座 和 server 插座 factories via createSocketFactorycreateServerSocketFactory respectively.

最后,我们可以轻松使用此DSL并使用它创建一些套接字:

 val fac = 插座Factory {
         键 Manager {
            open("certsandstores/clientkeystore") withPass "123456" beingA "jks"
        }
        相信Manager {
            open("certsandstores/myTruststore") withPass "123456" beingA "jks"
        }
        插座s {
            cipherSuites =
            listOf("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
                    "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
                    "TLS_DHE_RSA_WITH_AES_128_CBC_SHA",
                    "TLS_DHE_RSA_WITH_AES_256_CBC_SHA")
            timeout = 10_000
        }
    }

 val 插座 = fac.createSocket("192.168.3.200", 9443)

Let’s recap: The top-level function 插座Factory expects a 拉姆达, which has access to ProviderConfiguration members since it’s the 拉姆达’s receiver. Therefore we can call 键 Manager(), 相信Manager()插座s() without any additional prefix here. The functions 键 Manager()相信Manager() take an instance of Store, which we create 通过 calling ProviderConfiguration::openStore's 中缀 functions. The 插座s() method is different as it expects a function literal with SocketConfiguration receiver, which is a data class 和 therefore 提供 access to its properties directly.

我希望这是可以理解的。完全了解lamda在Kotlin中的工作方式(绝对是带有接收器的人)绝对是不可避免的。

In my humble opinion, this is a very clear definition of a SocketFactory 和 much easier to understand than the standard Java way shown earlier. Another feature provided 通过 a DSL like this is the possibility to make use of all language features 和 other methods that are available in the receiver’s contexts. You could easily read values from a file for creating the store configurations 要么 use loops, ifwhen constructs etc. whenever you need to:

 val fac = 插座Factory {
        相信Manager {
            if (System.currentTimeMillis() % 2 == 0L) {
                open("any") withPass "123456" beingA "jks"
            } else {
                open("other") withPass "123456" beingA "jks"
            }
        }
    }

的GitHub上的库

我们刚刚看过的代码可以在 的GitHub。如果您有任何想法或疑虑,请告诉我。

对于那些是TLS特别是JSSE lib的专家的人:我知道,该库尚未包含JSSE的许多案例和可能性。我希望找到对这种库感兴趣的人,以便我们找到相应的扩展方法。

包起来

我们已经通过使用JSSE库设置TLS连接的示例了解了为什么DSL可以是一种更好的向客户端提供API的方式.Kotlin是编写此类API的一种非常好的语言,因为它的静态类型和出色的功能特征。还有许多其他Kotlin DSL示例,您可以在GitHub上找到它们。首先,看看 科特林test 要么 科特林x.html 例如。

如果您想看一下我的示例,甚至想要贡献自己的力量,可以在这里找到代码: 安全性。随时提供任何反馈意见,我总是很乐意提供帮助。

如果您想了解有关Kotlin的美丽功能的更多信息,请推荐这本书 行动中的科特林 给你,我想带你去我的 其他文章 ðŸ™,


1。还有适用于Gradle的Kotlin DSL: 摇篮-Script-Kotlin

 

关于11条想法“在Kotlin中创建DSL

发表评论

您的电子邮件地址不会被公开。 必需的地方已做标记 *