MiniSpring框架学习笔记-Pointcut:如何批量匹配代理方法?
- 19. Pointcut:如何批量匹配代理方法?
- 一、先看最终效果
- 二、先分清 Advice、Pointcut 和 Advisor
- 三、定义 Pointcut 和 MethodMatcher
- 四、按方法名实现切点
- 五、用 PointcutAdvisor 组合切点和通知
- 六、ProxyFactoryBean 要能识别 Advisor
- 七、动态代理根据切点决定是否增强
- 八、验证匹配结果
- 小结
教程:https://github.com/YaleGuo/minis
极客时间:手把手带你写一个 MiniSpring
19. Pointcut:如何批量匹配代理方法?
上一节已经把增强逻辑从动态代理中抽成了MethodInterceptor,不过,哪些方法需要增强仍然写死在代理类里:
if("doAction".equals(method.getName())&&interceptor!=null){Class<?>targetClass=this.target==null?null:this.target.getClass();MethodInvocationinvocation=newReflectiveMethodInvocation(proxy,this.target,method,args,targetClass);returninterceptor.invoke(invocation);}这样只能增强doAction()。如果又增加了doSomething()、doSave()等方法,就要继续修改代理类。
这显然不是一个可扩展的做法。本节要引入Pointcut(切点),把“哪些方法需要增强”也变成独立配置。
先注意一个容易混淆的地方:JDK 生成的代理对象仍然代理接口中的所有方法,Pointcut决定的是“哪个方法调用需要执行增强”,不是“只为哪些方法生成代理”。
一、先看最终效果
项目中的IAction现在有两个业务方法:
packagecom.chenhai.test;publicinterfaceIAction{voiddoAction();voiddoSomething();}调用方还是照常从容器中获取代理对象:
IActionaction=(IAction)context.getBean("action");action.doAction();action.doSomething();beans.xml使用mappedName="do*"批量匹配所有以do开头的方法:
<beanid="realaction"class="com.chenhai.test.Action1"/><beanid="beforeAdvice"class="com.chenhai.test.MyBeforeAdvice"/><beanid="advisor"class="com.chenhai.aop.NameMatchMethodPointcutAdvisor"><propertytype="com.chenhai.aop.Advice"name="advice"ref="beforeAdvice"/><propertytype="String"name="mappedName"value="do*"/></bean><beanid="action"class="com.chenhai.aop.ProxyFactoryBean"><propertytype="java.lang.Object"name="target"ref="realaction"/><propertytype="String"name="interceptorName"value="advisor"/></bean>因此,doAction()和doSomething()都会先执行前置通知:
----------my interceptor before method call---------- real action doAction........ ----------my interceptor before method call---------- real action doSomething........整个判断过程可以先记成下面这条主线:
代理对象收到方法调用 ↓ 从 Advisor 取得 MethodInterceptor ↓ 从 Pointcut 取得 MethodMatcher ↓ 判断当前方法是否匹配 do* ↓ 命中:执行增强,再调用真实方法 未命中:直接调用真实方法核心理解
在当前 MiniSpring 教学版中,AOP 代理的创建最终落在这一行代码上:
this.singletonInstance=getProxy(createAopProxy());
createAopProxy()先根据配置组装目标对象和Advisor,得到具体的JdkDynamicAopProxy;getProxy()再调用 JDK 动态代理 API,生成业务代码最终拿到的代理对象。哪个对象需要被代理、使用哪段增强逻辑、匹配哪些方法,都写在 XML 中,再由 IoC 容器通过 setter 自动注入。以上面的配置为例:
action(ProxyFactoryBean) ├─ target → realaction(Action1,被代理的真实对象) └─ interceptorName → advisor ↓ NameMatchMethodPointcutAdvisor ├─ advice → beforeAdvice(MyBeforeAdvice,具体增强逻辑) └─ mappedName → do*(方法匹配规则)所以,
ProxyFactoryBean先拿到target=realaction和interceptorName=advisor,再从容器中取得NameMatchMethodPointcutAdvisor。这个Advisor已经由容器注入了beforeAdvice和do*:前者决定“怎么增强”,后者决定“哪些方法需要增强”。框架最后根据这些对象自动生成代理,业务代码不需要手动创建或组装代理对象。
二、先分清 Advice、Pointcut 和 Advisor
切点可以理解成一个筛选条件,有点像 SQL 中的where:它只负责找出需要增强的方法,不负责执行增强。
本章几个核心对象的职责如下:
| 对象 | 负责什么 |
|---|---|
Advice/MethodInterceptor | 做什么增强 |
Pointcut | 在哪里增强 |
MethodMatcher | 判断当前方法是否命中 |
PointcutAdvisor | 把增强逻辑和切点组合起来 |
它们之间的关系是:
PointcutAdvisor ├─ MethodInterceptor:怎么增强 └─ Pointcut └─ MethodMatcher:当前方法是否匹配这样一来,代理只负责组织调用,不再关心具体匹配规则是方法名、注解还是其他条件。
三、定义 Pointcut 和 MethodMatcher
先定义切点接口。当前教学版只匹配方法,所以Pointcut只需要提供MethodMatcher:
packagecom.chenhai.aop;/** * 切点,表示“在哪里增强”。 */publicinterfacePointcut{MethodMatchergetMethodMatcher();}MethodMatcher专门回答“这个方法要不要增强”:
packagecom.chenhai.aop;importjava.lang.reflect.Method;/** * 判断某个方法是否需要被 AOP 增强。 */publicinterfaceMethodMatcher{booleanmatches(Methodmethod,Class<?>targetClass);}这里同时传入了method和targetClass,是为了让匹配器既能检查方法,也能检查真实目标类。当前按名称匹配的实现只使用method,targetClass暂时没有参与判断。
完整的 Spring 切点还可以同时筛选目标类和方法。本章只保留方法匹配,是为了先把最小调用链跑通。
最后扩展Advisor,让它同时提供增强和切点:
packagecom.chenhai.aop;/** * 带切点的 Advisor。 */publicinterfacePointcutAdvisorextendsAdvisor{PointcutgetPointcut();}普通Advisor只回答“怎么增强”,PointcutAdvisor还可以回答“在哪里增强”。
四、按方法名实现切点
接下来实现NameMatchMethodPointcut。它既是Pointcut,也直接承担MethodMatcher的匹配工作:
packagecom.chenhai.aop;importcom.chenhai.util.PatternMatchUtils;importjava.lang.reflect.Method;/** * 按方法名匹配的切点。 * * mappedName 可以是精确名称,也可以包含简单通配符 *。 */publicclassNameMatchMethodPointcutimplementsMethodMatcher,Pointcut{privateStringmappedName="";publicvoidsetMappedName(StringmappedName){// 避免后续匹配时出现空指针。this.mappedName=mappedName==null?"":mappedName;}publicStringgetMappedName(){returnthis.mappedName;}@Overridepublicbooleanmatches(Methodmethod,Class<?>targetClass){if(method==null){returnfalse;}StringmethodName=method.getName();returnthis.mappedName.equals(methodName)||isMatch(methodName,this.mappedName);}protectedbooleanisMatch(StringmethodName,StringmappedName){returnPatternMatchUtils.simpleMatch(mappedName,methodName);}@OverridepublicMethodMatchergetMethodMatcher(){// 当前对象自己就能匹配方法,所以直接返回 this。returnthis;}}支持的匹配方式包括:
mappedName | 可以匹配 |
|---|---|
doAction | 精确匹配doAction |
do* | doAction、doSomething |
*Action | 所有以Action结尾的方法 |
*Action* | 所有名称中包含Action的方法 |
do*Action | 以do开头、以Action结尾的方法 |
这里的*是简单通配符,不是正则表达式,也不支持?等其他通配符。
真正的通配符算法放在工具类中,切点只保留 AOP 相关职责:
publicstaticbooleansimpleMatch(Stringpattern,Stringstr){if(pattern==null||str==null){returnfalse;}intfirstIndex=pattern.indexOf('*');if(firstIndex==-1){returnpattern.equals(str);}if(firstIndex==0){if(pattern.length()==1){returntrue;}intnextIndex=pattern.indexOf('*',1);if(nextIndex==-1){returnstr.endsWith(pattern.substring(1));}Stringpart=pattern.substring(1,nextIndex);if(part.isEmpty()){returnsimpleMatch(pattern.substring(nextIndex),str);}intpartIndex=str.indexOf(part);while(partIndex!=-1){StringremainingPattern=pattern.substring(nextIndex);StringremainingString=str.substring(partIndex+part.length());if(simpleMatch(remainingPattern,remainingString)){returntrue;}partIndex=str.indexOf(part,partIndex+1);}returnfalse;}returnstr.length()>=firstIndex&&pattern.substring(0,firstIndex).equals(str.substring(0,firstIndex))&&simpleMatch(pattern.substring(firstIndex),str.substring(firstIndex));}这段算法的核心思路是:*前面的内容必须精确匹配,遇到*后再递归匹配剩余部分。匹配算法本身不是本章重点,知道NameMatchMethodPointcut把它当成工具使用即可。
五、用 PointcutAdvisor 组合切点和通知
现在还差一个对象,把Advice和NameMatchMethodPointcut放在一起:
packagecom.chenhai.aop;/** * 使用方法名切点的 Advisor。 */publicclassNameMatchMethodPointcutAdvisorimplementsPointcutAdvisor{privateAdviceadvice;privateMethodInterceptormethodInterceptor;privateStringmappedName;privatefinalNameMatchMethodPointcutpointcut=newNameMatchMethodPointcut();publicNameMatchMethodPointcutAdvisor(){}publicNameMatchMethodPointcutAdvisor(Adviceadvice){setAdvice(advice);}publicvoidsetAdvice(Adviceadvice){this.advice=advice;// 统一适配成代理能够执行的 MethodInterceptor。setMethodInterceptor(adaptAdvice(advice));}publicAdvicegetAdvice(){returnthis.advice;}@OverridepublicMethodInterceptorgetMethodInterceptor(){returnthis.methodInterceptor;}@OverridepublicvoidsetMethodInterceptor(MethodInterceptormethodInterceptor){this.methodInterceptor=methodInterceptor;}@OverridepublicPointcutgetPointcut(){returnthis.pointcut;}publicvoidsetMappedName(StringmappedName){this.mappedName=mappedName;// XML 注入 mappedName 时,同时更新内部切点。this.pointcut.setMappedName(mappedName);}publicStringgetMappedName(){returnthis.mappedName;}privateMethodInterceptoradaptAdvice(Adviceadvice){if(adviceinstanceofMethodBeforeAdvice){returnnewMethodBeforeAdviceInterceptor((MethodBeforeAdvice)advice);}if(adviceinstanceofAfterReturningAdvice){returnnewAfterReturningAdviceInterceptor((AfterReturningAdvice)advice);}if(adviceinstanceofMethodInterceptor){return(MethodInterceptor)advice;}thrownewIllegalArgumentException("Unsupported advice type: "+advice);}}XML 注入会触发两个关键 setter:
setAdvice()把前置、后置通知适配成统一的MethodInterceptor;setMappedName()把do*交给内部的NameMatchMethodPointcut。
最终,advisor同时持有了“怎么增强”和“增强哪些方法”。
六、ProxyFactoryBean 要能识别 Advisor
上一节的interceptorName只能指向一个普通Advice。现在 XML 中配置的是:
<propertytype="String"name="interceptorName"value="advisor"/>虽然属性名还叫interceptorName,但它现在既可以指向Advice,也可以指向完整的Advisor。ProxyFactoryBean需要先识别取出来的对象:
privatesynchronizedvoidinitializeAdvisor(){if(this.advisor!=null||this.interceptorName==null||this.interceptorName.trim().isEmpty()){return;}if(this.beanFactory==null){thrownewIllegalStateException("BeanFactory is required to resolve interceptorName");}ObjectinterceptorOrAdvisor;try{interceptorOrAdvisor=this.beanFactory.getBean(this.interceptorName);}catch(BeansExceptione){thrownewIllegalStateException("Resolve interceptor failed: "+this.interceptorName,e);}this.advisor=adaptAdvisor(interceptorOrAdvisor);}privateAdvisoradaptAdvisor(ObjectinterceptorOrAdvisor){if(interceptorOrAdvisorinstanceofAdvisor){// Advisor 已经包含通知;PointcutAdvisor 还会包含切点。return(Advisor)interceptorOrAdvisor;}if(!(interceptorOrAdvisorinstanceofAdvice)){thrownewIllegalArgumentException("Unsupported interceptor/advisor type: "+interceptorOrAdvisor);}// 兼容上一章:单独的 Advice 包成不带切点的 DefaultAdvisor。DefaultAdvisordefaultAdvisor=newDefaultAdvisor();defaultAdvisor.setMethodInterceptor(adaptAdvice((Advice)interceptorOrAdvisor));returndefaultAdvisor;}这里保留了对上一章配置方式的兼容:
- 配置
PointcutAdvisor:按切点选择方法; - 配置普通
Advice:包装成DefaultAdvisor,对所有方法执行增强。
七、动态代理根据切点决定是否增强
最后回到JdkDynamicAopProxy.invoke()。原来写死的"doAction"被替换成了shouldApply(method):
@OverridepublicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{MethodInterceptorinterceptor=getMethodInterceptor();if(interceptor!=null&&shouldApply(method)){Class<?>targetClass=this.target==null?null:this.target.getClass();MethodInvocationinvocation=newReflectiveMethodInvocation(proxy,this.target,method,args,targetClass);returninterceptor.invoke(invocation);}// 没有命中切点的方法仍然正常调用真实对象。returninvokeTarget(method,args);}privatebooleanshouldApply(Methodmethod){if(this.advisor==null){returnfalse;}if(!(this.advisorinstanceofPointcutAdvisor)){// 不带切点的普通 Advisor 默认匹配所有方法。returntrue;}Class<?>targetClass=this.target==null?null:this.target.getClass();Pointcutpointcut=((PointcutAdvisor)this.advisor).getPointcut();returnpointcut!=null&&pointcut.getMethodMatcher()!=null&&pointcut.getMethodMatcher().matches(method,targetClass);}判断逻辑分为三种情况:
- 没有
Advisor,不执行增强; - 是普通
Advisor,默认对所有方法执行增强; - 是
PointcutAdvisor,只有MethodMatcher匹配成功才执行增强。
无论有没有命中切点,真实方法都要执行。切点只控制增强是否介入,不能让未命中的方法直接返回null。
八、验证匹配结果
项目中的测试直接验证了do*的匹配结果:
NameMatchMethodPointcutpointcut=newNameMatchMethodPointcut();pointcut.setMappedName("do*");assertTrue(pointcut.matches(IAction.class.getMethod("doAction"),Action1.class));assertTrue(pointcut.matches(IAction.class.getMethod("doSomething"),Action1.class));assertFalse(pointcut.matches(Object.class.getMethod("toString"),Action1.class));它还会通过容器取得代理对象,验证两个do方法都先执行前置通知,再执行真实方法。这样既检查了匹配器,也检查了 XML、Advisor和动态代理组成的完整调用链。
当前版本仍然是一个教学实现:
- 一个
NameMatchMethodPointcut只保存一个mappedName; - 只按方法名匹配,还不支持类、注解和参数类型等条件;
- 一个代理仍然只配置一个
Advisor,还没有多拦截器链。
小结
这一节把硬编码的方法名判断抽成了独立切点:
Advice:做什么增强 Pointcut:在哪里增强 Advisor:把二者组合起来 JdkDynamicAopProxy:匹配成功后执行增强以后再增加匹配规则时,只需要实现新的Pointcut或MethodMatcher,不必继续修改具体的动态代理逻辑。
AOP的核心要点都围绕this.singletonInstance = getProxy(createAopProxy())这行代码展开,最终就是为了让框架自动生成代理对象。然后注意哪个类需要被代理,具体怎么代理增强(用哪个代理增强类)都写在配置文件里,通过IOC容器自动set注入依赖关系。先拿到需要被代理的对象,target,interceptorName–》com.chenhai.aop.NameMatchMethodPointcutAdvisor,然后通过set注入Advice(具体增强类)、mappedName(匹配规则)