直接引用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直接引用MrAdvice.dll
通过下图可以看到,成功解决后,可以实现自动缓存了。
实现AOP拦截实现AOP拦截
下面是全部的演示程序源码。
演示程序解决方案目录一览
该项目是一个控制台项目,解决方案如下图所示:
演示程序解决方案演示程序解决.
MrAdvice.dll是直接引用的,不是通过nuget安装的,至于这个dll文件的获取,你可以通过nuget获取了找到它即可。
演示程序的源码
控制台入口的代码比较简单,单纯的调用接口。
程序入口代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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拦截代码,具体下面会给出的;另外还有一个输出结果(响应消息)的类。整个源码是放到一个文件里面的,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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;
}
///
/// 缓存key
///
///
///
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;
}
}
///
/// 输出结果
///
public class ResponseResult
{
public int Code { get; set; }
public string Message { get; set; }
//.....其它属性
}
核心的AOP拦截代码
该代码是用于实现自动缓存功能,思路就是在调用业务方法前,根据缓存key,缓存key按一定规则生成,保证唯一就可以了,具体源码中有说明,从缓存里面取出数据,如果存在缓存就直接返回给调用者即可,并终止业务方法的执行(体现在不调用context.Proceed()方法上);如果不存在缓存数据或者缓存过期了,则调用业务方法获取数据后并缓存就可以了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
///
/// 用AOP来实现自动缓存
///
public class AutoCacheAttribute : Attribute, IMethodAdvice
{
///
/// 滑动过期
///
public bool EnableSliding { get; set; }
///
/// 缓存时间,分钟
///
public int CacheMinutes { get; set; }
///
/// 构造函数
///
/// 缓存时间,分钟,默认5分钟,小于等于0永久缓存
/// 使用滑动过期缓存控制策略
public AutoCacheAttribute(int cacheMinutes = 5, bool enableSliding = false)
{
EnableSliding = enableSliding;
CacheMinutes = cacheMinutes;
}
///
/// AOP组件拦截方法,用于实现自动缓存,有缓存时直接返回;
/// 没有缓存时,调用被拦截方法后,有返回值则将数据自动缓存起来
///
///
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);
}
}
}
///
/// 获取缓存key,key的规则为: md5(类全名|方法名|参数列表拆分数组|参数值的json数组),这样可以保证唯一
///
///
///
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;
}
}
///
/// 缓存扩展方法,可使用其它缓存替代
///
public static class CacheExtensions
{
private static MemoryCache cache = new MemoryCache("https://jhrs.com");
///
/// 设置缓存,一直不过期
///
///
///
///
public static void SetCache(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);
}
///
/// 设置缓存,固定过期时间
///
///
///
///
///
public static void SetCache(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);
}
///
/// 设置缓存,滑动过期
///
///
///
///
///
public static void SetCache(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);
}
///
/// 获取缓存数据
///
/// 对象类型
/// <缓存key/param>
/// 返回的缓存数据对名
///
public static bool TryGetCache(this string key, out T value)
{
value = default(T);
if (cache.Contains(key))
{
value = (T)cache.Get(key);
return true;
}
return false;
}
///
/// 获取字符串MD5值
///
///
///
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而已,代码不复如,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public static class JsonExtensions
{
///
/// 将对象转换为JSON字符串
///
/// 要转换的对象
/// 是否小写名称
///
///
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);
}
///
/// 把Json字符串转换为强类型对象
///
public static T FromJson(string json)
{
if (string.IsNullOrWhiteSpace(json)) return default(T);
json = JsonDateTimeFormat(json);
return JsonConvert.DeserializeObject(json);
}
///
/// 处理Json的时间格式为正常格式
///
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文件
配置节点为如下:
1
2
3
4
5
6
7
这台计算机上缺少此