Kotlin 中的 Spring 项目
本节提供了一些在 Kotlin 中开发 Spring 项目的具体提示和建议。
默认最终
默认情况下,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
注解的类型会自动开放,因为这些注解是用 @Component
进行元注解的。
某些涉及代理和 Kotlin 编译器自动生成 final 方法的用例需要特别注意。例如,具有属性的 Kotlin 类将生成相关的 final
getter 和 setter。为了能够代理相关方法,应该优先使用类型级别的 @Component
注解而不是方法级别的 @Bean
,以便通过 kotlin-spring
插件打开这些方法。一个典型的用例是 @Scope
及其流行的 @RequestScope
特化。
start.spring.io 默认启用了 kotlin-spring
插件。因此,实际上,你可以像在 Java 中一样编写 Kotlin bean,而无需额外的 open
关键字。
Spring Framework 文档中的 Kotlin 代码示例没有在类及其成员函数上显式指定 open
。这些示例是为使用 kotlin-allopen
插件的项目编写的,因为这是最常用的设置。
使用不可变类实例进行持久化
在 Kotlin 中,在主构造函数中声明只读属性是方便且被认为是最佳实践,如以下示例所示:
class Person(val name: String, val age: Int)
您可以选择添加数据关键字,以使编译器自动从主构造函数中声明的所有属性派生以下成员:
-
equals()
和hashCode()
-
形式为
"User(name=John, age=42)"
的toString()
-
与属性声明顺序对应的
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 constructor
。
如果你确实需要使用字段注入,可以使用 lateinit var
构造,如以下示例所示:
@Component
class YourBean {
@Autowired
lateinit var mongoTemplate: MongoTemplate
@Autowired
lateinit var solrClient: SolrClient
}
内部函数名称重整
具有 internal
可见性修饰符 的 Kotlin 函数在编译为 JVM 字节码时,其名称会被混淆,这在通过名称注入依赖项时会产生副作用。
例如,这个 Kotlin 类:
@Configuration
class SampleConfiguration {
@Bean
internal fun sampleBean() = SampleBean()
}
翻译为此 Java 表示形式的已编译 JVM 字节码:
@Configuration
@Metadata(/* ... */)
public class SampleConfiguration {
@Bean
@NotNull
public SampleBean sampleBean$demo_kotlin_internal_test() {
return new SampleBean();
}
}
因此,与之相关的 bean 名称用 Kotlin 字符串表示为 "sampleBean\$demo_kotlin_internal_test"
,而不是常规 public
函数用例中的 "sampleBean"
。确保在按名称注入此类 bean 时使用混淆后的名称,或者添加 @JvmName("sampleBean")
来禁用名称混淆。
注入配置属性
在 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()
Checked 异常
Java 和 Kotlin 异常处理 非常相似,主要区别在于 Kotlin 将所有异常视为未检查异常。然而,当使用代理对象(例如,带有 @Transactional
注解的类或方法)时,抛出的已检查异常默认会被包装在一个 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
的典型用例是将处理程序方法映射到特定路径和方法。在 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 的声明处型变,这允许在声明类型时定义型变,而在 Java 中这是不可能的,因为 Java 仅支持使用处型变。
例如,在 Kotlin 中声明 List<Foo>
在概念上等同于 java.util.List<? extends Foo>
,因为 kotlin.collections.List
被声明为 interface List<out E> : kotlin.collections.Collection<E>。
在使用 Java 类时,需要通过在泛型类型上使用 out
Kotlin 关键字来考虑这一点,例如在从 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 Jupiter(JUnit 5)允许构造函数注入 bean,这在 Kotlin 中非常有用,可以使用 val
而不是 lateinit var
。您可以使用 @TestConstructor(autowireMode = AutowireMode.ALL) 来为所有参数启用自动装配。
您还可以在 junit-platform.properties
文件中通过 spring.test.constructor.autowire.mode = all
属性将默认行为更改为 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 测试类可以使用 @TestInstance(TestInstance.Lifecycle.PER_CLASS)
注解来启用测试类的单实例化,这允许在非静态方法上使用 @BeforeAll
和 @AfterAll
注解,这非常适合 Kotlin。
您还可以在 junit-platform.properties
文件中通过 junit.jupiter.testinstance.lifecycle.default = per_class
属性将默认行为更改为 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)
}
}
}