Java自学者论坛

 找回密码
 立即注册

手机号码,快捷登录

恭喜Java自学者论坛(https://www.javazxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,会员资料板块,购买链接:点击进入购买VIP会员

JAVA高级面试进阶训练营视频教程

Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程Go语言视频零基础入门到精通Java架构师3期(课件+源码)
Java开发全终端实战租房项目视频教程SpringBoot2.X入门到高级使用教程大数据培训第六期全套视频教程深度学习(CNN RNN GAN)算法原理Java亿级流量电商系统视频教程
互联网架构师视频教程年薪50万Spark2.0从入门到精通年薪50万!人工智能学习路线教程年薪50万大数据入门到精通学习路线年薪50万机器学习入门到精通教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程MySQL入门到精通教程
查看: 444|回复: 0

直接引用MrAdvice.dll文件不能实现AOP拦截,教你1分钟解决这个问题

[复制链接]
  • TA的每日心情
    奋斗
    2024-11-24 15:47
  • 签到天数: 804 天

    [LV.10]以坛为家III

    2053

    主题

    2111

    帖子

    72万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    726782
    发表于 2021-6-4 01:33:16 | 显示全部楼层 |阅读模式

    直接引用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直接引用MrAdvice.dll
     

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

    实现AOP拦截实现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 目录MrAdvice 目录
    • 修改项目文件,即修改csproj文件,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.html

    哎...今天够累的,签到来了1...
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|小黑屋|Java自学者论坛 ( 声明:本站文章及资料整理自互联网,用于Java自学者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2025-2-2 14:48 , Processed in 0.063932 second(s), 29 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表