Bean的高级装配

环境与Profile

配置profile bean

Spring 3.1引入了profile bean功能:为某个bean指定它所属的profile环境,当该profile处于active状态时,这个bean定义才有效。

在Java配置中,可以使用@Profile注解来指定某个bean属于哪一个profile。

@Configuration
@Profile("dev")
open class DevelopmentProfileConfig {
}
1
2
3
4

在上面的例子中,将@Profile注解应用在类级别上,表示这个配置类中定义的bean只有在dev profile激活时才有效。若dev profile没有激活,这个类中带有@Bean注解的方法都会被忽略。

在Spring 3.1中,只能在类级别上使用@Profile注解。从Spring 3.2开始,可以在方法级别上使用。

@Configuration
open class DevelopmentProfileConfig {

    @Bean
    @Profile("dev")
    open fun embeddedDataSource(): DataSource {
        // ...
    }
    
    @Bean
    @Profile("prod")
    open fun jndiDataSource(): DataSource {
        // ...
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

可以通过<beans>元素的profile属性,在XML中配置profile bean。

<beans profile="dev">
    <!-- ... -->
</beans>
1
2
3

可以在XML文件的根<beans>元素中嵌套定义<beans>元素,每个<beans>元素负责一个profile环境的定义,这样就能够将所有profile bean定义放在同一个XML文件中。

<beans>
    <beans profile="dev">
        <!-- ... -->
    </beans>
    <beans profile="prod">
        <!-- ... -->
    </beans>
</beans>
1
2
3
4
5
6
7
8

激活profile

Spring依赖spring.profiles.activespring.profiles.default这两个独立的属性来确定哪个profile是激活的。

  1. 如果设置了spring.profiles.active,那么就用它的值来确定要激活的profile。
  2. 如果没有设置spring.profiles.active,就查找spring.profiles.default值。
  3. 如果两个值都没有设置,那就没有激活的profile,因此只会创建那些没有定义在profile中的bean。

有多种方式来设置这两个属性:

  • 作为DispatcherServlet的初始化参数。
<!-- web.xml -->
<web-app>
    <servlet>
        <servlet-name>appServlet</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>
    </servlet>
</web-app>
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 作为Web应用的上下文参数。
<!-- web.xml -->
<web-app>
    <context-param>
        <param-name>spring.profiles.default</param-name>
        <param-value>dev</param-value>
    </context-param>
</web-app>
1
2
3
4
5
6
7
  • 作为JNDI条目。

  • 作为环境变量。

  • 作为JVM的系统属性。

  • 在集成测试类上,使用@ActiveProfiles注解来设置。

@RunWith(SpringJUnit4ClassRunner::class)
@ContextConfiguration(classes = [SystemConfig::class])
@ActiveProfiles(value = ["dev"])
class CDPlayerTest {
    // ...
}
1
2
3
4
5
6

其实,这两个属性都可以同时赋予多个profile名称,用逗号分割。

条件化Bean

Spring 4引入了一个新的@Conditional注解,它和@Bean注解一起使用。如果指定的条件成立,就会创建对应的Bean,否则会忽略这个Bean定义。

@Conditional注解接收一个Class类为参数,该类可以是任意实现了Condition接口的类型。这个Condition实现类要实现matches()方法,此方法的Boolean结果决定是否创建@Conditional注解对应的Bean。

import org.springframework.context.annotation.Condition
import org.springframework.context.annotation.ConditionContext
import org.springframework.core.type.AnnotatedTypeMetadata

class MagicExistsCondition : Condition {

    override fun matches(context: ConditionContext, metadata: AnnotatedTypeMetadata): Boolean {
        // 检查环境中是否存在magic属性
        val env = context.environment
        return env.containsProperty("magic")
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
// MagicExistsCondition的matches方法返回true时,才会创建MagicBean
@Bean
@Conditional(MagicExistsCondition::class)
open fun magicBean(): MagicBean {
    return MagicBean()
}
1
2
3
4
5
6

善于利用matches()方法中的ConditionContext和AnnotatedTypeMetadata对象来做条件判断的决策。

通过ConditionContext,可做到如下几点:

  • 借助getRegistry()返回的BeanDefinitionRegistry检查bean定义。
  • 借助getBeanFactory()返回的ConfigurableListableBeanFactory检查bean是否存在,甚至探查bean的属性。
  • 借助getEnvironment()返回的Environment检查环境变量是否存在指定属性,以及其值是什么。
  • 借助getResourceLoader()返回的ResourceLoader读取并探查所加载的资源。
  • 借助getClassLoader()返回的ClassLoader加载并检查类是否存在。

自动装配的歧义性

仅有一个bean匹配时,Spring自动装配能正常工作。

interface Dessert {
    fun name(): String
}

@Component
open class Cake : Dessert {
    override fun name(): String = "Cake"
}
1
2
3
4
5
6
7
8
@Autowired
private lateinit var dessert: Dessert
1
2

如果有多于一个的bean能够匹配时,这种歧义性会阻碍Spring自动装配,并抛出NoUniqueBeanDefinitionException。

interface Dessert {
    fun name(): String
}

@Component
open class Cake : Dessert {
    override fun name(): String = "Cake"
}

@Component
open class Cookie : Dessert {
    override fun name(): String = "Cookie"
}

@Component
open class IceCream : Dessert {
    override fun name(): String = "IceCream"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

解决这种歧义性的方法有:

  • 将可选bean中的某一个设为首选(primary bean)。

  • 使用限定符(qualifier)来帮助Spring将可选bean的范围缩小到只有一个。

首选bean

在声明bean时,通过将其中一个可选bean设为首选bean,能够避免自动装配时的歧义性。当遇到歧义性时,Spring将会使用首选bean。

使用@Primary来声明首选bean。

  • 它能够与@Component组合用在组件扫描的bean上。
@Component
@Primary
open class Cake : Dessert {
    override fun name(): String = "Cake"
}
1
2
3
4
5
  • 也可以将它与@Bean组合用在Java配置的bean声明中。
@Bean
@Primary
fun Dessert cake() = Cake()
1
2
3

如果使用XML配置,可使用<bean>元素中的primary属性来指定首选bean。

<bean id="cake"
    class="tech.daking.bean.Cake"
    primary="true" />
1
2
3

注意,首选bean要确保只有一个,若有多个首选bean,Spring自动装配的歧义性就再次出现。

限定符

首选bean只能标示一个优先的可选方案,而无法将可选方案限定到唯一一个无歧义性的结果。

而Spring限定符能够在所有可选的bean上进行缩小范围的操作,最终能够达到只有一个bean满足所规定的限制条件。

默认限定符

所有bean都会给定一个默认的限定符,其值与bean的ID相同。

@Qualifier注解可以与@Autowired@Inject协同使用,在注入时指明要注入规定限定符的bean。

// 创建ID为cookie的bean,因此,bean的限定符为cookie
@Component
open class Cookie : Dessert {
    override fun name(): String = "Cookie"
}
1
2
3
4
5
// 指定注入限定符为cookie的bean
@Autowired
@Qualifier(value = "cookie")
private lateinit var Cookie: Dessert
1
2
3
4

自定义的限定符

可以为bean设置自定义的限定符,而不依赖于其bean ID。

在bean声明上添加@Qualifier注解来创建自定义的限定符。

  • @Component的bean声明上使用@Qualifier创建自定义的限定符。
@Component
@Qualifier("cold")
open class IceCream : Dessert {
    override fun name(): String = "IceCream"
}
1
2
3
4
5
  • @Bean的bean声明上使用@Qualifier创建自定义的限定符。
@Bean
@Qualifier("cold")
open fun cold(): Dessert = IceCream()
1
2
3

在注入时使用@Qualifier注解引用指定的自定义限定符。

@Autowired
@Qualifier(value = "cold")
private lateinit var cold: Dessert
1
2
3

自定义的限定符注解

可以创建自定义的限定符注解来表达bean所希望限定的特性。

首先,定义一个新的自定义的限定符注解。在定义该注解时必须添加@Qualifier注解,让其具有@Qualifier注解的特性。

@Target(AnnotationTarget.CLASS, AnnotationTarget.CONSTRUCTOR,
        AnnotationTarget.FIELD, AnnotationTarget.FUNCTION, AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class Cold
1
2
3
4
5

接着,为bean添加该自定义的限定符注解。

@Component
@Cold
open class IceCream : Dessert {
    override fun name(): String = "IceCream"
}
1
2
3
4
5

最后,在注入时使用自定义的限定符注解。

@Autowired
@Cold
private lateinit var cold: Dessert
1
2
3

可同时使用多个这样的自定义的限定符注解,从而将可选范围缩小到只有一个bean满足需求。

@Autowired
@Cold
@Creamy
private lateinit var cold: Dessert
1
2
3
4

bean的作用域

作用域种类

默认情况下,Spring应用上下文中所有bean都是以单例的形式创建的。

Spring定义了多种作用域,可以基于它们来创建bean。

  • 单例(Singleton):在整个应用中,只创建一个bean实例。

  • 原型(Prototype):每次注入或者通过Spring应用上下文获取时,都会创建一个新的bean实例。

  • 会话(Session):在Web应用中,为每个会话创建一个bean实例。

  • 请求(Request):在Web应用中,为每次请求创建一个bean实例。

在bean声明(@Component@Bean)上使用@Scope来指定该bean的作用域。

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
open class Cookie : Dessert { ... }
1
2
3

注意,单例原型作用域分别使用Spring库中的ConfigurableBeanFactory类的SCOPE_SINGLETONSCOPE_PROTOTYPE。而会话请求作用域分别使用Spring Web库中的WebApplicationContext类的SCOPE_SESSIONSCOPE_REQUEST

如果是XML配置,使用<bean>元素的scope属性来指定该bean的作用域。各作用域对应的字符串分别为:singleton、prototype、session和request。

<bean id="cookie"
    class="tech.daking.bean.Cookie"
    scope="prototype" />
1
2
3

作用域代理

在Web应用中,常常使用会话或请求作用域来声明一些与用户或网络请求相关的bean。比如,购物车bean。

@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION,
        proxyMode = ScopedProxyMode.TARGET_CLASS)
open class ShoppingCart { ... }
1
2
3
4

上面的ShoppingCart bean,@Scope注解的value属性设为WebApplicationContext.SCOPE_SESSION,表明这是一个会话作用域的bean。而proxyMode属性是设置该bean的作用域代理,下面进行重点讲述。

@Component
open class StoreService {
    @Autowired
    lateinit var shoppingCart: ShoppingCart
}
1
2
3
4
5

上面的StoreService bean是一个单例bean,会在Spring应用上下文加载时创建。当它创建时,Spring会试图将ShoppingCart bean注入到其shoppingCart字段中。但ShoppingCart bean是会话作用域,此时并不存在,要直到某个用户创建会话才会出现ShoppingCart实例。

另外,系统中会有多个ShoppingCart bean实例(每个用户对应一个),我们不能让某个固定的ShoppingCart bean实例注入到StoreService bean中。

因此,要使用作用域代理机制让一个ShoppingCart bean代理注入到StoreService bean中,当调用ShoppingCart的方法时,该代理会对其进行懒解析并委托给会话作用域内真正的ShoppingCart bean实例来处理。

会话或请求作用域的代理-w505

@Scope注解的proxyMode属性值为ScopedProxyMode枚举类对象,有以下几个可选值:

  • DEFAULT:默认代理模式。默认情况下等同于NO,除非在组件扫描指令级别中修改了默认代理模式。

  • NO:不创建作用域代理。

  • INTERFACES:利用JDK动态代理来创建一个基于接口的代理对象。若bean是接口,使用此代理模式。

  • TARGET_CLASS:使用CGLIB来创建一个扩展目标类的代理对象。若bean是类,使用此代理模式。

如果是XML配置,在<bean>元素中使用Spring aop命名空间中的<aop:scoped-proxy>子元素来指定作用域的代理模式。

<bean id="cart"
    class="tech.daking.bean.ShoppingCart"
    scope="session">
    <aop:scoped-proxy />
</bean>
1
2
3
4
5

<aop:scoped-proxy>默认是使用CGLIB创建目标类的代理,可将其proxy-target-class属性设为false让它生成基于接口的代理。

运行时值注入

Spring的依赖注入,不仅支持将一个bean对象注入到另一个bean对象的属性或构造器参数中,还支持将一个值注入到一个bean的属性或构造器参数中。

Spring提供了两种在运行时注入值的方法:

  • 属性占位符(Property placeholder)。

  • Spring表达式语言(SpEL)。

属性占位符

class BlankDisc(val title: String, val artist: String) {
    fun play() {
        println("Playing $title by $artist.")
    }
}
1
2
3
4
5
# src/main/resources/app.properties
disc.title=Jay
disc.artist=Jay Chou
1
2
3
@Configuration
@PropertySource("classpath:app.properties")
open class CDPlayerConfig {
    @Autowired
    private lateinit var env: Environment

    @Bean
    open fun blankDisc(): BlankDisc {
        return BlankDisc(
                env.getProperty("disc.title"),
                env.getProperty("disc.artist")
        )
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Autowired
private lateinit var blankDisc: BlankDisc

@Test
fun playBlankDisc() {
    blankDisc.play() // Playing Jay by Jay Chou.
}
1
2
3
4
5
6
7