|
对于最新稳定版本,请使用 Spring Framework 7.0.6! |
使用 Kotlin 的 Spring 项目
本节提供了一些在使用 Kotlin 开发 Spring 项目时值得参考的具体提示和建议。
默认为 final
默认情况下,Kotlin 中的所有类都是 final。
类上的 open 修饰符与 Java 的 final 相反:它允许其他类继承此类。这也适用于成员函数,即它们需要被标记为 open 才能被重写。
虽然 Kotlin 面向 JVM 的设计通常与 Spring 框架配合顺畅,但如果忽视了这一特定的 Kotlin 特性,可能会导致应用程序无法启动。这是因为 Spring Bean(例如默认情况下出于技术原因需要在运行时被动态代理的 @Configuration 注解类)通常由 CGLIB 进行代理。解决方法是在每个由 CGLIB 代理的 Spring Bean 类及其成员函数上添加 open 关键字,但这很快会变得繁琐,并且违背了 Kotlin 保持代码简洁和可预测的原则。
也可以通过使用 @Configuration(proxyBeanMethods = false) 来避免配置类的 CGLIB 代理。
有关更多详细信息,请参阅 proxyBeanMethods Javadoc。 |
幸运的是,Kotlin 提供了一个
kotlin-spring
插件(kotlin-allopen 插件的预配置版本),可自动为使用以下任一注解或元注解标注的类型及其成员函数打开类:
-
@Component -
@Async -
@Transactional -
@Cacheable
元注解支持意味着使用 @Configuration、@Controller、@RestController、@Service 或 @Repository 注解的类型会自动被注册为 Bean,因为这些注解本身都通过元注解方式标注了 @Component。
start.spring.io 默认启用了 kotlin-spring 插件。因此,在实际开发中,你可以像编写 Java Bean 一样编写 Kotlin Bean,而无需额外添加 open 关键字。
Spring Framework 文档中的 Kotlin 代码示例并未在类及其成员函数上显式指定 open 关键字。这些示例是为使用 kotlin-allopen 插件的项目编写的,因为这是最常用的配置。 |
使用不可变类实例进行持久化
在 Kotlin 中,将只读属性声明在主构造函数中既方便,也被视为一种最佳实践,如下例所示:
class Person(val name: String, val age: Int)
您可以选择添加 the data keyword
,以使编译器自动从主构造函数中声明的所有属性派生以下成员:
-
equals()和hashCode() -
toString()的形式为"User(name=John, age=42)" -
componentN()函数,按照属性声明的顺序对应各个属性 -
copy()函数
如下例所示,即使 Person 属性是只读的,这也允许轻松修改各个属性:
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 注解的类生成合成的无参构造函数。
如果您需要为其他持久化技术利用此类机制,可以配置
kotlin-noarg
插件。
从 Kay 发布版本开始,Spring Data 支持 Kotlin 不可变类实例,并且如果模块使用 Spring Data 对象映射(例如 MongoDB、Redis、Cassandra 等),则不再需要 kotlin-noarg 插件。 |
依赖注入
我们的建议是尽量优先使用构造函数注入,并配合 val 声明的只读属性(在可能的情况下使用非空类型),如下例所示:
@Component
class YourBean(
private val mongoTemplate: MongoTemplate,
private val solrClient: SolrClient
)
具有单个构造函数的类,其参数会自动进行自动装配(autowired)。
因此,在上面所示的示例中,无需显式使用 @Autowired constructor。 |
如果你确实需要使用字段注入,可以使用 lateinit var 结构,如下例所示:
@Component
class YourBean {
@Autowired
lateinit var mongoTemplate: MongoTemplate
@Autowired
lateinit var solrClient: SolrClient
}
注入配置属性
在 Java 中,你可以使用注解(例如 @Value("${property}"))来注入配置属性。
然而,在 Kotlin 中,$ 是一个保留字符,用于
字符串插值。
因此,如果你想在 Kotlin 中使用 @Value 注解,需要通过编写 $ 来转义 @Value("\${property}") 字符。
如果您使用 Spring Boot,您可能应该使用
@ConfigurationProperties
而不是 @Value 注解。 |
或者,您可以通过声明以下配置 Bean 来自定义属性占位符的前缀:
@Bean
fun propertyConfigurer() = PropertySourcesPlaceholderConfigurer().apply {
setPlaceholderPrefix("%{")
}
你可以使用配置 Bean 来自定义现有代码(例如 Spring Boot 的 Actuator 或 @LocalServerPort),这些代码使用了 ${…} 语法,如下例所示:
@Bean
fun kotlinPropertyConfigurer() = PropertySourcesPlaceholderConfigurer().apply {
setPlaceholderPrefix("%{")
setIgnoreUnresolvablePlaceholders(true)
}
@Bean
fun defaultPropertyConfigurer() = PropertySourcesPlaceholderConfigurer()
受检异常
Java 和 Kotlin 的异常处理 非常相似,主要区别在于 Kotlin 将所有异常都视为非检查型异常(unchecked exceptions)。然而,在使用代理对象时(例如带有 @Transactional 注解的类或方法),所抛出的检查型异常(checked exceptions)默认会被包装在 UndeclaredThrowableException 中。
若要在返回结果中获取与 Java 一致的原始异常,方法应使用
@Throws
注解显式声明所抛出的受检异常(例如 @Throws(IOException::class))。
注解数组属性
Kotlin 注解与 Java 注解在大多数情况下是相似的,但数组属性(在 Spring 中被广泛使用)的行为有所不同。正如 Kotlin 官方文档 中所解释的那样,你可以省略 value 属性名(这与其他属性不同),并将其指定为一个 vararg 参数。
要理解其含义,可以以 @RequestMapping(这是 Spring 中最广泛使用的注解之一)为例。该 Java 注解的声明如下:
public @interface RequestMapping {
@AliasFor("path")
String[] value() default {};
@AliasFor("value")
String[] path() default {};
RequestMethod[] method() default {};
// ...
}
@RequestMapping 的典型用例是将处理方法映射到特定的路径和 HTTP 方法。在 Java 中,你可以为注解的数组属性指定单个值,该值会自动转换为数组。
这就是为什么我们可以写成
@RequestMapping(value = "/toys", method = RequestMethod.GET) 或
@RequestMapping(path = "/toys", method = RequestMethod.GET)。
然而,在 Kotlin 中,你必须写成 @RequestMapping("/toys", method = [RequestMethod.GET])
或 @RequestMapping(path = ["/toys"], method = [RequestMethod.GET])(使用命名数组属性时需要指定方括号)。
针对这个特定的 method 属性(最常见的一种),可以使用快捷注解作为替代方案,例如 @GetMapping、@PostMapping 等。
如果未指定 @RequestMapping 的 method 属性,则将匹配所有 HTTP 方法,而不仅仅是 GET 方法。 |
声明站点方差
在使用 Kotlin 编写的 Spring 应用程序中处理泛型类型时,对于某些使用场景,可能需要理解 Kotlin 的声明处型变(declaration-site variance),它允许在声明类型时定义型变,而 Java 仅支持使用处型变(use-site variance),无法做到这一点。
例如,在 Kotlin 中声明 List<Foo> 在概念上等同于 java.util.List<? extends Foo>,因为
kotlin.collections.List 被声明为
interface List<out E> : kotlin.collections.Collection<E>。
在使用 Java 类时,需要通过在泛型类型上使用 Kotlin 的 out 关键字来考虑这一点,
例如在编写从 Kotlin 类型到 Java 类型的 org.springframework.core.convert.converter.Converter 时。
class ListOfFooConverter : Converter<List<Foo>, CustomJavaList<out Foo>> {
// ...
}
在转换任意类型的对象时,可以使用星号投影(*)来代替 out Any。
class ListOfAnyConverter : Converter<List<*>, CustomJavaList<*>> {
// ...
}
| Spring Framework 目前尚未利用声明位置的变型类型信息来注入 Bean, 请订阅 spring-framework#22313 以跟踪相关进展。 |
测试
| 如果你正在使用 Spring Boot,请参阅相关的文档。 |
构造函数注入
正如在专门章节中所述,
JUnit 5 允许对 Bean 进行构造函数注入,这在 Kotlin 中非常有用,
以便使用val而非lateinit var。您可以使用
@TestConstructor(autowireMode = AutowireMode.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 5 开始,Kotlin 测试类可以使用 @TestInstance(TestInstance.Lifecycle.PER_CLASS)
注解来启用测试类的单例实例化,从而允许在非静态方法上使用 @BeforeAll
和 @AfterAll 注解,这与 Kotlin 非常契合。
你也可以通过一个包含 PER_CLASS 属性的 junit-platform.properties 文件,将默认行为更改为 junit.jupiter.testinstance.lifecycle.default = per_class。
以下示例演示了在非静态方法上使用 @BeforeAll 和 @AfterAll 注解:
@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 创建类似规范的测试。 以下示例展示了如何实现这一点:
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)
}
}
}