面向切面編程(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) {   


本文轉自:http://www.iteye.com/topic/1116696

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章