Bean的高级装配
环境与profile
项目开发需要多个环境,有开发环境,测试环境,生产环境等,在每一种环境中的配置可能不一样,比如:数据库的配置,开发环境需要配置开发的数据库dev,测试的时候需要使用测试环境中的数据库,而产品部分发布到现场那么就需要生产环境的正式数据库,所以我们可以创建多个配置类,或者配置文件,根据不同的环境使用对应的配置文件,那么怎么配置比较方便呢?
现在就要使用@Profile注解,此注解在spring3.2之后既可以用在类上,也可以用在方法上,如下:
@Configuration
public class DataSourceConfig {
@Bean
@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();
}
}
既然我们配置了两个profile,那么具体使用哪一个怎么去激活呢?
Spring在确定哪个profile处于激活状态时, 需要依赖两个独立的属性: spring.profiles.active和spring.profiles.default。 如果设置了spring.profiles.active属性的话, 那么它的值就会用来确定哪个profile是激活的。 但如果没有设置spring.profiles.active属性的话, 那Spring将会查找spring.profiles.default的值。如果spring.profiles.active和spring.profiles.default均没有设置的话, 那就没有激活的profile, 因此只会创建那些没有定义在profile中的bean。有多种方式来设置这两个属性:
- 作为DispatcherServlet的初始化参数;
- 作为Web应用的上下文参数;
- 作为JNDI条目;
- 作为环境变量;
- 作为JVM的系统属性;
- 在集成测试类上, 使用@ActiveProfiles注解设置。
你尽可以选择spring.profiles.active和spring.profiles.default的最佳组合方式以满足需求 。比如在web应用中设置spring.profile.default的web.xml文件如下:
条件化的Bean
Spring4引入新的 @Conditional 注解,它可以用到带有@Bean注解的方法上。如:
@Configuration
public class ConditionConfig {
@Bean
@Conditional(ConditionImpl.class)
public Computer getBean(){
return new Computer();
}
}
其中ConditionImpl是接口Condition的实现类,接口Condition是Spring的接口,此接口源码如下:
@FunctionalInterface
public interface Condition {
/**
* Determine if the condition matches.
* @param context the condition context
* @param metadata metadata of the {@link org.springframework.core.type.AnnotationMetadata class}
* or {@link org.springframework.core.type.MethodMetadata method} being checked
* @return {@code true} if the condition matches and the component can be registered,
* or {@code false} to veto the annotated component's registration
*/
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
只有一个方法matches,所以实现类的此方法返回为true时创建Computer的bean,若返回为false,则不创建bean。
我们发现matches方法有两个参数:ConditionContext context, AnnotatedTypeMetadata metadata
ConditionContext是个接口,有以下方法以及它们的作用:
- 借助getRegistry()返回的BeanDefinitionRegistry检查bean定义;
- 借助getBeanFactory()返回的ConfigurableListableBeanFactory检查bean是否存在,甚至探查bean的属性;
- 借助getEnvironment()返回的Environment检查环境变量是否存在以及它的值是什么;
- 读取并探查getResourceLoader()返回的ResourceLoader所加载的资源;
- 借助getClassLoader()返回的ClassLoader加载并检查类是否存在。
AnnotatedTypeMetadata也是个接口:
public interface AnnotatedTypeMetadata {
boolean isAnnotated(String var1);
@Nullable
Map<String, Object> getAnnotationAttributes(String var1);
@Nullable
Map<String, Object> getAnnotationAttributes(String var1, boolean var2);
@Nullable
MultiValueMap<String, Object> getAllAnnotationAttributes(String var1);
@Nullable
MultiValueMap<String, Object> getAllAnnotationAttributes(String var1, boolean var2);
}
借助isAnnotated()方法, 我们能够判断带有@Bean注解的方法是不是还有其他特定的注解。 借助其他的那些方法, 我们能够检查@Bean注解的方法上其他注解的属性。
比如@Profile注解就是基于@Conditional和Condition实现的,其中ProfileCondition实现了Condition接口:
class ProfileCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
//获取profile的所有属性
MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
if (attrs != null) {
//取profile的value属性
for (Object value : attrs.get("value")) {
//判断profile是否激活
if (context.getEnvironment().acceptsProfiles(Profiles.of((String[]) value))) {
return true;
}
}
return false;
}
return true;
}
}
Bean的作用域
前边讲过了bean的作用域,以下是bean的几种作用域:
作用域 | 描述 |
---|---|
单例singleton | 在spring IoC容器仅存在一个Bean实例,Bean以单例方式存在,默认值 |
原型prototype | 每次从容器中调用Bean时,都返回一个新的实例,即每次调用getBean()时,相当于执行newXxxBean() |
请求request | 每次HTTP请求都会创建一个新的Bean,该作用域仅适用于WebApplicationContext环境 |
会话session | 同一个HTTP Session共享一个Bean,不同Session使用不同的Bean,仅适用于WebApplicationContext环境 |
全局global-session | 一般用于Portlet应用环境,该运用域仅适用于WebApplicationContext环境 |
Java代码配置bean的时候可以使用@Scope注解设置bean的作用域,如:
@Configuration
public class JavaConfig {
@Bean(name = "iphonePc")
@Scope("session")
public IphoneComputer iphoneComputer(){
return new IphoneComputer();
}
}
当然在XML装配中<bean>标签中也有属性scope用于设置bean的作用域。
运行时值注入
我们项目在运行时可能需要读取一些配置文件中的值,避免硬编码。Spring提供了两种在运行时求值的方式:
- 属性占位符(Property placeholder)
- Spring表达式语言(SpEL)
我们只讲属性占位符,这也是总用到的。
@Properties注解可以读取配置文件,属性value为数组,所以设置多个配置文件的路径,如下创建PropertyBean的时候通过读取配置文件app.properties的配置项给属性赋值。
@Configuration
@PropertySource("classpath:/app.properties")
public class PropertyConfig {
@Autowired
private Environment environment;
@Bean
public PropertyBean getBean(){
return new PropertyBean(
environment.getProperty("appname"),
environment.getProperty("port")
);
}
}
在这里使用了Environment类,该类继承PropertyResolver,其中一些重要方法需要学习:
- String getProperty(String key)
- String getProperty(String key, String defaultValue)
- T getProperty(String key)
- T getProperty(String key, String defaultValue)
这个四个方法是获取属性值得,即配置项。
还有几个方法:
- boolean containsProperty(String key):判断是否包含某个配置项
- String[] getActiveProfiles():返回激活profile名词的数组
- String[] getDefaultProfiles():返回默认profile名词的数组
- boolean acceptsProfiles(Profiles profiles):判断profile是否激活,在上面讲到@Profile的ProfileCondition实现了接口Condition并且通过ConditionContext获取到Environment,再判断profile是否激活
注解@Value属性占位符使我们常用到读取配置文件的方法,如:
@Value("${appname}")
String appname;
@Value("${port}")
String port;
@Bean
public PropertyBean getBeanByValue(){
return new PropertyBean(
environment.getProperty(appname),
environment.getProperty(port)
);
}