Kotlin DSLs 在 Spring 生态系统中的应用

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

Kotlin 是一种优美的语言,它能让你仅凭 Kotlin 自身的语法就能轻松地将旧的 Java 库变得更加简洁。然而,当您编写 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可以这样编写,使得lambda的this引用——接收者——可以指向框架选择的任意上下文对象。所以,我们不必写成{ 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配置类转译(sort of)成函数式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方法和泛型的reified类型来查找和注入另一个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 MVCSpring 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中的Promise或Reactor中的Publisher<T>)调用链、回调或类似的东西弄乱代码。如果你正在使用Spring的响应式堆栈,那么你已经准备好使用协程了,因为我们已经努力使你在任何地方使用响应式类型的地方都可以await。你只需要亲眼看看才会相信。

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,如果进行函数式HTTP端点,请确保我们已经开启了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中,可以在除了Kotlin编译器和语言之外没有其他工具的情况下完成其中的大部分工作。

这是一个简单的例子,它将一些数据写入数据库,然后使用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>s)。这些管道可以有很多操作,每个操作都链接在一起。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和Service级别的横切关注点变得非常容易。它还集成了gRPC和websocket等功能。它很容易理解:你使用RouteLocatorBuilder来定义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 Initializr开始。确保选择Kotlin作为您的语言。您甚至可以要求使用Kotlin语言的Gradle构建!

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

当然,如果您负担得起,我们将在八月份在拉斯维加斯举办大型旗舰活动——SpringOne@VMWare Explore。来参加吧。论文征集(CFP)在三月底之前都开放,所以请随时提交。我们很乐意能在拉斯维加斯见到您!

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

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

了解更多

获得支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一份简单的订阅。

了解更多

即将举行的活动

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

查看所有