きり丸の技術日記

技術検証したり、資格等をここに残していきます。

Javaで起動後DBの値をキャッシュに持つ等をPostConstructで処理させる(SpringBoot)

Javaのオープンチャットにて、「SpringBootの起動時にDBアクセスしてマスタデータを保持することは可能ですか?」といった質問が出てきました。

この記事は、解決方法として挙げられたPostConstructという処理を理解していなかったので、PostConstructを勉強するための記事です。

環境

  • Java
    • 15
  • org.springframework.boot
    • 2.4.5
  • Lombok
    • 1.18.20

※ DIするためにSpringBootを使用していますが、PostConstruct自体はJavaの機能なので、他のフレームワークでも使用できると思います。

ユースケース

  • アプリ起動後にDBのデータをキャッシュに持たせたい
  • アプリ起動時の設定ファイルの設定をログに出力したい
  • DBと設定ファイルが不正な状態の時にアプリ起動を中止させたい

要約

  • DI対象としてインスタンスを生成したタイミングでPostConstructを実行する
  • 単純なインスタンスを作成したタイミングでは実行しない

動作確認

設定ファイルのテスト方法に関しては、こちらの記事を参考にしてください。

プロダクションコード

URLを設定するための抽象クラスExternalPropertiesに、Facebookへの設定を読み込ませる具象クラスFacebookPropertiesを用意する。

@Getter
@Setter
@Slf4j
@ToString
public abstract class ExternalProperties {
  private String host;
  private String protocol;
  private String port;
  private String endpoint;
  private String timeout;

  public URI getUri() {
    return URI.create(protocol + "://" + host + ":" + port + "/" + endpoint);
  }

  @PostConstruct
  public void display() {
    log.info("********************");
    log.info(this.toString());
    log.info("********************");
  }

  @PostConstruct
  public void validate() {
    if (host == null) {
      throw new RuntimeException("設定されていない!");
    }
  }
}

@Getter
@Setter
@Configuration
@ConfigurationProperties(prefix = "external.facebook")
@ToString(callSuper = true)
public class FacebookProperties extends ExternalProperties {
}

テストコード

テストクラスを用意してPostConstructが呼ばれることを確認する。

@SpringJUnitConfig(initializers = ConfigDataApplicationContextInitializer.class)
@EnableConfigurationProperties({FacebookProperties.class})
class FacebookPropertiesTests {
  @Autowired
  FacebookProperties properties;

  @Test
  void AutowiredしないときはPostConstructが呼ばれない() {
    new FacebookProperties();

    // 起動ログを目視で確認する
    // エラーも発生しない
  }
}

EnableConfigurationPropertiesでDI対象としてFacebookPropertiesのインスタンスが生成されるタイミングで実行されました。

PostConstructという名称から、コンストラクタでのインスタンス生成後に呼ばれるかと思っておりましたが、new FacebookProperties()したタイミングでは、呼ばれることはありませんでした。

...ログ省略
14:52:22.700 [main] INFO kirimaru.config.ExternalProperties - ********************
14:52:22.873 [main] INFO kirimaru.config.ExternalProperties - FacebookProperties(super=ExternalProperties(host=localhost, protocol=http, port=10080, endpoint=facebook, timeout=3))
14:52:22.874 [main] INFO kirimaru.config.ExternalProperties - ********************
...ログ省略

備考

この機能を知ったうえでも、PostConstructを使用せずに、mainメソッドに書く方が分かりやすいのではないかと思っていました。

その件についても、オープンチャットに書いたところ以下の回答を得られました。

  • ユースケさんの回答
    • Springでインジェクトされるコンポーネントに依存しない処理ならそれでもいいですね。Springに用意してもらうDataSourceを使ったり、コンポーネントのフィールド変数においたりするにはPostConstruct使うのがいいですね
  • kisさんの回答
    • @PostConstructは必要な初期処理がそれぞれのクラスで書けて、mainで気にする必要が無いところですかねー

f:id:nainaistar:20210605154007p:plain

ソースコード

ExternalProperties.java github.com

テストコード: github.com

終わりに

職場では既にPostConstructを使っていたのですが、その方が既に現場にいないこともあって、なぜ使用しているかを聞くことができませんでした。

PostConstructを使用するとソースコード間の結合度が緩くなりすぎて処理が追えなくなるため、ソースコードの可読性を意識するのであればmain処理に書くべきでは?と思っていたこともあり、勉強していませんでした。

今回の件で、PostConstructを使用するユースケースを勉強することができたので、今後の設計レベルを一段階上げられるように出来そうです。


この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

類似記事

SpringBootでpropertiesやymlの設定ファイルが読み込めることのテストを書く【Java】 nainaistar.hatenablog.com

【2021】SpringBootでpropertiesやymlの設定ファイルが読み込めることのテストを書く nainaistar.hatenablog.com

f:id:nainaistar:20210605153751p:plain

Javaで最近使っているParameterizedTest【2021年版】

※ JUnit5のアップデートを知るための記事ではありません。私の知識アップデートをお伝えする記事になっています。


以前、ParameterizedTestに関する記事を書きました。その時は、テストコードの表現力を意識して、ArgumentsSourceを使ったほうが良いという結論を出していました。

しかし、もっとよりよい表現ができるテストが書けることを知ったので、それをアップデートした記事となります。

環境

  • Java
    • 15

ParameterizedTestのテスト表現方法を変更する

ParameterizedTestにはname変数があります。デフォルトでは、「[{index}]{argumentsWithNames}」となります。

f:id:nainaistar:20210514190004p:plain

name変数に値を設定することで、テストを表現できます。

@ParameterizedTest(name = "契約日が{0}, 解約日が{1}の時、{2}")

f:id:nainaistar:20210514190017p:plain

staticが必要なクラスでstaticを使わずに済むようにする

デフォルトではテストのインスタンスはメソッドごとに作成します。しかし、TestInstanceアノテーションを使うことで、クラスごとの生成に変更することもできます。メリットとしては、staticでなければ使えないBeforeAllAfterAll、ParameterizedTestで使用したいMethodSource等を使用できるようになります。

個人的にはNestedと組み合わせて使用でき、ネストしたクラス内に処理を閉じさせることができる点が好きです。

@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class CanExpire {
  @MethodSource(value = "canExpire")
  @ParameterizedTest(name = "契約日が{0}, 解約日が{1}の時、{2}")
  void test_01(LocalDate start, LocalDate end, boolean result) {
    // 省略
  }

  private Stream<Arguments> canExpire() {
    return Stream.of();
  }
}

※ なお、Java17からは不要になったようです。

JUnitのNestedなMethodSourceの注意点 - 日々常々


具体的にTestInstanceを変更するメリットを知りたい人は、参考先のStackOverflowを読んでください。

ソースコード

テストコード: github.com

終わりに

ParameterizedTestname属性を追加するだけで、可読性が上がるので非常に便利です。以前のParameterizedTestも、値を一部だけ修正する方法が使えるので便利なんですがテスト結果の表示がちょっとだけイマイチでした。

ちゃんとテストを書きつつ、保守性が高いコードを書くためのテクニックを今後も磨いていきたいです。


この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

類似

パラメータだけ変えて中身は同じテストをしたい(ParameterizedTest) nainaistar.hatenablog.com

参考

StackOverflow: What use is @TestInstance annotation in JUnit 5? stackoverflow.com

f:id:nainaistar:20210514190327p:plain

SpringのSpELでネストしたクラスの値をリフレクションして取得する

Java単体でもリフレクションで値を取得できます。しかし、ネストされた値を取得することは非常に面倒です。

Springではもっと簡単に値を取得することができます。それは、Spring式言語(Spring Expression Language、以後 SpELと表現します)を使用した方法です。

今回の記事では、SpELを活用してネストしたクラスの値をリフレクションして取得することを目指します。

環境

  • Java
    • 15
  • Spring Boot
    • 2.4.5
  • Lombok

ゴール

  • 別のクラスからリフレクションを使って値を取得する

前提

  • Getterがprivateではないこと

ユースケース

  • ネストするクラスから目的の変数を取得したい

例えば、「メールに必要な文言を巨大なネストするクラスから取得したい。しかも、簡単に書き換えられるようにマスタで管理したい。」という要件があったとします。

この要件を満たすために、マスタではchild.grandChild.taxという文言だけを管理しておいて、その値を元にリフレクションをする。

といったケースに使えると思います。

クラス構成

この記事ではユースケースを意識するために、親子関係をもつクラスを使用します。

- Parent
  - Child
    - GrandChild
@Value
@Builder
public class Parent {
  private Child child;
  private List<Child> children;
}
@Value
@Builder
public class Child {
  private GrandChild grandChild;
  private List<GrandChild> grandChildren;
}
@Value
@Builder
public class GrandChild {
  private String tax;
  private Map<String, Integer> animals = 
    Map.of("dog", 1, "cat", 3, "mouse", 10);  
}

ネストしないクラスをリフレクションで取得する

まずはもっとも簡単な方法です。GrandChildクラスのtax変数を取得します。

リフレクション対象のインスタンスをまずは生成します。

その後、StandardEvaluationContextのコンストラクタにインスタンスを渡して生成します。

最後にSpELを解釈するためのExpressionParserを生成し、リフレクションします。

@Test
void test_01_01() throws Exception {
  // リフレクション対象インスタンス生成
  var target = GrandChild.builder()
          .tax(123)
          .build();

  // StandardEvaluationContextを生成する
  StandardEvaluationContext context = new StandardEvaluationContext(target);
  // ExpressionParserを生成する
  ExpressionParser expressionParser = new SpelExpressionParser();
  // インスタンスの変数名「tax」を指定して取得する
  Object tax = expressionParser.parseExpression("tax").getValue(context);

  assertThat(tax).isEqualTo(123);
}

ネストするクラスをリフレクションで取得する

Parentクラスの変数のChildクラスの変数のGrandChildクラスのtax変数を取得します。SpELとしてはchild.grandchild.taxで表現します。

ネストしていない場合と処理はまったく変わりません。

@Test
void test_01() throws Exception {
  Parent target = Parent.builder()
      .child(Child.builder()
          .grandChild(GrandChild.builder()
              .tax(123)
              .build())
          .build())
      .build();

  StandardEvaluationContext context = new StandardEvaluationContext(target);
  ExpressionParser expressionParser = new SpelExpressionParser();
  Object tax = expressionParser.parseExpression("child.grandChild.tax").getValue(context);

  assertThat(tax).isEqualTo(123);
}

リストやマップの項目をリフレクションで取得する

処理は同じですので、SpELのみ記載します。Java単体だとコレクション処理が面倒なので、非常に嬉しいですね。

リストの項目を取得する

配列の順番を指定して取得できます。

  • children[0].grandChildren[0].tax

マップの項目を取得する

マップのKeyを指定することで、対応するValueを取得することができます。

  • children[0].grandChildren[0].animals['dog']

SpEL式をもっと使いこなす

単純なリフレクションだけでなく、SpELを使用することができます。無理に使うケースは無いと考えていますが、一応覚えておきましょう。

次の例は、マップのサイズが1より大きい場合はtrueを返却するSpELです。

  • children[0].grandChildren[0].animals.size() > 1

SpELをもっと知りたい場合は、こちらのページが参考になりました。

www.baeldung.com

ソースコード

実装コード https://github.com/hirotoKirimaru/cucumber-sample/blob/63230957325a487f23c6800457f5565506e9c551/src/test/java/kirimaru/biz/domain/ReflectionTests.java#L26

終わりに

Springの機能を使用すると簡単にリフレクションをすることができます。リフレクション = SpELではないので、なかなか検索されないワードだと思いますが、便利ですのでぜひ使ってみてください。


この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

参考

baeldung: Spring Expression Language Guide www.baeldung.com

f:id:nainaistar:20210507182249p:plain