模块(类)之间解耦利器:EventPublishSubscribeUtils 事件发布订阅工具类
如果熟悉C#语言的小伙伴们一般都会知道委托、事件的好处,只需在某个类中提前定义好公开的委托或事件(委托的特殊表现形式)变量,然后在其它类中就可以很随意的订阅该委托或事件,当委托或事件被触发执行时,会自动通知所有的订阅者进行消费处理。(观察者模式用委托来实现是最好不过了,DDD所提倡的事件驱动其根本理念也是如此),当然我这里想到的是不需要在每个类中进行定义委托或事件,而是由一个统一的中介者(即EventPublishSubscribeUtils)来提供事件的订阅及发布操作,这样各模块之间无需直接依赖,只需通过中介者完成发布通知与订阅回调即可,何乐而不为呢?
这里我先借助C#语言独有的委托类型快速实现了一个简易的EventPublishSubscribeUtils,代码如下:
/// <summary>
/// 自定义事件发布订阅回调工具类(业务解藕、关注点分离,避免互相依赖)--演示版
/// EventBus简化版,观察者模式
/// author:zuowenjun
/// </summary>
public static class EventPublishSubscribeUtils
{
private static ConcurrentDictionary<Type, EventHandler<object>> EventHandlers { get; } = new ConcurrentDictionary<Type, EventHandler<object>>();
private static void removeRegisters(ref EventHandler<object> srcEvents, EventHandler<object> removeTargetEvents)
{
var evtTypes = removeTargetEvents.GetInvocationList().Select(d => d.GetType());
var registeredEventHandlers = Delegate.Combine(srcEvents.GetInvocationList().Where(ei => evtTypes.Contains(ei.GetType())).ToArray());
srcEvents -= (EventHandler<object>)registeredEventHandlers;
}
public static void Register<T>(EventHandler<object> eventHandlers)
{
EventHandlers.AddOrUpdate(typeof(T), eventHandlers,
(t, e) =>
{
//先根据订阅委托类型匹匹配过滤掉之前已有的相同订阅,然后再重新订阅,防止重复订阅,多次执行的情况。
removeRegisters(ref e, eventHandlers);
e += eventHandlers;
return e;
});
}
public static void UnRegister<T>(EventHandler<object> eventHandlers = null)
{
Type eventMsgType = typeof(T);
if (eventHandlers == null)
{
EventHandlers.TryRemove(eventMsgType, out eventHandlers);
return;
}
var e = EventHandlers[eventMsgType];
removeRegisters(ref e, eventHandlers);
}
public static void PublishEvent<T>(T eventMsg, object sender)
{
Type eventMsgType = eventMsg.GetType();
if (EventHandlers.ContainsKey(eventMsgType))
{
EventHandlers[eventMsgType].Invoke(sender, eventMsg);
}
}
}
然后使用就比较简单了,我们只需通过EventPublishSubscribeUtils.Register注册订阅事件消息,通过EventPublishSubscribeUtils.PublishEvent发布事件通知,这样就可以让两个甚至多个不相关的模块(类)能够通过消息类型实现1对多的通讯与协同处理。使用示例代码如下:
class EventMessage
{
public string Name { get; set; }
public string Msg { get; set; }
public DateTime CreatedDate { get; set; }
}
class DemoA
{
public DemoA()
{
EventHandler<object> eventHandlers = EventCallback1;
eventHandlers += EventCallback2;
EventPublishSubscribeUtils.Register<EventMessage>(eventHandlers);
}
private void EventCallback1(object sender, object e)
{
string json = JsonConvert.SerializeObject(e);
System.Diagnostics.Debug.WriteLine($"EventCallback1=> sender:{sender},e:{json}");
}
private void EventCallback2(object sender, object e)
{
string json = JsonConvert.SerializeObject(e);
System.Diagnostics.Debug.WriteLine($"EventCallback2=> sender:{sender},e:{json}");
}
}
class DemoB
{
public void ShowMsg(string name, string msg)
{
System.Diagnostics.Debug.WriteLine($"ShowMsg=> name:{name},msg:{msg}");
var eventMsg = new EventMessage
{
Name = name,
Msg = msg,
CreatedDate = DateTime.Now
};
EventPublishSubscribeUtils.PublishEvent(eventMsg, nameof(DemoB.ShowMsg));
}
}
//main方法中使用:
var demoA = new DemoA();
var demoB = new DemoB();
demoB.ShowMsg("梦在旅途", "i love csharp and java!");
从上述示例代码中可以看出,DemoA与DemoB各为独立,互不依赖,它们都不知道有对方的存在,它们只关心业务的处理,通过执行demoB.ShowMsg方法进而触发回调demoA.EventCallback1,demoA.EventCallback2方法,是不是比起直接从DemoA中调DemoB更好呢?
c#有委托类型(方法的引用),那如果是在java中该如何实现呢?
其实同理,我们可以借助匿名内部类+匿名实现类的方式(如:函数式接口)实现与C#异曲同工的效果,同样可以实现类似的事件发布与订阅功能,如下便是采用java语言的实现EventPublishSubscribeUtils类的代码:
这个因项目需要,我特意实现了两种模式,一种支持1对多的普通方式,另一种支持1对1的订阅回调方式,有返回值。
/**
* 自定义事件发布订阅回调工具类(业务解藕、关注点分离,避免互相依赖)
* EventBus简化版,观察者模式
* <pre>
* 支持两种模式
* 1.无返回值:订阅事件消费(register)+ 发布事件消息(publishEvent/publishEventAsync)
* 2.有返回值:监听回调通知处理(listenCallback)+通知回调(notifyCallback),通过notifyMessageType+MessageChannel 即可标识唯一的一组通知回调与监听回调处理
* <pre>
* @author zuowenjun
* @date 20200310
*/
public final class EventPublishSubscribeUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(EventPublishSubscribeUtils.class);
private static final Map<Class<?>, LinkedList<Consumer<Object>>> eventConsumers = new ConcurrentHashMap<>();
private static final Map<Class<?>, ConcurrentHashMap<MessageChannel, Function<Object, Object>>> callbackFuncs = new ConcurrentHashMap<>();
private EventPublishSubscribeUtils() {
}
/**
* 注册事件回调消费者
* 用法:EventSubscribeConsumeUtils.register(this::xxxx方法) 或lambda表达式
* 注意:若回调方法添加了事务注解,则应指派其代理对象的方法来完成回调,如:
* EventSubscribeConsumeUtils.register((xxxService)SpringUtils.getBean(this.class)::xxxx方法)
*
* @param eventConsumer
*/
public static void register(Class<?> eventMessageType, Consumer<Object> eventConsumer) {
if (eventConsumer == null) {
return;
}
LinkedList<Consumer<Object>> eventConsumerItems = null;
if (!eventConsumers.containsKey(eventMessageType)) {
eventConsumers.putIfAbsent(eventMessageType, new LinkedList<>());
}
eventConsumerItems = eventConsumers.get(eventMessageType);
eventConsumerItems.add(eventConsumer);
}
/**
* 取消订阅回调
*
* @param eventMessageType
* @param eventConsumer
*/
public static void unRegister(Class<?> eventMessageType, Consumer<Object> eventConsumer) {
if (!eventConsumers.containsKey(eventMessageType)) {
return;
}
LinkedList<Consumer<Object>> eventConsumerItems = eventConsumers.get(eventMessageType);
int eventConsumerIndex = eventConsumerItems.indexOf(eventConsumer);
if (eventConsumerIndex == -1) {
return;
}
eventConsumerItems.remove(eventConsumerIndex);
}
/**
* 发布事件,同步触发执行回调事件消费者方法(存在阻塞等待),即事件消息生产者
* 用法:在需要触发事件消息回调时调用,如:publishEvent(eventMessage);
*
* @param eventMessage
*/
public static <T> void publishEvent(T eventMessage) {
Class<?> eventMessageType = eventMessage.getClass();
if (!eventConsumers.containsKey(eventMessageType)) {
return;
}
LOGGER.info("事件已发布,正在执行通知消费:{}", JSONObject.toJSONString(eventMessage));
for (Consumer<Object> eventConsumer : eventConsumers.get(eventMessageType)) {
try {
eventConsumer.accept(eventMessage);
} catch (Exception ex) {
LOGGER.error("eventConsumer.accept error:{},eventMessageType:{},eventMessage:{}",
ex, eventMessageType, JSONObject.toJSONString(eventMessage));
}
}
}
/**
* 发布事件,异步触发执行回调事件消费者方法(异步非阻塞),即事件消息生产者
* 用法:在需要触发事件消息回调时调用,如:publishEventAsync(eventMessage);
*
* @param eventMessage
*/
public static <T> void publishEventAsync(final T eventMessage) {
Executor asyncTaskExecutor = (Executor) SpringUtils.getBean("asyncTaskExecutor");
asyncTaskExecutor.execute(() -> {
publishEvent(eventMessage);
});
}
/**
* 监听回调处理(需要有返回值),即有返回值的回调消费者
*
* @param notifyMessageType
* @param messageChannel
* @param callbackFunc
*/
public static void listenCallback(Class<?> notifyMessageType, MessageChannel messageChannel, Function<Object, Object> callbackFunc) {
if (!callbackFuncs.containsKey(notifyMessageType)) {
callbackFuncs.putIfAbsent(notifyMessageType, new ConcurrentHashMap<>());
}
Map<MessageChannel, Function<Object, Object>> functionMap = callbackFuncs.get(notifyMessageType);
if (!functionMap.containsKey(messageChannel)) {
functionMap.putIfAbsent(messageChannel, callbackFunc);
} else {
LOGGER.error("该通知消息类型:{}+消息通道:{},已被订阅监听,重复订阅监听无效!", notifyMessageType.getSimpleName(), messageChannel.getDescription());
}
}
/**
* 通知回调(同步等待获取监听回调的处理结果),即生产者
*
* @param notifyMessage
* @param messageChannel
* @param <R>
* @return
*/
@SuppressWarnings("unchecked")
public static <R> R notifyCallback(Object notifyMessage, MessageChannel messageChannel) {
Class<?> notifyMessageType = notifyMessage.getClass();
Map<MessageChannel, Function<Object, Object>> functionMap = callbackFuncs.getOrDefault(notifyMessageType, null);
if (functionMap != null) {
Function<Object, Object> callbackFunction = functionMap.getOrDefault(messageChannel, null);
if (callbackFunction != null) {
LOGGER.info("通知回调消息已发布,正在执行回调处理:{},messageChannel:[{}]", JSONObject.toJSONString(notifyMessage), messageChannel.getDescription());
Object result = callbackFunction.apply(notifyMessage);
try {
return (R) result;
} catch (ClassCastException castEx) {
throw new ClassCastException(String.format("监听回调处理后返回值实际类型与发布通知回调待接收的值预期类型不一致,导致类型转换失败:%s," +
"请确保notifyCallback与listenCallback针对通知消息类型:%s+消息通道:%s返回值类型必需一致。",
castEx.getMessage(), notifyMessageType.getSimpleName(), messageChannel.getDescription()));
}
}
}
return null;
}
}
当然如果需要实现1对1的通讯,除了指定消息类型外,还需要指定消息通讯通道(即:唯一标识),目的是可以实现同一种消息类型,支持不同的点对点的处理。
/**
* 自定义消息通道
* 作用:用于识别同一个消息类型下不同的监听回调者(notifyMessage+messageChannel 即可标识唯一的一组通知回调[生产者]与监听回调[消费者])
* @author zuowenjun
* @date 2020-03-31
*/
public enum MessageChannel {
None("无效"),
MSG_A("测试消息A"),
;
private String description;
MessageChannel(String description) {
this.description=description;
}
public String getDescription() {
return description;
}
}
使用方法示例代码如下:
@Service
public class DemoAService {
private static final Logger LOGGER = LoggerFactory.getLogger(DemoAService.class);
public void showMsg(String name, String msg) {
System.out.printf("【%1$tF %1$tT.%1$tL】hello!%s,DemoAService showMsg:%s %n", new Date(), name, msg);
EventMessage eventMessage = new EventMessage();
eventMessage.setName("aaa");
eventMessage.setMsg("test");
eventMessage.setCreatedDate(new Date());
EventPublishSubscribeUtils.publishEvent(eventMessage);
String msgJsonStr = EventPublishSubscribeUtils.notifyCallback(eventMessage, MessageChannel.MSG_A);
System.out.printf("【%1$tF %1$tT.%1$tL】DemoAService showMsg notifyCallback json result:%2$s %n", new Date(), msgJsonStr);
}
}
@Service
public class DemoBService {
@PostConstruct
public void init(){
//订阅消费,无返回值,支持1对多,即:同一个消息类型可同时被多个消费者订阅
EventPublishSubscribeUtils.register(EventMessage.class,this::showFinishedMsg);
//订阅监听回调,有返回值,只能1对1
EventPublishSubscribeUtils.listenCallback(EventMessage.class, MessageChannel.MSG_A,this::getMsgCallbak);
}
private void showFinishedMsg(Object eventMsg){
EventMessage eventMessage=(EventMessage)eventMsg;
System.out.printf("【%1$tF %1$tT.%1$tL】%s,receive msg:%s doing...%n",
eventMessage.getCreatedDate(),eventMessage.getName(),eventMessage.getMsg());
//模拟逻辑处理
try {
Thread.sleep(500L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("【%1$tF %1$tT.%1$tL】%s,do finished!!!%n",new Date(),eventMessage.getName());
}
private String getMsgCallbak(Object eventMsg){
EventMessage eventMessage=(EventMessage)eventMsg;
eventMessage.setMsg(eventMessage.getMsg()+"--callback added!");
eventMessage.setCreatedDate(new Date());
System.out.printf("【%1$tF %1$tT.%1$tL】%s,do msg callback!!!%n",new Date(),eventMessage.getName());
return JSONObject.toJSONString(eventMessage);
}
}
//在某个入口的service中调用(如需演示可定义实现ApplicationRunner的run方法,在run方法中执行调用即可):
@Autowired
private DemoAService demoAService;
demoAService.showMsg("zwj","i love java 2020!");
如上代码所示,我们借助于EventPublishSubscribeUtils,解耦了两个Service Bean之间的依赖,避免了循环依赖的问题,去掉了之前为了解决循环依赖而使用@Lazy注解的方式,更易于扩展与更改。其实Spring底层也使用了类似的Event机制,说明这种方式还是有合适的用武之地的。
这里我通过简单的关系图来对比未引用EventPublishSubscribeUtils前与引用后的区别,大家可以感受一下哪种更方便:
之前:
之后:
最后,关于业务解耦,分清业务边界,我个人认为跨进程通讯使用MQ,同进程跨多模块(类,或者说跨多业务边界)可使用Event事件驱动思路来解决。大家觉得如何呢?如果有更好的方案欢迎评论交流,谢谢。
模块(类)之间解耦利器:EventPublishSubscribeUtils 事件发布订阅工具类的更多相关文章
- 使用MediatR重构单体应用中的事件发布/订阅
标题:使用MediatR重构单体应用中的事件发布/订阅 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p/10640280.html 源代码:https ...
- ASP.NET Core中实现单体程序的事件发布/订阅
标题:ASP.NET Core中实现单体程序的事件发布/订阅 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p/10468058.html 项目源代码: ...
- guava的事件发布订阅功能
事件的重要性,不用说很重要,在很多时候我们做完一个操作的时候,需要告知另外一个对象让他执行相应操作,比如当用户注册成功的时候,需要抛出一个注册成功的事件,那么有监听器捕获到这个事件,完成后续用户信息初 ...
- Blazor+Dapr+K8s微服务之事件发布订阅
我们要实现的是:在blazorweb服务中发布一个事件,并传递事件参数,然后在serviceapi1服务中订阅该事件,接收到blazorweb服务中发布的事件和参数. 1 在blazo ...
- spring#事件发布订阅
1. 如果在应用中发生了某些事件,事件会被拦截和处理就好了,这样就有了很大的灵活性,至少代码不会紧密的耦合在一起, 代码的解耦就是业务的解耦,业务A的代码不用手动的调用业务B的代码,业务B只需要监听相 ...
- javascript 自定义事件 发布-订阅 模式 Event
* javascript自定义事件 var myEvent = document.createEvent("Event"); myEvent.initEvent("myE ...
- 封装一个简单好用的打印Log的工具类And快速开发系列 10个常用工具类
快速开发系列 10个常用工具类 http://blog.csdn.net/lmj623565791/article/details/38965311 ------------------------- ...
- js 事件发布订阅销毁
在vue中 通过$on订阅事件,通过$emit触发事件以此可用来事件跨组件传值等功能,但是有个弊端就是通过这种方式订阅的事件可能会触发多次. 特别是通过$on订阅的事件中如果有http请求,将会造成触 ...
- Redis的消息订阅/发布 Utils工具类
package cn.cicoding.utils; import org.json.JSONException; import org.json.JSONObject; import redis.c ...
随机推荐
- Powershell 输出信息过多,结尾显示省略号
有时候我们通过powershell指令去查询某些信息时,因为输出结果过多,导致一部分重要信息被省略号代替,如下图 面对这种情况无论是 |fl 还是 out-file 亦或是 export-csv都无 ...
- developerWorks 中文社区
https://www.ibm.com/developerworks/community/groups/service/html/communityview?communityUuid=3302cc3 ...
- 文件密码忘记了怎么办,教你如何使用Python破解密码
前言 文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理. 作者:轻松学编程小梁 PS:如有需要Python学习资料的小伙伴可以加点击下 ...
- 猜数字和飞机大战(Python零基础入门)
前言 最近有很多零基础初学者问我,有没有适合零基础学习案例,毕竟零基础入门的知识点是非常的枯燥乏味的,如果没有实现效果展示出来,感觉学习起来特别的累,今天就给大家介绍两个零基础入门的基础案例:猜数字游 ...
- F. Count Prime Pairs
单点时限: 2.0 sec 内存限制: 512 MB 对于数组a,如果i≠j并且ai+aj是一个质数,那么我们就称(i,j)为质数对,计算数组中质数对的个数. 输入格式 第一行输入一个n,表示数组的长 ...
- vue2.x学习笔记(四)
接着前面的内容:https://www.cnblogs.com/yanggb/p/12563162.html. 模板语法 vue使用了基于html的模板语法,允许开发者声明式地将dom绑定到底层vue ...
- ApiPost V3创事记:一个痛并快乐着的创业故事
前言 无论是对于国家,还是对于我们个人,2020年4月,是注定是一个不同往年的4月.一场突如起来的疫情打破了我们原来的生活曲线,让我们知道了什么是苦难,什么是团结,什么是坚持,什么是胜利. 一.大幕开 ...
- pytorch 手写数字识别项目 增量式训练
dataset.py ''' 准备数据集 ''' import torch from torch.utils.data import DataLoader from torchvision.datas ...
- 百度paddlepaddle学习体会
一个偶然从微信公众号中刷到了<python小白逆袭A1大神>的文章,让我不经意的邂逅了飞桨(paddlepaddle),通过加入飞桨训练营一周的学习.实践,对飞桨有了很多的了解(飞桨官网: ...
- 9个小技巧让你的 if else看起来更优雅
if else 是我们写代码时,使用频率最高的关键词之一,然而有时过多的 if else 会让我们感到脑壳疼,例如下面这个伪代码: 是不是很奔溃?虽然他是伪代码,并且看起来也很夸张,但在现实中,当我们 ...