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
外掛程式的預先組態版本),可自動為使用下列其中一個註解註解或 meta-annotation 的類型開啟類別及其成員函數
-
@Component
-
@Async
-
@Transactional
-
@Cacheable
Meta-annotation 支援表示使用 @Configuration
、@Controller
、@RestController
、@Service
或 @Repository
註解的類型會自動開啟,因為這些註解使用 @Component
進行 meta-annotation。
某些涉及 Proxy 的使用案例以及 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 註解註解的類別產生合成的 no-arg 建構子。
如果您需要為其他持久化技術利用這種機制,您可以組態 kotlin-noarg
外掛程式。
從 Kay 版本列車開始,如果模組使用 Spring Data 物件對應(例如 MongoDB、Redis、Cassandra 與其他),Spring Data 支援 Kotlin 不可變類別實例,並且不需要 kotlin-noarg 外掛程式。 |
注入相依性
偏好建構子注入
我們的建議是嘗試偏好使用建構子注入與 val
唯讀(以及盡可能非可為 Null)屬性,如下列範例所示
@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("%{")
}
您可以使用組態 Bean 自訂使用 ${…}
語法的現有程式碼(例如 Spring Boot Actuator 或 @LocalServerPort
),如下列範例所示
@Bean
fun kotlinPropertyConfigurer() = PropertySourcesPlaceholderConfigurer().apply {
setPlaceholderPrefix("%{")
setIgnoreUnresolvablePlaceholders(true)
}
@Bean
fun defaultPropertyConfigurer() = PropertySourcesPlaceholderConfigurer()
已檢查例外
Java 與 Kotlin 例外處理 非常接近,主要差異在於 Kotlin 將所有例外視為未檢查的例外。但是,當使用 Proxy 物件(例如使用 @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 類別時,例如在編寫從 Kotlin 型別到 Java 型別的 org.springframework.core.convert.converter.Converter
時,需要透過在泛型型別上使用 out
Kotlin 關鍵字來考慮這一點。
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 (JUnit 5) 允許 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 (JUnit 5),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://127.0.0.1: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)
}
}
}