Spring框架参考手册 --- Kotlin的语言支持

原文地址

Kotlin是一种针对JVM(和其他平台)的静态类型语言,它允许编写简洁优雅的代码,同时与用Java编写的现有库提供非常好的互操作性。

Spring Framework为Kotlin提供了一流的支持,让开发人员编写Kotlin应用程序,就好像Spring Framework是原生的Kotlin框架一样。除了Java之外,参考文档的大多数代码示例都是用Kotlin编写的。

使用Kotlin构建Spring应用程序的最简单方法是利用Spring Boot及其专用的Kotlin支持。这个全面的教程将教你如何使用start.spring.io用Kotlin构建Spring Boot应用程序。

必要条件

Spring Framework支持Kotlin 1.7+,并要求类路径上存在Kotlin-stdlib和Kotlin-reflect。如果您在start.spring.io上引导Kotlin项目,则默认情况下会提供它们。

Jackson Kotlin模块是使用Jackson序列化或反序列化Kotlin类的JSON数据所必需的,因此,如果您有这样的需要,请确保将com.fasterxml.jjackson.module:Jackson模块Kotlin依赖项添加到您的项目中。当在类路径中找到它时,它会自动注册。

扩展

Kotlin扩展提供了用附加功能扩展现有类的能力。Spring Framework Kotlin API使用这些扩展为现有的Spring API添加新的特定于Kotlin的便利功能。

Spring Framework KDoc API列出并记录了所有可用的Kotlin扩展和DSL。

请记住,需要导入Kotlin扩展才能使用。例如,这意味着只有导入org.springframework.context.support.registerBean时,GenericApplicationContext.registerBean的Kotlin扩展才可用。也就是说,与静态导入类似,在大多数情况下,IDE应该会自动建议导入。

例如, Kotlin提供reified关键字为JVM泛型类型擦除(Type Erasure)提供了一种解决方案,Spring框架提供了一些扩展来利用这一特性。这允许更好的Kotlin API RestTemplate,用于Spring WebFlux的新WebClient,以及其他各种API。

其他库,如Reactor和Spring Data,也为其API提供了Kotlin扩展,从而提供了更好的整体Kotlin开发体验。

Type Erasure — 泛型被引入Java语言,以在编译时提供更严格的类型检查,并支持泛型编程。为了实现泛型,Java编译器将类型擦除应用于:

  • 将泛型类型中的所有类型参数替换为其Bound,如果类型参数无Bound,则使用Object。因此,生成的字节码只包含普通的类、接口和方法。
  • 如有必要,请插入类型强制转换以保护类型安全性。
  • 生成桥接方法以保留扩展泛型类型中的多态性。

类型擦除确保不会为参数化类型创建新的类;因此,泛型不会产生运行时开销。

要在Java中检索User对象的列表,通常需要编写以下内容:

Flux<User> users  = client.get().retrieve().bodyToFlux(User.class)

使用Kotlin和Spring Framework扩展,您可以编写以下内容:

val users = client.get().retrieve().bodyToFlux<User>()
// 或(两者等效)
val users : Flux<User> = client.get().retrieve().bodyToFlux()

与Java一样,Kotlin中的用户是强类型的,但Kotlin巧妙的类型推理允许更短的语法。

Null-safety

Kotlin的关键特性之一是null-safety,它在编译时干净利落地处理空值,而不是在运行时碰到著名的NullPointerException。通过可空性声明和表达“值或无值”语义,而无需支付包装器(如Optional)的成本,这使得应用程序更安全。(Kotlin允许使用可空值的函数结构。请参阅Kotlin null-safety的综合指南。)

虽然Java不允许在其类型系统中表达null-safety,但Spring Framework通过org.springframework.lang包中声明的工具友好型注释,提供了整个Spring Framework API的null-safety。默认情况下,Kotlin中使用的Java API中的类型被识别为平台类型,因此可以放宽null检查。Kotlin对JSR-305注释和Spring nullability注释的支持为Kotlin开发人员提供了整个Spring Framework API的null-safety,其优点是在编译时处理与null相关的问题。

Reactor或Spring Data等库提供了空安全API来利用此功能。

您可以通过添加带有以下选项的-Xjsr305编译器标志来配置JSR-305检查:-Xjsr305={strict|warn|ignore}。

对于kotlin 1.1+版本,默认行为与-Xjsr305=warn相同。strict值需要在从Spring API推断的Kotlin类型中考虑Spring Framework API的null-safety,但使用时要知道,即使在小版本之间,Spring API的nullability 声明也可能发生变化,并且将来可能会添加更多检查。

目前还不支持泛型类型参数、可变参数和数组元素的nullability ,但应该在即将发布的版本中支持。

类与接口

Spring Framework支持各种Kotlin构造,例如通过主构造函数实例化Kotlin类、不可变类数据绑定和具有默认值的函数可选参数。

Kotlin参数名是通过专用的KotlinReflectionParameterNameDiscoverer识别的,它允许查找接口方法参数名,而不需要在编译期间启用Java 8 -parameters编译器标志。(为了完整起见,我们仍然建议使用带有-java-parameters标志的Kotlin编译器来公开标准Java参数。)

可以将配置类声明为顶级类或嵌套类,但不能声明为内部类,因为后者需要对外部类的引用。

注释

Spring框架还利用Kotlin null-safety来确定是否需要HTTP参数,而无需显式定义所需属性。这意味着@RequestParam name: String?被视为非必需的,相反,@RequestParam name: String被视为必需的。Spring Messaging @Header注释也支持这个特性。

以类似的方式,带有@Autowired、@bean或@Inject的Spring bean注入使用这些信息来确定是否需要bean。

例如,@Autowired lateinit var thing: Thing意味着类型为Thing的bean必须在应用程序上下文中注册,而如果不存在此类bean,@Autowired lateinit var thing: Thing?则不会引发错误。

遵循相同的原则,@Bean fun play(toy: Toy, car: Car?) = Baz(toy, Car)意味着Toy类型的bean必须在应用程序上下文中注册,而Car类型bean可能存在,也可能不存在。同样的行为也适用于自动装配的构造函数参数。

如果对具有属性或主构造函数参数的类使用bean验证,则可能需要annotation use-site targets,如@field:NotNull@get:Size(min=5,max=15)

annotation use-site targets
当您注释一个属性或主构造函数参数时,有多个Java元素是从相应的Kotlin元素生成的,因此在生成的Java字节码中有多个可能的注释位置。要指定应如何准确生成注释,请使用以下语法:

class Example(@field:Ann val foo,    // 注释Java字段
              @get:Ann val bar,      // 注释Java getter
              @param:Ann val quux)   // 注释Java构造函数参数

可以使用相同的语法对整个文件进行注释。要做到这一点,在文件的顶层,在package指令之前,或者在所有导入文件之前(如果文件在默认包中)放一个带有目标文件的注释:

@file:JvmName("Foo")

package org.jetbrains.demo

如果你有多个具有相同目标的注释,你可以通过在目标后面添加括号并将所有注释放入括号内来避免重复目标:

class Example {
     @set:[Inject VisibleForTesting]
     var collaborator: Collaborator
}

支持的use-site targets的完整列表为:

  • file
  • property(具有此目标的注释对Java不可见)
  • field
  • get(getter属性)
  • set(setter属性)
  • receiver (扩展函数或属性的接收器参数)
  • param (构造函数参数)
  • setparam (属性setter参数)
  • delegate (存储委派属性的委派实例的字段)

要注释扩展函数的接收器参数,请使用以下语法:

fun @receiver:Fancy String.myExtension() { ... }

如果未指定use-site target,则会根据正在使用的注释的@target注释来选择目标。如果存在多个适用目标,则使用以下列表中的第一个适用目标:

  • param
  • property
  • field

Bean定义DSL

Spring Framework通过使用lambdas作为XML或Java配置(@configuration和@Bean)的替代品,支持以功能性的方式注册Bean。简而言之,它允许您使用充当FactoryBean的lambda注册bean。这种机制非常有效,因为它不需要任何反射或CGLIB代理。

例如,在Java中,您可以编写以下内容:

class Foo {}

class Bar {
	private final Foo foo;
	public Bar(Foo foo) {
		this.foo = foo;
	}
}

GenericApplicationContext context = new GenericApplicationContext();
context.registerBean(Foo.class);
context.registerBean(Bar.class, () -> new Bar(context.getBean(Foo.class)));

在Kotlin中,使用reified 类型参数和GenericApplicationContext的Kotlin扩展,您可以编写以下内容:

class Foo

class Bar(private val foo: Foo)

val context = GenericApplicationContext().apply {
	registerBean<Foo>()
	registerBean { Bar(it.getBean()) }
}

当类Bar只有一个构造函数时,您甚至可以只指定bean类,构造函数参数将按类型自动装配:

val context = GenericApplicationContext().apply {
	registerBean<Foo>()
	registerBean<Bar>()
}

为了实现更具声明性的方法和更干净的语法,Spring Framework提供了Kotlin bean定义DSL。它通过一个干净的声明性API声明了ApplicationContextInitializer,使您能够处理概要文件和环境,以自定义bean的注册方式。

在以下示例中,请注意:

  • 类型推断通常允许避免为ref(“bazBean”)等bean引用指定类型
  • 在本例中,可以使用Kotlin顶级函数用于可调用引用(如bean(::myRouter))来声明bean
  • 当指定bean<Bar>()bean(::myRouter)时,参数是按类型自动装配的
  • 只有当foobar profile处于活动状态时,才会注册FooBar bean
class Foo
class Bar(private val foo: Foo)
class Baz(var message: String = "")
class FooBar(private val baz: Baz)

val myBeans = beans {
	bean<Foo>()
	bean<Bar>()
	bean("bazBean") {
		Baz().apply {
			message = "Hello world"
		}
	}
	profile("foobar") {
		bean { FooBar(ref("bazBean")) }
	}
	bean(::myRouter)
}

fun myRouter(foo: Foo, bar: Bar, baz: Baz) = router {
	// ...
}

这个DSL是程序化的,这意味着它允许通过if表达式、for循环或任何其他Kotlin构造自定义bean的注册逻辑。

然后,您可以使用此beans()函数在应用程序上下文中注册beans,如下例所示:

val context = GenericApplicationContext().apply {
	myBeans.initialize(this)
	refresh()
}

Web

路由器DSL

Spring Framework附带Kotlin路由器DSL,有三种风格:

  • WebMvc.fn DSL with router { }
  • WebFlux.fn Reactive DSL with router { }
  • WebFlux.fn Coroutines DSL with coRouter { }

这些DSL允许您编写干净且惯用的Kotlin代码来构建RouterFunction实例,如下例所示:

@Configuration
class RouterRouterConfiguration {

	@Bean
	fun mainRouter(userHandler: UserHandler) = router {
		accept(TEXT_HTML).nest {
			GET("/") { ok().render("index") }
			GET("/sse") { ok().render("sse") }
			GET("/users", userHandler::findAllView)
		}
		"/api".nest {
			accept(APPLICATION_JSON).nest {
				GET("/users", userHandler::findAll)
			}
			accept(TEXT_EVENT_STREAM).nest {
				GET("/users", userHandler::stream)
			}
		}
		resources("/**", ClassPathResource("static/"))
	}
}

这个DSL是程序化的,这意味着它允许通过if表达式、for循环或任何其他Kotlin构造自定义bean的注册逻辑。当您需要根据动态数据(例如,来自数据库)注册路由时,这可能很有用。

MockMvc DSL

Kotlin DSL通过MockMvc Kotlin扩展提供,以便提供更惯用的Kotlin API并允许更好的发现性(不使用静态方法)。

val mockMvc: MockMvc = ...
mockMvc.get("/person/{name}", "Lee") {
	secure = true
	accept = APPLICATION_JSON
	headers {
		contentLanguage = Locale.FRANCE
	}
	principal = Principal { "foo" }
}.andExpect {
	status { isOk }
	content { contentType(APPLICATION_JSON) }
	jsonPath("$.name") { value("Lee") }
	content { json("""{"someBoolean": false}""", false) }
}.andDo {
	print()
}

Kotlin脚本模板

Spring Framework提供了一个ScriptTemplateView,它支持JSR-223通过使用脚本引擎来渲染模板。

通过利用scripting-jsr223依赖项,可以使用这样的功能来使用Kotlin.html DSL或Kotlin多行插值字符串来渲染基于Kotlin的模板。
build.gradle.kts

dependencies {
        runtime("org.jetbrains.kotlin:kotlin-scripting-jsr223:${kotlinVersion}")
}

配置通常使用ScriptTemplateConfigurer和ScriptTemplateViewResolver bean完成。
KotlinScriptConfiguration.kt

@Configuration
class KotlinScriptConfiguration {

    @Bean
	fun kotlinScriptConfigurer() = ScriptTemplateConfigurer().apply {
		engineName = "kotlin"
		setScripts("scripts/render.kts")
		renderFunction = "render"
		isSharedEngine = false
	}

    @Bean
    fun kotlinScriptViewResolver() = ScriptTemplateViewResolver().apply {
        setPrefix("templates/")
        setSuffix(".kts")
    }
}

Kotlin多平台序列化

Spring MVC、Spring WebFlux和Spring Messaging(RSocket)支持Kotlin多平台序列化。内置支持目前针对CBOR、JSON和ProtoBuf格式。

要启用它,请按照这些说明添加相关的依赖项和插件。对于Spring MVC和WebFlux,如果Kotlin序列化和Jackson都在类路径中,则默认情况下会对它们进行配置,因为Kotlin串行化旨在仅串行化用@Serializable注释的Kotlin类。使用Spring Messaging(RSocket),如果您想要自动配置,如果需要Jackson手动配置KotlinSerializationJsonMessageConverter,请确保Jackson、GSON或JSONB都不在类路径中。

协程

Kotlin Coroutines是Kotlin轻量级线程,允许以命令式方式编写非阻塞代码。在语言方面,挂起函数为异步操作提供了抽象,而在库方面,kotlinx.coroutines提供了async{}等函数和Flow等类型。

Spring Framework在以下范围内为Coroutines提供支持:

  • Spring MVC和WebFlux注释的@Controller中支持Deferred和Flow返回值
  • 在Spring MVC和WebFlux中对挂起函数的支持
  • WebFlux客户端和服务器功能API的扩展。
  • WebFlux.fn coRouter { } DSL
  • WebFlux CoWebFilter
  • RSocket@MessageMapping注释方法中的挂起函数和Flow 支持
  • RSocketRequester的扩展
  • Spring AOP

依赖项

当kotlinx-coroutines-core和kotlinx-coroutines-reactor依赖项在类路径中时,启用协程支持:
build.gradle.kts

dependencies {

	implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${coroutinesVersion}")
	implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:${coroutinesVersion}")
}

支持1.4.0及以上版本。

Reactive如何转化为Coroutines?

对于返回值,从Reactive到Coroutines API的转换如下:

  • fun handler(): Mono<Void>变为suspend fun handler()
  • fun handler(): Mono<T>变为suspend fun handler(): Tsuspend fun handler(): T?取决于Mono是否为空(具有更静态类型的优势)
  • fun handler(): Flux<T>变为fun handler(): Flow<T>

对于输入参数:

  • 如果不需要惰性,fun handler(mono: Mono<T>)将变为fun handler(value: T),因为可以调用挂起的函数来获取值参数。
  • 如果需要懒惰,fun handler(mono: Mono<T>)变成fun handler(supplier: suspend () → T)fun handler(supplier: suspend () → T?)

Flow在Coroutines世界中相当于Flux,适用于热流或冷流、有限流或无限流,主要区别如下:

  • Flow为push-based,Flux为push-pull混合型
  • Backpressure 通过挂起函数实现
  • Flow只有一个挂起的collect方法,运算符作为扩展实现
  • 由于有了Coroutines,运算符易于实现
  • 扩展允许将自定义运算符添加到Flow
  • Collect 操作是挂起函数
  • map操作符支持异步操作(不需要flatMap),因为它需要一个挂起的函数参数

阅读这篇关于Going Reactive with Spring、Coroutines和Kotlin Flow的博客文章,了解更多详细信息,包括如何与Corousines并行运行代码。

Controllers

这里是一个Coroutines@RestController的例子。

@RestController
class CoroutinesRestController(client: WebClient, banner: Banner) {

	@GetMapping("/suspend")
	suspend fun suspendingEndpoint(): Banner {
		delay(10)
		return banner
	}

	@GetMapping("/flow")
	fun flowEndpoint() = flow {
		delay(10)
		emit(banner)
		delay(10)
		emit(banner)
	}

	@GetMapping("/deferred")
	fun deferredEndpoint() = GlobalScope.async {
		delay(10)
		banner
	}

	@GetMapping("/sequential")
	suspend fun sequential(): List<Banner> {
		val banner1 = client
				.get()
				.uri("/suspend")
				.accept(MediaType.APPLICATION_JSON)
				.awaitExchange()
				.awaitBody<Banner>()
		val banner2 = client
				.get()
				.uri("/suspend")
				.accept(MediaType.APPLICATION_JSON)
				.awaitExchange()
				.awaitBody<Banner>()
		return listOf(banner1, banner2)
	}

	@GetMapping("/parallel")
	suspend fun parallel(): List<Banner> = coroutineScope {
		val deferredBanner1: Deferred<Banner> = async {
			client
					.get()
					.uri("/suspend")
					.accept(MediaType.APPLICATION_JSON)
					.awaitExchange()
					.awaitBody<Banner>()
		}
		val deferredBanner2: Deferred<Banner> = async {
			client
					.get()
					.uri("/suspend")
					.accept(MediaType.APPLICATION_JSON)
					.awaitExchange()
					.awaitBody<Banner>()
		}
		listOf(deferredBanner1.await(), deferredBanner2.await())
	}

	@GetMapping("/error")
	suspend fun error() {
		throw IllegalStateException()
	}

	@GetMapping("/cancel")
	suspend fun cancel() {
		throw CancellationException()
	}

}

还支持使用@Controller进行视图渲染。

@Controller
class CoroutinesViewController(banner: Banner) {

	@GetMapping("/")
	suspend fun render(model: Model): String {
		delay(10)
		model["banner"] = banner
		return "index"
	}
}

WebFlux.fn

以下是通过coRouter{}DSL和相关处理程序定义的Coroutines路由器的示例。

@Configuration
class RouterConfiguration {

	@Bean
	fun mainRouter(userHandler: UserHandler) = coRouter {
		GET("/", userHandler::listView)
		GET("/api/user", userHandler::listApi)
	}
}
class UserHandler(builder: WebClient.Builder) {

	private val client = builder.baseUrl("...").build()

	suspend fun listView(request: ServerRequest): ServerResponse =
			ServerResponse.ok().renderAndAwait("users", mapOf("users" to
			client.get().uri("...").awaitExchange().awaitBody<User>()))

	suspend fun listApi(request: ServerRequest): ServerResponse =
				ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyAndAwait(
				client.get().uri("...").awaitExchange().awaitBody<User>())
}

Transactions

通过Spring Framework 5.2中提供的Reactive事务管理的程序变体来支持Coroutines上的事务。

为了挂起函数,提供了TransactionalOperator.executeAndAwait扩展。

import org.springframework.transaction.reactive.executeAndAwait

class PersonRepository(private val operator: TransactionalOperator) {

    suspend fun initDatabase() = operator.executeAndAwait {
        insertPerson1()
        insertPerson2()
    }

    private suspend fun insertPerson1() {
        // INSERT SQL statement
    }

    private suspend fun insertPerson2() {
        // INSERT SQL statement
    }
}

对于Kotlin Flow,提供了Flow<T>.transactional扩展。

import org.springframework.transaction.reactive.transactional

class PersonRepository(private val operator: TransactionalOperator) {

    fun updatePeople() = findPeople().map(::updatePerson).transactional(operator)

    private fun findPeople(): Flow<Person> {
        // SELECT SQL statement
    }

    private suspend fun updatePerson(person: Person): Person {
        // UPDATE SQL statement
    }
}

Kotlin中的Spring项目

本节为在Kotlin开发Spring项目提供了一些具体的提示和建议。

默认Final

默认情况下,Kotlin中的所有类都是final。类上的open修饰符与Java的final相反:它允许其他人从这个类继承。这也适用于成员函数,因为它们需要标记为open 才能被重写。

虽然Kotlin的JVM-friendly设计通常与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进行元注释的。

start.spring.io默认启用kotlin-spring插件。因此,在实践中,您可以像在Java中一样,在没有任何额外的open关键字的情况下编写Kotlin-beans。

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

使用不可变类实例实现持久性

在Kotlin中,在主构造函数中声明只读属性是方便的,并且被认为是最佳实践,如以下示例所示:

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

您可以选择添加data关键字,使编译器自动从主构造函数中声明的所有属性派生以下成员:

  • 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)需要默认构造函数,从而阻止了这种设计。幸运的是,有一个解决这个“default constructor hell”的方法,因为Kotlin提供了一个Kotlin - JPA插件,可以为带有JPA注释的类生成合成的无参数构造函数。

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

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

注入依赖项

支持构造函数注入

我们的建议是尝试使用val只读(并且在可能的情况下不可为null)属性来支持构造函数注入,如下例所示:

@Component
class YourBean(
	private val mongoTemplate: MongoTemplate,
	private val solrClient: SolrClient
)

具有单个构造函数的类会自动装配其参数。这就是为什么在上面显示的示例中不需要显式的@Autowired构造函数。

如果您确实需要使用字段注入,可以使用lateinit-var构造,如下例所示:

@Component
class YourBean {

	@Autowired
	lateinit var mongoTemplate: MongoTemplate

	@Autowired
	lateinit var solrClient: SolrClient
}

内部函数Name Mangling

带有内部可见性修饰符的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”,而不是常规公共函数用例的“sampleBean”。在按名称注入这种bean时,请确保使用修改过的名称,或者添加@JvmName(“sampleBean”)来禁用名称修改。

注入配置属性

在Java中,可以使用注释(如@Value(“ p r o p e r t y ”))注入配置属性。但是,在 K o t l i n 中, {property}”))注入配置属性。但是,在Kotlin中, property))注入配置属性。但是,在Kotlin中,是用于字符串插值的保留字符。

因此,如果您希望在Kotlin中使用@Value注释,则需要通过编写@Value(“\${property}”)来转义$字符。

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

作为替代方案,您可以通过声明以下配置bean来自定义属性占位符前缀:

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

您可以使用配置bean自定义使用${…}语法的现有代码(例如Spring Boot actuators或@LocalServerPort),如下面的示例所示:

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

@Bean
fun defaultPropertyConfigurer() = PropertySourcesPlaceholderConfigurer()

Checked Exceptions

Java和Kotlin异常处理非常接近,主要区别在于Kotlin将所有异常视为未检查的异常。然而,当使用代理对象(例如用@Transactional注释的类或方法)时,抛出的检查异常默认情况下将包装在UndeclaredThrowableException中。

要获得像Java中那样抛出的原始异常,方法应该用@Throws进行注释,以显式指定抛出的检查异常(例如@Throws(IOException::class))。

注释数组属性

Kotlin注释大多类似于Java注释,但数组属性(在Spring中广泛使用)的行为不同。正如Kotlin文档中所解释的,与其他属性不同,您可以省略值属性名称,并将其指定为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方法。

Declaration-site variance

在一些用例中,处理用Kotlin编写的Spring应用程序中的泛型类型可能需要理解Kotlin declaration-site variance,它允许在声明类型时定义差异,而这在只支持use-site variance的Java中是不可能的。

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

当使用Java类时,需要通过在泛型类型上使用out Kotlin关键字来考虑这一点,例如,当将org.springframework.core.convert.converter.Converter从Kotlin类型写入Java类型时。

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

当转换任何类型的对象时,可以使用带有*的star projection,而不是out any。

class ListOfAnyConverter : Converter<List<*>, CustomJavaList<*>> {
    // ...
}

Spring Framework还没有利用declaration-site variance类型的信息来注入bean。

Testing

本节介绍了Kotlin和Spring Framework的组合测试。推荐的测试框架是JUnit 5和Mockk。

虽然可以使用JUnit4来测试Kotlin代码,但JUnit5是默认提供的,建议使用。JUnit5允许一个测试类被实例化一次,并为该类的所有测试重用。这使得在非静态方法上使用@BeforeAll和@AfterAll注释成为可能,这非常适合Kotlin。

对于mock Kotlin classes,建议使用MockK。如果您需要MockK等效于Mockito特定的@MockBean和@SpyBean注释,您可以使用SpringMockK,它提供类似的@Mock Bean和@SpykBean注释。

构造器注入

如专用部分所述,JUnit Jupiter(JUnit 5)允许构造函数注入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生命周期

Kotlin允许您在反引号(')之间指定有意义的测试函数名。在JUnit Jupiter (JUnit 5)中,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()
  }
}

Specification-like测试

您可以使用JUnit 5和Kotlin创建specification-like测试。以下示例显示了如何执行此操作:

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

入门

学习如何使用Kotlin构建Spring应用程序的最简单方法是查看教程

start.spring.io

在Kotlin中启动新的Spring Framework项目的最简单方法是在start.Spring.io上创建一个新的Spring Boot项目。

选择Web Flavor

Spring Framework有两个不同的web堆栈:Spring MVC和Spring WebFlux。

如果您想创建处理延迟、长期连接或流场景的应用程序,建议使用Spring WebFlux。

对于其他用例,特别是当您使用诸如JPA之类的阻塞技术时,建议选择SpringMVC。

资源

我们向学习如何使用Kotlin和Spring Framework构建应用程序的人推荐以下资源:

例子

以下Github项目提供了可以学习甚至扩展的示例:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值