高级装配
环境与profile
在开发软件的时候,将应用程序从开发环境,迁移到测试环境,或者是迁移到生产环境都是一项挑战。最常见的就是对于DataSource的配置,这三种环境我们会根据不同的策略来生成DataSource bean。我们需要有一种方式来配置DataSource,使其在每种环境下都会选择对应的配置。
配置profile bean
要使用profile,首先要将所有不同的bean定义整理到一个或多个profile之中,在将应用部署到不同环境中,要确保对应的profile文件处于激活状态。
在Java配置中,使用@Profile注解指定某个bean属于哪一个profile。
import javax.sql.DataSource; import org.springframework.context.annotation.Bean; 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; @Configuration @Profile("dev") public class DevelopProfileConfig { @Bean(destroyMethod="shutdown") public DataSource dataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.H2) .addScript("classpath:scheam.sql") .addScripts("classpath:test-data.sql") .build(); } }
在这里@profile注解是应用在类级别了。它告诉Spring这个配置类中的bean只有在dev profile 激活时在会创建。如果dev profile没有激活的话,那么这个配置类下的bean会被忽略。
如果我们为每一个配置环境都去配置一个配置类,那无疑会增加配置类的个数,不利于维护。从Spring3.2开始,就可以在方法级别上使用@profile注解,与@Bean注解一同使用。这样的话,我们就可以将多个bean放在一个配置类中了。
import javax.sql.DataSource; import org.springframework.context.annotation.Bean; 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 org.springframework.jndi.JndiObjectFactoryBean; @Configuration public class DataSourceConfig { @Bean(destroyMethod="shutdown") @Profile("dev") public DataSource embeddedDataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.H2) .addScript("classpath:scheam.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才会被创建,但是可能会有其他的bean并没有声明在一个给定的profile范围内。没有指定profile的bean始终会被创建,与激活哪个profile无关。
在XML配置中,我们可以使用<beans>元素的profile属性,在XML中配置profile bean。
在这里使用beans元素中嵌套定义beans元素的方式,而不是为每个环境都创建一个profile XML文件,这样将所有的profile bean定义在同一个XML文件中
profile.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:p="http://www.springframework.org/schema/p" xmlns:jee="http://www.springframework.org/schema/jee" xsi:schemaLocation="http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <beans profile="dev"> <jdbc:embedded-database id="datasource"> <jdbc:script location="classpath:scheam.sql"/> <jdbc:script location="classpath:test-data.sql"/> </jdbc:embedded-database> </beans> <beans profile="qa"> <bean id="datasource" class="org.apach.commons.dbcp.BesicDataSource" destroy-method="close" p:url="jdbc:h2:tcp://dbserver/~/test" p:driverClassName="org.h2.Driver" p:username="sa" p:password="password" p:initialSize="20" p:maxActive="30" /> </beans> <beans profile="prod"> <jee:jndi-lookup id="datasource" jndi-name="jdbc/myDatabase" resource-ref="true" proxy-interface="javax.sql.DataSource" ></jee:jndi-lookup> </beans> </beans>
激活profile
Spring在确定哪个profile处于激活状态时,需要依赖两个属性:spring.profiles.active和spring.profiles.default。如果设置了spring.profiles.active就会根据它的值确定哪个profile是激活的。但如果没有设置spring.profiles.active属性的话,Spring就会去找spring.profiles.default的值.如果这两个属性都没有设置,那就没有激活的profile。
可以有多种方式设置这两个属性:
- 作为DispatcherServlet的初始化参数
- 作为Web应用的上下文参数
- 作为JNDI条目
- 作为环境变量
- 作为JVM的系统属性
- 在集成测试类上,使用@ActiveProfiles注解设置
我在这里使用前两种方式,前两种方式可以直接在web.xml中设置:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5"> <servlet> <servlet-name>springDispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!-- <init-param> <param-name>spring-profiles.default</param-name> <param-value>dev</param-value> </init-param> --> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springDispatcherServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring/applicationContext.xml</param-value> </context-param> <context-param> <param-name>spring.profiles.defaule</param-name> <param-value>dev</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> </web-app>
当应用部署到其他环境时,我们根据情况再来设置spring.profiles.active即可.spring.profiles.active的设置优先使用。可以注意到spring.profiles.active和spring.profiles.default的profiles是复数形式,可以设置多个profile名称,并以逗号分隔,我们可以设置多个彼此不相关的profile。
使用profile进行测试
在运行集成测试的时候,Spring提供了@ActiveProfiles注解,我们可以死哦那个它来制定运行测试要激活哪个profile.
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes= {Config.class}) @ActiveProfiles("dev") public class PersistenceTest { ... }
条件化创建bean
Spring的profile机制是根据哪个profile处于激活状态来条件化初始bean的一种体现。Spring4中可以使用一种更为通用的机制来实现条件化bean的定义,在这种机制下,条件完全由自己决定。Spring4使用的是@Conditional注解定义条件化bean。
@Conditional注解可以用在@Bean注解的方法上。如果给定条件为true,就会创建这个bean,否则的话,这个bean会被忽略。
@Bean @Conditional(MagicConditon.class) public MagicBean magicBean() { return new MagicBean(); }
@conditional注解给定了一个Class,它指明了条件——在这里是MagicCondition(实现了Condition接口)。给定@conditional注解的类可以是任意实现了Condition接口的类型。
public interface Condition { boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata); }
这个接口比较简单,只有一个matches()方法作为实现,如果该方法返回true就会创建提供了@Conditional注解的bean,否则就不会创建.
matches()方法会得到ConditionContext和AnnotatedTypeMetadata两个入参。ConditionContext接口会有一些方法检查bean的定义或bean的属性,AnnotatedTypeMetadata则能够让我们检查带有@Bean注解的方法上还有其他注解,或是检查@Bean注解的方法上其他注解的属性.
在Spring4开始,@profile注解就是根据@Conditional和Condition实现的,我们可以参照它的实现。
处理自动装配的歧义性
使用Spring的自动封装来装配bean真的是太方便了,但是,这是有且只有一个bean匹配所需条件时,自动装配才有效。如果有多个bean匹配结果的话,这种歧义性会阻碍Spring自动装配。例如使用@Autowired注解标注Dessert接口
@Autowired private Dessert dessert;
但是这个接口有三个实现:
@Component public class Cake implements Dessert {} @Component public class Cookies implements Dessert {} @Component public class IceCream implements Dessert {}
这三个实现类都用@Component注解,当组件扫描的时候,它们都会在Spring上下文中创建bean。然后当Spring试图自动装配Dessert的时候,它没有唯一,无歧义的值。Spring就会抛出NoUniqueBeanDefinitonException。
其实在使用中,自动装配的歧义性并不常见,因为一般情况是给定的类型只有一个实现类,自动装配可以很好地运行。但是,如果真的出现歧义性,Spring提供了:可以将某一个可选bean设为首选(primary)的bean,或者使用限定(qualifier)来帮助Spring将可选的bean'范围缩小到只有一个bean.
标示首选bean
当声明bean的时候,通过将其中一个可选的bean设置为首选(primary)bean能够避免自动装配的歧义性。当出现歧义性的时候会直接使用首选的bean。Spring使用的正是@Primary注解,这个注解能够与@Component组合在组件扫描的bean上,也可以与@Bean组合用在Java配置的bean声明上。
@Component @Primary public class Cake implements Dessert {}
或者使用的是JavaConfig则如下:
@Bean @Primary public Dessert iceCream(){ return new IceCream(); }
如果使用XML配置bean的话,同样可以实现这样的功能。<bean>元素有一个primary属性来指定首选的bean:
<bean id="iceCream" class="cn.lynu.IceCream" primary="true"/>
但是,如果你标记了多个首选bean,那么又会带来新的歧义性,事实上也就没有了首选,Spring也无法正常工作。解决歧义性使用限定符是一种更为强大的机制
限定自动装配的bean
使用首选bean只能标示一个优先的选择方案,当首选bean数量超过一个,我们就没有其他方法进一步缩小范围。而且,我们每次使用该类型,都会自动使用首选bean,如果在某处不想使用就不好处理了。使用限定可以缩小可选bean的范围直到只有一个bean满足条件,如果还存在歧义性,那么你还可以继续使用更多的限定来进一步缩小范围。@Qualifier注解就是限定,它可以与@Autowired协同使用,在注入时指定想要注入的是哪个bean。
@Autowired @Qualifier("iceCream") private Dessert dessert;
我们已经知道了使用@Component注解声明的类,默认使用bean的ID是首字母小写的类名。因此@Qualifier("iceCream")指向的正是IceCream类的实例.这种情况是将限定与要注入的bean的名称是紧密耦合的,对类名的任何修改都会影响限定失败。这个时候,我们可以使用自定义的限定。
自定义的限定
我们可以自定义的设置限定,而不是依赖beanID作为限定,在这里@Qualifier可以与@Component或@Bean配合使用。
组件:
@Component @Qualifier("cold") public class IceCream implements Dessert {}
或是在JavaConfig方式中:
@Bean @Qualifier("cold") public Dessert iceCream() { return new IceCream(); }
这样我们在注入的时候就可以使用"cold"这个名称了:
@Autowired @Qualifier("cold") private Dessert dessert;
使用自定义的限定注解
当一个限定不能解决问题的时候,可以使用多个限定来缩小范围,也就是使用多个@Qualifier,但是在Java中不允许出现两个相同的注解(Java8可以,但也要求该注解本身实现@Repeatable注解,不过,Spring的@Qualifier没有实现),这个时候就需要我们创建一个注解了,该注解使用@Qualifier标注,就直接使用我们自定义的注解即可。
@Target({ElementType.FIELD,ElementType.METHOD,ElementType.CONSTRUCTOR,ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Qualifier public @interface Cold {}
表明bean限定的时候就可以用:
@Component @Clod public class IceCream implements Dessert {}
我们在注入的时候直接使用这个@Cold注解即可:
@Autowired @Cold private Dessert dessert;
我们可以自定义多个注解来进一步缩小范围
bean的作用域
在默认情况下,Spring应用上下文的bean都是作为单例的形式被创建,不管给定的bean被注入到其他的bean多少次,每次注入的都是同一个实例。但有时我们使用的类是易变的,这个时候使用单例就不太好了,因为对象会被修改。
Spring提供了多种作用域,可以基于这些作用域创建bean:
- 单例(singleton):这整个应用中只创建bean的一个实例
- 原型(Prototype):每次注入的时候都会创建一个新的bean的实例(多例)
- 会话(Session):在Web开发中,为每一个会话创建一个bean实例
- 请求(Request):在Web开发中,为每一个请求创建一个bean实例
需要选择除单例之外的作用域,要使用@Scope注解,它可以与@Component或@Bean一起使用。
@Component @Scope(value=ConfigurableBeanFactory.SCOPE_PROTOTYPE) public class Notepad {}
使用ConfigurableBeanFactory.SCOPE_PROTOTYPE常量比较安全,当也可以使用字面量的方式:@Scope("prototype")
如果使用JavaConfig的方式:
@Bean @Scop(value=ConfigurableBeanFacory.SCOPE_PROTOTYPE) public Notepad notepad(){ return Notepad(); }
如果使用XML的方式,可以在<bean>元素的scope属性设置:
<bean id="notepad" class="cn.lynu.Notepad" scope="prototype"/>
使用会话或请求作用域的bean
使用会话或请求作用域,还是用@Scope注解,它的使用方式与原型作用域大致相同:
@Component @Scope(value="session",proxyMode=ScopedProxyMode.TARGET_CLASS) public class ShoppingCart {}
Spring会在Web应用的每一个会话中创建一个ShoppingCart实例,但是在对每一个的会话操作中,这个bean实际相当于单例. 注意这里使用了proxyMode属性,这个属性可以解决将会话或请求bean注入到单例bean中所遇到的问题:例如将ShoppingCart bean注入到单例的StoreService bean
@Component public class StoreService{ @AutoWired private ShoppingCart shoppingCart; }
当扫描到StoreService 就会创建这个bean,并试图将ShoppingCart bean注入进去,但是ShoppingCart是会话作用域的,此时不存在,只有例如某个用户登录系统,创建会话之后,这个bean才存在;而且会话会有多个,ShoppingCart实例也会有多个,我们不想注入某个固定的实例,应该是当需要使用ShoppingCart实例恰好是当前会话中的那一个。
使用proxyMode就会先创建一个ShoppingCart bean的代理,将这个代理给StoreService ,当StoreService 真正使用的时候,代理会调用会话中真正的ShoppingCart bean。需要注意的是:如果ShoppingCart是一个接口,需要使用 ScopedProxyMode.INTERFACES JDK动态代理,但如果ShoppingCart是一个具体类,它必须使用CDLib来做代理,必须将proxyMode属性设置为ScopedProxyMode.TERGET_CLASS.表明要生成代理类的拓展类的方式创建代理。
在XML中声明作用域代理
如果使用XML的方式就不能使用@Scope注解及其proxyMode属性了。<bean>元素的scope属性能够设置bean的作用域,但是如何指定代理模式呢?
<bean id="cart" class="cn.lynu.ShoppingCart" scope="session"> <aop:scoped-proxy /> </bean>
默认情况下,它使用CGLib常见目标类的代理,但是我们也可以将proxy-target-class属性设置为false,就是用JDK的代理
<bean id="cart" class="cn.lynu.ShoppingCart" scope="session"> <aop:scoped-proxy proxy-target-class="false" /> </bean>
运行时值注入
有的时候,我们可能会希望避免硬编码,而且想让这些值在运行的时候再确定。为了实现这种功能,Spring提供了两种在运行时求值的方式:
- 属性占位符
- Spring表达式语言(SpEL)
注入外部的值
在Spring中,处理外部属性最简单的但是就是声明属性源并通过Spring的Environment来检索属性:
@Configuration @ComponentScan @PropertySource("classpath:app.properties") public class Config { @Autowired Environment environment; @Bean public BlankDisc disc() { return new BlankDisc(environment.getProperty("disc.title"), environment.getProperty("disc.artist")); } }
文件 app.properties 就是属性源,使用@propertySource注解声明,这个属性文件会加载到Spring的Environment,我们再注入Environment,就可以通过Environment的方法获得属性了。使用getPropperty()方法及其重载方法时,当属指定的属性不存在时,可以通过重载方法给一个默认值,如果没有默认值就会返回null。如果希望这个属性必须存在,那么可以使用getRequiredProperty()方法,Environment还有一些其他方法。
直接从Environment中获得属性的值,在JavaConfig的方式中非常方便。Spring还提供了占位符装配属性的方法,这些占位符的值来源于一个属性源。
使用属性占位符
Spring中可以使用属性占位符的方式将值插入到Spring bean中,在Spring装配中,占位符的形式为使用"${...}"包裹的属性名称。
<bean id="agtPeppers" class="cn.lynu.BlankDisc" c:_title="${disc.title}" c:_artist="${disc.artist}"/>
这样XML文件中没有任何的硬编码,属性的值都是从属性源中获得。如果我们使用的是组件扫描和自动装配,没有使用XML的话,在这种情况下,我们使用@Value注解:
@Component public class BlankDisc { private String title; private String artist; public BlankDisc() {} public BlankDisc(@Value("${disc.title}")String title, @Value("${disc.artist}")String artist) { this.title = title; this.artist = artist; } }
最后,为了属性占位符可以使用,我们还需要配置一个PropertySourcePlaceholdConfigurer,它可以根据Spring的Environment及其属性源来解析占位符。
@Bean public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() { return new PropertySourcesPlaceholderConfigurer(); }
如果使用XML,需要context名称空间的<context:property-placeholder>元素,这个元素会生成PropertySourcePlaceholdConfigurer bean:
<context:property-placeholder/>