Spring高级装配

一、环境与profile

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

1、配置profile bean

       在3.1版本中,Spring引入了bean profile的功能。要使用profile,你首先要将所有不同的bean定义整理到一个或多个profile之中,在将应用部署到每个环境时,要确保对应的profile处于激活(active)的状态。 在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;
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();
  }

}

       在此例中@Profile("dev")表示该方法在dev profile激活时才会创建,即该Bean才会被创建。如果dev profile没有激活的话,那么带有@Bean注解的方法都会被忽略掉。当然你也可在类级别中使用@Profile注解。

       注:有时候可能会有其他的bean并没有声明在一个给定的profile范围内。没有指定profile的bean始终都会被创建,与激活哪个profile没有关系。

2、在XML中配置profile

       我们也可以通过元素的profile属性,在XML中配置profile bean。




  
    
      
      
    
  
  
  
    
  

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

3、激活profile

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

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

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

例:spring.profiles.default的web.xml

Spring高级装配_第1张图片

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

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

       你还可以同时激活多个profile,这可以通过列出多个profile名称,并以逗号分隔来实现。当然,同时启用dev和prod profile可能也没有太大的意义,不 过你可以同时设置多个彼此不相关的profile。

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

@RunWi th ( SpringJUnit4C1assRunner. class)
@ContextConfiguration (classes= {PersistenceTestConfig. class}}
@ActiveProfiles{"dev")
public class Persis tenceTest {
}

 

二、条件化的的bean

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

// 条件化创建bean
@Bean
@Conditional (MagicExi stsCondition. class)	
public MagicBean magicBean()] {
    return new MagicBean() ;
}
可以看到,@Conditional中给定了一个Class,它指明了条件——在本例中,也就是MagicExistsCondition。@Conditional将会通过Condition接口进行条件对比: 
public interface Condition {
    boolean matches (Condi tionContext ctxt, Annota tedTypeMetadata metadata) ; .
}

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

 

三、处理自动装配的歧义性

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

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

1、标示首选的的bean

       在Spring中,可以通过@Primary来表达最喜欢的方案。@Primary能够与@Component组合用在组件扫描的bean上,也可以与@Bean组合用在Java配置的bean声明中。

(1)@Component

@Component
@APrimary
public class Icecream impl ements Dessert {
...
}

(2)通过Java配置显式地声明

@Bean
@Primary
public Dessert iceCream() {
    return new IceCream() ;
}

(3)使用XML配置bean

2、限定自动装配的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")指向的是组件扫描时所创建的bean,并且这个bean是IceCream类的实例。注意,此例中这里的问题在于setDessert()方法上所指定的限定符与要注入的bean的名称是紧耦合的。对类名称的任意改动都会导致限定符失效。

3、创建自定义的的限定符

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

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

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

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

@Bean
@Qualifier l"cold")
public Dessert iceCreaml) {
    return new IceCream() ;
}

4、使用自定义的限定符注解

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

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

       此例有了两个带有“cold”限定符的甜点。在自动装配Dessert bean的时候,我们再次遇到了歧义性的问题,需要使用更多的限定符来将可选范围限定到只有一个bean。这里所需要做的就是创建一个注解,它本身要使 用@Qualifier注解来标注。这样我们将不再使用@Qualifier("cold"),而是使用自定义的@Cold注解,该注解的定义如下所示:

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

       当你不想用@Qualifier注解的时候,可以类似地创建@Soft、@Crispy和@Fruity。通过在定义时添加@Qualifier注解,它们就具有了@Qualifier注解的特性。它们本身实际上就成为了限定符注解

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

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

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

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

 

四、bean的作用域

       在默认情况下,Spring应用上下文中所有bean都是作为以单例(singleton)的形式创建的。也就是说,不管给定的一个bean被注入到其他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的话,可以使用元素的scope属性来设置作用域:
 

 

五、运行时值注入

       有的时候,我们可能会希望避免硬编码值,而是想让这些值在运行时再确定。为了实现这些功能,Spring提供了两种在运行时求值的方式:

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

1、注入外部的值

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

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()实现的。

(1)Spring的Environment:

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

  • String getProperty(String key)

  • String getProperty(string key, String defaultvalue)

  • T getProperty(String key. ClasscT> type)

  • T getProperty(String key, Class type, T defaultValue)

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

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

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

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

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

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

2、解析属性占位符

       Spring一直支持将属性定义到外部的属性的文件中,并使用占位符值将其插入到Spring bean中。在Spring装配中,占位符的形式为使用“${... }”包装的属性名称。

 

       此例中title构造器参数所给定的值是从一个属性中解析得到的,这个属性的名称为disc.title。artist参数装配的是名为disc.artist的属性值。按照这种方式,XML配置没有使用任何硬编码的值,它的值是从配置文件以外的一个源中解析得到的。

       如果我们依赖于组件扫描和自动装配来创建和初始化应用组件的话,那么就没有指定占位符的配置文件或类了。在这种情况下,我们可以使用@Value注解,它的使用方式与@Autowired注解非常相似。

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

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

@Bean
public static Proper tySourcesPlaceholderConfigurer placeholderConfigurer() {
    return new Pr oper tySourcesPlaceholderConfigurer() ;
}

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



    

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

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

       SpEL拥有很多特性,包括:

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

(1)SpEL样例

       是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.tit1e'] }

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

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

       在XML配置中,你可以将SpEL表达式传入的value属性中,或者将其作为p-命名空间或c-命名空间条目的值。

(2)表示字面值

#{3.14}                 // 浮点值
#{9.87E4}		// 科学计数法,其值为98700
#{‘hello’}		// String类型
#{false}		// Boolean值

(3)引用bean、属性和方法

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

# {sgtPeppers}

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

# {sgtPeppers.artist}

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

       除了引用bean的属性,我们还可以调用bean上的方法。

#{ artistSelector. selectArtist() }

       为了避免出现NullPointerException,我们可以使用类型安全的运算符:

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

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

(4)在表达式中使用类型

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

T (java. lang .Math)

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

T(java. lang.Math) .PI

(5)SpEL运算符

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

Spring高级装配_第2张图片

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

       这不仅是使用SpEL中乘法运算符(*)的绝佳样例,它也为你展现了如何将简单的表达式组合为更为复杂的表达式。在这里PI的值乘以2,然后再乘以radius属性的值,这个属性来源于ID为circle的bean。实际上,它计算了circle bean中所定义圆的周长。

#{T(java.lang.Math) .PI . circle. radius^2] //计算圆的面积
#{scoreboara.scoe > 1000 ? ”ine!" :"Isei") // 三元表达式

       三元运算符的一个常见场景就是检查null值,并用一个默认值来替代null。例如,如下的表达式会判断disc.title的值是不是null,如果是null的话,那么表达式的计算结果就会是“Rattle and Hum”:

#{disc. title ?: 'Rattle and Hum' }

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

(6)计算正则表达式

       当处理文本时,有时检查文本是否匹配某种模式是非常有用的。SpEL通过matches运算符支持表达式中的模式匹配。matches运算符对String类型的文本(作为左边参数)应用正则表达式(作为右边参数)。matches的运算结果会返回一个Boolean类型的值:如果与正则表达式相匹配,则返回true;否则返回false。

#{admin. email matches ' [a-ZA-Z0-9._ 8+-]+@[a-ZA-Z0-9.-] +\l.com.’}

(7)计算集合

       SpEL中最令人惊奇的一些技巧是与集合和数组相关的。最简单的事情可能就是引用列表中的一个元素了:

#{jukebox. songs[4] .title}

       这个表达式会计算songs集合中第五个(基于零开始)元素的title属性,这个集合来源于ID为jukebox bean。

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

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

       SpEL还提供了另外两个查询运算符:“.^[]”和“.$[]”,它们分别用来在集合中查询第一个匹配项和最后一个匹配项。

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

#{jukebox. songs.! [title] }

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

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

 

你可能感兴趣的:(Spring)