领先一步
VMware 提供培训和认证,以加快您的进步。
了解更多更新:另请参阅 Spring Fu 实验项目。
自从我们最初宣布(社区反响非常好!)Spring Framework 5 中正式支持 Kotlin 以来,我们一直在努力增强对 Kotlin 的支持,并结合 Spring WebFlux 中的最新改进。
为了演示这些功能以及如何将它们结合使用,我创建了一个新的 spring-kotlin-functional 演示应用程序,它是一个独立的 Spring WebFlux 应用程序,使用 Kotlin 开发,具有 Mustache 模板渲染、JSON REST Web 服务和服务器发送事件流功能。请不要犹豫,在预计于 9 月发布的 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 代理(因此 kotlin-spring
插件对于响应式应用程序不需要),并且非常适合 Java 8 或 Kotlin 等语言。您可以此处查看 Java 与 Kotlin 语法的概述。
在 spring-kotlin-functional 中,Bean 在包含 Bean 定义的 Beans.kt
文件中声明。DSL 在概念上通过一个简洁的声明式 API 声明一个 Consumer<GenericApplicationContext>
,它允许您处理配置文件和 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
注释声明非空是默认行为,并且我们在特定参数或返回值可以为 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-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 内联类型参数 的众所周知的类型擦除问题。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 创建服务器发送事件端点以及服务器端模板渲染(在此应用程序中为 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://127.0.0.1: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()
}
}
##结论
我们期待您对这些新功能的反馈!请注意,8 月份是我们改进 API 的最后机会,因为 最终的 Spring Framework 5.0 发布候选版本 预计将在月底发布。因此,请随时使用 spring-kotlin-functional,对其进行分叉,添加诸如 Spring Data Reactive Fluent API 等新功能。
在我们这边,我们现在正在处理文档。
祝您夏季编码愉快 ;-)