javascript
我居然手写了Spring框架
手寫完了
剛參加工作那會接觸java還是用的struct的時代,后面在SSH火爆時代的時候我轉戰.net,多年之后公司轉java技術棧已經是Spring的天下,源碼嚼了很多遍于是很想嘗試把這套東西用在.net平臺上。社區有個Spring.net項目已經多年不維護了,而且還是xml配置模式非基于注解的,無法與現有的SpringBoot項目同日而語。在SpringBoot項目中的常用的注解和擴展機制我都在這個項目中實現了,可以看下面介紹的已實現的功能一覽!
Annotation是注解的意思,在java項目里面 注解的概念和 csharp里面的 Attribute 的概念是一樣的。
本項目是基于Autofac(巨人的肩膀)的基礎之上構建,選擇用Autofac是它擴展性非常好,在實現Spring的細節上提供了便捷
本項目的所有實現都參考Spring的設計思想,但是并不是純粹的把java的代碼換成csharp,功能上效果是和Spring看齊的,但代碼實現上是自己實現的
本項目的目的
基于參考 Java的 Spring注解方式開發思想,
所有容器的注冊 和 裝配 都是依賴標簽來完成。
這樣一來 一方面很容易分清楚 哪些是DI 哪些非DI, 哪些是攔截器,哪些需要攔截器,輕松實現切面編程,
代碼也好看,吸收java的spring框架的優越的地方,配合.net語法的優越性,編程效率能夠大大提升。
本篇文章主要介紹高階玩法,基礎玩法可以看項目wiki
開源地址:https://github.com/yuzd/Autofac.Annotation
支持的標簽一覽
| AutoConfiguration | 打在class上面 | 自動裝配class里面帶有Bean標簽的方法 |
| Bean | 打在方法上面 | 配合AutoConfiguration標簽使用 |
| Component | 打在class上面 | 自動注冊 |
| Autowired | 打在構造方法的Parameter,類的Property,類的Field | 自動裝配 |
| PropertySource | 打在class上面 | 配合Value標簽使用,設置Value的數據源,支持json,xml,支持資源內嵌 |
| Value | 打在構造方法的Parameter,類的Property,類的Field | 靜態/動態(例如nacos)數據裝配,支持強大的EL表達式 |
| Pointcut | 打在class上面 | 切面配置,一個切面攔截N多個對象,配合Before After AfterReturn AfterThrows Around 實現攔截器鏈 |
| Import | 打在繼承了ImportSelector的class上面 | 擴展注冊Component |
| Order | 打在了class上面,和Compoment一起使用 | 值越小的越先加載 |
| Conditional | 打在class或者方法上面 | 條件加載,自定義實現的 |
| ConditionOnBean | 打在標有Bean注解的方法上面 | 條件加載 |
| ConditionOnMissingBean | 打在標有Bean注解的方法上面 | 條件加載 |
| ConditionOnClass | 打在class或者方法上面 | 條件加載 |
| ConditionOnMissingClass | 打在class或者方法上面 | 條件加載 |
| ConditionOnProperty | 打在class或者方法上面 | 條件加載 |
| ConditionOnProperties | 打在class或者方法上面 | 條件加載 |
| PostConstruct | 打在方法上面 | 當類初始化完成后調用 |
| PreDestory | 打在方法上面 | 當容器Dispose前調用 |
基本使用略過
基本使用可以參考詳細的wiki文檔:
Wiki
下面講講高階玩法
1. 攔截器原理簡單介紹
用了Castle.Core組件 把你想要實現攔截器的目標類生成一個代理類。
然后織入攔截器,有2種方式
類攔截器:class + 方法為virtual的方式
這種方式需要 從容器中是根據一個classType來獲取到目標實例
接口型攔截器:interface + 方法重寫的方式
這種方式需要 從容器中是根據一個interfaceType來獲取到目標實例
攔截器開關
在你想要實現攔截的目標類上打開開關 【[Component(EnableAspect = true)]】,如上面的解釋,打開Aspect開關標識這個class你想要走代理包裝,還可以根據InterceptorType屬性值設定你是哪種方式的攔截器
InterceptorType屬性解釋 Class 使用class的虛方法模式 【默認方式】 Interface 使用接口模式 目的是打個標簽就能夠攔截目標方法
使得我們自定義的方法能夠
在指定的目標方法執行之前先執行(比如參數校驗)
或者在指定的目標方法執行之后執行(比如說檢驗返回值,或其他收尾工作)
或者環繞目標的方法,比如日志or事務:TransactionScope或者記錄方法執行的時間或者日志
攔截器標簽攔截器類型使用說明 AspectArround(抽象標簽類) 環繞攔截 重寫OnInvocation方法 AspectBefore(抽象標簽類) 前置攔截器 重寫Before方法 AspectAfter(抽象標簽類) 后置攔截器(不管目標方法成功失敗都會執行) 重寫After方法 AspectAfterReturn(抽象標簽類) 后置攔截器(只有目標方法成功才會執行) 重寫AfterReturn方法 AspectAfterThrows(抽象標簽類) 錯誤攔截器(只有目標方法失敗才會執行) 重寫AfterThrows方法 每個攔截器方法都有一個
攔截器的方法參數 AspectContext 屬性說明
名稱說明 ComponentContext DI容器,可以從中取得你已注冊的實例 Arguments 目標方法的參數 TargetMethod 目標方法的MethodInfo ReturnValue 目標方法的返回 Method 目標方法的代理方法MethodInfo 前置攔截器 (Before)
首先要自己寫一個類繼承 前置攔截器AspectBefore(抽象標簽類)
實現該抽象類的Before方法
public class TestHelloBefore:AspectBefore{public override Task Before(AspectContext aspectContext){Console.WriteLine("TestHelloBefore");return Task.CompletedTask;}}[Component(EnableAspect = true)]//注意這里需要打開開關 否則無效public class TestHello{[TestHelloBefore]public virtual void Say(){Console.WriteLine("Say");}}
首先要自己寫一個類繼承后置攔截器AspectAfter(抽象標簽類)
實現該抽象類的After方法
public class TestHelloAfter:AspectAfter{//這個 returnValue 如果目標方法正常返回的話 那就是目標方法的返回值// 如果目標方法拋異常的話 那就是異常本身public override Task After(AspectContext aspectContext,object returnValue){Console.WriteLine("TestHelloAfter");return Task.CompletedTask;}}[Component(EnableAspect = true)]public class TestHello{[TestHelloAfter]public virtual void Say(){Console.WriteLine("Say");}}
首先要自己寫一個類繼承攔截器AspectReturn(抽象標簽類)
實現該抽象類的After方法
public class TestHelloAfterReturn:AspectAfterReturn{//result 是目標方法的返回 (如果目標方法是void 則為null)public override Task AfterReturn(AspectContext aspectContext, object result){Console.WriteLine("TestHelloAfterReturn");return Task.CompletedTask;}}[Component(EnableAspect = true)]public class TestHello{[TestHelloAfterReturn]public virtual void Say(){Console.WriteLine("Say");}}
首先要自己寫一個類繼承攔截器AspectReturn(抽象標簽類)
實現該抽象類的After方法
public class TestHelloAfterThrows:AspectAfterThrows{public override Task AfterThrows(AspectContext aspectContext, Exception exception){Console.WriteLine(exception.Message);return Task.CompletedTask;}}[Component(EnableAspect = true)]public class TestHello{[TestHelloAfterThrows]public virtual void Say(){Console.WriteLine("Say");throw new ArgumentException("exception");}}
首先要自己寫一個類繼承攔截器AspectArround(抽象標簽類)
實現該抽象類的OnInvocation方法
public class TestHelloAround:AspectArround{public override async Task OnInvocation(AspectContext aspectContext, AspectDelegate _next){Console.WriteLine("around start");await _next(aspectContext);Console.WriteLine("around end");}}[Component(EnableAspect = true)]public class TestHello{[TestHelloAround]public virtual void Say(){Console.WriteLine("Say");}}
先執行TestHelloAround的OnInvocation方法
然后TestHelloAround的OnInvocation方法里面執行的 await _next(aspectContext); 就會執行被攔截方法TestHello的Say方法;
先執行TestHelloAround,打印 “around start” 然后執行到里面的_next(aspectContext)會觸發下面
執行TestHelloBefore 打印 “TestHelloBefore”
執行目標方法 打印 “Say”
打印 “around end” TestHelloAround運行結束
執行TestHelloAfter 打印 “TestHelloAfter”
因為是目標方法成功執行 TestHelloAfterReturn 打印 “TestHelloAfterReturn”
先執行TestHelloAround,打印 “around start” 然后執行到里面的_next(aspectContext)會觸發下面
執行TestHelloBefore 打印 “TestHelloBefore”
執行目標方法 打印 “Say”
打印 “around end” TestHelloAround運行結束
執行TestHelloAfter 打印 “TestHelloAfter”
因為是目標方法異常 執行 TestHelloAfterThrows 打印異常信息
先執行TestHelloAround2 打印 “TestHelloAround2 start” 然后執行到里面的_next(aspectContext)會觸發下面
執行TestHelloBefore2 打印 “TestHelloBefore2” 然后進入到
執行TestHelloAround1 打印 “TestHelloAround1 start” 然后執行到里面的 _next(aspectContext)會觸發下面
執行TestHelloBefore1 打印 “TestHelloBefore1”
執行目標方法 SayGroup 打印 “SayGroup”
TestHelloAround1運行結束 打印 “TestHelloAround1 end”
執行 TestHelloAfter1 打印 “TestHelloAfter1”
執行 TestHelloAfterReturn1 打印 “TestHelloAfterReturn1”
TestHelloAround2運行結束 打印 “TestHelloAround2 end”
執行 TestHelloAfter2 打印 “TestHelloAfter2”
執行 TestHelloAfterReturn2 打印 “TestHelloAfterReturn2”
執行的順序如下圖
因為一個class上可以打多個Pointcut切面,一個Pointcut切面可以根據name去匹配對應攔截方法
Around切入點 必須要指定?AspectContext類型?和 AspectDelegate類型的2個參數,且返回類型要是Task 否則會報錯
除了Around切入點以外其他的切入點的返回值只能是Task或者Void 否則會報錯
除了Around切入點以外其他的切入點可以指定?AspectContext類型?參數注入進來
After切入點 可以指定Returing參數,可以把目標方法的返回注入進來,如果目標方法拋異常則是異常本身
AfterReturn切入點 可以指定Returing參數,可以把目標方法的返回注入進來
AfterThrows切入點 可以指定 Throwing參數,可以把目標方法拋出的異常注入進來
只要你參數類型是你注冊到DI容器,運行時會自動從DI容器把類型注入進來
可以使用Autowired,Value標簽來修飾參數
Pointcut1Controller.TestSuccess 和 TestThrow 2個方法 會被匹配
Pointcut2Controller.TestThrow 和 TestThrow 2個方法 會被匹配
PostProcessBeforeInitialization
PostProcessAfterInitialization
前置攔截器方法的執行順序為:先執行 TestHelloBefor的Before方法再執行你的Say方法
后置攔截器 (After) 不管目標方法成功還是拋異常都會執行
執行順序為:先執行你的SayAfter方法再執行 TestHelloAfter的After方法
這里要特別注意的是 After 攔截器 是不管你的目標方法(SayAfter是成功還是拋異常)
都被會執行到的
成功返回攔截器 (AfterReturn)只有目標方法成功的時候才會執行
執行順序為:先執行你的Say方法再執行 TestHelloAfterReturn的AfterReturn方法
如果你的Say方法拋出異常那么就不會執行TestHelloAfterReturn的AfterReturn方法
異常攔截器 (AfterThrows)
執行順序為:先執行你的Say方法再執行 TestHelloAfterThrows的AfterThrows方法
如果你的Say方法不拋出異常那么就不會執行 TestHelloAfterThrows的AfterThrows方法
環繞攔截器(Around)
注意:OnInvocation方法除了AspectContext參數以外 還有一個 AspectDelegate _next 參數,
需要在你的Around攔截器方法顯示調用 _next(aspectContext) 方法,否則目標方法不會被調用
方法的執行順序為:
如果Around Befor After AfterReturn AfterThrows 一起用
正常case
[Component(EnableAspect = true)]public class TestHello{[TestHelloAround,TestHelloBefore,TestHelloAfter,TestHelloAfterReturn,TestHelloAfterThrows]public virtual void Say(){Console.WriteLine("Say");}}代碼的執行順序為:
由于是目標方法成功返回 沒有異常,所以不會走進TestHelloAfterThrows
異常case
[Component(EnableAspect = true)]public class TestHello{[TestHelloAround,TestHelloBefore,TestHelloAfter,TestHelloAfterReturn,TestHelloAfterThrows]public virtual void Say(){Console.WriteLine("Say");throw new ArgumentException("exception");}}代碼的執行順序為:
如上述執行順序和spring是一致的
多組的情況
public class TestHelloBefore1:AspectBefore{public override Task Before(AspectContext aspectContext){Console.WriteLine("TestHelloBefore1");return Task.CompletedTask;}}public class TestHelloAfter1:AspectAfter{//這個 returnValue 如果目標方法正常返回的話 那就是目標方法的返回值// 如果目標方法拋異常的話 那就是異常本身public override Task After(AspectContext aspectContext,object returnValue){Console.WriteLine("TestHelloAfter1");return Task.CompletedTask;}}public class TestHelloAfterReturn1:AspectAfterReturn{//result 是目標方法的返回 (如果目標方法是void 則為null)public override Task AfterReturn(AspectContext aspectContext, object result){Console.WriteLine("TestHelloAfterReturn1");return Task.CompletedTask;}}public class TestHelloAround1:AspectArround{public override async Task OnInvocation(AspectContext aspectContext, AspectDelegate _next){Console.WriteLine("TestHelloAround1 start");await _next(aspectContext);Console.WriteLine("TestHelloAround1 end");}}public class TestHelloAfterThrows1:AspectAfterThrows{public override Task AfterThrows(AspectContext aspectContext, Exception exception){Console.WriteLine("TestHelloAfterThrows1");return Task.CompletedTask;}}//public class TestHelloBefore2:AspectBefore{public override Task Before(AspectContext aspectContext){Console.WriteLine("TestHelloBefore2");return Task.CompletedTask;}}public class TestHelloAfter2:AspectAfter{//這個 returnValue 如果目標方法正常返回的話 那就是目標方法的返回值// 如果目標方法拋異常的話 那就是異常本身public override Task After(AspectContext aspectContext,object returnValue){Console.WriteLine("TestHelloAfter2");return Task.CompletedTask;}}public class TestHelloAfterReturn2:AspectAfterReturn{//result 是目標方法的返回 (如果目標方法是void 則為null)public override Task AfterReturn(AspectContext aspectContext, object result){Console.WriteLine("TestHelloAfterReturn2");return Task.CompletedTask;}}public class TestHelloAround2:AspectArround{public override async Task OnInvocation(AspectContext aspectContext, AspectDelegate _next){Console.WriteLine("TestHelloAround2 start");await _next(aspectContext);Console.WriteLine("TestHelloAround2 end");}}public class TestHelloAfterThrows2:AspectAfterThrows{public override Task AfterThrows(AspectContext aspectContext, Exception exception){Console.WriteLine("TestHelloAfterThrows2");return Task.CompletedTask;}}[Component(EnableAspect = true)]public class TestHello{[TestHelloAround1(GroupName = "Aspect1",OrderIndex = 10),TestHelloBefore1(GroupName = "Aspect1",OrderIndex = 10),TestHelloAfter1(GroupName = "Aspect1",OrderIndex = 10),TestHelloAfterReturn1(GroupName = "Aspect1",OrderIndex = 10),TestHelloAfterThrows1(GroupName = "Aspect1",OrderIndex = 10)][TestHelloAround2(GroupName = "Aspect2",OrderIndex = 1),TestHelloBefore2(GroupName = "Aspect2",OrderIndex = 1),TestHelloAfter2(GroupName = "Aspect2",OrderIndex = 1),TestHelloAfterReturn2(GroupName = "Aspect2",OrderIndex = 1),TestHelloAfterThrows2(GroupName = "Aspect2",OrderIndex = 1)]public virtual void SayGroup(){Console.WriteLine("SayGroup");}}如上面的代碼在目標方法上打了2組 那么對應的執行順序是:
2. 面向切面編程
上面介紹了利用Aspect標簽來完成攔截器功能
Aspect是一對一的方式,我想要某個class開啟攔截器功能我需要針對每個class去配置。
比如說 我有2個 controller 每個controller都有2個action方法,
[Component]public class ProductController{public virtual string GetProduct(string productId){return "GetProduct:" + productId;}public virtual string UpdateProduct(string productId){return "UpdateProduct:" + productId;}}[Component]public class UserController{public virtual string GetUser(string userId){return "GetUser:" + userId;}public virtual string DeleteUser(string userId){return "DeleteUser:" + userId;}}如果我需要這2個controller的action方法都在執行方法前打log 在方法執行后打log
按照上一節Aspect的話 我需要每個controller都要配置。如果我有100個controller的話我就需要配置100次,這樣我覺得太麻煩了。所以我參考了Spring的Pointcut切面編程的方式實現了,下面看如何用Pointcut的方式方便的配置一種切面去適用于N個對象。
定義一個切面:創建一個class 上面打上Pointcut的標簽 如下:
Pointcut標簽類有如下屬性:
| Name | 名稱Pointcut切面的名稱(默認為空,和攔截方法進行匹配,參考下面說明) |
| RetType | 匹配目標類的方法的返回類型(默認是%) |
| NameSpace | 匹配目標類的namespace(默認是%) |
| ClassName | 匹配目標類的類名稱(和下面的AttributeType參數二選一必填) |
| AttributeType | 匹配特定的標簽(和上面的ClassName參數二選一必填) |
| MethodName | 匹配目標類的方法名稱(默認是%) |
切面如何匹配
// *Controller 代表匹配 只要是Controller結尾的類都能匹配// Get* 代表上面匹配成功的類下 所以是Get打頭的方法都能匹配[Pointcut(Class = "*Controller",Method = "Get*")]public class LoggerPointCut{} // *Controller 代表匹配 只要是Controller結尾的類都能匹配// Get* 代表上面匹配成功的類下 所以是Get打頭的方法都能匹配[Pointcut(ClassName = "*Controller",MethodName = "Get*")]public class LoggerPointCut{}定義好了一個Pointcut切面后 需要定義這個切面的攔截方法(也叫切入點)
配合Pointcut切面標簽,可以在打了這個標簽的class下定義攔截方法,
在方法上得打上特定的標簽,有如下幾種:
| Before標簽 | 在匹配成功的類的方法執行前執行 |
| After標簽 | 在匹配成功的類的方法執行后執行(不管目標方法成功還是失敗) |
| AfterReturn標簽 | 在匹配成功的類的方法執行后執行(只是目標方法成功) |
| AfterThrows標簽 | 在匹配成功的類的方法執行后執行(只是目標方法拋異常時) |
| Around標簽 | 環繞目標方法,承接了匹配成功的類的方法的執行權 |
以上3種標簽有一個可選的參數:Name (默認為空,可以和Pointcut的Name進行mapping)
切入點標簽所在方法的參數說明:
按照上面的配置
執行順序
單個切面順序如下圖
多個切面執行的順序如下圖
關于順序是和上面用Aspect是一致的,只不過是1:N,1個切面來切N個目標
切面功能與Spring相比缺少了一個靈活的切點表達式,所以功能會弱很多,這塊目前我還沒有很好的設計思路,歡迎來教育!
3. BeanPostProcessor的設計
參考Spring框架,
在類的初始化過程中進行自定義邏輯而設計的BeanPostProcessor,有2個方法:
1. PostProcessBeforeInitialization
該方法在bean實例化完畢(且已經注入完畢),屬性設置或自定義init方法執行之前執行!
2. PostProcessAfterInitialization
該方法在bean實例化完畢(且已經注入完畢),在屬性設置或自定義init方法執行之后
一個使用場景例子:自定義一個注解來封裝自定義邏輯
先定義一個自定義注解
/// <summary> /// 測試自己實現一個自定義註解 /// </summary> [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)] public sealed class Soa : Attribute {/// <summary>/// 構造函數/// </summary>public Soa(Type type){Type = type;}/// <summary>/// 注冊的類型/// </summary>internal Type Type { get; set; } }這個注解的名字叫Soa,然后有一個構造方法,傳參為一個Class Type
下面需要實現一個BeanPostProcessor
[Component] public class SoaProcessor : BeanPostProcessor {//在實例化后且屬性設值之前執行public object PostProcessBeforeInitialization(object bean){Type type = bean.GetType();找到bean下所有的字段var fieldInfos = type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic);foreach (var field in fieldInfos){//看字段上面有沒有打Soa自定義注解var soaAnnotation = field.GetCustomAttribute(typeof(Soa)) as Soa;if (soaAnnotation == null){continue;}//有的話根據注解的參數Type來實例化對象并設值var instance = Activator.CreateInstance(soaAnnotation.Type) as ISoa;if (instance == null){continue;}field.SetValue(bean, instance);}return bean;}//不管返回public object PostProcessAfterInitialization(object bean){return bean;} }好了,實現一個BeanPostProcessor就是寫一個類繼承并實現它的接口即可。
然后打上[Compoment]注冊到容器中即可。
下面測試效果
[Component] public class Test11Models1 {[Soa(typeof(SoaTest1))] private ISoa Soa1;[Soa(typeof(SoaTest2))] private ISoa Soa2;public string getSoa1(){return Soa1.say();}public string getSoa2(){return Soa2.say();} }public interface ISoa {string say(); }public class SoaTest1 : ISoa {public string say(){return nameof(SoaTest1);} }public class SoaTest2 : ISoa {public string say(){return nameof(SoaTest2);} }單元測試一下
[Fact] public void Test1() {var builder = new ContainerBuilder();builder.RegisterSpring(r => r.RegisterAssembly(typeof(TestBeanPostProcessor).Assembly));var container = builder.Build();var isRegisterd = container.TryResolve(out Test11Models1 model1);Assert.True(isRegisterd);Assert.Equal("SoaTest1",model1.getSoa1());Assert.Equal("SoaTest2",model1.getSoa2()); }Test11Models1這個類打了[Compoment]注冊到容器,當從容器獲取它的時候會走到上面的SoaProcessor。然后識別到里面有打了自定義注解[Soa],并根據注冊的參數實例化。
Spring是一個非常龐大的框架,里面包含了非常多的細節,比如處理依賴循環,單例如何Autowired多例,FactoryBean,代理類的生成以及兼容async await,新出的valueTask的方法代理等等,這個項目是我2018年開始寫的,多次重構,每次重構也是反映對spring源碼的理解程度不一樣;這個過程非常有趣(一次次推翻我自以為看了源碼就‘懂了’spring),目前最新版4.0.4 基本上包含了常用的spring功能,還會不斷更新(看我是否越來越‘懂’spring),感興趣可以看看單元測試
我是正東,學的越多不知道也越多。如果決定去深究一個東西, 一定要完全搞懂, 并認真總結一篇博客讓以后能在短時間拾起來 ( 因為不搞懂你很難寫一篇半年后還能理解的博客 )
歡迎白嫖點贊!
總結
以上是生活随笔為你收集整理的我居然手写了Spring框架的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .net core ——微服务内通信Th
- 下一篇: 合肥.NET俱乐部第二期技术沙龙活动预告