DB接続設定をパラメータで指定するSpring Bootアプリケーション

Spring BootでDB接続を行うコマンドラインアプリケーションを作ってみた。
それ自体はよくあるアプリケーションなのだが、DBの接続情報はコマンドのパラメータで指定できるように実装されているのはあまり見かけないので実装方法を纏めておこうと思う。

## 環境

- OpenJDK 13.0.2
- Gradle 6.2.1
- Sprinig Boot 2.2.5
- Apache Commons CLI 1.4

## 初期処理

`org.springframework.boot.CommandLineRunner`インタフェースを実装したクラスを実装するとSpring Bootさんが起動処理後に呼び出してくれるらしい。

- examples.cli.MainRunner

```
@Component
@Lazy(false)
public class MainRunner implements CommandLineRunner {

    private static String HELP_MESSAGE = "java -jar cli-examples.jar -k <key> [option]";

    @Autowired
    ApplicationContext context;

    @Override
    public void run(String... args) throws Exception {
        Options options = new Options();
        options.addOption("?", "help", false, "ヘルプメッセージを表示します。");
        options.addOption("h", "host", true, "DBのホスト名を指定してください。この値デフォルト値は\"localhost\"です。");
        options.addOption("p", "port", true, "DBのポート番号を指定してください。この値デフォルト値は\"5432\"です。");
        options.addOption("U", "dbUserId", true, "DBのユーザIDを指定してください。この値デフォルト値は\"postgres\"です。");
        options.addOption("P", "dbPassword", true, "DBのパスワードを指定してください。この値デフォルト値は\"postgres\"です。");
        options.addOption("d", "dbName", true, "データベース名を指定してください。この値デフォルト値は\"postgres\"です。");
        options.addOption("k", "key", true, "データ検索時のキーを指定してください。この値は必須です。");

        // オプションの解析
        CommandLine cl = null;
        try {
            cl = new DefaultParser().parse(options, args);
        } catch (UnrecognizedOptionException e) {
            System.err.println("不明なオプションが指定されています。[" + e.getMessage() + "]");
        } catch (MissingArgumentException e) {
            System.err.println("オプション引数が入力されていません。[" + e.getMessage() + "]");
        }
        if (cl == null || cl.hasOption("?")) {
            new HelpFormatter().printHelp(HELP_MESSAGE, options);
            return;
        }

        // データベースの接続情報を取得する
        DataBaseInfo.create(cl);
        if (DataBaseInfo.instance == null) {
            new HelpFormatter().printHelp(HELP_MESSAGE, options);
            return;
        }

        // データ検索用のデータを取得する
        ParamInfo paramInfo = ParamInfo.create(cl);
        if (paramInfo == null) {
            new HelpFormatter().printHelp(HELP_MESSAGE, options);
            return;
        }

        // サービスを実行する
        MainService service = context.getBean(MainService.class);
        long count = service.execute(paramInfo);
        System.out.println("キー（" + paramInfo.key + "）の検索結果：" + count + "件");
    }

}
```

こんな感じで実装してみた。
`@Lazy(false)`については後述するとして、パラメータの解析にはApache Commons CLIを使用した。
ヘルプを表示するとこんな感じになる。

```
usage: java -jar cli-examples.jar -k <key> [option]
 -?,--help               ヘルプメッセージを表示します。
 -d,--dbName <arg>       データベース名を指定してください。この値デフォルト値は"postgres"です。
 -h,--host <arg>         DBのホスト名を指定してください。この値デフォルト値は"localhost"です。
 -k,--key <arg>          データ検索時のキーを指定してください。この値は必須です。
 -p,--port <arg>         DBのポート番号を指定してください。この値デフォルト値は"5432"です。
 -P,--dbPassword <arg>   DBのパスワードを指定してください。この値デフォルト値は"postgres"です。
 -U,--dbUserId <arg>     DBのユーザIDを指定してください。この値デフォルト値は"postgres"です。
```

Pythonとかだと標準ライブラリにパラメータ解析クラスが組み込まれてるのでJavaでも組み込んでほしい。

## DB接続設定の初期化

入力パラメータの解析処理は上記ソース内の以下の箇所。

```
DataBaseInfo.create(cl);
```

Spring Bootさんは起動時コンポーネントの初期化などを行うため、登録しているコンポーネントにDBアクセス関連のクラスをDIしているとデータソースの初期化などを行ってしまい、エラーが発生する。
そのため、Spring Boot v2.2.0から実装されたLazy Initializationでデータソース初期化処理をコンポーネント取得時に行うようにする。
設定方法は`application.yml`に`spring.main.lazy-initialization`の設定を追加するだけ。

- application.yml

```
spring:
  main:
    banner-mode: log
    lazy-initialization: true
```

で、データソースのインスタンス生成処理は以下の通り。

- examples.cli.App

```
@SpringBootApplication
@Configuration
public class App {

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

    @Bean
    public DataSource dataSource() {
        if (DataBaseInfo.instance == null) {
            throw new IllegalStateException("DB接続情報の初期化が完了していません。");
        }

        StringBuilder builder = new StringBuilder();
        builder.append("jdbc:postgresql://");
        builder.append(DataBaseInfo.instance.host).append(":").append(DataBaseInfo.instance.port);
        builder.append("/").append(DataBaseInfo.instance.dbName);

        SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
        dataSource.setDriverClass(org.postgresql.Driver.class);
        dataSource.setUrl(builder.toString());
        dataSource.setUsername(DataBaseInfo.instance.userId);
        dataSource.setPassword(DataBaseInfo.instance.password);
        Properties connectionProperties = new Properties();
        connectionProperties.setProperty("autoCommit", "false");
        dataSource.setConnectionProperties(connectionProperties);
        return dataSource;
    }

}
```

また、初期処理を行っている`examples.cli.MainRunner`クラスでは、`@Lazy(false)`をつけてLazy Initializationを無効にしている。（どうせすぐ呼ばれるので`@Lazy(false)`つける意味はないかもしれない）

## DBアクセスを行うクラス

`examples.cli.MainRunner`クラスでは入力パラメータ解析前に実行されるため、普通にDBアクセスを行うクラスをDIしていると起動時にエラーとなる。

そのため、DBアクセスを行うクラスの取得はApplicationContextから直接取得するように実装している。

```
// サービスを実行する
MainService service = context.getBean(MainService.class);
long count = service.execute(paramInfo);
```

このように実装することで、データソース初期化処理をコマンドパラメータ解析後に行えるようになっている。

DBアクセスを行うクラスは以下の通り。

```
@Component
public class MainServiceImpl implements MainService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Transactional
    @Override
    public long execute(ParamInfo paramInfo) {
        Long selectCount = jdbcTemplate.queryForObject(
                "select count(*) from main_tbl where key = ?",
                new Object[] { paramInfo.key },
                Long.class);
        return selectCount;
    }

}
```

## Lazy Initializationを使わない実装方法

諸事情でLazy Initializationが使えない場合は、動的にデータソースを切り替えられる`org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource`を使用すれば実現できる。

`spring.main.lazy-initialization`の設定の設定を削除してデータソース初期化処理を以下のように実装する。

```
@SpringBootApplication
@Configuration
public class App {

    private static final String DS_NONE_KEY = "ds_none";
    private static final String DS_PGSQL_KEY = "ds_pgsql";

    static DynamicRoutingDataSourceResolver resolver = new DynamicRoutingDataSourceResolver();

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

    @Bean
    public DynamicRoutingDataSourceResolver dataSource() throws ClassNotFoundException {
        SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
        Map<Object, Object> dataSources = new HashMap<>();
        dataSources.put(DS_NONE_KEY, dataSource);
        resolver.setTargetDataSources(dataSources);
        resolver.setDefaultTargetDataSource(dataSource);
        return resolver;
    }

    static class DynamicRoutingDataSourceResolver extends AbstractRoutingDataSource {

        private String dataSourcekey = DS_NONE_KEY;

        @Override
        protected Object determineCurrentLookupKey() {
            return dataSourcekey;
        }

        public void initDataSource(DataBaseInfo dbInfo) {
            StringBuilder builder = new StringBuilder();
            builder.append("jdbc:postgresql://");
            builder.append(dbInfo.host).append(":").append(dbInfo.port);
            builder.append("/").append(dbInfo.dbName);

            SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
            dataSource.setDriverClass(org.postgresql.Driver.class);
            dataSource.setUrl(builder.toString());
            dataSource.setUsername(dbInfo.userId);
            dataSource.setPassword(dbInfo.password);
            Properties connectionProperties = new Properties();
            connectionProperties.setProperty("autoCommit", "false");
            dataSource.setConnectionProperties(connectionProperties);

            Map<Object, Object> dataSources = new HashMap<>();
            dataSources.put(DS_PGSQL_KEY, dataSource);
            dataSourcekey = DS_PGSQL_KEY;

            setTargetDataSources(dataSources);
            setDefaultTargetDataSource(dataSource);
            afterPropertiesSet();
        }
    }

}
```

起動時の初期化処理では空のデータソースを設定しておき、コマンドパラメータ解析後に`initDataSource`メソッドを呼び出すとデータソースが切り替わる仕組み。

もっといい方法があるかもしれないが、自分で調べられる限界はここまで。

※以下の設定を利用してDBの初期化処理を手動で行うとしたが、Lazy Initializationと同じような気がしたのでやらなかった。

```
@EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class })
```

## ソース

実装したソースは以下の通り。

[cli-examples](https://github.com/specialweek1128/cli-examples)


## 参考

今回参考にしたサイトは以下の通り。

- [Spring Boot で CLI アプリをつくる](https://qiita.com/yo1000/items/496ab2cf50b5704b5c58)
- [Spring Boot 2.2.0でLazy Initializationを試してみた](https://qiita.com/forests-k/items/a533d18a6797a1ff29b3)
- [[Spring Boot] 動的データソース変更](https://qiita.com/syukai/items/0d4bf27f82fef9965cdd)

## おわりに

初投稿は大変便利なSpring Bootさんでした。
こんな感じでたわいもない事をメモ代わりに投稿していこうかと思う。