Spring in Acton 4 读书笔记之根据开发环境装配 bean

字数1,481 大约花费7分钟

目录

  1. 1. 编译时定义 bean
  2. 2. 运行时生成 bean
  3. 3. 在环境中设置当前 Profile
  4. 4. 测试 Profile 以指定环境

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 {

/**
* The set of profiles for which the annotated component should be registered.
*/

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

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() {
//JNDI 中没有配置数据库,所以 dataSource 应该为 null
assertNull(dataSource);
}
}

}

可以看到,开发环境的 dataSource 生成成功,并且能够正确的查询数据。而生产环境由于没有在 JNDI 中配置数据库,dataSource 为空。

谈谈 IT的文章均为原创或翻译(翻译会注明外文来源),转载请以链接形式标明本文地址: http://tantanit.com/springinacton4-du-shu-bi-ji-zhi-gen-ju-kai-fa-huan-jing-zhuang-pei-bean/

谈谈IT

欢迎关注官方微信公众号获取最新原创文章