きり丸の技術日記

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

SpringのValueに初期値を与える(エラーを起こさずにnullも初期値にする)

小ネタ。@Valueで初期値を与えたい時の記法をメモします。マッピングするプロパティがない場合、BeanCreationExceptionが発生してしまうので、それが発生しないようにします。

環境

  • Java
    • 17
  • SpringBootTest
    • 2.7.4

ゴール

  • SpringのValueで初期値を渡す
  • マッピング先がない場合にExceptionではなくnullを設定する
# 次のExceptionが発生しないようにする
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'kirimaru.config.SpringValueTests': Injection of autowired dependencies failed; nested exception is java.lang.IllegalArgumentException: Could not resolve placeholder 'app.config.appName2' in value "${app.config.appName2}"

対応

@Value(${placeholder:defaultValue})と記載する。

app.config.appNameにアプリケーション名を設定していた場合、@Value("${app.config.appName:defaultName}")と記載すると設定値を表示します。設定されていない場合はデフォルト値のdefaultNameを表示します。

nullを初期値に渡したい場合は、@Value("${placeholder:#{null}}")として#{null}を渡す必要があります。もし#{}を使用しない場合はnullという文字列が渡ってしまいます。

テスト

次のテストコードで動作確認しました。今回はDIしていることだけを確認したいため、例外が発生することを確認したい場合はSpringBootTestを使用してください。

@SpringJUnitConfig(SpringValueTests.Config.class)
@TestPropertySource(properties = {"app.config.appName=testApplication"})
class SpringValueTests {

  @Value("${app.config.appName:defaultName}")
  private String appName;
  @Value("${app.config.appName2:defaultName}")
  private String appName2;
  @Value("${app.config.appName3:null}")
  private String appName3;
  @Value("${app.config.appName4:#{null}}")
  private String appName4;

  @ComponentScan({"kirimaru.biz.domain.hogehoge"})
  public static class Config {}

  @Test
  void test_01() {
    SoftAssertions softly = new SoftAssertions();

    softly.assertThat(appName).isEqualTo("testApplication");
    softly.assertThat(appName2).isEqualTo("defaultName");
    softly.assertThat(appName3).isEqualTo("null");
    softly.assertThat(appName4).isNull();

    softly.assertAll();
  }
}

ソースコード

終わりに

開発環境ではproxyを経由したいが、本番環境ではproxyを経由したくない。しかし、変な設定値を設定ファイルに残したくない。

そのようなユースケースで使用しました。地味にやり方が分からなかったので、まとめられてよかったです。

類似情報

SpringでRedisを使う(データベース、キャッシュとしての使い方)

本業はJavaのバックエンドエンジニアなのですが、Redisを使ったことがないので、素振りします。

今回の記事では、データベース、キャッシュとしての機能を検証することをゴールとします。メッセージブローカー、キューとしての挙動は記載いたしません。

環境

  • Java
    • 17
  • Redis
    • 7.0.5
  • org.springframework.boot
    • 2.6.4
  • org.testcontainers:junit-jupiter
    • 1.17.4
  • Gradle
  • Lombok
  • Docker

対応

Gradleを記載する

Redisを使えるように、依存関係に含めます。テストについても記載するのでTestContainersについても、依存関係に含めます。

implementation 'org.springframework.boot:spring-boot-starter-web:2.6.4'
implementation 'org.springframework.boot:spring-boot-starter-data-redis:2.6.4'

testImplementation 'org.springframework.boot:spring-boot-starter-test:2.6.4'
testImplementation 'org.testcontainers:junit-jupiter:1.17.4'

エンティティの用意

適当なエンティティを用意します。@RedisHash(テーブル名)をエンティティに付与すると、そのテーブル名でRedisへのアクセスを行ってくれます。

@RedisHash("user")
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UserDto implements Serializable {
    private String id;
    private String name;
}

DAO(Mapper)の用意

org.springframework.data.repository.CrudRepositoryを継承し、<T, ID>を指定するだけで、データアクセスができます。

@Repository
public interface UserRedisMapper extends CrudRepository<UserDto, String> {
}

設定ファイルの用意

デフォルトの場合、特に指定しなくても接続できます。接続情報を指定してください。

spring.redis:
 database: 0
 host: localhost
 port: 6379 #6379がウェルノウンポート
 password: null
 timeout: 200

テストコードの準備

TestContainersを使用したいので、TestContainersRedisを準備します。また、Dockerを使用するので、Dockerを起動しておいてください。

@SpringBootApplication
@Testcontainers
public class RepositoryApplication {

  @Container
  private static final GenericContainer<?> redis;

  static {
    // Redisのバージョン指定
    redis = new GenericContainer<>(DockerImageName.parse("redis:latest"))
        .withExposedPorts(6379);
    
    redis.start();
  
    // 接続情報の書き換え
    System.setProperty("spring.redis.host", redis.getHost());
    System.setProperty("spring.redis.port", redis.getFirstMappedPort().toString());
  }
}

テストコード本体

基本的には、@DataRedisTestを付与するだけで、MyBatis等々と同じように記載できます。@SpringBootTestよりも多少は軽く起動できます。

@DataRedisTest
class UserRedisMapperTests {
  @Autowired
  UserRedisMapper mapper;

  @BeforeEach
  void setUp() {
    mapper.deleteAll();
  }

  @Test
  void test_01() {
    assertThat(mapper.findAll()).isEmpty();
  }

  @Test
  void test_02() {
    var USER1 = UserDto.builder().id("1").name("1").build();
    var USER2 = UserDto.builder().id("2").name("2").build();

    mapper.saveAll(List.of(USER1, USER2));

    assertThat(
        mapper.findAll()
    ).isEqualTo(List.of(USER1, USER2));
  }
}

懸念点

ローカルでは動作確認できたのですが、GitHub Actionsでは動きませんでした。

動かそうとすると、次のエラーが発生します。49154ポートなんてどこでも使用していないのですが…。

org.springframework.data.redis.RedisConnectionFailureException: Unable to connect to Redis; nested exception is io.lettuce.core.RedisConnectionException: Unable to connect to localhost/<unresolved>:49154

TestContainersGitHub Actionsで使用すると同じようになりそうですね。PostgreSQLも似た事象が発生しました。いろいろググっても、TestContainersのissueを見ても解決方法が分からなかったので、教えていただければ非常に喜びます。

ソースコード

Redisだけ別プロジェクトにしています。

終わりに

メッセージブローカー、キューとしての使い方は公式ドキュメントに載っていたので、そちらは一応素振りしてみたのですがイマイチよくわからず…。

正直、かなり浅い記事なのはわかっているのですが、とりあえず一応本業でRedisを採用するときになったら、すぐに使いこなせそうです。

参考情報

SpringのDisabledIfを素振りする

条件によってはテストを実行しないアノテーション、JUnit5DisabledIfだけでなく、SpringDisabledIfもあります。

基本的にはJUnit5DisabledIfシリーズで十分なことが多いのですが、Spring側で用意されているものも素振りします。

環境

  • Java
    • 17
  • SpringBootTest
    • 2.7.4

前提

設定ファイルの準備

SpringDisabledIfを使用する最大のメリットは、設定ファイルによって挙動を変更できるところです。そのため、application.ymlを定義します。

app:
  config:
    appName: "Kirimaru"
    local: "true"

テストクラスのセットアップ

テストで設定ファイルを読み込めるようにします。

// 設定ファイルを読み込むアノテーションとinitializers
@SpringJUnitConfig(initializers = ConfigDataApplicationContextInitializer.class)

他にも設定ファイルを読み込めるのであればSpringBootTest等々のアノテーションでも問題ありません。ただ、ユニットテストレベルでは不要なDI等々も行ってしまうので、SpringJUnitConfigのほうが早く起動できます。

@SpringBootTest

対応

Spring機能を使用したいので、loadContexttrueを設定します。

${path}

${path}を使用すると、設定ファイルの値を読み込めます。なお、読み込んだ値がtruefalseである必要があります。先ほどapp.config.localtrueを設定しているので、次のテストは動かないように制御できます。

@DisabledIf(value = "${app.config.local}", loadContext = true)
@Test
void test_01() {
  fail();
}

#{expression}

#{expression}を使用すると、中に条件式を記載できます。Springで設定ファイルを読み込むクラスはEnvironmentクラスです。Environmentクラスを利用して、app.config.appNameの値がkirimaruのときに、テストが動かないように制御するには次の式を使用します。

@DisabledIf(
  value = "#{environment.getProperty('app.config.appName').equalsIgnoreCase('kirimaru')}",
  loadContext = true
)
@Test
void test_02() {
  fail();
}

なお、#{}内部で${}は使用できないようです。できるのかもしれませんが、私には理解できませんでした。似たような書き方を聞いている掲示板はあるのですが、解決にはいたっていないようです。

ログを見る限り、app.config.appNameKirimaruは読み込めているのですが、Kirimaruを元にBean名を探しているようでした。

@DisabledIf(value = "#{${app.config.appName}.equalsIgnoreCase('kirimaru')}", loadContext = true)
org.junit.jupiter.engine.execution.ConditionEvaluationException: Failed to evaluate condition [org.springframework.test.context.junit.jupiter.DisabledIfCondition]: Expression parsing failed; nested exception is org.springframework.expression.spel.SpelEvaluationException: EL1008E: Property or field 'Kirimaru' cannot be found on object of type 'org.springframework.beans.factory.config.BeanExpressionContext' - maybe not public or not valid?

備考

JUnit5DisabledIfシリーズも調べるとおもしろいので、ぜひ興味があるときに調べてみてください。

org.junit.jupiter.apiの5.8.2には次のシリーズがありました。

  • DisabledIf
  • DisabledIfEnvironmentVariable
  • DisabledIfSystemProperty
  • DisabledOnJre
  • DisabledOnOs
  • DisabledForJreRange
  • および、上記とは逆のEnabledIfシリーズ

適切ではないですがローカルで動いてCI上で落ちるテストがある場合は、@DisabledOnOs(OS.LINUX)のように回避することもあります。

ソースコード

終わりに

SpringDisabledIfが便利に使えそうだったのですが、検証に時間がかかってしまいました。loadContexttrueにする必要があったり、SpEL式がうまく書けなかったり。

これを覚えたことで今後回避したいアノテーションを作るときには、ちょっとした作り込みをする必要がなくなったので、未来の工数を大きく削減できました。

特定の環境では動かしたくないテストは大量にあるので、今後も活用していきたいです。

参考情報

類似記事