问题引出
这是在一个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类的类加载,这个问题觉得很有趣就去寻找资料了。
结论
通过查找资料终于解决了上述问题,即在应用启动过程中满足了以下两个条件:
- 在JDK8中,当Java程序启动并调用
main()
方法时,并不是所有的类都会立即加载到JVM中。JVM的类加载过程是按需进行的,即在需要使用某个类时才会进行加载。 具体来说,当Java程序启动时,JVM会先加载包含main()
方法的主类,也就是程序的入口类。然后,根据程序的执行流程和依赖关系,逐步加载其他被引用的类。这些类会在首次使用时进行加载,例如在代码中创建对象、调用静态方法或访问静态字段时。 - 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
收获
- 通过对该问题的研究让我对Java8有了进一步的了解,虽然一直在学JVM,但也只是一些皮毛,希望通过问题驱动能够逐渐补全知识体系;
- 对Spring容器的创建过程也有了更多的认识;
- 天工AI搜索很好用,好像支持上下文,我就通过追问的方式慢慢的理清了上述问题。AI对话就算了。
最后应该 不建议通过上述方式赋值吧?太容易出错了,且出错原因隐藏的很深,如果不抛出异常都无法察觉。 本人经验尚浅,可能会有一定的错误,希望能友好提出。