AOP原理

概念

  • AOP(Aspect-OrientedProgramming,面向方面編程),當(dāng)我們需要為分散的對(duì)象引入公共行為的時(shí)候,OOP顯得無能為力。

  • OOP允許你定義從上到下的關(guān)系,但并不適合定義從左到右的關(guān)系。例如日志功能。日志代碼往往水平地散布在所有對(duì)象層次中,而與它所散布到的對(duì)象的核心功能毫無關(guān)系。

  • 對(duì)于其他類型的代碼,如安全性、異常處理和透明的持續(xù)性也是如此。這種散布在各處的無關(guān)的代碼被稱為橫切(cross-cutting)代碼,在OOP設(shè)計(jì)中,它導(dǎo)致了大量代碼的重復(fù),而不利于各個(gè)模塊的重用。

  • AOP技術(shù)利用一種稱為“橫切”的技術(shù),將那些與業(yè)務(wù)無關(guān),卻為業(yè)務(wù)模塊所共同調(diào)用的邏輯或責(zé)任封裝起來,減少系統(tǒng)的重復(fù)代碼,降低模塊間的耦合度,而不留痕跡,利于未來的可操作性和可維護(hù)性。

  • AOP把軟件系統(tǒng)分為兩個(gè)部分:核心關(guān)注點(diǎn)和橫切關(guān)注點(diǎn)。業(yè)務(wù)處理的主要流程是核心關(guān)注點(diǎn)。橫切關(guān)注點(diǎn)經(jīng)常發(fā)生在核心關(guān)注點(diǎn)的多處,而各處都基本相似。比如權(quán)限認(rèn)證、日志、事務(wù)處理。Aop 的作用在于分離系統(tǒng)中的各種關(guān)注點(diǎn),將核心關(guān)注點(diǎn)和橫切關(guān)注點(diǎn)分離開來。

  • 實(shí)現(xiàn)AOP的技術(shù),主要分為兩大類:一是動(dòng)態(tài)代理技術(shù),利用截取消息的方式,在運(yùn)行期對(duì)該消息進(jìn)行裝飾,以取代原有對(duì)象行為的執(zhí)行;二是靜態(tài)織入的方式,引入特定的語法創(chuàng)建“方面”,在編譯期間織入有關(guān)“方面”的代碼。

使用場(chǎng)景

  • Authentication 權(quán)限
  • Caching 緩存
  • Context passing 內(nèi)容傳遞
  • Error handling 錯(cuò)誤處理
  • Lazy loading 懶加載
  • Debugging  調(diào)試
  • logging, tracing, profiling and monitoring 日志 跟蹤 優(yōu)化 校準(zhǔn)
  • Performance optimization 性能優(yōu)化
  • Persistence  持久化
  • Resource pooling 資源池
  • Synchronization 同步
  • Transactions 事務(wù)

術(shù)語

  • Joinpoint:攔截點(diǎn),如某個(gè)業(yè)務(wù)方法。
  • Pointcut:Joinpoint的表達(dá)式,表示攔截哪些方法。一個(gè)Pointcut對(duì)應(yīng)多個(gè)Joinpoint。
  • Advice: 要切入的邏輯。
  • Before Advice 在方法前切入。
  • After Advice 在方法后切入,拋出異常時(shí)也會(huì)切入。
  • After Returning Advice 在方法返回后切入,拋出異常則不會(huì)切入。
  • After Throwing Advice 在方法拋出異常時(shí)切入。
  • Around Advice 在方法執(zhí)行前后切入,可以中斷或忽略原有流程的執(zhí)行。
  • 公民之間的關(guān)系
關(guān)系

織入器通過在切面中定義pointcut來搜索目標(biāo)(被代理類)的JoinPoint(切入點(diǎn)),然后把要切入的邏輯(Advice)織入到目標(biāo)對(duì)象里,生成代理類。

實(shí)現(xiàn)原理

JDK動(dòng)態(tài)代理

使用動(dòng)態(tài)代理實(shí)現(xiàn)AOP需要有四個(gè)角色:被代理的類,被代理類的接口,織入器,和InvocationHandler,而織入器使用接口反射機(jī)制生成一個(gè)代理類,然后在這個(gè)代理類中織入代碼。被代理的類是AOP里所說的目標(biāo),InvocationHandler是切面,它包含了Advice和Pointcut。

接口動(dòng)態(tài)代理

如何使用動(dòng)態(tài)代理來實(shí)現(xiàn)AOP

下面的例子演示在方法執(zhí)行前織入一段記錄日志的代碼,其中Business是代理類,LogInvocationHandler是記錄日志的切面,IBusiness, IBusiness2是代理類的接口,Proxy.newProxyInstance是織入器。

public static void main(String[] args) {   
   // TODO Auto-generated method stub
        BusinessImp business = new BusinessImp();
        // 1.獲取對(duì)應(yīng)的ClassLoader
        ClassLoader classLoader = business.getClass().getClassLoader();
        // 2.獲取ElectricCar 所實(shí)現(xiàn)的所有接口
        Class[] interfaces = business.getClass().getInterfaces();
        // 3.設(shè)置一個(gè)來自代理傳過來的方法調(diào)用請(qǐng)求處理器,處理所有的代理對(duì)象上的方法調(diào)用
        InvocationHandler handler = new LogInvocationHandler(business);
        /*
         * 4.根據(jù)上面提供的信息,創(chuàng)建代理對(duì)象 在這個(gè)過程中, a.JDK會(huì)通過根據(jù)傳入的參數(shù)信息動(dòng)態(tài)地在內(nèi)存中創(chuàng)建和.class 文件等同的字節(jié)碼
         * b.然后根據(jù)相應(yīng)的字節(jié)碼轉(zhuǎn)換成對(duì)應(yīng)的class, c.然后調(diào)用newInstance()創(chuàng)建實(shí)例
         */
        Object o = Proxy.newProxyInstance(classLoader, interfaces, handler);
        ((InfBusiness1)o).fun1();
        ((InfBusiness2)o).fun2();
}   
  
/**  
* 打印日志的切面  
*/   
public static class LogInvocationHandler implements InvocationHandler {   
  
    private Object target; //目標(biāo)對(duì)象   
  
    LogInvocationHandler(Object target) {   
        this.target = target;   
    }   
  
    @Override   
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {   
       System.out.println("You are going to invoke " + method.getName()
                    + " ...");
            // 執(zhí)行原有邏輯
            Object rev = method.invoke(target, args);
            // 執(zhí)行織入的日志,你可以控制哪些方法執(zhí)行切入邏輯
            if (method.getName().equals("fun1")) {
                System.out.println("記錄日志");
            }
            System.out.println(method.getName()
                    + " invocation Has Been finished...");
            return rev;   
    }   
} 

接口IBusiness和IBusiness2定義省略。下面是業(yè)務(wù)類,需要代理的類。

public class BusinessImp implements IBusiness, IBusiness2 {   
  @Override
        public void fun2() {
            // TODO Auto-generated method stub
            System.out.println("business fun2");
        }

        /*
         * (non-Javadoc) <p>Description: </p>
         * 
         * @see taop.InfBusiness1#fun1()
         */
        @Override
        public void fun1() {
            // TODO Auto-generated method stub

            System.out.println("business fun1");

        }
}   

動(dòng)態(tài)代理原理

本節(jié)將結(jié)合動(dòng)態(tài)代理的源代碼講解其實(shí)現(xiàn)原理。動(dòng)態(tài)代理的核心其實(shí)就是代理對(duì)象的生成,即Proxy.newProxyInstance(classLoader, proxyInterface, handler)。讓我們進(jìn)入newProxyInstance方法觀摩下,核心代碼其實(shí)就三行。

//獲取代理類   
Class cl = getProxyClass(loader, interfaces);   
//獲取帶有InvocationHandler參數(shù)的構(gòu)造方法   
Constructor cons = cl.getConstructor(constructorParams);   
//把handler傳入構(gòu)造方法生成實(shí)例   
return (Object) cons.newInstance(new Object[] { h });  

其中g(shù)etProxyClass(loader, interfaces)方法用于獲取代理類,它主要做了三件事情:在當(dāng)前類加載器的緩存里搜索是否有代理類,沒有則生成代理類并緩存在本地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) {
                    // 代理類已經(jīng)存在則返回
                    return proxyClass;
                } else if (value == pendingGenerationMarker) {
                    // 如果代理類正在產(chǎn)生,則等待
                    try {
                        cache.wait();
                    } catch (InterruptedException e) {
                    }
                    continue;
                } else {
                    // 沒有代理類,則標(biāo)記代理準(zhǔn)備生成
                    cache.put(key, pendingGenerationMarker);
                    break;
                }
            } while (true);
        }

代理類的生成主要是以下這兩行代碼。

//生成代理類的字節(jié)碼文件并保存到硬盤中(默認(rèn)不保存到硬盤)   
proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces);   
//使用類加載器將字節(jié)碼加載到內(nèi)存中   
proxyClass = defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length);

ProxyGenerator.generateProxyClass()方法屬于sun.misc包下,Oracle并沒有提供源代碼,但是我們可以使用JD-GUI這樣的反編譯軟件打開jre\lib\rt.jar來一探究竟,以下是其核心代碼的分析。

//添加接口中定義的方法,此時(shí)方法體為空   
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]);   
  }   
}   
  
//添加一個(gè)帶有InvocationHandler的構(gòu)造方法   
MethodInfo localMethodInfo = new MethodInfo("<init>", "(Ljava/lang/reflect/InvocationHandler;)V", 1);   
  
//循環(huán)生成方法體代碼(省略)   
//方法體里生成調(diào)用InvocationHandler的invoke方法代碼。(此處有所省略)   
this.cp.getInterfaceMethodRef("InvocationHandler", "invoke", "Object; Method; Object;")   
  
//將生成的字節(jié)碼,寫入硬盤,前面有個(gè)if判斷,默認(rèn)情況下不保存到硬盤。   
localFileOutputStream = new FileOutputStream(ProxyGenerator.access$000(this.val$name) + ".class");   
localFileOutputStream.write(this.val$classFile);   

那么通過以上分析,我們可以推出動(dòng)態(tài)代理為我們生成了一個(gè)這樣的代理類。把方法doSomeThing的方法體修改為調(diào)用LogInvocationHandler的invoke方法。 生成的代理類源碼:

public class ProxyBusinessImp 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;   
}   
}   

小結(jié)

  • 從前兩節(jié)的分析我們可以看出,動(dòng)態(tài)代理在運(yùn)行期通過接口動(dòng)態(tài)生成代理類,這為其帶來了一定的靈活性,但這個(gè)靈活性卻帶來了兩個(gè)問題
  • 第一代理類必須實(shí)現(xiàn)一個(gè)接口,如果沒實(shí)現(xiàn)接口會(huì)拋出一個(gè)異常。
  • 第二性能影響,因?yàn)閯?dòng)態(tài)代理使用反射的機(jī)制實(shí)現(xiàn)的,首先反射肯定比直接調(diào)用要慢,經(jīng)過測(cè)試大概每個(gè)代理類比靜態(tài)代理多出10幾毫秒的消耗。
  • 其次使用反射大量生成類文件可能引起Full GC造成性能影響,因?yàn)樽止?jié)碼文件加載后會(huì)存放在JVM運(yùn)行時(shí)區(qū)的方法區(qū)(或者叫持久代)中,當(dāng)方法區(qū)滿的時(shí)候,會(huì)引起Full GC,所以當(dāng)你大量使用動(dòng)態(tài)代理時(shí),可以將持久代設(shè)置大一些,減少Full GC次數(shù)。

動(dòng)態(tài)字節(jié)碼生成Cglib

使用動(dòng)態(tài)字節(jié)碼生成技術(shù)實(shí)現(xiàn)AOP原理是在運(yùn)行期間目標(biāo)字節(jié)碼加載后,生成目標(biāo)類的子類,將切面邏輯加入到子類中,所以使用Cglib實(shí)現(xiàn)AOP不需要基于接口。

動(dòng)態(tài)字節(jié)碼

本節(jié)介紹如何使用Cglib來實(shí)現(xiàn)動(dòng)態(tài)字節(jié)碼技術(shù)。Cglib是一個(gè)強(qiáng)大的,高性能的Code生成類庫,它可以在運(yùn)行期間擴(kuò)展Java類和實(shí)現(xiàn)Java接口,它封裝了Asm,所以使用Cglib前需要引入Asm的jar。

public static void main(String[] args) {   
        byteCodeGe();   
    }   
  
    public static void byteCodeGe() {   
        //創(chuàng)建一個(gè)織入器   
        Enhancer enhancer = new Enhancer();   
        //設(shè)置父類   
        enhancer.setSuperclass(BusinessImp.class);   
        //設(shè)置需要織入的邏輯   
        enhancer.setCallback(new LogIntercept());   
        //使用織入器創(chuàng)建子類   
        IBusiness2 newBusiness = (IBusiness2) enhancer.create();   
        newBusiness.doSomeThing2();   
    }   
  
    /**  
     * 記錄日志  
     */   
    public static class LogIntercept implements MethodInterceptor {   
  
        @Override   
        public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable {   
            //執(zhí)行原有邏輯,注意這里是invokeSuper   
            Object rev = proxy.invokeSuper(target, args);   
            //執(zhí)行織入的日志   
            if (method.getName().equals("doSomeThing2")) {   
                System.out.println("記錄日志");   
            }   
            return rev;   
        }   
    }   

自定義類加載器

如果我們實(shí)現(xiàn)了一個(gè)自定義類加載器,在類加載到JVM之前直接修改某些類的方法,并將切入邏輯織入到這個(gè)方法里,然后將修改后的字節(jié)碼文件交給虛擬機(jī)運(yùn)行,那豈不是更直接。

自定義類加載器

Javassist是一個(gè)編輯字節(jié)碼的框架,可以讓你很簡單地操作字節(jié)碼。它可以在運(yùn)行期定義或修改Class。使用Javassist實(shí)現(xiàn)AOP的原理是在字節(jié)碼加載前直接修改需要切入的方法。這比使用Cglib實(shí)現(xiàn)AOP更加高效,并且沒太多限制,實(shí)現(xiàn)原理如下圖:

類加載器

我們使用系統(tǒng)類加載器啟動(dòng)我們自定義的類加載器,在這個(gè)類加載器里加一個(gè)類加載監(jiān)聽器,監(jiān)聽器發(fā)現(xiàn)目標(biāo)類被加載時(shí)就織入切入邏輯,咱們?cè)倏纯词褂肑avassist實(shí)現(xiàn)AOP的代碼:

//獲取存放CtClass的容器ClassPool   
ClassPool cp = ClassPool.getDefault();   
//創(chuàng)建一個(gè)類加載器   
Loader cl = new Loader();   
//增加一個(gè)轉(zhuǎn)換器   
cl.addTranslator(cp, new MyTranslator());   
//啟動(dòng)MyTranslator的main函數(shù)   
cl.run("jsvassist.JavassistAopDemo$MyTranslator", args);   

其中

public static class MyTranslator implements Translator {   
  
        public void start(ClassPool pool) throws NotFoundException, CannotCompileException {   
        }   
  
        /* *  
         * 類裝載到JVM前進(jìn)行代碼織入  
         */   
        public void onLoad(ClassPool pool, String classname) {   
            if (!"model$Business".equals(classname)) {   
                return;   
            }   
            //通過獲取類文件   
            try {   
                CtClass  cc = pool.get(classname);   
                //獲得指定方法名的方法   
                CtMethod m = cc.getDeclaredMethod("doSomeThing");   
                //在方法執(zhí)行前插入代碼   
                m.insertBefore("{ System.out.println(\"記錄日志\"); }");   
            } catch (NotFoundException e) {   
            } catch (CannotCompileException e) {   
            }   
        }   
    }   

小結(jié)

從本節(jié)中可知,使用自定義的類加載器實(shí)現(xiàn)AOP在性能上要優(yōu)于動(dòng)態(tài)代理和Cglib,因?yàn)樗粫?huì)產(chǎn)生新類,但是它仍然存在一個(gè)問題,就是如果其他的類加載器來加載類的話,這些類將不會(huì)被攔截

字節(jié)碼轉(zhuǎn)換

自定義的類加載器實(shí)現(xiàn)AOP只能攔截自己加載的字節(jié)碼,那么有沒有一種方式能夠監(jiān)控所有類加載器加載字節(jié)碼呢?有,使用Instrumentation,它是 Java 5 提供的新特性,使用 Instrumentation,開發(fā)者可以構(gòu)建一個(gè)字節(jié)碼轉(zhuǎn)換器,在字節(jié)碼加載前進(jìn)行轉(zhuǎn)換。本節(jié)使用Instrumentation和javassist來實(shí)現(xiàn)AOP。

構(gòu)建字節(jié)碼轉(zhuǎn)換器

首先需要?jiǎng)?chuàng)建字節(jié)碼轉(zhuǎn)換器,該轉(zhuǎn)換器負(fù)責(zé)攔截Business類,并在Business類的doSomeThing方法前使用javassist加入記錄日志的代碼。

public class MyClassFileTransformer implements ClassFileTransformer {   
  
    /**  
     * 字節(jié)碼加載到虛擬機(jī)前會(huì)進(jìn)入這個(gè)方法  
     */   
    @Override   
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,   
                            ProtectionDomain protectionDomain, byte[] classfileBuffer)   
            throws IllegalClassFormatException {   
        System.out.println(className);   
        //如果加載Business類才攔截   
        if (!"model/Business".equals(className)) {   
            return null;   
        }   
  
        //javassist的包名是用點(diǎn)分割的,需要轉(zhuǎn)換下   
        if (className.indexOf("/") != -1) {   
            className = className.replaceAll("/", ".");   
        }   
        try {   
            //通過包名獲取類文件   
            CtClass cc = ClassPool.getDefault().get(className);   
            //獲得指定方法名的方法   
            CtMethod m = cc.getDeclaredMethod("doSomeThing");   
            //在方法執(zhí)行前插入代碼   
            m.insertBefore("{ System.out.println(\"記錄日志\"); }");   
            return cc.toBytecode();   
        } catch (NotFoundException e) {   
        } catch (CannotCompileException e) {   
        } catch (IOException e) {   
            //忽略異常處理   
        }   
        return null;   
}   

注冊(cè)轉(zhuǎn)換器

使用premain函數(shù)注冊(cè)字節(jié)碼轉(zhuǎn)換器,該方法在main函數(shù)之前執(zhí)行。
public class MyClassFileTransformer implements ClassFileTransformer {   
    public static void premain(String options, Instrumentation ins) {   
        //注冊(cè)我自己的字節(jié)碼轉(zhuǎn)換器   
        ins.addTransformer(new MyClassFileTransformer());   
}   
}   

配置和執(zhí)行

需要告訴JVM在啟動(dòng)main函數(shù)之前,需要先執(zhí)行premain函數(shù)。首先需要將premain函數(shù)所在的類打成jar包。并修改該jar包里的META-INF\MANIFEST.MF 文件。

Manifest-Version: 1.0
Premain-Class: bci. MyClassFileTransformer
然后在JVM的啟動(dòng)參數(shù)里加上。javaagent:D:\java\projects\opencometProject\Aop\lib\aop.jar

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容