第三章 高级装配
完整代码请见:https://github.com/codercuixin/SpringInAction
第一部分 @Profile注解的使用
环境与profile 是否启用某个bean,常用于数据库bean
通过profile启用不同的bean,特别是对于各种不同的数据库(开发线,测试线,正式线),尤其管用。
1.1第一步 配置profile bean。通过@Profile修饰类或者方法名,来表明这个Bean是可以动态启动与否的
package com.myapp;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.jndi.JndiObjectFactoryBean;
import javax.sql.DataSource;
@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();
}
}
1.2.第二步,**profile。
通过@ActiveProfile来**指定的Profile,启用指定的数据库Bean。
package profiles;
import static org.junit.Assert.*;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import javax.sql.DataSource;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.myapp.DataSourceConfig;
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() {
// should be null, because there isn't a datasource configured in JNDI
assertNull(dataSource);
}
}
}
1.3.通过两个参数**profile
spring.profiles.active和spring.profiles.default,优先使用前者的配置。
设置这两个参数的方式有如下几种:(待补充完整)
- 作为DIspatcherServlet的初始化参数
- 作为Web应用的上下文参数
- 作为JNDI条目
- 作为环境变量
- 作为JVM的系统属性
- 在集成测试类上,使用@ActiveProfile注解设置。(也就是上面第二步演示的)
第二部分 条件化的bean
通过@Conditional, 可以用到Bean上,当条件为true,则创建该Bean;否则则不创建。
主要分为一下三步
1.像往常一样定义Bean的POJO类
2.编写org.springframework.context.annotation.Condition接口的类MagicExistsCondition,用来创建是否创建该Bean
3.将@Conditional(MagicExistsCondition.class)应用到Bean的JavaConfig上。
package conditional.habuma.restfun;
public class MagicBean {
}
package conditional.habuma.restfun;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;
/**
* * @Author: cuixin
* * @Date: 2018/8/30 18:32
*/
public class MagicExistsCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Environment environment = context.getEnvironment();
return environment.containsProperty("magic");
}
}
package conditional.habuma.restfun;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
/**
* * @Author: cuixin
* * @Date: 2018/8/30 18:32
*/
@Configuration
public class MagicConfig {
@Bean
@Conditional(MagicExistsCondition.class)
public MagicBean magicBean(){
return new MagicBean();
}
}
由于实现了match方法中带有两个参数,我们可以通过这两个参数做到什么呢?
ConditionContext接口的方法
public interface ConditionContext {
/**
* 返回BeanDefinitionRegistry,可用来判断Bean是否定义
*/
BeanDefinitionRegistry getRegistry();
/**
* 返回ConfigurableListableBeanFactory,可用来检查Bean是否存在,甚至探查Bean的属性
*/
@Nullable
ConfigurableListableBeanFactory getBeanFactory();
/**
* 返回Environment,可用来判断环境变量是否存在,且获取环境变量的值
*/
Environment getEnvironment();
/**
*返回ResourceLoader,可用来读取或探查已经加载的资源
*/
ResourceLoader getResourceLoader();
/**
* 返回ClassLoader,可用来加载类或判断类是否存在
*/
@Nullable
ClassLoader getClassLoader();
}
AnnotatedTypeMetadata 用来获取注解相关信息
public interface AnnotatedTypeMetadata {
boolean isAnnotated(String annotationName);
@Nullable
Map<String, Object> getAnnotationAttributes(String annotationName);
@Nullable
Map<String, Object> getAnnotationAttributes(String annotationName, boolean classValuesAsString);
@Nullable
MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationName);
@Nullable
MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationName, boolean classValuesAsString);
第三部分 处理自动装配的歧义性。
aaa@qq.com 注解只可以装配只有一个实现类的Bean
例如下面的Dessert有三个实现类,自动装配时,Spring就会不知道选哪一个,因而会报NoUniqueBeanDefinitionException错误。
public interface Dessert {
}
@Component
public class Cake implements Dessert {
}
@Component
public class Cookies implements Dessert {
}
@Component
public class IceCream implements Dessert {
}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = CakeConfig.class)
public class CakeTest {
@Autowired
private Dessert dessert;//Spring: emmm.... I don't which one to choose
@Test
public void getDessert(){
assertNotNull(dessert);
}
}
3.2 @Primary 可以指定某个实现类作为优先Bean创建
给蛋糕加个@Primary,表明首选蛋糕作为首选项。然后在执行Test,发现就不抱错了。
@Primary可以配合@Component, @Bean, @Autowired使用。
@Component
@Primary
public class Cake implements Dessert {
}
3.3 @Qualifie将使用的Bean限定到具体的实现类
由于@Qualifier是基于字符串去匹配Bean id的,所以你修改了类名就可能导致找不到对应的Bean了。但我尝试了一下,如果使用IDEA的Refactor->Rename,会帮我们自动更改多处的。
@Qualifie可以配合@Component, @Bean, @Autowired使用。
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = CakeConfig.class)
public class CakeTest {
@Autowired
@Qualifier("cookies")
private Dessert dessert;
@Test
public void getDessert(){
assertNotNull(dessert);
}
}
Spring实战中,为了解决@Qualifier“不够用”,拼命地建立自定义注解,我感觉是没有必要,有点画蛇添足的感觉。
四. bean的作用域
四种不同的作用域
单例(Singleton默认):在整个应用中,只创建bean的一个实例。
比如
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public Notepad notepad() {
return new Notepad();
}
原型(Prototype):每次注入或者通过spring应用上下文获取的时候,都会创建一个新的bean实例。
比如:
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Notepad notepad() {
return new Notepad();
}
会话(Session):在web应用中,为每个会话创建一个bean实例, 举个例子:
@Bean
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public Notepad notepad() {
return new Notepad();
}
请求(Request):在web应用中,为每个请求创建一个bean实例,举个栗子:
@Bean
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public Notepad notepad() {
return new Notepad();
}
这里需要注意的是proxyMode这个属性。(TODO:通过例子加深理解)
当值为ScopedProxyMode.TARGET_CLASS时,表示的该bean类型是具体类,只能使用CGLib来生成基于类的代理。
当值为ScopedProxyMode.INTERFACES时。
五.运行时注入
1.使用@PropertySource, @Environment注入外部值
app.properties的内容
disc.title=Sgt. Peppers Lonely Hearts Club Band
disc.artist=The Beatles
public class BlankDisc {
private final String title;
private final String artist;
public BlankDisc(String title, String artist){
this.title = title;
this.artist = artist;
}
public String getArtist() {
return artist;
}
public String getTitle() {
return title;
}
}
@Configuration
@PropertySource("classpath:/externals/com/soundsystem/app.properties")
public class EnvironmentConfig {
@Autowired
private Environment env;
@Bean
public BlankDisc blankDisc(){
return new BlankDisc(env.getProperty("disc.title"), env.getProperty("disc.artist"));
}
}
另外Environment的getProperty有4个重载方式可以选择
String getProperty(String key); //获取指定key的内容;如果找不到key就返回null
String getProperty(String key, String defaultValue);//获取指定key的内容;如果找不到key,就返回默认值
<T> T getProperty(String key, Class<T> targetType);//targetType用于说明该key的值类型
<T> T getProperty(String key, Class<T> targetType, T defaultValue);
2.属性占位符
${…}表示属性占位符,常配合@Value使用,举个栗子。
@Bean
public BlankDisc blankDisc2(@Value("${disc.title}") String title, @Value("${disc.artist}")String artist){
return new BlankDisc(title, artist);
}
3.使用Spring表达式语言进行装配
1.使用bean的id来引用Bean TODO 待补充实例
2.调用方法和访问对象的属性
3.对峙进行算数,关系和逻辑运算
4.正则表达式匹配
5.集合操作