目錄 Fireasy3 揭秘 -- 依賴註入與服務發現 Fireasy3 揭秘 -- 自動服務部署 Fireasy3 揭秘 -- 使用 SourceGeneraor 改進服務發現 Fireasy3 揭秘 -- 使用 SourceGeneraor 實現動態代理(AOP) Fireasy3 揭秘 -- ...
目錄
- Fireasy3 揭秘 -- 依賴註入與服務發現
- Fireasy3 揭秘 -- 自動服務部署
- Fireasy3 揭秘 -- 使用 SourceGeneraor 改進服務發現
- Fireasy3 揭秘 -- 使用 SourceGeneraor 實現動態代理(AOP)
- Fireasy3 揭秘 -- 使用 Emit 構建程式集
- Fireasy3 揭秘 -- 使用緩存提高反射性能
- Fireasy3 揭秘 -- 動態類型及擴展支持
- Fireasy3 揭秘 -- 線程數據共用的實現
- Fireasy3 揭秘 -- 配置管理及解析處理
- Fireasy3 揭秘 -- 資料庫適配器
- Fireasy3 揭秘 -- 解決資料庫之間的語法差異
- Fireasy3 揭秘 -- 獲取資料庫的架構信息
- Fireasy3 揭秘 -- 數據批量插入的實現
- Fireasy3 揭秘 -- 使用包裝器對數據讀取進行相容
- Fireasy3 揭秘 -- 數據行映射器
- Fireasy3 揭秘 -- 數據轉換器的實現
- Fireasy3 揭秘 -- 通用序列生成器和雪花生成器的實現
- Fireasy3 揭秘 -- 命令攔截器的實現
- Fireasy3 揭秘 -- 資料庫主從同步的實現
- Fireasy3 揭秘 -- 大數據分頁的策略
- Fireasy3 揭秘 -- 數據按需更新及生成實體代理類
- Fireasy3 揭秘 -- 用對象池技術管理上下文
- Fireasy3 揭秘 -- Lambda 表達式解析的原理
- Fireasy3 揭秘 -- 擴展選擇的實現
- Fireasy3 揭秘 -- 按需載入與惰性載入的區別與實現
- Fireasy3 揭秘 -- 自定義函數的解析與綁定
- Fireasy3 揭秘 -- 與 MongoDB 進行適配
- Fireasy3 揭秘 -- 模塊化的實現原理
實現 AOP(面向切麵編程)的實現方式有很多種,但無外乎靜態紡織和動態編織兩種。
動態編織 在 Fireasy 2 中使用 Emit
實現了動態編織。Emit 的缺點很明顯,首先就是要求對 IL
語言必須比較熟悉且考慮較全面,其次由於是動態編織,需要使用記憶體緩存來對代理類進行管理。
靜態編織 是指在代碼編譯時就將攔截器相關的代碼插入到代碼內部,運行時不耗用時間和記憶體空間。常用的有 MSBuild
(如PostSharp) 和 Code Analyzers
(本文中用到的 ISourceGenerator
)。
首先,定義攔截器的介面,Initialize/InitializeAsync
方法用於首次初始化,Intercept/InterceptAsync
用於攔截方法的執行和屬性的訪問。如下:
/// <summary>
/// 提供對類成員進行攔截的方法。
/// </summary>
public interface IInterceptor
{
/// <summary>
/// 使用上下文對象對當前的攔截器進行初始化。
/// </summary>
/// <param name="context">包含攔截定義的上下文。</param>
void Initialize(InterceptContext context);
/// <summary>
/// 將自定義方法註入到當前的攔截點。
/// </summary>
/// <param name="info">攔截調用信息。</param>
void Intercept(InterceptCallInfo info);
}
/// <summary>
/// 提供對類成員進行攔截的非同步方法。
/// </summary>
public interface IAsyncInterceptor
{
/// <summary>
/// 使用上下文對象對當前的攔截器進行初始化。
/// </summary>
/// <param name="context">包含攔截定義的上下文。</param>
ValueTask InitializeAsync(InterceptContext context);
/// <summary>
/// 將自定義方法註入到當前的攔截點。
/// </summary>
/// <param name="info">攔截調用信息。</param>
ValueTask InterceptAsync(InterceptCallInfo info);
}
InterceptCallInfo
是攔截的調用信息,其中,Arguments 屬性是調用的入參,ReturnValue 屬性是返回的值。如下:
/// <summary>
/// 用於通知客戶端的攔截信息。無法繼承此類。
/// </summary>
public sealed class InterceptCallInfo
{
/// <summary>
/// 獲取或設置定義的類型。
/// </summary>
public Type? DefinedType { get; set; }
/// <summary>
/// 獲取或設置當前被攔截的方法或屬性。
/// </summary>
public MemberInfo? Member { get; set; }
/// <summary>
/// 獲取或設置方法的返回類型。
/// </summary>
public Type? ReturnType { get; set; }
/// <summary>
/// 獲取或設置當前被攔截的目標對象。
/// </summary>
public object? Target { get; set; }
/// <summary>
/// 獲取或設置攔截的類型。
/// </summary>
public InterceptType InterceptType { get; set; }
/// <summary>
/// 獲取或設置方法的參數數組。
/// </summary>
public object[]? Arguments { get; set; }
/// <summary>
/// 獲取或設置方法的返回值。
/// </summary>
public object? ReturnValue { get; set; }
/// <summary>
/// 獲取或設置觸發的異常信息。
/// </summary>
public Exception? Exception { get; set; }
/// <summary>
/// 獲取或設置取消 Before 事件之後調用基類的方法。
/// </summary>
public bool Cancel { get; set; }
/// <summary>
/// 獲取或設置是否中斷後繼攔截器的執行。
/// </summary>
public bool Break { get; set; }
}
最後定義一個特性 InterceptAttribute
用於指定類或方法上的攔截器類型,只需要在類、屬性或方法上指定即可:
[InterceptAttribute(typeof(SampleInterceptor))]
接下來,我們定義好註入代碼的原型,以便下一步生成代碼。比如方法的切麵示例:
public override string Test(string str)
{
//定義由 InterceptAttribute 指定的攔截器實例
var interceptors = new List<IInterceptor> { new SampleInterceptor() };
var info = new InterceptCallInfo();
//攔截的對象為當前對象
info.Target = this;
//方法的入參
info.Arguments = new object[] { str };
//當前攔截的成員(方法)
info.Member = ((MethodInfo)MethodBase.GetCurrentMethod()).GetBaseDefinition();
try
{
//初始化
_Initialize(interceptors, info);
//通知攔截器方法即將被調用
_Intercept(interceptors, info, InterceptType.BeforeMethodCall);
//如果攔截器告知要取消執行
if (info.Cancel)
{
//返回值,如果方法為 void,那麼直接 return
//在攔截器中可以給 ReturnValue 進行賦值,然後 Cancel = true
return info.ReturnValue == null ? default : (string)info.ReturnValue;
}
//調用父方法
info.ReturnValue = base.Test(str);
//通知攔截器方法已調用完成
_Intercept(interceptors, info, InterceptType.AfterMethodCall);
}
catch (System.Exception exp)
{
info.Exception = exp;
//通知攔截器,有異常拋出
_Intercept(interceptors, info, InterceptType.Catching);
throw exp;
}
finally
{
//任何時候,都會走到這一步
_Intercept(interceptors, info, InterceptType.Finally);
}
//返回值
return info.ReturnValue == null ? default : (string)info.ReturnValue;
}
屬性的切麵也差不多,只是對 get 和 set 分別處理。我們來看一下 _Initialize
方法要實現的目的:
private void _Initialize(List<IInterceptor> interceptors, InterceptCallInfo callInfo)
{
if (!this._initMarks.Contains(callInfo.Member))
{
for (int i = 0; i < interceptors.Count; i++)
{
InterceptContext context = new InterceptContext(callInfo.Member, this);
interceptors[i].Initialize(context);
}
this._initMarks.Add(callInfo.Member);
}
}
它的目的是讓攔截器只調用一次 Initialize
方法。而 Intercept
方法用於通知攔截器進行攔截,在攔截器里給定 Break = true 時,後續的攔截器將不會被調用。
private void _Intercept(List<IInterceptor> interceptors, InterceptCallInfo callInfo, InterceptType interceptType)
{
callInfo.InterceptType = interceptType;
callInfo.Break = false;
for (int i = 0; i < interceptors.Count; i++)
{
if (callInfo.Break)
{
break;
}
interceptors[i].Intercept(callInfo);
}
}
接下來,我們用 ISourceGenretor
來實現代碼的生成。
上篇 使用 SourceGeneraor 改進服務發現 已經講解過 ISourceGenerator
的用法了,需要分別定義一個 ISyntaxContextReceiver
和 ISourceGenerator
的實現。
定義 DynamicProxySyntaxReceiver
類,用來接收語法節點,併進行分析,找出可以攔截的方法和屬性,以及攔截器等內容。這裡,我著重講解一下 AnalyseClassSyntax
方法的實現,如下:
/// <summary>
/// 分析類型語法。
/// </summary>
/// <param name="model"></param>
/// <param name="syntax"></param>
private void AnalyseClassSyntax(SemanticModel model, ClassDeclarationSyntax syntax)
{
var typeSymbol = (ITypeSymbol)model.GetDeclaredSymbol(syntax)!;
if (typeSymbol.IsSealed)
{
return;
}
var interceptorMetadataOfClass = FindInterceptorMetadata(typeSymbol);
var metadata = new ClassMetadata(typeSymbol);
//獲取所有成員
foreach (var memberSymbol in typeSymbol.GetMembers())
{
//如果不是方法或屬性
if (memberSymbol.Kind != SymbolKind.Method && memberSymbol.Kind != SymbolKind.Property)
{
continue;
}
if (memberSymbol is IMethodSymbol method)
{
//構造器需要重載,所以也要記錄下來
if (method.MethodKind == MethodKind.Constructor)
{
metadata.AddConstructor(method);
continue;
}
if (method.MethodKind != MethodKind.Ordinary)
{
continue;
}
}
//方法定義為 virtual 並且是公共的
if (!memberSymbol.IsVirtual || memberSymbol.DeclaredAccessibility != Accessibility.Public)
{
continue;
}
//查找方法上的 InterceptAttribute 特性
if (memberSymbol.GetAttributes().Any(s => s.AttributeClass!.ToDisplayString() == InterceptorAttributeName))
{
var interceptorMetadataOfMember = FindInterceptorMetadata(memberSymbol);
if (interceptorMetadataOfMember != null)
{
metadata.AddMember(memberSymbol, interceptorMetadataOfMember);
}
}
//沒找到方法上的特性,則使用類上定義的 InterceptAttribute 特性
else if (interceptorMetadataOfClass != null)
{
var hasIgnoreThrowExpAttr = HasIgnoreThrowExceptionAttribute(memberSymbol);
metadata.AddMember(memberSymbol, interceptorMetadataOfClass.Clone(!hasIgnoreThrowExpAttr));
}
}
if (metadata.IsValid)
{
_metadata.Add(FindUsings(syntax, metadata));
}
}
FindInterceptorMetadata
方法用於查找在類或方法、屬性上定義的 InterceptAttribute
特性,並記錄下攔截器的類型。如下:
/// <summary>
/// 獲取攔截器的元數據。
/// </summary>
/// <param name="symbol"></param>
/// <returns></returns>
private InterceptorMetadata? FindInterceptorMetadata(ISymbol symbol)
{
var types = new List<ITypeSymbol>();
foreach (AttributeData classAttr in symbol.GetAttributes().Where(s => s.AttributeClass!.ToDisplayString() == InterceptorAttributeName))
{
var interceptorType = GetInterceptorType(classAttr.ConstructorArguments[0].Value);
if (interceptorType != null)
{
types.Add(interceptorType);
}
}
if (!types.Any())
{
return null;
}
var hasIgnoreThrowExpAttr = HasIgnoreThrowExceptionAttribute(symbol);
return new InterceptorMetadata(types, !hasIgnoreThrowExpAttr);
}
在 ClassMetadata
里,定義了可被攔截的方法、屬性,以及構造器、引用的命名空間等信息,如下:
/// <summary>
/// 類的元數據。
/// </summary>
public class ClassMetadata
{
/// <summary>
/// 初始化 <see cref="ClassMetadata"/> 類的新實例。
/// </summary>
/// <param name="type">類型符號。</param>
public ClassMetadata(ITypeSymbol type)
{
Type = type;
}
/// <summary>
/// 獲取類型符號。
/// </summary>
public ITypeSymbol Type { get; }
/// <summary>
/// 獲取命名空間。
/// </summary>
public string Namespace => Type.ContainingNamespace.ToDisplayString();
/// <summary>
/// 獲取類型的全名。
/// </summary>
public string TypeFullName => Type.ToDisplayString();
/// <summary>
/// 獲取代理類的名稱。
/// </summary>
public string ProxyTypeName => $"{Type.Name}_proxy_";
/// <summary>
/// 獲取代理類的全名。
/// </summary>
public string ProxyTypeFullName => $"{Namespace}.{ProxyTypeName}";
/// <summary>
/// 獲取源代碼名稱。
/// </summary>
public string SourceCodeName => Type.ToDisplayString().Replace(".", "_") + ".cs";
/// <summary>
/// 獲取構造函數列表。
/// </summary>
public List<IMethodSymbol> Constructors { get; } = new();
/// <summary>
/// 獲取可攔截的方法。
/// </summary>
public Dictionary<IMethodSymbol, InterceptorMetadata> Methods { get; } = new();
/// <summary>
/// 獲取可攔截的屬性。
/// </summary>
public Dictionary<IPropertySymbol, InterceptorMetadata> Properties { get; } = new();
/// <summary>
/// 獲取引用的命名空間列表。
/// </summary>
public List<string> Usings { get; } = new();
/// <summary>
/// 添加可攔截的成員。
/// </summary>
/// <param name="symbol"></param>
/// <param name="metadata"></param>
public void AddMember(ISymbol symbol, InterceptorMetadata metadata)
{
if (symbol is IMethodSymbol method)
{
Methods.Add(method, metadata);
}
else if (symbol is IPropertySymbol property && property.Parameters.Count() == 0) //忽略索引器
{
Properties.Add(property, metadata);
}
}
/// <summary>
/// 添加構造方法。
/// </summary>
/// <param name="symbol"></param>
public void AddConstructor(IMethodSymbol symbol)
{
Constructors.Add(symbol);
}
/// <summary>
/// 添加引用的命名空間列表。
/// </summary>
/// <param name="usings"></param>
public void AddUsings(IEnumerable<string> usings)
{
Usings.AddRange(usings);
}
/// <summary>
/// 若有可攔截的方法或屬性,則此元數據有效。
/// </summary>
public bool IsValid => Methods.Any() || Properties.Any();
}
接下來,在 DynamicProxyGenerator
類的 Execute
方法里,獲取到以上記錄到的元數據,分別創建 DynamicProxyClassBuilder
對象來生成代碼,如下:
void ISourceGenerator.Execute(GeneratorExecutionContext context)
{
var mappers = new Dictionary<string, string>();
if (context.SyntaxContextReceiver is DynamicProxySyntaxReceiver receiver)
{
var metadatas = receiver.GetMetadatas();
metadatas.ForEach(s =>
{
context.AddSource(s.SourceCodeName, new DynamicProxyClassBuilder(s).BuildSource());
mappers.Add(s.TypeFullName, s.ProxyTypeFullName);
});
//代碼生成完畢後,還需要在部署器中,將父類和代理類添加到 Container 容器中
if (mappers.Count > 0)
{
context.AddSource("DynamicProxyServicesDeployer.cs", BuildDeploySourceCode(mappers));
}
}
}
DynamicProxyClassBuilder
類的核心的實現,在這裡,將對構造函數進行重載,對所有的方法、屬性進行切麵代碼的生成。由於篇幅有限,這裡只貼上類的構建方法,其他的對照原型進行參悟,也是比較容易理解的。如下:
/// <summary>
/// 生成源代碼。
/// </summary>
/// <returns></returns>
public SourceText BuildSource()
{
var sb = new StringBuilder();
foreach (var u in _metadata.Usings)
{
sb.AppendLine(u.ToString());
}
sb.AppendLine("using System.Reflection;");
sb.AppendLine($@"
namespace {_metadata.Namespace}
{{
public class {_metadata.ProxyTypeName} : {_metadata.TypeFullName}, IDynamicProxyImplemented
{{
private List<System.Reflection.MemberInfo> _initMarks = new ();
{BuildConstructors()}
{BuildInitializeMethod()}
{BuildInterceptMethod()}
{BuildMethods()}
{BuildProperties()}
}}
}}");
return SourceText.From(sb.ToString(), Encoding.UTF8);
}
那麼同樣的,項目編譯後,會生成一個 __DynamicProxyServicesDeployer
的部署器,目的是往 Container
里添加代理映射。如下:
[assembly: ServicesDeployAttribute(typeof(__DynamicProxyNs.__DynamicProxyServicesDeployer))]
internal class __DynamicProxyServicesDeployer : IServicesDeployer
{
void IServicesDeployer.Configure(IServiceCollection services)
{
Container.TryAdd(typeof(DependencyInjectionTests.TestDynamicProxyClass), typeof(TestDynamicProxyClass_proxy_));
Container.TryAdd(typeof(DynamicProxyTests.TestProxy), typeof(TestProxy_proxy_));
Container.TryAdd(typeof(ObjectActivatorTests.TestServiceProxy2), typeof(TestServiceProxy2_proxy_));
}
}
單元測試就不再贅述了,可以去這裡查看 動態代理單元測試 。
最後,奉上 Fireasy 3
的開源地址:https://gitee.com/faib920/fireasy3 ,歡迎大家前來捧場。
本文相關代碼請參考:
https://gitee.com/faib920/fireasy3/src/libraries/Fireasy.Common/DynamicProxy
https://gitee.com/faib920/fireasy3/src/libraries/Fireasy.Common.Analyzers/DynamicProxy
https://gitee.com/faib920/fireasy3/tests/Fireasy.Common.Tests/DynamicProxyTests.cs
更多內容請移步官網 http://www.fireasy.cn 。
作者:fireasy出處:http://fireasy.cnblogs.com
官網:http://www.fireasy.cn
版權聲明:本文的版權歸作者與博客園共有。轉載時須註明本文的詳細鏈接,否則作者將保留追究其法律責任。