主頁 >  其他 > 萬字張文,從零開始,徹底了解 Spring AOP事務。

萬字張文,從零開始,徹底了解 Spring AOP事務。

2020-11-08 03:14:48 其他

接上篇,上篇說了關于IOC的具體流程,這篇文章就來談談Spring AOP,關于AOP編程,也是從基礎開始,然后再深入,文章稍微有點長,建議先點贊收藏一波,能看到最后的小伙伴肯定是真愛,文尾也給大家送一波資料,
在這里插入圖片描述

另外提供免費的學習資料,學習技術內容包含有:Spring,Dubbo,MyBatis,RPC,原始碼分析,高并發、高性能、分布式,性能優化,微服務 高級架構開發等等,

需要的朋友可以點擊:點這個!點這個!,暗號:csdn,

在這里插入圖片描述

AOP 編程

在軟體業,AOP為Aspect Oriented Programming的縮寫,意為:面向切面編程,通過預編譯方式和運行期間動態代理實作程式功能的統一維護的一種技術,AOP是OOP的延續,是軟體開發中的一個熱點,也是Spring框架中的一個重要內容,是函式式編程的一種衍生范型,利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程式的可重用性,同時提高了開發的效率,

Spring的關鍵組件之一是AOP框架,盡管Spring IoC容器不依賴于AOP,但AOP是對Spring IoC的補充,以提供功能非常強大的中間件解決方案,

在涉及AOP之前我們先簡單了解一下代理模式,因為代理模式是SpringAOP的底層實作,

代理模式

代理模式是23種設計模式之一,它分為動態代理和靜態代理,代理模式可以使客戶端的訪問物件從真實物件變為代理物件,

為什么這么做呢?

代理模式可以屏蔽用戶對真實物件的訪問,這樣可以避免一些安全上的問題,也能夠做到不改變真實物件,對真實物件的功能進行擴展(代理物件實作附加操作進行擴展),真實物件的功能更加純粹,業務的分工更加明確,

那么如何實作代理模式呢?

  • 首先需要一個抽象主題(介面或者抽象類)
  • 創建代理物件和真實物件
  • 代理物件和真實物件都實作該抽象主題
  • 客戶端訪問代理物件

用代碼了解代理模式

靜態代理

引入場景:我喜歡一雙鞋,但在中國地區買不到,需要托朋友從國外代購,

這里“我”可以理解為客戶端、朋友是代理物件、出售鞋的商店為真實物件、抽象主題為賣這雙鞋,

// 我
public class Me {
}
// 抽象主題,賣鞋(介面)
public interface Subject {
    public void sellShoes();
}
// 商店
public class Store implements Subject{
    public void sellShoes() {
        System.out.println("鞋子售價為90刀");
    }
}
// 朋友
public class Friend implements Subject{

    // 朋友拿到商店物件,對應朋友去商店這一場景(代理物件拿到真實物件),
    private Store store;

    public void setStore(Store store) {
        this.store = store;
    }

    // 代理物件附加操作
    public void returnHome(){
        System.out.println("朋友回到國內,來到我家");
    }
    // 代理物件附加操作
    public void giveMe(){
        System.out.println("我付給了朋友一百刀");
    }
    public void sellShoes() {
        // 朋友在商店里買下了這雙鞋子(代理物件呼叫真實物件的方法)
        store.sellShoes();
        // 朋友回國
        returnHome();
        // 朋友把這雙鞋交給我,我付給它相應的費用(含關稅)
        giveMe();
    }
}

可以看到,朋友和商店都實作了Subject這個介面,原本我想買到這雙鞋應該直接訪問商店物件,但因為沒辦法訪問到該物件,我只能通過訪問“朋友”物件來實作我拿到這雙鞋的需求,

訪問“朋友”物件

// 我
public class Me {
    public static void main(String[] args) {
        // 創建真實物件
        Store store = new Store();
        // 創建代理物件
        Friend friend = new Friend();
        // 將真實物件傳給代理物件
        friend.setStore(store);
        //呼叫代理方法
        friend.sellShoes();
    }
}

輸出結果
在這里插入圖片描述

到這里簡單的代理操作就實作了,我們通過訪問“朋友”物件,確實解決了 原本需要去訪問“商店物件”才能拿到鞋的困擾,對應到代理模式中就是,我們繞過了真實物件,通過訪問代理物件實作了呼叫真實物件功能的操作,且代理物件的兩個附加操作也實作了對真實物件功能的擴展!

可能栗子舉的不太恰當,大家不要太去深究,明白這一代理操作的具體實作和思想才是主要,

代理模式有沒有弊端?

靜態代理模式中,每有一個真實物件 就會有一個代理物件,如果真實物件十分多的話…😥

動態代理

動態代理可以根據需要,通過反射機制在程式運行時,動態的為目標物件生成代理物件,

動態代理主要分為兩大類,一種是基于介面的(JDK),一種是基于類的(CGLIB)

jdk動態代理:

了解jdk動態代理之前我們需要了解兩個類:java.lang.reflect.Proxy 和 java.lang.reflect.InvocationHandler介面,

InvocationHandler: 該介面僅定義了一個方法

  • public object invoke(Object proxy,Method method,Object[] args)

第一個引數為呼叫該方法的代理實體
第二個引數為目標物件的方法
第三個引數為目標物件方法的引數

  • 當我們使用Proxy的靜態方法生成動態代理實體后,使用該實體呼叫介面中的任意方法,都會將呼叫的方法替換為 invoke方法,

  • Proxy:該類就是為我們生成動態代理的 類

  • Proxy提供了很多方法,我們最常用的是newProxyInstance方法

  • static Object newProxyInstanc(ClassLoader loader,Class[] interface,InvocationHandler h) :

  • 該靜態方法會回傳一個Object

回傳的Object就可以被當做代理類使用
三個引數(ClassLoader loader,Class[] interface,InvocationHandler h)

  1. loader:一個類加載器物件,我們通過反射來獲取目標物件(真實)的類加載器
  2. Class[] interface: 介面物件陣列,也是通過反射獲取的,生成的代理物件會實作這些介面,并可以呼叫介面中宣告的所有方法,
  3. h: InvocationHandler的物件實體,如果我們用來的生成代理類 的
    類(Friend)實作了這個介面(InvocationHandler),可以直接傳入這個類本身(this),
    我們通過newProxyInstance就可以得到真實物件所需要的代理物件

用代碼進行簡單的演示

  1. 使用靜態代理中的Subject公共主題和Store真實物件,不進行任何改動
  2. 創建一個實作了InvocationHandler介面的類(Friend),它必須實作Invoke方法,在Invoke方法中寫附加操作
// 首先實作 InvocationHandler介面
public class Friend implements InvocationHandler {
    // 被代理的介面物件
    private Object target;

    public Friend(Object target) {
        this.target = target;
    }

    // 寫一個獲取代理物件實體的方法
    public Object getProxy(){
        // Proxy中的newProxyInstance方法會創建一個動態的代理類
        return Proxy.newProxyInstance(this.getClass().getClassLoader(), target.getClass().getInterfaces(),this);
    }
    // InvocationHandler介面中的invoke方法:
    // 該方法在使用getProxy方法 生成代理類并呼叫介面中的方法時會被自動呼叫
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 附加操作
        returnHome();
        // method的invoke方法,通過反射獲取到目標物件中的方法,
        // 由于我們沒有將目標物件寫死,所有我們傳入動態的target,
        Object object = method.invoke(target,args);
        // 附加操作
        giveMe();
        return object;
    }
    //附加操作
    public void returnHome(){
        System.out.println("朋友回國后來到我家");
    }
    // 同上
    public void giveMe(){

        System.out.println("我付給了朋友指定的錢");
    }
}
  1. 在Me類中進行動態代理的呼叫測驗
public class Me {
    public static void main(String[] args) {
        // 創建真實物件
        Store store = new Store();
        // 創建 InvocationHandler物件的實體,并傳入目標物件(真實物件)
        Friend friend = new Friend(store);
        // 通過InvocationHandler的實體(friend)呼叫getProxy方法
        // 該方法會回傳一個代理物件的實體,我們只需要將我們寫好的Object型別轉換為需要的介面型別即可
        Subject proxy = (Subject) friend.getProxy();
        // invoke方法會在我們呼叫介面中的方法時,將該方法替換為它,
        // invoke方法會通過method.invoke拿到目標物件中的方法
        // 也就是Store中的方法,從而實作代理的操作,
        proxy.sellShoes();
    }
}

運行結果:

 D:\MORENANZHUANGDIZHI\jdk1.8\bin\java.exe "-javaagent:D:\MORENANZHUANGDIZHI\IntelliJ IDEA 2020.1.2\lib\idea_rt.jar=60769:D:\MORENANZHUANGDIZHI\IntelliJ IDEA 2020.1.2\bin" -Dfile.encoding=UTF-8 -classpath........repository\org\hamcrest\hamcrest-core\1.3\hamcrest-core-1.3.jar com.molu.proxy.Me
朋友回國后來到我家
售價為90刀
我付給了朋友一百刀

行程已結束,退出代碼0
  1. 為了凸顯動態代理的作用,我們再撰寫一個代購的栗子

引入場景: 我想要一臺Mac,但是國內…所以又托朋友…

  • 寫一個公共主題(介面)
  • 寫一個被代理的物件
  • 在Me類中進行動態代理物件生成的測驗
// 公共主題
public interface Mac {
    public void sellMac();
}
// 被代理的物件
package com.molu.proxy;

public class MacStore implements Mac{
    public void sellMac() {
        System.out.println("Mac售價為1899刀");
    }
}

公共主題(介面)和真實物件寫好后我們在Me類中動態的生成代理物件(沒有對動態生成代理實體的Friend類進行任何修改)

Me類

public class Me {
    public static void main(String[] args) {
        // 創建真實物件
        Store store = new Store();
        // 創建 InvocationHandler物件的實體,并傳入目標物件(真實物件)
        Friend friend = new Friend(store);
        // 通過InvocationHandler的實體呼叫getProxy方法
        //該方法會回傳一個代理物件的實體,我們只需要指定該實體需要實作的介面即可(強轉)
        Subject proxy = (Subject) friend.getProxy();
        // 通過這個動態生成代理實體來呼叫真實物件中的sellShoes()方法
        // proxy.sellShoes();


        // 生成第二個栗子的動態代理類,步驟同上一模一樣
        MacStore macStore = new MacStore();
        Friend friendMac = new Friend(macStore);
        Mac proxyMac = (Mac) friendMac.getProxy();
        proxyMac.sellMac();
    }
}

運行結果
在這里插入圖片描述

沒有任何問題,又成功的生成了macStore的代理物件,這樣我們就避免了反復寫代理類的問題,

jdk動態代理原理剖析

我們主要分析Friend類中的具體實作

  1. 首先來看一下我們手動寫的getProxy方法,它主要使用了Proxy類中的newProxyInstance方法,

這個方法回傳一個Object物件,這個Object物件有三個引數,這三個引數具體是什么,上文已經說過了,

回傳的這個物件通過Proxy的靜態方法生成,生成后就可以被當作一個代理物件來使用,

public Object getProxy(){ 
return Proxy.newProxyInstance(this.getClass().getClassLoader(), target.getClass().getInterfaces(),this); 
} 
  1. InvocationHandler中的invoke方法 這個方法

有三個引數,分別是代理物件的實體(com.sun.proxy.$Proxy0),目標物件的方法,方法的引數,

我們如果通過getProxy來生成代理實體,使用該實體呼叫介面中的方法——就會執行invoke方法,

invoke通過反射拿到真實物件中的方法,真正執行的也就是這個通過反射拿到的方法,

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    Object object = method.invoke(target,args);
    return object;
}
  1. Me類

1.首先我們創建目標物件(真實物件)的實體和InvocationHandler的實體(以下簡稱為 處理程式的實體 = =生成代理類的處理程式實體 = = InvocationHandler物件的實體),將目標物件傳入處理程式的實體中(也就是傳入了 friend== this 中)
在這里插入圖片描述

打上斷點后,確實看到Friend的實體中的target變成了MacStore,之后invoke方法會通過反射(method.invoke)拿到MacStore中的方法,

2.使用處理程式的實體呼叫getPorxy方法創建代理物件實體,該物件創建后型別默認為Object(因為我們在寫getProxy方法的時候回傳值寫的是Object),我們將它強轉為需要的介面型別即可,

3.通過生成的代理實體來呼叫介面中的方法時,處理程式的實體會自動呼叫invoke方法,

4.invoke()方法中的method.invoke(target,args)已經拿到了目標物件中的方法及引數(我們這沒有寫引數),所以呼叫invoke方法就等于是呼叫了目標物件中的方法,再將增強行為寫在method.invoke(target,args)上下,就可以實作一次代理的操作,

MacStore macStore = new MacStore();
Friend friendMac = new Friend(macStore);
Mac proxyMac = (Mac) friendMac.getProxy();
proxyMac.sellMac();

invoke方法自動呼叫

我們再來聊聊為什么invoke方法會被自動呼叫的問題

public Object invoke(Object proxy, Method method, Object[] args)
    throws Throwable;

invoke() 方法來自 InvocationHandler 介面

  • 我們先來看看它的第一個引數 proxy引數,直接輸出它的位元組碼檔案名,
System.out.println(proxy.getClass().getName());
// 輸出結果為:
	com.sun.proxy.$Proxy0

這個 Proxy0實際上就是我們的代理類實體,感興趣的朋友可以去將newProxyInstance回傳的object物件的位元組碼檔案名列印出來看一下,

也會是??Proxy0 實際上就是我們的代理類實體,感興趣的朋友可以去將newProxyInstance回傳的object物件的位元組碼檔案名列印出來看一下,

也會是Proxy0實際上就是我們的代理類實體,感興趣的朋友可以去將newProxyInstance回傳的object物件的位元組碼檔案名列印出來看一下,也會是??Proxy0

要明白為什么會自動呼叫invoke方法,我們需要查看一下$Proxy0物件反編譯檔案的原始碼,

  • 在main方法最前面添加該配置,運行后會生成代理類反編譯的class檔案
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");

生成路徑在idea作業空間下的com\sun\Proxy
$Proxy0.class檔案和源代碼不在一個目錄下
在這里插入圖片描述

點開代理物件反編譯的class檔案原始碼可以看到
public final class $Proxy0 extends Proxy implements Subject {
    // 發現它繼承了Proxy類,且實作了Subject介面(在生成該反編譯檔案時我將Mac相關代碼都注了,所以是Subject)
	..........
        // 重寫了 Subject 介面的 sellShoes方法
 public final void sellShoes() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    ..........

點進去第一行能獲得兩個資訊

public final class $Proxy0 extends Proxy implements Subject {
  1. 代理類的反編譯檔案繼承了Proxy類

也就是說它的父類是Proxy,那么它就會關聯一個 InvocationHandler 方法呼叫處理器

  1. 實作了我們寫的Subject介面

可能這就是為什么Jdk動態代理為什么必須要有介面才能使用,(單繼承的局限性,但是可以通過介面來多實作)

再往下看,可以看到 $Proxy0 重寫了sellShoes方法,該方法呼叫了super(Proxy) . h.invoke()方法,

  • 關于 h
    我們前面只在Proxy.newProxyInstance有所涉及,也就是我們傳入的第三個引數,一個InvocationHandler實體(this),
  • 而 $Proxy0 重寫的 sellShoes 方法中的 h 也是從 Proxy 類中取的引數極有可能就是我們傳進去的 this ,

接下來要做的就很明顯了,我們要看看newProxyInstance方法的原始碼(大家不要看到這一堆原始碼就害怕,因為我水平也不高 不會全展開來一個一個說,大家放心閱讀下去即可,需要看的兩處地方有用注解標出)

   @CallerSensitive
    public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        								/* 我們通過 newProxyInstance
        								 傳進來的InvocationHandler實體 h */
        throws IllegalArgumentException
    {
        Objects.requireNonNull(h);

        final Class<?>[] intfs = interfaces.clone();
        
        final SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
        }
        Class<?> cl = getProxyClass0(loader, intfs);
        // (o゚v゚)ノ這里
        try {
            if (sm != null) {
                checkNewProxyPermission(Reflection.getCallerClass(), cl);
            }

            final Constructor<?> cons = cl.getConstructor(constructorParams);
            final InvocationHandler ih = h;
            //  (o゚v゚)ノ還有這里
            if (!Modifier.isPublic(cl.getModifiers())) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        cons.setAccessible(true);
                        return null;
                    }

不難看出我們的 $Proxy0 就是該方法創建的,c1 為 $Proxy0 的參考物件

Class<?> cl = getProxyClass0(loader, intfs);
// 需要傳入一個類加載器和一個介面陣列 傳入的介面陣列在創建$Proxy0時會被自動實作

再往下看,有這么兩行代碼

/* final Constructor<?> cons = cl.getConstructor(constructorParams); 這行不管 */
final InvocationHandler ih = h;

第一行我們不細細展開,篇幅有限,我覺得我也沒辦法在原始碼上講的比較能夠讓人理解,所以我們將目光放到第二行,

  • 我們通過Proxy.newProxyInstance(… , … ,h)傳進來的h,被賦值給了InvocationHandler實體,
  • InvocationHandler ih = h,這個h實際上就是Friend實體,
  • 而在代理物件的反編譯檔案中又看到這么幾行代碼
public final void sellShoes() throws  {
        try {
            // super 是繼承的Proxy類
            // h 是我們傳進來的InvocationHandler實體(friend),
            // invoke方法就是我們寫在Friend類中的invoke方法,
            super.h.invoke(this, m3, (Object[])null);

很明了了,我們通過getProxy生成的代理物件實體 $Proxy0 ,呼叫sellShoes方法它最侄訓執行

public final void sellShoes() throws { try { super.h.invoke(this, m3, (Object[])null); } catch (RuntimeException | Error var2) { … }

方法,

繼而就呼叫了Friend中的invoke方法,也就實作了invoke方法的自動呼叫,

所以我們原以為的 通過代理物件實體呼叫介面中的方法實際上是通過$Proxy0呼叫了原始碼中的sellShoes()方法才對

// 使用代理物件的實體呼叫sellShoes方法
proxy.sellShoes();
// 你以為的
 public void sellShoes();

// 實際上的
public final void sellShoes() throws {
        try { super.h.invoke(this, m3, (Object[])null);
            ..........

再捋一捋

首先我們通過newProxyInstance中的Class<?> cl = getProxyClass0(loader, intfs);方法得到Proxy0(這個Proxy0(這個Proxy0(這個Proxy0會自動的實作我們傳入的介面),通過代理實體呼叫介面中的方法時,實際上就是通過 $Proxy0 呼叫原始碼里,重寫過的介面方法
重寫過的介面方法

public final void sellShoes() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    ..........
  • super 很容易理解,$Proxy0 繼承的父類,也就是Proxy,
  • Proxy中的h,不就是我們通過Proxy.newProxyInstance傳進去的this(InvocationHandler介面實體
    == friend == this)嗎,
    在這里插入圖片描述
  • 這個this ?就是實作了InvocationHandler介面的Friend實體啊,最后通過這個實體呼叫了invoke方法,
  • “ super.h.invoke(this, m3, (Object[])null); ”
  • 到這里,為什么invoke方法會被自動呼叫,不就顯得圖樣了嘛 ( ̄??),

講的比較淺,有很多地方沒有講和草草帶過,但并不妨礙我們get到為什么會自動呼叫invoke方法,

cglib動態代理

在jdk動態代理生成的代理物件實體($Proxy0)的原始碼中我們看到,jdk動態代理必須要有介面實作才能使用,這就造成了一定的局限性,所以在目標類沒有介面實作的情況下我們就會使用cglib動態代理,

cglib動態代理采用的是繼承思想,它針對類來實作代理,它會給目標類生成一個對應的子類,并覆寫其方法,

簡單點說就是:代理類會繼承目標類,并重寫目標類中的方法(由于使用了繼承,所以要避免使用final來修飾目標類),

使用cglib動態代理

匯入pom依賴

<!--匯入cglib依賴-->  
<dependency>
  	<groupId>cglib</groupId>
	<artifactId>cglib</artifactId>
	<version>3.3.0</version>
</dependency>

這里匯入pom依賴時需要注意版本問題,可能會有無法加載依賴的錯誤,根本原因是ASM支持與當前的cglib版本不一致,

可以選擇降低版本,來快速解決該問題,使用低版本的cglib,Mavan會自動匯入版本符合的ASM支持,
在這里插入圖片描述

cglib實作動態代理首選需要準備一個目標物件和一個生成動態代理的類

這里我們使用MacStore來充當目標物件,唯一的不同是沒有再繼承一個公共主題介面,

package com.molu.cglib;

public class MacStore {
    public void sellMac(){
        System.out.println("Mac售價為1899刀");
    }
}

撰寫Friend類,寫一個生成代理類的方法,重寫攔截器方法

// 繼承cglib中的MethodInterceptor介面
public class Friend implements MethodInterceptor
{
    // 創建目標物件的實體
    private Object target;
    // 通過構造器傳入目標物件
    public Friend(Object target) {
        this.target = target;
    }
	// 使用該方法來創建代理類
    public Object getProxy(){
        // 創建Enhancer物件
        Enhancer enhancer = new Enhancer();
        // 使用Enhancer物件中的方法設定父類(將目標類設定為代理類的父類)
        enhancer.setSuperclass(target.getClass());
        // 這里需要傳入一個CallBack物件,因為MethodInterceptor介面繼承了CallBack
        // 而我們的Friend又實作了CallBack所以我們直接傳入 this,這行代碼的意思是:
        // 設定攔截器,回呼物件為本身物件,
        enhancer.setCallback(this);
        // 回傳Enhancer中的create()方法拿到代理物件實體給呼叫者
        return enhancer.create();
    }
	// 重寫intercept方法
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        // 假裝有增強行為 ˋ(°▽°)`
		System.out.println("增強行為");
        // 使用代理類對方法的代理參考,來呼叫invoke方法
        Object object = methodProxy.invoke(target,objects);
        return object;
    }
}

在Me類中呼叫getProxy方法獲取動態代理實體

public class Me {
    public static void main(String[] args) {
        MacStore macStore = new MacStore();
        Friend friend = new Friend(macStore);
        MacStore proxy = (MacStore) friend.getProxy();
        proxy.sellMac();
    }
}


測驗結果:
D:\MORENANZHUANGDIZHI\jdk1.8\bin\java.exe......repository\cglib\cglib\3.2.12\cglib-3.2.12.jar;D:\MORENANZHUANGDIZHI\maven\maven-repository\org\ow2\asm\asm\7.1\asm-7.1.jar com.molu.cglib.Me
    
增強行為
Mac售價為1899刀

Process finished with exit code 0

cglib動態代理到這里就不再展開了,篇幅有限,

引入AOP

在理解了AOP的底層"代理模式"后我們來正式的引入AOP(一個沒注意就把這個鋪墊寫的長了點 (⊙﹏⊙) )

因為中間隔了一千字左右的動態代理涉及,估計大家對于還沒有謀面的AOP已經沒什么印象了,所以我將上文寫的一大段屁話再次參考過來,

在軟體業,AOP為Aspect Oriented Programming的縮寫,意為:面向切面編程,通過預編譯方式和運行期間動態代理實作程式功能的統一維護的一種技術,AOP是OOP的延續,是軟體開發中的一個熱點,也是Spring框架中的一個重要內容,是函式式編程的一種衍生范型,利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程式的可重用性,同時提高了開發的效率,

Spring的關鍵組件之一是AOP框架,盡管Spring IoC容器不依賴于AOP,但AOP是對Spring IoC的補充,以提供功能非常強大的中間件解決方案,

Spring AOP默認將標準JDK動態代理用于AOP代理,在業務類沒有介面的實作時,也可以使用cglib動態代理,

AspectJ

Spring使用AspectJ提供的用于切入點決議和匹配的庫來解釋與AspectJ 5相同的注釋,但是,AOP運行時仍然是純Spring AOP,并且不依賴于AspectJ編譯器或編織器,

我們就不像其他博客那樣 放幾張看完毫無頭緒,好像懂又好像沒懂的圖片了,直接進入正題,

常見的術語和概念

在實作AOP操作之前我們先對下面的這些術語和概念有一個比較粗淺的認識

  • 橫切關注點:跨越應用程式多個模塊的方法或者功能,即是 與我們業務邏輯毫無關系的部分也是我們需要關注的部分,如日志、安全、快取、事務等等…
  • 切面(ASPECT):橫切關注點 被模塊化 的特殊物件,即 它是一個類,
  • 通知(Advice):切面必須要完成的作業 即 它是類中的一個方法
  • 目標(Target): 被通知的物件
  • 代理(Proxy):向目標物件應用通知之后創建的物件
  • 切入點(PointCut):切面通知執行的"地點"的定義
  • 連接點(JoinPoint):與切入點匹配的執行點

SpringAOP中, 通過 **Advice(通知) **定義橫切邏輯,Spring支持五種型別的Advice

  • 前置通知**[Before advice]**:方法(連接點)前執行的通知,它會不阻止執行流程前進到連接點(除非它引發例外)
  • 正常回傳(后置)通知**[After returning advice]**:方法(連接點)正常執行完后運行的通知(沒有引發例外的情況)
  • 環繞通知**[Around advice]**:環繞通知圍繞在方法(連接點)執行前后運行,這是最強大的通知型別,能在方法呼叫前后自定義一些操作,
  • 例外回傳通知**[After throwing advice]**:方法(連接點)拋出例外時運行的通知
  • 最終通知**[Final advice]**:在方法(連接點)執行完成后執行的通知,與后置通知不同的是,它會無視拋出例外的情況,即拋出例外仍然會執行該通知,用人話說就是:“無論如何都會執行的通知”,補充,后置通知可以通過配置得到回傳值,而最終通知不行

更多內容可以移步Spring官網

實作AOP

原生API介面實作

使用AOP我們需要匯入織入包

<dependency>
	<!--匯入織入依賴-->
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.5</version>
</dependency>

匯入依賴后我們寫一個簡單的業務類

業務介面

public interface UserService {
    public void add();
    public void delete();
    public void update();
    public void select();
}

介面實作類

public class UserServiceImpl implements UserService{
    public void add() { System.out.println("增加了一個用戶"); }
    public void delete() { System.out.println("洗掉了一個用戶"); }
    public void update() { System.out.println("更新了用戶"); }
    public void select() { System.out.println("查詢用戶"); }
}

寫一個前置通知,這個通知類只做一件事情:“在我們呼叫介面實作類中的方法時 列印當前時間和呼叫的方法名”我們這邊只寫一個通知,其他的通知大多雷同,感興趣的朋友可以自己寫著玩玩,了解通知的概念和作用差不多就能簡單上手實作了,

// 繼承Spring原生的API介面MethodBeforeAdvice
public class Log implements MethodBeforeAdvice {
    // 在MethodBeforeAdvice介面的 before 方法寫我們具體的操作
    public void before(Method method, Object[] objects, Object o) throws Throwable {
        Date date = new Date();
        System.out.println(new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(date)
                + " 執行了" + method.getName() + "方法");
    }
}

在spring組態檔中注冊以上兩個Bean

<bean id="userService" class="com.molu.service.UserServiceImpl"/>
<bean id="log" class="com.molu.service.Log"/>

之后我們在applicationContext.xml中引入AOP的命名空間

xmlns:aop="http://www.springframework.org/schema/aop"
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd

引入命名空間后我們對AOP進行配置

aop:config

<aop:pointcut id=“pointcut” expression=“execution(* com.molu.service.UserServiceImpl.*(…))”/>

<aop:advisor advice-ref=“log” pointcut-ref=“pointcut”/>
</aop:config>

切入點中的execution運算式很好理解,它用來確定我們的通知會在哪些地方執行,

  • expression=“execution(* com.molu.service.UserServiceImpl.*(…))”

第一個 為所有的回傳型別
com.molu.service.UserServiceImpl. * (…) 表示com.molu.service包下的 UserServiceImpl 類的所有方法(所有的引數)

配置完成后我們呼叫UserServiceImpl中的任意方法,都會在方法前執行我們的log前置通知

測驗類

public class MyTest {
    public static void main(String[] args) {
        // 獲取Spring背景關系環境物件
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        // 使用背景關系環境物件拿到我們的UserServiceImpl Bean的實體
        // 因為AOP默認使用標準JDK動態代理,所以我們還需要將型別強轉為UserService介面
        UserService userService = (UserService) context.getBean("userService");
        // 呼叫add方法
        userService.add();
    }
}

測驗結果
在這里插入圖片描述

可以看到,在我們呼叫UserServiceImpl中的方法時,前置通知成功的被執行了,

自定義切面實作

切面(ASPECT):橫切關注點 被模塊化 的特殊物件,即 它是一個類,

我們還可以通過自定義一個類,將該類標記為一個切面,使用該類中的方法來實作通知的功能,

寫一個自定義類

public class DiyAspect {
    // 前置通知
    public void Before(){
        Date date = new Date();
        System.out.println(new SimpleDateFormat("yyyy-MM-dd HH-mm-ss").format(date)+ "時執行了該通知");
    }
    // 后置通知
    public void After(){
        System.out.println("方法執行完畢");
    }
}

在組態檔中注冊Bean,并將該類定義為一個切面,使用該類中的方法來執行通知功能,

<!--注冊Bean-->
    <bean id="diyAspect" class="com.molu.diy.DiyAspect"/>
    <!--進行AOP配置 -->
    <aop:config>
        <!--自定義切面-->
        <aop:aspect id="aspect" ref="diyAspect">
            <!--定義切入點-->
            <aop:pointcut id="pointcut" expression="execution(* com.molu.service.UserServiceImpl.*(..))"/>
            <!--前置通知設定為我們寫在 diyAspect 中的 Before方法-->
            <aop:before method="Before" pointcut-ref="pointcut"/>
            <!--后置通知設定為我們寫在 diyAspect 中的 After方法-->
            <aop:after method="After" pointcut-ref="pointcut"/>
        </aop:aspect>

    </aop:config>

MyTest測驗類不進行任何改動,直接運行測驗,
在這里插入圖片描述

這種通過自定義切面的方式 相對來說會更加簡單一些也更容易理解,但因為我們寫的只是普通方法,功能上自然是不如實作介面的方式強大

注解實作

使用注解實作之前,我們需要開啟AOP注解的支持 和自動掃描包 方便偷懶

<!--自動掃描包,使該包下的注解能夠生效-->
<context:component-scan base-package="com.molu.diy"/>
<!--開啟AOP注解支持-->
<aop:aspectj-autoproxy/>

寫一個Annotation類,在該類中定義一個方法為前置通知,使用注解進行標記,

@Component // 使用注解注冊Bean
@Aspect // 使用注解標記該類為一個切面
public class Annotation {

    @Before("execution(* com.molu.service.UserServiceImpl.*(..))")
    // 標記為前置通知,由于類中沒辦法參考切入點,所以切入點需要我們手動寫,這也是注解來實作AOP的一個不便之處,
    public void before(){
        System.out.println("我是前置通知~~~");
    }
}

Mytest測驗類不進行任何改動,直接進行測驗
在這里插入圖片描述

到這里AOP的三種常見的實作方式 就介紹的差不多了,三種方式各自有各自的好處,使用方面哪種簡單用哪種即可,

AOP并沒有我們想象中的那么難,主要的是理解這種面向切面的思想,

使用AOP后我們在業務中插入日志等功能會更加的便捷,且不會對業務類造成太多的影響,對日志等功能進行修改或洗掉也大多不會對業務類本身造成影響,也就做到了所謂的高內聚低耦合,能夠熟練的運用AOP 對寫出優質的代碼多多少少也會有一些幫助

宣告式事務

什么是事務

  • 事務可以簡單的理解為,將一組業務當作一個業務來處理,要么都成功要么都失敗
  • 事務在開發中十分的重要,它涉及資料的完整性和一致性

事務的ACID原則

事務的ACID原則在面試中會被經常問到,分別是,原子性( Atomicity )、一致性( Consistency )、隔離性( Isolation )和持久性( Durability ),這四個特性簡稱為 ACID 特性,

  • 原子性

簡單的說就是要么都成功要么都失敗

  • 一致性

事務執行的結果必須是使資料庫從一個一致性狀態變到另一個一致性狀態,

因此當資料庫只包含成功事務提交的結果時,就說資料庫處于一致性狀態,

如果資料庫系統 運行中發生故障,個別事務尚未完成就被迫中斷,這些未完成事務對資料庫所做的修改有一部分已寫入物理資料庫,這時資料庫就處于不一致的狀態,

  • 隔離性

多個業務可能操作同一個資源
我們需要保證這些業務操作資料時是互相隔離的,不會造成資料的損壞等問題,
確保完整性和一致性

  • 持久性

指事務一旦提交,它對資料庫中的資料的改變就應該是永久性的,不能回滾,
之后的其它操作或故障不應該對其執行結果有任何影響

開啟事務

Spring支持宣告式事務和編程式事務兩種事務管理模式,我們一般使用宣告式事務,

  • 編程式事務管理: 通過Transaction Template手動管理事務,實際應用中很少使用
  • 使用XML配置宣告式事務: 推薦使用(代碼侵入性最小),實際是通過AOP實作

宣告式事務會使用AOP,在指定的切入點中織入事務,

 <!--配置c3p0連接池-->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!--注入屬性-->
        <property name="driverClass" value="com.mysql.jdbc.Driver"/>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mybatis?serverTimezone=GMT%2B8&amp;useSSL=true&amp;useUnicode=true&amp;characterEncoding=UTF-8"/>
        <property name="user" value="root"/>
        <property name="password" value="手動馬賽克"/>
    </bean>
    <!--配置事務管理器-->
    <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <constructor-arg ref="dataSource"/>
    </bean>
    <!--配置事務通知-->
    <tx:advice id="interceptor" transaction-manager="dataSourceTransactionManager">
        <tx:attributes>
            <!-- * 表示我們每一個方法都會被織入事務-->
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>

    <!--配置事務切入-->
    <aop:config>
        <aop:pointcut id="txPointCut" expression="execution(* com.molu.service.*.*(..))"/>
        <aop:advisor advice-ref="interceptor" pointcut-ref="txPointCut"/>
    </aop:config>
    <!--配置完成后我們service中的所有類的所有方法,都會被織入事務-->
</beans>

以上就是如何開啟宣告式事務的全部操作,到這里我們從IOC到AOP的博客也結束了,非常感謝你能看到這里,希望對你有幫助!

最后

還有Java核心知識點+全套架構師學習資料和視頻+一線大廠面試寶典+面試簡歷模板可以領取+阿里美團網易騰訊小米愛奇藝快手嗶哩嗶哩面試題+Spring原始碼合集+Java架構實戰電子書+2020年最新大廠面試題,

需要的朋友可以點擊:點這個!點這個!,暗號:csdn,

在這里插入圖片描述
在這里插入圖片描述

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/206177.html

標籤:其他

上一篇:屌絲的逆襲之路,2年5個月13天,從外包到拿下阿里offer,面試學習+刷題筆記分享

下一篇:java中的native方法性能到底怎么樣?

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 2023年最新微信小程式抓包教程

    01 開門見山 隔一個月發一篇文章,不過分。 首先回顧一下《微信系結手機號資料庫被脫庫事件》,我也是第一時間得知了這個訊息,然后跟蹤了整件事情的經過。下面是這起事件的相關截圖以及近日流出的一萬條資料樣本: 個人認為這件事也沒什么,還不如關注一下之前45億快遞資料查詢渠道疑似在近日復活的訊息。 訊息是 ......

    uj5u.com 2023-04-20 08:48:24 more
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:47:46 more
  • vulnhub_Earth

    前言 靶機地址->>>vulnhub_Earth 攻擊機ip:192.168.20.121 靶機ip:192.168.20.122 參考文章 https://www.cnblogs.com/Jing-X/archive/2022/04/03/16097695.html https://www.cnb ......

    uj5u.com 2023-04-20 07:46:20 more
  • 從4k到42k,軟體測驗工程師的漲薪史,給我看哭了

    清明節一過,盲猜大家已經無心上班,在數著日子準備過五一,但一想到銀行卡里的余額……瞬間心情就不美麗了。最近,2023年高校畢業生就業調查顯示,本科畢業月平均起薪為5825元。調查一出,便有很多同學表示自己又被平均了。看著這一資料,不免讓人想到前不久中國青年報的一項調查:近六成大學生認為畢業10年內會 ......

    uj5u.com 2023-04-20 07:44:00 more
  • 最新版本 Stable Diffusion 開源 AI 繪畫工具之中文自動提詞篇

    🎈 標簽生成器 由于輸入正向提示詞 prompt 和反向提示詞 negative prompt 都是使用英文,所以對學習母語的我們非常不友好 使用網址:https://tinygeeker.github.io/p/ai-prompt-generator 這個網址是為了讓大家在使用 AI 繪畫的時候 ......

    uj5u.com 2023-04-20 07:43:36 more
  • 漫談前端自動化測驗演進之路及測驗工具分析

    隨著前端技術的不斷發展和應用程式的日益復雜,前端自動化測驗也在不斷演進。隨著 Web 應用程式變得越來越復雜,自動化測驗的需求也越來越高。如今,自動化測驗已經成為 Web 應用程式開發程序中不可或缺的一部分,它們可以幫助開發人員更快地發現和修復錯誤,提高應用程式的性能和可靠性。 ......

    uj5u.com 2023-04-20 07:43:16 more
  • CANN開發實踐:4個DVPP記憶體問題的典型案例解讀

    摘要:由于DVPP媒體資料處理功能對存放輸入、輸出資料的記憶體有更高的要求(例如,記憶體首地址128位元組對齊),因此需呼叫專用的記憶體申請介面,那么本期就分享幾個關于DVPP記憶體問題的典型案例,并給出原因分析及解決方法。 本文分享自華為云社區《FAQ_DVPP記憶體問題案例》,作者:昇騰CANN。 DVPP ......

    uj5u.com 2023-04-20 07:43:03 more
  • msf學習

    msf學習 以kali自帶的msf為例 一、msf核心模塊與功能 msf模塊都放在/usr/share/metasploit-framework/modules目錄下 1、auxiliary 輔助模塊,輔助滲透(埠掃描、登錄密碼爆破、漏洞驗證等) 2、encoders 編碼器模塊,主要包含各種編碼 ......

    uj5u.com 2023-04-20 07:42:59 more
  • Halcon軟體安裝與界面簡介

    1. 下載Halcon17版本到到本地 2. 雙擊安裝包后 3. 步驟如下 1.2 Halcon軟體安裝 界面分為四大塊 1. Halcon的五個助手 1) 影像采集助手:與相機連接,設定相機引數,采集影像 2) 標定助手:九點標定或是其它的標定,生成標定檔案及內參外參,可以將像素單位轉換為長度單位 ......

    uj5u.com 2023-04-20 07:42:17 more
  • 在MacOS下使用Unity3D開發游戲

    第一次發博客,先發一下我的游戲開發環境吧。 去年2月份買了一臺MacBookPro2021 M1pro(以下簡稱mbp),這一年來一直在用mbp開發游戲。我大致分享一下我的開發工具以及使用體驗。 1、Unity 官網鏈接: https://unity.cn/releases 我一般使用的Apple ......

    uj5u.com 2023-04-20 07:40:19 more