一个有趣的static final属性赋值问题

问题引出

这是在一个SpringBoot环境的项目中,在熟悉一个接口的过程发现了这个很有趣的工具类。

java 复制代码
public class ValidatorUtils {

    private static final Validator VALID = SpringUtil.getBean(Validator.class);

    //代码略
}

我第一直觉觉得这个属性VALID的赋值很怪,static final属性在类加载时期就确定值了,而SpringUtil.getBean()具有动态性,在运行期才能正确获取Bean,这种赋值很有可能会无法获取相应的Bean。解释如下:SpringUtil是Hutool的Spring工具(自行查看相关源码能更清楚),而getBean()方法

java 复制代码
public static <T> T getBean(Class<T> clazz) {
   return getBeanFactory().getBean(clazz);
}
public static ListableBeanFactory getBeanFactory() {
   final ListableBeanFactory factory =  null == beanFactory ? applicationContext : beanFactory;
   if(null == factory){
      throw new UtilException("No ConfigurableListableBeanFactory or ApplicationContext injected, maybe not in the Spring environment?");
   }
   return factory;
}

如果factory为空就会造成抛出异常导致程序启动不了,factory是Spring到Bean工厂,在Spring容器创建期间(加载Bean定义并创建Bean实例之前)通过BeanFactoryPostProcessor的回调方法注入,看下面的类签名。

java 复制代码
@Component
public class SpringUtil implements BeanFactoryPostProcessor, ApplicationContextAware {

   private static ConfigurableListableBeanFactory beanFactory;

   private static ApplicationContext applicationContext;
   
    @SuppressWarnings("NullableProblems")
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
       SpringUtil.beanFactory = beanFactory;
    }

}

可以看出beanFactory的初始化是在运行期间完成的(applicationContext在这里不讨论,和beanFactory是等价的,但注入时机更晚),如果没有执行BeanFactoryPostProcessor的回调方法,即在类加载阶段beanFactory的初始值是null。

这就引出了一个很矛盾的点,ValidatorUtils类的VALID属性(被static final修饰)必须在类加载阶段就要初始化值,并且只能初始化一次。而SpringUtil.getBean(Validator.class)操作在运行期间(Spring容器初始化)才能确定,如果在SpringUtil类加载阶段,相应的beanFactory为null。按照分析ValidatorUtils类的VALID属性应该无法通过SpringUtil.getBean(Validator.class)获取值(准确的来说是null == factory成立抛出异常)。但在启动过程中代码并没有报错,并且成功获取到了Bean。由此可以猜测SpringUtil的类加载和BeanFactoryPostProcessor的回调方法执行比ValidatorUtils类早。能力有限想不通为什么对象方法的执行能比类加载时机早,并且BeanFactoryPostProcessor的回调方法一定会早于ValidatorUtils类的类加载,这个问题觉得很有趣就去寻找资料了。

结论

通过查找资料终于解决了上述问题,即在应用启动过程中满足了以下两个条件:

  1. 在JDK8中,当Java程序启动并调用main()方法时,并不是所有的类都会立即加载到JVM中。JVM的类加载过程是按需进行的,即在需要使用某个类时才会进行加载。 具体来说,当Java程序启动时,JVM会先加载包含main()方法的主类,也就是程序的入口类。然后,根据程序的执行流程和依赖关系,逐步加载其他被引用的类。这些类会在首次使用时进行加载,例如在代码中创建对象、调用静态方法或访问静态字段时。
  2. Spring容器的初始化代码执行先于ValidatorUtils类的首次使用。

第2点我开始我也没搞懂为什么一定成立,直到在理顺问题时突然意识到如下代码(当时真的有点想笑)。

java 复制代码
public class Springboot01Application {

    public static void main(String[] args) {
        SpringApplication.run(Springboot01Application.class, args);
    }
    
}

通过上述两点,就保证了ValidatorUtils类的VALID属性能被正确赋值了。

当然我们可以验证下该结论,先使用ValidatorUtils类,按照预期会抛出异常。

java 复制代码
@SpringBootApplication
public class Springboot01Application {

    public static void main(String[] args) {
        BeanA beanA = new BeanA(); 
        //ValidatorUtils类的一个方法,通过调用方法让ValidatorUtils类先加载
        ValidatorUtils.validate(beanA);  
        SpringApplication.run(Springboot01Application.class, args);
    }

}

启动应用结果如下

less 复制代码
Exception in thread "main" java.lang.ExceptionInInitializerError
	at com.xiaoyan.springboot01.Springboot01Application.main(Springboot01Application.java:13)
Caused by: cn.hutool.core.exceptions.UtilException: No ConfigurableListableBeanFactory or ApplicationContext injected, maybe not in the Spring environment?
	at cn.hutool.extra.spring.SpringUtil.getBeanFactory(SpringUtil.java:76)
	at cn.hutool.extra.spring.SpringUtil.getBean(SpringUtil.java:122)
	at com.xiaoyan.springboot01.entity.ValidatorUtils.<clinit>(ValidatorUtils.java:16)
	... 1 more

收获

  1. 通过对该问题的研究让我对Java8有了进一步的了解,虽然一直在学JVM,但也只是一些皮毛,希望通过问题驱动能够逐渐补全知识体系;
  2. 对Spring容器的创建过程也有了更多的认识;
  3. 天工AI搜索很好用,好像支持上下文,我就通过追问的方式慢慢的理清了上述问题。AI对话就算了。

最后应该 不建议通过上述方式赋值吧?太容易出错了,且出错原因隐藏的很深,如果不抛出异常都无法察觉。 本人经验尚浅,可能会有一定的错误,希望能友好提出。

相关推荐
干到60岁退休的码农8 小时前
15.创建获取用户接口与JWT认证器
java·spring boot·mybatis
小羊没烦恼!8 小时前
Memory 记忆设计讨论:Agent Memory 的数据模型可以怎么设计
java·开发语言·前端·c++·c#
SL_staff8 小时前
战略落地的最后一公里:从目标到个人日历的自动化链路实现
java·github·全栈
用户3721574261358 小时前
Java 设置 Excel 数据验证:整数、日期、文本长度、下拉列表和时间
java
白远山9 小时前
智慧场馆解决方案软件开发实战:从架构设计到落地部署指南
java·开发语言·架构·需求分析
SL_staff9 小时前
面向业务人员的IoT概念翻译实践:JVS-IOT低代码配置原理与开发者视角解析
java·spring boot·物联网
m0_587383009 小时前
折扣卡CPS软件开发实战:从系统架构设计到上线指南
java·小程序·架构·需求分析
Zane19949 小时前
用了Optional,为什么NPE还是防不住?
java·后端
大模型码小白9 小时前
告别造假数据,直接连数据库查真实时序数据喂给 TimechoAI 大模型
java·数据库·人工智能·microsoft·架构
企业数字化笔记9 小时前
固定资产Excel批量导入怎么防重复?资产编码、重复检查和错误回执
java·后端·excel