1. 程式人生 > >直接引用MrAdvice.dll檔案不能實現AOP攔截,教你1分鐘解決這個問題

直接引用MrAdvice.dll檔案不能實現AOP攔截,教你1分鐘解決這個問題

直接引用MrAdvice.dll檔案不能實現AOP攔截,教你1分鐘解決這個問題。近日工作中,要實現一個功能,那就是業務層方法裡面實現自動快取。編寫業務的C#開發人員只關注如何將業務程式碼編寫正確就可以了,而快取的程式碼,大多類似,無非就是判斷是否有快取,有就取出返回,沒有就呼叫資料庫程式碼獲取資料再快取起來而已,於是這部分程式碼通過使用AOP的方式自動接管掉這種重複性程式碼。

MrAdvice開源專案github地址:https://github.com/ArxOne/MrAdvice

直接引用MrAdvice.dll檔案不能實現AOP攔截功能

1月份的時候寫過一篇使用AOP元件重構老舊 ado.net 程式碼,統一管理多表操作的事務的文章,在測試程式中使用的是MrAdvice這個開源元件,對它熟悉,就又使用它了。只不過這次使用有點特殊,以前開發是可以聯網的,可以很方便的使用nuget將其安裝到本地,而這次是因專案原因內外網隔離,且是斷網開發的,就只能在外網寫個測試程式,然後將MrAdvice.dll檔案複製到內網電腦,內網電腦通過引用dll的方式來使用該元件,結果是不會進入到攔截方法的。

直接引用MrAdvice.dll  

通過下圖可以看到,成功解決後,可以實現自動快取了。

實現AOP攔截 

下面是全部的演示程式原始碼。

演示程式解決方案目錄一覽

該專案是一個控制檯專案,解決方案如下圖所示:

演示程式解決.

MrAdvice.dll是直接引用的,不是通過nuget安裝的,至於這個dll檔案的獲取,你可以通過nuget獲取了找到它即可。

演示程式的原始碼

控制檯入口的程式碼比較簡單,單純的呼叫介面。

程式入口程式碼

class Program
    {
        static void Main(string[] args)
        {
            Console.Title = "jhrs.com AOP演示程式,通過直接引用MrAdvice.dll編寫的程式碼!";
            DateTime dtNow = DateTime.Now;
            IJhrscom api = new Jhrscom();
            var result = api.GetResult("這是a引數", dtNow, 12342);
            Console.WriteLine();
            Console.WriteLine($"第1次呼叫時返回結果是:"+result.ToJson());
            Console.WriteLine();
            result = api.GetResult("這是a引數", dtNow, 12342);
            Console.WriteLine();
            Console.WriteLine($"第2次呼叫時返回結果是來自第1次快取資料,只不過被改了下:" + result.ToJson());
            Console.WriteLine();
            //api.GetPatient(Guid.NewGuid(), result);
        }
    }

  

程式介面程式碼

程式介面程式碼主要是模擬業務方法裡面的一些類,定義了一個介面,一個實現類,另外實現類上面是標註了一個自動快取的特性(AutoCache),該特性的實現程式碼即為下面所述的核心的AOP攔截程式碼,具體下面會給出的;另外還有一個輸出結果(響應訊息)的類。整個原始碼是放到一個檔案裡面的,如下所示:

public interface IJhrscom
    {
        ResponseResult GetResult(string a, DateTime dateTime, int id);

        ResponseResult GetPatient(Guid id, ResponseResult t);
    }

    public class Jhrscom : IJhrscom
    {
        [AutoCache(10)]
        public ResponseResult GetPatient(Guid id, ResponseResult t)
        {
            string key = GetKey(new object[] { id, t });
            ResponseResult result = new ResponseResult() { Code = 4444, Message = "第2個方法" };
            return result;
        }

        [AutoCache(cacheMinutes: 12, enableSliding: true)]
        public ResponseResult GetResult(string a, DateTime dateTime, int id)
        {
            ResponseResult result = new ResponseResult() { Code = 1122, Message = "快取測試訊息" };
            string key = GetKey(new object[] { a, dateTime, id });
            return result;
        }

        /// <summary>
        /// 快取key
        /// </summary>
        /// <param name="pars"></param>
        /// <returns></returns>
        private string GetKey(params object[] pars)
        {
            var method = new StackFrame(1).GetMethod();
            var array = method.GetParameters();
            var key = array.Select(x => { return pars[x.Position].ToJson(); }).ToArray();

            var cacheKey = $"{method.DeclaringType.ToString()}|{method.Name.Replace("′", "")}|{string.Join("_", array.Select(x => x.Name))}|{string.Join("_", key)}".GetMd5();
            Console.WriteLine($"【{method.Name.Replace("′", "")}】實現類裡面的快取Key:" + cacheKey);
            return cacheKey;
        }
    }

    /// <summary>
    /// 輸出結果
    /// </summary>
    public class ResponseResult
    {
        public int Code { get; set; }
        public string Message { get; set; }

        //.....其它屬性
    }

  

核心的AOP攔截程式碼

該程式碼是用於實現自動快取功能,思路就是在呼叫業務方法前,根據快取key,快取key按一定規則生成,保證唯一就可以了,具體原始碼中有說明,從快取裡面取出資料,如果存在快取就直接返回給呼叫者即可,並終止業務方法的執行(體現在不呼叫context.Proceed()方法上);如果不存在快取資料或者快取過期了,則呼叫業務方法獲取資料後並快取就可以了。

/// <summary>
    /// 用AOP來實現自動快取
    /// </summary>
    public class AutoCacheAttribute : Attribute, IMethodAdvice
    {
        /// <summary>
        /// 滑動過期
        /// </summary>
        public bool EnableSliding { get; set; }

        /// <summary>
        /// 快取時間,分鐘
        /// </summary>
        public int CacheMinutes { get; set; }

        /// <summary>
        /// 建構函式
        /// </summary>
        /// <param name="cacheMinutes">快取時間,分鐘,預設5分鐘,小於等於0永久快取</param>
        /// <param name="enableSliding">使用滑動過期快取控制策略</param>
        public AutoCacheAttribute(int cacheMinutes = 5, bool enableSliding = false)
        {
            EnableSliding = enableSliding;
            CacheMinutes = cacheMinutes;
        }

        /// <summary>
        /// AOP元件攔截方法,用於實現自動快取,有快取時直接返回;
        /// 沒有快取時,呼叫被攔截方法後,有返回值則將資料自動快取起來
        /// </summary>
        /// <param name="context"></param>
        public void Advise(MethodAdviceContext context)
        {
            var key = GetKey(context);
            if (context.HasReturnValue && key.TryGetCache(out object m))
            {
                var r = m as ResponseResult;
                r.Message = "在攔截方法裡面改了快取裡面取出來的資料!";

                context.ReturnValue = r;
                //context.ReturnValue = m;  

                //context.Proceed();  //直接取出快取返回,不用執行原來取資料方法。
            }
            else
            {
                context.Proceed();//執行被攔截的方法
                if (context.HasReturnValue && context.ReturnValue != null)
                {
                    //被攔截方法有返回值,並且返回值不為null
                    if (EnableSliding && CacheMinutes > 0)
                        context.ReturnValue.SetCache(key, TimeSpan.FromMinutes(CacheMinutes));
                    else if (CacheMinutes > 0)
                        context.ReturnValue.SetCache(key, DateTime.Now.AddMinutes(CacheMinutes));
                    else
                        context.ReturnValue.SetCache(key);
                }
            }
        }

        /// <summary>
        /// 獲取快取key,key的規則為: md5(類全名|方法名|引數列表拆分陣列|引數值的json陣列),這樣可以保證唯一
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        private string GetKey(MethodAdviceContext context)
        {
            var array = context.TargetMethod.GetParameters();
            var key = array.Select(x => { return context.Arguments[x.Position].ToJson(); }).ToArray();

            var cacheKey = $"{context.Target.ToString()}|{context.TargetName}|{string.Join("_", array.Select(x => x.Name))}|{string.Join("_", key)}".GetMd5();
            return cacheKey;
        }
    }

    /// <summary>
    /// 快取擴充套件方法,可使用其它快取替代
    /// </summary>
    public static class CacheExtensions
    {
        private static MemoryCache cache = new MemoryCache("https://jhrs.com");

        /// <summary>
        /// 設定快取,一直不過期
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="value"></param>
        /// <param name="key"></param>
        public static void SetCache<T>(this T value, string key)
        {
            if (string.IsNullOrWhiteSpace(key)) throw new ArgumentException($"快取鍵引數{nameof(key)}不能為null或空");
            if (value == null) throw new ArgumentException($"快取值引數{nameof(value)}不能為null");
            CacheItemPolicy policy = new CacheItemPolicy();
            cache.Set(key, value, policy);
        }

        /// <summary>
        /// 設定快取,固定過期時間
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="value"></param>
        /// <param name="key"></param>
        /// <param name="absoluteExpiration"></param>
        public static void SetCache<T>(this T value, string key, DateTimeOffset? absoluteExpiration)
        {
            if (string.IsNullOrWhiteSpace(key)) throw new ArgumentException($"快取鍵引數{nameof(key)}不能為null或空");
            if (value == null) throw new ArgumentException($"快取值引數{nameof(value)}不能為null");
            CacheItemPolicy policy = new CacheItemPolicy() { AbsoluteExpiration = (DateTimeOffset)absoluteExpiration };
            cache.Set(key, value, policy);
        }

        /// <summary>
        /// 設定快取,滑動過期
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="value"></param>
        /// <param name="key"></param>
        /// <param name="slidingExpiration"></param>
        public static void SetCache<T>(this T value, string key, TimeSpan? slidingExpiration)
        {
            if (string.IsNullOrWhiteSpace(key)) throw new ArgumentException($"快取鍵引數{nameof(key)}不能為null或空");
            if (value == null) throw new ArgumentException($"快取值引數{nameof(value)}不能為null");
            CacheItemPolicy policy = new CacheItemPolicy() { SlidingExpiration = (TimeSpan)slidingExpiration };
            cache.Set(key, value, policy);
        }

        /// <summary>
        /// 獲取快取資料
        /// </summary>
        /// <typeparam name="T">物件型別</typeparam>
        /// <param name="key"><快取key/param>
        /// <param name="value">返回的快取資料對名</param>
        /// <returns></returns>
        public static bool TryGetCache<T>(this string key, out T value)
        {
            value = default(T);
            if (cache.Contains(key))
            {
                value = (T)cache.Get(key);
                return true;
            }
            return false;
        }

        /// <summary>
        /// 獲取字串MD5值
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static string GetMd5(this string value)
        {
            byte[] bytes = Encoding.UTF8.GetBytes(value);

            StringBuilder sb = new StringBuilder();
            MD5 hash = new MD5CryptoServiceProvider();
            bytes = hash.ComputeHash(bytes);
            foreach (byte b in bytes)
            {
                sb.AppendFormat("{0:x2}", b);
            }
            return sb.ToString();
        }
    }

  

附加的JSON擴充套件類

該擴充套件類只是方便將物件轉為JSON而已,程式碼不復如,如下所示:

 public static class JsonExtensions
    {
        /// <summary>
        /// 將物件轉換為JSON字串
        /// </summary>
        /// <param name="obj">要轉換的物件</param>
        /// <param name="camelCase">是否小寫名稱</param>
        /// <param name="indented"></param>
        /// <returns></returns>
        public static string ToJson(this object obj, bool camelCase = false, bool indented = false)
        {
            JsonSerializerSettings settings = new JsonSerializerSettings();
            if (camelCase)
            {
                settings.ContractResolver = new CamelCasePropertyNamesContractResolver();
            }
            if (indented)
            {
                settings.Formatting = Formatting.Indented;
            }
            return JsonConvert.SerializeObject(obj, settings);
        }

        /// <summary>
        /// 把Json字串轉換為強型別物件
        /// </summary>
        public static T FromJson<T>(string json)
        {
            if (string.IsNullOrWhiteSpace(json)) return default(T);
            json = JsonDateTimeFormat(json);
            return JsonConvert.DeserializeObject<T>(json);
        }

        /// <summary>
        /// 處理Json的時間格式為正常格式
        /// </summary>
        private static string JsonDateTimeFormat(string json)
        {
            json = Regex.Replace(json,
                @"\\/Date\((\d+)\)\\/",
                match =>
                {
                    DateTime dt = new DateTime(1970, 1, 1);
                    dt = dt.AddMilliseconds(long.Parse(match.Groups[1].Value));
                    dt = dt.ToLocalTime();
                    return dt.ToString("yyyy-MM-dd HH:mm:ss.fff");
                });
            return json;
        }
    }

  

解決直接引用MrAdvice.dll不能攔截的問題

出現這個問題的根源是,MrAdvice這個元件是在編譯時會給你的專案原始碼編織一些AOP攔截程式碼,熟悉PostSharp的應該對此瞭解,這也是在MrAdvice專案地址的issues處得到解答,地址是:https://github.com/ArxOne/MrAdvice/issues/140

所以我們需要在專案檔案csproj裡面新增一些配置,並且把MrAdvice的目錄複製到斷網開發專案的packages目錄。通過完成這兩個步驟就可以解決了。

You’ve missed the point: Mr Advice is a post-build weaver, which changes the assembly at build-time after the csc compiler has generated it. To achieve this, is inserts a task in the csproj. So if you want to do the same manually, you need to also add the build task in your csproj. If you have a VS2017 solution with a project working, you’ll only need to copy the lines that were added to the csproj into your own project.

解決步驟

  • 聯網新建一個專案,通過nuget安裝MrAdvice,然後在解決方案的packages目錄裡面將nuget下載的MrAdvice目錄包,複製到你斷網環境的解決方案的packages目錄,如下圖所示:
MrAdvice 目錄
  • 修改專案檔案,即修改csproj檔案,csproj檔案可以使用記事本或者其它軟體開啟,增加以下節點,如下圖所示:
csproj檔案

配置節點為如下:

<Import Project="..\packages\MrAdvice.2.8.8\build\MrAdvice.targets" Condition="Exists('..\packages\MrAdvice.2.8.8\build\MrAdvice.targets')" />
  <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
    <PropertyGroup>
      <ErrorText>這臺計算機上缺少此專案引用的 NuGet 程式包。使用“NuGet 程式包還原”可下載這些程式包。有關更多資訊,請參見 http://go.microsoft.com/fwlink/?LinkID=322105。缺少的檔案是 {0}。</ErrorText>
    </PropertyGroup>
    <Error Condition="!Exists('..\packages\MrAdvice.2.8.8\build\MrAdvice.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\MrAdvice.2.8.8\build\MrAdvice.targets'))" />
  </Target>

  

好了,通過以上步驟就可以在斷網環境裡面愉快的使用MrAdvice這個AOP攔截元件來省點體力勞動了。

原始碼可以在首發地址下載,本文首發於:

https://jhrs.com/2019/33367.