在系統整合時,難免會涉及廠商或其他系統提供的WCF/Web Service。

要呼叫別人寫的WCF/WS,技術層面一點都不難! 在專案中Add Service Reference / Add Web Reference,Visual Studio便會打理剩下的瑣碎細節,自動幫你建立好Proxy物件,使用對方的服務就跟呼叫自己專案的元件一樣方便。

不過,老江湖都知道,呼叫別人服務的凶險所在並不在技術! 跨系統整合最怕的就是 -- 發生問題時各系統互踢皮球,堅稱問題不在自己身上,複雜的案情讓李組長不只要皺眉,就算把臉糾成包子也無解;網頁取存過程有Log可查,但呼叫其他系統的Web Service,相關Log可能遠在天邊廠商的主機上,呼叫端預設並沒有記錄。若相關系統或廠商不配合主動提供,要追查Web Service/WCF的呼叫是否出錯就變得格外困難。

要解決這樣的難題,最簡單的做法就是每次呼叫WCF/WS時,將傳入參數以及對方的回應寫入Log檔。不過,找出所有呼叫WCF/WS的程式片段,再依不同方法不同參數硬刻(Hard-Coding)出寫Log的程式碼顯然有點愚公移山。於是我有個點子,何不針對該WCF/WS建立一個LogWrapper類別,提供與各WebMethod完全相同參數與傳回型別的介面。當呼叫端呼叫時,LogWrapper先偷偷將所有傳入參數寫入Log檔,再呼叫遠端真正的WCF/WS;取得結果後,先將結果寫入Log檔,再把結果傳回呼叫端。如此,呼叫端只要將原本直接呼叫WCF/WS的程式稍做修改,改呼叫LogWrapper,我們就能保留呼叫歷程的詳細記錄,要追查問題就方便多了!

我用一個很簡單(且無聊)的WCF來示範:

public class SpiderManService : ISpiderManService
{
    public string SuperCalc(int x, int y)
    {
        return string.Format("{0} + {1} = {2}",
            x, y, x + y);
    }
}

在ASP.NET專案中Add Service Reference後,我們可使用SpiderManServiceClient Class來呼叫它:

    protected void Page_Load(object sender, EventArgs e)
    {
        SMS.SpiderManServiceClient smsc = new SMS.SpiderManServiceClient();
        Response.Write(smsc.SuperCalc(1, 1));
        Response.End();
    }

下一步,我們就來產生一個SpideManServiceClientLogWrapper,把SuperCalc方法包起來! 借助.NET神奇的Reflection功能,我寫了一個LogWrapper程式碼產生器:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Reflection;
using System.Text;
 
public class LogWrapperCodeGen
{
    public static string GenWrapperClass(Type t)
    {
        StringBuilder sb = new StringBuilder();
        sb.AppendLine(@"using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Xml;
using System.Text;
using System.IO;
");
        sb.AppendFormat("public class {0}LogWrapper", t.ToString().Split('.').Last());
        sb.AppendLine(@"
{
    class Logger
    {
        public string MethodName;
        public string LogPath;
        private StringBuilder sb = new StringBuilder();
        private void log(string msg)
        {
            sb.AppendFormat(""{0:yyyy/MM/dd HH:mm:ss.fff} {1}\r\n"", DateTime.Now, msg);
        }
        private void log(string fmt, params object[] args)
        {
            log(string.Format(fmt, args));
        }
        public Logger(string methodName)
        {
            MethodName = methodName;
            LogPath = HttpContext.Current.Server.MapPath(
                string.Format(""~/App_Data/WSLog/{0:yyyyMMdd}.txt"", DateTime.Today));
            log(new String('=', 80));
            log(""Url = "" + HttpContext.Current.Request.Url.ToString()); 
            log(""IP = "" + HttpContext.Current.Request.UserHostAddress);
            log(""Call Method[{0}]"", methodName);
        }
        public void LogParam(string paramName, object paramValue)
        {
            log(""  Parameter[{0}] => {1}"", paramName, paramValue);
        }
        public void LogResult(string result)
        {
            log(""  Result => {0}"", result);
        }
        public void Flush()
        {
            using (StreamWriter sw = new StreamWriter(LogPath, true))
            {
                sw.Write(sb.ToString());
            }
        }
    }
");
        foreach (MethodInfo mi in t.GetMethods(
            //BindingFlags.DeclaredOnly 可以排除繼承來的Method
            BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly))
        {
            sb.AppendFormat("    public static {1} {0} (", mi.Name, mi.ReturnType.ToString());
            sb.Append(
                string.Join(", ", mi.GetParameters()
                .Select(o => string.Format("{0} {1}", o.ParameterType.ToString(), o.Name))
                .ToArray()));
            sb.AppendLine(")");
            sb.AppendLine("    {");
            sb.AppendFormat("        Logger log = new Logger(\"{0}\");\r\n", mi.Name);
            foreach (ParameterInfo pi in mi.GetParameters())
                sb.AppendFormat("        log.LogParam(\"{0}\", {0});\r\n", pi.Name);
            sb.AppendFormat("        {0} objWS = new {0}();\r\n", t.ToString());
            sb.AppendFormat("        {1} result = objWS.{0}(", 
                mi.Name, mi.ReturnType.ToString());
            sb.Append(
                string.Join(", ", 
                mi.GetParameters().Select(o => string.Format("{0}", o.Name)).ToArray()));
            sb.AppendLine(");");
            sb.AppendLine("        log.LogResult(result.ToString());");
            sb.AppendLine("        log.Flush();");
            sb.AppendLine("        return result;");
            sb.AppendLine("    }");
        }
        sb.AppendLine("}");
        return sb.ToString();
    }
}

呼叫LogWrapperCodeGen.GenWrapperClass(typeof(SMS.SpiderManServiceClient)),就可以得到SpiderManServiceClientLogWrapper的程式碼如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Xml;
using System.Text;
using System.IO;
 
public class SpiderManServiceClientLogWrapper
{
    class Logger
    {
        public string MethodName;
        public string LogPath;
        private StringBuilder sb = new StringBuilder();
        private void log(string msg)
        {
            sb.AppendFormat("{0:yyyy/MM/dd HH:mm:ss.fff} {1}\r\n", DateTime.Now, msg);
        }
        private void log(string fmt, params object[] args)
        {
            log(string.Format(fmt, args));
        }
        public Logger(string methodName)
        {
            MethodName = methodName;
            LogPath = HttpContext.Current.Server.MapPath(
                string.Format("~/App_Data/WSLog/{0:yyyyMMdd}.txt", DateTime.Today));
            log(new String('=', 80));
            log("Url = " + HttpContext.Current.Request.Url.ToString()); 
            log("IP = " + HttpContext.Current.Request.UserHostAddress);
            log("Call Method[{0}]", methodName);
        }
        public void LogParam(string paramName, object paramValue)
        {
            log("  Parameter[{0}] => {1}", paramName, paramValue);
        }
        public void LogResult(string result)
        {
            log("  Result => {0}", result);
        }
        public void Flush()
        {
            using (StreamWriter sw = new StreamWriter(LogPath, true))
            {
                sw.Write(sb.ToString());
            }
        }
    }
 
    public static System.String SuperCalc (System.Int32 x, System.Int32 y)
    {
        Logger log = new Logger("SuperCalc");
        log.LogParam("x", x);
        log.LogParam("y", y);
        SMS.SpiderManServiceClient objWS = new SMS.SpiderManServiceClient();
        System.String result = objWS.SuperCalc(x, y);
        log.LogResult(result.ToString());
        log.Flush();
        return result;
    }
}
 

將以上Class放進App_Code,再修改呼叫程式,改成SpiderManServiceClientLogWrapper.SuperCalc(1, 1):

    protected void Page_Load(object sender, EventArgs e)
    {
        //SMS.SpiderManServiceClient smsc = new SMS.SpiderManServiceClient();
        //Response.Write(smsc.SuperCalc(1, 1));
        Response.Write(SpiderManServiceClientLogWrapper.SuperCalc(1, 1));
        Response.End();
    }

就可以在App_Data/WSLog/yyyyMMdd.txt中找到詳細的呼叫記錄囉!

2010/10/21 05:38:16.852 ================================================================================
2010/10/21 05:38:16.852 Url = httq://localhost/ASPNET35/Default.aspx
2010/10/21 05:38:16.852 IP = ::1
2010/10/21 05:38:16.853 Call Method[SuperCalc]
2010/10/21 05:38:16.853   Parameter[x] => 1
2010/10/21 05:38:16.853   Parameter[y] => 1
2010/10/21 05:38:16.875   Result => 1 + 1 = 2

有了詳細呼叫Log,不管是要偵錯抓蟲射茶包,還是準備起訴抗辯上公堂,都能無往不利! (提到這點,相信老鳥們應該會發出會心一笑吧~)


Comments

# by Lan

我遇過某鳥廠幹的好事是這樣 : 寫進Log的是A,傳出去的變成A'

# by chicken

這種苦差事可以用 Policy Injection 方式,解的比較漂亮 :D 尤其像 WCF 這種已經是 remoting 型態的機制了,應該有更簡單的地方可以插入 Logging code.. http://columns.chicken-house.net/post/2008/11/18/Policy-Injection-Application-Block-e5b08fe799bce78fbe.aspx

Post a comment