浅入 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

随机推荐

  1. TopCoder SRM 590

     第一次做TC,不太习惯,各种调试,只做了一题...... Problem Statement     Fox Ciel is going to play Gomoku with her friend ...

  2. Python3基础 for循环 遍历并输出一个字符串的列表

    镇场诗:---大梦谁觉,水月中建博客.百千磨难,才知世事无常.---今持佛语,技术无量愿学.愿尽所学,铸一良心博客.------------------------------------------ ...

  3. 单源最短路径-Dijkstra算法

    1.算法标签 贪心 2.算法描述 具体的算法描述网上有好多,我觉得莫过于直接wiki,只说明一些我之前比较迷惑的. 对于Dijkstra算法,最重要的是维护以下几个数据结构: 顶点集合S : 表示已经 ...

  4. Python经常使用第三方工具、库、骨架

    Python ImagingLibrary(PIL):它提供强大的图形处理的能力,并提供广泛的图形文件格式支持.该库能进行图形格式的转换.打印和显示.还能进行一些图形效果的处理,如图形的放大.缩小和旋 ...

  5. vs2015基于VisualStudioOnline协同工作流程

    项目负责人登陆自己的vsonline新建项目就不多说了. 直接从邀请队友开始 项目负责人操作 被邀请的邮箱务必是可以登录visualstudio的邮箱 发送邀请后,被邀请人登陆自己的邮箱,查看邀请人发 ...

  6. 为什么用IP无法访问网站,域名可以访问?

    我们访问网站都是通过域名进行访问的,偶尔会使用网站IP进行访问,如学校通常使用IP登录教务处,但很多的时候我们无法通过ip进行访问其他网站,这就涉及到服务器的问题了. 网站都是依托在服务器上面的,而服 ...

  7. response对象的使用

    使用response对象提供的sendRedirect()方法可以将网页重定向到另一个页面.重定向操作支持将地址重定向到不同的主机上,这一点与转发是不同的.在客户端浏览器上将会得到跳转地址,并重新发送 ...

  8. Java基础笔记2

    1.   变量的定义 int money; int 变量类型   money 变量名 money=1000;变量的值 2.  自动类型转换 ①类型要兼容  容器 (水杯---竹篮---碗) ②目标类型 ...

  9. mac上安装vue项目

    mac上如何安装vue项目 一, mac系统安装brew /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/H ...

  10. Head First Python-Python简单处理文件

    前面介绍了自定义格式化输出列表函数printList(),下面再介绍下格式化列表项及列表项的排序. 这里有一组列表数据,记录运动员跑步时间的,要求按照时间大小进行排序.这里每项数据记录的时间格式不一样 ...