Spring 生态系统中的 Kotlin DSL

工程 | Josh Long | 2023年3月16日 | ...

Kotlin 是一种美丽的语言,它使得采用旧的 Java 库并使其更加简洁变得微不足道,这仅仅是由于 Kotlin 语法本身的优势。然而,当您编写 DSL 时,它会更加闪耀。

这里有一些内部信息供您参考:Spring 团队尽其所能保持一致性、在核心主题上保持一致,并使 Spring 变得比其各个部分的总和更好。您在每个主要版本中都能看到这一点:Spring Framework 2.0 中的 XML 命名空间。3.0 中的 Java 配置。Spring Boot 1.0 首次发布时(与 Spring Framework 4.0 同时发布)的条件和自动配置。Spring Framework 5.0 中的响应式编程。当然还有 Spring Framework 6.0 中的提前编译。并且每当 Java 或 Jakarta EE 等平台规范的基本版本更改时,所有基于相应 Spring Framework 版本构建的项目的最低要求也会随之更改。但 Kotlin 并非如此。它是那些有机增长的东西之一。没有来自上层的指令。它起源于 Spring Framework,不同的团队在看到机会时,就会在他们能够的时候为各自的项目添加适当的支持,通常与社区一起进行。Kotlin 太棒了。

Kotlin 具有几个使其易于构建 DSL 的特性

  • 接受 lambda 的函数可以函数调用的括号之外接受 lambda
  • 如果函数期望的唯一参数恰好是 lambda,则根本不需要指定括号
  • DSL 可以编写为使this引用(lambda 的接收者)指向框架选择的任意上下文对象。因此,与其让所有 DSL 都像这样:{ context -> context.a() },我们反而可以简单地编写{ a() }
  • 扩展函数是在不更改这些类型的源代码的情况下向现有类型添加新函数的类型安全方法。这意味着在 Java 中以一种方式工作的类型可以在 Kotlin 中具有替代的扩展行为。

在本博文中,我想介绍一些在广阔而精彩的 Spring 生态系统中 DSL 的示例,重点介绍一些(但并非全部!)我最喜欢的 DSL。如果您想在家中跟随操作,所有这些示例以及相应的 Kotlin 语言 Gradle 构建文件的代码在这里。检查dsls文件夹以获取我们将在本博文中查看的示例。

让我们直接深入。

Spring Framework 函数式 Bean 注册

早在 2017 年的 Spring Framework 5.0 中,我们就引入了函数式 Bean 注册。这是一种在ApplicationContextInitializer中以编程方式向 Spring Framework 注册 Bean 的方法。它避开了 Java 配置所需的某些反射和组件扫描。我们非常喜欢这种方法,事实上,当您使用 Spring 的 GraalVM 原生镜像支持时,我们会转译(某种程度上)您的@Configuration Java 配置类为函数式 Bean 注册,然后再将整个内容提供给 GraalVM 原生镜像编译器。这是一个不错的 DSL,但我喜欢它在使用 Kotlin 时如何整合在一起。在示例代码中我没有单独的示例,但在大多数示例中,我使用了函数式风格,因此我想先将其说明清楚

package com.example.beans

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.support.beans
import org.springframework.web.servlet.function.ServerResponse
import org.springframework.web.servlet.function.router

@SpringBootApplication
class FunctionalBeanRegistrationApplication

fun main(args: Array<String>) {
    runApplication<FunctionalBeanRegistrationApplication>(*args) {
        addInitializers(beans {
            bean {
            	val db = ref<javax.sql.DataSource>()
                CustomerService(db)
            }
        })
    }
}

那里还有其他一些优点:请注意,当使用 Spring Boot 时,您没有使用普通的SpringApplication.run(Class, String[] args),而是使用runApplicationrunApplication的最后一个参数是一个 lambda,其接收者是对调用SpringApplication#run时创建的GenericApplicationContext的引用。这使我们有机会对GenericApplicationContext进行后处理并调用addInitializers

然后,我们使用方便的beans DSL,而不是自己编写ApplicationContextInitializer<GenericApplicationContext>的实现。

我们还可以使用ref方法和 Bean 类型的具现化泛型来查找和注入另一个 Bean(类型为javax.sql.DataSource)。

请记住,Spring 不在乎您如何提供 Bean 定义:使用 XML、Java 配置、组件扫描、函数式 Bean 注册等,Spring 都很乐意。当然,您也可以从 Java 或 Kotlin 中查看示例应用程序中的所有这些内容。但是,同样,这并不重要:它们最终都成为规范化的BeanDefinition,然后这些定义被连接在一起,形成最终运行的应用程序。因此,您可以混合搭配。我经常这样做!

使用 Spring MVC 和 Spring Webflux 的函数式 HTTP 端点

每个人都知道 Spring 的@Controller抽象。但是,许多其他框架支持替代语法,类似于 Ruby 的 Sinatra,其中 lambda 与描述如何匹配传入请求的谓词相关联。Spring 最终在 Spring Framework 5 中获得了一个。Java 中的 DSL 简洁明了,但在 Kotlin 中则更加令人钦佩。这种函数式端点风格同时适用于 Spring MVCSprihng Webflux。但是,MVC 实现出现较晚,因此有些人可能还没有尝试过。

package com.example.fnmvc

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.support.beans
import org.springframework.web.servlet.function.ServerResponse
import org.springframework.web.servlet.function.router

@SpringBootApplication
class FnMvcApplication

fun main(args: Array<String>) {
    runApplication<FnMvcApplication>(*args) {
        addInitializers(beans {
            bean {
                router {
                    GET("/hello") {
                        ServerResponse.ok().body(mapOf("greeting" to "Hello, world!"))
                    }
                }
            }
        })
    }
}

非常简单:当收到 HTTP GET请求时,生成一个响应,在本例中为Map<String, String>。Spring MVC 将依次对其进行序列化,就像您从 Spring MVC @Controller处理程序方法返回Map<String, String>一样。不错!

协程

协程是在 Kotlin 中描述可扩展、并发代码的最强大方法之一,无需使用调用链(如 Javascript 中的 Promises 或 Reactor 中的Publisher<T>s)或回调等来使代码变得混乱。如果您在 Spring 中使用响应式堆栈,那么您已经可以开始使用协程了,因为我们已经努力确保在您原本使用响应式类型的所有地方也可以使用await-ed。您只需要亲眼见证才能相信。

package bootiful.reactive

import kotlinx.coroutines.flow.Flow
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.support.beans
import org.springframework.data.annotation.Id
import org.springframework.data.repository.kotlin.CoroutineCrudRepository
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.reactive.function.server.ServerResponse
import org.springframework.web.reactive.function.server.bodyAndAwait
import org.springframework.web.reactive.function.server.coRouter

@SpringBootApplication
class ReactiveApplication

fun main(args: Array<String>) {
    runApplication<ReactiveApplication>(*args) {
        addInitializers(beans {
            bean {
                val repo = ref<CustomerRepository>()
                coRouter {
                    GET("/customers") {
                        val customers : Flow<Customer> = repo.findAll()
                        ServerResponse.ok().bodyAndAwait(customers)
                    }
                }
            }
        })
    }
}

@RestController
class CustomerHttpController(private val repo: CustomerRepository) {

    @GetMapping("/customers/{id}")
    suspend fun customersById(@PathVariable id: Int): Customer {
        val customer:Customer = this.repo.findById(id) !!
        println("the id is ${customer.id} and the name is ${customer.name}")
        return customer
    }
}

data class Customer(@Id val id: Int, val name: String)

interface CustomerRepository : CoroutineCrudRepository<Customer, Int>

我希望代码看起来非常简单,但在幕后,库和 Kotlin 运行时正在执行一种特殊的魔法,这意味着,虽然从套接字返回从 http 服务器或底层数据库请求的数据时没有可用数据,但读取这些数据的线程并没有等待它。该线程可以自由地在堆栈的其余部分中重用,从而获得更大的可扩展性。我们所要做的就是切换到CoroutineCrudRepository,并且 - 如果使用函数式 HTP 端点 - 确保我们启用了coRouter而不是router。魔法。美味的魔法。但无论如何都是魔法。“我无法相信它不是阻塞式的命令式低效代码!” - Fabio

Spring Security

此示例查看自定义 Spring Security DSL。

package com.example.security

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.support.beans
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.invoke
import org.springframework.security.core.userdetails.User
import org.springframework.security.provisioning.InMemoryUserDetailsManager
import org.springframework.web.servlet.function.ServerResponse
import org.springframework.web.servlet.function.router

@SpringBootApplication
@EnableWebSecurity
class SecurityApplication

fun main(args: Array<String>) {
    runApplication<SecurityApplication>(*args) {
        addInitializers(beans {
            bean {
                val http = ref<HttpSecurity>()
                http {
                    httpBasic {}
                    authorizeRequests {
                        authorize("/hello/**", hasAuthority("ROLE_ADMIN"))
                    }
                }
                .run { http.build() }
            }

            bean {
                InMemoryUserDetailsManager(
                    User.withDefaultPasswordEncoder()
                        .username("user")
                        .password("password")
                        .roles("ADMIN")
                        .build()
                )
            }

            bean {
                router {
                    GET("/hello") {
                        ServerResponse.ok().body(mapOf("greeting" to "Hello, world!"))
                    }
                }
            }
        })
    }
}

该示例使用函数式 Bean 注册。大部分内容都很熟悉。可能新颖的是,我们正在使用注入的HttpSecurity引用并隐式调用一个扩展方法invoke,该方法为我们提供了一个 DSL,我们可以在其中配置诸如我们想要 HTTP BASIC、我们想要授权特定端点等内容。我们正在定义一个 Bean,因此我们需要返回一个值。

非常方便!

Spring Data MongoDB 类型安全查询

无数的第三方数据访问库都附带一个注释处理器,该处理器执行代码生成,以便您可以以类型安全的方式访问您的域模型,并由编译器保证检查。

这是一个简单的示例,它将一些数据写入数据库,然后使用 Kotlin 的字段引用机制查询它。

package com.example.mongodb

import org.springframework.boot.ApplicationRunner
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.annotation.Id
import org.springframework.data.mongodb.core.MongoOperations
import org.springframework.data.mongodb.core.find
import org.springframework.data.mongodb.core.query.Query
import org.springframework.data.mongodb.core.query.isEqualTo
import org.springframework.data.repository.CrudRepository

@SpringBootApplication
class MongodbApplication

fun main(args: Array<String>) {
    runApplication<MongodbApplication>(*args)
}

@Configuration
class TypeSafeQueryExampleConfiguration {

    @Bean
    fun runner(cr: CustomerRepository, mongoOperations: MongoOperations) = ApplicationRunner {
        cr.deleteAll()
        cr.save(Customer(null, "A"))
        cr.save(Customer(null, "B"))
        cr.findAll().forEach {
            println(it)
        }
        val customers: List<Customer> = mongoOperations.find<Customer>(
            Query(Customer::name isEqualTo "B")
        )
        println(customers)
    }
}

data class Customer(@Id val id: String?, val name: String)

interface CustomerRepository : CrudRepository<Customer, String>

否则这是一个典型的应用程序:我们有一个 Spring Data 存储库、一个实体等。我们甚至使用 Spring 的众所周知的\*Template变体之一!这里唯一特殊的地方是find()调用中的查询,我们在其中说Customer::name isEqualTo "B"

随 Spring Integration 的流程一起走

Spring Integration 是 Spring 最古老的项目之一,它提供了一种适合目的的方式来描述集成管道 - 我们称之为流程 - 以对事件进行操作(我们将它们建模为Mesasage<T>)。这些管道可以包含许多操作,每个操作都链接在一起。Spring Integration 提供了一个可爱的IntegrationFlow DSL,它使用上下文对象来提供 DSL。但是,至少在 Kotlin 中表达时感觉更清晰。

package com.example.integration

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.support.beans
import org.springframework.integration.dsl.integrationFlow
import org.springframework.integration.file.dsl.Files
import org.springframework.integration.file.transformer.FileToStringTransformer
import java.io.File

@SpringBootApplication
class IntegrationApplication

fun main(args: Array<String>) {
    runApplication<IntegrationApplication>(*args) {
        addInitializers(beans {
            bean {
                integrationFlow(
                    Files.inboundAdapter(File("/Users/jlong/Desktop/in")),
                    { poller { it.fixedDelay(1000) } }
                ) {
                    transform(FileToStringTransformer())
                    transform<String> { it.uppercase() }
                    handle {
                        println("new message: ${it.payload}")
                    }
                }
            }
        })
    }
}

这个入站流程对你来说有意义吗?它表示:每 1000 毫秒(一秒)扫描一次目录(我的电脑的$HOME/Desktop/in文件夹),并且当检测到新的java.io.File时,将其传递给transform操作,该操作会将File转换为String。然后,该String将发送到下一个transform操作,该操作会将文本转换为大写。然后,该大写文本将发送到最后一个操作handle,我将在其中打印出大写文本。

使用 Spring Cloud Gateway 轻松构建微代理

Spring Cloud Gateway 是我最喜欢的 Spring Cloud 模块之一。它使在 HTTP 和服务级别处理横切关注点变得微不足道。它还集成了 GRPC 和 Websocket 等功能。它很容易理解:您使用RouteLocatorBuilder定义routes,这些routes具有匹配传入请求的谓词。如果匹配,您可以在将请求发送到您指定的最终uri之前,对请求应用零个或多个过滤器。这是一个函数式管道,因此它在 Kotlin DSL 中表达得很好就不足为奇了。让我们看一个例子。


package com.example.gateway

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder
import org.springframework.cloud.gateway.route.builder.filters
import org.springframework.cloud.gateway.route.builder.routes
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpHeaders

@SpringBootApplication
class GatewayApplication

fun main(args: Array<String>) {
    runApplication<GatewayApplication>(*args)
}

@Configuration
class GatewayConfiguration {

    @Bean
    fun gateway(rlb: RouteLocatorBuilder) = rlb
        .routes {
            route {
                path("/proxy")
                filters {
                    setPath("/bin/astro.php")
                    addResponseHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*")
                }
                uri("https://www.7timer.info/")
            }
        }
}

此示例匹配绑定到localhost:8080/proxy的请求,并将请求转发到我在互联网上找到的一个随机开放的 HTTP Web 服务,该服务应该为您提供天气报告。我使用过滤器来增强响应,将自定义标头ACCESS_CONTROL_ALLOW_ORIGIN添加到响应中。在浏览器中尝试一下,因为我认为没有任何参数的默认响应是一些二进制数据 - 一张图片。

Kotlin 和 Spring 都是双赢

我仅仅触及了 Spring 及其产品组合中的一些很棒的 DSL,这些 DSL 提供了新的类型来执行与 Java DSL 中相同的功能。还存在大量现有的库,我们为它们编写了扩展函数 - 本质上是在旧结构上添加新的涂料,使它们更适合 Kotlin 开发人员。我最喜欢的例子是JdbcTemplate,它以某种形式存在了 20 多年,但感觉它是在昨天考虑到 Kotlin 而编写的!

像往常一样,您可以通过查看Spring Initializer开始。确保选择Kotlin作为您的语言。您甚至可以请求 Kotlin 语言的 Gradle 构建!

有很多很棒的(而且大部分是免费的)资源,包括指南 - 提供以文本为中心的演练,以及 Spring Academy(提供视频引导的演练,甚至提供认证路径!)介绍我们在本博文中介绍的各种 API 和项目,尽管是用 Java 编写的。Kotlin 本身是一门很棒的语言,而且很容易学习。在我的频道上,我有很多内容介绍Kotlin(和其他内容)

当然,如果您有资金,我们将在 8 月在拉斯维加斯举办我们的重要活动,SpringOne@VMWare Explore。加入我们。CFP 也开放至3 月底,所以请随时提交。我们很乐意在拉斯维加斯见到您!

获取 Spring 新闻

随时关注 Spring 新闻

订阅

领先一步

VMware 提供培训和认证,以加速您的进步。

了解更多

获取支持

Tanzu Spring 在一个简单的订阅中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

查看 Spring 社区中所有即将举行的活动。

查看全部