在过去的半年里,定期或者不定期地写点东西已经成为了我的一种习惯。可是最近两个月来一直忙于工作的事情一直足够的时间留给自己,虽然给自己列了很长一串写作计划,可是心有余而力不足。这一段工作主要是帮助公司开发一套分布式的开发框架,对一些技术和设计方法有了一些新的认识。这两天的工作主要是如何把Enterprise Library V3.1的PIAB(Policy Injection Application Block)引入到我们自己的框架中,为次对PIAB进行了一些研究,借此机会与大家一起分享。
一、Business Logic 和 Infrastructure Logic的分离
对于任何一个企业级应用的开发人员来说,他们编写的代码不仅仅是处理单纯的业务逻辑,同时还需要处理很多的非业务方面的逻辑,比如:Caching、Transaction Enlist、Authorization、Auditing、Exception Handling、Logging、Validation甚至是Performance Counter。我习惯把这些非业务逻辑成为Infrastructure Logic。由于这些Infrastructure Logic通常根据具体的需求注入的具体的业务逻辑的执行中,所以又被更多地成为Crosscutting Concern。
如果按照传统的OO的编程方式,我们通常将这些Business Concern和Crosscutting Concern糅合在一起,从而导致了某种意义的紧耦合。这种紧耦合在一个大型的企业级应用可能是会给应用的可扩展性带来致命的影响。所以必须以某种方式实现Business Concern和Crosscutting Concern的分离,这通常是AOP实现的目标。
举个简单的例子,我们现在有如下一个简单的订单处理操作,可能具体的业务逻辑很简单。但是此操作面临的非业务逻辑可能有很多。比如:
- 在处理之前进行Authorization,确定当前的用户是否具有此操作的权限。
- Auditing,记录处理的用户、处理时间等等。
- Transaction Enlist,将所有Data Access操作纳入同一个Transaction,保证数据的一致性。
- 进行Exception Handling。
- 如果出现异常,记录日志。
上面的这些可以体现在线面的代码中:
1: public void ProcessOrder(Order order)
2: {
3: Authorize();
4: Audit();
5: using (TransactionScope scope = new TransactionScope())
6: {
7: try
8: {
9: OrderDataAccess();
10: }
11: catch (Exception ex)
12: {
13: HandleExceptoin(ex);
14: Log();
15: }
16: }
17: }
可以看到上面这么多行代码中,和业务相关的就一行而已。虽然对这些非业务逻辑的实现通常通过调用一个封装好的方法或者组件完成,你往往只需要Copy Paste就可以了,但是将如此众多的Infrastructure Logic和Business Logic按照这样的方式组合在一起有很多的隐患。
- 将一些公共的Source code进行大规模的复制不能保证其同一性和正确性。我个人觉得,对于一个程序原来说,如果你频繁地使用到Ctrl + C和Ctrl + V,你应该想到你的代码需要重构。
- Infrastructure Logic和Business Logic这种耦合性导致对Infrastructure Logic的变动将获导致Source Code的改变。
- 这样的编程方式增加了程序员的压力,可能对于一个相对Junior的程序员来说,可能根本不知道这个Infrastructure Logic的具体作用何在,而实际上对于最终的程序员来讲,这些应该是对他们透明的。
所以我们需要寻求某种方式将这些Infrastructure Logic注入到某个具体的操作中,而不需要将所有的逻辑实现在具体的方法定义中。比如:你可以通过配置文件进行配置,你也可以通过添加Attribute一声明的方式来实现。而这一切都可以通过Enterprise Library的PIAB来实现。
二、PIAB(Policy Injection Application Block)Overview
PIAB在Enterprise Library 3.0中被引入(注意:不要把PIAB和DIAB混淆,DIAB-Dependence Injection Application Block将在Enterprise Library 4.0中被引入,个人觉得这将又是一个具有重大意义的Application Block,它的使用将会极大的减轻模块或组件的依赖关系,在Software Factory中已经有了广泛的应用)。PIAB提供了一种简单而有效的方式是你能够将你所需的非业务逻辑的Crosscutting concern注入到的某个方法的Invocation stack中。比如可以在方法的调用前注入Auditing的执行,在成功调用后执行Transaction commit的调用,在出现异常时进行Exception Handling的处理。
对于PIAB来说,Policy的意思是:“将对应的处理操作注入到对应的方法调用”。这实际上包含两方面的内容:要注入怎样的处理操作和如何与具体的方法匹配。前者封装在一个称作CallHandler的对象中,而匹配关系通过另一个称作MatchingRule的对象来表现。所以可以说Policy= CallHandler+MatchingRule。
PIAB给我们提供了很多系统自定义CallHandler,在一般情况下它能满足我们绝大部分的需求,比如:
- AuthorizationCallHandler: Enterprise Library Security Application Block的Authorization来实现。
- CachingCallHandler:将方法返回值作为Value、参数列表作为Key存入Cache,如果下次出现相同参数列表的调用,则直接将Cache中的值返回,从而免去了再次执行方法对性能的影响。像一般的Cache处理一样,你也可以设置Cache的过期时间。
- ExceptionCallHandler:通过调用Enterprise Library Exception Handing Application Block实现对一异常处理的支持。
- LogCallHandler:通过调用Enterprise Library Logging Application Block进行日志的处理。
- ValidationCallHandler:进行参数的验证等功能,该CallHandler依赖于Enterprise Library Validation Application Block。
- PerformanceCounterCallHandler。
至于MatchingRule,他实际上是定义了一种如何将CallHander和对应的Method进行匹配的规则。同样的,一系列的MatchingRule被定义出来,比如:基于Assembly的MatchingRule将CallHandler匹配到某个Assembly的所有对象上面;基于Custom Attribute的MatchingRule通过声明在某个元素上的某个Attribute进行匹配。此外还有基于NameSpace、Method Signature、等等的MatchingRule。
如何现有的CallHandler和MatchingRule不能满足你具体的要求,你还可以定义Custom CallHandler或者 Custom MatchingRule。在后续的部分将会介绍一个完整的定义Custom CallHandler的例子。
三、Get Started
经过上面对PIAB的介绍,我想大家对PIAB使用的目标又一个简单的认识,为了加深大家的映像,在本节中,我们做一个简单的Walkthrough,做一个简单的使用PIAB的例子。在这个例子通过PIAB实现两个主要的功能:Caching和Logging。为了简单起见,我们使用一个Console Application来实现这样一个例子。我们假设的一个场景时处理一个订单并将处理后的订单返回。
创建一个Console Application,并添加下面连个Dll Reference
- Microsoft.Practices.EnterpriseLibrary.PolicyInjection
- Microsoft.Practices.EnterpriseLibrary.PolicyInjection.CallHandlers
编写Order Processing的代码:
1: public class OrderProcessor:MarshalByRefObject
2: {
3: public Order Process(Order order)
4: {
5: Console.WriteLine("OrderProcessor.Process() is invocated!");
6: return order;
7: }
8: }
9: public class Order { }
至于为什么要继承MarshalByRefObject将会在后面对PIAB的实现原理中介绍。将对OrderProcessor的调用写在Main()。
1: static void Main(string[] args)
2: {
3: OrderProcessor processor = PolicyInjection.Create<OrderProcessor>();
4: Order order = new Order();
5: processor.Process(order);
6: processor.Process(order);
7: }
创建app.config,并通过Enterprise Library Configuration Console进行配置
首先添加一个Logging Application Block
然后 按照相同的方式添加一个Policy Injection Application Block,然后再PIAB节点添加一个Policy。在该Policy下的Match Rules节点下创建一个Method Name Matching Rule (该MatchingRule根据Method name进行CallHandler的匹配),对该MatchingRule进行如下的配置:
接着在Policy下的Handlers节点添加两个CallHandler:Caching Handler和Logging Handler。保持Caching Handler的默认配置,按如下进行Loging Handler的配置:
通过上面的配置,我们将会得到如下的Configuration:
1: <configuration>
2: <configSections>
3: <section name="policyInjection" type="Microsoft.Practices.EnterpriseLibrary.PolicyInjection.Configuration.PolicyInjectionSettings, Microsoft.Practices.EnterpriseLibrary.PolicyInjection, Version=3.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
4: <section name="loggingConfiguration" type="Microsoft.Practices.EnterpriseLibrary.Logging.Configuration.LoggingSettings, Microsoft.Practices.EnterpriseLibrary.Logging, Version=3.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
5: </configSections>
6: <policyInjection>
7: <policies>
8: <add name="Policy">
9: <matchingRules>
10: <add type="Microsoft.Practices.EnterpriseLibrary.PolicyInjection.MatchingRules.MemberNameMatchingRule, Microsoft.Practices.EnterpriseLibrary.PolicyInjection, Version=3.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
11: name="Member Name Matching Rule">
12: <matches>
13: <add match="Process" ignoreCase="false" />
14: </matches>
15: </add>
16: </matchingRules>
17: <handlers>
18: <add expirationTime="00:05:00" type="Microsoft.Practices.EnterpriseLibrary.PolicyInjection.CallHandlers.CachingCallHandler, Microsoft.Practices.EnterpriseLibrary.PolicyInjection.CallHandlers, Version=3.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
19: name="Caching Handler" />
20: <add logBehavior="After" beforeMessage="=================End================="
21: afterMessage="================Begin================" eventId="0"
22: includeParameterValues="true" includeCallStack="true" includeCallTime="true"
23: priority="-1" severity="Information" type="Microsoft.Practices.EnterpriseLibrary.PolicyInjection.CallHandlers.LogCallHandler, Microsoft.Practices.EnterpriseLibrary.PolicyInjection.CallHandlers, Version=3.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
24: name="Logging Handler">
25: <categories>
26: <add name="Audit" />
27: </categories>
28: </add>
29: </handlers>
30: </add>
31: </policies>
32: </policyInjection>
33: <loggingConfiguration name="Logging Application Block" tracingEnabled="true"
34: defaultCategory="General" logWarningsWhenNoCategoriesMatch="true">
35: <listeners>
36: <add source="Enterprise Library Logging" formatter="Text Formatter"
37: log="Application" machineName="" listenerDataType="Microsoft.Practices.EnterpriseLibrary.Logging.Configuration.FormattedEventLogTraceListenerData, Microsoft.Practices.EnterpriseLibrary.Logging, Version=3.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
38: traceOutputOptions="None" type="Microsoft.Practices.EnterpriseLibrary.Logging.TraceListeners.FormattedEventLogTraceListener, Microsoft.Practices.EnterpriseLibrary.Logging, Version=3.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
39: name="Formatted EventLog TraceListener" />
40: </listeners>
41: <formatters>
42: <add template="Timestamp: {timestamp} Message: {message} Category: {category} Priority: {priority} EventId: {eventid} Severity: {severity} Title:{title} Machine: {machine} Application Domain: {appDomain} Process Id: {processId} Process Name: {processName} Win32 Thread Id: {win32ThreadId} Thread Name: {threadName} Extended Properties: {dictionary({key} - {value} )}"
43: type="Microsoft.Practices.EnterpriseLibrary.Logging.Formatters.TextFormatter, Microsoft.Practices.EnterpriseLibrary.Logging, Version=3.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
44: name="Text Formatter" />
45: </formatters>
46: <categorySources>
47: <add switchValue="All" name="General">
48: <listeners>
49: <add name="Formatted EventLog TraceListener" />
50: </listeners>
51: </add>
52: </categorySources>
53: <specialSources>
54: <allEvents switchValue="All" name="All Events" />
55: <notProcessed switchValue="All" name="Unprocessed Category" />
56: <errors switchValue="All" name="Logging Errors & Warnings">
57: <listeners>
58: <add name="Formatted EventLog TraceListener" />
59: </listeners>
60: </errors>
61: </specialSources>
62: </loggingConfiguration>
63: </configuration>
我们现在来运行以下程序,你将会得到如下的输出结果:
从输出的结果可以看出,虽然我们在Main()两次调用了Process方法,但是真正执行的数次只有一次。这是Caching所致,第一次调用PIAB将返回值存入Cache,该Cache Entry的Key为参数Order对象。第二次调用由于传入的参数相同,所有直接将上次的结果返回。
上面我看到了Caching的效果,现在我们在看看Logging。默认情况下,Logging entry被写入Event Log。下图就是我们写入的Log。
EnterLib PIAB系列:
Enterprise Library Policy Injection Application Block 之一: PIAB Overview
Enterprise Library Policy Injection Application Block 之二: PIAB设计和实现原理
Enterprise Library Policy Injection Application Block 之三: PIAB的扩展—创建自定义CallHandler(提供Source Code下载)
Enterprise Library Policy Injection Application Block 之四:如何控制CallHandler的执行顺序