本系列博客为spring In Action 这本书的学习笔记
本篇博文说是装配Bean的一些高级装配, 其中包括了环境与profile/条件化的Bean/处理自动装配的歧义性/Bean的作用域.
我们在开发软件的时候, 有一个很大的挑战就是我们要将应用程序从开发环境迁移到生产环境. 开发环境的一些做法可能并不适合生产环境, 甚至即便迁移过去也无法正常工作. 数据库配置/加密算法以及外部系统的集成是跨环境部署时回发生的及各例子.
下面以一个数据库的例子来分析一下这种情况.
现在我们需要创建一个dataSource Bean, 在开发环境中, 使用一个嵌入式数据库回大大提高开发效率, 减少dataSource的复杂性, 并且可以使得每次启动数据库时, 它都处于一个给定的状态. 比如:
程序1: 开发时期的DataSource Bean
@Bean(destroyMethod = "shutdown")
public DataSource dataSource(){
return new EmbeddedDatabaseBuilder()
.addScript("classpath:schema.sql")
.addScript("classpath:test-data.sql").build();
}
使用EmbeddedDatabaseBuilder回搭建一个嵌入式的Hypersonic数据库, 它的模式(schema)定义在schema.sql中, 测试数据则是通国test-data.sql加载的.
当我们在开发环境中运行集成测试或是启动应用进行手动测试的时候, 这个DataSource是非常有用的.
尽管嵌入式数据库创建的DataSource非常适合开发环境, 但是对于生产环境来说, 这是一个糟糕的选择. 在生产环境中, 我们尽可能的希望使用JNDI从容器中获取一个DataSource, 在这样的场景中, 下面的@Bean方法会更加适合.
程序2: 生产环境中的DataSource Bean
@Bean
public DataSource dataSource(){
JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
jndiObjectFactoryBean.setJndiName("jdbc/myDS");
jndiObjectFactoryBean.setResourceRef(true);
jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
return (DataSource)jndiObjectFactoryBean;
}
通过JNDI获取DataSource能够让容器决定该如何创建这个DataSource, 甚至包括切换为容器管理的连接池. 尽管如此, JNDI管理的DataSource更加适合于生产环境, 对于简单的集成和开发环境来说, 这会带来不必要的复杂性.
这里的dataSource()方法互不相同, 虽然它们都会生成一个类型为javax.sql.DataSource的Bean. 但是它们的相似点也仅限如此了. 两个方法使用了完全不同的策略来生成DataSource Bean.
不同方式生成的DataSource表现了在不同环境中某个Bean都会有所不同. 我们必须要有一种方法来配置DataSource, 使其在每种环境下都会选择最为合适的配置.
其中一种解决办法就是在单独的配置类(或者XML文件)中配置每个Bean, 然后在构建阶段(可能会使用到maven或者profiles)确定要哪一个配置编译到可部署的应用中. 这种方式的问题在于要为每种环境重新构建应用.
所幸Spring提供的解决方案并不需要重新构建.
面对不同环境有不同方法生成不同版本的Bean, Spring提供了一种策略, 就是在运行时确定创建哪个版本的Bean. 这样的结果就是同一个部署单元(可能会时WAR文件)能够适用于所有的环境, 没有必要再重建应用.
Spring提供了bean profile的功能. 要使用profile, 先要将当前所有版本的Bean定义整理到一个或多个profile之中, 在将应用部署到具体环境的时候, 只要使得对应的profile处于激活状态(active)就可以了.
在Java配置中, 可以使用@Profile注解指定某个Bean属于哪一个Profile. 例如, 在配置类中, 嵌入式数据库的DataBSource可能会配置成如下所示:
程序3: dev profile
@Configuration
@Profile("dev") //这个profile的ID为dev
public class DevelopmentProfileConfig {
@Bean(destroyMethod = "shutdown")
public DataSource dataSource(){
return new EmbeddedDatabaseBuilder()
.addScript("classpath:schema.sql")
.addScript("classpath:test-data.sql").build();
}
}
通过上面的代码我们可以注意到@Profile注解是应用在类级别的, 它会告诉Spring这个配置类中的Bean只有在dev Profile激活时才会被创建. 如果dev Profile没有被激活, 那么这个JavaConfig中所有带有@Bean注解的方法都会被忽略掉.
@Profile注解里面的参数dev是这个profile配置类的ID为dev.
同样的, 适用于生产环境的配置类如下:
程序4: prod profile
@Configuration
@Profile("prod")
public class ProductionProfileConfig {
@Bean
public DataSource dataSource(){
JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
jndiObjectFactoryBean.setJndiName("jdbc/myDS");
jndiObjectFactoryBean.setResourceRef(true);
jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
return (DataSource)jndiObjectFactoryBean;
}
}
除了可以使用上面基于类使用的@Profile注解, 从Spring3.2开始, 也支持在方法级别上使用@Profile了, 这样我们就可以将dev和prod写在一个JavaConfig中了.
程序5: @Profile注解基于激活的profile实现Bean的装配
@Configuration
public class DataSourceConfig {
@Bean(destroyMethod = "shutdown")
@Profile("dev")
public DataSource dataSource(){
return new EmbeddedDatabaseBuilder()
.addScript("classpath:schema.sql")
.addScript("classpath:test-data.sql").build();
}
@Bean
@Profile("prod")
public DataSource jndiSource(){
JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
jndiObjectFactoryBean.setJndiName("jdbc/myDS");
jndiObjectFactoryBean.setResourceRef(true);
jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
return (DataSource)jndiObjectFactoryBean;
}
}
这样, 我们在方法级别上使用@Profile注解, 这样就可以将多个profile放在一个JavaConfig中了.
需要注意的是, 尽管每个DataSource Bean都被声明在一个profile中并且只有当规定的profile激活时, 相应的Bean才会被创建. 但是可能有的Bean并没有指定profile, 这样的Bean始终都会被创建, 与激活哪个profile没有关系.
在JavaConfig中我们可以使用@Profile标注, 在XML中我们可以使用
例如, 为了在XML中定义适用于开发阶段的嵌入式数据库DataSource Bean, 我们可以创建如下的XML文件.
程序6: 在XML文件中配置profile Bean
<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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.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>
我们还可以像上面的JavaConfig一样, 将多个profile Bean声明在一个XML文件中.
程序7: 将多个profile Bean声明在一个XML文件中
<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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd 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">
<beans 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 profile="prod">
<jee:jndi-lookup id="datasource" jndi-name="jdbc/myDatabase"
resource-ref="true" proxy-interface="javax.sql.DataSource"/>
beans>
beans>
现在我们已经会通过各种方式配置prifile Bean了, 现在就来看一下怎样在不同环境下使用不同的profile Bean
Spring在确定哪个profile处于激活状态时, 需要依赖两个独立的属性: spring.profiles.active和spring.profiles.default.
如果设置了active属性的值, Spring就会激活active对应的profile Bean; 如果没有设置active的值, Spring就会查找default所对应的值并激活相应的profile Bean; 如果也没有设置default的值, 那么Spring就只激活那些没有定义在profile中的Bean.
关于设置着两个值有下面几种方式:
作者推荐的一种方式就是将default的值使用DispatcherServlet的参数设置为开发环境的profile, 然后将应用部署到相应环境的时候再使用系统属性/环境变量等设置active就可以了.
下面的例子示范了在web.xml中设置default的值:
程序8: 在web.xml中设置default的值
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<display-name>Archetype Created Web Applicationdisplay-name>
<welcome-file-list>
<welcome-file>index.jspwelcome-file>
welcome-file-list>
<context-param>
<param-name>ContextConfigLocationparam-name>
<param-value>devparam-value>
context-param>
<servlet>
<servlet-name>springMVCservlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class>
<init-param>
<param-name>contextConfigLocationparam-name>
<param-value>/WEB-INF/mvc-dispatcher-servlet.xmlparam-value>
init-param>
<init-param>
<param-name>spring.profiles.defaultparam-name>
<param-value>devparam-value>
init-param>
<load-on-startup>1load-on-startup>
<async-supported>trueasync-supported>
servlet>
<servlet-mapping>
<servlet-name>springMVCservlet-name>
<url-pattern>/url-pattern>
servlet-mapping>
<context-param>
<param-name>contextConfigLocationparam-name>
<param-value>/WEB-INF/applicationContext.xmlparam-value>
context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListenerlistener-class>
listener>
web-app>
上面的代码是现在的web.xml的全部配置, 其中的(1)(2)处就是我们进行默认profile的设置.
按照这种方式设置的default, 所有的开发人员都能从版本控制软件中获得应用程序源码, 并使用开发环境的设置(如嵌入式数据库)运行代码, 而不需要额外的配置.
之后当程序部署在相应环境之后, 再使用适当的方式激活相应的profile就可以了.
我们可以使用@ActiveProfiles注解在运行时候激活相对应的profile.
比如下面的代码片段:
程序9: 使用@ActiveProfile标注激活profile
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {PersistenceTestConfig.class})
@ActiveProfiles("dev")
public class ActiveProfile {
//...
}
有时候我们会面对这样的需求, 比如我们希望某个Bean在另一个Bean创建之后才会被创建, Spring为这种条件化的配置提供了一种方案: 使用@Conditional注解, 它可以用到带有@Bean注解的方法上, 如果给定的条件计算结果为true, 就会创建这个Bean, 否则的话, 这个Bean就会被忽略.
例如现在有一个MagicBean类, 我们希望只有设置了magic环境属性的时候, Spring才会实例化这个类, 如果环境中没有这个类, 那么MagicBean将会被忽略.
程序10: 条件化的配置Bean
@Configuration
public class MagicConfig {
@Bean
@Conditional(MagicExistsCondition.class)
public MagicBean magicBean(){
return new MagicBean();
}
}
设置给@Conditional标注的类必须要实现Condition接口, 关于Condition接口的代码如下:
public interface Condition{
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
}
}
程序11: MagicExistsCondition
public class MagicExistsCondition implements Condition {
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
Environment environment = conditionContext.getEnvironment();
return environment.containsProperty("magic");
}
}
在上面的程序中, matchs()方法通过给定的ConditionContext对象进而得到Environmen对象, 并使用这个对象检查环境中是否存在名为magic的环境属性.
我们现在用到的仅仅只是ConditionContext的Environmen, 而实质上, ConditionContext能做的还有很多很多. ConditionContext是一个接口, 我们来看一下它都有哪些东西:
public interface ConditionContext{
BeanDefinitionRegistry getRegistry();
ConfigurableListableBeanFactory getBeanFactory();
Environment getEnvironment();
ResourceLoader getResourceLoader();
ClassLoader getClassLoader();
}
通过ConditionContext, 我们可以做到以下几点:
而AnnotatedTypeMetadata能够让我们检查带有@Bean注解的方法上还有什么其它的注解, 关于AnnotatedTypeMetadata接口的内容就不过多描述, 需要的时候去查相应的文档就可以了.
值得注意的是, 从Spring 4开始, @Profile注解进行了重构, 使其基于@Conditional和Condition实现. 其中, 引用ProfileCondition作为Condition的实现. 关于@Profile注解的代码和ProfileCondition的实现代码在这里就不贴出来了.
不知道你们在前面自动装配练习的时候有没有发现这样一个问题, 就是当有多个Bean都符合装配条件的时候, 在测试运行的时候就会抛出异常. 这也就是说, Spring在装配的时候出现了歧义, 它不知道到底应该装配这两个都符合装配条件的Bean中的哪一个.
为了说明这种歧义性, 我们来看一个例子:
在Eat类里面有使用了@Autowired注解标注的setDessert()方法:
程序12: 自动装配的Eat类
@Component
public class Eat {
private Dessert dessert;
@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 {
}
上面这三个类都实现了Dessert接口, 并且都使用了@Component标注, 都可以被装配到setDessert()方法里, 那么怎样才能确定到底是哪一个被装配呢?
emmmm, 在这三种甜点里面, 我最喜欢IceCream了, 所以我要把IceCream设为首选被装配的Bean.
使用@Primary标注将一个Bean标示为首选的Bean, 就像下面这样:
@Component
@Primary
public class IceCream implements Dessert {
}
@Configuration
public class EatConfig {
@Bean
@Primary
public Dessert iceCream(){
return new IceCream();
}
}
id="iceCream" class="AdvancedAssembly.ConditionalBean.Dessert.IceCream"
primary="true" />
上面标示首选Bean的解决方式虽然直接, 但是当有两个首选的Bean, 就没办法选出”更首选”的Bean了.
所以Spring提供了另外一种更强大的解决方式: 使用限定符限定自动装配的Bean.
Spring的限定符能够在所有可选的Bean上进行缩小范围的操作, 使得最终只有一个符合操作的Bean. 使用@Qualifier注解是使用限定符的主要方式.
先用一个例子看一下使用限定符的方式:
程序13: 使用限定符标注
@Component
public class Eat {
private Dessert dessert;
@Autowired
@Qualifier("iceCream")
public void setDessert(Dessert dessert){
this.dessert = dessert;
}
}
通过@Qulifier()标注里面的”iceCream”就能匹配到IceCream Bean. 可以将”iceCream”理解为是想要注入的Bean的ID. 但是实质上, 这个”iceCream”限定符是用来匹配限定符为iceCream的Bean, 因为其实每个Bean都有自己默认的限定符, 这个限定符默认为该Bean的ID, 因此@Qualifier(“iceCream”)就是将默认限定符为iceCream的Bean注入到setDessert()方法中.
但是这里又有一个问题, 就是如果现在将IceCream这个类的名改为StrawberryIceCream, 那么自动装配就会失败. 这说明setDessert()方法上所要指定的限定符与要注入的Bean的名称是紧耦合的, 这不符合Spring的理念, 所以我们可以通过创建自定义的限定符来解决这个问题.
我们可以为Bean自定义限定符, 而不是使用Bean默认的限定符. 可以在Bean的声明前加上@Qualifier标注来自定义限定符. 比如:
@Component
@Qualifier("cold")
public class IceCream implements Dessert {
}
那么, 在JavaConfig中关于setDessert()方法的@Qualifier标注里面就可以使用我们自定义的限定符了.
@Component
public class Eat {
private Dessert dessert;
@Autowired
@Qualifier("cold")
public void setDessert(Dessert dessert){
this.dessert = dessert;
}
}
这样, 我们就可以随意更改IceCream Bean而不用担心更改会破坏自动装配.
自定义的限定符通常命名为Bean的特征, 比如我们上面指定的cold就是IceCream的特征.
上面我们自定义了限定符, 但是, 仍然存在一些问题, 比如有多个Bean都具有相同的特征(cold)怎么办, 还是会出现歧义性的问题, 要对Bean的范围进行进一步的缩小.
前面我们自定义了限定符, 现在可以自定义限定符注解.
在这里不再在IceCream中使用@Qualifier(“cold”), 而是自定义一个@Cold注解.
程序14: 自定义一个@Cold标注
@Target({ElementType.CONSTRUCTOR, ElementType.FIELD,
ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Cold { }
在IceCream中使用@Cold标注
@Component
@Cold
public class IceCream implements Dessert {
}
在Eat中使用@Cold
@Component
public class Eat {
private Dessert dessert;
@Autowired
@Cold
public void setDessert(Dessert dessert){
this.dessert = dessert;
}
}
像@Cold这样的标注, 我们可以根据自己的需求自定义多个, 并根据自己的需求搭配使用, 也就是, 可以使用多个自定义限定符来限定一个Bean.
在默认情况下, Spring应用上下文中所有的Bean都是以单例的形式创建的. 也就是说, 一个Bean, 无论被注入到其它Bean里多少次, 每次所注入的都是同一个实例.
在大多数情况下, 单例的Bean都是理想的方案. 但是, 有时候你会发现你所使用的类是易变的, 它们会保持一些状态, 因此重用是不安全的. 在这种情况下, 将class声明为单例的Bean就不合理了, 因为对象会被污染, 稍后重用的时候会出现意想不到的问题.
考虑到这种情况的发生, Spring在创建Bean给出了多种可以选择的作用域:
单例是默认的作用域, 如果选择其它的作用域, 要使用@Scope注解, 它可以与Component或@Bean一起使用. 比如:
程序15: 在记事本Notepad Bean中设置作用域
@Component
@Scope(ConfigurableListableBeanFactory.SCOPE_PROTOTYPE)
public class Notepad {
}
要是通过XML进行设置的代码如下:
"notapad" class="AdvancedAssembly.Scope.Notepad"
scope="prototype" />
相应的, 在NotepadConfig中也要对作用域进行设置.
程序16: 在NotepadConfig中设置作用域
@Configuration
public class NotepadConfig {
@Bean
@Scope(ConfigurableListableBeanFactory.SCOPE_PROTOTYPE)
public Notepad notepad(){
return new Notepad();
}
}
上面在@Scope标注中使用了ConfigurableListableBeanFactory的SCOPE_PROTOTYPE, 其实也可以写成@Scope(“prototype”), 但是采用上面那种写法不容易出错, 所以尽量使用上面的写法.
下来我们对详细介绍一下会话和请求作用域
在电子商务中有一个典型的例子就是购物车. 想象一下, 如果购物车是单例的, 也就是说, 所有的用户都将共用一个购物车, 这不太合适吧. 但是如果将它设置为原型作用域, 也就说, 用户每浏览的一个网页都有相对应的购物车, 用户将一件上衣添加到购物车, 又转而去另一个页面添加一个裤子到购物车, 等到付款时, 难道还要分别去上衣和裤子的页面去买单吗?
显然是不合适的. 正确的做法应该是, 每一个用户拥有一个购物车.
在这种情况下, 我们就可以将购物车Bean设置为会话作用域.
程序17: 会话作用域的购物车Bean
@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION,
proxyMode = ScopedProxyMode.INTERFACES)
public class ShoppingCart {
}
在这里将value的值设置为WebApplicationContext.SCOPE_SESSION, 这会告诉Spring为Web应用中的每个会话创建一个ShoppingCart Bean. 这也就是说, ShoppingCart Bean在整个应用中是多例的, 而在每一个会话中是单例的.
在Scope中还有另一个属性proxyMode, 它被设置为ScopedProxyMode.INTERFACES, 这个属性解决了将会话或请求作用域的 Bean注入到单例Bean中所遇到的问题.
在描述proxyMode属性之前, 先看一下proxyMode所解决问题的场景.
现在要将ShoppingCart Bean注入到单例StoreService Bean的Setter方法中, 如下:
@Component
public class StoreService {
private ShoppingCart shoppingCart;
@Autowired
public void setShoppingCart(ShoppingCart shoppingCart){
this.shoppingCart = shoppingCart;
}
}
因为StoreService Bean是单例的, 会在Spring应用上下文加载的时候创建, 而当它创建的时候, Spring会试图将ShoppingCart Bean注入到StoreService Bean中, 但是ShoppingCart Bean是会话作用域的, 此时并不存在, 直到某个用户进入系统, 创建了会话之后才会出现ShoppingCart实例.
另外, 系统中会有多个ShoppingCart实例, 每一个用户一个. 我们并不像让Spring注入某个固定的ShoppingCart Bean到StoreService Bean中. 我们希望的是当StoreService处理购物车功能时, 它所使用的ShoppingCart Bean恰好是当前会话对应的那一个.
那么现在需要解决的有两个问题:
现在详细看一下使用代理模式是如何解决上面两个问题的:
解决问题一:
在StoreService Bean创建而ShoppingCart Bean还不存在的时候, Spring会将一个ShoppingCart Bean的代理注入到StoreService Bean中, 这个代理会暴露与ShoppingCart Bean相同的方法, 所以StoreService Bean就会认为这个代理就是一个ShoppingCart Bean, 从而解决了StoreService Bean创建之初而ShoppingCart Bean还不存在无法满足依赖的问题.
解决问题二:
当StoreService Bean真正需要调用ShoppingCart Bean的方法的时候, 代理会对其进行解析并将调用委托给相应会话作用域中真正的ShoppingCart Bean.
现在, 我们带着对着两个问题解决的办法来讨论一下proxyMode属性. 如配置所示, proxyMode属性被设置成了ScopedProxyMode.INTERFACES, 这表明这个代理要实现ShoppingCart接口, 并将调用委托给真正的实现Bean.
在上面的代码中, ShoppingCart是一个接口, 代理需要实现的就是这个接口, 这是最理想的代理模式. 但是如果ShoppingCart是一个类的话, Spring就没有办法创建基于接口的代理了. 此时, 它必须使用CGLib来生成基于类的代理. 所以, 如果Bean的类型是具体类的话, 我们必须要将proxyMode属性设置为ScopedProxyMode.TARGET.CLASS, 以此来表示要以生成目标类扩展的方式来创建代理.
上面详细介绍了会话作用域的Bean装配问题, 请求作用域也是类似的, 请求作用域的Bean应该也以作用域代理的方式注入.
如果需要使用XML来声明会话或请求作用域的Bean, 就需要使用
我们使用Spring aop命名空间的一个新元素来指定代理模式:
<bean id="cart" class="AdvancedAssembly.Scope.ShoppingCart"
scope="session">
<aop:scoped-proxy proxy-target-class="false" />
bean>