Bean Definition DSL とは
https://docs.spring.io/spring-framework/docs/current/reference/html/languages.html#kotlin-bean-definition-dsl
Spring の Kotlinサポート機能で、SpringコンポーネントをDSLで登録する方法です。
従来の @Component
などのように宣言的にコンポーネント登録せず、明示的にコンポーネントを登録していきます。
- 従来のコンポーネント登録
@Configuration
class Config {
@Bean
fun myHandler() = MyHandler()
}
とか
- 従来のコンポーネント登録その2
@Component
class MyHandler {
fun findAll(req: ServerRequest) = ok().body("hello", String::class.java)
}
といったいつものやつに対して、Bean Definition DSLはアノテーションを使用せず
- Bean Definition DSL によるコンポーネント登録
@SpringBootApplication
class Application
fun main(args: Array<String>) {
runApplication<Application>(*args) {
addInitializers(beans);
}
}
private val beans: BeanDefinitionDsl = beans {
bean<MyHandler>()
}
こんな感じで、 アノテーションではなく、 bean<型>()
として明示的に(?)コンポーネントを登録していきます。
addInitializers(beans)
これで実際にContextに対してBean登録されてます。
このやり方はコンポーネントスキャンベースと比較して起動速度に関しては優位でもなさそうなのであまりメリットはわからないです。
https://towardsdatascience.com/a-benchmark-of-spring-annotations-vs-kotlin-dsl-209f54294325
- RouterFunctionDSL との組み合わせ
@SpringBootApplication
class Application
fun main(args: Array<String>) {
runApplication<Application>(*args) {
addInitializers(beans);
}
}
private val beans: BeanDefinitionDsl = beans {
bean<MyHandler>()
bean { route(ref()) }
}
private fun route(myHandler: MyHandler): RouterFunction<ServerResponse> = router {
GET("/", myHandler::findAll)
}
とかDSLの組み合わせで小規模なAPIサーバーをサクッと作れるのが楽なのかもしれないです。
困ったこと
事象
テストでBeanが登録されない!!
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ApplicationTests(
private val webTestClient: WebTestClient,
) : FunSpec({
test("access top") {
webTestClient
.get()
.uri("/")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk
}
こんなtestを実行しても、404になりテストが通りませんでした。
route
関数で設定した RouterFunction がコンポーネントとして登録されていなそうでした。
@SpringBootApplication
class Application
fun main(args: Array<String>) {
runApplication<Application>(*args) {
addInitializers(beans);
}
}
private val beans: BeanDefinitionDsl = beans {
bean<MyHandler>()
bean { route(ref()) }
}
private fun route(myHandler: MyHandler): RouterFunction<ServerResponse> = router {
GET("/", myHandler::findAll)
}
アノテーションベースのSpringTestならいい感じにコンポーネント登録してくれているので、頭がフリーズします。
原因
fun main(args: Array<String>) {
runApplication<Application>(*args) {
addInitializers(beans);
}
}
メイン関数内で addInitializers(beans: BeanDefinitionDsl)
してるからでした。
SpringExtension はプロダクションコードのメイン関数を呼び出しているわけではないので、当たり前といえば当たり前でした。
解決方針
テストコードもプロダクションコードと同じコンポーネントを指定
プロダクションコードがテストコードと同じコンポーネントを指定する必要があるなら、Beanを外部ファイルも読み取れるスコープに配置し
テストコードは @ContextConfiguration
で initializers に指定すればよい
ただし、 @ContextConfiguration.initializers
が求める型は Class<? extends ApplicationContextInitializer<?>>[]
なので、
val beans = beans {
bean<MyHandler>()
bean { route(ref()) }
}
と書かれた beans
を直接参照することはできない。
仕方がないので
class MyContextInitializer : ApplicationContextInitializer<GenericApplicationContext> {
override fun initialize(applicationContext: GenericApplicationContext) {
beans.initialize(applicationContext)
}
}
private val beans: BeanDefinitionDsl = beans {
bean<MyHandler>()
bean { route(ref()) }
}
fun route(myHandler: MyHandler): RouterFunction<ServerResponse> = router {
GET("/", myHandler::findAll)
}
Bean Definition DSL を context に登録するような ApplicationContextInitializer
を実装します。
- プロダクションコード
@SpringBootApplication
class Application
fun main(args: Array<String>) {
runApplication<Application>(*args) {
addInitializers(MyContextInitializer());
}
}
- テストコード
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ContextConfiguration(initializers = [MyContextInitializer::class])
class ApplicationTests(
とすることで無事にプロダクションとテストで同じコンポーネントを生成できました。
疑問
「テストコードの一部コンポーネントのみ動作を変えたbeanをinjectしたい。」みたいなニーズってあると思うのですが、その場合はどうするのが正しいかがわからないです。
アノテーションベースなら @Conditional
みたいなアノテーションで1発にも思えるのですが、なにかうまいやり方があるのか。