代碼的動態編譯並執行是一個.NET平台提供給我們的很強大的工具用以靈活擴展(當然是面對內部開發人員)復雜而無法估算的邏輯,並通過一些額外的代碼來擴展我們已有 的應用程序。這在很大程度上給我們提供了另外一種擴展的方式(當然這並不能算是嚴格意義上的擴展,但至少為我們提供了一種思路)。
動態代碼執行可以應用在諸如模板生成,外加邏輯擴展等一些場合。一個簡單的例子,為了網站那的響應速度,HTML靜態頁面往往是我們最好的選擇,但基於數據驅動的網站往往又很難用靜態頁面實現,那麼將動態頁面生成html的工作或許就是一個很好的應用場合。另外,對於一些模板的套用,我們同樣可以用它來做。另外這本身也是插件編寫的方式。
最基本的動態編譯
.Net為我們提供了很強大的支持來實現這一切我們可以去做的基礎,主要應用的兩個命名空間是:System.CodeDom.Compiler和Microsoft.CSharp或Microsoft.VisualBasic。另外還需要用到反射來動態執行你的代碼。動態編譯並執行代碼的原理其實在於將提供的源代碼交予CSharpCodeProvider來執行編譯(其實和CSC沒什麼兩樣),如果沒有任何編譯錯誤,生成的IL代碼會被編譯成DLL存放於於內存並加載在某個應用程序域(默認為當前)內並通過反射的方式來調用其某個方法或者觸發某個事件等。之所以說它是插件編寫的一種方式也正是因為與此,我們可以通過預先定義好的借口來組織和擴展我們的程序並將其交還給主程序去觸發。一個基本的動態編譯並執行代碼的步驟包括:
· 將要被編譯和執行的代碼讀入並以字符串方式保存
· 聲明CSharpCodeProvider對象實例
· 調用CSharpCodeProvider實例的CompileAssemblyFromSource方法編譯
· 用反射生成被生成對象的實例(Assembly.CreateInstance)
· 調用其方法
以下代碼片段包含了完整的編譯和執行過程:
復制代碼 代碼如下:
//get the code to compile
string strSourceCode = this.txtSource.Text;
// 1.Create a new CSharpCodePrivoder instance
CSharpCodeProvider objCSharpCodePrivoder = new CSharpCodeProvider();
// 2.Sets the runtime compiling parameters by crating a new CompilerParameters instance
CompilerParameters objCompilerParameters = new CompilerParameters();
objCompilerParameters.ReferencedAssemblies.Add("System.dll");
objCompilerParameters.ReferencedAssemblies.Add("System.Windows.Forms.dll");
objCompilerParameters.GenerateInMemory = true;
// 3.CompilerResults: Complile the code snippet by calling a method from the provider
CompilerResults cr = objCSharpCodePrivoder.CompileAssemblyFromSource(objCompilerParameters, strSourceCode);
if (cr.Errors.HasErrors)
{
string strErrorMsg = cr.Errors.Count.ToString() + " Errors:";
for (int x = 0; x < cr.Errors.Count; x++)
{
strErrorMsg = strErrorMsg + "\r\nLine: " +
cr.Errors[x].Line.ToString() + " - " +
cr.Errors[x].ErrorText;
}
this.txtResult.Text = strErrorMsg;
MessageBox.Show("There were build erros, please modify your code.", "Compiling Error");
return;
}
// 4. Invoke the method by using Reflection
Assembly objAssembly = cr.CompiledAssembly;
object objClass = objAssembly.CreateInstance("Dynamicly.HelloWorld");
if (objClass == null)
{
this.txtResult.Text = "Error: " + "Couldn't load class.";
return;
}
object[] objCodeParms = new object[1];
objCodeParms[0] = "Allan.";
string strResult = (string)objClass.GetType().InvokeMember(
"GetTime", BindingFlags.InvokeMethod, null, objClass, objCodeParms);
this.txtResult.Text = strResult;
需要解釋的是,這裡我們在傳遞編譯參數時設置了GenerateInMemory為true,這表明生成的DLL會被加載在內存中(隨後被默認引用入當前應用程序域)。在調用GetTime方法時我們需要加入參數,傳遞object類型的數組並通過Reflection的InvokeMember來調用。在創建生成的Assembly中的對象實例時,需要注意用到的命名空間是你輸入代碼的真實命名空間。以下是我們輸入的測試代碼(為了方便,所有的代碼都在外部輸入,動態執行時不做調整):
復制代碼 代碼如下:
using System;
namespace Dynamicly
{
public class HelloWorld
{
public string GetTime(string strName)
{
return "Welcome " + strName + ", Check in at " + System.DateTime.Now.ToString();
}
}
}
運行附件中提供的程序,可以很容易得到一下結果:
改進的執行過程
現在一切看起來很好,我們可以編譯代碼並把代碼加載到當前應用程序域中來參與我們的活動,但你是否想過去卸載掉這段程序呢?更好的去控制程序呢?另外,當你運行這個程序很多遍的時候,你會發現占用內存很大,而且每次執行都會增大內存使用。是否需要來解決這個問題呢?當然需要,否則你會發現這個東西根本沒用,我需要執行的一些大的應用會讓我的服務器crzay,不堪重負而瘋掉的。
要解決這個問題我們需要來了解一下應用程序域。.NET Application Domain是.NET提供的運行和承載一個活動的進程(Process)的容器,它將這個進程運行所需的代碼和數據,隔離到一個小的范圍內,稱為Application Domain。當一個應用程序運行時,Application Domains將所有的程序集/組件集加載到當前的應用程序域中,並根據需要來調用。而對於動態生成的代碼/程序集,我們看起來好像並沒有辦法去管理它。其實不然,我們可以用Application Domain提供的管理程序集的辦法來動態加載和移除Assemblies來達到我們的提高性能的目的。具體怎麼做呢,在前邊的基礎上增加以下步驟:
· 創建另外一個Application Domain
· 動態創建(編譯)代碼並保存到磁盤
· 創建一個公共的遠程調用接口
· 創建遠程調用接口的實例。並通過這個接口來訪問其方法。
換句話來講就是將對象加載到另外一個AppDomain中並通過遠程調用的方法來調用。所謂遠程調用其實也就是跨應用程序域調用,所以這個對象(動態代碼)必須繼承於MarshalByRefObject類。為了復用,這個接口被單獨提到一個工程中,並提供一個工廠來簡化每次的調用操作:
復制代碼 代碼如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
namespace RemoteAccess
{
/// <summary>
/// Interface that can be run over the remote AppDomain boundary.
/// </summary>
public interface IRemoteInterface
{
object Invoke(string lcMethod,object[] Parameters);
}
/// <summary>
/// Factory class to create objects exposing IRemoteInterface
/// </summary>
public class RemoteLoaderFactory : MarshalByRefObject
{
private const BindingFlags bfi = BindingFlags.Instance | BindingFlags.Public | BindingFlags.CreateInstance;
public RemoteLoaderFactory() {}
public IRemoteInterface Create( string assemblyFile, string typeName, object[] constructArgs )
{
return (IRemoteInterface) Activator.CreateInstanceFrom(
assemblyFile, typeName, false, bfi, null, constructArgs,
null, null, null ).Unwrap();
}
}
}
接下來在原來基礎上需要修改的是:
· 將編譯成的DLL保存到磁盤中。
· 創建另外的AppDomain。
· 獲得IRemoteInterface接口的引用。(將生成的DLL加載到額外的AppDomain)
· 調用InvokeMethod方法來遠程調用。
· 可以通過AppDomain.Unload()方法卸載程序集。
以下是完整的代碼,演示了如何應用這一方案。
復制代碼 代碼如下:
//get the code to compile
string strSourceCode = this.txtSource.Text;
//1. Create an addtional AppDomain
AppDomainSetup objSetup = new AppDomainSetup();
objSetup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;
AppDomain objAppDomain = AppDomain.CreateDomain("MyAppDomain", null, objSetup);
// 1.Create a new CSharpCodePrivoder instance
CSharpCodeProvider objCSharpCodePrivoder = new CSharpCodeProvider();
// 2.Sets the runtime compiling parameters by crating a new CompilerParameters instance
CompilerParameters objCompilerParameters = new CompilerParameters();
objCompilerParameters.ReferencedAssemblies.Add("System.dll");
objCompilerParameters.ReferencedAssemblies.Add("System.Windows.Forms.dll");
// Load the remote loader interface
objCompilerParameters.ReferencedAssemblies.Add("RemoteAccess.dll");
// Load the resulting assembly into memory
objCompilerParameters.GenerateInMemory = false;
objCompilerParameters.OutputAssembly = "DynamicalCode.dll";
// 3.CompilerResults: Complile the code snippet by calling a method from the provider
CompilerResults cr = objCSharpCodePrivoder.CompileAssemblyFromSource(objCompilerParameters, strSourceCode);
if (cr.Errors.HasErrors)
{
string strErrorMsg = cr.Errors.Count.ToString() + " Errors:";
for (int x = 0; x < cr.Errors.Count; x++)
{
strErrorMsg = strErrorMsg + "\r\nLine: " +
cr.Errors[x].Line.ToString() + " - " +
cr.Errors[x].ErrorText;
}
this.txtResult.Text = strErrorMsg;
MessageBox.Show("There were build erros, please modify your code.", "Compiling Error");
return;
}
// 4. Invoke the method by using Reflection
RemoteLoaderFactory factory = (RemoteLoaderFactory)objAppDomain.CreateInstance("RemoteAccess","RemoteAccess.RemoteLoaderFactory").Unwrap();
// with help of factory, create a real 'LiveClass' instance
object objObject = factory.Create("DynamicalCode.dll", "Dynamicly.HelloWorld", null);
if (objObject == null)
{
this.txtResult.Text = "Error: " + "Couldn't load class.";
return;
}
// *** Cast object to remote interface, avoid loading type info
IRemoteInterface objRemote = (IRemoteInterface)objObject;
object[] objCodeParms = new object[1];
objCodeParms[0] = "Allan.";
string strResult = (string)objRemote.Invoke("GetTime", objCodeParms);
this.txtResult.Text = strResult;
//Dispose the objects and unload the generated DLLs.
objRemote = null;
AppDomain.Unload(objAppDomain);
System.IO.File.Delete("DynamicalCode.dll");
對於客戶端的輸入程序,我們需要繼承於MarshalByRefObject類和IRemoteInterface接口,並添加對RemoteAccess程序集的引用。以下為輸入:
復制代碼 代碼如下:
using System;
using System.Reflection;
using RemoteAccess;
namespace Dynamicly
{
public class HelloWorld : MarshalByRefObject,IRemoteInterface
{
public object Invoke(string strMethod,object[] Parameters)
{
return this.GetType().InvokeMember(strMethod, BindingFlags.InvokeMethod,null,this,Parameters);
}
public string GetTime(string strName)
{
return "Welcome " + strName + ", Check in at " + System.DateTime.Now.ToString();
}
}
}
這樣,你可以通過適時的編譯,加載和卸載程序集來保證你的程序始終處於一個可控消耗的過程,並且達到了動態編譯的目的,而且因為在不同的應用程序域中,讓你的本身的程序更加安全和健壯。
示例代碼下載:http://xiazai.jb51.net/201311/yuanma/DynamicCompiler(jb51.net).rar