记录到日志中的异常栈缺失
.NET项目中有这样一个场景:
使用反射处理所有业务调用,在反射调用点使用try-catch集中处理异常,并将异常信息记录到日志。其中日志记录是异步的。
问题:
记录到日志中的异常的StackTrace和有时候和Debug时抛出的异常的StackTrace不一样。
原因:
由于记录日志是异步的,如果记录日志发生在throw之前,记录到日志中的异常的StackTrace就是正确的(异常真正发生点到throw点的所有StackTrace);如果记录日志发生在throw之后,记录到日志中的异常的StackTrace就是不正确的(只有当前throw点到代码入口点的StackTrace);
因为抛出的异常(或是该异常的引用)刚好是用来记录日志用的异常实例,且throw动作会对抛出的异常(和记录日志的异常实例是同一个引用)重新生成StackTrace,所以记录日志用的异常的StackTrace被重新生成了。
(备注1:StackTrace是Exception的只读属性,throw时会对扔出的异常生成StackTrace)
(备注2:如果记录日志是同步的且先记录日志后throw,则应该不会发生这个问题)
解决方案:
- 如果只需要记录最内部的异常(异常真正发生点),可在catch中先将内部异常剥出来,将该异常记录日志,然后将该异常复制一个副本,throw异常的副本;
- 如果要记录完整的调用栈(包含反射异常等)且抛出最内部异常,可以先记录该异常(包含完整调用栈),然后将最内部异常剥出来,复制该异常的一个副本,throw异常的副本;
- 如果要记录完整的调用栈且抛出原始异常,可以先记录该异常(包含完整调用栈),然后递归复制异常链上的所有异常的副本并按原顺序合成新的异常链(原始异常链的深复制副本),throw异常链的副本。但是该做法会丢失异常链中间节点的所有StackTrace;
♦用到的方法:
1 // 深赋值异常链上的所有异常,但是会丢失异常链上非叶子节点的异常的StackTrace。最内部的异常信息会完整保留,因为最内部的异常不是副本 2 public static Exception CloneInnerExceptionRecursive(Exception ex) 3 { 4 Exception exception = null; 5 6 if (ex.InnerException == null) 7 { 8 exception = ex; 9 } 10 else 11 { 12 Stack<Exception> stack = new Stack<Exception>(); 13 stack.Push(ex); 14 Exception innerException = ex.InnerException; 15 while (innerException != null) 16 { 17 if (innerException.InnerException == null) 18 { 19 break; 20 } 21 stack.Push(innerException); 22 innerException = innerException.InnerException; 23 } 24 25 try 26 { 27 Exception chain = innerException; 28 Exception temp = null; 29 while (stack.Count > 0) 30 { 31 temp = stack.Pop(); 32 chain = CloneException(temp, chain); 33 } 34 exception = chain; 35 } 36 catch 37 { 38 } 39 40 if (exception == null || exception.InnerException == null) 41 { 42 exception = ex.InnerException; 43 } 44 } 45 46 return exception; 47 }
1 // 复制异常 2 public static Exception CloneException(Exception ex, Exception inner) 3 { 4 return (Exception)ex.GetType().GetConstructor(new Type[] { typeof(string), typeof(Exception) }).Invoke(new object[] { ex.Message, inner }); 5 }