一个有趣的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对话就算了。

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

相关推荐
XiaoLeisj2 小时前
【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题
java·开发语言·java-ee
paopaokaka_luck2 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
dayouziei2 小时前
java的类加载机制的学习
java·学习
Yaml44 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~4 小时前
Spring Boot详解:从入门到精通
java·spring boot·后端
hong1616884 小时前
Spring Boot中实现多数据源连接和切换的方案
java·spring boot·后端
aloha_7894 小时前
从零记录搭建一个干净的mybatis环境
java·笔记·spring·spring cloud·maven·mybatis·springboot
记录成长java5 小时前
ServletContext,Cookie,HttpSession的使用
java·开发语言·servlet
睡觉谁叫~~~5 小时前
一文解秘Rust如何与Java互操作
java·开发语言·后端·rust
程序媛小果5 小时前
基于java+SpringBoot+Vue的旅游管理系统设计与实现
java·vue.js·spring boot