Spring Framework 5 Kotlin APIs, the functional way

工程 | Sébastien Deleuze | August 01, 2017 | ...

更新:另請參閱 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 新的 Kotlin DSL 定義函式式 Bean

Spring Framework 5 引入了一種使用 Lambda 表示式註冊 Bean 的新方法。這種方法非常高效,不需要任何反射或 CGLIB 代理(因此響應式應用程式不需要 kotlin-spring 外掛),並且與 Java 8 或 Kotlin 等語言非常契合。您可以在此處檢視 Java 與 Kotlin 語法的概述。

spring-kotlin-functional 中,Bean 在一個包含 Bean 定義的 Beans.kt 檔案中宣告。該 DSL 透過一個清晰的宣告式 API 在概念上聲明瞭一個 Consumer<GenericApplicationContext>,它允許您使用 profile 和 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) 的快捷方式。

Spring 和 Reactor API 的空安全

Kotlin 的一項關鍵特性是空安全,它允許在編譯時處理 null 值,而不是在執行時遇到臭名昭著的 NullPointerException。這透過清晰的空值宣告使您的應用程式更安全,無需付出包裝器(如 Optional)的代價即可表達“有值或無值”的語義。(Kotlin 允許對可空值使用函式式構造;請查閱這篇關於 Kotlin 空安全的綜合指南。)

雖然 Java 不允許在其型別系統中表達空安全,但我們透過對工具友好的註解為 Spring API 引入了一定程度的空安全:包級別的 @NonNullApi 註解宣告非空是預設行為,我們明確地在特定引數或返回值可能為 null 的地方使用了 @Nullable 註解。我們為整個 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 WebFlux 的 Kotlin DSL 進行函式式路由

spring-kotlin-functional 沒有使用 @RestController@RequestMapping,而是透過專門的 Kotlin DSL 使用 WebFlux 函式式 API。

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 響應的 Handler。這裡是 UserHandler,它利用了 Spring Framework 5 直接在 Spring JAR 中提供的 Kotlin 擴充套件功能,使用 Kotlin 具體化型別引數 (reified type parameters) 來避免眾所周知的型別擦除問題。相同的程式碼在 Java 中將需要額外的 ClassParameterizedTypeReference 引數。

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,對其進行分支,新增新功能,如 Spring Data Reactive Fluent API 等等。

在我們這邊,我們現在正在編寫文件。

愉快的夏日程式設計 ;-)

獲取 Spring 通訊

透過 Spring 通訊保持聯絡

訂閱

先行一步

VMware 提供培訓和認證,助您加速發展。

瞭解更多

獲得支援

Tanzu Spring 透過一個簡單的訂閱提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位制檔案。

瞭解更多

即將舉行的活動

檢視 Spring 社群所有即將舉行的活動。

檢視全部