SpringBoot基礎篇AOP之高階使用技能

一灰灰發表於2019-03-10

更多相關內容,檢視: spring.hhui.top/

前面一篇博文 190301-SpringBoot基礎篇AOP之基本使用姿勢小結 介紹了aop的簡單使用方式,在文章最後,丟擲了幾個問題待解決,本篇博文則將針對前面的問題,看下更多關於AOP的使用說明

I. 高階技能

1. 註解攔截方式

前面一文,主要介紹的是根據正規表示式來攔截對應的方法,接下來演示下如何通過註解的方式來攔截目標方法,實現也比較簡單

首先建立註解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AnoDot {
}
複製程式碼

接著在目標方法上新增註解,這裡藉助前面博文中工程進行說明,新建一個com.git.hui.boot.aop.demo2.AnoDemoBean,注意這個包路徑,是不會被前文的AnoAspect定義的Advice攔截的,這裡新建一個包路徑的目的就是為了儘可能的減少干擾項

@Component
public class AnoDemoBean {
    @AnoDot
    public String genUUID(long time) {
        try {
            System.out.println("in genUUID before process!");
            return UUID.randomUUID() + "|" + time;
        } finally {
            System.out.println("in genUUID finally!");
        }
    }
}
複製程式碼

接下來定義對應的advice, 直接在前面的AnoAspect中新增(不知道前文的也沒關係,下面貼出相關的程式碼類,前文的類容與本節內容無關)

@Aspect
@Component
public class AnoAspect {
    @Before("@annotation(AnoDot)")
    public void anoBefore() {
        System.out.println("AnoAspect ");
    }
}
複製程式碼

測試程式碼

@SpringBootApplication
public class Application {
    private AnoDemoBean anoDemoBean;

    public Application(AnoDemoBean anoDemoBean) {
        this.anoDemoBean = anoDemoBean;
        this.anoDemoBean();
    }

    private void anoDemoBean() {
        System.out.println(">>>>>>>" + anoDemoBean.genUUID(System.currentTimeMillis()));
    }
    
    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }
}
複製程式碼

輸出結果如下,在執行目標方法之前,會先執行before advice中的邏輯

AnoAspect 
in genUUID before process!
in genUUID finally!
>>>>>>>3a5d749d-d94c-4fc0-a7a3-12fd97f3e1fa|1551513443644
複製程式碼

2. 多個advice攔截

一個方法執行時,如果有多個advice滿足攔截規則,是所有的都會觸發麼?通過前面一篇博文知道,不同型別的advice是都可以攔截的,如果出現多個相同型別的advice呢?

在前面一篇博文的基礎上進行操作,我們擴充套件下com.git.hui.boot.aop.demo.DemoBean

@Component
public class DemoBean {
    @AnoDot
    public String genUUID(long time) {
        try {
            System.out.println("in genUUID before process!");
            return UUID.randomUUID() + "|" + time;
        } finally {
            System.out.println("in genUUID finally!");
        }
    }
}
複製程式碼

對應的測試切面內容如

@Aspect
@Component
public class AnoAspect {

    @Before("execution(public * com.git.hui.boot.aop.demo.*.*(*))")
    public void doBefore(JoinPoint joinPoint) {
        System.out.println("do in Aspect before method called! args: " + JSON.toJSONString(joinPoint.getArgs()));
    }

    @Pointcut("execution(public * com.git.hui.boot.aop.demo.*.*(*))")
    public void point() {
    }

    @After("point()")
    public void doAfter(JoinPoint joinPoint) {
        System.out.println("do in Aspect after method called! args: " + JSON.toJSONString(joinPoint.getArgs()));
    }

    /**
     * 執行完畢之後,通過 args指定引數;通過 returning 指定返回的結果,要求返回值型別匹配
     *
     * @param time
     * @param result
     */
    @AfterReturning(value = "point() && args(time)", returning = "result")
    public void doAfterReturning(long time, String result) {
        System.out.println("do in Aspect after method return! args: " + time + " ans: " + result);
    }

    @Around("point()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("do in Aspect around ------ before");
        Object ans = joinPoint.proceed();
        System.out.println("do in Aspect around ------- over! ans: " + ans);
        return ans;
    }

    @Before("point()")
    public void sameBefore() {
        System.out.println("SameAspect");
    }

    @Before("@annotation(AnoDot)")
    public void anoBefore() {
        System.out.println("AnoAspect");
    }
}
複製程式碼

測試程式碼如下

@SpringBootApplication
public class Application {
    private DemoBean demoBean;

    public Application(DemoBean demoBean) {
        this.demoBean = demoBean;
        this.demoBean();
    }

    private void demoBean() {
        System.out.println(">>>>> " + demoBean.genUUID(System.currentTimeMillis()));
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }
}
複製程式碼

輸出結果如下,所有的切面都執行了,也就是說,只要滿足條件的advice,都會被攔截到

do in Aspect around ------ before
AnoAspect
do in Aspect before method called! args: [1551520547268]
SameAspect
in genUUID before process!
in genUUID finally!
do in Aspect around ------- over! ans: 5f6a5616-f558-4ac9-ba4b-b4360d7dc238|1551520547268
do in Aspect after method called! args: [1551520547268]
do in Aspect after method return! args: 1551520547268 ans: 5f6a5616-f558-4ac9-ba4b-b4360d7dc238|1551520547268
>>>>> 5f6a5616-f558-4ac9-ba4b-b4360d7dc238|1551520547268
複製程式碼

3. 巢狀攔截

巢狀的方式有幾種case,先看第一種

a. 呼叫方法不滿足攔截規則,呼叫本類中其他滿足攔截條件的方法

這裡我們藉助第一節中的bean來繼續模擬, 在AnoDemoBean類中,新增一個方法

@Component
public class AnoDemoBean {

    public String randUUID(long time) {
        try {
            System.out.println("in randUUID start!");
            return genUUID(time);
        } finally {
            System.out.println("in randUUID finally!");
        }
    }

    @AnoDot
    public String genUUID(long time) {
        try {
            System.out.println("in genUUID before process!");
            return UUID.randomUUID() + "|" + time;
        } finally {
            System.out.println("in genUUID finally!");
        }
    }
}
複製程式碼

對應的切面為

@Aspect
@Component
public class NetAspect {

    @Around("@annotation(AnoDot)")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("In NetAspect doAround before!");
        Object ans = joinPoint.proceed();
        System.out.println("In NetAspect doAround over! ans: " + ans);
        return ans;
    }
}
複製程式碼

然後測試case需要改為直接呼叫 AnoDemoBean#randUUID,需要看這個方法內部呼叫的genUUID是否會被切面攔截住

@SpringBootApplication
public class Application {
    private AnoDemoBean anoDemoBean;

    public Application(AnoDemoBean anoDemoBean) {
        this.anoDemoBean = anoDemoBean;
        this.anoDemoBean();
    }

    private void anoDemoBean() {
        System.out.println(">>>>>>>" + anoDemoBean.randUUID(System.currentTimeMillis()));
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }
}
複製程式碼

輸出結果如下,沒有切面的日誌,表明這種場景下,不會被攔截

in randUUID start!
in genUUID before process!
in genUUID finally!
in randUUID finally!
>>>>>>>0c6a5ccf-30c0-4ac0-97f2-3dc063580f3d|1551522176035
複製程式碼

b. 呼叫方法不滿足攔截規則,呼叫其他類中滿足攔截條件的方法

依然使用前面的例子進行說明,不過是稍稍改一下AnoDemoBean,呼叫第二節中的DemoBean的方法

DemoBean的程式碼如下

@AnoDot
public String genUUID(long time) {
    try {
        System.out.println("in DemoBean genUUID before process!");
        return UUID.randomUUID() + "|" + time;
    } finally {
        System.out.println("in DemoBean genUUID finally!");
    }
}
複製程式碼

然後AnoDemoBean的程式碼如下

@Component
public class AnoDemoBean {
    @Autowired
    private DemoBean demoBean;

    public String randUUID(long time) {
        try {
            System.out.println("in AnoDemoBean randUUID start!");
            return genUUID(time) + "<<<>>>" + demoBean.genUUID(time);
        } finally {
            System.out.println("in AnoDemoBean randUUID finally!");
        }
    }

    @AnoDot
    public String genUUID(long time) {
        try {
            System.out.println("in AnoDemoBean genUUID before process!");
            return UUID.randomUUID() + "|" + time;
        } finally {
            System.out.println("in AnoDemoBean genUUID finally!");
        }
    }
}
複製程式碼

測試程式碼和前面完全一致,接下來看下輸出

in AnoDemoBean randUUID start!
in AnoDemoBean genUUID before process!
in AnoDemoBean genUUID finally!
### 上面三行為 anoDemoBean#randUUID方法呼叫 anoDemoBean#genUUID方法的輸出結果,可以看到沒有切面執行的日誌輸出
### 下面的為呼叫 demoBean#genUUID 方法,可以看到切面(NetAspect#doAround)執行的日誌
In NetAspect doAround before!
in DemoBean genUUID before process!
in DemoBean genUUID finally!
In NetAspect doAround over! ans: f35b8878-fbd0-4840-8fbe-5fef8eda5e31|1551522532092
### 最後是收尾
in AnoDemoBean randUUID finally!
>>>>>>>e516a35f-b85a-4cbd-aae0-fa97cdecab47|1551522532092<<<>>>f35b8878-fbd0-4840-8fbe-5fef8eda5e31|1551522532092
複製程式碼

從上面的日誌分析中,可以明確看出對比,呼叫本類中,滿足被攔截的方法,也不會走切面邏輯;呼叫其他類中的滿足切面攔截的方法,會走切面邏輯

c. 呼叫方法滿足切面攔截條件,又呼叫其他滿足切面攔截條件的方法

這個和兩個case有點像,不同的是直接呼叫的方法也滿足被切面攔截的條件,我們主要關注點在於巢狀呼叫的方法,會不會進入切面邏輯,這裡需要修改的地方就很少了,直接把 AnoDemoBean#randUUID方法上新增註解,然後執行即可

@Component
public class AnoDemoBean {
    @Autowired
    private DemoBean demoBean;

    @AnoDot
    public String randUUID(long time) {
        try {
            System.out.println("in AnoDemoBean randUUID start!");
            return genUUID(time) + "<<<>>>" + demoBean.genUUID(time);
        } finally {
            System.out.println("in AnoDemoBean randUUID finally!");
        }
    }

    @AnoDot
    public String genUUID(long time) {
        try {
            System.out.println("in AnoDemoBean genUUID before process!");
            return UUID.randomUUID() + "|" + time;
        } finally {
            System.out.println("in AnoDemoBean genUUID finally!");
        }
    }
}
複製程式碼

輸出結果如下

## 最外層的切面攔截的是 AnoDemoBean#randUUID 方法的執行
In NetAspect doAround before!
in AnoDemoBean randUUID start!
in AnoDemoBean genUUID before process!
in AnoDemoBean genUUID finally!
### 從跟上面三行的輸出,可以知道內部呼叫的 AnoDemoBean#genUUID 即便滿足切面攔截規則,也不會再次走切面邏輯
### 下面4行,表明其他類的方法,如果滿足切面攔截規則,會進入到切面邏輯
In NetAspect doAround before!
in DemoBean genUUID before process!
in DemoBean genUUID finally!
In NetAspect doAround over! ans: d9df7388-2ef8-4b1a-acb5-6639c47f36ca|1551522969801

in AnoDemoBean randUUID finally!
In NetAspect doAround over! ans: cf350bc2-9a9a-4ef6-b496-c913d297c960|1551522969801<<<>>>d9df7388-2ef8-4b1a-acb5-6639c47f36ca|1551522969801
>>>>>>>cf350bc2-9a9a-4ef6-b496-c913d297c960|1551522969801<<<>>>d9df7388-2ef8-4b1a-acb5-6639c47f36ca|1551522969801
複製程式碼

從輸出結果進行反推,一個結論是

  • 執行的目標方法,如果呼叫了本類中一個滿足切面規則的方法A時,在執行方法A的過程中,不會觸發切面邏輯
  • 執行的目標方法,如果呼叫其他類中一個滿足切面規則的方法B時,在執行方法B的過程中,將會觸發切面邏輯

4. AOP攔截方法作用域

前面測試的被攔截方法都是public,那麼是否表明只有public方法才能被攔截呢?

從第三節基本可以看出,private方法首先淘汰出列,為啥?因為private方法正常來講只能內部呼叫,而內部呼叫不會走切面邏輯;所以接下來需要關注的主要放在預設作用域和protected作用域

@Component
public class ScopeDemoBean {

    @AnoDot
    String defaultRandUUID(long time) {
        try {
            System.out.println(" in ScopeDemoBean defaultRandUUID before!");
            return UUID.randomUUID() + " | default | " + time;
        } finally {
            System.out.println(" in ScopeDemoBean defaultRandUUID finally!");
        }
    }

    @AnoDot
    protected String protectedRandUUID(long time) {
        try {
            System.out.println(" in ScopeDemoBean protectedRandUUID before!");
            return UUID.randomUUID() + " | protected | " + time;
        } finally {
            System.out.println(" in ScopeDemoBean protectedRandUUID finally!");
        }
    }

    @AnoDot
    private String privateRandUUID(long time) {
        try {
            System.out.println(" in ScopeDemoBean privateRandUUID before!");
            return UUID.randomUUID() + " | private | " + time;
        } finally {
            System.out.println(" in ScopeDemoBean privateRandUUID finally!");
        }
    }

}
複製程式碼

我們不直接使用這個類裡面的方法,藉助前面的 AnoDemoBean, 下面給出了通過反射的方式來呼叫private方法的case

@Component
public class AnoDemoBean {
    @Autowired
    private ScopeDemoBean scopeDemoBean;

    public void scopeUUID(long time) {
        try {
            System.out.println("-------- default --------");
            String defaultAns = scopeDemoBean.defaultRandUUID(time);
            System.out.println("-------- default: " + defaultAns + " --------\n");


            System.out.println("-------- protected --------");
            String protectedAns = scopeDemoBean.protectedRandUUID(time);
            System.out.println("-------- protected: " + protectedAns + " --------\n");


            System.out.println("-------- private --------");
            Method method = ScopeDemoBean.class.getDeclaredMethod("privateRandUUID", long.class);
            method.setAccessible(true);
            String privateAns = (String) method.invoke(scopeDemoBean, time);
            System.out.println("-------- private: " + privateAns + " --------\n");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
複製程式碼

測試case

@SpringBootApplication
public class Application {
    private AnoDemoBean anoDemoBean;

    public Application(AnoDemoBean anoDemoBean) {
        this.anoDemoBean = anoDemoBean;
        this.anoDemoBean();
    }

    private void anoDemoBean() {
        anoDemoBean.scopeUUID(System.currentTimeMillis());
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }
}
複製程式碼

輸出結果如下,從日誌列印來看,protected和default方法的切面都走到了

-------- default --------
In NetAspect doAround before!
 in ScopeDemoBean defaultRandUUID before!
 in ScopeDemoBean defaultRandUUID finally!
In NetAspect doAround over! ans: 2ad7e509-c62c-4f25-b68f-eb5e0b53196d | default | 1551524311537
-------- default: 2ad7e509-c62c-4f25-b68f-eb5e0b53196d | default | 1551524311537 --------

-------- protected --------
In NetAspect doAround before!
 in ScopeDemoBean protectedRandUUID before!
 in ScopeDemoBean protectedRandUUID finally!
In NetAspect doAround over! ans: 9eb339f8-9e71-4321-ab83-a8953d1b8ff8 | protected | 1551524311537
-------- protected: 9eb339f8-9e71-4321-ab83-a8953d1b8ff8 | protected | 1551524311537 --------

-------- private --------
 in ScopeDemoBean privateRandUUID before!
 in ScopeDemoBean privateRandUUID finally!
-------- private: 1826afac-6eca-4dc3-8edc-b4ca7146ce28 | private | 1551524311537 --------
複製程式碼

5. 小結

本篇博文篇幅比較長,主要是測試程式碼比較佔用地方,因此有必要簡單的小結一下,做一個清晰的歸納,方便不想看細節,只想獲取最終結論的小夥伴

註解攔截方式:

  • 首先宣告註解
  • 在目標方法上新增註解
  • 切面中,advice的內容形如 @Around("@annotation(AnoDot)")

多advice情況:

  • 多個advice滿足攔截場景時,全部都會執行

巢狀場景

  • 執行的目標方法,如果呼叫了本類中一個滿足切面規則的方法A時,在執行方法A的過程中,不會觸發切面邏輯
  • 執行的目標方法,如果呼叫其他類中一個滿足切面規則的方法B時,在執行方法B的過程中,將會觸發切面邏輯

作用域

  • public, protected, default 作用域的方法都可以被攔截

優先順序

這個內容因為特別多,所以有必要單獨拎出來,其主要的分類如下

  • 同一aspect,不同advice的執行順序
  • 不同aspect,advice的執行順序
  • 同一aspect,相同advice的執行順序

II. 其他

0. 專案

1. 一灰灰Blog

一灰灰的個人部落格,記錄所有學習和工作中的博文,歡迎大家前去逛逛

2. 宣告

盡信書則不如,以上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現bug或者有更好的建議,歡迎批評指正,不吝感激

3. 掃描關注

一灰灰blog

QrCode

知識星球

goals

相關文章