Spring Projects in Kotlin

本部分提供了一些专门提示和建议,帮助你用 Kotlin 开发 Spring 项目。

This section provides some specific hints and recommendations worth for developing Spring projects in Kotlin.

Final by Default

默认情况下, all classes and member functions in Kotlin are final.类上的 open 修饰符与 Java 的 final 相反:它允许其他人继承此类。这也适用于成员函数,必须将其标记为 open 才能被覆盖。

By default, all classes and member functions in Kotlin are final. The open modifier on a class is the opposite of Java’s final: It allows others to inherit from this class. This also applies to member functions, in that they need to be marked as open to be overridden.

虽然 Kotlin 适用的 JVM 友好设计与 Spring 通常无缝衔接,但如果未考虑此事实,这个特定的 Kotlin 特性可能会阻止应用程序启动。这是因为 Spring Bean(例如默认情况下需要在运行时进行扩展以满足技术原因的 @Configuration 注释的类)通常由 CGLIB 代理。解决方法是对每个类和 CGLIB 代理的 Spring Bean 的成员函数添加一个 open 关键字,这可能会很快变得痛苦且违反 Kotlin 的简洁且可预测代码原则。

While Kotlin’s JVM-friendly design is generally frictionless with Spring, this specific Kotlin feature can prevent the application from starting, if this fact is not taken into consideration. This is because Spring beans (such as @Configuration annotated classes which by default need to be extended at runtime for technical reasons) are normally proxied by CGLIB. The workaround is to add an open keyword on each class and member function of Spring beans that are proxied by CGLIB, which can quickly become painful and is against the Kotlin principle of keeping code concise and predictable.

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

It is also possible to avoid CGLIB proxies for configuration classes by using @Configuration(proxyBeanMethods = false). See proxyBeanMethods Javadoc for more details.

幸运的是,Kotlin 提供了一个`kotlin-spring` 插件(kotlin-allopen 插件的预配置版本)https://kotlinlang.org/docs/compiler-plugins.html#kotlin-spring-compiler-plugin[,该插件会自动替使用下列注释或元注释的类型开放类及其成员函数:

Fortunately, Kotlin provides a kotlin-spring plugin (a preconfigured version of the kotlin-allopen plugin) that automatically opens classes and their member functions for types that are annotated or meta-annotated with one of the following annotations:

  • @Component

  • @Async

  • @Transactional

  • @Cacheable

元注释支持意味着注释有 @Configuration@Controller@RestController@Service@Repository 的类型自动开放,因为这些注释通过 @Component 元注释。

Meta-annotation support means that types annotated with @Configuration, @Controller, @RestController, @Service, or @Repository are automatically opened since these annotations are meta-annotated with @Component.

一些涉及代理和 Kotlin 编译器自动生成 final 方法的用例需要特别小心。例如,具有属性的 Kotlin 类将生成相关的 final getter 和 setter。为了能够代理相关方法,类型级别 @Component 注释应优先于方法级别 @Bean,以便通过 kotlin-spring 插件打开这些方法。典型用例是 @Scope 及其流行的 @RequestScope 特化。

Some use cases involving proxies and automatic generation of final methods by the Kotlin compiler require extra care. For example, a Kotlin class with properties will generate related final getters and setters. In order to be able to proxy related methods, a type level @Component annotation should be preferred to method level @Bean in order to have those methods opened by the kotlin-spring plugin. A typical use case is @Scope and its popular @RequestScope specialization.

start.spring.io 默认启用 kotlin-spring 插件。因此,在实践中,您可以编写 Kotlin bean,而无需任何其他 open 关键字,就像在 Java 中一样。

start.spring.io enables the kotlin-spring plugin by default. So, in practice, you can write your Kotlin beans without any additional open keyword, as in Java.

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

The Kotlin code samples in Spring Framework documentation do not explicitly specify open on the classes and their member functions. The samples are written for projects using the kotlin-allopen plugin, since this is the most commonly used setup.

Using Immutable Class Instances for Persistence

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

In Kotlin, it is convenient and considered to be a best practice to declare read-only properties within the primary constructor, as in the following example:

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

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

You can optionally add the data keyword to make the compiler automatically derive the following members from all properties declared in the primary constructor:

  • equals() and hashCode()

  • toString() of the form "User(name=John, age=42)"

  • componentN() functions that correspond to the properties in their order of declaration

  • copy() function

如下例所示,即使 Person 属性是只读的,这也允许轻松更改各个属性:

As the following example shows, this allows for easy changes to individual properties, even if Person properties are read-only:

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

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

公共持久化技术(如 JPA)需要默认构造函数,防止此类设计。幸运的是,对此有一个解决方法https://stackoverflow.com/questions/32038177/kotlin-with-jpa-default-constructor-hell[“default constructor hell”],因为 Kotlin 提供 kotlin-jpa插件,为用 JPA 注解注释的类生成合成的无参数构造函数。

Common persistence technologies (such as JPA) require a default constructor, preventing this kind of design. Fortunately, there is a workaround for this “default constructor hell”, since Kotlin provides a kotlin-jpa plugin that generates synthetic no-arg constructor for classes annotated with JPA annotations.

如果您需要为其他持久化技术利用此类机制,则可以配置 kotlin-noarg插件。

If you need to leverage this kind of mechanism for other persistence technologies, you can configure the kotlin-noarg plugin.

截至 Kay 发布列车,Spring Data 支持 Kotlin 不可变类实例并且不需要 kotlin-noarg 插件,如果模块使用 Spring Data 对象映射(例如 MongoDB、Redis、Cassandra 和其他映射)。

As of the Kay release train, Spring Data supports Kotlin immutable class instances and does not require the kotlin-noarg plugin if the module uses Spring Data object mappings (such as MongoDB, Redis, Cassandra, and others).

Injecting Dependencies

Favor constructor injection

我们的建议是尝试优先使用 val 只读(且在可能的情况下不可为空的)https://kotlinlang.org/docs/properties.html[属性]构造函数注入,如下例所示:

Our recommendation is to try to favor constructor injection with val read-only (and non-nullable when possible) properties, as the following example shows:

@Component
class YourBean(
	private val mongoTemplate: MongoTemplate,
	private val solrClient: SolrClient
)

具有单个构造函数的类会自动装配其参数。这就是在上例中不必显式使用 @Autowired constructor 的原因。

Classes with a single constructor have their parameters automatically autowired. That’s why there is no need for an explicit @Autowired constructor in the example shown above.

如果您真的需要使用字段注入,则可以使用`lateinit var` 构造,如下例所示:

If you really need to use field injection, you can use the lateinit var construct, as the following example shows:

@Component
class YourBean {

	@Autowired
	lateinit var mongoTemplate: MongoTemplate

	@Autowired
	lateinit var solrClient: SolrClient
}

Internal functions name mangling

带有 internal 可见性修饰符 的 Kotlin 函数在编译为 JVM 字节码时会改动其名称,这在按名称注入依赖项时会产生副作用。

Kotlin functions with the internal visibility modifier have their names mangled when compiled to JVM bytecode, which has a side effect when injecting dependencies by name.

例如,此 Kotlin 类:

For example, this Kotlin class:

@Configuration
class SampleConfiguration {

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

转换为此已编译 JVM 字节码的 Java 表示形式:

Translates to this Java representation of the compiled JVM bytecode:

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

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

因此,表示为 Kotlin 字符串的相关 bean 名称是`"sampleBean\$demo_kotlin_internal_test"`, 而不是 "sampleBean",这是针对常规`public` 函数用例的情况。确保在按名称注入此类 bean 时使用混乱的名称,或添加`@JvmName("sampleBean")` 以禁用名称混乱。

As a consequence, the related bean name represented as a Kotlin string is "sampleBean\$demo_kotlin_internal_test", instead of "sampleBean" for the regular public function use-case. Make sure to use the mangled name when injecting such bean by name, or add @JvmName("sampleBean") to disable name mangling.

Injecting Configuration Properties

在 Java 中,您可以使用注解(例如 @Value("${property}"))注入配置属性。然而,在 Kotlin 中,$ 是一个保留字符,用于https://kotlinlang.org/docs/idioms.html#string-interpolation[字符串插值]。

In Java, you can inject configuration properties by using annotations (such as @Value("${property}")). However, in Kotlin, $ is a reserved character that is used for string interpolation.

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

Therefore, if you wish to use the @Value annotation in Kotlin, you need to escape the $ character by writing @Value("\${property}").

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

If you use Spring Boot, you should probably use @ConfigurationProperties instead of @Value annotations.

或者,您可以通过声明以下配置 bean 来自定义属性占位符前缀:

As an alternative, you can customize the property placeholder prefix by declaring the following configuration beans:

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

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

You can customize existing code (such as Spring Boot actuators or @LocalServerPort) that uses the ${…​} syntax, with configuration beans, as the following example shows:

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

@Bean
fun defaultPropertyConfigurer() = PropertySourcesPlaceholderConfigurer()

Checked Exceptions

Java 和 Kotlin 异常处理 非常接近,主要区别在于 Kotlin 将所有异常视为未检查的异常。然而,在使用代理对象(例如用 @Transactional 注解的类或方法)时,抛出的已检查异常在默认情况下将被 UndeclaredThrowableException 包装。

Java and Kotlin exception handling are pretty close, with the main difference being that Kotlin treats all exceptions as unchecked exceptions. However, when using proxied objects (for example classes or methods annotated with @Transactional), checked exceptions thrown will be wrapped by default in an UndeclaredThrowableException.

要像在 Java 中一样获得抛出的原始异常,应使用 @Throws对方法进行注释,以明确指定抛出的已检查异常(例如 @Throws(IOException::class))。

To get the original exception thrown like in Java, methods should be annotated with @Throws to specify explicitly the checked exceptions thrown (for example @Throws(IOException::class)).

Annotation Array Attributes

Kotlin 注解与 Java 注解非常相似,但数组属性(它们在 Spring 中广泛使用)的行为不同。如https://kotlinlang.org/docs/annotations.html[Kotlin 文档]中所述,与其他属性不同,您可以省略 value 属性名称,并将其指定为 vararg 参数。

Kotlin annotations are mostly similar to Java annotations, but array attributes (which are extensively used in Spring) behave differently. As explained in the Kotlin documentation you can omit the value attribute name, unlike other attributes, and specify it as a vararg parameter.

要了解这意味着什么,请考虑`@RequestMapping`(这是最广泛使用的 Spring 注解之一)作为一个示例。此 Java 注解声明如下:

To understand what that means, consider @RequestMapping (which is one of the most widely used Spring annotations) as an example. This Java annotation is declared as follows:

public @interface RequestMapping {

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

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

	RequestMethod[] method() default {};

	// ...
}

@RequestMapping 的典型用例是将处理程序方法映射到特定路径和方法。在 Java 中, 您可以在注解数组属性中指定一个值, 它将自动转换成为一个数组。

The typical use case for @RequestMapping is to map a handler method to a specific path and method. In Java, you can specify a single value for the annotation array attribute, and it is automatically converted to an array.

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

That is why one can write @RequestMapping(value = "/toys", method = RequestMethod.GET) or @RequestMapping(path = "/toys", method = RequestMethod.GET).

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

However, in Kotlin, you must write @RequestMapping("/toys", method = [RequestMethod.GET]) or @RequestMapping(path = ["/toys"], method = [RequestMethod.GET]) (square brackets need to be specified with named array attributes).

针对这个特定的 method 属性(最常见的一个), 它的一个备用方案是使用一个快捷注解, 如 @GetMapping, @PostMapping 等。

An alternative for this specific method attribute (the most common one) is to use a shortcut annotation, such as @GetMapping, @PostMapping, and others.

如果没有指定 @RequestMapping method 属性,将匹配所有 HTTP 方法,而不仅仅是 GET 方法。

If the @RequestMapping method attribute is not specified, all HTTP methods will be matched, not only the GET method.

Declaration-site variance

在用 Kotlin 编写的 Spring 应用程序中处理泛型类型时,某些用例可能需要理解 Kotlin 声明站点协变,它允许在声明类型时定义协变,这在仅支持使用站点协变的 Java 中是不可能的。

Dealing with generic types in Spring applications written in Kotlin may require, for some use cases, to understand Kotlin declaration-site variance which allows to define the variance when declaring a type, which is not possible in Java which supports only use-site variance.

例如,List<Foo> 在 Kotlin 中声明在概念上等同于 java.util.List<? extends Foo>,因为`kotlin.collections.List` 声明为https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/[interface List<out E> : kotlin.collections.Collection<E>]。

For example, declaring List<Foo> in Kotlin is conceptually equivalent to java.util.List<? extends Foo> because kotlin.collections.List is declared as interface List<out E> : kotlin.collections.Collection<E>.

在使用 Java 类时, 需要使用泛型类型上的 out Kotlin 关键字来考虑这一点, 例如当编写从 Kotlin 类型到 Java 类型的 org.springframework.core.convert.converter.Converter 时。

This needs to be taken into account by using the out Kotlin keyword on generic types when using Java classes, for example when writing a org.springframework.core.convert.converter.Converter from a Kotlin type to a Java type.

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

在转换任何种类的对象时, 可以使用带有 * 的星形投影来代替 out Any

When converting any kind of objects, star projection with * can be used instead of out Any.

class ListOfAnyConverter : Converter<List<*>, CustomJavaList<*>> {
    // ...
}

Spring Framework 尚未利用声明站点差异类型信息来注入 bean,订阅 spring-framework#22313 以追踪相关进度。

Spring Framework does not leverage yet declaration-site variance type information for injecting beans, subscribe to spring-framework#22313 to track related progresses.

Testing

本节讨论 Kotlin 和 Spring Framework 结合进行的测试。建议的测试框架是 JUnit 5 连同用于模拟的 Mockk

This section addresses testing with the combination of Kotlin and Spring Framework. The recommended testing framework is JUnit 5 along with Mockk for mocking.

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

If you are using Spring Boot, see this related documentation.

Constructor injection

dedicated section 中所述,JUnit Jupiter(JUnit 5)允许对 Bean 采用构造函数注入,这在 Kotlin 中非常有用,以使用 val 代替 lateinit var。您可以使用 @TestConstructor(autowireMode = AutowireMode.ALL) 为所有参数启用自动装配。

As described in the dedicated section, JUnit Jupiter (JUnit 5) allows constructor injection of beans which is pretty useful with Kotlin in order to use val instead of lateinit var. You can use @TestConstructor(autowireMode = AutowireMode.ALL) to enable autowiring for all parameters.

你还可以使用具有 spring.test.constructor.autowire.mode = all 属性的 junit-platform.properties 文件将默认行为更改为 ALL

You can also change the default behavior to ALL in a junit-platform.properties file with a spring.test.constructor.autowire.mode = all property.

@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 Lifecycle

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

Kotlin lets you specify meaningful test function names between backticks (`). With JUnit Jupiter (JUnit 5), Kotlin test classes can use the @TestInstance(TestInstance.Lifecycle.PER_CLASS) annotation to enable single instantiation of test classes, which allows the use of @BeforeAll and @AfterAll annotations on non-static methods, which is a good fit for Kotlin.

还可以使用具有 junit.jupiter.testinstance.lifecycle.default = per_class 属性的 junit-platform.properties 文件将默认行为更改为 PER_CLASS

You can also change the default behavior to PER_CLASS in a junit-platform.properties file with a junit.jupiter.testinstance.lifecycle.default = per_class property.

以下示例演示了在非静态方法上的 @BeforeAll@AfterAll 注解:

The following example demonstrates @BeforeAll and @AfterAll annotations on non-static methods:

@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()
  }
}

Specification-like Tests

您可以使用 JUnit 5 和 Kotlin 创建类规范测试。以下示例展示了如何执行此操作:

You can create specification-like tests with JUnit 5 and Kotlin. The following example shows how to do so:

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)
     }
  }
}