ConditionalOnClass を使うライブラリを書いたときにハマったことメモ

概要

Spring Boot の機能として auto configuration を作る Spring Boot ライブラリを作成する場合に、特定のライブラリが読まれているときのみ動く設定などを使いたい場合がある。そういう場合に ConditionalOnClass アノテーションを使うだろうと思う。(そもそも AutoConfiguration を作りたいと思うモチベーションについては割愛)

AutoConfiguration やその作り方については下記記事を参照

以下、自分が ConditionalOnClass アノテーションを利用したときにハマったこと。

実際にハマったこと

ConditonalOnClass はコードを見ると TYPE と METHOD につけることが可能。

https://github.com/spring-projects/spring-boot/blob/v1.5.6.RELEASE/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnClass.java#L32

基本的には ConditionalOnClass アノテーションは Confuguration アノテーションが書かれているクラスやその中で利用する想定とする。そして実際に ConditionalOnClass がついている場合全体を通した場合下記のように挙動が変わる。

  • Configuration Class についていてクラスパスに指定クラスが存在しない場合、そのクラスを Bean として生成しない。
  • メソッドについていてクラスパスに指定クラスが存在しない場合、そのメソッドを実行しない。

差が分からないように見えるが、前者の場合は "生成" されるためそのクラス内で利用している import 対象のクラスがクラスパスに存在しないと生成に失敗する。

これは通常開発時はビルド時にエラーとして失敗するものだが、今回の場合はクラスパスに存在するかしないかで制御するライブラリ側のコードなので compileOnly とするため、このライブラリ利用側のアプリではクラスパスに存在しない場合がある。

利用側アプリではそのライブラリを追加して及び使用したクラスをクラスパスに追加しないときにランタイムで Configuration クラスの生成に失敗するのでアプリのビルドは成功するが起動することができない。

例えば下記のようにそのクラスが存在しなければその Bean 生成はしないというような意図で書いたコードである。(AutoConfiguration 用の spring.factories などの説明は割愛)

@Configuraion
public class MyConfiguration {
   @Bean
   @ConditionalOnClass(MyClassService.class)
   public MyClassService myClassService() {
      return new MyClassService();
   }
}

この場合、 MyConfiguration クラスは普通にインスタンス化しようとするためこのライブラリの利用者は実行時にエラーになる。

解決案

ある指定クラスが存在する場合のみ Bean としたい場合はクラス全体につけるのが良い。当然そのクラスにあるメソッドは指定クラスがないと使われないのでメソッド側に ConditionalOnClass アノテーションを付ける必要はない。

@Configuraion
@ConditionalOnClass(MyClassService.class)
public class MyConfiguration {
   @Bean
   public MyClassService myClassService() {
      return new MyClassService();
   }
}

実際のコードの例としては DataSource あたりが参考になるのではないかと思う。このコードでは各ライブラリ依存部分を中で更に別クラスを書いて ConditionalOnClass を使っている。

https://github.com/spring-projects/spring-boot/blob/35d062f50df674a9564834d14df603db536cc5f5/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration.java