Spring启动过程中Application事件的监听与处理.md

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u014453515/article/details/85264935

这篇博客是解决一个实际问题,在解决过程中梳理SpringApplicationEvent的运行机制和使用方法。这个问题是,微服务架构下,需要依次启动多个服务,服务之间存在运行时的依赖关系,必须保证多个服务的启动顺序。所以决定从Spring的Application事件入手。

1.Application Events and Listeners

我查了一些资料,通用的解决方案是:创建一个监听类,实现org.springframework.context.ApplicationListener,并实现它的onApplicationEvent方法。
SpringApplicationEvent 有6种事件:

  • ApplicationStartingEvent:除了基础的注册监听和初始化之外,在开始运行时做任何处理动作之前发送
  • ApplicationEnvironmentPreparedEvent 在上下文中使用的环境已知,但是Context尚未创建之前发送
  • ApplicationPreparedEvent 在Spring刷新Context开始之前,而仅当加载bean定义之后发送
  • ApplicationStartedEvent 在刷新上下文之后,但在调用任何应用程序(ApplicationRunner)和命令行运行程序(CommandLineRunner)之前发送
  • ApplicationReadyEvent 在调用应用程序和命令行运行程序后发送。 它表示应用程序已准备好为请求提供服务。
  • ApplicationFailedEvent 在启动过程中出现异常

前5种事件,是Spring按照启动前后顺序,依次生成的。我们想要监听启动之前的事件和启动完成的事件,只需关注ApplicationStartingEvent和ApplicationReadyEvent
这是我的代码实现:

@Component
public class ApplicationEventListener implements ApplicationListener<SpringApplicationEvent >{
    
    @Value("${spring.application.name}")
    private String appName;
    private Logger log = Logger.getLogger(this.getClass());
    @Override
    public void onApplicationEvent(SpringApplicationEvent event) {
     
        if(event instanceof ApplicationStartingEvent) {//启动之前
   
            
        }else if(event instanceof ApplicationReadyEvent ){//启动成功之后
       
        }
    }

然而调试之后,发现监听时间并没有生效,于是去看了一下官方文档Application Events and Listeners

Some events are actually triggered before the ApplicationContext is created, so you cannot register a listener on those as a @Bean. You can register them with the SpringApplication.addListeners(…) method or the SpringApplicationBuilder.listeners(…) method.
If you want those listeners to be registered automatically, regardless of the way the application is created, you can add a META-INF/spring.factories file to your project and reference your listener(s) by using the org.springframework.context.ApplicationListener key, as shown in the following example:
org.springframework.context.ApplicationListener=com.example.project.MyListener

官方提供了2种方案:

1.1.在启动器中添加监听器,然后启动。SpringApplication.addListeners(…)或者SpringApplicationBuilder.listeners(…)。

public static void main(String[] args) {
        new SpringApplicationBuilder(DiscoveryServiceApplication.class)
            .listeners(new ApplicationEventListener())
            .run(args);
}
    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(DiscoveryServiceApplication.class);
        app.addListeners(new ApplicationEventListener());
        app.run(args);
    }

1.2.从配置文件配置监听类

在META-INF/spring.factories中添加下面配置
org.springframework.context.ApplicationListener=com.xxx.listener.ApplicationEventListener

测试两种方式均有效,可以执行到监听方法。如此,我们的探索工作已经基本完成了。但是,作为研发人员不是应该有庖丁解牛的精神吗?
下面我们进行下知识的延伸。

2.实现CommandLineRunner和ApplicationRunner在Spring启动后执行

实现这两个接口同样能完成,在Spring容器启动后做一些操作的需求。他们的执行顺序是在
ApplicationStartedEvent 之后, ApplicationReadyEvent 之前执行。

@Component
public class MyApplicationRunner implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments args) throws Exception {
    }

@Component
public class MyCommandLineRunner implements CommandLineRunner {
 
    @Override
    public void run(String... args) throws Exception {
    }

这两个接口传递的参数不同,可有不同的用法。
如果有多个实现类,而你需要他们按一定顺序执行的话,可以在实现类上加上@Order注解。@Order(value=整数值)。SpringBoot会按照@Order中的value值从小到大依次执行。

3.SpringApplicationEvent与ApplicationRunner的使用区别

你可能要问了,既然标题1提供了6中SpringApplicationEvent的统一处理方式,那么为什么还需要ApplicationRunner 和CommandLineRunner 这两个接口呢?

我总结了两个原因:

1.生命周期不同,执行时可使用的资源不同

在SpringApplicationEvent的例子中,我使用 @Value(" s p r i n g . a p p l i c a t i o n . n a m e &quot; ) A p p l i c a t i o n S t a r t i n g E v e n t S p r i n g B e a n a p p N a m e n u l l B e a n A p p l i c a t i o n S t a r t e d E v e n t A p p l i c a t i o n C o n t e x t @ V a l u e ( &quot; {spring.application.name}&quot;)注解。当ApplicationStartingEvent来的时候,Spring并没有定义和初始化Bean,也没有扫描包。这时的appName为null,如果要依赖注入其他Bean时,当然也为空。 直到执行ApplicationStartedEvent事件时,ApplicationContext初始化完成,刷新之后,@Value(&quot; {spring.application.name}")才生效,这时才能使用上下文所提供的各种资源。所以如果ApplicationStartedEvent以前的几个事件,我们需要操作参数,只能从启动参数里面传递。(参考下面的代码)。

ApplicationRunner 和CommandLineRunner这两个接口的实现方法会在ApplicationContext刷新之后,这时各种配置资源、Bean都初始化完成,可以使用上下文所提供的各种资源,比如依赖某一个Bean等等。
因此不同事件处理时所能获取到的资源是不同的,依次来决定使用哪种方式。当然如果是“启动完成之后的需求”,两种方式是效果一样的。
注意:测试时ApplicationReadyEvent 和它之前的5个事件可能会执行两次,为什么会执行两次,参考Spring事件——onApplicationEvent执行两次

2.可传递的参数不同

CommandLineRunner 和SpringApplicationEvent一样,传递String数组,而ApplicationRunner传递的是ApplicationArguments。他们处理的都是启动参数,如----spring.profiles.active=discovery。其实ApplicationArguments和String数组传递的值没有什么本质区别,只不过String数组里面传递的是空格隔开的原始字符串,如“spring.profiles.active=discovery”,而ApplicationArguments将原始字符串拆成了键值对,如“spring.profiles.active”和“discovery”。因此这个区别可忽略不计。

4.解决现实问题的实现

如果我们需要启动多个Spring项目,其中一个项目可能要依赖其他项目的接口,必须等依赖的项目启动完成。我的思路是:
项目启动的时,写一个锁文件作为标志,启动完成之后,将改标志文件删除。我们必须等待所有的基础项目启动完成之后,再启动其他项目。循环检测如果正在启动的项目,有锁文件,则脚本进程先休眠几秒,如此循环。直到没有锁文件,表明启动成功,再启动下一个。

@Component
public class ApplicationEventListener implements ApplicationListener<SpringApplicationEvent >{
    private Logger log = Logger.getLogger(this.getClass());
    @Value("${spring.application.name}")
    private String appName;
    
    public String tempPath = "../tmp";
    @Override
    public void onApplicationEvent(SpringApplicationEvent event) {
        String[] args = event.getArgs();
        if(args.length ==0 ) {//MVC容器发出的事件不关注
            return ;
        }else if(appName == null) {
            String[] str = args[0].split("=");
            appName = str[1];
        }
        if(event instanceof ApplicationStartingEvent) {//正在启动
            File appFile = getAppTmpDir();
            if(appFile!=null) {
                File lock = new File(appFile,appName+".lock");
                if(!lock.exists()) {
                    try {
                        lock.createNewFile();
                        log.info("[+] ========= create lock path:" + lock.getAbsolutePath());
                    } catch (IOException e) {
                        log.error("[-] 启动时创建lock文件失败",e);
                    }
                }
            }
            
        }else if(event instanceof ApplicationReadyEvent){
            File appFile = getAppTmpDir();
            if(appFile!=null) {
                File lock = new File(appFile,appName+".lock");
                if(lock.exists()) {
                    log.info("[+] ========= create lock path:" + lock.getAbsolutePath());
                    lock.delete();
                }
            }
        }
        
    }
    
    private File getAppTmpDir() {
        try {
            File tempDir = new File(tempPath);
            if(!tempDir.exists()) {
                tempDir.mkdir();
            }
            log.info("[+] ========= temp path:" + tempDir.getAbsolutePath());
            File appFile = new File(tempDir,appName);
            if(!appFile.exists()) {
                appFile.mkdirs();
            }
            log.info("[+] ========= app path:" + appFile.getAbsolutePath());
            return appFile;
        }catch( Exception e) {
            log.error("[-] 启动时创建temp文件失败",e);
        }
        return null;
    }

下面是多个项目的启动脚本:
比如下面3个项目,我们需要依次启动

APPS=(app1 app2 app3)
for app in ${APPS[@]}
do
    echo Starting ${app}.........
    ${BINDIR}/start_service.sh ${app} >/dev/null 2>&1 &
    echo Start ${app} Successfully
    waiting=0
    while [ ! -e ${TMPDIR}/${app}/${app}.lock ] && [ ${waiting} -lt 300 ]
    do
        echo "Waiting ${app} ..."
        let "waiting += 3"
        sleep 3s
    done
done

打完收工。

猜你喜欢

转载自blog.csdn.net/u014453515/article/details/85264935