一、Spring 高级装配
1.1 配置profile bean
1)在Java类中配置profile
Spring引入了bean profile的功能。要使用profile,你首先要将所有不同的bean定义整理到一个或多个profile之中,在将应用部署到每个环境时,要确保对应的profile处于激活(active)的状态。
在Java配置中,可以使用@Profile注解指定某个bean属于哪一个profile。例如,在配置类中,嵌入式数据库的DataSource可能会配置成如下所示:
import org.springframework.context.annotation.*;
@Configuration
@Profile("dev")
public class MyApplicationContext {
}
希望你能够注意的是@Profile注解应用在了类级别上。它会告诉Spring这个配置类中的bean只有在dev profile激活时才会创建。如果dev profile没有激活的话,那么带有@Bean注解的方法都会被忽略掉。
在Spring 3.1中,只能在类级别上使用@Profile注解。不过,从Spring 3.2开始,你也可以在方法级别上使用@Profile注解,
与@Bean注解一同使用。
import org.springframework.context.annotation.*;
@Configuration
public class MyApplicationContext {
@Bean
@Profile("dev")
public Apple apple() {
return new Apple();
}
}
2)在XML中配置profile
我们也可以通过<beans>元素的profile属性,在XML中配置profile bean。
<?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"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context"
xmlns:util="http://www.springframework.org/schema/util" xmlns:jpa="http://www.springframework.org/schema/data/jpa"
xmlns:c="http://www.springframework.org/schema/c"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util.xsd
http://www.springframework.org/schema/data/jpa
http://www.springframework.org/schema/data/jpa/spring-jpa.xsd"
profile="dev">
</beans>
你还可以在根<beans>元素中嵌套定义<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:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context"
xmlns:util="http://www.springframework.org/schema/util" xmlns:jpa="http://www.springframework.org/schema/data/jpa"
xmlns:c="http://www.springframework.org/schema/c"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util.xsd
http://www.springframework.org/schema/data/jpa
http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">
<beans profile="dev"></beans>
<beans profile="qa"></beans>
<beans profile="prod"></beans>
</beans>
3)激活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。你可以同时激活多个profile,这可以通过列出多个profile名
称,并以逗号分隔来实现。
有多种方式来设置这两个属性:
- 作为DispatcherServlet的初始化参数;
- 作为Web应用的上下文参数;
- 作为JNDI条目;
- 作为环境变量;
- 作为JVM的系统属性;
- 在集成测试类上,使用@ActiveProfiles注解设置。
最常用的一种方式是使用DispatcherServlet的参数将spring.profiles.default设置为开发环境的profile,我会在Servlet上下文中进行设置(为了兼顾到ContextLoaderListener)。
例如,在Web应用中,设置spring.profiles.default的web.xml文件会如下所示:
如果不是Web工程可以使用一下方式设置
System.setProperty("spring.profiles.active","qa" );
1.2 条件化的bean
假设你希望一个或多个bean只有在应用的类路径下包含特定的库时才创建。或者我们希望某个bean只有当另外某个特定的bean也声明了之后才会创建。我们还可能要求只有某个特定的环境变量设置之后,才会创建某个bean。
在Spring 4之前,很难实现这种级别的条件化配置,但是Spring 4引入了一个新的@Conditional注解,它可以用到带有@Bean注解的方法上。如果给定的条件计算结果为true,就会创建这个bean,否则的话,这个bean会被忽略。
@Bean
@Conditional(AppleConditional.class)
public Apple apple3() {
return new Apple("apple3", "yellow", null);
}
,@Conditional中给定了一个Class,它指明了本例中的条件,也就是AppleConditional。@Conditional注解将会通过Condition接口进行条件对比:
public interface Condition {
boolean matches(ConditionContext var1, AnnotatedTypeMetadata var2);
}
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
public class AppleConditional implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
return false;
}
}
设置给@Conditional的类可以是任意实现了Condition接口的类型。可以看出来,这个接口实现起来很简单直接,只需提供matches()方法的实现即可。如果matches()方法返回true,那么就会创建带有@Conditional注解的bean。如果matches()方法
返回false,将不会创建这些bean。
其中ConditionContext类的提供了一些有用的方法
- 借助getRegistry()返回的BeanDefinitionRegistry检查bean定义;
- 借助getBeanFactory()返回的ConfigurableListableBeanFactory检查bean是否存在,甚至探查bean的属性;
- 借助getEnvironment()返回的Environment检查环境变量是否存在以及它的值是什么;
- 读取并探查getResourceLoader()返回的ResourceLoader所加载的资源;
- 借助getClassLoader()返回的ClassLoader加载并检查类是否存在。
AnnotatedTypeMetadata则能够让我们检查带有@Bean注解的方法上还有什么其他的注解。像ConditionContext一
样,AnnotatedTypeMetadata也是一个接口。
public interface AnnotatedTypeMetadata {
boolean isAnnotated(String var1);
Map<String, Object> getAnnotationAttributes(String var1);
Map<String, Object> getAnnotationAttributes(String var1, boolean var2);
MultiValueMap<String, Object> getAllAnnotationAttributes(String var1);
MultiValueMap<String, Object> getAllAnnotationAttributes(String var1, boolean var2);
}
1.3 处理自动装配的歧义性
1)标示首选的bean
在声明bean的时候,通过将其中一个可选的bean设置为首选(primary)bean能够避免自动装配时的歧义性。当遇到歧义性的时候,Spring将会使用首选的bean,而不是其他可选的bean。实际上,你所声明就是“最喜欢”的bean。但是,如果你标示了两个或更多的首选bean,那么它就无法正常工作了。
@Bean
@Primary
public Apple apple3() {
return new Apple("apple3", "yellow", null);
}
<bean id="apple" class="mine.Apple" primary="true" />
2)限定自动装配的bean
设置首选bean的局限性在于@Primary无法将可选方案的范围限定到唯一一个无歧义性的选项中。它只能标示一个优先的可选方案。当首选bean的数量超过一个时,我们并没有其他的方法进一步缩小可选范围。
与之相反,Spring的限定符能够在所有可选的bean上进行缩小范围的操作,最终能够达到只有一个bean满足所规定的限制条件。如果将所有的限定符都用上后依然存在歧义性,那么你可以继续使用更多的限定符来缩小选择范围。
@Qualifier注解是使用限定符的主要方式。它可以与@Autowired和@Inject协同使用,在注入的时候指定想要注入进去的是哪个bean。为@Qualifier注解所设置的参数就是想要注入的bean的ID。
@Autowired
@Qualifier("apple")
private Apple apple;
3)创建自定义的限定符
我们可以为bean设置自己的限定符,而不是依赖于将bean ID作为限定符。在这里所需要做的就是在bean声明上添加@Qualifier注解。
@Component
@Qualifier("apple")
public class Apple implements Fruit{...}
@Bean
@Qualifier("apple")
public Apple apple3() {
return new Apple("apple3", "yellow", null);
}
4)创建自定义的限定符注解
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface AppleOne {
}
1.4 bean的作用域
在默认情况下,Spring应用上下文中所有bean都是作为以单例(singleton)的形式创建的。也就是说,不管给定的一个bean被注入到其他bean多少次,每次所注入的都是同一个实例。
Spring定义了多种作用域,可以基于这些作用域创建bean,包括:
- 单例(Singleton):在整个应用中,只创建bean的一个实例。
- 原型(Prototype):每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的bean实例。
- 会话(Session):在Web应用中,为每个会话创建一个bean实例。
- 请求(Rquest):在Web应用中,为每个请求创建一个bean实例。
单例是默认的作用域,但是正如之前所述,对于易变的类型,这并不合适。如果选择其他的作用域,要使用@Scope注解,它可以与@Component或@Bean一起使用。
// 声明为原型bean,也可以使用 @Scope("prototype")来声明
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Apple apple3() {
return new Apple("apple3", "yellow", null);
}
// 声明为原型bean
<bean id="apple" class="mine.Apple" scope="prototype" />
1)使用会话和请求作用域
在Web应用中,如果能够实例化在会话和请求范围内共享的bean,那将是非常有价值的事情。例如,在典型的电子商务应用中,可能会有一个bean代表用户的购物车。如果购物车是单例的话,那么将会导致所有的用户都会向同一个购物车中添加商品。另一方面,如果购物车是原型作用域的,那么在应用中某一个地方往购物车中添加商品,在应用的另外一个地方可能就不可用了,因为在这里注入的是另外一个原型作用域的购物车。
就购物车bean来说,会话作用域是最为合适的,因为它与给定的用户关联性最大。要指定会话作用域,我们可以使用@Scope注解,它的使用方式与指定原型作用域是相同的:
@Bean
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.INTERFACES)
这里,我们将value设置成了WebApplicationContext中的SCOPE_SESSION常量(它的值是session)。对于给定的会话只会创建一个实例,在当前会话相关的操作中,这个bean实际上相当于单例的。
@Scope同时还有一个proxyMode属性,它被设置成了ScopedProxyMode.INTERFACES。这个属性解决了将会话或请求作用域的bean注入到单例bean中所遇到的问题。
如果被注释的是类而不是接口的话需要使用如下配置:
@Bean
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
2)在XML中声明作用域代理
如果你需要使用XML来声明会话或请求作用域的bean,那么就不能使用@Scope注解及其proxyMode属性了。<bean>元素的scope属性能够设置bean的作用域,但是该怎样指定代理模式呢?
要设置代理模式,我们需要使用Spring aop命名空间的一个新元素:
<bean id="apple" class="mine.Apple" scope="session" >
<aop:scoped-proxy />
</bean>
<aop:scoped-proxy>是与@Scope注解的proxyMode属性功能相同的Spring XML配置元素。它会告诉Spring为bean创建一个作用域代理。默认情况下,它会使用CGLib创建目标类的代理。但是我们也可以将proxy-target-class属性设置为false,进而要求它生成基于接口的代理:
<bean id="apple" class="mine.Apple" scope="session" >
<aop:scoped-proxy proxy-target-class="false"/>
</bean>
1.5 运行时值注入
有时候硬编码是可以的,但有的时候,我们可能会希望避免硬编码值,而是想让这些值在运行时再确定。为了实现这些功能,Spring提供了两种在运行时求值的方式:
- 属性占位符(Property placeholder)。
- Spring表达式语言(SpEL)。
1)注入外部的值
在Spring中,处理外部值的最简单方式就是声明属性源并通过Spring的Environment来检索属性。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.*;
import org.springframework.core.env.Environment;
@Configuration
@ComponentScan
@PropertySource("classpath:/mine/app.properties")
public class MyApplicationContext {
@Autowired
Environment env;
@Bean
public Apple apple() {
String name = env.getProperty("apple.name");
String color = env.getProperty("apple.color");
return new Apple(name, color, null);
}
}
app.properties文件内容如下
apple.name=apple
apple.color=red
2)深入学习Spring的Environment
当我们去了解Environment的时候会发现,程序上述示列所示的getProperty()方法并不是获取属性值的唯一方法,getProperty()方法有四个重载的变种形式:
public interface Environment extends PropertyResolver {
// 获取指定的值
String getProperty(String key);
// 如果没有指定的值反回defaultValue
String getProperty(String key, String defaultValue);
// 获取指定的值,并且转化为对应的类型
String getProperty(String key, Class<T> type);
// 获取指定的值,并且转化为对应的类型,如果没有指定的值反回defaultValue
String getProperty(String key, Class<T> type, T defaultValue);
// 将属性解析为类
<T> Class<T> getPropertyAsClass(String key, Class<T> targetType);
// 检查某个属性是否存在
boolean containsProperty(String key);
// 返回激活profile名称的数组
String[] getActiveProfiles();
// 返回默认profile名称的数组
String[] getDefaultProfiles();
// 如果environment支持给定profile的话,就返回true
boolean acceptsProfiles(String... profiles);
}
3)解析属性占位符
Spring一直支持将属性定义到外部的属性的文件中,并使用占位符值将其插入到Spring bean中。在Spring装配中,占位符的形式为使用“${… }”包装的属性名称。作为样例,我们可以在XML中按照如下的配置设置:
<bean id="apple" class="mine.Apple" c:name="${apple.name}" c:color="${apple.color}">
如果我们依赖于组件扫描和自动装配来创建和初始化应用组件的话,那么就没有指定占位符的配置文件或类了。在这种情况下,我们可以使用@Value注解,它的使用方式与@Autowired注解非常相似。比如,在Apple类中,构造器可以改成如下所示:
public Apple(@Value("${apple.name}") String name) {
this.name = name;
}
为了使用占位符,我们必须要配置一个PropertyPlaceholderConfigurer bean或PropertySourcesPlaceholderConfigurer bean。
从Spring3.1开始,推荐使用PropertySourcesPlaceholderConfigurer,因为它能够基于Spring Environment及其属性源来解析占位符。
@Bean
public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
如果你想使用XML配置的话,Spring context命名空间中的<context:propertyplaceholder>元素将会为你生成PropertySourcesPlaceholderConfigurer bean:
<context:property-placeholder />