ramostear.comramostear.com 谭朝红的技术分享博客

格言 编程是一门技术,也是一门艺术 !

5个Spring Event奇技淫巧,你GET到了吗?

5个Spring Event奇技淫巧,你GET到了吗?

5个Spring Event奇技淫巧,你GET到了吗?

前言:

谈到事件,接触过前端或GUI编程(JavaScript,Swing)的同学应该有较深刻印象。如事件源、事件的监听、回调等概念应该耳熟能详。而在Web应用程序中则很少用到事件。但在Web应用程序中,也可以轻松实现面向事件的编程。

1、为什么需要面向事件编程

​ 在本文中,将基于Spring框架来谈谈关于面向事件编程的5个奇技淫巧。在开始主要内容之前,先了解一下为什么需要面向事件编程。首先看一个生活中的一个场景来帮助我们快速的了解面向事件编程所带来的好处。以客户到银行柜台取现为例,在较早以前,银行柜台取现需要一个接一个的排长龙等待柜台业务员处理取现业务,在此期间,客户不能离开队伍去做其他的事情,否则需要重新排队。这好比常规的面向过程的编程,需要依次执行每条逻辑语句,直到所有的语句都执行完毕,方法才返回结果。现在,银行柜台取现多了一台叫号机,需要办理取现业务的客户先通过叫号机领取一张业务号,如果等待人数过多的时候,客户可以先处理自身的事情,直到柜台叫到自己的号时,才到柜台办理取现业务。这就是一个典型(不太严谨)面向事件的处理过程。首先,客户通过叫号机注册一个取现的事件,此时的叫号机,相当于事件发布器(事件注册中心),客户相当于事件源,当柜台收到有客户需要取现的消息时,就会广播提示客户办理取现业务,柜台就相当于一个事件监听器。

​ 如上所述,面向事件的编程(也称作事件驱动编程)是基于对消息信号的反应的编程。面向事件编程需要满三个条件:

  • 1、事件源:消息的源头,如点击一个按钮、访问某个页面、新增一条数据等。
  • 2、事件注册中心:事件注册中心用于存储和发布事件。
  • 3、事件监听器:用于接收特定的消息,并作出对应的反应。

下面通过一张图更为直观的了解面向事件编程的基本原理:

图 1-1 事件处理机制

2、Spring Events

​ Spring Events 是Spring Framework的一部分,但在应用程序中并不经常使用,甚至被忽略。然而,Spring的Application拥有强大的事件发布并注册事件监听器的能力,它拥有一套完整的事件发布与处理机制。Spring 4.1开始引入@EventListener注解来监听具体类型的事件,它的出现,消除了使用Spring定义一个事件监听器的复杂操作。仅仅通过一个简单的注解,就可以完成一个监听器的定义,你不需要额外的进行其他的配置;这简直太棒了。在Spring Framework中,事件的发布是由ApplicationContext提供的,如果想完成一个完整的面向事件(也称为事件驱动编程)编程,你需要遵循以下三个基本的原则:

  • 1、定义一个事件,且扩展ApplicationEvent
  • 2、事件发布者应该注入一个ApplicationEventPublisher对象
  • 3、监听器实现ApplicationListener接口或者使用@EventListener注解

在开始介绍面向事件编程的具体实施细节之前,先通过一张类图了解一下Spring的事件机制设计类图:

图 2-1 Spring Event 类图

​ 通过类图,先快速对Spring Event事件类图做一个简单的介绍:

  • 1、ApplicationEventPublisher是Spring的事件发布接口,所有的事件都是通过该接口提供的publishEvent方法发布事件的。
  • 2、ApplicationEventMulticaster是Spring事件广播器,它将提供一个SimpleApplicationEventMulticaster实现类,如果没有提供任何可用的自定义广播器,Spring将采用默认的广播规则。
  • 3、ApplicationListener是Spring事件监听接口,所有的事件监听器都需要实现此接口,并通过onApplicationEvent方法处理具体的逻辑。
  • 4、在Spring Framework中,ApplicationContext本身就具备监听器的能力,我们也可以通过ApplicationContext发布先关的事件。

​ 通过分析,可以看到:当一个事件通过ApplicationEventPublisher发布出去之后,ApplicationEventMulticaster广播器就会开始工作,它将去ApplicationContext中寻找对应的事件监听器(ApplicationListener),并执行对应监听器中的onApplicationEvent方法,从而完成事件监听的全部逻辑。

3、自定义Spring Event

​ 本次内容将使用Spring Boot 2.1.5快速搭建所需要的环境。首先将演示如何自定义一个事件,并监听该事件且对监听到的事件做出反应。

​ 我们将使用IntelliJ IDEA创建一个Web项目,并在pom.xml文件中添加如下的依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.ramostear</groupId>
    <artifactId>spring-boot-event</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-boot-event</name>
    <description>Demo project for Spring events</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

说明:

这将是一个完整的演示Spring Event事件处理机制的项目,因为期间需要演示事务管理的特性,所以加入了MySql数据库依赖。

​ 本次的内容将依据这样一个场景展开:当成功添加一条用户信息后,将发送一条邮件到用户的邮箱账户。首先,需要定义两个实体,用户实体和邮件详情实体。具体代码如下:

接下来是邮件详情实体类:

持久化类通过扩展JPA的JpaRepository类快速获得CRUD能力。用户持久化类和邮件详情持久化类分别如下:

UserRepository.java

package com.ramostear.repository;

import com.ramostear.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 * Created by Ramostear on 2019/5/31 0031.
 */
public interface UserRepository extends JpaRepository<User,Long> {
}

EmailDetailRepository.java

package com.ramostear.repository;

import com.ramostear.domain.EmailDetail;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 * Created by Ramostear on 2019/5/31 0031.
 */
public interface EmailDetailRepository extends JpaRepository<EmailDetail,Long> {
}

接下来,将定义一个用户业务接口,提供创建和查找用户两个方法,代码如下:

package com.ramostear.service;

import com.ramostear.domain.User;

/**
 * Created by Ramostear on 2019/5/31 0031.
 */
public interface UserService {

    User created(User user);


    User findOne(Long id);
}

基本的环境已经准备就绪,现在开始定义定义第一个事件类:SendEmailEvent.java,其代码如下所示

如之前所述,自定义事件需要扩展ApplicationEvent类,并提供自己的实现细节。这里的事件需要为其提供一个用户ID,在接下来的监听器中,通过此ID来甄别具体的用户,并发送邮件。

接下来,我们将使用@EventListener注解实现一个事件监听器,其源码如下:

说明:

在此类中,我们注入了EmailDetailRepository和UserRepository两个依赖项,并使用@EventListener注解对sendEmail方法进行了标注。在方法中,使用了Thread.sleep(1000*3)让程序休眠3秒,以模拟发送邮件时所耗费的时间。@Slf4j注解是Lombok提供的快速使用Logger的一个快捷方式。@Component注解用于标注此类是一个普通的组件,将被Spring自动扫描并管理。

​ 准备好了事件源以及事件监听器,现在来看看如何发布一个事件。发布事件将在用户业务实现类中进行操作。UserServiceImpl.java类的实现细节如下:

​ 如前面章节所讲,如果使用Spring发布一个事件,需要在具体的类中注入一个ApplicationEventPublisher对象。在用户业务实现类中,通过构造函数的方式注入ApplicationEventPublisher对象和UserRepository持久化对象。在created(User user)方法中,当成功保存用户数据后,通过发布一个SendEmailEvent事件,将邮件发送给用户的邮箱。与此同时,我们记录了程序执行完所耗费的事件,在下一小节中我们将使用到这个数据。

​ 最后,我们定义一个控制器,提供一个处理添加用户请求的方法,验证发送邮件事件是否被监听器成功监听。控制器代码如下:

启动项目,并使用PostMan工具对POST /users API进行测试:

图 3-1 Postman测试结果

经测试,成功返回了用户信息,且耗时3530ms。我们在看看控制台的日志信息:

图 3-2 日志记录信息

​ 至此,第一个基于Spring Event的面向事件的编程案例完成。在这个案例中,虽然我们解决了事件的发送和监听的编码实现问题,但可能会有同学会想,这样子的处理方式和将发布事件的代码替换成直接调用邮件持久化类保存邮件信息没有任何区别,如同一开始说的案例,虽然有叫号机叫号,解决排队的问题,但等待的时间依旧没有改变,这和直接排队在时间耗费上没有根本的变化。那么针对这样一个问题,我们在接下来的内容中将讲解如何实现异步事件。异步事件就好比你现在从普通储蓄客户变成了VIP会员,你可以不去银行柜台叫号,而是通过电话或者其他手段预约一个时间去办理业务,银行客户经理会给你预留相应的时间,当预约时间到达之后,你才去银行柜台办理业务。

4、Spring 异步事件

​ 在该小节的基础上,将原有的事件改造成异步事件,以节省客户端等待服务端响应的时间。使用Spring实现异步事件有两种方式,第一种方式是使用@Async注解对监听器进行标注,这是最为简单的一种实现方式;第二种是通过配置类对Spring的事件广播器进行配置,设置Multicaster对异步操作的支持,此方法比第一种方法稍微复杂一些,但可以拥有对事件广播器更多的控制权。接下来,将分别介绍这两种实现方式。

4-1、使用@Async注解实现异步事件

​ 在现有的工程中,我们需要对两个类进行改造。第一处改造是在项目主类上加入@EnableAsync注解,开启对异步操作的支持。代码如下:

package com.ramostear;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableAsync
public class SpringBootEventApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootEventApplication.class, args);
    }
}

第二步是改造监听器,在原来的方法上加入@Async注解,其余部分保持不变,代码如下:

    @Async
    @EventListener
    public void sendEmail(SendEmailEvent event){
       ...
    }

通过上述两个步骤,我们就将Spring默认的同步事件改造为异步事件。重新启动项目,依旧使用Postman对接口POST /users进行测试,结果如下:

图 4-1 @Async异步事件

通过上图我们可以看到,服务端成功返回用户信息,注意观察,从客户端发起请求到服务端响应结束总共耗时480ms,而在第一个案例中,请求和响应的总耗时为3530ms。最后我们再来看看控制台的日志信息,确定一下数据库是否成功保存用户邮件:

图 4-2 @Async异步事件控制台日志信息

通过上述的测试结果,我们可以看到,异步事件已经生效,在相同的环境下,同样的业务请求,使用同步事件和异步事件,服务端响应时间存在巨大的差异。同步事件耗时3530ms,异步事件耗时480ms,这就是异步事件带来的巨大优势。

4-2 、使用配置文件开启对异步事件的支持

​ 在4-1中,我们学会了如何使用@Async定义异步事件,那本小节将展示另外一种方式获取对异步事件的支持。首先需要定义一个配置类,该类用于配置SimpleApplicationEventMulticaster类对于异步事件的支持。其源码如下:

package com.ramostear.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ApplicationEventMulticaster;
import org.springframework.context.event.SimpleApplicationEventMulticaster;
import org.springframework.core.task.SimpleAsyncTaskExecutor;

/**
 * Created by Ramostear on 2019/5/31 0031.
 */
@Configuration
public class AsyncSpringEventConfig {

    @Bean(name = "applicationEventMulticaster")
    public ApplicationEventMulticaster applicationEventMulticaster(){
        SimpleApplicationEventMulticaster multicaster = new SimpleApplicationEventMulticaster();
        multicaster.setTaskExecutor(new SimpleAsyncTaskExecutor());
        return multicaster;
    }
}

​ 在此配置文件中,applicationEventMulticaster方法将返回一个SimpleApplicationEventMulticaster对象。在返回此事件发布器之前,我们为此事件发布器设置了一个任务执行器:SimpleAsyncTaskExecutor,当应用程序中有新的异步事件时,将会调用此异步任务执行器的submitListenable(Callable task)方法,发布异步事件任务,并选择相应的监听器处理事件。定义好配置类后,还需要将主类中的@EnableAsync和监听器中的@Async两个注解注释掉,其余部分保持不变。最后重启项目,并测试API接口。

图 4-3 异步事件发布器配置测试

如图4-3所示,测试成功返回用户信息,其请求和响应总耗时为492ms。通过配置的方式开启Spring对异步事件的支持已生效。

5、事件过滤

​ 如果我们想对用户事件进行限制,某一部分满足条件的用户才发送邮件,可以使用condition属性来指定限制的条件。这就好比银行柜台提供的VIP预约取现业务,它分不同等级的VIP,只有达到某个条件时,才享有预约服务。在原来的基础上,我们做如下的改动,当用户ID大于5时,才发送邮件给用户。代码如下:

    @EventListener(condition = "#event.userId > 5")
    public void sendEmail(SendEmailEvent event){
       ...
    }

现在,数据库中用户ID最大为3,我们先添加两个用数据,看邮件详情表是否会新增数据。启动项目,测试API接口:

图 5-1 条件过滤测试一

服务端成功返回结果,用户ID=4,现在看看数据库是否有用户邮件数据被添加:

图 5-2 数据库查询结果一

从查询结果可以看到,并未给ID=4的用户发送邮件。控制台也只输出了插入用户数据的SQL语句:

Hibernate: 
    insert 
    into
        t_user
        (email, first_name, last_name) 
    values
        (?, ?, ?)
2019-06-03 06:14:19.334  INFO 4652 --- [p-nio-80-exec-2] c.r.service.impl.UserServiceImpl         : total times :58

接下来我们再新增两条用户数据,观察当用户ID=6时,是否会为用户发送邮件。图5-3是当用户ID=6时,数据库的查询结果以及控制台的输出:

图 5-3 数据库查询结果二

此时,控制台的输出信息如下所示:

图 5-4 控制台日志信息

由此可以看出,我们的事件过滤条件已生效。

6、事务处理

​ 现在,Spring Event的相关使用技巧以及介绍了大部分,最后让我们来考虑这样一个问题,如果在保存用户的时候发生错误,事件将会怎么样?在监听器处理事件时,保存用户的事务还未提交会怎么样?按照常规的逻辑,当用户持久化事务未提交,或者保存过程中发生异常时,不应该给用户发送邮件。试想一下,如果本次案例的场景是发生在用户注册的业务中,如果用户尚未注册成功,就给用户发送一封邮件,那是多么尴尬的事情。这就涉及到事件的事务控制。在Spring中,可以使用@TransactionalEventListener注解对监听器进行标注,一旦使用此注解,被标注的方法将被纳入到事务管理的范围。此注解是@EventListener的扩展,它允许你将事件的监听绑定到某个事务阶段。可以通过phase属性对事务阶段进行设置。下面是phase可设置的所有事务阶段:

  • 1、AFTER_COMMIT(默认值),用于在事务成功完成后出发事件
  • 2、AFTER_ROLLBACK,用于事务回滚是出发事件
  • 3、AFTER_COMPLETION,用于事务完成后出发事件(AFTER_COMMIT和AFTER_ROLLBACK的一种模糊表述)
  • 4、BEFORE_COMMIT,用于事务提交前出发事件

最后给出一个简单的示例:

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT,condition = "#event.userId >=5")
    public void sendEmail(SendEmailEvent event){
        Optional<User> optional = userRepository.findById(event.getUserId());
        if(optional.isPresent()){
            User user = optional.get();
            log.info("User Detail:{ id:{},firstName:{},email:{}",user.getId(),user.getFirstName(),user.getEmail());
            try {
                Thread.sleep(1000*3);
            } catch (InterruptedException e) {
                log.error(e.getLocalizedMessage());
            }
            emailDetailRepository.save(EmailDetail.builder().
                    userId(user.getId()).
                    email(user.getEmail()).
                    sendTime(new Date()).
                    build());
            log.info("User email already send.");
        }
    }

其余代码保持不变。

总结

以上就是关于Spring Event的5个实用技巧,如果你想了解更多细节内容,我已将全部源码上传至Github仓库,你可以通过此链接获取全部源码。如有问题,请在文章最后给我留言。

(转载本站文章请注明作者和出处:谭朝红-ramostear.com,未经允许请勿做任何商业用途)

发表评论