Spring 高级装配

本文介绍了如何在Spring中使用profile进行环境配置,包括配置profile的方法、激活profile的方式,以及如何通过@Profile和@Conditional注解处理不同环境下的bean。重点讲解了如何解决自动装配的歧义性和如何使用@Scope定义bean的作用域,包括会话和请求作用域的使用。
摘要由CSDN通过智能技术生成

环境和profile

我们学习了Spring profile,它解决了Spring bean要跨各种部署环境的通用问题。在运行时,通过将环境相关的bean与当前激活的profile进行匹配,Spring能够让相同的部署单元跨多种环境运行,而不需要进行重新构建。

配置profile

在Java中配置profile,可以使用@Profile注解指定某个bean属于哪一个profile。Spring这个配置类中的bean只有在dev profile激活时才会创建。如果devprofile没有激活的话,那么带有@Bean注解的方法都会被忽略掉。从Spring 3.2开始,你也可以在方法级别上使用@Profile注解,与@Bean注解一同使用。这样的话,就能将这两个bean的声明放到同一个配置类之中。
在xml文件中配置profile,我们也可以通过元素的profile属性,在XML中配置profile bean;你还可以在根元素中嵌套定义元素,而不是为每个环境都创建一个profile XML文件。这能够将所有的profile bean定义放到同一个XML文件中。

激活profile

Spring在确定哪个profile处于激活状态时,需要依赖两个独立的属性:spring.profiles.active和spring.profiles.default;如果存在spring.profile.active的属性,则优先通过其确定激活哪个profile,否则就根据spring.profiles.default确认;如果到没有,就没有激活的profile。有多种方式来设置这两个属性:

  • 作为DispatcherServlet的初始化参数;
  • 作为Web应用的上下文参数;
  • 作为JNDI条目;
  • 作为环境变量;
  • 作为JVM的系统属性;
  • 在集成测试类上,使用@ActiveProfiles注解设置。
    我所喜欢的一种方式是使用DispatcherServlet的参数将spring.profiles.default设置为开发环境的profile,我会在Servlet上下文中进行设置(为了兼顾到ContextLoaderListener)。
<context-param>
        <param-name>spring.profiles.default</param-name><!--为上下文设置默认的profile-->
        <param-value>dev</param-value>
    </context-param>
    <listen>
        <listen-class>
            org.springframework.web.context.ContextLoaderListenr
        </listen-class>
    </listen>
    <servlet>
        <servlet-name>appServlet</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet
        </servlet-class>
        <init-param>
            <param-name>spring.profiles.default</param-name><!--为servlet设置默认的profile-->
            <param-value>dev</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
profile测试

Spring提供了@ActiveProfiles注解,我们可以使用它来指定运行测试时要激活哪个profile。在集成测试时,通常想要激活的是开发环境的profile。

条件化的bean

Spring 4引入了一个新的@Conditional注解,它可以用到带有@Bean注解的方法上。如果给定的条件计算结果为true,就会创建这个bean,否则的话,这个bean会被忽略。条件化的配置bean

@Bean
@Conditional(MagicExistsCondition.class)//条件化地创建bean
public MagicBean magicBean(){
    return new MagicBean();
}

可以看到,@Conditional中给定了一个Class,它指明了条件——在本例中,也就是MagicExistsCondition。@Conditional将会通过Condition接口进行条件对比:

public interface Condition{
    boolean matches(ConditionContext ctxt,AnnoatedTypeMetadata metadata);
}

这个接口实现起来很简单直接,只需提供matches()方法的实现即可。如果matches()方法返回true,那么就会创建带有@Conditional注解的bean。如果matches()方法返回false,将不会创建这些bean。
在本例中,我们需要创建Condition的实现并根据环境中是否存在magic属性来做出决策。程序展现了MagicExistsCondition,这是完成该功能的Condition实现类:

public class MagicExistsCondition implements Condition{
    public boolean matches(ConditionContext ctxt,AnnoatedTypeMetadata metadata){
        Environment env = context.getEnvironment();
        return env.containsProperty("magic");//检查magic属性
    }
}

MagicExistsCondition中只是使用了ConditionContext得到的Environment,但Condition实现的考量因素可能会比这更多。matches()方法会得到ConditionContext和AnnotatedTypeMetadata对象用来做出决策。

public interface ConditionContext{
    
    BeanDefinitionRegistry getRegistry();             //检查bean定义
    ConfigurableListableBeanFactory getBeanFactory();//检查bean是否存在,检查bean的属性
    Environment getEnvironment();                   //检查环境变量是否存在以及它的值是什么
    ResourceLoader getResourceLoader();             //读取ResourceLoader加载的资源
    ClassLoader getClassLoader();                   //加载并检查类是否存在
}

AnnotatedTypeMetadata则能够让我们检查带有@Bean注解的方法上还有什么其他的注解。像ConditionContext一样,AnnotatedTypeMetadata也是一个接口。它如下所示:

public interface AnnotatedTypeMetadata{
    //借助isAnnotated()方法,判断带有@bean注解的方法是否还有其他特定的注解。
    boolean isAnnotated(String annotationType);
    //检查@bean注解的方法上其他注解的属性
    Map<String,Object> getAnnotationAttributes(String annotationType);
    Map<String,Object> getAnnotationAttributes(String annotationType,boolean classValuesAsString);
    MultiValueMap<String,Object> getAllAnnotationAttributes(String annotationType);
    MultiValueMap<String,Object> getAllAnnotationAttributes(String annotationType,boolean classValuesAsString);
}

处理自动装配的歧义性

自动装配能够提供很大的帮助,不过,仅有一个bean匹配所需的结果时,自动装配才是有效的。如果不仅有一个bean能够匹配结果的话,这种歧义性会阻碍Spring自动装配属性、构造器参数或方法参数。

标示首选bean

在声明bean的时候,通过将其中一个可选的bean设置为首选(primary)bean能够避免自动装配时的歧义性。当遇到歧义性的时候,Spring将会使用首选的bean,而不是其他可选的bean。标示首选bean:

// 自动装配
@component
@Primary
public class IceCrean implements Dessert{
}

// java 代码装配
@Bean
@Primary
public Dessert iceCream(){
return new IceCreat();
}
<bean id="iceCream" class="com.desserteater.IceCream" Primary="true"/>

当首选bean的数量超过一个的时候,设置首选bean就没有意义了。

限定自动装配的bean

Spring的限定符能够在所有可选的bean上进行缩小范围的操作,最终能够达到只有一个bean满足所规定的限制条件。如果将所有的限定符都用上后依然存在歧义性,那么你可以继续使用更多的限定符来缩小选择范围。限定自动装配的bean:

@Autowired
@Qualifier("iceCream")
public void setDessert(Dessert dessert){
    this.dessert = dessert;
}

基于默认的bean ID作为限定符是非常简单的,但这有可能会引入一些问
题。如果你重构了IceCream类,将其重命名为Gelato的话,如果这样的话,bean的ID和默认的限定符会变为gelato,这就无法匹配setDessert()方法中的限定符。自动装配会失败。
创建自定义的限定符:
在这里所需要做的就是在bean声明上添加@Qualifier注解:

@Component
// 在这种情况下,cold限定符分配给了IceCreambean
@Qualifier("cold")
public class IceCream implements Dessert{...}

因为它没有耦合类名,因此你可以随意重构IceCream的类名,而不必担心会破坏自动装配:

@Autowired
@Qualifier("cold")
public class setDessert(Dessert dessert){
    this.dessert = dessert;
}
// Java代码装配
@Bean
@Qualifier("cold")
public Dessert iceCream{
    return new IceCream();
}

使用自定义的限定符注解:
如果多个bean都具备相同特性的话,在自动装配的时候也会存在歧义性问题;我们可以创建自定义的限定符注解,借助这样的注解来表达bean所希望限定的特性。这里所需要做的就是创建一个注解,它本身要使用@Qualifier注解来标注。这样我们将不再使用@Qualifier(“cold”),而是使用自定义的@Cold注解,该注解的定义如下所示:

@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPOlicy.RUNTIME)
@Qualifier
public @interface Cold{}

同样,可以创建其他特性注解(@Creamy),直接以自定义注解符:

@Component
@Cold
@Creamy
public class IceCream implement Dessert{}

bean的作用域

在默认情况下,Spring应用上下文中所有bean都是作为以单例(singleton)的形式创建的。也就是说,不管给定的一个bean被注入到其他bean多少次,每次所注入的都是同一个实例。有时候,可能会发现,你所使用的类是易变的(mutable),它们会保持一些状态,因此重用是不安全的。
Spring定义了多种作用域,可以基于这些作用域创建bean,包括:

  • 单例(Singleton):在整个应用中,只创建bean的一个实例。
  • 原型(Prototype):每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的bean实例。
  • 会话(Session):在Web应用中,为每个会话创建一个bean实例。
  • 请求(Rquest):在Web应用中,为每个请求创建一个bean实例。
    如果选择其他的作用域,要使用@Scope注解,它可以与@Component或@Bean一起使用。
// 自动装配
@Component
@Scope(ConfigurableBeanFacory.SCOPE_PROTOTYPE)
public class Notepad{
}

// java代码装配
@Bean
@Scope(ConfigurableBeanFacory.SCOPE_PROTOTYPE)
public Notepad Notepad(){
	return new Notepad();
}

使用ConfigurableBeanFacory类的SCOPE_PROTOTYPE常量设置了原型作用域。你当然也可以使用@Scope(“prototype”),但是使用SCOPE_PROTOTYPE常量更加安全并且不易出错。
如果你使用XML来配置bean的话,可以使用元素的scope属性来设置作用域:

<bean id="notepad" class="com.oschina.Notepad" scope="prototype"></bean>
使用会话和请求作用域:

在Web应用中,如果能够实例化在会话和请求范围内共享的bean,那将是非常有价值的事情。例如,在典型的电子商务应用中,可能会有一个bean代表用户的购物车。如果购物车是单例的话,那么将会导致所有的用户都会向同一个购物车中添加商品。另一方面,如果购物车是原型作
用域的,那么在应用中某一个地方往购物车中添加商品,在应用的另外一个地方可能就不可用了,因为在这里注入的是另外一个原型作用域的购物车。
就购物车bean来说,会话作用域是最为合适的,因为它与给定的用户关联性最大。要指定会话作用域,我们可以使用@Scope注解,它的使用方式与指定原型作用域是相同的:

@Component
@Scope(value=WebApplicationContext.SCOPE_SESSION, proxyMode=ScopeProxyMode.INTERFACES)
public ShoppingCart cart(){
}

这里,我们将value设置成了WebApplicationContext中的SCOPE_SESSION常量(它的值是session)。这会告诉Spring为Web应用中的每个会话创建一个ShoppingCart。这会创建多个ShoppingCart bean的实例,但是对于给定的会话只会创建一个实例,在当前会话相关的操作中,这个bean实际上相当于单例的。
要注意的是,@Scope同时还有一个proxyMode属性,它被设置成了ScopedProxyMode.INTERFACES。这个属性解决了将会话或请求作用域的bean注入到单例bean中所遇到的问题。这表明这个代理要实现ShoppingCart接口,并将调用委托给实现bean。但如果ShoppingCart是一个具体的类的话,Spring就没有办法创建基于接口的代理了。此时,它必须使用CGLib来生成基于类的代理。所以,如果bean类型是具体类的话,我们必须要将proxyMode属性设置为ScopedProxyMode.TARGET_CLASS,以此来表明要以生成目标类扩展的方式创建代理
在XML中声明作用域代理:

<bean id="cart" class="com.oschina.ShoppingCart" scope="session">
    <aop:scoped-proxy/>
</bean>

aop:scoped-proxy是与@Scope注解的proxyMode属性功能相同的SpringXML配置元素。它会告诉Spring为bean创建一个作用域代理。默认情况下,它会使用CGLib创建目标类的代理。但是我们也可以将proxy-target-class属性设置为false,进而要求它生成基于接口的代理.

<bean id="cart" class="com.oschina.ShoppingCart" scope="session">
    <aop:scoped-proxy proxy-target-class="false" />
</bean>

为了使用aop:scoped-proxy元素,我们必须在XML配置中声明Spring的aop命名空间

运行时值注入

当讨论依赖注入的时候,我们通常所讨论的是将一个bean引用注入到另一个bean的属性或构造器参数中。它通常来讲指的是将一个对象与另一个对象进行关联。但是bean装配的另外一个方面指的是将一个值注入到bean的属性或者构造器参数中。之前的实现都是硬编码,有时候硬编码是可以的,但有的时候,我们可能会希望避免硬编码值,而是想让这些值在运行时再确定。为了实现这些功能,Spring提供了两种在运行时求值的方式:

  • 属性占位符(Property placeholder)
  • Spring表达式语言(SpEL)
注入外部的值:
package com.oschina
@Configuration
//声明属性源
@PropertySource("classpath:/com/oschina/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 Club Band
disc.artist=The Beatles

深入学习Spring的Environment:
getProperty()方法并不是获取属性值的唯一方法,getProperty()方法
有四个重载的变种形式:

String getProperty(String key)
String getProperty(String key, String defaultValue)
T getProperty(String key, Class<T> Type)
T getProperty(String key, Class<T> Type, T defaultValue)

前两种形式的getProperty()方法都会返回String类型的值。你可以稍微对@Bean方法进行一下修改,这样在指定属性不存在的时候,会使用一个默认值.剩下的两种getProperty()方法与前面的两种非常类似,但是可以将从属性文件中得到的String类型的值重载为interger。如果想检查一下某个属性是否存在的话,那么可以调用EnvironmentcontainsProperty()方法。如果想将属性解析为类的话,可以使用getPropertyAsClass()方法。除了属性相关的功能以外,Environment还提供了一些方法来检查哪些profile处于激活状态:

  • String[] getActiveProfiles():返回激活profile名称的数组;
  • String[] getDefaultProfiles():返回默认profile名称的数组;
  • boolean acceptsProfiles(String… profiles):如果environment支持给定profile的话,就返回true。
    解析属性占位符:
    在spring装配中,占位符的形式为使用“${…}”包装的属性名称:
<bean id="sgtPeppers" class="soundsystem.BlankDisc" c:_title="${disc.title}" c:_artist="${disc.artist} />"

为了使用占位符,我们必须要配置一个PropertySourcesPlaceholderConfigurer,因为它能够基于Spring Environment及其属性源来解析占位符。如果你想使用XML配置的话,Spring context命名空间中的context:propertyplaceholder元素将会为你生成PropertySourcesPlaceholderConfigurer bean:

<context:property-placeholder />
使用Spring表达式语言进行装配

spring3引入了spring表达式语言(spring expression language ,SpEL),它能够以一种强大和简洁的方式将值装配到bean属性和构造器参数中,在这个过程中使用的表达式会在运行时计算的得到值,使用SpEL,你可以实现超乎想象的装配效果,这是使用其他的装配技术难以做到的。
SpEL拥有很多特性,包括:

  • 使用bean的ID来引用bean
  • 调用方法和访问对象的属性
  • 对值进行算术、关系和逻辑运算
  • 正则表达式匹配
  • 集合操作
    SpEL样例:
    SpEL表达式使用"#{…}",这与属性占位符有些类似,属性占位符是"${…}"。

除去"#{…}"标记之后,剩下的就是SpEL表达式体了,也就是一个数字常量。这个表达式的计算结果就是数字1,这恐怕不会让你感到丝毫惊讶。

#{T(System).currentTimeMillis()}```
它的最终结果是计算表达式的那一刻的当前时间的毫秒数。T()表达式会将java.lang.System视为Java中对应的类型,因此可以调用其static修饰的currentTimeMillis()方法。
SpEL表达式也可以应用其他的bean或其他bean的属性。例如,如下的表达式会计算得到ID为sgtPeppers的bean的artist属性:
```#{sgtPeppers.artist}```
我们还可以通过systemProperties对象引用系统属性:

#{systemProperties[‘disc.title’]}```
这只是SpEL的几个基础样例。
如果通过组件扫描创建bean的话,在注入属性和构造器参数时,我们可以使用@Value注解,这与之前看到的属性占位符非常相似。不过,在这里我们所使用的不是占位符表达式,而是SpEL表达式。例如,下面的样例展示了BlankDisc,它会从系统属性中获取专辑名称和艺术家的名字:

public BlankDisc(@Value{'#systemProperties['disc.title']'} string title,
    @Value{'#systemProperties['disc.artist']'} string artist){
    this.title = title;
    this.artist=artist;
}

在XML配置中,你可以将SpEL表达式传入或的value属性中,或者将其作为p-命名空间或c-命名空间条目的值。例如,在如下BlankDisc bean的XML声明中,构造器参数就是通过SpEL表达式设置的:

<bean id="sgtPeppers" class="soundsystem.BlankDisc" c:title="#systemProperties['disc.title']"
c:_artist="#[systemProperties['disc.artist']]">
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值