Spring in Action(Spring实战)的第三章第一节(3.1 Environments and profiles)讲述了根据开发环境装配bean,本文是阅读这一节的心得笔记。
开发中遇到的最大挑战之一,是环境的变化。数据库配置、加密算法以及与外部系统的集成等都和开发环境相关。
编译时定义bean
以数据库为例,在开发的时候,会倾向于使用测试数据,比如在Spring的配置类里,可能会像下面这样配置EmbeddedDatabaseBuilder(),以创建一个DataSource的bean:
1 2 3 4 5 6 7
| @Bean(destroyMethod="shutdown") public DataSource dataSource() { return new EmbeddedDatabaseBuilder() .addScript("classpath:schema.sql") .addScript("classpath:test-data.sql") .build(); }
|
这样,在开发环境中,可以根据需要,在schema.sql和test-data.sql中增加模拟数据。生产环境不能这么做,而更倾向于使用JNDI,创建一个DataSource的bean:
1 2 3 4 5 6 7 8 9
| @Bean public DataSource dataSource() { JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean(); jndiObjectFactoryBean.setJndiName("jdbc/myDS"); jndiObjectFactoryBean.setResourceRef(true); jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class); return (DataSource) jndiObjectFactoryBean.getObject(); }
|
使用JNDI,容器可以决定怎样创建DataSource,包括从连接池中交接DataSource。当然,这些对生产环境很有用,但对开发情况下的测试以及简单的集成测试来说,一般并不需要。
而QA环境可能想用另外的配置:
1 2 3 4 5 6 7 8 9 10 11
| @Bean(destroyMethod="close") public DataSource dataSource() { BasicDataSource dataSource = new BasicDataSource(); dataSource.setUrl("jdbc:h2:tcp://dbserver/~/test"); dataSource.setDriverClassName("org.h2.Driver"); dataSource.setUsername("sa"); dataSource.setPassword("password"); dataSource.setInitialSize(20); dataSource.setMaxActive(30); return dataSource; }
|
可以使用例如Maven profiles的方式,在不同环境进行重新编译(build)来解决和环境相关的问题,但是,重新build可能引入新的bug,而这是灾难性的。
运行时生成bean
Spring的方案和上述方案差不多,只是改成了运行时配置。生产环境和开发环境使用同一套代码,避免在发布时,因为更改java代码,重新编译代码引起问题。可以在方法或类上加Profile标签,这样,只有环境满足条件时,才会生成相应的bean,而没有加这个标签的bean是在任何条件下都会生成的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @Configuration public class DataSourceConfig {
@Bean(destroyMethod = "shutdown") @Profile("dev") public DataSource embeddedDataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.H2) .addScript("classpath:schema.sql") .addScript("classpath:test-data.sql") .build(); }
@Bean @Profile("prod") public DataSource jndiDataSource() { JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean(); jndiObjectFactoryBean.setJndiName("jdbc/myDS"); jndiObjectFactoryBean.setResourceRef(true); jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class); return (DataSource) jndiObjectFactoryBean.getObject(); }
}
|
也可以使用XML做到这一点,本笔记略。
Profile 标签的定义如下:
1 2 3 4 5 6 7 8 9 10 11 12
| @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) @Documented @Conditional(ProfileCondition.class) public @interface Profile {
String[] value();
}
|
可以看出,Profile标签是运行时生效,并且支持标注在类和方法上,同时,被@Conditional标注,表明只有满足ProfileCondition条件时,才生效。ProfileCondition定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class ProfileCondition implements Condition {
@Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { if (context.getEnvironment() != null) { MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName()); if (attrs != null) { for (Object value : attrs.get("value")) { if (context.getEnvironment().acceptsProfiles(((String[]) value))) { return true; } } return false; } } return true; }
}
|
可以看出,判断是否匹配的逻辑是,从@Profile标签中获取可接受的Profile值列表(可以有多个值),再从上下文中获取环境,判断环境中当前是否包含这些Profile值,以判断当前环境是否满足条件。那么,如何在环境中设置当前Profile值呢?
在环境中设置当前Profile
Spring使用spring.profiles.active和spring.profiles.default属性来确定当前Profile。spring.profiles.active不为空时,spring.profiles.active的值是当前Profile,spring.profiles.active的值为空时,spring.profiles.default的值是当前Profile,两个都为空时,则所有加了Profile标签的bean都不生成,没有加Profile标签的bean才会生成。可以使用以下方式设置这两个属性:
- 作为DispatcherServlet的参数
- 作为web应用上下文的参数
- 作为JNDI的entry
- 作为环境变量
- 作为JVM系统属性
- 在一个集成测试类上使用@ActiveProfiles标签
你可以自己选择最合适的方式。
测试Profile以指定环境
当代码里使用了Profile来指定环境相关的操作后,有时候需要在测试环境中模拟与生产环境相同的Profile,来进行测试。Spring提供了@ActiveProfiles标签,指定测试程序运行时的当前Profile。下面,在添加开发环境配置初始化sql文件schema.sql和test-data.sql后,以及上文中的DataSourceConfig的配置类后,进行数据库测试(这些代码都可以在官方样例下载)。
schema.sql如下:
1 2 3 4
| create table Things ( id identity, name varchar(100) );
|
test-data.sql如下:
1
| insert into Things (name) values ('A')
|
测试代码如下:
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
| public class DataSourceConfigTest { @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes=DataSourceConfig.class) @ActiveProfiles("dev") public static class DevDataSourceTest { @Autowired private DataSource dataSource;
@Test public void shouldBeEmbeddedDatasource() { assertNotNull(dataSource); JdbcTemplate jdbc = new JdbcTemplate(dataSource); List<String> results = jdbc.query("select id, name from Things", new RowMapper<String>() { @Override public String mapRow(ResultSet rs, int rowNum) throws SQLException { return rs.getLong("id") + ":" + rs.getString("name"); } });
assertEquals(1, results.size()); assertEquals("1:A", results.get(0)); } }
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes=DataSourceConfig.class) @ActiveProfiles("prod") public static class ProductionDataSourceTest { @Autowired private DataSource dataSource;
@Test public void shouldBeEmbeddedDatasource() { assertNull(dataSource); } }
}
|
可以看到,开发环境的dataSource生成成功,并且能够正确的查询数据。而生产环境由于没有在JNDI中配置数据库,dataSource为空。