Spring 做爲 Java 中最流行的框架,主要歸功於其提供的 IOC 和 AOP 功能。本文將討論 Spring AOP 的實現。第一節將介紹 AOP 的相關概念,若熟悉可跳過,第二節中結合源碼介紹 Spring 是如何實現 AOP 的各概念。java
進行織入操做的程序執行點。正則表達式
常見類型:spring
方法調用(Method Call):某個方法被調用的時點。數組
方法調用執行(Method Call Execution):某個方法內部開始執行的時點。app
方法調用是在調用對象上的執行點,方法調用執行是在被調用對象的方法開始執行點。框架
構造方法調用(Constructor Call):對某個對象調用其構造方法的時點。ide
構造方法執行(Constructor Call Execution):某個對象構造方法內部開始執行的時點。模塊化
字段設置(Field Set):某個字段經過 setter 方法被設置或直接被設置的時點。函數
字段獲取(Field Get):某個字段經過 getter 方法被訪問或直接被訪問的時點。學習
異常處理執行(Exception Handler Execution):某些類型異常拋出後,異常處理邏輯執行的時點。
類初始化(Class Initialization):類中某些靜態類型或靜態塊的初始化時點。
Jointpoint 的表述方式。
常見表述方式:
單一橫切關注點邏輯的載體,織入到 Joinpoint 的橫切邏輯。
具體形式:
對橫切關注點邏輯進行模塊化封裝的 AOP 概念實體,包含多個 Pointcut 和相關 Advice 的定義。
織入:將 Aspect 模塊化的橫切關注點集成到 OOP 系統中。
織入器:用於完成織入操做。
在織入過程當中被織入橫切邏輯的對象。
將上述 6 個概念放在一塊,以下圖所示:
在瞭解 AOP 的各類概念後,下面將介紹 Spring 中 AOP 概念的具體實現。
前文提到 AOP 的 Joinpoint 有多種類型,方法調用、方法執行、字段設置、字段獲取等。而在 Spring AOP 中,僅支持方法執行類型的 Joinpoint,但這樣已經能知足 80% 的開發須要,若是有特殊需求,可求助其餘 AOP 產品,如 AspectJ。因爲 Joinpoint 涉及運行時的過程,至關於組裝好全部部件讓 AOP 跑起來的最後一步。因此將介紹完其餘概念實現後,最後介紹 Joinpoint 的實現。
因爲 Spring AOP 僅支持方法執行類別的 Joinpoint,所以 Pointcut 須要定義被織入的方法,又由於 Java 中方法封裝在類中,因此 Pointcut 須要定義被織入的類和方法,下面看其實現。
Spring 用 org.springframework.aop.Pointcut
接口定義 Pointcut 的頂層抽象。
public interface Pointcut {
// ClassFilter用於匹配被織入的類
ClassFilter getClassFilter();
// MethodMatcher用於匹配被織入的方法
MethodMatcher getMethodMatcher();
// TruePoincut的單例對象,默認匹配全部類和方法
Pointcut TRUE = TruePointcut.INSTANCE;
}
複製代碼
咱們能夠看出,Pointcut
經過 ClassFilter
和 MethodMatcher
的組合來定義相應的 Joinpoint。Pointcut
將類和方法拆開來定義,是爲了可以重用。例若有兩個 Joinpoint,分別是 A 類的 fun()
方法和 B 類的 fun()
方法,兩個方法簽名相同,則只需一個 fun()
方法的 MethodMatcher
對象,達到了重用的目的,ClassFilter
同理。
下面瞭解下 ClassFilter
和 MethodMatcher
如何進行匹配。
ClassFilter
使用**matches
方法**匹配被織入的類,定義以下:
public interface ClassFilter {
// 匹配被織入的類,匹配成功返回true,失敗返回false
boolean matches(Class<?> clazz);
// TrueClassFilter的單例對象,默認匹配全部類
ClassFilter TRUE = TrueClassFilter.INSTANCE;
}
複製代碼
MethodMatcher
也是使用 matches
方法 匹配被織入的方法,定義以下:
public interface MethodMatcher {
// 匹配被織入的方法,匹配成功返回true,失敗返回false
// 不考慮具體方法參數
boolean matches(Method method, Class<?> targetClass);
// 匹配被織入的方法,匹配成功返回true,失敗返回false
// 考慮具體方法參數,對參數進行匹配檢查
boolean matches(Method method, Class<?> targetClass, Object... args);
// 一個標誌方法
// false表示不考慮參數,使用第一個matches方法匹配
// true表示考慮參數,使用第二個matches方法匹配
boolean isRuntime();
// TrueMethodMatcher的單例對象,默認匹配全部方法
MethodMatcher TRUE = TrueMethodMatcher.INSTANCE;
}
複製代碼
看到 matches
方法的聲明,你是否會以爲有點奇怪,在 ClassFilter
中不是已經對類進行匹配了嗎,那爲何在 MethodMatcher
的 matches
方法中還有一個 Class<?> targetClass
參數。請注意,這裏的 Class<?>
類型參數將不會進行匹配,而僅是爲了找到具體的方法。例如:
public boolean matches(Method method, Class<?> targetClass) {
Method targetMethod = AopUtils.getMostSpecificMethod(method, targetClass);
...
}
複製代碼
在MethodMatcher
相比ClassFilter
特殊在有兩個 matches
方法。將根據 isRuntime()
的返回結果決定調用哪一個。而MethodMatcher
因isRuntime()
分爲兩個抽象類 StaticMethodMatcher
(返回false,不考慮參數)和 DynamicMethodMatcher
(返回true,考慮參數)。
Pointcut
也因 MethodMathcer
可分爲 StaticMethodMatcherPointcut
和 DynamicMethodMatcherPointcut
,相關類圖以下所示:
DynamicMethodMatcherPointcut
本文將不介紹,主要介紹下類圖中列出的三個實現類。
(1)NameMatchMethodPointcut
經過指定方法名稱,而後與方法的名稱直接進行匹配,還支持 「*」 通配符。
public class NameMatchMethodPointcut extends StaticMethodMatcherPointcut implements Serializable {
// 方法名稱
private List<String> mappedNames = new ArrayList<>();
// 設置方法名稱
public void setMappedNames(String... mappedNames) {
this.mappedNames = new ArrayList<>(Arrays.asList(mappedNames));
}
@Override
public boolean matches(Method method, Class<?> targetClass) {
for (String mappedName : this.mappedNames) {
// 根據方法名匹配,isMatch提供「*」通配符支持
if (mappedName.equals(method.getName()) || isMatch(method.getName(), mappedName)) {
return true;
}
}
return false;
}
// ...
}
複製代碼
(2)JdkRegexpMethodPointcut
內部有一個 Pattern 數組,經過指定正則表達式,而後和方法名稱進行匹配。
(3)AnnotationMatchingPointcut
根據目標對象是否存在指定類型的註解進行匹配。
Advice 爲橫切邏輯的載體,Spring AOP 中關於 Advice 的接口類圖以下所示:
(1)MethodBeforeAdvice
橫切邏輯將在 Joinpoint 方法以前執行。可用於進行資源初始化或準備性工做。
public interface MethodBeforeAdvice extends BeforeAdvice {
void before(Method method, Object[] args, @Nullable Object target) throws Throwable;
}
複製代碼
下面來實現一個 MethodBeforeAdvice
,看下其效果。
public class PrepareResourceBeforeAdvice implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("準備資源");
}
}
複製代碼
定義一個 ITask
接口:
public interface ITask {
void execute();
}
複製代碼
ITask
的實現類 MockTask
:
public class MockTask implements ITask {
@Override
public void execute() {
System.out.println("開始執行任務");
System.out.println("任務完成");
}
}
複製代碼
Main 方法以下,ProxyFactory
、Advisor
在後續會進行介紹,先簡單瞭解下,經過ProxyFactory
拿到代理類,Advisor
用於封裝 Pointcut
和 Advice
。
public class Main {
public static void main(String[] args) {
MockTask task = new MockTask();
ProxyFactory weaver = new ProxyFactory(task);
weaver.setInterfaces(new Class[]{ITask.class});
// 內含一個NameMatchMethodPointcut
NameMatchMethodPointcutAdvisor advisor = new NameMatchMethodPointcutAdvisor();
// 指定NameMatchMethodPointcut的方法名
advisor.setMappedName("execute");
// 指定Advice
advisor.setAdvice(new PrepareResourceBeforeAdvice());
weaver.addAdvisor(advisor);
ITask proxyObject = (ITask) weaver.getProxy();
proxyObject.execute();
}
}
/** output: 準備資源 開始執行任務 任務完成 **/
複製代碼
能夠看出在執行代理對象 proxyObject
的 execute
方法時,先執行了 PrepareResourceBeforeAdvice
中的 before
方法。
(2)ThrowsAdvice
橫切邏輯將在 Joinpoint 方法拋出異常時執行。可用於進行異常監控工做。
ThrowsAdvice 接口未定義任何方法,但約定在實現該接口時,定義的方法需符合以下規則:
void afterThrowing([Method, args, target], ThrowableSubclass) 複製代碼
前三個參數爲 Joinpoint 的相關信息,可省略。ThrowableSubclass
指定須要攔截的異常類型。
例如可定義多個 afterThrowing
方法捕獲異常:
public class ExceptionMonitorThrowsAdvice implements ThrowsAdvice {
public void afterThrowing(Throwable t) {
System.out.println("發生【普通異常】");
}
public void afterThrowing(RuntimeException e) {
System.out.println("發生【運行時異常】");
}
public void afterThrowing(Method m, Object[] args, Object target, ApplicationException e) {
System.out.println(target.getClass() + m.getName() + "發生【應用異常】");
}
}
複製代碼
修改下 MockTask
的內容:
public class MockTask implements ITask {
@Override
public void execute() {
System.out.println("開始執行任務");
// 拋出一個自定義的應用異常
throw new ApplicationException();
// System.out.println("任務完成");
}
}
複製代碼
修改下 Main
的內容:
public class Main {
public static void main(String[] args) {
MockTask task = new MockTask();
ProxyFactory weaver = new ProxyFactory(task);
weaver.setInterfaces(new Class[]{ITask.class});
NameMatchMethodPointcutAdvisor advisor = new NameMatchMethodPointcutAdvisor();
advisor.setMappedName("execute");
// 指定異常監控Advice
advisor.setAdvice(new ExceptionMonitorThrowsAdvice());
weaver.addAdvisor(advisor);
ITask proxyObject = (ITask) weaver.getProxy();
proxyObject.execute();
}
}
/** output: 開始執行任務 class com.chaycao.spring.aop.MockTaskexecute發生【應用異常】 **/
複製代碼
當拋出 ApplicationException
時,被相應的 afterThrowing
方法捕獲到。
(3)AfterReturningAdvice
橫切邏輯將在 Joinpoint 方法正常返回時執行。可用於處理資源清理工做。
public interface AfterReturningAdvice extends AfterAdvice {
void afterReturning(@Nullable Object returnValue, Method method, Object[] args, @Nullable Object target) throws Throwable;
}
複製代碼
實現一個資源清理的 Advice :
public class ResourceCleanAfterReturningAdvice implements AfterReturningAdvice {
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("資源清理");
}
}
複製代碼
修改 MockTask
爲正常執行成功, 修改 Main
方法爲指定 ResourceCLeanAfterReturningAdvice
,效果以下:
/** output: 開始執行任務 任務完成 資源清理 **/
複製代碼
(4)MethodInterceptor
至關於 Around Advice,功能十分強大,可在 Joinpoint 方法先後執行,甚至修改返回值。其定義以下:
public interface MethodInterceptor extends Interceptor {
Object invoke(MethodInvocation invocation) throws Throwable;
}
複製代碼
MethodInvocation
是對 Method
的封裝,經過 proceed()
對方法進行調用。下面舉個例子:
public class AroundMethodInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("準備資源");
try {
return invocation.proceed();
} catch (Exception e) {
System.out.println("監控異常");
return null;
} finally {
System.out.println("資源清理");
}
}
}
複製代碼
上面實現的 invoke 方法,一會兒把前面說的三種功能都實現了。
以上 4 種 Advice 會在目標對象類的全部實例上生效,被稱爲 per-class 類型的 Advice。還有一種 per-instance 類型的 Advice,可爲實例添加新的屬性或行爲,也就是第一節提到的 Introduction。
(5)Introduction
Spring 爲目標對象添加新的屬性或行爲,須要聲明接口和其實現類,而後經過攔截器將接口的定義和實現類的實現織入到目標對象中。咱們認識下 DelegatingIntroductionInterceptor
,其做爲攔截器,當調用新行爲時,會委派(delegate)給實現類來完成。
例如,想在原 MockTask
上進行增強,但不修改類的聲明,可聲明一個新的接口 IReinfore
:
public interface IReinforce {
String name = "加強器";
void fun();
}
複製代碼
再聲明一個接口的實現類:
public class ReinforeImpl implements IReinforce {
@Override
public void fun() {
System.out.println("我變強了,能執行fun方法了");
}
}
複製代碼
修改下 Main 方法:
public class Main {
public static void main(String[] args) {
MockTask task = new MockTask();
ProxyFactory weaver = new ProxyFactory(task);
weaver.setInterfaces(new Class[]{ITask.class});
// 爲攔截器指定須要委託的實現類的實例
DelegatingIntroductionInterceptor delegatingIntroductionInterceptor =
new DelegatingIntroductionInterceptor(new ReinforeImpl());
weaver.addAdvice(delegatingIntroductionInterceptor);
ITask proxyObject = (ITask) weaver.getProxy();
proxyObject.execute();
// 使用IReinfore接口調用新的屬性和行爲
IReinforce reinforeProxyObject = (IReinforce) weaver.getProxy();
System.out.println("經過使用" + reinforeProxyObject.name);
reinforeProxyObject.fun();
}
}
/** output: 開始執行任務 任務完成 經過使用加強器 我變強了,能執行fun方法了 **/
複製代碼
代理對象 proxyObject
便經過攔截器,可使用 ReinforeImpl
實現類的方法。
Spring 中用 Advisor
表示 Aspect,不一樣之處在於 Advisor
一般只持有一個 Pointcut
和一個 Advice
。Advisor
根據 Advice
分爲 PointcutAdvisor
和 IntroductionAdvisor
。
經常使用的 PointcutAdvisor
實現類有:
(1) DefaultPointcutAdvisor
最通用的實現類,能夠指定任意類型的 Pointcut
和除了 Introduction
外的任意類型 Advice
。
Pointcut pointcut = ...; // 任意類型的Pointcut
Advice advice = ...; // 除了Introduction外的任意類型Advice
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor();
advisor.setPointcut(pointcut);
advisor.setAdvice(advice);
複製代碼
(2)NameMatchMethodPointcutAdvisor
在演示 Advice 的代碼中,已經有簡單介紹過,內部有一個 NameMatchMethodPointcut
的實例,可持有除 Introduction
外的任意類型 Advice
。
Advice advice = ...; // 除了Introduction外的任意類型Advice
NameMatchMethodPointcutAdvisor advisor = new NameMatchMethodPointcutAdvisor();
advisor.setMappedName("execute");
advisor.setAdvice(advice);
複製代碼
(3)RegexpMethodPointcutAdvisor
內部有一個 RegexpMethodPointcut
的實例。
只能支持類級別的攔截,和 Introduction
類型的 Advice
。實現類有 DefaultIntroductionAdvisor
。
DelegatingIntroductionInterceptor introductionInterceptor =
new DelegatingIntroductionInterceptor(new ReinforeImpl());
DefaultIntroductionAdvisor advisor = new DefaultIntroductionAdvisor(introductionInterceptor, IReinforce.class);
複製代碼
在演示 Advice 的代碼中,咱們使用 ProxyFactory
做爲織入器
MockTask task = new MockTask();
// 織入器
ProxyFactory weaver = new ProxyFactory(task);
weaver.setInterfaces(new Class[]{ITask.class});
NameMatchMethodPointcutAdvisor advisor = new NameMatchMethodPointcutAdvisor();
advisor.setMappedName("execute");
advisor.setAdvice(new PrepareResourceBeforeAdvice());
weaver.addAdvisor(advisor);
// 織入,返回代理對象
ITask proxyObject = (ITask) weaver.getProxy();
proxyObject.execute();
複製代碼
ProxyFactory
生成代理對象方式有:
ProxyFactory
生成的方式,即便實現了接口,也能使用CGLIB。在以前的演示代碼中,咱們沒有啓動 Spring 容器,也就是沒有使用 Spring IOC 功能,而是獨立使用了 Spring AOP。那麼 Spring AOP 是如何與 Spring IOC 進行整合的?是採用了 Spring 整合最經常使用的方法 —— FactoryBean
。
ProxyFactoryBean
繼承了 ProxyFactory
的父類 ProxyCreatorSupport
,具備了建立代理類的能力,同時實現了 FactoryBean
接口,當經過 getObject
方法得到 Bean 時,將獲得代理類。
在以前的演示代碼中,咱們直接爲 ProxyFactory
指定一個對象爲 Target。在 ProxyFactoryBean
中不只能使用這種方式,還能夠經過 TargetSource
的形式指定。
TargetSource
至關於爲對象進行了一層封裝,ProxyFactoryBean
將經過 TargetSource
的 getTarget
方法來得到目標對象。因而,咱們能夠經過 getTarget
方法來控制得到的目標對象。TargetSource
的幾種實現類有:
(1)SingletonTargetSource
很簡單,內部只持有一個目標對象,直接返回。和咱們直接指定對象的效果是同樣的。
(2)PrototypeTargetSource
每次將返回一個新的目標對象實例。
(3)HotSwappableTartgetSource
運行時,根據特定條件,動態替換目標對象類的具體實現。例如當一個數據源掛了,能夠切換至另一個。
(4)CommonsPool2TargetSource
返回有限數目的目標對象實例,相似一個對象池。
(5)ThreadLocalTargetSource
爲不一樣線程調用提供不一樣目標對象
終於到了最後的 Joinpoint,咱們經過下面的示例來理解 Joinpoint 的工做機制。
MockTask task = new MockTask();
ProxyFactory weaver = new ProxyFactory(task);
weaver.setInterfaces(new Class[]{ITask.class});
PrepareResourceBeforeAdvice beforeAdvice = new PrepareResourceBeforeAdvice();
ResourceCleanAfterReturningAdvice afterAdvice = new ResourceCleanAfterReturningAdvice();
weaver.addAdvice(beforeAdvice);
weaver.addAdvice(afterAdvice);
ITask proxyObject = (ITask) weaver.getProxy();
proxyObject.execute();
/** output 準備資源 開始執行任務 任務完成 資源清理 **/
複製代碼
咱們知道 getProxy
會經過動態代理生成一個 ITask
的接口類,那麼 execute
方法的內部是如何先執行了 beforeAdvice
的 before
方法,接着執行 task
的 execute
方法,再執行 afterAdvice
的 after
方法呢?
答案就在生成的代理類中。在動態代理中,代理類方法調用的邏輯由 InvocationHandler
實例的 invoke
方法決定,那答案進一步鎖定在 invoke
方法。
在本示例中,ProxyFactory.getProxy
會調用 JdkDynamicAopProxy.getProxy
獲取代理類。
// JdkDynamicAopProxy
public Object getProxy(@Nullable ClassLoader classLoader) {
if (logger.isTraceEnabled()) {
logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource());
}
Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
}
複製代碼
在 getProxy
中爲 newProxyInstance
的 InvocationHandler
參數傳入 this
,即 JdkDynamicAopProxy
就是一個 InvocationHandler
的實現,其 invoke
方法以下:
// JdkDynamicAopProxy
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 經過advised(建立對象時初始化)得到指定的advice
// 會將advice用相應的MethodInterceptor封裝下
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
if (chain.isEmpty()) {
Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
}
else {
// 建立一個MethodInvocation
MethodInvocation invocation =
new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
// 調用procced,開始進入攔截鏈(執行目標對象方法和MethodInterceptor的advice)
retVal = invocation.proceed();
}
return retVal;
}
複製代碼
首先得到指定的 advice,這裏包含 beforeAdvice
和 afterAdvice
實例,但會用 MethodInterceptor
封裝一層,爲了後面的攔截鏈。
再建立一個 RelectiveMethodInvocation
對象,最後經過 proceed
進入攔截鏈。
RelectiveMethodInvocation
就是 Spring AOP 中 Joinpoint 的一個實現,其類圖以下:
首先看下 RelectiveMethodInvocation
的構造函數:
protected ReflectiveMethodInvocation( Object proxy, @Nullable Object target, Method method, @Nullable Object[] arguments, @Nullable Class<?> targetClass, List<Object> interceptorsAndDynamicMethodMatchers) {
this.proxy = proxy;
this.target = target;
this.targetClass = targetClass;
this.method = BridgeMethodResolver.findBridgedMethod(method);
this.arguments = AopProxyUtils.adaptArgumentsIfNecessary(method, arguments);
this.interceptorsAndDynamicMethodMatchers = interceptorsAndDynamicMethodMatchers;
}
複製代碼
作了些相關屬性的賦值,而後看向 proceed
方法,如何調用目標對象和攔截器。
public Object proceed() throws Throwable {
// currentInterceptorIndex從-1開始
// 當達到已調用了全部的攔截器後,經過invokeJoinpoint調用目標對象的方法
if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
return invokeJoinpoint();
}
// 得到攔截器,調用其invoke方法
// currentInterceptorIndex加1
Object interceptorOrInterceptionAdvice =
this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
}
複製代碼
currentInterceptorIndex
從 -1 開始,interceptorsAndDynamicMethodMatchers
裏有兩個攔截器,再因爲減 1,全部調用目標對象方法的條件是currentInterceptorIndex
等於 1。
首先因爲 -1 != 1
,會得到包含了 beforeAdvice
的 MethodBeforeAdviceInterceptor
實例, currentInterceptorIndex
加 1 變爲 0。調用其 invoke
方法,因爲是 Before-Advice,因此先執行 beforeAdvice
的 before
方法,而後調用 proceed
進入攔截鏈的下一環。
// MethodBeforeAdviceInterceptor
public Object invoke(MethodInvocation mi) throws Throwable {
this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis());
return mi.proceed();
}
複製代碼
又回到了 proceed
方法,0 != 1
,再次得到 advice,此次得到的是包含 afterAdvice
的 AfterReturningAdviceInterceptor
實例, currentInterceptorIndex
加 1 變爲 1。調用其 invoke
方法,因爲是 After-Returning-Adivce,因此會先執行 proceed
進入攔截鏈的下一環。
// AfterReturningAdviceInterceptor
public Object invoke(MethodInvocation mi) throws Throwable {
Object retVal = mi.proceed();
this.advice.afterReturning(retVal, mi.getMethod(), mi.getArguments(), mi.getThis());
return retVal;
}
複製代碼
再次來到 proceed
方法,1 == 1
,已調用完全部的攔截器,將執行目標對象的方法。 而後 return 返回,回到 invoke
中,調用 afterAdvice
的 afterReturning
。
因此在 Joinpoint 的實現中,經過 MethodInterceptor
完成了 目標對象方法和 Advice 的前後執行。
在瞭解了 Spring AOP 的實現後,筆者對 AOP 的概念更加清晰了。在學習過程當中最令筆者感興趣的是 Joinpoint 的攔截鏈,一開始不知道是怎麼實現的,以爲很神奇 😲 。最後學完了,總結下,好像也很簡單,經過攔截器的 invoke
方法和MethodInvocation.proceed
方法(進入下一個攔截器)的相互調用。好像就這麼回事。😛