Spring学习笔记(七) --- 运行时值注入

本系列博客为spring In Action 这本书的学习笔记

我们知道Spring的特性之一就是它的依赖注入机制. 那么当我们提到依赖注入最先想到的应该是Bean与Bean之间的依赖注入, 也就是我们前面大篇幅谈到的装配Bean, 但是依赖注入的另一个方面就是指将一个值注入到Bean的属性或者构造器参数中, 本篇博客就是来探讨一下关于值注入的一些问题.


一. 运行时值注入

在说明运行时值注入之前, 我们先来看一下在前面的博客里出现过的值注入的例子.

程序1: 在JavaConfig中的值注入的例子

@Bean
public CompactDisc jay(){
    return new Jay("魔杰座", "周杰伦");
}

程序2: 在XML中的值注入例子

<bean id="jay" class="AssemblingBean.SoundSystem_XML.Jay" >
    <constructor-arg index="0" value="魔杰座"/>
    <constructor-arg index="1" value="周杰伦"/>
bean>

在前面的时候, 我们是这样使用值注入来组装这个jay Bean的, 可以看到, 无论是使用JavaConfig还是使用XML, 在实现值注入的时候都是将值硬编码在配置类中. 有的时候硬编码是可以的, 但是很多时候, 我们会希望能避免硬编码值, 而是让这些值在运行的时候再确定并且注入, 也就是运行时值注入.


为了实现运行时注入, Spring提供了两种方式:

  • 1. 属性占位符
  • 2. Spring表达式语言

属性占位符比较简单, 但是Spring表达式语言功能更为强大, 下面来详细看一下它们的用法.

1. 使用属性占位符注入外部的值

在Spring中, 处理外部值的最简单方式就是声明属性源并通过Spring的Environment来检索属性.
下面的例子就展现了一个基本的Spring配置类, 它使用外部的属性来装配jay Bean.

程序3: 使用PropertySource注解和Environment

@Configuration
@PropertySource("classpath:/AdvancedAssembly/RunTimeInjection/app.properties") //声明属性源
public class ExpressiveConfig {
    @Autowired
    Environment environment;

    @Bean
    public CompactDisc jay(){
        return new Jay(
                environment.getProperty("jay.title"),
                environment.getProperty("jay.artist")
        ); //检索属性值
    }
}

程序4: app.properties

jay.title = "魔杰座"
jay.artist = "周杰伦"

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

为什么从Environment中就能拿到属性文件里面的属性值呢? 这就要看一下Environment中都有什么了.

(1) 深入学习Spring的Environment

1. 获取属性的值: getProperty()

在Environment中关于getProperty()方法有四个重载的形式, 如下:

String getProperty(String key);
String getProperty(String key, String defaultValue);
T getProperty(String key, Class type);
T getProperty(String key, Class type, T DefaultValue);

前面两种形式的getProperty()方法都会返回一个String类型的值, 不同的是第二种形式多一个String型的defaultValue参数, 这个参数是当要获取的指定属性不存在的收, 默认使用的一个值. 比如:

程序5: 使用第二种形式的getProperty()获取属性值

@Configuration
@PropertySource("classpath:/AdvancedAssembly/RunTimeInjection/app.properties") //声明属性源
public class ExpressiveConfig {
    @Autowired
    Environment environment;

    @Bean
    public CompactDisc jay(){
        return new Jay(
                environment.getProperty("jay.title", "周杰伦的专辑"),
                environment.getProperty("jay.artist", "周杰伦")
        );
    }
}

剩下的两种形式与前两种形式非常相似, 但是它们的存在就是为了给获取除String类型的属性提供便利. 比如现在要获取的属性的值的含义为用户个数, 如果我们使用前两种形式的getProperty(), 那么我们在获取到值之后还要将这个值从String类型强转为Integer类型. 但是如果我们使用后两种形式的getProperty()方法就不必这么做. 比如:

int userCount = environment.getProperty("user.count", Integer.class, 30);

如果在使用getProperty()方法时要取得的属性不存在并且没有设置默认值, 那么获取到的值就会是null. 如果你希望这个属性必须要定义, 那么可以使用getRequiredProperty()方法. 比如:

@Bean
public CompactDisc jay(){
    return new Jay(
            environment.getRequiredProperty("jay.title"),
            environment.getRequiredProperty("jay.artist")
    );
}

在这里如果jay.title或jay.artist没有定义的话, 将会抛出IllegalStateException异常.

2. 其它与属性相关的方法

(1) 检查属性是否存在: containsProperty()

boolean titleExists = environment.containsProperty("jay.title");

(2) 将属性解析为类: getPropertyAsClass()

Class cdClass = environment.getPropertyAsClass("disc.class", CompactDisc.class);

3. 通过Environment检查那些profile处于激活状态

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

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

(2) 解析属性占位符

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

程序6: 在XML中使用占位符解析构造器参数


<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:c="http://www.springframework.org/schema/c"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="jay" class="AdvancedAssembly.RunTimeInjection.Jay"
                    c:title="${jay.title}"
                    c:artist="${jay.artist}" />

beans>

可以看到, title和artist构造器参数的值时从一个属性中解析到的, 这个属性的名称为jay.title和jay.artist. 在XML文件中, 并没有使用任何硬编码的值, 它的值时从XML配置文件以外的一个源中解析得到的.

如果我们依赖与组件扫描和自动装配来创建和初始化应用组件的话, 那么就没有指定占位符的配置文件或类了, 在这种情况下, 我们可以在Java代码中使用@Value注解来使用占位符, 比如在Jay的类中, 它的构造器可以这样写:

程序7: 在Java代码中使用占位符

public Jay(
        @Value("${jay.title}") String title,
        @Value("${jay.artist}") String artist){
    this.title = title;
    this.artist = artist;
}

为了使用占位符, 我们必须要配置一个PropertySourcesPlaceholderConfigurer Bean. 例如如下的@Bean方法在Java中配置了PropertySourcesPlaceholderConfigurer:

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

或者在XML配置文件中使用Spring context命名空间中的元素来生成PropertySourcesPlaceholderConfigurer. 如下:


<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 />
beans>

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

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

Spring表达式语言, 即SpEL, 它能够以一种强大和简洁的方式将值装配到Bean的属性和构造器参数中, 在这个过程中所使用的表达式会在运行时计算得到值. SpEL有以下特性:

  • (1) 使用Bean的ID来引用Bean;
  • (2) 调用方法和访问对象的属性;
  • (3) 对值进行算术/关系/逻辑运算;
  • (4) 正则表达式匹配;
  • (5) 集合操作.

因为之前没有接触过关于SpEL表达式, 所以我们先来看几个SpEL表达式的例子, 看看如何将它注入到Bean中.

(1) SpEL样例

SpEL表达式要放到”#{…}”中, 这与属性占位符有些相似, 除去”#{}”标记之后, 里面剩下的就是SpEL表达式. 来看最简单的一个SpEL表达式:

SpEL表达式例1:
#{1}
这个表达式的结果就是数字1.

当然在实际的应用程序中, 我们会使用一些稍微复杂的表达式, 比如:

SpEL表达式例2:
#{T(System).currentTimeMillis()}
这个表达式的结果是当前时间的毫秒数. T()表达式会将java.lang.System时为Java中对应的类型, 因此可以调用其static修饰的currentTimeMillis()方法.

SpEL表达式还可以引用其它的Bean或者其它Bean的属性, 比如下面的表达式的计算结果为jay Bean的artist属性:

SpEL表达式例3:
#{jay.artist}

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

SpEL表达式例4:
#{systemProperties[‘jay.artist’]}


看过上面几个简单的例子之后, 我们可以对之前采用占位符的程序6和程序7进行改动了.

程序8: 对程序6进行改动


<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:c="http://www.springframework.org/schema/c"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="jay" class="AdvancedAssembly.RunTimeInjection.Jay"
          c:title="#{systemProperties['jay.title']}"
          c:artist="#{systemProperties['jay.artist']}" />

beans>

程序9: 对程序7进行改动

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

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

(2) 在SpEL表达式中表示字面值

上面的SpEL表达式1是最简单的SpEL表达式, 它所表示的就是它的字面值1. 那么我们可以推想到, SpEL可以表示的字面值不仅仅是int类型的, 还可以表示其它类型(浮点型/String型以及Boolean值). 比如下面的例子:

SpEL表达式例5:
#{3.14159}
#{9.87E4} //数值还可以使用科学计数法
#{‘hello’}
#{false}

(3) 在SpEL表达式中引用Bean/属性/方法

除了上面表示字面值, SpEL还可以通过ID引用其它Bean. 比如我们可以使用SpEL将一个Bean装配到另一个Bean的属性中, 此时要使用Bean的ID作为SpEL表达式.

我们可以引用一整个Bean, 也可以引用Bean的一个属性或者一个方法, 比如:

SpEL表达式例6:
#{jay}
#{jay.title}
#{jay.printSongName()} //假如jay Bean有一个printSongName()方法, 我们也可以在SpEL中调用这个方法.

对于被调用方法的返回值来说, 我们同样可以调用它的方法. 比如printSongName()方法返回值为String类型, 我们还可以调用toUpperCase()将这个String类型的值改为大写.

#{jay.printSongName().toUpperCase()}

现在我们假设printSongName()的返回值可能为null, 那么此时再调用toUpperCase()就会出现NullPointerException异常, 为了避免这种情况, 我们可以将”.”运算符改为类型安全的运算符”?.”, 这个运算符能够在访问它右边的内容之前确保它所对应的元素不是null, 如果是null的话, 将不会调用右边的内容, 并返回null. 比如:

#{jay.printSongName()?.toUpperCase()}

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

如果要在SpEL中访问类的方法或者常量的话, 要使用T()这个关键的运算符. 比如为了在SpEL中表达Java的Math类, 要按照下面的方式使用T()运算符:

T(java.lang.Math)

要使用Math类的常量PI, 需要这样:

T(java.lang.Math).PI

与之类似, 我们还可以调用Math类的方法:

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

(5) SpEL运算符

SpEL提供了多个运算符, 这些运算符可以用在SpEL表达式的值上, 下面列出了SpEL中的运算符:

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

比如来看一个例子(circle是一个Bean):

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

简单的运算符使用在这里我就不过多举例了, 我们就挑一些特殊的来看一看.

比如我们要比较两个数字是不是相等, 可以这样写(下面两种书写方式的结果都是一样的):

#{counter.total == 100}或者#{counter.total eq 100}

SpEL提供了三元运算符”?:”, 而关于”?:”有两种含义和用法.

  • ternary: 与Java中的”?:”三元运算符用途相同, 比如:
    • #{student.score > 59 ? “ok” : “no”}
  • Elvis: 用来检查null值, 比如:
    -#{jay.title} ?: “jay”(意思就是, 先判断jay.title是否为null, 如果为null, 那么计算结果就是”jay”)

(6) 计算正则表达式

比如最经典的正则匹配邮箱的例子:

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

(7) 计算集合

SpEL其中一个神奇的地方就在于它还可以计算集合和数组相关的值. 它可以通过下标来获得集合或数组中某一元素的值, 当然, 它的下标是基于0的. 比如:

#{jay.songs[4].title}

又比如:

#{‘hello world’[0]} //这个表达式的计算结果为’h’

(8) 其它的运算符

SpEL还提供了一些其它比较特殊的运算符, 下面我们都来逐一学习一下.

1. 查询运算符”.?[ ]”
查询运算符专门用来对集合进行过滤, 得到集合的一个子集. 比如:

#{cdPlayer.discs .?[artist eq ‘周杰伦’]}

2. 查询运算符”.^[ ]”和”.$[ ]”

  • “.^[ ]”用来在集合中查询第一个匹配项
  • “.$[ ]”用来在集合中查询最后一个匹配项

3. 投影运算符”.![ ]”
投影运算符会从集合的每个成员中选择特定的属性放到另一个集合中.

比如现在我们要将CD播放器中正在播放的CD里面的所有的歌曲名拿出来:

#{cdPlayer.discs .![title]}

它还可以与其它SpEL运算符任意使用:

#{cdPlayer.discs .?[artist eq ‘周杰伦’] .![title]}

以上就是一些简单的关于SpEL功能, 要知道这只是SpEL功能的皮毛. 在动态值装配到Bean的时候, SpEL是一种很便利和强大的方式, 但是注意不要让你的SpEL表达式太过复杂和智能, 否则会给测试带来麻烦.


到这里就将Spring装配Bean的高级技巧讲完了, 下一篇博客我们将对Spring的面向切面编程进行探索!

好啦, 晚安~

你可能感兴趣的:(Spring)