1、环境与profile
在开发软件时,有一个很大的挑战就是将应用程序从一个环境迁移到另外一个环境。开发阶段中,某些环境相关做法可能并不适合迁移到生产环境中,甚至即便迁移过去也无法正常工作。数据库配置、加密算法以及与外部系统的集成是跨环境部署时会发生变化的几个典型例子。
在不同的环境中某个bean会有所不同。
1.1配置profile bean
Spring为环境相关的bean所提供的解决方案是等到运行时再确定该创建哪个bean不该创建哪个bean,从而使同一个部署单元能够适用于所有的环境,没有必要进行重新构建。
Spring引入了bean profile的功能。 要使用profile,首先要将所有不同的bean整理到一个或多个profile之中,在将应用部署到每个环境时,要确保对应的profile处于激活(active)的状态。
在Java配置中,可以使用@Profile注解指定某个bean属于哪一个profile。
package soundsystem;
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 javax.sql.DataSource;
@Configuration
@Profile("dev")
public class DevelopmentProfileConfig {
@Bean(destroyMethod = "shutdown")
public DataSource dataSource(){
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:schema.sql")
.addScript("classpath:test-data.sql")
.build();
}
}
@Profile注解应用在了类级别上。它会告诉Spring这个配置类中的bean只有在dev profile激活时才会被创建。
也可以在方法级别上使用@Profile注解,与@Bean注解一同使用。
1.1.1在XML中配置profile
也可以通过<beans>
元素的profile属性,在XML中配置profile bean。
1.2激活profile
Spring在确定哪个profile处于激活状态时,需要依赖两个独立的属性:spring.profiles.active和spring.profiles.default。如果设置了spring.profiles.active属性的话,它的值就会用来确定哪个profile是激活的。如果没有设置spring.profiles.active,就会查找spring.profiles.default的值。
有多种方式来设置这两个属性:
- 作为DispatcherServlet的初始化参数;
- 作为Web应用的上下文参数
- 作为JNDI条目
- 作为环境变量
- 作为JVM的系统属性
- 在集成测试类上,使用@ActiveProfiles注解设置
1.2.1使用profile进行测试
Spring提供了@ActiveProfiles注解,使用它来指定运行时要激活哪个profile。
2、条件化的bean
Spring4引入了一个新的@Conditional注解,用到带有@Bean注解的方法上,可以实现条件化的配置。如果给定的条件计算结果为true,就会创建这个bean,否则,这个bean会被忽略。
一个名为MagicBean的类,条件化地配置MagicBean,只有设置了magic环境属性的时候,Spring才会实例化这个类
@Bean
@Conditional(MagicExistsCondition.class)
public MagicBean magicBean(){
return new MagicBean();
}
@Conditional中给定了一个Class,它指明了条件。@Conditional将会通过Condition接口条件对比:
public interface Condition {
boolean matches(ConditionContext ctxt,
AnnotatedTypeMetadata metadata);
}
设置给@Conditional的类可以是任意实现了Condition接口的类型。这个接口看起来很简单,只需提供matches()方法的实现即可。如果matches()方法返回true,那么就会创建带有@Conditional注解的bean。如果matches方法返回false,将不会创建这些bean。
创建Condition的实现并根据环境中是否存在magic属性来做出决策:
public class MagicExistsCondition implements Condition {
public boolean matches(ConditionContext ctxt, AnnotatedTypeMetadata metadata) {
Environment environment = ctxt.getEnvironment();
return environment.containsProperty("magic");
}
}
matches()方法根据ConditionContext和AnnotatedTypeMetadata对象来做出决策。
ConditionContext是一个接口:
public interface ConditionContext {
BeanDefinitionRegistry getRegistry();
ConfigurableListableBeanFactory getBeanFactory();
Environment getEnvironment();
ResourceLoader getResourceLoader();
ClassLoader getClassLoader();
}
通过ConditionContext,可以做到如下几点:
- 借助getRegistry()返回的BeanDefinitionRegistry检查bean定义;
- 借助getBeanFactory()返回的ConfigurableListableBeanFactory检查bean是否存在,甚至探测bean的属性
- 借助getEnvironment()返回的Environment检查环境变量是否存在以及它的值是什么
- 读取并探查getResourceLoader()返回的ResourceLoader所加载的资源
- 借助getClassLoader()返回的ClassLoader加载并检查类是否存在。
AnnotatedTypeMetadata接口则能够让我们检查带有@Bean注解的方法上还有什么其他的注解:
public interface AnnotatedTypeMetadata {
MergedAnnotations getAnnotations();
default boolean isAnnotated(String annotationName) {
return this.getAnnotations().isPresent(annotationName);
}
@Nullable
default Map<String, Object> getAnnotationAttributes(String annotationName) {
return this.getAnnotationAttributes(annotationName, false);
}
@Nullable
default Map<String, Object> getAnnotationAttributes(String annotationName, boolean classValuesAsString) {
MergedAnnotation<Annotation> annotation = this.getAnnotations().get(annotationName, (Predicate)null, MergedAnnotationSelectors.firstDirectlyDeclared());
return !annotation.isPresent() ? null : annotation.asAnnotationAttributes(Adapt.values(classValuesAsString, true));
}
@Nullable
default MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationName) {
return this.getAllAnnotationAttributes(annotationName, false);
}
@Nullable
default MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationName, boolean classValuesAsString) {
Adapt[] adaptations = Adapt.values(classValuesAsString, true);
return (MultiValueMap)this.getAnnotations().stream(annotationName).filter(MergedAnnotationPredicates.unique(MergedAnnotation::getMetaTypes)).map(MergedAnnotation::withNonMergedAttributes).collect(MergedAnnotationCollectors.toMultiValueMap((map) -> {
return map.isEmpty() ? null : map;
}, adaptations));
}
}
借助isAnnotated()方法,能够判断带有@Bean注解的方法是不是还有其他特定的注解。借助其他的那些方法,可以检查@Bean注解的方法上其他注解的属性。
@Profile注解基于@Conditional和Condition实现
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ProfileCondition.class)
public @interface Profile {
/**
* The set of profiles for which the annotated component should be registered.
*/
String[] value();
}
@Profile本身也使用了@Conditional注解,并且引用了ProfileCondition作为Condition实现。
class ProfileCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
if (attrs != null) {
for (Object value : attrs.get("value")) {
if (context.getEnvironment().acceptsProfiles(Profiles.of((String[]) value))) {
return true;
}
}
return false;
}
return true;
}
}
ProfileCondition通过AnnotatedTypeMetadata得到了用于@Profile注解的所有属性。借助该属性,它会明确地检查value属性,该属性包含了bean的profile名称。然后根据通过ConditionContext得到的Environment来检查【借助acceptsProfiles()方法】该profile是否处于激活状态。
3、处理自动装配的歧义性
仅有一个bean匹配所需的结果时,自动装配才是有效的。如果不仅有一个bean能够匹配结果的话,这种歧义会阻碍Spring自动装配属性、构造器参数或方法参数。
@Autowired
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
Dessert是一个接口,并且有三个实现了这个接口:
@Component
public class Cake implements Dessert {...}
@Component
public class Cookies implements Dessert {...}
@Component
public class IceCream implements Dessert {...}
3.1标示首选的bean
在声明bean的时候,通过将其中一个可选的bean设置为首选(primary)bean能够避免自动装配时的歧义性。当遇到歧义性的时候,Spring将会使用首选的bean,而不是其他可选的bean。
将@Component注解的Icecream bean声明为首选的bean:
@Primary
@Component
public class IceCream implements Dessert {...}
可以通过Java配置显示地声明IceCream
@Bean
@Primary
public Dessert iceCream(){
return new IceCream();
}
使用XML配置bean的话,
<bean>
元素有一个primary属性用来指定首选的bean:
<bean id="iceCream" class="enjoy.IceCream"
primary="true"/>
但是,如果标示了两个或者多个首选bean,那么它就无法正常工作了。
3.2限定自动装配的bean
限定符是一种更为强大的机制。
设置首选bean的局限性在于@Primary无法将可选方案的范围限定到唯一一个无歧义的选项中。它只能标示一个优先的可选方案。
Spring的限定符能够在所有可选的bean上进行缩小范围的操作,最终能够达到只有一个bean满足所规定的限制条件。如果将所有的限定符都用上后依然存在歧义性,那么可以继续使用更多的限定符来缩小选择范围。
@Qualifier注解是使用限定符的主要方式。
@Autowired
@Qualifier("iceCream")
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
为@Qualifier注解所设置的参数就是想要注入的bean的ID。所有使用@Component注解声明的类都会创建为bean,并且bean的ID为首字母变为小写的类名。
但是setDessert()方法上所指定的限定符与要注入的bean的名称是紧耦合的。对类名称的任意改动都会导致限定符失败。
3.2.1创建自定义的限定符
可以为bean设置自己的限定符,而不是依赖于将bean ID作为限定符。
在bean声明上添加@Qualifier注解。它可以与@Component组合使用
@Component
@Qualifier("cold")
public class IceCream implements Dessert {...}
因为它没有耦合类名,因此可以随意重构IceCream的类名,而不必担心会破坏自动装配。在注入的地方,只要引用cold限定符就可以了:
@Autowired
@Qualifier("cold")
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
当通过Java配Hi显示定义bean的时候,@Qualifier也可以与@Bean注解一起使用
@Bean
@Qualifier("cold")
public Dessert iceCream(){
return new IceCream();
}
3.2.2使用自定义的限定符注解
如果多个bean都具备相同特性的话,可以创建自定义的限定符注解,借助这样的注解来表达bean所希望限定的特性。
创建一个注解,它本身要使用@Qualifier注解来标注。
@Target({ElementType.CONSTRUCTOR,ElementType.FIELD, ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Cold {}
4、bean的作用域
在默认情况下,Spring应用上下文中所有bean搜时作为单例的形式创建的。如果所使用的类是易变的,会保持一些状态,重用是不安全的。
Spring定义了多种作用域,可以基于这些作用域创建bean:
- 单例(Singleton):在整个应用中,只创建bean的一个实例
- 原型(Prototype):每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的bean实例
- 会话(Session):在Web应用中,为每个会话创建一个bean实例
- 请求(Request):在Web应用中,为每个请求创建一个bean实例
单例是默认的作用域,但是正如之前所述,对于易变的类型,这并不合适。如果选择其他的作用域,要使用@Scope注解,它可以与@Component或@Bean一起使用。
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class NotePad {...}
4.1使用会话和请求作用域
就购物车bean来说,会话作用域是最为合适的,因为它与给定的用户关联性最大。
@Component
@Scope(
value = WebApplicationContext.SCOPE_SESSION,
proxyMode = ScopedProxyMode.INTERFACES
)
public class ShoppingCart{...}
将ShoppingCart bean注入到单例StoreService bean的Setter方法中
@Component
public class StoreService {
ShoppingCart shoppingCart;
@Autowired
public void setShoppingCart(ShoppingCart shoppingCart) {
this.shoppingCart = shoppingCart;
}
}
ShoppingCart bean是会话作用域的,此时并不存在。知道某个用户进入系统,创建会话了之后,才会出现ShoppingCart实例。
Spring并不会将实际的ShoppingCart bean注入到StoreService中,Spring会注入一个到ShoppingCart bean的代理。这个代理会暴露与ShoppingCart相同的方法,所以StoreService会认为它就是一个购物车。当StoreService调用ShoppingCart的方法时,代理会对其进行懒解析并将调用委托给会话作用域内真正的ShoppingCart bean。
proxyMode属性被设置为ScopedProxyMode.INTERFACES,表明这个代理要实现ShoppingCart接口,并将调用委托给实现bean。
Spring必须使用CGLib来生成基于类的代理。如果bean类型是具体类的化,必须要将proxyMode属性设置为ScopedProxyMode.TARGET_CLASS,以此来表明要以生成目标类扩展的方式创建代理。
请求作用域的bean会面临相同的装配问题,请求作用域的bean应该也以作用域代理的方式进行注入。
4.2在XML中声明作用域代理
如果需要使用XML来声明会话或请求作用域的bean,那么就不能使用@Scope注解及其proxyMode属性了。<bean>
元素的scope属性能够设置bean的作用域,要设置代理模式,需要使用Spring aop命名空间的一个新元素:
<bean id="cart" class="enjoy.ShoppingCart"
scope="session">
<aop:scoped-proxy/>
</bean>
aop:scoped-proxy/是与@Scope注解的proxyMode属性功能相同的Spring XML配置元素。它会告诉Spring为bean创建一个作用域代理。默认情况下,它会使用CGLib创建目标类的代理。也可以将proxy-target-class属性设置为false,进而要求它生成基于接口的代理。
5、运行时值注入
Spring提供了两种在运行时求值的方式:
- 属性占位符
- Spring表达式语言(SpEL)
5.1注入外部的值
在Spring中,处理外部值的最简单方式就是声明属性源并通过Spring的Environment来检索属性。
@Configuration
@PropertySource("classpath:app.properties")
public class ExpressiveConfig {
@Autowired
Environment env;
@Bean
public BlankDisc disc(){
return new BlankDisc(
env.getProperty("disc.title"),//检查属性值
env.getProperty("disc.artist")
);
}
}
在本例中,@PropertySource引用了类路径中一个名为app.properties的文件,文件内容大致为:
disc.title=Sgt. Peppers lonely hearts
disc.artist=The Beatles
这个属性文件会加载到Spring的Environment中,稍后可以从这里检索属性。在disc()方法中,会创建一个新的BlankDisc,它的构造器参数是从属性文件中获取的。
5.1.1深入学习Spring的Environment
getProperty()方法并不是获取属性值的唯一方法,getProperty()方法有四个重载的变种形式:
- String getProperty(String key)
- String getProperty(String key, String defaultValue)
- T getProperty(String key, Class type)
- T getProperty(String key,Class type, T defaultValue)
如果在使用getProperty()方法的时候没有指定默认值,并且这个属性没有定义的话,获取到的值是null。如果希望这个属性必须要定义,那么可以使用getRequiredProperty()方法。
如果想检查一下某个属性是否存在的话,可以调用containsProperty()方法。
如果想将属性解析为类的话,可以使用getPropertyAsClass()方法。
直接从Environment中检索属性是非常方便的,尤其是在Java配置中装配bean的时候。但是,Spring也提供了通过占位符装配属性的方法,这些占位符的值会来源于一个属性源。
5.1.2解析属性占位符
Spring一直支持将属性定义到外部的属性的文件中,并使用占位符值将其插入到Spring bean中。
在Spring装配中,占位符的形式为使用"${…}"包装的属性。
<context:property-placeholder location="app.properties"/>
<bean id="sgtPeppers"
class="soundsystem.SgtPeppers"
c:title="${disc.title}"
c:artist="${disc.artist}"/>
按照这种方式,XML配置没有使用任何硬编码的值,它的值是从配置文件以外的一个源中解析得到的。
如果我们依赖于组件扫描和自动装配来创建和初始化应用组件的话,那么就没有指定占位符的配置文件或类了。可以使用@Value注解:
public SgtPeppers(@Value("${disc.title}") String title, @Value("${disc.title}")String artist) {
this.title = title;
this.artist = artist;
}
为了使用占位符,必须配置一个PropertySourcesPlaceholderConfigurer bean,因为它能够基于Spring Environment及其属性源来解析占位符。
@Bean
public PropertySourcesPlaceholderConfigurer placeholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
在XML配置文件中,<context:property-placeholder>
元素将会生成PropertySourcesPlaceholderConfigurer bean。
解析外部属性能够将值的处理推迟到运行时,但是它的关注点在于根据名称解析来自于Spring Environment和属性源的属性。
5.2使用Spring表达式语言进行装配
Spring3引入了Spring表达式语言(Spring Expression Language,SpEL),它能够以一种强大和简洁的方式将值装配到bean属性和构造器参数中,在这个过程中所使用的表达式会在运行时计算得到值。
SpEL拥有很多特性,包括:
- 使用bean的ID来引用bean;
- 调用方法和访问对象的属性
- 对值进行算术、关系和逻辑运算
- 正则表达式匹配
- 集合操作
5.2.1SpEL样例
SpEL表达式要放到"#{…}"之中。
#{1}:数字常量表达式
#{T(System).currentTimeMillis()}:T()表达式会将java.lang.System视为Java中对应的类型,因此可以调用其static修饰的currentTimeMillis()方法
#{sgtPeppers.artist}:引用其他的bean或其他bean的属性
#{systemProperties['disc.title']}:通过systemProperties对象引用系统属性
5.2.2表示字面值
可以用来标示浮点数、String值以及Boolean值
5.2.3引用bean、属性和方法
#{sgtPeppers}:将一个bean装配到另一个 bean
#{sgtPeppers.artist}:引用sgtPeppers的artist属性
#{artistSelector.selectArtist()}:调用bean上的方法
#{artistSelector.selectArtist()?.toUpperCase()}:使用类型安全的运算符"?."这个运算符能够在访问它右边的内容之前,确保它所对应的元素不是null
5.2.4在表达式中使用类型
如果要在SpEl中访问类的作用域的方法和常量的话,要依赖T()这个关键的运算符。T()元素安抚的真正价值在于它能够访问目标类型的静态方法和常量。
5.2.5SpEL运算符
SpEl提供了多个运算符,包括算术运算符、比较运算符、逻辑运算符、条件运算符和正则表达式。
三元运算符的一个常见场景就是检查null,并用一个默认值来替代null:
#{disc.title ?: ‘Rattle and Hum’}
5.2.6计算正则表达式
SpEL通过matches运算符支持表达式中的模式匹配。matches的运算结果会返回一个Boolean类型的值。
5.2.7计算集合
#{jukebox.songs[4].title}
SpEL提供查询运算符(.?[]),对集合进行过滤,得到集合的一个子集
.^[]和.$[]
分别用来在集合中查询第一个匹配项和最后一个匹配项。
.![]投影运算符,从集合的每个成员中选择特定的属性放在另外一个集合中。