1前言

在springboot启动的过程中会产生一系列事件,我们开发的时候可以自定义一些事件监听处理器.根据自己的需要在针对每个事件做一些业务处理.

2Application Events

springboot 启动的时候会按顺序产生如下几种事件:

  • 1ApplicationStartingEvent :springboot应用启动且未作任何处理(除listener注册和初始化)的时候发送ApplicationStartingEvent
  • 2ApplicationEnvironmentPreparedEvent:确定springboot应用使用的Environment且context创建之前发送这个事件
  • 3ApplicationPreparedEvent:context已经创建且没有refresh发送个事件
  • 4ApplicationStartedEvent:context已经refresh且application and command-line runners(如果有) 调用之前发送这个事件
  • 5 ApplicationReadyEvent://application and command-line runners (如果有)执行完后发送这个事件,此时应用已经启动完毕.这个事件比较常用,常常在系统启动完后做一些初始化操作
  • 6 ApplicationFailedEvent:应用启动失败后产生这个事件

3实现

3.1模拟启动,进行初始化操作

首先模拟系统启动后需要进行业务操作,这里只是范例,所以只打印一句话:

package com.simos.service;

import org.springframework.stereotype.Service;

/**
 * Created by l2h on 18-4-16.
 * Desc:系统初始化后执行一些业务操作
 * @author l2h
 */
@Service
public class SystemInitService {
public void systemInit(){
    System.out.println("应用初始化后,进行一些业务操作,如启动某些工作线程,初始化系统某些参数");
}
}

3.2事件监听处理实现

通过实现ApplicationListener接口,完成事件的监听处理:

package com.simos.listener;

import com.simos.service.SystemInitService;
import org.springframework.boot.context.event.*;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;

/**
 * Created by l2h on 18-4-16.
 * Desc:事件监听处理
 * @author l2h
 */
public class SimosApplicationListener implements ApplicationListener<ApplicationEvent>{
@Override
public void onApplicationEvent(ApplicationEvent event) {
    //springboot应用启动且未作任何处理(除listener注册和初始化)的时候发送ApplicationStartingEvent
    if (event instanceof ApplicationStartingEvent){
        System.out.println("ApplicationStarting");
        return;
    }
    //确定springboot应用使用的Environment且context创建之前发送这个事件
    if (event instanceof ApplicationEnvironmentPreparedEvent){
        System.out.println("ApplicationEnvironmentPrepared");
        return;
    }
    //context已经创建且没有refresh发送个事件
    if (event instanceof ApplicationPreparedEvent){
        System.out.println("ApplicationPrepared");
        return;
    }
    //context已经refresh且application and command-line runners(如果有) 调用之前发送这个事件
    if (event instanceof ApplicationStartedEvent){
        System.out.println("ApplicationStarted");
        return;
    }
    //application and command-line runners (如果有)执行完后发送这个事件,此时应用已经启动完毕
    if (event instanceof ApplicationReadyEvent){
        ApplicationContext context = ((ApplicationReadyEvent) event).getApplicationContext();
        SystemInitService initService = context.getBean(SystemInitService.class);
        initService.systemInit();
        return;
    }
    //应用启动失败后产生这个事件
    if (event instanceof ApplicationFailedEvent){
        System.out.println("ApplicationFailed");
        return;
    }
}
}

3.3监听器listener注册

第一种方式是手动注册,即在SpringApplication初始化的时候添加进去

/**
 * Created by l2h on 18-4-9.
 * Desc: 应用入口 main
 * @author l2h
 */
@SpringBootApplication
public class QuickStartApplication {
public static void  main(String[]args){
    new SpringApplicationBuilder().sources(QuickStartApplication.class)
            .listeners(new SimosApplicationListener()).run(args);
}
}

第二种方式是,自动注册(springboot本身listener实现也是通过这种方式).在resources目录下添加META-INF目录,然后在META-INF目录里添加spring.factories文件,文件内容是:

org.springframework.context.ApplicationListener=\
com.simos.listener.SimosApplicationListener

通过自动注册的方式main入口就与无listener时一样:

/**
 * Created by l2h on 18-4-9.
 * Desc: 应用入口 main
 * @author l2h
 */
@SpringBootApplication
public class QuickStartApplication {
public static void  main(String[]args){
  SpringApplication.run(QuickStartApplication.class,args);
}
}

还可以自定义事件以及事件的监听

ApplicationEvent以及Listener是Spring为我们提供的一个事件监听、订阅的实现,内部实现原理是观察者设计模式,设计初衷是为了系统业务逻辑解耦,提高可扩展性及可维护性。事件发布者并不需要考虑谁去监听,监听具体的内容是什么,发布者的工作只是为了发布时间而已。

举个栗子:

我们在平时拔河比赛中,裁判员给我们吹响了开始的信号,也就是给我们发布了一个开始的事件,而拔河双方人员都在监听着这个事件,一旦事件发布后双方人员就开始往自己方使劲。而裁判并不关心你比赛的过程,只是给你发布事件你执行就可以了。

现在我们写个demo,再springboot平台上通过ApplicationEvents以及Listener来完成简单的注册时间流程。

UserBean:

package com.nicecloud.model;
 
import lombok.Data;
 
@Data
public class UserBean {
    //用户名
    private String name;
    //密码
    private String password;
}
package com.nicecloud.controller;
 
import com.nicecloud.model.UserBean;
import com.nicecloud.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
 
@RestController
public class UserController {
    @Autowired
    private UserService userService;
 
    @GetMapping("/register")
    public String register(UserBean userBean) {
        userService.register(userBean);
        return "注册成功!";
    }
}
package com.nicecloud.service;
 
import com.nicecloud.event.UserRegisterEvent;
import com.nicecloud.model.UserBean;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
 
@Service
@Slf4j
public class UserService {
 
    /**
     * 事件发布由ApplicationContext对象管控的,发布事件前需要注入ApplicationContext对象调用publishEvent完成事件发布
     */
    @Autowired
    ApplicationContext applicationContext;
 
    /**
     * 用户注册方法
     *
     * @param userBean
     */
    public void register(UserBean userBean) {
        log.info("start=====================================================================");
        log.info("业务逻辑...");
        // 发布UserRegisterEvent事件,时间发布是由ApplicationContext对象管控的,
        applicationContext.publishEvent(new UserRegisterEvent(this, userBean));
    }
}
package com.nicecloud.myproject;
 
import com.nicecloud.model.UserBean;
import org.springframework.context.ApplicationEvent;
 
public class UserRegisterEvent extends ApplicationEvent {
 
    /**
     * 注册用户对象
     */
    private UserBean user;
 
    /**
     * 重写构造函数
     *
     * @param source 发生事件的对象
     * @param user 注册用户对象
     */
    public UserRegisterEvent(Object source, UserBean user) {
        super(source);
        this.user = user;
    }
}

自定义时间UserRegisterEvent继承了ApplicationEvent,继承后必须重载构造函数,构造函数的参数可以任意指定,其中source参数指的是发生事件的对象,一般我们在发布事件时使用的是this关键字代替本类对象,而user参数是我们自定义的注册用户对象,该对象可以在监听内被获取。

3、实现监听

在Spring内部中有多种方式实现监听如:@EventListener注解、实现ApplicationListener泛型接口、实现SmartApplicationListener接口等,我们下面来讲解下这三种方式分别如何实现。

3.1 @EventListener实现监听

注解方式比较简单,并不需要实现任何接口,具体代码实现如下所示:

package com.nicecloud.listener;
 
import com.nicecloud.event.UserRegisterEvent;
import com.nicecloud.model.UserBean;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
 
@Component
@Slf4j
public class AnnotationRegisterListener {
    /**
     * 注册监听实现方法
     * 只需要让监听类被Spring管理即可,@EventListener注解会根据方法内配置的时间完成监听。
     * 接下来可启动项目来测试事件发布时是否被监听者所感知
     *
     * @param userRegisterEvent
     */
    @EventListener
    public void register(UserRegisterEvent userRegisterEvent) {
        // 获取注册用户对象
        UserBean user = userRegisterEvent.getUser();
        // 打印注册用户信息
        log.info("@EventListener注册信息,用户名:"+user.getName()+",密码:"+user.getPassword());
    }
}

只需要让我们的监听类被Spring所管理即可,在我们用户注册监听实现方法上添加@EventListener注解,该注解会根据方法内配置的事件完成监听。下面我们启动项目来测试下我们事件发布时是否被监听者所感知。

测试事件监听

使用SpringBootApplication方式启动成功后,我们来访问下地址:http://127.0.0.1:8080/register?name=admin&password=123456,界面输出内容肯定是“注册成功”,这个是没有问题的,我们直接查看控制台输出内容,如下所示:

说明使用@EventListener注解配置的监听已经生效了,当我们在UserService内发布了注册事件时,监听方法自动被调用并且输出内信息到控制台。

3.2 ApplicationListener实现监听

这种方式也是Spring之前比较常用的监听事件方式,在实现ApplicationListener接口时需要将监听事件作为泛型传递,监听实现代码如下所示:

package com.nicecloud.listener;
 
import com.nicecloud.event.UserRegisterEvent;
import com.nicecloud.model.UserBean;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
 
@Component
@Slf4j
public class RegisterListener implements ApplicationListener<UserRegisterEvent> {
 
    /**
     * 原始方式实现,用户注册监听
     * 实现接口后需要使用@Component注解来声明该监听需要被Spring注入管理,当有UserRegisterEvent时间发布时,监听程序会自动调用onApplicationEvent方法
     *
     * @param userRegisterEvent
     */
    @Override
    public void onApplicationEvent(UserRegisterEvent userRegisterEvent) {
        // 获取注册用户信息
        UserBean user = userRegisterEvent.getUser();
        // 打印注册用户信息
        log.info("ApplicationListener接口注册信息,用户名:"+user.getName()+",密码:"+user.getPassword());
    }
}

 

实现接口后需要使用@Component注解来声明该监听需要被Spring注入管理,当有UserRegisterEvent事件发布时监听程序会自动调用onApplicationEvent方法并且将UserRegisterEvent对象作为参数传递。
我们UserService内的发布事件不需要修改,我们重启下项目再次访问之前的地址查看控制台输出的内容如下所示:

控制台打印了我们监听内输出用户信息,事件发布后不会考虑具体哪个监听去处理业务,甚至可以存在多个监听同时需要处理业务逻辑。

在注册时如果不仅仅是记录注册信息到数据库,还需要发送邮件通知用户,当然我们可以创建多个监听同时监听UserRegisterEvent事件,接下来我们先来实现这个需求。

邮件通知监听

我们使用注解的方式来完成邮件发送监听实现,代码如下所示:


package com.nicecloud.listener;
 
import com.nicecloud.event.UserRegisterEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
 
@Component
@Slf4j
public class RegisterUserEmailLister {
    /**
     * 发送邮件监听实现
     *
     * @param userRegisterEvent
     */
    @EventListener
    public void sendMail(UserRegisterEvent userRegisterEvent) {
        userRegisterEvent.getUser();
        log.info("用户注册成功,发送邮件通知!");
    }
}

 

监听编写完成后,我们重启项目,再次访问注册请求地址查看控制台输出内容如下所示:

单纯看打印结果一切正常,但是事件监听是无序的,监听到的事件先后顺序完全随机出现的。我们接下来使用SmartApplicationListener实现监听方式来实现该逻辑。

3.3 SmartApplicationListener实现有序监听

对注册用户以及发送邮件的监听重新编写,注册用户写入数据库监听代码如下所示:

package com.nicecloud.listener;
 
import com.nicecloud.event.UserRegisterEvent;
import com.nicecloud.model.UserBean;
import com.nicecloud.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.event.SmartApplicationListener;
import org.springframework.stereotype.Component;
 
/**
 * SmartApplicationListener接口继承了全局监听ApplicationListener,并且泛型对象使用的ApplicationEvent来作为全局监听
 * 即使用SmartApplicationListener作为监听父接口的实现,监听所有事件发布。
 *
 */
@Component
@Slf4j
public class UserRegisterListener implements SmartApplicationListener{
 
    /**
     * @param aClass 监听到的事件类型
     *
     * @return
     */
    @Override
    public boolean supportsEventType(Class<? extends ApplicationEvent> aClass) {
        // 只有UserRegisterEvent监听类型才会执行下面逻辑
        return aClass == UserRegisterEvent.class;
    }
 
    @Override
    public boolean supportsSourceType(Class<?> sourceType) {
        // 只有在UserService内发布的事件类型是UserRegisterEvent事件时,才会执行下面逻辑
        return sourceType == UserService.class;
    }
 
    /**
     * 同步情况下监听执行顺序,数值越小证明优先级越高,执行顺序越靠前
     *
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }
 
    /**
     * supportsEventType & supportsSourceType两个方法返回true时调用该方法执行业务逻辑
     *
     * @param applicationEvent
     */
    @Override
    public void onApplicationEvent(ApplicationEvent applicationEvent) {
        // 转换事件类型
        UserRegisterEvent userRegisterEvent = (UserRegisterEvent) applicationEvent;
        // 获取注册用户对象信息
        UserBean userBean = userRegisterEvent.getUser();
        // 完成注册业务逻辑
        log.info("注册信息,用户名:" + userBean.getName() + ",密码:" + userBean.getPassword());
    }
}

 

SmartApplicationListener接口继承了全局监听ApplicationListener,并且泛型对象使用的ApplicationEvent来作为全局监听,可以理解为使用SmartApplicationListener作为监听父接口的实现,监听所有事件发布。

既然是监听所有的事件发布,那么SmartApplicationListener接口添加了两个方法supportsEventType、supportsSourceType来作为区分是否是我们监听的事件,只有这两个方法同时返回true时才会执行onApplicationEvent方法。

可以看到除了上面的方法,还提供了一个getOrder方法,这个方法就可以解决执行监听的顺序问题,return的数值越小证明优先级越高,执行顺序越靠前。

注册成功发送邮件通知监听代码如下所示:

package com.nicecloud.listener;
 
import com.nicecloud.event.UserRegisterEvent;
import com.nicecloud.model.UserBean;
import com.nicecloud.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.event.SmartApplicationListener;
import org.springframework.stereotype.Component;
 
/**
 * SmartApplicationListener接口继承了全局监听ApplicationListener,并且泛型对象使用的ApplicationEvent来作为全局监听
 * 即使用SmartApplicationListener作为监听父接口的实现,监听所有事件发布。
 */
@Component
@Slf4j
public class UserRegisterSendMailListener implements SmartApplicationListener{
    /**
     * 该方法返回true&supportsSourceType同样返回true时,才会调用该监听内的onApplicationEvent方法
     *
     * @param aClass 接收到的监听事件类型
     * @return
     */
    @Override
    public boolean supportsEventType(Class<? extends ApplicationEvent> aClass) {
        //只有UserRegisterEvent监听类型才会执行下面逻辑
        return aClass == UserRegisterEvent.class;
    }
 
    /**
     * 该方法返回true&supportsEventType同样返回true时,才会调用该监听内的onApplicationEvent方法
     * @param aClass
     * @return
     */
    @Override
    public boolean supportsSourceType(Class<?> aClass) {
        //只有在UserService内发布的UserRegisterEvent事件时才会执行下面逻辑
        return aClass == UserService.class;
    }
 
    /**
     * supportsEventType & supportsSourceType 两个方法返回true时调用该方法执行业务逻辑
     * @param applicationEvent 具体监听实例,这里是UserRegisterEvent
     */
    @Override
    public void onApplicationEvent(ApplicationEvent applicationEvent) {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
 
        //转换事件类型
        UserRegisterEvent userRegisterEvent = (UserRegisterEvent) applicationEvent;
        //获取注册用户对象信息
        UserBean user = userRegisterEvent.getUser();
        log.info("用户:"+user.getName()+",注册成功,发送邮件通知。");
    }
 
    /**
     * 同步情况下监听执行的顺序
     * @return
     */
    @Override
    public int getOrder() {
        return 1;
    }
}

在getOrder方法内我们返回的数值为“1”,这就证明了需要在保存注册用户信息监听后执行,下面我们重启项目访问注册地址查看控制台输出内容如下所示:

如果说我们不希望在执行监听时等待监听业务逻辑耗时,发布监听后立即要对接口或者界面做出反映,我们该怎么做呢?

4、使用@Async实现异步监听

@Aysnc其实是Spring内的一个组件,可以完成对类内单个或者多个方法实现异步调用,这样可以大大的节省等待耗时。内部实现机制是线程池任务ThreadPoolTaskExecutor,通过线程池来对配置@Async的方法或者类做出执行动作。

线程任务池配置

我们创建一个ListenerAsyncConfiguration,并且使用@EnableAsync注解开启支持异步处理,我们自定义的监听异步配置类实现了AsyncConfigurer接口并且实现内getAsyncExecutor方法以提供线程任务池对象的获取。
我们只需要在异步方法上添加@Async注解就可以实现方法的异步调用,为了证明这一点,我们在发送邮件onApplicationEvent方法内添加线程阻塞3秒,具体代码如下所示:


  1. package com.nicecloud.config;

  2. import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;

  3. import org.springframework.context.annotation.Configuration;

  4. import org.springframework.scheduling.annotation.AsyncConfigurer;

  5. import org.springframework.scheduling.annotation.EnableAsync;

  6. import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

  7. import java.util.concurrent.Executor;

  8. @Configuration

  9. @EnableAsync

  10. public class ListenerAsyncConfiguration implements AsyncConfigurer {

  11. /**

  12. * 获取异步线程池执行对象

  13. *

  14. * @return

  15. */

  16. @Override

  17. public Executor getAsyncExecutor() {

  18. // 使用spring内置线程池对象

  19. ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();

  20. // 设置线程池参数

  21. taskExecutor.setCorePoolSize(5);

  22. taskExecutor.setMaxPoolSize(10);

  23. taskExecutor.setQueueCapacity(25);

  24. taskExecutor.initialize();

  25. return taskExecutor;

  26. }

  27. @Override

  28. public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {

  29. return null;

  30. }

  31. }

下面我们重启下项目,访问注册地址,查看界面反映是否也有延迟。
我们测试发现访问界面时反映速度要不之前还要快一些,我们去查看控制台时,可以看到注册信息输出后等待3秒后再才输出邮件发送通知,而在这之前界面已经做出了反映。

注意:如果存在多个监听同一个事件时,并且存在异步与同步同时存在时则不存在执行顺序。

 

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐