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 編譯器自動生成 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)
您可以選擇新增 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 註解的類生成合成的無參建構函式。
如果您需要為其他持久化技術利用這種機制,可以配置 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
}
內部函式名稱混淆
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 名稱是 "sampleBean\$demo_kotlin_internal_test",而不是常規 public 函式用例的 "sampleBean"。在按名稱注入此類 bean 時,請務必使用混淆名稱,或者新增 @JvmName("sampleBean") 以停用名稱混淆。
注入配置屬性
在 Java 中,您可以使用註解(例如 @Value("${property}"))注入配置屬性。但是,在 Kotlin 中,$ 是一個保留字元,用於 字串插值。
因此,如果您希望在 Kotlin 中使用 @Value 註解,您需要透過編寫 @Value("\${property}") 來轉義 $ 字元。
如果您使用 Spring Boot,您應該可能使用 @ConfigurationProperties 而不是 @Value 註解。 |
作為替代方案,您可以透過宣告以下 PropertySourcesPlaceholderConfigurer bean 來定製屬性佔位符字首:
@Bean
fun propertyConfigurer() = PropertySourcesPlaceholderConfigurer().apply {
setPlaceholderPrefix("%{")
}
您可以透過宣告多個 PropertySourcesPlaceholderConfigurer bean 來支援使用標準 ${…} 語法的元件(例如 Spring Boot 執行器或 @LocalServerPort)以及使用自定義 %{…} 語法的元件,示例如下:
@Bean
fun kotlinPropertyConfigurer() = PropertySourcesPlaceholderConfigurer().apply {
setPlaceholderPrefix("%{")
setIgnoreUnresolvablePlaceholders(true)
}
@Bean
fun defaultPropertyConfigurer() = PropertySourcesPlaceholderConfigurer()
此外,可以透過 JVM 系統屬性(或透過 SpringProperties 機制)設定 spring.placeholder.escapeCharacter.default 屬性來全域性更改或停用預設跳脫字元。
受檢異常
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 中是不可能的。
例如,在 Kotlin 中宣告 List<Foo> 在概念上等同於 java.util.List<? extends Foo>,因為 kotlin.collections.List 被宣告為 interface List<out E> : kotlin.collections.Collection<E>。
在使用 Java 類時,透過在泛型型別上使用 out Kotlin 關鍵字來考慮到這一點,例如當從 Kotlin 型別編寫 org.springframework.core.convert.converter.Converter 到 Java 型別時。
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 允許建構函式注入 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,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("https://: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)
}
}
}