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

spring.properties 相当のファイルを増やす

概要

共通モジュールとそれを利用するモジュールを、同一 gradle プロジェクト内で開発したいときなど 共通モジュール用と利用側モジュールでそれぞれ設定ファイルを書きたい場合がある。

Spring Boot の設定について

Spring Bootでは 設定(プロパティ)を key=value で対応付されている。設定方法はいろいろあり、一覧としては下記のようになる。設定ファイルはこのうち 12, 13, 14 あたりが該当する。

f:id:nise_nabe:20170917161424p:plain

24. Externalized Configuration

具体的な実装を説明すると、設定のプロパティは Java コード上ではおもに org.springframework.core.env.PropertyResolver の実装クラス、つまり ConfigurablaeEnvironment クラスで表現されている。ConfigurableEnvironment 実装クラスは複数の PropertySource を登録することができるようになっている。application.properties は 何らかの方法によって Properties ファイルに変換され、 PropertiesPropertySource としてここに登録され、利用時にキーに対応したものを取得する処理を行っている。

spring.properties 相当のファイルを増やす方法

とりあえず自分の調べた限りだと下記3つぐらいの方法がありそう。

  • profile として共通モジュール用または利用モジュール用を定義する
    • 利点:楽
    • 欠点:さらに別の profile が設定に影響する場合が面倒
  • ApplicationContextInitializer を使って追加した設定ファイルをロードさせる
    • 利点:細かく制御できる
    • 欠点:実装を頑張る必要がある。 IDE サポートがなさそう。
  • spring.config.name をカンマ区切りで使う
    • 利点:標準の機能。良さそう。
    • 欠点:(自分の調べた範囲だと)ドキュメントになさそうで IDE サポートがいまのところない
profile として共通モジュール用または利用モジュール用を定義する

一応 profile として追加することで簡易的に対応する方法もある。ロード順でいうと 12, 13 に相当する部分を利用する。 ただしこの方法の場合、 application.properties のようにそれそのもの + 別の profile と組み合わせるような設定はできない。

ApplicationContextInitializer を使って追加した設定ファイルをロードさせる

標準の application.application(yml) については ConfigFileApplicationListener という ApplicatoinListener のクラスがあり、ApplicationEnvironmentPreparedEvent が発生したときに呼ばれるクラスでロードしている。

(実はクラスは EnvironmentPostProcessor を動かすクラスでもあるため設定後に EnvironmentPostProcess として Factories に 登録しておくことによって設定を書き換えるということができる。)

コードを見ると SpringApplication では Environment 初期化 → ApplicationContext 初期化という順番で処理をしており、Environment 初期化時または、初期化後の何らかのタイミングで Environment の設定に上書きするという処理を入れるとうまくいきそう。コード上をみると以下の方法がありそうということがわかる。

タイミング1: SpringApplicationRunListener#environmentPrepared() で拾う。

タイミング2: EnviromnebtPostProcessor#postProcessEnvironment() で拾う。

タイミング3: ApplicationContextInitializer を使う

タイミング4: SpringApplicationRunListener#contextPrepared() で拾う。

Spring Boot で書かれている github 上のドキュメントには下記のように書かれている。

spring-boot/howto.adoc at v1.5.6.RELEASE · spring-projects/spring-boot · GitHub

  • Programmatically per application by calling the addListeners and addInitializers methods on SpringApplicationbefore you run it.
  • Declaratively per application by setting context.initializer.classes or context.listener.classes.
  • Declaratively for all applications by adding a META-INF/spring.factories and packaging a jar file that the applications all use as a library.

ドキュメントでは タイミング3 について書かれているように見える。 ここで設定ファイルをロードする処理を自分で書くとよさそう。

(Initializer を使ったコード例は時間がないので一旦省略)

spring.config.name をカンマ区切りで使う

spring.config.name はデフォルトの application という名前のファイル名を変更する際に利用する、ようにみえる。しかし実装を見てみると spring.config.name を カンマで区切って指定すると複数の設定ファイルを読むようになる。後に書かれたほうが強い。これをカンマ区切りで書くとよさそう。

https://github.com/spring-projects/spring-boot/blob/v1.5.6.RELEASE/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigFileApplicationListener.java#L639...L653

ただし spring.config.name は設定ファイルを読み込む前に設定しないと意味がない。

24. Externalized Configuration

spring.config.name and spring.config.location are used very early to determine which files have to be loaded so they have to be defined as an environment property (typically OS env, system property or command line argument).

設定コード例

SpringApplication を使う場合。

    public static void main(String[] args) {
        SpringApplication application = new SpringApplication(MyApplication.class);
        Map<String, Object> defaultProperties = new HashMap<>();
        defaultProperties.put("spring.config.name", "application,myapplication");
        application.setDefaultProperties(defaultProperties);
        application.run(args);
    }

SpringApplicationBuilder を使う場合。

    public static void main(String[] args) {
        new SpringApplicationBuilder(MyApplication.class)
                .properties("spring.config.name=application,myapplication")
                .run(args);

    }

ちなみに SpringApplication#defaultProperties() や SpringApplicationBuilder#properties() などで登録した値は MapPropertySource として ConfigurableEnvironment に登録される。優先順位としては一番最後となる。

IDE での spring.config.name の設定(Intellij IDEA)

結論からいうと現状では変更はできるが複数設定はできない様子。

spring.config.name は下記のブログにあるように Intellij IDEA 側の設定を変更することで、追加した設定ファイル上でも spring.properties の補完が動くなどのサポートが得られる。

IntelliJ IDEA 2017.2: Spring Boot Improvements | IntelliJ IDEA Blog

File > Project Structure の中の Facets 以下に下記のように "Customize Spring Boot" とある部分をクリック

f:id:nise_nabe:20170917153502p:plain

すると下記のように spring.config.name を設定するところが出る。ここを編集すると、一応既存の 設定ファイル名を「書き換える」ことはできる。

f:id:nise_nabe:20170917153726p:plain

ただし、現状ではここは spring.config.name のように , 区切りで入力しても反映されない。Spring Boot のドキュメントにも記載されてないのでそれはそうかなという気はしなくもないが、まあ対応してくれると便利ですね。

とりあえずは当初の目的通り モジュールそれぞれで applicatoin.yml と myapplication.yml を持つことを想定し、モジュールごとに上記設定をすると一応は解決となる。

以上。

Mockito でモックオブジェクトを初期化する

環境

内容

Mockito の Mock アノテーションを使ってモックを使ったテストを書きたい場合、 モックオブジェクトを初期化する処理を入れる必要がある。方法は調べた限り3つある様子。

Mock (Mockito 2.8.9 API)

MockitoAnnotations.initMocks(this) method has to be called to initialize annotated objects. In above example, initMocks() is called in @Before (JUnit4) method of test's base class. For JUnit3 initMocks() can go to setup() method of a base class. Instead you can also put initMocks() in your JUnit runner (@RunWith) or use the built-in MockitoJUnitRunner.

方法1 Before アノテーションを使って initMocks() を呼ぶ。

下記は javadoc から。(テストケースで継承するという実装の是非はここでは問わない。) このコードでは Before アノテーションで各テストケース毎に初期化する MockitoAnnotations.initMocks(this) を呼び出す基底クラスを作り、継承先でモックオブジェクトを定義している。

   public class ArticleManagerTest extends SampleBaseTestCase {

       @Mock private ArticleCalculator calculator;
       @Mock(name = "database") private ArticleDatabase dbMock;
       @Mock(answer = RETURNS_MOCKS) private UserProvider userProvider;
       @Mock(extraInterfaces = {Queue.class, Observer.class}) private  articleMonitor;

       private ArticleManager manager;

       @Before public void setup() {
           manager = new ArticleManager(userProvider, database, calculator, articleMonitor);
       }
   }

   public class SampleBaseTestCase {

       @Before public void initMocks() {
           MockitoAnnotations.initMocks(this);
       }
   }
 

方法2 RunWith アノテーションに MockeitoJUnitRunner を指定する。

下記コードは javadoc から。

MockitoJUnitRunner.class で指定するとデフォルトでは MockitoJUnitRunner.Strict が使われるものと同等となる。javadoc を見る限りだと 下記コードのように StrictStubs を使うことが推奨されている。

ちなみに MockitoJUnitRunner を使う場合は Test アノテーションがないと動かない。( JUnit 側の Runner のバリデーションの問題)

 @RunWith(MockitoJUnitRunner.StrictStubs.class)
 public class ExampleTest {

     @Mock
     private List list;

     @Test
     public void shouldDoSomething() {
         list.add(100);
     }
 }
 

方法3 JUnit の MethodRule を実装した MockitoRule を使う

SpringRunner や Theories などの Runner を使いたいため MokcitJUnitRunner が指定できない場合などは Mockito のほうを Rule で動かしてやると同じような挙動をする。使用する際には MocketoJUnit から取得するらしい。

 public class ExampleTest {

     //Creating new rule with recommended Strictness setting
     @Rule public MockitoRule rule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);

     @Mock
     private List list;

     @Test
     public void shouldDoSomething() {
         list.add(100);
     }
 }
 

ドキュメント

spring で logback を使う際に application.yml の値を使う

https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-logging.html#_environment_properties

The <springProperty> tag allows you to surface properties from the Spring Environment for use within Logback. This can be useful if you want to access values from your application.properties file in your logback configuration.

どうやら springProperty を使うと良いらしい。多分こういうこと。

application.yml

my:
  log:
    prop: DEBUG 

logback-spring.xml

...
  <springProperty name="myLogProp" source="my.log.prop" />

  <logger name="com.example.log">
    <appender-ref ref="${myLogProp}" />
  </logger> 
...

Gradle で SNAPSHOT バージョンのライブラリを使う場合は --refresh-dependencies をつける

開発バージョンとして -SNAPSHOT を付けたものを gradle プロジェクトで利用している場合、なぜかいつまでたっても新しくアップロードした SNAPSHOT のバージョンを使ってくれない問題があった。

SNAPSHOT バージョンは下記のように maven で開発中のバージョンとして用いられるものである。

https://maven.apache.org/guides/getting-started/index.html#What_is_a_SNAPSHOT_version

https://docs.gradle.org/current/userguide/dependency_management.html#sub:cache_refresh

https://docs.gradle.org/current/userguide/dependency_management.html#sub:dynamic_versions_and_changing_modules

Alternatively, sometimes the module you request can change over time, even for the same version. An example of this type of changing module is a Maven SNAPSHOT module, which always points at the latest artifact published. In other words, a standard Maven snapshot is a module that never stands still so to speak, it is a “changing module”.

SNAPSHOT がつくバージョンは changing module と呼ばれるものらしい。

“gradle SNAPSHOT” などでググると下記のような記事が出て来る。

https://discuss.gradle.org/t/how-to-get-gradle-to-download-newer-snapshots-to-gradle-cache-when-using-an-ivy-repository/7344/2

どうやら下記のように changing modules のキャッシュ時間(デフォルト 24 時間)を調整せよとのことらしい。

https://docs.gradle.org/current/dsl/org.gradle.api.artifacts.ResolutionStrategy.html

    // don't cache changing modules at all
    cacheChangingModulesFor 0, 'seconds'

自分の環境( というか Intellij IDEA 上の gradle ?)がおかしいのか効果がなかったので おとなしく –refresh-dependencies つけることにしたことで解決した。実行に時間はかかるようになるが SNAPSHOT バージョンを使っていても新規にリポジトリに反映したバージョンを使ってくれるようになった。

Jackson で OffsetDateTime を ISO8601 形式にシリアライズする

JavaTimeModule を使って シリアライズする場合に OffsetDateTime などが変な数字になる場合がある。

@Data
class MyObject {
    @JsonProperty("datetime")
    private OffsetDateTime dateTime;
}

ObjectMapper mapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());

MyObject paramObject = new MyObject();
paramObject.setDateTime(OffsetDateTime.now());

Map<String, String> map = objectMapper.convertValue(paramObject, new TypeReference<Map<String, String>>() {});

map.get("datetime"); // ここがタイムスタンプになる

下記のようにシリアライズされる際に WRITE_DATES_AS_TIMESTAMPS かどうかを確認している箇所がある。

https://github.com/FasterXML/jackson-datatype-jsr310/blob/jackson-datatype-jsr310-2.8.4/src/main/java/com/fasterxml/jackson/datatype/jsr310/ser/InstantSerializerBase.java#L81

https://github.com/FasterXML/jackson-datatype-jsr310/blob/jackson-datatype-jsr310-2.8.4/src/main/java/com/fasterxml/jackson/datatype/jsr310/ser/JSR310FormattedSerializerBase.java#L165...L174

https://github.com/FasterXML/jackson-databind/blob/jackson-databind-2.8.4/src/main/java/com/fasterxml/jackson/databind/SerializationFeature.java#L162...L184

ObjectMapper がデフォルトで WRITE_DATES_AS_TIMESTAMPS が有効になっているのでこれを解除する。

@Data
class MyObject {
    @JsonProperty("datetime")
    private OffsetDateTime dateTime;
}

ObjectMapper mapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

MyObject paramObject = new MyObject();
paramObject.setDateTime(OffsetDateTime.now());

Map<String, String> map = objectMapper.convertValue(paramObject, new TypeReference<Map<String, String>>() {});

map.get("datetime"); // ISO8601 形式の文字列となる

apache HttpClient の backoff について

この記事の目的

コードリーディング後の理解用メモ

対象

概要

apache http client には 動的にコネクション数を調整できる機能がある。用途としてはリクエスト先のサーバが高負荷になった場合などにコネクション数を減らして負荷を一時的に減らせるようにするような目的で使えそう。

正確な記述などはすっとばして理解の範囲で記載

f:id:nise_nabe:20170103141336p:plain

関連クラスやメンバ

  • org.apache.http.impl.client.HttpClientBuilder
    • backoffManager
    • connectionBackoffStrategy
  • org.apache.http.impl.execchain.BackoffStrategyExec
  • org.apache.http.client.ConnectionBackoffStrategy
  • org.apache.http.client.BackoffManager
  • org.apache.http.impl.client.AIMDBackoffManager
  • org.apache.http.impl.client.DefaultBackoffStrategy

簡単な使い方

もっとも簡単なコード

PoolingHttpClientConnectionManager manager = new PoolingHttpClientConnectionManager();

CloseableHttpClient client = HttpClientBuilder.create()
                                              .setBackoffManager(new AIMDBackoffManager(manager))
                                              .setConnectionBackoffStrategy(new DefaultBackoffStrategy())
                                              .build()

コネクション数制御。 HttpClientBuilder で設定したコネクション数設定は初期値として使われる。もちろん BackoffManager で調整されていくので運用中の数字とはことなる場合がある。

PoolingHttpClientConnectionManager manager = new PoolingHttpClientConnectionManager();

AIMDBackoffManager backoffManager = new AIMDBackoffManager(manager);
backoffManager.setPerHostConnectionCap(10);

CloseableHttpClient client = HttpClientBuilder.create()
                                              .setBackoffManager(backoffManager)
                                              .setConnectionBackoffStrategy(new DefaultBackoffStrategy())
                                              .build();

メモ

  • HttpClientBuilder で BackoffStrategyExec を作る
  • BackoffStrategyExec が ConnectionBackoffStrategy や BackoffManager を利用する
  • コネクション数の増え方や減らし方は BackoffManager で実装する
  • Backoff する条件は ConnectionBackoffStrategy で実装する
  • Exec は Strategy の shouldBackoff() を見てどうするかきめる
    • true -> Manager の backoff() を実行
    • false -> Manager の probe() を実行

HttpClient 内の実装について

  • AIMDBackoffManager について

    • AIMD = Additional increase, multiplicative decrease (加算増加乗算減少)
    • デフォルトでは最大2コネクション
      • 初期設定でプールの方の設定を大きくしていても次の更新でこの数字になる
      • 正確にはホストごとの最大コネクション数
    • 増加や減少を抑える時間(クールダウン)はデフォルト 5 秒
      • 最後の増加と減少から 5 秒ごとにプールの最大接続数が 1 ふえる
      • 最後の減少から 5 秒ごとにプールの最大接続数が半分になる
    • BackoffStrategyExec が shouldBackoff() の true/false で呼び出し変えてるだけなので最大で消費してなくても最大値は増えてく
  • DefaultBackoffStrategy について

    • SocketTimeoutException や ConnectException のような例外が発生した場合に backoff すべきと返す
    • http status が 503 の場合に backoff すべきと返す
      • 注意としては ServiceUnavailableRetryExec などを利用したときに backoff せずにリトライが優先される場合がある
        • HttpClientBuilder#build() の実装でそういう順番でチェインしている 参考

参考文献

Additive increase/multiplicative decrease - Wikipedia