Sprig AOP的實現及源碼解析

来源:https://www.cnblogs.com/zhaosq/archive/2018/11/27/10020760.html
-Advertisement-
Play Games

在介紹AOP之前,想必很多人都聽說AOP是基於動態代理和反射來實現的,那麼在看AOP之前,你需要弄懂什麼是動態代理和反射及它們又是如何實現的。 想瞭解JDK的動態代理及反射的實現和源碼分析,請參見下麵三篇文章 JDK的動態代理源碼分析之一 (http://blog.csdn.net/weililan ...


      在介紹AOP之前,想必很多人都聽說AOP是基於動態代理和反射來實現的,那麼在看AOP之前,你需要弄懂什麼是動態代理和反射及它們又是如何實現的。

想瞭解JDK的動態代理及反射的實現和源碼分析,請參見下麵三篇文章

JDK的動態代理源碼分析之一 (http://blog.csdn.net/weililansehudiefei/article/details/73655925)

JDK的動態代理源碼分析之二(http://blog.csdn.net/weililansehudiefei/article/details/73656923)

Java反射機制 (http://blog.csdn.net/weililansehudiefei/article/details/70194940)

那麼接下里進入AOP的環節。

AOP即面向切麵編程,剛學AOP的時候,單是各種AOP的概念都搞的有點懵,什麼切麵,切點,通知,織入、連接點、目標對象。。。。AOP的原理都沒看呢,這些詞語的意思就已經讓人不想看了。本文將在實現AOP的時候,講解我理解的這些AOP的術語,對應的AOP的代碼和動作。

本文將先從AOP代碼實現入手,然後分析AOP的底層代碼及其原理。

一、AOP的Demo

如果我們把對象的繼承關係看成縱向關係,就像一棵樹,多個不同類的多個繼承關係就相當於有一排的樹。AOP的好處就在於,你想對這些樹進行相同的操作時,這個時候,不用縱向的為每個樹定義操作方法,你只需要橫向的一刀切,給他們提供個共有的操作方法。

Spring的AOP是支持JDK的動態代理和Cglib的動態代理的。JDK的動態代理是針對介面的,而Cglib是針對類的。本文針對JDK的動態代理。

首先定義一個介面:起名字時候特意給這個介面名,帶上了Interface,這樣後面會更引人註意一些。介面很簡單,裡面一個抽象方法eat()

package com.weili.cn;

/**

* 動物介面,提供一個eat的抽象方法
* Created by zsqweilai on 17/6/27.
*/
public interface AnimalInterface {
   public abstract void eat();
}

實現類:作為一個吃貨,實現類裡面當然得列印 chi  chi  chi。撐死我吧!!!
這個實現類裡面,只有一個方法,這個方法就是AOP的切點。雖然切點這個概念本身並不一定是Method,但在Spring中,所有的切點都是Method。我們增強的是方法。

package com.weili.cn;

/**
* Created by zsqweilai  on 17/6/27.
動物介面實現類
*/
public class Animal implements AnimalInterface{

public void eat() {
System.out.println("Animal類中 chi chi chi");
}
}

切麵類,又稱增強類。因為我們是要用這個類的方法,來給原先的切點方法增強。切麵類中,我們要去執行的方法,稱為通知。所謂織入通知,就是將切麵類裡面的方法,和切點的方法進行聯繫。

 package com.weili.cn;

import org.aopalliance.intercept.Joinpoint;

/**
* Created by zsqweilai on 17/6/27.
*/
public class AdviceAnimal {
  public void animalEmpty(){
   //System.out.println("joint before "+ joinPoint.getClass().getName());
   System.out.println("我餓了");
 }

   public void animalFull(){
    System.out.println("吃飽了");
 }

   public void animalEat(){
   System.out.println("正在吃");
 }

}

接下來通過xml配置的方式,在xml文件裡面配置AOP。
配置<aop:pointcut>的時候,通過expressi表達式,定義了com.weili.cn這個包下的所有類的所有方法 為切入點。也就是說,這個包下的所有方法,在調用執行的時候,會被Spring增強。具體在這裡的增強,就是在執行這些切點方法之前和之後,會分別執行animalEmptyanimalFull方法。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">


<bean id="animal" class="com.weili.cn.Animal"/>

<bean id="adviceAnimal" class="com.weili.cn.AdviceAnimal"/>

<aop:config>
<aop:aspect id="myaop" ref="adviceAnimal">
<aop:pointcut id="logPointcut" expression="execution(* com.weili.cn.*.*(..))" />
<aop:before method="animalEmpty" pointcut-ref="logPointcut" />
<aop:after method="animalFull" pointcut-ref="logPointcut" />
</aop:aspect>
</aop:config>
</beans>

最後就是調用的方法了。
我得說明一點,在我們進行spring-aop.xml解析的時候,aop還沒實現呢。在第二行getBean的時候,才真正進行aop。具體的源碼那裡 會說明。

package com.weili.cn;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* Hello world!
*
*/
public class App 
{
public static void main( String[] args )
{
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("spring-aop.xml");
AnimalInterface animal = (AnimalInterface) ctx.getBean("animal");
animal.eat();
}
}


緊接著就是Output輸出了。所以,我們可以看到,獲取的bean,確實是增強後的bean。那麼就趕緊看看源碼吧。

我餓了
Animal類中 chi chi chi
吃飽了

二、AOP源碼分析

源碼解析這塊,首先就是bean載入。之前也說了,AOP標簽也是自定義標簽,它的解析也和我們之前自定義標簽一樣,走自定義標簽的解析流程。不同的是,AOP調用的是AOP自己的解析器。由於在 Spring源碼解析之二 ------ 自定義標簽的解析和註冊 中已經很詳細的描述了自定義標簽的解析流程,所以這裡我們就不再去一一看bean標簽的解析註冊。

所以AOP的源碼分析,我們將從調用類裡面的第二行,ctx.getBean("animal")開始。在你調試走到這裡的時候,在ctx中可以看到解析和註冊的bean,我們不妨先來看一下。

如下圖,這個是在第一行代碼執行完畢後,ctx的各個屬性。可以在下圖看到,singlentonObjects中,已經存放了代理生成的animal。生層bean的過程在之前的裡面已經講的比較清楚了,這裡就不再說明。畢竟AOP嘛,我們需要知道,它是如何在我們需要執行的方法前後將我們需要執行的方法執行完成的。

ctx.getBean("animal")獲取完animal bean後,接下來調用eat()方法。這個時候,會進入JdkDynamicAopProxy類的invoke方法。
在這個invoke方法中,先是獲取代理類targetClass,然後根據method和targetClass獲取此方法對應的攔截器執行鏈chain。

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object oldProxy = null;
boolean setProxyContext = false;
TargetSource targetSource = this.advised.targetSource;
Class<?> targetClass = null;
Object target = null;

Boolean var10;
try {
if(this.equalsDefined || !AopUtils.isEqualsMethod(method)) {
if(!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {
Integer var18 = Integer.valueOf(this.hashCode());
return var18;
}

Object retVal;
if(!this.advised.opaque && method.getDeclaringClass().isInterface() && method.getDeclaringClass().isAssignableFrom(Advised.class)) {
retVal = AopUtils.invokeJoinpointUsingReflection(this.advised, method, args);
return retVal;
}

if(this.advised.exposeProxy) {
oldProxy = AopContext.setCurrentProxy(proxy);
setProxyContext = true;
}

target = targetSource.getTarget();
if(target != null) {
targetClass = target.getClass();
}
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 invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
retVal = invocation.proceed();
}

Class<?> returnType = method.getReturnType();
if(retVal != null && retVal == target && returnType.isInstance(proxy) && !RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {
retVal = proxy;
} else if(retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) {
throw new AopInvocationException("Null return value from advice does not match primitive return type for: " + method);
}

Object var13 = retVal;
return var13;
}

var10 = Boolean.valueOf(this.equals(args[0]));
} finally {
if(target != null && !targetSource.isStatic()) {
targetSource.releaseTarget(target);
}

if(setProxyContext) {
AopContext.setCurrentProxy(oldProxy);
}
}
return var10;
}

這個chain的內容如下。通過名字可以看到,一個是afterAdvice,一個是beforeAdvice。獲取chain後,構造出一個MethodInvoke方法,然後執行proceed方法。

進入proceed方法。currentInterceptorIndex的初始化值為-1.緊接著就如invoke方法。這裡的this是我們的eat方法。

public Object proceed() throws Throwable {
if(this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
return this.invokeJoinpoint();
} else {
Object interceptorOrInterceptionAdvice =
this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
if(interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
InterceptorAndDynamicMethodMatcher dm = (InterceptorAndDynamicMethodMatcher)interceptorOrInterceptionAdvice;
return dm.methodMatcher.matches(this.method,
this.targetClass, this.arguments)?dm.interceptor.invoke(this):this.proceed();
} else {
return ((MethodInterceptor)interceptorOrInterceptionAdvice).invoke(this);
}
}
}

在invoke方法里,這裡的mi是我們的interface裡面的eat方法。然後執行mi的proceed()方法。

public Object invoke(MethodInvocation mi) throws Throwable {
MethodInvocation oldInvocation = (MethodInvocation)invocation.get();
invocation.set(mi);

Object var3;
try {
var3 = mi.proceed();
} finally {
invocation.set(oldInvocation);
}

return var3;
}

這個時候,會繼續回到開始時候的proceed方法。這個時候獲取到的是

    interceptorOrInterceptionAdvice,也就是前面攔截器的list裡面的第二個,after的那個方法。然後繼續遞歸調用,會到鏈表的最後一個before方法。
    最終會調用before裡面的方法,

public Object invoke(MethodInvocation mi) throws Throwable {
this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis());
return mi.proceed();
}

    然後回去執行invokeJoinpoint方法,

public static Object invokeJoinpointUsingReflection(Object target, Method method, Object[] args) throws Throwable {
   try {
   ReflectionUtils.makeAccessible(method);
   return method.invoke(target, args);
   } catch (InvocationTargetException var4) {
      throw var4.getTargetException();
   } catch (IllegalArgumentException var5) {
      throw new AopInvocationException("AOP configuration seems to be invalid: tried calling method [" + method + "] on target ["         + target + "]", var5);
  } catch (IllegalAccessException var6) {
      throw new AopInvocationException("Could not access method [" + method + "]", var6);
   }
  }
}

  最後執行after方法。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 今天寫一個wpf的demo,用到綁定數據,給控制項綁定了數據源,但是數據卻沒有顯示出來,排查代碼發現綁定數據源的的成員用的是欄位不是屬性。 前端代碼: 後臺代碼: 如果把Employe的name,去掉{get;set;},改為一個欄位, public string name;數據就無法綁定了。原因是屬 ...
  • C#的Dictionary類型的值,知道key後,value可以修改嗎?答案是肯定能修改的。我在遍歷的過程中可以修改Value嗎?答案是也是肯定能修改的,但是不能用For each迴圈。否則會報以下的Exception. 之所以會報Exception是For each本身的問題,和Dictionar ...
  • 1.非同步同步的定義 同步方法:多個任務一個一個執行,同一時刻系統中只有一個任務在執行 非同步方法:發起一個調用,並不等著計算結束,而是直接去運行下一行;剛纔的計算,會啟動一個新的線程去執行 2.非同步同步的比較 2.1. 同步方法卡界面,因為UI線程忙於計算;非同步多線程方法不卡界面,主線程閑置,計算任務 ...
  • ASP.NET -- WebForm: HttpResponse 類的方法和屬性 ...
  • Extreme Drift賽車游戲C#源碼詳解(1) 接著上次的源碼分析: MainMenu場景的UGUI部分: Canvas中的EnoughMoney: 作用:當買車後金幣不足彈出的頁面 內部一個Text和一個Button,讓我有點疑惑的是,Button綁定的點擊事件是BuyVehicle函數: ...
  • using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Data.Objects.DataClasses; using ZBService.Model; us ...
  • 基於01和02 要得到如圖所示的熱力地圖(我從NuGet上下載的包沒有heatmap.js文件,沒法直接搞熱力圖,只好暫時先搞著地圖。後面儘量搞一下),一般要設置四個參數——title、tooltip、toolbox、series title其實是所有圖表共用的。tooltip也是如此,之前寫過此處 ...
  • 原鏈接: "UWP忽略短時間內重覆觸發的事件 超威藍火" 做移動端開發的可能都會遇到這種需求,當用戶點擊一個按鈕之後,由於沒有非同步,或者設備性能很差等等原因,程式卡住了。但是用戶不知道是咋回事啊,就開始狂點按鈕,結果請求很多次資源,或者構造了很多重覆視圖。安卓上有很多介紹如何忽略重覆點擊的情況,uw ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...