高级装配

环境与profile

迁移环境可能的问题

1、数据库配置,在开发环境中, 我们可能会使用嵌入式数据库, 并预先加载测试数据。 例如, 在Spring配置类中, 我们可能会在一个带有@Bean注解的方法上使
EmbeddedDatabaseBuilder

  @Bean(destroyMethod = "shutdown")
  public DataSource embeddedDataSource() {
    return new EmbeddedDatabaseBuilder()
        .addScript("classpath:schema.sql")
        .addScript("classpath:test-data.sql")
        .build();
  }
这会创建一个类型为 javax.sql.DataSource bean
使用 EmbeddedDatabaseBuilder 会搭建一个嵌入式的 Hypersonic 数据库, 它的模式( schema ) 定义在 schema.sql 中, 测试数据则是通过 test-data.sql 加载的。
create table Things (
  id identity,
  name varchar(100)
);
insert into Things (name) values ('A')
在开发环境中运行集成测试或者启动应用进行手动测试的时候,这个 DataSource 是很有用的。 每次启动它的时候, 都能让数据库处于一个给定的状态。尽管 EmbeddedDatabaseBuilder 创建的 DataSource 非常适于开发环境, 但是对于生产环境来说, 这会是一个糟糕的选择。 在生产环境的配置中, 你可能会希望使用 JNDI 从容器中获取一个 DataSource 。 在这样场景中, 如下的 @Bean 方法会更加合适
  @Bean
  public DataSource jndiDataSource() {
    JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
    jndiObjectFactoryBean.setJndiName("jdbc/myDS");
    jndiObjectFactoryBean.setResourceRef(true);
    jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
    return (DataSource) jndiObjectFactoryBean.getObject();
  }
通过 JNDI 获取 DataSource 能够让容器决定该如何创建这个 DataSource , 甚至包括切换为容器管理的连接池。 即便如此, JNDI 管理的 DataSource 更加适合于生产环境, 对于简单的集成和开发测试环境来说, 这会带来不必要的复杂性。

配置profile bean

Spring为环境相关的bean所提供的解决方案其实与构建时的方案没有太大的差别。 在这个过程中需要根据环境决定该创建哪个bean和不创建哪个bean。 不过Spring并不是在构建的时候做出这样的决策, 而是等到运行时再来确定。 这样的结果就是同一个部署单元(可能会是WAR文件) 能够适用于所有的环境, 没有必要进行重新构建。在3.1版本中,Spring引入了bean profile的功能。 要使用profile, 首先要将所有不同的bean定义整理到一个或多个profile之中, 在将应用部署到每个环境时, 要确保对应的profile处于激活(active) 的状态。

Java配置中, 可以使用@Profile注解指定某个bean属于哪一个profile。 

例如:

嵌入式数据库

@Configuration
@Profile("dev")
public class DataSourceConfig { 
  @Bean(destroyMethod = "shutdown")
  public DataSource embeddedDataSource() {
    return new EmbeddedDatabaseBuilder()
        .setType(EmbeddedDatabaseType.H2)
        .addScript("classpath:schema.sql")
        .addScript("classpath:test-data.sql")
        .build();
  }
}
@Profile 注解应用在了类级别上。 它会告诉 Spring 这个配置类中的 bean 只有在 dev profile 激活时才会创建。 如果 dev profile 没有激活的话, 那么带有 @Bean 注解的方法都会被忽略掉。
适用于生产环境的配置
@Configuration
@Profile("prod")
public class DataSourceConfig { 
  @Bean
  public DataSource jndiDataSource() {
    JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
    jndiObjectFactoryBean.setJndiName("jdbc/myDS");
    jndiObjectFactoryBean.setResourceRef(true);
    jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
    return (DataSource) jndiObjectFactoryBean.getObject();
  }
}
只有 prod profile 激活的时候, 才会创建对应的 bean
以上为3.1中的, 只能在类级别上使用 @Profile 注解 , 3.2 开始, 可以在方法级别上使用 @Profile 注解,与 @Bean 注解一同使用。 这样的话, 就能将这两个 bean 的声明放到同
一个配置类之中

@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();
  }
}
没有指定 profile bean 始终都会被创建, 与激活哪个 profile 没有关系。
在XML中配置profile
可以通过 <beans> 元素的 profile 属性, 在 XML 中配置 profile bean

可以如此,但会创建多个xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
  xmlns:jee="http://www.springframework.org/schema/jee" xmlns:p="http://www.springframework.org/schema/p"
  xsi:schemaLocation="
    http://www.springframework.org/schema/jee
    http://www.springframework.org/schema/jee/spring-jee.xsd
    http://www.springframework.org/schema/jdbc
    http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
    http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd"
    profile="dev">
    <jdbc:embedded-database id="dataSource" type="H2">
      <jdbc:script location="classpath:schema.sql" />
      <jdbc:script location="classpath:test-data.sql" />
    </jdbc:embedded-database>
</beans>
如下可以合并

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
  xmlns:jee="http://www.springframework.org/schema/jee" xmlns:p="http://www.springframework.org/schema/p"
  xsi:schemaLocation="
    http://www.springframework.org/schema/jee
    http://www.springframework.org/schema/jee/spring-jee.xsd
    http://www.springframework.org/schema/jdbc
    http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
    http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd">
  <beans profile="dev">
    <jdbc:embedded-database id="dataSource" type="H2">
      <jdbc:script location="classpath:schema.sql" />
      <jdbc:script location="classpath:test-data.sql" />
    </jdbc:embedded-database>
  </beans>
  <beans profile="prod">
    <jee:jndi-lookup id="dataSource"
      lazy-init="true"
      jndi-name="jdbc/myDatabase"
      resource-ref="true"
      proxy-interface="javax.sql.DataSource" />
  </beans>
</beans>
激活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 注解设置
1、使用 DispatcherServlet 的初始化参数 ,在servlet的上下文中进行设置( 兼顾到 ContextLoaderListener
web.xml

profile 使用的都是复数形式。 这意味着你可以同时激活多个 profile , 这可以通过列出多个 profile 名称, 并以逗号分隔来实现

测试

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);
    }
  }
  
  @RunWith(SpringJUnit4ClassRunner.class)
  @ContextConfiguration("classpath:datasource-config.xml")
  @ActiveProfiles("dev")
  public static class DevDataSourceTest_XMLConfig {
    @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("classpath:datasource-config.xml")
  @ActiveProfiles("prod")
  public static class ProductionDataSourceTest_XMLConfig {
    @Autowired(required=false)
    private DataSource dataSource;  
    @Test
    public void shouldBeEmbeddedDatasource() {
      // should be null, because there isn't a datasource configured in JNDI
      assertNull(dataSource);
    }
  }
}
条件化的bean

希望一个或多个bean只有在应用的类路径下包含特定的库时才创建。 或者希望某个bean只有当另外某个特定的bean也声明了之后才会创建。 我们还可能要求只有某个特定的环境变量设置之后, 才会创建某个bean

spring4给了解决方案, Spring 4 引入了一个新的 @Conditional 注解, 它可以用到带有 @Bean 注解的方法上。 如果给定的条件计算结果为 true , 就会创建这个 bean , 否则的话, 这个 bean 会被忽略
@Configuration
public class MagicConfig {
  @Bean
  @Conditional(MagicExistsCondition.class)//条件话的创建bean
  public MagicBean magicBean() {
    return new MagicBean();
  }
}
public class MagicExistsCondition implements Condition {
  @Override
  public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    Environment env = context.getEnvironment();
    return env.containsProperty("magic");
  } 
}
如果返回false,不会创建bean,返回true,创建bean
matches() 方法很简单但功能强大。 它通过给定的 ConditionContext 对象进而得到 Environment 对象, 并使用这个对象检查环境中是否存在名为 magic 的环境属性
public interface ConditionContext {
	BeanDefinitionRegistry getRegistry();
	ConfigurableListableBeanFactory getBeanFactory();
	Environment getEnvironment();
	ResourceLoader getResourceLoader();
        ClassLoader getClassLoader();
}
通过 ConditionContext , 我们可以做到如下几点:
借助
getRegistry() 返回的 BeanDefinitionRegistry 检查 bean 定义;
借助
getBeanFactory() 返回的 ConfigurableListableBeanFactory 检查 bean 是否存在,甚至探查 bean 的属性;
借助
getEnvironment() 返回的 Environment 检查环境变量是否存在以及它的值是什么;
读取并探查
getResourceLoader() 返回的 ResourceLoade r 所加载的资源;
借助
getClassLoader() 返回的 ClassLoader 加载并检查类是否存在。
AnnotatedTypeMetadata 则能够让我们检查带有 @Bean 注解的方法上还有什么其他的注解
public interface AnnotatedTypeMetadata {
	boolean isAnnotated(String annotationType);
	Map<String, Object> getAnnotationAttributes(String annotationType);
	Map<String, Object> getAnnotationAttributes(String annotationType, boolean classValuesAsString);
	MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationType);
	MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationType, boolean classValuesAsString);
}
借助 isAnnotated() 方法, 我们能够判断带有 @Bean 注解的方法是不是还有其他特定的注解。 借助其他的那些方法, 我们能够检查 @Bean 注解的方法上其他注解的属性。
Spring 4 开始, @Profile 注解进行了重构, 使其基于 @Conditional Condition 实现
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@Conditional(ProfileCondition.class)
public @interface Profile {
	String[] value();
}
@Profile 本身也使用了 @Conditional 注解, 并且引用 ProfileCondition 作为 Condition 实现。 如下所示, ProfileCondition 实现了 Condition 接口, 并且在做出决策的过程中, 考虑到了 ConditionContext AnnotatedTypeMetadata 中的多个因素。
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;
	}
}
ProfileCondition 通过 AnnotatedTypeMetadata 得到了用于 @Profile 注解的所有属性。 借助该信息, 它会明确地检查 value 属性, 该属性包含了 bean profile 名称。 然后, 它根据通过 ConditionContext 得到的 Environment 来检查[借助 acceptsProfiles() 方法] 该 profile 是否处于激活状态
处理自动装配的歧义性
	@Autowired
	public void setDessert(Dessert dessert){
		this.dessert(dessert);
	}
@Component
public class IceCream implements Dessert{

}
@Component
public class Cookies implements Dessert {

}
这三个实现均使用了 @Component 注解, 在组件扫描的时候, 能够发现它们并将其创建为 Spring 应用上下文里面的 bean 。 然后, 当 Spring 试图自动装配 setDessert() 中的 Dessert 参数时, 它并没有唯一、 无歧义的可选值 , Spring 会抛出 NoUniqueBeanDefinitionException
解决方案

1、将可选bean中的某一个设为首选(primary) 的bean, 

2、或者使用限定符(qualifier) 来帮助Spring将可选的bean的范围缩小到只有一个bean

标示首选的bean
在声明 bean 的时候, 通过将其中一个可选的 bean 设置为首选( primary bean 能够避免自动装配时的歧义性。 当遇到歧义性的时候, Spring 将会使用首选的 bean , 而不是其他可选的 bean

如:

@Primary
@Component
public class Cookies implements Dessert {
	@Autowired
	public void setDessert(Dessert dessert){
		this.setDessert(dessert);
	}
}
  @Bean
  @Primary
  public MagicBean magicBean() {
    return new MagicBean();
  }
<bean id="iceCream" class="com.desserteater.IceCream" primary="true"/>
如果设置多个首选,歧义性问题还会出现
限定自动转配的bean
@Qualifier 注解是使用限定符的主要方式。 它可以与 @Autowired @Inject 协同使用, 在注入的时候指定想要注入进去的是哪个 bean
如:

将iceCream注入到setDessert中

	@Autowired
	@Qualifier("iceCream")
	public void setDessert(Dessert dessert){
		this.setDessert(dessert);
	}
所有使用 @Component 注解声明的类都会创建为 bean , 并且 bean ID 为首字母变为小写的类名, 因此, @Qualifier("iceCream") 指向的是组件扫描时所创建的 bean , 并且这个 bean IceCream 类的实例。
@Qualifier("iceCream") 所引用的 bean 要具有 String 类型的 “iceCream” 作为限定符。 如果没有指定其他的限定符的话, 所有的 bean 都会给定一个默认的限定符, 这个限定与 bean ID 相同。 因此, 框架会将具有 “iceCream” 限定符的 bean 注入到 setDessert() 方法中。 这恰巧就是 ID iceCream bean , 它是 IceCream 类在组件扫描的时候创建的。
对类名称的任意改动都会导致限定符失效。
创建自定义的限定符
bean 设置自己的限定符, 而不是依赖于将 bean ID 作为限定符。 在这里所需要做的就是在 bean 声明上添加 @Qualifier 注解
@Component 组合使用
@Qualifier("cold")
@Component
public class Cookies implements Dessert {

}
cold限定符分配给了Cookies bean,可以随便定义类名,因为没有耦合
	@Autowired
	@Qualifier("cold")
	public void setDessert(Dessert dessert){
		this.setDessert(dessert);
	}
注入的地方只要引用cold限定符就可以
当通过 Java 配置显式定义 bean 的时候, @Qualifier 也可以与 @Bean 注解一起使用 
	@Bean
	@Qualifier("cold")
	public Dessert iceCream(Dessert dessert){
		return new Cookies();
	}
自定义名字,可以选择描述性术语
使用自定义的限定符注解
java8允许出现重复的注解,只要这个注解本身定义的时候带有@Repeatable注解就可以,但spring的@Qualifier注解并没有在定义的时候添加@Repeatable注解
面向特性的限定符要比基于 bean ID 的限定符更好一些。 但是, 如果多个 bean 都具备相同特性的话, 这种做法也会出现问题

如新的子类有相同的限定符,但我们可以创建自定义的限定符注解

@Target({ElementType.CONSTRUCTOR,ElementType.FIELD,ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Cold {}
自定义注解, 在定义时添加 @Qualifier 注解, 它们就具有了 @Qualifier 注解的特性
@Cold
@Component
public class Cookies implements Dessert {
	@Autowired
	@Cold
	public void setDessert(Dessert dessert){
		this.setDessert(dessert);
	}
	@Bean
	@Cold
	public Dessert iceCream(Dessert dessert){
		return new Cookies();
	}
}
可以定义多个注解,放入,满足条件的就会注入成功
bean的作用域
在默认情况下, Spring 应用上下文中所有 bean 都是作为以单例( singleton ) 的形式创建的。 也就是说,不管给定的一个 bean 被注入 到其他 bean 多少次, 每次所注入的都是同一个实例。在大多数情况下, 单例 bean 是很理想的方案。 初始化和垃圾回收对象实例所带来的成本只留给一些小规模任务, 在这些任务中, 让对象保持无状态并且在应用中反复重用这些对象可能并不合理。有时候, 可能会发现, 你所使用的类是易变的( mutable ) , 它们会保持一些状态, 因此重用是不安全的。 在这种情况下, 将 class 声明为单例的 bean 就不是什么好主意了, 因为对象会被污染, 稍后重用的时候会出现意想不到的问题
Spring 定义了多种作用域, 可以基于这些作用域创建 bean , 包括:
单例(
Singleton ) : 在整个应用中, 只创建 bean 的一个实例。
原型(
Prototype ) : 每次注入或者通过 Spring 应用上下文获取的时候, 都会创建一个新的 bean 实例。
会话(
Session ) : 在 Web 应用中, 为每个会话创建一个 bean 实例。
请求(
Rquest ) : 在 Web 应用中, 为每个请求创建一个 bean 实例。
单例是默认的作用域,对于易变的类型, 这并不合适。 如果选择其他的作用域, 要使用@Scope注解, 它可以与@Component@Bean一起使用
如: 使用组件扫描来发现和声明 bean , 那么你可以在 bean 的类上使用 @Scope 注解, 将其声明为原型 bean
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Notepad {
}
String SCOPE_SINGLETON = "singleton";
String SCOPE_PROTOTYPE = "prototype";
也可以使用 @Scope("prototype") , 但是使用 SCOPE_PROTOTYPE 常量更加安全并且不易出错
Java 配置中将 Notepad 声明为原型 bean , 可以组合使用 @Scope @Bean 来指定所需的作用域
  @Bean
  @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
  public Notepad notepad() {
    return new Notepad();
  }
使用 XML 来配置 bean 的话, 可以使用 <bean> 元素的 scope 属性来设置作用域
<bean class="com.myapp.Notepad" scope="prototype" />
以上都会创建新的实例
使用会话和请求作用域
Web 应用中, 如果能够实例化在会话和请求范围内共享的 bean , 那将是非常有价值的事情
例如, 在典型的电子商务应用中, 可能会有一个 bean 代表用户的购物车。 如果购物车是单例的话, 那么将会导致所有的用户都会向同一个购物车中添加商品。 另一方面, 如果购物车是原型作用域的, 那么在应用中某一个地方往购物车中添加商品, 在应用的另外一个地方可能就不可用了, 因为在这里注入的是另外一个原型作用域的购物车。就购物车 bean 来说, 会话作用域是最为合适的, 因为它与给定的用户关联性最大。 要指定会话作用域, 我们可以使用 @Scope 注解, 它的使用方式与指定原型作用域是相同的
	@Component
	@Scope(
			value=WebApplicationContext.SCOPE_SESSION,
			proxyMode=ScopedProxyMode.INTERFACES)
	public ShoppingCart cart(){
		return new ShoppingCart();
	}
value 设置成了 WebApplicationContext 中的 SCOPE_SESSION 常量(它的值是 session ) 。 这会告诉 Spring Web 应用中的每个会话创建一个 ShoppingCart 。 这会创建多
ShoppingCart bean 的实例, 但是对于给定的会话只会创建一个实例, 在当前会话相关的操作中, 这个 bean 实际上相当于单例的。
@Scope 同时还有一个 proxyMode 属性, 它被设置成了 ScopedProxyMode.INTERFACES 。 这个属性解决了将会话或请求 作用域的 bean 注入到单例 bean 中所遇到的问题
如果 ShoppingCart 是接口而不是类的话, 这是可以的(也是最为理想的代理模式) 。 但如果 ShoppingCart 是一个具体的类的话, Spring 就没有办法创建基于接口的代理了。 此时, 它必须使用 CGLib 来生成基于类的代理。 所以, 如果 bean 类型是具体类的话, 我们必须 要将 proxyMode 属性设置为 ScopedProxyMode.TARGET_CLASS , 以此来表明要以生成目标类扩展的方式创建代理。
请求作用域的 bean 会面临相同的装配问题。 因此, 请求作用域的 bean 应该也以作用域代理的方式进行注入。

在xml中声明作用域原理
要设置代理模式, 我们需要使用 Spring aop 命名空间的一个新元素
	<bean id="cart" class="com.myapp.ShoppingCart" scope="session">
		<aop:scoped-proxy>
	</bean>
<aop:scoped-proxy> 是与 @Scope 注解的 proxyMode 属性功能相同的 Spring XML 配置元素。 它会告诉 Spring bean 创建一个作用域代理。 默认情况下, 它会使用 CGLib 创建目标类的代理。 但是我们也可以将 proxy-target-class 属性设置为 false , 进而要求它生成基于接口的代理:
	<bean id="cart" class="com.myapp.ShoppingCart" scope="session">
		<aop:scoped-proxy proxy-target-class="false">
	</bean>
声明 Spring aop 命名空间

运行时注入
解决硬编码问题, Spring 提供了两种在运行时求值的方式:属性占位符( Property placeholder ) 。 Spring 表达式语言( SpEL ) 。
注入外部的值
Spring 中, 处理外部值的最简单方式就是声明属性源并通过 Spring Environment 来检索属性。 
@Configuration
@PropertySource("classpath:/com/soundsystem/app.properties")
public class EnvironmentConfig {
  @Autowired
  Environment env;
  @Bean
  public BlankDisc blankDisc() {
    return new BlankDisc(
        env.getProperty("disc.title"),
        env.getProperty("disc.artist"));
  } 
}
@PropertySource 引用了类路径中一个名为 app.properties 的文件
这个属性文件会加载到 Spring Environment 中, 稍后可以从这里检索属性。 同时, 在 disc() 方法中, 会创建一个新的 BlankDisc ,它的构造器参数是从属性文件中获取的, 而这是通过调用 getProperty() 实现的
Environment

四种getProperty方法

String getProperty(String key);
String getProperty(String key, String defaultValue);
<T> T getProperty(String key, Class<T> targetType);
<T> T getProperty(String key, Class<T> targetType, T defaultValue);
在值不存在时给定默认值

@Configuration
public class EnvironmentConfigWithDefaults {
  @Autowired
  Environment env; 
  @Bean
  public BlankDisc blankDisc() {
    return new BlankDisc(
        env.getProperty("disc.title", "Rattle and Hum"),
        env.getProperty("disc.artist", "U2"));
  } 
}
如果我们从属性文件中得到的是一个 String 类型的值, 那么在使用之前还需要将其转换为 Integer 类型。 但是, 如果使用重载形式的 getProperty() 的话, 就能非常便利地解决这个问题
int connectionCount = env.getProperty("db.connection", Integer.class, 30);
在使用 getProperty() 方法的时候没有指定默认值, 并且这个属性没有定义的话, 获取到的值是 null 。 如果你希望这个属性必须要定义, 那么可以使用 getRequiredProperty() 方法
@Configuration
public class EnvironmentConfigWithRequiredProperties {
  @Autowired
  Environment env; 
  @Bean
  public BlankDisc blankDisc() {
    return new BlankDisc(
        env.getRequiredProperty("disc.title"),
        env.getRequiredProperty("disc.artist"));
  }  
}
如果 disc.title disc.artist 属性没有定义的话, 将会抛出 IllegalStateException 异常
检查属性是否存在

调用EnvironmentcontainsProperty()方法

boolean titleExists = env.containsProperty("disc.title");
将属性解析为类

Class<CompactDisc> cdClass = env.getPropertyAsClass("disc.class", CompactDisc.class);
Environment 还提供了一些方法来检查哪些 profile 处于激活状态:
String[] getActiveProfiles() : 返回激活 profile 名称的数组;
String[] getDefaultProfiles() : 返回默认 profile 名称的数组;
boolean acceptsProfiles(String... profiles) : 如果 environment 支持给定 profile 的话, 就返回 true
解析属性占位符
Spring 装配中, 占位符的形式为使用 ${... } 包装的属性名称 

XML 中按照如下的方式解析 BlankDisc 构造器参数
	<bean id="sgtPeppers" class="soundsystem.BlankDisc"
	c:_title="${disc.title}"
	c:_artist="${disc.artist}"/>
title 构造器参数所给定的值是从一个属性中解析得到的, 这个属性的名称为 disc.title artist 参数装配的是名为 disc.artist 的属性值。 按照这种方式, XML 配置没有使用任何硬
编码的值, 它的值是从配置文件以外的一个源中解析得到的

依赖于组件扫描和自动装配来创建和初始化应用组件的话,那么就没有指定占位符的配置文件或类了。 在这种情况下, 我们可以使用 @Value 注解, 它的使用方与 @Autowired 注解非常相似
  public BlankDisc(@Value("${disc.title}") String title, @Value("${disc.artist}")String artist) {
    this.title = title;
    this.artist = artist;
  }
为了使用占位符, 我们必须要配置一个 PropertyPlaceholderConfigurer bean PropertySourcesPlaceholderConfigurer bean 。 从 Spring3.1 开始, 推荐使
PropertySourcesPlaceholderConfigurer , 因为它能够基于 Spring Environment 及其属性源来解析占位符。 

java配置
  @Bean
  public static PropertySourcesPlaceholderConfigurer placeholderConfigurer(){
	  return new PropertySourcesPlaceholderConfigurer();
  }
xml配置

Spring context命名空间中的<context:propertyplaceholder>元素将会生成PropertySourcesPlaceholderConfigurer bean

解析外部属性能够将值的处理推迟到运行时, 但是它的关注点在于根据名称解析来自于Spring Environment和属性源的属性。 
使用spring表达式语言进行装配
Spring 3引入了Spring表达式语言
SpEL拥有很多特性, 包括:
使用
beanID来引用bean
调用方法和访问对象的属性;
对值进行算术、 关系和逻辑运算;
正则表达式匹配;
集合操作。

SpEL样例
SpEL能够用在依赖注入以外的其他地方。 例如, Spring Security支持使用SpEL表达式定义安全限制规则。 另外, 如果你在Spring MVC应用中使用Thymeleaf模板作为视图
的话, 那么这些模板可以使用
SpEL表达式引用模型数据
SpEL表达式要放到#{ ... }之中, 这与属性占位符有些类似, 属性占位符需要放到${ ... }之中
#{1}结果就是1

#{T(System).currentTImeMillis()}   
它的最终结果是计算表达式的那一刻当前时间的毫秒数。 T()表达式会将java.lang.System视为Java中对应的类型, 因此可以调用其static修饰的currentTimeMillis()方法
SpEL表达式也可以引用其他的bean或其他bean的属性。 例如, 如下的表达式会计算得到IDsgtPeppersbeanartist属性
#{sgtPeppers.artist}
如果通过组件扫描创建bean的话, 在注入属性和构造器参数时, 我们可以使用@Value注解 ,类似属性占位符

  public BlankDisc(@Value("#{systemProperties['disc.title']}") String title, @Value("#{systemProperties['disc.artist']")String artist) {
    this.title = title;
    this.artist = artist;
  }
XML 配置中, 你可以将 SpEL 表达式传入 <property> <constructor-arg> value 属性中, 或者将其作为 p- 命名空间或 c- 命名空间条目的值。 例如, 在如下 BlankDisc bean XML 声明中, 构造器参数就是通过 SpEL 表达式设置的
	<bean id="sgtPeppers" class="soundsystem.BlankDisc"
	c:_title="#{systemProperties['disc.title']}"
	c:_artist="#{systemProperties['disc.artist']"/>
表示字面值
SpEL不仅 表示整数字面量的,它实际上还可以用来表示浮点数、 String 值以及 Boolean 值,科学计数法

#{3.1415}
#{9.87E4}
#{'Hello'}
#{false}
引用 bean 、 属性和方法
使用 SpEL 将一个 bean 装配到另外一个 bean 的属性中, 此时要使用 bean ID 作为 SpEL 表达式
#{sgtPeppers}
在一个表达式中引用 sgtPeppers artist 属性
#{sgtPeppers.artist}
表达式主体的第一部分引用了一个 ID sgtPeppers bean , 分割符之后是对 artist 属性的引用
还可以调用 bean 上的方法
#{artistSelector.selectArtist()}

可以对返回值调用方法
#{artistSelector.selectArtist().toUpperCase()}

避免返回值为空

#{artistSelector.selectArtist()?.toUpperCase()}

“?.”运算符。 这个运算符能够在访问它右边的内容之前, 确保它所对应的元素不是null ,如果selectArtist()的返回值是null的话, 那么SpEL将不会调用toUpperCase()方法。 表达
式的返回值会是
null
在表达式中使用类型
SpEL中访问类作用域的方法和常量的话, 要依赖T()这个关键的运算符
如使用java的Math类

T(java.lang.Math)
这里所示的 T() 运算符的结果会是一个 Class 对象, 代表了 java.lang.Math
T() 运算符的真正价值在于它能够访问目标类型的静态方法和常量
如下

T(java.lang.Math).PI

T(java.lang.Math).random()

SpEL运算符

运算符类型 运 算 符
算术运算 + -* /%^
比较运算 < > == <= >= lt gt eq le ge
逻辑运算 and or not
条件运算 ?: (ternary) ?: (Elvis)
正则表达式 matches

和java中一致

#{counter.total==100}等价于文本型eq#{counter.total eq 100}

三元运算符类似

#{100>0?100,10}

判null时

#{disc.title?:'100'},为空,值为100

计算正则表达式

#{admin.email matches 'a-zA-Z0-9._%+-+@[a-zA-Z0-9.-]+\\.com'}

计算集合

#{jukebox[4].title}
“[]” 运算符用来从集合或数组中按照索引获取元素, 实际上, 它还可以从 String 中获取一个字符
#{'abcd'[3]}=d
SpEL 还提供了查询运算符( .?[] ) , 它会用来对集合进行过滤, 得到集合的一个子集
#{jukebox.songs.?[artist eq 'Aerosmith']}
另外两个查询运算符: .^[] .$[] , 它们分别用来在集合中查询第一个匹配项和最后一个匹配项。
SpEL 还提供了投影运算符( .![] ) , 它会从集合的每个成员中选择特定的属性放到另外一个集合中
假设我们不想要歌曲对象的集合, 而是所有歌曲名称的集合。 如下的表达式会将 title 属性投影到一个新的 String 类型的集合中

#{jukebox.songs.![title]}

使用如下的表达式获得Aerosmith所有歌曲的名称列表

#{jukebox.songs.?[artist eq 'Aerosmoth'].![title]}








  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值