本节提供了一些对开发 Spring 项目有价值的具体提示和建议 在Kotlin。Spring中文文档

默认为最终

默认情况下,Kotlin 中的所有类和成员函数都是最终。 类上的修饰符与 Java 的修饰符相反:它允许其他人从中继承 类。这也适用于成员函数,因为它们需要标记为要重写。openfinalopenSpring中文文档

虽然 Kotlin 的 JVM 友好型设计通常与 Spring 无摩擦,但 Kotlin 的这一特定功能 如果不考虑这一事实,可能会阻止应用程序启动。这是因为 Spring Bean(例如,默认情况下需要在运行时扩展的带注释的类,以便进行技术扩展 原因)通常由 CGLIB 代理。解决方法是在每个类上添加一个关键字,然后 由 CGLIB 代理的 Spring Bean 的成员函数,可以 很快就会变得痛苦,并且违背了 Kotlin 保持代码简洁和可预测的原则。@ConfigurationopenSpring中文文档

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

幸运的是,Kotlin 提供了一个 kotlin-spring 插件(插件的预配置版本),可以自动打开类 以及使用以下项之一进行注释或元注释的类型的成员函数 附注:kotlin-allopenSpring中文文档

元注释支持意味着用 、 、 、 或 注释的类型会自动打开,因为这些 注释用 元注释。@Configuration@Controller@RestController@Service@Repository@ComponentSpring中文文档

一些涉及代理和 Kotlin 编译器自动生成最终方法的用例需要额外的 关心。例如,具有属性的 Kotlin 类将生成相关的 getter 和 setter。挨次 为了能够代理相关方法,类型级注释应优先于方法级 以便让插件打开这些方法。一个典型的用例是及其流行的专业化。final@Component@Beankotlin-spring@Scope@RequestScope

start.spring.io 使 默认情况下的插件。因此,在实践中,您可以编写 Kotlin Bean 没有任何额外的关键字,就像在 Java 中一样。kotlin-springopenSpring中文文档

Spring Framework 文档中的 Kotlin 代码示例没有明确指定类及其成员函数。这些示例是为项目编写的 使用插件,因为这是最常用的设置。openkotlin-allopen
也可以通过使用 来避免配置类的 CGLIB 代理。 有关更多详细信息,请参阅 proxyBeanMethods Javadoc@Configuration(proxyBeanMethods = false)
一些涉及代理和 Kotlin 编译器自动生成最终方法的用例需要额外的 关心。例如,具有属性的 Kotlin 类将生成相关的 getter 和 setter。挨次 为了能够代理相关方法,类型级注释应优先于方法级 以便让插件打开这些方法。一个典型的用例是及其流行的专业化。final@Component@Beankotlin-spring@Scope@RequestScope
Spring Framework 文档中的 Kotlin 代码示例没有明确指定类及其成员函数。这些示例是为项目编写的 使用插件,因为这是最常用的设置。openkotlin-allopen

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

在 Kotlin 中,声明只读属性很方便,并且被认为是最佳实践 在主构造函数中,如以下示例所示:Spring中文文档

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

您可以选择添加 data 关键字,以使编译器自动从声明的所有属性派生以下成员 在主构造函数中:Spring中文文档

如以下示例所示,即使属性是只读的,也可以轻松更改单个属性:PersonSpring中文文档

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 注释的类生成合成的 no-arg 构造函数。Spring中文文档

如果需要将这种机制用于其他持久性技术,可以配置 kotlin-noarg 插件。Spring中文文档

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

注入依赖项

Favor 构造函数注入

我们的建议是尝试使用只读(和 如果可能,则不为空)属性, 如以下示例所示:valSpring中文文档

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

如果你真的需要使用场注入,你可以使用构造, 如以下示例所示:lateinit varSpring中文文档

@Component
class YourBean {

	@Autowired
	lateinit var mongoTemplate: MongoTemplate

	@Autowired
	lateinit var solrClient: SolrClient
}

内部函数名称 mangling

带有可见性修饰符的 Kotlin 函数具有 当编译为 JVM 字节码时,它们的名称会被篡改,这在按名称注入依赖项时会产生副作用。internalSpring中文文档

例如,这个 Kotlin 类:Spring中文文档

@Configuration
class SampleConfiguration {

	@Bean
	internal fun sampleBean() = SampleBean()
}

转换为编译后的 JVM 字节码的 Java 表示形式:Spring中文文档

@Configuration
@Metadata(/* ... */)
public class SampleConfiguration {

	@Bean
	@NotNull
	public SampleBean sampleBean$demo_kotlin_internal_test() {
		return new SampleBean();
	}
}

因此,表示为 Kotlin 字符串的相关 Bean 名称为 , 而不是常规函数用例。确保在注入时使用损坏的名称 这样的 bean 按名称,或添加以禁用名称 mangling。"sampleBean\$demo_kotlin_internal_test""sampleBean"public@JvmName("sampleBean")Spring中文文档

具有单个构造函数的类会自动连接其参数。 这就是为什么在所示示例中不需要显式的原因 以上。@Autowired constructor

注入配置属性

在 Java 中,您可以使用注解(例如 )注入配置属性。 但是,在 Kotlin 中,是用于字符串插值的保留字符。@Value("${property}")$Spring中文文档

因此,如果您想在 Kotlin 中使用注释,您需要通过编写 来转义字符。@Value$@Value("\${property}")Spring中文文档

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

或者,可以通过声明 以下配置 Bean:Spring中文文档

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

您可以自定义现有代码(例如 Spring Boot 执行器或 ) 它使用配置 Bean 的语法,如以下示例所示:@LocalServerPort${…​}Spring中文文档

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

@Bean
fun defaultPropertyConfigurer() = PropertySourcesPlaceholderConfigurer()
如果你使用 Spring Boot,你可能应该使用 @ConfigurationProperties 而不是注解。@Value

已检查的异常

Java 和 Kotlin 异常处理非常接近,主要区别在于 Kotlin 将所有异常视为 未经检查的异常。但是,当使用代理对象(例如类或方法) 注释 ),选中的异常将默认包装在 一。@TransactionalUndeclaredThrowableExceptionSpring中文文档

要像在 Java 中一样获得抛出的原始异常,方法应使用 @Throws 进行注释,以显式指定抛出的选中异常(例如 )。@Throws(IOException::class)Spring中文文档

注释数组属性

Kotlin 注解大多类似于 Java 注解,但数组属性(它们 在 Spring 中广泛使用)的行为不同。如 Kotlin 文档中所述,您可以省略 属性名称,与其他属性不同,并将其指定为参数。valuevarargSpring中文文档

要理解这意味着什么,请考虑(这是最广泛的之一 以 Spring 注解为例。此 Java 注解声明如下:@RequestMappingSpring中文文档

public @interface RequestMapping {

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

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

	RequestMethod[] method() default {};

	// ...
}

典型的用例是将处理程序方法映射到特定路径 和方法。在 Java 中,您可以为 annotation array 属性指定单个值, 它会自动转换为数组。@RequestMappingSpring中文文档

这就是为什么人们可以写或.@RequestMapping(value = "/toys", method = RequestMethod.GET)@RequestMapping(path = "/toys", method = RequestMethod.GET)Spring中文文档

但是,在 Kotlin 中,您必须写 or (方括号需要 使用命名数组属性指定)。@RequestMapping("/toys", method = [RequestMethod.GET])@RequestMapping(path = ["/toys"], method = [RequestMethod.GET])Spring中文文档

此特定属性(最常见的属性)的替代方法是 使用快捷方式批注,如 、 等。method@GetMapping@PostMappingSpring中文文档

如果未指定该属性,则所有 HTTP 方法都将 匹配,而不仅仅是方法。@RequestMappingmethodGET
如果未指定该属性,则所有 HTTP 方法都将 匹配,而不仅仅是方法。@RequestMappingmethodGET

申报站点差异

对于某些用例,在用 Kotlin 编写的 Spring 应用程序中处理泛型类型可能需要了解 Kotlin 声明站点方差,允许在声明类型时定义方差,这在仅支持 use-site 的 Java 中是不可能的 方差。Spring中文文档

例如,在 Kotlin 中声明在概念上等同于 because 声明为接口 List<out E> : kotlin.collections.Collection<E>List<Foo>java.util.List<? extends Foo>kotlin.collections.ListSpring中文文档

在使用 Java 类时,需要通过在泛型类型上使用 Kotlin 关键字来考虑这一点。 例如,将 a 从 Kotlin 类型写入 Java 类型时。outorg.springframework.core.convert.converter.ConverterSpring中文文档

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

转换任何类型的对象时,可以使用星形投影代替 .*out AnySpring中文文档

class ListOfAnyConverter : Converter<List<*>, CustomJavaList<*>> {
    // ...
}
Spring Framework 没有利用声明站点方差类型信息来注入 bean, 订阅 spring-framework#22313 跟踪相关 进展。
Spring Framework 没有利用声明站点方差类型信息来注入 bean, 订阅 spring-framework#22313 跟踪相关 进展。

测试

本节介绍如何使用 Kotlin 和 Spring Framework 的组合进行测试。 推荐的测试框架是 JUnit 5 以及用于模拟的 MockkSpring中文文档

如果您使用的是 Spring Boot,请参阅此相关文档

构造函数注入

专用部分所述, JUnit Jupiter (JUnit 5) 允许构造函数注入 bean ,这在 Kotlin 中非常有用 为了使用代替 .您可以使用 @TestConstructor(autowireMode = AutowireMode.ALL) 启用所有参数的自动接线。vallateinit varSpring中文文档

还可以将默认行为更改为在具有属性的文件中。ALLjunit-platform.propertiesspring.test.constructor.autowire.mode = all
@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 Jupiter (JUnit 5),Kotlin 测试类可以使用注解来启用测试类的单个实例化,从而允许在非静态方法上使用和注解,非常适合 Kotlin。`@TestInstance(TestInstance.Lifecycle.PER_CLASS)@BeforeAll@AfterAllSpring中文文档

还可以将默认行为更改为在具有属性的文件中。PER_CLASSjunit-platform.propertiesjunit.jupiter.testinstance.lifecycle.default = per_class

以下示例演示了非静态方法并对其进行了批注:@BeforeAll@AfterAllSpring中文文档

@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中文文档

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)
     }
  }
}
如果您使用的是 Spring Boot,请参阅此相关文档
还可以将默认行为更改为在具有属性的文件中。ALLjunit-platform.propertiesspring.test.constructor.autowire.mode = all
还可以将默认行为更改为在具有属性的文件中。PER_CLASSjunit-platform.propertiesjunit.jupiter.testinstance.lifecycle.default = per_class