近日工做中,要實現一個功能,那就是業務層方法裏面實現自動緩存。編寫業務的C#開發人員只關注如何將業務代碼編寫正確就能夠了,而緩存的代碼,大多相似,無非就是判斷是否有緩存,有就取出返回,沒有就調用數據庫代碼獲取數據再緩存起來而已,因而這部分代碼經過使用AOP的方式自動接管掉這種重複性代碼。git
MrAdvice開源項目github地址:https://github.com/ArxOne/MrAdvicegithub
直接引用MrAdvice.dll文件不能實現AOP攔截功能數據庫
因項目緣由內外網隔離,且是斷網開發的,就只能在外網寫個測試程序,而後將MrAdvice.dll文件複製到內網電腦,內網電腦經過引用dll的方式來使用該組件,結果是不會進入到攔截方法的。
json
經過下圖能夠看到,成功解決後,能夠實現自動緩存了。數組
下面是所有的演示程序源碼。緩存
演示程序解決方案目錄一覽ide
該項目是一個控制檯項目,解決方案以下圖所示:
函數
MrAdvice.dll是直接引用的,不是經過nuget安裝的,至於這個dll文件的獲取,你能夠經過nuget獲取了找到它便可。post
演示程序的源碼測試
控制檯入口的代碼比較簡單,單純的調用接口。
程序入口代碼
程序接口代碼
程序接口代碼主要是模擬業務方法裏面的一些類,定義了一個接口,一個實現類,另外實現類上面是標註了一個自動緩存的特性(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>