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

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

相关推荐
hqxstudying27 分钟前
JAVA项目中邮件发送功能
java·开发语言·python·邮件
最初的↘那颗心31 分钟前
Java HashMap深度解析:原理、实现与最佳实践
java·开发语言·面试·hashmap·八股文
小兔兔吃萝卜37 分钟前
Spring 创建 Bean 的 8 种主要方式
java·后端·spring
亲爱的马哥1 小时前
重磅更新 | 填鸭表单TDuckX2.9发布!
java
Java中文社群1 小时前
26届双非上岸记!快手之战~
java·后端·面试
whitepure1 小时前
万字详解Java中的面向对象(二)——设计模式
java·设计模式
whitepure1 小时前
万字详解Java中的面向对象(一)——设计原则
java·后端
2301_793086872 小时前
SpringCloud 02 服务治理 Nacos
java·spring boot·spring cloud
回家路上绕了弯2 小时前
MySQL 详细使用指南:从入门到精通
java·mysql
小七rrrrr2 小时前
动态规划法 - 53. 最大子数组和
java·算法·动态规划