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.
还可以使用 |
It is also possible to avoid CGLIB proxies for configuration classes by using |
幸运的是,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`类及其成员函数。范例是为使用 |
The Kotlin code samples in Spring Framework documentation do not explicitly specify
|
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()
andhashCode()
-
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 不可变类实例并且不需要 |
As of the Kay release train, Spring Data supports Kotlin immutable class instances and
does not require the |
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
)
具有单个构造函数的类会自动装配其参数。这就是在上例中不必显式使用 |
Classes with a single constructor have their parameters automatically autowired.
That’s why there is no need for an explicit |
如果您真的需要使用字段注入,则可以使用`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,则你可能应该使用 |
If you use Spring Boot, you should probably use
|
或者,您可以通过声明以下配置 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.
如果没有指定 |
If the |
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
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.
你还可以使用具有 |
You can also change the default behavior to |
@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.
还可以使用具有 |
You can also change the default behavior to |
以下示例演示了在非静态方法上的 @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)
}
}
}