Enterprise Library深入解析與靈活應用(5):創建一個簡易版的批處理執行器,認識Enterprise Library典型的配置方式和對象創建方式
最近負責一個框架性項目的升級,主要是從.NET Framework 3.0建議到.NET .NET Framework 3.5,開發工具也從VS2005遷移到VS2008。但是最讓我頭疼的是,原來Team Foundation Server 2005不能正常工作,公司暫時還沒有購買VSTS 2008的打算。基於TFS 2005的Team Build功能不能使用了,導致原本通過Team Build實現的功能需要手工來做,涉及到的包括:Source Code的編譯、文檔的生成、VS項目類型的模板的創建、腳本的合並、安裝包的生成等等。由於絕大部分的功能分為兩類:文件係統的管理(目錄/文件的創建、移動、拷貝和刪除)和可執行文件的執行,所以我本打算寫一個bat文件搞定就可以了,在操作過程中覺得可擴展性太差了,於是花了半天的時間寫了一個GUI的工具。
這個工具執行一組批處理,也可以看成是一個Sequential Workflow的執行器,我把它成為Batch Job Executor。在使用Batch Job Executor過程中,通過配置可以對批處理的每個步驟、或者是Workflow的每個Activity進行自由地定義。從功能上將,這個小工具僅僅是個小玩意兒,不登大雅之堂。 不過考慮到Batch Job Executor的涉及和實現是基於Enterprise Library典型的實現方式,比如基於EL的配置和對象創建方式,對於那些希望進一步了解EL的讀者,或許可以通過這個小小的例子一窺EL的設計原理。對於那些EL的設計不時很了解的讀者,對於以下的內容,可能在理解上可能比較困難。最好是下載源代碼,結合下麵的介紹,希望會幫助了更好的理解EL。(Source Code 下載:https://files.cnblogs.com/artech/Artech.BatchJobExecutor.zip)
一、Batch Job Executor使用
使用Batch Job Executor最重要的步驟就是通過配置配處理的每一個步驟進行設置,在這裏我們組成Batch的步驟成為Job Step。我們可以先來看看下麵的配置示例:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="batchJobExecutor" type="Artech.BatchJobExecutor.Configuration.BatchJobExecutorSettings,Artech.BatchJobExecutor"/>
</configSections>
<batchJobExecutor defaultBatchJob="Batch Job 1">
<variables>
<add name="RootLocation" value="E:\Other Projects\Artech.BatchJobExecutor\"/>
<add name="OutputLocation" value="E:\Output\"/>
</variables>
<batchJobs>
<add name="Batch Job 1" description="The first batch job">
<steps>
<!--step 1-->
<add name="Create Temp Directory" type="Artech.BatchJobExecutor.DirectoryCreationJobStep,Artech.BatchJobExecutor"
directoryToCreate="$OutputLocation$" />
<!--step 2-->
<add name="Notepad" type="Artech.BatchJobExecutor.ExecutableJobStep,Artech.BatchJobExecutor" executableFile="Notepad" waitForProcessExit="false">
<arguments>
<add name="param1" value="E:\readme.txt"/>
</arguments>
</add>
<!--step 3-->
<add name="Copy to Output Location" type="Artech.BatchJobExecutor.DirectoryMoveJobStep,Artech.BatchJobExecutor"
source="$RootLocation$ConsoleApplication2\Bin" destination="$OutputLocation$" />
<!--step 4-->
<add name="Execute Command 1" type="Artech.BatchJobExecutor.ExecutableJobStep,Artech.BatchJobExecutor" executableFile="$OutputLocation$debug\ConsoleApplication1.exe">
<arguments>
<add name="param1" value="1st Patameter"/>
<add name="param2" value="2nd Patameter"/>
</arguments>
</add>
<!--step 5-->
<add name="Delete Temp Directory" type="Artech.BatchJobExecutor.DirectoryDeletionJobStep,Artech.BatchJobExecutor" directoryToDelete="$OutputLocation$" />
</steps>
</add>
<add name="Batch Job 2" description="2nd batch job">
<steps>
… …
</steps>
</add>
</batchJobs>
</batchJobExecutor>
</configuration>
這個配置包含兩個部分:變量的定義和Batch Job的定義。前者定義在<variables>配置節中,一個常用的變量,比如基地址,可以通過name-value的方式在這裏定義。在本例中,我們定義兩個變量(RootLocation和OutputLocation),對變量的引用通過$variable name$的方式實現。而後者呢,則通過<batchJobs>配置節進行定義,我們可以定義一個活著多個Batch Job,在本例中我一共定義了兩個批處理:Batch Job 1和Batch Job 2。
第一個批處理由5個步驟組成,它們分別是:
- Step 1:創建臨時輸出目錄,路經通過變量定義
- Step 2:通過Notepad打開一個.txt文件,文件路徑為E:\readme.txt
- Step 3:將原目錄移到Step1創建了輸出目錄
- Step 4:執行Step 3移到輸出目錄下的可執行文件,參數通過<arguments>配置節指定
- Step 5:移出Step 1創建的臨時目錄
有了上麵的配置,運行我們Batch Job Executor,將會得到下麵的界麵。兩個批處理名稱在下拉框中列出,對於選中的當前批處理,5個Job Step在下麵的Grid中列出來。點擊“Start”按鈕,批處理便開始執行,下麵的進度條現實當前的進度。
二、Batch Job Executor的設計
1、Job Step
構成一個批處理的步驟通過抽象類JobStep表示,除了定義了Name和Description屬性外,定義一個抽象的Execute()方法,Job Step的所有邏輯通過該方法實現。
namespace Artech.BatchJobExecutor
{
public abstract class JobStep
{
public string Name
{ get; set; }
public string Description
{ get; set; }
public abstract void Execute();
}
}
由於大部分Job Step用於基於文件係統的操作,我創建了另一個抽象類DirectoryFileJobStep,暫時還沒有想到需要定義什麼具體的操作,鼓且定義創建出來,以備以後不時之需:
namespace Artech.BatchJobExecutor
{
public abstract class DirectoryFileJobStep : JobStep
{
}
}
創建了4個具體的JobStep,分別用於進行目錄的創建、移動和刪除,以及.exe文件的執行(ExecutableJobStep),它們的關係通過下麵的類型表示。
2、Job Step Configuration
由於所有Job Step都需要通過配置進行設置,所以配置的定義顯得尤為重要。在這裏我們采用Enterprise Library的Xxx-XxxData-XxxAssembler的結構(比如Exception Handler的定義就采用這樣的結構)。其中Xxx代表具體使用某種功能的類型(比如WrapHandler),XxxData(比如WrapHandlerData)表示Xxx對應的配置,而XxxAssembler(WrapHandlerAssembler)則實現通過XxxData對Xxx的創建。
我們Job Step的結構大體也由上麵3個部分構成,我們以ExecutableJobStep為例,它的結構大體可以通過下麵的類圖表示:
先來看看ExecutableJobStep的定義(隻列出重要部分)。三個字段分別表示可執行文件的路徑、參數和是否需要等待進程結束才能開始下一步驟。Execute()中通過開啟進程的方式執行可執行文件。
namespace Artech.BatchJobExecutor
{
[ConfigurationElementType(typeof(ExecutableJobStepData))]
public class ExecutableJobStep : JobStep
{
private string _executableFile;
private string _arguments;
private bool _waitForProcessExit;
… …
public ExecutableJobStep(string executableFile, string arguments, bool waitForProcessExit)
{
if (string.IsNullOrEmpty(executableFile))
{
throw new ArgumentNullException("executableFile");
}
this._executableFile = executableFile;
this._arguments = arguments;
this._waitForProcessExit = waitForProcessExit;
}
public override void Execute()
{
Process process = null;
if (string.IsNullOrEmpty(this.Arguments))
{
process = Process.Start(this.ExecutableFile);
}
else
{
process = Process.Start(this.ExecutableFile, this.Arguments);
}
if (this._waitForProcessExit)
{
process.WaitForExit();
}
}
}
}
需要特別注意的是在ExecutableJobStep 上,通過ConfigurationElementTypeAttribute指定了與之相匹配的配置類型(ExecutableJobStepData)。ExecutableJobStep 的三個屬性(executableFile、arguments和waitForProcessExit)都定義在ExecutableJobStepData。ExecutableJobStepData集成我們自定義的基類:JobStepData,下麵是JobStepData的定義。JobStepData繼承自NameTypeConfigurationElement(定了兩個Configuration Property:Name和Type的ConfigurationElement),這是一個在Enterprise Library廣泛使用的配置類型,因為分別自定義的類型都是通過它的Type屬性進行配置的。
namespace Artech.BatchJobExecutor.Configuration
{
public class JobStepData : NameTypeConfigurationElement
{
[ConfigurationProperty("description", IsRequired = false, DefaultValue = "")]
public string Description
{
get
{
return this["description"] as string;
}
}
public JobStepData()
{
}
public JobStepData(string name, Type type)
: base(name, type)
{
}
}
}
ExecutableJobStepData直接繼承自JobStepData ,定了3個配置屬性分別於ExecutableJobStep的三個屬性:executableFile、arguments和waitForProcessExit,以及參數列表。而以name-value形式定義的參數又定義在ArgumentEntry中。
namespace Artech.BatchJobExecutor.Configuration
{
[Assembler(typeof(ExecutableFileJobStepAssmbler))]
public class ExecutableJobStepData : JobStepData
{
[ConfigurationProperty("executableFile", IsRequired = true)]
public string ExecutableFile
{
get
{
return this["executableFile"] as string;
}
}
[ConfigurationProperty("arguments", IsRequired = false)]
public NamedElementCollection<ArgumentEntry> Arguments
{
get
{
return this["arguments"] as NamedElementCollection<ArgumentEntry>;
}
}
[ConfigurationProperty("waitForProcessExit", IsRequired = false, DefaultValue = true)]
public bool WaitForProcessExit
{
get
{
return (bool)this["waitForProcessExit"];
}
}
}
public class ArgumentEntry : NamedConfigurationElement
{
[ConfigurationProperty("value", IsRequired = true)]
public string Value
{
get
{
return this["value"] as string;
}
}
public ArgumentEntry()
{ }
public ArgumentEntry(string name)
: base(name)
{ }
}
}
在ExecutableJobStepData上應用了AssemblerAttribute,並指明了Assembler的類型:ExecutableFileJobStepAssmbler。而通過ExecutableFileJobStepAssmbler,則可以通過配置創建具體的ExecutableFileJobStep對象。ExecutableFileJobStepAssmbler實現了接口:IAssembler<JobStep, JobStepData>,在Assemble方法中,通過配置對象(objectConfiguration)創建ExecutableFileJob對象。由於可執行文件的路徑(ExecutableFile屬性)可能通過定義的變量定義,所以BatchJobExecutorSettings.ApplyVariable對變量進行解析。
namespace Artech.BatchJobExecutor.Configuration
{
public class ExecutableFileJobStepAssmbler : IAssembler<JobStep, JobStepData>
{
#region IAssembler<JobStep,JobStepData> Members
public JobStep Assemble(IBuilderContext context, JobStepData objectConfiguration, IConfigurationSource configurationSource, ConfigurationReflectionCache reflectionCache)
{
ExecutableJobStepData jobStepData = objectConfiguration as ExecutableJobStepData;
string spliter = " ";
StringBuilder arguments = new StringBuilder();
foreach (ArgumentEntry argument in jobStepData.Arguments)
{
arguments.Append(BatchJobExecutorSettings.ApplyVariable(argument.Value) + spliter);
}
JobStep jobStep = new ExecutableJobStep(BatchJobExecutorSettings.ApplyVariable(jobStepData.ExecutableFile), arguments.ToString().Trim(spliter.ToCharArray()), jobStepData.WaitForProcessExit);
jobStep.Name = jobStepData.Name;
jobStep.Description = jobStepData.Description;
return jobStep;
}
#endregion
}
}
從上麵的類圖,我們會發現我們漏掉了一個對象CustomJobStepData,它繼承自JobStepData類型,並實現了兩個重要的接口:IHelperAssistedCustomConfigurationData<CustomJobStepData>和ICustomProviderData。要說到具體的作用和實現,可能需要很多的文字才能闡述清楚,在這裏我們可以把CustomJobStepData看成是能夠實現配置文件中的配置內容和具體配置類型的適配。
我們有了配置相關的輔助類型,最終需要通過配置來創建與之匹配的對象,在EL中顯得相對簡單,我們隻需要調用AssemblerBasedObjectFactory<TObject, TConfiguration>類型的Create方法就可以了。為此我創建了一個特殊的工廠類:JobStepCustomFactory ,用於創建具體的JobStep。
namespace Artech.BatchJobExecutor
{
public class JobStepCustomFactory : AssemblerBasedObjectFactory<JobStep, JobStepData>
{
public static JobStepCustomFactory Instance = new JobStepCustomFactory();
}
}
3、整個配置
在一開始,我們就介紹了如果進行批處理的配置,我們現在來看看,該配置類如何來定義:BatchJobExecutorSettings。
namespace Artech.BatchJobExecutor.Configuration
{
public class BatchJobExecutorSettings : SerializableConfigurationSection
{
[ConfigurationProperty("variables", IsRequired = true)]
public NamedElementCollection<VariableEntry> Variables
{
get
{
return this["variables"] as NamedElementCollection<VariableEntry>;
}
}
[ConfigurationProperty("batchJobs", IsRequired = true)]
public NamedElementCollection<BatchJobEntry> BatchJobs
{
get
{
return this["batchJobs"] as NamedElementCollection<BatchJobEntry>;
}
}
[ConfigurationProperty("defaultBatchJob", IsRequired = true)]
public string DefaultBatchJob
{
get
{
return this["defaultBatchJob"] as string;
}
}
public static BatchJobExecutorSettings GetConfigurationSection()
{
return ConfigurationSourceFactory.Create().GetSection("batchJobExecutor") as BatchJobExecutorSettings;
}
private static NamedElementCollection<VariableEntry> variables;
public static string ApplyVariable(string statement)
{
if (variables == null)
{
variables = GetConfigurationSection().Variables;
}
foreach (VariableEntry variable in variables)
{
statement = statement.Replace("$" + variable.Name + "$", variable.Value);
}
return statement;
}
}
}
整個Batch Job Executor的配置大體由以下兩個部分組成:
- 變量列表:這是一個NamedElementCollection<VariableEntry>類型,VariableEntry定義如下,
namespace Artech.BatchJobExecutor.Configuration
{
public class VariableEntry : NamedConfigurationElement
{
[ConfigurationProperty("value", IsRequired = true)]
public string Value
{
get
{
return this["value"] as string;
}
}
public VariableEntry()
{ }
public VariableEntry(string name)
: base(name)
{ }
}
}
- Batch Job列表:NamedElementCollection<BatchJobEntry>類型,BatchJobEntry定義如下:
namespace Artech.BatchJobExecutor.Configuration
{
public class BatchJobEntry : NamedConfigurationElement
{
[ConfigurationProperty("description", IsRequired = false)]
public string Description
{
get
{
return this["description"] as string;
}
}
[ConfigurationProperty("steps", IsRequired = true)]
public NameTypeConfigurationElementCollection<JobStepData, CustomJobStepData> Activities
{
get
{
return this["steps"] as NameTypeConfigurationElementCollection<JobStepData, CustomJobStepData>;
}
}
}
}
表示Job Step序列的單個步驟的類型是NameTypeConfigurationElementCollection<JobStepData, CustomJobStepData>。
除了以上兩個主要成員之外,在根節點上還定義了默認的Batch Job的名稱,以及輔助方法ApplyVariable用於解析包含變量的表達式。
4、Batch Job的Batch Job Factory
我們最後還看看Batch Job的定義和創建,下麵的類圖列出來整個BatchJob創建體係的結構:通過BatchJobFactory創建BatchJob對象,BatchJobFactory最終通過EL的EnterpriseLibraryFactory實現對象的創建,而BatchJobFactory在進行對象創建工程中,會根據BatchJob類型指定的實現了ICustomFacotory的具體類型來創建對象,而我們定義的BatchJobCustomFactory實現了該接口,以及實現真正的對象創建過程。由於在配置中每個BatchJob都具有一個具體的、唯一的名稱,一般地,我們通過傳入具體的名稱創建對應的BatchJob。但是如果我們在創建過程中,不曾傳入BatchJob的名稱,我們希望的是創建默認的BatchJob。EL中通過一個特殊的接口IConfigurationNameMapper實現了Default Name和具體的Batch Jon Name的匹配。BatchJobMapper實現了該接口,實現了我們需要的名稱匹配關係。在這裏我就不一一介紹了,有興趣的朋友可以下載代碼自行研究。
實際上,關於對象的創建一直是EL關注的問題,也是EL的核心所在。EL的ObjectBuild和ObjectBuild2就是專門為對象創建而設計的。ObjectBuild和ObjectBuild2是整個EL的基石,也是Unity、Software Factory的根基所在,涉及的類型比較複雜,非三言兩語就能概括,有機會的話,我會寫一些關於此方麵的內容。
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
最後更新:2017-10-30 16:04:16