写在前面

使用Springboot开发时,想要项目启动完成后立马初始化某些数据或者方法,需要怎么做呢?

或者说,我想在springboot启动的任意一个时间节点,想插入一段自己的方法,需要怎么做呢?

在这之前,我们应该了解一下Springboot启动的整个过程和Spring容器启动的整个过程:

springboot启动流程,手把手打断点一步步看运行步骤
spring系列-注解驱动原理及源码-spring容器创建流程

一、直接在启动类中写逻辑

直接在主启动类中写逻辑是最简单的方式,可以在springboot启动前、启动后写一些自己的业务逻辑,或者说设置某些参数。

一般主启动类

我们平常的主启动类一般都是这样写的:

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}

改良后的主启动类

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication springApplication = new SpringApplication(DemoApplication.class);

        // TODO springboot启动之前执行某些逻辑,或者使用springApplication初始化某些参数
        ConfigurableApplicationContext run = springApplication.run(args);

        // TODO springboot启动之后执行某些逻辑,可以使用ConfigurableApplicationContext获取运行时上下文以及运行环境

    }

}

其中,springApplication有许多方法:
在这里插入图片描述
而springboot启动后的返回值,也有很多方法:
在这里插入图片描述

总结

使用springboot的主启动类,启动前、启动后的参数,可以为所欲为~

不过,这种方式总归并不是很优雅,启动类做的事有点多了~或许有更好的方式来实现springboot启动前我想做的事、启动后我想做的事,

二、使用ApplicationRunner或CommandLineRunner

如果在SpringApplication启动后需要运行一些特定的代码,可以实现ApplicationRunner或CommandLineRunner接口。这两个接口的工作方式相同,都提供了一个run方法,这个方法在SpringApplication.run(…)完成之前被调用。

该方式非常适合于应该在应用程序启动之后但在它开始接受流量之前运行的任务。

使用CommandLineRunner

CommandLineRunner是个接口,有一个run()方法。为了使用CommandLineRunner我们需要创建一个类实现该接口并覆盖run()方法。使用@Component注解实现类。当SpringApplication.run()启动spring boot程序时,启动完成之前,CommandLineRunner.run()会被执行。CommandLineRunner的run()方法接收启动服务时传过来的参数。

CommandLineRunner接口的run()方法接收String数组作为参数。

import java.util.Arrays;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class CommandLineRunnerBean implements CommandLineRunner {
    private static final Logger logger = LoggerFactory.getLogger(CommandLineRunnerBean.class);  
    public void run(String... args) {
    	// TODO 我们自己的业务逻辑
        String strArgs = Arrays.stream(args).collect(Collectors.joining("|"));
        logger.info("Application started with arguments:" + strArgs);
    }
} 

使用ApplicationRunner

ApplicationRunner和CommandLineRunner的作用相同。在SpringApplication.run()完成spring boot启动之前,ApplicationRunner的run()方法会被执行。

ApplicationRunner接口的run()方法接收ApplicationArguments对象作为参数。

import java.util.Arrays;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Component
public class ApplicationRunnerBean implements ApplicationRunner {
    private static final Logger logger = LoggerFactory.getLogger(ApplicationRunnerBean.class);  
    @Override
    public void run(ApplicationArguments arg0) throws Exception {
            String strArgs = Arrays.stream(arg0.getSourceArgs()).collect(Collectors.joining("|"));
            logger.info("Application started with arguments:" + strArgs);
    }
} 

设置执行顺序

可以使用@Order注解或Ordered接口,来设置ApplicationRunner或CommandLineRunner的执行顺序。

@Component
public class CommandLineRunnerBean implements CommandLineRunner, Ordered {
    private static final Logger logger = LoggerFactory.getLogger(CommandLineRunnerBean.class);  
    public void run(String... args) {
        String strArgs = Arrays.stream(args).collect(Collectors.joining("|"));
        logger.info("Application started with arguments:" + strArgs);
    }

    /**
     * 数字越小优先级越高
     */
    @Override
    public int getOrder() {
        return 0;
    }
}
@Component
@Order(0) // 默认是最低优先级(数字为Integer.MAX_VALUE),数字越小优先级越高
public class CommandLineRunnerBean implements CommandLineRunner {
    private static final Logger logger = LoggerFactory.getLogger(CommandLineRunnerBean.class);  
    public void run(String... args) {
        String strArgs = Arrays.stream(args).collect(Collectors.joining("|"));
        logger.info("Application started with arguments:" + strArgs);
    }
} 

原理分析

1、在启动类SpringApplication的run方法中,我们一直找下去可以看到核心方法:

// org.springframework.boot.SpringApplication#run(java.lang.String...)
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);
		callRunners(context, applicationArguments); // 关键!调用所有的Runner
	}
	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;
}

2、callRunners方法

// org.springframework.boot.SpringApplication#callRunners
private void callRunners(ApplicationContext context, ApplicationArguments args) {
	List<Object> runners = new ArrayList<>();
	// 获取所有的ApplicationRunner
	runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
	// 获取所有的CommandLineRunner
	runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
	// 根据@Order 或者Ordered接口排序,底层是实现Comparator接口,并且使用Integer.compare方法排序
	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);
		}
	}
}

总结

ApplicationRunner或CommandLineRunner的用法是一样的,甚至可以混用,因为都是在SpringApplication.run(…)的最后一步执行的。

而且ApplicationRunner或CommandLineRunner执行的时候,springboot的各种环境都已经初始化完成了,不会影响正常的http请求等业务逻辑执行,所以即使ApplicationRunner或CommandLineRunner执行的时间比较长,也不会影响springboot实际启动的时间。

所以说,想在springboot启动后执行某些方法,使用ApplicationRunner或CommandLineRunner是非常方便好用的!

再配合ApplicationContextAware 和EnvironmentAware 获取上下文及运行环境,就是为所欲为了!
Springboot普通类获取运行时环境,获取运行时容器,获取Bean,等等获取运行时一切参数总结大全

三、使用@PostConstruct 注解

@PostConstruct 注解是java自带的一个注解,PostConstruct注解用于需要在依赖注入完成后执行的方法上,以执行任何初始化。

他要求必须在将类投入服务之前调用此方法。所有支持依赖注入的类都必须支持此注释。即使类不请求注入任何资源,也必须调用带有PostConstruct注释的方法。给定类中只有一个方法可以用此注释进行注释。

应用PostConstruct注释的方法必须满足以下所有条件: 该方法必须没有任何参数,除非是拦截器,在这种情况下,它接受interceptors规范定义的InvocationContext对象。

使用@PostConstruct

@Component
public class StartInit {

    @PostConstruct
    public void init() {
        System.out.println("@PostConstruct===============================");
    }

}

分析

加上该注解的方法会在项目启动的时候执行,可以理解为Spring容器在对类自动初始化全局的单一实例的过程中,执行完一个Bean的构造方法后会执行该Bean的@PostConstruct方法(如果有),然后初始化下一个Bean。可作为一些数据的常规化加载,比如数据字典之类的。

被@PostConstruct修饰的方法会在服务器加载Servle的时候运行,并且只会被服务器执行一次。PostConstruct在构造函数之后执行一般加载顺序

综上所述,@PostConstruct会阻塞系统启动,假如说方法执行时间过长,会影响springboot的启动。

其实,严格来说@PostConstruct在springboot中,算是bean生命周期,不算是springboot的生命周期中的一部分。

所以,@PostConstruct能不用还是别用了。。。

四、使用ApplicationContextInitializer

和下面那个差不多,这里先略过,后续有用到,继续更新。

五、(重点)使用SpringApplicationRunListener

SpringApplicationRunListener接口是SpringApplication运行方法的侦听器。

有些事件实际上是在创建ApplicationContext之前触发的,因此您不能将这些事件上的侦听器注册为@Bean。

使用SpringApplicationRunListener

1、代码

import org.springframework.boot.ConfigurableBootstrapContext;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;

import java.time.Duration;

/**
 * SpringBoot生命周期各个环节的监听器
 */

public class MySpringApplicationRunListener implements SpringApplicationRunListener {

    //这里的SpringApplication对象是事件源对象,所有的事件都是在这上面产生,这个构造函数必须写,否则会报错
    public MySpringApplicationRunListener(SpringApplication application, String[] args) {
    }

    @Override
    public void starting(ConfigurableBootstrapContext bootstrapContext) {
        //基本用不到,可能会用于检测硬件条件
        System.out.println("Startting……启动中");
        SpringApplicationRunListener.super.starting(bootstrapContext);
    }

    @Override
    public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
        //加载配置信息
        System.out.println("environmentPrepared……环境变量准备中");
        SpringApplicationRunListener.super.environmentPrepared(bootstrapContext, environment);
    }

    @Override
    public void contextPrepared(ConfigurableApplicationContext context) {
        System.out.println("contextPrepared……上下文对象准备");
        SpringApplicationRunListener.super.contextPrepared(context);
    }

    @Override
    public void contextLoaded(ConfigurableApplicationContext context) {
        System.out.println("contextLoaded……上下文对象开始加载");
        SpringApplicationRunListener.super.contextLoaded(context);
    }

    @Override
    public void started(ConfigurableApplicationContext context, Duration timeTaken) {
        //到这个地方启动就完成了,Ioc容器就初始化好了
        System.out.println("started……上下文对象加载完成");
        SpringApplicationRunListener.super.started(context, timeTaken);
    }

    @Override
    public void started(ConfigurableApplicationContext context) {
        SpringApplicationRunListener.super.started(context);
    }

    @Override
    public void ready(ConfigurableApplicationContext context, Duration timeTaken) {
        System.out.println("ready……准备启动项目");
        SpringApplicationRunListener.super.ready(context, timeTaken);
    }

    @Override
    public void running(ConfigurableApplicationContext context) {
        System.out.println("running……已经启动完成,开始运行");
        SpringApplicationRunListener.super.running(context);
    }

    @Override
    public void failed(ConfigurableApplicationContext context, Throwable exception) {
        System.out.println("failed……启动失败");
        SpringApplicationRunListener.super.failed(context, exception);
    }
}

2、在resources下创建META-INF/spring.factories文件,将官方的接口指向自定义的实现类
(在主启动类用代码set进去也行,但是不优雅)

org.springframework.boot.SpringApplicationRunListener=\
  com.cxf.demo.config.MySpringApplicationRunListener

3、启动项目,观察打印结果

SpringApplicationRunListener原理

还是看主启动类SpringApplication.run(…)

// org.springframework.boot.SpringApplication#run(java.lang.String...)
public ConfigurableApplicationContext run(String... args) {
   // 应用停止监听器
   StopWatch stopWatch = new StopWatch();
   stopWatch.start(); // 记录应用的启动时间
 
   // 创建引导上下文(Context环境)createBootstrapContext()
   DefaultBootstrapContext bootstrapContext = createBootstrapContext();
   ConfigurableApplicationContext context = null;
   // 设置headless属性方法(java.awt.headless),让当前应用进入headless模式(自力更生模式,详情自行百度)
   configureHeadlessProperty();
   //获取所有 RunListener(运行监听器)【为了方便所有Listener进行事件感知】
   SpringApplicationRunListeners listeners = getRunListeners(args);
   // 遍历 SpringApplicationRunListener 调用 starting 方法,相当于通知所有对系统正在启动过程感兴趣的人,项目正在 starting。
   listeners.starting(bootstrapContext, this.mainApplicationClass);
   try {
      // 保存命令行参数;ApplicationArguments
      ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
      // 准备运行时环境
      ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
      configureIgnoreBeanInfo(environment);
      // 打印banner
      Banner printedBanner = printBanner(environment);
      // 创建IOC容器
      // 根据项目类型(Servlet)创建容器,当前会创建 AnnotationConfigServletWebServerApplicationContext
      context = createApplicationContext();
      context.setApplicationStartup(this.applicationStartup);
      // 准备ApplicationContext IOC容器的基本信息
      prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
      // 刷新IOC容器,调用IOC容器的经典初始化过程,创建容器中的所有组件
      refreshContext(context);
      // 容器刷新完成后工作,方法是空的
      afterRefresh(context, applicationArguments);
      // 监控花费的时间
      stopWatch.stop();
      if (this.logStartupInfo) {
         new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
      }
      // 所有监听器 调用 listeners.started(context); 通知所有的监听器 started
      listeners.started(context);
      // 调用所有runners
      callRunners(context, applicationArguments);
   }
   // 如果有异常,调用Listener 的 failed方法
   catch (Throwable ex) {
      handleRunFailure(context, ex, listeners);
      throw new IllegalStateException(ex);
   }
 
   try {
      // 调用所有监听器的 running 方法 listeners.running(context); 通知所有的监听器 running
      listeners.running(context);
   }
   // running如果有问题。继续通知 failed 。调用所有 Listener 的 failed;通知所有的监听器 failed
   catch (Throwable ex) {
      handleRunFailure(context, ex, null);
      throw new IllegalStateException(ex);
   }
   return context;
}

在这里插入图片描述
我们可以看到,在主启动类的run方法中,获取了SpringApplicationRunListeners之后,每个切入点都会执行SpringApplicationRunListeners的固定的方法,所以可以完整地监听整个springboot的生命周期。

六、(重点)使用ApplicationListener

ApplicationListener是由应用程序事件监听器实现的接口。基于Observer设计模式的标准java.util.EventListener接口。 从Spring 3.0开始,ApplicationListener可以通用地声明它感兴趣的事件类型。当向Spring ApplicationContext注册时,事件将被相应地过滤,监听器仅被用于匹配事件对象。

有些事件实际上是在创建ApplicationContext之前触发的,因此您不能将这些事件上的侦听器注册为@Bean。

事件侦听器不应该运行可能很长的任务,因为默认情况下它们在同一个线程中执行,运行时间过长会影响springboot启动速度。

使用ApplicationListener

1、代码

import org.springframework.boot.SpringApplication;
import org.springframework.boot.availability.AvailabilityChangeEvent;
import org.springframework.boot.context.event.*;
import org.springframework.boot.web.context.WebServerInitializedEvent;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.core.env.ConfigurableEnvironment;

/**
 * 接收任意事件的回调,共有9大事件
 */
public class TestApplicationListener implements ApplicationListener<ApplicationEvent> {

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        if(event instanceof ApplicationStartingEvent){
            // 1.ApplicationStartingEvent在运行开始时发送,但在任何处理之前发送,侦听器和初始化器的注册除外。
            SpringApplication springApplication = ((ApplicationStartingEvent) event).getSpringApplication();
            System.out.println("ApplicationListener================ApplicationStartingEvent");
        }

        if(event instanceof ApplicationEnvironmentPreparedEvent){
            // 2.当要在上下文中使用的Environment 已知但在创建上下文之前,将发送ApplicationEnvironmentPreparedEvent。
            SpringApplication springApplication = ((ApplicationEnvironmentPreparedEvent ) event).getSpringApplication();
            ConfigurableEnvironment environment = ((ApplicationEnvironmentPreparedEvent) event).getEnvironment();
            System.out.println("ApplicationListener================ApplicationEnvironmentPreparedEvent");
        }

        if(event instanceof ApplicationContextInitializedEvent){
            // 3.ApplicationContextInitializedEvent 在prepared  ApplicationContext 并且调用了ApplicationContextInitializers之后,但在加载任何bean定义之前发送。
            SpringApplication springApplication = ((ApplicationContextInitializedEvent ) event).getSpringApplication();
            ConfigurableApplicationContext applicationContext = ((ApplicationContextInitializedEvent) event).getApplicationContext();
            System.out.println("ApplicationListener================ApplicationContextInitializedEvent");
        }

        if(event instanceof ApplicationPreparedEvent){
            // 4.ApplicationPreparedEvent在refresh 开始之前发送,但在加载bean定义之后发送。
            SpringApplication springApplication = ((ApplicationPreparedEvent ) event).getSpringApplication();
            ConfigurableApplicationContext applicationContext = ((ApplicationPreparedEvent) event).getApplicationContext();
            System.out.println("ApplicationListener================ApplicationPreparedEvent");
        }

        if(event instanceof ApplicationStartedEvent){
            // 5.在refreshed上下文之后,调用任何应用程序和命令行运行程序之前,发送ApplicationStartedEvent。
            SpringApplication springApplication = ((ApplicationStartedEvent ) event).getSpringApplication();
            ConfigurableApplicationContext applicationContext = ((ApplicationStartedEvent) event).getApplicationContext();
            System.out.println("ApplicationListener================ApplicationStartedEvent");
        }

        if(event instanceof AvailabilityChangeEvent){
            // 6.AvailabilityChangeEvent在LivenessState.CORRECT之后立即发送。更正以表明应用程序被视为活动的。
            Object payload = ((AvailabilityChangeEvent) event).getPayload();
            System.out.println("ApplicationListener================AvailabilityChangeEvent");
        }

        if(event instanceof ApplicationReadyEvent){
            // 7.在调用任何应用程序和命令行运行程序后,将发送ApplicationReadyEvent。
            SpringApplication springApplication = ((ApplicationReadyEvent ) event).getSpringApplication();
            ConfigurableApplicationContext applicationContext = ((ApplicationReadyEvent) event).getApplicationContext();
            System.out.println("ApplicationListener================ApplicationReadyEvent");
        }

        if(event instanceof AvailabilityChangeEvent){
            // 8.AvailabilityChangeEvent在ReadinessState.ACCEPTING_TRAFFIC之后立即发送。表示应用程序已准备好为请求提供服务。
            Object payload = ((AvailabilityChangeEvent) event).getPayload();
            System.out.println("ApplicationListener================AvailabilityChangeEvent2");
        }

        if(event instanceof ApplicationFailedEvent){
            // 9.如果启动时出现异常,将发送ApplicationFailedEvent。
            SpringApplication springApplication = ((ApplicationFailedEvent ) event).getSpringApplication();
            ConfigurableApplicationContext applicationContext = ((ApplicationFailedEvent) event).getApplicationContext();
            System.out.println("ApplicationListener================ApplicationFailedEvent");
        }

        /**
         * 上面的列表只包括绑定到SpringApplication的SpringApplicationEvents。除此之外,还会在ApplicationPreparedEvent之后和ApplicationStartedEvent之前发布以下事件:
         */
        if(event instanceof WebServerInitializedEvent){
            // web服务器准备就绪后,将发送WebServerInitializedEvent。
            // ServletWebServerInitializedEvent 和ReactiveWebServerInitializedEvent分别是servlet和reactive变量。
            System.out.println("ApplicationListener================WebServerInitializedEvent");
        }

        if(event instanceof ContextRefreshedEvent){
            // 刷新ApplicationContext时,将发送ContextRefreshedEvent。
            System.out.println("ApplicationListener================ContextRefreshedEvent");
        }

    }
}

2、在resources下创建META-INF/spring.factories文件,将官方的接口指向自定义的实现类
(在主启动类用代码set进去也行,但是不优雅)

org.springframework.context.ApplicationListener=\
  com.cxf.demo.config.TestApplicationListener

3、启动项目,观察打印结果

4、注意!
ApplicationListener的event可以单独监听,比如说,我可以单独监听ApplicationStartingEvent:

public class TestApplicationListener implements ApplicationListener<ApplicationStartingEvent> {

    @Override
    public void onApplicationEvent(ApplicationStartingEvent event) {
        // 1.ApplicationStartingEvent在运行开始时发送,但在任何处理之前发送,侦听器和初始化器的注册除外。
        SpringApplication springApplication = event.getSpringApplication();
        System.out.println("ApplicationListener================ApplicationStartingEvent");

    }
}

ApplicationListener原理

同上面【SpringApplicationRunListener原理】

七、(重中之重)使用@EventListener

ApplicationListener有一个缺陷,那就是每次只能监听一个事件,而@EventListener每次可以监听多个事件。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐