详解SSJ(Spring3.x mvc + Spring3.x Core + JPA2.x)轻量级集成开发—第6章 剖析Spring3.x AOP特性02

上一章 详解SSJ(Spring3.x mvc + Spring3.x Core + JPA2.x)轻量级集成开发—第5章 剖析Spring3.x AOP特性01

下一章 详解SSJ(Spring3.x mvc + Spring3.x Core + JPA2.x)轻量级集成开发—第7章 笔者带走进Spring3.x MVC的世界

 

目录

一、基于Schema风格配置AOP增强处理;

二、基于Schema风格配置全局切入点;

三、Spring AOP与AspectJ AOP的关系;

四、使用@Aspect定义切面;

五、基于Annotation风格配置AOP增强处理;

六、基于Annotation风格配置全局切入点;

 

前言

笔者在上一章节中,为大家详细且深入的讲解了有关AOP的一些基础知识。那么从本章开始,咱们要开始接着学习有关Spring AOP的后续知识,这些知识包括:增强处理、切入点配置、以及如何使用基于AspectJ的方式配置Spring AOP。但是在学习这些技术之前,笔者还是要提醒各位,掌握基础相对于进阶来说是必经之路,没有一步登天,所以耐下心学习,才能够让你成长的更快。

其次,有些朋友在阅读笔者博文的时候,往往容易误解笔者博文的标题。总是觉得笔者在讲解的时候少了些许后续章节应该包含的东西,在此笔者有必要澄清一些事实。笔者打算对每一章的知识点,逐步分析逐步讲解,这是需要极其漫长的过程和时间的。所以并不会在某一章节就对所有的知识点进行全方位概括,当然如果是因为博文的标题误导了你,笔者只能对你说声抱歉。

最后还要提及一点的是,由于笔者会经常维护、更新早期博文的内容,所以如果大家有兴趣还是可以再次进行阅读,或许你会有意想不到的收获。

 

一、详解AOP之增强处理

上一章中,笔者为大家简单的介绍了增强处理的一些概念。还记得什么是增强处理吗?增强处理无非就是定义了通知的执行时机、执行顺序。如果你无法理解什么是执行时机与执行顺序,那么请你回想下咱们在上一章编写JDK动态代理时,是否显式的指定过代理业务的执行?其实增强处理无法就是隐式的帮你做了这些事情而已。在Spring AOP中,增强处理一共包含如下5种类型:

1、前置通知(Before advice):代理业务执行于被拦截的方法之前;

2、后置通知(After advice):代理业务执行于被拦截的方法之后;

3、环绕通知(Around advice):代理业务执行于被拦截的方法之前或之后

4、异常通知(After throwing advice):代理业务执行于被拦截的方法抛出异常后;

5、返回时通知(After returning advice):代理业务执行于被拦截的方法返回之前;

 

在基于Schema的AOP中配置中,我们可以通过使用如下标签配置Spring AOP的增强处理:

1、<aop:before/>:用于配置前置通知(Before advice);

2、<aop:after/>:用于配置后置通知(After advice);

3、<aop:around/>:用于配置环绕通知(Around advice);

4、<aop:after-throwing/>:用于配置异常通知(After throwing advice);

5、<aop:after-returning/>:用于配置返回通知(After returning advice);

 

上述标签我们可以将其称之为增强处理标签,因为这些标签均带有部分相同属性。分别为:method、pointcut、pointcut-ref、throwing和returning。其中属性“method”用于指定作为增强处理类型的切面类的指定方法。属性“pointcut”允许定义一个表达式,该表达式的作用就是拦截指定切入点,并且该表达式支持通配符“*”和“..”。其中通配符“*”代表了任意命名模式的匹配,而“..”则代表了接受0—N个任意类型参数的方法。属性“pointcut-ref”用于引用一个已经存在的切入点名称,换句话来说,该属性可以用于引用一个全局拦截切入点表达式。属性“throwing”仅针对<aop:after-throwing/>标签有效,该属性用于指定一个参数,异常通知(After throwing advice)便可以通过这个参数访问委托对象的指定方法抛出的异常。属性“returning”同样仅针对<aop:after-returning/>标签有效,该属性用于指定一个参数,返回时通知(After returning advice)便可以通过这个参数访问委托对象的指定方法的返回值。

 

笔者打算先从前置通知(Before advice)和后置通知(After advice)开始讲起,因为这两个通知是Spring AOP增强处理中最为简单,同时也是最容易理解的。前置通知(Before advice)无非就是在切入点执行之前首先执行代理业务,而后置通知(After advice)则反之。

基于Schema的风格配置Before and After增强处理:

<aop:config>
	<aop:aspect id="logAspect" ref="logBean" order="1">
		<!-- 前置通知 -->
		<aop:before method="logTest"
			pointcut="execution(* org.johngao.bean.LoginBean.*(..))" />
		<!-- 后置通知 -->
		<aop:after method="logTest"
			pointcut="execution(* org.johngao.bean.LoginBean.*(..))" />
	</aop:aspect>
</aop:config>
<bean name="logBean" class="org.johngao.bean.LogBean" />

 

在上述配置文件中,笔者为切入点定义了2个通知,分别为前置通知(Before advice)和后置通知(After advice)。在切入点执行之前首先执行前置通知(Before advice),然后再执行切入点,最后执行后置通知(After advice)。其中属性“pointcut”的值为:"execution(* org.johngao.bean.LoginBean.*(..))" 。也就是说指定通知将拦截org.johngao.bean包下LoginBean类型的所有带参或无参方法。当你看到这里的时候,或许你已经明白,在一个切入点内部,允许同时定义多个通知

Before and After通知实现:

/**
 *	模拟前置通知、后置通知
 *
 * 	@author JohnGao
 * 
 * 	@param JoinPoint: 连接点
 * 
 * 	@return void
 */
public void logTest(JoinPoint joinPoint)
{
	/* 获取委托对象指定方法参数 */
	Object[] params = joinPoint.getArgs();
	for(Object param: params)
		System.out.println(param);
	
	/* 获取委托对象指定方法名称 */
	System.out.println(joinPoint.getSignature().getName());
	
	/* 获取委托对象实例 */
	joinPoint.getTarget();
	
	/* 获取代理对象实例 */
	joinPoint.getThis();
	System.out.println("日志记录...");
}

 

在上述程序示例中,笔者使用到了AspectJ提供的JoinPoint接口。开发人员使用JoinPoint接口可以很方便的访问到委托对象的上下文信息,并且在实际开发过程中,这些上下文信息对于开发人员而言至关重要。JoinPoint接口的常用方法如下:

方法名称 方法返回值 方法描述
getArgs() Object[] 获取委托对象的目标方法参数列表,注意:由于返回数组所以需要拆分
getSignature() Signature 获取委托对象的方法签名
getTarget() Object 获取委托对象实例
getThis() Object 获取代理对象实例

 

当然JoinPoint接口还派生有另一个常用的扩展接口,那便是ProceedingJoinPoint接口。该接口仅限用于环绕通知(Around advice),并且相对JoinPoint接口而言,ProceedingJoinPoint接口为开发人员提供有2个新增用于执行委托对象的方法,分别为:

方法名称 方法返回值 方法描述
proceed() Object 用于执行委托对象的目标方法
proceed(java.lang.Object[] args) Object 用于执行委托对象的目标方法,用新参数替换原先入参

 

至于如何使用ProceedingJoinPoint接口,大家目前不必着急,笔者在后续章节自然会进行讲解。为了更加形象的描述不同通知的执行时机与执行顺序,笔者接下来会为大家展示各种通知执行时序图。

Before and After增强处理执行时序图:

详解SSJ(Spring3.x mvc + Spring3.x Core + JPA2.x)轻量级集成开发—第6章 剖析Spring3.x AOP特性02_第1张图片

 

当大家理解前置通知(Before advice)和后置通知(After advice)后,笔者接下来将会为大家讲解异常通知(After throwing advice)的配置和使用。所谓异常通知(After throwing advice)无非就是在切入点抛出异常之后执行代理业务,Spring的增强处理为开发人员提供了多种环境下执行通知的机会,在此笔者不得不佩服Rod Johnson丰富的想象力及“创造力”。

基于Schema的风格配置After throwing增强处理:

<aop:config>
	<aop:aspect id="logAspect" ref="logBean" order="1">
		<!-- 异常通知 -->
		<aop:after-throwing method="logTest"
			pointcut="execution(* org.johngao.bean.LoginBean.*(..))" />
	</aop:aspect>
</aop:config>
<bean name="logBean" class="org.johngao.bean.LogBean" />

 

如果你定义的切入点并没有往外抛出异常,通知是无法执行的。所以使用异常通知(After throwing advice)的时候,我们务必需要将为切入点throws异常。

还记得笔者在上一章节提到过的throwing属性吗?其仅针对<aop:after-throwing/>标签有效,该属性用于指定一个参数,异常通知(After throwing advice)便可以通过这个参数访问委托对象的指定方法抛出的异常。使用throwing属性指定参数:

<aop:after-throwing method="logTest"
	pointcut="execution(* org.johngao.bean.LoginBean.*(..))" 
	throwing="exception" />

 

捕获委托对象的指定方法抛出的异常:

/**
 *	模拟异常通知
 *
 * 	@author JohnGao
 * 
 * 	@param JoinPoint: 连接点, Exception: 异常信息
 * 
 * 	@return void
 */
public void logTest(JoinPoint joinPoint, Exception exception)
{
	System.out.println(exception);
}

 

通过上述程序示例我们可以看出,一旦委托对象的目标方法抛出异常后,我们便可以在通知中通过属性“exception”访问所抛出的异常信息。当然定义在通知中的方法参数,Spring的IOC容器自然会负责其初始化工作,所以你无须关心这些“琐事”,尽可能的关注于你的业务既可。

After throwing advice增强处理执行时序图:

详解SSJ(Spring3.x mvc + Spring3.x Core + JPA2.x)轻量级集成开发—第6章 剖析Spring3.x AOP特性02_第2张图片

 

返回时通知(After returning advice)其实也比较简单,该通知执行于被拦截的方法返回之前。也就是说当返回时通知(After returning advice)拦截切入点后,应当首先执行切入点,最后切入点即将返回时再执行通知。

基于Schema的风格配置After returning增强处理:

<aop:config>
	<aop:aspect id="logAspect" ref="logBean" order="2">
		<!-- 返回时通知 -->
		<aop:after-returning method="logTest"
			pointcut="execution(* org.johngao.bean.LoginBean.*(..))" />
	</aop:aspect>
</aop:config>
<bean name="logBean" class="org.johngao.bean.LogBean" />

 

还记得笔者在上一章节提到过的returning属性吗?其仅针对<aop:after-returning/>标签有效,该属性用于指定一个参数,返回时通知(After returning advice)便可以通过这个参数访问委托对象的指定方法的返回值。使用returning属性指定参数:

<aop:after-returning method="logTest"
	pointcut="execution(* org.johngao.bean.LoginBean.*(..))" 
	returning="rvt" />

 

获取委托对象的目标方法返回值:

/**
 *	模拟返回时通知
 *
 * 	@author JohnGao
 * 
 * 	@param JoinPoint: 连接点, Object: 返回值信息
 * 
 * 	@return void
 */
public void logTest(JoinPoint joinPoint, Object rvt)
{
	System.out.println(rvt);
}

 

通过上述程序示例我们可以看出,如果需要获取委托对象的目标方法的返回值时,我们可以通过定义<aop:after-returning/>标签中的“returning”属性即可。但这里需要注意的是,returning属性的属性名称务必和通知方法中的属性名称保持一致

 

After returning advice增强处理执行时序图:

详解SSJ(Spring3.x mvc + Spring3.x Core + JPA2.x)轻量级集成开发—第6章 剖析Spring3.x AOP特性02_第3张图片 

 

笔者至此已经为大家讲解了前置(Before advice)、后置(After advice)、异常(After throwing advice)、返回时(After returning advice)等4种通知的配置和使用,那么接下来笔者就开始讲解环绕通知(Around advice)。至于为什么需要将环绕通知(Around advice)放在最后一个讲解?并不是因为它相对其他几个通知更复杂,而是因为这个通知更特殊

但从字面上的理解,很多开发人员会错误的将环绕通知(Around advice)理解为是前置通知(Before advice)和后置通知(After advice)的结合体。以为该通知无非就是在切入点的执行前后各执行一次,其实这是错误的,笔者希望大家不要因为字面的显浅理解就妄下定论,免得贻笑大方。

环绕通知(Around advice)适用的场景更多的基于并发环境,并且该通知可以满足于切入点之前或者之后执行,甚至可以根据实际业务来判断是否执行。这才是使用环绕通知(Around advice)真正的目的。

基于Schema的风格配置Around增强处理:

<aop:config>
	<aop:aspect id="logAspect" ref="logBean" order="1">
		<!-- 环绕通知 -->
		<aop:around method="logTest"
			pointcut="execution(* org.johngao.bean.LoginBean.*(..))" />
	</aop:aspect>
</aop:config>
<bean name="logBean" class="org.johngao.bean.LogBean" />

 

ProceedingJoinPoint接口派生于JoinPoint接口,该接口仅限于环绕通知(Around advice)获取委托对象的上下文信息使用。笔者刚才也提到过,在实际开发过程中,我们可以使用环绕通知(Around advice)根据实际业务,决定通知是否执行,以及何时执行。

Around通知实现

/**
 *	模拟环绕通知
 *
 * 	@author JohnGao
 * 
 * 	@param ProceedingJoinPoint: 连接点
 * 
 * 	@return void
 */
public void logTest(ProceedingJoinPoint proceedingJoinPoint) 
		throws Throwable
{
	System.out.println("日志记录...");
	
	/* 执行委托对象的目标方法 */
	proceedingJoinPoint.proceed();
	System.out.println("日志记录...");
}

 

ProceedingJoinPoint接口的proceed()方法用于执行委托对象的目标方法。然而ProceedingJoinPoint接口还提供有另外一个带参的proceed(java.lang.Object[] args)方法,该方法除了可以用于执行委托对象的目标方法外,还支持使用新参数替换原先入参。

使用proceed(java.lang.Object[] args)方法替换原先入参:

/* 执行委托对象的目标方法 */
proceedingJoinPoint.proceed(new Object[]{"新入参,替换原先入参"});

 

一旦使用proceed(java.lang.Object[] args)方法替换原先入参后,新参数将会 覆盖之前传递给委托对象的目标方法参数。当然如果你不希望替换原先入参,笔者建议你还是使用proceed()方法即可。

二、基于Schema风格配置全局切入点

在5种通知标签中,我们可以通过使用pointcut属性定义一个表达式,该表达式的作用就是拦截指定切入点。只不过所配置的切入点的作用域仅针对对应的切面有效,换句话来说使用pointcut属性定义的拦截切入点表达式作用域是局部的。但是在实际开发过程中,我们往往需要定义一些通用的全局拦截切入点表达式,该如何实现呢?值得庆幸的是Spring为咱们提供有<aop:pointcut/>标签以满足开发人员的需求。

使用<aop:pointcut/>标签定义全局拦截切入点表达式相当简单,你仅仅只需要声明,然后在通知标签中使用属性“pointcut-ref”引用即可。这样一来,我们就可以定义一些通用的拦截切入点表达式,而不必每次都在通知标签中重复定义

使用<aop:pointcut/>标签定义全局拦截切入点表达式:

<aop:config>
	<aop:pointcut expression="execution(* org.johngao.bean.LoginBean.*(..))"
		id="pointcutTest" />
	<aop:aspect id="logAspect" ref="logBean" order="1">
		<!-- 环绕通知 -->
		<aop:around method="logTest" pointcut-ref="pointcutTest" />
	</aop:aspect>
</aop:config>
<bean name="logBean" class="org.johngao.bean.LogBean" />

 

三、Spring AOP与AspectJ AOP的关系

AspectJ是Java平台诞生的第一个AOP Framework,可以毫不客气的说AspectJ已经成为AOP领域的规范制定者。遵循AspectJ规范,也就是在遵循AOP的标准。目前市面上诸多AOP Framework都在借鉴AspectJ的一些思想,其中就包括Spring。值得庆幸的是AspectJ完全是开源的,并且完全采用Java语言编写的,开发人员可以自由下载AspectJ的源码进行研究和学习。

Spring针对AspectJ进行了很好的集成支持,并且Spring允许开发人员直接在Spring中使用AspectJ进行AOP编程。当然笔者并不打算对AspectJ进行深入讲解,感兴趣的朋友可以自行下载AspectJ的依赖构件及API。

或许谈到现在,有很多朋友还是不明白Spring AOP与AspectJ到底存在什么关系,难道仅仅只是遵循了AspectJ的规范进行自定义编制吗?其实不是的,Spring AOP与AspectJ从严格意义上来说完全是两码事,Spring AOP更像是一个粘合剂。除了允许你使用Spring的原生AOP实现,同时还支持你使用AspectJ作为Spring的AOP实现。早在Spring1.x的时代,由于那时候Spring还并未集成AspectJ,所以那时候的开发人员都只能使用Spring的原生AOP。但现在不同了,你完全可以使用AspectJ作为你的AOP实现,并且脱离Spring,AspectJ同样也能够单独使用。对于目前的开发团队而言,已经很少有人继续使用Spring的原生AOP来满足项目需要,更多的均是采用集成AspectJ的方式。因为这不仅仅是遵循一种规范,更重要的是使用AspectJ可以简化编码量。所谓开发实惠,就是这个道理,选择解耦的同时,注重开发效率也是一个优秀团队应该考虑的首要问题。

在上一章中,笔者跟大家提及过JDK动态代理和cglib动态代理。至于为什么要学习这2种代理,那是因为几乎所有的AOP Framework的底层实现均是使用这2种代理方式。JDK动态代理相对于cglib是有局限性的,因为JDK自身只支持基于接口的代理,而不支持类型的代理。当遇到代理类型是类类型的时候,我们或许可以考虑使用cglib。Spring AOP同样也是这么做的,Spring缺省使用JDK动态代理来作为AOP底层实现,那是为了实现高内聚、低耦合,Spring遵循了面向接口编程而已。只有当需要代理的为非接口类型时,Spring才会自动切换cglib作为AOP的底层实现。

最后你只需要明白一旦在Spring中使用AspectJ进行AOP编程,Spring AOP则依赖AspectJ,而AspectJ的底层实现仍然是采用JDK动态代理和cglib动态代理。

Spring AOP与AspectJ AOP的依赖关系图:

详解SSJ(Spring3.x mvc + Spring3.x Core + JPA2.x)轻量级集成开发—第6章 剖析Spring3.x AOP特性02_第4张图片

 

提示:

笔者在此还要补充一点的是,AspectJ采用的是编译时增强的解决方案。这与Spring原生的AOP实现是不同的,Spring的原生AOP实现采用的是代理实现方式,这2种底层实现方式的不同,并不代表着性能差距会很大,你只需关注你的业务即可。

 

四、使用@Aspect定义切面

AspectJ允许使用Annotation的方式来定义切面、增强类型以及切入点。当然你不用去关注Spring与AspectJ底层依赖的一些“琐事”,因为你根本没有必要理解这些东西,你只需要明白接下来咱们要做的事情就是在Spring中使用AspectJ提供的Annotation进行AOP编程即可。

我们可以使用AspectJ提供的@Aspect来定义一个切面类,但是在正式使用之前,我们必须要在IOC配置文件中添加AspectJ的支持,因为只有这样才能正常使用AspectJ AOP。

启动AspectJ AOP支持:

<!-- 启动AspectJ支持 -->
<aop:aspectj-autoproxy />

 

在第四章的时候,笔者为大家讲解了<context:component-scan/>标签的使用。既然我们使用的是AspectJ定义切面,那么必然我们还需要在自动扫包的时候,能够自动扫描定义的所有切面组件。<context:component-scan/>标签中包含了一个<context:include-filter/>子标签,该标签的作用就是IOC容器启动时,除了会自动扫描项目中所有的Bean组件外,还会扫描定义好的所有切面组件。

使用<context:include-filter/>标签自动扫描切面组件

<context:component-scan base-package="*">
	<context:include-filter type="annotation"
		expression="org.aspectj.lang.annotation.Aspect" />
</context:component-scan>

 

如果使用Annotation的方式定义切面,咱们就可以将以前以Schema风格定义在配置文件中的切面信息完全移除。开发人员只需在切面类上方加上@Aspect标注即可。

使用@Aspect定义切面类:

@Aspect
public class LogBean 
{
    //...
}

 

通过上述程序示例我们可以看出,使用@Aspect可成功定义一个切面类。相对于以Schema风格定义切面类来说确实方便了不少,同时也为IOC配置文件进行了极大的瘦身

 

五、基于Annotation风格配置AOP增强处理

使用Annotation的方式配置AOP增强处理,主要使用到的Annotation为如下5种:
1、@Before:用于配置前置通知(Before advice);

2、@After:用于配置后置通知(After advice);

3、@Around:用于配置环绕通知(Around advice);

4、@AfterThrowing:用于配置异常通知(After throwing advice);

5、@afterReturning:用于配置返回通知(After returning advice);

 

使用@Before配置前置通知(Before advice):

@Aspect
public class LogBean 
{
	@Before("execution(* org.johngao.bean.*.*(..))")
	public void logTest(JoinPoint joinPoint) 
	{
		System.out.println("日志记录...");
	}
}

 

使用@After用于配置后置通知(After advice):

@Aspect
public class LogBean 
{
	@After("execution(* org.johngao.bean.*.*(..))")
	public void logTest(JoinPoint joinPoint) 
	{
		System.out.println("日志记录...");
	}
}

 

使用@Around配置环绕通知(Around advice):

@Aspect
public class LogBean 
{
	@Around("execution(* org.johngao.bean.*.*(..))")
	public void logTest(ProceedingJoinPoint proceedingJoinPoint) 
			throws Throwable
	{
		System.out.println("日志记录...");
		
		/* 执行委托对象的目标方法 */
		proceedingJoinPoint.proceed();
		System.out.println("日志记录...");
	}
}

 

使用@AfterThrowing配置异常通知(After throwing advice):

@Aspect
public class LogBean 
{
	@AfterThrowing(pointcut = "execution(* org.johngao.bean.*.*(..))", 
			throwing="exception")
	public void logTest(JoinPoint joinPoint, Exception exception) 
	{
		System.out.println("异常: " + exception);
		System.out.println("日志记录...");
	}
}

 

使用@AfterReturning:用于配置返回通知(After returning advice):

@Aspect
public class LogBean 
{
	@AfterReturning(pointcut = "execution(* org.johngao.bean.*.*(..))", 
			returning ="rvt")
	public void logTest(JoinPoint joinPoint, Object rvt) 
	{
		System.out.println("返回值: " + rvt);
		System.out.println("日志记录...");
	}
}

 

在@AfterThrowing和@AfterReturning的内部,都包含一个叫做pointcut的属性。在基于Schema风格的配置中,我们都是使用这个属性来定义拦截切入点表达式。但是其内部缺省的value属性也可以用于定义拦截切入点表达式,那么我们应该如何选择呢?来看看Spring的官方解释

详解SSJ(Spring3.x mvc + Spring3.x Core + JPA2.x)轻量级集成开发—第6章 剖析Spring3.x AOP特性02_第5张图片

 

通过Spring的官方解释我们可以看出,缺省情况下以value属性定义拦截切入点表达式,如果显示指定pointcut作为拦截切入点表达式后,pointcut将重写value属性的定义。

 

六、基于Annotation风格配置全局切入点

在基于Schema的配置风格中,我们可以使用<aop:pointcut/>标签来配置全局拦截点表达式。而一旦我们使用基于Annotation的方式后,则可以使用@Pointcut的方式配置全局拦截点表达式。@Pointcut可以适用于委托对象的目标方法之上,同样也可以适用于通知上。当然具体怎么使用就根据项目需要或者你的个人喜好而定。

使用@Pointcut配置全局切入点:

@Aspect
public class LogBean {
	@Before(value ="testPointcut()")
	public void logTest() {
		System.out.println("日志记录...");
	}
	
	@Pointcut("execution(* org.johngao.bean.*.*(..))")
	public void testPointcut() {
		//...
	}
}

 

如果其他切面类也需要使用这个全局的拦截点表达式,则可以使用类名.方法名的方式进行引用

@Before(value ="LogBean.testPointcut()")

 

本章内容到此结束,由于时间仓库,本文或许有很多不尽人意的地方,希望各位能够理解和体谅。关于下一章的内容,笔者打算讲解Spring3.x MVC相关的内容。

你可能感兴趣的:(spring,maven,jpa)