您的位置:首页 > 编程语言 > Java开发

Spring AOP深度详解 综合

2010-12-24 11:59 381 查看

Spring AOP 的关键概念

posted in Java, Java EE, Spring

静态AOP:通过修改应用程序实际的字节码来完成织入过程,从而根据需求修改和扩展代码,由于最终结果就是普通的Java字节码,因此性能较高。但是注意其缺点是,如果想作任何的修改,即使只是加入一个新的JointPoint,都必须重新编译整个程序。

动态AOP:织入过程是在运行时动态进行的。具体的实现有若干种,在Spring中是为所有目标对象创建代理(但不能为final类生成代理),以便Advice可以按需要被使用。虽然其性能比不上静态的AOP,但是对应用的修改不必重新编译主程序的代码。

建议首选Spring AOP,只在功能达不到需求的时候,或者在程序性能调优时发现性能较差时才改用AspectJ。在Spring AOP中,第一部份是AOP core,它提供了完全解耦的,纯编程式的AOP功能;第二部份则是为让我们能在程序中更简便地使用AOP而给出的一组框架服务。Spring AOP仅是所有AOP特性的一个子集,但涵盖了最常用的90%的功能。

Jointpoint:一个程序执行过程中的特定点。可以是方法的调用,方法执行过程本身,类的初始化,对象的实例化等。

Advice:在某一特定的jointpoint运行的代码。可以分为before advice, after advice等。

Pointcut:用来定义某一个advice应该何时执行的一组jointpoint。比如一个典型的pointcut就是对某一个类的所有方法调用的集合,通常我们会组建复杂的pointcut来控制advice什么时候执行。

Aspect: advice和pointcut的组合。这个组合定义了一段程序中应该包括的逻辑以及何时应该执行该逻辑。

Weaving:将aspect真正加入程序代码的过程。这里可以分为编译时织入和运行时织入。

Target: 如果一个对象的执行过程受到某个AOP操作的修改,那么它就是一个目标对象。

Introduction:通过引入,我们可以在一个对象中加入新的方法或字段,以改变它的结构,你可以使用引入来让任何对象实现一个特定的接口,而不需要对这个对象的类型显式地实现这个接口。

Spring内部有两种实现代理的方法:JDK动态代理和CGLIB代理,且CGLIB代理的性能较优。Spring AOP只支持方法级别的jointPoint。

接口Advisor代表一个Aspect,但这个接口不建议直接使用,而是使用其子接口,来创建各种不同的aspect。

ProxyFactory的addAddvice方法,这个方法内部会将提供的advice包装在一个DefaultPointCutAdvisor对象里面,它也是PointCutAdvisor的标准实现,将其配置为包含所有默认方法调用的pointcut。如果想对创建的Advisor有更多的控制,或者要向代理中添加一个introduction,那么可以自行创建Advisor并调用addAdvisor方法。同时,ProxyFactory也提供了removeAdvice,removeAdvisor等方法来删除之前加入的advisor。

前置advice是最有用的advice之一,它可以修改传递给目标方法的参数,也可以抛出一个异常来阻止目标方法的执行。

后置advice是在方法调用完成后才执行的。我们没有办法改变已经执行的方法的参数,只能读取而不能修改目标方法的执行路径或阻止目标方法的执行,更重要的是也无法修改目标方法的返回值。不过,还是可以抛出一个异常让调用者看到的是异常而不是返回值。

包围通知在功能上综合了前置通知和后置通知,并且可以修改方法的返回值,或者阻止方法的执行。即将目标方法的实现完全更换成新的代码。Spring的很多功能,如远程代理支持和事务管理,都是由拦截器完成的。同时,拦截器也是剖析程序运行的好办法。

抛出通知和后置通知一样,是在jointPoint之后运行的,不过抛出通知只在方法抛出一个异常时才执行。此外,抛出通知对程序运行本身不能做任何改变。我们可以做的只是改变抛出异常的类型,比如需要使用一个API时,它的异常设计非常糟糕,那就可以抛出通知来通知该API中所有的类,这样就能重新设计异常继承的结构,从而使之更易管理并且具备自描述性。同时,也可以用抛出通知来提供一个集中的错误日志,以此减少散布在程序各处的记录错误日志代码的数量。

要注意的是 ThrowsAdvice只是一个标志接口,里面的方法是通过反射来调用 的,其实现类必须以下面的形式来实现方法:

void afterThrowing([Method,args,target,] ThrowableSubclass);

其中前三个参数是可选的。特别地,对于抛出的每一个异常,Spring会选择签名与抛 出的异常最匹配的一个方法,如果抛出的的Advice定义了两个afterThrowing方法, 而且它们的异常类型一样,但分别有1个和4个参数,那Spring会运行含有4个参数 的afterThorwing方法。

注意,如果一个throws-advice方法本身抛出一个exception,它会覆盖原始的异常, 即改变抛出给用户的异常,改写后的一般是一个RuntimeException,因为这与任何方 法签名都是兼容的。然而,如果一个throws-advice方法抛出一个checked Exception, 那么它必须匹配目标方法抛出的异常,所以这在一定程度上与目标方法的签名是耦合 的。永远不要抛出一个非声明的与目标方法签名不兼容的checked exception。

需明确的几个概念:

1、通知(Advice):用于告知系统将有哪些新的行为。
2、切入点(Pointcut):定义了通知应该在应用到那些连接点
3、目标对象(Target):被通知的对象。
4、 代理(Proxy):将通知应用到目标对象后创建的对象。

Spring有两种代理创建方式:

1.如果目标对象实现了一个或多个接口暴露的方法,Spring将使用JDK的java.lang.reflect.Proxy创建代理。这个类让Spring动态产生一个新的类,它实现了所需的接口,织入了通知,并且代理目标的所有请求。(这篇主要介绍这个方式)
2.如果目标对象没有实现任何接口,Spring使用CGLIB库生成目标对象的子类。在创建这个子类的时候,Spring将通知织入,并且将对目标对象的调用委托给这个子类。

下面以一个实例说明Spring AOP的基本开发方法:
一.创建通知
Spring连接点模型是建立在方法拦截上,这意味着你编写的Spring通知会在方法调用周围的各个地方织入系统中。



图1
TestAopServiceAdvice实现了接口MethodBeforeAdvice(前置通知),并实现它的惟一的方法before,这个类就可以在调用目标对象前被调用。同样的还有AfterReturningAdvice(后置通知),MethodInterceptor(环绕通知),异常通知(ThrowsAdvice),引入通知等。
在这个方法中我们输出了一个字符串TestAopServiceAdvice,用于验证这个方法是否在目标对象前调用了。
注意:我们无法改变before方法中的参数args和target中的值,args中存的是原来要传入目标对象的变量,target即指目标对象。
二.配置Spring XML配置文件
要在Spring中实现AOP,一般情况下需要配置4个bean:
1.目标对象(target)
2.通知(advice)
3.切入点(pointcut)
4.代理(proxy)



图2
切入点又分为静态切入点和动态切入点
1、 静态切入点的意思是通知总是被执行,也是最常用的一种切入点。
2、 动态切入点根据运行时方法的参数值决定通知是否被执行。
在图2中,定义了使用了一个Spring提供的静态切入点
NameMatchMethodPointAdvisor,它保证了当被调用的方法的名字与给出的映射名字相匹配的时候,这个切入点才匹配。
Spring提供的另一个静态切入点为RegexpMethodPointcutAdvisor,让你可以利用正则表达式来定义切入点.
使用ProxyBeanFactory可以创建一个被通知的类,即代理对象。它的最常用的二个控制行为的属性是:
1、proxyInterfaces:代理应该实现的接口列表
2、interceptorNames:需要应用到目标对象上的通知Bean的名字。可以是拦截器、Advisor或其他通知类的名字。
注:在用容器的getBean方法时,应该是getBean(代理类的名字),而不是getBean(目标对象的名字),否则AOP无法工作。

1.1 AOP的概念

简单地说,AOP就是将那些与业务无关,却为业务模块所共同调用的逻辑或责任,例如事务处理(Transactions),日志管理(logging, tracing, profiling and monitoring 记录跟踪 优化 校准),权限控制(Authentication),同步(Synchronization), 封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。
实质上,AOP只是OOP的一种补充或某种改进,它转换了编程的范式和视角,关注了一直以来被OOP忽略或者说未能解决好的角落,使开发人员可以更好地将本不该彼此纠缠在一起的责任(如银行业务和事务处理)分离开来。通过面向切面的编程,可以将程序的责任分开,对象与切面互不干扰。面向切面的模块并非显式地为对象所调用,而是通过或注入或截取的方式,去获得被封装的对象内部方法间的消息,然后做出相应地处理。也许面向切面的模式破坏了对象的封装,却正其如此,方才能降低模块与模块之间的耦合度。同样地,通过对“方面”的封装,将这些通用的功能从不同的类中分离出来,使不同的模块都能共享同样的“方面”,这也极大地减少了重复代码。
Spring 的AOP核心技术还是动态代理,Spring的代理实现方式有两种,默认使用CGLIB,也可以选择使用JDK的代理,这一切对开发人员来说是透明的,可以不关心这些。

1.2 AOP的基本术语

以Spring为例:
1.面(Aspect): 一个关注点的模块化,这个关注点可能会横切多个对象。事务管理是J2EE应用中一个关于横切关注点的很好的例子。 在Spring AOP中,切面可以使用通用类(基于模式的风格) 或者在普通类中以 @Aspect 注解(@AspectJ风格)来实现。

2.连接点(Joinpoint):在程序执行过程中某个特定的点,比如某方法调用的时候或者处理异常的时候。 在Spring AOP中,一个连接点 总是 代表一个方法的执行。 通过声明一个 org.aspectj.lang.JoinPoint 类型的参数可以使通知(Advice)的主体部分获得连接点信息。

3.知(Advice): 在切面的某个特定的连接点(Joinpoint)上执行的动作。通知有各种类型,其中包括“around”、“before”和“after”等通知。 通知的类型将在后面部分进行讨论。许多AOP框架,包括Spring,都是以拦截器做通知模型,并维护一个以连接点为中心的拦截器链。

4.切入点(Pointcut 匹配连接点(Joinpoint)的断言。通知和一个切入点表达式关联,并在满足这个切入点的连接点上运行(例如,当执行某个特定名称的方法时)。 切入点表达式如何和连接点匹配是AOP的核心:Spring缺省使用AspectJ切入点语法。

5.引入(Introduction (也被称为内部类型声明(inter-type declaration))。声明额外的方法或者某个类型的字段。 Spring允许引入新的接口(以及一个对应的实现)到任何被代理的对象。 例如,你可以使用一个引入来使bean实现 IsModified 接口,以便简化缓存机制。

6.目标对象(Target Object 被一个或者多个切面(aspect)所通知(advise)的对象。也有人把它叫做 被通知(advised) 对象。 既然Spring AOP是通过运行时代理实现的,这个对象永远是一个 被代理(proxied) 对象。

7.AOP代理(AOP Proxy AOP框架创建的对象,用来实现切面契约(aspect contract)(包括通知方法执行等功能)。 在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。 注意:Spring 2.0最新引入的基于模式(schema-based)风格和@AspectJ注解风格的切面声明,对于使用这些风格的用户来说,代理的创建是透明的。

8.织入(Weaving把切面(aspect)连接到其它的应用程序类型或者对象上,并创建一个被通知(advised)的对象。 这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。 Spring和其他纯Java AOP框架一样,在运行时完成织入。

通知的类型:

1.前置通知(Before advice 在某连接点(join point)之前执行的通知,但这个通知不能阻止连接点前的执行(除非它抛出一个异常)。

2.返回后通知(After returning advice 在某连接点(join point)正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回。

3.抛出异常后通知(After throwing advice 在方法抛出异常退出时执行的通知。

4.后通知(After (finally) advice当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。

5.环绕通知(Around Advice 包围一个连接点(join point)的通知,如方法调用。这是最强大的一种通知类型。 环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它们自己的返回值或抛出异常来结束执行。











此前对于AOP的使用仅限于声明式事务,除此之外在实际开发中也没有遇到过与之相关的问题。最近项目中遇到了以下几点需求,仔细思考之后,觉得采用AOP 来解决。一方面是为了以更加灵活的方式来解决问题,另一方面是借此机会深入学习Spring AOP相关的内容。本文是权当本人的自己AOP学习笔记,以下需求不用AOP肯定也能解决,至于是否牵强附会,仁者见仁智者见智。

对部分函数的调用进行日志记录,用于观察特定问题在运行过程中的函数调用情况

监控部分重要函数,若抛出指定的异常,需要以短信或邮件方式通知相关人员

金控部分重要函数的执行时间

事实上,以上需求没有AOP也能搞定,只是在实现过程中比较郁闷摆了。

需要打印日志的函数分散在各个包中,只能找到所有的函数体,手动添加日志。然而这些日志都是临时的,待问题解决之后应该需要清除打印日志的代码,只能再次手动清除^_^!

类 似1的情况,需要捕获异常的地方太多,如果手动添加时想到很可能明天又要手动清除,只能再汗。OK,该需求相对比较固定,属于长期监控的范畴,并不需求临 时添加后再清除。然而,客户某天要求,把其中20%的异常改为短信提醒,剩下的80%改用邮件提醒。改之,两天后,客户抱怨短信太多,全部改成邮件提 醒...

该需求通常用于监控某些函数的执行时间,用以判断系统执行慢的瓶颈所在。瓶颈被解决之后,烦恼同情况1

终于下定决心,采用AOP来解决!代码如下:

切面类TestAspect

Java代码

package com.spring.aop;

/**

* 切面

*

*/

public class TestAspect {

public void doAfter(JoinPoint jp) {

System.out.println("log Ending method: "

+ jp.getTarget().getClass().getName() + "."

+ jp.getSignature().getName());

}

public Object doAround(ProceedingJoinPoint pjp) throws Throwable {

long time = System.currentTimeMillis();

Object retVal = pjp.proceed();

time = System.currentTimeMillis() - time;

System.out.println("process time: " + time + " ms");

return retVal;

}

public void doBefore(JoinPoint jp) {

System.out.println("log Begining method: "

+ jp.getTarget().getClass().getName() + "."

+ jp.getSignature().getName());

}

public void doThrowing(JoinPoint jp, Throwable ex) {

System.out.println("method " + jp.getTarget().getClass().getName()

+ "." + jp.getSignature().getName() + " throw exception");

System.out.println(ex.getMessage());

}

private void sendEx(String ex) {

//TODO 发送短信或邮件提醒

}

}

Java代码

package com.spring.service;

/**

* 接口A

*/

public interface AService {

public void fooA(String _msg);

public void barA();

}

Java代码

package com.spring.service;

/**

*接口A的实现类

*/

public class AServiceImpl implements AService {

public void barA() {

System.out.println("AServiceImpl.barA()");

}

public void fooA(String _msg) {

System.out.println("AServiceImpl.fooA(msg:"+_msg+")");

}

}

Java代码

package com.spring.service;

/**

* Service类B

*/

public class BServiceImpl {

public void barB(String _msg, int _type) {

System.out.println("BServiceImpl.barB(msg:"+_msg+" type:"+_type+")");

if(_type == 1)

throw new IllegalArgumentException("测试异常");

}

public void fooB() {

System.out.println("BServiceImpl.fooB()");

}

}

ApplicationContext

Java代码

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xmlns:aop="http://www.springframework.org/schema/aop"

xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd"
default-autowire="autodetect">

<aop:config>

<aop:aspect id="TestAspect" ref="aspectBean">

<!--配置com.spring.service包下所有类或接口的所有方法-->

<aop:pointcut id="businessService"

expression="execution(* com.spring.service.*.*(..))" />

<aop:before pointcut-ref="businessService" method="doBefore"/>

<aop:after pointcut-ref="businessService" method="doAfter"/>

<aop:around pointcut-ref="businessService" method="doAround"/>

<aop:after-throwing pointcut-ref="businessService" method="doThrowing" throwing="ex"/>

</aop:aspect>

</aop:config>

<bean id="aspectBean" class="com.spring.aop.TestAspect" />

<bean id="aService" class="com.spring.service.AServiceImpl"></bean>

<bean id="bService" class="com.spring.service.BServiceImpl"></bean>

</beans>

测试类AOPTest

Java代码

public class AOPTest extends AbstractDependencyInjectionSpringContextTests {

private AService aService;

private BServiceImpl bService;

protected String[] getConfigLocations() {

String[] configs = new String[] { "/applicationContext.xml"};

return configs;

}

/**

* 测试正常调用

*/

public void testCall()

{

System.out.println("SpringTest JUnit test");

aService.fooA("JUnit test fooA");

aService.barA();

bService.fooB();

bService.barB("JUnit test barB",0);

}

/**

* 测试After-Throwing

*/

public void testThrow()

{

try {

bService.barB("JUnit call barB",1);

} catch (IllegalArgumentException e) {

}

}

public void setAService(AService service) {

aService = service;

}

public void setBService(BServiceImpl service) {

bService = service;

}

}

运行结果如下:

Java代码

log Begining method: com.spring.service.AServiceImpl.fooA

AServiceImpl.fooA(msg:JUnit test fooA)

log Ending method: com.spring.service.AServiceImpl.fooA

process time: 0 ms

log Begining method: com.spring.service.AServiceImpl.barA

AServiceImpl.barA()

log Ending method: com.spring.service.AServiceImpl.barA

process time: 0 ms

log Begining method: com.spring.service.BServiceImpl.fooB

BServiceImpl.fooB()

log Ending method: com.spring.service.BServiceImpl.fooB

process time: 0 ms

log Begining method: com.spring.service.BServiceImpl.barB

BServiceImpl.barB(msg:JUnit test barB type:0)

log Ending method: com.spring.service.BServiceImpl.barB

process time: 0 ms

log Begining method: com.spring.service.BServiceImpl.barB

BServiceImpl.barB(msg:JUnit call barB type:1)

log Ending method: com.spring.service.BServiceImpl.barB

method com.spring.service.BServiceImpl.barB throw exception

测试异常

《Spring参考手册》中定义了以下几个AOP的重要概念,结合以上代码分析如下:

切面(Aspect) :官方的抽象定义为“一个关注点的模块化,这个关注点可能会横切多个对象”,在本例中,“切面”就是类TestAspect所关注的具体行为,例如,AServiceImpl.barA()的调用就是切面TestAspect所关注的行为之一。“切面”在ApplicationContext中<aop:aspect>来配置。

连接点(Joinpoint) :程序执行过程中的某一行为,例如,AServiceImpl.barA()的调用或者BServiceImpl.barB(String _msg, int _type)抛出异常等行为。

通知(Advice) :“切面”对于某个“连接点”所产生的动作,例如,TestAspect中对com.spring.service包下所有类的方法进行日志记录的动作就是一个Advice。其中,一个“切面”可以包含多个“Advice”,例如TestAspect

切入点(Pointcut) :匹配连接点的断言,在AOP中通知和一个切入点表达式关联。例如,TestAspect中的所有通知所关注的连接点,都由切入点表达式execution(* com.spring.service.*.*(..))来决定

目标对象(Target Object) :被一个或者多个切面所通知的对象。例如,AServcieImpl和BServiceImpl,当然在实际运行时,Spring AOP采用代理实现,实际AOP操作的是TargetObject的代理对象。

AOP代理(AOP Proxy) 在Spring AOP中有两种代理方式,JDK动态代理和CGLIB代理。默认情况下,TargetObject实现了接口时,则采用JDK动态代理,例如,AServiceImpl;反之,采用CGLIB代理,例如,BServiceImpl。强制使用CGLIB代理需要将
<aop:config>
proxy-target-class
属性设为true

通知(Advice)类型

前置通知(Before advice) :在某连接点(JoinPoint)之前执行的通知,但这个通知不能阻止连接点前的执行。ApplicationContext中在<aop:aspect>里面使用<aop:before>元素进行声明。例如,TestAspect中的doBefore方法

后通知(After advice) :当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。ApplicationContext中在<aop:aspect>里面使用<aop:after>元素进行声明。例如,TestAspect中的doAfter方法,所以AOPTest中调用BServiceImpl.barB抛出异常时,doAfter方法仍然执行

返回后通知(After return advice) :在某连接点正常完成后执行的通知,不包括抛出异常的情况。ApplicationContext中在<aop:aspect>里面使用<after-returning>元素进行声明。

环绕通知(Around advice) :包围一个连接点的通知,类似Web中Servlet规范中的Filter的doFilter方法。可以在方法的调用前后完成自定义的行为,也可以选择不执行。ApplicationContext中在<aop:aspect>里面使用<aop:around>元素进行声明。例如,TestAspect中的doAround方法。

抛出异常后通知(After throwing advice) : 在方法抛出异常退出时执行的通知。 ApplicationContext中在<aop:aspect>里面使用<aop:after-throwing>元素进行声明。例如,TestAspect中的doThrowing方法。

切入点表达式

通常情况下,表达式中使用”execution“就可以满足大部分的要求。表达式格式如下:

Java代码

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

modifiers-pattern:方法的操作权限

ret-type-pattern:返回值

declaring-type-pattern:方法所在的包

name-pattern:方法名

parm-pattern:参数名

throws-pattern:异常

其中,除ret-type-pattern和name-pattern之外,其他都是可选的。上例中,execution(* com.spring.service.*.*(..))表示com.spring.service包下,返回值为任意类型;方法名任意;参数不作限制的所有方法。

通知参数

可以通过args来绑定参数,这样就可以在通知(Advice)中访问具体参数了。例如,<aop:aspect>配置如下

Java代码

<aop:config>

<aop:aspect id="TestAspect" ref="aspectBean">

<aop:pointcut id="businessService"

expression="execution(* com.spring.service.*.*(String,..)) and args(msg,..)" />

<aop:after pointcut-ref="businessService" method="doAfter"/>

</aop:aspect>

</aop:config>

TestAspect的doAfter方法中就可以访问msg参数,但这样以来AService中的barA()和BServiceImpl中的barB()就不再是连接点,因为execution(* com.spring.service.*.*(String,..))只配置第一个参数为String类型的方法。其中,doAfter方法定义如下:

Java代码

public void doAfter(JoinPoint jp,String msg)

访问当前的连接点

任何通知(Advice)方法可以将第一个参数定义为
org.aspectj.lang.JoinPoint
类型。
JoinPoint
接口提供了一系列有用的方法, 比如
getArgs()
(返回方法参数)、
getThis()
(返回代理对象)、
getTarget()
(返回目标)、
getSignature()
(返回正在被通知的方法相关信息)和
toString()
(打印出正在被通知的方法的有用信息。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: