@Value注解原理

526 阅读2分钟

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 可得知,没有占位符${}时,直接返回常量;有占位符${}时,按照占位符从配置中获取值。取不到且没有默认值时,报错 image.png 查看PropertyPlaceholderHelper placeholderPrefix属性可发现,该属性只能通过构造函数赋值。 image.png 再查看 PropertyPlaceholderHelper 构造函数的引用,发现占位符都是 ${} 格式。默认值分隔符也仅支持 : 格式 image.png SystemPropertyUtils: image.png

4.1.2、配置值注入原理

通过debug调用栈,我们可以找到如下代码,该逻辑解析了@Value拿到结果后,是通过反射的方式注入到变量中

AutowiredAnnotationBeanPostProcessor: image.png

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;
}

服务启动时就会报错:

image.png 根据报错堆栈就知道处理点在哪了

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配置

加载源码:

image.png spring.factories:

org.springframework.boot.SpringApplicationRunListener=com.eden.research.demo.MyConverterListener

项目结构:

image.png 结果验证:

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完成。

6、示例代码

github.com/Naturaler/r…