略。
二、条件化的bean
Spring 4引入了一个新的@Conditional注解,它可以用到带有@Bean注解的方法上。如果给定的条件计算结果为true,就会创建这个bean,否则的话,这个bean会被忽略。
举例:假设有一个名为MagicBean的类,我们希望只有设置了magic环境属性的时候,Spring才会实例化这个类。如果环境中没有这个属性,那么MagicBean将会被忽略。
package com.springinaction.ch03.conditional; public class MagicBean {}
使用@Conditional注解条件化地配置bean:
package com.springinaction.ch03.conditional; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; @Configuration public class MagicConfig { @Bean @Conditional(MagicExistsCondition.class) public MagicBean magicBean() { return new MagicBean(); } }
设置给@Conditional的类(例如MagicExistsCondition)可以是任意实现了Condition接口的类型(这个接口是Spring中的接口)。这个接口实现起来很简单直接,只需提供matches()方法的实现即可。如果matches()方法返回true,那么就会创建带有@Conditional注解的bean。如果matches()方法返回false,将不会创建这些bean。
package com.springinaction.ch03.conditional; import org.springframework.context.annotation.Condition; import org.springframework.context.annotation.ConditionContext; import org.springframework.core.env.Environment; import org.springframework.core.type.AnnotatedTypeMetadata; public class MagicExistsCondition implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { Environment env = context.getEnvironment(); return env.containsProperty("magic"); } }
它通过给定的ConditionContext对象进而得到Environment对象,并使用这个对象检查环境中是否存在名为magic的环境属性。只要属性存在即可满足要求。如果满足这个条件的话,matches()方法就会返回true。所带来的结果就是条件能够得到满足,所有@Conditional注解上引用MagicExistsCondition的bean都会被创建。matches()方法会得到ConditionContext和AnnotatedTypeMetadata对象用来做出决策。
测试:
package com.springinaction.ch03.conditional.test; import static org.junit.Assert.*; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import com.springinaction.ch03.conditional.MagicConfig; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes=MagicConfig.class) public class MagicExistsTest { @BeforeClass public static void setProperty() { System.setProperty("magic", "true"); } @Autowired private ApplicationContext context; /* * This test will fail until you set a "magic" property. * You can set this property as an environment variable, a JVM system property, by adding a @BeforeClass * method and calling System.setProperty() or one of several other options. */ @Test public void shouldNotBeNull() { assertTrue(context.containsBean("magicBean")); } }
ConditionContext是一个接口:
通过ConditionContext,我们可以做到如下几点:
1、借助getRegistry()返回的BeanDefinitionRegistry检查bean定义;
2、借助getBeanFactory()返回的ConfigurableListableBeanFactory检查bean是否存在,甚至探查bean的属性;
3、借助getEnvironment()返回的Environment检查环境变量是否存在以及它的值是什么;
4、读取并探查getResourceLoader()返回的ResourceLoader所加载的资源;
5、借助getClassLoader()返回的ClassLoader加载并检查类是否存在。
三、处理自动装配的歧义性
Dessert是一个接口,并且有三个类实现了这个接口,分别为Cake、Cookies和IceCream:
@Component public class Cake implements Dessert {} @Component public class Cookies implements Dessert {} @Component public class IceCream implements Dessert {}
在自动化装配时,
@Autowired public void setDessert(Dessert dessert){ this.dessert=dessert; }
因为这三个实现均使用了@Component注解,在组件扫描的时候,能够发现它们并将其创建为Spring应用上下文里面的bean。然后,当Spring试图自动装配setDessert()中的Dessert参数时,它并没有唯一、无歧义的可选值。之后会发生异常(NoUniqueBeanDefinitionException)。
解决歧义性的方案有:
在Spring中,可以通过@Primary来表达最喜欢的方案。@Primary能够与@Component组合用在组件扫描的bean上:
import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; @Component @Primary public class IceCream implements Dessert { }
@Primary与@Bean组合用在Java配置的bean声明中:
@Bean @Primary public Dessert iceCream(){ return new IceCream(); }用XML配置bean,<bean>元素有一个primary属性用来指定首选的bean:
如果你标示了两个或更多的首选bean,那么它就无法正常工作了。
3.2 限定自动装配的bean
@Qualifier注解是使用限定符的主要方式。它可以与@Autowired和@Inject协同使用,在注入的时候指定想要注入进去的是哪个bean。例如想要确保将IceCream注入到setDessert()之中:
@Autowired @Qualifier("iceCream") public Dessert setDessert(Dessert dessert){ this.dessert = dessert; }
为@Qualifier注解所设置的参数就是想要注入的bean的ID。setDessert()方法上所指定的限定符与要注入的bean的名称是紧耦合的。对类名称的任意改动都会导致限定符失败。
如果没有指定其他的限定符的话,所有的bean都会给定一个默认的限定符,这个限定符与bean的ID相同。
创建自定义的限定符
可以为bean设置自己的限定符,而不是依赖于bean ID作为限定符。自定义限制符的名称:
@Component @Qualifier("cold") public class IceCream implements Dessert {...}
cold限定符分配给了IceCream bean。因为它没有耦合类名,因此可以随意重构IceCream 的类名,而不必担心破坏自动装配。在注入的地方,只要引用cold限定符就可以了:
@Autowired @Qualifier("cold") public Dessert setDessert(Dessert dessert){ this.dessert= dessert; }
注意:当使用自定义的@Qualifier值时,最佳实践是为bean选择特征性或描述性的术语,而不是使用随意的名字。
使用自定义的限定符注解
如果多个bean都具备相同特性的话,这种做法也会出现重匹配的问题。(可以用多组描述区分)但是由于Java中不容许相同注解的出现,故需要自定义注解。
创建自定义的限定符注解,借助这样的注解来表达bean所希望限定的特性。这里所需要做的就是创建一个注解,它本身要使用@Qualifier注解来标注。这样我们将不再使用@Qualifier("cold"),而是使用自定义的@Cold注解。其用法和@Qualifier一样。
@Target({ElementType.TYPE,ElementType.METHOD,ElementType.FIELD,ElementType.CONSTRUCTOR}) @Retention(RetentionPolicy.RUNTIME) @Qualifier public @interface Cold { }使用限定符注解:
@Component @cold public class IceCream implements Dessert {...}
四、bean的作用域
Spring定义了多种作用域,可以基于这些作用域创建bean,包括:
单例(Singleton):在整个应用中,只创建bean的一个实例。
原型(Prototype):每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的bean实例。
会话(Session):在Web应用中,为每个会话创建一个bean实例。
请求(Rquest):在Web应用中,为每个请求创建一个bean实例。
单例是默认的作用域。
4.1 原型
使用组件扫描来发现和声明bean,那么你可以在bean的类上使用@Scope注解,将其声明为原型bean:
import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; import org.springframework.beans.factory.config.ConfigurableBeanFactory; @Component @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)//或为(@Scope("prototype")) public class Notepad { }
想在Java配置中将Notepad声明为原型bean,那么可以组合使用@Scope和@Bean来指定所需的作用域:
@Bean @Scope("prototype") public NodePad nodePad(){ return new NodePad(); }使用XML来配置bean的话,可以使用<bean>元素的scope属性来设置作用域:
不管使用哪种方式来声明作用域,每次注入或从Spring应用上下文中检索该属性的时候,都会创建新的实例。这样导致的结果是每次操作都能得到自己的Notepad实例。
4.2会话与请求域
在典型的电子商务应用中,可能会有一个bean代表用户的购物车。如果购物车是单例的话,那么将会导致所有的用户都会向同一个购物车中添加商品。如果购物车是原型的话,那么在应用中某一个地方往购物车中添加商品,在应用中的另一个地方可能就不可用了,因为在这里注入的是另外一个原型作用域的购物车。
就购物车bean来说,会话作用域是最为合适的,因为它与给定的用户关联性最大。
@Component @Scope(value=WebApplicationContext.SCOPE_SESSION, proxyMode=ScopedProxyMode.INTERFACES) public ShoppingCart cart() { ... }
将value设置成了WebApplicationContext中的SCOPE_SESSION常量(它的值是session)。这会告诉Spring为Web应用中的每个会话创建一个ShoppingCart。这会创建多个ShoppingCart bean的实例,但是对于给定的会话只会创建一个实例,在当前会话相关的操作中,这个bean实际上相当于单例的。
而proxyMode属性被设置成了ScopedProxyMode.INTERFACES。这个属性解决了将会话或请求作用域的bean注入到单例bean中所遇到的问题。例如将ShoppingCart bean注入到单例StoreService bean的Setter方法中:
@Component
public class StoreService {
@Autowired
public void setShoppingCart(ShoppingCart shoppingCart) {
this.shoppingCart = shoppingCart;
}
}
因为StoreService 是一个单例的bean,会在Spring应用上下文加载的时候创建。当它创建的时候,Spring会试图将ShoppingCart bean注入到setShoppingCart()方法中。但ShoppingCart bean是会话作用域的,此时并不存在,直到某个用户进入系统,创建会话之后,才会出现ShoppingCart 实例。
Spring并不会将实际的ShoppingCart bean注入到StoreService中,Spring会注入一个到ShoppingCart bean的代理,这个代理会暴露与ShoppingCart相同的方法,所以StoreService会认为它就是一个购物车。但是,当StoreService调用ShoppingCart的方法时,代理会对其进行懒解析并将调用委托给会话作用域内真正的ShoppingCart bean。
proxyMode属性被设置成了ScopedProxyMode.INTERFACES,这表明这个代理要实现ShoppingCart接口,并将调用委托给实现bean。如果ShoppingCart是接口而不是类的话,这是可以的(也是最为理想的代理模式)。但如果ShoppingCart是一个具体的类的话,Spring就没有办法创建基于接口的代理了。此时,它必须使用CGLib来生成基于类的代理。所以,如果bean类型是具体类的话,我们必须要将proxyMode属性设置为ScopedProxyMode.TARGET_CLASS,以此来表明要以生成目标类扩展的方式创建代理。
请求作用域的bean应该也以作用域代理的方式进行注入。
4.3在xml中声明作用域及代理
如果需要使用XML来声明会话或请求作用域的bean,那么就使用<bean>元素的scope属性;要设置代理模式,需要使用Spring aop命名空间的一个新元素:
<?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:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd"> <bean id="cart" class="com.myapp.ShoppingCart" scope="session"> <aop:scoped-proxy/> </bean> </beans>
<aop:scoped-proxy>是与@Scope注解的proxyMode属性功能相同的Spring XML配置元素。它会告诉Spring为bean创建一个作用域代理。默认情况下,它会使用CGLib创建目标类的代理。但是我们也可以将proxy-target-class属性设置为false,进而要求它生成基于接口的代理:
<?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:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd"> <bean id="cart" class="com.myapp.ShoppingCart" scope="session"> <aop:scoped-proxy proxy-target-class="false"/> </bean> </beans>
五、运行时植入
当希望避免硬编码值,而是想让这些值在运行时再确定。Spring提供了两种在运行时求值的方式:
属性占位符(Property placeholder)。
Spring表达式语言(SpEL)。
5.1注入外部值
在Spring中,处理外部值的最简单方式就是声明属性源并通过Spring的Environment来检索属性。
(1)BlankDisc.java
package com.springinaction.ch03.externals; 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 getTitle() { return title; } public String getArtist() { return artist; } }
(2)EnvironmentConfig.java
package com.springinaction.ch03.externals; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; import org.springframework.core.env.Environment; import com.springinaction.ch03.externals.BlankDisc; @Configuration @PropertySource("classpath:/com/springinaction/ch03/externals/app.properties") public class EnvironmentConfig { @Autowired Environment env; @Bean public BlankDisc blankDisc() { return new BlankDisc( env.getProperty("disc.title"), env.getProperty("disc.artist")); } }
(3)app.properties
disc.title=Sgt. Peppers Lonely Hearts Club Band disc.artist=The Beatles
测试:
package com.springinaction.ch03.externals.test; import static org.junit.Assert.*; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import com.springinaction.ch03.externals.BlankDisc; import com.springinaction.ch03.externals.EnvironmentConfig; public class EnvironmentInjectionTest { @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes=EnvironmentConfig.class) public static class InjectFromProperties { @Autowired private BlankDisc blankDisc; @Test public void assertBlankDiscProperties() { assertEquals("The Beatles", blankDisc.getArtist()); assertEquals("Sgt. Peppers Lonely Hearts Club Band", blankDisc.getTitle()); } } }
占位符
Spring一直支持将属性定义到外部的属性的文件中,并使用占位符值将其插入到Spring bean中。在Spring装配中,占位符的形式为使用“${... }”包装的属性名称。
(1) BlankDisc.java
package com.springinaction.ch03.placeholder; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component public class BlankDisc { private String title; private String artist; public BlankDisc(@Value("${disc.title}") String title, @Value("${disc.artist}") String artist) { this.title = title; this.artist = artist; } public String getTitle() { return title; } public String getArtist() { return artist; } }
或XML版本:
<bean id="blankDisc" class="com.springinaction.ch03.placeholder.BlankDisc" c:title="${disc.title}" c:artist="${disc.artist}"> </bean>注意:不能使用“c:_title”,只能使用"c:title"或"c:_0","c:"命名空间后面直接跟构造参数名称或者下标
(2) PlaceholderConfig.java
package com.springinaction.ch03.placeholder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; @Configuration @ComponentScan("com.springinaction.ch03.placeholder") @PropertySource("classpath:/com/springinaction/ch03/placeholder/app.properties") public class PlaceholderConfig { /* * 3.1开始官方文档推荐优先使用PropertySourcesPlaceholderConfigurer取代PropertyPlaceholderConfigurer */ @Bean public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() { return new PropertySourcesPlaceholderConfigurer (); } }
为了使用占位符,必须配置一个PropertyPlaceholderConfigurer bean或PropertySourcesPlaceholderConfigurer bean。从Spring3.1开始,推荐使用PropertySourcesPlaceholderConfigurer,因为它能够基于Spring Environment及其属性源来解析占位符。
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:c="http://www.springframework.org/schema/c" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd"> <bean id="blankDisc" class="com.springinaction.ch03.placeholder.BlankDisc" c:_0="${disc.title}" c:_1="${disc.artist}"> </bean> <context:property-placeholder location="classpath:/com/springinaction/ch03/placeholder/app.properties"/> </beans>
测试:
package com.springinaction.ch03.placeholder.test; import static org.junit.Assert.*; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import com.springinaction.ch03.placeholder.BlankDisc; import com.springinaction.ch03.placeholder.PlaceholderConfig; public class EnvironmentInjectionTest { @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes=PlaceholderConfig.class) public static class InjectFromProperties { @Autowired private BlankDisc blankDisc; @Test public void assertBlankDiscProperties() { assertEquals("The Beatles", blankDisc.getArtist()); assertEquals("Sgt. Peppers Lonely Hearts Club Band", blankDisc.getTitle()); } } }