1. 程式人生 > >個人對AOP概念的理解

個人對AOP概念的理解

一、什麼是AOP

AOP 是Aspect Oriented Programing 的簡稱,被譯為“面向方面程式設計”。相信看到這個術語,剛接觸的人肯定是很難理解的。下面個人就按照自己的理解將其解釋下,如果有什麼不妥的地方,還請指出~

一般情況下,如果我們的程式碼出現了很多重複的,比如在 Pig、Horse、Cat 等類中,它們都擁有共同的方法 eat(),run(), 按照軟體重構的思想理念,我們可以將這些方法寫到一個父類 Animal中,並將Pig、Horse 和 Cat 等繼承它。這樣確實減少了很多重複性的程式碼,但是有時情況並非如此簡單。如下面的程式碼。

Service介面:

package com.zkp.service;

public interface FormService {
	public void removeTopic(int topicId);
	
	public void removeMessage(int messageId);
}
Service 實現:
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等方法
        ...
}
對上面這種情況,每個列出的方法裡面都包含了啟動和結束監聽器monitor的程式碼,假設我們這個monitor是用來監視一個函式執行時的效能的。對於這種情況,我們沒法採取之前以繼承父類的方式去消除這些重複程式碼。因此便有了AOP,將這些重複的程式碼以橫向切割的方式抽取出來,放到一個獨立的模組中,讓原本的業務類裡面只含有業務程式碼。

二、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方法進行代理