【AspNetCore原始碼】設計模式 - 提供者模式
AspNetCore原始碼發現日誌模組的設計模式(提供者模式),特此記錄
學習設計模式的好處是,我們可以容易擴充套件它達到我們要求,除了要知道如何擴充套件它,還應該在其他地方應用它
類圖 & 分析
角色分析
日誌工廠 ( LoggerFactory --> ILoggerFactory)
- 提供註冊提供者
- 建立日誌記錄器(Logger)
日誌記錄器(Logger --> ILogger)
- 寫入日誌記錄(遍歷所有日誌提供者的Logger)
- 這裡所有註冊的日誌提供者聚合
日誌提供者(ConsoleLoggerProvider --> ILoggerProvider)
- 建立具體日誌記錄器
具體日誌記錄者(ConsoleLogger,EventLogLogger)
- 將日誌寫入具體媒介(控制檯,Windows事件日誌)
現在來看看這個模式
1. 提供標準的日誌寫入介面(ILogger)
2. 提供日誌提供者介面(ILoggerProvider)
3. 提供註冊提供者介面(ILoggerFactory.AddProvider)
這裡只是列出部分類和方法,整個Logging要比這個還多,為什麼寫個日誌要整那麼多東西?
程式唯一不會變就是不斷在變化,這個也是為什麼要設計模式運用到程式當中的原因,讓程式可擴充套件來應對這種變化。
AspNetCore內建 8種日誌記錄提供程式 ,但肯定還是遠遠不夠,因為有的可能想把日誌寫在文字,有的想寫在Mongodb,有的想寫在ElasticSearch等等,Microsoft不可能把所有的都實現,就算實現也未必適合你的業務使用。
假設現在需要把日誌寫在Mongo,只需要
1. 實現Mongodb的ILogger - 將日誌寫到Mongodb
2. 實現Mongodb的ILoggerProvider - 建立Mongodb的Logger
3. 把Provider註冊到AspNetCore - ILoggerFactory.AddProvider
這裡都是新增程式碼達到實現把日誌寫入到Mongodb,這就是6大設計原則之一對擴充套件開放(可以新增自己的日誌),對修改封閉(不需要修改到內部的方法)
AspNetCore程式碼實現(只列出介面)
ILoggerFactory
ILogger CreateLogger(string categoryName); void AddProvider(ILoggerProvider provider);
CreateLogger : 這個和ILoggerProvider提供的CreateLogger雖然都是現實ILogger介面,但是做的事情不一樣,LoggerFactory建立的是Logger例項,裡面聚合了具體寫日誌的Logger,遍歷它們輸出。
categoryName : 可以指定具體,若使用泛型相當於typeof(T).FullName,這個用於篩選過濾日誌
AddProvider : 註冊一個新的提供者,然後遍歷現有的Logger,把新的Provider新增到現有logger裡面
ILoggerProvider
ILogger CreateLogger(string categoryName);
CreateLogger : 用於建立具體寫日誌Logger(例如Console)
ILogger
void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter); bool IsEnabled(LogLevel logLevel); IDisposable BeginScope<TState>(TState state);
Log<TState>(....): 輸出日誌
bool IsEnabled : 指定的日誌級別是否可用
IDisposable BeginScope<TState>() : 開啟日誌作用域,將這個域範圍的日誌都放一起
AspNetCore使用第三方日誌元件(Log4Net)
AspNetCore使用Log4Net作為記錄很簡單,只需
1. 安裝包:
dotnet install Microsoft.Extensions.Logging.Log4Net.AspNetCore
2. Configure 新增:
loggerFactory.AddLog4Net();
3. 新增log4net.config配置檔案
看看Microsoft.Extensions.Logging.Log4Net.AspNetCore如何實現ILogger和ILoggerProvider介面(程式碼有擷取)
Log4NetProvider
public ILogger CreateLogger(string categoryName) => this.loggers.GetOrAdd(categoryName, this.CreateLoggerImplementation); private Log4NetLogger CreateLoggerImplementation(string name) { var options = new Log4NetProviderOptions { Name = name, LoggerRepository = this.loggerRepository.Name }; options.ScopeFactory = new Log4NetScopeFactory(new Log4NetScopeRegistry()); return new Log4NetLogger(options); }
Log4NetLogger
switch (logLevel) { case LogLevel.None: break; case LogLevel.Critical: { string overrideCriticalLevelWith = options.OverrideCriticalLevelWith; if (!string.IsNullOrEmpty(overrideCriticalLevelWith) && overrideCriticalLevelWith.Equals(LogLevel.Critical.ToString(), StringComparison.OrdinalIgnoreCase)) { log.Critical(text, exception); } else { log.Fatal(text, exception); } break; } case LogLevel.Debug: log.Debug(text, exception); break; case LogLevel.Error: log.Error(text, exception); break; ...... }
log4net的ILog是沒有Trace和Critical方法,這兩個是擴充套件方法,呼叫log4net log4net.Repository.Hierarchy.Logger.Log()方法
log4net 裡面有Fatal代表日誌最高級別,AspNetCore的Critical是日誌最高級別,習慣log4net可能習慣用Fatal,這個時候只需要在註冊的時候
loggerFactory.AddLog4Net(new Log4NetProviderOptions() { OverrideCriticalLevelWith = "Critical" });
在Controller呼叫
_logger.LogCritical("Log Critical");
看看效果
2020-04-27 13:42:05,042 [10] FATAL LoggingPattern.Controllers.WeatherForecastController (null) - Log Critical
奇怪,沒有按預期發生。這個元件是開源的,可以下載下來除錯看看,github克隆下來 Microsoft.Extensions.Logging.Log4Net.AspNetCore
除錯過程
1. 將Microsoft.Extensions.Logging.Log4Net.AspNetCore.csproj的SignAssembly設定false(這個是程式集強簽名)
<SignAssembly>false</SignAssembly>
2. 將引用改成引用本地,我這裡是放在跟專案平級
<ItemGroup> <ProjectReference Include="..\Microsoft.Extensions.Logging.Log4Net.AspNetCore\src\Microsoft.Extensions.Logging.Log4Net.AspNetCore\Microsoft.Extensions.Logging.Log4Net.AspNetCore.csproj" /> </ItemGroup>
我這裡是用VSCode,如果用VS不用這麼麻煩
3. 然後就可以打斷點,在寫日誌和之前看到的那個判斷打個斷點
4. 接下來就是看看這個值怎麼來的
builder.Services.AddSingleton<ILoggerProvider>(new Log4NetProvider(options)); public Log4NetProvider(Log4NetProviderOptions options) { }
註冊一個單例的Log4NetProvider,參入引數options,Logger是在Provider的CreateLogger建立,現在看看CreateLogger
public ILogger CreateLogger(string categoryName) => this.loggers.GetOrAdd(categoryName, this.CreateLoggerImplementation); private Log4NetLogger CreateLoggerImplementation(string name) { var options = new Log4NetProviderOptions { Name = name, LoggerRepository = this.loggerRepository.Name }; options.ScopeFactory = new Log4NetScopeFactory(new Log4NetScopeRegistry()); return new Log4NetLogger(options); }
到這裡就清楚了,CreateLoggerImplementation裡面又new了一個options,然後沒有給OverrideCriticalLevelWith賦值(我認為這是個Bug,應該也很少人會用這個功能)這裡之所以沒用單例的options,因為要給每個Logger的目錄名稱動態賦值。
給這個庫作者提了Issues和PR
新增自定義的日誌記錄器
假設現在需要把日誌加入到Mongodb,只需完成下面幾個步驟
1. 新增Mongodb驅動,(dotnet-cli)
dotnet add package MongoDB.Driver
2. 實現介面ILogger
public class MongodbLogger : ILogger { private readonly string _name; private MongoDB.Driver.IMongoDatabase _database; public MongodbLogger(string name, MongoDB.Driver.IMongoDatabase database) { _name = name; _database = database; } public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) { var collection = _database.GetCollection<dynamic>(logLevel.ToString().ToLower()); string message = formatter(state, exception); collection.InsertOneAsync(new { time = DateTime.Now, name = _name, message, exception }); } public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None; public System.IDisposable BeginScope<TState>(TState state) => NullScope.Instance; }
3. 實現ILoggerProvider介面
public class MongodbProvider : ILoggerProvider { private readonly ConcurrentDictionary<string, MongodbLogger> _loggers = new ConcurrentDictionary<string, MongodbLogger>(); private MongoDB.Driver.IMongoDatabase _database; public MongodbProvider(MongoDB.Driver.IMongoDatabase database) { _database = database; } public ILogger CreateLogger(string categoryName) => _loggers.GetOrAdd(categoryName, name => new MongodbLogger(categoryName, this._database)); public void Dispose() => this._loggers.Clear(); }
4. 新增MongodbLogging擴充套件函式(非必須)
public static ILoggerFactory AddMongodb(this ILoggerFactory factory, string connetionString = "mongodb://127.0.0.1:27017/logging") { var mongoUrl = new MongoDB.Driver.MongoUrl(connetionString); var client = new MongoDB.Driver.MongoClient(mongoUrl); factory.AddProvider(new MongodbProvider(client.GetDatabase(mongoUrl.DatabaseName))); return factory; }
5. Configure註冊MongodbLogging
loggerFactory.AddMongodb();
執行效果
擴充套件
設計模式的好處是,我們可以容易擴充套件它達到我們要求,除了要知道如何擴充套件它,還應該在其他地方應用它,例如我們經常需要訊息通知使用者,但是通知渠道(提供者),在一開始未必全部知道,例如一開始只有簡訊,郵件通知,隨著業務發展可能需要增加微信推送,提供者模式就很好應對這一種情況,很容易畫出下面類圖。
當需要擴充套件傳送訊息渠道,只需要實現ISenderProvider(哪個提供),ISender(如何傳送)
當需要擴充套件傳送訊息渠道,只需要實現ISenderProvider(哪個提供),ISender(如何傳送)
轉發請標明出處:https://www.cnblogs.com/WilsonPan/p/12793220.html
示例程式碼:https://github.com/WilsonPan/AspNetCoreExamples/tree/master/LoggingPattern