提升自己
VMware 提供培训和认证,助你快速提升。
了解更多Kotlin 是一种优美的语言,仅凭其语法本身,就可以轻松地将旧的 Java 库变得更加简洁。然而,它在编写 DSL 时尤其出彩。
内部消息:Spring 团队竭尽全力保持凝聚力,围绕核心主题进行对齐,并使 Spring 优于其各部分的总和。这在每个主要版本中都有体现:Spring Framework 2.0 中的 XML 命名空间,3.0 中的 Java Config,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 变得容易
this
引用——即接收者——可以指向框架选择的任意上下文对象。因此,DSLs 不必都写成这样:{ context -> context.a() }
,我们可以直接写成 { a() }
。在这篇博客中,我想介绍 Spring 生态系统(Springdom)这个广阔而精彩的世界中的一些 DSL 示例,重点介绍一些(但不是全部!)我最喜欢的 DSL。如果你想在家中跟着操作,所有这些示例的代码以及相应的 Kotlin 语言 Gradle 构建文件在这里。请查看 dsls
文件夹以获取我们将在本博客中看到的示例。
让我们直接深入了解吧。
我们在 2017 年的 Spring Framework 5.0 中引入了函数式 bean 注册。这是一种在 ApplicationContextInitializer
中以编程方式向 Spring Framework 注册 bean 的方式。它避免了 Java 配置所需的一些反射和组件扫描。我们非常喜欢这种方法,事实上,当你使用 Spring 的 GraalVM native image 支持时,我们会将你的 @Configuration
Java 配置类某种程度上转译成函数式 bean 注册,然后再将整体提交给 GraalVM native image 编译器。这是一种不错的 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)
,而是使用 runApplication
。runApplication
的最后一个参数是一个 lambda,它的接收者是对调用 SpringApplication#run
时创建的 GenericApplicationContext
的引用。这给了我们一个机会来后处理 GenericApplicationContext
并调用 addInitializers
。
然后,我们使用方便的 beans
DSL,而不是自己编写 ApplicationContextInitializer<GenericApplicationContext>
的实现。
我们还可以使用 ref
方法和 bean 类型的具体化泛型来查找并注入另一个 bean(类型为 javax.sql.DataSource
)。
请记住,Spring 不在意你如何提供你的 bean 定义:使用 XML、Java Configuration、组件扫描、函数式 bean 注册等,Spring 都能正常工作。当然,你也可以在 Java 或 Kotlin 的示例应用中看到所有这些方法。但是,再说一次,这不重要:它们最终都会被规范化为 BeanDefinition
,然后连接在一起形成最终运行的应用。所以你可以混合使用。我经常这么做!
大家都知道 Spring 的 @Controller
抽象。不过,许多其他框架支持一种替代语法,类似于 Ruby 的 Sinatra,其中 lambda 与描述如何匹配传入请求的谓词相关联。Spring 最终在 Spring Framework 5 中引入了这种语法。Java 中的 DSL 很简洁,但在 Kotlin 中更令人称赞。这种函数式端点风格在 Spring MVC 和 Spring 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>
一样。很不错!
协程是描述可伸缩、并发代码的最强大方式之一,而不会让代码因调用链(如 Javascript 中的 Promises 或 Reactor 中的 Publisher<T>
)或回调等变得混乱。如果你正在使用 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 服务器或底层数据库请求的数据的 socket 中没有可用数据,但读取该数据的线程并没有等待。该线程可以自由地在栈的其余部分中重用,从而实现更高的可伸缩性。我们所要做的就是切换到 CoroutineCrudRepository
,并且——如果使用函数式 HTTP 端点——确保我们启用了 coRouter
而不是 router
。魔法。美味的魔法。但无论如何都是魔法。“我简直不敢相信这不是阻塞式命令式低效代码!”——Fabio
这个例子介绍了自定义的 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,所以需要返回一个值。
非常方便!
无数第三方数据访问库都附带一个注解处理器,该处理器执行代码生成,以便你可以以类型安全的方式访问你的领域模型,并由编译器保证检查。在 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 repository、一个 entity 等。我们甚至使用了 Spring 著名的 \*Template
变体之一!这里唯一例外的是 find()
调用中的查询,我们写的是 Customer::name isEqualTo "B"
。
Spring Integration 是最古老的 Spring 项目之一,它提供了一种恰如其分的方式来描述集成管道——我们称之为流(flows)——用于处理事件(我们将其建模为 Message<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 模块之一。它使得在 HTTP 和服务层面处理横切关注点变得轻而易举。它还集成了 GRPC 和 websockets 等功能。它很容易理解:你使用 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
。在浏览器中尝试一下,因为我认为没有任何参数的默认响应是一些二进制数据——一张图片。
我只涉及了 Spring 以及整个产品组合中一些出色的 DSL,它们提供了新的类型来完成与 Java DSL 中相同的事情。还有大量现有的库,我们为其编写了扩展函数——本质上是在旧结构上添加新涂层,使其更符合 Kotlin 开发者的习惯用法。我最喜欢的例子是 JdbcTemplate
,它以某种形式存在了 20 多年,但感觉就像是昨天刚为 Kotlin 编写的一样!
你可以像往常一样,通过访问Spring Initializer 来开始。请确保选择 Kotlin
作为你的语言。你甚至可以选择 Kotlin 语言的 Gradle 构建文件!
有很多很棒的(且大部分是免费的)资源,包括指南——提供以文本为主导的讲解,以及 Spring Academy(提供视频指导讲解,甚至提供认证途径!)介绍了我们在本博客中介绍的各种 API 和项目,尽管是以 Java 为主。Kotlin 本身是一门不错的语言,也很容易学习。我在我的频道上有很多关于Kotlin(以及其他内容)的视频。
当然,如果你负担得起,我们将在今年八月在拉斯维加斯举办我们重要的支柱活动,SpringOne@VMWare Explore。欢迎参加。CFP 也开放到三月底,所以请随时提交。我们期待在拉斯维加斯见到你!