一、日志

目前较为流行的三款日志框架为:log4net、Nlog、Serilog。三者都可存储日志至文件、数据库等。(常用输出位置为上述两种)。

主要需要做的工作:需要封装常用的日志。

1. 基本了解

(1).log4net

简介

log4net是一个始于 2001 年的领先的日志记录框架,最初是 Java 框架 log4j 的端口。多年来,Apache Logging Services 项目持续进行开发,没有其他框架能像 log4net 一样久经考验。log4net 是所有现代 .NET 日志记录框架的鼻祖,在日志框架中,日志级别(log levels)、记录器(logger)和输出模块(appenders/targets/sinks)等概念几乎都是通用的。相信所有多年使用 .NET 编程的朋友对 log4net 都相当熟悉。

log4net 好用、稳定且灵活,但是它的配置相对来说比较复杂一些,而且很难实现结构化的日志记录。

  1. using log4net;
  2. using log4net.Config;
  3. using System;
  4. using System.IO;
  5. using System.Reflection;
  6. namespace LoggingDemo.Log4Net
  7. {
  8. class Program
  9. {
  10. private static readonly ILog log = LogManager.GetLogger(typeof(Program));
  11. static void Main(string[] args)
  12. {
  13. var logRepository = LogManager.GetRepository(Assembly.GetEntryAssembly());
  14. XmlConfigurator.Configure(logRepository, new FileInfo("log4net.config"));
  15. log.Debug("Starting up");
  16. log.Debug("Shutting down");
  17. Console.ReadLine();
  18. }
  19. }
  20. }
  1. <log4net>
  2. <appender name="Console" type="log4net.Appender.ConsoleAppender">
  3. <layout type="log4net.Layout.PatternLayout">
  4. <!-- Pattern to output the caller's file name and line number -->
  5. <conversionPattern value="%utcdate %5level [%thread] - %message%newline" />
  6. </layout>
  7. </appender>
  8. <appender name="RollingFile" type="log4net.Appender.RollingFileAppender">
  9. <file value="logfile.log" />
  10. <appendToFile value="true" />
  11. <maximumFileSize value="100KB" />
  12. <maxSizeRollBackups value="2" />
  13. <layout type="log4net.Layout.PatternLayout">
  14. <conversionPattern value="%utcdate %level %thread %logger - %message%newline" />
  15. </layout>
  16. <appender>
  17. <root>
  18. <level value="DEBUG" />
  19. <appender-ref ref="Console" />
  20. <appender-ref ref="RollingFile" />
  21. </root>
  22. </log4net>

关于log4net的结论:

配置是log4net的失败。它很复杂,XML 很难让人清楚的了解每一项配置的具体作用。幸运的是,可以使用代码对其进行配置,但是对于这方面的文档记录不是很清晰。事实上,”没有很好的文档记录”几乎是所有log4net的口号。远在2009年,官方的示例很复杂,并且专注于诸如记录到SQL数据库之类的事情。 log4net有很多不同的appenders (类似一个用于写入控制台,另一个用于写入文件大小为 100KB 的滚动文件),所以你很可能不会因为特定的日志记录案例而被遗忘。

(2).NLog

简介

NLog也是一个相当老的项目,最早的版本发布于 2006 年,不过目前仍在积极开发中。NLog 从 v4.5 版本开始新增了对结构化日志记录的支持。

与 log4net 相比,NLog 的配置更加容易,并且基于代码的配置也比较简洁。NLog 中的默认设置比 log4net 中的默认设置会更合理一些。需要注意的一点是,当使用这两个框架,您可能会遇到同一个问题,那就是配置有问题(比如忘记复制配置文件)时,不会得到任何提示,也不会输出日志信息。假如您将应用部署上线以后遇到这个情况,这将是致命的,因为许多问题的检查都是依赖于日志记录的。当然,这么设计的初衷是避免让应用程序因日志问题而导致崩溃。

  1. using NLog;
  2. using System;
  3. namespace LoggingDemo.Nlog
  4. {
  5. class Program
  6. {
  7. static void Main(string[] args)
  8. {
  9. LogManager.LoadConfiguration("nlog.config");
  10. var log = LogManager.GetCurrentClassLogger();
  11. log.Debug("Starting up");
  12. log.Debug("Shutting down");
  13. Console.ReadLine();
  14. }
  15. }
  16. }
  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  4. <targets>
  5. <target name="logfile" xsi:type="File" fileName="logfile.txt" />
  6. <target name="logconsole" xsi:type="Console" />
  7. </targets>
  8. <rules>
  9. <logger name="*" minlevel="Debug" writeTo="logconsole" />
  10. <logger name="*" minlevel="Debug" writeTo="logfile" />
  11. </rules>
  12. </nlog>

这里是NLog和log4net的区别。配置文件仍然是 XML,但它是一个看起来更干净的 XML 文件。我们不需要编写自己的格式。

还有一种方法可以使用代码配置日志记录。

  1. var config = new NLog.Config.LoggingConfiguration();
  2. var logfile = new NLog.Targets.FileTarget("logfile") { FileName = "logfile.txt" };
  3. var logconsole = new NLog.Targets.ConsoleTarget("logconsole");
  4. config.AddRule(LogLevel.Debug, LogLevel.Fatal, logconsole);
  5. config.AddRule(LogLevel.Debug, LogLevel.Fatal, logfile);
  6. NLog.LogManager.Configuration = config;

关于NLog的结论:

NLog和log4net之间有一些小的区别。NLog更易于配置,并且支持比log4net更干净的基于代码的配置。我认为NLog中的默认值也比log4net中的默认值更明智。 我在这两个框架中遇到的一个问题是,如果记录器出现问题(在我的情况下忘记复制配置文件),那么就没有关于问题所在的提示。这个想法是,日志记录问题不应该使应用程序关闭。我可以理解这种愿望,但如果在启动期间日志记录失败,那应该是致命的。日志记录不是,也不应该是事后的想法。许多自动化问题检测依赖于日志输出,没有日志输出是很严重的。

(3).Serilog

简介

具体Serilog介绍:Serilog浅析

从Serilog的官方介绍中,我们可以发现其框架是.net中的诊断日志库,可以在所有的.net平台上运行。支持结构化日志记录,对复杂、分布式、异步应用程序的支持非常出色。

Serilog 日志记录框架发布于 2013 年,相对来说是一个较新的框架。与其他日志框架不同的是,Serilog 在设计时考虑了强大的结构化事件数据,提供了开箱即用的结构化日志实现。所以 Serilog 对结构化日志的支持非常好,而且配置简洁。Serilog 中的日志可以发送到许多终端,Serilog 称这些终端为“输出模块库(sinks)”。您可以在 https://github.com/serilog/serilog/wiki/Provided-Sinks 页面查看非常全面的列表。

Serilog 中还有一个功能强大的概念是Enricher,可以通过各种方式来丰富日志事件的属性,从而向日志添加新的信息。NuGet 中提供了一些预建的 Enricher,您也可以通过实现 ILogEventEnricher 构建自己的 Enricher。

Serilog是基于日志事件(log events),而不是日志消息(log message)。可以将日志事件格式化为控制台的可读文本或者将事件化为JSON格式。应用程序中的日志语句会创建LogEvent对象,而连接到管道的接收器(sinks)会知道如何记录它们。(接收器包括各种终端、控制台、文本、SqlServer、ElasticSearch等等可用的列表

结构化概念

  1. 非结构化日志
对自由格式文本的解析往往依赖于正则表达式,并且依赖于不变的文本。这会使解析自由格式的文本变得非常脆弱(即解析与代码中的确切文本紧密耦合)。 还考虑搜索/查找的情况,例如

<font style="color:rgb(17, 17, 17);">SELECT text FROM logs WHERE text LIKE "Disk quota"; </font>

LIKE条件需要与每个text行值进行比较;再次,这在计算上是相对浪费的,尤其是在使用通配符时:

<font style="color:rgb(17, 17, 17);">SELECT text FROM logs WHERE text LIKE "Disk %"; </font>

  1. 结构化日志
结构化日志,顾名思义不再是自由格式的日志,而是遵循了一定的结构:例如,每一行日志就是一个JSON结构。好处显而易见:简化日志解析,使得日志的后续处理、分析或查询变得方便高效。详见。 + 官方解释: 通常情况下,您会发现日志信息基本上包含两部分内容:消息模板,而 .NET 通常只接受诸如 string.Format(…) 这样的的输入字符串。比如:

var position = new { Latitude = 25, Longitude = 134 };

var elapsedMs = 34;

log.Information("Processed Position, Latitude:{0}, Longitude: {1} in Elapsed:{2} ms.", position.Latitude, position.Longitude, elapsedMs);

这条日志只是简单地被转换为文本输出到日志文件中:

[INF] Processed Position, Latitude:25, Longitude: 134 in Elapsed:34 ms.

这看起来很好,但它可以更好! 当我们遇到问题的时候,我们需要根据一些已知的信息来检索日志记录。比如,假设我们已知 Latitude 为 25,Longitude 为 134,我们要查找这条日志的话,该怎么做呢?由于上面输出的日志信息是简单的文本,有经验的您可能立马会想到使用正则表达式或者简单的字符串匹配,但这样不仅不够直观,实现起来也比较麻烦。有没有更好的方法呢? 如果我们在存储日志的时候,将其中包含值的部分作为特征提取出来,形成由键和值组成的有结构的 JSON 对象,作为每条日志记录的属性(properties):

<font style="color:rgb(68, 74, 68);">{</font><font style="color:rgb(255, 0, 0);">"Position"</font><font style="color:rgb(68, 74, 68);">:</font> <font style="color:rgb(68, 74, 68);">{</font><font style="color:rgb(255, 0, 0);">"Latitude"</font><font style="color:rgb(68, 74, 68);">:</font> <font style="color:rgb(136, 0, 0);">25</font><font style="color:rgb(68, 74, 68);">,</font> <font style="color:rgb(255, 0, 0);">"Longitude"</font><font style="color:rgb(68, 74, 68);">:</font> <font style="color:rgb(136, 0, 0);">134</font><font style="color:rgb(68, 74, 68);">},</font> <font style="color:rgb(255, 0, 0);">"Elapsed"</font><font style="color:rgb(68, 74, 68);">:</font> <font style="color:rgb(136, 0, 0);">34</font><font style="color:rgb(68, 74, 68);">}</font>

然后,在我们检索的时候只需要查找日志记录的 properties 就可以了,它是结构化的,检索起来既方便又直观。 Serilog 帮我们实现了这一点,您只需改动一行代码就可以了:

log.Information(<font style="color:rgb(163, 21, 21);">"Processed {@Position} in {Elapsed:000} ms."</font>, position, elapsedMs);

Position 前面的 @ 解构操作符,它告诉 Serilog 需要将传入的对象序列化,而不是调用 ToString() 转换它。 Elapsed 之后的 :000 是一个标准的 .NET 格式字符串,它决定该属性的呈现方式。
  • 通俗:
使用结构化日志记录,与磁盘错误相关的日志消息在JSON中可能如下所示:

<font style="color:rgb(68, 74, 68);">{</font> <font style="color:rgb(255, 0, 0);">"level"</font><font style="color:rgb(68, 74, 68);">:</font> <font style="color:rgb(163, 21, 21);">"DEBUG"</font><font style="color:rgb(68, 74, 68);">,</font> <font style="color:rgb(255, 0, 0);">"user"</font><font style="color:rgb(68, 74, 68);">:</font> <font style="color:rgb(163, 21, 21);">"username"</font><font style="color:rgb(68, 74, 68);">,</font> <font style="color:rgb(255, 0, 0);">"error_type"</font><font style="color:rgb(68, 74, 68);">:</font> <font style="color:rgb(163, 21, 21);">"disk"</font><font style="color:rgb(68, 74, 68);">,</font> <font style="color:rgb(255, 0, 0);">"text"</font><font style="color:rgb(68, 74, 68);">:</font> <font style="color:rgb(163, 21, 21);">"Disk quota ... exceeded by user ..."</font> <font style="color:rgb(68, 74, 68);">}</font>

这种结构的字段可以很容易地映射到例如 SQL表列名,这意味着查找可以更具体/更细粒度:

<font style="color:rgb(0, 0, 255);">SELECT</font> <font style="color:rgb(0, 0, 255);">user</font>, text <font style="color:rgb(0, 0, 255);">FROM</font> logs <font style="color:rgb(0, 0, 255);">WHERE</font> error_type <font style="color:rgb(171, 86, 86);">=</font> "disk";

您可以在希望经常搜索/查找其值的列上放置索引,只要您不对LIKE这些列值使用子句即可。您可以将日志消息细分为特定类别的内容越多,查找的对象就越有针对性。例如,除了error_type上面示例中的字段/列之外,您甚至可以设置为be “error_category”: “disk”, “error_type”: “quota”或诸如此类。 结构越多,你的日志消息,通过解析/检索系统(如fluentdelasticsearchkibana),可以利用该结构,并以更快的速度和更低的CPU /内存执行任务。
  1. 总结
这不仅与速度和效率有关,更重要的是使用结构化日志记录和“结构化查询”时,能以特定格式捕获以及呈现结构化日志,同时提供对开发者与程序友好的解析支持。可以更方便地以其为条件进行筛选,搜索结果的相关性将更高。如果没有这种搜索,那么在不同上下文中出现的任何单词都会给您带来大量无关的点击。

关于Serilog结论

API更现代,更易于设置,维护得更好,并且默认情况下执行结构化日志记录。添加扩充器的功能使您能够拦截和修改消息,这非常有用。 在大多数情况下,Serilog的延迟是NLog的延迟的一半,吞吐量是其两倍。原文链接https://blog.datalust.co/serilog-tutorial/ ## 2. 使用方式 三种日志框架均通过Nuget安装。 ### (1)asp.net mvc #### log4net 1. 配置 log4net.config、web.config中添加个性化配置。web.config主要配置日志存储路径,方便系统上线后查看或者更改日志位置。(并不是必须在webconfig中配置) 2. 初始化 初始化Log4net(global.asax): 我们需要调用XmlConfigurator类的configure方法来初始化Log4net
  1. log4net.Config.XmlConfigurator.Configure();
  1. 使用

需要输出日志的控制器中:

  1. //获取日志记录器
  2. private static readonly ILog Log = LogManager.GetLogger(typeof(DefaultController));

Nlog

基本与log4net相同。NLog使用路由表(routing table)进行配置,但log4net却使用层次性的appender配置,这样就让NLog的配置文件非常容易阅读,并便于今后维护。

  1. public readonly Logger Logger = NLog.LogManager.GetCurrentClassLogger();

Serilog

不需要单独的config配置文件。

最简单的配置:

  1. var log = new LoggerConfiguration()
  2. .WriteTo.Console()
  3. .WriteTo.File(System.Web.Hosting.HostingEnvironment.MapPath("~/ErrorLog/Warning/log.txt")
  4. .CreateLogger())

其中可以通过filter、Enrich、sinks等增加个性化配置。从结构化日志方面看Serilog可定制化程度高。

其他框架不予以详细介绍了,基本使用方式差不多。

(2)asp.net core

.net core中注意一下:可以使用app.UseSerilogRequestLogging();设置请求管道。说白了就是为每个请求添加一个单一的“摘要”日志消息。也就是说,这个配置项专为请求设计的。 重要的是UseSerilogRequestLogging()调用应出现在诸如MVC之类的处理程序之前。 中间件不会对管道中出现在它之前的组件进行时间或日志记录。通过将UseSerilogRequestLogging()放在它们之后,可以将其用于从日志中排除杂乱的处理程序,例如UseStaticFiles()。 为了减少每个HTTP请求需要构造,传输和存储的日志事件的数量。 在同一事件上具有许多属性还可以使请求详细信息和其他数据的关联更加容易。 默认情况下,以下请求信息将作为属性添加:
  • 请求方法
  • 请求路径
  • 状态码
  • 响应时间
您可以使用UseSerilogRequestLogging()上的选项回调来修改用于请求完成事件的消息模板,添加其他属性或更改事件级别:
  1. app.UseSerilogRequestLogging(options =>
  2. {
  3. // 自定义消息模板
  4. options.MessageTemplate = "Handled {RequestPath}";
  5. // 发出调试级别的事件,而不是默认事件
  6. options.GetLevel = (httpContext, elapsed, ex) => LogEventLevel.Debug;
  7. //将其他属性附加到请求完成事件
  8. options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
  9. {
  10. diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value);
  11. diagnosticContext.Set("RequestScheme", httpContext.Request.Scheme);
  12. };
  13. });

详情

3. 日志工作原理

(1).log4net

工作原理(一)

要知道Log4net究竟是咋干活的,咱们可以从下面这个脉络简图入手。你的程序中的语句log4net.LogManager.GetLogger().Info("hello world!");就会引发log4net如下内部工作流程。不要管上面的对象(Appender/Filter等等)是什么,先看着这个流程,理清log4net工作的脉络,然后再按节点一一打通。
系统异常%26日志 - 图1

  1. 第一件事就是找调度(LogManager)要个干活的工人(Logger,写日志的对象),当然,方法是调用LogManager.GetLogger()。找个什么样工人,究竟是那个工人会被挑中,这里面有些曲折,会涉及到Repository(高级话题,下回分解)。咱先不管这么多,知道有个能干活的工人(Logger) 肯定是被找来了。
    另外有些麻烦事儿,这工人有经纪人ILoggerWrapper(Logger需要实现ILogger, 而ILoggerWrapper唯一的方法就是得到ILogger实例),经纪人又有代理ILog(ILog继承于ILoggerWrapper)。代理ILog存在的意义在于给你提供方便的接口函数(Info,Warn等等),而不是工人提供的void Log(string callerFullName, Level level, object message, Exception t)。 不管关系多复杂,虽然你让干什么活都得先对代理说,但最后还都是告诉了工人,一个字也没落。

  2. 你通过Info(“hello”)告诉工人干活了,工人Logger一定先看看这事能不能干。你的配置里说只写Info这个级别以上的信息,咱就不能写Debug和Warn。这种情况你需要付出性能代价(一个函数调用和一个整数形式的级别比较)。然后,工人Logger就创建一个任务包LoggingEvent,把你要做的事儿用任务包的形式包起来,以后的流程就都针对任务包LoggingEvent处理了。
    任务包LoggingEvent里信息丰富,包含:时间代码位置、工人的名字、信息、线程名、用户名、信息、异常、上下文等等。

  3. 接下来,Appender们登场了。原来工人自己不干具体的活,手里拽着一堆马仔,自己成了工头,告诉Appender去DoAppend(),让马仔们干活。注意,这里说得是“马仔们”,就是说同时会有多个马仔都在写东东。究竟那些马仔能被选中完成这光荣的任务,还要由客户您来决定,如:<appender-ref ref="ConsoleAppender" />
    这些马仔及其特长:

马仔 特长
ConsoleAppender 在控制台上写日志
ColoredConsoleAppender ConsoleAppender的徒弟,青出于蓝,写出来的东东还可以带颜色,花花绿绿的,煞是好看
FileAppender 往文件里写日志
RollingFileAppender 往可滚动的文件里写日志,就是说它会按客户要求控制文件大小和数量,一个文件写满就帮你再开另一个接着写
ForwardingAppender 帮其他Appender传任务包的人,当然自己可以干一点雁过拔毛的事(做一些过滤的事情,比如说你要写的东东里包含不雅的词汇,它可以让你变得文明一些)
NetSendAppender 往Windows的Messager写日志
ASPNetTraceAppender 在Asp.Net的Trace里写日志
ADONetAppender 往数据库里写日志
EventLogAppender 往Windows事件里写日志
RemotingAppender 把日志转给另外的Remoting服务,如:一个专门的集中的日志服务器
SmtpAppender 通过邮件把日志发出去
SmtpPickupDirAppender 把日志包成邮件,发在指定目录,等待专门的Smtp代理去发送
TelnetAppender 把日志发到Telnet控制台
UdpAppender 通过UDP协议把日志发给另外的一个主机,或者组播给一些主机

当然,你可以自己搞个马仔,比如发个短信什么的,可以取名叫MobilePagerAppender(从AppenderSkeleton继承),通过配置告诉log4net就行。

  1. 说到这儿,检查员Filter登场。这活最终究竟干不干,马仔还得通过Decide()再问问检查员们。注意,这里说得是“检查员们”,就是说所有在册的检查员都点头,这话才能干。如何让检查员在册,看配置文件,如:
  1. <appender name="FF" type="log4net.Appender.ForwardingAppender" >
  2. <filter type="log4net.Filter.LevelRangeFilter">
  3. <param name="LevelMin" value="DEBUG"/>
  4. <param name="LevelMax" value="INFO"/>
  5. </filter>
  6. <appender-ref ref="ConsoleAppender" />
  7. </appender>

雁过拔毛的马仔ForwardingAppender和检查员LevelRangeFilter配合工作,把大于Debug和小于Info的东东通知给马仔ConsoleAppender,让它写到控制台上。

每个检查员都有自己的关注点,如下:

检查员 特长
LevelMatchFilter 日志级别等于指定的级别才放行
LevelRangeFilter 按日志级别范围做比较,可取区间内的放行。如必须大于Warn小于Info
StringMatchFilter 对你的言论进行检查,符合字符串比对条件的放行。如:必须包含“芝麻开门”的字符串才让写。
比对条件可以是简单的带通配符的字符串,也可以是正则表达式(帅!)
PropertyFilter StringMatchFilter的徒弟,对LoggingEvent的某个属性进行检查,符合字符串匹配条件才放行
LoggerMatchFilter 检查工头(Logger)的名字,如果是以指定的字符串开头的才放行。如:只要是姓“张”的工头发下来的任务包,都让过。
DenyAllFilter 这个检查员最黑,什么都不让过
  1. 检查员们点头后,这事就必须要干了。怎么干?客户要写的东东究竟用什么格式输出?这活由排版员Layout来干。下面是排版员的名单:
排版员 特长
ExceptionLayout 对LoggingEvent中的异常信息message进行排版
PatternLayout 最常用的排版员,通过一堆标识符来决定版式。
如:”%date %-5level- %message” 表示要以此输出日志日期、级别(5个字母的宽度)、信息
SimpleLayout 最简单的版式: [level] - [message]
XmlLayout 把日志写到XML文件中去,写成一个Element
XmlLayoutSchemaLog4j 把日志写到XML文件中去,写成一个Element,其格式需符合log4j对事件定义的DTD.

排版员需要排版LoggingEvent的信息的字符串内容RenderedMessage,例如文章开头的“hello world!”。除了“hello world!”这样的字符串,信息message还可以是任意的对象。因此需要针对对象进行专门的排版,由Render(对象打印机)来干。
你可以针对自己的信息对象搞Render。如打印订单信息的OrderRenader,一旦在订单处理中发生错误,把订单的主要信息打印出来,方便调试。别忘了:OrderRenader必须实现log4net.ObjectRenderer.IObjectRenderer。

  1. 一切就绪,各个马仔就做最后的输出,有打印屏幕的,有写文件的,有在网络上发数据的,八仙过海,各显神通。

整个流程走完,相信我们接触到的Logger、Appender、Filter、Layout、Render都已不再陌生。log4net良好的实现了事件过滤、格式排版的高度扩展性和可配置性。
log4net的这处理模式可以看作是一种扩展的Publish/Subscribe模式,完全可以应用到我们自己的应用程序中去,比如说订单处理,可以实现对不同订单的过滤,实现不同的订单的提交目的地(写数据库、发邮件、短信通知等等)。

最后,给出Repository、Appender、Filter、Layout、Render的关系简图:
系统异常%26日志 - 图2

虽然,Repository在下回分解,但这里还需要简单说两句。Repository可以说成基于一个log4net配置节创建的log4net容器,它根据log4net配置节的指示创建以上其他的对象并保有他们的实例,随时为你所用。一般而言,你的应用程序不需要关心它,用缺省的容器即可。

工作原理(二)

Repository可以说成基于一个log4net配置节创建的log4net容器,它根据log4net配置节的指示创建其他所有对象(Logger/Appender/Filter/Layout等等)并保有他们的实例,随时为你所用。

每个Repository都有自己唯一的名字,如 root。

一般而言一个AppDomain(或者说一个进程)有一个Repository,该AppDomain下所有程序集Assembly都可以使用这个Repository。Repository需要实现ILoggerRepository,log4net中log4net.Repository.Hierarchy.Hierarchy就通过继承LoggerRepositorySkeleton实现了ILoggerRepository,它也是log4net中唯一实现ILoggerRepository的类。

Hierarchy

那么Hierarchy是什么呢?

Hierarchy里存放着通过配置文件创建的所有Logger。由于Logger们是有父子关系的,因此Hierarchy通过继承树来存放所有的Logger。根节点就是我们熟悉的Root,如例:

Logger 名 日志级别 从父Logger继承的级别
root INFO INFO
my none INFO
my.net DEBUG DEBUG
my.net.tcp none DEBUG

对应配置文件,应该是:

  1. <root>
  2. <level value="INFO" />
  3. <appender-ref ref="ConsoleAppender" />
  4. </root>
  5. <logger name=" my">
  6. <appender-ref ref="ConsoleAppender" />
  7. </logger>
  8. <logger name=" my.net ">
  9. <appender-ref ref="ConsoleAppender" />
  10. </logger>
  11. <logger name=" my.net.tcp">
  12. <filter type="log4net.Filter.LevelRangeFilter">
  13. <param name="LevelMin" value="DEBUG"/>
  14. <param name="LevelMax" value="INFO"/>
  15. </filter>
  16. <appender-ref ref=" ColoredConsoleAppender" />
  17. </logger>

上例中,定义了三个Logger,都将存放在Hierarchy中。三个Logger形成继承关系,子Logger中未定义的属性都将从父Logger中继承。

一旦你的应用程序通过log4net.LogManager.GetLogger()得到ILog(也就是logger的代理),那么将从Hierarchy的继承树中找出对应的Logger。

log4net.LogManager.GetLogger() 得到 root
log4net.LogManager.GetLogger(“my”) 得到 my logger

这样,你就可以为程序集中不同的命名空间甚至是某个类设置相应的log4net配置。如上例“my.net.tcp”就可以实现和其父Logger不同的日志行为。

使用不同的Repository

如果你的应用程序中不同程序集需要使用不同配置节,或者说需要使用不同的log4net配置文件,那就使用不同的Repository。

如在my.net.tcp程序集中,加入语句:[assembly: log4net.Config.DOMConfigurator(ConfigFile=”my.net.tcp.config”, Watch=true)]

这样,你的就可以单独使用一份配置文件,创建一个新的Repository。

你也可以为自己的Repository命名: [assembly: log4net.Config.AliasRepository(“myrepository”)]

如何共用Repository

不作上面所说的所有改动,一个AppDomain中所有程序集都共用缺省的Repository,但是当需要共用另一个Repository时,就需要做一些工作。产生这样的需求包括:

  1. 两个应用程序共用一份log4net配置,对日志做同样的处理
  2. 两个AppDomain需要共用一份log4net配置,对日志做同样的处理。特别时在运行时动态升级程序集时,这个需求显得尤其关键。

首先记载log4net的程序集需要为Repository命名:[assembly: log4net.Config.AliasRepository(“myrepository”)]

后续的程序集,只需要引用它即可:[assembly: log4net.Config.Repository(“myrepository”)]

这种方式下,两个AppDomain写同一份日志文件时,可能产生文件共享冲突的错误(文件已经被锁定,不能写),需要修改配置,在RollingLogFileAppender中加入lockingModel配置,如:

  1. <appender name="RollingLogFileAppender" type="log4net.Appender.RollingFileAppender">
  2. <param name="File" value="log\\TaskScheduleServer.log" />
  3. <lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
  4. </appender>

(2).Nlog

Nlog日志工作原理与log4net相似,下面介绍一下Nlog日志写入方式:

  1. protected override void InitializeTarget()
  2. {
  3. base.InitializeTarget();
  4. var appenderFactory = GetFileAppenderFactory();
  5. if (InternalLogger.IsTraceEnabled)
  6. {
  7. InternalLogger.Trace("{0}: Using appenderFactory: {1}", this, appenderFactory.GetType());
  8. }
  9. _fileAppenderCache = new FileAppenderCache(OpenFileCacheSize, appenderFactory, this);
  10. if ((OpenFileCacheSize > 0 || EnableFileDelete) && (OpenFileCacheTimeout > 0 || OpenFileFlushTimeout > 0))
  11. {
  12. int openFileAutoTimeoutSecs = (OpenFileCacheTimeout > 0 && OpenFileFlushTimeout > 0) ? Math.Min(OpenFileCacheTimeout, OpenFileFlushTimeout) : Math.Max(OpenFileCacheTimeout, OpenFileFlushTimeout);
  13. InternalLogger.Trace("{0}: Start autoClosingTimer", this);
  14. _autoClosingTimer = new Timer(
  15. (state) => AutoClosingTimerCallback(this, EventArgs.Empty),
  16. null,
  17. openFileAutoTimeoutSecs * 1000,
  18. openFileAutoTimeoutSecs * 1000);
  19. }
  20. }

使用了一个计时器的方式来做一个异步写入

FileTarget.cs 写入到文件 实现类

具体实现方法:

  1. protected override void Write(LogEventInfo logEvent)
  2. {
  3. var logFileName = GetFullFileName(logEvent);
  4. if (string.IsNullOrEmpty(logFileName))
  5. {
  6. throw new ArgumentException("The path is not of a legal form.");
  7. }
  8. using (var targetStream = _reusableFileWriteStream.Allocate())
  9. {
  10. using (var targetBuilder = ReusableLayoutBuilder.Allocate())
  11. using (var targetBuffer = _reusableEncodingBuffer.Allocate())
  12. {
  13. RenderFormattedMessageToStream(logEvent, targetBuilder.Result, targetBuffer.Result, targetStream.Result);
  14. }
  15. ProcessLogEvent(logEvent, logFileName, new ArraySegment<byte>(targetStream.Result.GetBuffer(), 0, (int)targetStream.Result.Length));
  16. }
  17. }

这里使用了 Using 来释放资源。

Nlog是写完立即释放 优点是 不会占用多余资源 每次写完既释放 缺是IO操作消耗高为了解决这个问题 Nlog 使用了 时间控件 采用 定时 定量写入策略。

(3).Serilog

Serilog源码解析

系统异常%26日志 - 图3

RollingFileSink实现类分析:

  1. public void Emit(LogEvent logEvent)
  2. {
  3. if (logEvent == null) throw new ArgumentNullException(nameof(logEvent));
  4. lock (_syncRoot)
  5. {
  6. if (_isDisposed) throw new ObjectDisposedException("The log file has been disposed.");
  7. var now = Clock.DateTimeNow;
  8. AlignCurrentFileTo(now);
  9. while (_currentFile?.EmitOrOverflow(logEvent) == false && _rollOnFileSizeLimit)
  10. {
  11. AlignCurrentFileTo(now, nextSequence: true);
  12. }
  13. }
  14. }
找到当前文件
  1. void AlignCurrentFileTo(DateTime now, bool nextSequence = false)
  2. {
  3. if (!_nextCheckpoint.HasValue)
  4. {
  5. OpenFile(now);
  6. }
  7. else if (nextSequence || now >= _nextCheckpoint.Value)
  8. {
  9. int? minSequence = null;
  10. if (nextSequence)
  11. {
  12. if (_currentFileSequence == null)
  13. minSequence = 1;
  14. else
  15. minSequence = _currentFileSequence.Value + 1;
  16. }
  17. CloseFile();
  18. OpenFile(now, minSequence);
  19. }}
打开文件
  1. void OpenFile(DateTime now, int? minSequence = null)
  2. {
  3. var currentCheckpoint = _roller.GetCurrentCheckpoint(now);
  4. // We only try periodically because repeated failures
  5. // to open log files REALLY slow an app down.
  6. _nextCheckpoint = _roller.GetNextCheckpoint(now) ?? now.AddMinutes(30);
  7. var existingFiles = Enumerable.Empty<string>();
  8. try
  9. {
  10. if (Directory.Exists(_roller.LogFileDirectory))
  11. {
  12. existingFiles = Directory.GetFiles(_roller.LogFileDirectory, _roller.DirectorySearchPattern)
  13. .Select(f => Path.GetFileName(f));
  14. }
  15. }
  16. catch (DirectoryNotFoundException) { }
  17. var latestForThisCheckpoint = _roller
  18. .SelectMatches(existingFiles)
  19. .Where(m => m.DateTime == currentCheckpoint)
  20. .OrderByDescending(m => m.SequenceNumber)
  21. .FirstOrDefault();
  22. var sequence = latestForThisCheckpoint?.SequenceNumber;
  23. if (minSequence != null)
  24. {
  25. if (sequence == null || sequence.Value < minSequence.Value)
  26. sequence = minSequence;
  27. }
  28. const int maxAttempts = 3;
  29. for (var attempt = 0; attempt < maxAttempts; attempt++)
  30. {
  31. _roller.GetLogFilePath(now, sequence, out var path);
  32. try
  33. {
  34. _currentFile = _shared ?
  35. (IFileSink)new SharedFileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding) :
  36. new FileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding, _buffered, _hooks);
  37. _currentFileSequence = sequence;
  38. }
  39. catch (IOException ex)
  40. {
  41. if (IOErrors.IsLockedFile(ex))
  42. {
  43. SelfLog.WriteLine("File target {0} was locked, attempting to open next in sequence (attempt {1})", path, attempt + 1);
  44. sequence = (sequence ?? 0) + 1;
  45. continue;
  46. }
  47. throw;
  48. }
  49. ApplyRetentionPolicy(path, now);
  50. return;
  51. }
  52. }

SharedFileSink方式写入:

  1. public SharedFileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding? encoding = null)
  2. {
  3. if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 1)
  4. throw new ArgumentException("Invalid value provided; file size limit must be at least 1 byte, or null");
  5. _path = path ?? throw new ArgumentNullException(nameof(path));
  6. _textFormatter = textFormatter ?? throw new ArgumentNullException(nameof(textFormatter));
  7. _fileSizeLimitBytes = fileSizeLimitBytes;
  8. var directory = Path.GetDirectoryName(path);
  9. if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory))
  10. {
  11. Directory.CreateDirectory(directory);
  12. }
  13. // FileSystemRights.AppendData sets the Win32 FILE_APPEND_DATA flag. On Linux this is O_APPEND, but that API is not yet
  14. // exposed by .NET Core.
  15. _fileOutput = new FileStream(
  16. path,
  17. FileMode.Append,
  18. FileSystemRights.AppendData,
  19. FileShare.ReadWrite,
  20. _fileStreamBufferLength,
  21. FileOptions.None);
  22. _writeBuffer = new MemoryStream();
  23. _output = new StreamWriter(_writeBuffer,
  24. encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
  25. }
可以看到并未使用using 释放资源
  1. bool IFileSink.EmitOrOverflow(LogEvent logEvent)
  2. {
  3. if (logEvent == null) throw new ArgumentNullException(nameof(logEvent));
  4. lock (_syncRoot)
  5. {
  6. try
  7. {
  8. _textFormatter.Format(logEvent, _output);
  9. _output.Flush();
  10. var bytes = _writeBuffer.GetBuffer();
  11. var length = (int) _writeBuffer.Length;
  12. if (length > _fileStreamBufferLength)
  13. {
  14. var oldOutput = _fileOutput;
  15. _fileOutput = new FileStream(
  16. _path,
  17. FileMode.Append,
  18. FileSystemRights.AppendData,
  19. FileShare.ReadWrite,
  20. length,
  21. FileOptions.None);
  22. _fileStreamBufferLength = length;
  23. oldOutput.Dispose();
  24. }
  25. if (_fileSizeLimitBytes != null)
  26. {
  27. try
  28. {
  29. if (_fileOutput.Length >= _fileSizeLimitBytes.Value)
  30. return false;
  31. }
  32. catch (FileNotFoundException) { } // Cheaper and more reliable than checking existence
  33. }
  34. _fileOutput.Write(bytes, 0, length);
  35. _fileOutput.Flush();
  36. return true;
  37. }
  38. catch
  39. {
  40. // Make sure there's no leftover cruft in there.
  41. _output.Flush();
  42. throw;
  43. }
  44. finally
  45. {
  46. _writeBuffer.Position = 0;
  47. _writeBuffer.SetLength(0);
  48. }
  49. }
  50. }
最终实现 可以看出只是将文件流缓存区清空了

FileSink 方式写入:

打开文件方式
  1. internal FileSink(
  2. string path,
  3. ITextFormatter textFormatter,
  4. long? fileSizeLimitBytes,
  5. Encoding? encoding,
  6. bool buffered,
  7. FileLifecycleHooks? hooks)
  8. {
  9. if (path == null) throw new ArgumentNullException(nameof(path));
  10. if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 1) throw new ArgumentException("Invalid value provided; file size limit must be at least 1 byte, or null.");
  11. _textFormatter = textFormatter ?? throw new ArgumentNullException(nameof(textFormatter));
  12. _fileSizeLimitBytes = fileSizeLimitBytes;
  13. _buffered = buffered;
  14. var directory = Path.GetDirectoryName(path);
  15. if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory))
  16. {
  17. Directory.CreateDirectory(directory);
  18. }
  19. Stream outputStream = _underlyingStream = System.IO.File.Open(path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read);
  20. outputStream.Seek(0, SeekOrigin.End);
  21. if (_fileSizeLimitBytes != null)
  22. {
  23. outputStream = _countingStreamWrapper = new WriteCountingStream(_underlyingStream);
  24. }
  25. // Parameter reassignment.
  26. encoding = encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
  27. if (hooks != null)
  28. {
  29. outputStream = hooks.OnFileOpened(path, outputStream, encoding) ??
  30. throw new InvalidOperationException($"The file lifecycle hook `{nameof(FileLifecycleHooks.OnFileOpened)}(...)` returned `null`.");
  31. }
  32. _output = new StreamWriter(outputStream, encoding);
  33. }
也可以看到并未使用using 释放资源。 最终实现:
  1. bool IFileSink.EmitOrOverflow(LogEvent logEvent)
  2. {
  3. if (logEvent == null) throw new ArgumentNullException(nameof(logEvent));
  4. lock (_syncRoot)
  5. {
  6. if (_fileSizeLimitBytes != null)
  7. {
  8. if (_countingStreamWrapper!.CountedLength >= _fileSizeLimitBytes.Value)
  9. return false;
  10. }
  11. _textFormatter.Format(logEvent, _output);
  12. if (!_buffered)
  13. _output.Flush();
  14. return true;
  15. }
  16. }
也能看到只是清空了缓冲区。 serilog是将文件资源常驻内存的方式写入。优点是写入效率快;缺点是内存一直被占用,如果文件意外被删除,在不重启程序的情况下不会在进行日志写入。

二、异常

在.NET中,异常是指成员没有完成它的名称宣称可以完成的行动。在异常的机制中,异常和某件事情的发生频率无关。 异常处理四要素包括:
  • 一个表示异常详细信息的类类型;
  • 一个向调用者引发异常类实例的成员;
  • 调用者的一段调用异常成员的代码块;
  • 调用者的一段处理将要发生异常的代码块。
异常类类型包括:
  • 基类:System.Exception;
  • 系统级异常:System.SystemException;
  • 应用程序级异常:System.ApplicationException。

1. 导致异常的原因:

  1. 一、代码错误,包括语法错误、逻辑错误
  2. 二、资源不可用,这是由系统访问了未经授权的资源而引起的错误。
  3. 三、公共语言运行库,这是有CLR内部引起的错误。

2. 在.NET中有如下的异常类:

(1).由System.SystemException派生的异常类型:

System.AccessViolationException 在试图读写受保护内存时引发的异常。
System.ArgumentException 在向方法提供的其中一个参数无效时引发的异常。
System.Collections.Generic.KeyNotFoundException 指定用于访问集合中元素的键与集合中的任何键都不匹配时所引发的异常。
System.IndexOutOfRangeException 访问数组时,因元素索引超出数组边界而引发的异常。
System.InvalidCastException 因无效类型转换或显示转换引发的异常。
System.InvalidOperationException 当方法调用对于对象的当前状态无效时引发的异常。
System.InvalidProgramException 当程序包含无效Microsoft中间语言(MSIL)或元数据时引发的异常,这通常表示生成程序的编译器中有bug。
System.IO.IOException 发生I/O错误时引发的异常。
System.NotImplementedException 在无法实现请求的方法或操作时引发的异常。
System.NullReferenceException 尝试对空对象引用进行操作时引发的异常。
System.OutOfMemoryException 没有足够的内存继续执行程序时引发的异常。
System.StackOverflowException 挂起的方法调用过多而导致执行堆栈溢出时引发的异常。

(2).由System.ArgumentException派生的异常类型:

System.ArgumentNullException 当将空引用传递给不接受它作为有效参数的方法时引发的异常。
System.ArgumentOutOfRangeException 当参数值超出调用的方法所定义的允许取值范围时引发的异常。

(3).由System.ArithmeticException派生的异常类型:

System.DivideByZeroException 试图用零除整数值或十进制数值时引发的异常。
System.NotFiniteNumberException 当浮点值为正无穷大、负无穷大或非数字(NaN)时引发的异常。
System.OverflowException 在选中的上下文中所进行的算数运算、类型转换或转换操作导致溢出时引发的异常。

(4).由System.IOException派生的异常类型:

System.IO.DirectoryNotFoundException 当找不到文件或目录的一部分时所引发的异常。
System.IO.DriveNotFoundException 当尝试访问的驱动器或共享不可用时引发的异常。
System.IO.EndOfStreamException 读操作试图超出流的末尾时引发的异常。
System.IO.FileLoadException 当找到托管程序却不能加载它时引发的异常。
System.IO.FileNotFoundException 试图访问磁盘上不存在的文件失败时引发的异常。
System.IO.PathTooLongException 当路径名或文件名超过系统定义的最大长度时引发的异常。

(5).其他常见异常类型:

ArrayTypeMismatchException 试图在数组中存储错误类型的对象。
BadImageFormatException 图形的格式错误。
DivideByZeroException 除零异常。
DllNotFoundException 找不到引用的dll。
FormatException 参数格式错误。
MethodAccessException 试图访问私有或者受保护的方法。
MissingMemberException 访问一个无效版本的dll。
NotSupportedException 调用的方法在类中没有实现。
PlatformNotSupportedException 平台不支持某个特定属性时抛出该错误。

(6).常见具体的异常对象:

ArgumentNullException 一个空参数传递给方法,该方法不能接受该参数
ArgumentOutOfRangeException 参数值超出范围
ArithmeticException 出现算术上溢或者下溢
ArrayTypeMismatchException 试图在数组中存储错误类型的对象
BadImageFormatException 图形的格式错误
DivideByZeroException 除零异常
DllNotFoundException 找不到引用的DLL
FormatException 参数格式错误
IndexOutOfRangeException 数组索引超出范围
InvalidCastException 使用无效的类
InvalidOperationException 方法的调用时间错误
NotSupportedException 调用的方法在类中没有实现
NullReferenceException 试图使用一个未分配的引用
OutOfMemoryException 内存空间不够
StackOverflowException 堆栈溢出

C#异常类一、基类Exception

C#异常类二、常见的异常类

1、SystemException类:该类是System命名空间中所有其他异常类的基类。(建议:公共语言运行时引发的异常通常用此类)
2、ApplicationException类:该类表示应用程序发生非致命错误时所引发的异常(建议:应用程序自身引发的异常通常用此类)

C#异常类三、与参数有关的异常类

此类异常类均派生于SystemException,用于处理给方法成员传递的参数时发生异常
1、ArgumentException类:该类用于处理参数无效的异常,除了继承来的属性名,此类还提供了string类型的属性ParamName表示引发异常的参数名称。
2、FormatException类:该类用于处理参数格式错误的异常。

C#异常类四、与成员访问有关的异常

1、MemberAccessException类:该类用于处理访问类的成员失败时所引发的异常。失败的原因可能的原因是没有足够的访问权限,也可能是要访问的成员根本不存在(类与类之间调用时常用)
2、MemberAccessException类的直接派生类:
i、FileAccessException类:该类用于处理访问字段成员失败所引发的异常
ii、MethodAccessException类:该类用于处理访问方法成员失败所引发的异常
iii、MissingMemberException类:该类用于处理成员不存在时所引发的异常

C#异常类五、与数组有关的异常

以下三个类均继承于SystemException类
1、IndexOutOfException类:该类用于处理下标超出了数组长度所引发的异常
2、ArrayTypeMismatchException类:该类用于处理在数组中存储数据类型不正确的元素所引发的异常
3、RankException类:该类用于处理维数错误所引发的异常

C#异常类六、与IO有关的异常

1、IOException类:该类用于处理进行文件输入输出操作时所引发的异常。
2、IOException类的5个直接派生类:
i、DirectionNotFoundException类:该类用于处理没有找到指定的目录而引发的异常。
ii、FileNotFoundException类:该类用于处理没有找到文件而引发的异常。
iii、EndOfStreamException类:该类用于处理已经到达流的末尾而还要继续读数据而引发的异常。
iv、FileLoadException类:该类用于处理无法加载文件而引发的异常。
v、PathTooLongException类:该类用于处理由于文件名太长而引发的异常。

C#异常类七、与算术有关的异常

1、ArithmeticException类:该类用于处理与算术有关的异常。
2、ArithmeticException类的派生类:
i、DivideByZeroException类:表示整数货十进制运算中试图除以零而引发的异常。
ii、NotFiniteNumberException类:表示浮点数运算中出现无穷打或者非负值时所引发的异常。 在.NET框架中的异常类都派生自SystemException 类。这个类的大部分常用成员如下: HelpLink是一个链接到帮助文件的链接,该帮助文件提供异常的相关信息。
Message是指明一个错误细节的文本。
Source导致异常的对象或应用的名称。
StackTrace是堆栈中调用的方法列表。
TargetSite是抛出异常的方法名称。

在该层次中有两个重要的类,他们派生自System.Exception类:

  • SystemException———该类用于通常由.NET允许库抛出的异常,或者由几乎所有的应用程序抛出的异常。例如,如果.NET运行库检测到栈已满,他就会抛出StackOverflowException异常。另一方面,如果检测到调用方法时参数不对,就可以在自己的代码中选择抛出ArgumentException异常或其子类。SystemException异常的子类包括表示致命错误和非致命错误的异常。
  • ApplicationException——在.NETFramework最初的设计中,是打算把这个类作为自定义应用程序异常类的基类的。不过,CLR抛出的一些异常类也派生自这个类。应用程序抛出的异常则派生自SystemException。因此从ApplicationException派生自自定义异常类型没有任何好处,取而代之的是,可以直接从Exception基类派生自定义异常类。  

其他可能会用到的异常类包括:

  • StackOverflowException———-如果分配给栈的内存区域已满,就会抛出这个异常。如果一个方法连续地递归调用自己,就可能发生栈溢出。这一般是一个致命错误,因为它禁止应用程序执行除了中断以外的其他任务。在这种情况下,甚至也不可能执行到finally块。通常用户自己不能处理像这样的错误,而应退出应用程序。
  • EndOfStreamException———-这个异常通常是因为读到文件末尾而抛出的,流表示数据源之间的数据流。
  • OverflowException——-如果要在checked上下文中把包含-40的int类型数据强制转换为uint(32位无符号整数)数据,就会抛出这个异常
  • MemberAccessException—————该类用于处理访问类的成员失败时所引发的异常。失败的原因可能的原因是没有足够的访问权限,也可能是要访问的成员根本不存在(类与类之间调用时常用)
  • IndexOutOfException———-该类用于处理下标超出了数组长度所引发的异常

3. .NET的异常处理方式:

发生异常时,系统将搜索可以处理该异常的最近的 catch 子句(根据该异常的运行时类型来确定)。首先,搜索当前的方法以查找一个词法上包含着它的 try 语句,并按顺序考察与该 try 语句相关联的各个 catch 子句。如果上述操作失败,则在调用了当前方法的方法中,搜索在词法上包含着当前方法调用代码位置的 try 语句。此搜索将一直进行下去,直到找到可以处理当前异常的 catch 子句(该子句指定一个异常类,它与当前引发该异常的运行时类型属于同一个类或是该运行时类型所属类的一个基类)。注意,没有指定异常类的 catch 子句可以处理任何异常。 找到匹配的 catch 子句后,系统将把控制转移到该 catch 子句的第一条语句。在 catch 子句的执行开始前,系统将首先按顺序执行嵌套在捕捉到该异常的 try 语句里面的所有 try 语句所对应的全部 finally 子句。

(1).try块

包含的代码通常需要执行一些通用的资源清理操作,或者需要从异常中恢复,或者两者都需要。try块还可以包含也许会抛出异常的代码。

(2).catch块

包含的是响应一个异常需要执行的代码。如果没有任何捕捉类型与抛出的异常匹配,CLR会去调用栈的更高一层搜索一个与异常匹配的捕捉类型。

(3).finally块

包含的代码是保证会执行的代码。finally块所有代码执行完毕后,线程退出finally块,执行紧跟在finally块之后的语句。

系统异常%26日志 - 图4

4. Exception的常用属性的源码解析:

(1).Message:包含辅助性文字说明,指出抛出异常的原因。

  1. public virtual String Message {
  2. get {
  3. if (_message == null) {
  4. if (_className==null) {
  5. _className = GetClassName();
  6. }
  7. return Environment.GetRuntimeResourceString("Exception_WasThrown", _className);
  8. } else {
  9. return _message;
  10. }
  11. }
  12. }

(2).Data:对一个“键/值对”集合的引用。

  1. public virtual IDictionary Data {
  2. [System.Security.SecuritySafeCritical] // auto-generated
  3. get {
  4. if (_data == null)
  5. if (IsImmutableAgileException(this))
  6. _data = new EmptyReadOnlyDictionaryInternal();
  7. else
  8. _data = new ListDictionaryInternal();
  9. return _data;
  10. }
  11. }

(3).Source:包含生成异常的程序集名称。

  1. public virtual String Source {
  2. #if FEATURE_CORECLR
  3. [System.Security.SecurityCritical] // auto-generated
  4. #endif
  5. get {
  6. if (_source == null)
  7. {
  8. StackTrace st = new StackTrace(this,true);
  9. if (st.FrameCount>0)
  10. {
  11. StackFrame sf = st.GetFrame(0);
  12. MethodBase method = sf.GetMethod();
  13. Module module = method.Module;
  14. RuntimeModule rtModule = module as RuntimeModule;
  15. if (rtModule == null)
  16. {
  17. System.Reflection.Emit.ModuleBuilder moduleBuilder = module as System.Reflection.Emit.ModuleBuilder;
  18. if (moduleBuilder != null)
  19. rtModule = moduleBuilder.InternalModule;
  20. else
  21. throw new ArgumentException(Environment.GetResourceString("Argument_MustBeRuntimeReflectionObject"));
  22. }
  23. _source = rtModule.GetRuntimeAssembly().GetSimpleName();
  24. }
  25. }
  26. return _source;
  27. }
  28. #if FEATURE_CORECLR
  29. [System.Security.SecurityCritical] // auto-generated
  30. #endif
  31. set { _source = value; }
  32. }

5. 异常处理的常用方法:

(1).提取异常及其内部异常堆栈跟踪

  1. /// <summary>
  2. /// 提取异常及其内部异常堆栈跟踪
  3. /// </summary>
  4. /// <param name="exception">提取的例外</param>
  5. /// <param name="lastStackTrace">最后提取的堆栈跟踪(对于递归), String.Empty or null</param>
  6. /// <param name="exCount">提取的堆栈数(对于递归)</param>
  7. /// <returns>Syste.String</returns>
  8. public static string ExtractAllStackTrace(this Exception exception, string lastStackTrace = null, int exCount = 1)
  9. {
  10. var ex = exception;
  11. const string entryFormat = "#{0}: {1}\r\n{2}";
  12. //修复最后一个堆栈跟踪参数
  13. lastStackTrace = lastStackTrace ?? string.Empty;
  14. //添加异常的堆栈跟踪
  15. lastStackTrace += string.Format(entryFormat, exCount, ex.Message, ex.StackTrace);
  16. if (exception.Data.Count > 0)
  17. {
  18. lastStackTrace += "\r\n Data: ";
  19. foreach (var item in exception.Data)
  20. {
  21. var entry = (DictionaryEntry)item;
  22. lastStackTrace += string.Format("\r\n\t{0}: {1}", entry.Key, exception.Data[entry.Key]);
  23. }
  24. }
  25. //递归添加内部异常
  26. if ((ex = ex.InnerException) != null)
  27. return ex.ExtractAllStackTrace(string.Format("{0}\r\n\r\n", lastStackTrace), ++exCount);
  28. return lastStackTrace;
  29. }

(2).检查字符串是空的或空的,并抛出一个异常

  1. /// <summary>
  2. /// 检查字符串是空的或空的,并抛出一个异常
  3. /// </summary>
  4. /// <param name="val">值测试</param>
  5. /// <param name="paramName">参数检查名称</param>
  6. public static void CheckNullOrEmpty(string val, string paramName)
  7. {
  8. if (string.IsNullOrEmpty(val))
  9. throw new ArgumentNullException(paramName, "Value can't be null or empty");
  10. }

(3).检查参数不是无效,并抛出一个异常

  1. /// <summary>
  2. /// 检查参数不是无效,并抛出一个异常
  3. /// </summary>
  4. /// <param name="param">检查值</param>
  5. /// <param name="paramName">参数名称</param>
  6. public static void CheckNullParam(object param, string paramName)
  7. {
  8. if (param == null)
  9. throw new ArgumentNullException(paramName, paramName + " can't be null");
  10. }

(4).请检查参数1不同于参数2

  1. /// <summary>
  2. /// 请检查参数1不同于参数2
  3. /// </summary>
  4. /// <param name="param1">值1测试</param>
  5. /// <param name="param1Name">name of value 1</param>
  6. /// <param name="param2">value 2 to test</param>
  7. /// <param name="param2Name">name of vlaue 2</param>
  8. public static void CheckDifferentsParams(object param1, string param1Name, object param2, string param2Name)
  9. {
  10. if (param1 == param2) {
  11. throw new ArgumentException(param1Name + " can't be the same as " + param2Name,
  12. param1Name + " and " + param2Name);
  13. }
  14. }

(5).检查一个整数值是正的(0或更大)

  1. /// <summary>
  2. /// 检查一个整数值是正的(0或更大)
  3. /// </summary>
  4. /// <param name="val">整数测试</param>
  5. public static void PositiveValue(int val)
  6. {
  7. if (val < 0)
  8. throw new ArgumentException("The value must be greater than or equal to 0.");
  9. }
异常处理器(程序):对于程序中出现的异常,在C#中是使用一种被称为“异常处理器(程序)”的错误捕获机制来进行处理的, 你可以认为异常处理器(程序)就是发生错误时,能够接受并处理错误的接受者和处理者。

microsoft官方文档也给出了一些解决方案,具体可点击链接查看。

(6).全局异常处理

asp.net core

.Net Core中有各种Filter,分别是AuthorizationFilter、ResourceFilter、ExceptionFilter、ActionFilter、ResultFilter。可以把他们看作是.Net Core自带的AOP的扩展封装。 今天来看其中的一种:ExceptionFilter(用于全局的异常处理) 新建一个.Net Core MVC的项目

系统异常%26日志 - 图5

这里我们可以看到代码运行到16行时会报一个索引项超出集合范围的错误 按照常规的思维我们在代码中会加异常处理,如下:
  1. try
  2. {
  3. var range = Enumerable.Range(1, 3).ToArray();
  4. var result = range[4];
  5. return View();
  6. }
  7. catch (Exception ex)
  8. {
  9. throw new Exception(ex.Message);
  10. }
但是每个方法都这样加会不会觉得很烦?有没有想过只写一次就可以了 上代码:

系统异常%26日志 - 图6

  1. using Microsoft.AspNetCore.Hosting;
  2. using Microsoft.AspNetCore.Mvc;
  3. using Microsoft.AspNetCore.Mvc.Filters;
  4. using Microsoft.AspNetCore.Mvc.ModelBinding;
  5. using Microsoft.AspNetCore.Mvc.ViewFeatures;
  6. using System;
  7. using System.Collections.Generic;
  8. using System.Linq;
  9. using System.Threading.Tasks;
  10. namespace ExceptionFilter.Filter
  11. {
  12. public class CustomerExceptionFilter : Attribute, IExceptionFilter
  13. {
  14. private readonly IHostingEnvironment _hostingEnvironment;
  15. private readonly IModelMetadataProvider _modelMetadataProvider;
  16. public CustomerExceptionFilter(IHostingEnvironment hostingEnvironment,
  17. IModelMetadataProvider modelMetadataProvider)
  18. {
  19. _hostingEnvironment = hostingEnvironment;
  20. _modelMetadataProvider = modelMetadataProvider;
  21. }
  22. /// <summary>
  23. /// 发生异常进入
  24. /// </summary>
  25. /// <param name="context"></param>
  26. public async void OnException(ExceptionContext context)
  27. {
  28. if (!context.ExceptionHandled)//如果异常没有处理
  29. {
  30. if (_hostingEnvironment.IsDevelopment())//如果是开发环境
  31. {
  32. var result = new ViewResult { ViewName = "../Handle/Index" };
  33. result.ViewData = new ViewDataDictionary(_modelMetadataProvider,
  34. context.ModelState);
  35. result.ViewData.Add("Exception", context.Exception);//传递数据
  36. context.Result = result;
  37. }
  38. else
  39. {
  40. context.Result = new JsonResult(new
  41. {
  42. Result = false,
  43. Code = 500,
  44. Message = context.Exception.Message
  45. });
  46. }
  47. context.ExceptionHandled = true;//异常已处理
  48. }
  49. }
  50. }
  51. }
我们在方法中先以特性来使用,加上这句代码:

<font style="color:rgb(0, 0, 0);background-color:rgb(242, 244, 245);">[TypeFilter(</font><font style="color:rgb(0, 0, 255);background-color:rgb(242, 244, 245);">typeof</font><font style="color:rgb(0, 0, 0);background-color:rgb(242, 244, 245);">(CustomerExceptionFilter))]</font>

系统异常%26日志 - 图7

之后会跳到这个视图:../Handle/Index ,会将异常信息传入到此视图 视图页代码:

<font style="color:rgb(0, 0, 0);background-color:rgb(242, 244, 245);"> <p>Message:@ViewData[</font><font style="color:rgb(128, 0, 0);background-color:rgb(242, 244, 245);">"Exception"</font><font style="color:rgb(0, 0, 0);background-color:rgb(242, 244, 245);">]</p></font>

(可以自行封装。。。) 我们还可以定义成全局的,在Startup类中的ConfigureServices方法中加入这句代码
  1. services.AddControllersWithViews(option =>
  2. {
  3. option.Filters.Add<CustomerExceptionFilter>();
  4. });
  5. //3.0以下的版本好像应该这样写:services.AddMvc();
关于AddMvc/AddMvcCore/AddControllers等区别,移步https://www.yuque.com/schafferyy/net/twgia8 关于配置全局异常中间件捕获异常,请移步https://zhuanlan.zhihu.com/p/341228413 #### asp.net mvc 1. 在Global.asax里添加Application_Error事件(注意:Application_Start外部添加这个方法) csharp protected void Application_Error(Object sender, EventArgs e) { Exception lastError = Server.GetLastError();//获取异常 if (lastError != null) { //异常信息 string strExceptionMessage = string.Empty; //对HTTP 404做额外处理,其他错误全部当成500服务器错误 HttpException httpError = lastError as HttpException; if (httpError != null) { //获取错误代码 int httpCode = httpError.GetHttpCode(); //获取异常信息 strExceptionMessage = httpError.Message; if (httpCode == 400 || httpCode == 404) { Response.StatusCode = 404; //跳转到指定的静态404信息页面 Response.WriteFile("~/404.html"); //一定要调用Server.ClearError()否则会触发错误详情页(就是黄页) Server.ClearError(); return; } } strExceptionMessage = lastError.Message;//得到错误信息,可以写到日志里 /*----------------------------------------------------- * 此处代码可根据需求进行日志记录,或者处理其他业务流程 * ---------------------------------------------------*/ /* 跳转到静态页面一定要用Response.WriteFile方法 */ //Response.StatusCode = 500;展示错误状态码 Response.WriteFile("~/error.html"); //一定要调用Server.ClearError()否则会触发错误详情页(就是黄页) Server.ClearError(); } } 2. 注销掉FilterConfig类里的filters.Add(new HandleErrorAttribute()),否则会默认自定义。 使用此方法一定要把GlobalFilter全局过滤器中的HandleErrorAttribute注册取消掉,也可以将配置文件中的customErrors节点关闭,否则HTTP 500的错误将不会被Application_Error事件捕获。

系统异常%26日志 - 图8

系统异常%26日志 - 图9

  1. 在web.con.fig配置节里添加配置,关闭自定义的模式,否则VS会默认开启
  1. <customErrors mode="Off"></customErrors>
  2. <!--注意大小写-->
  1. 在控制器里写错误的代码
  1. public ActionResult Show()
  2. {
  3. int a = 0;
  4. int b = 1;
  5. int c = b/a;
  6. return Content(c.ToString());
  7. }
捕获到异常之后我们可以很容易地跳转到静态页面
  1. protected void Application_Error(object sender, EventArgs e)
  2. {
  3. Exception exception = Server.GetLastError();
  4. var httpStatusCode = (exception as HttpException)?.GetHttpCode() ?? 700; //如果为空则走自定义
  5. var httpContext = ((MvcApplication)sender).Context;
  6. httpContext.ClearError();
  7. switch (httpStatusCode)
  8. {
  9. case 404:
  10. httpContext.Response.Redirect("~/Error/404.htm");
  11. break;
  12. default:
  13. httpContext.Response.Redirect("~/Error/500.htm");
  14. break;
  15. }
  16. }
在一般情况下我们也可以指向一个控制器
  1. protected void Application_Error(object sender, EventArgs e)
  2. {
  3. Exception exception = Server.GetLastError();
  4. var httpStatusCode = (exception as HttpException)?.GetHttpCode() ?? 700; //如果为空则走自定义
  5. var httpContext = ((MvcApplication)sender).Context;
  6. httpContext.ClearError();
  7. var routeDic = new RouteValueDictionary
  8. {
  9. {"controller", "Home"},
  10. { "action","Error"}
  11. };
  12. httpContext.Response.RedirectToRoute("Default", routeDic);
  13. }
但是在实际的业务中遇到了一些http请求的问题,在处理一部分代码抛出的异常时会出现“服务器无法在已发送HTTP标头之后······”这一系列异常,如“设置状态”、“追加标头”等,这个时候跳转要使用另一种写法
  1. protected void Application_Error(object sender, EventArgs e)
  2. {
  3. Server.ClearError();
  4. Response.TrySkipIisCustomErrors = true;
  5. var routeData = new RouteData();
  6. IController controller = new HomeController();
  7. routeData.Values.Add("controller", "Home");
  8. routeData.Values.Add("action", "Error");
  9. controller.Execute(new RequestContext(new HttpContextWrapper(Context), routeData));
  10. Response.End();
  11. }
这里要注意的一点是如果要使用Area中的控制器不能写成routeData.Values.Add,而是使用DataTokens
  1. routeData.DataTokens.Add("area", "TestArea");