使用发布者订阅者模式的事件管理/日志记录






4.71/5 (4投票s)
2005年10月21日
9分钟阅读

60501

574
解释基于发布者订阅者模式的事件管理/日志记录组件。
引言
您很有可能在您的某个应用程序中遇到过需要记录事件的情况。这可能源于需要跟踪长时间运行的作业或记录调试类型信息。无论情况如何,这在应用程序中都是一个相当普遍的需求。
嗯,我也有这种需求,所以我构建了一个小型的事件管理/日志记录组件,可以轻松定制以满足与事件管理和日志记录相关的各种需求。
在本文中,我解释了我创建的事件管理组件。我的事件管理组件可以执行或提供钩子来执行:
- 将事件异步记录到各种类型的事件日志。
- 跟踪长时间运行的进程或调试类型信息。
- 将事件记录到文件、数据库或应用程序中实现的某些内存对象。
- 将事件记录到不同的事件日志中。例如,安全事件可以记录到文件中,而其他信息类型事件可以直接记录到 Windows 应用程序事件日志等。
- 打开和关闭事件日志记录。此外,微调事件的记录方式。例如,在开发过程中,只记录调试类型事件,但在部署过程中则不记录。
- “监听”应用程序引发的事件。
- 自定义每个事件日志中记录的事件类型。例如,一个事件日志可能只关心安全事件,而另一个事件日志可能同时关心调试和安全事件。
- 按记录的事件类型过滤事件日志。例如,过滤所有错误事件。
如果您需要这些功能中的任何一个,您可以使用我的组件,或者至少使用我在此处介绍的一些概念。
那么,这个完整的事件管理组件是如何工作的呢?在深入细节之前,让我给您一个简要的概述。
概述
在应用程序中,我有一个全局事件管理器的概念。当发生任何值得注意的事件时,应用程序负责通知全局事件管理器。简而言之,应用程序“抛出”事件,并让事件管理器处理其余部分。这是通过一个名为 HandleEvent()
的简单静态方法实现的。HandleEvent
方法中的一个参数是一个名为 IEvent
的接口。在应用程序中,您可以实现自己的 IEvent
版本。此接口定义了事件发生位置、事件名称、事件时间、任何相关消息等信息。我的想法是,应用程序可以有许多 IEvent
的自定义实现,每个都可以在适用时随时随地引发。
事件管理器最初加载了一些在 app.config 文件中定义的默认订阅者。当应用程序引发事件时,事件管理器会通知任何订阅者。然后,每个订阅者依次确定事件的记录方式和位置。每个订阅者都可以配置为处理所有事件或特定事件集。同样,订阅者可以配置为启用或禁用,即是否进行日志记录。同样,app.config 是指定这些设置的位置。
最后,事件管理器公开了方法 AddSubscriber
和 RemoveSubscriber
。这些方法允许在运行时添加和删除额外的订阅者。这在长时间运行的作业等情况下效果很好。例如,在长时间运行的作业期间,方法可以在特定的检查点引发事件,允许任何注册的订阅者接收这些通知并将相关的状态信息中继给最终用户。
值得注意的是,事件管理组件是基于发布者-订阅者模式的(设计模式是另一个远远超出本文范围的宏大主题)。
在深入组件中每个类的细节之前,让我提一下组件上下文中使用的一些关键概念。
Reflection(反射)
在应用程序启动时使用反射,以加载事件管理器的正确实例以及 app.config 中定义的默认订阅者。app.config 定义了 IEventManager
、IEventLogger
(s) 和 IEvent
(s) 的类类型(见下文)。
线程
在示例应用程序中使用线程异步调用长时间运行的作业。更重要的是,线程用于在通过事件管理器引发事件时通知订阅者。
委托(Delegates)
委托是一种特殊类型的托管类,允许您使用类型安全的函数指针。每个委托类型都基于单个方法签名。在事件管理组件中,每个事件“记录器”或订阅者类都实现了一个名为 OnReceiveEvent
的特定函数。每当事件“抛给”事件管理器时,事件管理器都会为每个注册的订阅者调用此方法。
工厂模式
在启动时与反射一起使用工厂模式,以创建应用程序中正在实现的事件管理器的正确实例。EventManagerSettingsHandler
负责加载包含所有正确缓存对象的事件管理器。
最后,我还想提一下,可以下载一个功能齐全的示例,您可以在其中“闲暇”时探索所有详细信息。
好的,既然我们已经定义了事件管理组件和使用的关键概念,现在让我们描述主要的类/接口及其作用。之后,我们将深入我们都了解和喜爱的 .NET 代码。
类和接口
IEventManager
这是一个定义全局事件管理器的接口,它将接收来自应用程序的所有事件通知。此接口的实现是负责接收应用程序引发的事件的类。同样,它也负责通知任何注册的订阅者。
IEventLogger
这是一个定义事件管理器的订阅者的接口。我互换使用“订阅者”和“记录器”这两个词,因为它们实际上是一回事。从一个角度来看,
IEventLogger
的实现订阅了发送给事件管理器的事件,而从另一个角度来看,它们处理事件的物理记录。IEventLogger
的每个实现都将定义它应如何处理从IEventManager
接收到的事件。例如,一个记录器可能写入文件,而另一个可能写入应用程序事件日志。IEvent
这是一个定义应用程序中引发的事件的接口。用户可以选择实现其应用程序特定的
IEvent
版本。EventManagementSettingsHandler
这个类负责读取应用程序配置文件并分别实例化
IEventManager
和IEventLogger
(s) 的相应类类型。在示例代码中,IEventManager
的实际实现是ApplicationEventManager
类。请注意,本文的范围不包括读取 app.config 中的自定义节,因此只需说明EventManagementSettingsHandler
包含所有使其能够读取 app.config 中<EventManagment>
节的管道。还有一些其他的辅助类,但本文不涵盖它们。EventManagementFactory
这个类包含一个名为
getEventManager
的静态方法。此方法提供对由EvenManagementSettingsHandler
创建的事件管理器的访问。请注意,一旦创建了事件管理器,它就会被缓存到工厂中。因此,应用程序总是调用getEventManger
来获取应用程序事件管理器的句柄。
代码
有了所有这些背景知识,现在让我们卷起袖子看看代码。我们需要检查的第一项是 app.config 中的一个节点。
<!--Event Management Section for Logging Will create the correct
Event Managemer object through reflection and set up
all the default subscribers to the Event Management -->
<EventManagement>
<daEventManager xmlns="CSI.Common.Framework.EventManagement.EventManager"
name="MyEventManager"
type="CSI.Common.Framework.EventManagement.ApplicationEventManager"
mode="on">
<daEventLogger name="ApplicationLogger" mode="on"
type="CSI.Common.Framework.EventManagement.MessageQueueLogger">
<!--Since Events are defined then only accept these type of events-->
<daEvent name="ApplicationEvents" mode="on"
type="CSI.Common.Framework.EventManagement.ApplicationEvent" />
</daEventLogger>
<!-- Security Logger to only Receive Security Events.
Note this is not implemented
<daEventLogger name="SecurityLogger" mode="on" type="SecurityLogger">
<daEvent name="SecurityEvents" mode="off" type="SecurityEvent"/>
</daEventLogger>
Could Implement your own file Logger.
Note, not implement only there for sample purposes
<daEventLogger name="FileLogger" mode="on"
type="CSI.Common.Framework.EventManagement.FileLogger">
All Events Logged when no events defined
</daEventLogger>-->
</daEventManager>
</EventManagement>
正如我之前所述,EventManagementSettingsHandler
负责读取 app.config 的这一节,以创建事件管理器的全局实例。在上面的节点中,<daEventManager>
标签的属性类型定义了 .Common.Framework.EventManagement.ApplicationEventManager
。这是将为应用程序创建的事件管理器的类类型。一旦创建,下一步是创建所有订阅者。对于 <daEventManager>
标签下的每个 <daEventLogger>
,将创建相应的订阅者并将其注册到 ApplicationEventManager
。请注意,第一个 <daEventLogger>
定义了类型 CSI.Common.Framework.EventManagement.MessageQueueLogger
。因此,我们的事件管理器将至少有一个 MessageQueueLogger
类型的订阅者。关于节点的最后一个值得注意的项目是每个订阅者都将处理 <daEvent>
标签中定义的所有事件。如果未定义 <daEvents>
标签,则相应的订阅者/记录器将处理所有事件。
实现这一切的代码在 EventManagerFactory.GetEventManger
类中。特别注意以下部分:
If _eventManager Is Nothing Then
myManagerSettings = _
CType(ConfigurationSettings.GetConfig("EventManagement" & _
"/daEventManager"), daEventManager)
'First we load the correct event manager
myEventManager = _
Activator.CreateInstance(Type.GetType(myManagerSettings.type))
'Next we load up any listeners
If myManagerSettings.daEventLogger Is Nothing = False Then
For Each myLoggerSettings InmyManagerSettings.daEventLogger
If myLoggerSettings.mode = "on" Then
myLogger = _
Activator.CreateInstance(Type.GetType(myLoggerSettings.type))
myLogger.LoggerName= myLoggerSettings.name
myEventManager.AddSubscriber(myLogger)
'For Each Logger Add the Specific Events that it listens to
'If none defined then it will listen to all event types passed in
If myLoggerSettings.daEvent Is Nothing = False Then
For Each myEventSettings In myLoggerSettings.daEvent
If myEventSettings.mode = "on" Then
myEvent = _
Activator.CreateInstance(Type.GetType(myEventSettings.type))
myLogger.AddHandledEvent(myEvent)
End If
Next
End If
End If
Next
End If
_eventManager = myEventManager
End If
现在我们已经加载了事件管理器,让我们检查一下我们如何引发事件以及事件引发时会发生什么。为了引发事件,我们将在应用程序中调用类似以下的代码:
CSI.Common.Framework.EventManagement.EventManagerFactory._
GetEventManager().HandleEvent(New _
CSI.Common.Framework.EventManagement.ApplicationEvent("Start LongEvent", _
Me, "Start Long Event", _
CSI.Common.Framework.EventManagement.EventTypes.Information), Me)
此代码将创建一个新的应用程序事件,并将其发送到我们的事件管理器。事件管理器接下来所做的是将此事件广播给任何正在监听的订阅者。
让我们检查一下 ApplicationEvent
中两个重要方法的代码,然后描述它们的功能。
Public Sub HandleEvent(ByRef anEvent As IEvent, _
ByRef source As Object) Implements _
CSI.Common.Framework.EventManagement.IEventManager.HandleEvent
'Now Call allthe Broadcasters and tell them about the event
OnReceiveEvent(source, anEvent)
End Sub
Public Sub OnReceiveEvent(ByRef sender As Object, ByRef evt AsEventArgs)
Dim targets() As System.Delegate
SyncLock Me
Try
targets = Me.EventReceivedEvent.GetInvocationList()
Catch ex As Exception
'No listeners/subscribers
Exit Sub
End Try
End SyncLock
'Call all broadcasters asychronously
If targets.Length > 0 Then
Dim listener As _
CSI.Common.Framework.EventManagement.IEventManager.EventReceivedEventHandler
Dim ar As IAsyncResult
For Each listener Intargets
ar =listener.BeginInvoke(sender, _
CType(evt,System.EventArgs), Nothing, Nothing)
Next
End If
End Sub
正如我们所看到的,HandleEvent
调用 OnReceiveEvent
。OnReceiveEvent
的作用是遍历 IEventManager
中声明的事件 EventReceived
的所有委托引用,并调用每个注册订阅者中的相应函数 OnReceiveEvent
。请记住,委托是一种特殊类型的托管类,允许您使用类型安全的函数指针。我们通过 AddSubcriber
方法将所有这些指向 OnReceiveEvent
的指针注册到我们的订阅者对象中。
Public Sub AddSubscriber(ByRef aSubscriber As IEventLogger)_
Implements IEventManager.AddSubscriber
Try
If Me._subscribers.Contains(aSubscriber) Then
Exit Sub
End If
Me._subscribers.Add(aSubscriber.LoggerName, aSubscriber)
AddHandler Me.EventReceived, _
AddressOf aSubscriber.OnReceiveEvent
Catch ex As Exception
'Do nothing if already in the collection
'Throw ex
End Try
End Sub
值得一提的是 OnReceiveEvent
方法中的 SyncLock
。它的作用是锁定事件管理器,以便我们不会在获取所有已注册订阅者时被应用程序的某些其他部分添加另一个订阅者而陷入困境。这样我们就可以确保在事件引发时所有订阅者都会收到通知。
要讨论的最后一个想法是当我们的事件管理器调用订阅者对象中的 OnReceiveEvent
函数时会发生什么。很酷的是,这真的取决于每个单独的实现。如果我们看一下 MessageQueueLogger
类,这是我示例中的一个默认实现,我们会看到:
Public Overrides Sub OnReceiveEvent(ByRef sender _
As Object, ByRef evt As System.EventArgs)
Try
If Me._handledEvents.Count = 0 Then
Me._eventLog.Enqueue(evt)
Else
If Me._handledEvents.ContainsValue(evt.GetType) Then
Me._eventLog.Enqueue(evt)
End If
End If
Catch ex As Exception
Throw ex
End Try
End Sub
在这个特定的实现中,我们首先检查订阅者是否已设置为处理引发的事件。如果是,那么我们将事件放入我们的消息队列。其他实现可能会获取此事件并将其直接放入 Windows 事件日志中。
此时,如果您觉得这个事件管理组件可以用于/定制您的应用程序,那么我认为最好的方法是下载并逐步查看示例源代码。示例代码为了示例目的进行了精简。我建议遵循表单 GenericForm
中的 Sub Button1_Clic
k。此表单被设置为事件管理器的订阅者。它还将展示如何从事件管理器中添加订阅者、处理事件和删除订阅者。
就是这样。请注意,示例代码为了本文目的进行了精简,但它功能齐全。祝您好运,如果您有任何进一步的问题,请随时通过 brianmrush@yahoo.com 与我联系。
尽情享用!