更好的 TypeInitializationException(innerException 也为 null)
Better TypeInitializationException (innerException is also null)
简介
当用户在 NLog 配置中创建错误(如无效 XML)时,我们 (NLog) 会抛出一个 NLogConfigurationException
。异常包含错误的描述。
但有时如果对 NLog 的第一次调用来自静态 field/property.
,则此 NLogConfigurationException
由 System.TypeInitializationException
"eaten"
例子
例如如果用户有这个程序:
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);
但是用户会看到:
"Copy exception details to the clipboard":
System.TypeInitializationException was unhandled
Message: An unhandled exception of type 'System.TypeInitializationException' occurred in mscorlib.dll
Additional information: The type initializer for 'TypeInitializationExceptionTest.Program' threw an exception.
所以消息不见了!
问题
- 为什么 innerException 不可见? (在 Visual Studio 2013 年测试)。
- 我可以向
TypeInitializationException
发送更多信息吗?喜欢留言?我们已经发送了一个 innerException。
- 我们可以使用另一个例外吗?或者
Exception
上是否有属性以便报告更多信息?
- 是否有其他方式向用户提供(更多)反馈?
注释
- 当然我们对用户编写的程序没有任何影响。
- 我是 NLog 的维护者之一。
- 你喜欢自己测试吗?签出 https://github.com/NLog/NLog/tree/TypeInitializationException-tester 并开始 NLog/src/NLog.netfx45.sln
编辑:
请注意,我是图书馆的维护者,不是图书馆的用户。 我无法更改调用代码!
我现在看到的唯一解决办法是:
将静态初始化(字段)移动到具有 try catch
的静态构造函数
System.TypeInitializationException 总是或几乎总是由于未正确初始化 class 的静态成员而发生。
您必须通过调试器检查 LogManager.GetCurrentClassLogger()
。我确定错误发生在该部分代码中。
//go into LogManager.GetCurrentClassLogger() method
private static Logger logger = LogManager.GetCurrentClassLogger();
此外,我建议您仔细检查 app.config 并确保您没有包含任何错误内容。
System.TypeInitializationException
每当静态构造函数抛出异常时,或者每当您尝试访问静态构造函数抛出异常的 class 时抛出。
当 .NET
加载类型时,它必须在 您第一次 使用类型。有时,初始化需要 运行 代码。当该代码失败时,您会得到 System.TypeInitializationException
.
根据 docs
When a class initializer fails to initialize a type, a TypeInitializationException is created and passed a reference to the exception thrown by the type's class initializer. The InnerException property of TypeInitializationException holds the underlying exception.
TypeInitializationException uses the HRESULT COR_E_TYPEINITIALIZATION, that has the value 0x80131534.
For a list of initial property values for an instance of TypeInitializationException, see the TypeInitializationException constructors.
1) InnerException
不可见,这是此类异常的典型表现。 Visual Studio 的版本无关紧要(确保选中 "Enable the exception assistant" 选项 - Tools> Options>Debugging>General
)
2) 通常TypeInitializationException
隐藏了真正的异常,可以通过InnerException查看。但是下面的测试示例显示了如何填充内部异常信息:
public class Test {
static Test() {
throw new Exception("InnerExc of TypeInitializationExc");
}
static void Main(string[] args) {
}
}
But sometimes this NLogConfigurationException is "eaten" by an
System.TypeInitializationException if the first call to NLog is from a
static field/property.
没什么奇怪的。有人在某处错过了 try catch
块。
我看到的原因是入口点class的类型初始化失败。由于没有初始化任何类型,因此类型加载器没有任何关于 TypeInitializationException
中失败类型的报告。
但是如果你把logger的Static initializer改成其他的class然后在Entry方法中引用那个class。你会得到 InnerException on TypeInitialization 异常。
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。
希望您现在有了保持入口点干净的想法,Bootstrap 来自 Main() 的应用程序而不是入口点 class.[=20= 的静态 属性 ]
更新 1
您还可以利用 Lazy<>
来避免在声明时执行配置初始化。
class Program
{
private static Lazy<Logger> logger = new Lazy<Logger>(() => 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的源码,好像已经实现了,很有道理。根据 属性 "NLog should not throw exception unless specified by property LogManager.ThrowExceptions
in LogManager.cs" 的评论。
修复 - 在 LogFactory class 中,私有方法 GetLogger() 具有导致异常发生的初始化语句。如果你引入一个带有 属性 ThrowExceptions
检查的 try catch 那么你可以防止初始化异常。
if (cacheKey.ConcreteType != null)
{
try
{
newLogger.Initialize(cacheKey.Name, this.GetConfigurationForLogger(cacheKey.Name, this.Configuration), this);
}
catch (Exception ex)
{
if(ThrowExceptions && ex.MustBeRethrown())
throw;
}
}
此外,将这些 exceptions/errors 存储在某个地方会很棒,这样就可以跟踪记录器初始化失败的原因,因为它们由于 ThrowException
而被忽略。
问题是在首次引用 class 时发生静态初始化。在您的 Program
中,它甚至发生在 Main()
方法之前。因此,根据经验 - 避免任何可能在静态初始化方法中失败的代码。至于您的特定问题 - 请改用惰性方法:
private static Lazy<Logger> logger =
new Lazy<Logger>(() => LogManager.GetCurrentClassLogger());
static void Main() {
logger.Value.Log(...);
}
所以当您第一次访问记录器时,记录器的初始化将会发生(并且可能会失败)——而不是在一些疯狂的静态上下文中。
更新
坚持最佳实践最终是图书馆用户的负担。所以如果是我,我会保持原样。如果你真的必须自己解决它,那么选择很少:
1) 不要抛出异常 - 永远 - 这是日志引擎中的有效方法,以及 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
class 的实现包装懒惰的方法(我知道你的 Logger
class 要复杂得多,为了这个问题让我们假设它只有一种方法 Log
并且需要 string className
来构建 Logger
实例。
class LoggerProxy : Logger {
private Lazy<Logger> m_Logger;
// add all arguments you need to construct the logger instance
public LoggerProxy(string className) {
m_Logger = new Lazy<Logger>(() => return new Logger(className));
}
public void Log(string message) {
m_Logger.Value.Log(message);
}
}
static Logger GetCurrentClassLogger() {
var className = GetClassName();
return new LoggerProxy(className);
}
你将摆脱这个问题(真正的初始化将在调用第一个日志方法时发生,并且它是向后兼容的方法);唯一的问题是您添加了另一层(我不希望性能大幅下降,但一些日志记录引擎确实进行了微优化)。
我在之前的评论中说过我无法重现您的问题。然后我想到你 只是 在弹出的异常对话框中寻找它,它不显示 显示详细信息 link .
所以这里是您可以到达 InnerException
的方法,因为 它肯定在那里 ,除了 Visual Studio 由于某种原因没有报告它(可能是因为它是 vendettamit 发现的入口点类型)。
所以,当你 运行 tester 分支时,你会得到以下对话框:
并且它不显示 查看详细信息 link。
将异常详细信息复制到剪贴板也不是特别有用:
System.TypeInitializationException was unhandled
Message: An unhandled exception of type 'System.TypeInitializationException' occurred in mscorlib.dll
Additional information: The type initializer for 'TypeInitializationExceptionTest.Program' threw an exception.
现在,使用 OK 按钮关闭对话框并前往 Locals 调试 window。您将看到以下内容:
看到了吗? InnerException
绝对 是 那里,你发现了一个 VS 怪癖:-)
我只是指出你在这里处理的潜在问题。您正在与调试器中的错误作斗争,它有一个非常简单的解决方法。使用工具 > 选项 > 调试 > 常规 > 勾选 "Use Managed Compatibility Mode" 复选框。同时取消勾选“仅我的代码”以获得最有用的调试报告:
如果勾选“仅我的代码”,则异常报告的信息量较少,但仍可通过单击 "View Detail" link.
轻松深入了解
选项名称含糊不清。它真正 所做的是告诉Visual Studio 使用旧版本的调试引擎。任何使用 VS2013 或 VS2015 的人都会遇到新引擎的问题,可能是 VS2012。这也是之前 NLog 中没有解决这个问题的基本原因。
虽然这是一个很好的解决方法,但并不容易发现。程序员也不会特别喜欢使用旧引擎,旧引擎不支持闪亮的新功能,如 return 值调试和 64 位代码的 E+C。很难猜测这是新引擎中的一个真正的错误、疏忽还是技术限制。这太丑了所以不要犹豫把它标记为"bug",我强烈建议你把它带到connect.microsoft.com。当它得到修复时,每个人都会领先,我记得至少有一次为此挠头。使用 Debug > Windows > Exceptions > 当时打勾的 CLR Exceptions 对其进行了深入研究。
这种非常不幸的行为的解决方法肯定是丑陋的。您必须延迟引发异常,直到程序执行进展到足够远为止。我不太了解你的代码库,但延迟解析配置直到第一个日志命令应该处理它。或者存储异常对象并在第一个日志命令上抛出它,可能更容易。
简介
当用户在 NLog 配置中创建错误(如无效 XML)时,我们 (NLog) 会抛出一个 NLogConfigurationException
。异常包含错误的描述。
但有时如果对 NLog 的第一次调用来自静态 field/property.
,则此NLogConfigurationException
由 System.TypeInitializationException
"eaten"
例子
例如如果用户有这个程序:
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);
但是用户会看到:
"Copy exception details to the clipboard":
System.TypeInitializationException was unhandled Message: An unhandled exception of type 'System.TypeInitializationException' occurred in mscorlib.dll Additional information: The type initializer for 'TypeInitializationExceptionTest.Program' threw an exception.
所以消息不见了!
问题
- 为什么 innerException 不可见? (在 Visual Studio 2013 年测试)。
- 我可以向
TypeInitializationException
发送更多信息吗?喜欢留言?我们已经发送了一个 innerException。 - 我们可以使用另一个例外吗?或者
Exception
上是否有属性以便报告更多信息? - 是否有其他方式向用户提供(更多)反馈?
注释
- 当然我们对用户编写的程序没有任何影响。
- 我是 NLog 的维护者之一。
- 你喜欢自己测试吗?签出 https://github.com/NLog/NLog/tree/TypeInitializationException-tester 并开始 NLog/src/NLog.netfx45.sln
编辑:
请注意,我是图书馆的维护者,不是图书馆的用户。 我无法更改调用代码!
我现在看到的唯一解决办法是:
将静态初始化(字段)移动到具有 try catch
System.TypeInitializationException 总是或几乎总是由于未正确初始化 class 的静态成员而发生。
您必须通过调试器检查 LogManager.GetCurrentClassLogger()
。我确定错误发生在该部分代码中。
//go into LogManager.GetCurrentClassLogger() method
private static Logger logger = LogManager.GetCurrentClassLogger();
此外,我建议您仔细检查 app.config 并确保您没有包含任何错误内容。
System.TypeInitializationException
每当静态构造函数抛出异常时,或者每当您尝试访问静态构造函数抛出异常的 class 时抛出。
当 .NET
加载类型时,它必须在 您第一次 使用类型。有时,初始化需要 运行 代码。当该代码失败时,您会得到 System.TypeInitializationException
.
根据 docs
When a class initializer fails to initialize a type, a TypeInitializationException is created and passed a reference to the exception thrown by the type's class initializer. The InnerException property of TypeInitializationException holds the underlying exception. TypeInitializationException uses the HRESULT COR_E_TYPEINITIALIZATION, that has the value 0x80131534. For a list of initial property values for an instance of TypeInitializationException, see the TypeInitializationException constructors.
1) InnerException
不可见,这是此类异常的典型表现。 Visual Studio 的版本无关紧要(确保选中 "Enable the exception assistant" 选项 - Tools> Options>Debugging>General
)
2) 通常TypeInitializationException
隐藏了真正的异常,可以通过InnerException查看。但是下面的测试示例显示了如何填充内部异常信息:
public class Test {
static Test() {
throw new Exception("InnerExc of TypeInitializationExc");
}
static void Main(string[] args) {
}
}
But sometimes this NLogConfigurationException is "eaten" by an System.TypeInitializationException if the first call to NLog is from a static field/property.
没什么奇怪的。有人在某处错过了 try catch
块。
我看到的原因是入口点class的类型初始化失败。由于没有初始化任何类型,因此类型加载器没有任何关于 TypeInitializationException
中失败类型的报告。
但是如果你把logger的Static initializer改成其他的class然后在Entry方法中引用那个class。你会得到 InnerException on TypeInitialization 异常。
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。
希望您现在有了保持入口点干净的想法,Bootstrap 来自 Main() 的应用程序而不是入口点 class.[=20= 的静态 属性 ]
更新 1
您还可以利用 Lazy<>
来避免在声明时执行配置初始化。
class Program
{
private static Lazy<Logger> logger = new Lazy<Logger>(() => 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的源码,好像已经实现了,很有道理。根据 属性 "NLog should not throw exception unless specified by property LogManager.ThrowExceptions
in LogManager.cs" 的评论。
修复 - 在 LogFactory class 中,私有方法 GetLogger() 具有导致异常发生的初始化语句。如果你引入一个带有 属性 ThrowExceptions
检查的 try catch 那么你可以防止初始化异常。
if (cacheKey.ConcreteType != null)
{
try
{
newLogger.Initialize(cacheKey.Name, this.GetConfigurationForLogger(cacheKey.Name, this.Configuration), this);
}
catch (Exception ex)
{
if(ThrowExceptions && ex.MustBeRethrown())
throw;
}
}
此外,将这些 exceptions/errors 存储在某个地方会很棒,这样就可以跟踪记录器初始化失败的原因,因为它们由于 ThrowException
而被忽略。
问题是在首次引用 class 时发生静态初始化。在您的 Program
中,它甚至发生在 Main()
方法之前。因此,根据经验 - 避免任何可能在静态初始化方法中失败的代码。至于您的特定问题 - 请改用惰性方法:
private static Lazy<Logger> logger =
new Lazy<Logger>(() => LogManager.GetCurrentClassLogger());
static void Main() {
logger.Value.Log(...);
}
所以当您第一次访问记录器时,记录器的初始化将会发生(并且可能会失败)——而不是在一些疯狂的静态上下文中。
更新
坚持最佳实践最终是图书馆用户的负担。所以如果是我,我会保持原样。如果你真的必须自己解决它,那么选择很少:
1) 不要抛出异常 - 永远 - 这是日志引擎中的有效方法,以及 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
class 的实现包装懒惰的方法(我知道你的 Logger
class 要复杂得多,为了这个问题让我们假设它只有一种方法 Log
并且需要 string className
来构建 Logger
实例。
class LoggerProxy : Logger {
private Lazy<Logger> m_Logger;
// add all arguments you need to construct the logger instance
public LoggerProxy(string className) {
m_Logger = new Lazy<Logger>(() => return new Logger(className));
}
public void Log(string message) {
m_Logger.Value.Log(message);
}
}
static Logger GetCurrentClassLogger() {
var className = GetClassName();
return new LoggerProxy(className);
}
你将摆脱这个问题(真正的初始化将在调用第一个日志方法时发生,并且它是向后兼容的方法);唯一的问题是您添加了另一层(我不希望性能大幅下降,但一些日志记录引擎确实进行了微优化)。
我在之前的评论中说过我无法重现您的问题。然后我想到你 只是 在弹出的异常对话框中寻找它,它不显示 显示详细信息 link .
所以这里是您可以到达 InnerException
的方法,因为 它肯定在那里 ,除了 Visual Studio 由于某种原因没有报告它(可能是因为它是 vendettamit 发现的入口点类型)。
所以,当你 运行 tester 分支时,你会得到以下对话框:
并且它不显示 查看详细信息 link。
将异常详细信息复制到剪贴板也不是特别有用:
System.TypeInitializationException was unhandled
Message: An unhandled exception of type 'System.TypeInitializationException' occurred in mscorlib.dll
Additional information: The type initializer for 'TypeInitializationExceptionTest.Program' threw an exception.
现在,使用 OK 按钮关闭对话框并前往 Locals 调试 window。您将看到以下内容:
看到了吗? InnerException
绝对 是 那里,你发现了一个 VS 怪癖:-)
我只是指出你在这里处理的潜在问题。您正在与调试器中的错误作斗争,它有一个非常简单的解决方法。使用工具 > 选项 > 调试 > 常规 > 勾选 "Use Managed Compatibility Mode" 复选框。同时取消勾选“仅我的代码”以获得最有用的调试报告:
如果勾选“仅我的代码”,则异常报告的信息量较少,但仍可通过单击 "View Detail" link.
轻松深入了解选项名称含糊不清。它真正 所做的是告诉Visual Studio 使用旧版本的调试引擎。任何使用 VS2013 或 VS2015 的人都会遇到新引擎的问题,可能是 VS2012。这也是之前 NLog 中没有解决这个问题的基本原因。
虽然这是一个很好的解决方法,但并不容易发现。程序员也不会特别喜欢使用旧引擎,旧引擎不支持闪亮的新功能,如 return 值调试和 64 位代码的 E+C。很难猜测这是新引擎中的一个真正的错误、疏忽还是技术限制。这太丑了所以不要犹豫把它标记为"bug",我强烈建议你把它带到connect.microsoft.com。当它得到修复时,每个人都会领先,我记得至少有一次为此挠头。使用 Debug > Windows > Exceptions > 当时打勾的 CLR Exceptions 对其进行了深入研究。
这种非常不幸的行为的解决方法肯定是丑陋的。您必须延迟引发异常,直到程序执行进展到足够远为止。我不太了解你的代码库,但延迟解析配置直到第一个日志命令应该处理它。或者存储异常对象并在第一个日志命令上抛出它,可能更容易。