Springboot应用系列—外部化配置之@Value的最佳实践

2,235 阅读5分钟

一. 写在前面

  很久没有写文章了。一是下半年工作太忙了,二是有几篇热门文章不知道被多少有名有号的大V无情洗稿了,追责无果实在觉得委屈心累。(还有一个重要的原因:懒,哈哈哈~)。

  通过最近这段时间的思考,决定从今往后还是要持续输出,不为名利,为自己的学习经历留下印记而已。

二. 背景

  距离上一次系统学习Springboot官方文档要追溯到2年前了。最近趁着春节假期计划再梳理一遍Springboot官文,温故而知新嘛。

  在对于第二章节外部化配置系统梳理之后,发现合理的使用外部化配置可以使得项目配置管理更加优雅,使用更加简洁。在Springboot中支持多种风格的外部化配置方式:Java属性文件、YAML文件、环境变量、命令行参数等。Springboot官方推荐:【应当坚持使用一种风格,不要混搭使用】本篇文章将梳理@Value的使用实践。

三. @Value的最佳实践

  @Value非常强大,使用也非常简单。它的注入的数据来源有多种途径,所以给人一种感觉使用方式有多种。其实如果阅读过相关的源码就会发现:它的本质就是依赖注入。

3.1 直接注入

@Value("zhangsan")
private Stirng stuName;

3.2 注入系统属性

  在Spring容器当中,Environment环境变量主要包含了两部分:系统环境变量和JVM环境变量。只要是Environment中存在的信息,都可以直接使用占位符注入。
如果需要某个系统属性的情况下,则可以采用此种方式来获取。

@Value("${os.name}") // 可以这样使用的,因为spring把系统环境变量都放入了Environment当中了。
private Stirng osName;

3.3 结合application.yml配置文件

这种方式应该是我们应用的最多的方式。如下所示:

application.yml中:
stu: 
  name: zhangsan


@Value("${stu.name}")
private Stirng stuName;

// 如果在application中没有配置,在启动时会报错。可以通过指定默认值的方式避免报错:
@Value("${stu.name:zhangsan}")
private String stuName;

  如果我们的项目越来越大,单个配置越来越多了,如果都采取这种方式注入的话,那么application.yml配置文件将会变得越来越冗杂,我们基于配置分层分类的思想可以来优化这种情况,具体实现手段有很多种。比如在application.yml同级目录下新建config/my.properties文件:

my.name=leon
my.home=GE_LAI_MEI

然后在application.yml配置文件中配置:

spring:
  config:
    import: classpath:config/my.properties

这样我们就可以将所有的配置都放在my.properties当中了。 一样可以通过占位符来注入:

@Value("${my.name}")
private String myName;

@Value("${my.home}")
private String myHome;

在实际项目中还有一个现实问题,不同的环境需要不同的配置,也可以基于占位符来实现。比如配置文件目录结构如下: 配置文件目录结构

在application.yml中配置如下:

spring:
    profiles:
        active: pro # 激活pro
    config:
        import: classpath:config/my-${spring.profiles.active}.properties

通过这样的方式就可以实现配置的环境区分。这里只是一个demo举例,实际上的项目配置更为复杂,我们一步步往下看。

接下来看看解析数组的情况:

my.name=leon-dev
my.home=GE_LAI_MEI
demo.intParam=12
demo.strParam=word
demo.strArr=a,b,c,d

配置类如下:

package com.leon.springbootdemo.config;

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * @author sunliming11
 * @date created in 2021/2/13
 */
@Data
@Component
@ToString
public class DemoConfig {

    @Value("${demo.intParam}")
    private Integer intParam;
    @Value("${demo.strParam}")
    private String strParam;
    @Value("${demo.strArr}")
    private String [] strArr;
}

启动类如下:

@SpringBootApplication
public class SpringBootDemoApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext applicationContext = SpringApplication.run(SpringBootDemoApplication.class, args);
        DemoConfig demoConfig = applicationContext.getBean(DemoConfig.class);
        System.out.println(demoConfig.toString());
    }
}

  执行之后,控制台打印如下: 说明配置生效。
如果我们将String[] strArr 改为List<String> list,会怎样呢?我这个版本是可以执行的。但是在以前的版本中,如果在yml配置文件不管是行内还是行外配置还是我这种单独配置的方式,都会注入失败。这个问题在springboot issue 被提到过,应该是后续版本解决了这个问题了吧。但我看到(于2021.02.15看到的)有一个阅读量上万的关于@Value注解的使用说明文章,里面却还在说这种方式会注入失败。。。所以不禁再一次感慨纸上得来终觉浅,绝知此事要躬行
对于我个人而言要记住一点就是:一般情况下对于List、Set、Map、Object的注入方式尽量不要采用@Value的方式注入,因为从@Value的使用方式、实现原理、源码层面(后文解释源码)来看,它并不适用于这种复杂的注入场景。但如果配置较为简单,且不作为SDK包给别人使用的话,还是可以考虑使用的,一句话:分场景。

3.4 @Value结合SpringEL表达式

  上文介绍到@Value不适用于复杂的注入场景,但有时候我们又不想因为@Value提供不了复杂注入而引入额外的配置项目,这个时候就可以基于SpringEL表达式提供的强大能力实现一些骚操作了。

3.4.1 基于SpringEL表达式注入随机数
@Value("#{T(java.lang.Math).random() * 1000}")
private Double randomNum;
3.4.2 基于SpringEL表达式注入List、Set等
@Value("#{'${demo.strArr}'.split(',')}")
private List<String> list;
3.4.3 基于SpringEL表达式注入Object
@Value("#{T(com.leon.springbootdemo.config.DemoConfig).convert('${demo.obj}')}")
private ChildConfig childConfig;

完整的Config类如下:

package com.leon.springbootdemo.config;

import com.leon.p01.common.util.JsonUtil;
import lombok.Data;
import lombok.ToString;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * @author sunliming11
 * @date created in 2021/2/13
 */
@Data
@Component
@ToString
public class DemoConfig {

    @Value("${demo.intParam}")
    private Integer intParam;
    @Value("${demo.strParam}")
    private String strParam;
    @Value("#{'${demo.strArr}'.split(',')}")
    private List<String> list;
    @Value("#{T(com.leon.springbootdemo.config.DemoConfig).convert('${demo.obj}')}")
    private ChildConfig childConfig;

    public static ChildConfig convert(String jsonStr) {
        return JsonUtil.json2Obj(jsonStr, ChildConfig.class);
    }
}

@Data
@ToString
class ChildConfig {
    private Boolean enabled;
    private String url;
}

配置文件如下:

my.name=leon-dev
my.home=GE_LAI_MEI
demo.intParam=12
demo.strParam=word
demo.strArr=a,b,c,d
demo.obj={"enabled": true, "url": "https://jd.com"}

启动执行结果如下: 可以看到,注入成功了。
我们通过自定义解析的方式实现了注入,在SpringEL表达式中T(...)中是自定义的解析器。也就是说我们可以通过自定义解析器实现任何我们想要解析的数据并注入。

3.4.4 注入其他bean的属性

通过SpringEL表达式还可以注入其他bean的属性。

@Bean
public Person person() {
  return new Person("leon");
}

...
@Component
public class TempConfig {
  @Value("#{persion.name}")
  private String perName;
}

3.5 直接注入一个Resource文件

如果我们在classpath下配置了一个jdbc.properties文件。我们可以通过@Value直接将其注入成一个Resource资源。

@Value("classpath:jdbc.properties")
private Resource resourceFile; // 注入文件资源

@Test
public void contextLoads() throws IOException {
    System.out.println(resourceFile); //class path resource [jdbc.properties]
    String s = FileUtils.readFileToString(resourceFile.getFile(), StandardCharsets.UTF_8);
    System.out.println(s);
    //输出:
    //db.username=fangshixiang
    //db.password=fang
    //db.url=jdbc:mysql://localhost:3306/mytest
    //db.driver-class-name=com.mysql.jdbc.Driver
}

3.6 @Value的源码解析

通过上面的例子可以看到,@Value的功能非常强大,使用非常简约。这说明它背后的运作机制一定不简单的。
但如果我们对于Spring的生命周期了解的话,很容易就能找到其对应的解析入口了。 在spring的生命周期中,真正创建bean的方法是:

org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean

  在这个方法中初始化bean的方法委托给了initializeBean()。但是在执行初始化之前会先执行一个populateBean()方法。该方法承担着spring的依赖注入。而最终实现依赖注入的是通过后置处理器AutowiredAnnotationBeanPostProcessor来实现的,通过其无参构造方法可以发现它主要是解析@Autowired@Value类型的。其内部又是委托给InjectionMetadata来实现最终的注入的。

  整个过程的大致逻辑就是这样的,但是其内部的具体实现十分复杂,涉及到很多细节点,比如说SpringEL表达式的解析过程等等。具体的解析原理打算单独一片文章进行记录。

四. 总结

  本篇文章的目的在于系统梳理一下@Value的使用方式,作为手册记录以备用时查阅。当然知道其背后的运作原理才是主要核心目的。和@Value有相似功能但工作原理不同的的还有@ConfigurationProperties

  实际上@Value/@ConfigurationProperties等技术本质上是Springboot的外部化配置的不同解决方案。除了这两种之外还有其他外部化配置的解决方案,比如:@PropertySource或者@ImportResource等都很常见。只是各自的应用场景不同罢了。我们要做的就是知道主流的外部化配置方案以及背后运作的原理和应用场景,做到知其所以然。

  下一篇文章打算系统梳理其他常见的外部化配置方案。