日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > asp.net >内容正文

asp.net

.NET 为大型应用接入 ApplicationStartupManager 启动流程框架

發布時間:2023/12/4 asp.net 49 豆豆
生活随笔 收集整理的這篇文章主要介紹了 .NET 为大型应用接入 ApplicationStartupManager 启动流程框架 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

對于大型的應用軟件,特別是客戶端應用軟件,應用啟動過程中,需要執行大量的邏輯,包括各個模塊的初始化和注冊等等邏輯。大型應用軟件的啟動過程都是非常復雜的,而客戶端應用軟件是對應用的啟動性能有所要求的,不同于服務端的應用軟件。設想,用戶雙擊了桌面圖標,然而等待幾分鐘,應用才啟動完畢,那用戶下一步會不會就是點擊卸載了。為了權衡大型應用軟件在啟動過程,既需要執行復雜的啟動邏輯,又需要關注啟動性能,為此過程造一個框架是一個完全合理的事情。我所在的團隊為啟動過程造的庫,就是本文將要和大家介紹我所在團隊開源的 dotnetCampus.ApplicationStartupManager 啟動流程框架的庫

背景

這個庫的起源是一次聽 VisualStudio 團隊的分享,當時大佬們告訴我,為了優化 VisualStudio 的啟動性能,他的團隊制定了一個有趣的方向,那就是在應用啟動的時候將 CPU 和內存和磁盤跑滿。當然,這是一個玩笑的話,本來的意思是,在 VisualStudio 應用啟動的時候,應該充分壓榨計算機的性能。剛好,我所在的團隊也有很多個大型的應用,代碼的 MergeRequest 數都破萬的應用。這些應用的邏輯復雜度都是非常高的,原本只能是采用單個線程執行,從而減少模塊之間的依賴復雜度導致的坑。但在后續為了優化應用軟件的啟動性能,考慮到進行機器性能的壓榨策略,其中就包括了多線程的方式

然而在開多線程的時候,自然就會遇到很多線程相關的問題,最大的問題就是如何處理各個啟動模塊之間的依賴關系。如果沒有一個較好的框架來進行處理,只靠開發者的個人能力來處理,做此重構是完全不靠譜的,或者說這個事情是做不遠的,也許這個版本能優化,但下個版本呢

還有一點非常重要的是如何做啟動性能的監控,如分析各個啟動項的耗時情況。在進行逐個啟動業務模塊的性能優化之前,十分有必要進行啟動模塊的性能測量。而有趣的是,啟動模塊是非常和妖魔的用戶環境相關的,也就是在實驗室里測量的結果,和實際的用戶使用的結果是有很大的誤差的。這也就給啟動流程框架提了一個重要的需求,那就是能支持方便的對各個啟動模塊進行性能測量監控

由于有多個項目都期望接入啟動流程框架,因此啟動流程框架應該做到足夠的抽象,最好不能有耦合單一項目的功能

經過了大概一年的開發時間,在 2019 年正式將啟動流程框架投入使用。當前在近千萬臺設備上跑著啟動流程框架的邏輯

當前此啟動流程框架的庫在 GitHub 上,基于最友好的 MIT 協議,也就是大家可以隨便用的協議進行開源,開源地址:?https://github.com/dotnet-campus/dotnetCampus.ApplicationStartupManager

功能

我所在的團隊開源的?ApplicationStartupManager?啟動流程框架的庫提供了如下的賣點

  • 自動構建啟動流程圖

  • 支持高性能異步多線程的啟動任務項執行

  • 支持 UI 線程自動調度邏輯

  • 動態分配啟動任務資源

  • 支持接入預編譯框架

  • 支持所有的 .NET 應用

  • 啟動流程耗時監控

啟動流程圖

各個啟動任務項之間,必然存在顯式或隱式依賴,如依賴某個邏輯或模塊初始化,或者依賴某個服務的注冊,或者有執行時機的依賴。在開發者梳理完成依賴之后,給各個啟動任務項確定相互之間的依賴關系,即可根據此依賴關系構建出啟動流程圖

假設有以下幾個啟動任務項,啟動任務項之間有相互的依賴關系,如下圖,使用箭頭表示依賴關系

  • 啟動任務項 A :最先啟動的啟動任務項,如日志或容器的初始化啟動任務項

  • 啟動任務項 B :一些基礎服務,但是需要依賴 A 啟動任務項完成才能執行

  • 啟動任務項 C :依賴 B 啟動任務項的執行完成

  • 啟動任務項 D :另一個獨立的模塊,和 B C E 啟動任務項沒有聯系,但是也依賴 A 啟動任務項的完成

  • 啟動任務項 E :同時依賴 B C 啟動任務項的完成

  • 啟動任務項 F :同時依賴 A D 啟動任務項的完成

以上的啟動任務項可以構成一個有向無環啟動流程圖,每個啟動任務項都可以有自己的前置或后置。那為什么需要是無環呢?要是有兩個啟動任務項是相互等待依賴的,那就自然就無法成功啟動了,如下圖,有三個啟動任務項都在相互依賴,那也就是說無論哪個啟動任務項先啟動,都是不符合預期的,因為先啟動的啟動任務項的前置沒有被滿足,啟動過程中邏輯上是存在有前置依賴沒有執行

為了更好的構建啟動流程圖,在邏輯上也加上了兩個虛擬的節點,那就是啟動點和結束點,無論是哪個啟動任務項,都會依賴虛擬的啟動點,以及都會跟隨著結束點

另外,具體業務方也會定義自己的關聯啟動過程,也就是預設的啟動節點,關鍵啟動過程點將被各個啟動項所依賴,如此即可人為將啟動過程分為多個階段

例如可以將啟動過程分為如下階段

  • 啟動點:虛擬的節點,表示應用啟動,用于構建啟動流程圖

  • 基礎設施:表示在此之前應該做啟動基礎服務的邏輯,例如初始化日志,初始化容器等等。其他啟動任務項可以依賴基礎設施,從而認為在基礎設施之后執行的啟動任務項,基礎設施已準備完成

  • 窗口啟動:在客戶端程序的窗口初始化之前,需要完成 UI 的準備邏輯,例如樣式資源和必要的數據準備,或者 ViewModel 的注入等。在窗口啟動之后,即可對 UI 元素執行邏輯,或者注冊 UI 強相關邏輯。或者是在窗口啟動之后,執行那些不需要在主界面顯示之前執行的啟動任務項,從而提升主界面顯示性能

  • 應用啟動:完成了啟動的邏輯,在應用啟動之后的啟動任務項都是屬于可以慢慢執行的邏輯,例如觸發應用的自動更新,例如執行一下日志文件清理等等

  • 結束點:虛擬的節點,表示應用啟動過程完全完成,用于構建啟動流程圖

如圖,每個啟動任務項可以選擇依賴的是具體的某個啟動任務項,也可以選擇依賴的是關鍵啟動過程點

通過此邏輯,可以為后續的優化做準備,也方便上層業務開發者開發業務層的啟動任務項。讓上層業務開發者可以比較清晰了解自己新寫的啟動任務項應該放在哪個地方,也可以提供了調試各個模塊的啟動任務項的依賴情況,了解是否存在循環的依賴邏輯

高性能異步多線程的啟動任務項執行

為了更好的壓榨機器性能,進行多線程啟動是必要的。在完成了啟動流程圖的構建之后,即可將啟動任務項畫成樹形,自然也就方便進行多線程調度。基于 .NET 的 Task 方式調度,可以實現多線程異步等待,解決多個啟動任務項的依賴在多線程情況下的線程安全問題

如使用線程池的 Task 調度,可以從邏輯上,將不同的啟動任務項的啟動任務鏈劃分為給不同的線程執行。實際執行的線程是依靠線程池調度,甚至實際執行上,線程池只是用了兩個實際線程在執行

對應用的啟動過程中,在不明白 .NET 線程池調度機制的情況下,將在開啟多線程問題上稍微有一點爭議。核心爭議的就是如果一個應用啟動過程中,占滿了 CPU 資源,是否就讓用戶電腦卡的不能動了。其實上面這個問題不好回答,如果大家有此疑惑,那就請聽我細細分析一下。首先一點就是問題本身,先問 問題 本身一個問題,如果只是開一個線程啟動,會不會也讓用戶的電腦卡的不能動了?答案是 是的,完全取決于用戶電腦,包括電腦配置以及電腦的妖魔環境,例如一個渣配的設備配合國產的好幾個殺毒軟件一起,那么在應用啟動的瞬間,就有大量的殺毒工作在執行,自然就卡的不能動了。而且,電腦卡的不能動了,是不是和 CPU 被占滿是必然關系?答案是 完全不是,應用啟動過程中,一定會存在 DLL 加載的過程,特別是應用的冷啟動過程,大量的文件讀寫,對于一些機械盤來說,將會占滿磁盤的讀寫,自然也就能讓電腦卡的不能動了,這個過程和是否開啟多線程,其實關系很小,畢竟機械盤和 CPU 之間的性能擺在這。第二個是卡的時間是否重要,例如應用開了多線程就卡了 500 毫秒,而如果應用啟動只用單線程則需要 4 x 500ms = 2s 的耗時,那是否此時開多線程劃得來呢?這個是需要權衡的,不同的應用邏輯自然不同,例如生產力工具,我本來開機就是為了用此工具,例如寫代碼用的 VisualStudio 工具,我打開了這個應用,過程中自然沒有其他同步使用的需求,卡了就卡了咯。最后一個問題就是,開啟 .NET 的多線程完全不等于占滿了 CPU 資源,別忘了 IO 異步哦

當然了,會接入應用流程的開發者肯定不屬于新手,相信對于線程方面知識已有所了解,會自己選擇合適的方式執行啟動任務項。這也側面告訴大家,本啟動流程框架的庫接入是有一定的門檻的

支持 UI 線程自動調度邏輯

對于客戶端應用,自然有一個特殊的線程是 UI 線程,啟動過程,有很多邏輯是需要在 UI 線程執行的。由于 .NET 系的各個應用框架的 UI 線程調度都不咋相同,因此需要啟動流程框架執行一定量的適配

在具體的啟動任務項上標記當前的啟動任務項需要在 UI 線程執行即可,框架層將會自動調度啟動任務項到 UI 線程執行

設計上,默認將會調度啟動任務項到非 UI 線程執行

動態分配啟動任務資源

在用戶端的各個啟動任務項的耗時和在實驗室里測試的結果,無論是開發機還是測試機,大多數時候都是有很大的差值的。如果按照固定的順序去執行啟動任務項,自然有很多啟動時間都在空白的等待上。本啟動流程框架庫支持在啟動過程中,自動根據各個啟動任務項的耗時,動態進行調度

核心方法就是構建出來的啟動流程圖,支持各個任務的等待邏輯,基于 Task 等待機制,即可進行動態調度等待邏輯,從而實現動態編排啟動任務項,在緊湊的時間內讓多條線程排滿啟動任務的執行。如果對應的上層業務開發者能正確使用 Task 機制,例如正確使用異步等待,可以實現在啟動過程中極大隱藏

支持接入預編譯框架

啟動過程是屬于性能敏感的部分,各個模塊的啟動任務項如何收集是一個很大的問題。啟動部分屬于性能敏感部分,不合適采用反射的機制。好在?dotnet campus?里面有技術儲備,在 2018 年的時候就開源了?SourceFusion?預編譯框架,后面在 2020 年時吸取了原有?SourceFusion?的挖坑經驗,重新開源了?dotnetCampus.Telescope?預編譯框架,新開源的?dotnetCampus.Telescope?也放在?SourceFusion?倉庫中

在?ApplicationStartupManager?啟動流程框架開發之初就考慮了對接預編譯框架,通過預編譯提供了無須反射即可完成啟動任務項收集的能力,可以極大減少因為啟動過程中反射程序集的性能損耗

對接了預編譯框架,相當于原本需要在用戶端執行的邏輯的時間,搬到開發者編譯時,在開發者編譯時執行了原本需要在用戶端執行的邏輯。如此可以減少用戶端的執行邏輯的時間

接入了預編譯框架,可以實現在開發者編譯時,將所有項目的啟動任務項收集起來,包括啟動任務項類型和委托創建啟動任務項,以及啟動任務項的 Attribute 特性

啟動流程耗時監控

對于大型應用來說,很重要的一點就是關注在用戶端的運行效果。啟動過程中,監控是十分重要的。監控最大的意義在于:

第一,可以了解到在用戶設備上,各個啟動任務項的實際執行耗時情況,從而在后續版本進行性能優化的時候,有數據支撐。否則憑借在開發或測試端有限的設備上,很難跑出真正的性能瓶頸。如不僅關注在用戶設備上的 95 線啟動分布,所謂 95 線就是在百分之九十五的用戶上的啟動耗時分布,也可以關注關注 95 線到 99 線中間的用戶的啟動分布,了解一些比較特殊的設備的環境,從而做特別的優化

第二,可以做版本對比,做預警。對于大型應用,基本都有灰發和預發機制,通過在灰發過程中監控啟動耗時,可以對接預警機制,在某個啟動任務項耗時上升時告訴開發者。如此可以有利項目的長遠開發

最后一點,是可以告訴用戶,啟動的慢,是慢在哪一步。這個機制集中在提供了開放性上,例如 Visual Studio 將會不斷告訴你,啟動慢是哪個插件導致的

使用方法

在抽離了各個項目的定制化需求之后,啟動流程框架的庫只有核心的邏輯,這也就意味著在使用的時候,還需要具體的業務方自己加入初始化邏輯和適配業務的具體邏輯。換句話說是,接入啟動流程框架不是簡單安裝一下庫,然后調用 API 即可,而是需要根據應用的業務需求,進行一部分對接的工作。好在啟動流程框架只有在大型項目或者預期能做到大型的項目才適用,相比于大型應用的其他邏輯,對接啟動流程框架的代碼量基本可以忽略。對于小型項目或非多人協作的項目,自然是不合適的

整個?ApplicationStartupManager?啟動流程框架設計上是高性能的,減少各個部分的性能內損。但是在上啟動流程框架本身就存在一定的框架性能損耗,如果對應的只是小項目或非多人協作的項目,假設可以自己編排啟動任務項,那自然自己編排啟動任務項如此做是能達到性能最高的

應用?ApplicationStartupManager?啟動流程框架能解決的矛盾點在于項目的復雜度加上多人協作的溝通,與啟動性能之間的矛盾。接入啟動流程框架可以讓上層業務開發者屏蔽對啟動過程細節的干擾,方便上層業務開發者根據業務需求加入啟動任務項,方便啟動模塊維護者定位和處理啟動任務項的性能

按照慣例,在使用 .NET 的某個庫的第一步就是通過 NuGet 安裝庫

第一步使用 NuGet 安裝?ApplicationStartupManager?庫。如果項目使用 SDK 風格的項目文件格式,可以在 csproj 項目文件上添加如下的代碼進行安裝

<ItemGroup><PackageReference Include="dotnetCampus.ApplicationStartupManager" Version="0.0.1-alpha01" /></ItemGroup>

為了方便讓大家看到?ApplicationStartupManager?啟動流程框架庫的效果,我采用了放在?https://github.com/dotnet-campus/dotnetCampus.ApplicationStartupManager?里的例子代碼來作為例子

新建三個項目,分別如下

  • WPFDemo.Lib1:代表底層的各個組件庫,特別指業務組件

  • WPFDemo.Api:應用的 API 層的程序集,將在這里部署啟動流程的框架邏輯

  • WPFDemo.App:應用的頂層,也就是 Main 函數所在的程序集,在這里觸發啟動的邏輯

大概的抽象之后的應用的模型架構如下,不過為了演示方便,就將 Business 層和 App 層合一,將眾多的 Lib 組件合為一個 Lib1 項目

新建完成項目,也安裝完成 NuGet 包,現在就是開始在 API 層搭建應用相關聯的啟動框架邏輯。為什么在安裝完成了 NuGet 包之后,還需要 API 做額外的邏輯?每個應用都有自己獨特的邏輯,每個應用的啟動任務項所需的參數是不相同的,每個應用的日志記錄方式也可以是不相同的,不同類型的應用的啟動節點也是不相同的,如此這些都是需要做應用相關的定制的

先定義應用相關的預設的啟動節點

/// <summary>/// 包含預設的啟動節點。/// </summary>public class StartupNodes{/// <summary>/// 基礎服務(日志、異常處理、容器、生命周期管理等)請在此節點之前啟動,其他業務請在此之后啟動。/// </summary>public const string Foundation = "Foundation";/// <summary>/// 需要在任何一個 Window 創建之前啟動的任務請在此節點之前。/// 此節點之后將開始啟動 UI。/// </summary>public const string CoreUI = "CoreUI";/// <summary>/// 需要在主 <see cref="Window"/> 創建之后啟動的任務請在此節點之后。/// 此節點完成則代表主要 UI 已經初始化完畢(但不一定已顯示)。/// </summary>public const string UI = "UI";/// <summary>/// 應用程序已完成啟動。如果應該顯示一個窗口,則此窗口已布局、渲染完畢,對用戶完全可見,可開始交互。/// 不被其他業務依賴的模塊可在此節點之后啟動。/// </summary>public const string AppReady = "AppReady";/// <summary>/// 任何不關心何時啟動的啟動任務應該設定為在此節點之前完成。/// </summary>public const string StartupCompleted = "StartupCompleted";}

定義完成之后,即可通過此將啟動過程分為如下階段

再定義一個和應用業務方相關的日志類型,不同的應用記錄日志的方式大部分都是不相同的,所使用的底層日志記錄也都是不相同的

/// <summary>/// 和項目關聯的日志/// </summary>public class StartupLogger : StartupLoggerBase{public void LogInfo(string message){Debug.WriteLine(message);}public override void ReportResult(IReadOnlyList<IStartupTaskWrapper> wrappers){var stringBuilder = new StringBuilder();foreach (var keyValuePair in MilestoneDictionary){stringBuilder.AppendLine($"{keyValuePair.Key} - [{keyValuePair.Value.threadName}] Start:{keyValuePair.Value.start} Elapsed:{keyValuePair.Value.elapsed}");}Debug.WriteLine(stringBuilder.ToString());}}

如例子上的日志就是記錄到?Debug.WriteLine?輸出,同時日志里也添加了 LogInfo 方法

繼續定制應用業務相關的啟動任務項的參數,如例子代碼的項目就用到了?dotnetCampus.CommandLine?提供的命令行參數解析,各個啟動任務項也許會用到命令行參數,因此也就需要帶入到啟動任務項的參數里面,作為一個屬性。例子代碼的項目也用到了?dotnetCampus.Configurations 高性能配置文件庫?提供的應用軟件配置功能,也是各個啟動任務項所需要的,放入到啟動任務項的參數

加上和應用業務相關的屬性之后的啟動任務項的參數定義如下

public class StartupContext : IStartupContext{public StartupContext(IStartupContext startupContext, CommandLine commandLine, StartupLogger logger, FileConfigurationRepo configuration, IAppConfigurator configs){_startupContext = startupContext;Logger = logger;Configuration = configuration;Configs = configs;CommandLine = commandLine;CommandLineOptions = CommandLine.As<Options>();}public StartupLogger Logger { get; }public CommandLine CommandLine { get; }public Options CommandLineOptions { get; }public FileConfigurationRepo Configuration { get; }public IAppConfigurator Configs { get; }public Task<string> ReadCacheAsync(string key, string @default = ""){return Configuration.TryReadAsync(key, @default);}private readonly IStartupContext _startupContext;public Task WaitStartupTaskAsync(string startupKey){return _startupContext.WaitStartupTaskAsync(startupKey);}}

為了繼續承接 WaitStartupTaskAsync 的功能,于是構造函數依然帶上 IStartupContext 用于獲取框架里默認提供的啟動任務項的參數。上面代碼的?Configuration?和?Configs?兩個屬性都是?dotnetCampus.Configurations 高性能配置文件庫提供的功能,可以使用 COIN 格式進行配置文件的讀寫

完成了啟動任務項的參數的定義,就可以來定制具體應用的啟動任務項的基類型了。因為啟動任務項的基類型一定是和啟動任務項的參數相關,而啟動任務項的參數每個應用都有所不同,因此啟動任務項的基類型也就不同。即使不同的程度只有啟動任務項的參數,代碼層面可以使用泛形來解決,但也會因為泛形的將會讓業務層的代碼量較多,不如在應用上再定義

/// <summary>/// 表示一個和當前業務強相關的啟動任務/// </summary>public class StartupTask : StartupTaskBase{protected sealed override Task RunAsync(IStartupContext context){return RunAsync((StartupContext) context);}protected virtual Task RunAsync(StartupContext context){return CompletedTask;}}

如上代碼,所有的應用的業務端都應該繼承 StartupTask 作為啟動任務項的基類。繼承之后,依然是重寫 RunAsync 方法,在此方法里面執行業務邏輯

這里設計上讓 RunAsync 作為一個虛方法而不是一個抽象方法是因為有一些應用業務上需要一點占坑用的啟動任務項,這些啟動任務項沒有實際邏輯功能,只是為了優化啟動流程的編排而添加。另外重要的一點在于可以讓上層業務開發者在編寫到一些只有同步的邏輯時,解決不知道如何返回 RunAsync 的 Task 的問題,可以讓上層業務開發者自然返回 base.RunAsync 方法的結果,從而減少了各個詭異的返回 Task 的方法

在完成了定制啟動任務基類型之后,就需要編寫基于 StartupManagerBase 的和應用業務相關的 StartupManager 類型,在這里的邏輯需要包含如何啟動具體的啟動任務項的邏輯,代碼如下

/// <summary>/// 和項目關聯的啟動管理器,用來注入業務相關的邏輯/// </summary>public class StartupManager : StartupManagerBase{public StartupManager(CommandLine commandLine, FileConfigurationRepo configuration, Func<Exception, Task> fastFailAction, IMainThreadDispatcher mainThreadDispatcher) : base(new StartupLogger(), fastFailAction, mainThreadDispatcher){var appConfigurator = configuration.CreateAppConfigurator();Context = new StartupContext(StartupContext, commandLine, (StartupLogger) Logger, configuration, appConfigurator);}private StartupContext Context { get; }protected override Task<string> ExecuteStartupTaskAsync(StartupTaskBase startupTask, IStartupContext context, bool uiOnly){return base.ExecuteStartupTaskAsync(startupTask, Context, uiOnly);}}

以上代碼通過重寫 ExecuteStartupTaskAsync 方法實現在調用具體的啟動任務項傳入業務相關的 StartupContext 參數

如果應用有更多的需求,可以重寫 StartupManagerBase 更多方法,包括導出所有的啟動項的 ExportStartupTasks 方法,重寫此方法可以讓應用定義如何導出所有的啟動任務項。重寫 AddStartupTaskMetadataCollector 方法可以讓應用定義如何加入被管理的程序集中的啟動信息等

以上幾步完成之后,還有一項需要完成的是,剛才新建的 WPFDemo.Api 項目其實沒有加上 WPF 的依賴,而在應用里面,是有啟動任務項需要依賴在 UI 線程執行,于是就在加上 WPF 的依賴的 WPFDemo.App 上完成定義

class MainThreadDispatcher : IMainThreadDispatcher{public async Task InvokeAsync(Action action){await Application.Current.Dispatcher.InvokeAsync(action);}}

以上的基礎完成之后,就可以在 Program.cs 的主函數將啟動框架跑起來,進入到 WPFDemo.App 項目的 Program 類型,在主函數里面先解析命令行,然后再創建 App 再跑起啟動框架

[STAThread]static void Main(string[] args){var commandLine = CommandLine.Parse(args);var app = new App();//開始啟動任務StartStartupTasks(commandLine);app.Run();}

在 StartStartupTasks 方法里面使用 Task.Run 的方式在后臺線程跑起來啟動框架,如此可以讓主線程也就是此應用的 UI 線程開始跑起來界面相關邏輯

private static void StartStartupTasks(CommandLine commandLine){Task.Run(() =>{// 1. 讀取應用配置// 應用將會根據配置決定啟動的行為var configFilePath = "App.coin";var repo = ConfigurationFactory.FromFile(configFilePath);// 2. 對接預編譯模塊,獲取啟動任務項var assemblyMetadataExporter = new AssemblyMetadataExporter(BuildStartupAssemblies());// 3. 創建啟動框架和跑起來var startupManager = new StartupManager(commandLine, repo, HandleShutdownError, new MainThreadDispatcher())// 3.1 導入預設的應用啟動節點,這是必要的步驟,業務方的各個啟動任務項將會根據此決定啟動順序.UseCriticalNodes(StartupNodes.Foundation,StartupNodes.CoreUI,StartupNodes.UI,StartupNodes.AppReady,StartupNodes.StartupCompleted)// 3.2 導出程序集的啟動項.AddStartupTaskMetadataCollector(() =>// 這是預編譯模塊收集的應用的所有的啟動任務項assemblyMetadataExporter.ExportStartupTasks());startupManager.Run();});}

以上的例子應用里面,有業務是需要根據配置決定啟動過程,因此需要先讀取應用配置。應用配置選取?dotnetCampus.Configurations 高性能配置文件庫?可以極大減少因為讀取配置而占用太多啟動時間。以上的例子里,還對接了預編譯模塊。預編譯模塊的功能是收集應用里的所有啟動任務項,如此可以極大提升收集啟動任務項的耗時,也不需要讓上層業務開發者需要手工注冊啟動任務項

以上代碼即可實現在 Main 函數啟動之后,跑起來啟動框架。不過上面代碼編譯還不能通過,因為還沒有完成 AssemblyMetadataExporter 的邏輯,這個預編譯模塊相關邏輯

這不等價于這套啟動框架強依賴于預編譯模塊,而是說可選接入預編譯模塊。只需要有任何的邏輯,能對接 AddStartupTaskMetadataCollector 方法,在此方法里面能傳入獲取應用所需的啟動任務項即可。無論使用任何的方式,包括反射等都是可以的。接入預編譯模塊只是為了優化性能,減少收集啟動任務項的耗時

接下來就是預編譯模塊的接入邏輯,本文不涉及 Telescope 預編譯模塊的原理部分,只包含如何接入的方法

和 .NET 的其他庫一樣,為了接入預編譯模塊,就需要先安裝 NuGet 庫。通過 NuGet 安裝?dotnetCampus.Telescope?庫,如果是新 SDK 風格的項目文件,可以編輯 csproj 項目文件,添加如下代碼安裝

<ItemGroup><PackageReference Include="dotnetCampus.TelescopeSource" Version="1.0.0-alpha02" /></ItemGroup>

不同于其他的庫,由于?dotnetCampus.Telescope?預編譯框架是對項目代碼本身進行處理的,需要每個用到預編譯都安裝此庫,因此需要為以上三個項目都安裝,而不能靠引用依賴自動安裝

安裝完成之后,在項目上新建一個 AssemblyInfo.cs 的文件,給程序集添加特性。按照約定,需要將 AssemblyInfo.cs 文件放入到 Properties 文件夾里面。這個 Properties 文件夾算是一個特別的文件夾,在 Visual Studio 里新建就可以看到此文件夾的圖標和其他文件夾不相同

在 AssemblyInfo.cs 文件里面添加如下代碼

[assembly: dotnetCampus.Telescope.MarkExport(typeof(WPFDemo.Api.StartupTaskFramework.StartupTask), typeof(dotnetCampus.ApplicationStartupManager.StartupTaskAttribute))]

以上就是對接預編譯框架的代碼,十分簡單。通過給程序集加上?dotnetCampus.Telescope.MarkExportAttribute?可以標記程序集的導出預編譯的類型,傳入的兩個參數分別是導出的類型的基類型以及所繼承的特性

以上代碼表示導出所有繼承?WPFDemo.Api.StartupTaskFramework.StartupTask?類型,且標記了?otnetCampus.ApplicationStartupManager.StartupTaskAttribute?特性的類型

標記之后,重新構建代碼,將會在 obj 文件夾找到 AttributedTypesExport.g.cs 生成文件,如在本文的例子項目里面,生成文件的路徑如下

C:\lindexi\Code\ApplicationStartupManager\demo\WPFDemo\WPFDemo.Api\obj\Debug\net6.0\TelescopeSource.GeneratedCodes\AttributedTypesExport.g.cs

假設有一個叫 Foo1Startup 的啟動任務項定義如下

[StartupTask(BeforeTasks = StartupNodes.CoreUI, AfterTasks = StartupNodes.Foundation)]public class Foo1Startup : StartupTask{protected override Task RunAsync(StartupContext context){context.Logger.LogInfo("Foo1 Startup");return base.RunAsync(context);}}

那么生成的 AttributedTypesExport.g.cs 將包含以下代碼

using dotnetCampus.ApplicationStartupManager; using dotnetCampus.Telescope; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using WPFDemo.Api.StartupTaskFramework;namespace dotnetCampus.Telescope {public partial class __AttributedTypesExport__ : ICompileTimeAttributedTypesExporter<StartupTask, StartupTaskAttribute>{AttributedTypeMetadata<StartupTask, StartupTaskAttribute>[] ICompileTimeAttributedTypesExporter<StartupTask, StartupTaskAttribute>.ExportAttributeTypes(){return new AttributedTypeMetadata<StartupTask, StartupTaskAttribute>[]{new AttributedTypeMetadata<StartupTask, StartupTaskAttribute>(typeof(WPFDemo.Api.Startup.Foo1Startup),new StartupTaskAttribute(){BeforeTasks = StartupNodes.CoreUI,AfterTasks = StartupNodes.Foundation},() => new WPFDemo.Api.Startup.Foo1Startup()),};}} }

也就是自動收集了程序集里面的啟動項,生成收集的代碼

可以在啟動框架模塊里面,新建一個叫 AssemblyMetadataExporter 的類型來從 AttributedTypesExport.g.cs 拿到收集的類型。從 Telescope 拿到?__AttributedTypesExport__?生成類型的方法是調用 AttributedTypes 的 FromAssembly 方法,代碼如下

IEnumerable<AttributedTypeMetadata<StartupTask, StartupTaskAttribute>> collection = AttributedTypes.FromAssembly<StartupTask, StartupTaskAttribute>(_assemblies);

以上代碼傳入的?_assemblies?參數就是需要獲取收集的啟動任務項程序集列表,調用以上代碼,將會從傳入的各個程序集里獲取預編譯收集的類型

將此收集的返回值封裝為 StartupTaskMetadata 即可返回給啟動框架

using System.Reflection;using dotnetCampus.ApplicationStartupManager; using dotnetCampus.Telescope;namespace WPFDemo.Api.StartupTaskFramework {public class AssemblyMetadataExporter{public AssemblyMetadataExporter(Assembly[] assemblies){_assemblies = assemblies;}public IEnumerable<StartupTaskMetadata> ExportStartupTasks(){var collection = Export<StartupTask, StartupTaskAttribute>();return collection.Select(x => new StartupTaskMetadata(x.RealType.Name.Replace("Startup", ""), x.CreateInstance){Scheduler = x.Attribute.Scheduler,BeforeTasks = x.Attribute.BeforeTasks,AfterTasks = x.Attribute.AfterTasks,//Categories = x.Attribute.Categories,CriticalLevel = x.Attribute.CriticalLevel,});}public IEnumerable<AttributedTypeMetadata<TBaseClassOrInterface, TAttribute>> Export<TBaseClassOrInterface, TAttribute>() where TAttribute : Attribute{return AttributedTypes.FromAssembly<TBaseClassOrInterface, TAttribute>(_assemblies);}private readonly Assembly[] _assemblies;} }

回到 Program.cs 里面,新建一個 BuildStartupAssemblies 方法,此方法里面,寫明需要收集啟動任務項的程序集列表,交給 AssemblyMetadataExporter 去獲取

class Program{private static void StartStartupTasks(CommandLine commandLine){Task.Run(() =>{var assemblyMetadataExporter = new AssemblyMetadataExporter(BuildStartupAssemblies());// 忽略其他邏輯});}private static Assembly[] BuildStartupAssemblies(){// 初始化預編譯收集的所有模塊。return new Assembly[]{// WPFDemo.Apptypeof(Program).Assembly,// WPFDemo.Lib1typeof(Foo2Startup).Assembly,// WPFDemo.Apitypeof(Foo1Startup).Assembly,};}}

通過 StartupManager 的 AddStartupTaskMetadataCollector 即可將導出的啟動任務項加入到啟動框架

var assemblyMetadataExporter = new AssemblyMetadataExporter(BuildStartupAssemblies());var startupManager = new StartupManager(/*忽略代碼*/)// 導出程序集的啟動項.AddStartupTaskMetadataCollector(() => assemblyMetadataExporter.ExportStartupTasks());startupManager.Run();

如此即可完成所有的應用的啟動框架配置邏輯,接下來就是各個業務模塊編寫啟動邏輯

通過添加各個業務模塊的啟動任務項演示啟動框架的使用方法

在 WPFDemo.App 添加 MainWindowStartup 用來做主窗口的啟動,代碼如下

using System.Threading.Tasks;using dotnetCampus.ApplicationStartupManager;using WPFDemo.Api.StartupTaskFramework;namespace WPFDemo.App.Startup {[StartupTask(BeforeTasks = StartupNodes.AppReady, AfterTasks = StartupNodes.UI, Scheduler = StartupScheduler.UIOnly)]internal class MainWindowStartup : StartupTask{protected override Task RunAsync(StartupContext context){var mainWindow = new MainWindow();mainWindow.Show();return CompletedTask;}} }

以上代碼通過 StartupTask 特性標記了啟動任務項需要在 AppReady 之前執行完成,需要在 UI 之后執行,要求調度到主線程執行。對于主窗口顯示,自然是需要等待其他的 UI 相關邏輯執行完成,如 ViewModel 注冊和樣式字典初始化等才能顯示的。而只有在主窗口準備完成之后,才能算 AppReady 應用完成,因此可以如此編排啟動任務項

接下來再添加一個和業務相關的啟動任務項,添加 BusinessStartup 實現業務,業務要求在主界面添加一個按鈕。因此如需求,需要讓 BusinessStartup 在 MainWindowStartup 執行完成之后才能啟動,代碼如下

[StartupTask(BeforeTasks = StartupNodes.AppReady, AfterTasks = "MainWindowStartup", Scheduler = StartupScheduler.UIOnly)]internal class BusinessStartup : StartupTask{protected override Task RunAsync(StartupContext context){if (Application.Current.MainWindow.Content is Grid grid){grid.Children.Add(new Button(){HorizontalAlignment = HorizontalAlignment.Center,VerticalAlignment = VerticalAlignment.Bottom,Margin = new Thickness(10, 10, 10, 10),Content = "Click"});}return CompletedTask;}}

可以看到,在 BusinessStartup 里,通過 AfterTasks 設置了?MainWindowStartup?字符串,也就表示了需要在 MainWindowStartup 執行完成之后才能執行

此外,依賴關系是可以跨多個項目的,例如在基礎設施里面有 WPFDemo.Lib1 程序集的 LibStartup 表示某個組件的初始化,這個組件屬于基礎設施,通過 BeforeTasks 指定要在 Foundation 預設啟動節點啟動

[StartupTask(BeforeTasks = StartupNodes.Foundation)]class LibStartup : StartupTask{protected override Task RunAsync(StartupContext context){context.Logger.LogInfo("Lib Startup");return base.RunAsync(context);}}

如上可以看到,在此框架設計上,給了 StartupTask 類型的 RunAsync 作為虛方法,方便業務對接時,做同步邏輯,可以通過調用基類方法返回 Task 對象

以上代碼只是標記了 BeforeTasks 而沒有標記 AfterTasks 那么將會默認給 AfterTasks 賦值為虛擬的啟動點,也就是不需要等待其他啟動項

在 WPFDemo.Api 程序集里面有一個 OptionStartup 表示根據命令行決定執行的邏輯,這個也屬于基礎設施,但是依賴于 LibStartup 的執行完成,代碼如下

[StartupTask(BeforeTasks = StartupNodes.Foundation, AfterTasks = "LibStartup")]class OptionStartup : StartupTask{protected override Task RunAsync(StartupContext context){context.Logger.LogInfo("Command " + context.CommandLineOptions.Name);return CompletedTask;}}

如此即可實現讓 OptionStartup 在 LibStartup 之后執行,且在 Foundation 之前執行

以上的代碼的啟動圖如下,其中 LibStartup 和 OptionStartup 沒有要求一定要在 UI 線程,默認是調度到線程池里執行

在 BeforeTasks 和 AfterTasks 都是可以傳入多個不同的啟動項列表,多個之間使用分號分割。也可以換成使用 BeforeTaskList 和 AfterTaskList 使用數組的方式,例如有 WPFDemo.Api 程序集的 Foo1Startup 和在 WPFDemo.Lib1 的 Foo2Startup 和 Foo3Startup 啟動任務項,其中 Foo3Startup 需要依賴 Foo1Startup 和 Foo2Startup 的執行完成,可以使用如下代碼

[StartupTask(BeforeTasks = StartupNodes.CoreUI, AfterTaskList = new[] { nameof(WPFDemo.Lib1.Startup.Foo2Startup), "Foo1Startup" })]public class Foo3Startup : StartupTask{protected override Task RunAsync(StartupContext context){context.Logger.LogInfo("Foo3 Startup");return base.RunAsync(context);}}

以上就是應用接入?ApplicationStartupManager?啟動流程框架的方法,以及業務方編寫啟動任務項的例子。以上的代碼放在?https://github.com/dotnet-campus/dotnetCampus.ApplicationStartupManager?的例子項目

總結

以上是生活随笔為你收集整理的.NET 为大型应用接入 ApplicationStartupManager 启动流程框架的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。