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

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

相关推荐
TPBoreas几秒前
手搓一个不用中间件的分表策略
java
、十一、1 分钟前
Tomcat的工作模式是什么?
java·tomcat
killsime3 分钟前
JavaWeb开发 : tomcat+Servlet+JSP
java·servlet·tomcat·javaweb
刽子手发艺23 分钟前
云服务器部署springboot项目、云服务器配置JDK、Tomcat
java·后端·部署
北漂编程小王子26 分钟前
maven <scope>import</scope>配置作用
java·maven·maven import
BIGSHU092326 分钟前
java接口对接标准
java
m0_5474866636 分钟前
数据结构试题库1
java·数据结构·算法
多多*37 分钟前
后端并发编程操作简述 Java高并发程序设计 六类并发容器 七种线程池 四种阻塞队列
java·开发语言·前端·数据结构·算法·状态模式
_im.m.z39 分钟前
Mac配置和启动 Tomcat
java·macos·tomcat·ssm框架
白初&1 小时前
文件上传代码分析
java·c++·python·php·代码审计