ZKWeb网站框架的动态编译的实现原理
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.Native和Microsoft.DiaSymReader.PortablePdb才能支持生成pdb文件。
安装了这个包以后Roslyn会自动识别并使用。
实现动态编译插件系统的流程
在ZKWeb框架中,插件是一个文件夹,网站的配置文件中的插件列表就是文件夹的列表。
在网站启动时,会查找每个文件夹下的*.cs文件对比文件列表和修改时间是否与上次编译的不同,如果不同则重新编译该文件夹下的代码。
网站启动后,会监视*.cs和*.dll文件是否有变化,如果有变化则重新启动网站以重新编译。
ZKWeb的插件文件夹结构如下
- 插件文件夹
- bin:程序集文件夹
- net: .Net Framework编译的程序集
- 插件名称.dll: 编译出来的程序集
- 插件名称.pdb: 调试文件
- CompileInfo.txt: 储存了文件列表和修改时间
- netstandard: .Net Core编译的程序集
- 同net文件夹下的内容
- net: .Net Framework编译的程序集
- src 源代码文件夹
- static 静态文件的文件夹
- 其他文件夹……
- bin:程序集文件夹
通过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;会尝试载入System和System.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上,必须设置这个标志,具体可以看https://github.com/dotnet/roslyn/issues/13267。
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("\r\n",
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网站框架的动态编译的实现原理的更多相关文章
- ZKWeb网站框架介绍
框架地址 https://github.com/zkweb-framework/ZKWeb https://github.com/zkweb-framework/ZKWeb.Plugins 新的文档地 ...
- [译]MVC网站教程(一):多语言网站框架
本文简介 本博文介绍了 Visual Studio 工具生成的 ASP.NET MVC3 站点的基本框架:怎样实现网站的语言的国际化与本地化功能,从零开始实现用户身份认证机制,从零开始实现用户注册机制 ...
- (转载)JAVA动态编译--字节代码的操纵
在一般的Java应用开发过程中,开发人员使用Java的方式比较简单.打开惯用的IDE,编写Java源代码,再利用IDE提供的功能直接运行Java 程序就可以了.这种开发模式背后的过程是:开发人员编写的 ...
- java动态编译笔记
1 前言 Java的动态编译知识,真真在实际开发中并不是经常遇到.但是学习java动态编译有助于我们从更深一层次去了解java.对掌握jdk的动态代理模式,这样我们在学习其他一些开源框架的时候就能够知 ...
- .NET中的动态编译
代码的动态编译并执行是一个.NET平台提供给我们的很强大的工具用以灵活扩展(当然是面对内部开发人员)复杂而无法估算的逻辑,并通过一些额外的代码来扩展我们已有 的应用程序.这在很大程度上给我们提供了另外 ...
- JIT(动态编译)和AOT(静态编译)编译技术比较
Java 应用程序的性能经常成为开发社区中的讨论热点.因为该语言的设计初衷是使用解释的方式支持应用程序的可移植性目标,早期 Java 运行时所提供的性能级别远低于 C 和 C++ 之类的编译语言.尽管 ...
- [改善Java代码]慎用动态编译
建议17: 慎用动态编译 //=========这篇博文暂时理解不透......... 动态编译一直是Java的梦想,从Java 6版本它开始支持动态编译了,可以在运行期直接编译.java文件,执行. ...
- ASP.NET 动态编译、预编译和 WebDeployment 项目(转)
概述 在 Web 服务器上,既可以部署源文件,也可以部署编译后程序集. 若部署源文件,则当用户访问时,Web 应用程序会被动态编译,并缓存该程序集,以便下次访问. 否则,若部署程序集,Web 应用程序 ...
- 动态编译添加php模块
注意:转载请注明出处:http://www.programfish.com/blog/?p=85 在很多时候我们用linux里搭建web服务器的时候会需要编译安装php套件,而在编译安装后可能又会需要 ...
随机推荐
- JavaWeb——Filter
一.基本概念 之前我们用一篇博文介绍了Servlet相关的知识,有了那篇博文的知识积淀,今天我们学习Filter将会非常轻松,因为Filter有很多地方和Servlet类似,下面在讲Filter的时候 ...
- MVC Core 网站开发(Ninesky) 2.1、栏目的前台显示
上次创建了栏目模型,这次主要做栏目的前台显示.涉及到数据存储层.业务逻辑层和Web层.用到了迁移,更新数据库和注入的一些内容. 一.添加数据存储层 1.添加Ninesky.DataLibrary(与上 ...
- AFNetworking 3.0 源码解读(十)之 UIActivityIndicatorView/UIRefreshControl/UIImageView + AFNetworking
我们应该看到过很多类似这样的例子:某个控件拥有加载网络图片的能力.但这究竟是怎么做到的呢?看完这篇文章就明白了. 前言 这篇我们会介绍 AFNetworking 中的3个UIKit中的分类.UIAct ...
- var和dynamic的区别
1.var 1.均是声明动态类型的变量. 2.在编译阶段已经确定类型,在初始化的时候必须提供初始化的值. 3.无法作为方法参数类型,也无法作为返回值类型. 2.dynamic 1.均是声明动态类型的变 ...
- CSS 3学习——transition 过渡
以下内容根据官方规范翻译以及自己的理解整理. 1.介绍 这篇文档介绍能够实现隐式过渡的CSS新特性.文档中介绍的CSS新特性描述了CSS属性的值如何在给定的时间内平滑地从一个值变为另一个值. 2.过渡 ...
- 基于Composer Player 模型加载和相关属性设置
主要是基于达索软件Composer Player.的基础上做些二次开发. public class ComposerToolBarSetting { public bool AntiAliasingO ...
- 访问者模式(visitorpattern)
/** * 访问者模式 * @author TMAC-J * 在客户端和元素之间添加一个访问者 * 当你需要添加一些和元素关系不大的需求时,可以直接放在访问者里面 * 或者是元素之间有一些公共的代码块 ...
- C# 工厂模式+虚方法(接口、抽象方法)实现多态
面向对象语言的三大特征之一就是多态,听起来多态比较抽象,简而言之就是同一行为针对不同对象得到不同的结果,同一对象,在不同的环境下得到不同的状态. 实例说明: 业务需求:实现一个打开文件的控制台程序的d ...
- SSIS 包部署 Package Store 后,在 IS 中可以执行,AGENT 执行却报错
可以执行 SSIS Package ,证明用 SSIS Package 的账户是可以执行成功的.SQL Server Agent 默认指定账号是 Network Service. 那么可以尝试一下将 ...
- Web前端需要熟悉大学里【高大上】的计算机专业课吗?
作为一名刚刚大学毕业,进入新的学习阶段的研究生,我必须说大学的专业课非常重要!不管你信不信,事实就是如此! 一.大学学习的专业课非常重要,它决定了我们能走到什么高度 前端的发展非常快,我常常觉得刚刚关 ...