AOP隨筆

小白先生哦發表於2021-05-24

AOP: Aspect-Oriented Programming 面向切面程式設計。

首先明確一個點:AOP是一個概念。那麼對於一個概念,其實現方式多種多樣,分為靜態AOP、動態AOP,而對於動態AOP的實現又分為動態代理和動態位元組碼增強實現。

這裡分享的AOP主要是基於Spring的AOP實現。

AOP的一些基礎概念

  • 連線點(Joinpoint):程式執行的某個特定位置,比如方法呼叫前、方法呼叫後、方法丟擲異常後等,這些點都可以是連線點。

  • 切點(PointCut):一個程式有若干個連線點,並不是所有的連線點都是需要關注的,需要我們關注的那部分連線點就是切點。

  • 通知(增強)(Advice):就是在切點處要插入的邏輯;分為前置、後置、異常、返回、環繞通知;

  • 切面(Advice):切點和通知組合在一起形成切面,對應的切面橫切整個應用程式。切面實現了橫切關注點的模組化。

  • 目標物件(Target):需要被增強的業務物件。

  • 織入(Weaving):將通知增強新增到目標物件的具體連線點的過程。具體來說,就是生成代理物件並將切面融入到業務流程的過程。

  • 代理類(Proxy):一個類在被織入器織入增強邏輯後產生的一個類就是代理類。

上面這些點在Spring中的體現就是一個一個的註解。

Spring AOP的使用方式

比如我們現在有一個除法計算類,這個方法計算可能會拋錯,在拋錯的時候我們需要記錄一下日誌,那麼使用方式如下:

(說明:很多地方在介紹AOP的時候,都喜歡以日誌來當做案例,但是日誌記錄並不是一個好的AOP使用場景,因為系統中各個日誌記錄點和記錄格式不具有通用性。)

  • 首先定義實現方法:

    public class MathCalculator {
    
        public int div(int i, int j) {
            return i / j;
        }
    }
    
  • 然後定義切面:

    @Aspect
    public class LogAspects {
    
        @Pointcut("execution(public int MathCalculator.*(..))")
        public void poinCut() {
        }
    
        @Around("poinCut()")
        public Object logArround(ProceedingJoinPoint joinPoint) throws Throwable {
            System.out.println("環繞開始!");
            try {
                return joinPoint.proceed();
            } catch (Exception e) {
                System.out.println("環繞異常!");
                throw e;
            } finally {
                System.out.println("環繞結束!");
            }
        }
    }
    

我們在切面中定義了一個環繞通知,這樣就對MathCalculator的div方法呼叫進行了環繞處理。

Spring AOP的實現方式

上面提到動態AOP有兩種實現方式:動態代理和動態位元組碼增強。

在Spring中主要是通過動態代理實現的:基於JDK的動態代理和基於CGLIB的動態代理。

要了解Spring AOP的實現原理,只要把上面兩個技術瞭解後就能明白相應的原理了。

JDK動態代理

  • 1、JDK動態代理是基於介面的,所以我們首先來建立一個介面

    /**
     * 抽象主題介面
     **/
    public interface Subject {
    
        void doSomething();
    }
    
  • 2、建立對應介面的實現類

    /**
     * 真實主題類
     **/
    public class RealSubject implements Subject {
        @Override
        public void doSomething() {
            System.out.println("doSomething....");
        }
    }
    
  • 3、建立代理類,實現 java.lang.reflect.InvocationHandler 介面

    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    
    /**
     * jdkd動態代理類
     **/
    public class JDKDynamicProxy implements InvocationHandler {
    
        private Object target;
    
        public JDKDynamicProxy(Object target) {
            this.target = target;
        }
    
        /**
         * 獲取被代理介面例項物件
         */
        public <T> T getProxy() {
            return (T) Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("Do something before");
            Object result = method.invoke(target, args);
            System.out.println("Do something after");
            return result;
        }
    }
    
  • 4、建立測試類

    import com.lnjecit.proxy.dynamic.jdk.JDKDynamicProxy;
    
    /**
     * 測試類
     **/
    public class Ceshi {
        public static void main(String[] args) {
            Subject subject = new JDKDynamicProxy(new RealSubject()).getProxy();
            subject.doSomething();
        }
    }
    

像這樣執行Ceshi類的main方法就能發現對應的doSomething()方法被我們的邏輯進行了攔截。

  • 5、動態代理原理

    我們開啟生成的代理類,可以看到內容如下:

    import com.lnjecit.proxy.Subject;
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    import java.lang.reflect.UndeclaredThrowableException;
    
    public final class $Proxy0 extends Proxy implements Subject {
        private static Method m1;
        private static Method m3;
        private static Method m2;
        private static Method m0;
    
        public $Proxy0(InvocationHandler var1) throws  {
            super(var1);
        }
        
        public final void doSomething() throws  {
            try {
                super.h.invoke(this, m3, (Object[])null);
            } catch (RuntimeException | Error var2) {
                throw var2;
            } catch (Throwable var3) {
                throw new UndeclaredThrowableException(var3);
            }
        }
        
        // 為了精簡程式碼,下面省略了重寫的 equals、hashCode、toString、toString 方法
    
        static {
            try {
                m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
                m3 = Class.forName("com.lnjecit.proxy.Subject").getMethod("doSomething");
                m2 = Class.forName("java.lang.Object").getMethod("toString");
                m0 = Class.forName("java.lang.Object").getMethod("hashCode");
            } catch (NoSuchMethodException var2) {
                throw new NoSuchMethodError(var2.getMessage());
            } catch (ClassNotFoundException var3) {
                throw new NoClassDefFoundError(var3.getMessage());
            }
        }
    }
    

    可以看到,動態生成的代理類有如下特性:

    1、繼承了Proxy類,實現了代理的介面,由於java不能多繼承,這裡已經繼承了Proxy類了,不能再繼承其他的類,所以JDK的動態代理不支援對實現類的代理,只支援介面的代理。

    2、提供了一個使用InvocationHandler作為引數的構造方法。

    3、生成靜態程式碼塊來初始化介面中方法的Method物件,以及Object類的equals、hashCode、toString方法。

    4、重寫了Object類的equals、hashCode、toString,它們都只是簡單的呼叫了InvocationHandler的invoke方法,即可以對其進行特殊的操作,也就是說JDK的動態代理還可以代理上述三個方法。

    5、代理類實現代理介面的sayHello方法中,只是簡單的呼叫了InvocationHandler的invoke方法,我們可以在invoke方法中進行一些特殊操作,甚至不呼叫實現的方法,直接返回。

    上面的InvocationHandler就是我們實現橫切邏輯的地方,它是橫切邏輯的載體。

CGLIB動態代理

CGLIB使用如下:

  • 1、定義代理目標類

    public class UserService {
        public String getUserName(Long userId) {
            System.out.println("獲取使用者名稱..");
            return "user" + userId;
        }
    }
    
  • 2、實現MethodInterceptor,定義事務攔截器

    public class TransactionInterceptor implements MethodInterceptor {
        Object target;
    
        public TransactionInterceptor(Object target) {
            this.target = target;
        }
    
        /**
         * proxy:代理物件,CGLib動態生成的代理類例項
         * method:目標物件的方法,上文中實體類所呼叫的被代理的方法引用
         * args:目標物件方法的引數列表,引數值列表
         * methodProxy:代理物件的方法,生成的代理類對方法的代理引用
         */
        @Override
        public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
            System.out.println("開啟事務..." + proxy.getClass().getSimpleName());
            Object objValue = null;
            try {
                // 反射呼叫目標類方法
                objValue = method.invoke(target, args);
                System.out.println("返回值為:" + objValue);
            } catch (Exception e) {
                System.out.println("呼叫異常!" + e.getMessage());
            } finally {
                System.out.println("呼叫結束,關閉事務...");
            }
            return objValue;
        }
    
        /**
         * 獲取代理例項
         */
        public Object getTargetProxy() {
            // Enhancer類是cglib中的一個位元組碼增強器,它可以方便的為你所要處理的類進行擴充套件
            Enhancer eh = new Enhancer();
            // 1.將目標物件所在的類作為Enhancer類的父類
            eh.setSuperclass(target.getClass());
            // 2.通過實現MethodInterceptor實現方法回撥
            eh.setCallback(this);
            // 3. 建立代理例項
            return eh.create();
        }
    }
    
  • 3、使用

    public static void main(String[] args) {
        // 1. 建立目標例項
        UserService userService = new UserService();
        // 2. 建立事務攔截器
        TransactionInterceptor transactionInterceptor = new TransactionInterceptor(userService);
        // 3. 建立代理例項
        UserService userServiceProxy = (UserService) transactionInterceptor.getTargetProxy();
        // 4. 使用代理例項呼叫目標方法
        userServiceProxy.getUserName(6L);
    }
    

說明:CGLIB是基於繼承來實現的。它首先建立一個類,繼承代理目標類,然後重寫其中的方法,然後在重寫的方法中將呼叫委託給目標物件,然後在委託呼叫前後實現橫切邏輯。

通過上面兩個例子可以看出,JDK的動態代理不是通過繼承實現的,那麼就是通過實現介面實現的。而CGLIB則是通過動態生成代理類的子類來實現的。