ZKWeb網(wǎng)站框架是一個自主開發(fā)的網(wǎng)頁框架,實(shí)現(xiàn)了動態(tài)插件和自動編譯功能。 ZKWeb把一個文件夾當(dāng)成是一個插件,無需使用csproj或xproj等形式的項(xiàng)目文件管理,并且支持修改插件代碼后自動重新編譯加載。
下面將說明ZKWeb如何實(shí)現(xiàn)這個功能,您也可以參考下面的代碼和流程在自己的項(xiàng)目中實(shí)現(xiàn)。 ZKWeb的開源協(xié)議是MIT,有需要的代碼可以直接搬,不需要擔(dān)心協(xié)議問題。
實(shí)現(xiàn)動態(tài)編譯依賴的主要技術(shù) 編譯:?Roslyn Compiler Roslyn是微軟提供的開源的c# 6.0編譯工具,可以通過Roslyn來支持自宿主編譯功能。 要使用Roslyn可以安裝nuget包Microsoft.CodeAnalysis.CSharp。 微軟還提供了更簡單的Microsoft.CodeAnalysis.CSharp.Scripting包,這個包只需簡單幾行就能實(shí)現(xiàn)c#的動態(tài)腳本。
加載dll:?System.Runtime.Loader 在.Net Framework中動態(tài)加載一個dll程序集可以使用Assembly.LoadFile,但是在.Net Core中這個函數(shù)被移除了。 微軟為.Net Core提供了一套全新的程序集管理機(jī)制,要求使用AssemblyLoadContext來加載程序集。 遺憾的是我還沒有找到微軟官方關(guān)于這方面的說明。
生成pdb:?Microsoft.DiaSymReader.Native,?Microsoft.DiaSymReader.PortablePdb 為了支持調(diào)試編譯出來的程序集,還需要生成pdb調(diào)試文件。 在.Net Core中,Roslyn并不包含生成pdb的功能,還需要安裝Microsoft.DiaSymReader.Native和Microsoft.DiaSymReader.PortablePdb才能支持生成pdb文件。 安裝了這個包以后Roslyn會自動識別并使用。
實(shí)現(xiàn)動態(tài)編譯插件系統(tǒng)的流程 在ZKWeb框架中,插件是一個文件夾,網(wǎng)站的配置文件中的插件列表就是文件夾的列表。 在網(wǎng)站啟動時,會查找每個文件夾下的*.cs文件對比文件列表和修改時間是否與上次編譯的不同,如果不同則重新編譯該文件夾下的代碼。 網(wǎng)站啟動后,會監(jiān)視*.cs和*.dll文件是否有變化,如果有變化則重新啟動網(wǎng)站以重新編譯。 ZKWeb的插件文件夾結(jié)構(gòu)如下
插件文件夾
net: .Net Framework編譯的程序集
netstandard: .Net Core編譯的程序集
插件名稱.dll: 編譯出來的程序集
插件名稱.pdb: 調(diào)試文件
CompileInfo.txt: 儲存了文件列表和修改時間
同net文件夾下的內(nèi)容
bin:程序集文件夾
src 源代碼文件夾
static 靜態(tài)文件的文件夾
其他文件夾……
通過Roslyn編譯代碼文件到程序集dll 在網(wǎng)站啟動時,插件管理器在得到插件文件夾列表后會使用Directory.EnumerateFiles遞歸查找該文件夾下的所有*.cs文件。 在得到這些代碼文件路徑后,我們就可以傳給Roslyn讓它編譯出dll程序集。 ZKWeb調(diào)用Roslyn編譯的完整代碼可以查看這里,下面說明編譯的流程:
首先調(diào)用CSharpSyntaxTree.ParseText來解析代碼列表到語法樹列表,我們可以從源代碼列表得出List<SyntaxTree>。 parseOptions是解析選項(xiàng),ZKWeb會在.Net Core編譯時定義NETCORE標(biāo)記,這樣插件代碼中可以使用#if NETCORE來定義.Net Core專用的處理。 path是文件路徑,必須傳入文件路徑才能調(diào)試生成出來的程序集,否則即使生成了pdb也不能捕捉斷點(diǎn)。
var parseOptions = CSharpParseOptions.Default;
#if NETCORE parseOptions = parseOptions.WithPreprocessorSymbols(
"NETCORE" );
#endif var syntaxTrees = sourceFiles.Select(path => CSharpSyntaxTree.ParseText(File.ReadAllText(path), parseOptions, path, Encoding.UTF8))
.ToList();
接下來需要分析代碼中的using來找出代碼依賴了哪些程序集,并逐一載入這些程序集。 例如遇到using System.Threading;會嘗試載入System和System.Threading程序集。
LoadAssembliesFromUsings(syntaxTrees);
LoadAssembliesFromUsings的代碼如下,雖然比較長但是邏輯并不復(fù)雜。 關(guān)于IAssemblyLoader將在后面闡述,這里只需要知道它可以按名稱載入程序集。
protected void LoadAssembliesFromUsings (IList<SyntaxTree> syntaxTrees ) { ? ?
var assemblyLoader = Application.Ioc.Resolve<IAssemblyLoader>(); ? ?
foreach (
var tree
in syntaxTrees) { ? ? ? ?
foreach (
var usingSyntax in ((CompilationUnitSyntax )tree.GetRoot ( )).Usings) { ? ? ? ? ? ?
var name = usingSyntax.Name; ? ? ? ? ? ?
var names =
new List<
string >(); ? ? ? ? ? ?
while (name !=
null ) { ? ? ? ? ? ? ? ?
if (name
is QualifiedNameSyntax) { ? ? ? ? ? ? ? ? ? ?
var qualifiedName = (QualifiedNameSyntax)name; ? ? ? ? ? ? ? ? ? ?
var identifierName = (IdentifierNameSyntax)qualifiedName.Right;names.Add(identifierName.Identifier.Text);name = qualifiedName.Left;}
else if (name is IdentifierNameSyntax ) { ? ? ? ? ? ? ? ? ? ?
var identifierName = (IdentifierNameSyntax)name;names.Add(identifierName.Identifier.Text);name =
null ;}} ? ? ? ? ? ?
if (names.Contains(
"src" )) { ? ? ? ? ? ? ? ?
continue ;}names.Reverse(); ? ? ? ? ? ?
for (
int c =
1 ; c <= names.Count; ++c) { ? ? ? ? ? ? ? ?
var usingName =
string .Join(
"." , names.Take(c)); ? ? ? ? ? ? ? ?
if (LoadedNamespaces.Contains(usingName)) { ? ? ? ? ? ? ? ? ? ?
continue ;} ? ? ? ? ? ? ? ?
try {assemblyLoader.Load(usingName);}
catch {}LoadedNamespaces.Add(usingName);}}}
}
經(jīng)過上面這一步后,代碼依賴的所有程序集應(yīng)該都載入到當(dāng)前進(jìn)程中了, 我們需要找出這些程序集并且傳給Roslyn,在編譯代碼時引用這些程序集文件。 下面的代碼生成了一個List<PortableExecutableReference>對象。
var assemblyLoader = Application.Ioc.Resolve<IAssemblyLoader>();
var references = assemblyLoader.GetLoadedAssemblies().Select(assembly => assembly.Location).Select(path => MetadataReference.CreateFromFile(path)).ToList();
最后調(diào)用Roslyn編譯,傳入語法樹列表和引用程序集列表可以得到目標(biāo)程序集。 使用Emit函數(shù)編譯后會返回一個EmitResult對象,里面保存了編譯中出現(xiàn)的錯誤和警告信息。 注意編譯出錯時Emit不會拋出例外,需要手動檢查EmitResult中的Success屬性。
var compilation = CSharpCompilation.Create(assemblyName).WithOptions(
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary,optimizationLevel: optimizationLevel)).AddReferences(references).AddSyntaxTrees(syntaxTrees);
var emitResult = compilation.Emit(assemblyPath, pdbPath);
if (!emitResult.Success) { ? ?
throw new CompilationException(
string .Join(
"\r\n" , emitResult.Diagnostics));
}
到此已經(jīng)完成了代碼文件(cs)到程序集(dll)的編譯,下面來看如何載入這個程序集。
載入程序集 在.Net Framework中,載入程序集文件非常簡單,只需要調(diào)用Assembly.LoadFile。 在.Net Core中,載入程序集文件需要定義AssemblyLoadContext,并且所有相關(guān)的程序集都需要通過同一個Context來載入。 需要注意的是AssemblyLoadContext不能用在.Net Framework中,ZKWeb為了消除這個差異定義了IAssemblyLoader接口。 完整的代碼可以查看 IAssemblyLoader CoreAssemblyLoader NetAssemblyLoader
.Net Framework的載入只是調(diào)用了Assembly中原來的函數(shù),這里就不再說明了。 .Net Core使用的載入器定義了AssemblyLoadContext,代碼如下: 代碼中的plugin.ReferenceAssemblyPath指的是插件自帶的第三方dll文件,用于載入插件依賴但是主項(xiàng)目中沒有引用的dll文件。
private class LoadContext :
AssemblyLoadContext { ? ?
protected override Assembly Load (AssemblyName assemblyName ) { ? ? ? ?
try { ? ? ? ? ? ?
return Assembly.Load(assemblyName);}
catch { ? ? ? ? ? ?
var pluginManager = Application.Ioc.Resolve<PluginManager>(); ? ? ? ? ? ?
foreach (
var plugin
in pluginManager.Plugins) { ? ? ? ? ? ? ? ?
var path = plugin.ReferenceAssemblyPath(assemblyName.Name); ? ? ? ? ? ? ? ?
if (path !=
null ) { ? ? ? ? ? ? ? ? ? ?
return LoadFromAssemblyPath(path);}} ? ? ? ? ? ?
throw ;}}
}
定義了LoadContext以后需要把這個類設(shè)為單例,載入時都通過這個Context來載入。 因?yàn)?Net Core目前無法獲取到所有已載入的程序集,只能獲取程序本身依賴的程序集列表, 這里還添加了一個ISet<Assembly> LoadedAssemblies用于記錄歷史載入的所有程序集。
public Assembly Load (string name ) { ? ?name = ReplacementAssemblies.GetOrDefault(name, name); ? ?
var assembly = Context.LoadFromAssemblyName(
new AssemblyName(name));LoadedAssemblies.Add(assembly); ? ?
return assembly;
}
public Assembly Load (AssemblyName assemblyName ) { ? ?
var assembly = Context.LoadFromAssemblyName(assemblyName);LoadedAssemblies.Add(assembly); ? ?
return assembly;
}
public Assembly Load (byte [] rawAssembly ) { ? ?
using (
var stream =
new MemoryStream(rawAssembly)) { ? ? ? ?
var assembly = Context.LoadFromStream(stream);LoadedAssemblies.Add(assembly); ? ? ? ?
return assembly;}
}
public Assembly LoadFile (string path ) { ? ?
var assembly = Context.LoadFromAssemblyPath(path);LoadedAssemblies.Add(assembly); ? ?
return assembly;
}
到這里已經(jīng)可以載入編譯的程序集(dll)文件了,下面來看如何實(shí)現(xiàn)修改代碼后自動重新編譯。
檢測代碼文件變化并自動重新編譯 ZKWeb使用了FileSystemWatcher來檢測代碼文件的變化,完整代碼可以查看這里。 主要的代碼如下
Action stopWebsite = () => { ? ?
var stoppers = Application.Ioc.ResolveMany<IWebsiteStopper>();stoppers.ForEach(s => s.StopWebsite());
};Action<
string > onFileChanged = (path) => { ? ?
var ext = Path.GetExtension(path).ToLower(); ? ?
if (ext ==
".cs" || ext ==
".json" || ext ==
".dll" ) {stopWebsite();}
};Action<FileSystemWatcher> startWatcher = (watcher) => {watcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName;watcher.Changed += (sender, e) => onFileChanged(e.FullPath);watcher.Created += (sender, e) => onFileChanged(e.FullPath);watcher.Deleted += (sender, e) => onFileChanged(e.FullPath);watcher.Renamed += (sender, e) => { onFileChanged(e.FullPath); onFileChanged(e.OldFullPath); };watcher.EnableRaisingEvents =
true ;
};
var pathManager = Application.Ioc.Resolve<PathManager>();
pathManager.GetPluginDirectories().Where(p => Directory.Exists(p)).ForEach(p => { ? ?
var pluginFilesWatcher =
new FileSystemWatcher();pluginFilesWatcher.Path = p;pluginFilesWatcher.IncludeSubdirectories =
true ;startWatcher(pluginFilesWatcher);
});
這段代碼監(jiān)視了插件文件夾下的cs, json, dll文件, 一旦發(fā)生變化就調(diào)用IWebsiteStopper來停止網(wǎng)站,網(wǎng)站下次打開時將會重新編譯和載入插件。 IWebsiteStopper是一個抽象的接口,在Asp.Net中停止網(wǎng)站調(diào)用了HttpRuntime.UnloadAppDomain,而在Asp.Net Core中停止網(wǎng)站調(diào)用了IApplicationLifetime.StopApplication。
Asp.Net停止網(wǎng)站會卸載當(dāng)前的AppDomain,下次刷新網(wǎng)頁時會自動重新啟動。 而Asp.Net Core停止網(wǎng)站會終止當(dāng)前的進(jìn)程,使用IIS托管時IIS會在自動重啟進(jìn)程,但使用自宿主時則需要依賴外部工具來重啟。
寫在最后 ZKWeb實(shí)現(xiàn)的動態(tài)編譯技術(shù)大幅度的減少了開發(fā)時的等待時間, 主要節(jié)省在不需要每次都按快捷鍵編譯和不需要像其他模塊化開發(fā)一樣需要從子項(xiàng)目復(fù)制dll文件到主項(xiàng)目,如果dll文件較多而且用了機(jī)械硬盤,復(fù)制時間可能會比編譯時間還要漫長。
我將會在這個博客繼續(xù)分享ZKWeb框架中使用的技術(shù)。 如果有不明白的部分,歡迎加入ZKWeb交流群522083886詢問,
相關(guān)文章:?
原文地址:http://www.cnblogs.com/zkweb/p/5857355.html
.NET社區(qū)新聞,深度好文,微信中搜索 dotNET跨平臺 或掃描二維碼關(guān)注
總結(jié)
以上是生活随笔 為你收集整理的ZKWeb网站框架的动态编译的实现原理 的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔 推薦給好友。