SpringBoot 基础篇@Value 中哪些你不知道的知识点[1]
看到这个标题,有点夸张了啊,@Value
这个谁不知道啊,不就是绑定配置么,还能有什么特殊的玩法不成?
(如果下面列出的这些问题,已经熟练掌握,那确实没啥往下面看的必要了)
@Value
对应的配置不存在,会怎样?接下来,限于篇幅问题,将针对上面提出的问题的前面几条进行说明,最后两个放在下篇
先创建一个用于测试的 SpringBoot 项目,源码在最后贴出,友情提示源码阅读更友好
本项目借助SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ IDEA
进行开发
在配置文件中,加一些用于测试的配置信息
application.yml
auth:
jwt:
token: TOKEN.123
expire: 1622616886456
whiteList: 4,5,6
blackList:
- 100
- 200
- 300
tt: token:tt_token; expire:1622616888888
通过${}
来引入配置参数,当然前提是所在的类被 Spring 托管,也就是我们常说的 bean
如下,一个常见的使用姿势
@Component
public class ConfigProperties {
@Value("${auth.jwt.token}")
private String token;
@Value("${auth.jwt.expire}")
private Long expire;
}
接下来,引入一个配置不存在的注入,在项目启动的时候,会发现抛出异常,导致无法正常启动
/**
* 不存在,使用默认值
*/
@Value("${auth.jwt.no")
private String no;
抛出的异常属于BeanCreationException
, 对应的异常提示 Caused by: java.lang.IllegalArgumentException: Could not resolve placeholder 'auth.jwt.no' in value "${auth.jwt.no}"
所以为了避免上面的问题,一般来讲,建议设置一个默认值,规则如 ${key:默认值}
, 在分号右边的就是默认值,当没有相关配置时,使用默认值初始化
/**
* 不存在,使用默认值
*/
@Value("${auth.jwt.no}")
private String no;
在配置文件中 whiteList,对应的 value 是 4,5,6
, 用英文逗号分隔,对于这种格式的参数值,可以直接赋予List<Long>
/**
* 英文逗号分隔,转列表
*/
@Value("${auth.jwt.whiteList}")
private List<Long> whiteList;
上面这个属于正确的使用姿势,但是下面这个却不行了
/**
* yml数组,无法转换过来,只能根据 "auth.jwt.blackList[0]", "auth.jwt.blackList[1]" 来取对应的值
*/
@Value("${auth.jwt.blackList:10,11,12}")
private String[] blackList;
虽然我们的配置参数 auth.jwt.blackList
是数组,但是就没法映射到上面的 blackList (即使换成 List<String>
也是不行的,并不是因为声明为String[]
的原因)
我们可以通过查看 Evnrionment 来看一下配置是怎样的
通过auth.jwt.blackList
是拿不到配置信息的,只能通过auth.jwt.blackList[0]
, auth.jwt.blackList[1]
来获取
那么问题来了,怎么解决这个呢?
要解决问题,关键就是需要知道@Value
的工作原理,这里直接给出关键类 org.springframework.context.support.PropertySourcesPlaceholderConfigurer
关键点就在上面圈出的地方,找到这里,我们就可以动手开撸,一个比较猥琐的方法,如下
// 使用自定义的bean替代Spring的
@Primary
@Component
public class MyPropertySourcesPlaceHolderConfigure extends PropertySourcesPlaceholderConfigurer {
@Autowired
protected Environment environment;
/**
* {@code PropertySources} from the given {@link Environment}
* will be searched when replacing ${...} placeholders.
*
* @see #setPropertySources
* @see #postProcessBeanFactory
*/
@Override
public void setEnvironment(Environment environment) {
super.setEnvironment(environment);
this.environment = environment;
}
@SneakyThrows
@Override
protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess, ConfigurablePropertyResolver propertyResolver) throws BeansException {
// 实现一个拓展的PropertySource,支持获取数组格式的配置信息
Field field = propertyResolver.getClass().getDeclaredField("propertySources");
boolean access = field.isAccessible();
field.setAccessible(true);
MutablePropertySources propertySource = (MutablePropertySources) field.get(propertyResolver);
field.setAccessible(access);
PropertySource source = new PropertySource<Environment>(ENVIRONMENT_PROPERTIES_PROPERTY_SOURCE_NAME, this.environment) {
@Override
@Nullable
public String getProperty(String key) {
// 对数组进行兼容
String ans = this.source.getProperty(key);
if (ans != null) {
return ans;
}
StringBuilder builder = new StringBuilder();
String prefix = key.contains(":") ? key.substring(key.indexOf(":")) : key;
int i = 0;
while (true) {
String subKey = prefix + "[" + i + "]";
ans = this.source.getProperty(subKey);
if (ans == null) {
return i == 0 ? null : builder.toString();
}
if (i > 0) {
builder.append(",");
}
builder.append(ans);
++i;
}
}
};
propertySource.addLast(source);
super.processProperties(beanFactoryToProcess, propertyResolver);
}
}
说明:
通常,@Value
只修饰基本类型,如果我想将配置转换为实体类,可性否?
当然是可行的,而且还有三种支持姿势
PropertyEditor
Converter
Formatter
接下来针对上面配置的auth.jwt.tt
进行转换
auth:
jwt:
tt: token:tt_token; expire:1622616888888
映射为 Jwt 对象
@Data
public class Jwt {
private String source;
private String token;
private Long expire;
// 实现string转jwt的逻辑
public static Jwt parse(String text, String source) {
String[] kvs = StringUtils.split(text, ";");
Map<String, String> map = new HashMap<>(8);
for (String kv : kvs) {
String[] items = StringUtils.split(kv, ":");
if (items.length != 2) {
continue;
}
map.put(items[0].trim().toLowerCase(), items[1].trim());
}
Jwt jwt = new Jwt();
jwt.setSource(source);
jwt.setToken(map.get("token"));
jwt.setExpire(Long.valueOf(map.getOrDefault("expire", "0")));
return jwt;
}
}
请注意PropertyEditor
是 java bean 规范中的,主要用于对 bean 的属性进行编辑而定义的接口,Spring 提供了支持;我们希望将 String 转换为 bean 属性类型,一般来讲就是一个 POJO,对应一个 Editor
所以自定义一个 JwtEditor
public class JwtEditor extends PropertyEditorSupport {
@Override
public void setAsText(String text) throws IllegalArgumentException {
setValue(Jwt.parse(text, "JwtEditor"));
}
}
接下来就需要注册这个 Editor
@Configuration
public class AutoConfiguration {
/**
* 注册自定义的 propertyEditor
*
* @return
*/
@Bean
public CustomEditorConfigurer editorConfigurer() {
CustomEditorConfigurer editorConfigurer = new CustomEditorConfigurer();
editorConfigurer.setCustomEditors(Collections.singletonMap(Jwt.class, JwtEditor.class));
return editorConfigurer;
}
}
说明
JwtEditor
与Jwt
对象,在相同的包路径下面的时候,不需要上面的主动注册,Spring 会自动注册 (就是这么贴心)上面这个配置完毕之后,就可以正确的被注入了
/**
* 借助 PropertyEditor 来实现字符串转对象
*/
@Value("${auth.jwt.tt}")
private Jwt tt;
Spring 的 Converter 接口也比较常见,至少比上面这个用得多一些,使用姿势也比较简单,实现接口、然后注册即可
public class JwtConverter implements Converter<String, Jwt> {
@Override
public Jwt convert(String s) {
return Jwt.parse(s, "JwtConverter");
}
}
注册转换类
/**
* 注册自定义的converter
*
* @return
*/
@Bean("conversionService")
public ConversionServiceFactoryBean conversionService() {
ConversionServiceFactoryBean factoryBean = new ConversionServiceFactoryBean();
factoryBean.setConverters(Collections.singleton(new JwtConverter()));
return factoryBean;
}
再次测试,同样可以注入成功
最后再介绍一个 Formatter 的使用姿势,它更常见于本地化相关的操作
public class JwtFormatter implements Formatter<Jwt> {
@Override
public Jwt parse(String text, Locale locale) throws ParseException {
return Jwt.parse(text, "JwtFormatter");
}
@Override
public String print(Jwt object, Locale locale) {
return JSONObject.toJSONString(object);
}
}
同样注册一下(请注意,我们使用注册 Formatter 时,需要将前面 Converter 的注册 bean 给注释掉)
@Bean("conversionService")
public FormattingConversionServiceFactoryBean conversionService2() {
FormattingConversionServiceFactoryBean factoryBean = new FormattingConversionServiceFactoryBean();
factoryBean.setConverters(Collections.singleton(new JwtConverter()));
factoryBean.setFormatters(Collections.singleton(new JwtFormatter()));
return factoryBean;
}
当 Converter 与 Formatter 同时存在时,后者优先级更高
限于篇幅,这里就暂告一段落,针对前面提到的几个问题,做一个简单的归纳小结
@Value
声明的配置不存在时,抛异常(项目会起不来)${xxx:defaultValue})
可以解决上面的问题yaml
配置中的数组,无法直接通过@Value
绑定到列表/数组上PropertyEditor
实现类型转换Converter
实现类型转换 (更推荐使用这种方式)Formater
实现类型转换除了上面的知识点之外,针对最开始提出的问题,给出答案
@Value
支持字面量,也支持 SpEL 表达式既然已经看到这里了,那么就再提两个问题吧,在 SpringCloud 微服务中,如果使用了 SpringCloud Config,也是可以通过@Value
来注入远程配置的,那么这个原理又是怎样的呢?
@Value
绑定的配置,如果想实现动态刷新,可行么?如果可以怎么玩?
(顺手不介意的话,关注下微信公众号"一灰灰 blog", 下篇博文就给出答案)
系列博文,配合阅读效果更好哦
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现 bug 或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
SpringBoot 基础篇@Value 中哪些你不知道的知识点: https://spring.hhui.top/spring-blog/2021/06/06/210606-SpringBoot%E5%9F%BA%E7%A1%80%E7%AF%87-Value%E4%B8%AD%E5%93%AA%E4%BA%9B%E4%BD%A0%E4%B8%8D%E7%9F%A5%E9%81%93%E7%9A%84%E7%9F%A5%E8%AF%86%E7%82%B9/
[2]https://github.com/liuyueyi/spring-boot-demo: https://github.com/liuyueyi/spring-boot-demo
[3]https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-boot/002-properties-value: https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-boot/002-properties-value
[4]【基础系列】实现一个自定义配置加载器(应用篇): https://spring.hhui.top/spring-blog/2020/05/07/200507-SpringBoot%E7%B3%BB%E5%88%97%E6%95%99%E7%A8%8B%E4%B9%8B%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E8%87%AA%E5%AE%9A%E4%B9%89%E9%85%8D%E7%BD%AE%E5%8A%A0%E8%BD%BD%E5%99%A8/
[5]【基础系列】SpringBoot 配置信息之默认配置: https://spring.hhui.top/spring-blog/2018/09/25/180925-SpringBoot%E5%9F%BA%E7%A1%80%E7%AF%87%E9%85%8D%E7%BD%AE%E4%BF%A1%E6%81%AF%E4%B9%8B%E9%BB%98%E8%AE%A4%E9%85%8D%E7%BD%AE/
[6]【基础系列】SpringBoot 配置信息之配置刷新: https://spring.hhui.top/spring-blog/2018/09/22/180922-SpringBoot%E5%9F%BA%E7%A1%80%E7%AF%87%E9%85%8D%E7%BD%AE%E4%BF%A1%E6%81%AF%E4%B9%8B%E9%85%8D%E7%BD%AE%E5%88%B7%E6%96%B0/
[7]【基础系列】SpringBoot 基础篇配置信息之自定义配置指定与配置内引用: https://spring.hhui.top/spring-blog/2018/09/21/180921-SpringBoot%E5%9F%BA%E7%A1%80%E7%AF%87%E9%85%8D%E7%BD%AE%E4%BF%A1%E6%81%AF%E4%B9%8B%E8%87%AA%E5%AE%9A%E4%B9%89%E9%85%8D%E7%BD%AE%E6%8C%87%E5%AE%9A%E4%B8%8E%E9%85%8D%E7%BD%AE%E5%86%85%E5%BC%95%E7%94%A8/
[8]【基础系列】SpringBoot 基础篇配置信息之多环境配置信息: https://spring.hhui.top/spring-blog/2018/09/20/180920-SpringBoot%E5%9F%BA%E7%A1%80%E7%AF%87%E9%85%8D%E7%BD%AE%E4%BF%A1%E6%81%AF%E4%B9%8B%E5%A4%9A%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE%E4%BF%A1%E6%81%AF/
[9]【基础系列】SpringBoot 基础篇配置信息之如何读取配置信息: https://spring.hhui.top/spring-blog/2018/09/19/180919-SpringBoot%E5%9F%BA%E7%A1%80%E7%AF%87%E9%85%8D%E7%BD%AE%E4%BF%A1%E6%81%AF%E4%B9%8B%E5%A6%82%E4%BD%95%E8%AF%BB%E5%8F%96%E9%85%8D%E7%BD%AE%E4%BF%A1%E6%81%AF/
[10]https://blog.hhui.top: https://blog.hhui.top
[11]http://spring.hhui.top: http://spring.hhui.top