diff --git a/pom.xml b/pom.xml
index 29e5cacfae026346e79ac7b6e058665615f361e4..c90dc3c319dda5ce651faa60b4c5d5e4010287fc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -15,6 +15,7 @@
rec-spring-boot-starter
rec-socket
rec-common
+ rec-spi
diff --git a/rec-admin/rec-admin-biz/pom.xml b/rec-admin/rec-admin-biz/pom.xml
index af101c55af5a67de97aca352e5b78f31476e2b2b..aeda1db31e2692de12a0b42465365f1fa3adf227 100644
--- a/rec-admin/rec-admin-biz/pom.xml
+++ b/rec-admin/rec-admin-biz/pom.xml
@@ -22,6 +22,11 @@
rec-admin-dal
${revision}
+
+ cn.icanci.rec
+ rec-spi
+ ${revision}
+
org.mapstruct
mapstruct
diff --git a/rec-admin/rec-admin-biz/src/main/java/cn/icanci/rec/admin/biz/config/EventConfig.java b/rec-admin/rec-admin-biz/src/main/java/cn/icanci/rec/admin/biz/config/EventConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..67023c01462b2e98cc4ff44465379bbf20bdd9a3
--- /dev/null
+++ b/rec-admin/rec-admin-biz/src/main/java/cn/icanci/rec/admin/biz/config/EventConfig.java
@@ -0,0 +1,38 @@
+package cn.icanci.rec.admin.biz.config;
+
+import cn.icanci.rec.spi.event.DefaultEventDispatcher;
+import cn.icanci.rec.spi.event.EventDispatcher;
+
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author icanci
+ * @since 1.0 Created in 2022/11/11 18:05
+ */
+@Configuration
+public class EventConfig implements ApplicationContextAware {
+ /**
+ * Spring 上下文
+ */
+ private ApplicationContext context;
+
+ /**
+ * 事件分发器
+ *
+ * @return 返回事件分发器
+ */
+ @Bean("eventDispatcher")
+ public EventDispatcher eventDispatcher() {
+ return new DefaultEventDispatcher();
+ }
+
+ @Override
+ public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+ context = applicationContext;
+ }
+
+}
diff --git a/rec-spi/pom.xml b/rec-spi/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..511f9dd725fda80049b6e09c4d04be7eb6e13afd
--- /dev/null
+++ b/rec-spi/pom.xml
@@ -0,0 +1,26 @@
+
+
+
+ rec-parent
+ cn.icanci.rec
+ ${revision}
+
+ 4.0.0
+
+ rec-spi
+
+
+ 8
+ 8
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
\ No newline at end of file
diff --git a/rec-spi/src/main/java/cn/icanci/rec/spi/event/AbstractEventDispatcher.java b/rec-spi/src/main/java/cn/icanci/rec/spi/event/AbstractEventDispatcher.java
new file mode 100644
index 0000000000000000000000000000000000000000..bc21f85e5a230f9f7b086be98447bf1c1fa618e8
--- /dev/null
+++ b/rec-spi/src/main/java/cn/icanci/rec/spi/event/AbstractEventDispatcher.java
@@ -0,0 +1,82 @@
+package cn.icanci.rec.spi.event;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executor;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+
+/**
+ * 事件分发抽象处理器
+ *
+ * @author icanci
+ * @since 1.0 Created in 2022/11/11 18:01
+ */
+public abstract class AbstractEventDispatcher implements EventDispatcher, ApplicationContextAware {
+ /**
+ * Spring 容器
+ */
+ protected ApplicationContext applicationContext;
+
+ /**
+ * 事件监听器列表
+ */
+ protected Map, List> eventListMap = new ConcurrentHashMap<>();
+
+ /**
+ * 监听器
+ */
+ protected List listeners = new LinkedList();
+
+ /**
+ * 注册事件类型
+ */
+ protected Set> eventClasses = new HashSet<>();
+
+ /**
+ * 排序器
+ */
+ protected static final ListenerComparator LISTENER_COMPARATOR = new ListenerComparator();
+
+ /**
+ * Asynchronous executor
+ */
+ protected Executor taskExecutor;
+
+ /**锁*/
+ private final ReadWriteLock lock = new ReentrantReadWriteLock();
+
+ @Override
+ public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+ this.applicationContext = applicationContext;
+ try {
+ lock.writeLock().lock();
+ register();
+ } finally {
+ lock.writeLock().unlock();
+ }
+ }
+
+ /**
+ * 注册事件和监听器
+ */
+ protected abstract void register();
+
+ /**
+ * 获取事件执行线程池
+ *
+ * @return 事件执行线程池
+ */
+ protected abstract Executor getTaskExecutor();
+
+ /**
+ * 设置任务执行器
+ *
+ * @param taskExecutor 事件执行线程池
+ */
+ public abstract void setTaskExecutor(Executor taskExecutor);
+}
diff --git a/rec-spi/src/main/java/cn/icanci/rec/spi/event/AnonymityEventHandler.java b/rec-spi/src/main/java/cn/icanci/rec/spi/event/AnonymityEventHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..6810aa52f5dfd13f53716642e72d8d6494fc2a32
--- /dev/null
+++ b/rec-spi/src/main/java/cn/icanci/rec/spi/event/AnonymityEventHandler.java
@@ -0,0 +1,25 @@
+package cn.icanci.rec.spi.event;
+
+/**
+ * 事件执行
+ *
+ * @author icanci
+ * @since 1.0 Created in 2022/11/11 18:01
+ */
+public class AnonymityEventHandler implements Runnable {
+ /** 事件 */
+ private BaseEvent event;
+
+ /** 事件监听器 */
+ private BaseEventListener listener;
+
+ public AnonymityEventHandler(BaseEvent event, BaseEventListener listener) {
+ this.event = event;
+ this.listener = listener;
+ }
+
+ @Override
+ public void run() {
+ listener.onEvent(event);
+ }
+}
diff --git a/rec-spi/src/main/java/cn/icanci/rec/spi/event/BaseEvent.java b/rec-spi/src/main/java/cn/icanci/rec/spi/event/BaseEvent.java
new file mode 100644
index 0000000000000000000000000000000000000000..2f52fdcb95539e0559dc0c6ced5e415f91a0f0c8
--- /dev/null
+++ b/rec-spi/src/main/java/cn/icanci/rec/spi/event/BaseEvent.java
@@ -0,0 +1,18 @@
+package cn.icanci.rec.spi.event;
+
+import java.util.EventObject;
+
+/**
+ * @author icanci
+ * @since 1.0 Created in 2022/11/11 18:01
+ */
+public class BaseEvent extends EventObject {
+
+ public BaseEvent(Object source) {
+ super(source);
+ }
+
+ public BaseEvent() {
+ super("BaseEvent");
+ }
+}
diff --git a/rec-spi/src/main/java/cn/icanci/rec/spi/event/BaseEventListener.java b/rec-spi/src/main/java/cn/icanci/rec/spi/event/BaseEventListener.java
new file mode 100644
index 0000000000000000000000000000000000000000..6dd2fa3e227b0204d67f3a2905e6896bc00df3d0
--- /dev/null
+++ b/rec-spi/src/main/java/cn/icanci/rec/spi/event/BaseEventListener.java
@@ -0,0 +1,47 @@
+package cn.icanci.rec.spi.event;
+
+import java.util.EventListener;
+
+/**
+ * @author icanci
+ * @since 1.0 Created in 2022/11/11 18:01
+ */
+public abstract class BaseEventListener implements EventListener {
+
+ /**
+ * 接受Event方法
+ *
+ * @param event 事件
+ */
+ protected abstract void event(T event);
+
+ /**
+ * 处理 Event
+ *
+ * @param event 事件
+ */
+ public final void onEvent(T event) {
+ if (isSupport(event)) {
+ event(event);
+ }
+ }
+
+ /**
+ * 是否支持
+ *
+ * @param event event
+ * @return 返回是否支持
+ */
+ protected boolean isSupport(T event) {
+ return true;
+ }
+
+ /**
+ * 监听器执行的顺序 数值越小,优先级越高
+ *
+ * @return 返回顺序
+ */
+ protected int order() {
+ return 0;
+ }
+}
diff --git a/rec-spi/src/main/java/cn/icanci/rec/spi/event/DefaultEventDispatcher.java b/rec-spi/src/main/java/cn/icanci/rec/spi/event/DefaultEventDispatcher.java
new file mode 100644
index 0000000000000000000000000000000000000000..a4c138623a642d3f2956a50d859bc6d3aa4c7b6c
--- /dev/null
+++ b/rec-spi/src/main/java/cn/icanci/rec/spi/event/DefaultEventDispatcher.java
@@ -0,0 +1,159 @@
+package cn.icanci.rec.spi.event;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+/**
+ * 事件分发器默认实现
+ *
+ * @author icanci
+ * @since 1.0 Created in 2022/11/11 18:01
+ */
+public class DefaultEventDispatcher extends AbstractEventDispatcher {
+
+ /**
+ * 注册事件和监听器
+ */
+ @Override
+ protected void register() {
+ String[] names = applicationContext.getBeanDefinitionNames();
+ for (String name : names) {
+ Object bean = applicationContext.getBean(name);
+ if (bean instanceof BaseEvent) {
+ eventClasses.add(bean.getClass());
+ }
+ if (bean instanceof BaseEventListener) {
+ listeners.add((BaseEventListener) bean);
+ }
+ }
+
+ postEventListenerMapper();
+ }
+
+ /**
+ * 映射mapper
+ */
+ private void postEventListenerMapper() {
+ if (listeners.isEmpty() || eventClasses.isEmpty()) {
+ return;
+ }
+
+ for (Class> eventClass : eventClasses) {
+ for (BaseEventListener listener : listeners) {
+ if (!isSupport(eventClass, listener)) {
+ continue;
+ }
+ List baseEventListeners = eventListMap.get(eventClass);
+ if (baseEventListeners == null || baseEventListeners.isEmpty()) {
+ baseEventListeners = new ArrayList<>();
+ }
+ baseEventListeners.add(listener);
+ eventListMap.put(eventClass, baseEventListeners);
+ }
+ }
+
+ // 清空 help gc
+ listeners = null;
+ eventClasses = null;
+
+ // 排序
+ Collection> listCollection = eventListMap.values();
+ for (List baseEventListeners : listCollection) {
+ baseEventListeners.sort(LISTENER_COMPARATOR);
+ }
+ }
+
+ /**
+ * 是否支持加入到listener
+ *
+ * @param eventClass eventClass
+ * @param listener listener
+ * @return 是否支持加入到listener
+ */
+ private boolean isSupport(Class> eventClass, BaseEventListener listener) {
+ Type type = listener.getClass().getGenericSuperclass();
+ ParameterizedType parameterizedType = (ParameterizedType) type;
+ Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
+ Type actualTypeArgument = actualTypeArguments[0];
+ return eventClass == actualTypeArgument;
+ }
+
+ /**
+ * 获取任务执行器
+ *
+ * @return 任务执行器
+ */
+ @Override
+ protected Executor getTaskExecutor() {
+ if (taskExecutor == null) {
+ taskExecutor = Executors.newCachedThreadPool(new NamedThreadFactory("beedong-event", true));
+ }
+ return taskExecutor;
+ }
+
+ /**
+ * 设置任务执行器
+ *
+ * @param taskExecutor taskExecutor
+ */
+ public void setTaskExecutor(Executor taskExecutor) {
+ this.taskExecutor = taskExecutor;
+ }
+
+ /**
+ * 移除监听器
+ *
+ * @param baseEvent baseEvent 事件
+ * @param baseEventListener baseEventListener 监听器
+ */
+ public void remove(BaseEvent baseEvent, BaseEventListener baseEventListener) {
+ List listeners = eventListMap.get(baseEvent.getClass());
+ if (listeners == null || listeners.isEmpty()) {
+ return;
+ }
+ listeners.remove(baseEventListener);
+ }
+
+ /**
+ * 分发事件
+ *
+ * @param baseEvent baseEvent
+ */
+ @Override
+ public void fire(BaseEvent baseEvent) {
+ fire(baseEvent, true);
+ }
+
+ /**
+ * 分发事件
+ *
+ * @param baseEvent baseEvent
+ * @param sync 是否同步发送
+ */
+ @Override
+ public void fire(BaseEvent baseEvent, boolean sync) {
+ List listeners = eventListMap.get(baseEvent.getClass());
+ if (listeners == null || listeners.isEmpty()) {
+ return;
+ }
+
+ // 同步发送
+ if (sync) {
+ for (BaseEventListener listener : listeners) {
+ listener.onEvent(baseEvent);
+ }
+ } else { // 异步发送
+ Executor taskExecutor = getTaskExecutor();
+ for (BaseEventListener listener : listeners) {
+ taskExecutor.execute(new AnonymityEventHandler(baseEvent, listener));
+ }
+ }
+
+ }
+
+}
diff --git a/rec-spi/src/main/java/cn/icanci/rec/spi/event/EventDispatcher.java b/rec-spi/src/main/java/cn/icanci/rec/spi/event/EventDispatcher.java
new file mode 100644
index 0000000000000000000000000000000000000000..c8921d52d80cd8109693a9733c18bec7d37b8b20
--- /dev/null
+++ b/rec-spi/src/main/java/cn/icanci/rec/spi/event/EventDispatcher.java
@@ -0,0 +1,24 @@
+package cn.icanci.rec.spi.event;
+
+/**
+ * 事件分发器抽象
+ *
+ * @author icanci
+ * @since 1.0 Created in 2022/11/11 18:01
+ */
+public interface EventDispatcher {
+ /**
+ * 分发事件 同步发送
+ *
+ * @param event event
+ */
+ void fire(final BaseEvent event);
+
+ /**
+ * 分发事件
+ *
+ * @param event event
+ * @param sync 是否同步发送
+ */
+ void fire(final BaseEvent event, boolean sync);
+}
diff --git a/rec-spi/src/main/java/cn/icanci/rec/spi/event/ListenerComparator.java b/rec-spi/src/main/java/cn/icanci/rec/spi/event/ListenerComparator.java
new file mode 100644
index 0000000000000000000000000000000000000000..538715647eddec7ea174969f5ca0c939a0679937
--- /dev/null
+++ b/rec-spi/src/main/java/cn/icanci/rec/spi/event/ListenerComparator.java
@@ -0,0 +1,14 @@
+package cn.icanci.rec.spi.event;
+
+import java.util.Comparator;
+
+/**
+ * @author icanci
+ * @since 1.0 Created in 2022/11/11 18:01
+ */
+public class ListenerComparator implements Comparator {
+ @Override
+ public int compare(BaseEventListener o1, BaseEventListener o2) {
+ return o1.order() - o2.order();
+ }
+}
diff --git a/rec-spi/src/main/java/cn/icanci/rec/spi/event/NamedThreadFactory.java b/rec-spi/src/main/java/cn/icanci/rec/spi/event/NamedThreadFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..82cb91056adfedbcd64eaf56e1a7db5451f9ccb4
--- /dev/null
+++ b/rec-spi/src/main/java/cn/icanci/rec/spi/event/NamedThreadFactory.java
@@ -0,0 +1,34 @@
+package cn.icanci.rec.spi.event;
+
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * @author icanci
+ * @since 1.0 Created in 2022/11/11 18:01
+ */
+public class NamedThreadFactory implements ThreadFactory {
+
+ private final AtomicInteger threadNum = new AtomicInteger(1);
+
+ private final String prefix;
+
+ private final boolean daemon;
+
+ private final ThreadGroup group;
+
+ public NamedThreadFactory(String prefix, boolean daemon) {
+ this.prefix = prefix + "-thread-";
+ this.daemon = daemon;
+ SecurityManager s = System.getSecurityManager();
+ group = (s == null) ? Thread.currentThread().getThreadGroup() : s.getThreadGroup();
+ }
+
+ @Override
+ public Thread newThread(Runnable runnable) {
+ String name = prefix + threadNum.getAndIncrement();
+ Thread ret = new Thread(group, runnable, name, 0);
+ ret.setDaemon(daemon);
+ return ret;
+ }
+}
diff --git a/rec-spi/src/main/java/cn/icanci/rec/spi/package-info.java b/rec-spi/src/main/java/cn/icanci/rec/spi/package-info.java
new file mode 100644
index 0000000000000000000000000000000000000000..1acebcd53cdfd0ac8e609f3d8675739d6fff9bde
--- /dev/null
+++ b/rec-spi/src/main/java/cn/icanci/rec/spi/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * @author icanci
+ * @since 1.0 Created in 2022/11/11 18:01
+ */
+package cn.icanci.rec.spi;
\ No newline at end of file