SpringBoot - Google EventBus、AsyncEventBus

介绍

EventBus 顾名思义,事件总线,是一个轻量级的发布/订阅模式的应用模式,最初设计及应用源与 google guava 库。

相比于各种 MQ 中间件更加简洁、轻量,它可以在单体非分布式的小型应用模块内部使用(即同一个JVM范围)。

我们也可以把它和 MQ 中间件结合起来使用,使用 EventBus 作为当前应用程序接收中间件 MQ 消息的统一入口,然后应用内部基于 EventBus 进行分发订阅,以达到高内聚低耦合的目的(当应用内部需要消费多种不同 MQ 中间件消息时,不需要在当前应用的好多不同代码位置都编写 MQ 消费代码)。

EventBus 整体设计和流程比较简单,由注册、发布和订阅三个要点组成,如下:
SpringBoot - Google EventBus、AsyncEventBus_第1张图片

注意事项

本文对 google guava 库中的 EventBus 进行实例说明,注意事项要先进行特别说明。

  • EventBus 默认为同步调用,同一个 EventBus 中注册的多个订阅处理,再事件下发后是被总线串行逐个调用的,如果其中一个方法占用事件较长,则同一个 EventBus 中的其他事件处于等待状态,且发送消息事件的代码调用处也是同步调用等待的状态。
  • 同一个 EventBus 对象,不仅仅在同一个 post 调用中串行执行,在多次并发 post 调用时,多个 post 调用之间也是串行等待执行的关系,这个要特别注意,应用不当会导致严重的消息消费处理性能瓶颈问题!

所以推荐使用异步的方式处理,异步处理主要包括 “EventBus 使用线程池统一异步” 和 “订阅消费处理代码自己使用线程异步” 两种方式。这里我更推荐使用前者,因为后者对开发者有一定的要求,加入开发者某个耗时的业务订阅实现没有自行使用线程异步处理,则会影响其他处的订阅处理。

代码示例

1、添加 pom 依赖

        
        <dependency>
            <groupId>com.google.guavagroupId>
            <artifactId>guavaartifactId>
            <version>32.1.2-jreversion>
        dependency>
        
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <version>1.18.28version>
            <scope>providedscope>
        dependency>

2、创建一个Java接口用于自动注册

package com.example.demospringbean.eventbus;

/**
 * 用于自动注册事件订阅类的接口
 * 
 * @author shanhy
 * @date 2023-08-30 12:06
 */
public interface EventBusListener {
}

3、编写总配置类

package com.example.demospringbean.eventbus;

import com.google.common.eventbus.EventBus;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;

/**
 * EventBus 配置类
 *
 * @author shanhy
 * @date 2023-08-30 11:11
 */
@Configuration
public class EventBusConfiguration {

    /**
     * 实例化 EventBus 对象,并自动注册所有订阅类对象
     *
     * @param eventListenerList 所有实现了 EventBusListener 接口的实现类
     * @return
     */
    @Bean
    public EventBus eventBus(List<EventBusListener> eventListenerList){
        // 异步处理,按照自己需要,实现自己的 Executor 逻辑,例如为了防止线程长期占用需要增加超时机制等
//      EventBus eventBus = new AsyncEventBus(new Executor() {
//            public void execute(Runnable command) {
//                new Thread(command).start();
//            }
//        });
        EventBus eventBus = new EventBus();
        if(eventListenerList != null && !eventListenerList.isEmpty()) {
            eventListenerList.iterator().forEachRemaining(eventListener -> eventBus.register(eventListener));
        }
        return eventBus;
    }

}

4、编写订阅测试类

package com.example.demospringbean.eventbus;

import com.google.common.eventbus.Subscribe;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 * @author shanhy
 * @date 2023-08-30 11:19
 */
@Component
public class EventSub1 implements EventBusListener {

    @Subscribe
    public void handlerEvent(String test) {
        System.out.println("11111>>>>>" + test);
    }

    @Subscribe
    public void handlerEvent2(String test) throws InterruptedException {
        TimeUnit.SECONDS.sleep(5);
        System.out.println("22222>>>>>" + test);
    }

}

package com.example.demospringbean.eventbus;

import lombok.Builder;
import lombok.Data;

/**
 * @author shanhy
 * @date 2023-08-30 13:19
 */
@Data
@Builder
public class User {

    private String name;
    private int age;

}

package com.example.demospringbean.eventbus;

import com.google.common.eventbus.Subscribe;
import org.springframework.stereotype.Component;

/**
 * @author shanhy
 * @date 2023-08-30 11:19
 */
@Component
public class EventSub2 implements EventBusListener {

    @Subscribe
    public void handlerEvent(String test){
        System.out.println("33333>>>>>" + test);
    }

    @Subscribe
    public void handlerEvent2(User user){
        System.out.println("44444>>>>>" + user.getName());
    }

}

5、编写消息事件发送测试

package com.example.demospringbean;

import com.example.demospringbean.eventbus.User;
import com.google.common.eventbus.EventBus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 接口示例
 *
 * @author shanhy
 * @date 2023-03-20 15:49
 */
@RestController
@RequestMapping("/test")
public class TestController {

    @Autowired
    private EventBus eventBus;

    @GetMapping("/testEvent1")
    public String testEvent1(){
        eventBus.post("Hello");
        return "OK";
    }

    @GetMapping("/testEvent2")
    public String testEvent2(){
        eventBus.post(User.builder().name("Tome").age(22).build());
        return "OK";
    }
}

代码说明:

1、以上代码使用的 EventBus、未使用 AsyncEventBus,并加入了线程 sleep,是为了运行代码可以观察其串行处理效果(浏览器开2个Tab同时调用 /testEvent1 观察输出),让你能更明显的感受到这种处理会给程序带来多大的性能问题(推荐实际业务生产中使用 AsyncEventBus)。

2、@Subscribe 注解修饰的事件处理方法,其参数和发送事件时的消息体会自动按类型关联对应。只有相同类型的消息体才会被消费处理。例如示例中 /testEvent1 接口发送的 “Hello” 字符串,不会触发 handlerEvent2(User user) 方法的执行,同理执行示例中 /testEvent2 接口发送 User 对象时,只会触发 handlerEvent2(User user) 方法。


(END)

你可能感兴趣的:(Spring,Boot,Java开发,java,消息订阅消费)