1. 程式人生 > >SpringBoot 通過自定義註解實現AOP切面程式設計例項

SpringBoot 通過自定義註解實現AOP切面程式設計例項

一直心心念的想寫一篇關於AOP切面例項的博文,拖更了許久之後,今天終於著手下筆將其完成。

基礎概念

1、切面(Aspect)

首先要理解‘切’字,需要把物件想象成一個立方體,傳統的面向物件變成思維,類定義完成之後(封裝)。每次例項化一個物件,對類定義中的成員變數賦值,就相當於對這個立方體進行了一個定義,定義完成之後,那個物件就在那裡,不卑不亢,不悲不喜,等著被使用,等著被回收。

面向切面程式設計則是指,對於一個我們已經封裝好的類,我們可以在編譯期間或在執行期間,對其進行切割,把立方體切開,在原有的方法裡面新增(織入)一些新的程式碼,對原有的方法程式碼進行一次增強處理。而那些增強部分的程式碼,就被稱之為切面,如下面程式碼例項中的通用日誌處理程式碼,常見的還有事務處理、許可權認證等等。

2、切入點(PointCut)

要對哪些類中的哪些方法進行增強,進行切割,指的是被增強的方法。即要切哪些東西。

3、連線點(JoinPoint)

我們知道了要切哪些方法後,剩下的就是什麼時候切,在原方法的哪一個執行階段加入增加程式碼,這個就是連線點。如方法呼叫前,方法呼叫後,發生異常時等等。

4、通知(Advice)

通知被織入方法,改如何被增強。定義切面的具體實現。那麼這裡面就涉及到一個問題,空間(切哪裡)和時間(什麼時候切,在何時加入增加程式碼),空間我們已經知道了就是切入點中定義的方法,而什麼時候切,則是連線點的概念,如下面例項中,通用日誌處理(切面),@Pointcut規則中指明的方法即為切入點,@Before、@After是連線點,而下面的程式碼就是對應通知。

@Before("cutMethod()")
public void begin() {
    System.out.println("[email protected]== lingyejun blog logger : begin");
}

5、目標物件(Target Object)

被一個或多個切面所通知的物件,即為目標物件。

6、AOP代理物件(AOP Proxy Object)

AOP代理是AOP框架所生成的物件,該物件是目標物件的代理物件。代理物件能夠在目標物件的基礎上,在相應的連線點上呼叫通知。

7、織入(Weaving)

將切面切入到目標方法之中,使目標方法得到增強的過程被稱之為織入。

例項程式碼

相關依賴包

<dependencies>
        <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjrt</artifactId>
        <version>1.8.6</version>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.8.6</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>4.3.6.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-autoconfigure</artifactId>
        <version>1.3.8.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>4.2.8.RELEASE</version>
        <scope>test</scope>
    </dependency>
</dependencies>

定義和實現日誌切面

package com.lingyejun.annotation;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * @Author: Lingye
 * @Date: 2018/11/11
 * @Describe:
 * 定義日誌切面
 * @Lazy 註解:容器一般都會在啟動的時候例項化所有單例項 bean,如果我們想要 Spring 在啟動的時候延遲載入 bean,需要用到這個註解
 * value為true、false 預設為true,即延遲載入,@Lazy(false)表示物件會在初始化的時候建立
 *
 * @Modified By:
 */
@Aspect
@Component
@Lazy(false)
public class LoggerAspect {

    /**
     * 定義切入點:對要攔截的方法進行定義與限制,如包、類
     *
     * 1、execution(public * *(..)) 任意的公共方法
     * 2、execution(* set*(..)) 以set開頭的所有的方法
     * 3、execution(* com.lingyejun.annotation.LoggerApply.*(..))com.lingyejun.annotation.LoggerApply這個類裡的所有的方法
     * 4、execution(* com.lingyejun.annotation.*.*(..))com.lingyejun.annotation包下的所有的類的所有的方法
     * 5、execution(* com.lingyejun.annotation..*.*(..))com.lingyejun.annotation包及子包下所有的類的所有的方法
     * 6、execution(* com.lingyejun.annotation..*.*(String,?,Long)) com.lingyejun.annotation包及子包下所有的類的有三個引數,第一個引數為String型別,第二個引數為任意型別,第三個引數為Long型別的方法
     * 7、execution(@annotation(com.lingyejun.annotation.Lingyejun))
     */
    @Pointcut("@annotation(com.lingyejun.annotation.Lingyejun)")
    private void cutMethod() {

    }

    /**
     * 前置通知:在目標方法執行前呼叫
     */
    @Before("cutMethod()")
    public void begin() {
        System.out.println("[email protected]== lingyejun blog logger : begin");
    }

    /**
     * 後置通知:在目標方法執行後呼叫,若目標方法出現異常,則不執行
     */
    @AfterReturning("cutMethod()")
    public void afterReturning() {
        System.out.println("[email protected]== lingyejun blog logger : after returning");
    }

    /**
     * 後置/最終通知:無論目標方法在執行過程中出現一場都會在它之後呼叫
     */
    @After("cutMethod()")
    public void after() {
        System.out.println("[email protected]== lingyejun blog logger : finally returning");
    }

    /**
     * 異常通知:目標方法丟擲異常時執行
     */
    @AfterThrowing("cutMethod()")
    public void afterThrowing() {
        System.out.println("[email protected]== lingyejun blog logger : after throwing");
    }

    /**
     * 環繞通知:靈活自由的在目標方法中切入程式碼
     */
    @Around("cutMethod()")
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {
        // 獲取目標方法的名稱
        String methodName = joinPoint.getSignature().getName();
        // 獲取方法傳入引數
        Object[] params = joinPoint.getArgs();
        Lingyejun lingyejun = getDeclaredAnnotation(joinPoint);
        System.out.println("[email protected]== lingyejun blog logger --》 method name " + methodName + " args " + params[0]);
        // 執行源方法
        joinPoint.proceed();
        // 模擬進行驗證
        if (params != null && params.length > 0 && params[0].equals("Blog Home")) {
            System.out.println("[email protected]== lingyejun blog logger --》 " + lingyejun.module() + " auth success");
        } else {
            System.out.println("[email protected]== lingyejun blog logger --》 " + lingyejun.module() + " auth failed");
        }
    }

    /**
     * 獲取方法中宣告的註解
     *
     * @param joinPoint
     * @return
     * @throws NoSuchMethodException
     */
    public Lingyejun getDeclaredAnnotation(ProceedingJoinPoint joinPoint) throws NoSuchMethodException {
        // 獲取方法名
        String methodName = joinPoint.getSignature().getName();
        // 反射獲取目標類
        Class<?> targetClass = joinPoint.getTarget().getClass();
        // 拿到方法對應的引數型別
        Class<?>[] parameterTypes = ((MethodSignature) joinPoint.getSignature()).getParameterTypes();
        // 根據類、方法、引數型別(過載)獲取到方法的具體資訊
        Method objMethod = targetClass.getMethod(methodName, parameterTypes);
        // 拿到方法定義的註解資訊
        Lingyejun annotation = objMethod.getDeclaredAnnotation(Lingyejun.class);
        // 返回
        return annotation;
    }
}

自定義一個註解

package com.lingyejun.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @Author: Lingye
 * @Date: 2018/11/11
 * @Describe: 
 * @Modified By:
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Lingyejun {

    /**
     * 何種場景下的通用日誌列印
     *
     * @return
     */
    String module();
}  

呼叫切面類

package com.lingyejun.annotation;

import org.springframework.stereotype.Component;

/**
 * @Author: Lingye
 * @Date: 2018/11/11
 * @Describe:
 * @Modified By:
 */
@Component
public class LoggerApply {

    @Lingyejun(module = "http://www.cnblogs.com/lingyejun/")
    public void lingLogger(String event) throws Exception {
        System.out.println("lingLogger(String event) : lingyejun will auth by blog address");
        throw new Exception();
    }
}  

測試程式碼

package com.lingyejun.annotation;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
public class AnnotationTest {

    @Autowired
    private LoggerApply loggerApply;

    @Test
    public void testAnnotationLogger() {
        try {
            loggerApply.lingLogger("Blog Home");
        } catch (Exception e) {
            System.out.println("a exception be there");
        }
    }
}

效果展示