Kotlin 中的领域特定语言

ElijahBolt 6年前
   <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>