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

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

相关推荐
腥臭腐朽的日子熠熠生辉29 分钟前
解决maven失效问题(现象:maven中只有jdk的工具包,没有springboot的包)
java·spring boot·maven
ejinxian31 分钟前
Spring AI Alibaba 快速开发生成式 Java AI 应用
java·人工智能·spring
杉之37 分钟前
SpringBlade 数据库字段的自动填充
java·笔记·学习·spring·tomcat
圈圈编码1 小时前
Spring Task 定时任务
java·前端·spring
俏布斯1 小时前
算法日常记录
java·算法·leetcode
27669582921 小时前
美团民宿 mtgsig 小程序 mtgsig1.2 分析
java·python·小程序·美团·mtgsig·mtgsig1.2·美团民宿
爱的叹息1 小时前
Java 连接 Redis 的驱动(Jedis、Lettuce、Redisson、Spring Data Redis)分类及对比
java·redis·spring
程序猿chen1 小时前
《JVM考古现场(十五):熵火燎原——从量子递归到热寂晶壁的代码涅槃》
java·jvm·git·后端·java-ee·区块链·量子计算
松韬2 小时前
Spring + Redisson:从 0 到 1 搭建高可用分布式缓存系统
java·redis·分布式·spring·缓存
绝顶少年2 小时前
Spring Boot 注解:深度解析与应用场景
java·spring boot·后端