`
掌心童话
  • 浏览: 14734 次
  • 性别: Icon_minigender_1
  • 来自: 大连
社区版块
存档分类
最新评论

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,不错的编程思想。

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics