第三章 高级装配

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

配置profile bean

  在3.1版本中, Spring引入了bean profile的功能。 要使用profile, 你首先要将所有不同的bean定义整理到一个或多个profile之中, 在将应用部署
  到每个环境时, 要确保对应的profile处于激活(active) 的状态。
  在Java配置中, 可以使用@Profile注解指定某个bean属于哪一个profile。 例如, 在配置类中, 嵌入式数据库的DataSource可能会配置成如
  下所示

package com.myapp;

import javax.activation.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 DevelopmentProfileConfig {

    @Bean(destroyMethod = "shutdown")
    public DataSource dataSource() {
        return (DataSource) new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .addScript("classpath:schema.sql")
                .addScript("classpath:test-data.sql")
                .build();
    }
}
package com.myapp;

import javax.activation.DataSource;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.jndi.JndiObjectFactoryBean;

@Configuration
@Profile("prod")
public class ProductionProfileConfig {

    @Bean
    public DataSource dataSource() {
        JndiObjectFactoryBean jndiObjectFactory = new JndiObjectFactoryBean();
        jndiObjectFactory.setJndiName("jdbc/myDS");
        jndiObjectFactory.setResourceRef(true);
        jndiObjectFactory.setProxyInterface(javax.sql.DataSource.class);
        return (DataSource) jndiObjectFactory.getObject();
    }
}

  在Spring 3.1中, 只能在类级别上使用@Profile注解。 不过, 从Spring 3.2开始, 你也可以在方法级别上使用@Profile注解, 与@Bean注解一同使用。 这样的话, 就能将这两个bean的声明放到同一个配置类之中, 如下所示:
 @Profile注解基于激活的profile实现bean的装配

package com.myapp;

import javax.activation.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 embbededDataSource() {
        return (DataSource) new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .addScript("classpath:schema.sql")
                .addScript("classpath:test-data.sql")
                .build();
    }

    @Bean
    @Profile("prod")
    public DataSource jndiDataSource() {
        JndiObjectFactoryBean jndiObjectFactory = new JndiObjectFactoryBean();
        jndiObjectFactory.setJndiName("jdbc/myDS");
        jndiObjectFactory.setResourceRef(true);
        jndiObjectFactory.setProxyInterface(javax.sql.DataSource.class);
        return (DataSource) jndiObjectFactory.getObject();
    }
}

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

重复使用元素来指定多个profile

<?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: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: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="java.sql.DataSource"
            />
        </beans>
</beans>

激活profile

  Spring在确定哪个profile处于激活状态时, 需要依赖两个独立的属性: spring.profiles.activespring.profiles.default。 如果设置了spring.profiles.active属性的话,

那么它的值就会用来确定哪个profile是激活的。 但如果没有设置spring.profiles.active属性的话, 那Spring将会查找spring.profiles.default的值。 如果spring.profiles.activespring.profiles.default均没有设置的话, 那就没有激活的profile, 因此只会创建那些没有定义在
profile中的bean

  有多种方式来设置这两个属性:
    作为DispatcherServlet的初始化参数;
    作为Web应用的上下文参数;
    作为JNDI条目;
    作为环境变量;
    作为JVM的系统属性;
    在集成测试类上, 使用@ActiveProfiles注解设置。

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" 
    xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" >
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/spring/root-context.xml</param-value>
</context-param>

<!-- 为上下文设置默认的profile -->
<context-param>
    <param-name>spring.profiles.default</param-name>
    <param-value>dev</param-value>
</context-param>

<listener>
    <listener-class>
    org.springframework.web.context.ContextLoaderListener
    </listener-class>
</listener>

<servlet>
    <servlet-name>appServlet</servlet-name>
    <servlet-class>
        org.springframework.web.servlet.DispatcherServlet
    </servlet-class>
    <!-- 为Servlet设置默认的profile -->
    <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>appServlet</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>

Spring提供了@ActiveProfiles注解, 我们可以使用它来指定运行测试时要激活哪个profile

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = PersistenceTestConfig.class)
@ActiveProfiles("dev")
public class DatasourceTest {
    ...
}

条件化的bean

package com.magic;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;

@Configuration
public class magicConfigure {

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

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

public interface Condition {

    /**
     * Determine if the condition matches.
     * @param context the condition context
     * @param metadata metadata of the {@link org.springframework.core.type.AnnotationMetadata class}
     * or {@link org.springframework.core.type.MethodMetadata method} being checked.
     * @return {@code true} if the condition matches and the component can be registered
     * or {@code false} to veto registration.
     */
    boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);

}

设置给@Conditional的类可以是任意实现了Condition接口的类型。 可以看出来, 这个接口实现起来很简单直接, 只需提
matches()方法的实现即可。 如果matches()方法返回true, 那么就会创建带有@Conditional注解的bean。 如果matches()方法返
false, 将不会创建这些bean

在本例中, 我们需要创建Condition的实现并根据环境中是否存在magic属性来做出决策。 程序清单3.5展现
MagicExistsCondition, 这是完成该功能的Condition实现类:

package com.magic;

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 context, AnnotatedTypeMetadata metadata) {
        Environment env = context.getEnvironment();
        return env.containsProperty("magic");
    }

}

MagicExistsCondition中只是使用了ConditionContext得到的Environment, 但Condition实现的考量因素可能会比这更
多。 matches()方法会得到ConditionContextAnnotatedTypeMetadata对象用来做出决策。
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注解的方法上还有什么其他的注解。 像ConditionContext
样, AnnotatedTypeMetadata也是一个接口。 它如下所示 :

package org.springframework.core.type;

import java.util.Map;

import org.springframework.util.MultiValueMap;

public interface AnnotatedTypeMetadata {

    boolean isAnnotated(String annotationName);

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

    Map<String, Object> getAnnotationAttributes(String annotationName, boolean classValuesAsString);

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

    MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationName, boolean classValuesAsString);

}

借助isAnnotated()方法, 我们能够判断带有@Bean注解的方法是不是还有其他特定的注解。 借助其他的那些方法, 我们能够检
@Bean注解的方法上其他注解的属性。
非常有意思的是, 从Spring 4开始, @Profile注解进行了重构, 使其基于@ConditionalCondition实现。 作为如何使
@ConditionalCondition的例子, 我们来看一下在Spring 4中, @Profile是如何实现的。
@Profile注解如下所示:

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

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

package org.springframework.context.annotation;

import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.MultiValueMap;

class ProfileCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        if (context.getEnvironment() != null) {
            MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
            if (attrs != null) {
                for (Object value : attrs.get("value")) {
                    if (context.getEnvironment().acceptsProfiles(((String[]) value))) {
                        return true;
                    }
                }
                return false;
            }
        }
        return true;
    }

}

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

处理自动装配的歧义性

为了阐述自动装配的歧义性, 假设我们使用@Autowired注解标注了setDessert()方法 ;

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

在本例中, Dessert是一个接口, 并且有三个类实现了这个接口, 分别为CakeCookiesIceCream

@Component
public class Cake implements Dessert

@Component
public class Cookies implements Dessert

@Component
public class IceCream implements Dessert

因为这三个实现均使用了@Component注解, 在组件扫描的时候, 能够发现它们并将其创建为Spring应用上下文里面的bean。 然后, 当Spring
试图自动装配setDessert()中的Dessert参数时, 它并没有唯一、 无歧义的可选值。 在从多种甜点中做出选择时, 尽管大多数人并不会有
什么困难, 但是Spring却无法做出选择。 Spring此时别无他法, 只好宣告失败并抛出异常。 更精确地讲, Spring会抛
NoUniqueBeanDefinitionException

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

标示首选的bean

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

@Component
@Primary
public class IceCream implements Dessert 

如果你使用XML配置bean的话, 同样可以实现这样的功能。 <bean>元素有一个primary属性用来指定首选的bean

<bean id="iceCream" class="com.condition.IceCream" primary="true"/>
@Component
@Primary
public class Cake implements Dessert

现在, 有两个首选的Dessert beanCakeIceCream。 这带来了新的歧义性问题。 就像Spring无法从多个可选的bean中做出选择一样,
它也无法从多个首选的bean中做出选择。 显然, 如果不止一个bean被设置成了首选bean, 那实际上也就是没有首选bean了。
就解决歧义性问题而言, 限定符是一种更为强大的机制, 下面就将对其进行介绍。

限定自动装配的bean

设置首选bean的局限性在于@Primary无法将可选方案的范围限定到唯一一个无歧义性的选项中。 它只能标示一个优先的可选方案。 当首选
bean的数量超过一个时, 我们并没有其他的方法进一步缩小可选范围。
@Qualifier注解是使用限定符的主要方式。 它可以与@Autowired@Inject协同使用, 在注入的时候指定想要注入进去的是哪个bean
例如, 我们想要确保要将IceCream注入到setDessert()之中:

 

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

 

实际上, 还有一点需要补充一下。 更准确地讲, @Qualifier("iceCream")所引用的bean要具有String类型的“iceCream”作为限定符。 如
果没有指定其他的限定符的话, 所有的bean都会给定一个默认的限定符, 这个限定符与beanID相同。 因此, 框架会将具有“iceCream”限定符
bean注入到setDessert()方法中。 这恰巧就是IDiceCreambean, 它是IceCream类在组件扫描的时候创建的。
基于默认的bean ID作为限定符是非常简单的, 但这有可能会引入一些问题。 如果你重构了IceCream类, 将其重命名为Gelato的话, 那此时会
发生什么情况呢? 如果这样的话, beanID和默认的限定符会变为gelato, 这就无法匹配setDessert()方法中的限定符。 自动装配会失
败。

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

@Component
@Qualifier("cold")
public class IceCream implements Dessert

在这种情况下, cold限定符分配给了IceCreambean。 因为它没有耦合类名, 因此你可以随意重构IceCream的类名, 而不必担心会破坏自动
装配。 在注入的地方, 只要引用cold限定符就可以了:

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

可能想到的解决方案就是在注入点和bean定义的地方同时再添加另外一个@Qualifier注解。 Cookies类大致就会如下所示:

@Component
@Qualifier("cold")
@Qualifier("fruity")
public class Cookies implements Dessert

IceCream类同样也可能再添加另外一个@Qualifier注解:

@Component
@Qualifier("cold")
@Qualifier("creamy")
public class IceCream implements Dessert 

 

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

这里只有一个小问题: Java不允许在同一个条目上重复出现相同类型的多个注解。 [1]如果你试图这样做的话, 编译器会提示错误。 在这里, 使
@Qualifier注解并没有办法(至少没有直接的办法) 将自动装配的可选bean缩小范围至仅有一个可选的bean
但是, 我们可以创建自定义的限定符注解, 借助这样的注解来表达bean所希望限定的特性。 这里所需要做的就是创建一个注解, 它本身要使
@Qualifier注解来标注。 这样我们将不再使用@Qualifier("cold"), 而是使用自定义的@Cold注解, 该注解的定义如下所示:

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

同样, 你可以创建一个新的@Creamy注解来代替@Qualifier("creamy")

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

当你不想用@Qualifier注解的时候, 可以类似地创建@Soft@Crispy@Fruity。 通过在定义时添加@Qualifier注解, 它们就具有
@Qualifier注解的特性。 它们本身实际上就成为了限定符注解。
现在, 我们可以重新看一下IceCream, 并为其添加@Cold@Creamy注解, 如下所示:

@Component
@Cold
@Creamy
public class Cookies implements Dessert

bean的作用域

Spring定义了多种作用域, 可以基于这些作用域创建bean, 包括:
  单例(Singleton) : 在整个应用中, 只创建bean的一个实例。
  原型(Prototype) : 每次注入或者通过Spring应用上下文获取的时候, 都会创建一个新的bean实例。
  会话(Session) : 在Web应用中, 为每个会话创建一个bean实例。
  请求(Rquest) : 在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.scope.Notepad" scope="prototype"/>

不管你使用哪种方式来声明原型作用域, 每次注入或从Spring应用上下文中检索该bean的时候, 都会创建新的实例。 这样所导致的结果就是每
次操作都能得到自己的Notepad实例。

使用会话和请求作用域

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

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

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

假设我们要将ShoppingCart bean注入到单例StoreService beanSetter方法中, 如下所示:

@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的代理, 如图3.1所示。 这个
代理会暴露与ShoppingCart相同的方法, 所以StoreService会认为它就是一个购物车。 但是, 当StoreService
ShoppingCart的方法时, 代理会对其进行懒解析并将调用委托给会话作用域内真正的ShoppingCart bean
现在, 我们带着对这个作用域的理解, 讨论一下proxyMode属性。 如配置所示, proxyMode属性被设置成
ScopedProxyMode.INTERFACES, 这表明这个代理要实现ShoppingCart接口, 并将调用委托给实现bean
如果ShoppingCart是接口而不是类的话, 这是可以的(也是最为理想的代理模式) 。 但如果ShoppingCart是一个具体的类的话, Spring
就没有办法创建基于接口的代理了。 此时, 它必须使用CGLib来生成基于类的代理。 所以, 如果bean类型是具体类的话, 我们必须要
proxyMode属性设置为ScopedProxyMode.TARGET_CLASS, 以此来表明要以生成目标类扩展的方式创建代理。
尽管我主要关注了会话作用域, 但是请求作用域的bean会面临相同的装配问题。 因此, 请求作用域的bean应该也以作用域代理的方式进行注
入 。

XML中声明作用域代理

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

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

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

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

运行时值注入

Spring提供了两种在运行时求值的方式:
  属性占位符(Property placeholder)。
  Spring表达式语言(SpEL)。

 注入外部的值
Spring中, 处理外部值的最简单方式就是声明属性源并通过SpringEnvironment来检索属性。 例如, 下面程序清单展现了一个基本的
Spring配置类, 它使用外部的属性来装配BlankDisc bean

package soundsystem;

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/soundsystem/app.properties")
public class ExpressiveCongig {

    @Autowired
    private Environment env;

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

}

深入学习SpringEnvironment
当我们去了解Environment的时候会发现, 程序清单3.7所示的getProperty()方法并不是获取属性值的唯一方法, getProperty()
法有四个重载的变种形式:

    String getProperty(String key);

    String getProperty(String key, String defaultValue);

    <T> T getProperty(String key, Class<T> targetType);

    <T> T getProperty(String key, Class<T> targetType, T defaultValue);
    

前两种形式的getProperty()方法都会返回String类型的值。
剩下的两种getProperty()方法与前面的两种非常类似, 但是它们不会将所有的值都视为String类型。

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

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

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

在这里, 如果disc.titledisc.artist属性没有定义的话, 将会抛出IllegalStateException异常。
如果想检查一下某个属性是否存在的话, 那么可以调用EnvironmentcontainsProperty()方法:

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

最后, 如果想将属性解析为类的话, 可以使用getPropertyAsClass()方法:

Class<CompactDisc> adClass = env.getPropertyAsClass("disc.class", CompactDisc.class);

除了属性相关的功能以外, Environment还提供了一些方法来检查哪些profile处于激活状态:
  String[] getActiveProfiles(): 返回激活profile名称的数组;
  String[] getDefaultProfiles(): 返回默认profile名称的数组;
  boolean acceptsProfiles(String... profiles): 如果environment支持给定profile的话, 就返回true

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

<bean   id="sgPeppers" 
        class="soundsystem.SgtPeppers" 
        c:title="${disc.title}"
        c:artist="${disc.artist}" />
</bean>

可以看到, title构造器参数所给定的值是从一个属性中解析得到的, 这个属性的名称为disc.titleartist参数装配的是名为
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 beanPropertySourcesPlaceholderConfigurer
bean。 从Spring 3.1开始, 推荐使用PropertySourcesPlaceholderConfigurer, 因为它能够基于Spring Environment及其属性源来
解析占位符。
如下的@Bean方法在Java中配置了PropertySourcesPlaceholderConfigurer

    @Bean
    public static PropertySourcesPlaceholderConfigurer placeholderConfigure() {
        return new PropertySourcesPlaceholderConfigurer();
    }

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

<context:property-placeholder/>

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

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

SpEL拥有很多特性, 包括:
  使用beanID来引用bean
  调用方法和访问对象的属性;
  对值进行算术、 关系和逻辑运算;
  正则表达式匹配;
  集合操作。

SpEL样例

需要了解的第一件事情就是SpEL表达式要放到#{ ... }之中, 这与属性占位符有些类似, 属性占位符需要放到${ ... }之中。 下面所
展现的可能是最简单的SpEL表达式了:
#{1}

除去#{ ... }标记之后, 剩下的就是SpEL表达式体了, 也就是一个数字常量。 这个表达式的计算结果就是数字1, 这恐怕并不会让你感到
丝毫惊讶。
当然, 在实际的应用程序中, 我们可能并不会使用这么简单的表达式。 我们可能会使用更加有意思的表达式, 如:

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

#{sgtPeppers.artist}

我们还可以通过systemProperties对象引用系统属性:
#{systemProperties["disc.title"]}

如果通过组件扫描创建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表达式传入<property><constructor-arg>value属性中, 或者将其作为p-命名空间或c-命名空间条
目的值。 例如, 在如下
BlankDisc beanXML声明中, 构造器参数就是通过SpEL表达式设置的:

<bean    id="sgPeppers" 
        class="soundsystem.SgtPeppers" 
        c:title="#{systemProperties[disc.title]}"
        c:artist="#{systemProperties[disc.artist]]}" />
</bean>

表示字面值
我们在前面已经看到了一个使用
SpEL来表示整数字面量的样例。 它实际上还可以用来表示浮点数、 String值以及Boolean值。
下面的
SpEL表达式样例所表示的就是浮点值:

#{3.14159}

数值还可以使用科学记数法的方式进行表示。 如下面的表达式计算得到的值就是98,700

#{9.87E4}

SpEL表达式也可以用来计算String类型的字面值, 如:

#{'Hello'}
最后, 字面值
truefalse的计算结果就是它们对应的Boolean类型的值。 例如:

#{false}

引用bean、 属性和方法
SpEL所能做的另外一件基础的事情就是通过ID引用其他的bean。 例如, 你可以使用SpEL将一个bean装配到另外一个bean的属性中, 此时要
使用
bean ID作为SpEL表达式(在本例中, 也就是sgtPeppers) :

#{sgtPeppers}
现在, 假设我们想在一个表达式中引用
sgtPeppersartist属性:

#{sgtPeppers.artist}
表达式主体的第一部分引用了一个
IDsgtPeppersbean, 分割符之后是对artist属性的引用。
除了引用
bean的属性, 我们还可以调用bean上的方法。 例如, 假设有另外一个bean, 它的IDartistSelector, 我们可以在SpEL表达式
中按照如下的方式来调用
beanselectArtist()方法:

#{artistSelector.selectArtist()}
对于被调用方法的返回值来说, 我们同样可以调用它的方法。 例如, 如果
selectArtist()方法返回的是一个String, 那么可以调
toUpperCase()将整个艺术家的名字改为大写字母形式:

#{artistSelector.selectArtist().toUpperCase()}
如果selectArtist()的返回值不是null的话, 这没有什么问题。 为了避免出现NullPointerException, 我们可以使用类型安全的运
算符:

#{artistSelector.selectArtist()?.toUpperCase()}

与之前只是使用点号(.) 来访问toUpperCase()方法不同, 现在我们使用了“?.”运算符。 这个运算符能够在访问它右边的内容之前, 确保它
所对应的元素不是
null。 所以, 如果selectArtist()的返回值是null的话, 那么SpEL将不会调用toUpperCase()方法。 表达式的返
回值会是
null
在表达式中使用类型
如果要在
SpEL中访问类作用域的方法和常量的话, 要依赖T()这个关键的运算符。 例如, 为了在SpEL中表达JavaMath类, 需要按照如下
的方式使用
T()运算符:

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

T{java.lang.Math}.PI
与之类似, 我们可以调用
T()运算符所得到类型的静态方法。 我们已经看到了通过T()调用System.currentTimeMillis()。 如下的这个
样例会计算得到一个
01之间的随机数:

T{java.lang.Math}.random()
SpEL运算符

SpEL提供了多个运算符, 这些运算符可以用在SpEL表达式的值上。 表3.1概述了这些运算符。

 

posted @ 2018-01-24 18:25  110528844  阅读(226)  评论(0编辑  收藏  举报