大家好,我是小富~

从装备文件中获取特点应该是SpringBoot开发中最为常用的功用之一,但便是这么常用的功用,依然有很多开发者在这个方面踩坑。

我整理了几种获取装备特点的办法,目的不仅是要让大家学会怎么运用,更重要的是澄清装备加载、读取的底层原理,一旦呈现问题能够分析出其症结所在,而不是一报错取不到特点,无头苍蝇般的重启项目,在句句卧槽中逐步抓狂~

以下示例源码 Springboot 版本均为 2.7.6

下边咱们一一过下这几种玩法和原理,看看有哪些是你没用过的!话不多说,开端搞~

一、Environment

运用 Environment 办法来获取装备特点值非常简略,只要注入Environment类调用其办法getProperty(特点key)即可,但知其然知其所以然,简略了解下它的原理,由于后续的几种获取装备的办法都和它休戚相关。

@Slf4j
@SpringBootTest
public class EnvironmentTest {
    @Resource
    private Environment env;
    @Test
    public void var1Test() {
        String var1 = env.getProperty("env101.var1");
        log.info("Environment 装备获取 {}", var1);
    }
}

1、什么是 Environment?

Environment 是 springboot 中心的环境装备接口,它提供了简略的办法来访问运用程序特点,包括体系特点、操作体系环境变量、命令行参数、和运用程序装备文件中界说的特点等等。

2、装备初始化

Springboot 程序发动加载流程里,会执行SpringApplication.run中的prepareEnvironment()办法进行装备的初始化,那初始化过程每一步都做了什么呢?

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
			DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
      /** 
      * 1、创立 ConfigurableEnvironment 目标:首先调用 getOrCreateEnvironment() 办法获取或创立
      * ConfigurableEnvironment 目标,该目标用于存储环境参数。假如现已存在 ConfigurableEnvironment 目标,则直接运用它;不然,根据用户的装备和默许装备创立一个新的。
      */
      ConfigurableEnvironment environment = getOrCreateEnvironment();
      /**
      * 2、解析并加载用户指定的装备文件,将其作为 PropertySource 增加到环境目标中。该办法默许会解析 application.properties 和 application.yml 文件,并将其增加到 ConfigurableEnvironment 目标中。
      * PropertySource 或 PropertySourcesPlaceholderConfigurer 加载运用程序的定制化装备。
      */
      configureEnvironment(environment, applicationArguments.getSourceArgs());
      // 3、加载一切的体系特点,并将它们增加到 ConfigurableEnvironment 目标中
      ConfigurationPropertySources.attach(environment);
      // 4、通知监听器环境参数现已准备就绪
      listeners.environmentPrepared(bootstrapContext, environment);
      /**
      *  5、将默许的特点源中的一切特点值移到环境目标的行列末尾,
      这样用户自界说的特点值就能够掩盖默许的特点值。这是为了防止用户无意中掩盖了 Spring Boot 所提供的默许特点。
      */
      DefaultPropertiesPropertySource.moveToEnd(environment);
      Assert.state(!environment.containsProperty("spring.main.environment-prefix"),
          "Environment prefix cannot be set via properties.");
      // 6、将 Spring Boot 运用程序的特点绑定到环境目标上,以便能够正确地读取和运用这些装备特点
      bindToSpringApplication(environment);
      // 7、假如没有自界说的环境类型,则运用 EnvironmentConverter 类型将环境目标转换为标准的环境类型,并增加到 ConfigurableEnvironment 目标中。
      if (!this.isCustomEnvironment) {
        EnvironmentConverter environmentConverter = new EnvironmentConverter(getClassLoader());
        environment = environmentConverter.convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());
      }
      // 8、再次加载体系装备,以防止被其他装备掩盖
      ConfigurationPropertySources.attach(environment);
      return environment;
}

看看它的装备加载流程过程:

  • 创立 环境目标 ConfigurableEnvironment 用于存储环境参数;
  • configureEnvironment 办法加载默许的 application.propertiesapplication.yml 装备文件;以及用户指定的装备文件,将其封装为 PropertySource 增加到环境目标中;
  • attach(): 加载一切的体系特点,并将它们增加到环境目标中;
  • listeners.environmentPrepared(): 发送环境参数装备现已准备就绪的监听通知;
  • moveToEnd(): 将 体系默许 的特点源中的一切特点值移到环境目标的行列末尾,这样用户自界说的特点值就能够掩盖默许的特点值。
  • bindToSpringApplication: 运用程序的特点绑定到 Bean 目标上;
  • attach(): 再次加载体系装备,以防止被其他装备掩盖;

上边的装备加载流程中,各种装备特点会封装成一个个笼统的数据结构 PropertySource中,这个数据结构代码格式如下,key-value形式。

public abstract class PropertySource<T> {
    protected final String name; // 特点源称号
    protected final T source; // 特点源值(一个泛型,比如Map,Property)
    public String getName();  // 获取特点源的名字  
    public T getSource(); // 获取特点源值  
    public boolean containsProperty(String name);  //是否包括某个特点  
    public abstract Object getProperty(String name);   //得到特点名对应的特点值   
} 

PropertySource 有许多的完结类用于办理运用程序的装备特点。不同的 PropertySource 完结类能够从不同的来历获取装备特点,例如文件、环境变量、命令行参数等。其间涉及到的一些完结类有:

6 种方式读取 Springboot 的配置,老鸟都这么玩(原理+实战)

  • MapPropertySource: Map 键值对的目标转换为 PropertySource 目标的适配器;
  • PropertiesPropertySource: Properties 目标中的一切装备特点转换为 Spring 环境中的特点值;
  • ResourcePropertySource: 从文件体系或许 classpath 中加载装备特点,封装成 PropertySource目标;
  • ServletConfigPropertySource: Servlet 装备中读取装备特点,封装成 PropertySource 目标;
  • ServletContextPropertySource: Servlet 上下文中读取装备特点,封装成 PropertySource 目标;
  • StubPropertySource: 是个空的完结类,它的作用仅仅是给 CompositePropertySource 类作为默许的父级特点源,以防止空指针反常;
  • CompositePropertySource: 是个复合型的完结类,内部保护了 PropertySource调集行列,能够将多个 PropertySource 目标合并;
  • SystemEnvironmentPropertySource: 操作体系环境变量中读取装备特点,封装成 PropertySource 目标;

上边各类装备初始化生成的 PropertySource 目标会被保护到调集行列中。

List<PropertySource<?>> sources = new ArrayList<PropertySource<?>>()

装备初始化结束,运用程序上下文AbstractApplicationContext会加载装备,这样程序在运转时就能够随时获取装备信息了。

	private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,
			ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
			ApplicationArguments applicationArguments, Banner printedBanner) {
    // 运用上下文加载环境目标
		context.setEnvironment(environment);
		postProcessApplicationContext(context);
    .........
  }

3、读取装备

看明白上边装备加载的流程,其实读取装备就容易理解了,无非便是遍历行列里的PropertySource,拿特点称号name匹配对应的特点值source

PropertyResolver是获取装备的要害类,其内部提供了操作PropertySource 行列的办法,中心办法getProperty(key)获取装备值,看了下这个类的依靠关系,发现 Environment 是它子类。

6 种方式读取 Springboot 的配置,老鸟都这么玩(原理+实战)

那么直接用 PropertyResolver 来获取装备特点其实也是能够的,到这咱们就大致明白了 Springboot 装备的加载和读取了。

@Slf4j
@SpringBootTest
public class EnvironmentTest {
    @Resource
    private PropertyResolver env;
    @Test
    public void var1Test() {
        String var1 = env.getProperty("env101.var1");
        log.info("Environment 装备获取 {}", var1);
    }
}

二、@Value 注解

@Value注解是Spring框架提供的用于注入装备特点值的注解,它可用于类的成员变量办法参数构造函数参数上,这个记住很重要!

在运用程序发动时,运用 @Value 注解的 Bean 会被实例化。一切运用了 @Value 注解的 Bean 会被加入到 PropertySourcesPlaceholderConfigurer 的后置处理器调集中。

当后置处理器开端执行时,它会读取 Bean 中一切 @Value 注解所标示的值,并经过反射将解析后的特点值赋值给标有 @Value 注解的成员变量、办法参数和构造函数参数。

需求留意,在运用 @Value 注解时需求保证注入的特点值现已加载到 Spring 容器中,不然会导致注入失败。

怎么运用

src/main/resources目录下的application.yml装备文件中增加env101.var1特点。

env101:
  var1: var1-大众号:程序员小富

只要在变量上加注解 @Value("${env101.var1}")就能够了,@Value 注解会主动将装备文件中的env101.var1特点值注入到var1字段中,跑个单元测试看一下成果。

@Slf4j
@SpringBootTest
public class EnvVariablesTest {
    @Value("${env101.var1}")
    private String var1;
    @Test
    public void var1Test(){
        log.info("装备文件特点: {}",var1);
    }
}

毫无悬念,成功拿到装备数据。

6 种方式读取 Springboot 的配置,老鸟都这么玩(原理+实战)

虽然@Value注解办法运用起来很简略,假如运用不当还会遇到不少坑。

1、缺失装备

假如在代码中引证变量,装备文件中未进行配值,就会呈现相似下图所示的过错。

6 种方式读取 Springboot 的配置,老鸟都这么玩(原理+实战)

为了防止此类过错导致服务发动反常,咱们能够在引证变量的一起给它赋一个默许值,以保证即便在未正确配值的情况下,程序依然能够正常运转。

@Value("${env101.var1:我是小富}")
private String var1;

2、静态变量(static)赋值

还有一种常见的运用误区,便是将 @Value 注解加到静态变量上,这样做是无法获取特点值的。静态变量是类的特点,并不归于目标的特点,而 Spring是基于目标的特点进行依靠注入的,类在运用发动时静态变量就被初始化,此刻 Bean还未被实例化,因而不可能经过 @Value 注入特点值。

@Slf4j
@SpringBootTest
public class EnvVariablesTest {
    @Value("${env101.var1}")
    private static String var1;
    @Test
    public void var1Test(){
        log.info("装备文件特点: {}",var1);
    }
}

即便 @Value 注解无法直接用在静态变量上,咱们依然能够经过获取已有 Bean实例化后的特点值,再将其赋值给静态变量来完结给静态变量赋值。

咱们能够先经过 @Value 注解将特点值注入到一般 Bean中,然后在获取该 Bean对应的特点值,并将其赋值给静态变量。这样,就能够在静态变量中运用该特点值了。

@Slf4j
@SpringBootTest
public class EnvVariablesTest {
    private static String var3;
    private static String var4;
    @Value("${env101.var3}")
    public void setVar3(String var3) {
        var3 = var3;
    }
    EnvVariablesTest(@Value("${env101.var4}") String var4){
        var4 = var4;
    }
    public static String getVar4() {
        return var4;
    }
    public static String getVar3() {
        return var3;
    }
}

3、常量(final)赋值

@Value 注解加到final要害字上相同也无法获取特点值,由于 final 变量必须在构造办法中进行初始化,并且一旦被赋值便不能再次更改。而 @Value 注解是在 bean 实例化之后才进行特点注入的,因而无法在构造办法中初始化 final 变量。

@Slf4j
@SpringBootTest
public class EnvVariables2Test {
    private final String var6;
    @Autowired
    EnvVariables2Test( @Value("${env101.var6}")  String var6) {
        this.var6 = var6;
    }
    /**
     * @value注解 final 获取
     */
    @Test
    public void var1Test() {
        log.info("final 注入: {}", var6);
    }
}

4、非注册的类中运用

只要标示了@Component@Service@Controller@Repository@Configuration容器办理注解的类,由 Spring 办理的 bean 中运用 @Value注解才会生效。而对于一般的POJO类,则无法运用 @Value注解进行特点注入。

/**
 * @value注解 非注册的类中运用
 * `@Component`、`@Service`、`@Controller`、`@Repository` 或 `@Configuration` 等
 * 容器办理注解的类中运用 @Value注解才会生效
 */
@Data
@Slf4j
@Component
public class TestService {
    @Value("${env101.var7}")
    private String var7;
    public String getVar7(){
       return this.var7;
    }
}

5、引证办法不对

假如咱们想要获取 TestService 类中的某个变量的特点值,需求运用依靠注入的办法,而不能运用 new 的办法。经过依靠注入的办法创立 TestService 目标,Spring 会在创立目标时将目标所需的特点值注入到其间。

  /**
   * @value注解 引证办法不对
   */
  @Test
  public void var7_1Test() {
      TestService testService = new TestService();
      log.info("引证办法不对 注入: {}", testService.getVar7());
  }

最后总结一下 @Value注解要在 Bean的生命周期内运用才干生效。

三、@ConfigurationProperties 注解

@ConfigurationProperties注解是 SpringBoot 提供的一种愈加快捷来处理装备文件中的特点值的办法,能够经过主动绑定和类型转换等机制,将指定前缀的特点调集主动绑定到一个Bean目标上。

加载原理

在 Springboot 发动流程加载装备的 prepareEnvironment() 办法中,有一个重要的过程办法 bindToSpringApplication(environment),它的作用是将装备文件中的特点值绑定到被 @ConfigurationProperties 注解符号的 Bean目标中。但此刻这些目标还没有被 Spring 容器办理,因而无法完结特点的主动注入。

那么这些Bean目标又是什么时候被注册到 Spring 容器中的呢?

这就涉及到了 ConfigurationPropertiesBindingPostProcessor 类,它是 Bean后置处理器,担任扫描容器中一切被 @ConfigurationProperties 注解所符号的 Bean目标。假如找到了,则会运用 Binder 组件将外部特点的值绑定到它们身上,从而完结主动注入。

6 种方式读取 Springboot 的配置,老鸟都这么玩(原理+实战)

  • bindToSpringApplication 主要是将特点值绑定到 Bean 目标中;
  • ConfigurationPropertiesBindingPostProcessor 担任在 Spring 容器发动时将被注解符号的 Bean 目标示册到容器中,并完结后续的特点注入操作;

怎么运用

演示运用 @ConfigurationProperties 注解,在 application.yml 装备文件中增加装备项:

env101:
  var1: var1-大众号:程序员小富
  var2: var2-大众号:程序员小富

创立一个 MyConf 类用于承载一切前缀为env101的装备特点。

@Data
@Configuration
@ConfigurationProperties(prefix = "env101")
public class MyConf {
    private String var1;
    private String var2;
}

在需求运用var1var2特点值的当地,将 MyConf 目标示入到依靠目标中即可。

@Slf4j
@SpringBootTest
public class ConfTest {
    @Resource
    private MyConf myConf;
    @Test
    public void myConfTest() {
        log.info("@ConfigurationProperties注解 装备获取 {}", JSON.toJSONString(myConf));
    }
}

四、@PropertySources 注解

除了体系默许的 application.yml 或许 application.properties 文件外,咱们还可能需求运用自界说的装备文件来完结愈加灵敏和个性化的装备。与默许的装备文件不同的是,自界说的装备文件无法被运用主动加载,需求咱们手动指定加载。

@PropertySources 注解的完结原理相对简略,运用程序发动时扫描一切被该注解标示的类,获取到注解中指定自界说装备文件的途径,将指定途径下的装备文件内容加载到 Environment 中,这样能够经过 @Value 注解或 Environment.getProperty() 办法来获取其间界说的特点值了。

怎么运用

在 src/main/resources/ 目录下创立自界说装备文件 xiaofu.properties,增加两个特点。

env101.var9=var9-程序员小富
env101.var10=var10-程序员小富

在需求运用自界说装备文件的类上增加 @PropertySources 注解,注解 value特点中指定自界说装备文件的途径,能够指定多个途径,用逗号隔开。

@Data
@Configuration
@PropertySources({
        @PropertySource(value = "classpath:xiaofu.properties",encoding = "utf-8"),
        @PropertySource(value = "classpath:xiaofu.properties",encoding = "utf-8")
})
public class PropertySourcesConf {
    @Value("${env101.var10}")
    private String var10;
    @Value("${env101.var9}")
    private String var9;
}

成功获取装备了

6 种方式读取 Springboot 的配置,老鸟都这么玩(原理+实战)

可是当我试图加载.yaml文件时,发动项目居然报错了,经过一番摸索我发现,@PropertySources 注解只内置了PropertySourceFactory适配器。也便是说它只能加载.properties文件。

6 种方式读取 Springboot 的配置,老鸟都这么玩(原理+实战)

那假如我想要加载一个.yaml类型文件,则需求自行完结yaml的适配器 YamlPropertySourceFactory

public class YamlPropertySourceFactory implements PropertySourceFactory {
    @Override
    public PropertySource<?> createPropertySource(String name, EncodedResource encodedResource) throws IOException {
        YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
        factory.setResources(encodedResource.getResource());
        Properties properties = factory.getObject();
        return new PropertiesPropertySource(encodedResource.getResource().getFilename(), properties);
    }
}

而在加载装备时要显现的指定运用 YamlPropertySourceFactory适配器,这样就完结了@PropertySource注解加载 yaml 文件。

@Data
@Configuration
@PropertySources({
        @PropertySource(value = "classpath:xiaofu.yaml", encoding = "utf-8", factory = YamlPropertySourceFactory.class)
})
public class PropertySourcesConf2 {
    @Value("${env101.var10}")
    private String var10;
    @Value("${env101.var9}")
    private String var9;
}

五、YamlPropertiesFactoryBean 加载 YAML 文件

咱们能够运用 YamlPropertiesFactoryBean 类将 YAML 装备文件中的特点值注入到 Bean 中。

@Configuration
public class MyYamlConfig {
    @Bean
    public static PropertySourcesPlaceholderConfigurer yamlConfigurer() {
        PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer();
        YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean();
        yaml.setResources(new ClassPathResource("xiaofu.yml"));
        configurer.setProperties(Objects.requireNonNull(yaml.getObject()));
        return configurer;
    }
}

能够经过 @Value 注解或 Environment.getProperty() 办法来获取其间界说的特点值。

@Slf4j
@SpringBootTest
public class YamlTest {
    @Value("${env101.var11}")
    private String var11;
    @Test
    public void  myYamlTest() {
        log.info("Yaml 装备获取 {}", var11);
    }
}

六、自界说读取

假如上边的几种读取装备的办法你都不喜欢,就想自己写个更流批的轮子,那也很好办。咱们直接注入PropertySources获取一切特点的装备行列,你是想用注解完结还是其他什么办法,就能够随心所欲了。

@Slf4j
@SpringBootTest
public class CustomTest {
    @Autowired
    private PropertySources propertySources;
    @Test
    public void customTest() {
        for (PropertySource<?> propertySource : propertySources) {
            log.info("自界说获取 装备获取 name {} ,{}", propertySource.getName(), propertySource.getSource());
        }
    }
}

总结

咱们能够经过 @Value 注解、Environment 类、@ConfigurationProperties 注解、@PropertySource 注解等办法来获取装备信息。

其间,@Value 注解适用于单个值的注入,而其他几种办法适用于批量装备的注入。不同的办法在功率、灵敏性、易用性等方面存在差异,在挑选装备获取办法时,还需求考虑个人编程习惯和业务需求。

假如注重代码的可读性和可保护性,则能够挑选运用 @ConfigurationProperties 注解;假如更注重运转功率,则能够挑选运用 Environment 类。总归,不同的场景需求挑选不同的办法,以到达最优的效果。

我是小富,下期见~

以上案例地址:github.com/chengxy-nds…