第三章 高级装配

配置profile bean

Spring为环境相关的bean所提供的解决方案其实与构建时的方案没有太大的差别。当然,在这个过程中需要根据环境决定该创建哪个bean和不创建哪个bean。不过Spring并不是在构建的时候做出这样的决策,而是等到运行时再来确定。这样的结果就是同一个部署单元能够适用于所有的环境,没有必要进行重新构建。

在3.1版本中,Spring引入了bean profile功能。注解使用方式:在类文件上注解@Profile("dev")。

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

例如,生产环境的配置类如下:

package com.chenjl.config;

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

import javax.sql.DataSource;

@Profile("prod")
@Configuration
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.getObject();
    }
}

在方法级别上使用配置,如下:

package com.chenjl.config;

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;

import javax.sql.DataSource;

@Configuration
public class DataSourceConfig {

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

    @Bean
    @Profile("dev")
    public DataSource embeddedDataSource() {
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .addScript("classpath:schema.sql")
                .addScript("classpath:test-data.sql")
                .build();
    }

}

尽管每个DataSource bean都被声明在一个profile中,并且只有当规定的profile激活时,响应的bean才会被创建,但是可能会有其它的bean并没有声明在一个给定的profile范围内。没有指定profile的bean始终都会被创建,与激活哪个profile没有关系。

在XML中配置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"
       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>

还可以在根<beans>元素中嵌套定义<beans>元素,而不是为每个环境都创建一个profile XML文件。这能够将所有的profile bean定义放到同一个XML文件中。

重复使用元素来指定多个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"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="
        http://www.springframework.org/schema/jee
        http://www.springframework.org/schema/jee/spring-jee.xsd
        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="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="qa">
        <bean id="dataSource"
              class="com.chenjl.config.BasicDataSource"
              destroy-method="close"
              p:url="jdbc:h2:tcp://dbserver/~/test"
              p:driverClassName="org.h2.Driver"
              p:username="sa"
              p:password="password"
              p:initialSize="20"
              p:maxActive="30"/>
    </beans>

    <beans profile="prod">
        <jee:jndi-lookup id="dataSource"
                         jndi-name="jdbc/myDatabase"
                         resource-ref="true"
                         proxy-interface="javax.sql.DataSource"/>
    </beans>

</beans>

除了所有的bean定义到了同一个XML文件之中,这种配置方式与定义在单独的XML文件中的实际效果是一样的。这里有三个bean,类型都是javax.sql.DataSource,并且ID都是dataSource。但是在运行时,只会创建一个bean,这取决于处于激活状态的是哪个profile。

激活profile

Spring在确定哪个profile处于激活状态时,需要依赖两个独立的属性:spring.profiles.active和spring.profiles.default。如果设置了spring.profiles.active属性的话,它的值就会用来确定哪个profiles是激活的;如果没有设置spring.profiles.active属性的话,那Spring将会查找spring.profiles.default的值。如果spring.profiles.active和spring.profiles.default均没有设置的话,那就没有激活的profile,因此只会创建那些没有定义在profile中的bean。

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

1、作为DispatcherServlet的初始化参数;

2、作为Web应用的上下文参数;

3、作为JNDI条目;

4、作为环境变量;

5、作为JVM的系统属性;

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

设置案例如下:

<?xml version="1.0" encoding="UTF-8"?>

<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         id="WebApp_ID" version="3.1">

  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/config/spring-config.xml</param-value>
  </context-param>
  
  <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>
    <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.profiles.default,所有的开发人员都能从版本控制软件中获得应用程序源码,并使用开发环境的设置运行代码,而不需要任何额外的配置。

当应用程序部署到QA、生产或其它环境之中时,负责部署的人根据情况使用系统属性、环境变量或JNDI设置spring.profiles.active即可。当设置spring.profiles.active之后,至于spring.profiles.default设置成什么值已经无所谓了,系统会优先使用spring.profiles.active中所设置的profile。

你可能已经注意到profiles是复数形式,你猜到可以设置多个激活的profile,并以逗号分隔来实现。

使用profile进行测试

当运行集成测试时,通常会希望采用与生产环境相同的配置进行测试。但是,如果配置中的bean定义在了profile中,那么在运行测试时,我们就需要有一种方式来启用合适的profile。

Spring提供了@ActiveProfiles注解,我们可以使用它来指定运行测试时要激活哪个profile。在集成测试时,通常想要激活的是开发环境的profile。例如,下面的测试代码片段展现了使用@ActiveProfiles激活dev profile:

package com.chenjl.test;

import org.junit.runner.RunWith;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

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

在条件化创建bean方面,Spring的profile机制是一种很棒的方法,这里的条件要基于哪个profile处于激活状态来判断。Spring 4.0中提供了一种更为通用的机制来实现条件化的bean定义,在这种机制之中 ,条件完全由你来确定。

条件化的bean

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

例如,假设有一个名为MagicBean的类,我们希望只有设置了magic属性的时候,Spring才会实例化这个类。如果环境中没有这个属性,那么MagicBean将会被忽略。在下面的代码中,条件化地配置了MagicBean。

package com.chenjl.config;

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

@Configuration
public class Config {

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

}
package com.chenjl.config;

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");
    }
}
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.context.annotation;

import org.springframework.core.type.AnnotatedTypeMetadata;

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

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

Condition中地matches方法会得到ConditionContext和AnnotatedTypeMetadata对象用来做决策。

ConditionContext是一个接口,大致如下所示:

public interface ConditionContext {
    // 返回bean定义
    BeanDefinitionRegistry getRegistry();
    // 检查bean是否存在,甚至探查bean的属性
    ConfigurableListableBeanFactory getBeanFactory();
    // 检查环境变量是否存在以及它的值是什么
    Environment getEnvironment();
    // 返回ResourceLoad所加载的资源
    ResourceLoader getResourceLoader();
    // 加载并检查类是否存在
    ClassLoader getClassLoader();
}
AnnotatedTypeMetadata能让我们检查带有@Bean注解的方法上还有什么其它的注解。
isAnnotated判断带有@Bean注解的方法是不是还有其它特定的注解。然后借助其它的那些方法,我们能够检查@Bean注解的方法上其它注解的属性。
从Spring4开始,@Profile注解进行了重构,使其基于@Conditional和@Condition实现。
在Spring4中,@Profile注解实现如下:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@Conditional({ProfileCondition.class})
public @interface Profile {
    String[] value();
}

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

下面的代码是ProfileCondition检查某个bean profile是否可用

class ProfileCondition implements Condition {

    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得到的Enviroment来检查该profile是否处于激活状态。

处理自动装配的歧义性

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

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

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

在本例中,Dessert是一个接口,并且有三个类实现了这个接口,分别为Cookie、Cake、IceCream:

@Component
public class Cake implements Dessert{}

@Component
public class Cookie implements Dessert{}

@Component
public class IceCream implements Dessert{}

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

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

标示首选的bean

当遇到歧义时,Spring将会使用首选的bean,而不是其它可选的bean。

下面的代码将@Component注解的IceCream bean声明为首选的bean:

@Component
@Primary
public class IceCream implements Dessert{}

其可以用在使用Java来配置bean上面,也可以用在XML配置上面。

不过,如果标识了两个或者多个首选的bean时,就又带来了新的歧义性。

限定自动装配的bean

@Qualifier注解是使用限定符的主要方式。在注入的时候指定想要注入的是哪个bean。例如,我们想要确保要将IceCream注入到setDessert()之中:

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

这是使用限定符的最简单的例子。为@Qualifier注解所设置的参数就是想要注入的bean的ID。所有使用@Component注解声明的类都会创建为bean,并且bean的ID为首字母变为小写的类名。因此,@Qualifier("iceCream")所引用的bean要具有String类型的"iceCream"作为限定符。如果没有指定限定符的话,所有的bean都会给定一个默认的限定符,这个限定符与bean的ID相同。因此,框架会将具有“iceCream”限定符的bean注入到setDessert()方法中。

基于默认的bean ID作为限定符是非常简单的,但这有可能会引入一些问题。如果你重构了IceCream类,将其重命名为Gelato的话,那此时自动装配就失败了。

这里的问题在于setDessert()方法上所指定的限定符与要注入的bean的名称是紧耦合的,对类名称的随意改动都会导致限定符失败。

创建自定义的限定符

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

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

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

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

通过Java配置显示定义的时候,@Qualifier也可以与@Bean注解一起使用:

@Bean
    @Qualifier("cake")
    public Dessert dessert() {
        return new Cake();
    }

使用自定义的限定符注解

面向特性的限定符要比基于bean ID的限定符更好一些。但是,如果多个bean都引用相同特性的话,这种方式也会出现问题。

所以我们可以创建自定义的限定符注解,借助这样的注解来表达bean所希望限定的特性。这里所需要做的就是创建一个注解,它本身要使用@Qualifier注解来标注。这样我们将不再使用@Qualifier("cold")注解来标注,而是使用自定义的@Cold注解,该注解的定义如下所示:

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

通过在自定义时添加@Qualifier注解,它们就具有了@Qualifier注解的特性。它们本身实际上就成为了限定符注解。

现在,我们就可以重新给IceCream添加注解了,如下:

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

最终,在注入点,我们使用必要的限定符注解进行任意组合,从而可以将可选范围缩小到只有一个bean满足要求。为了得到IceCream bean,setDessert()方法可以这样使用注解:

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

通过声明自定义的限定符注解,我们可以同时使用多个限定符,不会再有java编译器的限制或错误。于此同时,相对于使用原始的@Qualifier并借助String类型来指定限定符,自定义的注解也更为类型安全。

bean的作用域

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

在大多数情况下,单例是最理想的方案。初始化和垃圾回收对象实例所带来的成本只留给一些小规模的任务,在这些任务中,让对象保持无状态并且在应用中反复重用这些对象可能并不合理。

有时候,你可能会发现,你所使用的类是易变的(mutable),它们会保持一些状态,因此重用是不安全的。在这种情况下,将class声明为单例的bean就不是什么好主意了,因为对象会被污染,稍后重用的时候会出现意想不到的问题。

Spring定义了多种作用域,可以基于这些作用域创建bean:

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

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

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

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

单例是默认的作用域,但是正如之前所述,对于易变的类型,这并不合适。如果选择其它的作用域,要使用@Scope注解,它可以与@Component或@Bean一起使用。

例如,如果你使用组件扫描来发现和声明bean,那么你可以在bean的类上使用@Scope注解,将其声明为原型bean:

    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public Dessert dessert() {
        return new Cake();
    }

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

<bean id="notepad" class="com.chenjl.config.IceCream" scope="prototype"/>

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

使用会话和请求作用域

 在Web应用中,如果能够实例化在会话和请求范围内共享的bean,那将是非常有价值的事。例如,在典型的电子商务应用中,可能会有一个bean代表用户的购物车。如果购物车是单例的话,那么将会导致所有的用户都会向同一个购物车中添加商品。另一方面,如果购物车是原型作用域的,那么在应用中某一个地方添加商品,在应用的另一个地方可能就不用了,因为在这里注入的是另外一个原型作用域的购物车。

就购物车bean来说,会话作用域是最为合适的,因为它与给定的用户关联性最大。要指定会话最用域,我们可以使用@Scope注解,它的使用方式与指定原型作用域是相同的:

@Bean
    @Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.INTERFACES)
    public ShopCart cat() {
        return new ShopCart();
    }

这里,将value值设置为WebApplicationContext中的SCOPE_SESSION常量(它的值是session)。这会告诉Spring为Web应用中的每个会话创建一个ShopCart。这会创建多个ShopCart bean的实例,但是对于给定的会话只会创建一个实例,在当前会话相关的操作中,这个bean实际相当于单例的。

本例中,@Scope同时还有一个proxyMode属性,它被设置成了ScopedProxyMode.INTERFACES。这个属性解决了将会话或请求作用域的bean注入到单例bean中所遇到的问题。在描述proxyMode属性之前,先来看下proxyMode所要解决问题的场景。

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

因为StoreService是一个单例的bean,会在Spring应用上下文加载的时候创建。当它创建的时候,Spring会试图将ShopCart bean注入到setShopCart()方法中。但是ShopCart bean是会话作用域的,此时并不存在。直到某个用户进入系统,创建了会话之后,才会出现ShopCart实例。

另外,系统中将会有多个ShopCart实例:每个用户一个。我们并不想让Spring注入某个固定的ShopCart实例到StoreService中。我们希望的是当StoreService处理购物车功能时,它所使用的实例恰好是当前会话的那一个。

Spring并不会将实际的ShopCart bean注入到StoreService中,Spring会注入一个到ShopCart bean的代理,这个代理会暴漏与ShopCart相同的方法,所以StoreService会认为它就是一个购物车。但是,当StoreService调用ShopCart的方法时,代理会对其进行懒解析并将调用委托给会话作用域内真正的ShopCart bean。

本例中,proxyMode属性被设置成了ScopedProxyMode.INTERFACES,这表明这个代理要实现ShopCart接口,并将调用委托给实现bean。

如果ShopCart是接口而不是类的话,这是可以的(也是最为理想的代理模式)。但如果ShopCart是一个具体的类的话,Spring就没有办法创建基于接口的代理了。此时,它必须使用CGLib来生成基于类的代理。所以,如果bean类型是具体类的话,我们必须要将proxyMode属性设置为ScopedProxyMode.TARGET_CLASS,以此来表明要以生成目标类扩展的方式创建代理。

尽管我主要关注了会话作用域,但是请求作用域的bean会面临相同的装配问题。因此,请求作用域的bean应该也以作用域代理的方式进行注入。

在XML中声明作用域代理

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

要设置代理模式,我们需要使用Spring aop命名空间的一个新元素:

<bean id="cart" class="com.chenjl.config.ShopCart" 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.chenjl.config.ShopCart" scope="session">
        <aop:scoped-proxy proxy-target-class="false"/>
    </bean>

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

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

运行时值注入

当讨论依赖注入的时候,我们通常讨论的是将一个bean引用注入到另一个bean的属性或构造器参数中。它通常来讲指的是将一个对象与另一个对象进行关联。

但是bean装配的另一个方面指的是将一个值注入到bean属性或者构造器参数中。前面章节中已经实现过将值注入到bean属性中,但它是硬编码的,有时候硬编码是可以的,但有时候,我们可能希望避免硬编码值,而是想让这些值在运行时再确定。为了实现这些功能,Spring提供了两种在运行时求值的方式:

1、属性占位符(Property placeholder)

2、Spring表达式语言(SpEL)

很快你就会发现这两种技术的用法是类似的,不过它们的目的和行为是有所差别的。

注入外部的值

在Spring中,处理外部值的最简单方式就是声明属性源并通过Spring的Environment来检索属性。例如,下面的代码展现了基本的配置类,它使用基本的属性来装配BlankDisc bean。

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

    @Autowired
    Environment env;

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

这个属性文件会加载到Spring的Enviroment中,稍后可以从这里检索属性。同时,在disc()方法中,会创建一个新的BlankDisc,它的构造器参数是从属性文件中获取的,而这是通过调用getProperty()实现的。

深入学习Spring的Enviroment

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

String getProperty(String var1);

String getProperty(String var1, String var2);

<T> T getProperty(String var1, Class<T> var2);

<T> T getProperty(String var1, Class<T> var2, T var3);

前两种形式的getProperty()方法都会返回String类型的值。以下代码会在查找属性找不到的时候返回一个默认值:

@Bean
    public BlankDisc disc() {
        return new BlankDisc(env.getProperty("disk.title", "凤凰传奇"), env.getProperty("disk.artist", "最炫民族风"));
    }

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

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

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

return new BlankDisc(env.getRequiredProperty("disk.title"), env.getRequiredProperty("disk.artist"));

在这里,如果属性没有定义的话,将抛出IllegalStateException异常。

如果想检查某个属性是否存在的话,那么可以调用Environment的containsProperty()方法:

boolean titleExitst = env.containsProperty("disk.title");

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

Class<BlankDisc> propertyAsClass = env.getPropertyAsClass("disc.class", BlankDisc.class);

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

String[] getActiveProfiles():返回激活profile名称的数组;

String[] getDefaultProfiles():返回默认profile名称的数组;

boolean acceptsProfiles(String... profiles):如果environment支持给定profile的话,就返回true。

使用acceptsProfiles确保给定bean所需的profile处于激活状态。

直接从Environment中检索属性是非常方便的,尤其是在Java配置中装配bean的时候。但是,Spring也提供了通过占位符装配属性的方法,这些占位符的值会来源于一个属性源。

解析属性占位符

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

<bean id = "blankDisc" class="com.chenjl.config.BlankDisc" c:artist="${disk.artist}" c:title="${disk.title}"/>

可以看到,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或PropertySourcesPlaceholderConfigurer bean。从Spring 3.1开始,推荐使用PropertySourcesPlaceholderConfigurer,因为它能够基于Spring Environment及其属性源来解析占位符。

如下的@Bean方法在Java中配置了PropertySourcesPlaceholderConfigurer:

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

如果你想使用XML配置的话,Spring context命名空间中的<context:propertyplaceholder>元素将会为你生成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 http://www.springframework.org/schema/context/spring-context.xsd">
    <context:property-placeholder/>

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

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

 Spring 3引入了Spring表达式语言(Spring Expression Language,SpEL),它能够以一种强大和简洁的方式将值装配到bean属性和构造器参数中,在这个过程中所使用的表达式会在运行时计算得到值。使用SpEL,你可以实现超乎想象的装配效果,这是使用其它的装配技术难以做到的。

SpEL拥有很多属性,包括:

1、使用bean的ID来引用bean;

2、调用方法和访问对象的属性;

3、对值进行算术、关系和逻辑运算;

4、正则表达式匹配;

5、集合操作。

并且SpEL能够用在依赖注入以外的其它地方。Spring Security支持使用SpEL表达式定义安全限制规则。另外,如果你在Spring MVC应用中使用Thymeleaf模板作为视图的话,那么这些模板可以使用SpEL表达式引用模型数据。

作为起步,我们看几个SpEL表达式的样例,以及如何将其注入到bean中。然后我们会深入学习一些SpEL的基础表达式,它们能够组合起来形成更为强大的表达式。

SpEL是一种非常灵活的表达式语言,它要放到“#{...}”之中,这与属性占位符有些类似,属性占位符需要放到"${...}"之中。下面所展现的可能是最简单的SpEL表达式了:

#{1}

除去"#{...}"标记之后,剩下的就是SpEL表达式体了,也就是一个数字常量。这个表达式的计算结果就是数字1。

在实际的应用程序中,可能使用会使用更加有意思的表达式,如:

#{T(System).currentTimeMills()}

它的最终结果是计算表达式的那一刻当前时间的毫秒数。T()表达式会将java.lang.System视为Java中对应的类型,因此可以调用其static修饰的currentTimeMills()方法。

SpEL表达式也可以引用其它bean或其它bean的属性。例如,如下的表达式会计算得到ID为sgtPeppers的bean的artist属性:

#{sgtPeppers.artist}

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

#{systemProperties['disc.title']}

在bean装配的时候如何使用这些表达式呢?

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

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

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

<bean id="blankDisc2" class="com.chenjl.config.BlankDisc"
          c:title="#{systemProperties['disc.title']}"
          c:artist="#{systemProperties['disc.artist']}"/>

我们已经看过了几个简单的样例,也学习了如何将SpEL解析得到的值注入到bean中,那现在就来继续学习一下SpEL所支持的基础表达式。

表示字面值

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

下面的SpEL表达式样例所表示的就是浮点值:#{3.14159}

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

#{9.87E4}

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

#{‘Hello’}

 最后,字面量true和false的计算结果就是它们对应的Boolean类型的值。例如:

#{false}

 在SpEL中使用字面量其实没有太大的意思,但更有意思的SpEL表达式是由更简单的表达式组成的,因此了解在SpEL中如何使用字面量还是很有用处的。

引用bean、属性和方法

 SpEL所能做的另外一件事情就是通过ID引用其它的bean。例如,你可以使用SpEL将一个bean装配到另外一个bean的属性中,此时要使用bean ID作为SpEL表达式:

#{sgtPeppers}

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

表达式主体的第一部分引用了一个ID为sgtPeppers的bean,分割符之后是对artist属性的引用。

除了引用bean的属性,我们还可以调用bean的方法。例如,假设有另外一个bean,它的ID为artistSelector,我们可以在SpEL表达式中按照如下的方式来调用bean的selectArtist()方法:

#{artistSelector.selectArtist()}

对于被调用方法的返回值来说,我们同样可以调用它的方法。

为了避免出现NullPointException,我们可以使用类型安全的运算符:#{artistSelector.selectArtist()?.toUpperCase()}

如果返回的不是NULL的话,将会调用toUpperCase()方法;否则返回NULL。

SpEL运算符

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

 作为使用上述运算符的一个简单样例,我们看一下下面这个SpEL表达式:

#{2*T(java.lang.Math).PI*circle.radius}  计算圆的周长

#{T(java.lang.Math).PI*circle.radius^2}  计算圆的面积

 等等组合使用。

还可以计算正则表达式

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

上面正则是判断一个字符串是否包含有效的邮件地址。

还可以计算数组

#{jukebox.songs[4].title}

 这个表达式会计算songs集合中第五个元素的title属性。

 示例代码地址:https://github.com/1977288116/AdvancedAssembly.git

 

posted @ 2020-05-29 22:28  咸鱼,也有梦想!  阅读(118)  评论(0)    收藏  举报