Springdom 世界中的 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,以便 lambda 的 this 參考(即接收者)可以指向框架選擇的任意上下文物件。因此,我們可以只寫 { a() },而不是讓所有 DSL 都像這樣:{ context -> context.a() }
  • 擴充函式是一種型別安全的方式,可以在不更改這些型別的原始碼的情況下,將新函式新增到現有型別。這表示在 Java 中以一種方式工作的型別可以在 Kotlin 中具有替代的擴充行為。

在本部落格中,我想介紹 Springdom 廣闊而精彩世界中的一些 DSL 範例,重點介紹一些(但不是全部!)我最喜歡的 DSL。如果您想在家中一起學習,此處提供所有這些範例的程式碼以及相應的 Kotlin 語言 Gradle 建置檔案。檢查 dsls 資料夾以取得我們將在本部落格中查看的範例。

讓我們深入了解。

Spring Framework 函式 bean 註冊

我們早在 2017 年就在 Spring Framework 5.0 中引入了函式 bean 註冊。這是一種在 ApplicationContextInitializer 中以程式設計方式向 Spring Framework 註冊 bean 的方法。它繞過了 Javca 配置所需的一些反射和元件掃描。我們非常喜歡這種方法,而且實際上,當您使用 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 MVC Sprihng 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> 一樣。太棒了!

協程

協程是在 Kotklin 中描述可擴展、並行程式碼的最強大方法之一,而不會使用一連串的呼叫(類似於 Javascript 中的 Promise 或 Reactor 中的 Publisher<T>s)、回呼等來混淆程式碼。如果您在 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,如果執行函式 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 中,可以在不使用 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 專案之一,它提供了一種適合用途的方式來描述整合管線 - 我們稱它們為流程 - 來處理事件(我們將它們建模為 Message<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}")
                    }
                }
            }
        })
    }
}

這個 inbound 流程對您來說有意義嗎?它表示:每隔 1000 毫秒(一秒)掃描目錄(我電腦的 $HOME/Desktop/in 資料夾),當檢測到新的 java.io.File 時,將其傳遞給 transform 操作,該操作會將 File 轉換為 String。然後將 String 傳送到下一個 transform 操作,該操作會將文字轉換為大寫。然後將該大寫文字傳送到最後一個操作 handle,我在其中印出大寫文字。

使用 Spring Cloud Gateway 輕鬆建立微代理

Spring Cloud Gateway 是我最喜歡的 Spring Cloud 模組之一。它可以輕鬆地處理 HTTP 和服務層級的橫切關注點。它還整合了 GRPC 和 websockets 之類的東西。它很容易理解:您可以使用 RouteLocatorBuilder 來定義 routes,這些 routes 具有匹配傳入請求的述詞 (predicates)。如果它們匹配,您可以在將請求傳送到您指定的最終 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,它們提供了新的類型來執行與 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 社群中所有即將舉行的活動。

查看全部