Java系统开发必备的解耦知识:Spring Event事件机制解耦业务场景

1,464 阅读8分钟

一、背景

在日常的业务开发中,我们要思考一下,某个方法的当前流程是否有主流程和子流程业务的区分,比如针对子流程来说:

  • 子流程的业务是否执行时间长,是否需要减少主流程的响应时间
  • 子流程的业务是否不需要与主流程在一个事务控制中
  • 子流程的业务的执行结果成功与否,是否与主流程无关紧要,失败了就是失败了
  • 子流程的业务如果出现错误,是否不能影响子流程

当想到这些问题以后,我们常见的操作就是进行业务的异步化,这是其一个方面,那既然异步化了,涉及到了另外的一个点,就是在异步化之前如何尽可能得与业务解耦,避免主流程改动以后,关联的子流程需要大量改动或重构。本文的主题,业务的解耦,通过对一个业务中子流程的解耦,完成业务扩展与主流程和子流程之间的关联影响。

通常,类似的业务场景:

某个业务执行完成后,进行短信、邮件、飞书提醒相关人员进行通知。

这种类似的比较多,那么这个时候,我们便可以使用一定的子流程的拆分,去和主流程解耦,将这种容易需求变化且和主流程无严格绑定的业务的拆分到其他代码中。

本文暂时使用Spring Event机制解耦主流程和子流程,达到一定的封装与扩展性。

二、Spring Event原生事件机制说明与使用

实际业务开发过程中,业务逻辑可能非常复杂,核心业务 + N 个子业务。如果都放到一块儿去做,代码可能会很长,耦合度不断攀升,维护起来也麻烦,甚至头疼。还有一些业务场景不需要在一次请求中同步完成,比如邮件发送、短信发送等。

Spring Event(Application Event)其实就是一个观察者设计模式,一个 Bean 处理完成任务后希望通知其它 Bean 或者说一个 Bean 想观察监听另一个Bean 的行为。

01

2.1、自定义事件

首先我们可以自定义一个事件类,需要继承 ApplicationEvent 的类成为一个事件类,通常事件类可以代码做了某个业务之后的一个具体的动作行为:

@Data
@ToString
public class SendMessageToUserEvent extends ApplicationEvent {

  /** 该类型事件携带的信息 */
  private String userId;

  public SendMessageToUserEvent(Object source, String userId) {
    super(source);
    this.userId = userId;
  }
}

2.2、自定义监听器

然后定义一个监听器并计划处理该事件,实现 ApplicationListener 接口或者使用 @EventListener 注解:

@Slf4j
@Component
public class UserMessageSendListener implements ApplicationListener<SendMessageToUserEvent> {

  /** 使用 onApplicationEvent 方法对消息进行接收处理 */
  @Override
  public void onApplicationEvent(SendMessageToUserEvent event) {
    String userId = event.getUserId();
    long start = System.currentTimeMillis();
    Thread.sleep(2000);
    long end = System.currentTimeMillis();
    log.info("{}:事件处理完成:({})毫秒", userId, (end - start));
  }
}

2.3、自定义发布者

事件和监听器都有了,然后需要通过一个调度去发布事件,一般通过通过 ApplicationEventPublisher 发布事件,这里模拟一个订单业务

@Slf4j
@Service
@RequiredArgsConstructor
public class UserService {

  /** 注入ApplicationContext用来发布事件 */
  private final ApplicationContext applicationContext;

  /**
   * 下单
   *
   * @param orderId 订单ID
   */
  public String buyOrder(String orderId) {
    long start = System.currentTimeMillis();
    // 1.查询商品数据
    // 2.校验价格
    // 3.组装参数
    // 4.保存订单数据入库
    // 5.短信通知 (同步处理)
    String userId = UserContextHolder.getUserId();
    applicationContext.publishEvent(new SendMessageToUserEvent(this, userId));
    long end = System.currentTimeMillis();
    log.info("完成,总耗时:({})毫秒", end - start);
    return "成功";
  }
}

有些场景可以结合@EventListener注解、@EnableAsync注解、@Async注解使用,更简单。

2.4、扩展方向

1、项目增加异步线程池进行处理,让某些事件的操作编程异步执行,提高主流程的响应时间,比如启动类上面增加@EnableAsync启动

2、定义某个方法上增加@EventListener注解定义为事件监听器去监听某个事件,更方便

三、项目中的基于Spring Event的事件封装

我们可以基于上面的标准玩法和流程,在进一步进行封装,让当前项目的开发者可以统一调用方式,便于后续替换底层事件框架,然后在我们项目中定义全局的事件访问入口,进行事件的派发,同时以便后续需求的扩展在事件层进行。

3.1、定义事件基础类

如果当前系统是一个商城系统,我们可以定义一个基础的BasicMallEvent事件类,该类继承自Spring的ApplicationEvent事件。

如果当前系统是一个Saas系统,我们也可以定义一个基础的BasicSaasEvent事件类,该类继承自Spring的ApplicationEvent事件。

如果当前系统是一个CRM系统,我们也可以定义一个基础的BasicCrmEvent事件类,该类继承自Spring的ApplicationEvent事件。

本文以商城系统为例,可以定义一个事件类,系统中的所有事件类可以继承自该接口,代码如下所示:

/**
 * 商城事件基础类,基于Spring Event
 * @date 2023-01-04
 */
public class BasicMallEvent extends ApplicationEvent implements MallEvent {

    public BasicMallEvent(Object source) {
        super(source);
    }

}

3.2、定义抽象事件监听器

该类默认继承ApplicationListener类,监听BasicMallEvent事件,提供supportsEventType扩展方法,子类需提供具体支持的事件类型,提供doMallEvent扩展方法,子类需实现此方法进行事件开发:

/**
 * 商城事件抽象配置的统一处理的监听器
 * @date 2023-01-04
 */
public abstract class AbstractMallEventListener implements ApplicationListener<BasicMallEvent> {

    public AbstractMallEventListener() {
    }

    @Override
    public void onApplicationEvent(BasicMallEvent event) {
        if(supportsEventType(event.getClass())){
            this.doMallEvent(event);
        }
    }

    /**
     * 判断子类的事件类型是否匹配
     * @param eventType 事件类型Class
     * @return 是否支持
     */
    public abstract boolean supportsEventType(Class<? extends BasicMallEvent> eventType);

    /**
     * 为子类暴露新的事件方法
     * @param event 事件对象
     */
    protected abstract void doMallEvent(BasicMallEvent event);


}

其中,该类声明另外两个抽象方法,一个方法是子类需要告诉抽象类,当前是否是支持的事件类型,另外一个方法是子类需要实现的事件处理业务代码,对下层屏蔽了默认的onApplicationEvent方法。

3.3、定义事件管理器

这里使用单例设计模式进行作为入口进行调用(如果自己从来没用过设计模式,不如把这个事件机制的实现逻辑引入到项目中吧),在理解上可以进行调用,并封装了SpringContextUtil工具类由于从ApplicationContext进行事件派发操作:

/**
 * 商城事件发布管理器
 * @date 2023-01-04
 */
public class MallEventManager{

    /**
     * 单例类
     */
    private static final MallEventManager INSTANCE = new MallEventManager();

    private MallEventManager(){
    }

    /**
     * 全局的单例的访问方法
     * @return 返回事件管理器的实例
     */
    public static MallEventManager getInstance() {
        return INSTANCE;
    }


    /**
     * 统一发布事件
     * @param mallEvent 商城事件 需继承自BasicMallEvent
     */
    public void publishEvent(BasicMallEvent mallEvent){
        SpringContextUtil.getContext().publishEvent(mallEvent);
    }

}

这就是在日常工作开发中,刻意使用单例设计模式进行操作,因为有些业务需求并不是CRUD层面去开发的,通常需要提供一个全局访问的入口,然后让调用者更容易理解的方式去调用,而非各种Service进行来回进行注入。

3.4、业务实战使用案例

3.4.1、业务实战使用案例1:取消订单发送短信

事件派发:

获得MallEventManager的实例,然后派发事件,通常事件的第1个参数是事件源,通常写this即可,然后可以传递多个参数存储到事件中,一般不建议过多的参数,统一封装为DTO参数传递:

MallEventManager.getInstance().publishEvent(new OrderCancelApplyNotifyEvent(this, orders));

其中,OrderCancelApplyNotifyEvent为继承了BasicMallEvent类的真实业务类型事件,订单取消通知事件。使用时直接通过单例的方式调用即可。

3.4.2、业务实战使用案例1:主流程业务完成后,低耦合调用下个子业务逻辑

常见的写法是一个service D中注入serviceA、serviceB、serviceC方法,但是某些情况下,serviceC属于和主流程关联不大的东西,完全可以让serviceA、serviceB的事务执行完成后去调用serviceC,但是某些情况下,这样做会比较耦合:service D中强依赖了serviceC。此种情况下, 便可以派发一个事件,在事件监听器中调用C,降低了耦合度,同时提高一个扩展性,假设未来有逻辑和serviceC类似,完全可以在监听器中去代码实现,避免改动serviceD。

四、其他类似的事件模型

4.1、Guava的EventBus事件总线驱动模型

blog.csdn.net/peterwangha…

www.jianshu.com/p/eec6c29a9…

4.2、Nacos的通知中心的事件机制

www.cnblogs.com/lukama/p/14…

4.3、网友开发的事件处理模型,支持单事件多处理器

gitee.com/Aqishi/knig…

五、总结

本文介绍了Spring Event事件机制的案例、业务封装调用代码实现,感兴趣的可以根据在当前工作中实际运用一下哦。

未来优化方向:

(1)、后续可在事件监听器的基础上,增加扩展事件处理器Handler,让同一个事件监听器支持N个业务处理器。

六、参考资料

1、mp.weixin.qq.com/s?\_\_biz=M…

2、blog.51cto.com/u\_11554106…

喜欢本篇文章的,请点赞、收藏、分享、评论哦,一起和我交流吧。