高级装配

高级装配

本章主要内容包括:

  • Spring profile
  • 条件化的bean声明
  • 自动装配与歧义性
  • bean的作用域
  • Spring表达式语言

1 环境与profile

在开发软件的过程中,有一个很大的挑战就是将应用程序从一个环境迁移到另外一个环境。开发阶段中,某些环境相关做法可能并不适合迁移到生成环境中,甚至即便迁移到生成环境中去。数据库配置、加密算法以及与外部系统的集成是跨环境部署时会发生变化的几个典型例子。

1.1 配置profile bean

Spring为环境相关的bean所提供的解决方案其实与构建时的方案没有太大的区别。当然,在这个过程中需要根据环境决定该创建哪个bean和不创建哪个bean。不过Spring并不是在构建的时候做这样的决策,而是等到运行时再来确定。这样的结果就是同一个部署单元(可能是WAR文件)能够使用于所有的环境,没有必要进行重新构建。
Spring引入了bean profile功能。要使用profile,首先要将所有不同的bean定义整理到一个或多个profile之中,在将应用部署到每个环境时,要确保对应的profile处于激活(active)状态。
在Java配置中,可以使用@Profile注解指定某一bean属于哪一个profile。如下为开发环境数据源bean:

package com.godman.profile;

public class DevDataSource implements TestDataSource {
    private String url;
    private String driverClassName;
    private String username;
    private String password;

    public void setUrl(String url) {
        this.url = url;
    }

    public void setDriverClassName(String driverClassName) {
        this.driverClassName = driverClassName;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public void getConnection() {
        System.out.printf("connect dev datasource: {url: "+this.url+", driverClassName: "+this.driverClassName+", username: "+this.username+", password: "+this.password+"}");
    }
}

下面为生产环境数据源bean:

package com.godman.profile;

public class ProDataSource implements TestDataSource {
    private String url;
    private String driverClassName;
    private String username;
    private String password;

    public void setUrl(String url) {
        this.url = url;
    }

    public void setDriverClassName(String driverClassName) {
        this.driverClassName = driverClassName;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public void getConnection() {
        System.out.printf("connect pro datasource: {url: "+this.url+", driverClassName: "+this.driverClassName+", username: "+this.username+", password: "+this.password+"}");
    }
}

以下为开发环境的JavaConfig配置:

package com.godman.config;

import com.godman.profile.DevDataSource;
import com.godman.profile.TestDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

@Configuration
@Profile("dev")
public class DevDataSourceConfig {

    @Bean
    public TestDataSource dataSource(){
        DevDataSource dataSource = new DevDataSource();
        dataSource.setDriverClassName("devDriverClass");
        dataSource.setUrl("http://dev");
        dataSource.setUsername("dev");
        dataSource.setPassword("dev");
        return dataSource;
    }
}

以下为生产环境的JavaConfig配置:

package com.godman.config;

import com.godman.profile.ProDataSource;
import com.godman.profile.TestDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

@Configuration
@Profile("pro")
public class ProDataSourceConfig {

    @Bean
    public TestDataSource dataSource(){
        ProDataSource dataSource = new ProDataSource();
        dataSource.setDriverClassName("proDriverClass");
        dataSource.setUrl("http://pro");
        dataSource.setUsername("pro");
        dataSource.setPassword("pro");
        return dataSource;
    }
}

值得注意的是@Profile注解应用在了类级别上。它会告诉Spring这个配置类中的bean只有在profile激活时才会创建。如果profile没有激活的话,那么带有@Bean注解的方法都会被忽略掉。
在Spring3.1中,只能在类级别上使用@Profile注解。不过,从Spring3.2开始,也可以在方法级别上使用@Profile注解,与@Bean注解一同使用。

package com.godman.config;

import com.godman.profile.DevDataSource;
import com.godman.profile.ProDataSource;
import com.godman.profile.TestDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

@Configuration
public class DataSourceConfig {

    @Bean
    @Profile("dev")
    public TestDataSource devDataSource(){
        DevDataSource dataSource = new DevDataSource();
        dataSource.setDriverClassName("devDriverClass");
        dataSource.setUrl("http://dev");
        dataSource.setUsername("dev");
        dataSource.setPassword("dev");
        return dataSource;
    }

    @Bean
    @Profile("pro")
    public TestDataSource proDataSource(){
        ProDataSource dataSource = new ProDataSource();
        dataSource.setDriverClassName("proDriverClass");
        dataSource.setUrl("http://pro");
        dataSource.setUsername("pro");
        dataSource.setPassword("pro");
        return dataSource;
    }
}

这里需要注意的一个问题是,尽管每个DataSource bean都被声明在一个profile中,并且只有当规定的profile激活时,相应的bean才会被创建,但是可能会有其他的bean并没有声明在一个给定的profile范围内。没有指定profile的bean始终都会被创建,与激活哪个profile无关。
下面是测试代码:

package test.com.godman.config;

import com.godman.config.DataSourceConfig;
import com.godman.profile.TestDataSource;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/** 
 * DataSourceConfig Tester. 
 * 
 * @author <Authors name> 
 * @since <pre>七月 22, 2018</pre> 
 * @version 1.0 
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes= DataSourceConfig.class)
@ActiveProfiles("dev")
public class DataSourceConfigTest { 

    @Autowired
    private TestDataSource dataSource;

    @Test
    public void connect(){
        dataSource.getConnection();
    }
}

其中使用了@ActiveProfiles注解来指定运行测试时要激活哪个profile。结果如下:

在XML中配置profile

我们也可以通过<beans>元素的profile属性,在XML中配置profile bean。如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <beans profile="pro">
        <bean id="proDataSource" class="com.godman.profile.ProDataSource">
            <property name="url" value="http://pro"></property>
            <property name="driverClassName" value="proDriverClassName"></property>
            <property name="username" value="pro"></property>
            <property name="password" value="pro"></property>
        </bean>
    </beans>

    <beans profile="div">
        <bean id="devDataSource" class="com.godman.profile.DevDataSource">
            <property name="url" value="http://div"></property>
            <property name="driverClassName" value="divDriverClassName"></property>
            <property name="username" value="div"></property>
            <property name="password" value="div"></property>
        </bean>
    </beans>
</beans>

可以为每一个profile单独配置一个XML文件,也可以在根<beans>中嵌套定义<beans>元素。这里采用嵌套式的配置方式,问题是该如何激活某个profile呢?

1.2 激活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。通过以下方式可以激活profile:

  • 作为DispatcherServlet的初始化参数;
  • 作为Web应用的上下文参数;
  • 作为JNDI条目;
  • 作为环境变量;
  • 作为JVM的系统属性;
  • 在集成测试类上,使用@ActiveProfiles注解设置。

在spring.profiles.active和spring.profiles.default中,profile使用的都是复数形式。这意味着可以同时激活多个profile,这可以通过列出多个profile名称,并以逗号分隔来实现。

2 条件化bean

假设希望一个或多个bean只有在应用的类路径下包含特定的库时才创建。或者某个bean只有当另外某个特定的bean也声明之后才会创建。还可能要求只有某个特定的环境变量设置之后,才会创建某个bean。
在Spring4之前,很难实现这种级别的条件化配置,但是Spring4引入了一个新的@Conditional注解,它可以用到带有@Bean注解的方法上。如果给定的条件计算结果为true,就会创建这个bean,否则这个bean就会被忽略。如下创建一个MagicBean类:

package com.godman.conditional;

public class MagicBean {

    public void magic(){
        System.out.println("magic...");
    }
}

这个MagicBean只有当环境变量中包含magic属性才会被创建,如下:

package com.godman.config;

import com.godman.conditional.MagicBean;
import com.godman.conditional.MagicExistsCondition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MagicConfig {

    @Bean
    @Conditional(MagicExistsCondition.class)
    public MagicBean magicBean(){
        return new MagicBean();
    }
}

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

public interface Condition {
    boolean matches(ConditionContext var1, AnnotatedTypeMetadata var2);
}

设置给@Conditional的类可以是任意实现了Condition接口的类型。很明显:只要实现的matches()方法返回true,那么就会创建这个带有@Conditional注解的bean,否则就会忽略。

package com.godman.conditional;

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 conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        Environment environment = conditionContext.getEnvironment();
        return environment.containsProperty("magic");
    }
}

此时进行测试:

package test.com.godman.conditional;

import com.godman.conditional.MagicBean;
import com.godman.config.MagicConfig;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
 * MagicBean Tester. 
 * 
 * @author <Authors name> 
 * @since <pre>七月 22, 2018</pre> 
 * @version 1.0 
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = MagicConfig.class)
public class MagicBeanTest { 

    @Autowired
    private MagicBean magicBean;

    @Test
    public void magic(){
        Assert.assertNotNull(magicBean);
        magicBean.magic();
    }
}

运行结果如下:

因为此时系统变量中并没有设置magic环境变量,所以Spring并没有创建MagicBean。手动设置magic环境变量:

再次运行测试,结果如下:

MagicExistsCondition中只是使用了ConditionContext得到的Environment,但Condition实现的考量因素可能会比这更多。matches()会得到ConditionContext和AnnotatedTypeMetadata对象用来做出决策。ConditionContext是一个接口,大致如下:

public interface ConditionContext {
    BeanDefinitionRegistry getRegistry();

    ConfigurableListableBeanFactory getBeanFactory();

    Environment getEnvironment();

    ResourceLoader getResourceLoader();

    ClassLoader getClassLoader();
}

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

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

AnnotatedTypeMetadata则能够让我们检查带有@Bean注解的方法上还有什么其他的注解。AnnotatedTypeMetadata也是一个接口:

public interface AnnotatedTypeMetadata {
    boolean isAnnotated(String var1);

    Map<String, Object> getAnnotationAttributes(String var1);

    Map<String, Object> getAnnotationAttributes(String var1, boolean var2);

    MultiValueMap<String, Object> getAllAnnotationAttributes(String var1);

    MultiValueMap<String, Object> getAllAnnotationAttributes(String var1, boolean var2);
}

借助isAnnotated()方法,我们能够判断带有@Bean注解的方法是不是还有其他特定的注解。借助其他的那些方法,我们能够检查@Bean注解的方法上其他注解的属性。
另外,从Spring4开始,@Profile注解进行了重构,使其基于@Conditional和Condition实现。作为如何使用@Conditional和Condition的例子,来看一下在Spring4中,@Profile是如何实现的:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional({ProfileCondition.class})
public @interface Profile {
    String[] value();
}

注意:@Profile本身也使用了@Conditional注解,并且引用ProfileCondition作为Condition实现。如下所示,ProfileCondition实现了Condition接口,并且在做出决策的过程中,考虑到了ConditionContext和AnnotatedTypeMetadata中的多个因素

class ProfileCondition implements Condition {
    ProfileCondition() {
    }

    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        if (context.getEnvironment() != null) {
            MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
            if (attrs != null) {
                Iterator var4 = ((List)attrs.get("value")).iterator();

                Object value;
                do {
                    if (!var4.hasNext()) {
                        return false;
                    }

                    value = var4.next();
                } while(!context.getEnvironment().acceptsProfiles((String[])((String[])value)));

                return true;
            }
        }

        return true;
    }
}

ProfileCondition通过AnnotatedTypeMetadata得到了@Profile注解的所有属性。借助该信息,它会明确地检查value属性,该属性包含了bean的profile名称。然后,它通过ConditionContext得到的Environment来检查(借助acceptsProfiles()方法)该profile是否处于激活状态。

3 处理自动装配的歧义性

自动装配能够提供很大的帮助,因为它会减少装配应用程序组件时所需要的显示配置的数量。不过,仅有一个bean匹配所需结果时,自动装配才是有效的。如果不仅有一个bean能够匹配结果的话,这种歧义性会阻碍Spring自动装配属性、构造器参数或方法参数。
为了阐述自动装配的歧义性,我们定义一个接口Dessert:

package com.godman.dessert;

public interface Dessert {
    void taste();
}

并且有3个类实现了这个接口,分别是:Cake、Cookies和IceCream:

@Component
public class Cake implements Dessert {  
    @Override
    public void taste() {
        System.out.println("taste cake...");
    }
}

@Component
public class Cookies implements Dessert {
    @Override
    public void taste() {
        System.out.println("taste cookies...");
    }
}

@Component
public class IceCream implements Dessert {
    @Override
    public void taste() {
        System.out.println("taste iceCream...");
    }
}

按如下代码测试:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = DessertConfig.class)
public class DessertConfigTest { 

    @Autowired
    private Dessert dessert;

    @Test
    public void taste(){
        dessert.taste();
    }
} 

因为这三个类实现均使用了@Component注解,在组件扫描的时候,能够发现它们并将其创建为Spring应用上下文里面的bean。然后当Spring试图自动装配dessert参数时,它并没有唯一、无歧义的可选值。此时Spring会抛出NoUniqueBeanDefinitionException:

nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.godman.dessert.Dessert' available: expected single matching bean but found 3: cake,cookies,iceCream

但是,当确实发生歧义的时候,Spring提供了多种可选方案来解决这样的问题。可以将可选bean中的某一个设为首选(primary)的bean,或者使用限定符(qualifier)来帮助Spring将可选的bean的范围缩小到只有一个bean。

3.1 标示首选的bean

在声明bean的时候,通过将其中一个可选的bean设置为首选(primary)bean能够避免自动装配时的歧义性。当遇到歧义性的时候,Spring将会使用首选的bean,而不是其他可选的bean。@Primary能够与@Component组合用在组件扫描的bean上,也可以与@Bean组合用在Java配置的bean声明中:

@Component
@Primary
public class Cake implements Dessert {

    @Override
    public void taste() {
        System.out.println("taste cake...");
    }
}

或者

@Configuration
@ComponentScan
public class DessertConfig {

    @Bean
    @Primary
    public Dessert cookies(){
        return new Cookies();
    }
}

如果使用的是XML配置bean的话,同样可以通过<bean>元素的primary属性来指定首选的bean:

<bean id="iceCream" class="com.godman.dessert.IceCream" primary="true"/>

此时再次运行测试代码,结果如下:

不管采用哪种方式标示首选bean,效果都是一样的,都是在告诉Spring在遇到歧义性的时候要选择首选的bean。但是,如果标记了两个或更多的首选bean,那么它就无法正常工作了。

nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.godman.dessert.Dessert' available: more than one 'primary' bean found among candidates: [cake, iceCream, cookies]

就像Spring无法从多个可选的bean中做出选择一样,它也无法从多个首选的bean中做出选择。
就解决歧义性而言,限定符是一种更为强大的机制。

3.2 限定自动装配的bean

设置首选bean的局限性在于@Primary无法将可选方案的范围限定到唯一一个无歧义性的选项中。它只能标示一个优选的方案。当首选bean的数量超过一个时,将不能正常工作。
Spring的限定符能够在所有可选的bean上进行缩小范围的操作,最终能够达到只有一个bean满足所规定的限制条件。如果将所有的限定符都用上后依然存在歧义性,那么可以继续使用更多的限定符来缩小选择范围。
@Qualifier注解是使用限定符的主要方式,它可以与@Autowired和@Inject协同使用,在注入的时候指定想要注入进去的是哪个bean。如下测试代码:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = DessertConfig.class)
public class DessertConfigTest { 

    @Autowired
    @Qualifier("iceCream")
    private Dessert dessert;

    @Test
    public void taste(){
        dessert.taste();
    }
} 

其运行结果如下:

这个是最简单的使用限定符的例子,为@Qualifier注解所设置的参数就是想要注入的bean的ID。更准确地讲,@Qualifier(“iceCream”)所引用的bean要具有String类型的”iceCream”作为限定符。如果没有指定其他的限定符的话,所有的bean都会给一个默认的限定符,这个限定符与bean的ID相同。因此Spring会将具有”iceCream”限定符的bean注入到dessert属性中。
但是以上所指定的限定符与要注入的bean的名称是紧耦合的,一旦重构IceCream类并将其重命名的话,就将出现无法匹配的问题,导致自动装配失败。

创建自定义限定符

我们可以为bean设置自己的限定符,而不是依赖于将bean的ID作为限定符。在这里只需要在bean的声明上添加@Qualifier注解。它可以与@Component组合使用,如下:

@Component
@Qualifier("cold")
public class IceCream implements Dessert {
    @Override
    public void taste() {
        System.out.println("taste iceCream...");
    }
}

此时,cold限定符分配给了IceCream bean。因此没有耦合类名,所以可以随意重构IceCream的类名,而不用担心破坏自动装配。在注入的地方只需要引入cold限定符就可以了:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = DessertConfig.class)
public class DessertConfigTest { 

    @Autowired
    @Qualifier("cold")
    private Dessert dessert;

    @Test
    public void taste(){
        dessert.taste();
    }
}

当然,当通过Java配置显示定义bean的时候,@Qualifier也可以与@Bean注解组合使用:

@Configuration
@ComponentScan
public class DessertConfig {

    @Bean
    @Qualifier("crispy")
    public Dessert cookies(){
        return new Cookies();
    }
}

此时将测试代码的@Qualifier引用改为”crispy”,运行结果如下:

使用自定义的限定符注解

使用特性描述的限定符要比基于beanID的限定符更好一些。但是如果多个bean都具备相同特性的话,也会出现问题。例如:

@Component
@Qualifier("cold")
public class PopSicle implements Dessert {
    @Override
    public void taste() {
        System.out.println("taste popSicle...");
    }
}

可以想到的解决方法是再添加另外一个@Qualifier注解,但是Java不允许在同一个条目上重复出现相同类型的多个注解(Java8允许出现,只要这个注解本身在定义的时候带有@Repeatable注解即可,不过Spring的@Qualifier注解并没有在定义时添加@Repeatable)。此时使用@Qualifier注解并不能直接将自动装配的可选bean缩小范围至仅有一个可选的bean。
但是,可以创建自定义的限定符注解,它本身要使用@Qualifier注解来标识,借助这样的注解来表达bean所希望限定的特性。自定义@Cold注解,如下:

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

同样可以在创建一个@Creamy注解和@Fruity注解:

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

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

然后为IceCream类和PopSicle类重新添加注解:

@Component
@Cold
@Creamy
public class IceCream implements Dessert {
    @Override
    public void taste() {
        System.out.println("taste iceCream...");
    }
}

@Component
@Cold
@Fruity
public class PopSicle implements Dessert {
    @Override
    public void taste() {
        System.out.println("taste popSicle...");
    }
}

更改测试代码,如下:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = DessertConfig.class)
public class DessertConfigTest { 

    @Autowired
    @Cold
    @Creamy
    private Dessert dessert;

    @Test
    public void taste(){
        dessert.taste();
    }
} 

其运行结果如下:

4 bean的作用域

在默认情况下,Spring应用上下文中所有的bean都是作为以单例(singleton)的形式创建的。也就是说,不管给定的一个bean被注入到其他bean多少次,每次注入的都是同一个实例。Spring定义了多种作用域,可以基于这些作用域创建bean,包括:

  • 单例(Singleton):在整个应用中,只创建bean的一个实例。
  • 原型(Prototype):每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的bean实例。
  • 会话(Session):在Web应用中,为每个会话创建一个bean实例。
  • 请求(Request):在Web应用中,为每个请求创建一个bean实例。

单例是默认的作用域,但是对于易变的类型并不合适。如果选择其他的作用域,要使用@Scope注解,它可以与@Component或@Bean一起使用。如下,使用组件扫描来发现和声明bean,可以在bean的类上使用@Scope注解,将其声明为原型bean:

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Notepad {
}

这里,使用ConfigurableBeanFactory类的SCOPE_PROTOTYPE常量设置了原型作用域。当然也可以使用@Scope(“prototype”),但是使用SCOPE_PROTOTYPE常量更加安全且不易出错。

如果想在Java配置中将Notepad声明为原型bean,那么可以组合使用@Scope和@Bean来指定所需的作用域:

@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Notepad notepad{
    return new Notepad();
}

同样可以使用XML来配置bean的作用域,可以使用<bean>元素的scope属性来设置作用域:

<bean id="notepad" class="com.godman.scope.Notepad" scope="prototype"/>

不管以哪种方式来声明原型作用域,每次注入或从Spring应用上下文中检索该bean的时候,都会创建新的实例。

4.1 使用会话和请求作用域

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

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

这里,将value设置成了WebApplicationContext中的SCOPE_SESSION常量(它的值是session)。这会告诉Spring为Web应用中的每个会话创建一个ShoppingCart。这会创建多个ShoppingCart bean 的实例,但是对于给定的会话只会创建一个实例,在当前会话相关的操作中,这个bean实际上相当于单例的。
要注意的是,@Scope同时还有一个proxyMode属性,它被设置成了ScopedProxyMode.INTERFACES。这个属性解决了讲会话或请求作用域的bean注入到单例bean中所遇到的问题。在描述proxyMode属性之前,先来看一下proxyMode所解决问题的场景。
假设我们要将ShoppingCart bean 注入到单例StoreService bean 的Setter方法中,如下所示:

@Component
public class StoreService {

    @AutoWired
    public void setShoppingCart(ShoppingCart shoppingCart){
        this.shoppingCart = shoppingCart;
    }
}

因为StoreService是一个单例的bean,会在Spring应用上下文加载的时候创建。当它创建的时候,Spring会试图将ShoppingCart bean 注入到setShoppingCart()方法中。但是ShoppingCart bean是会话作用域的,此时并不存在。知道某个用户进入系统,创建了会话之后,才会出现ShoppingCart实例。
另外,系统中将会有多个ShoppingCart实例:每个用户一个。我们并不想让Spring注入某个固定的ShoppingCart实例到StoreService中。我们希望的是当StoreService处理购物车功能时,它所使用的ShoppingCart实例恰好是当前会话所对应的那一个。
Spring并不会将实际的ShoppingCart bean 注入到StoreService中,Spring会注入一个到ShoppingCart bean的代理。这个代理会暴露与ShoppingCart相同的方法,所以StoreService会认为它就是一个购物车。但是,当StoreService调用ShoppingCart的方法时,代理会对其进行懒解析并将调用委托给会话作用域内真正的ShoppingCart bean。
现在,我们带着对这个作用域的理解,来讨论以下proxyMode属性。如配置所示,proxyMode属性被设置成了ScopedProxyMode.INTERFACES,这表明这个代理要实现ShoppingCart接口,并将调用委托给实现bean。
如果ShoppingCart是接口不是实现类的话,这是可以的(也是最为理想的代理模式)。但如果ShoppingCart是一个具体的类的话,Spring就没办法创建基于接口的代理了。此时,它必须使用CGLib来生成基于类的代理。所以,如果bean类型是具体类的话,我们必须要将proxyMode属性设置为ScopedProxyMode.TARGET_CLASS,以此来表明要以生成目标类扩展的方式创建代理。
请求作用域的bean与会话作用域的bean面临着相同的配置问题,所以请求作用域的bean应该也以作用域代理的方式进行注入。

4.2 在XML中声明作用域代理

如果需要使用XML来声明会话或请求作用域的bean,那么自然就不能使用@Scope注解及其proxyMode属性了。<bean>元素的scope属性能够设置bean的作用域,但是该怎样指定代理模式呢?这时,我们需要使用Spring aop命名空间的一个新元素:

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

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

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

5 运行时值注入

当讨论依赖注入的时候,我们通常所讨论的是将一个bean引用注入到另一个bean的属性或构造器参数中。他通常来讲指的是将一个对象与另一个对象进行关联。但是bean装配的另一个方面指的是将一个值注入到bean的属性或者构造器参数中。例如:

@Bean
public CompactDisc sgtPeppers() {
    return new BlankDisc("吻别", "张学友");
}

或者采用XML配置方式:

<bean id="sgtPeppers" class="com.godman.xml.BlankDisc" c:_title="吻别" c:_artist="张学友"/>

但是这两种方式都是通过硬编码的方式实现的。如果我们希望避免硬编码值,而是想让这些值在运行时在确定,可以通过Spring提供的两种运行时求值的方式实现:

  • 属性占位符(Property placeholder)
  • Spring表达式语言(SpEL)

5.1 注入外部值

在Spring中,处理外部值最简单的方式就是声明属性源并通过Spring的Environment来检索属性。如下所示:

package com.godman.expressive;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;

@Configuration
@PropertySource("classpath:/com/godman/expressive/expressive.properties")
public class ExpressiveConfig {

    @Autowired
    Environment environment;

    @Bean
    public BlankDisc disc(){
        return new BlankDisc(environment.getProperty("disc.title"), 
            environment.getProperty("disc.artist"));
    }
}

以上所示代码中,通过@PropertySource注解引用了类路径中一个名为expressive.properties的文件,其内容为:

disc.title=Loving Strangers
disc.artist=Tiger

这个属性文件会加载到Spring的Environment中,稍后可以从这里检索属性。下面通过以下代码进行测试:

package test.com.godman.expressive;

import com.godman.expressive.BlankDisc;
import com.godman.expressive.ExpressiveConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
 * ExpressiveConfig Tester. 
 * 
 * @author <Authors name> 
 * @since <pre>七月 25, 2018</pre> 
 * @version 1.0 
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ExpressiveConfig.class)
public class ExpressiveConfigTest { 

    @Autowired
    private BlankDisc disc;

    @Test
    public void play() {
        disc.play();
    }

} 

测试结果为:

深入学习Spring的Environment

当我们查看Environment的时候会发现,getProperty()方法并不是获取属性值的唯一方法,getProperty()有四种重载的变种形式:

  • String getProperty(String var1);
  • String getProperty(String var1, String var2);
  • T getProperty(String var1, Class var2);
  • T getProperty(String var1, Class var2, T var3);

前两种都会返回String类型的值。第一种在之前已经使用过了,第二种可以在指定属性不存在时设置一个默认值:

@Bean
public BlankDisc disc2(){
    return new BlankDisc(environment.getProperty("disc.title", "祝福"), environment.getProperty("disc.artist", "张学友"));
}

剩下的两种getProperty()方法与前面的非常类似,但是她们不会将所有的值视为String类型。例如,如果想要获取的值所代表的的含义是连接池中所维持的连接数量。如果我们从属性文件中得到的是一个String类型的值,那么在使用之前还要将其转换为Integer类型。但是如果使用重载形式的getProperty()方法,就能非常方便的解决这个问题:

 int connectionCount = environment.getProperty("db.connection.count", Integer.class);

Environment还提供了几个与属性有关的方法,如果在使用getProperty()方法时没有指定默认值,并且这个属性没有定义的话,得到的值是null。如果希望这个属性必须定义,则可以使用getRequiredProperty()方法,如下所示:

@Bean
public BlankDisc disc3(){
    return new BlankDisc(environment.getRequiredProperty("disc.title"), environment.getRequiredProperty("disc.artist"));
}

此时,如果disc.title或者disc.artist属性没有定义的话,将会报出IllegalStateException异常。
如果想检查一下某个属性是否存在的话,可以调用:

boolean titleExists = environment.containsProperty("disc.title");

最后,如果想将属性解析为类的话,可以使用getPropertyAsClass()方法,不过现在此方法已经废弃,不建议再使用:

Class<BlankDisc> cdClass = environment.getPropertyAsClass("disc.class", BlankDisc.class);

除了属性相关的功能外,Environment还提供了一些方法来检查哪些profile处于激活状态:

  • String[] getActiveProfiles():返回激活profile名称的数组
  • String[] getDefaultProfiles():返回默认profile名称的数组
  • boolean acceptsProfiles(String… var1):如果Environment支持给定的profile的话就返回true

在ProfileCondition类的实现中,Environment是从ConditionContext中获取的,在bean创建之前,使用acceptsProfiles()方法来确保给定bean所需的profile处于激活状态。

class ProfileCondition implements Condition {
    ProfileCondition() {
    }

    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        if (context.getEnvironment() != null) {
            MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
            if (attrs != null) {
                Iterator var4 = ((List)attrs.get("value")).iterator();

                Object value;
                do {
                    if (!var4.hasNext()) {
                        return false;
                    }

                    value = var4.next();
                } while(!context.getEnvironment().acceptsProfiles((String[])((String[])value)));

                return true;
            }
        }

        return true;
    }
}
解析属性占位符

Spring一直支持将属性定义到外部的属性文件中,并使用占位符值将其插入到Spring bean中。在Spring装配中,占位符的形式为使用”${…}”包装的属性名称。例如,我们可以在XML中按照如下方式解析BlankDisc构造器参数:

<bean id="blankDisc" class="com.godman.expressive.BlankDisc" c:title="${disc.title}" c:artist="${disc.artist}"/>

可以看到,title构造器参数所给定的值是从一个属性中解析得到的,这个属性名称为disc.title,artist参数装配的是名为disc.artist的属性值。按照这种方式,XML配置没有使用任何硬编码的值,它的值是从配置文件以外的一个源中解析得到的。
如果我们依赖于组件和自动装配来创建和初始化应用组件的话,那么就没有指定占位符的配置文件或类了。在这种情况下可以使用@Value注解,它的形式与@Autowired注解非常相似。比如,在BlankDisc类中,构造器可以改为如下所示:

public BlankDisc(@Value("${disc.title}") String title, @Value("${disc.artist}") String artist){
    this.title = title;
    this.artist = artist;
}

为了使用占位符,我们必须要配置一个PropertyPlaceholderConfigurer bean或PropertySourcesPlanceholderConfigurer bean。从Spring3.1开始,推荐使用PropertySourcesConfigurer,因为它能够基于Environment及其属性源来解析占位符。

@Bean
public PropertySourcesPlaceholderConfigurer placeholderConfigurer(){
    return new PropertySourcesPlaceholderConfigurer();
}

如果想使用XML配置的话,Spring context命名空间中的元素将会为你生成PropertySourcesPlaceholderConfigurer bean:

<?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:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd 
       http://www.springframework.org/schema/context ">

    <context:property-placeholder/>

</beans>

解析外部属性能够将值的处理推迟到运行时,但是它的关注点在于根据名称解析来自于Spring Environment和属性源的属性。而Spring表达式语言提供了一种更通用的方式在运行时计算所要注入的值。

5.2 使用Spring表达式语言进行装配

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

  • 使用bean的ID来引用bean;
  • 调用方法和访问对象的属性;
  • 对值进行算术、关系和逻辑运算;
  • 正则表达式匹配;
  • 集合操作。
SpEl样例

SpEl表达式需要放到“#{…}”之中,这与属性占位符非常相似,属性占位符是放在“${…}”之中。如下为一个简单的例子:

#{1}

除去“#{…}”标记之后,剩下的就是SpEl表达式体了,这里是一个数字常量。很明显这里的表达式结果是1。当然在实际应用程序中,不可能使用这么简单的表达式。可能会使用如下的表达式:

#{T(System).currentTimeMillis()}

这个表达式的最终结果就是当前时刻的毫秒数。T{}表达式会将java.lang.System视为Java中的对应类型,因此可以调用其static修饰的currentTimeMillis()方法。
SpEl表达式也可以引用其他的bean或者其他bean的属性,如下是得到ID为blankDisc的bean的title属性:

#{blankDisc.title}

还可以通过systemProperties对象引用系统属性:

#{systemProperties['disc.title']}

如果通过组件扫描创建bean的话,在注入属性和构造器参数时,可以使用@Value注解,这与之前使用的属性占位符非常相似,不过这里使用的是SpEl表达式。

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-命名空间条目的值。如下:

<bean id="blankDisc" class="com.godman.expressive.BlankDisc" c:title="#{systemProperties['disc.title']}" c:artist="#{systemProperties['disc.artist']}"/>
表示字面量

SpEl表达式除了可以用来表示整数的值以外还可以用来表示浮点数、String值以及Boolean值:

#{3.1415926}
#{9.87E4}
#{'Hello'}
#{false}

这些表达式是最基础最简单的,在实际应用中并没有太多用处。

引用bean、属性和方法

SpEl表达式能做的另外一件基础的事就是通过ID引用其他的bean。例如将一个bean装配到另一个bean的属性中:

#{blankDisc}

又或者想引用blankDisc的title属性

#{blankDisc.title}

又或者想调用blankDisc的getTitle()方法:

#{blankDisc.getTitle()}

对于调用方法的返回值来讲,同样可以调用它的方法:

#{blankDisc.getTitle().toUpperCase()}

如果返回值不是null的话,没有什么问题。但是问了避免出现NullPointerException,可以使用类型安全的运算符:

#{blankDisc.getTitle()?.toUpperCase()}

与之前使用点号(.)来访问toUpperCase()方法不同,现在使用了”?.”运算符。这个运算符能够在访问它右边的内容之前,确保它所对应的元素不是null。所以,如果getTitle()的返回值如果是null的话,那么SpEl表达式将不会调用toUpperCase()方法,表达式的返回值将会是null。

在表达式中使用类型

如果需要在SpEl中访问类作用域的方法或者常量的话,需要依赖T()这个关键的运算符。如使用Math类,则需要:

#{java.lang.Math}

这里所示的T()运算符的结果会是一个Class对象,代表了java.lang.Math。如果需要的话甚至可以将其装配到一个Class类型的bean属性中。但是T()运算符的真正价值在于它能够访问目标类型的静态方法和常量

#{java.lang.Math}.PI
SpEl运算符

SpEl提供了多个运算符:

运算符类型运算符
算术运算+、-、*、/、%、^
比较运算<、>、==、<=、>=、lt、gt、eq、le、ge
逻辑运算and、or、not、!
条件运算?: (ternary)、?: (Elvis)
正则表达式matches

算术、比较、逻辑运算符这里不做多的介绍,来看一下三元条件运算符(ternary),它与Java中的三元运算符非常类似。如下:

#{scoreboard.score > 1000 ? 'Winner' : 'Loser'}

三元运算符的另外一个场景就是检查null值,并用一个默认值来代替null。如下的表达式会判断disc.title的值是不是null,如果是null的话,那么表达式的结果就是”hello”

#{disc.title ?: 'hello'}

这种表达式通常称为Elvis运算符。

计算正则表达式

当处理文本时,有时检查文本是否匹配某种模式是非常有用的。SpEl表达式通过matches运算符支持表达式中的模式匹配。matches运算符对String类型的文本(作为左边参数)应用正则表达式(作为右边参数),并返回一个Boolean类型的值。假设,需要判断一个字符串是否包含有效的邮件地址,可以如下所示:

#{admin.email matches '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.com'} 
计算集合

SpEl表达式最令人惊奇的技巧是与集合和数组相关的。最简单的就是引用列表中的一个元素:

#{jukebox.songs[0].title}

这个表达式会计算ID为jukebox的bean的songs集合的第一个元素的title属性。为了让这个表达式更丰富一些,假设从jukebox中随机选择一首歌:

#{jukebox.songs[T(java.lang.Math).random() * jukebox.songs.szie()].title}

“[]”运算符用来从集合或者数组中按照索引获取元素,实际上还可以从String中获取一个字符:

#{'This is a test'[3]}

SpEl表达式还提供了查询运算符(.?[]),它会用来对集合进行过滤,得到集合的一个子集。例如,如果想得到jukebox中artist属性为Aerosmith的所有歌曲:

#{jukebox.songs.?[artist eq 'Aerosmith']}

SpEl表达式还提供了另外两个查询运算符:“.^[]”和“.$[]”,它们分别用用来在集合中查询第一个匹配项和最后一个匹配项。例如,jukebox中artist属性为Aerosmith的第一首歌曲:

#{jukebox.songs.^[artsit eg 'Aerosmith']}

最后,SpEl表达式还提供了投影运算符(.![]),它会从集合中的每个成员中选择特定的属性放到另外一个集合中。例如,将jukebox中title属性放到一个新的集合中:

#{jukebox.songs.![title]}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值