对于最新的稳定版本,请使用 Spring Framework 7.0.6!spring-doc.cadn.net.cn

使用 Kotlin 的 Spring 项目

本节提供了一些针对使用 Kotlin 开发 Spring 项目的具体提示和建议。spring-doc.cadn.net.cn

默认为 final

默认情况下,Kotlin中的所有类都是finalopen修饰符用于类时,与Java中的final相反:它允许其他人继承此类。 这一点也适用于成员函数,即它们需要被标记为open才能被覆盖。spring-doc.cadn.net.cn

虽然 Kotlin 的 JVM 友好设计通常与 Spring 无缝配合,但如果未考虑到这一事实,此特定的 Kotlin 功能可能会阻止应用程序启动。这是因为 Spring Bean(例如默认需要在运行时扩展的 @Configuration 注解类)通常由 CGLIB 进行代理。解决方法是在每个由 CGLIB 代理的 Spring Bean 的类和成员函数上添加 open 关键字,这可能会变得很麻烦,并且违背了 Kotlin 保持代码简洁和可预测的原则。spring-doc.cadn.net.cn

也可以通过使用 @Configuration(proxyBeanMethods = false) 避免配置类的 CGLIB 代理。 有关更多详细信息,请参阅 proxyBeanMethods Javadoc

幸运的是,Kotlin 提供了一个 kotlin-spring 插件(kotlin-allopen 插件的预配置版本),它会自动打开使用以下注解之一进行注解或元注解的类型的类 和其成员函数:spring-doc.cadn.net.cn

元注解支持意味着,使用 @Configuration@Controller@RestController@Service@Repository 注解的类型会自动被打开,因为这些 注解被 @Component 元注解所注解。spring-doc.cadn.net.cn

start.spring.io 默认启用 kotlin-spring 插件。因此,实际上,您可以直接编写 Kotlin beans 而无需任何额外的 open 关键字,就像在 Java 中一样。spring-doc.cadn.net.cn

Spring Framework 文档中的 Kotlin 代码示例并未在类及其成员函数上显式指定 open。这些示例是为使用 kotlin-allopen 插件的项目编写的,因为这是最常用的设置。

使用不可变类实例进行持久化

在 Kotlin 中,将只读属性声明在主构造函数中是方便的,并且被认为是最佳实践,如下例所示:spring-doc.cadn.net.cn

class Person(val name: String, val age: Int)

您可以选择添加 关键字 data 以使编译器自动从主构造函数中声明的所有属性推导出以下成员:spring-doc.cadn.net.cn

如下面的示例所示,这允许对单个属性进行轻松更改,即使有Person个属性是只读的:spring-doc.cadn.net.cn

data class Person(val name: String, val age: Int)

val jack = Person(name = "Jack", age = 1)
val olderJack = jack.copy(age = 2)

常见的持久化技术(如JPA)需要一个默认构造函数,这会阻止这种设计。幸运的是,有一个解决方法可以避免这种 “默认构造函数的困境”, 因为Kotlin提供了一个 kotlin-jpa 插件,该插件为使用JPA注解标注的类生成合成的无参构造函数。spring-doc.cadn.net.cn

如果您需要为其他持久化技术利用这种机制,可以配置 the kotlin-noarg 插件。spring-doc.cadn.net.cn

从Kay发布版开始,Spring Data 支持 Kotlin 不可变类实例,并且如果模块使用 Spring Data 对象映射(如 MongoDB、Redis、Cassandra 等),则不需要 kotlin-noarg 插件。

注入依赖

我们建议尽量使用带有 val 个只读(在可能的情况下为非空)属性 的构造函数注入, 如下面的示例所示:spring-doc.cadn.net.cn

@Component
class YourBean(
	private val mongoTemplate: MongoTemplate,
	private val solrClient: SolrClient
)
带有单个构造函数的类会自动自动装配其参数。 这就是为什么在上面显示的示例中不需要显式的 @Autowired constructor 的原因。

如果您确实需要使用字段注入,可以使用 lateinit var 结构, 如下面的示例所示:spring-doc.cadn.net.cn

@Component
class YourBean {

	@Autowired
	lateinit var mongoTemplate: MongoTemplate

	@Autowired
	lateinit var solrClient: SolrClient
}

注入配置属性

在 Java 中,你可以通过使用注解(例如 @Value("${property}"))注入配置属性。 但是在 Kotlin 中,$ 是一个保留字符,用于 字符串插值spring-doc.cadn.net.cn

因此,如果您希望在Kotlin中使用@Value注解,需要通过编写@Value("\${property}")来转义$字符。spring-doc.cadn.net.cn

如果你使用 Spring Boot,你可能应该使用 @ConfigurationProperties 而不是 @Value 注解。

作为另一种选择,您可以通过声明以下配置 Bean 来自定义属性占位符前缀:spring-doc.cadn.net.cn

@Bean
fun propertyConfigurer() = PropertySourcesPlaceholderConfigurer().apply {
	setPlaceholderPrefix("%{")
}

您可以自定义现有代码(例如 Spring Boot 指标端点或 @LocalServerPort) ,该代码使用 ${…​} 语法,并通过配置 Bean,如下例所示:spring-doc.cadn.net.cn

@Bean
fun kotlinPropertyConfigurer() = PropertySourcesPlaceholderConfigurer().apply {
	setPlaceholderPrefix("%{")
	setIgnoreUnresolvablePlaceholders(true)
}

@Bean
fun defaultPropertyConfigurer() = PropertySourcesPlaceholderConfigurer()

已检查异常

Java 和 Kotlin 异常处理 非常接近,主要区别在于 Kotlin 将所有异常都视为未检查异常。然而,当使用代理对象(例如带有 @Transactional 注解的类或方法)时,默认会将抛出的已检查异常包装在 UndeclaredThrowableException 中。spring-doc.cadn.net.cn

为了像Java中那样获取原始抛出的异常,方法应使用 @Throws 进行注解,以明确指定抛出的检查型异常(例如 @Throws(IOException::class))。spring-doc.cadn.net.cn

注解数组属性

Kotlin 注解与 Java 注解大部分相似,但数组属性(在 Spring 中广泛使用)的行为有所不同。如 Kotlin 文档 中所述,您可以省略 value 属性名称,与其他属性不同,可以将其指定为 vararg 参数。spring-doc.cadn.net.cn

要理解这意味着什么,请考虑 @RequestMapping(这是最广泛使用的Spring注解之一)作为一个例子。这个Java注解的声明如下:spring-doc.cadn.net.cn

public @interface RequestMapping {

	@AliasFor("path")
	String[] value() default {};

	@AliasFor("value")
	String[] path() default {};

	RequestMethod[] method() default {};

	// ...
}

@RequestMapping 的典型用例是将处理方法映射到特定路径和方法。在 Java 中,您可以为注解数组属性指定一个值,并且它会自动转换为数组。spring-doc.cadn.net.cn

这就是为什么可以编写 @RequestMapping(value = "/toys", method = RequestMethod.GET)@RequestMapping(path = "/toys", method = RequestMethod.GET) 的原因。spring-doc.cadn.net.cn

但是,在Kotlin中,您必须编写 @RequestMapping("/toys", method = [RequestMethod.GET])@RequestMapping(path = ["/toys"], method = [RequestMethod.GET])(方括号需要 通过命名数组属性来指定)。spring-doc.cadn.net.cn

此特定 method 属性的替代方法(最常见的方法)是使用快捷注解,例如 @GetMapping@PostMapping 和其他注解。spring-doc.cadn.net.cn

如果 @RequestMapping method 属性未指定,则所有 HTTP 方法都会被匹配,而不仅仅是 GET 方法。

声明-site 协变

在使用 Kotlin 编写的 Spring 应用程序中处理泛型类型时,某些用例可能需要理解 Kotlin 的声明处变型,它允许在声明类型时定义变型,而 Java 仅支持使用处变型,无法实现这一点。spring-doc.cadn.net.cn

例如,在 Kotlin 中声明 List<Foo> 在概念上等同于 java.util.List<? extends Foo>,因为 kotlin.collections.List 被声明为 interface List<out E> : kotlin.collections.Collection<E>spring-doc.cadn.net.cn

在使用Java类时,例如从Kotlin类型向Java类型编写org.springframework.core.convert.converter.Converter时,需要通过在泛型类型上使用out Kotlin关键字来考虑这一点。spring-doc.cadn.net.cn

class ListOfFooConverter : Converter<List<Foo>, CustomJavaList<out Foo>> {
    // ...
}

在转换任何类型的对象时,可以使用 * 的星形投影来代替 out Anyspring-doc.cadn.net.cn

class ListOfAnyConverter : Converter<List<*>, CustomJavaList<*>> {
    // ...
}
Spring 框架尚未利用声明点的变型类型信息来注入 Bean, 请订阅 spring-framework#22313 以跟踪相关进展。

测试

本节介绍使用 Kotlin 和 Spring Framework 组合进行测试的内容。 推荐的测试框架是 JUnit 5 以及用于模拟的 Mockkspring-doc.cadn.net.cn

如果使用 Spring Boot,请参阅 相关文档

构造器注入

如在专门的部分中所述, JUnit 5 允许通过构造函数注入 beans,这在使用 Kotlin 时非常有用, 以便使用 val 而不是 lateinit var。您可以使用 @TestConstructor(autowireMode = AutowireMode.ALL) 来为所有参数启用自动注入。spring-doc.cadn.net.cn

@SpringJUnitConfig(TestConfig::class)
@TestConstructor(autowireMode = AutowireMode.ALL)
class OrderServiceIntegrationTests(val orderService: OrderService,
                                   val customerService: CustomerService) {

    // tests that use the injected OrderService and CustomerService
}

PER_CLASS 生命周期

Kotlin 允许您在反引号(`)之间指定有意义的测试函数名称。 从 JUnit 5 开始,Kotlin 测试类可以使用 @TestInstance(TestInstance.Lifecycle.PER_CLASS) 注解来启用测试类的单例实例化,这允许在非静态方法上使用 @BeforeAll@AfterAll 注解,这非常适合 Kotlin。spring-doc.cadn.net.cn

您也可以通过一个带有 junit-platform.properties 属性的 PER_CLASS 文件将默认行为更改为 junit.jupiter.testinstance.lifecycle.default = per_classspring-doc.cadn.net.cn

以下示例演示了非静态方法上的 @BeforeAll@AfterAll 注释:spring-doc.cadn.net.cn

@TestInstance(TestInstance.Lifecycle.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 HTML page`() {
    client.get().uri("/users")
        .accept(TEXT_HTML)
        .retrieve()
        .bodyToMono<String>()
        .test()
        .expectNextMatches { it.contains("Foo") }
        .verifyComplete()
  }

  @AfterAll
  fun afterAll() {
    application.stop()
  }
}

类似规范的测试

你可以使用 JUnit 5 和 Kotlin 创建类似规范的测试。 以下示例展示了如何操作:spring-doc.cadn.net.cn

class SpecificationLikeTests {

  @Nested
  @DisplayName("a calculator")
  inner class Calculator {
     val calculator = SampleCalculator()

     @Test
     fun `should return the result of adding the first number to the second number`() {
        val sum = calculator.sum(2, 4)
        assertEquals(6, sum)
     }

     @Test
     fun `should return the result of subtracting the second number from the first number`() {
        val subtract = calculator.subtract(4, 2)
        assertEquals(2, subtract)
     }
  }
}

WebTestClient Kotlin中的类型推断问题

由于存在 类型推断问题,您必须 使用 Kotlin 的 expectBody 扩展(例如 .expectBody<String>().isEqualTo("toys")), 因为它为 Kotlin 与 Java API 的问题提供了解决方法。spring-doc.cadn.net.cn

参见相关 SPR-16057 问题。spring-doc.cadn.net.cn