Spring运行时值注入
0 前言
在Spring中的Bean配置方式一文中我简单介绍了在Spring中如何配置Bean,通过Bean的配置我们可以让类进入Spring的容器中,这样就可以在需要该类对象的时候直接进行注入。在类注入的过程中,我们可能需要对类中某些属性进行初始值设置,常见的是在配置第三方库的时候。譬如在使用Guava的LoadingCache
做本地缓存的是可能需要设置最大容量maximumSize
、在使用JestClient做elasticsearch操作的时候需要指定url
等。
解决该问题的方法大致有三种:
- 直接在相关的方法中写入默认值
.maximumSize(1000000)
- 在类中定义静态的常量值
private static int MAXIMUM_SIZE = 100000;
.maximumSize(MAXIMUM_SIZE);
- 在属性文件中进行设置,然后在类中读取属性值
@Value("${loading_cache.maximum_size}")
private int maximumSize;
.maximumSize(maximumSize);
第一种和第二种方法看起来简单,但是实际上存在着一些问题:
- 方法1是一种很不好的编程习惯,直接在代码中通过硬编码赋值,这种值也称为魔法值。当原先指定的值要修改的时候,要寻找代码中所有赋值的地方,非常容易出错。
- 方法2如果在单独的文件中设置默认值,例如常见的枚举值和一些常量值,那么就可以避免到处修改的情况。但是它和方法1都存在一个共同的问题——值实在编译的时候确定的。
实际项目开发中,通常会有多个不同的环境,譬如开发环境、测试环境和线上环境,在不同的环境中,系统配置的属性值可能不一样,例如数据库在不同环境下的相关配置。为了解决这个问题,一方面我们要对不同环境进行不同的配置;另一方面属性的配置要在运行时确定,这样才不会去代码中手动修改。
1 运行时注入值
在Spring中实现运行时注入值的方法有三种:
- 通过
Environment
类来检索属性 - 通过属性占位符
- 通过Spring表达式语句SpEL
1.1 mock场景
假定我们现在采用Guava的LoadingCache做本地缓存,需要在运行时指定maximumSize
和expireTime
。
- maximumSize:缓存最大容量
- expireTime:过期时间,按秒算
代码如下:
@Configuration
public class ConfigTest {
private int maximumSize;
private int expireTime;
@Bean
public LoadingCache<Integer, Integer> testLoadingCache() {
return CacheBuilder.newBuilder()
.maximumSize(maximumSize)
.expireAfterAccess(expireTime, TimeUnit.SECONDS)
.build(
new CacheLoader<Integer, Integer>() {
@Override
public Integer load(Integer key) throws Exception {
return key + 1;
}
}
);
}
}
此时maximumSize
和expireTime
都没有初始化值,需要在运行时注入值。项目开发中,我们往往将属性配置都放在一个或者多个后缀为.properties文件中,例如如下的application.properties.
# 最大容量
guava.loadingcache.maximumsize=100000
# 过期时间
guava.loadingcache.expiretime=100
1.1 通过Environment
检索属性
步骤:
- 通过@PropertySource指定属性源
- 通过Environment检索属性
代码如下:
@Configuration
# 1. 指定属性源
@PropertySource("classpath:application.properties")
public class ConfigTest {
private int maximumSize;
private int expireTime;
# 2. 注入Environment
@Autowired
private Environment environment;
@Bean
public LoadingCache<Integer, Integer> testLoadingCache() {
# 通过getProperty获取值
maximumSize = environment.getProperty("guava.loadingcache.maximumsize", Integer.class);
expireTime = environment.getProperty("guava.loadingcache.expiretime", Integer.class);
return CacheBuilder.newBuilder()
.maximumSize(maximumSize)
.expireAfterAccess(expireTime, TimeUnit.SECONDS)
.build(
new CacheLoader<Integer, Integer>() {
@Override
public Integer load(Integer key) throws Exception {
return key + 1;
}
}
);
}
}
1.2 通过属性占位符
属性占位符需要用到@Value属性,形如@Value("${property.name}")
代码如下:
@Configuration
public class ConfigTest {
@Value("${guava.loadingcache.maximumsize}")
private int maximumSize;
@Value("${guava.loadingcache.expiretime}")
private int expireTime;
@Autowired
private Environment environment;
@Bean
public LoadingCache<Integer, Integer> testLoadingCache() {
return CacheBuilder.newBuilder()
.maximumSize(maximumSize)
.expireAfterAccess(expireTime, TimeUnit.SECONDS)
.build(
new CacheLoader<Integer, Integer>() {
@Override
public Integer load(Integer key) throws Exception {
return key + 1;
}
}
);
}
}
需要注意的是,采用这种方法必须提供PropertyPlaceholderConfigurer
bean或者PropertySourcesPlaceholderConfigurer
bean。有如下两种方式:
- JavaConfig中配置
PropertySourcesPlaceholderConfigurer
- 配置文件根节点下添加
<context:property-placeholder/>
1.3 通过Spring表达式SpEL
SpEL功能很强大,形如@Value("#{expression}")。它和属性占位符的区别在于:
- 不需要
PropertySourcesPlaceholderConfigurer
bean的帮助 - expression能够执行简单的运算,调用其他对象或者bean中的方法
但是在实际开发中,我并没有碰到多少需要采用SpEL才能完成的任务,一般属性占位符就可以了。对上文提到的虚拟场景并不怎么适用。如果感兴趣,可以去官网了解一下。
2 总结
一般情况下,我们回在配置中的默认值放在属性文件中,采用属性占位符的方式去捕获属性值。但是由于Spring中Bean一般是单例的,所以只能加载一次。如果项目中需要定时的加载配置文件,则可以采用Environment的方法去尝试(注意不是采用@Autowired方式自动注入)。SpEL用在View的很方便,在实际项目代码中有时候并不需要那么强大的计算能力。