乐趣区

关于springboot:6-种方式读取-Springboot-的配置老鸟都这么玩原理实战

大家好,我是小富~

从配置文件中获取属性应该是 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 实现类能够从不同的起源获取配置属性,例如文件、环境变量、命令行参数等。其中波及到的一些实现类有:

  • 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 是它子类。

那么间接用 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);
    }
}

毫无悬念,胜利拿到配置数据。

尽管 @Value 注解形式应用起来很简略,如果使用不当还会遇到不少坑。

1、缺失配置

如果在代码中援用变量,配置文件中未进行配值,就会呈现相似下图所示的谬误。

为了防止此类谬误导致服务启动异样,咱们能够在援用变量的同时给它赋一个默认值,以确保即便在未正确配值的状况下,程序仍然可能失常运行。

@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 组件将内部属性的值绑定到它们身上,从而实现主动注入。

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

胜利获取配置了

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

那如果我想要加载一个 .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 类。总之,不同的场景须要抉择不同的形式,以达到最优的成果。

我是小富,下期见~

以上案例地址:https://github.com/chengxy-nds/Springboot-Notebook

退出移动版