领先一步
VMware 提供培训和认证,助您加速进步。
了解更多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的特性
this引用——接收者——可以指向框架选择的任意上下文对象。所以,我们不必写成{ context -> context.a() }这样的形式,而可以简单地写成{ a() }。在这篇博文中,我想介绍一些Spring领域广阔而精彩的世界中的DSL示例,重点介绍我最喜欢的一些(但不是全部!)DSL。如果您想在家跟着做,所有这些示例的代码以及相应的Kotlin语言Gradle构建文件都在这里。请检查dsls文件夹以获取我们将在本篇博文中介绍的示例。
我们开始吧。
我们在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),而是使用runApplication。runApplication的最后一个参数是一个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的@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>一样。不错!
协程是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 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存储库,一个实体等等。我们甚至使用了Spring的一个著名\*Template变体!唯一特殊的是find()调用中的查询,我们在其中说Customer::name isEqualTo "B"。
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模块之一。它使处理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。尝试在浏览器中运行它,因为我认为在没有任何参数的情况下,默认响应是一些二进制数据——一张图片。
我只介绍了一些Spring和整个产品组合中存在的很棒的DSL,这些DSL提供了新的类型来执行与Java DSL中可能实现相同的操作。还有大量现有的库,我们为它们编写了扩展函数——基本上是在旧结构上添加了新的样式,使其更符合Kotlin开发者的习惯。我最喜欢的例子是JdbcTemplate,它已经存在了20多年,但感觉它就像是为了Kotlin而编写的!
一如既往,您可以从查看Spring Initializr开始。确保选择Kotlin作为您的语言。您甚至可以要求使用Kotlin语言的Gradle构建!
有许多很棒的(而且大多是免费的)资源,包括指南——它们提供以文本为中心的演练,以及Spring Academy(视频指导的演练,甚至还提供认证路径!)来介绍本博文中介绍的各种API和项目,尽管是用Java编写的。Kotlin本身是一种不错的语言,而且很容易学习。我在我的频道上有很多内容介绍Kotlin(和其他东西)。
当然,如果您负担得起,我们将在八月份在拉斯维加斯举办大型旗舰活动——SpringOne@VMWare Explore。来参加吧。论文征集(CFP)在三月底之前都开放,所以请随时提交。我们很乐意能在拉斯维加斯见到您!