深入AOP原理與應用

AOP(Aspect Oriented Programming)就是面向切面編程,也是一種編程思想,接觸了JAVA是Spring框架後我才瞭解AOP,在我的工作中會經常用到,舉個存儲分層的例子,就像硬盤、內存和CPU中的寄存器,對應的高性能應用系統會有普通數據庫、Redis和本地內存:

那麼這裏的緩存操作我們可以抽出來統一做,這裏我們就用到了AOP,切點就是對數據的存取方法,還有就是調用外部系統的接口獲取數據時,我們也可以用AOP來實現統一的緩存操作,我們通常用的AOP的框架是aspectj,實現的原理是動態代理,動態代理的方案有JDK Proxy、cglib等,cglib是代碼的動態生成技術,用asm提供的動態生成JAVA字節碼的技術,而JDK的動態代理是一種設計模式,依懶接口的實現。寫一個AOP簡單如下

@Aspect
@Component
public class CacheUpdateProcessor {

    @Around("@annotation(com.xxx.xxx.cache.CacheUpdate)")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {

        Object result = joinPoint.proceed();
        
        try {
            // 獲取被切方法的所有入參
            Object[] methodArgs = joinPoint.getArgs();
            Signature signature = joinPoint.getSignature();
            if (signature instanceof MethodSignature) {

            }
        } catch (Exception e) {
            
        }
        return result;
    }

}
我們還在spring的XML配置文件中加上

<aop:aspectj-autoproxy proxy-target-class="true" />
表示使用cglib動態代理技術織入增強,利用cglib的好處是提高了系統的性能,利用註解讓spring幫我們完成了自動代碼織入,註解@Around中的參數就是織入的切點,around表示包圍了整個方法,被切的代碼的執行可以通過joinPoint(連接點)來調用,當然除了around的還有其他的織入方式,例如before和after,表示在被切方法前執行和被切方法後執行。

我們可以在around、before或after的方法裏進行統一的緩存處理,而在需要進行此類操作的地方只需要加個被設置爲切點的註解即可,如果還需要傳方法參數以外的數據,可以對自定義的註解進行優化,例如我自定義的的作爲切點的註解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface CacheUpdate {
    
    CacheKeys key();
    
    Class<?> type();
    
}
獲取註解中的參數方法如下:


                Method aopMethod = methodSignature.getMethod();
                CacheUpdate param = aopMethod.getAnnotation(CacheUpdate.class);
                // 獲取參數的類
                Class clazz = param.type();
                CacheKeys keys = param.key();
對於@around、@before和@after中value的值有個專業的名詞:pointcut expression(切點表達式)

Aspectj的源碼中是這樣說明的:

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

    /**
     * The pointcut expression where to bind the advice
     */
    String value();
    
    /**
     * When compiling without debug info, or when interpreting pointcuts at runtime,
     * the names of any arguments used in the advice declaration are not available.
     * Under these circumstances only, it is necessary to provide the arg names in 
     * the annotation - these MUST duplicate the names used in the annotated method.
     * Format is a simple comma-separated list.
     */
    String argNames() default "";

}
pointcut expression的作用就是爲了說明切點是什麼,在哪,比如是某個註解、是某個方法或某個類中的所有方法等等。

接下來說明AOP是如何實現代碼織入的,織入是在類加載前的環節,當某個類被加載時,如果發現有切面,這時會生成一個新的類,這個新的類會被動態地址增加一些JAVA的指令操作,然後加載這個新的類。這個織入的操作其實就像人爲的修改class文件一樣,如果對java虛擬機的彙編指令熟悉,完全可以手動修改,然而現在只是有專門的程序幫我們完成的這個操作而矣,這個專門的程序熟悉JAVA的指令集,它就是asm,而具體的要把代碼加在什麼位置,怎麼加,則是由具體的上層代碼指定,像@around、@before和@after。

ASM中有一個ClassReader和一個ClassWriter,見名就知其義了,前者是用來解析class文件的,而後者是用來寫入class文件的,ASM主要的設計思想是Visitor訪問者模式,與迭代器模式不同的是訪問者模式的訪問邏輯是由實現類來決定的,ASM的上層是ClassVisitor,而ClassWriter就是其的一個實現,還有其它的一些實現:

下面是我用ASM寫的簡單的代碼動態生成的例子:

測試類,只有一個名爲out的方法

public class Test {

    public void out(String data){
        
        System.out.println("method out : " + data);
        
    }

}
動態生成代碼,先輸出原字節碼,分隔符下是新生成的代碼

public class AsmTest {

    public static void main(String[] args) throws Exception {
        
        ClassReader cr = new ClassReader(Test.class.getName());
        ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
        
        System.out.println(new String(cw.toByteArray(),"utf-8"));
        
        cr.accept(new ClassAdapter(cw){
            
            @Override
            public MethodVisitor visitMethod(int access, String name, String desc,
                    String signature, String[] exceptions) {
                
                if("out".equals(name)){
                    
                    MethodVisitor visitor = cv.visitMethod(Opcodes.ACC_PUBLIC, "out", "(Ljava/lang/String;)V", null, null);
                    visitor.visitLdcInsn("Before execute");
                    visitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");
                    visitor.visitEnd();
                    return visitor;
                }
                
                return cv.visitMethod(access, name, desc, signature, exceptions);
            }
            
        }, 0);
        System.out.println("---------分隔符---------");
        System.out.println(new String(cw.toByteArray(),"utf-8"));
    }
}
測試的結果如下:


如果想要了解ASM的,可以要多看看JVM和其指令集了,也就是JAVA的彙編

最後總結一下,其實AOP重要的是思想,至於如何實現AOP,可以有很多種方式,只是在衆多方式中,利用ASM的JAVA字節碼動態自動生成的方式可能性能是最好的。

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