Spring 3.1 新機能 - Bean Definition Profiles と Environment XML版

3.1からProfileという仕組みを使ってBeanの定義を簡単に切り替えられました。RailsとかGrailsだとかSeasar2とかにも似たのがありますね。それです。

やり方はXMLで定義する方法と、Javaのクラスに対してアノテーションを設定する方法がありますが、まずはXMLの方から見ていきます。

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
                      http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd
                      http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.1.xsd
                      http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.1.xsd">

  <context:property-placeholder location="classpath*:META-INF/spring/*.properties" />

  <context:component-scan base-package="org.yamkazu.spring31" />

  <bean class="org.springframework.orm.jpa.JpaTransactionManager" id="transactionManager">
    <property name="entityManagerFactory" ref="entityManagerFactory" />
  </bean>

  <tx:annotation-driven transaction-manager="transactionManager" />

  <bean class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean" id="entityManagerFactory">
    <property name="persistenceUnitName" value="persistenceUnit" />
    <property name="dataSource" ref="dataSource" />
  </bean>

  <beans profile="production">
    <bean class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close" id="dataSource">
      <property name="driverClassName" value="${database.driverClassName}" />
      <property name="url" value="${database.url}" />
      <property name="username" value="${database.username}" />
      <property name="password" value="${database.password}" />
      <property name="testOnBorrow" value="true" />
      <property name="testOnReturn" value="true" />
      <property name="testWhileIdle" value="true" />
      <property name="timeBetweenEvictionRunsMillis" value="1800000" />
      <property name="numTestsPerEvictionRun" value="3" />
      <property name="minEvictableIdleTimeMillis" value="1800000" />
    </bean>
  </beans>

  <beans profile="dev">
    <jdbc:embedded-database id="dataSource" type="H2" />
  </beans>

</beans>

profileはbeansの属性として定義します。ここではproductionとdevという二つのProfileを定義して実行時のProfile情報を元にdataSourceが切り替わるようにしています。beansは上の様にbeansの中に入れ子にしてもいいですし(ネストで書けるようになったのは3.1からです)、従来通りファイル自体に切り離して、rootエレメントとして定義しても問題ありません。

あとは実行時にこのProfileを指定して実行するだけです。この実行時のProfileを指定するのがEnvironmentというしくみです。テストから見てみます。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:/META-INF/spring/*.xml")
@ActiveProfiles({ "dev" })
public class AccountRepositoryTest {
    @Inject
    AccountRepository accountRepository;
    @Test
    public void 一件登録して取得する() {
        Account account = new Account();
        account.setName("aaa");
        accountRepository.persist(account);
        Account find = accountRepository.find(account.getId());
        assertThat(find.getName(), is(equalTo("aaa")));
    }
}

従来のSpringのJUnitでテストをする設定に加えて@ActiveProfilesというアノテーションを指定してProfile情報を定義します。分かりやすいようにわざと配列形式で指定しましたが、有効にしたいProfileは複数定義することが可能です。実はProfileの定義も複数指定できます。例えば

  <beans profile="dev1,dev2">
    <jdbc:embedded-database id="dataSource" type="H2" />
  </beans>

のような形です。

次はプログラム内でEnvironmentを指定する方法を見ていきます。

public class AccountRepositoryXmlTest {

    @Test
    public void 一件登録して取得する() {
        GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
        ctx.getEnvironment().setActiveProfiles("dev");
        ctx.load("classpath:/META-INF/spring/*.xml");
        ctx.refresh();

        AccountRepository accountRepository = ctx.getBean(AccountRepository.class);

        Account account = new Account();
        account.setName("aaa");
        accountRepository.persist(account);
        Account find = accountRepository.find(account.getId());
        assertThat(find.getName(), is(equalTo("aaa")));
    }

}

contextのgetEnvironment().setActiveProfiles("xx")を呼び出して設定します。

実行時のシステムプロパティーに設定する方法もあります。

public class AccountRepositoryXmlTest {

    @Test
    public void 一件登録して取得する() {
        GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
        ctx.load("classpath:/META-INF/spring/*.xml");
        ctx.refresh();

        AccountRepository accountRepository = ctx.getBean(AccountRepository.class);

        Account account = new Account();
        account.setName("aaa");
        accountRepository.persist(account);
        Account find = accountRepository.find(account.getId());
        assertThat(find.getName(), is(equalTo("aaa")));
    }

}

上記のコードを

-Dspring.profiles.active="dev"

というspring.profiles.activeに有効にしたいprofileを指定することが出来ます。

ちなみにWebアプリケーションの場合はweb.xmlからEnvironmentを指定できます。

<servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>spring.profiles.active</param-name>
        <param-value>production</param-value>
    </init-param>
</servlet>

Spring 3.1 新機能 - AnnotationConfigContextLoader

3.0 から Javaconfig という XML でなく Java のコードで Spring の設定が記述できる機能が追加されていましたが、この機能が Test でも使えるようになりました。

従来 3.0 のテストケースは @ContextConfiguration を使用してこんな定義をしていました。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/META-INF/spring/*.xml")
public class AccountRepositoryTest {

3.1 からは AnnotationConfigContextLoader を指定すると Javaconfig をロード可能なります。面白いのは内部クラスで @Configuration が付いているものを自動でロードしてくれることです。

こんな使い方が出来ます。

package org.yamkazu.spring31;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;

import javax.inject.Inject;
import javax.sql.DataSource;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.AnnotationConfigContextLoader;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader = AnnotationConfigContextLoader.class)
public class AccountRepositoryWithJavaConfigTest {

    @Configuration
    @EnableTransactionManagement
    @ComponentScan(basePackages = "org.yamkazu.spring31", excludeFilters = { @Filter(type = FilterType.ASSIGNABLE_TYPE, value = TestConfig.class) })
    static class TestConfig {

        @Bean
        public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
            LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean();
            emf.setDataSource(dataSource());
            return emf;
        }

        @Bean
        public DataSource dataSource() {
            // @formatter:off
            return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2)
                                                .addScript("classpath:/schema.sql")
                                                .addScript("classpath:/test-data.sql")
                                                .build();
            // @formatter:on
        }

        @Bean
        public JpaTransactionManager transactionManager() {
            return new JpaTransactionManager();
        }

    }

    @Inject
    AccountRepository repository;

    @Test
    public void test() throws Exception {
        Account account = new Account();
        account.setName("aaa");
        repository.persist(account);

        Account find = repository.find(account.getId());
        assertThat(find.getName(), is(equalTo("aaa")));
    }

}

テスト対象の周辺クラスを、モックに差し替えるといったテストのコンテキストで色々小回りが効きそうな印象です。

ちょっとわからなかったのが、@ComponentScanを使用する場合、かつ自身のテストクラスのパッケージが含まれている場合(テストクラスはテスト対象と同じパッケージにするのでかぶること多いじゃないかなぁ)、@ComponentScan と AnnotationConfigContextLoader が 同じBeanを登録しようとしてコンフリクトしてしまうことです。excludeFiltersを設定することで回避できましたが、これが解なのかは不明。

任意の@Configurationのクラスしてする場合は@ContextConfigurationのclassesに指定すれば良いです。

参考: http://blog.springsource.com/2011/06/21/spring-3-1-m2-testing-with-configuration-classes-and-profiles/