目錄html
1.Aop介紹java
2.Aop的基本概念git
3.Aop的織入方式web
4.Aop之靜態織入編程
5.Aop之動態織入跨域
a.使用.net真實代理實現織入緩存
b.使用Unity框架的攔截器實現織入app
Aop介紹
咱們先看一下wiki百科的介紹
Traditional software development focuses on decomposing systems into units of primary functionality, while recognizing that there are other issues of concern that do not fit well into the primary decomposition. The traditional development process leaves it to the programmers to code modules corresponding to the primary functionality and to make sure that all other issues of concern are addressed in the code wherever appropriate. Programmers need to keep in mind all the things that need to be done, how to deal with each issue, the problems associated with the possible interactions, and the execution of the right behavior at the right time. These concerns span multiple primary functional units within the application, and often result in serious problems faced during application development and maintenance. The distribution of the code for realizing a concern becomes especially critical as the requirements for that concern evolve – a system maintainer must find and correctly update a variety of situations.框架
Aspect-oriented software development focuses on the identification, specification and representation of cross-cutting concerns and their modularization into separate functional units as well as their automated composition into a working system.分佈式
傳統的軟件開發關注將系統分解成爲一個主要的功能單元,然而卻發現一些問題並不適合分解方式。這種傳統的開發過程讓編碼人員合做編寫主要的功能模塊,以此保證那些主要的關注點可以正確的被編碼。編碼人員須要記住全部須要被完成的事情,如何處理每一個問題,問題可能的關聯關係,以此肯定代碼在正確的時候以正確的方式被執行。這些關注點在應用裏面跨域了多個主要供單元,這常常在開發和維護時引起一些嚴重的問題。這些分佈式代碼致使的問題變得愈來愈迫切得須要獲得解決-一個系統維護人員必須解決這種問題。
面向切面軟件開發須要關注這些識別的,詳細的,具備表明性的切面問題,將其模塊化到功能搗衣而且自動將這些代碼組合到一個工做中的系統。
英語比較蹩腳,翻譯比較澀,總結起來的意思就是,Aop是將一些已經識別的切面關注的功能封裝,並能自動將該功能組合到須要的地方。
我對Aop的理解就是,一些被封裝好的,通用的業務單元,真正的編程人員不須要關注的部分,並能動態或靜態的將這部分功能組合到業務中去。舉個簡單的例子,咱們在代碼中,常常要判斷是否用戶登陸,若是未登陸,就須要跳轉到指定的頁面,僞代碼以下:
public string GetNews(){ /*判斷是否登陸,若是已經登陸,則執行後面的業務代碼 若是沒有登陸,則跳轉到登陸頁面*/ //業務代碼
}
咱們能夠來看一下簡單的流程圖
從圖中咱們能夠將代碼中的登陸的判斷業務分解成一個單獨的業務單元,在須要的地方打上一個標籤,告訴系統這裏須要執行,那麼其餘編碼人員就不須要再寫重複相似的代碼了。這就是Aop解決的問題。
在介紹Aop的實現方式前,咱們先了解一下Aop的幾個知識點,這有助於咱們理解Aop的實際技術。
8)weaving(插入):是指應用aspects到一個target對象建立proxy對象的過程:complie time,classload time,runtime
目前在.NET平臺中,支持的織入方式有倆中,一種是靜態織入,即編譯時織入,另一個是動態織入,即運行時織入。倆中方式各有優缺點,使用靜態織入,能夠不破壞代碼結構,這裏的破壞代碼結構是你須要使用多餘配置,寫一些多餘的代碼,或必須依賴某種方式(這裏你們也許也還不太明白,能夠看完後面倆種方式的具體代碼比較,再回頭來看,會比較好理解)。使用動態織入的優勢就是能夠動態調試。倆中織入方式是互補的,即動態織入的優勢也是靜態織入的缺點,同理,靜態織入的優勢亦是動態織入的缺點。你們在作技術選型時能夠根據本身的實際狀況進行選擇。
目前成熟的框架有PostSharp,這個框架是商業框架,意思就是須要付費,這裏就不具體介紹了,須要瞭解的土豪請到官網查看,具體如何使用請查閱http://www.javashuo.com/tag/文檔。
BSF.Aop .Net 免費開源,靜態Aop織入(直接修改IL中間語言)框架,相似PostSharp(收費),實現先後Aop切面和INotifyPropertyChanged注入方式。其原理是在編譯生成IL後,藉助Mono.Cecil的AssemblyDefinition讀取程序集,並檢測須要注入的點,並將指定的代碼注入到程序集中。有想具體深刻研究的同窗,能夠到 BSF.Aop中下載源碼進行研究。遺憾的是這個只實現了倆個切入點,並無在異常時提供切入點。
咱們模擬一個日誌記錄的例子,咱們先建一個項目。
1. 在項目中引用BSF.Aop.dll,Mono.Cecil.dll,Mono.Cecil.Pdb.dll,Microsoft.Build.dll;
2. 添加一個類LogAttribute並繼承Aop.Attributes.Around.AroundAopAttribute(切面);
3. 重寫AroundAopAttribute的Before和After方法,並寫入邏輯代碼;
4. 新建一個測試類LogTest,並添加Execute方法,並在Execute方法上面添加LogAttribute標籤;
5. 咱們在main裏面new一個LogTest對象並調用看看輸出結果;
具體的代碼以下:
public class LogTest { [LogAttribute] public void Execute(int a) { a = a * 100; System.Console.WriteLine("Hello world!" + a); } } public class LogAttribute : AroundAopAttribute { public virtual void Before(AroundInfo info) { System.Console.WriteLine("Log before executed value is" + info.Params["a"]); } public virtual void After(AroundInfo info) { System.Console.WriteLine("Log after executed value is" + info.Params["a"]); } }
static void Main(string[] args) { Aop.AopStartLoader.Start(null); new LogTest().Execute(2); Console.ReadLine(); }
執行代碼輸出:
上例代碼中
使用.NET提供的遠程代理,即RealProxies來實現。
1.先建一個Aop代理類AopClassAttribute繼承於ProxyAttribute,這個標籤會告訴代理,這個類須要被代理建立調用;
/// <summary> /// 標記一個類爲Aop類,表示該類能夠被代理注入 /// </summary> public class AopClassAttribute : ProxyAttribute { public override MarshalByRefObject CreateInstance(Type serverType) { AopProxy realProxy = new AopProxy(serverType); return realProxy.GetTransparentProxy() as MarshalByRefObject; } }
2.定義Aop的屬性,並定義織入點
/// <summary> /// Attribute基類,經過實現該類來實現切面的處理工做 /// </summary> public abstract class AopAttribute : Attribute { /// <summary> /// 調用以前會調用的方法 /// 1.若是不須要修改輸出結果,請返回null /// 2.若是返回值不爲null,則不會再調用原始方法執行,而是直接將返回的參數做爲結果 /// </summary> /// <param name="args">方法的輸入參數列表</param> /// <param name="resultType">方法的返回值類型</param> public abstract object PreCall(object[] args, Type resultType); /// <summary> /// 調用以後會調用的方法 /// </summary> /// <param name="resultValue">方法的返回值</param> /// <param name="args">方法的輸入參數列表</param> public abstract void Called(object resultValue, object[] args); /// <summary> /// 調用出現異常時會調用的方法 /// </summary> /// <param name="e">異常值</param> /// <param name="args">方法的輸入參數列表</param> public abstract void OnException(Exception e, object[] args); }
3.定義代理的邏輯過程,這裏我對returnvalue作了判斷,是爲了實現緩存更新和添加的切面代碼作的,在這裏我實現了三個切入點的調用,具體可看註釋部分
/// <summary> /// 主要代理處理類 /// </summary> internal class AopProxy : RealProxy { public AopProxy(Type serverType) : base(serverType) { } public override IMessage Invoke(IMessage msg) { if (msg is IConstructionCallMessage) return InvokeConstruction(msg); else return InvokeMethod(msg); } private IMessage InvokeMethod(IMessage msg) { IMethodCallMessage callMsg = msg as IMethodCallMessage; IMessage returnMessage; object[] args = callMsg.Args; var returnType = (callMsg.MethodBase as System.Reflection.MethodInfo).ReturnType;//方法返回類型 object returnValue = null;//方法返回值 AopAttribute[] attributes = callMsg.MethodBase.GetCustomAttributes(typeof(AopAttribute), false) as AopAttribute[]; try { if (attributes == null || attributes.Length == 0) return InvokeActualMethod(callMsg); //前切點 foreach (AopAttribute attribute in attributes) returnValue = attribute.PreCall(args, returnType); //若是之前切面屬性都沒有返回值,則調用原始的方法;不然不調用 //主要是作緩存相似的業務 if (returnValue == null) { returnMessage = InvokeActualMethod(callMsg); returnValue = (returnMessage as ReturnMessage).ReturnValue; } else returnMessage = new ReturnMessage(returnValue, args, args.Length, callMsg.LogicalCallContext, callMsg); //後切點 foreach (AopAttribute attribute in attributes) attribute.Called(returnValue,args); } catch (Exception e) {
//異常切入點 foreach (AopAttribute attribute in attributes) attribute.OnException(e, args); returnMessage = new ReturnMessage(e, callMsg); } return returnMessage; } private IMessage InvokeActualMethod(IMessage msg) { IMethodCallMessage callMsg = msg as IMethodCallMessage; object[] args = callMsg.Args; object o = callMsg.MethodBase.Invoke(GetUnwrappedServer(), args); return new ReturnMessage(o, args, args.Length, callMsg.LogicalCallContext, callMsg); } private IMessage InvokeConstruction(IMessage msg) { IConstructionCallMessage constructCallMsg = msg as IConstructionCallMessage; IConstructionReturnMessage constructionReturnMessage = this.InitializeServerObject((IConstructionCallMessage)msg); RealProxy.SetStubData(this, constructionReturnMessage.ReturnValue); return constructionReturnMessage; } }
4.定義上下文邊界對象,想要使用Aop的類須要繼承此類(這個是這種Aop方式破壞性最大的地方,由於須要繼承一個類,而面向對象單繼承的特性致使了業務類不能再繼承其餘的類。能夠想象一下你有一個查詢基類,而後另外一個查詢類想要繼承查詢基類,而又想使用Aop,這時就尷尬了);
/// <summary> /// Aop基類,須要注入的類須要繼承該類 /// 對代碼繼承有要求,後續能夠改進一下 /// 注意,須要記錄的不支持上下文綁定,若是須要記錄,使用代理模式解決 /// </summary> public abstract class BaseAopObject : ContextBoundObject { }
5.定義Advice部分,即實際的業務邏輯,繼承於AopAttribute
public class IncreaseAttribute : AopAttribute { private int Max = 10; public IncreaseAttribute(int max) { Max = max; } public override object PreCall(object[] args, Type resultType) { if (args == null || args.Count() == 0 || !(args[0] is ExampleData)) return null; var data = args[0] as ExampleData; string numString = args[0].ToString(); data.Num = data.Num * 100; Console.WriteLine(data.Num); return null; } public override void Called(object resultValue, object[] args) { if (args == null || args.Count() == 0 || !(args[0] is ExampleData)) return; var data = args[0] as ExampleData; string numString = args[0].ToString(); data.Num = data.Num * 100; Console.WriteLine(data.Num); } public override void OnException(Exception e, object[] args) { } }
public class ExampleData
{
public int Num { get; set; }
}
6.完成了上面的部分,咱們就能夠來使用Aop了,定義一個須要使用Aop的類,繼承於BaseAopObject,並在類上面加上[AopClass],在須要切入的方法上加上剛纔定義的[IncreaseAttribute]
[AopClass] public class Example : BaseAopObject { [IncreaseAttribute(10)] public static void Do(ExampleData data) { Add(data); } [IncreaseAttribute(10)] public static ExampleData Add(ExampleData data) { return new ExampleData { Num = ++data.Num }; } }
能夠看到,使用上面這種織入方式,對代碼的侵入性太大,會限制代碼的可擴展性。因此我比較不建議使用。
另外一種方式是藉助Ioc的代理來作Aop切面注入,這裏咱們以Unity做爲Ioc容器,以以前寫的關於Unity Ioc中的例子來介紹Aop。
1.添加AopAttribute(定義鏈接點),這裏有個循環引用,就是AopHandler和AopAttribute之間,不過並不影響使用,若有須要你們能夠本身解決一下;
/// <summary> /// 標記一個類或方法爲代理,表示該類或方法能夠被代理 /// </summary> [AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = true)] public abstract class AopAttribute : HandlerAttribute { /// <summary> /// 請勿重寫該方法 /// </summary> /// <param name="container"></param> /// <returns></returns> public override ICallHandler CreateHandler(IUnityContainer container) { return new AopHandler(); } /// <summary> /// 調用以前會調用的方法 /// 1.若是不須要修改輸出結果,請返回null,ouputs返回new object[0] /// 2.若是返回值不爲null,則不會再調用原始方法執行,而是直接將返回的參數做爲結果 /// </summary> /// <param name="inputArgs">方法的輸入參數列表</param> /// <param name="outputs">方法中的out值,若是沒有請返回null</param> /// <returns>返回值</returns> public abstract object PreCall(object[] inputArgs, out object[] outputs); /// <summary> /// 調用以後會調用的方法 /// </summary> /// <param name="resultValue">方法的返回值</param> /// <param name="inputArgs">方法的輸入參數列表</param> /// <param name="outputs">方法中的out值,若是沒有則該參數值爲null</param> public abstract void Called(object resultValue, object[] inputArgs, object[] outputs); /// <summary> /// 調用出現異常時會調用的方法 /// </summary> /// <param name="e">異常值</param> /// <param name="inputArgs">方法的輸入參數列表,鍵爲參數名,值爲參數值</param> public abstract void OnException(Exception e, Dictionary<string, object> inputArgs); }
2.添加AopHandler(代理類);
/// <summary> /// 主要代理處理類 /// </summary> internal class AopHandler : ICallHandler { public int Order { get; set; } = 1; public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext) { IMethodReturn returnValue = null; object attrReturnValue = null; object[] outputs = null; Dictionary<string, object> inputs = new Dictionary<string, object>(); //假若有忽略特性,直接忽略,不進行AOP代理 IgnoreAttribute[] ignoreAttributes = input.MethodBase.GetCustomAttributes(typeof(IgnoreAttribute), true) as IgnoreAttribute[]; if (ignoreAttributes != null && ignoreAttributes.Length > 0) return input.CreateMethodReturn(attrReturnValue, outputs); AopAttribute[] attributes = input.MethodBase.GetCustomAttributes(typeof(AopAttribute), true) as AopAttribute[]; try { if (attributes == null || attributes.Length == 0) return getNext()(input, getNext); for (var i = 0; i < input.Arguments.Count; i++) inputs.Add(input.Inputs.ParameterName(i), input.Inputs[i]); foreach (AopAttribute attribute in attributes) attrReturnValue = attribute.PreCall(inputs.Values.ToArray(), out outputs); //若是之前切面屬性都沒有返回值,則調用原始的方法;不然不調用 //主要是作緩存相似的業務 if (attrReturnValue == null) { returnValue = getNext()(input, getNext); outputs = new object[returnValue.Outputs.Count]; for (var i = 0; i < returnValue.Outputs.Count; i++) outputs[i] = returnValue.Outputs[i]; } else returnValue = input.CreateMethodReturn(attrReturnValue, outputs); if (returnValue.Exception != null) throw returnValue.Exception; foreach (AopAttribute attribute in attributes) attribute.Called(returnValue.ReturnValue, inputs.Values.ToArray(), outputs); } catch (Exception e) { foreach (AopAttribute attribute in attributes) attribute.OnException(e, inputs); returnValue = input.CreateExceptionMethodReturn(e); } return returnValue; } }
3..定義一個咱們本身的功能塊(業務邏輯),這裏仍是以日誌爲例;
public class LogAttribute : AopAttribute
{
public override void Called(object resultValue, object[] inputArgs, object[] outputs)
{
Console.WriteLine("Called");
}
public override void OnException(Exception e, Dictionary<string, object> inputArgs)
{
Console.WriteLine("exception:" + e.Message);
}
public override object PreCall(object[] inputArgs, out object[] outputs)
{
Console.WriteLine("PreCall");
outputs = new object[0];
return null;
}
}
5.接下來咱們稍微改造一下咱們的印鈔機;
/// <summary> /// 印鈔機 /// </summary> public class CashMachine { public CashMachine() { } public void Print(ICashTemplate template) { string templateContent = template.GetTemplate("人民幣"); System.Console.WriteLine(templateContent); } } /// <summary> /// 印鈔模塊 /// </summary> public interface ICashTemplate { /// <summary> /// 獲取鈔票模板 /// </summary> /// <returns></returns> [Log] string GetTemplate(string flag); } /// <summary> /// 人民幣鈔票模板 /// </summary> public class CNYCashTemplate : ICashTemplate { public CNYCashTemplate() { } public string GetTemplate(string flag) { return "這是人民幣模板!" + flag + " 這是返回值。"; } } /// <summary> /// 美鈔鈔票模板 /// </summary> public class USDCashTemplate : ICashTemplate { public USDCashTemplate() { } public string GetTemplate(string flag) { throw new Exception("哎呀,美鈔模板有問題呀!"); } }
6.而後咱們在命令行的Main裏改造一下;
static void Main(string[] args) { try { ICashTemplate usdTemplate = new USDCashTemplate(); ICashTemplate rmbTemplate = new CNYCashTemplate(); new CashMachine().Print(rmbTemplate); new CashMachine().Print(usdTemplate); } catch (Exception) { } Console.ReadLine(); }
7.啓動一下看看結果
8.能夠看到,只輸出了GetTemplate方法的輸出,並無輸出日誌,咱們要使用Ioc來註冊對象才能使用,繼續改造Main方法;
static void Main(string[] args) { UnityContainer container = new UnityContainer(); container.AddNewExtension<Interception>().RegisterType<ICashTemplate, CNYCashTemplate>("cny"); container.Configure<Interception>().SetInterceptorFor<ICashTemplate>("cny", new InterfaceInterceptor()); container.AddNewExtension<Interception>().RegisterType<ICashTemplate, USDCashTemplate>("usd"); container.Configure<Interception>().SetInterceptorFor<ICashTemplate>("usd", new InterfaceInterceptor()); try {new CashMachine().Print(container.Resolve<ICashTemplate>("cny")); new CashMachine().Print(container.Resolve<ICashTemplate>("usd")); } catch (Exception) { } Console.ReadLine(); }
9.啓動運行,看一下結果;
能夠看到,三個方法都執行了,而在拋出異常時是不會執行Called的方法的;
10.上面咱們是直接使用了UnityContainer來註冊對象,而沒有使用咱們以前封裝的Ioc,咱們還有更簡單的方式,就是採用配置的方式來註冊對象和攔截器實現Aop。在實際,使用一個單獨的文件來配置ioc會更易於維護。咱們先添加一個unity.config文件;
<?xml version="1.0" encoding="utf-8" ?> <unity xmlns= "http://schemas.microsoft.com/practices/2010/unity "> <sectionExtension type="Microsoft.Practices.Unity.InterceptionExtension.Configuration.InterceptionConfigurationExtension, Unity.Interception.Configuration"/> <!--注入對象--> <typeAliases> <!--表示單例--> <typeAlias alias="singleton" type="Unity.Lifetime.ContainerControlledLifetimeManager,Unity.Abstractions" /> <!--表示每次使用都進行建立--> <typeAlias alias="transient" type="Unity.Lifetime.TransientLifetimeManager,Unity.Abstractions" /> </typeAliases> <container name= "Default"> <extension type="Interception"/> <!--type表示接口 格式爲 帶命名空間的接口,程序集名 mapTo表示須要注入的實體類 name表示注入實體的name--> <register type= "IocWithUnity.ICashTemplate,IocWithUnity" mapTo= "IocWithUnity.CNYCashTemplate,IocWithUnity" name="cny"> <!--定義攔截器--> <interceptor type="InterfaceInterceptor"/> <policyInjection/> <!--定義對象生命週期--> <lifetime type="singleton" /> </register> <!--type表示接口 格式爲 帶命名空間的接口,程序集名 mapTo表示須要注入的實體類 name表示注入實體的name--> <register type= "IocWithUnity.ICashTemplate,IocWithUnity" mapTo= "IocWithUnity.USDCashTemplate,IocWithUnity" name="usd"> <!--定義攔截器--> <interceptor type="InterfaceInterceptor"/> <policyInjection/> <!--定義對象生命週期--> <lifetime type="singleton" /> </register> </container> </unity>
11.再配置app.config(WEB項目應該是web.config);
<?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Unity.Configuration"/> </configSections> <unity configSource="unity.config"/> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" /> </startup> </configuration>
12.將咱們以前寫的IocContainer修改一下讀取配置;
public static class IocContainer { private static IUnityContainer _container = null; static IocContainer() { _container = new UnityContainer(); object unitySection = ConfigurationManager.GetSection("unity"); if (unitySection == null) return; UnityConfigurationSection section = (UnityConfigurationSection)unitySection; section.Configure(_container, "Default"); } /// <summary> /// 註冊一個實例做爲T的類型 /// </summary> /// <typeparam name="T">須要註冊的類型</typeparam> /// <param name="instance">須要註冊的實例</param> public static void Register<T>(T instance) { _container.RegisterInstance<T>(instance); } /// <summary> /// 註冊一個名爲name的T類型的實例 /// </summary> /// <typeparam name="T">須要註冊的類型</typeparam> /// <param name="name">關鍵字名稱</param> /// <param name="instance">實例</param> public static void Register<T>(string name, T instance) { _container.RegisterInstance(name, instance); } /// <summary> /// 將類型TFrom註冊爲類型TTo /// </summary> /// <typeparam name="TFrom"></typeparam> /// <typeparam name="TTo"></typeparam> public static void Register<TFrom, TTo>() where TTo : TFrom { _container.RegisterType<TFrom, TTo>(); } /// <summary> /// 將類型TFrom註冊爲類型TTo /// </summary> /// <typeparam name="TFrom"></typeparam> /// <typeparam name="TTo"></typeparam> /// <typeparam name="lifetime"></typeparam> public static void Register<TFrom, TTo>(LifetimeManager lifetime) where TTo : TFrom { _container.RegisterType<TFrom, TTo>(lifetime); } /// <summary> /// 將類型TFrom註冊名爲name類型TTo /// </summary> /// <typeparam name="TFrom"></typeparam> /// <typeparam name="TTo"></typeparam> public static void Register<TFrom, TTo>(string name) where TTo : TFrom { _container.RegisterType<TFrom, TTo>(name); } /// <summary> /// 經過關鍵字name來獲取一個實例對象 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="name"></param> /// <returns></returns> public static T Resolve<T>(string name) { return _container.Resolve<T>(name); } /// <summary> /// 獲取一個爲T類型的對象 /// </summary> /// <typeparam name="T"></typeparam> /// <returns></returns> public static T Resolve<T>() { return _container.Resolve<T>(); } /// <summary> /// 獲取全部註冊類型爲T的對象實例 /// </summary> /// <typeparam name="T">須要獲取的類型的對象</typeparam> /// <returns></returns> public static IEnumerable<T> ResolveAll<T>() { return _container.ResolveAll<T>(); } }
注意:配置時有一個坑 <section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Unity.Configuration"/> 這句話在5.0版本後dll的名稱改了,之前是Microsoft.Practices.Unity.Configuration,如今是Unity.Configuration,若是你在運行時碰到找不到文件或程序集xxx時,能夠注意看一下你的具體的dll的文件名。包括後面的unity.config裏面的lifetime的配置也是,你們須要注意一下本身的版本,而後找到對應的命名空間和dll文件進行配置。
13.接下來咱們運行一下看看結果如何;
總結:能夠看到,靜態織入方式相對較簡單,對代碼破壞性近乎於0,其原理大體是在編譯前,將須要的代碼添加到咱們添加了Attribute的地方,若是用反編譯工具反編譯生成的dll就能夠看到實際編譯後的代碼。這種織入方式的缺點是不易於調試工做,由於生成的pdb文件與咱們的源代碼文件其實是不同的。而採用真實代理的方式進行織入,這種方式比較原生,但對代碼侵入性較大,並且效率也較低。使用ioc框架的攔截器進行攔截織入的方式,是當下比較好的一種方式,可是也是有一個約束,就是對象必須通過ioc容器來委託建立。基於這些比較,各位看官能夠選擇適合本身的織入方式。
本文原創,若有轉載,請註明出處。