springboot提前注册bean(ApplicationContextInitializer)

33 阅读7分钟

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,这包括了实例化、属性填充、初始化回调等。

所以该问题出现的原因是因为PropertiesUtilMyService之后才被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 机制使得:

  1. 自动配置:Spring Boot 应用一启动,就能自动发现并加载所有依赖 jar 包中的配置。
  2. 解耦:你的应用不需要任何代码上的改动,只需要引入一个 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