Spring Frameworkで設定値(プロパティ値)をデータベースから取得する方法

Spring Frameworkで設定値を扱う場合は、プロパティファイル、システムプロパティ(-Dで指定した値)、OSの環境変数、JNDI（Webアプリケーション環境のみ）などに設定値を指定することができ、アプリケーション側では`@Value`や`Environment`を介して取得することができます。

例えば、以下のような感じです。

```properties:設定値の指定例(propertiesファイル)
services.user.url=http://user.service.com/{id}
services.company.url=http://company.service.com/{id}
```

```java:設定値の参照例
@Service
public class MyService {

  private final RestTemplate restTemplate = new RestTemplate();
  private final Environment environment;

  @Value("${services.user.url}") // 設定値をBean生成時にインジェクション
  private String userServiceUrl;

  public MyService(Environment environment) {
    this.environment = environment;
  }

  public User getUser(String id) {
    User user = restTemplate.getForObject(userServiceUrl, User.class, id);
    return user;
  }

  public Company getCompany(String id) {
    String companyServiceUrl = environment.getProperty("services.company.url"); // 設定値を実行時に取得
    Company company = restTemplate.getForObject(companyServiceUrl, User.class);
    return company;
  }

}
```

なお、Spring Bootだと「`@Value`」ではなく「[Type-safe Configuration Properties](https://docs.spring.io/spring-boot/docs/2.1.7.RELEASE/reference/htmlsingle/#boot-features-external-config-typesafe-configuration-properties)」を使うのが一般的だと思いますが、ここでは**意図的に**「`@Value`」を使ってプロパティプレースホルダ（「`${...}`」）を使うように指定ます。（解説はしませんが、ここで紹介する方法は「Type-safe Configuration Properties」にも適用できる内容になっています）

Spring Framworkには（残念ながら？）データベースから設定値を取得するためのクラスは提供されていませんが、設定値の保存先は`PropertySource`というクラスによって抽象化されているので、データベースから設定値を取得する`PropertySource`を作成して`Environment`に適用すればアプリケーションから参照できるようになります。

# データベースから設定値を取得する`PropertySource`の作成

起動時にデータベースから設定値を取得・キャッシュし、任意のタイミングでキャッシュを更新するようなクラスを作成してみます（スレッドセーフ性が怪しい気もするけど・・・）。

```java
package com.example.demo;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.jdbc.core.JdbcTemplate;

import javax.sql.DataSource;
import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class JdbcPropertySource extends EnumerablePropertySource<DataSource> implements InitializingBean {

  private final JdbcTemplate jdbcTemplate;
  private final String[] tableNames;
  private Map<String, Object> properties = Collections.emptyMap();

  public JdbcPropertySource(String name, DataSource dataSource, String... tableNames) {
    super(name, dataSource);
    this.jdbcTemplate = new JdbcTemplate(dataSource);
    this.tableNames = tableNames.length == 0 ? new String[]{"t_properties"} : tableNames;
  }

  @Override
  public String[] getPropertyNames() {
    return properties.keySet().toArray(new String[0]);
  }

  @Override
  public Object getProperty(String name) {
    Map<String, Object> currentProperties = properties;
    return currentProperties.get(name);
  }

  @Override
  public void afterPropertiesSet() {
    load();
  }

  public void load() {
    Map<String, Object> loadedProperties = Stream.of(tableNames)
        .flatMap(tableName -> jdbcTemplate.queryForList("SELECT name, value FROM " + tableName).stream())
        .collect(Collectors.toMap(e -> (String) e.get("name"), e -> e.get("value")));
    this.properties = loadedProperties;
  }

}
```

# `Environment`への適用

```java
package com.example.actuatordemo;

import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;

import javax.sql.DataSource;

@Configuration
public class MyConfiguration {

  // DataSourceの設定
  @Bean
  DataSource dataSource() {
    return new EmbeddedDatabaseBuilder()
        .setType(EmbeddedDatabaseType.H2)
        .setName("demo")
        .addScript("classpath:init-db.sql")
        .build();
  }

  // 今回作成したJdbcPropertySourceの設定
  @Bean
  JdbcPropertySource jdbcPropertySource(DataSource dataSource) {
    return new JdbcPropertySource("jdbcProperties", dataSource);
  }

  // 今回作成したJdbcPropertySourceをEnvironmentへ適用するためのBean定義
  @Bean
  static BeanFactoryPostProcessor environmentPropertySourcesCustomizer() {
    return bf -> {
      ConfigurableEnvironment environment = bf.getBean(ConfigurableEnvironment.class);
      JdbcPropertySource propertySource = bf.getBean(JdbcPropertySource.class);
      // OS環境変数の次の優先順位で適用（優先順位は要件に応じて決める）
      environment.getPropertySources()
          .addAfter(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, propertySource);
    };
  }

  // プロパティプレースホルダを有効化するためのBean定義
  @Bean
  static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
    return new PropertySourcesPlaceholderConfigurer();
  }

}
```

今回は`BeanFactoryPostProcessor`の仕組みを使って`Environment`へ適用していますが、この方法が最適な方法か？はあまり自信はありません。もっと良い方法があればコメントいただければ幸いです。

# データベースの用意

本エントリーでは組込DBを使うので、テーブルとデータを投入するSQLを用意します。

```sql:src/main/resources/init-db.sql
drop table t_properties if exists;
create table t_properties (
  name varchar(512) not null primary key,
  value text
);

insert into t_properties (name, value) values ('services.user.url','http://dev01/services/user/{id}');
insert into t_properties (name, value) values ('services.company.url','http://dev01/services/company/{id}');
```

# まとめ

今携わっている案件（非Spring Boot案件・・・）で、（ひょっとしたら）一部の設定値をデータベースにもたせることが（システム運用的な観点で）求められるかもしれないので、技術的にどうすればできるかな〜と思ってプロトタイプを作ってみました。スレッドセーフ性の担保などいくつか改善の余地はありそうですが、Spring Frameworkの拡張ポイントを使って実現できそうなのがわかって一安心w