美文网首页Java架构技术进阶
一分钟教你引用MrAdvice.dll文件实现AOP拦截

一分钟教你引用MrAdvice.dll文件实现AOP拦截

作者: can_4999 | 来源:发表于2019-11-19 20:27 被阅读0次

    近日工作中,要实现一个功能,那就是业务层方法里面实现自动缓存。编写业务的C#开发人员只关注如何将业务代码编写正确就可以了,而缓存的代码,大多类似,无非就是判断是否有缓存,有就取出返回,没有就调用数据库代码获取数据再缓存起来而已,于是这部分代码通过使用AOP的方式自动接管掉这种重复性代码。

    MrAdvice开源项目github地址:https://github.com/ArxOne/MrAdvice

    直接引用MrAdvice.dll文件不能实现AOP拦截功能

    因项目原因内外网隔离,且是断网开发的,就只能在外网写个测试程序,然后将MrAdvice.dll文件复制到内网电脑,内网电脑通过引用dll的方式来使用该组件,结果是不会进入到拦截方法的。

    通过下图可以看到,成功解决后,可以实现自动缓存了。

    下面是全部的演示程序源码。

    演示程序解决方案目录一览

    该项目是一个控制台项目,解决方案如下图所示:

    MrAdvice.dll是直接引用的,不是通过nuget安装的,至于这个dll文件的获取,你可以通过nuget获取了找到它即可。

    演示程序的源码

    控制台入口的代码比较简单,单纯的调用接口。

    程序入口代码

    程序接口代码

    程序接口代码主要是模拟业务方法里面的一些类,定义了一个接口,一个实现类,另外实现类上面是标注了一个自动缓存的特性(AutoCache),该特性的实现代码即为下面所述的核心的AOP拦截代码,具体下面会给出的;另外还有一个输出结果(响应消息)的类。整个源码是放到一个文件里面的,如下所示:

    1. public interface IJhrscom

    2. {

    3. ResponseResult GetResult(string a, DateTime dateTime, int id);

    4. ResponseResult GetPatient(Guid id, ResponseResult t);

    5. }

    6. public class Jhrscom : IJhrscom

    7. {

    8. [AutoCache(10)]

    9. public ResponseResult GetPatient(Guid id, ResponseResult t)

    10. {

    11. string key = GetKey(new object[] { id, t });

    12. ResponseResult result = new ResponseResult() { Code = 4444, Message = "第2个方法" };

    13. return result;

    14. }

    15. [AutoCache(cacheMinutes: 12, enableSliding: true)]

    16. public ResponseResult GetResult(string a, DateTime dateTime, int id)

    17. {

    18. ResponseResult result = new ResponseResult() { Code = 1122, Message = "缓存测试消息" };

    19. string key = GetKey(new object[] { a, dateTime, id });

    20. return result;

    21. }

    22. /// <summary>

    23. /// 缓存key

    24. /// </summary>

    25. /// <param name="pars"></param>

    26. /// <returns></returns>

    27. private string GetKey(params object[] pars)

    28. {

    29. var method = new StackFrame(1).GetMethod();

    30. var array = method.GetParameters();

    31. var key = array.Select(x => { return pars[x.Position].ToJson(); }).ToArray();

    32. var cacheKey = $"{method.DeclaringType.ToString()}|{method.Name.Replace("′", "")}|{string.Join("", array.Select(x => x.Name))}|{string.Join("", key)}".GetMd5();

    33. Console.WriteLine($"【{method.Name.Replace("′", "")}】实现类里面的缓存Key:" + cacheKey);

    34. return cacheKey;

    35. }

    36. }

    37. /// <summary>

    38. /// 输出结果

    39. /// </summary>

    40. public class ResponseResult

    41. {

    42. public int Code { get; set; }

    43. public string Message { get; set; }

    44. //.....其它属性

    45. }

    核心的AOP拦截代码

    该代码是用于实现自动缓存功能,思路就是在调用业务方法前,根据缓存key,缓存key按一定规则生成,保证唯一就可以了,具体源码中有说明,从缓存里面取出数据,如果存在缓存就直接返回给调用者即可,并终止业务方法的执行(体现在不调用context.Proceed()方法上);如果不存在缓存数据或者缓存过期了,则调用业务方法获取数据后并缓存就可以了。

    1. /// <summary>

    2. /// 用AOP来实现自动缓存

    3. /// </summary>

    4. public class AutoCacheAttribute : Attribute, IMethodAdvice

    5. {

    6. /// <summary>

    7. /// 滑动过期

    8. /// </summary>

    9. public bool EnableSliding { get; set; }

    10. /// <summary>

    11. /// 缓存时间,分钟

    12. /// </summary>

    13. public int CacheMinutes { get; set; }

    14. /// <summary>

    15. /// 构造函数

    16. /// </summary>

    17. /// <param name="cacheMinutes">缓存时间,分钟,默认5分钟,小于等于0永久缓存</param>

    18. /// <param name="enableSliding">使用滑动过期缓存控制策略</param>

    19. public AutoCacheAttribute(int cacheMinutes = 5, bool enableSliding = false)

    20. {

    21. EnableSliding = enableSliding;

    22. CacheMinutes = cacheMinutes;

    23. }

    24. /// <summary>

    25. /// AOP组件拦截方法,用于实现自动缓存,有缓存时直接返回;

    26. /// 没有缓存时,调用被拦截方法后,有返回值则将数据自动缓存起来

    27. /// </summary>

    28. /// <param name="context"></param>

    29. public void Advise(MethodAdviceContext context)

    30. {

    31. var key = GetKey(context);

    32. if (context.HasReturnValue && key.TryGetCache(out object m))

    33. {

    34. var r = m as ResponseResult;

    35. r.Message = "在拦截方法里面改了缓存里面取出来的数据!";

    36. context.ReturnValue = r;

    37. //context.ReturnValue = m;

    38. //context.Proceed(); //直接取出缓存返回,不用执行原来取数据方法。

    39. }

    40. else

    41. {

    42. context.Proceed();//执行被拦截的方法

    43. if (context.HasReturnValue && context.ReturnValue != null)

    44. {

    45. //被拦截方法有返回值,并且返回值不为null

    46. if (EnableSliding && CacheMinutes > 0)

    47. context.ReturnValue.SetCache(key, TimeSpan.FromMinutes(CacheMinutes));

    48. else if (CacheMinutes > 0)

    49. context.ReturnValue.SetCache(key, DateTime.Now.AddMinutes(CacheMinutes));

    50. else

    51. context.ReturnValue.SetCache(key);

    52. }

    53. }

    54. }

    55. /// <summary>

    56. /// 获取缓存key,key的规则为: md5(类全名|方法名|参数列表拆分数组|参数值的json数组),这样可以保证唯一

    57. /// </summary>

    58. /// <param name="context"></param>

    59. /// <returns></returns>

    60. private string GetKey(MethodAdviceContext context)

    61. {

    62. var array = context.TargetMethod.GetParameters();

    63. var key = array.Select(x => { return context.Arguments[x.Position].ToJson(); }).ToArray();

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

    65. return cacheKey;

    66. }

    67. }

    68. /// <summary>

    69. /// 缓存扩展方法,可使用其它缓存替代

    70. /// </summary>

    71. public static class CacheExtensions

    72. {

    73. private static MemoryCache cache = new MemoryCache("https://jhrs.com");

    74. /// <summary>

    75. /// 设置缓存,一直不过期

    76. /// </summary>

    77. /// <typeparam name="T"></typeparam>

    78. /// <param name="value"></param>

    79. /// <param name="key"></param>

    80. public static void SetCache<T>(this T value, string key)

    81. {

    82. if (string.IsNullOrWhiteSpace(key)) throw new ArgumentException($"缓存键参数{nameof(key)}不能为null或空");

    83. if (value == null) throw new ArgumentException($"缓存值参数{nameof(value)}不能为null");

    84. CacheItemPolicy policy = new CacheItemPolicy();

    85. cache.Set(key, value, policy);

    86. }

    87. /// <summary>

    88. /// 设置缓存,固定过期时间

    89. /// </summary>

    90. /// <typeparam name="T"></typeparam>

    91. /// <param name="value"></param>

    92. /// <param name="key"></param>

    93. /// <param name="absoluteExpiration"></param>

    94. public static void SetCache<T>(this T value, string key, DateTimeOffset? absoluteExpiration)

    95. {

    96. if (string.IsNullOrWhiteSpace(key)) throw new ArgumentException($"缓存键参数{nameof(key)}不能为null或空");

    97. if (value == null) throw new ArgumentException($"缓存值参数{nameof(value)}不能为null");

    98. CacheItemPolicy policy = new CacheItemPolicy() { AbsoluteExpiration = (DateTimeOffset)absoluteExpiration };

    99. cache.Set(key, value, policy);

    100. }

    101. /// <summary>

    102. /// 设置缓存,滑动过期

    103. /// </summary>

    104. /// <typeparam name="T"></typeparam>

    105. /// <param name="value"></param>

    106. /// <param name="key"></param>

    107. /// <param name="slidingExpiration"></param>

    108. public static void SetCache<T>(this T value, string key, TimeSpan? slidingExpiration)

    109. {

    110. if (string.IsNullOrWhiteSpace(key)) throw new ArgumentException($"缓存键参数{nameof(key)}不能为null或空");

    111. if (value == null) throw new ArgumentException($"缓存值参数{nameof(value)}不能为null");

    112. CacheItemPolicy policy = new CacheItemPolicy() { SlidingExpiration = (TimeSpan)slidingExpiration };

    113. cache.Set(key, value, policy);

    114. }

    115. /// <summary>

    116. /// 获取缓存数据

    117. /// </summary>

    118. /// <typeparam name="T">对象类型</typeparam>

    119. /// <param name="key"><缓存key/param>

    120. /// <param name="value">返回的缓存数据对名</param>

    121. /// <returns></returns>

    122. public static bool TryGetCache<T>(this string key, out T value)

    123. {

    124. value = default(T);

    125. if (cache.Contains(key))

    126. {

    127. value = (T)cache.Get(key);

    128. return true;

    129. }

    130. return false;

    131. }

    132. /// <summary>

    133. /// 获取字符串MD5值

    134. /// </summary>

    135. /// <param name="value"></param>

    136. /// <returns></returns>

    137. public static string GetMd5(this string value)

    138. {

    139. byte[] bytes = Encoding.UTF8.GetBytes(value);

    140. StringBuilder sb = new StringBuilder();

    141. MD5 hash = new MD5CryptoServiceProvider();

    142. bytes = hash.ComputeHash(bytes);

    143. foreach (byte b in bytes)

    144. {

    145. sb.AppendFormat("{0:x2}", b);

    146. }

    147. return sb.ToString();

    148. }

    149. }

    附加的JSON扩展类

    该扩展类只是方便将对象转为JSON而已,代码不复如,如下所示:

    1. public static class JsonExtensions

    2. {

    3. /// <summary>

    4. /// 将对象转换为JSON字符串

    5. /// </summary>

    6. /// <param name="obj">要转换的对象</param>

    7. /// <param name="camelCase">是否小写名称</param>

    8. /// <param name="indented"></param>

    9. /// <returns></returns>

    10. public static string ToJson(this object obj, bool camelCase = false, bool indented = false)

    11. {

    12. JsonSerializerSettings settings = new JsonSerializerSettings();

    13. if (camelCase)

    14. {

    15. settings.ContractResolver = new CamelCasePropertyNamesContractResolver();

    16. }

    17. if (indented)

    18. {

    19. settings.Formatting = Formatting.Indented;

    20. }

    21. return JsonConvert.SerializeObject(obj, settings);

    22. }

    23. /// <summary>

    24. /// 把Json字符串转换为强类型对象

    25. /// </summary>

    26. public static T FromJson<T>(string json)

    27. {

    28. if (string.IsNullOrWhiteSpace(json)) return default(T);

    29. json = JsonDateTimeFormat(json);

    30. return JsonConvert.DeserializeObject<T>(json);

    31. }

    32. /// <summary>

    33. /// 处理Json的时间格式为正常格式

    34. /// </summary>

    35. private static string JsonDateTimeFormat(string json)

    36. {

    37. json = Regex.Replace(json,

    38. @"\/Date((\d+))\/",

    39. match =>

    40. {

    41. DateTime dt = new DateTime(1970, 1, 1);

    42. dt = dt.AddMilliseconds(long.Parse(match.Groups[1].Value));

    43. dt = dt.ToLocalTime();

    44. return dt.ToString("yyyy-MM-dd HH:mm:ss.fff");

    45. });

    46. return json;

    47. }

    48. }

    解决直接引用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文件

    配置节点为如下:

    1. <Import Project="..\packages\MrAdvice.2.8.8\build\MrAdvice.targets" Condition="Exists('..\packages\MrAdvice.2.8.8\build\MrAdvice.targets')" />
    2. <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
    3. <PropertyGroup>
    4. <ErrorText>这台计算机上缺少此项目引用的 NuGet 程序包。使用“NuGet 程序包还原”可下载这些程序包。有关更多信息,请参见 http://go.microsoft.com/fwlink/?LinkID=322105。缺少的文件是 {0}。</ErrorText>
    5. </PropertyGroup>
    6. <Error Condition="!Exists('..\packages\MrAdvice.2.8.8\build\MrAdvice.targets')" Text="([System.String]::Format('(ErrorText)', '..\packages\MrAdvice.2.8.8\build\MrAdvice.targets'))" />
    7. </Target>

    相关文章

      网友评论

        本文标题:一分钟教你引用MrAdvice.dll文件实现AOP拦截

        本文链接:https://www.haomeiwen.com/subject/lvbgictx.html