看eShopOnContainers学一个EventBus
最近在看微软eShopOnContainers 项目,看到事件总线觉得不错,和大家分享一下
看完此文你将获得什么?
- eShop中是如何设计事件总线的
- 实现一个InMemory事件总线eShop中是没有InMemory实现的,这算是一个小小小的挑战
发布订阅模式
发布订阅模式可以让应用程序组件之间解耦,这是我们使用这种模式最重要的理由之一,如果你完全不知道这个东西,建议你先通过搜索引擎了解一下这种模式,网上的资料很多这里就不再赘述了。
eShop中的EventBus就是基于这种模式的发布/订阅。
发布订阅模式核心概念有三个:发布者、订阅者、调度中心,这些概念在消息队列中就是生产者、消费者、MQ实例。
在eShop中有两个EventBus的实现:
- 基于RabbitMq的
EventBusRabbitMQ
- 基于AzureServiceBus的
EventBusServiceBus
。
从IEventBus
开始
先来看一看,所有EventBus的接口IEventBus
public interface IEventBus
{
void Publish(IntegrationEvent @event);
void Subscribe<T, TH>()
where T : IntegrationEvent
where TH : IIntegrationEventHandler<T>;
void SubscribeDynamic<TH>(string eventName)
where TH : IDynamicIntegrationEventHandler;
void UnsubscribeDynamic<TH>(string eventName)
where TH : IDynamicIntegrationEventHandler;
void Unsubscribe<T, TH>()
where TH : IIntegrationEventHandler<T>
where T : IntegrationEvent;
}
嗯,乍一看看是有点眼晕的,仔细看它的核心功能只有三个:
- Publish 发布
- Subscribe 订阅
- Unsubscribe 取消订阅
这对应着发布订阅模式的基本概念,不过对于事件总线的接口添加了许多约束:
- 发布的内容(消息)必须是
IntegrationEvent
及其子类 - 订阅事件必须指明要订阅事件的类型,并附带处理器类型
- 处理器必须是
IIntegrationEventHandler
的实现类
Ok,看到这里先不要管Dynamic
相关的方法,然后记住这个两个关键点:
- 事件必须继承
IntegrationEvent
- 处理器必须实现
IIntegrationEventHandler<T>
且T
是IntegrationEvent
子类
另外,看下 IntegrationEvent
有什么
public class IntegrationEvent
{
public IntegrationEvent()
{
Id = Guid.NewGuid();
CreationDate = DateTime.UtcNow;
}
public Guid Id { get; }
public DateTime CreationDate { get; }
}
IEventBusSubscriptionsManager是什么
public interface IEventBusSubscriptionsManager
{
bool IsEmpty { get; }
event EventHandler<string> OnEventRemoved;
void AddDynamicSubscription<TH>(string eventName)
where TH : IDynamicIntegrationEventHandler;
void AddSubscription<T, TH>()
where T : IntegrationEvent
where TH : IIntegrationEventHandler<T>;
void RemoveSubscription<T, TH>()
where TH : IIntegrationEventHandler<T>
where T : IntegrationEvent;
void RemoveDynamicSubscription<TH>(string eventName)
where TH : IDynamicIntegrationEventHandler;
bool HasSubscriptionsForEvent<T>() where T : IntegrationEvent;
bool HasSubscriptionsForEvent(string eventName);
Type GetEventTypeByName(string eventName);
void Clear();
IEnumerable<SubscriptionInfo> GetHandlersForEvent<T>() where T : IntegrationEvent;
IEnumerable<SubscriptionInfo> GetHandlersForEvent(string eventName);
string GetEventKey<T>();
}
这个接口看起来稍显复杂些,我们来简化下看看:
public interface IEventBusSubscriptionsManager
{
void AddSubscription<T, TH>()
void RemoveSubscription<T, TH>()
IEnumerable<SubscriptionInfo> GetHandlersForEvent<T>()
}
最终,这三个方法就是我们要关注的,添加订阅、移除订阅、获取指定事件的订阅信息。
SubscriptionInfo
是什么?
public bool IsDynamic { get; }
public Type HandlerType{ get; }
SubscriptionInfo
中只有两个信息,这是不是一个Dynamic类型的Event以及这个Event所对应的处理器的类型。
这是你可能会有另一个疑问:
这个和IEventBus
有什么关系?
IEventBusSubscriptionsManager
含有更多功能:查看是否有订阅,获取事件的Type,获取事件的处理器等等IEventBusSubscriptionsManager
由IEventBus
使用,在RabbitMq和ServiceBus的实现中,都使用Manager去存储事件的信息,例如下面的代码:public void Subscribe<T, TH>()
where T : IntegrationEvent
where TH : IIntegrationEventHandler<T>
{
// 查询事件的全名
var eventName = _subsManager.GetEventKey<T>(); //向mq添加注册
DoInternalSubscription(eventName); // 向manager添加订阅
_subsManager.AddSubscription<T, TH>();
} private void DoInternalSubscription(string eventName)
{
var containsKey = _subsManager.HasSubscriptionsForEvent(eventName);
if (!containsKey)
{
if (!_persistentConnection.IsConnected)
{
_persistentConnection.TryConnect();
} using (var channel = _persistentConnection.CreateModel())
{
channel.QueueBind(queue: _queueName,
exchange: BROKER_NAME,
routingKey: eventName);
}
}
}
查询事件的名字是manager做的,订阅的时候是先向mq添加订阅,之后又加到manager中,manager管理着订阅的基本信息。
另外一个重要功能是获取事件的处理器信息,在rabbit mq的实现中,ProcessEvent方法中用manager获取了事件的处理器,再用依赖注入获得处理器的实例,反射调用Handle
方法处理事件信息:
private async Task ProcessEvent(string eventName, string message)
{
// 从manager查询信息
if (_subsManager.HasSubscriptionsForEvent(eventName))
{
using (var scope = _autofac.BeginLifetimeScope(AUTOFAC_SCOPE_NAME))
{
// 从manager获取处理器
var subscriptions = _subsManager.GetHandlersForEvent(eventName);
foreach (var subscription in subscriptions)
{
// Di + 反射调用,处理事件(两个都是,只是针对是否是dynamic做了不同的处理)
if (subscription.IsDynamic)
{
var handler = scope.ResolveOptional(subscription.HandlerType) as IDynamicIntegrationEventHandler;
dynamic eventData = JObject.Parse(message);
await handler.Handle(eventData);
}
else
{
var eventType = _subsManager.GetEventTypeByName(eventName);
var integrationEvent = JsonConvert.DeserializeObject(message, eventType);
var handler = scope.ResolveOptional(subscription.HandlerType);
var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType);
await (Task)concreteType.GetMethod("Handle").Invoke(handler, new object[] { integrationEvent });
}
}
}
}
}
IEventBusSubscriptionsManager的默认实现
在eShop中只有一个实现就是InMemoryEventBusSubscriptionsManager
类
这个类中有两个重要的字段
private readonly Dictionary<string, List<SubscriptionInfo>> _handlers;
private readonly List<Type> _eventTypes;
他们分别存储了事件列表和事件处理器信息词典
接下来就是实现一个
基于内存的事件总线
了
我们要做什么呢?IEventBusSubscriptionsManager 已经有了InMemory的实现了,我们可以直接拿来用,所以我们只需要自己实现一个EventBus就好了
先贴出最终代码:
public class InMemoryEventBus : IEventBus
{
private readonly IServiceProvider _provider;
private readonly ILogger<InMemoryEventBus> _logger;
private readonly ISubscriptionsManager _manager;
private readonly IList<IntegrationEvent> _events;
public InMemoryEventBus(
IServiceProvider provider,
ILogger<InMemoryEventBus> logger,
ISubscriptionsManager manager)
{
_provider = provider;
_logger = logger;
_manager = manager;
}
public void Publish(IntegrationEvent e)
{
var eventType = e.GetType();
var handlers = _manager.GetHandlersForEvent(eventType.FullName);
foreach (var handlerInfo in handlers)
{
var handler = _provider.GetService(handlerInfo.HandlerType);
var method = handlerInfo.HandlerType.GetMethod("Handle");
method.Invoke(handler, new object[] { e });
}
}
public void Subscribe<T, TH>()
where T : IntegrationEvent
where TH : IIntegrationEventHandler<T>
{
_manager.AddSubscription<T, TH>();
}
public void SubscribeDynamic<TH>(string eventName) where TH : IDynamicIntegrationEventHandler
{
throw new NotImplementedException();
}
public void Unsubscribe<T, TH>()
where T : IntegrationEvent
where TH : IIntegrationEventHandler<T>
{
_manager.RemoveSubscription<T, TH>();
}
public void UnsubscribeDynamic<TH>(string eventName) where TH : IDynamicIntegrationEventHandler
{
throw new NotImplementedException();
}
}
首先构造函数中声明我们要使用的东西:
public InMemoryEventBus(
IServiceProvider provider,
ILogger<InMemoryEventBus> logger,
ISubscriptionsManager manager)
{
_provider = provider;
_logger = logger;
_manager = manager;
}
这里要注意的就是IServiceProvider provider
这是 DI容器,当我们在切实处理事件的时候我们选择从DI获取处理器的实例,而不是反射创建,这要做的好处在于,处理器可以依赖于其它东西,并且可以是单例的
public void Subscribe<T, TH>()
where T : IntegrationEvent
where TH : IIntegrationEventHandler<T>
{
_manager.AddSubscription<T, TH>();
}
public void Unsubscribe<T, TH>()
where T : IntegrationEvent
where TH : IIntegrationEventHandler<T>
{
_manager.RemoveSubscription<T, TH>();
}
订阅和取消订阅很简单,因为我们是InMemory的所以只调用了manager的方法。
接下来就是最重要的Publish方法,实现Publish有两种方式:
使用额外的线程和Queue让发布和处理异步
为了简单起见,我们先写个简单易懂的同步的
public void Publish(IntegrationEvent e)
{
// 首先要拿到集成事件的Type信息
var eventType = e.GetType(); // 获取属于这个事件的处理器列表,可能有很多,注意获得的是SubscriptionInfo
var handlers = _manager.GetHandlersForEvent(eventType.FullName); // 不解释循环
foreach (var handlerInfo in handlers)
{
// 从DI中获取类型的实例
var handler = _provider.GetService(handlerInfo.HandlerType); // 拿到Handle方法
var method = handlerInfo.HandlerType.GetMethod("Handle"); // 调用方法
method.Invoke(handler, new object[] { e });
}
}
OK,我们的InMemoryEventBus就写好了!
要实践这个InMemoryEventBus,那么还需要一个IntegrationEvent
的子类,和一个IIntegrationEventHandler<T>
的实现类,这些都不难,例如我们做一个添加用户的事件,A在添加用户后,发起一个事件并将新用户的名字作为事件数据,B去订阅事件,并在自己的处理器中处理名字信息。
思路是这样的:
写一个
AddUserEvent:IntegrationEvent
,里面有一个UserId和一个UserName
。写一个
AddUserEventHandler:IIntegrationEventHandler<AddUserEvent>
,在Handle
方法中输出UserId和Name到日志。注册DI,你要注册下面这些服务:
IEventBus=>InMemoryEventBus
ISubscriptionsManager=>InMemorySubscriptionsManager
AddUserEventHandler=>AddUserEventHandler
在Startup中为刚刚写的事件和处理器添加订阅(在这里已经可以获取到IEventBus实例了)
写一个Api接口或是什么,调用IEventBus的Publish方法,new 一个新的
AddUserEvent
作为参数传进去。
OK!到这里一个切实可用的InMemoryEventBus就可以使用了。
看eShopOnContainers学一个EventBus的更多相关文章
- Android消息传递之基于RxJava实现一个EventBus - RxBus
前言: 上篇文章学习了Android事件总线管理开源框架EventBus,EventBus的出现大大降低了开发成本以及开发难度,今天我们就利用目前大红大紫的RxJava来实现一下类似EventBus事 ...
- 跟vczh看实例学编译原理——三:Tinymoe与无歧义语法分析
文章中引用的代码均来自https://github.com/vczh/tinymoe. 看了前面的三篇文章,大家应该基本对Tinymoe的代码有一个初步的感觉了.在正确分析"print ...
- 跟vczh看实例学编译原理——一:Tinymoe的设计哲学
自从<序>胡扯了快一个月之后,终于迎来了正片.之所以系列文章叫<看实例学编译原理>,是因为整个系列会通过带大家一步一步实现Tinymoe的过程,来介绍编译原理的一些知识点. 但 ...
- 跟vczh看实例学编译原理——零:序言
在<如何设计一门语言>里面,我讲了一些语言方面的东西,还有痛快的喷了一些XX粉什么的.不过单纯讲这个也是很无聊的,所以我开了这个<跟vczh看实例学编译原理>系列,意在科普一些 ...
- 看代码学知识之(2) ListView无数据时显示其他View
看代码学知识之(2) ListView无数据时显示其他View 今天看的一块布局是这样的: <!-- The frame layout is here since we will be show ...
- 看日记学git摘要~灰常用心的教程
看日记学git linux 命令行 cd ls / ls -a clear mkdir rmdir echo "hi, good day" > hi.txt touch he ...
- 看起来像一个输入框的input,实际上是有两个input
看起来像一个输入框的input,实际上是有两个input
- 使 div 元素看上去像一个按钮
使 div 元素看上去像一个按钮 div { appearance:button; -moz-appearance:button; /* Firefox */ -webkit-appearance:b ...
- 融e学 一个专注于重构知识,培养复合型人才的平台【获取考试答案_破解】
考试系统-融e学-一个专注于重构知识,培养复合型人才的平台.[获取答案] ganquanzhong 背景:今天去完成学校在融e学上开设的必修课和选修课考试,由于自己的时间有限(还有其他的事情要去做). ...
随机推荐
- linux下ftp命令的安装与使用
注意:实际使用时可以直接尝试第5步,如果提示找不到ftp命令,再进行第4步安装,如安装失败再进行第1步,然后顺序执行即可 1. 下载rpm文件 例如:ftp-0.17-51.1.el6.x86_64. ...
- 【Thinkphp 5】auth权限设置以及实现
1.将auth类下载好 放置目录: extend\auth\auth.php 2.将类中的SQL语句执行,可以在数据库中创建3张表 auth_group(用户组表) auth_ru ...
- Linux面试题(2)
一.Linux操作系统知识 1.常见的Linux发行版本都有什么?你最擅长哪一个?它的官网网站是什么?说明你擅长哪一块? Centos,Ubunto,fedora,openSUSE,Debian等,擅 ...
- Django REST framework反向生成url
Django REST framework是一个基于Django的框架,REST framework又是怎么反向生成url的呢?? 在前面的例子中,知道在REST framework中有6种版本控制的 ...
- Git知识总览(五) Git中的merge、rebase、cherry-pick以及交互式rebase
上篇博客聊了<git分支管理之rebase 以及 cherry-pick相关操作>本篇博客我们就以Learning Git中的关卡进行展开.下方列举了LearningGit中的 merge ...
- UVW源码漫谈(三)
咱们继续看uvw的源码,这次看的东西比较多,去除底层的一些东西,很多代码都是连贯的,耦合度也比较高了.主要包括下面几个文件的代码: underlying_type.hpp resource.hpp l ...
- 洛谷 [P3398] 仓鼠找sugar
树剖求LCA 我们可以发现,两条路径ab,cd相交,当且仅当 dep[lca(a,b)]>=dep[lca(c,d)]&(lca(lca(a,b),c)==lca(a,b)||lca(l ...
- BZOJ 4517: [Sdoi2016]排列计数 [容斥原理]
4517: [Sdoi2016]排列计数 题意:多组询问,n的全排列中恰好m个不是错排的有多少个 容斥原理强行推♂倒她 $恰好m个不是错排 $ \[ =\ \ge m个不是错排 - \ge m+1个不 ...
- I/O多路转接模型
body, table{font-family: 微软雅黑; font-size: 13.5pt} table{border-collapse: collapse; border: solid gra ...
- 在Android上编写模块化项目(翻译)
来源博客:Wang Jie's Blog 本文链接:http://blog.wangjiegulu.com/2018/02/13/writing_a_modular_project_on_androi ...