読者です 読者をやめる 読者になる 読者になる

だったら壁にでも話してろよ。

技術ネタはQiitaに移行したので、出口のない悩みや考えを垂れ流すことにします。

JSUG 2015 Spring in Summer で話をしてきた話

すこし日が空きましたが、先日 8月28日に日本Springユーザー会のイベント「Spring in Summer ~ 夏なのにSpring」でお話をしてきました。 なかなか人前で話をする機会がこれまでなかったので、お誘いいただいた関係者の皆様、貴重な機会をありがとうございました。

全体的に伝えたかったのは、オレオレフレームワーク作りだしちゃうと後々困るよってのと、Springは非常に良く出来ているのでちゃんと調べた上で使い倒していきましょうねってこと。

オレオレはホントダメ。わずかでも許しちゃダメ。とくにサービスを大きくしていきたいとか、今後も継続的に保守・アップデートしていきたいとか考えていたなら、絶対ダメ。プロジェクトがスケールしてくると、必然的に人は増えるわけですけど、そのときに受け入れ側のアーキテクチャがオレオレなのか、そうじゃないのかってのは本当に差が大きい。

ここをSpring標準に乗せておくだけで、プロジェクトの途中から参入してきたメンバーの立ち上がりもグッと早くなるし、必要な人員の募集要項も明瞭化する。Springができること、を条件にすればよいだけなので。オレオレが使えるかどうか?みたいな曖昧で不明瞭な判断基準を用いる必要はなくなる。

というわけで今更ですが、改めて。
Springおすすめです。

その他のセッションでは、やはりJosh Longさんのトークセッション&ライブコーディングが非常に聞き応え・見応えがあり、とても勉強になりました。 わたしの発表には、タイトルにもあるとおり一部でバッドノウハウの紹介なんかもあったわけなんですけど、Joshさんの何気ない一言にとても衝撃を受けました。

Josh:
「みなさんはフィールドインジェクションなんて使ってませんよね?」
「もちろんコンストラクタインジェクション使ってますよね」
「テストできなくなっちゃいますからね」

自分:
「あー、フィールドインジェクション多用してるわー」
「またバッドノウハウ増えたわー」

といった具合。

よくよく考えればアタリマエのことだし非常に理にかなったことなのだけど、何故かこれに気がついていなかった。コードサンプルの多くでフィールドインジェクションを使っていたり、これまでの慣例的にフィールドインジェクションを優先的に使っていた自分に反省。そして目からうろこだった。

経験的にも、知識的にも、得るものが多くたいへん良い会でした!

Spock x DBUnit でテストを書いてみる

Java プロジェクトで、Groovy を使えるようになって、Spock を試してみたところ、とても使いやすく、これはいいものだとすぐさま実感できた。

興味を刺激されたのでさらに調べてみると、Groovy を利用することで、DBUnit のデータ挿入もコード内で簡潔に済ませてしまう例などが見つかり、これは捗りそうだなーと感じた。

このへんとか。

d.hatena.ne.jp

ただ、Groovy の表現力をもってすれば、「もっといけるだろう」という期待もあった。ので、ちょっと不足していた Groovy の知識を補いつつ、いろいろ試してみた。ゴールは Spock ライクにデータ挿入をおこなえるようにすること。

Spring と、Mybatis を組み合わせた状態のテストコードになっているものの、データ挿入部分であれば、ほぼそのまま利用できるので載せておく。できたのが以下。

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <!-- 中略 -->

    <dependencies>
        <dependency>
            <groupId>com.yo1000</groupId>
            <artifactId>dbspock</artifactId>
            <version>0.1.2.RELEASE</version>
        </dependency>
    </dependencies>

    <repositories>
        <repository>
            <id>com.yo1000</id>
            <name>yo1000 maven repository</name>
            <url>http://yo1000.github.io/maven/</url>
        </repository>
    </repositories>
</project>

MybatisCustomerRepositorySpec.groovy

// 省略

@ContextConfiguration(loader = SpringApplicationContextLoader, classes = [ApplicationContext, TestContext])
@WebIntegrationTest(["server.port=55555"])
@Unroll
@Stepwise
@Transactional
@TransactionConfiguration
class MybatisCustomerRepositorySpec extends Specification {
    @Autowired
    DataSource dataSource;

    @Autowired
    MybatisCustomerRepository mybatisCustomerRepository;

    def "findSummaryByYearMonthテスト"() {
        setup:
        def tester = new DataSourceDatabaseTester(dataSource)

        def data = {
            _cols_ 'STR_ID' | 'STR_NAME' | 'STR_CREATED' | 'STR_MODIFIED'
            store  'ST1X'   | 'おみせ1X'  | '2015-04-01'  | '2015-04-01'
            store  'ST2X'   | 'おみせ2X'  | '2015-04-01'  | '2015-04-01'

            _cols_   'CST_ID' | 'CST_LASTNAME' | 'CST_FIRSTNAME' | 'CST_SEX' | 'CST_AGE' | 'CST_REGION' | 'CST_CREATED' | 'CST_MODIFIED'
            customer 'CS1X'   | 'みょうじ1X'    | 'なまえ1X'       | '1'       | 20        | '東京都'      | '2015-04-01'  | '2015-04-01'
            customer 'CS2X'   | 'みょうじ2X'    | 'なまえ2X'       | '2'       | 10        | '神奈川県'    | '2015-04-01'  | '2015-04-01'

            _cols_ 'SLS_ID' | 'SLS_SALES' | 'SLS_STR_ID' | 'SLS_CST_ID' | 'SLS_CREATED' | 'SLS_MODIFIED'
            sales  'SL1X'   | 1000        | 'ST1X'       | 'CS1X'       | '2015-04-01'  | '2015-04-01'
            sales  'SL2X'   | 2000        | 'ST1X'       | 'CS2X'       | '2015-04-01'  | '2015-04-01'
            sales  'SL3X'   | 3000        | 'ST2X'       | 'CS1X'       | '2015-05-01'  | '2015-05-01'
            sales  'SL4X'   | 4000        | 'ST2X'       | 'CS1X'       | '2015-05-01'  | '2015-05-01'

            build()
        }
        data.delegate = new SpockLikeFlatXmlBuilder()

        tester.dataSet = new FlatXmlDataSet(new StringReader(data.call()))
        tester.onSetup()

        expect:
        def items = mybatisCustomerRepository.findSummaryByYearMonth(new SearchCondition(
                storeId,
                new SimpleDateFormat("yyyyMM").parse(fromDate),
                new SimpleDateFormat("yyyyMM").parse(toDate)
        ));

        assert expect == items.size()

        where:
        storeId | fromDate | toDate   || expect
        "ST1X"  | "201503" | "201506" || 2
        "ST2X"  | "201503" | "201506" || 1
    }
}

Spock での where 同様に、テストデータの作成部分でも | を使用して、Spock ライクなデータの表現ができている。これを実現するために勉強した Groovy の機能が以下の3つ。

まずは、Spock のように表形式でデータを表現するために、| 演算子オーバーロードを実装する必要があった。これは難なく実装できた。実装については後述。

次に、クロージャ内でテーブル名として登場する、存在しない名前のメソッド呼び出し。これに難儀した。調べてみると、Groovy では (GroovyObject を継承したクラスでは)、存在しないメソッドがコールされた場合、invokeMethod が、処理をハンドリングすることがわかった。以下の記事などが大変参考になりました。感謝です。

13スライド目

www.slideshare.net

opamp.hatenablog.jp

そして、これらを利用して実装したクラスの動きを適用するために delegate で、このクラスに処理を移譲させた。なんだろう、われながら説明が非常にわかりにくい。。。なお、delegate については以下の記事が参考になりました。ありがとうございます。

uehaj.hatenablog.com

npnl.hatenablog.jp

さて、これらを経て実装したのが以下です。

package com.yo1000.dbspock

/**
 * Created by yoichi.kikuchi on 2015/07/13.
 */
class SpockLikeFlatXmlBuilder extends GroovyObjectSupport {
    def cols = []
    def items = []

    SpockLikeFlatXmlBuilder() {
        Object.metaClass.or = { x ->
            if (!(delegate instanceof List)) {
                return [delegate, x]
            }
            delegate << x
        }
    }

    @Override
    Object invokeMethod(String name, Object args) {
        if (!(args instanceof Object[]) || args.size() <= 0) {
            return super.invokeMethod(name, args)
        }

        def arg = args[0]

        if (name.toLowerCase() == "_cols_") {
            cols = arg
            return
        }

        def builder = new StringBuilder(name)

        for (def i = 0; i < cols.size(); i++) {
            builder.append(/ ${cols[i]}="${arg[i]}"/)
        }

        this.items << "<${builder.toString()}/>"
    }

    String build() {
        def builder = new StringBuilder("<dataset>")
        for (def item : this.items) {
            builder.append(item)
        }
        builder.append("</dataset>")

        return builder.toString()
    }
}

テストデータの定義を表現したクロージャの処理を、この作成したクラスに移譲することで、DBUnit が受け取れる FlatXml 形式の文字列に起こしてやる、ということをやっています。

こんな短いコードで Spock ライクにテストデータの投入ができるようになるのだから、Groovy はすごい。これでずいぶんとテストが捗るのではないかと期待している。

今回のコードは Github でも公開しつつ、また、Maven リポジトリとしても公開してみたので、データ投入をコード内で Spock ライクにおこなってみたいなんて場合には、ためしに使ってみてはどうか。

github.com

再掲 pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <!-- 中略 -->

    <dependencies>
        <dependency>
            <groupId>com.yo1000</groupId>
            <artifactId>dbspock</artifactId>
            <version>0.1.2.RELEASE</version>
        </dependency>
    </dependencies>

    <repositories>
        <repository>
            <id>com.yo1000</id>
            <name>yo1000 maven repository</name>
            <url>http://yo1000.github.io/maven/</url>
        </repository>
    </repositories>
</project>

Groovy でテストを書いてみる (STS: Spring Tool Suite)

IntelliJ IDEA に引き続き、Spring Tool Suite でも Groovy でテストを書いてみようと思い、試してみた。

使用した STS のバージョンは、STS 3.7.0.RELEASE (Based on Eclipse 4.5) の、Mac OS X 用のもの。基本的に Windows 版でも手順はさほど変わらない。

以下手順。

Groovy Eclipse Maven plugin のインストール

The Groovy Eclipse Maven plugin をインストールする。執筆時点 (2015-07-13) で、まだ Eclipse 4.5 用のプラグインが正式にリリースされていなかったため、今回はスナップショットから取得する。

http://docs.groovy-lang.org/latest/html/documentation/tools-groovyeclipse.html

  1. [Help] > [Install New Software...] メニューを選択。 f:id:Yoichi-KIKUCHI:20150713161752p:plain

  2. [Work with:] へ http://dist.springsource.org/snapshot/GRECLIPSE/e4.5 を入力して、出てきた一覧全てをチェック。 f:id:Yoichi-KIKUCHI:20150713162053p:plain

  3. [Next]、[Next]、... とダイアログを進めていき、ライセンス確認まできたら、"I accept the terms of the license agreement" をチェックして、[Finish] ボタンをクリック。 f:id:Yoichi-KIKUCHI:20150713163424p:plain

  4. 再起動を促されるので、[Yes] で IDE を再起動。 f:id:Yoichi-KIKUCHI:20150713163459p:plain

ここまでで、プラグインのインストールは終わり。

プロジェクトへ、Groovy の適用

ひきつづき、プロジェクトのインポートをおこない、プロジェクトに Groovy サポートを追加すしていく。すでに pom.xml の存在しているプロジェクトが手元にある前提で進めます。

  1. [File] > [Import...] メニューを選択。インポートダイアログで、[Maven] > [Existing Maven Projects] を選択し、プロジェクトをインポートする。 f:id:Yoichi-KIKUCHI:20150713181727p:plain

  2. プロジェクトがインポートできたら、プロジェクト ルートを右クリックして、[Configure] > [Convert to Groovy Project] を選択。 f:id:Yoichi-KIKUCHI:20150713190934p:plain

  3. これで Groovy サポートが有効になるが、クラスパスに Maven で取得した Groovy と、プラグインで追加される Groovy でライブラリ競合を起こすので、プラグイン側の Groovy を除去してしまう。プロジェクト ルートを右クリックして、[Groovy] > [Remove Groovy libraries from classpath] で、プラグイン側の Groovy を除去できる。 f:id:Yoichi-KIKUCHI:20150713192012p:plain

  4. 以上で、Groovy で記述されたテストも、実行可能になる。 f:id:Yoichi-KIKUCHI:20150713192050p:plain

ライブラリの競合に気づかず、しばらくハマった。。。

Groovy でテストを書いてみる (IntelliJ IDEA)

いつもは JUnit で済ませてしまうテストを、Groovy で書いてみようと思い、試してみたメモ。

今回使用したのは、IntelliJ IDEA 15 EAP (Early Access Program)。非常に簡単に Groovy サポートが有効になって驚いたが、IntelliJ IDEA は、そもそも Groovy をサポートしているので、むずかしいわけはなかった。

以下手順。

pom.xml に、依存関係を追加

<dependency>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy-all</artifactId>
    <version>2.4.3</version>
    <scope>test</scope>
</dependency>

以上。

試しにほぼ JUnit まんまのコードで動作を確認してみる。今回は比較のため、もとにした Java のコードも併記しておく。

Java でのテストコード。

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = {ApplicationContext.class, TestContext.class})
@WebIntegrationTest({"server.port=55555"})
public class SalesResourceTest {
    @Autowired
    private RestTemplate restTemplate;

    @Test
    public void getSalesSummary() throws Exception {
        SalesSummary[] items = restTemplate.getForObject(
                "http://localhost:55555/api/v1/store/{storeId}/sales/summary/{dateFrom}/{dateTo}",
                SalesSummary[].class, new HashMap<String, String>() {
                    {
                        this.put("storeId", "ST2");
                        this.put("dateFrom", "201412");
                        this.put("dateTo", "201506");
                    }
                }
        );

        Assert.assertThat(items, CoreMatchers.any(SalesSummary[].class));
    }
}

こちらが同様の内容を Groovy に直したもの。

@RunWith(SpringJUnit4ClassRunner)
@SpringApplicationConfiguration(classes = [ApplicationContext, TestContext])
@WebIntegrationTest(["server.port=55555"])
class SalesResourceTest {
    @Autowired
    RestTemplate restTemplate;

    @Test
    void getSalesSummary() {
        def items = restTemplate.getForObject(
                "http://localhost:55555/api/v1/store/{storeId}/sales/summary/{dateFrom}/{dateTo}",
                SalesSummary[], [
                        storeId: "ST2",
                        dateFrom: "201412",
                        dateTo: "201506"
                ]
        );

        Assert.assertThat(items, Matchers.any(SalesSummary[]));
    }
}

これだけだと煩雑だった部分がちょっとスッキリした程度で、そこまで強い魅力は感じづらいかもしれない。ただ、Groovy を動かせることで、できることもいろいろと増えていくので、今回はここまでで良しとする。

Spring Boot と MyBatis を組み合わせて使用する

クエリレベルでのチューニングの必要があるようなプロジェクトで、Hibernate を適用できないといった場合にたびたび採用されるであろう MyBatis。今回はこれを Spring Boot と組み合わせてみる。

Spring Boot を使用すると、コンフィグレーションのほとんどがアノテーションベースになるため、MyBatis もそうかと思えば、そうではない。MyBatis は MyBatis で、これまでどおり XML ベースのコンフィグレーションファイルが必要になるので、ここは事前に認識しておく。

Spring と MyBatis の連携には、mybatis-spring という、インテグレーションモジュールがでているので、まずは POM にこれらを追加する。

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.2.8</version>
</dependency>
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>1.2.2</version>
</dependency>
<dependency>
    <groupId>commons-dbcp</groupId>
    <artifactId>commons-dbcp</artifactId>
    <version>1.4</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-dbcp2</artifactId>
    <version>2.1</version>
</dependency>

Spring から MyBatis を使用する場合には、SqlSessionTemplate クラスを使用するので、これを構成する。

@SpringBootApplication
public class ApplicationContext {
    @Autowired
    @Bean
    public DataSourceInitializer dataSourceInitializer(
            @Qualifier("dataSource") DataSource dataSource) {
        DataSourceInitializer dataSourceInitializer = new DataSourceInitializer();
        dataSourceInitializer.setDataSource(dataSource);
        ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator();
        databasePopulator.addScript(new ClassPathResource("sql"));
        dataSourceInitializer.setDatabasePopulator(databasePopulator);
        dataSourceInitializer.setEnabled(false);

        return dataSourceInitializer;
    }

    @Autowired
    @Bean
    public DataSourceTransactionManager transactionManager(
            @Qualifier("dataSource") DataSource dataSource) {
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
        transactionManager.setDataSource(dataSource);

        return transactionManager;
    }

    @Autowired
    @Bean
    public SqlSessionTemplate sqlSessionTemplate(
            @Qualifier("dataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);

        ResourcePatternResolver resolver = ResourcePatternUtils.getResourcePatternResolver(
                new DefaultResourceLoader());

        // MyBatis のコンフィグレーションファイル
        bean.setConfigLocation(resolver.getResource("classpath:config.xml"));
        // MyBatis で使用する SQL ファイル群
        bean.setMapperLocations(resolver.getResources("classpath:sql/*.xml"));

        return new SqlSessionTemplate(bean.getObject());
    }

    @Primary
    @Autowired
    @Bean
    public DriverManagerDataSource dataSource() {
        // 今回の例は ORACLE、適宜変更する
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName("oracle.jdbc.driver.OracleDriver");
        dataSource.setUrl("jdbc:oracle:thin:@//localhost:1521/xe");
        dataSource.setUsername("system");
        dataSource.setPassword("manager");

        return dataSource;
    }
}

続いて、MyBatis 側でのコンフィグレーションファイルの内容。この設定ファイルがない場合、アンダースコア区切り (スネークケース) のカラム名が、キャメルケースのフィールドにマッピングされないため、ほとんど必須。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <!-- アンダースコア区切り (スネークケース) のカラム名をキャメルケースにマップする設定 -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <!-- SQL 内で AS によって設定された列名をマップする設定 -->
        <setting name="useColumnLabel" value="true"/>
    </settings>
    <!-- カラムタイプによりデフォルトとは異なる O/R 変換処理が必要になる場合に設定 -->
    <!--
    <typeHandlers>
        <typeHandler handler="package.name.LocalDateTypeHandler"/>
    </typeHandlers>
    -->
</configuration>

最後に Repository (永続化) レイヤでの SqlSessionTemplate の使い方と SQL ファイルのマッピングを例にあげておく。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 任意の名前空間を定義する -->
<mapper namespace="SalesRepository">
    <!-- 名前空間 + ID でプロジェクト全体で一意になるように名前を設定 -->
    <!-- ParameterType は SqlSessionTemplate で渡すパラメタの型 -->
    <!-- ResultType は SqlSessionTemplate で返ってくるデータの型 -->
    <select id="findSummaryByYearMonthRange"
            parameterType="jp.hotpepper.beauty.frozen.model.entity.SearchCondition"
            resultType="jp.hotpepper.beauty.frozen.model.entity.SalesSummary">
        SELECT
          STORE.STORE_ID        AS STORE_ID,
          STORE.NAME            AS STORE_NAME,
          COUNT(SALES.SALES_ID) AS SALES_COUNT,
          SUM(SALES.SALES)      AS SALES_TOTAL,
          AVG(CUSTOMER.AGE)     AS AGE_AVERAGE
        FROM
          SALES
        LEFT OUTER JOIN
          STORE
          ON SALES.STORE_ID = STORE.STORE_ID
        LEFT OUTER JOIN
          CUSTOMER
          ON SALES.CUSTOMER_ID = CUSTOMER.CUSTOMER_ID
        WHERE
          <!-- パラメタ型のフィールド名を #{ } で囲んで設定 -->
          <!-- パラメタ型が String など構造を持たない場合は任意の名前でよい -->
          SALES.CREATED BETWEEN #{from} AND #{to}
        GROUP BY
          STORE.STORE_ID,
          STORE.NAME
        ORDER BY
          STORE_ID
    </select>
</mapper>
@Repository
public class MybatisSalesRepository implements SalesRepository {
    @Autowired
    private SqlSessionTemplate template;

    @Override
    public List<SalesSummary> findSummaryByYearMonth(SearchCondition condition) {
        List<SalesSummary> items = this.<SalesSummary>getTemplate().selectList(
                // SQL を定義した XML での、名前空間 + ID を指定
                // ID だけでもプロジェクト全体で一意になる場合は、ID のみの指定も可能
                "SalesRepository.findSummaryByYearMonthRange",
                condition);

        return items;
    }

    protected SqlSessionTemplate getTemplate() {
        return template;
    }
}

全体としてはこのような形で、Spring Boot で MyBatis を使用できるようになる。MyBatis のコンフィグレーションファイルを、アプリケーションコンテキストで別途読み込まないといけないところに気付くまでにずいぶん時間がかかってしまった。

Spring Boot で applicationContext.xml の ref 属性を表現する

Spring Framework 4、および Boot あたりから、本格的にアノテーションベースのコンフィグレーションに移行が進んでいる。これに際して、Spring Framework が直接使用するコンフィグレーションに関しては、すべてアノテーションベースに移行しようと思い試したところ、表題にあるような、applicationContext.xml でごく普通に使用していた、ref 属性に相当する書き方はどのようになるのか、しばらく書き方がわからず、けっこうハマったので残しておく。

書き方はというと、参照される側 (@Bean 属性を適用した Bean メソッド) の名前をキーに使用して、参照する側 (ref 属性を使用したい Bean メソッド) の引数で、オートワイヤリングして、DI をかけてやる。名前の特定には、@Qualifier アノテーションを使用する。以下のようになる。

@Autowired
@Bean
public SqlSessionTemplate sqlSessionTemplate(
        @Qualifier("dataSource") DataSource dataSource) throws Exception {
    SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
    bean.setDataSource(dataSource);

    ResourcePatternResolver resolver = ResourcePatternUtils.getResourcePatternResolver(
            new DefaultResourceLoader());

    bean.setConfigLocation(resolver.getResource("classpath:mybatis-config.xml"));
    bean.setMapperLocations(resolver.getResources("classpath:sql/*.xml"));

    return new SqlSessionTemplate(bean.getObject());
}

@Primary
@Autowired
@Bean
public DriverManagerDataSource dataSource() {
    DriverManagerDataSource dataSource = new DriverManagerDataSource();
    // dataSource.set ...

    return dataSource;
}

このように ref 属性相当の書き方をしないとならない場面が少なからずあるので、必ずおさえておきたい。

Spring Boot で設定可能なプロパティを追加する 2

過去記事に関連して、より柔軟なプロパティの割り当て方に至ったため、改めてまとめておく。 yo1000.hateblo.jp

@ConfigurationProperties アノテーションを付けたクラスが、application.properties に設定の外部化をできるようになる、というのは以前紹介したとおり。だが、このアノテーション、クラスのみに適用可能というわけではないようだったので、いろいろ調べてみたところ、@Bean アノテーションと併用することで、Bean メソッドにも適用可能であることがわかった。

即ち以下のようになる。

@Bean
@ConfigurationProperties(prefix = "userconf.jdbc")
public JdbcConfiguration jdbcConfiguration() {
    return new JdbcConfiguration();
}

@SpringBootApplication
public static class JdbcConfiguration {
    private String driverClassName;
    private String connectionString;
    private String username;
    private String password;
    private String schema;

    // getter/setter
}

クラス側に直接 @ConfigurationProperties アノテーションを適用しない場合、設定可能となるプロパティとアノテーションの間で、少しばかり行が空いてしまうことになり、このあたりの見通しは若干犠牲になるかもしれない。ただ、Bean メソッドにこのアノテーションを適用することにはそれ以上の意味がある。

どのような場面で使用するかといえば、上記のような JDBC 接続設定クラスを作成したはいいが、接続先のデータソースが複数ある、といった場合にこれを活かすことができる。クラス側に @ConfigurationProperties を適用してしまうと、同様のフィールド一式を持ったクラスを複数作成しなければならないことになり、コードの重複が発生してしまうため、Bean メソッド側にアノテーションを適用して設定クラスの重複を回避する。