Java Web Application 自架構 五 AOP概念的引入

      各位朋友,新年快樂。

      上一篇裏,筆者將log4j2用自己的方法配置到了Spring中,用地還算不錯,不過有些牽強附會,Spring應該會開發出很好的log4j2整合方案,敬請期待。然後我們現在的課題重點,在於如何讓log的加入不再在寫新方法時被忽略,於是想到了AOP,只要配置好切入點,每個方法裏都會去調用切面中需要執行的語句。這一篇裏,筆者就將AOP引入,來完成一些像這樣的代碼重用的需求。

      其實AOP早就充斥在我們的Application中的,只是沒有主動去使用過它而已。比如JavaEE的Filter, 再像Spring中,到處都是AOP,它的看家王牌IoC,就是用AOP來實現的,現在這樣說有點兒勉強,等一下自己用了就會有深刻的體會。大家在以往帶有Spring的開發過程中Debug時應該有發現$Proxy 這樣的實例對象,筆者當時只是想到 代理模式這麼一個概念,其實它就是AOP所編譯出來的一種實例。另外,應該時刻提醒自己AOP不是什麼框架的,它是一種編程思想,與OOP是一個級別,不只在Java中可以有,其它的編程語言也可以有。在網上多做一些Research,就會有體會。

      例如我們即將要做的Java的AOP編程,要用到一個類庫,叫做aspectj,在它的aspectjrt.jar中,是有自己的編譯器的,很容易就聯繫到上文所提及的$Proxy實例,就是這個編譯器編譯出的一種實例對象。如同做javacc一樣,自定義的編譯器所編譯出的東西,可以是一種新的實例概念。

      你可能早就不耐煩了我上面的囉嗦,在其它地方開始Research到底如何將AOP加入,進而發現aspectj 的官網是down的,那麼所需的類庫aspectjrt.jar, aspectjweaver.jar等到哪下?OK,去Eclipse吧,如果你需要在Eclipse裏整合它的插件,可以搜AJDT,找到它的update Site後在Eclipse裏用它的Install New Software 功能。 這裏方便下大家:

Eclipse AJDT Update Site: 

http://download.eclipse.org/tools/ajdt/42/update

(Eclipse Juno用,3.8與4.2, 若是3.7或3.6的,只需將其中42改成37或36)

如果喜歡不借助IDE完成,到

http://www.eclipse.org/downloads/download.php?file=/tools/aspectj/aspectj-1.7.1.jar

裏去下載吧。截止目前,2013年元旦,它的最新版本是1.7.1,如果需要更新的版本那就是這裏了:http://www.eclipse.org/aspectj/downloads.php#stable_release

 

      你會發現下載下來的jar文件是個可執行的jar包,雙擊它來進行安裝。當然前提是你有JRE安裝在系統中。按照彈出的窗口中的提示進行安裝,完成後會有需要更改環境變量的tips,如下圖:


      我們的目的是取得所需jar包,所以你也可以不安裝,直接用解壓縮包的工具將那個可執行jar中lib下的所有jar取出來。或說在安裝好後的aspectj的安裝路徑下的lib中取得。將aspectjrt.jar和aspectjweaver.jar放到我們Web App的WEB-INF/lib下,並確保以下jars也同樣在lib中:

aopalliance.jar

cglib-x.x.x.jar (筆者的是2.2.2,好囧,真二)

org.springframework.aop-x.x.x.RELEASE.jar(3.1.2的)

 

      之後打開Spring 3.2.x的AOP教程文檔

http://static.springsource.org/spring/docs/3.2.x/spring-framework-reference/html/aop.html,當然,網絡上其他高手的博文也是可以的;不過還是建議讀官方文檔。最重要的是它比較全面,以後在其他程序上需要其他用法,都可以在官方文檔裏找到。

      接下來的註解式配置就是從Spring的教程文檔裏纔有看到的,不然筆者又會用自己的野方法自己去做一個@Bean配置return出一個配置好的類實例來。文檔中有寫了三種啓動SpringAOP的配置方法,這裏所需要的就是第一種在@Configuration註解的ApplicationContext類上,再加一個註解@EnableAspecjAutoProxy.之後,在com.xxxxx.wemodel.util下新建一個LogAspect類,該類需要用@Component註解爲Spring的Bean,然後用@Aspect註解爲切面。這樣我們就可以開始實現之前的想法,讓Log可以簡單地寫在切面上,不再之後有新加其它方法時被遺忘。

      繼續看文檔,第一個概念 @Pointcut,切入點,通常就是指執行某特定方法時。即

 

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)
          throws-pattern?)

 

      帶?是可有可無的成分,筆者這裏需要的是"execution(* com.xxxxx.webmodel.pin..*.*(..))"

就是說:在執行com.xxxxx.webmodel.pin包下以及一層子包下(第一個..)的所有類(第二個*)的所有方法(第三個*)的時候,無論該方法返回值是什麼(第一個*,),參數有多少是什麼(第二個..)。 文檔中還有許多例子來幫助讀者們理解切入點的概念。有需要的同志們可以細讀文檔。“execution”應該是aspectj的,還有許多 Spring自定義,如within,target等等。

      之後的其它概念@Before,@After, @AfterReturning,@AfterThrowing屬於同一類型,註解在方法上,意思是說:在切入點的這個時刻去執行所註解的方法。

比如

 

@Before("execution(* com.xxxxx.webmodel.pin..*.*(..))")
public void logMethodStart(){}

      它的意思就是 在執行所有pin包及一層子包下的所有類的所有方法之前,去調用logMethodStart()這個方法。依此類推@After自不用多說。再來看@AfterReturning與@AfterThrowing,格式如下:

@AfterReturning(value=" execution(* com.xxxxx.webmodel.pin..*.*(..))",returning="returnedObj")
public void logMethodSuccess(Object returnedObj){}
	
@AfterThrowing(value=" execution(* com.xxxxx.webmodel.pin..*.*(..))",throwing="ex")
public void logMethodFailed(Exception ex){}

 

 

      也就是說在執行方法有返回值時,和執行方法過程中有錯誤拋出時,去執行某些方法。而且可以將返回值與拋出的異常傳到要執行的方法中來。只需要將參數名配置到相應註解的屬性中,然後在執行方法的參數中寫出即可。

 

     爲了讓日誌的記錄更像樣,我們最起碼需要知道 是在執行哪個類的哪個方法時,去寫這個日誌,以及用哪個類名命名的logger,換句話說,我們需要具體的切入點的信息。那就用JoinPoint類的參數作爲第一個參數傳到方法裏來。別說沒提醒你:在文檔的Access to the current JoinPoint一小節裏有寫,它的getArgs()是用來獲取切入方法的參數,getTarget()是獲取切入方法的類對象,getThis()是獲取切入方法的類對象的代理對象,即完成切面操作的$Proxy對象,getSignature()是獲取切入方法的描述。

 

      然後我們還需要logContext, 比較簡單,因爲LogAspect本身就是一個Spring的Bean,將logContext注入進來是很簡單的事。這樣,就形成了如下的代碼:

package com.xxxxx.webmodel.util;

import javax.annotation.Resource;

import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.LoggerContext;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class LogAspect {
	private LoggerContext logCtx;
	@Resource
	public void setLogCtx(LoggerContext logCtx) {
		this.logCtx = logCtx;
	}
	@Before("execution(* com.xxxxx.webmodel.pin..*.*(..))")
	public void logMethodStart(JoinPoint joinPoint){
//		Object[] objs = joinPoint.getArgs();
		Signature sign = joinPoint.getSignature();
		Object target =joinPoint.getTarget();
//		Object thisT =joinPoint.getThis();
		Logger logger =logCtx.getLogger(target.getClass().getName());
		logger.info("Start to execute method " + sign);
	}
	
	@After("execution(* com.xxxxx.webmodel.pin..*.*(..))")
	public void logMethodEnd(JoinPoint joinPoint){
		Signature sign = joinPoint.getSignature();
		Object target =joinPoint.getTarget();
		Logger logger =logCtx.getLogger(target.getClass().getName());
		logger.info("End to execute method "+ sign);
	}

	@AfterReturning(value=" execution(* com.xxxxx.webmodel.pin..*.*(..))",returning="returnedObj")
	public void logMethodSuccess(JoinPoint joinPoint,Object returnedObj){
		Signature sign = joinPoint.getSignature();
		Object target =joinPoint.getTarget();
		Logger logger =logCtx.getLogger(target.getClass().getName());
		logger.info("Successfully executed method "+ sign +
				(returnedObj==null?".":". returned object is "+returnedObj));
	}
	
	@AfterThrowing(value="executeMethodsInPin()",throwing="ex")
	public void logMethodFailed(JoinPoint joinPoint,Exception ex){
		Signature sign = joinPoint.getSignature();
		Object target =joinPoint.getTarget();
		Logger logger =logCtx.getLogger(target.getClass().getName());
		logger.error("Failed to execute method "+ sign + ", Cause by "+ex.getMessage());
	}
	
}

 

      直接Junit方式運行之前寫的DAO的測試類PersistencePinText.java, 成功看到了log。不過,發現切入點太少了,只能定義一個包及其一層子包下的所有類的所有方法,需要改進一下吧。@Pointcut註解的方法就代表切入點的意思,只需要多加幾個即可。形成以下格式:

        @Pointcut("execution(* com.gxino.webmodel.pin..*.*(..))")
	private void executeMethodsInPin(){}
	
	@Pointcut("execution(* com.gxino.webmodel.hub..*.*(..))")
	private void executeMethodsInHub(){}

 

      那麼@Before裏的值怎麼寫呢? 一個還行,切入點多了,怎麼辦?看文檔。

有了,文檔裏有許多@Before(“anyMethod()&& args(account)”)的形式。也就是說裏面是可以把多個切入點加進去的。

 

嘗試1:

 

@Before("executeMethodsInPin() && executeMethodsInHub()")
public void logMethodStart(JoinPoint joinPoint){}

 

      發現沒有log了,難道,裏面是條件表達試?

嘗試2:

@Before("executeMethodsInPin() || executeMethodsInHub()")
public void logMethodStart(JoinPoint joinPoint){}


      Bingo,成功。

 

      到這裏筆者已然是頗有成就感了,那就一舉殲滅吧,前面的DAO層裏的每個方法重用的代碼好多,每個方法只有下面的一個Try塊體內的第一句話是不同的,其餘都相同。完全可以把相同部分放到一個方法裏來調用。可是相同的部分是分佈在那句不同的代碼上下兩部分的,而且有一個上下兩部分都要用到的變量,之前能想到的解決方案只能是把共享的那個變量從方法內抽出來成爲類的成員,然後把上下兩部分別寫成方法。但是,問題是共享的那個變量需要在方法體內被鎖住,因爲DAO是單例的,不鎖的話,方法並行執行就會出問題,可是鎖住又讓效率降低。

      實際上是我爲難自己:Hibernate的Session有時是getCurrentSession()的,有時是openSession()的,然後就需要一個boolean值needCloseSession來告訴執行完操作需不需要關閉session

這個可以用aspectj中的@Around來實現,仔細讀下文檔中的@Around的使用提示,@Around是用來共享 執行一個方法前後 的狀態的,最好不要在可以簡單用@Before和@After來完成的Case中去用@Around. 上面的case需要在執行session具體方法的前後共享一個變量needCloseSession.符合要求,那就來試一下吧:

直接在HibernatePersistencePin.java的文件裏append以下代碼:

 

@Component
@Aspect
class CommonStatement {
	private SessionFactory sessionFactory;	
	@Resource
	public void setSessionFactory(SessionFactory sessionFactory){
		this.sessionFactory = sessionFactory;
	}
	@Around("execution (* com.gxino.webmodel.pin.impl.HibernatePersistencePin.*(..))")
	public Object prepare(ProceedingJoinPoint pjp)throws Throwable{
		Object target =pjp.getTarget();
		//HibernatePersistencePin targetObj = (HibernatePersistencePin)proxy;
		boolean needCloseSession = false;
		boolean needThrowOut= false;
		try {
			Session session =null;
			try{ 
				session = sessionFactory.getCurrentSession();
				if(session ==null)throw new HibernateException("");
			}
			catch(HibernateException he){
				session = sessionFactory.openSession();
				needCloseSession = true;
			}
			Method setSession =target.getClass().getMethod("setSession", Session.class);
			setSession.invoke(target, session);
			Object object=null;
			try{
				object =pjp.proceed();
			}
			catch(Throwable t){
				needThrowOut= true;
				throw t;
			}
			if(needCloseSession)session.close();
			return object;
		} catch (Throwable t) {
			if(needThrowOut)throw t;
			else {t.printStackTrace();
			return null;}
		} 
		
	}
}

 

      然後在HibernatePersistencePin類中將seesionFactory類成員改成session, 然後精簡每一個方法。如下:

private Session session;
public void setSession(Session session) {
this.session = session; }

	@Override
	public void createPersistingEntity(Object persistingObj) throws Exception {
		try{
			session.save(persistingObj);
		}catch(HibernateException he){
			throw new Exception("Save Pojo failed, because of "+he.getMessage());
		}
}

 

      這一鬧,不得了,筆者有了意外收穫,體會到了Spring依賴注入是怎麼做到的。

上述代碼的執行過程是:調用HibernatePersistencePin的createPersistingEntity()時,會先調用CommonStatement.prepare(ProceedingJoinPoint pjp)方法,直到執行其中的object=pjp.proceed(); 該句就是去執行createPersistingEntity()的意思。當然,正確寫法應該是object=pjp.proceed(pjp.getArgs());

在這個過程中,HibernatePersistencePin中的seesion本身是沒有被實例化的,可是在CommonStatement.prepare中,pjp.proceed()之前,我們準備了session並用setSession的方法爲它注入了session,也就是所謂的setter注入。無論Session是什麼方式的,HibernatePersistencePin無需再關心關不關Session,一切都交給了CommonStatement了。

      OK,測試了一下,成功。AOP,不錯的編程思想。

 

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