個人對AOP概念的理解
一、什麼是AOP
AOP 是Aspect Oriented Programing 的簡稱,被譯為“面向方面程式設計”。相信看到這個術語,剛接觸的人肯定是很難理解的。下面個人就按照自己的理解將其解釋下,如果有什麼不妥的地方,還請指出~
一般情況下,如果我們的程式碼出現了很多重複的,比如在 Pig、Horse、Cat 等類中,它們都擁有共同的方法 eat(),run(), 按照軟體重構的思想理念,我們可以將這些方法寫到一個父類 Animal中,並將Pig、Horse 和 Cat 等繼承它。這樣確實減少了很多重複性的程式碼,但是有時情況並非如此簡單。如下面的程式碼。
Service介面:
Service 實現:package com.zkp.service; public interface FormService { public void removeTopic(int topicId); public void removeMessage(int messageId); }
對上面這種情況,每個列出的方法裡面都包含了啟動和結束監聽器monitor的程式碼,假設我們這個monitor是用來監視一個函式執行時的效能的。對於這種情況,我們沒法採取之前以繼承父類的方式去消除這些重複程式碼。因此便有了AOP,將這些重複的程式碼以橫向切割的方式抽取出來,放到一個獨立的模組中,讓原本的業務類裡面只含有業務程式碼。package com.zkp.service.impl; import com.zkp.service.FormService; public class FormServiceImpl implements FormService { private Monitor monitor; public void removeTopic(int topicId){ monitor.start(); System.out.println("模擬刪除topic記錄"); try{ Thread.currentThread().sleep(10); }catch(Exception e){ throw new RuntimeException(e); } monitor.end(); } public void removeMessage(int recordId){ monitor.start(); System.out.println("模擬刪除訊息記錄"); try{ Thread.currentThread().sleep(10); }catch(Exception e){ throw new RuntimeException(e); } monitor.end(); } //省略各種初始化和set、get等方法 ... }
二、AOP術語
1、連線點(Joinpoint)
在程式執行到某個特定的位置,如一個函式執行前、執行後,或者是某個類初始化前和初始化後等等,這些地方都可以叫做連線點。連線點就是AOP把橫切程式碼植入的地方。它由兩個資訊確定,一個是用方法表示的程式執行點,即確定到某個方法上。二是用相對點表示的方位,能夠確定到方法內的某個地方。
2、切點(Pointcut)
每個程式類都擁有一些方法,這些方法都有多個連線點。但是我們如果要找到特定的感興趣的連線點,就要通過“切點”來定位,而且需要注意的是,一個切點可以定位多個感興趣的連線點。在Spring中,切點是通過 org.springframework.aop.Pointcut 介面進行描述的,它使用類和方法作為連線點的查詢條件,因此切點只能定位到某個方法上。
3、增強(Advice)
增強就是我們常說的植入到目標類連線點上的一段橫切程式碼。不過,它除了擁有這段程式碼邏輯的屬性外,也包含了植入橫切程式碼的方位資訊。結合切點資訊和方位資訊,一段橫切程式碼便能被準確地植入到某些特定的位置中了。由於增強帶有方位資訊,因此在 spring 中,它所包含的增強介面都是帶方位名的,如AfterRetuningAdvice、ThrowsAdvice和BeforeAdvice等。
4、目標類(Target)
很容易理解,目標類就是我們要植入橫切程式碼的目標類。在AOP的幫助下,我們可以將剛剛的 FormServiceImpl 類中有關 monitor 的程式碼抽取出來,讓AOP動態地植入到相關的一些連線點中。
三、AOP實現
利用AOP思想向目標類植入橫切程式碼時,個人理解是:它其實是生成了一個新的類,在這個新類的被增強的方法中,就包含了業務程式碼和橫切程式碼,它也是我們常說的代理。雖然我們在spring中還是嚮往常一樣去呼叫自己寫的方法,但是對於被植入了橫切程式碼的類,我們實質上是呼叫了它的一個代理,然後執行了橫切邏輯和相關的業務程式碼。AOP底層也用到了Java的反射技術,它的實現有以下兩種,
1、基於JDK動態代理
JDK動態代理主要涉及到java.lang.reflect包中的兩個類,Proxy和InvocationHandler。其中InvocationHandler是個介面,可以通過實現此介面來定義橫切邏輯,並通過反射呼叫目標類程式碼,動態地將橫切邏輯和業務邏輯編織在一起。其中Handler程式碼如下所示。在invoke方法中,proxy是最終生成的代理例項,一般不會用到,method是被代理目標例項的某個具體方法,通過它可以發起對目標例項方法的反射呼叫;args是傳遞給代理例項某個方法的入引數組,在方法反射時呼叫:
package com.zkp.base;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class PerformanceHandler implements InvocationHandler{
private Object object;
private Monitor monitor;
/**
* object作為目標業務類
* */
public PerformanceHandler(Object object){
this.object = object;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
monitor.start(); // 效能監視程式碼
Object obj = method.invoke(object, args);
monitor.end(); // 效能監視程式碼
return obj;
}
...
}
而在具體業務類中,再也不需要包含效能監視程式碼,直接去掉橫切程式碼即可,修改後的FormServiceImpl類如下所示:
package com.zkp.service.impl;
import com.zkp.service.FormService;
public class FormServiceImpl implements FormService {
public void removeTopic(int topicId){
System.out.println("模擬刪除topic記錄");
try{
Thread.currentThread().sleep(10);
}catch(Exception e){
throw new RuntimeException(e);
}
}
public void removeMessage(int recordId){
System.out.println("模擬刪除訊息記錄");
try{
Thread.currentThread().sleep(10);
}catch(Exception e){
throw new RuntimeException(e);
}
}
}
接下來可以寫個測試類呼叫:
package com.zkp.test;
import java.lang.reflect.Proxy;
import com.zkp.base.PerformanceHandler;
import com.zkp.service.FormService;
import com.zkp.service.impl.FormServiceImpl;
public class TestFormService {
public static void main(String[] args){
FormService formServiceImpl = new FormServiceImpl(); // 希望被代理的目標業務類
PerformanceHandler handler = new PerformanceHandler(formServiceImpl); // 將效能監視程式碼編織到該業務類中
// 利用newInstance()靜態方法為handler船檢一個符合FormService介面的代理例項
FormService proxy = (FormService) Proxy.newProxyInstance(formServiceImpl.getClass().getClassLoader(),
formServiceImpl.getClass().getInterfaces(), handler);
proxy.removeTopic(100);
proxy.removeMessage(1000);
}
}
由上述實現可以看出,JDK代理只能為介面建立代理例項,因為newProxyInstance()的第二個引數就是業務類的介面。
2、基於CGLib動態代理
對於沒有通過介面定義業務方法的類,JDK無法搞定,因此CGLib就是一個替代者。CGLib採用非常底層的位元組碼技術,為一個類建立子類,並在自類中採用方法攔截的技術攔截所有父類方法的呼叫,並順勢織入橫切邏輯。它實現了 MethodInterceptor類:
package com.zkp.base;
import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
public class CGLibProxy implements MethodInterceptor {
private Enhancer enhancer = new Enhancer();
private Monitor monitor;
public Object getProxy( Class superClass ){
enhancer.setSuperclass(superClass); // 設定需要建立子類的類
enhancer.setCallback(this);
return enhancer.create(); // 通過位元組碼技術動態建立子類例項
}
@Override
public Object intercept(Object arg0, Method method, Object[] arg2, MethodProxy proxy) throws Throwable {
monitor.start();
Object obj = proxy.invokeSuper(arg0, arg2); // 通過代理類呼叫父類中方法
monitor.end();
return obj;
}
...
}
在上面的程式碼中,arg0為目標類例項,method為目標類方法的反射物件,arg2為動態入參,proxy為生成的代理類例項。注意到由於CGLib底層利用到了ASM,在引入cglib包的時候,我們也需要引入asm.jar包。
接下來利用CGLib為FormServiceImpl類建立代理物件,並測試代理物件方法,程式碼如下:
package com.zkp.test;
import com.zkp.base.CGLibProxy;
import com.zkp.service.impl.FormServiceImpl;
public class TestCGLibProxy {
public static void main(String[] args){
CGLibProxy proxy = new CGLibProxy();
FormServiceImpl formServiceImpl = (FormServiceImpl) proxy.getProxy(FormServiceImpl.class);
formServiceImpl.removeMessage(10000);
formServiceImpl.removeTopic(3000);
}
}
利用AOP織入橫切程式碼後Test類執行結果如下所示:
不過以上還有以下問題:
1、目標類所有方法都被織入了橫切程式碼,而有時我們只需要對目標類的部分方法進行動態織入;
2、通過硬編碼的方式指定了橫切程式碼的織入點,即在一個方法的開始和結束織入橫切邏輯;
3、手工編寫建立代理的過程,對每個介面或者實現類都需要各自編寫程式碼,沒有做到通用。
四、JDK和CGLib比較
除了JDK只能代理介面而CGLib可以代理具體實現類的差別外,CGLib建立的代理物件在效能上會比JDK建立的高不少,但是CGLib在建立代理物件的時候花費的時間卻比JDK多很多。因此,對於singleton的代理物件或者是具有實現池時,我們一般選用CGLib。而當需要頻繁地建立物件時,我們使用JDK動態代理技術。此外,由於CGLib採用動態建立子類的方式生成代理物件,因此沒法對目標類的final、private方法進行代理