【Spring系列】- 深入理解Spring AOP
文章目录
一、AOP产生的原因
开发中为了调试,或在进入生产环境后为了对系统进行监控,需要为这些业务需求的实现对象添加日志记录功能;或者,业务方法的执行需要一定的权限限制,那么方法执行前肯定需要有相应的安全检查功能
而这些则属于系统需求的范畴,虽然需求都很明确(加入日志记录、加入安全检查),但是要将这些需求以面向对象的方式实现并集成到整个的系统中去,则系统中的每个业务对象都需要加入日志记录,加入相应的安全检查,随着系统需求的增多,系统开发和维护的难度会越来越复杂
对于系统中普通的业务关注点,OOP可以很好地对其进行分解并使之模块化,但却无法更好地避免类似于系统需求的实现在系统中各处散落的问题
AOP全称为Aspect-OrientedProgramming,面向切面编程。使用AOP,我们可以对类似于Logging和Security等系统需求进行模块化的组织
任何一个软件系统就跟CREDIT系统一样,日志记录、安全检查、事务管理等系统需求就像一把把刀“恶狠狠”地横切到组织良好的各个业务功能模块之上。这些系统需求是系统中的横切关注点(cross-cuttingconcern),AOP引入了Aspect的概念,用来以模块化的形式对系统中的横切关注点进行封装。Aspect之对于AOP,就相当于Class之对于OOP。AOP仅是对00P方法的一种补足,当把以Class形式模块化的业务需求和以Aspect形式模块化的系统需求拼装到一起时,整个系统就算完成了
Spring AOP是Spring核心框架的重要组成部分,Spring AOP与Spring的IoC容器以及Spring框架对其他JavaEE服务的集成共同组成了Spring框架的“质量三角”
Spring AOP采用Java作为AOP的实现语言(AOL),在2.0引入了AspectJ的支持
二、Spring AOP实现机制
SpringAOP属于第二代AOP,采用动态代理机制和字节码生成技术实现。与最初的AspectJ采用编译器将横切逻辑织入目标对象不同,动态代理机制和字节码生成都是在运行期间为目标对象生成一个代理对象,而将横切逻辑织入到这个代理对象中,系统最终使用的是织入了横切逻辑的代理对象,而不是真正的目标对象。
1. 设计模式之代理模式
代理处于访问者与被访问者之间,可以隔离这两者之间的直接交互,访问者与代理打交道就好像在跟被访问者在打交道一样,因为代理通常几乎会全权拥有被代理者的职能,代理能够处理的访问请求就不必要劳烦被访问者来处理了。从这个角度来说,代理可以减少被访问者的负担。另外,即使代理最终要将访问请求转发给真正的被访问者,它也可以在转发访问请求之前或者之后加入特定的逻辑,比如安全访问限制等
ISubject: 该接口是对被访问者或被访问资源的抽象,某些场景下不使用类似的统一抽象接口也是可以的
SubjectImpl: 被访问者或被访问资源的具体实现类
SubjectProxy: 被访问者或者被访问资源的代理实现类,该类持有一个subject接口的具体实例
Client: 代表访问者的抽象角色,Client将会访问 ISubject类型的对象或资源。但Client无法直接请求其真正要访问的资源SubjectImpl,而是必须要通过ISubject资源的访问代理类subjectproxy进行
subjectImpl和subjectProxy都实现了相同的接口ISubject,而subjectProxy内部持有subjectImpl的引用。当Client通过reguest()请求服务的时候,SubjectProxy将转发该请求给subjectImpl。在将请求转发给被代理对象subjectImpl之前或者之后,都可以根据情况插入其他处理逻辑。甚至,可以不做请求转发,这样,就不会有subjectImpl的访问发生
如果subjectImpl是系统中的Joinpoint所在的对象,即目标对象,那么就可以为这个目标对象创建一个代理对象,然后将横切逻辑添加到这个代理对象中。当系统使用这个代理对象运行的时候,原有逻辑的实现和横切逻辑就完全融合到一个系统中
2. 动态代理
虽然Joinpoint相同 (比如都是reguest()方法的执行),但是目标对象类型是不一样的,故要为每种对象单独实现一个代理对象,可是这些代理对象所要添加的横切逻辑是一样的。这种为对应的目标对象创建静态代理的方法,可能因为要添加代理而代理逻辑一样的对象太多而很麻烦
JDK1.3之后引入了动态代理(Dynamic Proxy)机制,可以为指定的接口在系统运行期间动态地生成代理对象。动态代理机制的实现主要由java.lang.reflect.Proxy类和java.lang.reflect.InvocationHàndler接口组成
- 比如,虽然要为ISubject和IReguestable两种类型提供代理对象,但因为代理对象中要添加的横切逻辑是一样的,都是request()方法,所以,只需要实现一个InvocationHandler即可
// RequestCtrlInvocationHand1er定义
public class ReguestCtrlInvocationHandler implements InvocationHandler {
private static final Log logger =
LogFactory.getLog(RequestCtrlInvocationHandler.class);
private Object target;
public RequestCtrlInvocationHandler(Object target) {
this.target =target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("request")) {
TimeOfDay startTime = new TimeOfDay(0,0,0);
TimeOfDay endTime = new TimeOfDay(5.59.59);
TimeofDay currentTime = new TimeOfDay();
if (currentTime.isAfter(startTime) && currentTime.isBefore(endTime)) {
logger.warn("service is not available now.");
return null;
}
return method.invoke(target, args);
}
return null;
}
}
- 然后,就可以使用proxy类,根据RequestCtrlInvocationHandler的逻辑,为ISubject和IRequestable两种类型生成相应的代理对象实例
// 使用Proxy和RequestctrlInvocationHandier创建不同类型目标对象的动态代理
ISubject subject = (ISubject)Proxy.newProxyInstance(
ProxyRunner.class.getClassLoader(),
new Class[]{Isubject.class},
new RequestCtrlInvocationHandler(new SubjectImpl())
);
subject.request();
IRequestable reguestable = (IRequestable)Proxy.newProxyInstance(
ProxyRunner.class.getClassLoader(),
new Class[]{IRecuestable.class},
new RequestCtrlInvocationHandler(new RequestableImpl())
);
requestable.request();
- 即使还有更多的目标对象类型,只要它们织入的横切逻辑一样 ,用RecuestCtrlInvocationHandler类并通过proxy为它们生成相应的动态代理实例就可以满足要求
- 当Proxy动态生成的代理对象上相应的接口方法被调用时,对应的InvocationHandler就会拦截相应的方法调用,并进行相应处理。InvocationHandler就是实现横切逻辑的地方,它是横切逻辑的载体,作用跟Advice是一样的
- 动态代理虽好,但不能满足所有的需求。因为动态代理机制只能对实现了相应Interface的类使用,如果某个类没有实现任何的Interface,就无法使用动态代理机制为其生成相应的动态代理对象
- 默认情况下,如果Spring AOP发现目标对象实现了相应Interface,则采用动态代理机制为其生成代理对象实例。而如果目标对象没有实现任何Interface,SpringAOP会尝试使用一个称为 CGLIB(Code Generation Library) 的开源的动态字节码生成类库,为目标对象生成动态的代理对象实例
3. 动态字节码生成
使用动态字节码生成技术扩展对象行为的原理:可以对目标对象进行继承扩展,为其生成相应的子类,而子类可以通过覆写来扩展父类的行为,只要将横切逻辑的实现放到子类中,然后让系统使用扩展后的目标对象的子类,就可以达到与代理模式相同的效果了
使用继承的方式来扩展对象定义,也不能像静态代理模式那样,为每个不同类型的目标对象都单独创建相应的扩展子类,故要借助于CGLIB动态字节码生成库,在系统运行期间动态地为目标对象生成扩展子类
- 要对Reguestable类进行扩展,首先需要实现net.sf.cglib.proxy.Callback接口,不过更多的时候,我们会直接使用net.sf.cglib.proxy.MethodInterceptor接口(MethodInterceptor扩展了ca11back接口)
// ReguestCtrlCa11back类定义
public class RequestCtrlCallback implements MethodInterceptor {
private static final Log logger = LogFactory.getLog(RequestCtrlCallback.class);
public Object intercept(object object,Method method, Object[] args,MethodProxy proxy)throws Throwable {
if(method.getName().equals("recuest")) {
TimeOfDay startTime = new TimeofDay(0,0,0);
TimeOfDay endTime = new TimeofDay(5,59,59);
TimeOfDay currentTime = new TimeOfDay();
if(currentrime.isAfter(startTime)&& currentTime.isBefore(endTime)){
logger.warn("service is not available now.");
return null;
}
return proxy.invokeSuper(object,args);
}
return null;
}
}
- 这样,RequestCtrlCallback就实现了对request()方法请求进行访问控制的逻辑。现在要通过CGLIB的Enhancer为目标对象动态地生成一个子类,并将ReguestCtrlCallback中的横切逻辑附加到该子类中
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Requestable.class);
enhancer.setCallback(new RequestCtrlCallback());
Requestable proxy = (Requestable)enhancer.create();
proxy.request();
- 通过为enhancer指定需要生成的子类对应的父类,以及Callback实现,enhancer最终为我们生成了需要的代理对象实例
- 使用CGLIB对类进行扩展的唯一限制就是无法对nal方法进行覆写
三、Spring AOP中的 JoinPoint
在动态代理和CGLIB的支持下,Spring AOP框架的实现经过了两代,Spring2.0是分界线
-
Advice(通知): 某个连接点所采用的处理逻辑,也就是向连接点注入的代码
-
JointPoint(连接点):程序运行中的某个阶段点,比如方法的调用、异常的抛出等
-
Pointcut(切入点): JoinPoint的集合,是程序中需要注入Advice的位置的集合,指明Advice要在什么样的条件下才能被触发,在程序中主要体现为书写切入点表达式
-
Advisor(增强):是Pointcut和Advice的综合体,完整描述了一个advice将会在pointcut所定义的位置被触发
-
@Aspect(切面):通常是一个类的注解,里面可以定义切入点和通知
AOP的Joinpoint可以有许多种类型,如构造方法调用、字段的设置及获取、方法调用、方法执行等。但在Spring AOP中,仅支持方法级别的Joinpoint。更确切地说,只支持方法执行(MethodExecution)类型的Joinpoint
Spring AOP之所以如此,主要有以下几个原因:
- SpringAOP要提供一个简单而强大的AOP框架,并不想因大而全使得框架本身过于臃肿
- 对于类中属性(Field)级别的 Joinpoint,如果提供这个级别的拦截支持,那么就破坏了面向对象的封装,而且,完全可以通过对setter和getter方法的拦截达到同样的目的
- 如果应用需求非常特殊,完全超出了SpringAOP提供的那80%的需求支持,不妨求助于现有的其他AOP实现产品,如AspectJ。目前来看,AspectJ是Java平台对AOP支持最完善的产品,同时,Spring AOP也提供了对AspectJ的支持
四、Spring AOP中的 Pointcut
- Spring中的org.springframework.aop.Pointcut接口,定义了两个方法用来帮助捕捉系统中的相应Joinpoint,TruePointcut类型实例实现了Pointcut接口
- 如果Pointcut类型为TruePointcut,默认会匹配系统中的所有对象,以及对象上所有被支持的Joinpoint
// org.springframework.aop.Pointcut接口定义代码
public interface Pointcut {
// ClassFilter和MethodMatcher分别用于匹配将被执行织入操作的对象以及相应的方法
ClassFilter getClassFilter();
MethodMatcher getMethodMatcher();
Pointcut TRUE = TruePointcut.INSTANCE;
}
-
之所以将类型匹配和方法匹配分开定义,是因为可以重用不同级别的匹配定义,并且可以在不同的级别或者相同的级别上进行组合操作,或者强制让某个子类只Overide相应的方法定义等
-
classFilter接口的作用是对Joinpoint所处的对象进行Class级别的类型匹配
public interface ClassFilter{ boolean matches(Class clazz); ClassFilter TRUE = TrueClassFilter.INSTANCE; // 匹配到了则返回所有实例 }
- 当织入的目标对象的Class类型与Pointcut所规定的类型相符时,matches方法将会返回true,否则,返回false,即意味着不会对这个类型的目标对象进行织入操作
- 当然,如果类型对我们所捕捉的Joinpoint无所谓,那么Pointcut中使用的classFilter可以直接使用
classFilter TRUE = TrueClassFilter.INSTANCE;
-
相对于classFilter的简单定义,MethodMatcher则要复杂得多:
public interface MethodMatcher { boolean matches(Method method, Class targetClass); boolean isRuntime(); boolean matches(Method method, Class targetClass, Object[] args); MethodMatcher TRUE = TrueMethodMatcher.INSTANCE; }
- MethodMatcher通过重载(Overload),定义了两个matches方法
- 在对对象具体方法进行拦截的时候,可以忽略每次方法执行的时候调用者传入的参数Object[] args,也可以每次都检查这些方法调用参数,以强化拦截条件
- 如果只想在某方法之前插入某功能,那么该方法的参数对于Joinpoint捕捉就是可以忽略的;而如果在方法执行时要用到参数,那么这个方法的参数就是在匹配Joinpoint的时候必须要考虑的
- 在前一种情况下,isRuntime返回false,表示不会考虑具体Joinpoint的方法参数,这种类型的MethodMatcher称之为staticMethodMatcher。只有前一种matches方法将被执行,它的匹配结果将会成为其所属的Pointcut主要依据。因为不用每次都检査参数,那么对于同样类型的方法匹配结果,就可以在框架内部缓存以提高性能
- 当isRuntime方法返回true时,表明该MethodMatcher将会每次都对方法调用的参数进行匹配检査,这种类型的MethodMatcher称之为DynamicMethodMatcher。因为每次都要对方法参数进行检查,无法对匹配的结果进行缓存,所以,匹配效率相对于staticMethodmatcher来说要差
- 如果一个MethodMatcher为DynamicMethodMatcher,即isRuntime()返回true,并且第一个matches方法也返回true时,第二个matches方法将被执行,以进一步检有匹配条件
- 如果第一个matches方法返同false,那么不管这个MethodMatcher是staticMethodatcher还是DynamicMethodMatcher,该结果已经是最终的匹配结果,第二个matches方法将不被执行
-
本MethodMatcher类型的基础上,Pointcut可以分为两类,即staticMethodMatcherPointcut和DynamicMethodMatcherPointcut。因为StaticMethodMatcherpointcut具有朋显的性能优势,所以,Spring为其提供了更多支持
1. 常见的Pointcut
NameMatchMethodPointcut
-
这是最简单的Pointcut实现,属于staticMethodMatcherPointcut的子类,可以根据自身指定的一组方法名称与Joinpoint处的方法的方法名称进行匹配,比如:
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut(); // 传入一个或多个方法名 pointcut.setMappedName("matches"); pointcut.setMappedNames(new String[]("matches","isRuntime"});
-
但是,NameMatchMethodPointcut无法对重载(Overload)的方法名进行匹配,因为它仅对方法名进行匹配,不会考虑参数相关信息,而且也没有提供可以指定参数匹配信息的途径
-
NameMatchMethodPointcut除了可以指定方法名,以对指定的Joinpoint进行匹配,还可以使用* 通配符,实现简单的模糊匹配,如:
pointcut .setMappedNames (new String[]{"match*","*matches","mat*es");
-
如果基于* 通配符的NameMatchMethodpointcut依然无法满足对Joinpoint的匹配需求,则用正则表达式
JdkRegexpMethodPointcut 和 Perl5RegexpMethodPointcut
-
StaticMethodMatcherPointcut的子类中有一个专门提供基于正则表达式的实现分支,以抽象类AbstractRegexMethodPointcut为统帅,其下设两种具体实现:JdkRegexpMethodPointcut和Perl5RegexpMethodPointcut
-
与NameMatchMethodPointcut相似,AbstractRegexpMethodPointcut声明了pattern和patterns属性,可以指定一个或多个正则表达式的匹配模式
-
JdkRegexpMethodPointcut
- JdkRegexpMethodPointcut的实现基于JDK 1.4之后引入的JDK标准正则表达式,简单使用示例如下:
JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut(); pointcut.setPattern(".*match.*"); // 或者 pointcut.setPatterns(new String[]{".*match.*", ".*matches"});
-
注意!使用正则表达式来匹配 Joinpoint所处的方法时,正则表达式的匹配模式必须以匹配整个方法签名的形式指定,而不能像NameMatchMethodPointcut那样仅给出匹配的方法名称
-
也就是说,如果有对象定义如下:
package cn.spring21.sample; public class Bar{ public void dosth(){...}; }
那么,使用正则表达式
.*dosth.*
则会匹配cn.spring21.demo.Bar.dosth
但如果Pointcut使用正则表达式
dosth.*
,就无法捕捉到Bar的dosth方法也可以通过正则表达式中的转义:
cn\.spring21\sample\.Bar\.dosth
,指定更确切的方法匹配
-
Perl5RegexpMethodpointcut
- 如果当前应用还无法使用JDK 1.4或更高版本,或更喜欢perl5风格的正则表达式,那么可以使用perl5RegexpMethodPointcut。该Pointcut实现使用Jakarta ORO提供正则表达式支持
- 除了正则表达式的语法上可能有少许差异,Perl5RegexpMethodpointcut的使用和需要注意的问题与JdkRegexpMethodPointcut几乎相同:
- 可以通过pattern或者patterns对象属性指定一个或者多个正则表达式的匹配模式
- 指定的正则表达式匹配模式应该覆盖匹配整个方法签名,而不是只指定到方法名称部分
AnnotationMatchingPointcut
-
AnnotationMatchingPointcut只能用在使用JDK5或更高版本的应用中,因为注解是在Java5发布后才有的
-
AnnotationMatchingPointcut根据目标对象中是否存在指定类型的注解来匹配 Joinpoint,要使用该类型的Pointcut,首先需要声明相应的注解
-
针对GenericTargetObject类型,不同的AnnotationMatchingPointcut定义会产生不同的匹配行为:
@ClassLevelAnnotation public class GenericTargetobject { @MethodLevelAnnotation public void gMethod1(){ System.out.println("gMethod1"); } public void gMethod2(){ System.out.println("gMethod2"); } }
-
指定类级别的注解,则GenericTargetobject类中所有方法执行时,不管该方法指定了注解还是没有指定,将全部匹配
AnnotationMatchingPointcut pointcut = new AnnotationMatchingPointcut (ClassLevelAnnotation.class);
-
也可以通过AnnotationMatchingPointcut的静态方法,来构建类级别的注解对应的AnnotationMatchingPointcut实例:
AnnotationMatchingPointcut pointcut = AnnotationMatchingPointcut.forClassAnnotation(ClassLevelAnnotation.class);
-
如果只指定方法级别的注解而忽略类级别的注解,则该Pointcut仅匹配特定的标注了指定注解的方法定义,而忽略其他方法。对于Genericrargetobject,或者其他类中任何方法来说,只要它标注了@MethodLevelAnnotation,则这些方法将都会被匹配并织入相应的横切逻辑
AnnotationMatchingPointcut pointcut = AnnotationMatchingPointcut.forMethodAnnotation(MethodtevelAnnotation.class);
-
通过同时限定类级别的注解和方法级别的注解,可以进一步缩小“包围圈”。现在,只有标注了@ClassLevelAnnotation的类定义中同时标注了@MethodLevelAnnotation的方法才会被匹配,即只有gMethod1()方法才会被拦截
AnnotationMatchingPointcut pointcut = new AnnotationMatchingPointcut (ClassLevelAnnotation.class, MethodLevelAnnotation.class);
ComposablePointcut
-
Pointcut通常还提供逻辑运算功能,而composablePointcut就是Spring AOP提供的可以进行Pointcut逻辑运算的Pointcut实现。它可以进行Pointcut之间的 并运算、交运算,如:
ComposablePointcut pointcut1 = new ComposablePointcut(classFilter1, methodMatcher1); ComposablePointcut pointcut2 = new ComposablePointcut(classFilter2, methodMatcher2); ComposablePointcut unitedPoincut = pointcut1.union(pointcut2); ComposablePointcut intersectionPointcut = pointcut1.intersection(unitedPoincut); assertEquals(pointcut1, intersectionPointcut); ComposablePointcut pointcut3 = pointcut2.union(ciassFilter1) .intersection(methodMatcher1) ;
-
可见之前说的,Pointcut根据classFilter和MethodMatcher分别划分,是为了通过交并运算,进行不同classFilter、MethodMatcher和pointcut之间的复用和相互组合
-
如果只想进行Pointcut之间的逻辑运算,Spring AOP提供了org.springframework.aop.support.Pointcuts工具类,用法与ComposablePointcut类似:
Pointcut pointcut1 = ...; Pointcut pointcut2 = ...; Pointcut unitedPoincut = Pointcuts.union(pointcut1, pointcut2); Pointcut intersectionPointcut = Pointcuts.intersection(pointcut1, pointcut2);
ControlFlowPointcut
-
跟其他类型的Pointcut比,ControlFlowPointcut是最特殊的Pointcut类型。ControlFlowPointcut匹配程序的调用流程,不是对某个方法执行所在的 Joinpoint处的单一特征进行匹配
-
假设所拦截的目标对象如下:
public class TargetObject{ public void method1(){...}; } // 调用类 public class TargetCaller{ private TargetObject target; public void callMethod(){ target.method(); } public void setTarget(TargetObject target){ this.target = target; } }
-
如果使用之前的任何Pointcut实现,我们只能指定在Targetobject的method1方法每次执行的时候,都织入相应横切逻辑。也就是说,一旦通过Pointcut指定method1处为Joinpoint,那么对该方法的执行进行拦截是必定的,不管method1是被谁调用。而通过controlFlowPointcut,可以指定,只有当TargetObject的method1方法在TargetCaller类所声明的方法中被调用时,才对method1方法进行拦截,其他地方调用method1,不对method1进行拦截
-
虽然method1可以被多个对象在不同的执行流程内被调用,但是通过controlFlowPointcut,可以指定,只有按照特定的执行流程,才会触发method1处的 Joinpoint
ControlFlowPointcut pointcut = new ControlFlowPointcut(TargetCaller.class); Advice advice = ...; TargetObject target = new TargetObject(); // 注意此处注weaver为虚构概念,不是Spring中的织入器实现 TargetObject targetObjectToUse = weaver.weave(advice).to(target).accordingto(pointcut); // advice的逻辑在这里将不会被触发执行 targetObiectToUse.method1(): // advice的逻辑在这里将被触发执行,因为TargetCaller的ca1lMethod()将调用method1 TargetCaller caller = new TargetCaller(); caller.setTarget(targetObjectToUse); caller.callMethod();
当织入器按照Pointcut的规定,将Advice织入到目标对象之后,从任何其他地方调用method1,是不会触发Advice所包含的横切逻辑的执行的;只有在controlFlowPointcut规定的类内部调用目标对象的method1,才会触发Advice中横切逻辑的执行
-
如果在ControlFlowPointcut的构造方法中单独指定Class类型的参数,那么ControlFlowPointcut将尝试匹配指定的Class中声明的所有方法,跟目标对象的 Joinpoint处的方法流程组合;
所以如果只是想完成
TargetCaller.callMethod()
调用TargetObject.method1()
这样的流程匹配,而忽略TargetCaller中的其他方法与TargetObject中方法的Control Flow匹配,则可在构造ControlFlowPointcut时,传入第二个参数,即调用方法的名称:ControlFlowPointcut pointcut = new ControlFlowPointcut(TargetCaller.class, "callMethod");
-
因为ControlFlowPointcut类型的Pointcut需要在运行期间检查程序的调用栈,而且每次方法调用都需要检查,所以性能比较差。如果不是十分必要,应该尽量避免这种Pointcut的使用
2. 扩展Pointcut(Customize Pointcut)
如果以上Spring AOP提供的Pointcut类型不够用了,无法满足特殊需求了,则可以扩展SpringAOP的Pointcut定义,给出自定义的Pointcut实现
Spring AOP已经提供了相应的扩展抽象类支持,只需要继承相应的抽象父类,然后实现或重写相应方法逻辑即可。Spring AOP的Pointcut类型可以划分为StaticMethodMatcherPointcut和DynamicMethodMatcherPointcut两类。要实现自定义Pointcut,通常在这两个抽象类的基础上实现相应子类即可
-
自定义 StaticMethodMatcherPointcut
StaticMethodMatcherPointcut根据自身语意,为其子类提供了如下默认实现
- 默认所有StaticMethodMatcherPointcut的子类的ClassFilter均为
ClassFilter.TRUE
,即忽略类的类型匹配。如果具体子类需要对目标对象的类型做进一步限制,可以通过setClassFilter(Classrilter classFilter)
方法设置相应的ClassFilter实现 - 因为是StaticMethodMatcherPointcut,所以,其MethodMatcher的isRuntime方法返回false,同时第二个matches方法抛出UnsupportedperationException异常,以表示该方法不应该被调用到
- 最终,我们需要做的就是实现第一个matches方法即可
- 默认所有StaticMethodMatcherPointcut的子类的ClassFilter均为
-
自定义 DynamicMethodMatcherPointcut
DynamicMethodMatcherPointcut也为其子类提供了部分便利
getClassFilter()
方法默认返回ClassFilter.TRUE
,如果需要限定特定的目标对象类型,子类只要重写getClassFilter()
方法即可- 对应的MethodMatcher的
isRuntime
总是返回true,同时,StaticMethodMatcherPointcut提供了两个参数的matches方法的实现,默认直接返回true - 要实现自定义DynamicMethodMatcherPointcut,通常我们只需要实现第二个matches方法即可
- 我们也可以覆写一下两个参数的matches方法,这样,不用每次都得到三个参数的matches方法执行时,才检查所有的条件
3. loC 容器中的 Pointcut
Spring中的Pointcut实现都是普通的Java对象,所以,同样可以通过Spring的IoC容器来注册并使用它们。
如果某个Pointcut自身需要某种依赖,可以通过IOC容器为其注入;或者如果容器中的某个对象需要依赖于某个Pointcut,也可以把这个Pointcut注入到依赖对象中
不过,通常在使用Spring AOP的过程中,不会直接将某个Pointcut注册到容器,然后公开给容器中的对象使用。但将各个Pointcut以独立的形式注册到容器中使用是完全合情合理的,如下所示:
<bean id="nameMatchPointcut" class="org.springframework.aop,support .NameMatchMethodPointcut">
<property name="mappedNames">
<list>
<value>methodName1</value>
<value>methodName2</value>
</list>
</property>
</bean>
五、Spring AOP中的 Advice
Advice实现了将被织入到Pointcut规定的 Joinpoint处的横切逻辑
Spring中的Advice实现全部遵循AOP Alliance规定的接口,Spring中各种Advice类型实现与AOP Aliance中标准接口之间的关系如图:
在Spring中,Advice按照其自身实例(instance)能否在目标对象类的所有实例中共享这一标准,划分为两大类:per-class类型的Advice和per-instance类型的Advice
-
per-class类型的 Advice
- 该类型的Advice的实例可以在目标对象类的所有实例之间共享
- 这种类型的Advice通常只是提供方法拦截的功能,不会为目标对象类保存任何状态或添加新的特性
- 除了图中没有列出的Introduction类型的Advice外,图中的所有Advice均属per-class类型的Advice
-
per-instance类型的 Advice
- 该类型的Advice的实例不会在目标类所有对象实例之间共享,而是会为不同的实例对象保存它们各自的状态以及相关逻辑
- 在Spring AOP中,Introduction就是唯一的一种per-instance型Advice
- Introduction可以在不改动目标类定义的情况下,为目标类添加新的属性以及行为
- 在Spring中,为目标对象添加新的属性和行为必须声明相应的接口以及相应的实现。之后再通过特定的拦截器将新的接口定义以及实现类中的逻辑附加到目标对象之上。这样,目标对象(确切地说是目标对象的代理对象)就拥有了新的状态和行为
- 这个特定的拦截器就是org.springframework.aop.IntroductionInterceptor
六、拦截器匹配与执行的流程
以下是 invoke
方法处理拦截器链的详细步骤:
- 遍历拦截器链并检查是否匹配:
invoke
方法会首先获取与当前调用的方法相关的拦截器列表(也称为 Advisor 或 Interceptor Chain)。- 每个拦截器都有自己的匹配条件(如使用
Pointcut
表达式进行匹配),它们会检查当前调用的方法是否符合它们的拦截条件。 - 如果某个拦截器与当前方法匹配,则将其加入到即将执行的拦截器列表中。
- 拦截器链的执行(逐一调用拦截器):
- Spring 使用 责任链模式 来执行拦截器。它会创建一个拦截器链,将所有匹配的拦截器按顺序执行。
- 每个拦截器都实现了一个
MethodInterceptor
接口,定义了invoke()
方法。这个方法可以在调用目标方法之前或之后添加自定义逻辑。 - 拦截器被逐一调用时,每个拦截器都可以决定:
- 是否继续调用下一个拦截器。
- 是否立即执行目标方法。
- 是否改变返回值或处理异常。
- 调用目标对象的方法:
- 当所有拦截器都执行完毕后(或某个拦截器决定提前执行),才会调用实际的目标对象的方法。
- 目标方法的调用由代理对象内部保存的目标对象引用来完成。通常调用的方式是通过反射调用目标对象的方法。
- 处理返回值与异常:
- 目标方法执行后,代理逻辑还可以处理返回值或捕获异常。如果有
After
类型的拦截器(如AfterReturningAdvice
或AfterThrowingAdvice
),它们会在此时被调用,用来处理方法的返回结果或异常。
- 目标方法执行后,代理逻辑还可以处理返回值或捕获异常。如果有
- 例子:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 1. 获取当前方法的拦截器链
List<MethodInterceptor> interceptors = getInterceptorsForMethod(method);
// 2. 创建拦截器链的迭代器
MethodInvocation invocation = new ReflectiveMethodInvocation(target, method, args, interceptors);
// 3. 开始执行拦截器链,调用下一个拦截器
return invocation.proceed();
}
在 ReflectiveMethodInvocation
类中,proceed()
方法控制拦截器的逐一调用:
public Object proceed() throws Throwable {
// 如果没有拦截器了,调用目标对象的方法
if (this.currentInterceptorIndex == this.interceptors.size() - 1) {
return method.invoke(target, args);
}
// 不然,获取下一个拦截器并调用它的 invoke 方法
MethodInterceptor interceptor = interceptors.get(++currentInterceptorIndex);
return interceptor.invoke(this); // 传入当前 MethodInvocation 对象
}
- 拦截器链遍历和匹配:遍历过程中,每个拦截器检查当前方法是否符合它的拦截条件。如果符合,则加入拦截器链。
- 逐一调用拦截器:
proceed()
方法是一个递归调用过程,每次调用下一个拦截器的方法。每个拦截器可以决定是否继续调用下一个拦截器。 - 最终调用目标方法:当所有拦截器都完成或某个拦截器触发了对目标方法的调用时,最终会通过反射调用目标对象的方法。
- 拦截器链的作用:拦截器链的设计使得我们可以在方法调用前后加入任意的逻辑,比如日志记录、权限验证、事务管理等。