此版本仍在开发中,尚不被认为是稳定的。对于最新的稳定版本,请使用 Spring Framework 6.2.10spring-doc.cadn.net.cn

Kotlin 中的春季项目

本节提供了一些有助于开发 Spring 项目的具体提示和建议在 Kotlin 中。spring-doc.cadn.net.cn

默认情况下的最终

默认情况下,Kotlin 中的所有类和成员函数都是final. 这openmodifier 与 Java 的final:它允许其他人继承此 类。 这也适用于成员函数,因为它们需要标记为open被覆盖。spring-doc.cadn.net.cn

虽然 Kotlin 的 JVM 友好设计通常与 Spring 无摩擦,但如果不考虑这一事实,这个特定的 Kotlin 功能可能会阻止应用程序启动。这是因为Spring Bean(例如@Configuration默认情况下需要在运行时出于技术原因进行扩展的带注释的类原因)通常由 CGLIB 代理。解决方法是添加一个open关键字和成员函数,这可能会很快就会变得痛苦,并且违反了保持代码简洁和可预测的 Kotlin 原则。spring-doc.cadn.net.cn

还可以通过使用@Configuration(proxyBeanMethods = false). 看proxyBeanMethodsJavadoc了解更多详情。

幸运的是,Kotlin 提供了一个kotlin-spring插件(预配置的kotlin-allopenplugin)自动打开类及其成员函数,用于使用以下内容之一进行注释或元注释的类型 附注:spring-doc.cadn.net.cn

元注释支持意味着使用@Configuration,@Controller,@RestController,@Service@Repository会自动打开,因为这些注释是用@Component.spring-doc.cadn.net.cn

一些涉及代理和 Kotlin 编译器自动生成最终方法的用例需要额外的 关心。 例如,具有属性的 Kotlin 类将生成相关的finalgetter 和 setter。 挨次 为了能够代理相关的方法,类型级别@Component注释应优先于方法级别@Bean在 命令让这些方法由kotlin-spring插件。一个典型的用例是@Scope及其受欢迎的@RequestScope专业化。

start.spring.io 启用 这kotlin-spring插件。因此,在实践中,您可以编写 Kotlin bean而无需任何额外的open关键字,就像在 Java 中一样。spring-doc.cadn.net.cn

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

使用不可变类实例进行持久化

在 Kotlin 中,在主构造函数中声明只读属性很方便,并且被认为是最佳实践,如以下示例所示:spring-doc.cadn.net.cn

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

您可以选择添加data关键词使编译器自动从声明的所有属性派生以下成员在主构造函数中:spring-doc.cadn.net.cn

如以下示例所示,这允许轻松更改单个属性,即使Person属性是只读的:spring-doc.cadn.net.cn

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 注释的类生成合成无参数构造函数的插件。spring-doc.cadn.net.cn

如果您需要将这种机制用于其他持久性技术,您可以配置 这kotlin-noarg插件。spring-doc.cadn.net.cn

从 Kay 发布系列开始,Spring Data 支持 Kotlin 不可变类实例和 不需要kotlin-noarg插件,如果模块使用 Spring Data 对象映射 (如MongoDB、Redis、Cassandra等)。

注入依赖项

有利于构造函数注入

我们的建议是尝试使用val只读(和 不可为空)属性, 如以下示例所示:spring-doc.cadn.net.cn

@Component
class YourBean(
	private val mongoTemplate: MongoTemplate,
	private val solrClient: SolrClient
)
具有单个构造函数的类会自动自动连接其参数。 这就是为什么不需要显式的@Autowired constructor在所示示例中 以上。

如果确实需要使用字段注入,可以使用lateinit var构建 如以下示例所示:spring-doc.cadn.net.cn

@Component
class YourBean {

	@Autowired
	lateinit var mongoTemplate: MongoTemplate

	@Autowired
	lateinit var solrClient: SolrClient
}

内部函数名称 mangling

Kotlin 函数与internal 可见性修饰符具有 当编译成 JVM 字节码时,它们的名字会被破坏,这在按名称注入依赖项时会产生副作用。spring-doc.cadn.net.cn

例如,这个 Kotlin 类:spring-doc.cadn.net.cn

@Configuration
class SampleConfiguration {

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

转换为编译后的 JVM 字节码的 Java 表示:spring-doc.cadn.net.cn

@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")以禁用名称修改。spring-doc.cadn.net.cn

注入配置属性

在 Java 中,您可以使用注释(例如@Value("${property}")). 但是,在 Kotlin 中,是用于字符串插值的保留字符。$spring-doc.cadn.net.cn

因此,如果您希望使用@Valuecomments 时,您需要通过编写$@Value("\${property}").spring-doc.cadn.net.cn

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

或者,您可以通过声明 以后PropertySourcesPlaceholderConfigurer豆:spring-doc.cadn.net.cn

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

您可以支持组件(例如 Spring Boot 执行器或@LocalServerPort)使用 标准${…​}语法以及使用自定义%{…​}语法依据 声明多个PropertySourcesPlaceholderConfigurerbean,如以下示例 显示:spring-doc.cadn.net.cn

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

@Bean
fun defaultPropertyConfigurer() = PropertySourcesPlaceholderConfigurer()

此外,可以通过将 这spring.placeholder.escapeCharacter.default属性通过 JVM 系统属性(或 通过SpringProperties机制)。spring-doc.cadn.net.cn

已检查的异常

Java 和 Kotlin 异常处理非常接近,主要区别在于 Kotlin 将所有异常视为 未检查的异常。但是,当使用代理对象(例如类或方法 注释为@Transactional),抛出的已检查异常将默认包装在 一UndeclaredThrowableException.spring-doc.cadn.net.cn

要像在 Java 中那样获得抛出的原始异常,应该用@Throws显式指定抛出的检查异常(例如@Throws(IOException::class)).spring-doc.cadn.net.cn

注释数组属性

Kotlin 注释与 Java 注释大多相似,但数组属性(在 Spring 中广泛使用)的行为不同。正如 Kotlin 文档中所述,您可以省略 这valueattribute name,与其他属性不同,并将其指定为vararg参数。spring-doc.cadn.net.cn

要理解这意味着什么,请考虑@RequestMapping(这是最广泛的 使用 Spring 注释)作为示例。此 Java 注释声明如下:spring-doc.cadn.net.cn

public @interface RequestMapping {

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

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

	RequestMethod[] method() default {};

	// ...
}

典型用例@RequestMapping是将处理程序方法映射到特定路径 和方法。在 Java 中,您可以为注释数组属性指定单个值 它会自动转换为数组。spring-doc.cadn.net.cn

这就是为什么一个人可以写@RequestMapping(value = "/toys", method = RequestMethod.GET)@RequestMapping(path = "/toys", method = RequestMethod.GET).spring-doc.cadn.net.cn

但是,在 Kotlin 中,您必须编写@RequestMapping("/toys", method = [RequestMethod.GET])@RequestMapping(path = ["/toys"], method = [RequestMethod.GET])(方括号需要 使用命名数组属性指定)。spring-doc.cadn.net.cn

此特定method属性(最常见的一个)是 使用快捷方式注释,例如@GetMapping,@PostMapping,等。spring-doc.cadn.net.cn

如果@RequestMapping method属性,则所有 HTTP 方法都会 匹配,而不仅仅是GET方法。

申报地点差异

对于某些用例,在用 Kotlin 编写的 Spring 应用程序中处理泛型类型可能需要了解 Kotlin 声明站点方差,允许在声明类型时定义方差,这在仅支持 use-site 的 Java 中是不可能的 方差。spring-doc.cadn.net.cn

例如,声明List<Foo>在 Kotlin 中,在概念上等同于java.util.List<? extends Foo>因为kotlin.collections.List被声明为interface List<out E> : kotlin.collections.Collection<E>.spring-doc.cadn.net.cn

需要通过使用outKotlin 关键字, 例如,当编写org.springframework.core.convert.converter.Converter从 Kotlin 类型到 Java 类型。spring-doc.cadn.net.cn

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

转换任何类型的对象时,可以使用星形投影代替*out Any.spring-doc.cadn.net.cn

class ListOfAnyConverter : Converter<List<*>, CustomJavaList<*>> {
	// ...
}
Spring Framework 尚未利用声明站点方差类型信息来注入 bean, 订阅spring-framework#22313跟踪相关 进展。

测试

本节介绍结合使用 Kotlin 和 Spring Framework 进行测试。 推荐的测试框架是 JUnitMockk 进行模拟。spring-doc.cadn.net.cn

如果您使用的是 Spring Boot,请参阅此相关文档

构造函数注入

专用部分所述, JUnit Jupiter 允许 bean 的构造函数注入,这在 Kotlin 中非常有用 为了使用val而不是lateinit var.您可以使用@TestConstructor(autowireMode = AutowireMode.ALL)为所有参数启用自动布线。spring-doc.cadn.net.cn

您还可以将默认行为更改为ALLjunit-platform.properties文件,并带有spring.test.constructor.autowire.mode = 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,Kotlin 测试类可以使用`@TestInstance(TestInstance.Lifecycle.PER_CLASS)注释,以启用测试类的单个实例化,这允许使用@BeforeAll@AfterAll非静态方法的注释,非常适合 Kotlin。spring-doc.cadn.net.cn

您还可以将默认行为更改为PER_CLASSjunit-platform.properties文件,并带有junit.jupiter.testinstance.lifecycle.default = per_class财产。

以下示例演示了@BeforeAll@AfterAll非静态方法的注释:spring-doc.cadn.net.cn

@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测试 类支持。以下示例显示了如何执行此作:spring-doc.cadn.net.cn

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