Edited at

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

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

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


設定値の指定例(propertiesファイル)

services.user.url=http://user.service.com/{id}

services.company.url=http://company.service.com/{id}


設定値の参照例

@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」を使うのが一般的だと思いますが、ここでは意図的に@Value」を使ってプロパティプレースホルダ(「${...}」)を使うように指定ます。(解説はしませんが、ここで紹介する方法は「Type-safe Configuration Properties」にも適用できる内容になっています)

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


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

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

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への適用

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を用意します。


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