SpringBoot配置加载原理

前言

在日常开发中,项目的配置数据是再平常不过的操作。通常使用的配置方式有启动参数配置、配置文件配置、配置中心(Apollo、Nacos等)配置。然而,如果配置使用不合理就会有惨痛的教训(不要问我为什么知道的)。

场景描述

在SpringBoot项目中配置数据通常使用@Value注解进行数据的注入。大致场景是这样的:使用@Value注解为一个静态变量配置数据后,项目启动时该值为空,通过Apollo配置中心变更该配置数据后,该变量实际数据符合预期为变更值。 代码大致如下:

java 复制代码
public class ApolloValueService {
    @Value("${apollo.value:10}")
    public static Long staticApolloValue;
    @Value("${apollo.value:10}")
    private Long apolloValue;
    
    public String getApolloValue() {
        return "static value:" + staticApolloValue + " private value:" + apolloValue;
    }
}

通过GET请求调用该方法后,运行结果如下:

  • 项目启动时:

调用结果:"static value:null private value:10"

  • 通过Apollo变更apollo.value的值为11

调用结果:"static value:11 private value:11"

问题解决

上述现象的问题是对静态变量的数据注入不正确(虽然是在测试阶段发现并没有产生生产影响)这一问题解决起来并不麻烦,解决的方式有3种。

  • set方法
java 复制代码
@Value("${apollo.value:10}")
public void setStaticApolloValue(Long val) {
    staticApolloValue = val;
}
  • set变种
java 复制代码
@Autowired
public void setStaticApolloValue(@Value("${apollo.value:10}") Long val){
    staticApolloValue = val;
}
  • @PostConstruct注解赋值
java 复制代码
@Value("${apollo.value:10}")
private Long apolloValue;

@PostConstruct
public void init() {
    staticApolloValue = apolloValue;
}

原理分析

上述问题解决起来还是比较简单的,但是产生这一现象的原因是需要分析的。上述现象可以拆解为2部分分别为:项目启动时静态变量为什么为空、通过Apollo变更后静态变量的值为什么为变更值。

  • 项目启动时为空

为验证该现象,在ApolloValueService类中添加如下代码,观察静态变量数值情况,变更代码如下:

java 复制代码
public class ApolloValueService { 
    @Value("${apollo.value:10}") 
    public static Long staticApolloValue; 
    @Value("${apollo.value:10}") 
    private Long apolloValue; 
    
    static {
        System.out.println(String.format("--------静态变量加载,staticApolloValue:%s--------", staticApolloValue));
    }
    
    public String getApolloValue() { 
        return "static value:" + staticApolloValue + " private value:" + apolloValue; 
    } 
}

再次启动项目后,控制台输出结果:

--------静态变量加载,staticApolloValue:null--------

根据结果看项目在加载类过程中静态变量没有被成功赋值,在启动完成后该静态变量也没有被赋值。对代码进行调试后发现如下结论:

  • SpringBoot是通过反射进行变量注入
  • SpringBoot启动时会过滤静态变量的注入行为

如此,可以大致解释在项目启动后,静态变量为什么为null的现象了。staticApolloValue变量属于类的属性;apolloValue变量属于对象属性。在项目启动过程中,本次涉及的阶段顺序如下:

  1. 加载类信息(即是staticApolloValue静态变量)
  2. Servlet容器初始化
  3. Spring容器初始化
  4. 启动Servlet容器
  5. 启动完成

当类加载完成后,staticApollo静态变量并没有被赋值。@Value注解开始运行的时机在Spring容器初始化过程中(该过程会进行bean的创建)。@Value实现属性注入的原理是通过反射将值注入的。由于在Spring启动过程中会将在静态变量和静态方法上的@Value注解过滤,所以在Spring启动过程中并不会对该属性进行注入值。所以静态变量始终没有机会通过反射注入值。

  • Apollo变更后为预期值

在翻阅Apollo源码发现如下关键点:

  • Apollo客户端与服务端建立了长连接实时感知服务端的变化
  • 当服务单发生变更后,客户端会通过反射将变更量同步到项目中
  • Apollo通过反射进行变更时,并没有过来静态变量的注入

以上内容可以解释为什么通过Apollo变更变量值后静态变量的值符合预期

收获

在排查上述问题的过程中,对Spring源码和Apollo源码有了如下收获:

  1. 在使用apollo-start时,项目中存在3个bean(AutoUpdateConfigChangeListener、SpringValueProcessor、ApolloProcessor)其中SpringValueProcessor、ApolloProcessor2个类属于继承关系,所以@Value注解能够在项目启动过程中读取到Apollo对配置数据。
  2. 在bean初始化前后分别调用了postProcessBeforeInitialization()、postProcessAfterInitialization()方法,通过反射进行逻辑处理。
  3. 上述解决静态变量注入的3种方案,思路是相同的。都是在Spring启动过程创建bean时对静态变量赋值。
  4. 尽量不要对静态变量进行属性注入行为。
  5. 若无法避免使用静态变量属性注入,需使用正确的注入方式。
相关推荐
bing_1582 小时前
为什么选择 Spring Boot? 它是如何简化单个微服务的创建、配置和部署的?
spring boot·后端·微服务
三个蔡4 小时前
Java求职者面试:从Spring Boot到微服务的技术深度探索
java·大数据·spring boot·微服务·kubernetes
小鸡脚来咯4 小时前
SpringBoot 常用注解通俗解释
java·spring boot·后端
创码小奇客4 小时前
MongoDB 事务:数据世界的守护者联盟全解析
spring boot·mongodb·trae
中国lanwp5 小时前
springboot logback 默认加载配置文件顺序
java·spring boot·logback
cherishSpring5 小时前
在windows使用docker打包springboot项目镜像并上传到阿里云
spring boot·docker·容器
苹果酱05675 小时前
【Azure Redis 缓存】在Azure Redis中,如何限制只允许Azure App Service访问?
java·vue.js·spring boot·mysql·课程设计
慧一居士7 小时前
Kafka HA集群配置搭建与SpringBoot使用示例总结
spring boot·后端·kafka
uncofish8 小时前
springboot不连接数据库启动(原先连接了mysql数据库)
数据库·spring boot·mysql