2014年9月28日 星期日

[iThome 第七屆鐵人賽 05] 打造第一個通用服務 - Log

到目前為止,應該對於Autofac的使用有了基本的了解。在上一篇用了一個簡單的Log服務來說明Autofac如何和Mvc結合。

Log屬於任何一個系統必須有的服務,因此在這一篇,我們打造真的Log服務。

建立的流程

基本上會和我們在上一篇打造那個服務一樣,我們會:

1VZPk6I+EP00OY4lf1Q8AuL+Lnvy8DtHaCA1gVAhKM6nn442IuPurFM7tVNzschL0+l+73WQeXHV/9C8KX+qDCRz51nPvA1z3cBf4a8FThfAXc8vQKFFdoGcEdiJFyBwCOtEBu0k0CgljWimYKrqGlIzwXIlp0c0vBjSj8Au5fIe/V9kpqQe3OWI/weiKIdjnOX6stOa05Ajg5x30jydIdyz2xUfclFX/ZzY8CnBiQCPAhpeT0p6UaqaABrakStqV1BdlGOvdAZ6AklRP9/y5iUonVYKX7RPVR+DtPIN0viZk+2xy0W6z/21s3q65Nk+Gn4lVENNxf1tSqL0wGVH7bNkwaKIRSFLViyMWBCJ2oDOeQrMSuf1nHkRS3wWbdg6tlGBi+FhZxQG2fU6ZIG/A30Q9p03irZGq+erIZC0qC15YzervrCmn+VSHdOSazNrtEqhRYqjXNWG7Oz4VDNoAzQYjzAzSoTTBaoCo0/WPOSdBYlKk+VRyuPoXHdB9ipvXDtgnIxQXDOPMuADKfGgKt4vVQm3LFxeiR9VGRiPJUeuPqJRrKpG1Zayb6qS489niy8UamjyRqi3TEKdhVqrI65SK5BILZkGiRvgGiUYsK2wh2/w/orwRVoRb5Dd3bd/IhIrUZ1Gj7w7/XhwAb/Lc/bivSAaJDfiMC3oU7klXSdDgL71WRjE6DMuatBRJ6S9lZMAUXtt4ZgESxaE39XQ7hcbmr7NHzb0lKh/Yl66JN8175nKzzcvLsfP/Hnv5u+al7wC
建立Service和Component的流程
  1. 定義Log所會擁有的Service(interface)
  2. 實作一個我們框架會用的Log Component
  3. 註冊到ContainerBuilder

定義Log擁有的Service

基本上,一般的log都有一些log層級,可以讓我們寫log的時候區分那些屬於錯誤(Error),和那些是偵錯(Debug)時候看的。因此,有以下幾個層級:

  1. Trace
  2. Debug
  3. Info
  4. Warn
  5. Error
  6. Fatal

層級定義完成之後,我們要決定每一個層級要有那些寫log的方法:

  1. (string message) - 只是把message輸出
  2. (object outpuObject) - 把物件資訊印出來
  3. (string message, params object[] args) - message是訊息,可以用string format一樣的placeholder,而args是placeholder的值
  4. (string message, object outputObject) - 要輸出的訊息和把物件資訊印出來
當然,上面這些定義是符合我自己使用,而每一個情況不一樣,因此各位應該客制屬於自己需要的方法。

最後,由於有6個log層級和每一個層級都有4個方法,總共有24個需要定義,這邊我只定義某一個層級的4個方法,剩下都會一樣:

/// <summary>
/// Log功能的interface
/// </summary>
public interface ILog
{
/// <summary>
/// Traces 訊息
/// </summary>
/// <param name="message">訊息</param>
void Trace(string message);

/// <summary>
/// Traces 把某個物件內容dump出來
/// </summary>
/// <param name="outputObject">要dump的物件</param>
void Trace(object outputObject);

/// <summary>
/// Traces 訊息加上format的參數
/// </summary>
/// <param name="message">訊息</param>
/// <param name="args">format的參數</param>
void Trace(string message, params object[] args);

/// <summary>
/// Traces 把某個物件內容dump出來,並且在dump內容加上一段訊息
/// </summary>
/// <param name="message">加上的訊息</param>
/// <param name="outputObject">要dump的物件</param>
void Trace(string message, object outputObject);

// 。。。。 其他log層級的方法定義

定義ILog的實作


有了Service定義好了之後,我們就要來決定我們要如何實作ILog。


第一件事情是決定自己要使用的Log Framework。比較出名的有NLog和Log4Net。我這邊會選擇使用NLog。



NLog



設定一些注入用的參數


有用過Log Framework就會知道,通常我們會想要知道,是哪一個Class寫出了某一筆的log,因此在建立Log的class的時候能夠傳入要用這個log的Class 名稱。


我們也希望我們的Log Framework有這個功能,因此我們需要先定義出來,好讓Autofac能夠幫忙注入Class名稱。

/// <summary>
/// 使用Nlog作為ILog的實作
/// </summary>
public class NlogLogger : ILog
{
// NLog 物件
private Logger logger;

/// <summary>
/// Initializes a new instance of the <see cref="NlogLogger"/> class.
/// </summary>
public NlogLogger()
{
logger = NLog.LogManager.GetCurrentClassLogger();
}

/// <summary>
/// Initializes a new instance of the <see cref="NlogLogger"/> class.
/// </summary>
/// <param name="name">目前要使用Log的Class名字</param>
public NlogLogger(string name)
{
logger = NLog.LogManager.GetLogger(name);
}

// ..其他實作

我們透過的方式會是用Constructor來傳遞我們class的名稱。


定義interface的實作


再來我們要實際實作interface定義的24個方法。


有兩個部分需要特別處理:



  1. 怎麼把物件資訊dump出來
  2. log framework通常都能夠判斷某個層級是否需要開放輸出


針對第一個,我將會使用Json.Net來把物件資訊dump出來。這個有好有懷,好處是不用寫複雜的邏輯來把物件印出來,而且顯示的樣式是熟悉的Json格式。而壞處是我們多了一個json .Net的 dependency。



Json .Net


屬於必裝型的套件。主要工作室Class <=> Json之間互相的轉換。效能上面比內建的快(根據他們自己的評測)



針對第二個,我們每個層級的四個方法都用統一的一個方法做輸出,這樣就會做判斷需不需要實際呼叫:

// 還是在NlogLogger.cs 裡面

/// <summary>
/// Traces 訊息
/// </summary>
/// <param name="message">訊息</param>
public void Trace(string message)
{
if (logger.IsTraceEnabled)
{
logger.Trace(message);
}
}

/// <summary>
/// Traces 訊息加上format的參數
/// </summary>
/// <param name="message">訊息</param>
/// <param name="args">format的參數</param>
public void Trace(string message, params object[] args)
{
Trace(string.Format(message, args));
}

/// <summary>
/// Traces 把某個物件內容dump出來
/// </summary>
/// <param name="outputObject">要dump的物件</param>
public void Trace(object outputObject)
{
Trace(JsonConvert.SerializeObject(outputObject, Formatting.Indented));
}

/// <summary>
/// Traces 把某個物件內容dump出來,並且在dump內容加上一段訊息
/// </summary>
/// <param name="message">加上的訊息</param>
/// <param name="outputObject">要dump的物件</param>
public void Trace(string message, object outputObject)
{
Trace(message + Environment.NewLine +
JsonConvert.SerializeObject(outputObject, Formatting.Indented));
}

// ..其他層級實作如上


上面顯示了Trace層級的方法實際定義,其他幾個層級的實作會一樣。


在ContainerBuilder註冊


在這邊有一點是之前沒有講過的,我們的NlogLogger裡面是沒有無參數的建構子。而我們接受的參數只是一個string的參數叫做name。那麼,照著我們目前所了解的註冊裡面,是沒有辦法解決這個問題。不過,Autofac當然有想到這種情況,因此我們這邊乘著這個機會介紹一下。


我們先了解一下,string name的參數是要傳入什麼?


我們之前講過,log framework都有一個參數是記錄寫這個log的class 名稱。因此我們這個參數代表就是這個要用NlogLogger的class 名稱。


那要如何注入這個class名稱呢?在Autofac裡面有Module可以讓我們設定特殊的註冊邏輯。同時,在Module裡面有提供event讓我們可以再Autofac實例化component的時候,做一些事情。因此我們可以透過這個方法來注入我們class的名稱。



建立NlogModule


建立一個Autofac的Module需要建立一個Class繼承Autofac.Module


我們這邊會複寫兩個method:



  1. Load - Module註冊第一個會執行的方法。這邊可以設定我們的NlogLogger將會作為ILog service的Component。
  2. AttachToComponentRegistration這邊就是讓我們可以註冊一些在建立時候的事件



這一段的程式碼是參考網路上面的資料,因此有些註解是英文。

// NLogModule.cs

/// <summary>
/// Dependency Injection Module 用來註冊ILog將會使用NLog
/// </summary>
public class NLogModule : Autofac.Module
{
/// <summary>
/// Override to add registrations to the container.
/// </summary>
/// <param name="builder">The builder through which components can be
/// registered.</param>
/// <remarks>
/// Note that the ContainerBuilder parameter is unique to this module.
/// </remarks>
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<NlogLogger>()
.As<ILog>();
}

/// <summary>
/// 增加透過用Constructor或者Property的方式注入
/// <see cref="MvcInfrastructure.Common.Log.NlogLogger"/>
/// 為<see cref="MvcInfrastructure.Common.Log.ILog"/>的實作
/// </summary>
/// <param name="componentRegistry">The component registry.</param>
/// <param name="registration">The registration to attach functionality to.</param>
/// <remarks>
/// This method will be called for all existing <i>and future</i> component
/// registrations - ordering is not important.
/// </remarks>
protected override void AttachToComponentRegistration(IComponentRegistry componentRegistry,
IComponentRegistration registration)
{
// Handle constructor parameters. 處理Constructor注入
registration.Preparing += OnComponentPreparing;

// Handle properties. 處理Property注入
registration.Activated += (sender, e) =>
InjectLoggerProperties(e.Instance);
}
}

從上面可以看到,在AttachToComponentRegistration我們註冊了兩個事件,一個是用來處理Constructor的時候注入參數,另外一個是用Property的方式注入。



我們實作的版本是沒有允許用Property的方式注入,不過保留這個方法僅供參考。


接下來我們看一下,Constructor注入的方法是如何寫的:

//NLogModule.cs

// ....
/// <summary>
/// Called when [component preparing]. 用來增加Constructor方式注入
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The <see cref="PreparingEventArgs"/> instance containing the event data.</param>
private static void OnComponentPreparing(object sender, PreparingEventArgs e)
{
var t = e.Component.Activator.LimitType;
e.Parameters = e.Parameters.Union(
new[]
{
new ResolvedParameter((p, i) => p.ParameterType == typeof(ILog),
(p, i) => i.Resolve<ILog>(new NamedParameter("name", t.FullName)))
});
}

// .....

最後,看一下如果用Property Inject的話是如何實現

//NLogModule.cs

// ....

/// <summary>
/// Property注入的邏輯
/// </summary>
/// <param name="instance">目前被實例化的Class Instance</param>
private static void InjectLoggerProperties(object instance)
{
var instanceType = instance.GetType();

// Get all the injectable properties to set.
// If you wanted to ensure the properties were only UNSET properties,
// here's where you'd do it.
var properties = instanceType
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.PropertyType == typeof(ILog) && p.CanWrite && p.GetIndexParameters().Length == 0);

// Set the properties located.
foreach (var propToSet in properties)
{
propToSet.SetValue(instance, new NlogLogger(instanceType.FullName), null);
}
}

// .....

到這邊,我們的Autofac.Module就建立好了。


註冊Autofac.Module


之前註冊的時候也沒有提到如何註冊Autofac.Module,這一次一起介紹。


其他註冊的部分我們就不看了,就只看註冊Module的部分:

//Global.asax

// ....

Builder.RegisterModule<NLogModule>();

// .....

這樣註冊就完成了,至於使用,就和上一篇介紹那樣,在要用到ILog的Controller裡面,把Constructor有個參數接受ILog形態的參數即可。


結語


透過這一篇,我們就知道了如何建立一個Log的功能,並且如何作為我們第一個框架的服務。


在下一篇開始,將會介紹再用Mvc開發的時候,最常用到的ViewModel概念和為什麼要使用ViewModel。


有關於程式碼的部分,稍後會補上整個專案,以供參考。


3 則留言 :

  1. 您好~
    請問這系列的文章有程式碼可以參考嗎?

    回覆刪除
  2. 您好,因為時間的關係,這個系列我一直沒有時間做一個整理
    不過,我有個同事有整理一套 - 您可以參考看看

    https://github.com/matsurigoto/mvc-base-project

    如果有任何問題,也歡迎交流

    回覆刪除
    回覆
    1. 感謝Alan的分享,有問題再跟你請教~^^

      刪除