Abstruct

AP基盤/業務ロジック上で使用する静的プロパティを管理する方法としては、以下があるかと思います。

  • applicaiton.properties にプロパティを記載する。
  • @ConfigurationProperties を使って、オブジェクトにプロパティ値を読み込ませる。

参考:Spring Bootでプロパティファイルとのバインディング方法 - Qiita

この記事では、AP基盤/業務ロジック上で使用する動的プロパティMap<String, Object>として管理する方法について記載したいと思います。

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

1. Class Diagram Overview

AmaterasUML - Project Amateras を使ってクラス図を自動生成してみました。

1.1. Folder Structure(Overview)

フォルダ構成は以下のとおりです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
.
├── build.gradle
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
    └── main
        ├── java
        │   └── com
        │       └── example
        │           └── customproperty
        │               ├── CustomPropertyApplication.java
        │               ├── property
        │               │   ├── MyMapPropertySource.java
        │               │   ├── PropertyManager.java
        │               │   └── configuration
        │               │       └── SimpleConfiguration.java
        │               ├── restcontroller
        │               │   └── SimpleRestController.java
        │               └── service
        │                   └── SimpleService.java
        └── resources
            └── application.properties

1.2. CustomPropertyApplication(Overview)

SpringBootお決まりのクラス。
以下のドキュメントを参照ください。

Spring Boot を使用する - リファレンスドキュメント

1.3. SimpleConfiguration(Overview)

以下の3つのBeanを定義しています。

  • PropertySourceのBean定義
  • 今回作成するPropertySourceをEnvironmentへ適用するためのBean定義
  • プロパティプレースホルダを有効化するためのBean定義

1.4. SimpleRestController(Overview)

以下の3つのエンドポイントを作っています。

  • SimpleRestControllerの疎通確認用
  • Propertyを読み込む簡易なサービスの実行用
  • Propertyの更新処理用

1.5. SimpleService(Overview)

以下の1つのサービスを提供しています。

  • Propertyを読み込んで返却するサービス

1.6. PropertyManager(Overview)

ConfigurableEnvironmentのラッパークラス。
以下の2つのAPIを提供します。

  • プロパティの取得処理
  • プロパティの更新処理

Spring Frameworkが提供しているEnvironmentインタフェース(ConfigurableEnvironment) だと何でもできてしまうので、この部品でラップしています。
参考:Spring Bootの外部設定値の扱い方を理解する - Qiita

1.7. MyMapPropertySource(Overview)

InitializingBeanを実装して、EnumerablePropertySource を拡張したクラス。

@Override して3つのAPIを提供する。

  • String[] getPropertyNames()
    • Return the names of all properties contained by the source object (never null).
  • Object getProperty(String name)
    • Return the value associated with the given name, or null if not found.
  • void afterPropertiesSet()
    • Invoked by the containing BeanFactory after it has set all bean properties and satisfied BeanFactoryAware, ApplicationContextAware etc.

2. Source Code

2.1. CustomPropertyApplication

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package com.example.customproperty;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class CustomPropertyApplication {

	public static void main(String[] args) {
		SpringApplication.run(CustomPropertyApplication.class, args);
	}

}

2.2. SimpleConfiguration

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.example.customproperty.property.configuration;

import com.example.customproperty.property.MyMapPropertySource;
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 java.util.HashMap;

@Configuration
public class SimpleConfiguration {
    // 今回作成したMyMapPropertySourceをEnvironmentへ適用するためのBean定義
    @Bean
    static BeanFactoryPostProcessor environmentPropertySourcesCustomizer() {
        return beanFactory -> {
            var environment = beanFactory.getBean(ConfigurableEnvironment.class);
            var myMapPropertySource = beanFactory.getBean(MyMapPropertySource.class);

            // OS環境変数の次の優先順位で適用(優先順位は要件に応じて決める)
            environment.getPropertySources()
                    .addBefore(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, myMapPropertySource);
        };
    }

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

    // 今回作成したPropertySourceの設定
    @Bean
    MyMapPropertySource myMapPropertySource() {
        return new MyMapPropertySource("myMapProperties", new HashMap<String, Object>());
    }
}

2.3. SimpleRestController

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package com.example.customproperty.restcontroller;

import com.example.customproperty.property.PropertyManager;
import com.example.customproperty.service.SimpleService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Random;

@RestController
@Slf4j
public class SimpleRestController {

    @Autowired
    SimpleService simpleService;

    @Autowired
    PropertyManager propertyManager;

    // RestControllerの疎通確認用
    @RequestMapping("/test")
    public ResponseEntity<String> test(){
        return new ResponseEntity<String>("ok", HttpStatus.OK);
    }

    // Propertyを読み込むサービスの実行
    @RequestMapping("/execute")
    public ResponseEntity<String> execute() throws InterruptedException {
        // 処理中にプロパティが更新されたとしても、
        // 一貫して同じプロパティを参照できるかを確認するため、1秒 * 10回の読み込み処理を実施する。
        for(var i=0; i<10; i++){
            log.info(simpleService.service1("1"));
            log.info(simpleService.service1("2"));
            log.info(simpleService.service1("3"));

            Thread.sleep(1 * 1000);
        }

        return new ResponseEntity<String>("ok", HttpStatus.OK);
    }

    // Propertyの更新処理
    @RequestMapping("/map/update")
    public ResponseEntity<Boolean> updateMap(){
        var random = new Random();

        var map = new HashMap<String, Object>();
        map.put("1", random.nextInt(100));
        map.put("2", random.nextInt(100));
        map.put("3", random.nextInt(100));

        return new ResponseEntity<Boolean>(propertyManager.updateProperty(map), HttpStatus.OK);
    }
}

2.4. SimpleService

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package com.example.customproperty.service;

import com.example.customproperty.property.PropertyManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class SimpleService {
    @Autowired
    PropertyManager propertyManager;

    // プロパティ読み込みを実施するサービス
    public String service1(String key) {
        return (String)propertyManager.getProperty(key);
    }

}

2.5. PropertyManager

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.example.customproperty.property;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.stereotype.Component;

import java.util.HashMap;

@Aspect
@Component
public class PropertyManager {
    @Autowired
    private ConfigurableEnvironment environment;

    @Autowired
    private MyMapPropertySource myMapPropertySource;

    // トランザクション開始時の初期化処理
    @Before(value = "execution(* *..SimpleRestController.execute(..))")
    private void txStart(){
        myMapPropertySource.txStart();
    }

    // プロパティの取得処理
    public Object getProperty(String key){
        return environment.getProperty(key);
    }

    // プロパティの更新処理
    public Boolean updateProperty(HashMap<String, Object> map){
        var orgMap = myMapPropertySource.getProperties();
        for(var key : map.keySet()){
            if(orgMap.get(key) == null){
                orgMap.put(key, map.get(key));
            }else{
                orgMap.replace(key, map.get(key));
            }
        }
        myMapPropertySource.update(orgMap);
        environment.getPropertySources().replace("myMapProperties", myMapPropertySource);
        return true;
    }
}

2.6. MyMapPropertySource

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package com.example.customproperty.property;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.env.EnumerablePropertySource;

import java.util.HashMap;
import java.util.Map;

@Slf4j
public class MyMapPropertySource extends EnumerablePropertySource<Map<String, Object>> implements InitializingBean {

    private int latestIndex = 0;

    private ThreadLocal<Integer> currentIndex;

    private Map<String, Object>[] propertyList = new HashMap[2];

    public MyMapPropertySource(String name, Map<String, Object> source) {
        super(name, source);
    }

    @Override
    public String[] getPropertyNames() {
        return this.propertyList[this.currentIndex.get()].keySet().toArray(new String[0]);
    }

    void txStart() {
        // 現行スレッドの初期値を取得
        this.currentIndex = ThreadLocal.withInitial(() -> this.latestIndex);
    }

    @Override
    public Object getProperty(String name) {
        return this.propertyList[this.currentIndex.get()].get(name);
    }

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

    private void load(){
        var map = new HashMap<String, Object>();
        this.propertyList[this.latestIndex] = map;
        this.currentIndex = ThreadLocal.withInitial(() -> this.latestIndex);
    }

    boolean update(Map<String, Object> propertyMap){
        if(this.currentIndex.get() == 0){
            this.propertyList[1] = propertyMap;
            this.latestIndex = 1;
        }
        else{
            this.propertyList[0] = propertyMap;
            this.latestIndex = 0;
        }
        return true;
    }

    Map<String, Object> getProperties(){
        return new HashMap<>(this.propertyList[this.currentIndex.get()]);
    }
}

2.7. build.gradle

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
plugins {
	id 'org.springframework.boot' version '2.4.1'
	id 'io.spring.dependency-management' version '1.0.10.RELEASE'
	id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	compile 'org.springframework.boot:spring-boot-starter-aop'
}

test {
	useJUnitPlatform()
}

3. Digression

Map<String, Object> ではなくて、 DataSource を使った場合も記載しておきます。
(フォルダ構成本題と異なるソースコード/設定ファイル のみ。)

Spring Frameworkで設定値(プロパティ値)をデータベースから取得する方法 - Qiita を実装させて頂いただけです。

3.1. Folder Structure(Digression)

フォルダ構成は以下のとおりです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
.
├── build.gradle
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
    └── main
        ├── generated
        ├── java
        │   └── com
        │       └── example
        │           └── customproperty
        │               ├── CustomPropertyApplication.java
        │               ├── property
        │               │   ├── configuration
        │               │   │   └── SimpleConfiguration.java
        │               │   └── source
        │               │       └── JdbcPropertySource.java
        │               ├── restcontroller
        │               │   └── SimpleRestController.java
        │               └── service
        │                   └── SimpleService.java
        └── resources
            ├── application.properties
            └── init-db.sql

3.2. SimpleConfiguration(Digression)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.example.customproperty.property.configuration;

import com.example.customproperty.property.source.JdbcPropertySource;
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 SimpleConfiguration {
    // 今回作成したJdbcPropertySourceをEnvironmentへ適用するためのBean定義
    @Bean
    static BeanFactoryPostProcessor environmentPropertySourcesCustomizer() {
        return beanFactory -> {
            var environment = beanFactory.getBean(ConfigurableEnvironment.class);
            var propertySource = beanFactory.getBean(JdbcPropertySource.class);

            // OS環境変数の次の優先順位で適用(優先順位は要件に応じて決める)
            environment.getPropertySources()
                    .addBefore(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, propertySource);
        };
    }

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

    // 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);
    }
}

3.3. JdbcPropertySource(Digression)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.example.customproperty.property.source;

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) {
        var currentProperties = properties;
        return currentProperties.get(name);
    }

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

    public void load() {
        var 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;
    }

}

3.4. SimpleRestController(Digression)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.example.customproperty.restcontroller;

import com.example.customproperty.service.SimpleService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Random;

@RestController
@Slf4j
public class SimpleRestController {

    @Autowired
    SimpleService simpleService;

    // RestControllerの疎通確認用
    @RequestMapping("/test")
    public ResponseEntity<String> test(){
        return new ResponseEntity<String>("ok", HttpStatus.OK);
    }

    // ブログ通りの実装
    @RequestMapping("/service/{id}")
    public ResponseEntity<String> service(String id){
        return new ResponseEntity<String>(simpleService.getCompany(id), HttpStatus.OK);
    }
}

3.5. SimpleService(Digression)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package com.example.customproperty.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;

@Service
public class SimpleService {
    @Autowired
    Environment environment;

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

3.6. application.properties(Digression)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
services.user.url=http://user.service.com/{id}
services.company.url=http://company.service.com/{id}

# datasource
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:./h2db/demo
spring.datasource.username=username
spring.datasource.password=password

# connection pool use tomcat
spring.datasource.tomcat.maxActive=10
spring.datasource.tomcat.maxIdle=10
spring.datasource.tomcat.minIdle=10
spring.datasource.tomcat.initialSize=10
spring.datasource.tomcat.defaultAutoCommit=false

3.7. init-db.sql(Digression)

1
2
3
4
5
6
7
8
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}');

3.8. build.gradle(Digression)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
plugins {
	id 'org.springframework.boot' version '2.4.1'
	id 'io.spring.dependency-management' version '1.0.10.RELEASE'
	id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'

	implementation 'org.springframework.boot:spring-boot-starter-jdbc'
	runtime 'com.h2database:h2'
}

test {
	useJUnitPlatform()
}

4. Reference