1. 综述:
在很多情况下,@PostConstruct注解被用来做为应用程序启动完成后需要执行的一些初始化方法的注解,而这个jdk的注解在某些场景下会出现一些问题,在这种情况下,就不适合使用此注解达成目的。
2. @PostConstruct注解
2.1 原理
@PostConstruct注解被用来修饰一个非静态的方法,被该注解修饰的方法会在加载Servlet的时候执行,并且只会被执行一次。
2.2 问题原因:
这个注解是用来作为Servlet生命周期控制的注解,用在这方面没问题,但是,如果再在里面做加载bean的动作就不行了。因为在执行被该注解方法的时间点上,它只能保证自己的bean被装载,但并不能保证所有被需要的bean被装载,所以在方法内加载bean会报错,导致初始化异常。
2.3 规避办法:
如果非要使用这么做,可以这样规避:
- 在方法中开启子线程,使用线程自旋等待的方式获取bean
在子线程中处理数据,不阻塞主线程。这种方式调度线程中业务顺序与等待时间设定比较麻烦
2.4 结论:
@PostConstruct虽然用上面的办法可以规避这个问题,但是该注解并不是设计用于干这个事的,所以,尽可能换其他方法吧。
3. ApplicationRunner与CommandLineRunner
3.1 原理
SpringApplication启动初始化上下文后,调用callRunners方法,查找所有实现了这两个接口的类,对所有类排序后再逐个调用。
3.1.1 SpringApplication启动后,会在最后调用callRunners方法,传入content与参数
public ConfigurableApplicationContext run(String... args) {
long startTime = System.nanoTime();
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup);
}
listeners.started(context, timeTakenToStartup);
//调用Runners扩展方法
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}
try {
Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
listeners.ready(context, timeTakenToReady);
}
catch (Throwable ex) {
handleRunFailure(context, ex, null);
throw new IllegalStateException(ex);
}
return context;
}
3.1.2 然后将所有实现了这两个接口的类全部丢到List里面,再排序
private void callRunners(ApplicationContext context, ApplicationArguments args) {
List<Object> runners = new ArrayList<>();
//扫描所有实现了接口的方法加入集合
runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
//对实现类进行排序
AnnotationAwareOrderComparator.sort(runners);
for (Object runner : new LinkedHashSet<>(runners)) {
//逐个调用
if (runner instanceof ApplicationRunner) {
callRunner((ApplicationRunner) runner, args);
}
if (runner instanceof CommandLineRunner) {
callRunner((CommandLineRunner) runner, args);
}
}
}
3.1.3 排序完成后,再分别调用方法,传入参数
private void callRunner(CommandLineRunner runner, ApplicationArguments args) {
try {
//代入参数调用run方法
(runner).run(args.getSourceArgs());
}
catch (Exception ex) {
throw new IllegalStateException("Failed to execute CommandLineRunner", ex);
}
}
3.2 用法:
ApplicationRunner 与CommandLineRunner工作方式相同,唯一的区别在于两种方法入参方式不同,实现这两个接口就可以让应用程序代码在启动完成后,接收流量前被调用。如果实现了多次,则必须实现org.springframework.core.Ordered或者使用org.springframework.core.annotation.Order注解来定义他们之间的顺序。
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class MyCommandLineRunner implements CommandLineRunner {
@Override
public void run(String... args) {
// Do something...
}
}
4. 资料来源:
《spring-boot-reference-2.6.4》
7.1.10. Using the ApplicationRunner or CommandLineRunner