直接引用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是直接引用的,不是通過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目錄,如下圖所示:
- 修改項目文件,即修改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攔截組件來省點體力勞動了。
源碼可以在首發地址下載,本文首發於: