本文3.0版本文章
代碼已上傳Github+Gitee,文末有地址
上回《從壹開始前后端分離【 .NET Core2.0 Api + Vue 2.0 + AOP + 分布式】框架之九 || 依賴注入IoC學習 + AOP界面編程初探》咱們說到了依賴注入Autofac的使用,不知道大家對IoC的使用是怎樣的感覺,我個人表示還是比較可行的,至少不用自己再關心一個個復雜的實例化服務對象了,直接通過接口就滿足需求,當然還有其他的一些功能,我還沒有說到,拋磚引玉嘛,大家如果有好的想法,歡迎留言,也可以來群里,大家一起學習討論。昨天在文末咱們說到了AOP面向切面編程的定義和思想,我個人簡單使用了下,感覺主要的思路還是通過攔截器來操作,就像是一個中間件一樣,今天呢,我給大家說兩個小栗子,當然,你也可以合並成一個,也可以自定義擴展,因為我們是整個系列是基於Autofac框架,所以今天主要說的是基於Autofac的Castle動態代理的方法,靜態注入的方式以后有時間可以再補充。
時間真快,轉眼已經十天過去了,感謝大家的鼓勵,批評指正,希望我的文章,對您有一點點兒的幫助,哪怕是有學習新知識的動力也行,至少至少,可以為以后跳槽增加新的談資 [哭笑],這些天我們從面向對象OOP的開發,后又轉向了面向接口開發,到分層解耦,現在到了面向切面編程AOP,往下走將會是,分布式,微服務等等,技術真是永無止境啊!好啦,馬上開始動筆。
課前注意:
關於攔截器
1、保證你寫的autofac程序集批量依賴注入是有效的,且能正常運行;
2、攔截器有一定的要求,注意被攔截的對象是單純的類(EnableClassInterceptors),還是有接口代理模式(EnableInterfaceInterceptors),不同的方案,對應不同的方法;
3、如果被攔截的對象是單純的類,而沒有接口,除了用對應的攔截方案(上邊第2點)外,還要保證攔截的方法是虛方法(代理核心就是重寫);
4、代理類只要打斷點能進去就代表成功;
多個案例,查看我項目中的Demo:

零、今天完成的深紅色部分

一、AOP 之 實現日志記錄(服務層)
首先想一想,如果有一個需求(這個只是我的一個想法,真實工作中可能用不上),要記錄整個項目的接口和調用情況,當然如果只是控制器的話,還是挺簡單的,直接用一個過濾器或者一個中間件,還記得咱們開發Swagger攔截權限驗證的中間件么,那個就很方便的把用戶調用接口的名稱記錄下來,當然也可以寫成一個切面,但是如果想看下與Service或者Repository層的調用情況呢,好像目前咱們只能在Service層或者Repository層去寫日志記錄了,那樣的話,不僅工程大(當然你可以用工廠模式),而且耦合性瞬間就高了呀,想象一下,如果日志要去掉,關閉,修改,需要改多少地方!您說是不是,好不容易前邊的工作把層級的耦合性降低了。別慌,這個時候就用到了AOP和Autofac的Castle結合的完美解決方案了。
經過這么多天的開發,幾乎每天都需要引入Nuget包哈,我個人表示也不想再添加了,現在都已經挺大的了 ,今天不會辣!其實都是基於昨天的兩個Nuget包中已經自動生成的Castle組件(Autofac.Extras.DynamicProxy)。請看以下步驟:
1、定義服務接口與實現類
首先這里使用到了 BlogArticle 的實體類(這里我保留了sqlsugar的特性,沒需要的可以手動刪除):
public class BlogArticle
{
/// <summary>
/// 主鍵
/// </summary>
/// 這里之所以沒用RootEntity,是想保持和之前的數據庫一致,主鍵是bID,不是Id
[SugarColumn(IsNullable = false, IsPrimaryKey = true, IsIdentity = true)]
public int bID { get; set; }
/// <summary>
/// 創建人
/// </summary>
[SugarColumn(Length = 60, IsNullable = true)]
public string bsubmitter { get; set; }
/// <summary>
/// 標題blog
/// </summary>
[SugarColumn(Length = 256, IsNullable = true)]
public string btitle { get; set; }
/// <summary>
/// 類別
/// </summary>
[SugarColumn(Length = int.MaxValue, IsNullable = true)]
public string bcategory { get; set; }
/// <summary>
/// 內容
/// </summary>
[SugarColumn(IsNullable = true, ColumnDataType = "text")]
public string bcontent { get; set; }
/// <summary>
/// 訪問量
/// </summary>
public int btraffic { get; set; }
/// <summary>
/// 評論數量
/// </summary>
public int bcommentNum { get; set; }
/// <summary>
/// 修改時間
/// </summary>
public DateTime bUpdateTime { get; set; }
/// <summary>
/// 創建時間
/// </summary>
public System.DateTime bCreateTime { get; set; }
/// <summary>
/// 備注
/// </summary>
[SugarColumn(Length = int.MaxValue, IsNullable = true)]
public string bRemark { get; set; }
/// <summary>
/// 邏輯刪除
/// </summary>
[SugarColumn(IsNullable = true)]
public bool? IsDeleted { get; set; }
}
在IBlogArticleServices.cs定義一個獲取博客列表接口 ,並在BlogArticleServices實現該接口
public interface IBlogArticleServices :IBaseServices<BlogArticle>
{
Task<List<BlogArticle>> getBlogs();
}
public class BlogArticleServices : BaseServices<BlogArticle>, IBlogArticleServices
{
IBlogArticleRepository dal;
public BlogArticleServices(IBlogArticleRepository dal)
{
this.dal = dal;
base.baseDal = dal;
}
/// <summary>
/// 獲取博客列表
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public async Task<List<BlogArticle>> getBlogs()
{
var bloglist = await dal.Query(a => a.bID > 0, a => a.bID);
return bloglist;
}
}
2、在API層中添加對該接口引用
(注意RESTful接口路徑命名規范,我這么寫只是為了測試)
/// <summary>
/// 獲取博客列表
/// </summary>
/// <returns></returns>
[HttpGet]
[Route("GetBlogs")]
public async Task<List<BlogArticle>> GetBlogs()
{
return await blogArticleServices.getBlogs();
}
3、添加AOP攔截器
在Blog.Core新建文件夾AOP,並添加攔截器BlogLogAOP,並設計其中用到的日志記錄Logger方法或者類
關鍵的一些知識點,注釋中已經說明了,主要是有以下:
1、繼承接口IInterceptor
2、實例化接口IINterceptor的唯一方法Intercept
3、void Proceed();表示執行當前的方法和object ReturnValue { get; set; }執行后調用,object[] Arguments參數對象
4、中間的代碼是新建一個類,還是單寫,就很隨意了。
/// <summary>
/// 攔截器BlogLogAOP 繼承IInterceptor接口
/// </summary>
public class BlogLogAOP : IInterceptor
{
/// <summary>
/// 實例化IInterceptor唯一方法
/// </summary>
/// <param name="invocation">包含被攔截方法的信息</param>
public void Intercept(IInvocation invocation)
{
//記錄被攔截方法信息的日志信息
var dataIntercept = $"{DateTime.Now.ToString("yyyyMMddHHmmss")} " +
$"當前執行方法:{ invocation.Method.Name} " +
$"參數是: {string.Join(", ", invocation.Arguments.Select(a => (a ?? "").ToString()).ToArray())} \r\n";
//在被攔截的方法執行完畢后 繼續執行當前方法
invocation.Proceed();
dataIntercept += ($"被攔截方法執行完畢,返回結果:{invocation.ReturnValue}");
#region 輸出到當前項目日志
var path = Directory.GetCurrentDirectory() + @"\Log";
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
string fileName = path + $@"\InterceptLog-{DateTime.Now.ToString("yyyyMMddHHmmss")}.log";
StreamWriter sw = File.AppendText(fileName);
sw.WriteLine(dataIntercept);
sw.Close();
#endregion
}
}
提示:這里展示了如何在項目中使用AOP實現對 service 層進行日志記錄,如果你想實現異常信息記錄的話,很簡單,
注意,下邊方法僅僅是針對同步的策略,如果你的service是異步的,這里獲取不到,正確的寫法,在文章底部的 GitHub 代碼里,你可以查看我的源碼。
意思就說,同步和異步是分開來寫的:
下邊的是完整代碼:
/// <summary>
/// 實例化IInterceptor唯一方法
/// </summary>
/// <param name="invocation">包含被攔截方法的信息</param>
public void Intercept(IInvocation invocation)
{
//記錄被攔截方法信息的日志信息
var dataIntercept = "" +
$"【當前執行方法】:{ invocation.Method.Name} \r\n" +
$"【攜帶的參數有】: {string.Join(", ", invocation.Arguments.Select(a => (a ?? "").ToString()).ToArray())} \r\n";
try
{
MiniProfiler.Current.Step($"執行Service方法:{invocation.Method.Name}() -> ");
//在被攔截的方法執行完畢后 繼續執行當前方法,注意是被攔截的是異步的
invocation.Proceed();
// 異步獲取異常,先執行
if (IsAsyncMethod(invocation.Method))
{
//Wait task execution and modify return value
if (invocation.Method.ReturnType == typeof(Task))
{
invocation.ReturnValue = InternalAsyncHelper.AwaitTaskWithPostActionAndFinally(
(Task)invocation.ReturnValue,
async () => await TestActionAsync(invocation),
ex =>
{
LogEx(ex, ref dataIntercept);
});
}
else //Task<TResult>
{
invocation.ReturnValue = InternalAsyncHelper.CallAwaitTaskWithPostActionAndFinallyAndGetResult(
invocation.Method.ReturnType.GenericTypeArguments[0],
invocation.ReturnValue,
async () => await TestActionAsync(invocation),
ex =>
{
LogEx(ex, ref dataIntercept);
});
}
}
else
{// 同步1
}
}
catch (Exception ex)// 同步2
{
LogEx(ex, ref dataIntercept);
}
dataIntercept += ($"【執行完成結果】:{invocation.ReturnValue}");
Parallel.For(0, 1, e =>
{
LogLock.OutSql2Log("AOPLog", new string[] { dataIntercept });
});
_hubContext.Clients.All.SendAsync("ReceiveUpdate", LogLock.GetLogData()).Wait();
}
4、添加到Autofac容器中,實現注入
還記得昨天的容器么,先把攔截器注入,然后對程序集的注入方法中添加攔截器服務即可
builder.RegisterType<BlogLogAOP>();//可以直接替換其他攔截器!一定要把攔截器進行注冊
var assemblysServices = Assembly.Load("Blog.Core.Services");
//builder.RegisterAssemblyTypes(assemblysServices).AsImplementedInterfaces();//指定已掃描程序集中的類型注冊為提供所有其實現的接口。
builder.RegisterAssemblyTypes(assemblysServices)
.AsImplementedInterfaces()
.InstancePerLifetimeScope()
.EnableInterfaceInterceptors()//引用Autofac.Extras.DynamicProxy;
.InterceptedBy(typeof(BlogLogAOP));//可以直接替換攔截器
注意其中的兩個方法
.EnableInterfaceInterceptors()//對目標類型啟用接口攔截。攔截器將被確定,通過在類或接口上截取屬性, 或添加 InterceptedBy ()
.InterceptedBy(typeof(BlogLogAOP));//允許將攔截器服務的列表分配給注冊。
說人話就是,將攔截器添加到要注入容器的接口或者類之上。
5、運行項目,查看效果
嗯,你就看到這根目錄下生成了一個Log文件夾,里邊有日志記錄,當然記錄很簡陋,里邊是獲取到的實體類,大家可以自己根據需要擴展
這里,面向服務層的日志記錄就完成了,大家感覺是不是很平時的不一樣?
二、AOP 之 實現接口數據的緩存功能
想一想,如果我們要實現緩存功能,一般咱們都是將數據獲取到以后,定義緩存,然后在其他地方使用的時候,在根據key去獲取當前數據,然后再操作等等,平時都是在API接口層獲取數據后進行緩存,今天咱們可以試試,在接口之前就緩存下來。
1、定義 Memory 緩存類和接口
老規矩,定義一個緩存類和接口,你會問了,為什么上邊的日志沒有定義,因為我會在之后講Redis的時候用到這個緩存接口
/// <summary>
/// 簡單的緩存接口,只有查詢和添加,以后會進行擴展
/// </summary>
public interface ICaching
{
object Get(string cacheKey);
void Set(string cacheKey, object cacheValue);
}
/// <summary>
/// 實例化緩存接口ICaching
/// </summary>
public class MemoryCaching : ICaching
{
//引用Microsoft.Extensions.Caching.Memory;這個和.net 還是不一樣,沒有了Httpruntime了
private IMemoryCache _cache;
//還是通過構造函數的方法,獲取
public MemoryCaching(IMemoryCache cache)
{
_cache = cache;
}
public object Get(string cacheKey)
{
return _cache.Get(cacheKey);
}
public void Set(string cacheKey, object cacheValue)
{
_cache.Set(cacheKey, cacheValue, TimeSpan.FromSeconds(7200));
}
}
2、定義一個緩存攔截器
還是繼承IInterceptor,並實現Intercept
/// <summary>
/// 面向切面的緩存使用
/// </summary>
public class BlogCacheAOP : IInterceptor
{
//通過注入的方式,把緩存操作接口通過構造函數注入
private ICaching _cache;
public BlogCacheAOP(ICaching cache)
{
_cache = cache;
}
//Intercept方法是攔截的關鍵所在,也是IInterceptor接口中的唯一定義
public void Intercept(IInvocation invocation)
{
//獲取自定義緩存鍵
var cacheKey = CustomCacheKey(invocation);
//根據key獲取相應的緩存值
var cacheValue = _cache.Get(cacheKey);
if (cacheValue != null)
{
//將當前獲取到的緩存值,賦值給當前執行方法
invocation.ReturnValue = cacheValue;
return;
}
//去執行當前的方法
invocation.Proceed();
//存入緩存
if (!string.IsNullOrWhiteSpace(cacheKey))
{
_cache.Set(cacheKey, invocation.ReturnValue);
}
}
//自定義緩存鍵
private string CustomCacheKey(IInvocation invocation)
{
var typeName = invocation.TargetType.Name;
var methodName = invocation.Method.Name;
var methodArguments = invocation.Arguments.Select(GetArgumentValue).Take(3).ToList();//獲取參數列表,我最多需要三個即可
string key = $"{typeName}:{methodName}:";
foreach (var param in methodArguments)
{
key += $"{param}:";
}
return key.TrimEnd(':');
}
//object 轉 string
private string GetArgumentValue(object arg)
{
// PS:這里僅僅是很簡單的數據類型,如果參數是表達式/類等,比較復雜的,請看我的在線代碼吧,封裝的比較多,當然也可以自己封裝。 if (arg is int || arg is long || arg is string)
return arg.ToString();
if (arg is DateTime)
return ((DateTime)arg).ToString("yyyyMMddHHmmss");
return "";
}
}
注釋的很清楚,基本都是情況
3、注入緩存攔截器
ConfigureServices不用動,只需要改下攔截器的名字就行
注意:
//將 TService 中指定的類型的范圍服務添加到實現
services.AddScoped<ICaching, MemoryCaching>();//記得把緩存注入!!!
4、運行,查看效果
你會發現,首次緩存是空的,然后將Repository倉儲中取出來的數據存入緩存,第二次使用就是有值了,其他所有的地方使用,都不用再寫了,而且也是面向整個程序集合的
5、多個AOP執行順序問題
在我最新的 Github 項目中,我定義了三個 AOP :除了上邊兩個 LogAOP和 CacheAOP 以外,還有一個 RedisCacheAOP,並且通過開關的形式在項目中配置是否啟用:

那具體的執行順序是什么呢,這里說下,就是從上至下的順序,或者可以理解成挖金礦的形式,執行完上層的,然后緊接着來下一個AOP,最后想要回家,就再一個一個跳出去,在往上層走的時候,礦肯定就執行完了,就不用再操作了,直接出去,就像 break 一樣,可以參考這個動圖:

6、無接口如何實現AOP
上邊我們討論了很多,但是都是接口框架的,
比如:Service.dll 和與之對應的 IService.dll,Repository.dll和與之對應的 IRepository.dll,我們可以直接在對應的層注入的時候,匹配上 AOP 信息,但是如果我們沒有使用接口怎么辦?
這里大家可以安裝下邊的實驗下:
Autofac它只對接口方法 或者 虛virtual方法或者重寫方法override才能起攔截作用。
如果沒有接口
案例是這樣的:
如果我們的項目是這樣的,沒有接口,會怎么辦:

// 服務層類
public class StudentService
{
StudentRepository _studentRepository;
public StudentService(StudentRepository studentRepository)
{
_studentRepository = studentRepository;
}
public string Hello()
{
return _studentRepository.Hello();
}
}
// 倉儲層類
public class StudentRepository
{
public StudentRepository()
{
}
public string Hello()
{
return "hello world!!!";
}
}
// controller 接口調用
StudentService _studentService;
public ValuesController(StudentService studentService)
{
_studentService = studentService;
}

如果是沒有接口的單獨實體類
public class Love
{
// 一定要是虛方法
public virtual string SayLoveU()
{
return "I ♥ U";
}
}
//---------------------------
//只能注入該類中的虛方法
builder.RegisterAssemblyTypes(Assembly.GetAssembly(typeof(Love)))
.EnableClassInterceptors()
.InterceptedBy(typeof(BlogLogAOP));
三、還有其他的一些問題需要考慮
1、可以針對某一層的指定類的指定方法進行操作,這里就不寫了,大家可以自己實驗
配合Attribute就可以只攔截相應的方法了。因為攔截器里面是根據Attribute進行相應判斷的!!
builder.RegisterAssemblyTypes(assembly)
.Where(type => typeof(IQCaching).IsAssignableFrom(type) && !type.GetTypeInfo().IsAbstract) .AsImplementedInterfaces()
.InstancePerLifetimeScope()
.EnableInterfaceInterceptors()
.InterceptedBy(typeof(QCachingInterceptor));
2、時間問題,阻塞,浪費資源問題等
定義切面有時候是方便,初次使用會很別扭,使用多了,可能會對性能有些許的影響,因為會大量動態生成代理類,性能損耗,是特別高的請求並發,比如萬級每秒,還是不建議生產環節推薦。所以說切面編程要深入的研究,不可隨意使用,我說的也是九牛一毛,大家繼續加油吧!
3、靜態注入
基於Net的IL語言層級進行注入,性能損耗可以忽略不計,Net使用最多的Aop框架PostSharp(好像收費了;)采用的即是這種方式。
大家可以參考這個博文:https://www.cnblogs.com/mushroom/p/3932698.html
四、結語
今天的講解就到了這里了,通過這兩個小栗子,大家應該能對面向切面編程有一些朦朧的感覺了吧,感興趣的可以深入的研究,也歡迎一起討論,剛剛在緩存中,我說到了緩存接口,就引入了下次的講解內容,Redis的高性能緩存框架,內存存儲的數據結構服務器,可用作數據庫,高速緩存和消息隊列代理。下次再見咯~
1、網友好資料
- 帶你學習AOP框架之Aspect.Core[1]
五、Github && Gitee