更好的TypeInitializationException(innerException也为null)
介绍
当用户在NLog的配置中创建错误(如无效的XML)时,We(NLog)会抛出NLogConfigurationException
。 该例外包含描述错误的内容。
但是,如果第一次调用NLog来自静态字段/属性,有时这个NLogConfigurationException
被System.TypeInitializationException
“吃掉”。
例
例如,如果用户有此程序:
using System; using System.Collections.Generic; using System.Linq; using NLog; namespace TypeInitializationExceptionTest { class Program { //this throws a NLogConfigurationException because of bad config. (like invalid XML) private static Logger logger = LogManager.GetCurrentClassLogger(); static void Main() { Console.WriteLine("Press any key"); Console.ReadLine(); } } }
并且配置中存在错误,NLog抛出:
throw new NLogConfigurationException("Exception occurred when loading configuration from " + fileName, exception);
但是用户会看到:
“将exception详细信息复制到剪贴板”:
System.TypeInitializationException未处理消息:mscorlib.dll中发生未处理的类型’System.TypeInitializationException’的exception附加信息:’TypeInitializationExceptionTest.Program’的类型初始值设定项引发exception。
所以消息消失了!
问题
- 为什么innerException不可见? (在Visual Studio 2013中测试)。
- 我可以向
TypeInitializationException
发送更多信息吗? 像一条消息? 我们已经发送了一个innerException。 - 我们可以使用另一个例外,还是在
Exception
上有属性,以便报告更多信息? - 还有另一种方法可以向用户提供(更多)反馈吗?
笔记
- 当然,我们对用户编写的程序没有任何影响。
- 我是NLog的维护者之一。
- 你喜欢自己测试一下吗? Checkout https://github.com/NLog/NLog/tree/TypeInitializationException-tester并启动NLog / src / NLog.netfx45.sln
编辑 :
请注意我是图书馆维护者,而不是图书馆的用户。 我无法更改调用代码!
我只想指出你在这里遇到的根本问题。 您正在调试调试器中的错误,它有一个非常简单的解决方法。 使用工具>选项>调试>常规>勾选“使用托管兼容模式”复选框。 同时解开Just My Code以获取最丰富的调试报告:
如果勾选了“仅我的代码”,则exception报告的信息量较少,但仍可通过单击“查看详细信息”链接轻松钻取。
选项名称不必要地含糊不清。 它真正的作用是告诉Visual Studio使用旧版本的调试引擎。 任何使用VS2013或VS2015的人都会遇到新引擎的问题,可能是VS2012。 也是之前NLog没有解决这个问题的基本原因。
虽然这是一个非常好的解决方法,但它并不容易发现。 程序员也不会特别喜欢使用旧引擎,旧引擎不支持返回值调试和64位代码的E + C等shiny的新function。 这是否真的是一个错误,新引擎的疏忽或技术限制很难猜测。 这太难看了,所以不要犹豫,将它标记为“bug”,我强烈建议你把它带到connect.microsoft.com。 当它被修复时,每个人都会领先,我至少在我记忆中抓过一次。 通过使用Debug> Windows> Exceptions>勾选当时的CLRexception来深入研究它。
这种非常不幸的行为的解决办法肯定是丑陋的。 您必须延迟提升exception,直到程序执行进展得足够远。 我不太了解您的代码库,但延迟解析配置,直到第一个日志记录命令应该处理它。 或者存储exception对象并将其抛出到第一个日志命令上,可能更容易。
我看到的原因是因为入口点类的类型初始化失败。 由于没有初始化类型,因此Type loader无法报告TypeInitializationException
的失败类型。
但是如果将logger的Static初始化程序更改为其他类,然后在Entry方法中引用该类。 你将获得TypeInitializationexception的InnerException。
static class TestClass { public static Logger logger = LogManager.GetCurrentClassLogger(); } class Program { static void Main(string[] args) { var logger = TestClass.logger; Console.WriteLine("Press any key"); Console.ReadLine(); } }
现在您将获得InnerException,因为已加载Entry类型以报告TypeInitializationException。
希望你现在能够保持Entry point的清洁,并从Main()而不是Entry point class的static属性引导应用程序。
更新1
您还可以使用Lazy<>
来避免在声明时执行配置初始化。
class Program { private static Lazy logger = new Lazy (() => LogManager.GetCurrentClassLogger()); static void Main(string[] args) { //this will throw TypeInitialization with InnerException as a NLogConfigurationException because of bad config. (like invalid XML) logger.Value.Info("Test"); Console.WriteLine("Press any key"); Console.ReadLine(); } }
或者,在LogManager中尝试使用Lazy<>
进行记录器实例化,以便在实际发生第一个日志语句时进行配置初始化。
更新2
我分析了NLog的源代码,看起来它已经实现了,这很有道理。 根据对属性的评论“除非由LogManager.ThrowExceptions
中的属性LogManager.ThrowExceptions
指定,否则NLog不应抛出exception”。
修复 – 在LogFactory类中,私有方法GetLogger()具有导致exception发生的初始化语句。 如果您通过检查属性ThrowExceptions
引入try catch,则可以阻止初始化exception。
if (cacheKey.ConcreteType != null) { try { newLogger.Initialize(cacheKey.Name, this.GetConfigurationForLogger(cacheKey.Name, this.Configuration), this); } catch (Exception ex) { if(ThrowExceptions && ex.MustBeRethrown()) throw; } }
将这些exception/错误存储在某处也是很好的,这样就可以跟踪Logger初始化失败的原因,因为它们因ThrowException
而被忽略。
问题是在首次引用类时发生静态初始化。 在您的Program
它甚至在Main()
方法之前发生。 所以经验法则 – 避免任何可能在静态初始化方法中失败的代码。 至于你的特殊问题 – 使用懒惰的方法:
private static Lazy logger = new Lazy (() => LogManager.GetCurrentClassLogger()); static void Main() { logger.Value.Log(...); }
因此,当您第一次访问记录器时,记录器的初始化将发生(并且可能会失败) – 而不是在某些疯狂的静态上下文中。
UPDATE
您的图书馆用户最终必须坚持最佳实践。 所以,如果是我,我会保持原样。 如果您真的必须在最后解决它,那么几乎没有选择:
1)不要抛出exception – 永远 – 这是记录引擎中的有效方法,以及log4net
如何工作 – 即
static Logger GetCurrentClassLogger() { try { var logger = ...; // current implementation } catch(Exception e) { // let the poor guy now something is wrong - provided he is debugging Debug.WriteLine(e); // null logger - every single method will do nothing return new NullLogger(); } }
2)围绕Logger
类的实现包装惰性方法(我知道你的Logger
类要复杂得多,为了解决这个问题,我们假设它只有一个方法Log
,它需要string className
来构造Logger
实例。
class LoggerProxy : Logger { private Lazy m_Logger; // add all arguments you need to construct the logger instance public LoggerProxy(string className) { m_Logger = new Lazy (() => return new Logger(className)); } public void Log(string message) { m_Logger.Value.Log(message); } } static Logger GetCurrentClassLogger() { var className = GetClassName(); return new LoggerProxy(className); }
你将摆脱这个问题(真正的初始化将在调用第一个log方法时发生,它是向后兼容的方法); 唯一的问题是你已经添加了另一层(我不希望任何大幅度降级的性能,但一些日志引擎真的进入微优化)。
我现在看到的唯一解决方案是:
使用try catch
将静态初始化(fields)移动到静态构造函数
我在之前的评论中说过,我无法重现你的问题。 然后我发现你只是在弹出exception对话框中查找它,它不会显示节目详情链接。
因此,无论如何你都可以获得InnerException
,因为它肯定存在 ,除了Visual Studio因为某些原因没有报告它(可能因为它是入口点类型,因为它被认为是出了问题)。
因此,当您运行测试分支时,您将看到以下对话框:
并且它不显示“ 查看详细信息”链接。
将exception详细信息复制到剪贴板也不是特别有用:
System.TypeInitializationException未处理
消息:mscorlib.dll中发生了未处理的“System.TypeInitializationException”类型exception
附加信息:’TypeInitializationExceptionTest.Program’的类型初始化程序引发了exception。
现在,使用“ 确定”按钮关闭对话框,然后转向“ 本地”调试窗口。 这是你会看到的:
看到? InnerException
肯定在那里,你找到了一个VS怪癖:-)
System.TypeInitializationException总是或几乎总是从不正确的初始化类的静态成员发生。
您必须通过调试器检查LogManager.GetCurrentClassLogger()
。 我确定错误发生在代码部分内部。
//go into LogManager.GetCurrentClassLogger() method private static Logger logger = LogManager.GetCurrentClassLogger();
另外我建议你仔细检查你的app.config并确保你没有包含任何错误。
每当静态构造函数抛出exception时,或者每当您尝试访问静态构造函数引发exception的类时,都会抛出System.TypeInitializationException
。
当.NET
加载类型时,它必须在您第一次使用该类型之前准备它的所有静态字段。 有时,初始化需要运行代码。 当代码失败时,您将获得System.TypeInitializationException
。
根据文件
当类初始化程序无法初始化类型时,会创建TypeInitializationException并传递对类型的类初始值设定项引发的exception的引用。 TypeInitializationException的InnerException属性保存基础exception。 TypeInitializationException使用HRESULT COR_E_TYPEINITIALIZATION,其值为0x80131534。 有关TypeInitializationException实例的初始属性值列表,请参阅TypeInitializationException构造函数。
1)对于这种类型的exception, InnerException
是不可见的。 Visual Studio的版本无关紧要(确保选中“启用例外助手”选项 – Tools> Options>Debugging>General
)
2)通常, TypeInitializationException
隐藏了可以通过InnerException查看的真实exception。 但下面的测试示例显示了如何填充内部exception信息:
public class Test { static Test() { throw new Exception("InnerExc of TypeInitializationExc"); } static void Main(string[] args) { } }
但是,如果第一次调用NLog来自静态字段/属性,有时这个NLogConfigurationException会被System.TypeInitializationException“吃掉”。
没什么奇怪的。 有人错过了某处的try catch
块。