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 プラグインの事前構成済みバージョン)を提供します。このプラグインは、次のいずれかのアノテーションが付けられた、またはメタアノテーションが付けられた型のクラスとそのメンバー関数を自動的に開きます。

メタアノテーションのサポートとは、@Configuration@Controller@RestController@Service または @Repository でアノテーションが付けられた型は、これらのアノテーションが @Component でメタアノテーションされているため、自動的に開かれることを意味します。

プロキシや Kotlin コンパイラーによる最終メソッドの自動生成が関係する一部のユースケースでは、特別な注意が必要です。例: プロパティを持つ Kotlin クラスは、関連する final getter および setter を生成します。関連メソッドをプロキシできるようにするには、kotlin-spring プラグインによってこれらのメソッドが開かれるように、型 レベルの @Component アノテーションがメソッドレベルの @Bean よりも優先される必要があります。典型的な使用例は、@Scope とその人気のある @RequestScope 特化です。

start.spring.io は、デフォルトで kotlin-spring プラグインを有効にします。実際には、Java のように、open キーワードを追加せずに Kotlin Bean を作成できます。

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 など)にはデフォルトのコンストラクターが必要なので、この種の設計はできません。幸い、Kotlin は、JPA アノテーションが付けられたクラスの引数なしの合成コンストラクターを生成する kotlin-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 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 名は、通常の public 関数の使用例の "sampleBean" ではなく、"sampleBean\$demo_kotlin_internal_test" になります。このような Bean を名前で注入する場合は、必ずマングリングされた名前を使用するか、@JvmName("sampleBean") を追加して名前マングリングを無効にしてください。

構成プロパティの注入

Java では、アノテーション(@Value("${property}") など)を使用して構成プロパティを注入できます。ただし、Kotlin では、$ は文字列補間 (英語) に使用される予約文字です。

Kotlin で @Value アノテーションを使用する場合は、@Value("\${property}") を記述して $ 文字をエスケープする必要があります。

Spring Boot を使用する場合、おそらく @Value アノテーションの代わりに @ConfigurationProperties を使用する必要があります。

別の方法として、次の構成 Bean を宣言することにより、プロパティプレースホルダープレフィックスをカスタマイズできます。

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

次の例に示すように、${…​} 構文を使用する既存のコード(Spring Boot アクチュエーターや @LocalServerPort など)を構成 Bean でカスタマイズできます。

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

@Bean
fun defaultPropertyConfigurer() = PropertySourcesPlaceholderConfigurer()

チェック済みの例外

Java と Kotlin 例外処理 (英語) は非常に近く、主な違いは Kotlin はすべての例外を未チェックの例外として扱うことです。ただし、プロキシ化されたオブジェクト(たとえば、@Transactional アノテーションが付けられたクラスまたはメソッド)を使用する場合、スローされるチェック済み例外は、デフォルトで UndeclaredThrowableException にラップされます。

Java のようにスローされた元の例外を取得するには、メソッドに @Throws (英語) アノテーションを付けて、スローされたチェック済み例外(@Throws(IOException::class) など)を明示的に指定する必要があります。

アノテーション配列の属性

Kotlin アノテーションはほとんど Java アノテーションに似ていますが、配列属性(Spring で広く使用されています)の動作は異なります。Kotlin ドキュメント (英語) で説明したように、他の属性とは異なり、value 属性名を省略して、vararg パラメーターとして指定できます。

その意味を理解するために、例として @RequestMapping (最も広く使用されている Spring アノテーションの 1 つ)を検討してください。この 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 などのショートカットアノテーションを使用することもできます。

@RequestMappingmethod 属性が指定されていない場合、GET メソッドだけでなく、すべての HTTP メソッドが一致します。

宣言場所の差異

Kotlin で作成された Spring アプリケーションでジェネリクス型を扱う場合、ユースケースによっては、型を宣言するときに分散を定義できる Kotlin 宣言サイト分散 (英語) を理解することが必要になる場合がありますが、使用サイト分散のみをサポートする Java ではこれは不可能です。

例: kotlin.collections.List は interface List<out E> : kotlin.collections.Collection<E> (英語) として宣言されているため、Kotlin での List<Foo> の宣言は概念的には java.util.List<? extends Foo> と同等です。

これは、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 [GitHub] (英語) をサブスクライブします。

テスト

このセクションでは、Kotlin と Spring Framework を組み合わせたテストについて説明します。推奨されるテストフレームワークは、JUnit 5 (英語) とモック用の Mockk (英語) です。

Spring Boot を使用している場合は、この関連資料を参照してください。

コンストラクターインジェクション

専用セクションに従って、JUnit Jupiter (JUnit 5) では lateinit var の代わりに val を使用するために Kotlin で非常に役立つ Bean のコンストラクターインジェクションが可能です。@TestConstructor(autowireMode = AutowireMode.ALL) (Javadoc) を使用して、すべてのパラメーターのオートワイヤーを有効にすることができます。

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

仕様のようなテスト

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