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

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

}

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

© 2022 谈谈IT All Rights Reserved. 本站访客数人次 本站总访问量
Theme by hiero