🗺️

Spring Bootでネストされている設定値をMapで楽に安全に扱いたい

2022/02/09に公開

やりたいこと

こういう設定ファイルを

application.yml
app:
  sample:
    hoge:
      host: hoge.dev
      port: 80
    piyo:
      host: piyo.dev
      port: 8080

こんな感じで扱えるように、Map形式でうまい感じに取り込みたい。

String hogeHost = config.getSampleMap.get("hoge").getHost();
  • hostportをメンバにもつようなクラスをMapに持ちたい
  • 単に@ConstructorBindingではダメそう

普通のやり方

これでも全然OKそうだけど?

SampleConfig.java
@Configuration
@ConfigurationProperties("app")
@Getter
public class SampleConfig {
  private final Map<String, Sample> sample = new HashMap<>();
  
  @ConstructorBinding
  @Getter
  @RequiredArgsConstructor
  public static class Sample {
    private final String host;
    private final int port;
  }
}

ただし、他からMapへの変更(putとか)を許してしまうのでやりたくない。他に渡すときにMapをラップすればいいじゃん?と思って、

SampleConfig.java
@Configuration
@ConfigurationProperties("app")
@ConstructorBinding
public class SampleConfig {
  private final Map<String, Sample> sample = new HashMap<>();
  
  public Map<String, Sample> getSample() {
    return MapUtils.unmodifiableMap(sample);
  }
  
  @ConstructorBinding
  @Getter
  @RequiredArgsConstructor
  public static class Sample {
    private final String host;
    private final int port;
  }
}

このように変更すると、起動時にエラーになってしまう。

Caused by: java.lang.IllegalStateException: No setter found for property: sample
	at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:104)
	at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:83)
	at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:59)
	at org.springframework.boot.context.properties.bind.Binder.lambda$bindDataObject$5(Binder.java:473)
	at org.springframework.boot.context.properties.bind.Binder$Context.withIncreasedDepth(Binder.java:587)
	at org.springframework.boot.context.properties.bind.Binder$Context.withDataObject(Binder.java:573)
	at org.springframework.boot.context.properties.bind.Binder$Context.access$300(Binder.java:534)
	at org.springframework.boot.context.properties.bind.Binder.bindDataObject(Binder.java:471)
	at org.springframework.boot.context.properties.bind.Binder.bindObject(Binder.java:411)
	at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:340)
	... 24 more

実現方法

インタフェース越しに値をとってくるようにするとできそう。

SampleConfig.java
public interface SampleConfig {
  public Map<String, Sample> getUnmodifiedSample();
  
  @ConstructorBinding
  @Getter
  @RequiredArgsConstructor
  public static class Sample {
    private final String host;
    private final int port;
  }
}
SampleConfigImpl.java
@Configuration
@ConfigurationProperties("app")
@Getter
public class SampleConfigImpl implements SampleConfig {
  private final Map<String, Sample> sample = new HashMap<>();
  
  public Map<String, Sample> getUnmodifiedSample() {
    return MapUtils.unmodifiableMap(sample);
  }
}

Discussion