在過去的半年裡,定期或者不定期地寫點東西已經成為了我的一種習慣。可是最近兩個月來一直忙於工作的事情一直足夠的時間留給自己,雖然給自己列了很長一串寫作計劃,可是心有余而力不足。這一段工作主要是幫助公司開發一套分布式的開發框架,對一些技術和設計方法有了一些新的認識。這兩天的工作主要是如何把Enterprise Library V3.1的PIAB(Policy Injection Application Block)引入到我們自己的框架中,為次對PIAB進行了一些研究,借此機會與大家一起分享。
Part I:PIAB Overview
Part II:PIAB設計和實現原理
Part III:PIAB擴展-如何創建Custom CallHandler
一、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。
· 如果出現異常,記錄日志。
上面的這些可以體現在線面的Code中:
public void ProcessOrder(Order order)
{
Authorize();
Audit();
using (TransactionScope scope = new TransactionScope())
{
try
{
OrderDataAccess();
}
catch (Exception ex)
{
HandleExceptoin(ex);
Log();
}
}
}
可以看到上面這麼多行代碼中,和業務相關的就一行而已。雖然對這些非業務邏輯的實現通常通過調用一個封裝好的方法或者組件完成,你往往只需要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中被引入,個人覺得這將又是一個具有重大意義的AB,它的使用將會極大的減輕模塊或組件的依賴關系,在Software Factory中已經有了廣泛的應用)。PIAB提供了一種簡單而有效的方式是你能夠將你所需的非業務邏輯的Crosscutting concern注入到的某個方法的Invocation stack中。比如可以在方法的調用前注入Auditing的執行,在成功調用後執行Transaction commit的調用,在出現異常時進行Exception Handling的處理。
對於PIAB來說,Policy的意思是:“將對應的處理操作注入到對應的方法調用”。這實際上包含兩方面的內容:要注入怎樣的處理操作和如何與具體的方法匹配。前者封裝在一個稱作CallHandler的對象中,而匹配關系通過另一個稱作MatchingRule的對象來表現。所以可以說Policy= CallHandler+MatchingRule。
PIAB給我們提供了很多系統自定義CallHandler,在一般情況下它能滿足我們絕大部分的需求,比如:
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來實現這樣一個例子。我們假設的一個場景時處理一個訂單並將處理後的訂單返回。
Step I: 創建一個Console Application,並添加下面連個Dll Reference.
Microsoft.Practices.EnterpriseLibrary.PolicyInjection
Microsoft.Practices.EnterpriseLibrary.PolicyInjection.CallHandlers
Step II:編寫Order Processing的代碼:
public class OrderProcessor:MarshalByRefObject
至於為什麼要繼承MarshalByRefObject將會在後面對PIAB的實現原理中介紹。將對OrderProcessor的調用寫在Main()。
{
public Order Process(Order order)
{
Console.WriteLine("OrderProcessor.Process() is invocated!");
return order;
}
}
public class Order
{ }
static void Main(string[] args)
{
OrderProcessor processor = PolicyInjection.Create<OrderProcessor>();
Order order = new Order();
processor.Process(order);
processor.Process(order);
}
Step III:創建app.config,並通過Enterprise Library Configuration Console進行配置:
1、添加一個Logging Application Block
2、按照相同的方式添加一個Policy Injection Application Block,然後再PIAB節點添加一個Policy。在該Policy下的Match Rules節點下創建一個Method Name Matching Rule (該MatchingRule根據Method name進行CallHandler的匹配)。對該MatchingRule進行如下的配置:
那麼該Policy的CallHandler將會自動應用到Method name為Process的方法上。
3、在Policy下的Handlers節點添加兩個CallHandler:Caching Handler和Logging Handler。保持Caching Handler的默認配置,按如下進行Loging Handler的配置:
通過上面的配置,我們將會得到如下的Configuration:
Configuration<configSections>
<section name="policyInjection" type="Microsoft.Practices.EnterpriseLibrary.PolicyInjection.Configuration.PolicyInjectionSettings, Microsoft.Practices.EnterpriseLibrary.PolicyInjection, Version=3.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
<section name="loggingConfiguration" type="Microsoft.Practices.EnterpriseLibrary.Logging.Configuration.LoggingSettings, Microsoft.Practices.EnterpriseLibrary.Logging, Version=3.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
</configSections>
<policyInjection>
<policies>
<add name="Policy">
<matchingRules>
<add type="Microsoft.Practices.EnterpriseLibrary.PolicyInjection.MatchingRules.MemberNameMatchingRule, Microsoft.Practices.EnterpriseLibrary.PolicyInjection, Version=3.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
name="Member Name Matching Rule">
<matches>
<add match="Process" ignoreCase="false" />
</matches>
</add>
</matchingRules>
<handlers>
<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"
name="Caching Handler" />
<add logBehavior="After" beforeMessage="=================End================="
afterMessage="================Begin================" eventId="0"
includeParameterValues="true" includeCallStack="true" includeCallTime="true"
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"
name="Logging Handler">
<categories>
<add name="Audit" />
</categories>
</add>
</handlers>
</add>
</policies>
</policyInjection>
<loggingConfiguration name="Logging Application Block" tracingEnabled="true"
defaultCategory="General" logWarningsWhenNoCategoriesMatch="true">
<listeners>
<add source="Enterprise Library Logging" formatter="Text Formatter"
log="Application" machineName="" listenerDataType="Microsoft.Practices.EnterpriseLibrary.Logging.Configuration.FormattedEventLogTraceListenerData, Microsoft.Practices.EnterpriseLibrary.Logging, Version=3.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
traceOutputOptions="None" type="Microsoft.Practices.EnterpriseLibrary.Logging.TraceListeners.FormattedEventLogTraceListener, Microsoft.Practices.EnterpriseLibrary.Logging, Version=3.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
name="Formatted EventLog TraceListener" />
</listeners>
<formatters>
<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} )}"
type="Microsoft.Practices.EnterpriseLibrary.Logging.Formatters.TextFormatter, Microsoft.Practices.EnterpriseLibrary.Logging, Version=3.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
name="Text Formatter" />
</formatters>
<categorySources>
<add switchValue="All" name="General">
<listeners>
<add name="Formatted EventLog TraceListener" />
</listeners>
</add>
</categorySources>
<specialSources>
<allEvents switchValue="All" name="All Events" />
<notProcessed switchValue="All" name="Unprocessed Category" />
<errors switchValue="All" name="Logging Errors & Warnings">
<listeners>
<add name="Formatted EventLog TraceListener" />
</listeners>
</errors>
</specialSources>
</loggingConfiguration>
</configuration>
我們現在來運行以下程序:
從輸出的結果可以看出,雖然我們在Main()兩次調用了Process方法,但是真正執行的數次只有一次。這是Caching所致,第一次調用PIAB將返回值存入Cache,該Cache Entry的Key為參數Order對象。第二次調用由於傳入的參數相同,所有直接將上次的結果返回。
上面我看到了Caching的效果,現在我們在看看Logging。默認情況下,Logging entry被寫入Event Log。下圖就是我們寫入的Log。