SpringBoot初始化加载配置的八种方式总结
作者:独行客-编码爱好者
- @PostConstruct 注解
- InitializingBean 接口
- @Bean initMethod方法
- 构造器注入
- ApplicationListener
- CommandLineRunner
- ApplicationRunner
- SmartLifecycle
序号 | 初始化加载方式 | 执行时机 |
1 | @PostConstruct 注解(在方法加注解) | Bean对象初始化完成后执行( 该方法会在所有依赖字段注入后才执行 ) |
2 | 构造器注入(构造方法加 @Autowired注解 ) | Bean对象初始化完成后执行( 该方法会在所有依赖字段注入后才执行 ) |
3 | InitializingBean 接口(继承 InitializingBean接口,并实现 afterPropertiesSet()这个方法) | Bean对象初始化完成后执行( 该方法会在所有依赖字段注入后才执行 ) 3和4是一样的,实现方式不同 |
4 | @Bean initMethod方法 | Bean对象初始化完成后执行( 该方法会在所有依赖字段注入后才执行 ) 3和4是一样的,实现方式不同 |
5 | SmartLifecycle 接口(继承SmartLifecycle 接口),并实现start() 方法 | SmartLifecycle 的执行时机是在 Spring 应用上下文刷新完成之后,即所有的 Bean 都已经被实例化和初始化之后。 |
6 | ApplicationListener(继承 ApplicationListener接口,并实现onApplicationEvent()方法 与方法上加 @EventListener的效果一样 ) | 所有的Bean都初始化完成后才会执行方法 |
7 | CommandLineRunner 继承 CommandLineRunner,继承 run() 方法 ) | 应用启动后执行 |
8 | ApplicationRunner( 继承 CommandLineRunner,继承 run() 方法 ) | 应用启动后执行 |
背景
在日常开发时,我们常常需要 在SpringBoot 应用启动时执行某一段逻辑,如下面的场景:
获取一些当前环境的配置或变量
向缓存数据库写入一些初始数据
连接某些第三方系统,确认对方可以工作
在实现这些功能时,我们可能会遇到一些"坑"。 为了利用SpringBoot框架的便利性,我们不得不将整个应用的执行控制权交给容器,于是造成了大家对于细节是一无所知的。那么在实现初始化逻辑代码时就需要小心了,比如,我们并不能简单的将初始化逻辑在Bean类的构造方法中实现,类似下面的代码:
@Component public class InvalidInitExampleBean { @Autowired private Environment env; public InvalidInitExampleBean() { env.getActiveProfiles(); } }
注意:这里,我们在InvalidInitExampleBean的构造方法中试图访问一个自动注入的env字段,当真正执行时,你一定会得到一个空指针异常(NullPointerException)。原因在于,当构造方法被调用时,Spring上下文中的Environment这个Bean很可能还没有被实例化,同时也仍未注入到当前对象,所以并不能这样进行调用。
下面,我们来看看在SpringBoot中实现"安全初始化"的一些方法:
一、@PostConstruct 注解
@PostConstruct 注解其实是来自于 javax的扩展包中(大多数人的印象中是来自于Spring框架),它的作用在于声明一个Bean对象初始化完成后执行的方法。
来看看它的原始定义:
The PostConstruct annotation is used on a method that needs to be executed after dependency injection is done to perform any initialization.
也就是说,该方法会在所有依赖字段注入后才执行,当然这一动作也是由Spring框架执行的。
示例:
package com.cfcc.teis.load; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.env.Environment; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import java.util.Arrays; /** * @Description TODO * @date 2024/12/5 11:19 * @Version 1.0 * @Author gezongyang */ @Component @Slf4j public class InvalidInitExampleBean { @Autowired private Environment environment; /** * 该方法会在所有依赖字段(environment)注入后才执行,当然这一动作也是由Spring框架执行的。 */ @PostConstruct public void init() { //environment 已经注入 log.info("@PostConstruct execute:{}",Arrays.asList(environment.getDefaultProfiles())); } }
二、实现 InitializingBean 接口
InitializingBean 是由Spring框架提供的接口,其与@PostConstruct注解的工作原理非常类似。
如果不使用注解的话,你需要让Bean实例继承 InitializingBean接口,并实现afterPropertiesSet()这个方法。
示例:
package com.cfcc.teis.load; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.env.Environment; import org.springframework.stereotype.Component; import java.util.Arrays; /** * @Description 继承 InitializingBean接口,并实现afterPropertiesSet() * afterPropertiesSet() 会在所有依赖的字段(environment)注入后才执行 * @date 2024/12/5 11:37 * @Version 1.0 * @Author gezongyang */ @Component @Slf4j public class InitializingBeanExampleBean implements InitializingBean { @Autowired private Environment environment; /** * 这个方法会在environment注入后执行 */ @Override public void afterPropertiesSet() { //environment 已经注入 log.info("InitializingBean execute:{}", Arrays.asList(environment.getDefaultProfiles())); } }
三、@Bean initMethod方法
我们在声明一个Bean的时候,可以同时指定一个initMethod属性,该属性会指向Bean的一个方法,表示在初始化后执行。
示例:
package com.cfcc.teis.load; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.core.env.Environment; import org.springframework.stereotype.Component; import java.util.Arrays; /** * @Description 指定一个initMethod属性,该属性会指向Bean的一个方法,表示在初始化后执行。 * @date 2024/12/5 11:37 * @Version 1.0 * @Author gezongyang */ @Component @Slf4j public class InitializingBean { @Autowired private Environment environment; /** * 这个方法会在environment注入后执行 */ public void init() { //environment 已经注入 log.info("@Bean initMethod execute:{}", Arrays.asList(environment.getDefaultProfiles())); } /** * 这里将initMethod指向init方法,相应的我们也需要在Bean中实现这个方法: * @return */ @Bean(initMethod="init") public InitializingBean exBean() { return new InitializingBean(); } }
四、构造器注入
如果依赖的字段在Bean的构造方法中声明,那么Spring框架会先实例这些字段对应的Bean,再调用当前的构造方法。
package com.cfcc.teis.load; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.env.Environment; import org.springframework.stereotype.Component; import java.util.Arrays; /** * @Description 如果依赖的字段在Bean的构造方法中声明,那么Spring框架会先实例这些字段对应的Bean, * 再调用当前的构造方法。 * @date 2024/12/5 14:11 * @Version 1.0 * @Author gezongyang */ @Slf4j @Component public class LogicInConstructorExampleBean { private final Environment environment; /** * LogicInConstructorExampleBean(Environment environment) 调用在 * Environment 对象实例化之后 * @param environment */ @Autowired public LogicInConstructorExampleBean(Environment environment) { //environment实例已初始化 this.environment = environment; log.info("LogicInConstructor:{}", Arrays.asList(environment.getDefaultProfiles())); } }
五、实现ApplicationListener 接口
ApplicationListener 是由 spring-context组件提供的一个接口,主要是用来监听 “容器上下文的生命周期事件”。它的定义如下:
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener { /** * Handle an application event. * @param event the event to respond to */ void onApplicationEvent(E event); }
这里的event可以是任何一个继承于ApplicationEvent的事件对象。 对于初始化工作来说,我们可以通过监听ContextRefreshedEvent这个事件来捕捉上下文初始化的时机。
示例:
package com.cfcc.teis.load; import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.stereotype.Component; /** * @Description 当所有的Bean都初始化完成后,执行onApplicationEvent 方法 * @date 2024/12/5 14:19 * @Version 1.0 * @Author gezongyang */ @Slf4j @Component public class StartupApplicationListenerExample implements ApplicationListener<ContextRefreshedEvent> { public static int counter; /** * 这里的event可以是任何一个继承于ApplicationEvent的事件对象。 * 对于初始化工作来说,我们可以通过监听ContextRefreshedEvent这个事件来捕捉上下文初始化的时机。 * @param event */ @Override public void onApplicationEvent(ContextRefreshedEvent event) { log.info("ApplicationListener run!"); counter++; } }
在Spring上下文初始化完成后,这里定义的方法将会被执行。 与前面的 InitializingBean 不同的是,通过 ApplicationListener 监听的方式是全局性的,也就是当 所有 的 Bean 都 初始化完成 后才会执行方法。
Spring 4.2 之后引入了新的 @EventListener注解,可以实现同样的效果:
package com.cfcc.teis.load; import lombok.extern.slf4j.Slf4j; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; /** * @Description TODO * @date 2024/12/5 14:24 * @Version 1.0 * @Author gezongyang */ @Slf4j @Component public class EventListenerExampleBean { public static int counter; /** * 所有的Bean都初始化完成后,执行onApplicationEvent方法。 * @param event */ @EventListener public void onApplicationEvent(ContextRefreshedEvent event) { log.info("@EventListener execute", event.toString()); counter++; } }
六、实现 CommandLineRunner 接口
SpringBoot 提供了一个CommanLineRunner接口,用来实现在应用启动后的逻辑控制,其定义如下:
public interface CommandLineRunner { /** * Callback used to run the bean. * @param args incoming main method arguments * @throws Exception on error */ void run(String... args) throws Exception; }
此外,对于多个CommandLineRunner的情况下可以使用@Order注解来控制它们的顺序。
案例:
1 定义一个CommandDemo实现CommandLineRunner,并纳入到spring容器
package com.cfcc.teis.load; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; /** * @Description 应用启动后的回调函数,在应用启动后执行此方法 * @date 2024/12/5 14:32 * @Version 1.0 * @Author gezongyang */ @Component @Slf4j public class CommandDemo implements CommandLineRunner { /** * 此方法在应用启动后执行 * @param args */ @Override public void run(String... args) { log.info("CommandLineRunner run!"); } }
2 配置参数,然后执行启动类
3 打印结果:
=====应用已经启动成功======[aaa,bbb]
七、实现 ApplicationRunner 接口
package org.springframework.boot; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; /** * Interface used to indicate that a bean should <em>run</em> when it is contained within * a {@link SpringApplication}. Multiple {@link ApplicationRunner} beans can be defined * within the same application context and can be ordered using the {@link Ordered} * interface or {@link Order @Order} annotation. * * @author Phillip Webb * @since 1.3.0 * @see CommandLineRunner */ @FunctionalInterface public interface ApplicationRunner { /** * Callback used to run the bean. * @param args incoming application arguments * @throws Exception on error */ void run(ApplicationArguments args) throws Exception; }
与 CommandLineRunner接口类似, Spring boot 还提供另一个ApplicationRunner 接口来实现初始化逻辑。不同的地方在于 ApplicationRunner.run()方法接受的是封装好的ApplicationArguments参数对象,而不是简单的字符串参数。ApplicationArguments是对参数做了进一步的处理,可以解析key=value形式,我们可以通过name来获取value(而CommandLineRunner只是获取key=value整体)
package org.springframework.boot; import java.util.List; import java.util.Set; /** * Provides access to the arguments that were used to run a {@link SpringApplication}. * * @author Phillip Webb * @since 1.3.0 */ public interface ApplicationArguments { /** * Return the raw unprocessed arguments that were passed to the application. * @return the arguments */ String[] getSourceArgs(); /** * Return the names of all option arguments. For example, if the arguments were * "--foo=bar --debug" would return the values {@code ["foo", "debug"]}. * @return the option names or an empty set */ Set<String> getOptionNames(); /** * Return whether the set of option arguments parsed from the arguments contains an * option with the given name. * @param name the name to check * @return {@code true} if the arguments contain an option with the given name */ boolean containsOption(String name); /** * Return the collection of values associated with the arguments option having the * given name. * <ul> * <li>if the option is present and has no argument (e.g.: "--foo"), return an empty * collection ({@code []})</li> * <li>if the option is present and has a single value (e.g. "--foo=bar"), return a * collection having one element ({@code ["bar"]})</li> * <li>if the option is present and has multiple values (e.g. "--foo=bar --foo=baz"), * return a collection having elements for each value ({@code ["bar", "baz"]})</li> * <li>if the option is not present, return {@code null}</li> * </ul> * @param name the name of the option * @return a list of option values for the given name */ List<String> getOptionValues(String name); /** * Return the collection of non-option arguments parsed. * @return the non-option arguments or an empty list */ List<String> getNonOptionArgs(); }
ApplicationArguments可以接收key=value这样的参数,getOptionNames()方法可以得到key的集合,getOptionValues(String name)方法可以得到value这样的集合
示例:
import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.stereotype.Component; /** * @Description TODO * @date 2022/6/23 9:55 * @Version 1.0 * @Author gezongyang */ @Component public class ApplicationRunnerDemo implements ApplicationRunner { @Override public void run(ApplicationArguments args) throws Exception { System.out.println("====getOptionNames======"+args.getOptionNames()); System.out.println("====getOptionValues====="+args.getOptionValues("key")); } }
配置参数启动
打印结果:
====getOptionNames======[key] ====getOptionValues=====[value]
八、实现SmartLifecycle 接口
package org.springframework.context; public interface SmartLifecycle extends Lifecycle, Phased { boolean isAutoStartup(); void stop(Runnable var1); }
SmartLifecycle 是 Spring Framework 中的一个接口,它扩展了 Lifecycle 接口,并提供了更灵活的生命周期管理功能。使用 SmartLifecycle 可以更精细地控制应用组件的启动和关闭过程。以下是 SmartLifecycle 的一些主要特性:
特性:
isAutoStartup(): 返回一个布尔值,指示该组件是否应该在容器启动时自动启动。
getPhase(): 返回一个整数值,表示组件的启动顺序(阶段)。较低的数字表示较早的启动顺序。
start(): 启动组件,当返回时,组件应该已经启动完毕。
stop(Runnable callback): 停止组件,并且可以在停止完成后执行提供的回调函数。
isRunning(): 返回一个布尔值,指示组件当前是否处于运行状态。
案例:
package com.cfcc.teis.load; import lombok.extern.slf4j.Slf4j; import org.springframework.context.SmartLifecycle; import org.springframework.stereotype.Component; /** * @Description SmartLifecycle 可以更精细地控制应用组件的启动和关闭过程 * @date 2024/12/5 15:10 * @Version 1.0 * @Author gezongyang */ @Slf4j @Component public class SmartLifecycleDemo implements SmartLifecycle { private volatile boolean running = false; /** * SmartLifecycle run */ @Override public void start() { log.info("SmartLifecycle run!"); this.running = true; } @Override public void stop() { } /** * 在这里放置停止逻辑 * @param callback */ @Override public void stop(Runnable callback) { this.running = false; callback.run(); // 确保回调被执行 } @Override public boolean isRunning() { return this.running; } /** * 组件将在Spring上下文加载后自动启动 * @return */ @Override public boolean isAutoStartup() { return true; } /** * 定义启动顺序,数字越小越先启动 * @return */ @Override public int getPhase() { return 0; } }
注意事项
当你实现 SmartLifecycle 时,确保 start() 方法不会阻塞,因为它会延迟整个应用程序的启动。如果需要长时间运行的任务,考虑将其放入单独的线程中。
stop(Runnable callback) 方法中的回调是重要的,必须调用它来通知框架组件已经停止。如果你不打算使用某些方法,可以不重写它们;默认实现将提供合理的行为。
通过使用 SmartLifecycle,你可以更好地控制应用程序中不同组件的生命周期行为,这对于那些对启动和关闭有特殊需求的应用特别有用。
SmartLifecycle 的执行时机:
是在 Spring 应用上下文刷新完成之后,即所有的 Bean 都已经被实例化和初始化之后。具体来说,Spring 容器在调用 finishRefresh() 方法时会触发所有实现了 SmartLifecycle 接口的 Bean 的启动过程。这是通过容器中的 LifecycleProcessor 来管理的,默认使用的是 DefaultLifecycleProcessor。
SmartLifecycle 执行的具体流程:
上下文刷新完成后:当 Spring 应用上下文完成刷新(finishRefresh),意味着所有的 Bean 已经被加载并初始化完毕。
初始化生命周期处理器:Spring 会初始化 LifecycleProcessor,如果没有显式定义,则创建一个默认的 DefaultLifecycleProcessor。
调用 onRefresh():接着,Spring 会调用 LifecycleProcessor 的 onRefresh() 方法,这将导致调用 startBeans() 方法。
根据 phase 分组并排序:startBeans() 方法会获取所有实现了 Lifecycle 接口的 Bean,并根据它们的 phase 属性进行分组和排序。phase 值越小的 Bean 将越早启动。
检查 isAutoStartup():对于每个 SmartLifecycle Bean,如果它的 isAutoStartup() 方法返回 true,那么它的 start() 方法就会被自动调用。
执行 start() 方法:按照 phase 排序后的顺序,依次调用每个 SmartLifecycle Bean 的 start() 方法来启动组件。
设置运行状态:start() 方法应该确保组件处于运行状态,并且 isRunning() 方法应返回 true。
同样的逻辑也适用于停止组件的过程,但是是以相反的顺序执行的,即 phase 值较大的 Bean 会先停止。
因此,如果你希望在 Spring 容器完全准备好之后执行某些任务,比如开启消息监听、启动定时任务等,你可以实现 SmartLifecycle 接口并在 start() 方法中放置这些逻辑。同时,通过调整 getPhase() 返回的值,可以控制多个 SmartLifecycle Bean 之间的启动顺序。
以上就是SpringBoot初始化加载配置的八种方式总结的详细内容,更多关于SpringBoot初始化加载配置的资料请关注脚本之家其它相关文章!