Java框架-一站式事件实现

各位好,前面我们说了很多事件模型。为什么要用事件,在这里就不继续多讲了。 本篇文章主要讲如何封装事件,让我们统一用事件,一套代码完成事件。废话不多讲,直接上代码。

事件分为本地事件:意思是当前应用发的,当前应用自己消费。利用事件解耦,使代码看起来更优雅。

  • 如何统一代码
    • 消费者
    • 生产者
  • 本地事件
    • Spring事件实现
  • 远程事件
    • RedisMQ实现
    • RocketMQ实现

如何统一代码

消费者

消费者标签

java 复制代码
@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface MsgConsumer {

    String tag();

}

消费者如何写

java 复制代码
@Slf4j
@Component
@MsgConsumer(tag = EventTags.SYS_DEPT_ADD)
@RequiredArgsConstructor
public class DeptAddConsumer implements MessageConsumer<DeptAddEvent> {
   private final DeptService deptService;
   @Override
   public void consume(DeptAddEvent event) {
       deptService.consumeAdd(event);
   }
}

在类名上指定@MsgConsumer,并标记tag 从代码我们看到,该类实现了一个接口,并且还有泛化的参数 首先我们看泛化参数DeptAddEvent

java 复制代码
@NoArgsConstructor
@Data
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class DeptAddEvent extends BaseEvent {
    @ApiModelProperty(value = "组织id",required = true)
    @NotBlank(message = "组织id不能为空")
    private Long deptId;

    @ApiModelProperty(value = "组织名称",required = true)
    @NotBlank(message = "组织名称不能为空")
    private String deptName;
    @ApiModelProperty(value = "区域父子级编码")
    private String regionCode;
    @ApiModelProperty(value = "区域名称")
    private String regionName;
    @ApiModelProperty(value = "父级parentIds,若没有父级传0",required = true)
    @NotBlank(message = "父级parentIds不能为空")
    private String parentIds;

    @ApiModelProperty(value = "父级id,若没有父级传0",required = true)
    @NotBlank(message = "父级id不能为空")
    private Long parentId;

    @ApiModelProperty(value = "部门负责人id-列表")
    private String deptDirectorUserIds;

    @ApiModelProperty(value = "组织标签")
    private String deptTypeEnum;
}

该类继承了一个父类BaseEvent。所以我们所有的事件消息传递的我们都必须要继承该服务BaseEvent

java 复制代码
public abstract class BaseEvent implements Serializable {
    protected Long msgSerialNum;
    protected boolean idempotentEnabled = false;
    protected String tid = TraceUtils.putTidIfAbsent();
    protected String sid = TraceUtils.putSidIfAbsent();
    
    public BaseEvent() {
    }
}

然后再来看看接口如何定义

java 复制代码
public interface MessageConsumer<T extends BaseEvent> {
    void consume(T event);
}

消费者是不是看起来很简单,只要简单的继承MessageConsumer,定义消息体,并在类上增加@MsgConsumer(tag = "标签")

生产者

生产一般需要指定我发的是什么事件,所以我们定义了3个EventPublisher实现了EventPublisher接口

java 复制代码
public interface EventPublisher {

    <T extends BaseEvent> boolean sendMessage(T payload, String msgTag);
}

本地事件

Spring事件实现

Spring事件实现是指在Spring框架中使用事件机制进行组件之间的解耦和通信。 在Spring中,事件是通过ApplicationEvent和ApplicationListener两个接口来实现的。ApplicationEvent表示一个事件对象,它可以携带一些与事件相关的数据;ApplicationListener则是一个事件监听器,用于监听并处理特定的事件。

下面是Spring事件实现的基本步骤:

  1. 定义事件类:首先需要定义一个继承自ApplicationEvent的事件类,该类需要包含事件相关的数据。
  2. 创建事件发布者:在需要发布事件的地方,通过ApplicationEventPublisher接口提供的publishEvent方法发布事件。
  3. 注册事件监听器:在需要监听事件的地方,通过@EventListener注解将事件监听器注册到Spring容器中。
  4. 处理事件:当事件被发布后,所有注册的事件监听器都会收到通知,并执行相应的处理逻辑。
  5. 取消事件监听器:如果不再需要监听某个事件,可以通过Spring容器提供的ApplicationContext接口提供的removeBean方法取消事件监听器的注册。

总之,Spring事件实现是一种非常灵活和强大的组件通信方式,可以帮助开发者实现松耦合、高内聚的应用程序。

为了实现上面的统一代码消费逻辑,我首先定义了一个事件类

java 复制代码
public class LocalEvent<T extends BaseEvent> extends ApplicationEvent {

    //数据
    private T data;
    //tag
    private String tag;

    public LocalEvent(T data) {
        super(data);
    }

    public LocalEvent() {
        super(new Object());
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public String getTag() {
        return tag;
    }

    public void setTag(String tag) {
        this.tag = tag;
    }
}

再定义了一个消费

java 复制代码
@Slf4j
@Component
public class LocalEventHandler implements ApplicationListener<LocalEvent> {

    //    @Override
    public void onEvent(LocalEvent event, long sequence, boolean endOfBatch) {
        log.info("接收到的消息:{}", event.getData());

        String messageTag = event.getTag();

        ConsumerType msgProcessorType = ConsumerCache.getConsumer(messageTag);
        if (msgProcessorType == null) {
            log.warn("No message processor present for tag: " + messageTag);
            return;
        }
        BaseEvent baseMessage = event.getData();
        TraceUtils.putSid(baseMessage.getSid());
        TraceUtils.putTid(baseMessage.getTid());
        AuthContext.set(baseMessage.getLoginUser());
        MessageConsumer<BaseEvent> messageConsumer = ApplicationContextHelper.getBean(msgProcessorType.getConsumerClass());
        if (messageConsumer == null) {
            throw new RuntimeException("No bean mapped for message :" + JSONUtil.toJsonStr(baseMessage));
        }

        try {
            //判断消息是否需要幂等消费
            if (baseMessage.isIdempotentEnabled()) {
                //计算key
                String key = DigestUtils.md5Hex(messageTag + "#" + baseMessage.getMsgSerialNum()
                        + "#" + baseMessage + "#" + messageConsumer.getClass().getName());
                Boolean consumed = ApplicationContextHelper.getBean(RedisService.class).setNx(key, 1, 24, TimeUnit.HOURS);

                //判断消息是否已经被当前服务消费过,如果消费过,则忽略当前消息
                if (Boolean.FALSE.equals(consumed)) {
                    log.warn("repeat consume message:{}", baseMessage);
                    return;
                }
                try {
                    messageConsumer.consume(baseMessage);
                    if (sequence != -1) {

                    }
                } catch (Throwable e) {
                    //执行错误,删除key,下次可以重复消费
                    ApplicationContextHelper.getBean(RedisService.class).delete(key);
                    throw e;
                }
            } else {
                messageConsumer.consume(baseMessage);
                if (sequence != -1) {

                }
            }
        } catch (Throwable e) {
            throw e;
        } finally {
            TraceUtils.removeTid();
            TraceUtils.removeSid();
            AuthContext.remove();
        }

    }

    @Override
    public void onApplicationEvent(LocalEvent event) {
        onEvent(event, -1L, false);
    }
}

后面再来将onEvent的实现逻辑。到这里基本完成了一个spring的事件消费逻辑。

远程事件

RedisMQ实现

事件类

java 复制代码
public class RedisEvent<T extends BaseEvent> implements Serializable {

    /**
     * DATA
     */
    private T data;
    /**
     * TAG
     */
    private String tag;

    private String className;

    public RedisEvent() {
    }

    public RedisEvent(T data, String tag, String className) {
        this.data = data;
        this.tag = tag;
        this.className = className;
    }

    public RedisEvent(T data, String tag) {
        this.data = data;
        this.tag = tag;
        this.className = data.getClass().getName();
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public String getTag() {
        return tag;
    }

    public void setTag(String tag) {
        this.tag = tag;
    }

    public void setClassName(String className) {
        this.className = className;
    }

    public String getClassName() {
        return className;
    }
}

Hander实现

java 复制代码
@Slf4j
public class RedisMessageListener implements MessageListener {

    public static String topic = "dragon:jjb-saas-common:redis:topic";
    private final RedisService redisService;

    public RedisMessageListener(RedisService redisService) {
        this.redisService = redisService;
    }

    @Override
    public void onMessage(Message message, byte[] pattern) {
        RedisEvent<?> redisEvent = (RedisEvent<?>) redisService.getRedisTemplate().getValueSerializer().deserialize(message.getBody());
        assert redisEvent != null;
        ConsumerType msgProcessorType = ConsumerCache.getConsumer(redisEvent.getTag());
        if (msgProcessorType == null) {
            log.warn("No message processor present for tag: " + redisEvent.getTag());
            return;
        }
        BaseEvent baseMessage = redisEvent.getData();
        TraceUtils.putSid(baseMessage.getSid());
        TraceUtils.putTid(baseMessage.getTid());
        AuthContext.set(baseMessage.getLoginUser());
        MessageConsumer<BaseEvent> messageConsumer = ApplicationContextHelper.getBean(msgProcessorType.getConsumerClass());

        try {
            //判断消息是否需要幂等消费
            if (baseMessage.isIdempotentEnabled()) {
                //计算key
                String key = DigestUtils.md5Hex(redisEvent.getTag() + "#" + baseMessage.getMsgSerialNum()
                        + "#" + baseMessage + "#" + messageConsumer.getClass().getName());
                Boolean consumed = redisService.setNx(key, 1, 24, TimeUnit.HOURS);

                //判断消息是否已经被当前服务消费过,如果消费过,则忽略当前消息
                if (Boolean.FALSE.equals(consumed)) {
                    log.warn("repeat consume message:{}", baseMessage);
                    return;
                }
                try {
                    messageConsumer.consume(baseMessage);
                } catch (Throwable e) {
                    //执行错误,删除key,下次可以重复消费
                    ApplicationContextHelper.getBean(RedisService.class).delete(key);
                    throw e;
                }
            } else {
                messageConsumer.consume(baseMessage);
            }
        } catch (Throwable e) {
            throw e;
        } finally {
            TraceUtils.removeTid();
            TraceUtils.removeSid();
            AuthContext.remove();
        }
    }
}

RocketMQ实现

hander实现

java 复制代码
@Slf4j
@RequiredArgsConstructor
public class SinkConsumer {

    private final MessageConsumerFactory messageConsumerFactory;


    @StreamListener(Sink.INPUT)
    public void inputConsumer(Message<Object> message) {
        messageConsumerFactory.doConsume(message);
    }

}
java 复制代码
@RequiredArgsConstructor
@Slf4j
public class MessageConsumerFactory {


    private final RedisService redisService;

    public void doConsume(Message<Object> message) {

        log.info("接收到的消息:{}", message);

        //String messageTag = String.valueOf(message.getHeaders().get("rocketmq_TAGS"));
        String messageTag = String.valueOf(message.getHeaders().get("ROCKET_TAGS"));
        ConsumerType msgProcessorType = ConsumerCache.getConsumer(messageTag);
        if (msgProcessorType == null) {
            log.warn("No message processor present for tag: " + messageTag);
            return;
        }
        Object payload = message.getPayload();
        String jsonData = "";
        if(payload instanceof byte[]){
            byte[] p = (byte[]) payload;
            jsonData = new String(p);
        }else {
            jsonData = String.valueOf(message.getPayload());
        }
        BaseEvent baseMessage = JSONUtil.toBean(jsonData, msgProcessorType.getMessageClass());
        TraceUtils.putSid(baseMessage.getSid());
        TraceUtils.putTid(baseMessage.getTid());
        AuthContext.set(baseMessage.getLoginUser());
        MessageConsumer<BaseEvent> messageConsumer = ApplicationContextHelper.getBean(msgProcessorType.getConsumerClass());
        if (messageConsumer == null) {
            throw new RuntimeException("No bean mapped for message :" + JSONUtil.toJsonStr(message));
        }

        try {
            //判断消息是否需要幂等消费
            if (baseMessage.isIdempotentEnabled()) {
                //计算key
                String key = DigestUtils.md5Hex(messageTag + "#" + baseMessage.getMsgSerialNum()
                        + "#" + jsonData + "#" + messageConsumer.getClass().getName());
                Boolean consumed = redisService.setNx(key, 1, 24, TimeUnit.HOURS);

                //判断消息是否已经被当前服务消费过,如果消费过,则忽略当前消息
                if (Boolean.FALSE.equals(consumed)) {
                    log.warn("repeat consume message:{}", message);
                    return;
                }
                try {
                    messageConsumer.consume(baseMessage);
                } catch (Throwable e) {
                    //执行错误,删除key,下次可以重复消费
                    redisService.delete(key);
                    log.error("mq consumer error !", e);
                    throw e;
                }
            } else {
                messageConsumer.consume(baseMessage);
            }
        } catch (Throwable e) {
            log.error("mq consumer error !", e);
            throw e;
        } finally {
            TraceUtils.removeTid();
            TraceUtils.removeSid();
            AuthContext.remove();
        }

    }
}
相关推荐
红尘散仙1 小时前
我把终端小说阅读器接上了 AI Agent:TRNovel 现在能用 skill 生成书源了
人工智能·后端·rust
卷毛的技术笔记3 小时前
告别硬编码!Spring AI Alibaba 实现 AI Agent 智能工具调用(Tool Calling)
java·人工智能·后端·python·spring·ai编程
会编程的土豆3 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
喵个咪3 小时前
GoWind Toolkit Go后端代码生成 完整全流程实战
后端·go·orm
basketball6164 小时前
Go 语言从入门到进阶:4. 数组和MAP使用方法总结
开发语言·后端·golang
qq_2518364574 小时前
SpringBoot+Vue 共享电池柜管理系统 完整实现 前后端分离项目实战 完整代码
vue.js·spring boot·后端
zhangxingchao4 小时前
AI 大模型核心六:量化、Workflow 与 Agent、多轮 RAG
前端·人工智能·后端
IT_陈寒5 小时前
Vite打包时遇到的坑,原来问题出在这里
前端·人工智能·后端
ayqy贾杰6 小时前
基层管理的三板斧,在AI时代行不通了
前端·后端·团队管理
Apifox6 小时前
Apifox 5 月更新|Postman 导入优化、Runner 支持非 root 运行、请求代码自动带鉴权
前端·后端·安全