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

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

2020-11-06 07:15:52 後端開發

接上篇,上篇說了關于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/houduan/204104.html

標籤:java

上一篇:JDK集合原始碼之HashMap決議(上)

下一篇:Java面向物件(基礎篇):一

標籤雲
其他(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)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more