如果熟悉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 事件发布订阅工具类的更多相关文章

  1. 使用MediatR重构单体应用中的事件发布/订阅

    标题:使用MediatR重构单体应用中的事件发布/订阅 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p/10640280.html 源代码:https ...

  2. ASP.NET Core中实现单体程序的事件发布/订阅

    标题:ASP.NET Core中实现单体程序的事件发布/订阅 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p/10468058.html 项目源代码: ...

  3. guava的事件发布订阅功能

    事件的重要性,不用说很重要,在很多时候我们做完一个操作的时候,需要告知另外一个对象让他执行相应操作,比如当用户注册成功的时候,需要抛出一个注册成功的事件,那么有监听器捕获到这个事件,完成后续用户信息初 ...

  4. Blazor+Dapr+K8s微服务之事件发布订阅

    我们要实现的是:在blazorweb服务中发布一个事件,并传递事件参数,然后在serviceapi1服务中订阅该事件,接收到blazorweb服务中发布的事件和参数. 1         在blazo ...

  5. spring#事件发布订阅

    1. 如果在应用中发生了某些事件,事件会被拦截和处理就好了,这样就有了很大的灵活性,至少代码不会紧密的耦合在一起, 代码的解耦就是业务的解耦,业务A的代码不用手动的调用业务B的代码,业务B只需要监听相 ...

  6. javascript 自定义事件 发布-订阅 模式 Event

    * javascript自定义事件 var myEvent = document.createEvent("Event"); myEvent.initEvent("myE ...

  7. 封装一个简单好用的打印Log的工具类And快速开发系列 10个常用工具类

    快速开发系列 10个常用工具类 http://blog.csdn.net/lmj623565791/article/details/38965311 ------------------------- ...

  8. js 事件发布订阅销毁

    在vue中 通过$on订阅事件,通过$emit触发事件以此可用来事件跨组件传值等功能,但是有个弊端就是通过这种方式订阅的事件可能会触发多次. 特别是通过$on订阅的事件中如果有http请求,将会造成触 ...

  9. Redis的消息订阅/发布 Utils工具类

    package cn.cicoding.utils; import org.json.JSONException; import org.json.JSONObject; import redis.c ...

随机推荐

  1. AJ学IOS 之ipad开发qq空间项目横竖屏幕适配

    AJ分享,必须精品 一:效果图 先看效果 二:结构图 如图所示: 其中用到了UIView+extension分类 Masonry第三方框架做子控制器的适配 NYHomeViewController对应 ...

  2. 如何用 Python 绘制玫瑰图等常见疫情图

    新冠疫情已经持续好几个月了,目前,我国疫情已经基本控制住了,而欧美国家正处于爆发期,我们会看到很多网站都提供了多种疫情统计图,今天我们使用 Python 的 pyecharts 框架来绘制一些比较常见 ...

  3. T - Nash Matrix CodeForces - 1316D

    题意: 输入n行数,没行由2*n个数,表示一个坐标(x,y). 如果x和y==-1表示从该点(i,j)出发,按照构造的前移动不会停下. 否则就要到点(x,y)处停下. 题解: 首先处理-1  枚举每个 ...

  4. Rank of Tetris 杭电 拓扑排序加并查集

    自从Lele开发了Rating系统,他的Tetris事业更是如虎添翼,不久他遍把这个游戏推向了全球. 为了更好的符合那些爱好者的喜好,Lele又想了一个新点子:他将制作一个全球Tetris高手排行榜, ...

  5. 关于C++线程池的实现的思考

    今天突然对前些日子一直很疑惑的c++线程池有了新的想法.其实所谓的线程池无非就是两个技术点,一个,多线程,指工作线程和主线程分离,或者说数据接收和处理分两个线程,一般就是讲需要运行的函数放到子线程执行 ...

  6. CKEditor与定制

    一 开始使用 官网 基本示例: 搭建服务器(这里使用apache) 下载standard的ckeditor解压放在apache的htdocs的目录下 在htdoc下面新建index.html,写入代码 ...

  7. 2.hover的使用

    1. 自身的hover div :hover{  :hover前要有空格  } 2.hover指向子元素 father:hover .childer {   :hover前不能有空格   } 3.ho ...

  8. TeamViewer11 万全免费

    下载地址:百度网盘 c4xm TeamViewer 是一款简单易用且功能强大的远程控制软件,它能穿越内网,摆脱路由器或防火墙的限制,任何一方都不需要拥有固定IP地址.让不懂技术的朋友也能远程控制电脑, ...

  9. 磁盘性能测试工具之fio

    fio是测试磁盘性能的一个非常好的工具,用来对硬件进行压力测试和验证. 注意事项 CentOS 6.5等较老版本的操作系统用fdisk创建分区时,默认为非4KB对齐选择初始磁柱编号,对性能有较大的影响 ...

  10. Java的数组索引问题

    /* 数组操作的两个常见小问题: ArrayIndexOutOfBoundsException:数组索引越界异常 原因:你访问了不存在的索引. NullPointerException:空指针异常 原 ...