MiniSpring框架学习笔记-Pointcut:如何批量匹配代理方法? MiniSpring框架学习笔记-Pointcut如何批量匹配代理方法19. Pointcut如何批量匹配代理方法一、先看最终效果二、先分清 Advice、Pointcut 和 Advisor三、定义 Pointcut 和 MethodMatcher四、按方法名实现切点五、用 PointcutAdvisor 组合切点和通知六、ProxyFactoryBean 要能识别 Advisor七、动态代理根据切点决定是否增强八、验证匹配结果小结教程https://github.com/YaleGuo/minis极客时间手把手带你写一个 MiniSpring19. Pointcut如何批量匹配代理方法上一节已经把增强逻辑从动态代理中抽成了MethodInterceptor不过哪些方法需要增强仍然写死在代理类里if(doAction.equals(method.getName())interceptor!null){Class?targetClassthis.targetnull?null:this.target.getClass();MethodInvocationinvocationnewReflectiveMethodInvocation(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使用mappedNamedo*批量匹配所有以do开头的方法beanidrealactionclasscom.chenhai.test.Action1/beanidbeforeAdviceclasscom.chenhai.test.MyBeforeAdvice/beanidadvisorclasscom.chenhai.aop.NameMatchMethodPointcutAdvisorpropertytypecom.chenhai.aop.AdvicenameadvicerefbeforeAdvice/propertytypeStringnamemappedNamevaluedo*//beanbeanidactionclasscom.chenhai.aop.ProxyFactoryBeanpropertytypejava.lang.Objectnametargetrefrealaction/propertytypeStringnameinterceptorNamevalueadvisor//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.singletonInstancegetProxy(createAopProxy());createAopProxy()先根据配置组装目标对象和Advisor得到具体的JdkDynamicAopProxygetProxy()再调用 JDK 动态代理 API生成业务代码最终拿到的代理对象。哪个对象需要被代理、使用哪段增强逻辑、匹配哪些方法都写在 XML 中再由 IoC 容器通过 setter 自动注入。以上面的配置为例actionProxyFactoryBean ├─ target → realactionAction1被代理的真实对象 └─ interceptorName → advisor ↓ NameMatchMethodPointcutAdvisor ├─ advice → beforeAdviceMyBeforeAdvice具体增强逻辑 └─ mappedName → do*方法匹配规则所以ProxyFactoryBean先拿到targetrealaction和interceptorNameadvisor再从容器中取得NameMatchMethodPointcutAdvisor。这个Advisor已经由容器注入了beforeAdvice和do*前者决定“怎么增强”后者决定“哪些方法需要增强”。框架最后根据这些对象自动生成代理业务代码不需要手动创建或组装代理对象。二、先分清 Advice、Pointcut 和 Advisor切点可以理解成一个筛选条件有点像 SQL 中的where它只负责找出需要增强的方法不负责执行增强。本章几个核心对象的职责如下对象负责什么Advice/MethodInterceptor做什么增强Pointcut在哪里增强MethodMatcher判断当前方法是否命中PointcutAdvisor把增强逻辑和切点组合起来它们之间的关系是PointcutAdvisor ├─ MethodInterceptor怎么增强 └─ Pointcut └─ MethodMatcher当前方法是否匹配这样一来代理只负责组织调用不再关心具体匹配规则是方法名、注解还是其他条件。三、定义 Pointcut 和 MethodMatcher先定义切点接口。当前教学版只匹配方法所以Pointcut只需要提供MethodMatcherpackagecom.chenhai.aop;/** * 切点表示“在哪里增强”。 */publicinterfacePointcut{MethodMatchergetMethodMatcher();}MethodMatcher专门回答“这个方法要不要增强”packagecom.chenhai.aop;importjava.lang.reflect.Method;/** * 判断某个方法是否需要被 AOP 增强。 */publicinterfaceMethodMatcher{booleanmatches(Methodmethod,Class?targetClass);}这里同时传入了method和targetClass是为了让匹配器既能检查方法也能检查真实目标类。当前按名称匹配的实现只使用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.mappedNamemappedNamenull?:mappedName;}publicStringgetMappedName(){returnthis.mappedName;}Overridepublicbooleanmatches(Methodmethod,Class?targetClass){if(methodnull){returnfalse;}StringmethodNamemethod.getName();returnthis.mappedName.equals(methodName)||isMatch(methodName,this.mappedName);}protectedbooleanisMatch(StringmethodName,StringmappedName){returnPatternMatchUtils.simpleMatch(mappedName,methodName);}OverridepublicMethodMatchergetMethodMatcher(){// 当前对象自己就能匹配方法所以直接返回 this。returnthis;}}支持的匹配方式包括mappedName可以匹配doAction精确匹配doActiondo*doAction、doSomething*Action所有以Action结尾的方法*Action*所有名称中包含Action的方法do*Action以do开头、以Action结尾的方法这里的*是简单通配符不是正则表达式也不支持?等其他通配符。真正的通配符算法放在工具类中切点只保留 AOP 相关职责publicstaticbooleansimpleMatch(Stringpattern,Stringstr){if(patternnull||strnull){returnfalse;}intfirstIndexpattern.indexOf(*);if(firstIndex-1){returnpattern.equals(str);}if(firstIndex0){if(pattern.length()1){returntrue;}intnextIndexpattern.indexOf(*,1);if(nextIndex-1){returnstr.endsWith(pattern.substring(1));}Stringpartpattern.substring(1,nextIndex);if(part.isEmpty()){returnsimpleMatch(pattern.substring(nextIndex),str);}intpartIndexstr.indexOf(part);while(partIndex!-1){StringremainingPatternpattern.substring(nextIndex);StringremainingStringstr.substring(partIndexpart.length());if(simpleMatch(remainingPattern,remainingString)){returntrue;}partIndexstr.indexOf(part,partIndex1);}returnfalse;}returnstr.length()firstIndexpattern.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;privatefinalNameMatchMethodPointcutpointcutnewNameMatchMethodPointcut();publicNameMatchMethodPointcutAdvisor(){}publicNameMatchMethodPointcutAdvisor(Adviceadvice){setAdvice(advice);}publicvoidsetAdvice(Adviceadvice){this.adviceadvice;// 统一适配成代理能够执行的 MethodInterceptor。setMethodInterceptor(adaptAdvice(advice));}publicAdvicegetAdvice(){returnthis.advice;}OverridepublicMethodInterceptorgetMethodInterceptor(){returnthis.methodInterceptor;}OverridepublicvoidsetMethodInterceptor(MethodInterceptormethodInterceptor){this.methodInterceptormethodInterceptor;}OverridepublicPointcutgetPointcut(){returnthis.pointcut;}publicvoidsetMappedName(StringmappedName){this.mappedNamemappedName;// 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 注入会触发两个关键 settersetAdvice()把前置、后置通知适配成统一的MethodInterceptorsetMappedName()把do*交给内部的NameMatchMethodPointcut。最终advisor同时持有了“怎么增强”和“增强哪些方法”。六、ProxyFactoryBean 要能识别 Advisor上一节的interceptorName只能指向一个普通Advice。现在 XML 中配置的是propertytypeStringnameinterceptorNamevalueadvisor/虽然属性名还叫interceptorName但它现在既可以指向Advice也可以指向完整的Advisor。ProxyFactoryBean需要先识别取出来的对象privatesynchronizedvoidinitializeAdvisor(){if(this.advisor!null||this.interceptorNamenull||this.interceptorName.trim().isEmpty()){return;}if(this.beanFactorynull){thrownewIllegalStateException(BeanFactory is required to resolve interceptorName);}ObjectinterceptorOrAdvisor;try{interceptorOrAdvisorthis.beanFactory.getBean(this.interceptorName);}catch(BeansExceptione){thrownewIllegalStateException(Resolve interceptor failed: this.interceptorName,e);}this.advisoradaptAdvisor(interceptorOrAdvisor);}privateAdvisoradaptAdvisor(ObjectinterceptorOrAdvisor){if(interceptorOrAdvisorinstanceofAdvisor){// Advisor 已经包含通知PointcutAdvisor 还会包含切点。return(Advisor)interceptorOrAdvisor;}if(!(interceptorOrAdvisorinstanceofAdvice)){thrownewIllegalArgumentException(Unsupported interceptor/advisor type: interceptorOrAdvisor);}// 兼容上一章单独的 Advice 包成不带切点的 DefaultAdvisor。DefaultAdvisordefaultAdvisornewDefaultAdvisor();defaultAdvisor.setMethodInterceptor(adaptAdvice((Advice)interceptorOrAdvisor));returndefaultAdvisor;}这里保留了对上一章配置方式的兼容配置PointcutAdvisor按切点选择方法配置普通Advice包装成DefaultAdvisor对所有方法执行增强。七、动态代理根据切点决定是否增强最后回到JdkDynamicAopProxy.invoke()。原来写死的doAction被替换成了shouldApply(method)OverridepublicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{MethodInterceptorinterceptorgetMethodInterceptor();if(interceptor!nullshouldApply(method)){Class?targetClassthis.targetnull?null:this.target.getClass();MethodInvocationinvocationnewReflectiveMethodInvocation(proxy,this.target,method,args,targetClass);returninterceptor.invoke(invocation);}// 没有命中切点的方法仍然正常调用真实对象。returninvokeTarget(method,args);}privatebooleanshouldApply(Methodmethod){if(this.advisornull){returnfalse;}if(!(this.advisorinstanceofPointcutAdvisor)){// 不带切点的普通 Advisor 默认匹配所有方法。returntrue;}Class?targetClassthis.targetnull?null:this.target.getClass();Pointcutpointcut((PointcutAdvisor)this.advisor).getPointcut();returnpointcut!nullpointcut.getMethodMatcher()!nullpointcut.getMethodMatcher().matches(method,targetClass);}判断逻辑分为三种情况没有Advisor不执行增强是普通Advisor默认对所有方法执行增强是PointcutAdvisor只有MethodMatcher匹配成功才执行增强。无论有没有命中切点真实方法都要执行。切点只控制增强是否介入不能让未命中的方法直接返回null。八、验证匹配结果项目中的测试直接验证了do*的匹配结果NameMatchMethodPointcutpointcutnewNameMatchMethodPointcut();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注入依赖关系。先拿到需要被代理的对象targetinterceptorName–》com.chenhai.aop.NameMatchMethodPointcutAdvisor然后通过set注入Advice具体增强类、mappedName匹配规则