提升自己
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 也開放到三月底,所以請隨時提交。我們期待在拉斯維加斯見到你!