領先一步
VMware 提供培訓和認證,助您加速進步。
瞭解更多更新:另請參閱 Spring Fu 實驗專案。
自我們最初發布(受到社群熱烈歡迎!)的 Spring Framework 5 的官方 Kotlin 支援以來,我們一直在與 Spring WebFlux 的最新改進相結合,繼續致力於更強大的 Kotlin 支援。
為了演示這些功能以及它們如何協同工作,我建立了一個新的 spring-kotlin-functional 演示應用程式。這是一個獨立的 Spring WebFlux 應用程式,使用 Kotlin 開發,支援 Mustache 模板渲染、JSON REST Web 服務和 Server-Sent Events 流式傳輸功能。在預計於九月釋出的 Spring Framework 5 正式版之前,請隨時傳送您的反饋和建議。
Spring WebFlux 和 Reactor Netty 支援應用程式的程式設計引導,因為它們天生就設計為作為嵌入式 Web 伺服器執行。這在開發 Spring Boot 應用程式時顯然不是必需的,但在微服務架構或其他受限環境中,對於具有自定義引導的緊湊部署單元可能非常有用。
class Application {
private val httpHandler: HttpHandler
private val server: HttpServer
private var nettyContext: BlockingNettyContext? = null
constructor(port: Int = 8080) {
val context = GenericApplicationContext().apply {
beans().initialize(this)
refresh()
}
server = HttpServer.create(port)
httpHandler = WebHttpHandlerBuilder.applicationContext(context).build()
}
fun start() {
nettyContext = server.start(ReactorHttpHandlerAdapter(httpHandler))
}
fun startAndAwait() {
server.startAndAwait(ReactorHttpHandlerAdapter(httpHandler),
{ nettyContext = it })
}
fun stop() {
nettyContext?.shutdown()
}
}
fun main(args: Array<String>) {
Application().startAndAwait()
}
Spring Framework 5 引入了一種使用 lambda 註冊 Bean 的新方法。它非常高效,不需要任何反射或 CGLIB 代理(因此對於 Reactive 應用 kotlin-spring 外掛不是必需的),並且非常適合 Java 8 或 Kotlin 等語言。您可以在 此處概覽 Java 與 Kotlin 語法的區別。
在 spring-kotlin-functional 中,Bean 在 Beans.kt 檔案中定義。該 DSL 在概念上透過一個乾淨的宣告式 API 宣告一個 Consumer<GenericApplicationContext>,該 API 允許您處理 profiles 和 Environment 來自定義 Bean 的註冊方式。該 DSL 還允許透過 if 表示式、for 迴圈或任何其他 Kotlin 結構進行自定義 Bean 註冊邏輯。
beans {
bean<UserHandler>()
bean<Routes>()
bean<WebHandler>("webHandler") {
RouterFunctions.toWebHandler(
ref<Routes>().router(),
HandlerStrategies.builder().viewResolver(ref()).build()
)
}
bean("messageSource") {
ReloadableResourceBundleMessageSource().apply {
setBasename("messages")
setDefaultEncoding("UTF-8")
}
}
bean {
val prefix = "classpath:/templates/"
val suffix = ".mustache"
val loader = MustacheResourceTemplateLoader(prefix, suffix)
MustacheViewResolver(Mustache.compiler().withLoader(loader)).apply {
setPrefix(prefix)
setSuffix(suffix)
}
}
profile("foo") {
bean<Foo>()
}
}
在此示例中,bean<Routes>() 使用建構函式自動裝配,而 ref<Routes>() 是 applicationContext.getBean(Routes::class.java) 的簡寫。
Kotlin 的關鍵特性之一是 空安全,它允許在編譯時處理 null 值,而不是在執行時遇到臭名昭著的 NullPointerException。這透過清晰的可空性宣告使應用程式更安全,以“有值或無值”的語義,而無需付出類似 Optional 的包裝成本。(Kotlin 允許使用函式式結構處理可空值;請檢視這個 全面的 Kotlin 空安全指南。)
儘管 Java 不允許在其型別系統中表達空安全,但我們透過對工具友好的註解在 Spring API 中引入了一定程度的空安全:包級別的 @NonNullApi 註解宣告非空是預設行為,並且我們明確地將 @Nullable 註解放在特定的引數或返回值可能為 null 的地方。我們已經為整個 Spring Framework API 完成了這項工作(是的,這是一項巨大的努力!),並且像 Spring Data 這樣的其他專案也開始利用它。Spring 註解被 JSR 305 元註解(一個不活躍的 JSR,但被 IDEA、Eclipse、Findbugs 等工具支援)元註解,以向 Java 開發人員提供有用的警告。
在 Kotlin 方面,殺手級特性是——自 Kotlin 1.1.51 釋出以來——這些註解 被 Kotlin 識別,以便為整個 Spring API 提供空安全。這意味著當您使用 Spring 5 和 Kotlin 時,您的程式碼中不應該有 NullPointerException,因為編譯器不允許這樣做。您需要使用 -Xjsr305=strict 編譯器標誌才能在 Kotlin 型別系統中考慮這些註解。
spring-kotlin-functional 使用 WebFlux 函式式 API 和專用的 Kotlin DSL,而不是使用 @RestController 和 @RequestMapping。
router {
accept(TEXT_HTML).nest {
GET("/") { ok().render("index") }
GET("/sse") { ok().render("sse") }
GET("/users", userHandler::findAllView)
}
"/api".nest {
accept(APPLICATION_JSON).nest {
GET("/users", userHandler::findAll)
}
accept(TEXT_EVENT_STREAM).nest {
GET("/users", userHandler::stream)
}
}
resources("/**", ClassPathResource("static/"))
}
與 Bean DSL 類似,函式式路由 DSL 允許基於自定義邏輯和動態資料進行函式式路由註冊(這對於開發 CMS 或電子商務解決方案可能很有用,因為大多數路由取決於通過後臺管理介面建立的資料)。
路由通常指向處理程式,這些處理程式負責根據 HTTP 請求建立 HTTP 響應,透過可呼叫引用。這是 UserHandler,它利用 Spring Framework 5 在 Spring JAR 中直接提供的 Kotlin 擴充套件,透過 Kotlin reified 型別引數來避免眾所周知的型別擦除問題。在 Java 中相同的程式碼將需要額外的 Class 或 ParameterizedTypeReference 引數。
class UserHandler {
private val users = Flux.just(
User("Foo", "Foo", LocalDate.now().minusDays(1)),
User("Bar", "Bar", LocalDate.now().minusDays(10)),
User("Baz", "Baz", LocalDate.now().minusDays(100)))
private val userStream = Flux
.zip(Flux.interval(ofMillis(100)), users.repeat())
.map { it.t2 }
fun findAll(req: ServerRequest) =
ok().body(users)
fun findAllView(req: ServerRequest) =
ok().render("users", mapOf("users" to users.map { it.toDto() }))
fun stream(req: ServerRequest) =
ok().bodyToServerSentEvents(userStream)
}
請注意,使用 Spring WebFlux 可以輕鬆建立 Server-Sent Events 端點,以及伺服器端模板渲染(在此應用程式中為 Mustache)。
## 使用 WebClient、Reactor Test 和 JUnit 5 進行輕鬆測試
Kotlin 允許在反引號之間指定有意義的測試函式名稱,並且自 JUnit 5.0 RC2 起,Kotlin 測試類可以使用 @TestInstance(TestInstance.Lifecycle.PER_CLASS) 來啟用測試類的單例例項化,這允許在非靜態方法上使用 @BeforeAll 和 @AfterAll 註解,這非常適合 Kotlin。現在還可以透過帶有 junit.jupiter.testinstance.lifecycle.default = per_class 屬性的 junit-platform.properties 檔案來更改預設行為為 PER_CLASS。
class IntegrationTests {
val application = Application(8181)
val client = WebClient.create("https://:8181")
@BeforeAll
fun beforeAll() {
application.start()
}
@Test
fun `Find all users on JSON REST endpoint`() {
client.get().uri("/api/users")
.accept(APPLICATION_JSON)
.retrieve()
.bodyToFlux<User>()
.test()
.expectNextMatches { it.firstName == "Foo" }
.expectNextMatches { it.firstName == "Bar" }
.expectNextMatches { it.firstName == "Baz" }
.verifyComplete()
}
@Test
fun `Find all users on HTML page`() {
client.get().uri("/users")
.accept(TEXT_HTML)
.retrieve()
.bodyToMono<String>()
.test()
.expectNextMatches { it.contains("Foo") }
.verifyComplete()
}
@Test
fun `Receive a stream of users via Server-Sent-Events`() {
client.get().uri("/api/users")
.accept(TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux<User>()
.test()
.expectNextMatches { it.firstName == "Foo" }
.expectNextMatches { it.firstName == "Bar" }
.expectNextMatches { it.firstName == "Baz" }
.expectNextMatches { it.firstName == "Foo" }
.expectNextMatches { it.firstName == "Bar" }
.expectNextMatches { it.firstName == "Baz" }
.thenCancel()
.verify()
}
@AfterAll
fun afterAll() {
application.stop()
}
}
## 結論
我們期待您對這些新功能的反饋!請注意,八月是我們完善 API 的最後機會,因為 Spring Framework 5.0 的最終候選版本預計將於月底釋出。因此,請隨時嘗試 spring-kotlin-functional,fork 它,新增新功能,如 Spring Data Reactive Fluent API 等。
我們這邊正在著手編寫文件。
祝您編碼愉快 ;-)