手把手教你寫個AOP框架

 

Why AOP?

 

AOP(Aspect-Oriented Programming),意思是面向切面編程。傳統的OOP面向對象至關於站在一個上帝模式從上往下看,裏面的一塊塊都是一個對象,由我任意組合;而AOP不一樣之處在於,他是以一個旁觀者的身法,從「側面」看整個系統模塊,看看哪裏能夠見縫插針,將本身想要處理的一段業務邏輯「編織」進去。 java


Code duplication is the ultimate code smell. It’s a sign that something is very wrong with implementation or design.(重複的代碼會讓代碼的質量很糟糕。若是出現這個情況,那麼必定是實現或者設計環境出了問題)


OOP自己是極力反對「重複發明輪子」的,可是有時卻對重複的代碼顯得迫不得已,而AOP自己是一種很好的能解決這個問題的一種思想。 git

抽象了半天,仍是利用一個例子還更加形象的解釋吧。若是你要作一個權限系統,那麼確定須要在不少業務邏輯以前都加上一個權限判斷——只有符合條件的才能完成後面的操做。若是利用傳統思想,很顯然你會把作權限判斷的業務邏輯作封裝,而後在每一個業務邏輯執行以前都執行如下那片處理權限判斷的代碼。以下圖: github

看到沒,每次一個判斷每次一個判斷,若是讓這些權限判斷的代碼散落在系統的各個角落,那會是一個噩夢!就算採用OOP思想,將權限檢查的業務放在一個類中,照樣無濟於事。由於每段業務代碼開頭總有這麼一段抹不掉的身影(doSecurityCheck)。 web

這時,AOP老兄終於按耐不住,要出場大展身手了!這位老兄立刻說,放着那段業務邏輯代碼,我來處理! 正則表達式

他首先將權限處理的部分視做一個aspect(切面),而後想辦法在運行時把切面weave(編織)進業務邏輯中合適的位置。好比就像這樣作: spring

這樣,AOP就成功的幫我把權限驗證部分插入到調用代碼的前面執行。具體調用哪一個方法其實AOP並不知道,只要你把切面織入了用戶登陸,那後調用用戶登陸,只要你織入了用戶查詢,那就調用用戶查詢。並且不僅僅是隻掉某一個方法,它能夠挨着排的調用。 編程

這只是其中一個強大的用處,還有像日誌記錄、性能分析、事務處理等更多均可以利用到AOP的地方。 mvc


Think of AOP as complementing, not competing with, OOP. AOP can supplement OOP where it is weak. (AOP和OOP沒有競爭關係,相反,AOP可以很好的補充OOP的不足)

AOP基本術語

 

l  Aspect(切面):就是你想給程序織入的代碼片斷、如權限處理、日誌記錄等。 框架

l  Weaving(編織):就是給指定的程序加上額外的業務邏輯的過程,好比將權限驗證插入到用戶登陸的過程。 ide

l  Advice(通知):表示是在程序的哪裏織入切面,好比前面織入,仍是後面織入,或者是拋出異常的時候織入。

l  Joinpoint(鏈接點):表示給那個程序織入切面,也就是被代理的目標對象的目標方法。

l  Pointcut(切入點):表示給哪些程序織入切面,是鏈接點的集合,好比是用戶登陸和用戶查詢等都須要被織入。

 

爲了方便用戶使用AOP,須要定義幾種通知類型。

l  Before:前置通知,在業務邏輯以前通知

l  After:後置通知,在業務邏輯正常完結以後通知

l  End:結束通知,無論業務邏輯是否正常完結,都會在後面執行的通知

l  Error:錯誤通知,在業務邏輯拋出異常的時候通知

 

AOP執行流程

 

下圖展現了AOP核心調用過程,經過調用AOP代理類,開始一個一個調用後面的(前置)通知/攔截器鏈條,完成以後在調用目標方法,最後回來的時候接着調用(後置、結束)通知/攔截器鏈條。


如此一來就成功的完成了在AOP中給某個程序(目標方法)以前加上一段業務邏輯,以後加上一段業務邏輯的流程,而且殺傷力極大,能夠將目標方法的範圍進行任意控制。

 

動手實現一個AOP

 

前戲那麼長,高潮不會短!此次寫的AOP參考了不少Spring的代碼,吸取了大師補充的營養。

利用測試驅動開發的原則,咱們先來考慮考慮咱們會怎麼用(寫好測試代碼),而後想一想API怎麼設計(將接口寫好),最後考慮實現的問題。

        @Test
	public void testTranscation() {
		// 建立動態代理工廠,這是調用動態代理實現aop的初始點
		AopProxyFactory proxy = new AopProxyFactory();

		// 建立目標對象
		proxy.setTarget(new AopDemo());

		// 設置各個advice,以便在調用目標對象的指定方法時能夠出現這些advice
		proxy.addAdvice(new TransactionAspect());

		// 獲取代理對象
		IAopDemo p = (IAopDemo) proxy.getProxy();

		// 經過代理對象調用目標對象的指定方法
		p.doSomething();
	}


參考springAPI設計,咱們給出了上面這段測試代碼。這樣就能給AOP代理工廠配置目標對象,和各類各樣的通知,目前只有事務處理的通知。而後經過代理工廠獲取目標對象的代理對象,並完成類型轉換的過程。最後調用指定方法,完成在這個方法的周圍實現事務處理過程。

設置目標對象的方法無需贅言,就是看中了那個對象,設置進去,這樣就能搞個代理幫他作了。。。

添加通知的實現,我想設計的好用一點。什麼叫好用呢?也就是給你一些接口,BeforeAfterErrorEnd等,只要你定義的aspect類(切面)實現了任意一個接口,就能保證按照這個接口名字所顯示的那樣執行。好比我實現了Before接口和他的抽象方法,並在裏面加了個「記錄日誌」的功能,這樣,我之後就能在個人目標方法執行以前完成一次記錄日誌的過程了。這裏咱們使用的是事務處理的aspect切面:

@Component
@Match(methodMatch = "org.*.doSomething")
public class TransactionAspect implements Before, End, Error {

	@Override
	public void error(Method method, Object[] args, Exception e) {
		System.out.println("回滾事務");
	}

	@Override
	public void before(Method method, Object[] args) {
		System.out.println("開啓事務");
	}

	@Override
	public void end(Method method, Object[] args, Object retVal) {
		System.out.println("關閉事務");
	}

}


在代理工廠中,咱們使用Object  target存儲這個目標對象,並使用集合來記錄全部該類涉及到的通知。

        private Object target;

	private List<Advice> adviceList = new ArrayList<Advice>();

	/**
	 * 
	 * @Title: setTarget
	 * @Description: 設置目標
	 * @param @param target 設定文件
	 * @return void 返回類型
	 * @throws
	 */
	public void setTarget(Object target) {
		this.target = target;
	}

	public void addAdvice(Advice advice) {
		if (advice == null)
			throw new NullPointerException("添加的通知不能爲空");
		adviceList.add(advice);
	}

而在完成配置代理工廠後,須要經過這個代理工廠來獲取代理對象。在獲取代理對象以前,咱們把以前完成的配置(目標方法、通知集合)都初始化到AdvisedSupport對象中,將這個對象總體傳給後面的代理實現(jdkcglib)完成代理類的初始化,以及通知和目標方法的調用。

        public Object getProxy() {
		if (target == null)
			throw new NullPointerException("目標對象不能爲空");
		
		AdvisedSupport config=new AdvisedSupport(target, adviceList);

		AopProxy proxy = null;
		// 若該目標對象實現了接口,就優先選擇jdk動態代理;若是沒有實現任何接口,就只能採用cglib動態代理;
		if (config.hasInterfaces()) {
			logger.info("採用jdk動態代理");
			proxy = new JDKAopProxy(config);
		} else {
			logger.info("採用cglib動態代理");
			proxy = new CglibAopProxy(config);
		}
		return proxy.getProxy();
	}


這裏咱們會根據不一樣的狀況來判斷他是選擇jdk動態代理仍是選擇cglib動態代理。

這裏以jdk動態代理爲例,cglib留給讀者自行分析:


public class JDKAopProxy implements AopProxy, InvocationHandler {

	private AdvisedSupport config;

	public JDKAopProxy(AdvisedSupport config) {
		this.config = config;
	}

	@Override
	public Object getProxy() {
		return Proxy.newProxyInstance(config.getClassLoader(),
				config.getInterfaces(), this);
	}

	@Override
	public Object invoke(Object proxy, Method method, Object[] args)
			throws Throwable {
		return new ReflectiveMethodInvocation(config.getInterceptors(),config.getMatchers(), args,
				method, config.getTarget()).proceed();
	}

}



這裏咱們首先在getProxy初始化了代理類,而後當代理類的方法被調用時,會完成目標方法調用,這個步驟都是ReflectiveMethodInvocation對象完成的。這個類實現了MethodInvocation,目的是爲了完成以後的回調過程,這個後面能夠看到。在這個ReflectiveMethodInvocation類裏面,咱們存儲了足夠多的信息

        /**
	 * 通知advice
	 */
	private List<MethodInterceptor> chain;

	/**
	 * 每一個advice所配置的匹配信息
	 */
	private List<Matcher> matches;

	/**
	 * 執行目標方法須要的參數
	 */
	private Object[] arguments;

	/**
	 * 目標方法
	 */
	private Method method;

	/**
	 * 目標對象
	 */
	private Object target;
	
	/**
	 * 記錄當前advice鏈條(chain)所須要執行的方法的索引
	 */
	private int index;



目的很簡單:就是將多個通知鏈條和目標對象的方法自己的調用整合起來,造成邏輯完善的鏈條——前置通知在目標方法前面排着隊完成,若是目標方法拋出了異常就執行錯誤通知,一旦正常執行完成目標方法就執行後置通知,而結束通知時不論是是正常執行完目標方法仍是拋出了異常,最後都會執行的一個通知。

下面來看看這個類中處理一連串方法調用的核心方法proceed()

@Override
	public Object proceed() throws Throwable {
		//當鏈條走完的時候調用目標方法
		if (index == chain.size())
			return invokeJoinpoint();

		Matcher matcher = matches.get(index);

		// 查看是否匹配,
		if (matcher.matches(this.method, this.target.getClass())) {
			return chain.get(index++).invoke(this);
		} else {
			index++;
			return proceed();
		}

	}

	/**
	 * 
	 * @Title: invokeJoinpoint
	 * @Description: 調用鏈接點方法
	 * @param @return
	 * @param @throws Throwable 設定文件
	 * @return Object 返回類型
	 * @throws
	 */
	protected Object invokeJoinpoint() throws Throwable {
		return method.invoke(target, arguments);
	}



很簡單,就是當鏈條走完的時候,調用目標方法。不然就繼續指向鏈條上的方法。這裏有一個檢測是否匹配的過程,也就是我給個人切面類,也就是處理事務的切面TransactionAspect配置了一個註解Match,這個註解表示當目標類是org包下的某個類時,我就會對他的doSomething方法完成攔截,在這個方法周圍加上事務處理。

@Component
@Match(methodMatch = "org.*.doSomething")
public class TransactionAspect implements Before, End, Error {

	@Override
	public void error(Method method, Object[] args, Exception e) {
		System.out.println("回滾事務");
	}

	@Override
	public void before(Method method, Object[] args) {
		System.out.println("開啓事務");
	}

	@Override
	public void end(Method method, Object[] args, Object retVal) {
		System.out.println("關閉事務");
	}

}



具體判斷是否匹配,我採用的是正則表達式,也就是當目標類的某個方法被調用時,一旦檢測到他符合methodMatch配置的正則表達式,就給該方法先後加上指定的邏輯。若是發現不匹配,這繼續尋找鏈條上下一個通知,直到走完整個通知鏈條。

這裏你們確定會有一個問題:在鏈條上獲取到一個通知,執行該通知的時候,如何確保前置通知是再前面執行,後置通知是再後面執行呢?而且在完成調用以後確保執行後面的通知調用流程?

其實,spring在這裏用到了一個很巧妙的編程技巧——經過多態原理和回調函數來處理。

chain.get(index++).invoke(this);



這裏獲取到了第index個通知,拿到的是個接口類型,可是實現類出賣了他的本質,表示它究竟是前置仍是後置或者是其餘等。當執行invoke時,將該MethodInvocation的實現類ReflectiveMethodInvocation的對象的引用傳遞進去。如此調用的方法,實際上是這樣的:

能夠發現,當實現類是後置通知的時候,我會選擇AfterInterceptor來執行,當時前置通知的時候,會選擇BeforeInterceptor來執行。也就是,碰到合適的通知,就採用合適的攔截器處理。

之前置通知的方法攔截器爲例:

public class BeforeInterceptor implements MethodInterceptor {

	private Before advice;

	public BeforeInterceptor(Before advice) {
		this.advice =advice;
	}

	@Override
	public Object invoke(MethodInvocation mi) throws Throwable {
		advice.before(mi.getMethod(), mi.getArguments());
		return mi.proceed();
	}

}



看到這裏是否是有種似曾相識的感受,這不就是開始那個權限驗證的翻版麼?對啊,it is。這裏實現的很明確,就是在目標方法調用以前執行before中的業務邏輯,接着進行mi.proceed()又回調到了咱們MethodInvocation的實現類ReflectiveMethodInvocation中的proceed方法中了。

這樣,也就保證了鏈條的次序執行。

來看看咱們測試用例的輸出結果吧:

開啓事務

和哈哈哈哈哈...

關閉事務



目前還沒能把ioc和aop整合起來使用,還有像ioc在web mvc框架中如何使用都還沒提到,不過這些會在咱們之後的博客中不斷出現。

源代碼

 最後,放出源代碼:https://github.com/mjaow/my_spring

相關文章
相關標籤/搜索