KiMoGiGi 技术文集

不在乎选择什么,而在乎坚持多久……

IT博客 首页 联系 聚合 管理
  185 Posts :: 14 Stories :: 48 Comments :: 0 Trackbacks
一、需求

PatternLayout的配置格式化如下所示:
<layout type="log4net.Layout.PatternLayout">
    
<conversionPattern value="[%date{yyyy-MM-dd HH:mm:ss}] [%level] %message %exception %newline" />
</layout>

由PatternLayout的conversionPattern来设置一个“模板”信息。其变量都有“%”开头的单词标识。如%level、%message;变量也可以传入参数,如%date{ yyyy-MM-dd HH:mm:ss },其中yyyy-MM-dd HH:mm:ss就是%date的参数。

PatternLayout默认所可以支持的单词标识,请参考:
http://www.cnitblog.com/seeyeah/archive/2008/10/15/50291.html

现在我们需要扩展这些单词标记。
假设模板字符串定义如下所示:
 [%date{yyyy-MM-dd HH:mm:ss}] %o{Message},%o{User} %newline"
注意%o{Message},%o是我们要实现扩展的一个标记,标识传入的message的对象,后面的参数{Message}、{User}表示message的对象的2个属性名。

我们先设计一个传递日志消息的实体类,定义如下所示
class SampleMessage
{
    
public string Message { getset; }

    
public string User { getset; }
}


如下调用ILog的Info方法。

log.Info(new SampleMessage() { Message = "Test1", User = "User1" });
log.Info(
new SampleMessage() { Message = "Test2", User = "User2" });


我们想要的结果如下所示:
[2009-09-19 14:27:29] Test1,User1
[
2009-09-19 14:27:29] Test2,User2 


二、方案

Log4net用appender来记录日志的加载方式,内置就有很多种appender:RollingFileAppender、ConsoleAppender、AdoNetAppender等等。其中,日志信息的格式由appender中的layout掌控,log4net内置的layout是log4net.Layout.PatternLayout。

Appender(具体以RollingFileAppender为例,其他类型Appender类似)与layout的关系如下所示:
 


RollingFileAppender的基类FileAppender以及TextWriterAppender是文件日志类的公用基类。AppenderSkeleton是所有log4net的appender的基类,内部封装了常用方法,如线程锁定、日志等级过滤和支持一般的文件写入等。

另外一边的PatternLayout,结构跟Appender相似,IAppender包含一个ILayout负责格式化日志的格式。因此如果我们要扩展日志的格式化,就需要扩展PatternLayout。

三、实现

下面说明,扩展PatternLayout的实现过程。

Step1:实现一个Converter

Log4net内置提供的每个模板参数,都有对应的Converter做处理。如%message对应MessagePatternConverter;%date对应DatePatternConverter等。这个对应关系可以查看log4net的源代码PatternLayout.cs的静态构造函数,内部用一个静态的Hashtable管理关键字与Converter的关系:

        /// <summary>
        
/// Initialize the global registry
        
/// </summary>
        
/// <remarks>
        
/// <para>
        
/// Defines the builtin global rules.
        
/// </para>
        
/// </remarks>
        static PatternLayout()
        {
            s_globalRulesRegistry 
= new Hashtable(45);

            s_globalRulesRegistry.Add(
"literal"typeof(log4net.Util.PatternStringConverters.LiteralPatternConverter));
            s_globalRulesRegistry.Add(
"newline"typeof(log4net.Util.PatternStringConverters.NewLinePatternConverter));
            s_globalRulesRegistry.Add(
"n"typeof(log4net.Util.PatternStringConverters.NewLinePatternConverter));

            s_globalRulesRegistry.Add(
"c"typeof(LoggerPatternConverter));
            s_globalRulesRegistry.Add(
"logger"typeof(LoggerPatternConverter));

            s_globalRulesRegistry.Add(
"C"typeof(TypeNamePatternConverter));
            s_globalRulesRegistry.Add(
"class"typeof(TypeNamePatternConverter));
            s_globalRulesRegistry.Add(
"type"typeof(TypeNamePatternConverter));

再看看我们将要实现的模板字符串:
[%date{yyyy-MM-dd HH:mm:ss}] %o{Message},%o{User} %newline"
我们要实现的关键字是“o”,按照log4net的PatternLayout的设计,我们也要相应实现一个Converter去解析关键字是“o”的内容,我们定义这个类名为ObjectConverter。

下面是ObjectConverter的实现方式,实现较为简单,详细看注释:

    /// <summary>
    
/// 根据键值获取值的对象
    
/// </summary>
    public interface IGetObjectValueByKey
    {
        
string GetByKey(string name);
    }
    
    
/// <summary>
    
/// 对应%o的对象转换器
    
/// </summary>
    
/// <remarks>用于PatternLayout</remarks>
    public class ObjectConverter : PatternLayoutConverter
    {
        
static Func<objectstringobject> funcs;
        
static ObjectConverter()
        {
            
//********根据键值获取值的顺序
            
//从接口获取值
            funcs += GetValueByInterface;
            
//反射获取属性值
            funcs += GetValueByReflection;
            
//从索引值获取值
            funcs += GetValueByIndexer;
        }

        
/// <summary>
        
/// 实现PatternLayoutConverter.Convert抽象方法
        
/// </summary>
        
/// <param name="writer"></param>
        
/// <param name="loggingEvent"></param>
        protected override void Convert(TextWriter writer, LoggingEvent loggingEvent)
        {
            
//获取传入的消息对象
            object objMsg = loggingEvent.MessageObject;

            
if (objMsg == null)
            {
                
//如果对象为空输出log4net默认的null字符串
                writer.Write(SystemInfo.NullText);
                
return;
            }
            
if(string.IsNullOrEmpty(this.Option))
            {
                
//如果属性为空,输出消息对象的ToString()
                writer.Write(objMsg.ToString());
                
return;
            }

            
object val = GetValue(funcs, objMsg, Option);
            writer.Write(val 
== null ? "" : val.ToString());
        }

        
#region 静态方法
        
/// <summary>
        
/// 循环方法列表,根据键值获取值
        
/// </summary>
        
/// <param name="func">方法列表委托</param>
        
/// <param name="obj">对象</param>
        
/// <param name="name">键值</param>
        
/// <returns></returns>
        private static object GetValue(Func<objectstringobject> func, object obj, string name)
        {
            
object val = null;
            
if (func != null)
            {
                
foreach (Func<objectstringobject> del in func.GetInvocationList())
                {
                    val 
= del(obj, name);
                    
//如果获取的值不为null,则跳出循环
                    if (val != null)
                    {
                        
break;
                    }
                }
            }
            
return val;
        }

        
/// <summary>
        
/// 使用接口方式取值
        
/// </summary>
        
/// <param name="obj"></param>
        
/// <param name="name"></param>
        
/// <returns></returns>
        
/// <remarks>效率最高,避免了反射带来的效能损耗</remarks>
        private static object GetValueByInterface(object obj, string name)
        {
            
object val = null;
            IGetObjectValueByKey objConverter 
= obj as IGetObjectValueByKey;
            
if (objConverter != null)
            {
                val 
= objConverter.GetByKey(name);
            }
            
return val;
        }

        
/// <summary>
        
/// 反射对象的获取属性,获取属性值
        
/// </summary>
        
/// <param name="obj"></param>
        
/// <param name="name"></param>
        
/// <returns></returns>
        private static object GetValueByReflection(object obj, string name)
        {
            
object val = null;
            Type t 
= obj.GetType();
            var propertyInfo 
= t.GetProperty(name);
            
if (propertyInfo != null)
            {
                val 
= propertyInfo.GetValue(obj, null);
            }

            
return val;
        }

        
/// <summary>
        
/// 反射对象的索引器,获取值
        
/// </summary>
        
/// <param name="obj"></param>
        
/// <param name="name"></param>
        
/// <returns></returns>
        private static object GetValueByIndexer(object obj, string name)
        {
            
object val = null;

            MethodInfo getValueMethod 
= obj.GetType().GetMethod("get_Item");
            
if (getValueMethod != null)
            {
                val 
= getValueMethod.Invoke(obj, new object[] { name });
            }

            
return val;
        }
        
#endregion
     }



主要是ObjectConvertert按顺序用了3种从键值获取值的方式
1、    对象实现了我们所定义的接口IGetObjectValueByKey,直接调用方法获取。此方法效率最好,因为内部避免了反射所带来的损耗。
2、    用反射获取属性值
3、    用反射获取索引值


Step2:把Converter注册到PatternLayout


现在我们需要把已实现的ObjectConverter加入PatternLayout的解析逻辑中。
我们先从ILog中找到藏在里面的PatternLayout实例,实现如下代码所示:

var log = Log4NetCommon.GetLog("LogConfig1st");
var appender 
= log.Logger.Repository.GetAppenders()[0];
var layout 
= ((appender as AppenderSkeleton).Layout as PatternLayout);

Log4NetCommon是我们自己用于初始化Log4Net的工具类。log是我们一般主打使用日志的ILog实例,第二行我们找到对应的Appender,最后通过转换类型找到了PatternLayout。

PatternLayout提供了AddConverter方法,可以轻松加入我们刚实现Converter。
AddConverter有2个签名版本:
public void AddConverter(ConverterInfo converterInfo)
public void AddConverter(string name, Type type)

根据源代码对AddConverter的解析
Programmatic users should use the alternative <see cref="AddConverter(string,Type)"/> method.
我们还是按要求调用AddConverter(string name, Type type)的版本。
layout.AddConverter("o"typeof(ObjectConverter));


但目前还是不能解析模板中关键字“o”,为什么呢?

首先我们先简单了解一下PatternLayout如何、在什么时候注册关键字与Converter。

1、    在PatternLayout中,定义了一个s_globalRulesRegistry的静态Hashtable,Key为关键字,Value为对应的Converter类型。在PatternLayout的静态构造函数中,先会注册log4net内置的45个关键字。
2、    初始化log4net配置的时候,会调用PatternLayout的ActivateOptions初始化以上的配置,以及在ActivateOptions中会调用CreatePatternParser解析配置中的模板字符串。

在初始化log4net配置的时候,都还没来得及AddConverter,log4net就已经解析完毕了。因此在调用了AddConverter,即修改了所有有关PatternLayout配置时,必须再手动调用一次ActivateOptions重新解析一次模板字符串。

全部代码实现如下所示:

var log = Log4NetCommon.GetLog("LogConfig1st");

var appender 
= log.Logger.Repository.GetAppenders()[0];
var layout 
= ((appender as AppenderSkeleton).Layout as PatternLayout);
layout.AddConverter(
"o"typeof(ObjectConverter));
layout.ActivateOptions();

实现完毕!


三、全部代码下载

Log4NetPatternLayoutExtension.zip

posted on 2009-09-20 00:53 KiMoGiGi 阅读(2730) 评论(0)  编辑 收藏 引用 所属分类: C# / Winforms
只有注册用户登录后才能发表评论。