Spring 运行时注入属性值

2020/10/29 34

Spring

有时候,硬编码注入属性值是可以的,但有时候,我们会希望避免硬编码值,为了实现这些功能,Spring 提供了两种在运行时求值的方式:

这两种技术的用法是相似的,让我们先看一下属性占位符,它基表简单,之后再看一下更为强大的 SpEL

属性占位符

在 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 ExpressiveConfig {
    @Autowired
    Environment env;

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

在本例中,@PropertySource 引用了类路径中一个名为 app.properties 的文件,它大致会如下所示:

disc.title = Sgt. Peppers Lonely Hearts Club Band
disc.artist = The Beatles

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

如果我们想从属性文件中得到的 Integer 类型,就能通过重载的方法解决问题:

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

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

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

如果 disc.titledisc.artist 数据没有定义的话,就会抛出 IllegalStateException 异常。

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

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

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

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

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

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

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

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

解析占位符

在 Spring 装配中,占位符的形式为 ${ ... },例如:

<bean id="sgtPeppers"
      class="soundsystem.BlankDisc"
      c:_title="${disc.title}"
      c:_artist="${disc.artist}" />

如果我们依赖组件扫描和自动装配来初始化组件时,我们可以使用 @Value 注解:

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

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

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

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

Spring 表达式语言

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

SpEL 拥有很多特性,包括:

SpEL 表达式要放到 #{ ... } 中,下面所展现的可能就是最简单的 SpEL 表达式了:

#{1}

这个表达式的结果就是数字 1,然而这并不会让你感到惊讶。当然,在实际应用中,我们可能不会使用这么简单的表达式,我们可能会使用更加有意思的,如:

#{T(System).currentTimeMillis()}

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

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

#{sgtPeppers.artist}

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

#{systemProperties['disc.title']}

同样的,如果通过组件扫描创建 bean 的话,也可以使用 @Value 注解,与之前占位符非常类似:

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

以下是一些其它用法示例:

属性:
#{sgtPeppers.artist}

方法:
#{artistSelector.selectArtist()}
#{artistSelector.selectArtist().toUpperCase()}
#{artistSelector.selectArtist()?.toUpperCase()}

静态方法及常量:
#{T(java.lang.Math).PI}
#{T(java.lang.Math).random()}

运算符:
#{2 * T(java.lang.Math).PI * circle.radius}
#{T(java.lang.Math).PI * circle.radius ^ 2}
#{disc.title + ' by ' + disc.artist}
#{counter.total == 100}
#{counter.total eq 100}

三元运算:
#{scoreboard.score > 1000 ? "Winner!" : "Loser"}
#{disc.title ?: 'Rattle and Hum'}

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

数组:
#{jukebox.songs[4].title}
#{jukebox.songs[T(java.lang.Math).random() * jukebox.songs.size()].title}
#{'This is a test'[3]} 从 String 中取出一个字符

SpEL 还提供了查询运算符 .?[],它会对集合进行过滤,得到集合的一个子集。如下的表达式就是用了查询运算符得到了 Aerosmith 的所有歌曲:

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

SpEL 还提供了 .^[] 用来表示第一个匹配项,用 .$[] 表示最后一个匹配项:

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

最后,SpEL 还提供了投影运算符 .![] 用来把集合中的每个成员或成员属性放到另一个集合中。如下表达式会将 title 属性投影到一个新的 String 蜡型的集合中:

#{jukebox.songs.![title]}

实际上,投影操作可以与其它任意的运算发一起使用:

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