澳门至尊网站-首页

您的位置:澳门至尊网站 > 免费资源 > ZKWeb网站框架的动态编译的实现原理

ZKWeb网站框架的动态编译的实现原理

2019-10-25 21:35

ZKWeb网址框架是贰个废寝忘食开荒的网页框架,实现了动态插件和自行编写翻译功能。
ZKWeb把三个文件夹当成是一个插件,不需求使用csproj或xproj等形式的系列文件管理,并且帮助纠正插件代码后自行重新编写翻译加载。

上面将表达ZKWeb怎么着达成那么些职能,您也足以仿效上边包车型客车代码和流程在融洽的种类中达成。
ZKWeb的开源协议是MIT,有亟待的代码能够从来搬,不须求忧虑公约难点。

兑现动态编写翻译注重的首要手艺

编译: Roslyn Compiler
Roslyn是微软提供的开源的c# 6.0编写翻译工具,能够因而Roslyn来支撑自宿小编写翻译功效。
要采用Roslyn能够安装nuget包Microsoft.CodeAnalysis.CSharp
微软还提供了更简短的Microsoft.CodeAnalysis.CSharp.Scripting包,这一个包只需轻松几行就可以促成c#的动态脚本。

加载dll: System.Runtime.Loader
在.Net Framework中动态加载三个dll程序集能够运用Assembly.LoadFile,可是在.Net Core中这几个函数被移除了。
微软为.Net Core提供了后生可畏套全新的前后相继集管理机制,须求利用AssemblyLoadContext来加载程序集。
不满的是作者还一直不找到微软官方关于那方面包车型客车评释。

生成pdb: Microsoft.DiaSymReader.Native, 澳门至尊网站,Microsoft.DiaSymReader.PortablePdb
为了协助调节和测量检验编写翻译出来的程序集,还索要生成pdb调节和测量检验文件。
在.Net Core中,Roslyn并不含有生成pdb的机能,还亟需设置Microsoft.DiaSymReader.NativeMicrosoft.DiaSymReader.PortablePdb技巧支撑生成pdb文件。
设置了那些包现在Roslyn会自动识别并运用。

兑现动态编写翻译插件系统的流水生产线

在ZKWeb框架中,插件是四个文书夹,网址的配备文件中的插件列表便是文本夹的列表。
在网址运营时,会寻找种种文件夹下的*.cs文件相比文件列表和更改时间是不是与上次编写翻译的不等,假诺分裂则重复编写翻译该公文夹下的代码。
网址运转后,会监视*.cs*.dll文件是不是有变化,要是有转换则再度起动网站以重新编写翻译。
ZKWeb的插件文件夹结构如下

  • 插件文件夹
    • bin:程序集文件夹
      • net: .Net Framework编写翻译的前后相继集
        • 插件名称.dll: 编写翻译出来的次序集
        • 插件名称.pdb: 调节和测量试验文件
        • CompileInfo.txt: 储存了文本列表和改造时间
      • netstandard: .Net Core编写翻译的程序集
        • 同net文件夹下的剧情
    • src 源代码文件夹
    • static 静态文件的文书夹
    • 其余文件夹……

经过Roslyn编写翻译代码文件到程序集dll

在网址运维时,插件管理器在得到插件文件夹列表后会接受Directory.EnumerateFiles递归查找该公文夹下的兼具*.cs文件。
在获得这个代码文件路线后,大家就能够传给Roslyn让它编写翻译出dll程序集。
ZKWeb调用Roslyn编写翻译的欧洲经济共同体代码能够翻看这里,下边表明编写翻译的流水生产线:

先是调用CSharpSyntaxTree.ParseText来深入分析代码列表到语法树列表,我们能够从源代码列表得出List<SyntaxTree>
parseOptions是剖析选项,ZKWeb会在.Net Core编写翻译时定义NETCORE标记,那样插件代码中可以使用#if NETCORE来定义.Net Core专项使用的拍卖。
path是文本路线,必须传入文件路线工夫调度生成出来的程序集,不然即便生成了pdb也不能够捕捉断点。

// Parse source files into syntax trees
// Also define NETCORE for .Net Core
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;会尝试载入SystemSystem.Threading程序集。

// Find all using directive and load the namespace as assembly
// It's for resolve assembly dependencies of plugin
LoadAssembliesFromUsings(syntaxTrees);

LoadAssembliesFromUsings的代码如下,纵然相比长可是逻辑并不复杂。
关于IAssemblyLoader将要后头演讲,这里只须要驾驭它能够按名称载入程序集。

/// <summary>
/// Find all using directive
/// And try to load the namespace as assembly
/// </summary>
/// <param name="syntaxTrees">Syntax trees</param>
protected void LoadAssembliesFromUsings(IList<SyntaxTree> syntaxTrees) {
    // Find all using directive
    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) {
                // The type is "IdentifierNameSyntax" if it's single identifier
                // eg: System
                // The type is "QualifiedNameSyntax" if it's contains more than one identifier
                // eg: System.Threading
                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")) {
                // Ignore if it looks like a namespace from plugin 
                continue;
            }
            names.Reverse();
            for (int c = 1; c <= names.Count; ++c) {
                // Try to load the namespace as assembly
                // eg: will try "System" and "System.Threading" from "System.Threading"
                var usingName = string.Join(".", names.Take(c));
                if (LoadedNamespaces.Contains(usingName)) {
                    continue;
                }
                try {
                    assemblyLoader.Load(usingName);
                } catch {
                }
                LoadedNamespaces.Add(usingName);
            }
        }
    }
}

透过地点这一步后,代码正视的兼具程序集应该都载入到如今进程中了,
咱俩必要寻找这一个程序集何况传给Roslyn,在编写翻译代码时引用那么些程序集文件。
下边包车型地铁代码生成了二个List<PortableExecutableReference>对象。

// Add loaded assemblies to compile references
var assemblyLoader = Application.Ioc.Resolve<IAssemblyLoader>();
var references = assemblyLoader.GetLoadedAssemblies()
    .Select(assembly => assembly.Location)
    .Select(path => MetadataReference.CreateFromFile(path))
    .ToList();

创设编写翻译选项
此间须要调用微软非公开的函数WithTopLevelBinderFlags来设置IgnoreCorLibraryDuplicatedTypes。
以此标记让Roslyn能够忽视System.Runtime.Extensions和System.Private.CoreLib中重复的种类。
只要需求让Roslyn日常办事在windows和linux上,必得设置这些标识,具体能够看
Roslyn Scripting暗中同意会使用那些标记,操蛋的微软

// Create compilation options and set IgnoreCorLibraryDuplicatedTypes flag
// To avoid error like The type 'Path' exists in both
// 'System.Runtime.Extensions, Version=4.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'
// and
// 'System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e
var compilationOptions = new CSharpCompilationOptions(
    OutputKind.DynamicallyLinkedLibrary,
    optimizationLevel: optimizationLevel);
var withTopLevelBinderFlagsMethod = compilationOptions.GetType()
    .FastGetMethod("WithTopLevelBinderFlags", BindingFlags.Instance | BindingFlags.NonPublic);
var binderFlagsType = withTopLevelBinderFlagsMethod.GetParameters()[0].ParameterType;
compilationOptions = (CSharpCompilationOptions)withTopLevelBinderFlagsMethod.FastInvoke(
    compilationOptions,
    binderFlagsType.GetField("IgnoreCorLibraryDuplicatedTypes").GetValue(binderFlagsType));

最后调用Roslyn编写翻译,传入语法树列表和引用程序集列表可以收获指标程序集。
使用Emit函数编写翻译后会重回贰个EmitResult目的,里面保存了编写翻译中冒出的乖谬和警告音信。
潜心编译出错开上下班时间Emit不会抛出区别,须求手动物检疫查EmitResult中的Success属性。

// Compile to assembly, throw exception if error occurred
var compilation = CSharpCompilation.Create(assemblyName)
    .WithOptions(compilationOptions)
    .AddReferences(references)
    .AddSyntaxTrees(syntaxTrees);
var emitResult = compilation.Emit(assemblyPath, pdbPath);
if (!emitResult.Success) {
    throw new CompilationException(string.Join("rn",
        emitResult.Diagnostics.Where(d => d.WarningLevel == 0)));
}

到此已经形成了代码文件(cs)到程序集(dll)的编译,上面来看怎么样载入这几个程序集。

载入程序集

在.Net Framework中,载入程序集文件特别轻便,只需求调用Assembly.LoadFile
在.Net Core中,载入程序集文件须要定义AssemblyLoadContext,并且具备相关的主次集都亟需经过同一个Context来载入。
亟待静心的是AssemblyLoadContext不可能用在.Net Framework中,ZKWeb为了消除那一个差异定义了IAssemblyLoader接口。
总体的代码能够查看
IAssemblyLoader
CoreAssemblyLoader
NetAssemblyLoader

.Net Framework的载入只是调用了Assembly中原本的函数,这里就不再表达了。
.Net Core使用的载入器定义了AssemblyLoadContext,代码如下:
代码中的plugin.ReferenceAssemblyPath指的是插件自带的第三方dll文件,用于载入插件信任可是主项目中尚无援用的dll文件。

/// <summary>
/// The context for loading assembly
/// </summary>
private class LoadContext : AssemblyLoadContext {
    protected override Assembly Load(AssemblyName assemblyName) {
        try {
            // Try load directly
            return Assembly.Load(assemblyName);
        } catch {
            // If failed, try to load it from plugin's reference directory
            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自此供给把这些类设为单例,载入时都经过那个Context来载入。
因为.Net Core如今无法取获得独具已载入的程序集,只可以获取程序本人信赖的次序集列表,
此处还加多了二个ISet<Assembly> LoadedAssemblies用来记录历史载入的富有程序集。

/// <summary>
/// Load assembly by name
/// </summary>
public Assembly Load(string name) {
    // Replace name if replacement exists
    name = ReplacementAssemblies.GetOrDefault(name, name);
    var assembly = Context.LoadFromAssemblyName(new AssemblyName(name));
    LoadedAssemblies.Add(assembly);
    return assembly;
}

/// <summary>
/// Load assembly by name object
/// </summary>
public Assembly Load(AssemblyName assemblyName) {
    var assembly = Context.LoadFromAssemblyName(assemblyName);
    LoadedAssemblies.Add(assembly);
    return assembly;
}

/// <summary>
/// Load assembly from it's binary contents
/// </summary>
public Assembly Load(byte[] rawAssembly) {
    using (var stream = new MemoryStream(rawAssembly)) {
        var assembly = Context.LoadFromStream(stream);
        LoadedAssemblies.Add(assembly);
        return assembly;
    }
}

/// <summary>
/// Load assembly from file path
/// </summary>
public Assembly LoadFile(string path) {
    var assembly = Context.LoadFromAssemblyPath(path);
    LoadedAssemblies.Add(assembly);
    return assembly;
}

到这里已经足以载入编写翻译的顺序集(dll)文件了,上面来看如何贯彻修正代码后活动重新编写翻译。

检验代码文件变化并机关心珍视新编写翻译

ZKWeb使用了FileSystemWatcher来检查测试代码文件的改造,完整代码能够翻开这里。
最首要的代码如下

// Function use to stop website
Action stopWebsite = () => {
    var stoppers = Application.Ioc.ResolveMany<IWebsiteStopper>();
    stoppers.ForEach(s => s.StopWebsite());
};
// Function use to handle file changed
Action<string> onFileChanged = (path) => {
    var ext = Path.GetExtension(path).ToLower();
    if (ext == ".cs" || ext == ".json" || ext == ".dll") {
        stopWebsite();
    }
};
// Function use to start file system watcher
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;
};
// Monitor plugin directory
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);
});

这段代码监视了插件文件夹下的cs, json, dll文件,
若是发生变化就调用IWebsiteStopper来终止网址,网址下一次打开时将会重复编写翻译和载入插件。
IWebsiteStopper是二个虚无的接口,在Asp.Net中甘休网址调用了HttpRuntime.UnloadAppDomain,而在Asp.Net Core中甘休网航站调度室用了IApplicationLifetime.StopApplication

Asp.Net结束网址会卸载当前的AppDomain,后一次刷新网页时会自动重新开动。
而Asp.Net Core停止网址会告少年老成段落当前的过程,使用IIS托管时IIS会在电动重启进度,但使用自宿主时则供给依附外界工具来重启。

写在结尾

ZKWeb达成的动态编译技巧大幅的缩减了开拓时的等待时间,
重大节省在无需每一遍都按神速键编写翻译和无需像别的模块化开拓相仿供给从子项目复制dll文件到主项目,假设dll文件相当多而且用了形而上学硬盘,复制时间可能会比编译时间还要长久。

本身将会在这里个博客继续共享ZKWeb框架中应用的工夫。
生机勃勃旦有不掌握的片段,招待参预ZKWeb调换群522083886叩问,

本文由澳门至尊网站发布于免费资源,转载请注明出处:ZKWeb网站框架的动态编译的实现原理

关键词: