如果你看过我最近发表关于 Kotlin 的文章,你可能会注意到我曾经提到过 DSL(Domain Specific Languages,领域专用语言)。Kotlin 是一门提供了强大特性支持 DSL 的编程语言。这些特性中,我曾经介绍过具有接收者的函数字面量(Function Literals with Receiver),以及调用约定和中缀表达式。
这篇文章中,我们会看到 DSL 的概念,当然还有如何使用 Kotlin 创建一个相对简单的 DSL 示例。
举例来说,我常常在需要 HTTPS 通讯的情况下,艰难地使用 Java API 建立 SSL/TLS 连接。就在最近,我还不得不在我们的应用程序中实现了一个不同类型的 SSL/TLS。为了做这个事情,我再次想写一个小型库来支持类似的任务,以样板的方式避开所有困难。
领域专用语言这个术语现在使用得非常广泛,但就所要谈论的情况而言,它指的是某种“微型语言”。它以半陈述的方式描述特定领域对象的构造。用于创建 XML、HTML 或 UI 数据的 Groovy builders 就是一个例子。在我看来,最好的例子是 Gradle,它也是使用基于 Grovvy 的 DSL 来描述软件构建自动化。(顺便提一下,还有一个 Gradle-Script-Kotlin,是针对 Gradle 的 Kotlin DSL。)
把目标简化一下,DSL 是一种提供 API 的方式,这种 API 更清晰、更具可读性,最重要的是,它比传统 API 结构更明确。DSL 使用嵌入的描述而不是用一种命令的方式调用各个功能,这种方式会创建清晰的结构,我们甚至可以称之为“语法”。DSL 定义可以合并不同构造,应用于各个作用域,并在其中使用不同的功能。
大家都知道 Kotlin 是静态类型语言,它拥有像 Groovy 这样的动态类型语言所不具备的能力。最重要的是,静态类型允许在编译期检查错误,而且一般情况下会得到 IDE 更好的支持。
好了,别再浪费时间在理论上,我们来感受 DSL 的乐趣吧,有很多嵌入的 Lambda 哦!因此,你最好先搞懂如何在 Kotlin 中使用 Lambda!
本文的引言部分就说过我们会使用 Java API 建立 SSL/TLS 连接来作为示例。如果你对此并不熟悉,我们先来简单的介绍一下。
Java 安全套接字扩展 (Java Secure Socket Extension, JSSE) 是 Java SE 1.4 就引入的库,它提供通过 SSL/TLS 创建安全连接的功能,包括客户端/服务器认证、数据加密以及保证消息完整性。和许多其他人一样,我发现安全问题相当棘手,哪怕在日常工作中我们经常用到这些功能。原因之一可能就是需要组合大量 API。另一个原因建立这样的连接非常繁琐。来看看类层次结构:
相当多的类,不是吗?你通常从创建一个信息的密钥存储开始,然后配合一个随机数生成器建立 SSLContext。这可以用于工厂模式,用来创建你的 Socket。老实说,听起来并不难,不过我们来看看实现呢 —— 用 Java ...
我需要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 的 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连接。
在创建 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
,它需要配置我们刚刚看到的类。
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 的接入点。
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。 2KB翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。2KB项目(www.2kb.com,源码交易平台),提供担保交易、源码交易、虚拟商品、在家创业、在线创业、任务交易、网站设计、软件设计、网络兼职、站长交易、域名交易、链接买卖、网站交易、广告买卖、站长培训、建站美工等服务