SpringBoot基础篇之@Value中哪些你不知道的知识点

image

SpringBoot基础篇@Value中哪些你不知道的知识点

看到这个标题,有点夸张了啊,@Value 这个谁不知道啊,不就是绑定配置么,还能有什么特殊的玩法不成?

(如果下面列出的这些问题,已经熟练掌握,那确实没啥往下面看的必要了)

  • @Value对应的配置不存在,会怎样?
  • 默认值如何设置
  • 配置文件中的列表可以直接映射到列表属性上么?
  • 配置参数映射为简单对象的三种配置方式
  • 除了配置注入,字面量、SpEL支持是否了解?
  • 远程(如db,配置中心,http)配置注入可行否?

接下来,限于篇幅问题,将针对上面提出的问题的前面几条进行说明,最后两个放在下篇

I. 项目环境

先创建一个用于测试的SpringBoot项目,源码在最后贴出,友情提示源码阅读更友好

1. 项目依赖

本项目借助SpringBoot 2.2.1.RELEASE + maven 3.5.3 + IDEA进行开发

2. 配置文件

在配置文件中,加一些用于测试的配置信息

application.yml

auth:
  jwt:
    token: TOKEN.123
    expire: 1622616886456
    whiteList: 4,5,6
    blackList:
      - 100
      - 200
      - 300
    tt: token:tt_token; expire:1622616888888

II. 使用case

1. 基本姿势

通过${}来引入配置参数,当然前提是所在的类被Spring托管,也就是我们常说的bean

如下,一个常见的使用姿势

@Component
public class ConfigProperties {

    @Value("${auth.jwt.token}")
    private String token;

    @Value("${auth.jwt.expire}")
    private Long expire;
}

2. 配置不存在,抛异常

接下来,引入一个配置不存在的注入,在项目启动的时候,会发现抛出异常,导致无法正常启动

/**
 * 不存在,使用默认值
 */
@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}"

image

所以为了避免上面的问题,一般来讲,建议设置一个默认值,规则如 ${key:默认值}, 在分号右边的就是默认值,当没有相关配置时,使用默认值初始化

/**
 * 不存在,使用默认值
 */
@Value("${auth.jwt.no}")
private String no;

3. 列表配置

在配置文件中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来看一下配置是怎样的

image

通过auth.jwt.blackList是拿不到配置信息的,只能通过auth.jwt.blackList[0], auth.jwt.blackList[1]来获取

那么问题来了,怎么解决这个呢?

要解决问题,关键就是需要知道@Value的工作原理,这里直接给出关键类 org.springframework.context.support.PropertySourcesPlaceholderConfigurer

image

关键点就在上面圈出的地方,找到这里,我们就可以动手开撸,一个比较猥琐的方法,如下

// 使用自定义的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);
    }
}

说明:

  • 上面这种实现姿势很不优雅,讲道理应该有更简洁的方式,有请知道的老哥指教一二

4. 配置转实体类

通常,@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;
    }
}

4.1 PropertyEditor

请注意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;
    }
}

说明

  • 当上面的JwtEditorJwt对象,在相同的包路径下面的时候,不需要上面的主动注册,Spring会自动注册 (就是这么贴心)

上面这个配置完毕之后,就可以正确的被注入了

/**
 * 借助 PropertyEditor 来实现字符串转对象
 */
@Value("${auth.jwt.tt}")
private Jwt tt;
image

4.2 Converter

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

再次测试,同样可以注入成功

image

4.3 Formatter

最后再介绍一个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同时存在时,后者优先级更高

image

5. 小结

限于篇幅,这里就暂告一段落,针对前面提到的几个问题,做一个简单的归纳小结

  • @Value 声明的配置不存在时,抛异常(项目会起不来)
  • 通过设置默认值(语法 ${xxx:defaultValue})可以解决上面的问题
  • yaml配置中的数组,无法直接通过@Value绑定到列表/数组上
  • 配置值为英文逗号分隔的场景,可以直接赋值给列表/数组
  • 不支持将配置文件中的值直接转换为非简单对象,如果有需要有三种方式
    • 使用PropertyEditor实现类型转换
    • 使用Converter实现类型转换 (更推荐使用这种方式)
    • 使用Formater实现类型转换

除了上面的知识点之外,针对最开始提出的问题,给出答案

  • @Value支持字面量,也支持SpEL表达式
  • 既然支持SpEL表达式,当然就可以实现我们需求的远程配置注入了

既然已经看到这里了,那么就再提两个问题吧,在SpringCloud微服务中,如果使用了SpringCloud Config,也是可以通过@Value来注入远程配置的,那么这个原理又是怎样的呢?

@Value绑定的配置,如果想实现动态刷新,可行么?如果可以怎么玩?

(顺手不介意的话,关注下微信公众号"一灰灰blog", 下篇博文就给出答案)

III. 不能错过的源码和相关知识点

0. 项目

系列博文,配合阅读效果更好哦

1. 一灰灰Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容