SpringBoot事件监听机制,并实现业务解耦

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

applicationContext.publishEvent:相当于生产者

Event:相当于事件,在里面可以携带参数,该参数可以类比body,相当于消息体

Listener:相当于消费者

所以整体就是一个单机版的mq,在单机中也可以使用这种方式进行解耦和异步处理

本章目标

我们本章在SpringBoot平台上通过ApplicationEvents以及Listener来完成简单的注册事件流程。

构建项目

我们本章只是简单的讲解如何使用ApplicationEvent以及Listener来完成业务逻辑的解耦,不涉及到数据交互所以依赖需要引入的也比较少,项目pom.xml配置文件如下所示:

<dependencies>
    <!--web-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--lombok-->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.16.16</version>
    </dependency>
    <!--test-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
复制代码

Event事件

@Getter
public class UserRegisterEvent extends ApplicationEvent {

    private User user;

    public UserRegisterEvent(Object source,User user) {
        super(source);
        this.user = user;
    }
}
复制代码

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

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

创建User

我们简单创建一个用户实体,并添加两个字段:用户名、密码。实体代码如下所示:

@Data
public class User {

    private String username;

    private String password;

}
复制代码

创建UserController

创建一个@RestController控制器,对应添加一个注册方法简单实现,代码如下所示:

@RestController
public class UserController {

    @Autowired
    UserService userService;

    @GetMapping("/register")
    public void registerUser() {
        User user = new User();
        user.setUsername("小白");
        user.setPassword("123");
        userService.registerUser(user);
    }

}
复制代码

创建UserService

UserService内添加一个注册方法,该方法只是实现注册事件发布功能,代码如下所示:

@Slf4j
@Service
public class UserService {

    @Autowired
    ApplicationContext applicationContext;

    @Autowired
    private AsyncService asyncService;

    public void registerUser(User user) {
        log.info("执行业务代码!");
        applicationContext.publishEvent(new UserRegisterEvent(this, user));

        applicationContext.publishEvent(new UserMailEvent(this, user));

        asyncService.testAsync(user);
    }
}
复制代码

事件发布是由ApplicationContext对象管控的,我们发布事件前需要注入ApplicationContext对象调用publishEvent方法完成事件发布。

@EventListener实现监听

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

@Component
@Slf4j
public class AnnotationRegisterListener {

    @Async
    @EventListener(UserRegisterEvent.class)
    public void register(UserRegisterEvent userRegisterEvent) {
        User user = userRegisterEvent.getUser();
        log.info("监听注册,{}", user);
    }

    @Async
    @EventListener(UserMailEvent.class)
    public void sendMail(UserMailEvent userMailEvent) {
        User user = userMailEvent.getUser();
        log.info("监听发送邮件,{}", user);
    }

}
复制代码

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

ApplicationListener实现监听

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

@Slf4j
@Component
public class RegisterApplicationListener implements ApplicationListener<UserRegisterEvent> {

    @Async
    @Override
    public void onApplicationEvent(UserRegisterEvent userRegisterEvent) {
        log.info("ApplicationListener,{}", userRegisterEvent.getUser());
    }
}
复制代码

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

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

邮件通知监听

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

@Getter
public class UserMailEvent extends ApplicationEvent {

    private User user;

    public UserMailEvent(Object source,User user) {
        super(source);
        this.user = user;
    }
}
复制代码

我们看到控制台输出的内容感到比较疑惑,我注册时用户信息写入数据库应该在发送邮件前面,为什么没有在第一步执行呢? 好了,证明了一点,事件监听是无序的,监听到的事件先后顺序完全随机出现的。我们接下来使用SmartApplicationListener实现监听方式来实现该逻辑。

SmartApplicationListener实现有序监听

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

@Component
public class UserRegisterListener 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) {

        //转换事件类型
        UserRegisterEvent userRegisterEvent = (UserRegisterEvent) applicationEvent;
        //获取注册用户对象信息
        UserBean user = userRegisterEvent.getUser();
        //.../完成注册业务逻辑
        System.out.println("注册信息,用户名:"+user.getName()+",密码:"+user.getPassword());
    }

    /**
     * 同步情况下监听执行的顺序
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }
}
复制代码

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

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

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

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

@Component
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) {
        //转换事件类型
        UserRegisterEvent userRegisterEvent = (UserRegisterEvent) applicationEvent;
        //获取注册用户对象信息
        UserBean user = userRegisterEvent.getUser();
        System.out.println("用户:"+user.getName()+",注册成功,发送邮件通知。");
    }

    /**
     * 同步情况下监听执行的顺序
     * @return
     */
    @Override
    public int getOrder() {
        return 1;
    }
}
复制代码

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

使用@Async实现异步监听

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

线程任务池配置

我们创建一个ListenerAsyncConfiguration,并且使用@EnableAsync注解开启支持异步处理,具体代码如下所示:

@Configuration
@EnableAsync
public class ListenerAsyncConfiguration implements AsyncConfigurer
{
    /**
     * 获取异步线程池执行对象
     * @return
     */
    @Override
    public Executor getAsyncExecutor() {
        //使用Spring内置线程池任务对象
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        //设置线程池参数
        taskExecutor.setCorePoolSize(5);
        taskExecutor.setMaxPoolSize(10);
        taskExecutor.setQueueCapacity(25);
        taskExecutor.initialize();
        return taskExecutor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return null;
    }
}
复制代码

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

/*** supportsEventType & supportsSourceType 两个方法返回true时调用该方法执行业务逻辑
* @param applicationEvent 具体监听实例,这里是UserRegisterEvent
*/
@Override
@Async
public void onApplicationEvent(ApplicationEvent applicationEvent) {
    try {
        Thread.sleep(3000);//静静的沉睡3秒钟
    }catch (Exception e)
    {
        e.printStackTrace();
    }
    //转换事件类型
    UserRegisterEvent userRegisterEvent = (UserRegisterEvent) applicationEvent;
    //获取注册用户对象信息
    UserBean user = userRegisterEvent.getUser();
    System.out.println("用户:"+user.getName()+",注册成功,发送邮件通知。");
}
复制代码

总结

我们在传统项目中往往各个业务逻辑之间耦合性较强,因为我们在service都是直接引用的关联service或者jpa来作为协作处理逻辑,然而这种方式在后期更新、维护性难度都是大大提高了。然而我们采用事件通知、事件监听形式来处理逻辑时耦合性则是可以降到最小。

猜你喜欢

转载自juejin.im/post/7034494768977707045
今日推荐