springboot提前注册bean(ApplicationContextInitializer)
一、问题:
我在搬迁springmvc(SSM)的项目搬迁到springboot的过程中发现,在一个类里面的常量使用了一个工具类的一个方法,类似下面的代码
MyService.java
@Service
public class MyService {
public static final String abc = PropertiesUtil.loadConfigProperties("abc");
}
PropertiesUtil.java
@Component
public class PropertiesUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
/**
* 获取配置文件(apollo或者nacos)
* 这个方式是被改造过的方法,之前是通过properties文件获取配置数据
* 现在是通过配置中心可以通过热更新获取配置数据
*/
public static String loadConfigProperties(String str){
str = Optional.ofNullable(str).orElse("");
return applicationContext
.getEnvironment()
.getProperty(str);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if (PropertiesUtil.applicationContext == null){
PropertiesUtil.applicationContext = applicationContext;
}
}
}
实际代码肯定没有这么简单,这里只是提取了关键代码
这个工具类之前是通过本地的配置文件进行获取数据(IO操作),里面的applicationContext、@Component都是后续迭代加的(SSM迭代,并不是现在改造为springboot)
我跑项目(Springboot)的时候报错,有一个错误,一个异常(其实就是一个问题:空指针导致类初始化失败)
// 类初始化错误
java.lang.ExceptionInInitializerError
// 控制住异常
java.lang.NullPointerException
打断点发现applicationContext为空,在setApplicationContext方法和loadConfigProperties方法分别打上断点,会发现applicationContext并没有优先注册进去
二、分析:
1、在类加载的过程中,会有三步,加载、链接、初始化
加载阶段:
只是通过加载器把类的二进制流放进方法区(元空间)
链接阶段:
验证:是验证二进制头文件,版本号等等
准备:是负责为类的类变量(被static修饰的变量)分配内存,并设置默认初始化值(初始化静态变量)
- static变量,分配空间在准备阶段完成(设置默认值),赋值在初始化阶段完成
- static变量是final的基本类型,以及字面量,值已确定,赋值在准备阶段完成(clinit中不会包含该常量)
- static变量是final的引用类型,那么赋值也会在初始化阶段完成
解析:将类的二进制数据流中的符号引用替换为直接引用
初始化阶段:
初始化阶段就是执行类构造器方法
<clinit>()的过程,就是说把准备阶段初始化的静态变量赋值,比如静态成员变量 static int a = 5 在链接阶段中的准备只是给某类的 a 静态成员变量开辟空间并且赋值 0 ( int 的初始值是 0 ),那么在初始化阶段就是把 a 赋值 5
2、applicationContext为什么为空
在一个该代码的类中(MyService.java)(这个在本文最开始的地方)的成员属性是常量(被 static final 所修饰),通过类加载子系统可知
该MyService.java中的 abc 是在链接的准备阶段开辟空间
但MyService.java的成员属性abc的值是PropertiesUtil类(这个在本文最开始的地方)的loadConfigProperties方法,并不是字面量,所以不在链接阶段赋值(字面量可以理解为 String a = "a" , int b = 5 , "a" 和 5 就是字面量)
所以 abc 需要再在初始化阶段进行加载赋值
新加了两个静态属性(a、b)可以看见类加载时赋值的情况MyService.class
而Spring IOC容器要实例化一个Bean,首先必须知道这个类是什么。因此,类的加载、链接、初始化(即完整的JVM类加载过程)是创建Bean的必要前提。在JVM成功加载类之后,Spring利用反射机制,执行了一系列额外的、丰富的步骤来完整地“创建”一个Bean,这包括了实例化、属性填充、初始化回调等。
所以该问题出现的原因是因为PropertiesUtil在MyService之后才被spring IOC注入,PropertiesUtil的静态成员变量applicationContext没有被注入,所以为null,导致MyService的静态常量abc初始化赋值异常,紧接着MyService类初始化失败
那么聪明的小伙伴就要问了,博主博主,那么加一个@Lazy或者@DependsOn不就好了(如下),这样确实可以解决问题,但不要忘了我是项目搬迁,要改的类太多了,就算一个个改也不是不行,但一不小心昏头了怎么办,最好就是一个类不改,加点配置就好了
@Lazy
@Service
@Lazy
public class MyService {
public static final String abc = PropertiesUtil.loadConfigProperties("abc");
}
@DependsOn
@Service
@DependsOn("propertiesUtil")
public class MyService {
public static final String abc = PropertiesUtil.loadConfigProperties("abc");
}
3、为什么springmvc(SSM)项目没有报错
1、最开始是本地IO操作,并没有applicationContext字段所以正常运行
2、之后的迭代版本:
我发现该类是通过spring配置文件进行注册的
其实我这时头也是蒙的,spring的底层我并不是很熟,这就不献丑了,免得误导大家
我猜测 spring 会先加载并注册 xml 中配置的类
<bean id="propertiesUtil" class="com.test.PropertiesUtil"/>
4、思路
如果我猜测没问题,那么在 SpringApplication.run(App.class,args); 之前就可以先加载 spring 的 bean ,那么之后的事情就是这么通过某度查了
@SpringBootApplication
public class App {
public static void main(String[] args){
SpringApplication.run(App.class,args);
}
}
三、解决问题
方法一:ApplicationContextInitializer(springboot 2或者3都可以用)
ApplicationContextInitializer是 Spring 框架提供的一个接口,用于在 Spring 应用上下文(ApplicationContext)刷新之前对其进行自定义初始化。它允许开发者在上下文加载 Bean 定义之前,对上下文进行一些额外的配置或修改。
1、核心作用
- 在上下文刷新之前执行自定义逻辑:例如
设置环境属性、注册自定义的 Bean 定义、修改上下文配置等 - 扩展 Spring 上下文的功能:通过初始化器,可以在 Spring 启动的早期阶段介入,实现一些框架无法直接支持的功能
2、适用场景
- 在 Spring Boot 启动时,动态修改
环境变量或配置文件 - 在上下文刷新之前,注册
自定义的 Bean或后置处理器 - 在微服务架构中,根据不同的环境(如开发、测试、生产)
初始化不同的配置
3、代码实现
PriorityRegistrationClass.java
public class PriorityRegistrationClass implements ApplicationContextInitializer<GenericApplicationContext> {
@Override
public void initialize(GenericApplicationContext applicationContext) {
// 你可以注册多个Bean
applicationContext.registerBean(PropertiesUtil.class);
}
}
TestApplication.java启动器
@SpringBootApplication
public class TestApplication {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(TestApplication.class);
app.addInitializers(new PriorityRegistrationClass());
app.run(args);
}
}
合并:
@SpringBootApplication
public class TestApplication {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(TestApplication.class);
app.addInitializers(new PriorityRegistrationClass());
app.run(args);
}
/**
* 优先注册
*/
static class PriorityRegistrationClass implements ApplicationContextInitializer<GenericApplicationContext> {
@Override
public void initialize(GenericApplicationContext applicationContext) {
// 你可以注册多个Bean
applicationContext.registerBean(PropertiesUtil.class);
}
}
}
方法二:spring.factories(springboot 3 以下版本)
1、spring.factories是什么?
spring.factories 本质上是一个标准的 Java 属性文件(key=value 格式),位于项目的 META-INF 目录下。
它的作用是:向 Spring Boot 运行时“宣告”有哪些自动配置类、监听器、初始化器或其他需要被自动加载的组件。Spring Boot 在启动时会扫描所有 jar 包中的 META-INF/spring.factories 文件,并根据 key 来加载对应的类。
2、为什么需要它?(解决了什么问题)
在没有 spring.factories 之前,如果你想用一个第三方库,通常需要在你的配置文件中用 @ComponentScan 或 @Import 手动引入它的配置类。这样做很麻烦,而且第三方库无法“无声无息”地为你提供配置。
spring.factories 机制使得:
- 自动配置:Spring Boot 应用一启动,就能自动发现并加载所有依赖 jar 包中的配置。
- 解耦:你的应用不需要任何代码上的改动,只需要引入一个
starter依赖,它的功能就自动可用了。这正符合 Spring Boot “约定优于配置” 的理念。
3、如何使用?
使用场景主要分为两种:1. 作为使用者(99%的情况) 和 2. 作为框架/Starter的开发者。
4、代码实现:还是需要一个类继承与ApplicationContextInitializer,需要加载的类多的话用英文逗号(,)隔开
PriorityRegistrationClass
public class PriorityRegistrationClass implements ApplicationContextInitializer<GenericApplicationContext> {
@Override
public void initialize(GenericApplicationContext applicationContext) {
// 你可以注册多个Bean
applicationContext.registerBean(PropertiesUtil.class);
}
}
spring.factories
org.springframework.context.ApplicationContextInitializer=com.zhao.springboot2.config.PriorityRegistrationClass
Springboot2Application启动器
@SpringBootApplication
public class Springboot2Application {
public static void main(String[] args) {
SpringApplication.run(Springboot2Application.class, args);
}
}
5、其他常用的 Key
| Key | 用途 |
|---|---|
org.springframework.boot.autoconfigure.EnableAutoConfiguration | 最常用,用于声明自动配置类 |
org.springframework.context.ApplicationContextInitializer | 应用上下文初始化器 |
org.springframework.context.ApplicationListener | 应用事件监听器 |
org.springframework.boot.SpringApplicationRunListener | 用于监听 Spring Boot 启动过程 |
org.springframework.boot.env.EnvironmentPostProcessor | 用于在应用上下文创建前处理 Environment |