Kotlin 中的领域特定语言
ElijahBolt
7年前
<p>如果你看过 <a href="/misc/goto?guid=4959754796835574128" rel="nofollow,noindex">我最近发表</a> 关于 <a href="/misc/goto?guid=4958868881467951527" rel="nofollow,noindex">Kotlin</a> 的文章,你可能会注意到我曾经提到过 <strong> DSL( <strong>Domain Specific Languages</strong> ,领域专用语言) </strong> 。 <strong>Kotlin</strong> 是一门提供了强大特性支持 DSL 的编程语言。这些特性中,我曾经介绍过 <a href="/misc/goto?guid=4959754796956821251" rel="nofollow,noindex">具有接收者的函数字面量(Function Literals with Receiver)</a> ,以及 <a href="/misc/goto?guid=4959754797044219077" rel="nofollow,noindex">调用约定</a> 和 <a href="/misc/goto?guid=4959754797133151577" rel="nofollow,noindex">中缀表达式</a> 。</p> <p>这篇文章中,我们会看到 DSL 的概念,当然还有如何使用 Kotlin 创建一个相对简单的 DSL 示例。</p> <p>举例来说,我常常在需要 HTTPS 通讯的情况下,艰难地使用 Java API 建立 SSL/TLS 连接。就在最近,我还不得不在我们的应用程序中实现了一个不同类型的 SSL/TLS。为了做这个事情,我再次想写一个小型库来支持类似的任务,以样板的方式避开所有困难。</p> <h2>领域专用语言 (DSL)</h2> <p><em>领域专用语言</em> 这个术语现在使用得非常广泛,但就所要谈论的情况而言,它指的是某种“微型语言”。它以半陈述的方式描述特定领域对象的构造。用于创建 XML、HTML 或 UI 数据的 <a href="/misc/goto?guid=4959754797227375265" rel="nofollow,noindex">Groovy builders</a> 就是一个例子。在我看来,最好的例子是 <a href="/misc/goto?guid=4959638412523065904" rel="nofollow,noindex">Gradle</a> ,它也是使用基于 Grovvy 的 DSL 来描述软件构建自动化。(顺便提一下,还有一个 <a href="/misc/goto?guid=4959754797351051402" rel="nofollow,noindex">Gradle-Script-Kotlin</a> ,是针对 Gradle 的 Kotlin DSL。)</p> <p>把目标简化一下,DSL 是一种提供 <em>API</em> 的方式,这种 API 更清晰、更具可读性,最重要的是,它比传统 API 结构更明确。DSL 使用嵌入的描述而不是用一种 <em>命令的</em> 方式调用各个功能,这种方式会创建清晰的结构,我们甚至可以称之为“语法”。DSL 定义可以合并不同构造,应用于各个作用域,并在其中使用不同的功能。</p> <h3>为什么 Kotlin 特别适用于 DSL</h3> <p>大家都知道 Kotlin 是静态类型语言,它拥有像 Groovy 这样的动态类型语言所不具备的能力。最重要的是,静态类型允许在编译期检查错误,而且一般情况下会得到 IDE 更好的支持。</p> <p>好了,别再浪费时间在理论上,我们来感受 DSL 的乐趣吧,有很多嵌入的 Lambda 哦!因此,你最好先搞懂如何在 Kotlin 中使用 <a href="/misc/goto?guid=4959749546627544456" rel="nofollow,noindex">Lambda</a> !</p> <h2>Kotlin DSL 的示例</h2> <p>本文的引言部分就说过我们会使用 Java API 建立 SSL/TLS 连接来作为示例。如果你对此并不熟悉,我们先来简单的介绍一下。</p> <h3>Java 安全套接字扩展</h3> <p><a href="/misc/goto?guid=4959754797472141345" rel="nofollow,noindex">Java 安全套接字扩展 (Java Secure Socket Extension, JSSE)</a> 是 Java SE 1.4 就引入的库,它提供通过 SSL/TLS 创建安全连接的功能,包括客户端/服务器认证、数据加密以及保证消息完整性。和许多其他人一样,我发现安全问题相当棘手,哪怕在日常工作中我们经常用到这些功能。原因之一可能就是需要组合大量 API。另一个原因建立这样的连接非常繁琐。来看看类层次结构:</p> <p><img src="https://simg.open-open.com/show/0da9f906cc4ae48d574925f922eb99ad.png"></p> <p>相当多的类,不是吗?你通常从创建一个 <em>信息</em> 的 <em>密钥</em> 存储开始,然后配合一个随机数生成器建立 SSLContext。这可以用于工厂模式,用来创建你的 Socket。老实说,听起来并不难,不过我们来看看实现呢 —— 用 Java ...</p> <h3>使用 Java 设置 TLS 连接</h3> <p>我需要100多行代码来做到这一点。它展示了一个函数,可用于连接到具有可选相互身份验证的TLS服务器,如果这是双方的需要,客户端和服务器都需要彼此信任。</p> <p>JSSE Java:</p> <pre> <code class="language-kotlin">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(); }</code></pre> <p>好的,这是Java,对吧?嗯,代码相当的冗长 - 有许多被检查的异常和资源被处理,为简洁起见,我已经在这里简化了。</p> <p>下一步,我们将这些代码转换成简明的Kotlin代码,然后为愿意建立TLS连接的客户端提供DSL。</p> <h3>使用 Kotlin 设置 TLS 连接</h3> <p>Kotlin 的 SSLSocketFactory:</p> <pre> <code class="language-kotlin">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) }</code></pre> <p>您可能会注意到,我没有在这里进行一对一转换,这是因为在Kotlin的stdlib中提供了一些函数,这在许多情况下有很多帮助。这一小段源代码包含四种apply的用法,一种利用 <a href="/misc/goto?guid=4959754796956821251" rel="nofollow,noindex">扩展函数对象</a> 的方法。它允许我们通过在创建时传递上下文给它的lambda的语句块内重用,就像DSL一样,我们将在稍后看到。</p> <p>被apply的对象成为函数的receiver,然后可以通过这个receiver在lambda中使用,即可以调用成员,而不需要任何额外的前缀。如果仍然不明白,可以看看我的博客文章关于这些扩展函数对象的部分。</p> <p>我们已经看到,Kotlin可以比Java更简洁,但这是常识。我们现在想把这个代码包装在一个DSL中,然后客户端可以用它来进行TSL连接。</p> <h3>使用 Kotlin 创建 DSL</h3> <p>在创建 API 时要考虑的第一件事 —— 这也适用于 DSL,即客户端会被问到的:我们需要哪些配置参数。</p> <p>在我们的例子中,这是非常简单的。我们需要分别为 <em>keystore</em> 和 <em>truststore </em> 提供零个或一个描述。另外,重要的是要知道接受的密码套件和套接字链接超时。最后同样重要的是,必须要为我们的连接提供一组协议,例如 <em>TLSv1.2</em> 。对于每一个配置的值,缺省值都是可用的,必要时将需要使用。</p> <p>这可以很容易地封装在配置类中,我们称之为 ProviderConfiguration ,因为它稍后将会配置在我们的 TLSSocketFactoryProvider 中。</p> <p>配置</p> <p>DSL 配置类:</p> <pre> <code class="language-kotlin">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() } }</code></pre> <p>这里有三个可空属性,默认情况下,它们都为 null ,因为客户端可能不希望配置连接的所有内容。这里的重要方法是 sockets , keyManager , 和 trustManager ,它们拥有一个带有函数类型的参数。第一个 SocketConfiguration 是通过定义一个 <em>receiver </em> 的函数显式声明。这使得客户端可以传入一个 lambda 以访问 SocketConfiguration 中的所有成员,正如我们从扩展函数知道的这一点。</p> <p>socket 方法通过创建一个新的实例来提供 receiver,然后通过 apply 来调用传递的函数。然后将生成的配置实例用作内部属性的值。另外两个函数比较简单,因为它们定义了简单的函数类型,没有 receiver。他们只是期望一个函数被传递,返回一个 Store 的一个实例,然后被置于内部属性上。</p> <p>现在再来看看 Store 和 SocketConfiguration 类。</p> <p>DSL 配置类(2):</p> <pre> <code class="language-kotlin">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 } }</code></pre> <p>第一个类是一个简单的数据类,而且属性又是可空的。 Store 有点独特,因为它只定义了三个 infix 函数,实际上这上是属性的简单设置器。我们在这里使用 apply ,因为它之后会返回应用的对象。这使我们能够轻松地链接到设置器。目前尚未提及的一件事是 ProviderConfiguration 中的函数 open(name: String) 。很快就会看到这可以用作 Store 的工厂。这一切都结合在一起,可以定义我们的配置。但是在这之前,可以先看看客户端,先来看一下 TLSSocketFactoryProvider ,它需要配置我们刚刚看到的类。</p> <p>DSL 核心类</p> <p><em>TLSSocketFactoryProvider </em></p> <pre> <code class="language-kotlin">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 } }</code></pre> <p>这个类也不难理解,它的大部分内容都不显示在这里,因为我们已经从使用 Kotlin 的 <a href="/misc/goto?guid=4959754797568687197" rel="nofollow,noindex">SSLSocketFactory</a> 已获知,特别是 createSSLContext 。</p> <p>这个列表中最重要的是构造函数。它期望一个具有 ProviderConfiguration 的函数对象作为 receiver。在内部,它创建一个新的实例,并调用此函数来初始化配置。该配置用于 TLSSocketFactoryProvider 的其他函数,一旦调用了一个公共方法,即分别是 createSocketFactory 和 createServerSocketFactory ,就可以设置 SocketFactory 。</p> <p>为了将这些组合在一起,必须创建一个顶级函数,这将是客户端与 DSL 的接入点。</p> <p> </p> <p>来自:https://www.oschina.net/translate/creating-dsl-with-kotlin</p> <p> </p>