最近負責一個框架性項目的升級,主要是從.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。
一、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
由於大部分Job Step用於基於文件系統的操作,我創建了另一 個抽象類DirectoryFileJobStep,暫時還沒有想到需要定義什麼具體的操作,鼓 且定義創建出來,以備以後不時之需:
{
public abstract class JobStep
{
public string Name
{ get; set; }
public string Description
{ get; set; }
public abstract void Execute();
}
}
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
Batch Job列表: NamedElementCollection<BatchJobEntry>類型,BatchJobEntry定義如下 :
{
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)
{ }
}
}
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的根基所在,涉及的類型比較復雜,非 三言兩語就能概括,有機會的話,我會寫一些關於此方面的內容。
本文配套源碼