日志

浅入 ABP 系列(4):事件总线

 来源    2020-09-16    1  

浅入 ABP 系列(4):事件总线

版权护体©作者:痴者工良,微信公众号转载文章需要 《NCC开源社区》同意。

这一篇将来学习 ABP 中的事件总线,然后结合在我们的基架项目中,逐渐构建一个完整的系统。

源码地址:https://github.com/whuanle/AbpBaseStruct

事件总线

关于事件总线

ABP 中,为了方便进程间通讯,给开发者提供了一个叫 事件总线 的功能,事件总线分为 本地事件总线分布式事件总线,本篇文章讲的是 本地事件总线,系列教程中暂时不考虑讲解 分布式事件总线

事件总线 需要使用 Volo.Abp.EventBus 库,ABP 包中自带,不需要额外引入。

事件总线是通过 订阅-发布 形式使用的,某一方只需要按照格式推送事件,而不需要关注是谁接收了事件和如何处理事件。

你可以参考官方文档:https://docs.abp.io/zh-Hans/abp/latest/Local-Event-Bus

为什么需要这个东西

首先列举一下,你工作开发的项目中,编写 控制器时,是不是有这几种代码。

// 记录日志 1
Task.Run(()=>
{
	_apiLog.Info($"xxxxxxxx");
});
// 记录日志 2
catch(Exception ex)
{
	_apiLog.Error(ex);
}
// 记录日志 3
_apiLog.Info($"登陆信息:用户 [{userName}({clientAdrress})]\);

笔者认为,改善的上述问的方法之一是将函数的功能跟记录日志分开,函数执行任务时,只需要把状态和信息通过事件总线推送,而不需要了关注应该如何处理这些内容。

另外,还有当函数执行某些步骤时,产生了事件,开发者喜欢 new Thread 一个新的线程去执行别的任务,或者 Task.Run

其实,通过事件总线,我们更加好地隔离代码,遵从 单一职责原则 。当然还有很多方面值得使用事件总线,这里我们就不再扯淡了。

前面,我们编写了全局异常拦截器,还有日志组件,这一篇我们将通过事件总线,将 Web 程序的一些部件组合起来。

事件总线创建过程

订阅事件

创建一个服务来订阅事件,当程序中发生某种事件时,此服务将被调用。

事件服务必须继承 ILocalEventHandler<in TEvent> 接口,并实现以下函数:

Task HandleEventAsync(TEvent eventData);

一个系统中,事件服务可以有多个,每个服务的 TEvent 类型不能相同,因为 TEvent 的类型是调用服务的标识。当发生 TEvent 事件后,系统通过 TEvent 去找到这个服务。

事件服务创建完毕后,需要加入到依赖注入中,你可以多继承一个 ITransientDependency 接口,然后统一扫描程序集加入到 依赖注入容器中(第三篇提到过)。

事件

即上面提到的 TEvent

假设有一个系统中所有的事件服务都放到一个容器中,发布者只能传递一个事件,而不能指定谁来提供响应服务。

容器是通过 TEvent 来查找服务的。

事件就是一个模型类,也可以使用 int或者 string 等简单类型(请不要用简单类型做事件),用于传递信息。

一般使用 Event 做后缀。

发布事件

如果需要发布一个事件,只需要注入 ILocalEventBus 即可。

private readonly ILocalEventBus _localEventBus;

        public MyService(ILocalEventBus localEventBus)
        {
            _localEventBus = localEventBus;
        }

然后发布事件:

await _localEventBus.PublishAsync(
                new TEvent
                {
					... ...
                }
            );

全局异常加入事件总线功能

创建事件

AbpBase.Web 中,创建一个 Handlers 目录,再在 Handlers 目录下,创建 HandlerEvents 目录。

然后在 HandlerEvents 目录,创建一个 CustomerExceptionEvent.cs 文件。

CustomerExceptionEvent 作为一个异常事件,用于传递异常的信息,而不仅仅是将 Exception ex 记录就了事。

其文件内容如下:

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;

namespace AbpBase.Application.Handlers.HandlerEvents
{
    /// <summary>
    /// 全局异常推送事件
    /// </summary>
    public class CustomerExceptionEvent
    {
        /// <summary>
        /// 只记录异常
        /// </summary>
        /// <param name="ex"></param>
        public CustomerExceptionEvent(Exception ex)
        {
            Exception = ex;
        }

        /// <summary>
        /// 此异常发生时,用户请求的路由地址
        /// </summary>
        /// <param name="ex"></param>
        /// <param name="actionRoute"></param>
        public CustomerExceptionEvent(Exception ex, string actionRoute)
        {
            Exception = ex;
            Action = actionRoute;
        }

        /// <summary>
        /// 此异常发生在哪个类型的方法中
        /// </summary>
        /// <param name="ex"></param>
        /// <param name="method"></param>
        public CustomerExceptionEvent(Exception ex, MethodBase method)
        {
            Exception = ex;
            MethodInfo = (MethodInfo)method;
        }

        /// <summary>
        /// 记录异常信息
        /// </summary>
        /// <param name="ex"></param>
        /// <param name="actionRoute"></param>
        /// <param name="method"></param>
        public CustomerExceptionEvent(Exception ex, string actionRoute, MethodBase method)
        {
            Exception = ex;
            Action = actionRoute;
            MethodInfo = (MethodInfo)method;
        }

        /// <summary>
        /// 当前出现位置
        /// <example>
        /// <code>
        /// MethodInfo = (MethodInfo)MethodBase.GetCurrentMethod();
        /// </code>
        /// </example>
        /// </summary>
        public MethodInfo MethodInfo { get; private set; }

        /// <summary>
        /// 发生异常的 Action
        /// </summary>
        public string Action { get; private set; }

        /// <summary>
        /// 具体异常
        /// </summary>
        public Exception Exception { get; private set; }
    }
}

订阅事件

订阅事件,即将其定义为事件的响应者、服务提供者。

当异常发生后,异常的位置,推送异常信息,那么谁来处理这些信息呢?是订阅者。

这里我们定义一个异常日志处理类,来处理程序推送的异常信息。

AbpBase.Web 项目的 Handlers 目录中,添加一个 CustomerExceptionHandler 类,继承:

public class CustomerExceptionHandler : ILocalEventHandler<CustomerExceptionEvent>, ITransientDependency

服务要处理事件,必须继承 ILocalEventHandler<T>,而 ITransientDependency 是为了此服务可以可以自动注入到容器中。

其文件内容如下:

using AbpBase.Application.Handlers.HandlerEvents;
using Serilog;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus;

namespace AbpBase.Application.Handlers
{
    /// <summary>
    /// 全局异常记录日志
    /// </summary>
    public class CustomerExceptionHandler : ILocalEventHandler<CustomerExceptionEvent>, ITransientDependency
    {
        private readonly ILogger _ILogger;

        public CustomerExceptionHandler(ILogger logger)
        {
            _ILogger = logger;
        }

        public async Task HandleEventAsync(CustomerExceptionEvent eventData)
        {
            StringBuilder stringBuilder = new StringBuilder(256);
            stringBuilder.AppendLine();
            stringBuilder.Append("Action:    ");
            stringBuilder.AppendLine(eventData.Action);
            if (eventData.MethodInfo != null)
            {
                stringBuilder.Append("Class-Method:    ");
                stringBuilder.Append(eventData.MethodInfo?.DeclaringType.FullName);
                stringBuilder.AppendLine(eventData.MethodInfo?.Name);
            }

            stringBuilder.Append("Source:    ");
            stringBuilder.AppendLine(eventData.Exception.Source);
            stringBuilder.Append("TargetSite:    ");
            stringBuilder.AppendLine(eventData.Exception.TargetSite?.ToString());
            stringBuilder.Append("InnerException:    ");
            stringBuilder.AppendLine(eventData.Exception.InnerException?.ToString());
            stringBuilder.Append("Message:    ");
            stringBuilder.AppendLine(eventData.Exception.Message);
            stringBuilder.Append("HelpLink:    ");
            stringBuilder.AppendLine(eventData.Exception.HelpLink);
            _ILogger.Fatal(stringBuilder.ToString());
            await Task.CompletedTask;
        }
    }
}

这样写,记录的日志可以有很好的层次结构。

发布事件

定义了事件的格式和定义服务来订阅事件后,我们来创建一个发布者。

我们修改一下 WebGlobalExceptionFilter

增加依赖注入:

private readonly ILocalEventBus _localEventBus;

        public WebGlobalExceptionFilter(ILocalEventBus localEventBus)
        {
            _localEventBus = localEventBus;
        }

发布事件:

public async Task OnExceptionAsync(ExceptionContext context)
        {

            if (!context.ExceptionHandled)
            {
                await _localEventBus.PublishAsync(new CustomerExceptionEvent(context.Exception,
                    context.ActionDescriptor?.DisplayName));
                    ...
                    ...

测试

创建一个 Action :

[HttpGet("/T4")]
        public string MyWebApi4()
        {
            int a = 1;
            int b = 0;
            int c = a / b;
            return c.ToString();
        }

然后访问 https://localhost:5001/T4 ,会发现请求后报错

AbpBase.WebLogs 目录中,打开 -Fatal.txt 文件。

可以看到:

2020-09-16 18:49:27.750 +08:00 [FTL] 
Action:    ApbBase.HttpApi.Controllers.TestController.MyWebApi4 (ApbBase.HttpApi)
Source:    ApbBase.HttpApi
TargetSite:    System.String MyWebApi4()
InnerException:    
Message:    Attempted to divide by zero.
HelpLink:

除了异常信息外,我们还可以很方便的知道异常发生在 TestController.MyWebApi4 这个位置。

记录事件

如果在普通方法里面出现异常,我们这样这样记录:

catch (Exception ex)
            {
                ...
                new CustomerExceptionEvent(ex, MethodBase.GetCurrentMethod());
                ...
            }

MethodBase.GetCurrentMethod() 可以获取当前正在运行的方法,获得信息后将此参数传递给异常记录服务,会自动解析出具体是哪个地方发生异常。

由于目前 Web 程序中还没有编写什么服务,因此我们先结合到异常日志功能中,后面编写服务时,会再次用到事件总线。

完整代码参考:https://github.com/whuanle/AbpBaseStruct/tree/master/src/4/AbpBase

下一篇文章地址是 https://www.cnblogs.com/whuanle/p/13061059.html

相关文章
ABP的事件总线和领域事件(EventBus & Domain Events)
日志http://www.aspnetboilerplate.com/Pages/Documents/EventBus-Domain-Events EventBus EventBus是个单例,获得Even ...
1
ABP之事件总线(5)
日志前面已经对Castle Windsor的基本使用进行了学习,有了这个基础,接下来我们将把我们的事件总线再次向ABP中定义的事件总线靠近.从源码中可以知道在ABP中定义了Dictionary,存放三种类 ...
1
ABP之事件总线(4)
日志在上一篇的随笔中,我们已经初步完成了EventBus,但是EventBus中还有诸多的问题存在,那么到底有什么问题呢,接下来我们需要看一看ABP中的源码是如何定义EventBus的. 1.第一个点 在 ...
1
ABP之事件总线(3)
日志承接上一篇时间总线的学习,在上一篇中我们实现了取消显式注册事件的方式,采用使用反射的方式.这样的好处可以解除Publisher和Scriber的显式依赖,但是问题又来了,因为我们只有Publisher ...
ABP之事件总线(2)
日志在上一篇文章中,我们复习了一下事件的经典的发布订阅模式,同时对是事件源和时间处理逻辑进行抽象统一,用起来也没有问题.但是还是有很多的问题,比如说我们Handle方法其实是违背了单一性的原则的,里面混杂 ...
ABP之事件总线(1)
日志什么是事件总线呢?官方的文档说,它是一个单例对象,由其他的类共同拥有,可以用来触发和处理事件.这个东西确实比较陌生,为什么要使用事件总线,或者说事件总线的优势是什么???首先我们可以明确的是,事件总线 ...
2
ABP理论学习之事件总线和领域事件
日志返回总目录 本篇目录 事件总线 定义事件 触发事件 处理事件 句柄注册 取消注册 在C#中,我们可以在一个类中定义自己的事件,而其他的类可以注册该事件,当某些事情发生时,可以通知到该类.这对于桌面应用 ...
2
[Abp 源码分析]九、事件总线
日志0.简介 事件总线就是订阅/发布模式的一种实现,本质上事件总线的存在是为了降低耦合而存在的. 从上图可以看到事件由发布者发布到事件总线处理器当中,然后经由事件总线处理器调用订阅者的处理方法,而发布者和 ...
2
Android学习系列(43)--使用事件总线框架EventBus和Otto
日志事件总线框架 针对事件提供统一订阅,发布以达到组件间通信的解决方案. 原理 观察者模式. EventBus和Otto 先看EventBus的官方定义: Android optimized event ...
2
[2017-10-26]Abp系列——DTO入参验证使用方法及经验分享
日志本系列目录:Abp介绍和经验分享-目录 声明式的入参验证逻辑 声明式入参验证主要使用了System.ComponentModel.DataAnnotations中提供的各种验证参数的Attribute ...
1
[Abp vNext 源码分析] - 13. 本地事件总线与分布式事件总线 (Rabbit MQ)
日志一.简要介绍 ABP vNext 封装了两种事件总线结构,第一种是 ABP vNext 自己实现的本地事件总线,这种事件总线无法跨项目发布和订阅.第二种则是分布式事件总线,ABP vNext 自己封装 ...
1
android – 如何发送事件从服务到活动与Otto事件总线?
问答简单的BusProvider.getInstance().post()带来异常不是主线程. 如何发送事件从服务到活动与Otto事件总线?::要从任何线程(主或背景)发布并在主线程上接收,请尝试类似的 ...
1
android – RxJava事件总线
问答使用第一个版本的RxJava和RxAndroid我有以下类作为EventBus: public class RxBus { private static RxBus instance; private ...
1
android – RxJava作为事件总线,当只有一个事件发布时,onNext被多次调用
问答我正在用RxJava实现一个事件总线(RxBus). RxBus.java public class RxBus { private static final String TAG = LogUtil ...
1
RxJava作为Android项目中的事件总线 – 从总线中删除事件
问答我之前曾经使用过EventBus,它易于使用且易于理解.但是,这一次,我想尝试使用RxJava进行类似事件总线的通信,但是不清楚如何从RxJava中删除事件,或者更好的说,它应该如何正确设计以具有与E ...
c# – 使用MVVM模式时,与属性更改相关的代码是应该放入setter还是事件?
问答寻找有关放置代码的位置的指导,这取决于对属性的更改. 例如,我有一个视图模型,用于保存应用程序设置的状态 public SettingsViewModel(ISettingsRepository se ...
1
android – 使用Intents或事件总线在同一个应用程序内进行通信
问答我理解如何使用Intents与系统/其他应用程序通信.我理解如何在同一个应用程序中使用Intents.我也理解如何使用Otto在同一个应用程序内进行通信. 使用Otto与Intents在我的活动/服务 ...
1
java – 如何配置Vert.x事件总线以跨Docker容器集群工作?
问答在我目前的设置中,我使用的是Hazelcast集群管理器的默认多播选项.当我链接我的容器化Vertx模块的实例(通过Docker网络链接)时,我可以看到他们成功创建了Hazelcast集群.但是,当我 ...
1
使用Vert.x事件总线’本地'(java,单jvm)时是否可以避免序列化?
问答我的情况是: >单个JVM >仅Java(我不需要是多语言) >我不想支付序列化成本来在总线上发布不可变事件(发布对java对象的引用会起作用). 我理解vert.x事件总线的范围比 ...
1
android – 事件总线与本地广播管理器:哪一个是最好的
问答我的应用程序在很大程度上依赖于本地广播,因为每次活动调用都会调用广播注册方法,所以移动到任何事件总线都很好. 使用本地广播管理器的两个主要问题. >每项活动都需要注册 >注册方法执行时间( ...
1