本节提供了一些值得开发 Spring 项目的具体提示和建议 在 Kotlin 中。
默认为 Final
默认情况下,Kotlin 中的所有类和成员函数都是 final。
类上的修饰符与 Java 的相反:它允许其他人从这个
类。这也适用于成员函数,因为它们需要标记为要重写。openfinalopen
虽然 Kotlin 的 JVM 友好设计通常与 Spring 没有摩擦,但这个特定的 Kotlin 功能
如果不考虑这一事实,则可以阻止应用程序启动。这是因为
Spring bean(例如带注释的类,默认情况下需要在运行时扩展,以便技术
原因)通常由 CGLIB 代理。解决方法是在每个类上添加一个关键字,然后
member 函数,该函数可以
很快就会变得痛苦,并且违背了保持代码简洁和可预测的 Kotlin 原则。@Configurationopen
也可以使用 CGLIB 代理来避免配置类的 CGLIB 代理。
有关更多详细信息,请参见 proxyBeanMethods Javadoc。@Configuration(proxyBeanMethods = false) | 
幸运的是,Kotlin 提供了一个 kotlin-spring 插件(插件的预配置版本),可以自动打开类
及其成员函数,适用于使用以下项之一进行批注或元批注的类型
附注:kotlin-allopen
- 
@Component - 
@Async - 
@Transactional - 
@Cacheable 
元注释支持意味着使用 、 或 注释的类型会自动打开,因为这些
注释使用 .@Configuration@Controller@RestController@Service@Repository@Component
一些涉及代理和 Kotlin 编译器自动生成 final 方法的用例需要额外的
关心。例如,具有属性的 Kotlin 类将生成相关的 getter 和 setter。挨次
为了能够代理相关方法,类型级别注释应该优先于
order 来让插件打开这些方法。一个典型的用例是 及其流行的专业化。final@Component@Beankotlin-spring@Scope@RequestScope | 
start.spring.io 启用
插件。因此,在实践中,您可以编写 Kotlin bean
没有任何其他关键字,就像在 Java 中一样。kotlin-springopen
Spring Framework 文档中的 Kotlin 代码示例未明确指定类及其成员函数。这些示例是为项目编写的
使用插件,因为这是最常用的设置。openkotlin-allopen | 
也可以使用 CGLIB 代理来避免配置类的 CGLIB 代理。
有关更多详细信息,请参见 proxyBeanMethods Javadoc。@Configuration(proxyBeanMethods = false) | 
一些涉及代理和 Kotlin 编译器自动生成 final 方法的用例需要额外的
关心。例如,具有属性的 Kotlin 类将生成相关的 getter 和 setter。挨次
为了能够代理相关方法,类型级别注释应该优先于
order 来让插件打开这些方法。一个典型的用例是 及其流行的专业化。final@Component@Beankotlin-spring@Scope@RequestScope | 
Spring Framework 文档中的 Kotlin 代码示例未明确指定类及其成员函数。这些示例是为项目编写的
使用插件,因为这是最常用的设置。openkotlin-allopen | 
使用不可变类实例实现持久性
在 Kotlin 中,声明只读属性非常方便,并且被认为是一种最佳做法 在 Primary 构造函数中,如以下示例所示:
class Person(val name: String, val age: Int)
您可以选择添加 data 关键字,以使编译器自动从声明的所有属性中派生以下成员
在 Primary 构造函数中:
- 
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)需要默认构造函数,从而防止这种情况
一种设计。幸运的是,这个 “default constructor hell” 有一个解决方法,
因为 Kotlin 提供了一个 kotlin-jpa 插件,该插件为使用 JPA 注释注释的类生成合成 no-arg 构造函数。
如果需要将这种机制用于其他持久性技术,则可以配置
kotlin-noarg 插件。
从 Kay 版本系列开始, Spring Data 支持 Kotlin 不可变类实例和
如果模块使用 Spring Data 对象映射,则不需要插件
(例如 MongoDB、Redis、Cassandra 等)。kotlin-noarg | 
从 Kay 版本系列开始, Spring Data 支持 Kotlin 不可变类实例和
如果模块使用 Spring Data 对象映射,则不需要插件
(例如 MongoDB、Redis、Cassandra 等)。kotlin-noarg | 
注入依赖项
偏爱构造函数注入
我们的建议是尝试使用只读(并且
non-nullable when possible) 属性、
如下例所示:val
@Component
class YourBean(
	private val mongoTemplate: MongoTemplate,
	private val solrClient: SolrClient
)
具有单个构造函数的类会自动自动装配其参数。
这就是为什么在所示的示例中不需要显式
以上。@Autowired constructor | 
如果你真的需要使用字段注入,你可以使用 construct
如下例所示:lateinit var
@Component
class YourBean {
	@Autowired
	lateinit var mongoTemplate: MongoTemplate
	@Autowired
	lateinit var solrClient: SolrClient
}
内部函数名称修饰
带有 visibility 修饰符的 Kotlin 函数具有
它们的名称在编译为 JVM 字节码时被破坏,这在按名称注入依赖项时会产生副作用。internal
例如,以下 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 名称为 ,
而不是常规函数用例。确保在注入时使用 mangled 名称
这样的 bean by name,或者 add 来禁用名称修饰。"sampleBean\$demo_kotlin_internal_test""sampleBean"public@JvmName("sampleBean")
具有单个构造函数的类会自动自动装配其参数。
这就是为什么在所示的示例中不需要显式
以上。@Autowired constructor | 
注入配置属性
在 Java 中,您可以使用注释(例如 )来注入配置属性。
但是,在 Kotlin 中,是用于字符串插值的保留字符。@Value("${property}")$
因此,如果您希望在 Kotlin 中使用注释,则需要通过编写 .@Value$@Value("\${property}")
如果您使用 Spring Boot,则可能应该使用 @ConfigurationProperties 而不是 Comments。@Value | 
或者,您可以通过声明 以下配置 bean:
@Bean
fun propertyConfigurer() = PropertySourcesPlaceholderConfigurer().apply {
	setPlaceholderPrefix("%{")
}
您可以自定义现有代码(例如 Spring Boot 执行器或 )
使用带有配置 bean 的语法,如下例所示:@LocalServerPort${…}
@Bean
fun kotlinPropertyConfigurer() = PropertySourcesPlaceholderConfigurer().apply {
	setPlaceholderPrefix("%{")
	setIgnoreUnresolvablePlaceholders(true)
}
@Bean
fun defaultPropertyConfigurer() = PropertySourcesPlaceholderConfigurer()
如果您使用 Spring Boot,则可能应该使用 @ConfigurationProperties 而不是 Comments。@Value | 
检查的异常
Java 和 Kotlin 异常处理非常接近,主要区别在于 Kotlin 将所有异常视为
unchecked 异常。但是,当使用代理对象(例如类或方法
annotated with ),则默认情况下,抛出的选中异常将包装在
一。@TransactionalUndeclaredThrowableException
要像在 Java 中一样引发原始异常,方法应该用 @Throws 进行注释,以明确指定引发的检查异常(例如 )。@Throws(IOException::class)
注释数组属性
Kotlin 注解与 Java 注解大体相似,但数组属性(即
在 Spring 中广泛使用)的行为不同。如 Kotlin 文档中所述,您可以省略
属性名称,并将其指定为参数。valuevararg
要理解这意味着什么,请考虑(这是最广泛的
使用 Spring 注解)作为示例。此 Java 注释的声明如下:@RequestMapping
public @interface RequestMapping {
	@AliasFor("path")
	String[] value() default {};
	@AliasFor("value")
	String[] path() default {};
	RequestMethod[] method() default {};
	// ...
}
的典型用例是将处理程序方法映射到特定路径
和方法。在 Java 中,您可以为 annotation 数组属性指定单个值
它会自动转换为数组。@RequestMapping
这就是为什么可以写 或 .@RequestMapping(value = "/toys", method = RequestMethod.GET)@RequestMapping(path = "/toys", method = RequestMethod.GET)
但是,在 Kotlin 中,您必须编写 or(方括号需要
使用命名数组属性指定)。@RequestMapping("/toys", method = [RequestMethod.GET])@RequestMapping(path = ["/toys"], method = [RequestMethod.GET])
此特定属性(最常见的属性)的替代方法是
使用快捷方式注释,例如 、 和其他注释。method@GetMapping@PostMapping
如果未指定该属性,则所有 HTTP 方法都将
匹配,而不仅仅是 method。@RequestMappingmethodGET | 
如果未指定该属性,则所有 HTTP 方法都将
匹配,而不仅仅是 method。@RequestMappingmethodGET | 
声明 - 地点差异
对于某些用例,在用 Kotlin 编写的 Spring 应用程序中处理泛型类型可能需要理解 Kotlin declaration-site variance,允许在声明类型时定义 variance,这在仅支持 use-site 的 Java 中是不可能的 方差。
例如,在 Kotlin 中声明在概念上等同于 because 声明为接口 List<out E> : kotlin.collections.Collection<E>。List<Foo>java.util.List<? extends Foo>kotlin.collections.List
在使用 Java 类时,需要在泛型类型上使用 Kotlin 关键字来考虑这一点。
例如,将 a 从 Kotlin 类型写入 Java 类型时。outorg.springframework.core.convert.converter.Converter
class ListOfFooConverter : Converter<List<Foo>, CustomJavaList<out Foo>> {
    // ...
}
转换任何类型的对象时,可以使用 星形投影 with 代替 。*out Any
class ListOfAnyConverter : Converter<List<*>, CustomJavaList<*>> {
    // ...
}
| Spring 框架尚未利用 declaration-site variance 类型信息来注入 bean, 订阅 spring-framework#22313 以跟踪相关 进展。 | 
| Spring 框架尚未利用 declaration-site variance 类型信息来注入 bean, 订阅 spring-framework#22313 以跟踪相关 进展。 | 
测试
| 如果您使用的是 Spring Boot,请参阅此相关文档。 | 
构造函数注入
如专门部分所述,
JUnit Jupiter (JUnit 5) 允许 bean 的构造函数注入,这对 Kotlin 非常有用
以便使用 而不是 .您可以使用 @TestConstructor(autowireMode = AutowireMode.ALL) 为所有参数启用自动装配。vallateinit var
您还可以将默认行为更改为 in a file with a property。ALLjunit-platform.propertiesspring.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 (JUnit 5),Kotlin 测试类可以使用注解来启用测试类的单个实例化,从而允许在非静态方法上使用 AND 注解,这非常适合 Kotlin。`@TestInstance(TestInstance.Lifecycle.PER_CLASS)@BeforeAll@AfterAll
您还可以将默认行为更改为 in a file with a property。PER_CLASSjunit-platform.propertiesjunit.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)
     }
  }
}
| 如果您使用的是 Spring Boot,请参阅此相关文档。 | 
您还可以将默认行为更改为 in a file with a property。ALLjunit-platform.propertiesspring.test.constructor.autowire.mode = all | 
您还可以将默认行为更改为 in a file with a property。PER_CLASSjunit-platform.propertiesjunit.jupiter.testinstance.lifecycle.default = per_class |