【Spring实战】高级装配

本章内容:

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

Spring提供了多种技巧,借助它们可以实现更为高级的bean装配功能。

环境与profile

开发软件,有一个很大的问题就是将应用程序从一个环境迁移到另外一个环境。开发阶段,有些环境相关做法并不适合迁移到生产环境中。比如数据库配置、加密算法以及外部系统的集成。

比如,在开发环境中,我们可能会使用嵌入式数据库,并预先加载测试数据。例如,在Spring配置类中,我们可能会在一个带有@Bean注解的方法上使用EmbeddedDatabaseBuilder:

@Bean(destroyMethod="shutdown")
public DataSource dataSource() {
  return new EmbeddedDatabaseBuilder()
    .addScript("classpath:schema.sql")
    .addScript("classpath:test-data.sql")
    .build()
}

使用EmbeddedDatabaseBuilder会搭建一个嵌入式的Hypersonic数据库,它的模式(schema)定义在schema.sql中,测试数据则是通过test-data.sql加载的。

当你在开发环境中运行集成测试或者启动应用进行手动测试的时候,这个DataSource很有用。每次启动它的时候,都能让数据库处于一个给定的状态。

但是对于生产环境来说,这会是一个糟糕的选择。在生产环境的配置中,如果会希望使用JNDI从容器中获取一个DataSource。如下的@Bean方法会更加合适:

@Bean
public DataSource dataSource() {
  JndiObjectFactoryBean jndiIbjectFactoryBean = new JndiObjectFactoryBean();

  jndiIbjectFactoryBean.setJndiName("jdbc/myDS");
  jndiIbjectFactoryBean.setResourceRef(true);
  jndiIbjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
  return {DataSource} jndiIbjectFactoryBean.getObject();
}

通过JNDI获取Datasource能够让容器决定该如何创建这个DataSource,甚至包括切换为容器管理的连接池。即便如此,JNDI管理的DataSource更加适合于生产环境,对于简单的集成和开发测试环境来说,这会带来不必要的复杂性。

在QA环境中,你可以选择完全不同的DataSource配置,可以配置为Commons DBCP连接池,如下所示:

@Bean(destroyMethod="close")
public DataSource dataSource() {
  BasicDataSource dataSource = new BasicDataSource();
  dataSource.setUrl("jdbc:h2:tcp://dbserver/~/test");
  dataSource.setDriverClassName("org.h2.Driver");
  dataSource.setUsername("sa");
  dataSource.setPassword("password");
  dataSource.setInitialSize(20);
  dataSource.setMaxActive(30);

  return dataSource;
}

三个版本的dataSource()方法互不相同。虽然它们都会生成一个类型为javax.sql.DataSource的bean,但每个方法都使用了完全不同的策略来生成DataSource bean。

我们必须要有一种方法来配置DataSource,使其在每种环境下都会选择最为合适的配置。

其中一种方式就是在单独的配置类(或XML文件)中配置每个bean,然后在构建阶段(可能会使用Maven的profiles)确定要将哪一个配置编译到可部署的应用中。这种方式的问题在于**要为每种环境重新构建应用。

Spring所提供的解决方案并不需要重新构建。

配置profile bean

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

Spring引入了bean profile的功能。要使用profile,首先要将所有不同的bean定义整理到一个或多个profile之中,在将应用部署到每个环境的时候,要确保对应的profile处于激活(active)的状态。

Java配置profile

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

package com.myapp;

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

上面的实例的@Profile注解应用在了类级别上。它告诉Spring这个类中的bean只有在dev profile激活时才会创建。如果
dev profile没有激活的话,那么对应的带有@Bean注解的方法都会被忽略掉。

同时可能还需要配置一个生产环境的配置。

package com.myapp;

import javax.sql.DataSource;

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

@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();
  }

}

从Spring 3.2开始,可以在方法级别上使用@Profile注解。这样便可以将这两个bean的声明放到同一个配置类中。

package com.myapp;

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

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

}
在XML中配置profile

也可以通过<beans>元素的profile属性,在XML中配置profile bean。例如,为了在XML中定义适用于开发阶段的嵌入式数据库DataSourcebean,可以创建如下所示的XML文件:

<?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属性设置为prod,创建适用于生产环境的从JNDI获取的DataSource bean。同样可以创建基于连接池定义的DataSource bean,将其放在另外一个XML文件中,并标注为qa profile。所有的配置文件都会放到部署单元之中
,但是只有profile属性与当前激活profile相匹配的配置文件才会被用到。

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

<?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">

  <beans profile="dev">
    <jdbc:embedded-database id="dataSource" type="H2">
      <jdbc:script location="classpath:schema.sql" />
      <jdbc:script location="classpath:test-data.sql" />
    </jdbc:embedded-database>
  </beans>

  <beans profile="qa">
    <bean id="dataSource" 
    class="org.apache.commons.dbcp.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:maxAcitve="30" />
  </beans> 

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

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

激活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注解设置
使用DispatcherServlet的初始化参数

使用DispatcherServlet的参数将spring.profiles.default设置为开发环境的profile,会在Servlet上下文中进行设置(为了兼顾到ContextLoaderListener)。在Web应用中,设置spring.profiles.default的web.xml文件会如下所示:

web.xml

按照这种方式设置spring.profiles.default,所有的开发人员都能从版本控制软件中获得应用程序源码,并使用开发环境的设置运行代码,不需要任何额外的配置。

当应用程序部署到QA、生产或其他环境之中时,根据情况使用系统属性、环境变量或JNDI设spring.profiles.active即可。

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

使用profile进行测试

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

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

  @RunWith(SpringJUnit4ClassRunner.class)
  @ContextConfiguration(classes=DataSourceConfig.class)
  @ActiveProfiles("dev")
  public static class DevDataSourceTest {
    ...
  }

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

条件化的bean

暂时跳过

处理自动装配的歧义性

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

例如:

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

Dessert是一个接口,并且有三个类实现了这个接口:

@Component
public class Cake implements Dessert { ... }

@Component
public class Cookies implements Dessert { ... }

@Component
public class Iceream implements Dessert { ... }

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

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

标示首选的bean

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

@Primary能够与@Component组合用在组件扫描的bean上,也可以与@Bean组合用在Java配置的bean声明中。

@Component
@Primary
public class Iceream implements Dessert { ... }
@Bean
@Primary
public Dessert iceCream() {
  return new IceCream();
}

如果使用XML配置bean,同样可以实现这样的功能,通过使用<bean>元素中的primary属性指定首选的bean:

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

就解决歧义性问题而言,相较于使用首选标示,限定符是一种更为强大的机制。

限定自动装配的bean

设置首选bean的局限性在于@Primary无法将可选方案的范围限定到唯一一个无歧义性的选项中。它只能标示一个优先的可选方案。当首选bean多于一个,并没有其他的方法缩小可选范围。

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

@Qualifier注解是使用限定符的主要方式。可以与@Autowired和@Inject协同使用,在注入的时候指定想要注入进去的是哪个bean:

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

为@Qualifier注解所设置的参数就是想要注入的bean的ID。

所有使用@Component注解声明的类都会创建为bean,并且bean的ID为首字母变为小写的类名。因此,@Qualifier("iceCream")指向的是组件扫描时所创建的IceCream类的实例bean。

@Qualifier("iceCream")所引用的bean要具有String类型的“iceCream”作为限定符。

基于默认的bean ID作为限定符时,限定符与要注入的bean的名称是紧耦合的,有可能会引入一些问题。对类名称的任意改动都会导致限定符失效,无法匹配限定符。导致自动装配失败。

创建自定义的限定符

我们可以为bean设置自己的限定符,而不是依赖于将bean ID作为限定符。所需要做的就是在bean声明(@Component注解的类)上添加@Qualifier注解:

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

cold限定符分配给了IceCreambean。因为它没有耦合类名,因此可以随意重构IceCream的类名,而不必担心会破坏自动装配。

在注入的地方,只要引用cold限定符就可以了:

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

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

@Bean
@Qualifier("cold")
public Dessert iceCream() {
    return new IceCream();
}
使用自定义的限定符注解

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

再引入一个新的Dessert bean:

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

这样,在自动装配Dessert bean的时候,再次遇到了歧义性的问题,需要使用更多的限定符来将可选范围限定到只有一个bean。

可能的解决方案是在注入点和bean定义的地方同时再添加另外一个@Qualifier注解:

@Component
@Qualifier("cold")
@Qualifier("creamy")
public class IceCream implements Dessert() { ... }
@Component
@Qualifier("cold")
@Qualifier("fruity")
public class Popsicle implements Dessert { ... }

在注入点:

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

Java不允许在同一个条目上重复出现相同类型的多个注解。如果试图这样做,编译器会提示错误。

但是可以创建自定义的限定符注解,借助这样的注解来表达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注解,它们就具有了@Qualifier注解的特性。它们本身实际上就成为了限定符注解。

重新为IceCream添加@Cold和@Creamy注解:

@Component
@Cold
@Creamy
public class IceCream implements Dessert() { ... }

为Popsicle类添加@Cold和@Fruity注解:

@Component
@Cold
@Fruity
public class Popsicle implements Dessert() { ... }

最终,在注入点,我们使用必要的限定符注解进行任意组合,从而将可选范围缩小到只有一个bean满足需求:

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

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

没有在任何地方明确指定要将IceCream自动装配到setDessert()方法中。因此,setDessert()方法依然能够与特定的Dessert实现保持解耦。任意满足这些特征的bean都是可以的。

bean的作用域

默认情况下,Spring应用上下文中所有bean都是作为以单例(singleton)的形式创建的。也就是不管给定的一个bean被注入到其他bean多少次,每次所注入的都是同一个实例。

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

有时候,所使用的类是易变的(mutable),它们会保持一些状态,因此重用是不安全的。在这种情况下,不应该将class声明为单例的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_PROTYPE)
public Notepad notepad() {
    return new Notepad();
}

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

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

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

对购物车bean来说,会话作用域是最为合适的,因为它与给定的用户关联性最大。指定会话作用域,它的使用方式与指定原型作用域是相同的:

@Bean
@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中所遇到的问题。

假设我们要将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的代理,如下图所示:

作用域代理能够延迟注入请求和会话作用域的bean

这个代理会暴露于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="cart"
    class="com.myapp.ShoppingCart"
    scope="session">
  <aop:scope-proxy />
</bean>

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

<bean id="cart"
    class="com.myapp.ShoppingCart"
    scope="session">
  <aop:scope-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:aop="http://www.springframework.org/schema/aop"
  xsi:schemaLocation="
    http://www.springframework.org/schema/aop
    http://www.springframework.org/schema/aop/spring-aop.xsd
    http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd">
  ...
</beans>

运行时注入

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

bean装配的另外一个方面指的是将一个值注入到bean的属性或者构造器参数中。如果按照这样的方式来组装BlankDisc:

组装BlankDisc

尽管这实现了需求,为BlankDisc bean设置title和artist,但它在实现的时候是将值硬编码在配置类中的。

有时想让这些值在运行时再确定。Spring提供了两种在运行时求值的方式:

  • 属性占位符(Property placeholder)。
  • Spring表达式语言(SpEL)。
注入外部的值

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

package com.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 EnvironmentConfig {

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

@PropertySource引用了类路径中一个名为app.properties的文件。这个属性文件会加载到Spring的Environment中,稍后可以从这里检索属性。同时,在disc()方法中,会创建一个新的BlankDisc,它的构造器参数是从属性文件中获取的,这是通过调用getProperty()实现的。

深入学习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> rype, T defaultValue)

前两种形式的getProperty()方法都会返回String类型的值。你可以稍微对@Bean方法进行一下修改,这样在指定属性不存在的时候,会使用一个默认值:

  @Bean
  public BlankDisc blankDisc() {
    return new BlankDisc(
        env.getProperty("disc.title", "Rattle and Hum"),
        env.getProperty("disc.artist", "U2"));
  }

剩下的两种getProperty()方法与前面的两种非常类似,但是它们不会将所有的值都视为String类型。例如,你想要获取的值所代表的含义是连接池中所维持的连接数量。如果使用重载形式的getProperty()的话,就能非常便利地解决这个问题:

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

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

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

如果disc.title或disc.artist属性没有定义的话,将会抛出IllegalStateException异常。

想检查一下某个属性是否存在,可以调用Environment的containsProperty()方法:

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

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

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

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

  • String[] getActiveProfiles():返回激活profile名称的
    数组;
  • String[] getDefaultProfiles():返回默认profile名称的
    数组;
  • boolean acceptsProfiles(String... profiles):如
    果environment支持给定profile的话,就返回true。

除了直接从Environment中检索属性外,Spring也提供了通过占位符装配属性的方法,这些占位符的值会来源于一个属性源。

解析属性占位符

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

<bean id="sgtPeppers"
    class="com.soundsystem.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或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-4.0.xsd">

  <context:property-placeholder />
    
</beans>

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

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

Spring 3引入了Spring表达式语言(Spring Expression Language,SpEL),它能够以一种强大简洁的方式将值装配到bean属性和构造器参数中,在这个过程中所使用的表达式会在运行时计算得到值。

SpEL拥有很多特性,包括:

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

SpEL表达式要放到#{ ... }之中,这与属性占位符有些类似,属性占位符需要放到${ ... }之中。

#{1}

上面的例子除去#{ ... }标记之后,剩下的就是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']}

接下来看一下在bean装配的时候如何使用这些表达式。

如果通过组件扫描创建bean的话,在注入属性和构造器参数时,可以使用@Value注解,下面的样例展现了BlankDisc,它会从系统属性中获取专辑名称和艺术家的名字:

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

那现在就来学习一下SpEL所支持的基础表达式。

表示字面值

浮点值

#{3.14159}

科学计数法

#{9.87E4}

String类型字面值

#{'Hello'}

Boolean类型值(true | false)

#{true}
引用bean、属性和方法

使用SpEL可以将一个bean装配到另外一个bean的属性中

#{sgtPeppers}

在一个表达式中引用sgtPeppers的artist属性

#{sgtPeppers.artist}

还可以调用bean上的方法。

#{artistSelector.selectArtist()}

对被调用的返回值同样可以调用它的方法

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

上面没有考虑到返回值为null的情况,可以使用类型安全的运算符

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

?.这个运算符能够在访问它右边的内容之前,确保它所对应的元素不是null。如果selectArtist()的返回值是null的话,那么SpEL将不会调用toUpperCase()方法。表达式的返回值会是null

在表达式中使用类型

如果要在SpEL中访问类作用域的方法和常量的话,要依赖T()这个运算符。例如,为了在SpEL中表达Java的Math类,需要按照如下的方式使用T()运算符:

T(java.lang.Math)

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

例如需要将PI值装配到bean属性中

T(java.lang.Math).PI

类似地,我们可以调用T()运算符所得到类型的静态方法。如下样例会计算得到一个0到1之间的随机数:

T(java.lang.Math).random()
SpEl运算符

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

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

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

#{2 * T(java.lang.Math).PI * circle.radius}

SpEL还提供了查询运算符(.?[]),它会用来对集合进行过滤,得到集合的一个子集。作为阐述的样例,假设你希望得到jukeboxartist属性为Aerosmith的所有歌曲。如下的表达式就使用查询运算符得到了Aerosmith的所有歌曲:

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

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

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

SpEL还提供了投影运算符(.![]),它会从集合的每个成员中选择特定的属性放到另外一个集合中。作为样例,假设我们不想要歌曲对象的集合,而是所有歌曲名称的集合。如下的表达式会将title属性投影到一个新的String类型的集合中:

#{jukebox.songs.![title]}

投影操作也可以与其他任意的SpEL运算符一起使用。比如,我们可以使用如下的表达式获得Aerosmith所有歌曲的名称列表:

#{jukebox.songs.^[artist eq 'Aerosmith'].![title]}

推荐阅读更多精彩内容