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

かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は Spring Boot をいじっています。

Spring Boot でメール送信する Web アプリケーションを作る ( 番外編 )( DataSource Bean に application.properties の設定が反映される仕組みとは? )

はじめに

DataSource Bean で DataSourceBuilder.create().build() を呼び出してインスタンスを生成した時に application.properties の設定が反映されているものと思っていたのですが、この時点ではまだ反映されていませんでした。どのような仕組みで反映されているのか興味が湧いたので調べてみます。

現在確認できていることは?

DataSource Bean 内で DataSourceBuilder.create().build() で DataSource のインスタンスを生成した時には application.properties の設定は反映されていないのですが、Controller クラス等で @Autowired アノテーションで DI した時には dataSource に application.properties の設定が反映されています。

src/main/java/ksbysample/webapp/email/config の下の ApplicationConfig.java を以下のように変更した後、

@Configuration
public class ApplicationConfig {

    @Bean
    @ConfigurationProperties("spring.datasource")
    public DataSource dataSource() {
        org.apache.tomcat.jdbc.pool.DataSource dataSource
                = (org.apache.tomcat.jdbc.pool.DataSource)DataSourceBuilder.create().build();
        System.out.println("★★★ " + dataSource.getDriverClassName());
        System.out.println("★★★ " + dataSource.getUrl());
        return dataSource;
    }

}

bootRun タスクを実行して Tomcat を起動すると、この時点では dataSource に application.properties の設定が反映されておらず null と出力されることが確認できます。

f:id:ksby:20150508234805p:plain

次に、変更した内容を一旦元に戻してから src/main/java/ksbysample/webapp/email/web/mailsend の下の MailsendController.java を以下のように変更した後、

    @Autowired
    private DataSource dataSource;

    @RequestMapping
    public String index(MailsendForm mailsendForm
            , Model model) {
        org.apache.tomcat.jdbc.pool.DataSource dataSource
                = (org.apache.tomcat.jdbc.pool.DataSource)this.dataSource;
        System.out.println("★★★ " + dataSource.getDriverClassName());
        System.out.println("★★★ " + dataSource.getUrl());

        return "mailsend/mailsend";
    }

Tomcat を再起動してブラウザで http://localhost:8080/mailsend へアクセスすると dataSource に application.properties の設定が反映されていることが確認できます。

f:id:ksby:20150508235646p:plain

変更した内容を元に戻しておきます。

調査その1 ( DataSource Bean の自動コンフィギュレーションを行うクラスがあるのか? )

まずは Spring Boot の GitHub で DataSource Bean の自動コンフィギュレーションを行うクラスがあるか検索してみます。https://github.com/spring-projects/spring-boot にアクセスした後、"dataSource autoconfigure" で検索してヒットしたソース一覧を眺めていると DataSourceAutoConfiguration.java といういかにもそれらしいクラスがありました。

f:id:ksby:20150509014947p:plain

DataSourceAutoConfiguration.java をひと通り見てみた感想としては、

  • SpringBootCondition を継承したインナークラスが EmbeddedDataSourceCondition, NonEmbeddedDataSourceCondition, DataSourceAvailableCondition の3つ存在します。
  • SpringBootCondition を継承したクラスの getMatchOutcome メソッドreturn ConditionOutcome.match(...); を返すと、別のクラスで @Conditional アノテーションを付加しておくことでクラスを使用するか否かの判断条件として使用できるようです。例えば DataSourceAvailableCondition インナークラスの getMatchOutcome メソッドreturn ConditionOutcome.match(...); を返す場合には、@Conditional(DataSourceAutoConfiguration.DataSourceAvailableCondition.class) が付加された JdbcTemplateConfiguration クラスが使用可能になる、という感じです。
  • EmbeddedDataSourceCondition はクラスローダーに H2 Database, Java DB(Derby), HSQLDB のいずれかのドライバがあると return ConditionOutcome.match(...); を返します。
  • NonEmbeddedDataSourceCondition はクラスローダーに org.apache.tomcat.jdbc.pool.DataSource, com.zaxxer.hikari.HikariDataSource, org.apache.commons.dbcp.BasicDataSource, org.apache.commons.dbcp2.BasicDataSource のいずれかが存在すると return ConditionOutcome.match(...); を返します。DataSourceBuilder.java を参照しました。
  • NonEmbeddedDataSourceCondition が有効で、DataSource か XADataSource を返す Bean が存在しない場合、NonEmbeddedConfiguration クラスで DataSource Bean を生成します。この DataSource Bean の実装を見た感じだと、アプリケーションで DataSource Bean を定義する必要はない気がしますね。。。 あとで試してみましょう。
  • DataSourceAvailableCondition は DataSource か XADataSource を返す Bean が存在し、かつ EmbeddedDataSourceCondition か NonEmbeddedDataSourceCondition のいずれかが有効な場合には return ConditionOutcome.match(...); を返します。DataSource を返す Bean はアプリケーションで定義していなくても NonEmbeddedConfiguration クラスで DataSource Bean が自動生成される場合には return ConditionOutcome.match(...); を返していました。ApplicationConfig クラスの DataSource Bean をコメントアウトして確認しました。
  • DataSourceAutoConfiguration.java の中は @Conditional アノテーションや @ConditionalOnMissingBean アノテーションでどのインナークラスを使用するのかを判断しているんですね。まさかこんなにアノテーションでいろいろ条件判断させているとは驚きでした。
  • あとは DataSourceInitializer Bean が定義されていなければ、このクラスの中で DataSourceInitializer Bean を生成していました。

bootRun タスクを Debug 実行して NonEmbeddedDataSourceCondition, EmbeddedDataSourceCondition, DataSourceAvailableCondition がそれぞれ何を返しているか確認してみます。

それぞれのインナークラスで return ConditionOutcome.match(...);return ConditionOutcome.noMatch(...); を返すところにブレークポイントを指定した後、Debug 実行します。

f:id:ksby:20150509022002p:plain

  • NonEmbeddedDataSourceCondition は return ConditionOutcome.match(...); を返していました。
  • EmbeddedDataSourceCondition は return ConditionOutcome.noMatch(...); を返していました。
  • DataSourceAvailableCondition は return ConditionOutcome.match(...); を返していました。

この結果から使用可能と判断される DataSourceAutoConfiguration.java 内のインナークラスは JdbcTemplateConfiguration だけでした。JdbcTemplateConfiguration インナークラスは JdbcTemplate Bean、NamedParameterJdbcTemplate Bean を定義するだけで、DataSource Bean に何か処理はしていませんでした。

DataSourceAutoConfiguration.java では DataSource Bean に application.properties の設定は反映していませんでした。

調査その2 ( DataSource Bean にブレークポイントをセットして処理を追ってみる )

DataSource Bean にブレークポイントをセットして Debug モードで Tomcat を起動した後、デバッガから Step 実行してみます。

f:id:ksby:20150509204451p:plain

調査した結果、以下のことが分かりました。

  • Bean の生成は AbstractAutowireCapableBeanFactory クラスの createBean メソッドから行われています。
  • createBean メソッドから同じクラス内の doCreateBean メソッドが呼び出されます。
  • doCreateBean メソッドから同じクラス内の initializeBean メソッドが呼び出されます。
  • initializeBean メソッドの中で、wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName); が呼び出された時に設定が反映されていました。
  • applyBeanPostProcessorsBeforeInitialization メソッドの中で for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) { ... } という処理が書かれており、このループの中で ConfigurationPropertiesBindingProcessor クラスの postProcessBeforeInitialization メソッドが呼び出されて設定が反映されていました。

調査結果

@Bean に @ConfigurationProperties も合わせて付加しておくと ConfigurationPropertiesBindingProcessor クラスの postProcessBeforeInitialization メソッドが呼び出されて設定が反映される、という仕組みでした。