1. 程式人生 > >面向切面程式設計(3):AOP實現機制

面向切面程式設計(3):AOP實現機制

1 AOP各種的實現

  AOP就是面向切面程式設計,我們可以從幾個層面來實現AOP,如下圖。

圖1 AOP實現的不同層面

  在編譯器修改原始碼,在執行期位元組碼載入前修改位元組碼或位元組碼載入後動態建立代理類的位元組碼,以下是各種實現機制的比較。 

類別

機制

原理

優點

缺點

靜態AOP

靜態織入

在編譯期,切面直接以位元組碼的形式編譯到目標位元組碼檔案中。

對系統無效能影響。

靈活性不夠。

動態AOP

動態代理

在執行期,目標類載入後,為介面動態生成代理類,將切面植入到代理類中。

相對於靜態AOP更加靈活。

切入的關注點需要實現介面。對系統有一點效能影響。

動態位元組碼生成

在執行期,目標類載入後,動態構建位元組碼檔案生成目標類的子類,將切面邏輯加入到子類中。

沒有介面也可以織入(Weave)。

擴充套件類的例項方法為final時,則無法進行織入。

自定義類載入器

在執行期,目標載入前,將切面邏輯加到目標位元組碼裡。

可以對絕大部分類進行織入。

程式碼中如果使用了其他類載入器,則這些類將不會被織入。

位元組碼轉換

在執行期,所有類載入器載入位元組碼前,前進行攔截。

可以對所有類進行織入。

2 AOP裡的公民

  • Joinpoint:連線點,即攔截點,如某個業務方法。
  • Pointcut:切入點,Joinpoint的表示式,表示攔截哪些方法。一個Pointcut對應多個Joinpoint。
  • Advice:通知,要切入的邏輯。
  • Before Advice 在方法前切入。
  • After Advice 在方法後切入,丟擲異常時也會切入。
  • After Returning Advice 在方法返回後切入,丟擲異常則不會切入。
  • After Throwing Advice 在方法丟擲異常時切入。
  • Around Advice 在方法執行前後切入,可以中斷或忽略原有流程的執行。
  • 各公民之間的關係

    圖2 AOP概念之間的關係

  • 織入器(Weaver)通過在切面中定義pointcut來搜尋目標(被代理類)的JoinPoint(切入點),然後把要切入的邏輯(Advice)織入到目標物件裡,生成代理類。

3 AOP的實現機制

   本章節將詳細介紹AOP有各種實現機制。


3.1 動態代理

  Java在JDK1.3後引入的動態代理機制,使我們可以在執行期動態的建立代理類。使用動態代理實現AOP需要有四個角色:被代理的類,被代理類的介面,織入器,和InvocationHandler切面,而織入器使用介面反射機制生成一個代理類,然後在這個代理類中織入程式碼。被代理的類是AOP裡所說的目標,InvocationHandler是切面,它包含了Advice和Pointcut。

圖3 JDK動態代理

  那如何使用動態代理來實現AOP。下面的例子演示在方法執行前織入一段記錄日誌的程式碼,其中Business是代理類,LogInvocationHandler是記錄日誌的切面,IBusiness, IBusiness2是代理類的介面,Proxy.newProxyInstance是織入器。
清單一:動態代理的演示

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

// 介面IBusiness和IBusiness2定義省略

// 業務類,需要代理的類
class Business implements IBusiness, IBusiness2 {

    @Override
    public boolean doSomeThing() {
        System.out.println("執行業務邏輯");
        return true;
    }

    @Override
    public void doSomeThing2() {
        System.out.println("執行業務邏輯2");
    }

}

public class DynamicProxyDemo {

    public static void main(String[] args) {
        //需要代理的介面,被代理類實現的多個介面都必須在這裡定義   
        Class[] proxyInterface = new Class[]{IBusiness.class, IBusiness2.class};
        //構建AOP的Advice,這裡需要傳入業務類的例項   
        LogInvocationHandler handler = new LogInvocationHandler(new Business());
        //生成代理類的位元組碼載入器   
        ClassLoader classLoader = DynamicProxyDemo.class.getClassLoader();
        //織入器,織入程式碼並生成代理類   
        IBusiness2 proxyBusiness = (IBusiness2) Proxy.newProxyInstance(classLoader, proxyInterface, handler);
        //使用代理類的例項來呼叫方法。   
        proxyBusiness.doSomeThing2();
        ((IBusiness) proxyBusiness).doSomeThing();
    }
}

/**
 * 列印日誌的切面
 */
class LogInvocationHandler implements InvocationHandler {

    private Object target; //目標物件

    LogInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //執行原有邏輯   
        Object rev = method.invoke(target, args);
        //執行織入的日誌,你可以控制哪些方法執行切入邏輯   
        if (method.getName().equals("doSomeThing2")) {
            System.out.println("記錄日誌");
        }
        return rev;
    }
}
  輸出:
執行業務邏輯2   
記錄日誌   
執行業務邏輯 
  可以看到“記錄日誌”的邏輯切入到Business類的doSomeThing方法前了。

  下面將結合JDK動態代理的原始碼講解其實現原理。動態代理的核心其實就是代理物件的生成,即Proxy.newProxyInstance(classLoader, proxyInterface, handler)。讓我們進入newProxyInstance方法觀摩下,核心程式碼其實就三行。
清單二:生成代理類

//獲取代理類   
Class cl = getProxyClass(loader, interfaces);   
//獲取帶有InvocationHandler引數的構造方法   
Constructor cons = cl.getConstructor(constructorParams);   
//把handler傳入構造方法生成例項   
return (Object) cons.newInstance(new Object[] { h });  
  其中getProxyClass(loader, interfaces)方法用於獲取代理類,它主要做了三件事情:在當前類載入器的快取裡搜尋是否有代理類,沒有則生成代理類並快取在本地JVM裡。

清單三:查詢代理類

    // 快取的key使用介面名稱生成的List   
    Object key = Arrays.asList(interfaceNames);   
    synchronized (cache) {   
        do {   
            Object value = cache.get(key);   
            // 快取裡儲存了代理類的引用   
            if (value instanceof Reference) {   
                proxyClass = (Class) ((Reference) value).get();   
            }   
            if (proxyClass != null) {   
                // 代理類已經存在則返回   
                return proxyClass;   
            } else if (value == pendingGenerationMarker) {   
                // 如果代理類正在產生,則等待   
                try {   
                    cache.wait();   
                } catch (InterruptedException e) {   
                }   
                continue;   
            } else {   
                //沒有代理類,則標記代理準備生成   
                cache.put(key, pendingGenerationMarker);   
                break;   
            }   
        } while (true);   
    }
  代理類的生成主要是以下這兩行程式碼。

清單四:生成並載入代理類

//生成代理類的位元組碼檔案並儲存到硬碟中(預設不儲存到硬碟)   
proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces);   
//使用類載入器將位元組碼載入到記憶體中   
proxyClass = defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length);
  ProxyGenerator.generateProxyClass()方法屬於sun.misc包下,Oracle並沒有提供原始碼,但是我們可以使用JD-GUI這樣的反編譯軟體開啟jre\lib\rt.jar來一探究竟,以下是其核心程式碼的分析。
清單五:代理類的生成過程
    //新增介面中定義的方法,此時方法體為空   
    for (int i = 0; i < this.interfaces.length; i++) {   
      localObject1 = this.interfaces[i].getMethods();   
      for (int k = 0; k < localObject1.length; k++) {   
         addProxyMethod(localObject1[k], this.interfaces[i]);   
      }   
    }   
      
    //新增一個帶有InvocationHandler的構造方法   
    MethodInfo localMethodInfo = new MethodInfo("<init>", "(Ljava/lang/reflect/InvocationHandler;)V", 1);   
      
    //迴圈生成方法體程式碼(省略)   
    //方法體裡生成呼叫InvocationHandler的invoke方法程式碼。(此處有所省略)   
    this.cp.getInterfaceMethodRef("InvocationHandler", "invoke", "Object; Method; Object;")   
      
    //將生成的位元組碼,寫入硬碟,前面有個if判斷,預設情況下不儲存到硬碟。   
    localFileOutputStream = new FileOutputStream(ProxyGenerator.access$000(this.val$name) + ".class");   
    localFileOutputStream.write(this.val$classFile);   
  那麼通過以上分析,我們可以推出動態代理為我們生成了一個這樣的代理類。把方法doSomeThing的方法體修改為呼叫LogInvocationHandler的invoke方法。下面是Proxy.newProxyInstance為Business類生成的代理類。
清單六:生成的代理類原始碼
public class ProxyBusiness implements IBusiness, IBusiness2 {

    private LogInvocationHandler h;

    @Override
    public void doSomeThing2() {
        try {
            Method m = (h.target).getClass().getMethod("doSomeThing", null);
            h.invoke(this, m, null);
        } catch (Throwable e) {
            // 異常處理(略)   
        }
    }

    @Override
    public boolean doSomeThing() {
        try {
            Method m = (h.target).getClass().getMethod("doSomeThing2", null);
            return (Boolean) h.invoke(this, m, null);
        } catch (Throwable e) {
            // 異常處理(略)   
        }
        return false;
    }

    public ProxyBusiness(LogInvocationHandler h) {
        this.h = h;
    }

    //測試用   
    public static void main(String[] args) {
        //構建AOP的Advice   
        LogInvocationHandler handler = new LogInvocationHandler(new Business());
        new ProxyBusiness(handler).doSomeThing();
        new ProxyBusiness(handler).doSomeThing2();
    }
}
  從前兩節的分析我們可以看出,動態代理在執行期通過介面動態生成代理類,這為其帶來了一定的靈活性,但這個靈活性卻帶來了兩個問題,第一代理類必須實現一個介面,如果沒實現介面會丟擲一個異常。第二效能影響,因為動態代理使用反射的機制實現的,首先反射肯定比直接呼叫要慢,經過測試大概每個代理類比靜態代理多出10幾毫秒的消耗。其次使用反射大量生成類檔案可能引起Full GC造成效能影響,因為位元組碼檔案載入後會存放在JVM執行時區的方法區(或者叫持久代)中,當方法區滿的時候,會引起Full GC,所以當你大量使用動態代理時,可以將持久代設定大一些,減少Full GC次數。

3.2 動態位元組碼生成


  使用動態位元組碼生成技術實現AOP原理是在執行期間目標位元組碼載入後,生成目標類的子類,將切面邏輯加入到子類中,所以使用Cglib庫實現AOP不需要基於介面。


圖4 動態位元組碼生成

  下面介紹如何使用Cglib來實現動態位元組碼技術。Cglib是一個強大的、高效能的Code生成類庫,它可以在執行期間擴充套件Java類和實現Java介面,它封裝了Asm。

清單七:使用CGLib實現AOP

import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

// 類Business,可以不實現任何介面

public class CglibAopDemo {

    public static void main(String[] args) {
        byteCodeGe();
    }

    public static void byteCodeGe() {
        //建立一個織入器   
        Enhancer enhancer = new Enhancer();
        //設定父類   
        enhancer.setSuperclass(Business.class);
        //設定需要織入的邏輯   
        enhancer.setCallback(new LogIntercept());
        //使用織入器建立子類   
        Business newBusiness = (Business) enhancer.create();
        newBusiness.doSomeThing2();
    }

    /**
     * 記錄日誌
     */
    public static class LogIntercept implements MethodInterceptor {

        @Override
        public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            //執行原有邏輯,注意這裡是invokeSuper   
            Object rev = proxy.invokeSuper(target, args);
            //執行織入的日誌   
            if (method.getName().equals("doSomeThing2")) {
                System.out.println("記錄日誌");
            }
            return rev;
        }
    }
}
  這裡目標類是Busniess(無需從介面繼承);切面(即攔截器)是實現MethodInterceptor介面的類LogIntercept,用來記錄日誌;織入器是Enhancer。

3.3 自定義類載入器


  如果我們實現了一個自定義類載入器,在類載入到JVM之前直接修改某些類的方法,並將切入邏輯織入到這個方法裡,然後將修改後的位元組碼檔案交給虛擬機器執行,那豈不是更直接。

圖5 自定義類載入器

  Javassist是一個編輯位元組碼的框架,可以讓你很簡單地操作位元組碼。它可以在執行期定義或修改Class。使用Javassist實現AOP的原理是在位元組碼載入前直接修改需要切入的方法。這比使用Cglib實現AOP更加高效,並且沒太多限制,實現原理如下圖:

圖6 自定義類載入器實現原理

  我們使用系統類載入器啟動我們自定義的類載入器,在這個類載入器里加一個類載入監聽器,監聽器發現目標類被載入時就織入切入邏輯,咱們再看看使用Javassist實現AOP的程式碼:
清單八:啟動自定義的類載入器

package aopexample;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.Loader;
import javassist.NotFoundException;
import javassist.Translator;

public class JavassistAopDemo {

    public static void main(String[] args) throws NotFoundException, Throwable {
        //獲取存放CtClass的容器ClassPool   
        ClassPool cp = ClassPool.getDefault();
        //建立一個類載入器   
        Loader cl = new Loader();
        //增加一個轉換器   
        cl.addTranslator(cp, new MyTranslator());
        //啟動MyTranslator的main函式   
        cl.run("aopexample.JavassistAopDemo$MyTranslator", args);
    }

    // 內嵌類,轉換器
    public static class MyTranslator implements Translator {

        @Override
        public void start(ClassPool pool) throws NotFoundException,
                CannotCompileException {
        }

        /**
         * 類裝載到JVM前進行程式碼織入
         */
        @Override
        public void onLoad(ClassPool pool, String classname) {
            if (!"aopexample.Business".equals(classname)) {
                return;
            }
            //通過獲取類檔案   
            try {
                CtClass cc = pool.get(classname);
                //獲得指定方法名的方法   
                CtMethod m = cc.getDeclaredMethod("doSomeThing");
                //在方法執行前插入程式碼   
                m.insertBefore("{ System.out.println(\"記錄日誌\"); }");
            } catch (NotFoundException | CannotCompileException e) {
            }
        }

        public static void main(String[] args) {
            Business b = new Business();
            b.doSomeThing2();
            b.doSomeThing();
        }
    }
}

class Business {

    public boolean doSomeThing() {
        System.out.println("執行業務邏輯");
        return true;
    }

    public void doSomeThing2() {
        System.out.println("執行業務邏輯2");
    }
}
  輸出:
執行業務邏輯2   
記錄日誌   
執行業務邏輯
  這裡轉換器MyTranslator是一個切面,用作類載入監聽器。看起來是不是特別簡單,CtClass是一個class檔案的抽象描述。這裡先獲取一個類容器cp和一個類載入器c1,然後給c1增加一個監聽器。用c1載入的所有類都會放到cp容器中。當用c1載入類aopexample.JavassistAopDemo$MyTranslator並執行其中的main方法時,隨後載入aopexample.Business類。每載入一個類到JVM之前,監聽器裡的onLoad()觸發,它可以給類中的方法織入一段程式碼。咱們也可以使用insertAfter()在方法的末尾插入程式碼,使用insertAt()在指定行插入程式碼。

  從本節中可知,使用自定義的類載入器實現AOP在效能上要優於動態代理和Cglib,因為它不會產生新類,但是它仍然存在一個問題,就是如果其他的類載入器來載入類的話,這些類將不會被攔截。

3.4 位元組碼轉換

  自定義的類載入器實現AOP只能攔截自己載入的位元組碼,那麼有沒有一種方式能夠監控所有類載入器載入位元組碼呢?有,使用Instrumentation,它是 Java 5 提供的新特性,使用Instrumentation,開發者可以構建一個位元組碼轉換器,在位元組碼載入前進行轉換。本節使用Instrumentation和實現AOP(在方法執行插入程式碼要用到javassist)。

  使用Instrumentation,開發者可以構建一個獨立於應用程式的代理程式(Agent),用來監測和協助執行在JVM上的程式,甚至能夠替換和修改某些類的定義。有了這樣的功能,開發者就可以實現更為靈活的執行時虛擬機器監控和Java類操作了,這樣的特性實際上提供了一種虛擬機器級別支援的AOP實現方式,使得開發者無需對JDK做任何升級和改動,就可以實現某些AOP的功能了。
  開發者可以讓Instrumentation代理在main函式執行前執行。簡要說來就是如下幾個步驟:

  (1)編寫premain函式。編寫一個Java類,包含如下兩個方法當中的任何一個。

public static void premain(String agentArgs, Instrumentation inst);  [1]
public static void premain(String agentArgs); [2]
  其中,[1]的優先順序比[2] 高,將會被優先執行。在這個premain函式中,開發者可以進行對類的各種操作。agentArgs是premain函式得到的程式引數,隨同 “– javaagent”一起傳入。與main函式不同的是,這個引數是一個字串而不是一個字串陣列,如果程式引數有多個,程式將自行解析這個字串。Inst 是一個java.lang.instrument.Instrumentation的例項,由 JVM 自動傳入。用它來註冊轉換器監控代理,在JVM啟動main函式之前進行攔截,切入我們需要執行的邏輯。

  (2)jar檔案打包。將這個Java類打包成一 jar檔案,並在其中的manifest屬性當中加入” Premain-Class”來指定步驟1當中編寫的那個帶有premain的Java 類。

  (3)執行。執行Java程式時增加如下的JVM啟動引數:java -javaagent:<jar檔案的位置> [= 傳入premain的引數]

  首先需要建立位元組碼轉換器,使用java.lang.instrument.ClassFileTransformer。該轉換器負責攔截Business類,並在Business類的doSomeThing方法前使用javassist加入記錄日誌的程式碼。下面是完整程式碼:

清單九:使用JDK Instrument實現位元組碼轉換

package instrumentationexample;

import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.NotFoundException;

public class MyClassFileTransformer implements ClassFileTransformer {

    /**
     * 位元組碼載入到虛擬機器前會進入這個方法
     */
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
            ProtectionDomain protectionDomain, byte[] classfileBuffer)
            throws IllegalClassFormatException {
        System.out.println(className);
        //如果載入Business類才攔截   
        if (!"instrumentationexample/Business".equals(className)) {
            return null;
        }
        //javassist的包名是用點分割的,需要轉換下   
        if (className.contains("/")) {
            className = className.replaceAll("/", ".");
        }
        try {
            //通過包名獲取類檔案   
            CtClass cc = ClassPool.getDefault().get(className);
            //獲得指定方法名的方法   
            CtMethod m = cc.getDeclaredMethod("doSomeThing");
            //在方法執行前插入程式碼   
            m.insertBefore("{ System.out.println(\"記錄日誌\"); }");
            return cc.toBytecode();
        } catch (NotFoundException | CannotCompileException | IOException e) {
            //忽略異常處理
        }
        return null;
    }

    public static void premain(String options, Instrumentation ins) {
        //註冊我自己的位元組碼轉換器   
        ins.addTransformer(new MyClassFileTransformer());
    }

    public static void main(String[] args) {
        new Business().doSomeThing();
        new Business().doSomeThing2();
    }
}

class Business {

    public boolean doSomeThing() {
        System.out.println("執行業務邏輯");
        return true;
    }

    public void doSomeThing2() {
        System.out.println("執行業務邏輯2");
    }
}
  JDK instrument包中的ClassFileTransformer用作位元組碼轉換器,其transform()方法用於在JVM載入類的位元組碼時進行攔截,它使用Javassist在Business類的方法插入日誌程式碼。在premain函式中通過Instrumentation註冊我們定義的位元組碼轉換器,該方法在main函式之前執行。

  我們需要將這個有premain函式的類打包成InstrumentationExample.jar,並修改該jar包裡的META-INF\MANIFEST.MF檔案,加入Premain-Class屬性來指定premain所在的類。

Manifest-Version: 1.0
Premain-Class: instrumentationexample.MyClassFileTransformer
  用這個包做Instrumentation代理,就可以在執行Business類的doSomeThing方法時,織入日誌記錄邏輯。執行結果如下:
C:\dist>java -javaagent:InstrumentationExample.jar -jar InstrumentationExample.jar
java/lang/invoke/MethodHandleImpl
java/lang/invoke/MemberName$Factory
java/lang/invoke/LambdaForm$NamedFunction
java/lang/invoke/MethodType$ConcurrentWeakInternSet
java/lang/invoke/MethodHandleStatics
java/lang/invoke/MethodHandleStatics$1
java/lang/invoke/MethodTypeForm
java/lang/invoke/Invokers
java/lang/invoke/MethodType$ConcurrentWeakInternSet$WeakEntry
java/lang/Void
java/lang/IllegalAccessException
sun/misc/PostVMInitHook
sun/launcher/LauncherHelper
sun/launcher/LauncherHelper$FXHelper
instrumentationexample/Business
記錄日誌
執行業務邏輯
執行業務邏輯2
java/lang/Shutdown
java/lang/Shutdown$Lock

C:\dist>
  執行main函式,你會發現切入的程式碼無侵入性的織入進去了。從輸出中可以看到系統類載入器載入的類也經過了這裡。 當然,程式執行的main函式不一定要放在premain所在的這個jar檔案裡面,這裡只是為了例子程式打包的方便而放在一起的。

4 AOP實戰

  說了這麼多理論,那AOP到底能做什麼呢? AOP能做的事情非常多。

  • 效能監控,在方法呼叫前後記錄呼叫時間,方法執行太長或超時報警。
  • 快取代理,快取某方法的返回值,下次執行該方法時,直接從快取裡獲取。
  • 軟體破解,使用AOP修改軟體的驗證類的判斷邏輯。
  • 記錄日誌,在方法執行前後記錄系統日誌。
  • 工作流系統,工作流系統需要將業務程式碼和流程引擎程式碼混合在一起執行,那麼我們可以使用AOP將其分離,並動態掛接業務。
  • 許可權驗證,方法執行前驗證是否有許可權執行當前方法,沒有則丟擲沒有許可權執行異常,由業務程式碼捕捉。 

  以下實戰是我在詢盤管理的天使瀑布專案中使用AOP實現的一個簡單的方法監控。程式碼不是很複雜,關鍵是將監控程式碼和業務程式碼的分離和複用。(解釋:詢盤enquiry,又稱詢價,是指交易的一方為購買或出售某種商品,向對方口頭或書面發出的探詢交易條件的過程。其內容可繁可簡,可只詢問價格,也可詢問其他有關的交易條件。詢盤對買賣雙方均無約束力,接受詢盤的一方可給予答覆,亦可不做回答。但作為交易磋商的起點,商業習慣上,收到詢盤的一方應迅速作出答覆。常用於國際貿易、電子商務的交易 )

4.1 方法監控

  我使用Spring AOP監控詢盤生成方法的呼叫次數,以便於觀察整個詢盤生成的過程。設計思路如下:

圖7 用AOP統計詢盤生成的次數

  每個方法呼叫成功後,統計呼叫次數並存入快取伺服器,每天晚上11點50分從快取伺服器中獲取資料並存入資料庫。因為每天的方法呼叫次數近百萬,為了降低資料庫壓力不能實時入庫。

  只要配置了註解的方法將會被統計呼叫次數,有的方法需要方法呼叫成功後才記錄,而下面這個方法要求返回值為false才記錄:

@MethodInvokeTimesMonitor(value = "KEY_FILTER_NUM", returnValue = false)
public boolean evaluateMsg(String message) {}
  我使用的是Spring2.5.5和AspectJ的方式來配置AOP,首先需要啟用對AspectJ的支援。

啟動AOP

xsi:schemaLocation="
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.5.xsd"

<!--啟用對aspectJ的支援-->
<aop:aspectj-autoproxy proxy-target-class="true"/>
  proxy-target-class設定為true表示讓Spring使用CGlib來實現AOP,配置為false表示使用動態代理實現AOP,預設使用動態代理。其次定義@MethodInvokeTimesMonitor註解。

定義註解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MethodInvokeTimesMonitor { 

    /** 
     * 監控名稱,和資料庫儲存欄位名稱保持一致
     */
    String value();

    /**
     * 要求返回值為空或等returnValue才記錄
     */ 
    boolean returnValue() default true; 
}
  最後定義一個切面,在切面中定義攔截的方法和在方法返回後記錄呼叫次數的Advice,我們在這裡定義了攔截所有配置了註解的方法。
@Aspect
public class MethodAspect {

    @Resource
    private XpCacheClient eqUserFloattedCacheClient;

    /**
     * 切入點,所有配置MethodInvokeTimesMonitor註解的方法
     */
    @Pointcut("@annotation(com.alibaba.myalibaba.eq.monitor.MethodInvokeTimesMonitor)")
    public void allMethodInvokeTimesMonitor() {
    }

    /**
     * 統計方法的呼叫次數
     * @param methodInvokeTimesMonitor 註解傳遞的引數
     */
    @AfterReturning(value = "MethodAspect.allMethodInvokeTimesMonitor() && @annotation(methodInvokeTimesMonitor)", 
            returning = "retVal")
    public void statInvokeTimes(MethodInvokeTimesMonitor methodInvokeTimesMonitor, Object retVal) {
        String name = methodInvokeTimesMonitor.value();
        //獲取方法的返回值
        boolean returnValue = methodInvokeTimesMonitor.returnValue();
        //如果返回值不為空,則判斷返回值是否和要求的返回值一致,如果一致則記錄呼叫次數
        if (retVal != null && retVal instanceof Boolean && ((Boolean) retVal == returnValue)) {
            statInvokeTimes(name);
        }
        //如果無返回值,則直接記錄呼叫次數
        if (retVal == null) {
            statInvokeTimes(name);
        }
    }

    private void statInvokeTimes(String name) {
        //只快取當天的資料
        String key = getCacheKey(name);
        //沒有則為1,有則自增長1
        Integer num = eqUserFloattedCacheClient.get(key);
        if (num == null) {
            eqUserFloattedCacheClient.put(key, 1);
        } else {
            eqUserFloattedCacheClient.syncPut(key, ++num);
        }
    }

    private String getCacheKey(String name) {
        return Calendar.getInstance().get(Calendar.DAY_OF_MONTH) + "_" + name;
    }
}
  @Pointcut用於定義切入點表示式,為了表示式可以複用,所以在單獨的方法上配置。@AfterReturning表示在方法執行後進行切入,裡面的MethodAspect.allMethodInvokeTimesMonitor()表示使用這個方法的切入點表示式,而@annotation(methodInvokeTimesMonitor)表示將當引數傳遞給statInvokeTimes方法,returning = "retVal"則表示將被切入方法的返回值賦值給retVal,並傳遞給statInvokeTimes方法。
  定義MethodAspect切面為Spring的Bean,如果不配置則AOP不會生效。
<bean class="com.alibaba.myalibaba.eq.commons.monitor.MethodAspect"/>
  Spring預設採取的動態代理機制實現AOP,當動態代理不可用時(代理類無介面)會使用CGlib機制。但Spring的AOP有一定的缺點,第一個只能對方法進行切入,不能對介面,欄位,靜態程式碼塊進行切入(切入介面的某個方法,則該介面下所有實現類的該方法將被切入)。第二個同類中的互相呼叫方法將不會使用代理類。因為要使用代理類必須從Spring容器中獲取Bean。第三個效能不是最好的,從上面我們得知使用自定義類載入器,效能要優於動態代理和CGlib。
  可以獲取代理類:
    public IMsgFilterService getThis() {   
        return (IMsgFilterService) AopContext.currentProxy();   
    }   
      
    public boolean evaluateMsg () {   
        // 執行此方法將織入切入邏輯   
        return getThis().evaluateMsg(String message);   
    }   
      
    @MethodInvokeTimesMonitor("KEY_FILTER_NUM")   
    public boolean evaluateMsg(String message) {   
  不能獲取代理類:
    public boolean evaluateMsg () {   
        // 執行此方法將不會織入切入邏輯   
        return evaluateMsg(String message);   
    }   
      
    @MethodInvokeTimesMonitor("KEY_FILTER_NUM")   
    public boolean evaluateMsg(String message) {