1、@Value是什么
@Value是用于给成员变量、构造函数、方法等设置值的注解
2、@Value怎么使用
DemoComponent.class:
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
@Slf4j
public class DemoComponent {
@Value("${property.config}")
private String configFromProperty;
@Value("${default.config:whatever}")
private String defaultConfig;
private String constructorConfig;
public DemoComponent(@Value("${constructor.config:java}") String constructorConfig) {
this.constructorConfig = constructorConfig;
}
@PostConstruct
private void demo() {
log.info("configFromProperty: {}", configFromProperty);
log.info("defaultConfig: {}", defaultConfig);
log.info("constructorConfig: {}", constructorConfig);
}
}
application.properties:
property.config=hello world
启动服务输出结果:
configFromProperty: hello world
defaultConfig: whatever
constructorConfig: java
3、问题思考
- 除了 ${} ,@Value是否还支持其它格式?
- 如何注入List、Map?
- 是否支持自动刷新?
- 如何自定义converter
4、@Value原理
4.1、源码解析
4.1.1、@Value配置格式解读
查看 PropertyPlaceholderHelper 可得知,没有占位符${}时,直接返回常量;有占位符${}时,按照占位符从配置中获取值。取不到且没有默认值时,报错
查看PropertyPlaceholderHelper placeholderPrefix属性可发现,该属性只能通过构造函数赋值。
再查看 PropertyPlaceholderHelper 构造函数的引用,发现占位符都是 ${} 格式。默认值分隔符也仅支持 : 格式
SystemPropertyUtils:
4.1.2、配置值注入原理
通过debug调用栈,我们可以找到如下代码,该逻辑解析了@Value拿到结果后,是通过反射的方式注入到变量中
AutowiredAnnotationBeanPostProcessor:
4.2、源码解读思路
可能有人会好奇,我是怎么找到 PropertyPlaceholderHelper,怎么确定就是它负责@Value注解的解析工作呢?
方法很简单,我们可以写个有问题的代码,然后根据报错堆栈就知道了,哪里报错就是在哪里处理的。
比如如下代码未设置默认值
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class ErrorComponent {
@Value("${non.config}")
private String emptyConfig;
}
服务启动时就会报错:
根据报错堆栈就知道处理点在哪了
4.3、自定义converter
4.3.1、converter意义
除了基本类型和String外,如果要注入List、Set、Map等,就需要converter做转换处理。
Spring框架默认提供了 StringToCollectionConverter 等转换器。
StringToCollectionConverter 支持将逗号分隔的 String 对象转换为 List、Set。如果需要其它场景,如 String 转 Map,我们就可以自定义converter
默认 StringToCollectionConverter 效果示例:
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.List;
import java.util.Set;
@Component
@Slf4j
public class ValueConverterComponent {
@Value("${list.converter:a,b,c}")
private List<String> list;
@Value("${set.converter:a,b,c}")
private Set<String> set;
@PostConstruct
private void converter() {
log.info("list: {}", list);
log.info("set: {}", set);
}
}
输出结果:
list: [a,b,c]
set: [a, b, c]
4.3.2、自定义converter
StringToMapConverter:
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.GenericConverter;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@Slf4j
public class StringToMapConverter implements GenericConverter {
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
ConvertiblePair pair = new ConvertiblePair(String.class, Map.class);
return Collections.singleton(pair);
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
log.info("自定义 StringToMapConverter。source: {}", source);
if (source == null) {
return null;
}
String s = source.toString();
String[] kv = s.split(",");
Map<String, String> map = new HashMap<>(kv.length);
for (String item : kv) {
String[] pair = item.split(":");
map.put(pair[0], pair[1]);
}
return map;
}
}
自定义listener,将converter加入到容器中
MyConverterListener:
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ConfigurableBootstrapContext;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.core.convert.support.ConfigurableConversionService;
import org.springframework.core.env.ConfigurableEnvironment;
@Slf4j
public class MyConverterListener implements SpringApplicationRunListener {
// 注意,自定义listener必须包含 SpringApplication 和 String[] 2个入参的构造函数
public MyConverterListener(SpringApplication application, String[] args) {
}
@Override
public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
log.info("加载自定义listener");
ConfigurableConversionService conversionService = environment.getConversionService();
conversionService.addConverter(new StringToMapConverter());
}
}
Spring是通过spi机制加载SpringApplicationRunListener的,因此需要补充spring.factories配置
加载源码:
spring.factories:
org.springframework.boot.SpringApplicationRunListener=com.eden.research.demo.MyConverterListener
项目结构:
结果验证:
ValueConverterComponent:
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Component
@Slf4j
public class ValueConverterComponent {
@Value("${list.converter:a,b,c}")
private List<String> list;
@Value("${set.converter:a,b,c}")
private Set<String> set;
@Value("${map.converter:k1:v1,k2:v2}")
private Map<String, String> map;
@PostConstruct
private void converter() {
log.info("list: {}", list);
log.info("set: {}", set);
log.info("map: {}", map);
}
}
结果输出:
自定义 StringToMapConverter。source: k1:v1,k2:v2
list: [a, b, c]
set: [a, b, c]
map: {k1=v1, k2=v2}
5、总结
5.1、@Value注解可以有2种格式
@Value("abc")和@Value("${abc}"),前者按常量处理,后者会从配置中读取值
5.2、自动刷新机制
若@Value配置值是从配置文件加载时,只会在服务启动时加载一次,后续配置文件变更是不能实时监听到的,因此不支持自动刷新。如果接入了nacos、spring cloud config等配置中心,则可以做到自动刷新
5.3、如何注入List、Map
Spring通过converter将配置转换为各种形式,默认支持逗号分隔形式,将String转换为List、Set。如果要实现其它转换,可通过自定义Converter完成。