一、环境与profile
开发阶段中,某些环境相关做法可能并不适合迁移到生产环境中,甚至即便迁移过去也无法正常工作。数据库配置、加密算法以及与外部系统的集成是跨环境部署时会发生变化的几个典型例子。
Spring并不是在构建的时候做出这样的决策,而是等到运行时再来确定。在这个过程中需要根据环境决定该创建哪个bean和不创建哪个bean。这样的结果就是同一个部署单元(可能会是WAR文件)能够适用于所有的环境,没有必要进行重新构建。
Spring引入了bean profile的功能。要使用profile,你首先要将所有不同的bean定义整理到一个或多个profile之中,在将应用部署到每个环境时,要确保对应的profile处于激活(active)的状态。
在Java配置中,可以使用@Profile注解指定某个bean属于哪一个profile。
1.1Javaconfig中配置
1 package com.wang.three; 2 3 import javax.sql.DataSource; 4 5 import org.springframework.context.annotation.Bean; 6 import org.springframework.context.annotation.Configuration; 7 import org.springframework.context.annotation.Profile; 8 import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; 9 import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; 10 11 @Configuration 12 @Profile("dev") 13 public class DevelopmentProfileConfig { 14 @Bean(destroyMethod="shutdown") 15 public DataSource dataSource(){ 16 return new EmbeddedDatabaseBuilder() 17 .setType(EmbeddedDatabaseType.H2) 18 .addScript("classpath:schema.sql") 19 .addScript("classpath:test-data.sql") 20 .build(); 21 } 22 23 }
注释:@Profile注解应用在了类级别上。它会告诉Spring这个配置类中的bean只有在dev profile激活时才会创建。如果dev profile没有激活的话,那么带有@Bean注解的方法都会被忽略掉。
Spring 3.2开始,你也可以在方法级别上使用@Profile注解,与@Bean注解一同使用。可以在一个JavaConfig中配置多个profile
1 package com.wang.three; 2 3 import javax.sql.DataSource; 4 5 import org.springframework.context.annotation.Bean; 6 import org.springframework.context.annotation.Configuration; 7 import org.springframework.context.annotation.Profile; 8 import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; 9 import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; 10 import org.springframework.jndi.JndiObjectFactoryBean; 11 12 @Configuration 13 public class DatasourceConfig { 14 15 @Bean(destroyMethod="shutdown") 16 @Profile("dev") 17 public DataSource DevelopmentDataSource(){ 18 return new EmbeddedDatabaseBuilder() 19 .setType(EmbeddedDatabaseType.H2) 20 .addScript("classpath:schema.sql") 21 .addScript("classpath:test-data.sql") 22 .build(); 23 } 24 25 @Bean 26 @Profile("prod") 27 public DataSource ProductionDataSource(){ 28 JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean(); 29 jndiObjectFactoryBean.setJndiName("jdbc/myDs"); 30 jndiObjectFactoryBean.setResourceRef(true); 31 jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class); 32 return (DataSource)jndiObjectFactoryBean.getObject(); 33 } 34 }
尽管每个DataSource bean都被声明在一个profile中,并且只有当规定的profile激活时,相应的bean才会被创建,但是可能会有其他的bean并没有声明在一个给定的profile范围内。没有指定profile的bean始终都会被创建,与激活哪个profile没有关系。
1.2xml中配置
所有的配置文件都会放到部署单元之中(如WAR文件),但是只有profile属性与当前激活profile相匹配的配置文件才会被用到。
profile="dev"> 也可以新键其他的配置文件
<?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"> <jdbc:script location="classpath:schema.sql"/> <jdbc:script location="classpath:test-data.sql"/> </jdbc:embedded-database> </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: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>
有两个bean,类型都是javax.sql.DataSource,并且ID都是dataSource。但是在运行时,只会创建一个bean,这取决于处于激活状态的是哪个profile
1.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。
使用DispatcherServlet的参数将spring.profiles.default设置为开发环境的profile,我会在Servlet上下文中进行设置(为了兼顾到ContextLoaderListener)。
当应用程序部署到QA、生产或其他环境之中时,负责部署的人根据情况使用系统属性、环境变量或JNDI设置spring.profiles.active即可。
在spring.profiles.active和spring.profiles.default中,profile使用的都是复数形式。这意味着你可以同时激活多个profile,这可以通过列出多个profile名称,并以逗号分隔来实现。
Spring提供了@ActiveProfiles注解,我们可以使用它来指定运行测试时要激活哪个profile。
(@ActiveProfiles("dev"))
二、条件化的bean
Spring 4引入了一个新的@Conditional注解,它可以用到带有@Bean注解的方法上。如果给定的条件计算结果为true,就会创建这个bean,否则的话,这个bean会被忽略。
举例:假设有一个名为MagicBean的类,我们希望只有设置了magic环境属性的时候,Spring才会实例化这个类。如果环境中没有这个属性,那么MagicBean将会被忽略。
//MagicBean package com.wang.three;
public class MagicBean { }
使用@Conditional注解条件化地配置了MagicBean。
package com.wang.three; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; @Configuration public class MagicConfig { @Conditional(MagicExistsCondition.class) public MagicBean magicBean(){ return new MagicBean(); } }
设置给@Conditional的类(例如MagicExistsCondition)可以是任意实现了Condition接口的类型(这个接口是Spring中的接口)。
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) { // TODO Auto-generated method stub Environment env = context.getEnvironment(); return env.containsProperty("magic"); } }
这个接口实现起来很简单直接,只需提供matches()方法的实现即可。如果matches()方法返回true,那么就会创建带有@Conditional注解的bean。如果matches()方法返回false,将不会创建这些bean。
它通过给定的ConditionContext对象进而得到Environment对象,并使用这个对象检查环境中是否存在名为magic的环境属性。只要属性存在即可满足要求。如果满足这个条件的话,matches()方法就会返回true。所带来的结果就是条件能够得到满足,所有@Conditional注解上引用MagicExistsCondition的bean都会被创建。matches()方法会得到ConditionContext和AnnotatedTypeMetadata对象用来做出决策。
ConditionContext是一个接口:
通过ConditionContext,我们可以做到如下几点:
1、借助getRegistry()返回的BeanDefinitionRegistry检查bean定义;
2、借助getBeanFactory()返回的ConfigurableListableBeanFactory检查bean是否存在,甚至探查bean的属性;
3、借助getEnvironment()返回的Environment检查环境变量是否存在以及它的值是什么;
4、读取并探查getResourceLoader()返回的ResourceLoader所加载的资源;
5、借助getClassLoader()返回的ClassLoader加载并检查类是否存在。
AnnotatedTypeMetadata也是一个接口,能够让我们检查带有@Bean注解的方法上还有什么其他的注解。
借助isAnnotated()方法,我们能够判断带有@Bean注解的方法是不是还有其他特定的注解。借助其他的那些方法,我们能够检查@Bean注解的方法上其他注解的属性。
@Profile注解是基于@Conditional和Condition实现。
三、处理自动装配bean产生的歧义
自动装配歧义的产生:
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)。
解决方案:
3.1首选的bean
在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协同使用。
3.2.1 使用默认的限定符:如果没有指定其他的限定符的话,所有的bean都会给定一个默认的限定符,这个限定符与bean的ID相同。
@Autowired @Qualifier("iceCream") public Dessert setDessert(Dessert dessert){ this.dessert = dessert; }
3.2.2创建自定义的限定符
自定义了限制符的名称
@Component @Qualifier("cold") public class IceCream implements Dessert { }
进行引用
@Autowired @Qualifier("cold") public Dessert setDessert(Dessert dessert){ this.dessert= dessert; }
注意:当使用自定义的@Qualifier值时,最佳实践是为bean选择特征性或描述性的术语,而不是使用随意的名字。
3.2.3自定义的限定符注解
如果多个bean都具备相同特性的话,这种做法也会出现重匹配的问题。(可以用多组描述区分)但是由于Java中不容许相同注解的出现,故需要自定义注解
创建自定义的限定符注解,借助这样的注解来表达bean所希望限定的特性。这里所需要做的就是创建一个注解,它本身要使用@Qualifier注解来标注。这样我们将不再使用@Qualifier("cold"),而是使用自定义的@Cold注解。其用法和@Qualifier一样
@Target({ElementType.TYPE,ElementType.METHOD,ElementType.FIELD,ElementType.CONSTRUCTOR}) @Retention(RetentionPolicy.RUNTIME) @Qualifier public @interface Cold { }
四、bean的作用域
Spring定义了多种作用域,可以基于这些作用域创建bean,包括:
单例(Singleton):在整个应用中,只创建bean的一个实例。
原型(Prototype):每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的bean实例。
会话(Session):在Web应用中,为每个会话创建一个bean实例。
请求(Rquest):在Web应用中,为每个请求创建一个bean实例。
在默认情况下,Spring应用上下文中所有bean都是作为以单例(singleton)的形式创建的。也就是说,不管给定的一个bean被注入到其他bean多少次,每次所注入的都是同一个实例。
如果选择其他的作用域,要使用@Scope注解,它可以与@Component或@Bean一起使用。
4.1原型
使用组件扫描来发现和声明bean,那么你可以在bean的类上使用@Scope注解,将其声明为原型bean:
import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; @Component @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)//或为(@Scope("prototype")) public class Notepad { // the details of this class are inconsequential to this example }
想在Java配置中将Notepad声明为原型bean,那么可以组合使用@Scope和@Bean来指定所需的作用域:
@Bean @Scope("prototype") public NodePad nodePad(){ return new NodePad(); }
使用XML来配置bean的话,可以使用<bean>元素的scope属性来设置作用域:
4.2会话与请求域
就购物车bean来说,会话作用域是最为合适的,因为它与给定的用户关联性最大。
将value设置成了WebApplicationContext中的SCOPE_SESSION常量(它的值是session)。这会告诉Spring为Web应用中的每个会话创建一个ShoppingCart。这会创建多个ShoppingCart bean的实例,但是对于给定的会话只会创建一个实例,在当前会话相关的操作中,这个bean实际上相当于单例的。
对于:proxyMode属性,它被设置成了ScopedProxyMode.INTERFACES。这个属性解决了将会话或请求作用域的bean注入到单例bean中所遇到的问题。
例如当把其注入到一个单例中
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,以此来表明要以生成目标类扩展的方式创建代理。
请求作用域与之类似。
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.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <bean id="cart" class="com.wang.three.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.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <bean id="cart" class="com.wang.three.ShoppingCart" scope="session"> <aop:scoped-proxy proxy-target-class="false"/> </bean> </beans>
五、运行时植入
当望避免硬编码值,而是想让这些值在运行时再确定。Spring提供了两种在运行时求值的方式:
属性占位符(Property placeholder)。
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")); } }
5.1.1学习Spring的Environment
Spring中使用getProperty(),有四个重载
a、String getProperty(String key)
b、String getProperty(String key,String defaultValue)
c、T getProperty(String key,Class<T> type)
d、T getProperty(String key,Class<T> type, T defaultValue)
使用a、b都会返回的是一个String类型的值,但是b会在指定的属性key不存在的时候返回一个默认的值。
而对于它们不会将所有的值都视为String类型,而是根据type来确定。
int connectionCount = env.getProperty("db.connection.count",Interger.class,30);
如果disc.title或disc.artist属性没有定义的话且没有设置默认值,将会抛出IllegalStateException异常。
a、可以调用Environment的containsProperty()方法:检查一下某个属性是否存在的话。
boolean titleExists = env.containsProperty("disc.title");
b、将属性解析为类
自定义的CompactDisc的类
Class<CompactDisc> cdClass = env.getPropertyAsClass("disc.class", CompactDisc.class);
c、检验哪些profile处于激活状态
String[] getActiveProfiles():返回激活profile名称的数组;
String[] getDefaultProfiles():返回默认profile名称的数组;
boolean acceptsProfiles(String... profiles):如environment支持给定profile的话,就返回true。
(实际的例子没有写,详情看书91页和78页)
5.1.2占位符
Spring一直支持将属性定义到外部的属性的文件中,并使用占位符值将其插入到Spring bean中。在Spring装配中,占位符的形式为使用“${... }”包装的属性名称。
作为样例,我们可以在XML中按照如下的方式解析BlankDisc构造器参数:
<bean id="sgtPeppers" class="com.wang.second.BlankDisc" c:_title="${disc.title}" c:_artist="${disc.artist}" />
依赖于组件扫描和自动装配来创建和初始化应用组件的话,那么就没有指定占位符的配置文件或类了。在这种情况下,我们可以使用@Value注解。
public BlankDisc(@Value("${disc.title}") String title,@Value("${disc.artist}")String artist){ this.title=title; this.artist=artist; }
解析配置的占位符:
为了使用占位符,我们必须要配置一个PropertyPlaceholderConfigurer bean或PropertySourcesPlaceholderConfigurer bean。
推荐使用PropertySourcesPlaceholderConfigurer,因为它能够基于Spring Environment及其属性源来解析占位符。
使用@bean方法在Java中配置了PropertySourcesPlaceholderConfigurer
@Bean public static PropertySourcesPlaceholderConfigurer placeholderConfigurer(){ return new PropertySourcesPlaceholderConfigurer(); }
在xml中也可以配置,Spring context命名空间中的<context:propertyplaceholder>元素将会为你生成PropertySourcesPlaceholderConfigurer bean:
总之:解析外部属性能够将值的处理推迟到运行时,但是它的关注点在于根据名称解析来自于Spring Environment和属性源的属性。
5.2强大的SpEL(Spring 表达式语言)
可以实现外部注入等等,能够应用在DI之外的其他地方
简介:以一种强大和简洁的方式将值装配到bean属性和构造器参数中,在这个过程中所使用的表达式会在运行时计算得到值
SpEL拥有很多特性,包括:
使用bean的ID来引用bean;
调用方法和访问对象的属性;
对值进行算术、关系和逻辑运算;
正则表达式匹配;
集合操作
a、SpEL表达式要放到“#{ ... }”之中。
b、T()表达式
如果要在SpEL中访问类作用域的方法和常量的话,要依赖T()这个关键的运算符。
#{T(Syste).currentTimeMillis()}//会将java.lang.System视为Java中对应的类型
c、systemProperties对象引用系统属性
如:#{systemProperties('disc.title')}
类似属性占位符的例子,组件扫面创建bean
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表达式设置的:
d、字面值
整数、浮点数、String值和Boolean
#{1}、#{1.2}、#{9.87E3}、#{true}
f、引用bean、属性和方法
通过ID引用其他的bean 例:#{SgtPeppers}
或其属性#{SgtPeppers.artist}
或调用bean中方法,对于被调用方法的返回值来说,我们同样可以调用它的方法 #{artSelect.selectArtist().toUpperCase()} 。
为了安全起见:使用了“?.”运算符。这个运算符能够在访问它右边的内容之前,确保它所对应的元素不是null。
#{artSelect.selectArtist()?.toUpperCase()}
e、SqEL运算符
运算符类型 | 运算符 |
算数运算符 | +、-、 * 、/、%、^ |
比较运算符 | < 、 > 、 == 、 <= 、 >= 、 lt 、 gt 、 eq 、 le 、 ge |
逻辑运算符 | and 、 or 、 not 、│ |
条件运算符 | ?: (ternary) 、 ?: (Elvis) |
正则表达式 | matches |
三元运算符的一个常见场景就是检查null值,并用一个默认值来替代null。例如,如下的表达式会判断disc.title的值是不是null,如果是null的话,那么表达式的计算结果就会是“Rattleand Hum”:
f、正则表达式
g、计算集合
要从jukebox中随机选择一首歌:
取字符串的字符
查询运算符(.?[]),它会用来对集合进行过滤,得到集合的一个子集
//希望得到jukebox中artist属性为Aerosmith的所有歌曲
“.^[]”和“.$[]”,它们分别用来在集合中查询第一个匹配项和最后一个匹配项。
投影运算符(.![]),它会从集合的每个成员中选择特定的属性放到另外一个集合中。
//将title属性投影到一个新的String类型的集合中