使用 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 注解的类型会自动被打开,因为这些
注解被 @Component 元注解所注解。
某些涉及代理以及 Kotlin 编译器自动生成功能方法的用例需要特别注意。例如,一个包含属性的 Kotlin 类将生成相关的 final getter 和 setter 方法。为了能够代理这些相关方法,应优先在类型级别使用 @Component 注解,而不是在方法级别使用 @Bean,以便让 kotlin-spring 插件将这些方法设为开放。一个典型的用例是 @Scope 及其流行的 @RequestScope 特化形式。 |
start.spring.io 默认启用
kotlin-spring 插件。因此,实际上,您可以直接编写 Kotlin beans
而无需任何额外的 open 关键字,就像在 Java 中一样。
Spring Framework 文档中的 Kotlin 代码示例并未在类及其成员函数上显式指定
open。这些示例是为使用
kotlin-allopen 插件的项目编写的,因为这是最常用的设置。 |
使用不可变类实例进行持久化
在 Kotlin 中,将只读属性声明在主构造函数中是方便的,并且被认为是最佳实践,如下例所示:
class Person(val name: String, val age: Int)
您可以选择添加 关键字 data
以使编译器自动从主构造函数中声明的所有属性推导出以下成员:
-
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注解标注的类生成合成的无参构造函数。
如果您需要为其他持久化技术利用这种机制,可以配置
the 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()
}
转换为编译后的JVM字节码的以下Java表示形式:
@Configuration
@Metadata(/* ... */)
public class SampleConfiguration {
@Bean
@NotNull
public SampleBean sampleBean$demo_kotlin_internal_test() {
return new SampleBean();
}
}
因此,以 Kotlin 字符串表示的相关 bean 名称为 "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("%{")
}
您可以支持使用标准 ${…} 语法的组件(如 Spring Boot 监视器或 @LocalServerPort)与使用自定义 %{…} 语法的组件,方法是声明多个 PropertySourcesPlaceholderConfigurer bean,如下例所示:
@Bean
fun kotlinPropertyConfigurer() = PropertySourcesPlaceholderConfigurer().apply {
setPlaceholderPrefix("%{")
setIgnoreUnresolvablePlaceholders(true)
}
@Bean
fun defaultPropertyConfigurer() = PropertySourcesPlaceholderConfigurer()
此外,可以通过设置JVM系统属性(或通过SpringProperties机制)来全局更改或禁用默认的转义字符
已检查异常
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 方法。 |
声明-site 协变
在使用 Kotlin 编写的 Spring 应用程序中处理泛型类型时,某些用例可能需要理解 Kotlin 的声明处变型,它允许在声明类型时定义变型,而 Java 仅支持使用处变型,无法实现这一点。
例如,在 Kotlin 中声明 List<Foo> 在概念上等同于 java.util.List<? extends Foo>,因为
kotlin.collections.List 被声明为
interface List<out E> : kotlin.collections.Collection<E>。
在使用Java类时,例如从Kotlin类型向Java类型进行org.springframework.core.convert.converter.Converter时,需要对泛型类型使用outKotlin关键字予以考虑。
class ListOfFooConverter : Converter<List<Foo>, CustomJavaList<out Foo>> {
// ...
}
在转换任何类型的对象时,可以使用 * 的星形投影来代替 out Any。
class ListOfAnyConverter : Converter<List<*>, CustomJavaList<*>> {
// ...
}
| Spring 框架尚未利用声明点的变型类型信息来注入 Bean, 请订阅 spring-framework#22313 以跟踪相关进展。 |
测试
|
Kotlin 允许你在反引号 ( 具体示例,请参阅本节后续的 |
| 如果使用 Spring Boot,请参阅 相关文档。 |
构造器注入
如专门章节所述,
JUnit Jupiter允许构造器注入bean,这对Kotlin特别有用,
以便使用val而非lateinit var。您可以使用
@TestConstructor(autowireMode = AutowireMode.ALL)
来为所有参数启用自动装配。
您还可以在包含 spring.test.constructor.autowire.mode = all 属性的 junit-platform.properties 文件中将默认行为更改为 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 生命周期
使用JUnit Jupiter,Kotlin测试类可以使用@TestInstance(TestInstance.Lifecycle.PER_CLASS)注解来启用测试类的单例实例化,这允许在非静态方法上使用@BeforeAll和@AfterAll注解,这对于Kotlin来说是一个很好的匹配。
您还可以在包含 junit.jupiter.testinstance.lifecycle.default = per_class 属性的 junit-platform.properties 文件中将默认行为更改为 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()
}
}
类似规范的测试
您可以使用Kotlin和JUnit Jupiter的@Nested测试类支持创建类似规范的测试。以下示例展示了如何实现这一点:
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)
}
}
}