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 社群所有即將舉行的活動。

檢視所有