第三章 高级装配

本章内容

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



环境与profile

在开发软件的时候,将应用程序从一个环境迁移到另一个环境是很大的挑战,某些环境相关做法即使迁移过去也无法正常工作

    例如:我们在开发环境使用嵌入式数据库,并预先加载测试数据.
    在Sprng配置类中,我们在一个带有@bean注解的方式上使用EmbeddedDatabaseBuilder:
    
    //这会创建一个类型为javax.sql.DataSource的bean
    @Bean(destroyMethod="shutdown")
    public DataSource dataSource(){
        return new EmbeddedDatabaseBulider()
        .addScript("classpath:schems.sql")
         .addScript("classpath:test-data.sql")
         .build();
    } 

实现原理:使用EmbeddedDatabaseBulider会搭建一个嵌入式Hypersonic数据库,他的模式(schema)定义在schema.sql中,测试数据则是通过test-data.sql加载的

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

但是在生产模式下,比他更好的选择是使用JNDI从容器中取出一个DataSource

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

通过JNDI获取DataSource能够让容器决定如何创建这个DataSource,甚至包括切换为容器管理的连接池(尽管这样,JNDI还是更适用于开发环境,对于简单的集成和测试环境来说,太复杂了)

同时,在QA环境中,你可以选择完全不同的DateSource配置,可以配置为Commons DBCP连接池:

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

显然着三个版本你的dataSource()方法互不相同,他们的相似点仅限于都会生成一个类型为javax.sql.DataSource的bean.但我们必须要有一种使其在各种环境下都最为合适的配置,其中的一种方式就是在单独的配置类(或Xml文件中)进行配置,但是这样会需要代码在不同环境迁移是进行重新构建,幸好Spring提供了一种不需要重构的解决方案

配置profile Bean

Spring的解决办法是在运行时来做出决策,就是同一个部署单元(可能会是WAR文件)能够适用于所有的环境,没有必要重新构建

在3.1版本中,Spring引入了bean profile功能

使用profile功能
  • 首先要将不同的bean定义整理到一个或多个profile之中

  • 将应用部署到每个环境时,要确保对应的profile处于激活(active)的状态

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

      //在配置类中,嵌入式数据库的DateSource可能配置成如下所示
      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 = “”)
    public DataSource dataSource(){
    return (DataSource) new EmbeddedDatabaseBuilder()
    .setType(EmbeddedDatabaseType.H2)
    .addScript(“classpath:schema:sql”)
    .addScript(“classpath:test-data.sql”)
    .build();
    }

      }
    
@Profile注解应用在了类级别上,他会告诉Spring这个配置类中的bean只有在dev profile激活时才能创建,如果没有激活,那么带有@bean注解的方法都会被忽略掉
同时,你还需要一个适用于生产环境的配置:
    package com.myapp;


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.activation.DataSource;

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

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


}




}

在本例中,只有prod profile被激活的时候,才会创建对应的bean

在Spring3.1中,只能在类级别上使用bean,不过,从Spring3.2开始,就能在方法上使用@profile注解,与@bean注解一起使用这样的话就能将这两个bean的声明放在一个配置里

        //@profile注解基于激活的profile实现的bean的装配
        
        package com.myapp;
    
    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.activation.DataSource;
    
    
    @Configuration
    public class DataSourceConfig {
    

 //为dev profile装配的bean
    @Bean(destroyMethod = "")
    @Profile("dev")
    public DataSource dataSource(){
        return (DataSource) 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激活时bean才会被创建,没有指定profile的bean始终会被创建

在Xml中创建bean

我们可以借助的profile属性,在Xml中设置profile bean

<jdbc:embedded-database id="dataSource">

    <jdbc:script location="classpath:schema.sql"/>
    <jdbc:script location="classpath:test-data.sql"/>

</jdbc:embedded-database>

与此类似 ,我们也可以将profile设置为prod,创建适用于生产环境的从JNDI获取的DataSource bean,同样,可以创建基于连接池定义的DataSource bean,将其放在另一个Xml文件中,并将其标注为qa profile,所有的配置文件都会放到部署单元当中(如WAR文件),只有当profile被激活时,相关的配置文件才能被用到.

还可以在根元素中嵌套元素,而不是每个环境都创建一个profile文件,这样就可以将他们放在同一个Xml项目中

 //重复使用元素来指定多个profile
 
         //devprofile 的bean
        <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>
    
    
    //qafrofile的bean
        <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:maxActive="30"
           />
        </beans>
    //pordprofile的bean
        <beans>
            <bean profile="prod"/>
            <jee:jndi-lookup id="DataSource"
                             jndi-name="jdbc/myDatabase"
                             resource-ref="true"
                             proxy-interface="javax.sql.DataSource"
                             />
    
        </beans>
这种配置方式与定义在单独的xml文件中的效果是一样的,现在让我们看如何激活bean

激活profile

Sprinf在确定哪个profile处于激活状态时,需要依赖两个独立的属性

  • spring.profiles.active
  • spring.profiles.default

如果设置了spring.profiles.active属性的话,那么他的值就会确定哪个profile是激活的,但是没有spring.profiles.active属性的话,那Spring将会查找spring.profiles.default的值,如果两个都没有设置的话,那就没有激活的profile,只会创建那些没有定义在profile中的bean

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

你可以使用spring.profiles.active和spring.profiles.default的最佳组合满足需求

在web应用中,设置spring.prifiles.default

在Web应用的web.Xml文件中设置默认的profile

    <context-param> 
    <-- 为上下文设置的默认的profile-->
    <param-name>spring.profiles.default</param-name>
    <param-value>dev</param-value>
    </context-param>
    
    <init-param>
     <-- 为servlet设置的默认的profile-->
    <param-name>spring.profiles.default</param-name>
    <param-value>dev</param-value>
    
    </init-param>
  • 按照这种设置spring.profiles.default,所有的开发人员都能从版本控制软件中获得应用程序源码,并使用开发环境的设置(如嵌入式数据库)运行代码,而不需要任何额外的配置

  • 当应用程序部署到其他环境的时候,负责部署的人根据情况使用系统属性,环境变量或JNDI设置到spring.profiles.active即可,系统会优先使用spring.profiles.active的配置

  • 你可以同时设置多个不相关的profile

使用profile进行测试

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

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

  • 在集成测试时,通常想要激活的是开发环境的profile

      例如:
      @RunWith(SpringJUnit4ClassRuner.class)
      @ContextConfiguration(classes={PersistenceTestConfig.class})
      @ActiveProfiles{"dev"}
      public class PersistenceTest{
          ....
      }
    
Spring4提供了一种更为通用的机制来实现条件化的bean定义,在这种机制之中,条件完全由你确定,
  • Spring的@Conditional注解定义条件化的bean

条件化的bean

Spring4引入了@Condiional注解,他可以应用到带有@bean注解的方法上,如果给定的条件计算为true,就会创建这个bean,否则就会忽略.

    //例如有个MagicBean的类,我们希望只有设置的magic属性的时候,Spring才会实例化这个类
    @Bean
    @Conditional(MagicExistsCondition.class)
    public MagicBean magicBean(){
        
        return new MagicBean();
    }
  • 设置@Conditional的类可以是任意实现了Condition接口的类型,这个接口实现很简单

      //这是完成该功能的Condition实现类
      //在Conditional中检查是否存在magic属性
      
              package com.habuma.restfun;
      
      import org.springframework.context.annotation.ConditionContext;
      import org.springframework.context.annotation.Condition;
      import org.springframework.core.env.Environment;
      import org.springframework.core.type.AnnotatedTypeMetadata;
      
      
      public  class MagicExistscondition implements Condition {
    
    
      @Override
      public boolean matches(ConditionContext context, AnnotatedTypeMetadata annotatedTypeMetadata) {
    
      Environment env = context.getEnvironment();
      return env.containsProperty("magic");
    
      }
      
      }
    
matches()方法会得到ConditionContext和AnnotatedTypeMetadata对象来进行决策
ConditionContext是一个接口,通过ConditionContext可以做到下面几点
  • 借助getRegistry()返回的BeanDefinitionRegistry检查bean定义
  • 借助getBeanFactory()返回的ConfigurableListableBeanFactory检查bean是否存在,甚至探查bean的属性;
  • 借助getEnvironment()返回的Environment检查环境变量是否存在以及他的值是什么
  • 读取并探查getResourceLoader()返回的Resourceloader所加载的资源
  • 借助getClassLoader返回的ClassLoader加载并检查类是否存在
AnnotatedTypeMetadata也是一个接口
  • 借助isAnnoteated()方法,我们能够判断带有@Bean注解的方法是不是还有其他特定的注解
  • 借助其他那些方法,我们能够检查@Bean注解的方法上的其他注解的属性

Spring4时,@Profile进行了重构,使其基于@Conditional和Condition实现
    //@profile注解如下
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.Type,ElementMethon})
    @Documented
    @Conditional(ProfileCondition.class)
    public @interface profile{
        String[] value();
    }

@Profile本身也使用了@Conditional注解,并且引用ProfileCondition和Configdion实现.

  • ProfileCondition实现了Condition接口,并且在做出决策的过程中,考虑到了@ConditionContext和AnnotatedTypeMetadata中的多个因素
处理自动装配的歧义性

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

当Spring装配bean时遇到不是唯一的,有歧义的可选值Spring就无法做出选择,只好宣告失败并抛出异常(NoUniqueBeanDefinitionException):

歧义性非常罕见,但仍有可能发生

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

标示首选的bean

Spring中,可以通过@Primary来表达最好的方案
  • 1.@Primary能够与@Component组合用在组件扫描的bean上

      例如
     @Component
     @Primary
     public class IceCream implements Dessert{....}
    
  • 2.也可以通过java配置显式的与@Bean组合用在java声明的bean配置中

          @Bean
          @Pirmary
          public Dessert iceCream(){
              return new IceCream();
          }
    
  • 3.Xml配置中,元素有一个primary属性用来指定首选的bean:

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

但是不能同时标示两个及以上首选bean,这本是就是歧义

@primary的弊端

他只能标示一个首选bean,当首选bean的数量超过一个的时候,我们并没有其他的方法来进一步缩小可选范围
限定自动装配的bean(优化@primary的更好方案)

Spring的限定符能够在所有可选的bean上进行缩小范围的操作,最终能够到达的只有一个bean满足所规定的限制条件

@Qualifier注解是使用限定符的主要方式,他可以与@Autowired和@Inject协同使用,在注入的时候指定想要注入进去的是哪个bean
    例如:我们将IceCream的bean注入到setDessert()中
    
    @Autowired
    @Qualifier("iceCream")
    public void setDessert(Dessert dessert){
        
        this.dessert  = dessert;
    }
    
    //这是使用限定符最简单的例子(基于默认bean ID的限定符)
创建自定义的限定符

如果你重构了IceCream方法,默认的bean Id也会改变,这就无法匹配setDessert()方法的限定符,自动装配会失败

解决办法:我们可以为bean设置自己的限定符,而不是依赖于bean将bean Id作为限定符,这里需要做的就是在bean声明上添加上@Qualifier注解

  • 与@Component组合使用

      //cold限定符分配给了IceCream bean,因为他没有耦合类名,所以可以随意重构
      @Component
      @Qualifier("cold")
      
      public class IceCream implements Dessert{...}
    
  • 通过java配置显式定义bean

     //@Qualifier也可以与@Bean注解一起使用
      
      @Bean
      @Qualifier("cold")
      public Dessert iceCream(){
          
           return new IceCream();
      }
    

更重要的是,为bean选择具有描述性的词语,而不是随便一个

使用自定义的限定符注解

面向特性的限定符要比基于默认的bean Id的限定符要好,但是如果出现了两个相同的特性,我们就又要使用@Qualifier缩小范围,而使用重复的注解是java语言所不允许的,这时候可以使用自定义的限定符注解.

自定义注解的方式
    例如 @cold
    
    @Target({ElementType.CONSTRUCTOR,ElementType.FTELD,ElementType.METHOD,ElementType.Type})
    @Retention(RetentionPolicy.RUNTIME)
    @Qualifier
    public @interface Cold{}
    
    
    //他们就具有了@Qualifier注解的特性,他们本身实际上就成为了限定符注解

最终在注入点,我们可以使用多个必要的限定符进行的任意组合,从而将可选范围缩小到只有一个bean满足需求,我们可以使用多个自定义限定符而不必担心java编译器的限制和错误

bean的作用域

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

单例(singleton)的缺点

  • 初始化和垃圾回收对象实例所带来的的成本只留给一些小规模任务,在这些任务中,让对象保持无状态并且在应用中反复重用这些对象可能并不合理.

  • 有时候,可能会发现,你所使用的类是易变的(mutable),他们都包含一些状态,因此重用是不安全

Spring定义了多种作用域,可以给予这些作用域创建bean

包括:

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

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

  • 会话(session):在web应用中,为每个会话创建一个bean实例

  • 请求(Rquest):在web应用中,为每个请求创建一个bean实例

      单例是默认的作用域
    

选择其他的作用域,要使用@scope注解,它可以与@Component或@Bean一起使用

    例如使用组件扫描来发现和声明bean,那么可以在bean的类上使用@Scope注解,并且将它声明为原型bean
    
    @Component
    //也可以使用@Scope("prototype")但是后者更加安全
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public class Notepad{...}

如果使用javaConfig配置,可以组合使用@Scope和@Bean

    @Bean
    @Scope(ConfiguraableBeanFactory.SCOPE_PROTOTYPE)
        public Notepoad notepad(){
            retuen new Notepad();
        }

如果想使用Xml来配置bean的话,可以使用元素的scope属性来设置作用域:

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

这样所导致的结果就是每次操作都能得到自己的Notepad实例

使用会话和请求作用域

在Web应用中,如果能够实例化在会话和请求范围内共享的bean,那将是很有价值的事情

    例如:在典型的电子商务应用中,可能会有一个bean代表用户的购物车,如果购物车是单例的话,那么将会导致所有的用户都会像同一个购物车里添加商品
    如果购物车是原型作用域的,那么在应用中某一个地方往购物车里添加商品,在应用的另一个地方就不可用了,因为
    在这里注入的另一个原型作用域的购物车
    
    就购物车bean来说,会话作用域是最为合适的,因为它与给定的用户关联性最大.

会话作用域的使用方式与原型作用域是相同的:

    @Component
    @Scope(value = WebApplicationContext.SCOPE_SESSION,
           proxyMode= ScopeProxyMode.INTERFACES)
           
    public shoppingCart cart(){...}

对于给定的会话只会创建一个实例,在当前会话相关的操作中,这个bean实际上相当于单例的

@Scope中的proxyMode属性详解

这个属性解决了将会话或者请求作用域的bean注入到单例bean中的问题

问题:

  • 当spring应用加载上下文的时候,Spring会试图将bean注入到方法中,但bean是属于会话的,此时并没有被创建,直到某个用户进入了系统,bean才会被创建
  • 系统中将会有多个bean实例对应多个用户,我们希望方法处理是注入的bean刚好是当前用户的一个

解决方法

  • Spring并不会直接将bean实例注入到方法中,Spring会注入一个bean的代理(这个代理会暴露与bean相同的方法,让方法以为这就是bean本身)
  • 但是,当方法调用bean的方法时,代理会对其进行懒解析,并将调用委托给会话中真正的bean

proxyMode本身

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

  • 最为理想的代理模式是bean是接口而不是完整的类
  • 如果bean是一个完整的类的话,Spring就不能创建基于接口的代理了,他必须使用CGLIb生成基于类的代理
  • 此时proxyMode属性必须设置为ScopeProxyMode.TARGET_CLASS
请求作用域也会遇到相同的问题


在Xml中生成作用域代理

如果使用Xml来声明会话或者请求作用域的bean,那么就不能使用Scope注解及proxyMode属性了

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

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


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

第四章中,当我们使用Spring和面向切面编程的时候,会讨论Spring aop命名空间的更多知识.



运行时值注入

讨论依赖注入的时候,通常是将一个对象与另外一个对象进行关联,但是bean装配的另外一个方面指的是将一个值注入到bean的属性或者构造器参数中

运行时值注入可以避免硬编码值,让这些值在运行时再确定

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

这两种技术的用法是类似的,不过他们的目的和行为是有所差别的

  • 属性占位符相对简单
  • Spring(SPEL)表达式语言更为强大
注入外部的值

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

    //使用@PropertySouce注解和Environment
    
    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("/soundsystem/app.properties")
    public class ExpressiveConfig {

    @Autowired
    Environment env;

    @Bean
    public BlankDisc disc(){
        return new blankDisc(
                //检索属性值
                env.getProperty("disc.title"),
                env.getProperty("disc.artist")
        )
    }

@PropertySource引用了类路径中一个名为app.properties的属性文件

    disc.title = Small Love Song
    disc.artist = LinJunJie

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

深入学习Spring中的Environment

getProperty()方法有四个重载的变种形式

  • String getPorperty(String key)
  • String getProperty(String key,String defaultValue)
  • T getProperty(String key,Class<> type)
  • T getProperty(String key,Class type,T defaultValue)

前两种方法会返回String类型的值,我么你可以对前面的@Bean方法进行改写,这样当指定属性不存在的时候,会使用一个默认值:

@ Bean
public BlackDisc disc(){
    return new BlackDisc(
    env.getProperty("disc.title","Ice and Fire Song"),
    env.getProperty("disc.artist","Eason");
    )
    
}

剩下两种与前面两种相似,但是不会将所有值都视为String类型.

    //例如你要取得int型的连接池数量
    int connectionCount = env.getPorperty("db.connection.count",Integet.class,30);

getProperty()方法默认值为mull(当你访问的这个属性没有定义),如果强制要求属性必须定义,就要使用getRequiredProperty()方法,此时如果属性没有定义,将会抛出IllegalStateException异常.

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

      boolean titleExists = env.containsProperty("disc.title");
    
  • 如果将属性解析为类的话,可以使用getPropertyAsClass()方法:

Class cdClass = env.getPropertyAsClass(“disc.class”,CompactDisc.class)

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

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

//在Xml中按照如下的方式解析BlankDisc构造器参数

<bean id="sgtPepper"
class="soundsystem.BlankDisc"
c:_title="$(disc.title)"
c:_artist = "$(disc.artist)"
/>

Xml配置没有使用任何硬编码的值,它的值是是从配置文件以外的一个源中解析得到的(稍后讨论如何解析的)

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

    例如
    public BlackDisc(
       @Value("${disc.title}")String title,
       @Value("${disc.artist}")String artlist){
           
           this.title = title;
           this.artlist = artlist;
       }
    
    )

为了是用占位符,我们必须要配置一个PropertySourcesPlaceholderConfig,因为它能够基于Spring Environment及其属性源来解析占位符

    @Bean
    public 
    static PropertySourdesPlanceHolderConfigurer(){
        return new PorpertySourcesPlaceholderConfig();
    }

如果你想使用Xml配置的话,Spring context命名空间的context:propertyplaceholder/元素将会为你生成PropertySourcePlanceholderConfigurer bean:

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

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

Spring3后引入了Spring表达式语言(Spring Expression Language,SpEL)

SpEL可以实现难以想象的装配效果,来源于他的特性

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

SpEL还能用到依赖注入的其他地方

    例如:Spring Security支持SpEL表达式定义安全限制规则
    另外,如果你在SpringMvC中使用Thymeleaf模板作为视图的话,那么这些模板可以使用SpEL表达式引入模型数据
SpEL样例
首先,需要将SpEL表达式要放入#{…}中
        //这与属性占位符很像${...}
最简单的SpEL表达式
  #{1}
  //1就是SpEL本体
在实际的应用程序之中,我们会使用更有意义的表达式
    #{T(System).currentTimeMillis()}
    //T()表达式会将java.lang.system视为Java中对应的类型,因此可以调用Static修饰的currentTimeMillis()方法
SpEL表达式也可以引用其他的bean或其他bean的属性
    #{sgtPeppers.artist}
    //下面表达式会计算得到ID为sgtPeppers的artist的属性
我们还可以通过 SystemProperties对象引用系统属性
    SystemProperties['disc.title']

这只是SpEL的几个基础样例,下面看看bean装配的时候如何使用这些表达式

如果通过组件扫描创建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表达式传入或的value属性中,或者将其作为c-命名空间或p-命名空间的值
    <bean id="sgtPeppers"
    
    class = "soundsystem.BlankDisc"
    c:_title ="#{systemProperties['disc.title']}"
    c:_artist= "#{systemProperties['disc.artist']}"
    
    />
表示字面值

我们看过了SpEL表达式表达的整数字面量,SpEL还可以用来表示浮点数

    #{3.141592}

数值还可以通过科学计数法的方式进行表示,如下面的表达式得到的值98700

    #{9.87E4}

SpEL表达式也可以用来计算String的字面量值

    #{Hello}

最后,字面值true和false的计算结果就是他们对应的boolean类型的值

   #{false}

当组合更复杂的SpEL表达式时,你迟早会用到他们

引用bean,属性,方法

SpEL能做的另一件基础的事情就是通过Id引用其他的bean

    例如:你可以使用SpEL将一个bean装配到另一个bean的属性中,此时要使用bean Id作为SpEL表达式
    
    #{sgtPeppers}

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

    #{setPeppers.artList}

对于被调用方法的返回值来说,我们仍然可以调用他的方法

//比如获取方法的返回值后将它变成大写
#{artListSelect.selectArtList().toUpperCase()}
这时可能出现一个错误,加入selsetAlist()返回值是空的就会报出空指针异常NullPOintException异常,这时候为了避免那种异常就可以使用类型安全的运算符
#{artListSelect.selectArtList()?.toUpperCase()}

?.运算符是为了保证左边的内容不为空,如果左边的内容是空的话,那他右边的内容就不会运行,最后返回null

在表达式中使用类型

如果要在spEL中访问类作用域的方法和常量的话,需要依赖T()这个关键的运算符

例如,为了使用java中的Math方法,就要这样使用T()运算符

T(java.lang.Math)

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

//假如你需要将pi值装配到bean属性中,可以这样做

T(java.lang.Math).PI

//同时,我们可以调用T()运算符获得的静态方法.比如产生一个随机数

T(java.lang.Math).random()


SpEL提供了多种不同功能的运算符

运算符类型运算符
算数运算+,-,&,/,%,^
比较运算<,>,==,<=,>=,lt,gt,eq,le,ge
逻辑运算and,not,or,
条件运算?:(ternary),?:(Elvis)
正则表达式matches
SpEl中运算符的简单案例
计算圆的周长
#{2*T(java.lang.Math).PI+circle.radius}
遇到Spring值的时候,"+"运算符执行的是连接操作
#{disc.title+"by"+disc.artList}
SpEL有两种形式(文本形式和符号形式),用来在表达式的值进行对比,这两种形式在使用中可以随便选择
#{counter.total == 100}
//相等于
#{counter.total eq 100}
SpEL还提供了三元运算符.三元运算符使用和java中的三元运算符十分相似
#{scoreboard.score>1000 ?"winner":"loser"}
三元运算符的一个常见场景就是检查NULL值
//如果判断结果是null,就会使用Small Love Song来代替null
#{disc.title?:"Small Love Song"}
计算正则表达式

当处理文本时,检查文本是否匹配某种模式是非常有用的,SpEL通过matches运算符支持右边的表达式中的模式匹配,matches对Spring类型的文本(作为左边参数)应用正则表达式匹配

matches的运算结果会返回一个boolean的表达式的值:相匹配为ture,否则为false

//假设判断一个字符创是否包含有效的邮件地址,在这个场景下使用matches运算符

#{admin.email matches '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.com'}
SpEL最令人惊奇的一些技巧是与集合与数组相关的,最简单的事情就是引用列表中的一个元素:
//这个表达式会计算songs集合中第五个(基于0开始)元素的属性,这个集合来源于IDjukebox bean

#{jokebox.song[4].title}

//下一步让他丰富

#{jukebox.songs[T(java.lang.math).random()*kukeboxSongs.size()].title}
"[]"运算符还可以从字符串里获取一个字符,比如
#{'This is test'[3]}
SpEL还提供了查询运算符(.?[]),他会用来对集合进行过滤,得到一个集合的子集
//假设你希望通过Lovebox的artList中获得所有属性为lin的所有歌曲
//如果判断条件是true,那么条目就会放在新集合中,反之则不
#{Lovebox.songs.?[artList eq 'lin']}
SpEL还提供了另外两个查询运算符:".^ []"和 “.$[]”,分别用来匹配第一个匹配项和最后一个匹配项
SpEL还提供了投影运算符,他会从集合的每个成员中选择特定的属性放在另外一个集合中
假设,我们不想要所有歌曲对象的集合,而是歌曲名称的集合,那么下面的表达式会将title放在新的String类型的集合中:

#{jokebox.songs.![title]}
实际上,投影操作可以与其他操作一起使用
#{joekbox.songs.?[artList eq 'Aerosmith'].![title]

还有更多的机会用到SpEL,尤其是在自定义安全规则的时候.

提示

  • 在动态注入值到Springbean的时候,SpEL是一种很便利的强大的方式
  • 我们有时候会编写很强大的SpEL表达式,但是不要让你的表达式太智能(你的表达式越智能,对它的测试就越重要)
  • 尽量让表达式简洁
依赖注入能够将应用组件,与跨多个组件的任务进行解耦.下一章我们将会深入学习在Spring中如何创建切面
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值