MiniSpring框架学习笔记-Pointcut:如何批量匹配代理方法?

📅 2026/7/29 2:03:52 👁️ 阅读次数 📝 编程学习
MiniSpring框架学习笔记-Pointcut:如何批量匹配代理方法?

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,得到具体的JdkDynamicAopProxygetProxy()再调用 JDK 动态代理 API,生成业务代码最终拿到的代理对象。

哪个对象需要被代理、使用哪段增强逻辑、匹配哪些方法,都写在 XML 中,再由 IoC 容器通过 setter 自动注入。以上面的配置为例:

action(ProxyFactoryBean) ├─ target → realaction(Action1,被代理的真实对象) └─ interceptorName → advisor ↓ NameMatchMethodPointcutAdvisor ├─ advice → beforeAdvice(MyBeforeAdvice,具体增强逻辑) └─ mappedName → do*(方法匹配规则)

所以,ProxyFactoryBean先拿到target=realactioninterceptorName=advisor,再从容器中取得NameMatchMethodPointcutAdvisor。这个Advisor已经由容器注入了beforeAdvicedo*:前者决定“怎么增强”,后者决定“哪些方法需要增强”。框架最后根据这些对象自动生成代理,业务代码不需要手动创建或组装代理对象。

二、先分清 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);}

这里同时传入了methodtargetClass,是为了让匹配器既能检查方法,也能检查真实目标类。当前按名称匹配的实现只使用methodtargetClass暂时没有参与判断。

完整的 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*doActiondoSomething
*Action所有以Action结尾的方法
*Action*所有名称中包含Action的方法
do*Actiondo开头、以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 组合切点和通知

现在还差一个对象,把AdviceNameMatchMethodPointcut放在一起:

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:

  1. setAdvice()把前置、后置通知适配成统一的MethodInterceptor
  2. setMappedName()do*交给内部的NameMatchMethodPointcut

最终,advisor同时持有了“怎么增强”和“增强哪些方法”。

六、ProxyFactoryBean 要能识别 Advisor

上一节的interceptorName只能指向一个普通Advice。现在 XML 中配置的是:

<propertytype="String"name="interceptorName"value="advisor"/>

虽然属性名还叫interceptorName,但它现在既可以指向Advice,也可以指向完整的AdvisorProxyFactoryBean需要先识别取出来的对象:

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);}

判断逻辑分为三种情况:

  1. 没有Advisor,不执行增强;
  2. 是普通Advisor,默认对所有方法执行增强;
  3. 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:匹配成功后执行增强

以后再增加匹配规则时,只需要实现新的PointcutMethodMatcher,不必继续修改具体的动态代理逻辑。

AOP的核心要点都围绕this.singletonInstance = getProxy(createAopProxy())这行代码展开,最终就是为了让框架自动生成代理对象。然后注意哪个类需要被代理,具体怎么代理增强(用哪个代理增强类)都写在配置文件里,通过IOC容器自动set注入依赖关系。先拿到需要被代理的对象,target,interceptorName–》com.chenhai.aop.NameMatchMethodPointcutAdvisor,然后通过set注入Advice(具体增强类)、mappedName(匹配规则)