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("http://localhost: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 社区所有即将举行的活动。

查看全部