Kotlin 中的领域特定语言

Kotlin Java   2017-09-28 09:12:55 发布
您的评价:
     
4.0
收藏     0收藏
文件夹
标签
(多个标签用逗号分隔)

如果你看过 我最近发表 关于  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 ,因为客户端可能不希望配置连接的所有内容。这里的重要方法是 sockets ,  keyManager , 和  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 的接入点。

 

来自:https://www.oschina.net/translate/creating-dsl-with-kotlin

 

扩展阅读

Android资源库列表
Java的进化, "J++"? Kotlin初探与集成
Kotlin 开发Android 实战(一)
Java资源大全中文版
使用Kotlin开发Android应用

为您推荐

Nodejs学习资料
给 JavaScript 初心者的 ES2015 实战
非常实用的CSS使用技巧
iOS 苹果官方Demo合集
[译]Scala DSL教程: 实现一个web框架路由器

更多

Kotlin
Java
相关文档  — 更多
相关经验  — 更多
相关讨论  — 更多