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.NativeMicrosoft.DiaSymReader.PortablePdb才能支持生成pdb文件。
安装了这个包以后Roslyn会自动识别并使用。

实现动态编译插件系统的流程

在ZKWeb框架中,插件是一个文件夹,网站的配置文件中的插件列表就是文件夹的列表。
在网站启动时,会查找每个文件夹下的*.cs文件对比文件列表和修改时间是否与上次编译的不同,如果不同则重新编译该文件夹下的代码。
网站启动后,会监视*.cs*.dll文件是否有变化,如果有变化则重新启动网站以重新编译。
ZKWeb的插件文件夹结构如下

  • 插件文件夹

    • net: .Net Framework编译的程序集

    • netstandard: .Net Core编译的程序集

    • 插件名称.dll: 编译出来的程序集

    • 插件名称.pdb: 调试文件

    • CompileInfo.txt: 储存了文件列表和修改时间

    • 同net文件夹下的内容

    • bin:程序集文件夹

    • 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 Corevar parseOptions = CSharpParseOptions.Default;#if NETCOREparseOptions = parseOptions.WithPreprocessorSymbols("NETCORE");#endifvar 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 pluginLoadAssembliesFromUsings(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 directivevar 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.Threadingif (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 referencesvar assemblyLoader = Application.Ioc.Resolve<IAssemblyLoader>();var references = assemblyLoader.GetLoadedAssemblies().Select(assembly => assembly.Location).Select(path => MetadataReference.CreateFromFile(path)).ToList();

最后调用Roslyn编译,传入语法树列表和引用程序集列表可以得到目标程序集。
使用Emit函数编译后会返回一个EmitResult对象,里面保存了编译中出现的错误和警告信息。
注意编译出错时Emit不会抛出例外,需要手动检查EmitResult中的Success属性。

// Compile to assembly, throw exception if error occurredvar 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));
}

到此已经完成了代码文件(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 directlyreturn Assembly.Load(assemblyName);} catch {            // If failed, try to load it from plugin's reference directoryvar 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 existsname = 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 websiteAction stopWebsite = () => {    var stoppers = Application.Ioc.ResolveMany<IWebsiteStopper>();stoppers.ForEach(s => s.StopWebsite());
};// Function use to handle file changedAction<string> onFileChanged = (path) => {    var ext = Path.GetExtension(path).ToLower();    if (ext == ".cs" || ext == ".json" || ext == ".dll") {stopWebsite();}
};// Function use to start file system watcherAction<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 directoryvar 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询问,

相关文章: 

  • Roslyn项目系统简介

  • Roslyn开源第一年:试炼与凯旋

原文地址:http://www.cnblogs.com/zkweb/p/5857355.html


.NET社区新闻,深度好文,微信中搜索dotNET跨平台或扫描二维码关注

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/328124.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Hibernate基本概念 (4)

一、缓存&#xff1a;提高性能1.一级缓存&#xff1a;session级别 一个session共享2.二级缓存&#xff1a;进程或群集级别 不同session可以共享步骤&#xff1a;1.导jar包 2.添加xml放到src3.配置hibernate.cfg.xmla.开启二级缓存b。缓存管理类4.配置持久化类使用二级缓存 3…

Java中的内存泄露的几种可能

转载自 Java中的内存泄露的几种可能Java内存泄漏引起的原因&#xff1a;内存泄漏是指无用对象&#xff08;不再使用的对象&#xff09;持续占有内存或无用对象的内存得不到及时释放&#xff0c;从而造成内存空间的浪费称为内存泄漏。长生命周期的对象持有短生命周期对象的引用…

Visual Studio“15”启动速度提升

在Visual Studio“15”开发工作的技术预览阶段&#xff0c;微软称自己的主要目标之一是改善性能。他们已经对这些改进进行过一定程度的介绍&#xff0c;最近又通过更全面的信息进一步介绍了这些变化。本文将介绍这些让VS“15”启动速度更快的改进。 更快速地启动VS“15” 微软项…

介绍Java中的内存泄漏

转载自 介绍Java中的内存泄漏Java语言的一个关键的优势就是它的内存管理机制。你只管创建对象&#xff0c;Java的垃圾回收器帮你分配以及回收内存。然而&#xff0c;实际的情况并没有那么简单&#xff0c;因为内存泄漏在Java应用程序中还是时有发生的。 下面就解释下什么是内存…

2-6 基于SpringBoot的SpringSecurity环境快速搭建与验证

springboot是基于spring的一套全新的框架&#xff0c;其目的是为了简化spring的初始搭建和开发过程&#xff0c;不需要再做样板话的配置了 https://start.spring.io/ 上面这些都不想做权限拦截

DotLiquid模板引擎简介

DotLiquid是一个在.Net Framework上运行的模板引擎&#xff0c;采用Ruby的Liquid语法&#xff0c;这个语法广泛的用在Ruby on rails和Django等网页框架中。DotLiquid相比于Mvc默认模板引擎Razor的好处有&#xff1a; 因为不需要编译到程序集再载入首次渲染速度很快不会导致内存…

Hibernate基本概念 (5)

-----模板1.一对多(set)<set name"属性"><key column"关系外键"/><one-to-many class"实体类全名称"/></set>2.多对一<many-to-one name"" class"" column"关系外键"/>3.多对多(s…

Vue 阻止事件冒泡

转载自 Vue2学习笔记:事件对象、事件冒泡、默认行为1.事情对象<!DOCTYPE html> <html> <head><title></title><meta charset"utf-8"><script src"http://unpkg.com/vue/dist/vue.js"></script><scrip…

Windows Server 2016及System Center 2016正式商用

Windows Server 2016 及 System Center 2016 现已正式商用。作为微软全新一代的服务器操作系统和数据中心管理平台&#xff0c;它们将为企业 IT 带来全面的性能与安全性提升&#xff1b;为数据中心、私有云及公有云环境提供一致的混合云管理平台&#xff1b;并为在本地和云端开…

2-7 SpringBoot常用注解讲解

首先&#xff0c;讲解一下RestController RestController RestController是Controller和ResponseBody的结合。 RnableAutoConfiguration EnableAutoConfiguration springboot建议只能有一个有该注解的类 这个注解的作用是 根据你配置的依赖自动配置 根据jar包的配置…

vue-beauty UI库

vue-beauty UI库文档地址 一、全局配置全局CSS样式Polyfill二、组件&#xff08;1&#xff09;普通Button 按钮Icon 图标&#xff08;2&#xff09;布局Grid 栅格Layout 布局MorePanel 更多条件&#xff08;3&#xff09;导航Affix 固钉Breadcrumb …

ArrayList整理

ArrayList整理1&#xff0c;ArrayList特性2,ArrayList底层实现的特征1)&#xff0c;ArrayList初始化2)&#xff0c;初始容量3)&#xff0c;ArrayList的添加元素的add()方法4&#xff09;&#xff0c;ArrayList的删除方法remove(int index)其他的一些方法的操作其实都差不多&…

ASP.NET Core CORS 简单使用

CORS 全称"跨域资源共享"&#xff08;Cross-origin resource sharing&#xff09;。 跨域就是不同域之间进行数据访问&#xff0c;比如 a.sample.com 访问 b.sample.com 中的数据&#xff0c;我们如果不做任何处理的话&#xff0c;就会出现下面的错误&#xff1a; XM…

3-1 Apache Shiro权限管理框架介绍

Apache Shiro 这是一个功能强大的 shiro相对于security 更简单 易懂的授权方式

mybatis配置步骤

一&#xff0c;mybatis配置步骤 ​ 1&#xff0c;创建一个maven项目 ​ 2&#xff0c;在pom.xml文件中导入相关的jar包依赖 <properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><maven.compiler.source>1.8</maven…

vue的Prop属性

转载自 PropProp 的大小写 (camelCase vs kebab-case) HTML 中的特性名是大小写不敏感的&#xff0c;所以浏览器会把所有大写字符解释为小写字符。这意味着当你使用 DOM 中的模板时&#xff0c;camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名…

.NET Core 1.1 Preview 1上线:支持macOS 10.12/Linux Mint 18

自2014年以来微软陆续对.NET Framework的核心组件进行开源&#xff0c;去年2月公司完成进度并向开源社区发布.NET CoreCLR。经过一年多的发展&#xff0c;开发者于今年6月获得.NET Core 1.0&#xff1b;而现在公司再次推出了1.1 Preview 1版本。 本次版本更新包括添加了多款Lin…

3-7 基于SpringBoot的Apache Shiro环境快速搭建与配置实操

去网站上 spring.io https://start.spring.io/ 去网站拉一个模板下拉 下载一个模板 打开后看一下 使用的pom.xml 我们要用到数据库 使用一个数据库的管理 阿里巴巴的druid 这个是sping非常常用的工具包经常使用的 字符串日期操作都使用这个 springframwork是…

反射、HashMap、ArrayList与LinkedList区别

1&#xff0c;反射机制答&#xff1a;JAVA反射机制是在运行状态中&#xff0c;对于任意一个类&#xff0c;都能够知道这个类的所有属性和方法&#xff1b;对于任意一个对象&#xff0c;都能够调用它的任意方法和属性&#xff1b;这种动态获取信息以及动态调用对象方法的功能称为…

vue非编译的模块化写法

项目目录 ●/ ●index.html ●js/ ●main.js ●myComponent.js ●routes.js ●textComponent.js ●lib/ ●vue.js ●vue-router.js ●require.min.js 一、构建require.js环境 &#xff08;1&#x…