一、需求
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 { get; set; }
public string User { get; set; }
}
如下调用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:实现一个ConverterLog4net内置提供的每个模板参数,都有对应的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<object, string, object> 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<object, string, object> func, object obj, string name)
{
object val = null;
if (func != null)
{
foreach (Func<object, string, object> 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