Spring Aop原理之切点表达式解析
在前面的文章(Spring AOP切点表达式详解)中,我们总结了Spring Aop切点表达式的用法,而在上文(Spring Aop原理之Advisor过滤)中我们讲到,切点表达式的解析主要是在PatternParser.parsePointcut()方法中进行的。本文的主要目的是讲解Spring Aop是如何递归的对切点表达式进行解析,并且最终封装为一个Pointcut对象的。
这里我们首先举一个典型的切点表达式的例子:
@Pointcut(value = "!execution(public protected void com.business.Dog.*(..)) && args(name)", argNames = "name")
该切点将会匹配如下条件的反面的的所有方法:使用public修饰,返回值为void类型,类为com.business.Dog的方法,并且这些方法必须满足有一个名称为name的参数。
1. 解析入口
如下是PatternParser.parsePointcut()方法的实现:
public Pointcut parsePointcut() { // 转换一个Pointcut单元,比如上述的execution和args都属于一个切点单元 Pointcut p = parseAtomicPointcut(); // 判断当前切点单元之后是否是一对&&符号,如果是,则递归的将&&后面的切点表达式解析为一个Pointcut // 对象,然后将两个Pointcut对象使用AndPointcut连接起来 if (maybeEat("&&")) { p = new AndPointcut(p, parseNotOrPointcut()); } // 判断当前切点单元之后是否是以对||符号,如果是,则递归的将||后面的切点表达式解析为一个Pointcut // 对象,然后将两个Pointcut对象使用OrPointcut连接起来 if (maybeEat("||")) { p = new OrPointcut(p, parsePointcut()); } return p; }
在parsePointcut()方法中,其对切点表达式的解析是一个一个进行的,解析完成一个之后就判断其后是&&还是||,然后使用AndPointcut或者OrPointcut将操作符两边的结果进行组装。这里需要说明的是,AndPointcut和OrPointcut内部实际上也只是保存了两个Pointcut对象,在后面进行切点匹配时,其实际上还是将匹配过程委托给这两个Pointcut对象进行,最后将两个Pointcut匹配的结果进行取交或者取并。这里我们继续阅读parseAtomicPointcut()的实现代码:
private Pointcut parseAtomicPointcut() { // 判断当前表达式单元是否以!开头,如果是,则使用一个NotPointcut对!后面的匹配结果进行封装 if (maybeEat("!")) { int startPos = tokenSource.peek(-1).getStart(); Pointcut p = new NotPointcut(parseAtomicPointcut(), startPos); return p; } // 如果表达式是以括号开头,则直接对括号后面的表达式进行解析,因为在进行顺序解析时, // 括号的作用与解析的顺序是一致的 if (maybeEat("(")) { Pointcut p = parsePointcut(); eat(")"); return p; } // 判断表达式是否是以@符号开头,如果是,说明表达式是修饰注解类型的表达式单元,因而使用 // parseAnnotationPointcut()方法对注解类型的表达式进行解析 if (maybeEat("@")) { int startPos = tokenSource.peek().getStart(); Pointcut p = parseAnnotationPointcut(); int endPos = tokenSource.peek(-1).getEnd(); p.setLocation(sourceContext, startPos, endPos); return p; } // 当上述条件都不满足时,说明当前表达式是一个简单类型的表达式,如前面示例中的execution表达式, // 此时使用parseSinglePointcut()方法对该类型表达式进行解析 int startPos = tokenSource.peek().getStart(); Pointcut p = parseSinglePointcut(); int endPos = tokenSource.peek(-1).getEnd(); p.setLocation(sourceContext, startPos, endPos); return p; }
从上面的代码中可以看出,Spring在进行切点单元的解析的时候主要分为两种情况进行解析:
解析修饰注解类型的切点表达式,如@annotation,使用parseAnnotationPointcut()方法进行解析;
解析一般的切点表达式,如execution等,使用parseSinglePointcut()方法进行解析。
2. 注解表达式解析
对于注解类型的表达式的解析,使用的是parseAnnotationPointcut()方法进行,我们首先举例说明注解类型的表达式的用法:
@Around("@annotation(com.business.annotation.FruitAspect)")
这里@Around将会环绕使用FruitAspect注解标注的方法。下面是parseAnnotationPointcut()的源码:
public Pointcut parseAnnotationPointcut() { int start = tokenSource.getIndex(); IToken t = tokenSource.peek(); // 获取当前修饰注解类型的表达式的标识符,如annotation,args,within等, // 上述示例中获取到的就是annotation String kind = parseIdentifier(); IToken possibleTypeVariableToken = tokenSource.peek(); // 这里是做的校验,在标识符之后不能是使用<>声明的注解类型列表,如果是, // 获取的typeVariables就不为空,下面if判断中就会抛出异常 String[] typeVariables = maybeParseSimpleTypeVariableList(); if (typeVariables != null) { String message = "("; assertNoTypeVariables(typeVariables, message, possibleTypeVariableToken); } // 充值当前要解析的表达式所在位置的索引 tokenSource.setIndex(start); if (kind.equals("annotation")) { // 解析使用@annotation修饰的注解类型,只要目标方法上使用其后声明的注解就会被环绕 return parseAtAnnotationPointcut(); } else if (kind.equals("args")) { // 解析使用@args修饰的注解类型,其后可以使用多个注解参数类型,表示如果目标方法的参数 // 使用其中任意一个注解参数类型进行标注,该方法就会被环绕 return parseArgsAnnotationPointcut(); } else if (kind.equals("this") || kind.equals("target")) { // this和target其后都是指定一个类或接口类型,分别表示匹配代理对象为指定类型和 // 匹配目标对象为指定类型 return parseThisOrTargetAnnotationPointcut(); } else if (kind.equals("within")) { // 解析@within修饰的类型,其后接一个注解类型,表示目标类只要使用该注解进行标注,那么 // 其所有方法将会被环绕 return parseWithinAnnotationPointcut(); } else if (kind.equals("withincode")) { // 解析@withincode修饰的类型,其后接一个注解类型,表示目标方法只要使用该注解进行标注就会被环绕 return parseWithinCodeAnnotationPointcut(); } throw new ParserException("pointcut name", t); }
可以看到,对于注解类型的参数解析都在parseAnnotationPointcut()方法进行声明了,并且使用多个if语句进行了分发。我们这里以@annotation修饰的注解类型进行讲解,其余的几个修饰的类型与其解析方式非常类似,读者可自行阅读。如下是parseAtAnnotationPointcut()的源码:
private Pointcut parseAtAnnotationPointcut() { // 将annotation进行转换,并将解析的index置于其后 parseIdentifier(); // 判断annotation后是否为左括号 eat("("); // 如果左括号之后是右括号,而没有具体的注解类型,则抛出异常 if (maybeEat(")")) { throw new ParserException("@AnnotationName or parameter", tokenSource.peek()); } // 解析具体的注解或变量类型 ExactAnnotationTypePattern type = parseAnnotationNameOrVarTypePattern(); // 解析完注解类型之后其后应该是一个右括号,对其进行解析,并且将解析的index置于其后 eat(")"); // 将解析的结果封装为一个AnnotationPointcut return new AnnotationPointcut(type); }
这里parseAtAnnotationPointcut()主要是解析了annotation之后的左括号,注解类型和右括号,并将解析结果封装到了AnnotationPointcut对象中。对于注解类型的具体解析过程在parseAnnotationNameOrVarTypePattern()方法中,如下是该方法的源码:
protected ExactAnnotationTypePattern parseAnnotationNameOrVarTypePattern() { ExactAnnotationTypePattern p = null; int startPos = tokenSource.peek().getStart(); // 如果注解类型前使用了@符号,则抛出异常 if (maybeEat("@")) { throw new ParserException("@Foo form was deprecated in AspectJ 5 M2: " + "annotation name or var ", tokenSource.peek(-1)); } // 转换简单的注解类型名称,也即上述的com.business.annotation.FruitAspect p = parseSimpleAnnotationName(); int endPos = tokenSource.peek(-1).getEnd(); // 设置该注解类型的基本属性 p.setLocation(sourceContext, startPos, endPos); // 如果注解类型之后是一个左括号,则对括号中的注解属性类型进行解析,并且将其和前面解析到的 // 注解类型封装到ExactAnnotationFieldTypePattern中 if (maybeEat("(")) { String formalName = parseIdentifier(); p = new ExactAnnotationFieldTypePattern(p, formalName); eat(")"); } return p; }
这里parseAnnotationNameOrVarTypePattern()方法主要解析了两部分数据:①注解类型的全路径名;②注解属性类型,如此一个注解类型才真正解析完成。
3. 一般表达式类型解析
对于一般表达式类型的解析,主要是通过前面讲解的parseSinglePointcut()方法进行的,如下是该方法的实现源码:
public Pointcut parseSinglePointcut() { int start = tokenSource.getIndex(); IToken t = tokenSource.peek(); Pointcut p = t.maybeGetParsedPointcut(); if (p != null) { tokenSource.next(); return p; } // 获取当前表达式的类型 String kind = parseIdentifier(); if (kind.equals("execution") || kind.equals("call") || kind.equals("get") || kind.equals("set")) { // 对execution,call,get或者set类型的表达式进行解析。这里execution主要修饰的是 // 一整个方法,包括返回值和异常类型;call修饰的也是一个方法,只不过其目标是调用当前 // 方法的对象;get和set都是修饰的属性设值的方法。 p = parseKindedPointcut(kind); } else if (kind.equals("args")) { // 解析args类型的表达式,其后可以带多个全路径参数类型,目标方法如果参数类型与其匹配将会被环绕 p = parseArgsPointcut(); } else if (kind.equals("this")) { // 解析this类型的表达式,其后接一个类型,表示生成的代理对象必须是其后指定的类型 p = parseThisOrTargetPointcut(kind); } else if (kind.equals("target")) { // 解析target类型的表达式,其后接一个类型,表示被代理的目标对象必须是其后指定的类型 p = parseThisOrTargetPointcut(kind); } else if (kind.equals("within")) { // 解析within类型的表达式,其后也是接一个类型,表示目标类型必须是其后表达式的类型 p = parseWithinPointcut(); } else if (kind.equals("withincode")) { // 解析withincode类型的表达式,其后接一个方法,表示目标方法必须与其后指定的方法匹配 p = parseWithinCodePointcut(); } else if (kind.equals("cflow")) { p = parseCflowPointcut(false); } else if (kind.equals("cflowbelow")) { p = parseCflowPointcut(true); } else if (kind.equals("adviceexecution")) { eat("("); eat(")"); p = new KindedPointcut(Shadow.AdviceExecution, new SignaturePattern(Member.ADVICE, ModifiersPattern.ANY, TypePattern.ANY, TypePattern.ANY, NamePattern.ANY, TypePatternList.ANY, ThrowsPattern.ANY, AnnotationTypePattern.ANY)); } else if (kind.equals("handler")) { eat("("); TypePattern typePat = parseTypePattern(false, false); eat(")"); p = new HandlerPointcut(typePat); } else if (kind.equals("lock") || kind.equals("unlock")) { p = parseMonitorPointcut(kind); } else if (kind.equals("initialization")) { eat("("); SignaturePattern sig = parseConstructorSignaturePattern(); eat(")"); p = new KindedPointcut(Shadow.Initialization, sig); } else if (kind.equals("staticinitialization")) { eat("("); TypePattern typePat = parseTypePattern(false, false); eat(")"); p = new KindedPointcut(Shadow.StaticInitialization, new SignaturePattern(Member.STATIC_INITIALIZATION, ModifiersPattern.ANY, TypePattern.ANY, typePat, NamePattern.ANY, TypePatternList.EMPTY, ThrowsPattern.ANY, AnnotationTypePattern.ANY)); } else if (kind.equals("preinitialization")) { eat("("); SignaturePattern sig = parseConstructorSignaturePattern(); eat(")"); p = new KindedPointcut(Shadow.PreInitialization, sig); } else if (kind.equals("if")) { eat("("); if (maybeEatIdentifier("true")) { eat(")"); p = new IfPointcut.IfTruePointcut(); } else if (maybeEatIdentifier("false")) { eat(")"); p = new IfPointcut.IfFalsePointcut(); } else { if (!maybeEat(")")) { throw new ParserException( "in annotation style, if(...) pointcuts cannot contain code. " + "Use if() and put the code in the annotated method", t); } p = new IfPointcut(""); } } else { // 如果表达式的类型与上述所有类型都不符,那么可能当前类型是用户自定义的类型。对于用户 // 自定义类型,其只需要实现PointcutDesignatorHandler接口,其getDesignatorName()用于 // 返回当前表达式类型名,其parse()方法则用于将自定义的表达式转换为一个Pointcut对象 boolean matchedByExtensionDesignator = false; for (PointcutDesignatorHandler pcd : pointcutDesignatorHandlers) { if (pcd.getDesignatorName().equals(kind)) { p = parseDesignatorPointcut(pcd); matchedByExtensionDesignator = true; } } if (!matchedByExtensionDesignator) { tokenSource.setIndex(start); p = parseReferencePointcut(); } } return p; }
这里只列出了常用的几种表达式类型,并且对其含义进行了讲解,详细的用法请查看本人前面写的文章。这里parseSinglePointcut()方法与parseAnnotationPointcut()的结构非常类似,即这里只是起到一个分发的作用,具体的实现在具体的方法中进行。另外这里parseSinglePointcut()方法也提供了一个实现自定义的切点表达式的方法,具体的使用读者可以阅读PointcutDesignatorHandler接口的声明,对于Pointcut理解比较透彻的话实现自定义切点表达式还是比较简单的。对于切点表达式的解析,我们这里还是讲解最常用的一种,即execution类型表达式的解析,如下是parseKindedPointcut()的源码:
private KindedPointcut parseKindedPointcut(String kind) { // 解析的表达式必须是使用括号包围的 eat("("); SignaturePattern sig; Shadow.Kind shadowKind = null; if (kind.equals("execution")) { // 对execution类型的表达式进行解析,解析时分为Method和Contrustor类型分别处理 sig = parseMethodOrConstructorSignaturePattern(); if (sig.getKind() == Member.METHOD) { shadowKind = Shadow.MethodExecution; } else if (sig.getKind() == Member.CONSTRUCTOR) { shadowKind = Shadow.ConstructorExecution; } } else if (kind.equals("call")) { // 对call类型的表达式进行解析,分为Method和Contrustor分别进行处理 sig = parseMethodOrConstructorSignaturePattern(); if (sig.getKind() == Member.METHOD) { shadowKind = Shadow.MethodCall; } else if (sig.getKind() == Member.CONSTRUCTOR) { shadowKind = Shadow.ConstructorCall; } } else if (kind.equals("get")) { // 对get表达式进行解析 sig = parseFieldSignaturePattern(); shadowKind = Shadow.FieldGet; } else if (kind.equals("set")) { // 对set表达式进行解析 sig = parseFieldSignaturePattern(); shadowKind = Shadow.FieldSet; } else { throw new ParserException("bad kind: " + kind, tokenSource.peek()); } eat(")"); // 将解析后的结果封装为一个KindedPointcut return new KindedPointcut(shadowKind, sig); }
这里parseKindedPointcut()将Kinded类型的表达式在这里分为execution,call,get和set分别进行解析,只不过最后都是使用KindedPointcut进行封装解析结果。我们这里还是继续阅读execution类型的解析代码:
public SignaturePattern parseMethodOrConstructorSignaturePattern() { int startPos = tokenSource.peek().getStart(); // 判断当前方法是否使用指定注解进行了修饰,这里解析的方式与之前的@annotation不一样, // 这里是直接使用形如@chapter7.eg6.FruitAspect进行修饰即可 AnnotationTypePattern annotationPattern = maybeParseAnnotationPattern(); // 对访问权限修饰符进行解析,也即public,protected等,也可以使用多个进行组合, // 多个进行组合时只需要顺序列出即可;如果要过滤掉某些修饰符,比如过滤掉public修饰的 // 方法,则在前面加一个!即可,如:!public ModifiersPattern modifiers = parseModifiersPattern(); // 对方法返回值进行解析,这里也可以解析基本数据类型,如void,int等 TypePattern returnType = parseTypePattern(false, false); TypePattern declaringType; NamePattern name = null; MemberKind kind; // 对返回值类型进行判断,如果返回值类型用点“.”分隔的最后一部分是new关键字,那么就会认为其 // 是进行构造方法增强的切点表达式 if (maybeEatNew(returnType)) { kind = Member.CONSTRUCTOR; // 对于构造方法,这里returnType始终是TypePattern.ANY,而构造的对象类型是 // 通过declaringType来保存的 if (returnType.toString().length() == 0) { declaringType = TypePattern.ANY; } else { declaringType = returnType; } returnType = TypePattern.ANY; name = NamePattern.ANY; } else { // 如果解析的不是构造方法,则按照一般的方法解析方式进行解析 kind = Member.METHOD; IToken nameToken = tokenSource.peek(); // 首先解析方法所在的class类型 declaringType = parseTypePattern(false, false); if (maybeEat(".")) { // 如果类后面是一个“.”,则将其后的名称作为方法名进行解析 nameToken = tokenSource.peek(); name = parseNamePattern(); } else { // 如果类后面的名称不是“.”,则将类按照“.”分隔,并将最后一个“.”之后的部分当做方法名, // 之前的部分则作为类名;如果类中没有“.”,则将整个类名都作为方法名进行解析,并且 // 类名使用TypePattern.ANY表示,也即任意的类型名都行 name = tryToExtractName(declaringType); if (declaringType.toString().equals("")) { declaringType = TypePattern.ANY; } } // 如果通过上面的解析后得到的方法名还是null,则抛出异常 if (name == null) { throw new ParserException("name pattern", tokenSource.peek()); } // 获取方法名,判断方法名是否为new关键字,如果是,则抛出异常 String simpleName = name.maybeGetSimpleName(); if (simpleName != null && simpleName.equals("new")) { throw new ParserException("method name (not constructor)", nameToken); } } // 对方法后面括号中声明的参数进行解析 TypePatternList parameterTypes = parseArgumentsPattern(true); // 对参数后面括号外可能存在的异常抛出列表进行解析 ThrowsPattern throwsPattern = parseOptionalThrowsPattern(); // 将上述解析的结果使用SignaturePattern进行封装,并将其返回 SignaturePattern ret = new SignaturePattern(kind, modifiers, returnType, declaringType, name, parameterTypes, throwsPattern, annotationPattern); int endPos = tokenSource.peek(-1).getEnd(); ret.setLocation(sourceContext, startPos, endPos); return ret; }
这里parseMethodOrConstructorSignaturePattern()方法就是对基本切点表达式单元进行解析的整体流程,其主要分为如下几个步骤:
首先会解析当前方法是否需要使用某个注解进行修饰,比如如下的示例就要求目标方法必须使用FruitAspect注解进行修饰:
@Around("execution(@chapter7.eg6.FruitAspect public void chapter7.eg9.Dog.*(..))")
然后解析当前方法所使用的修饰符,即public,protected等,可以取非,需要注意的是,肯定的不能同时有多个,比如不能同时存在public protected,此时虽然表达式不会报错,但是基本上匹配不到任何方法,因为没有方法同时是public又是protected的。如下就是要求目标方法必须是public的,并且不能是protected修饰的:
@Around("execution(public !protected void chapter7.eg9.Dog.*(..))")
接着就是对返回值类型的解析,对返回值类型的解析又分为两种情况,一种是目标方法是构造方法,另一种则是目标方法是一般的方法。对于构造方法,只需要返回值的最后一部分是new即可,然后其后紧接着参数列表;而对于一般的方法,返回值之后则是当前方法所在的类,以及修饰的目标方法。如下是修饰构造方法的一个示例:
@Around("execution(public chapter7.eg9.Dog.new(..))")
如果需要修饰的方法不是构造方法,那么其修饰的就是一般方法,对于一般方法,返回值之后就是要修饰的方法所在的类和要修饰的方法,这里的类必须包含全路径名。比如如下修饰的一般方法:
@Around("execution(public void chapter7.eg9.Dog.*(..))")
在类和方法都解析完成之后,就是解析参数列表了,对于参数列表的解析,其会使用“,”对列表进行分割,并且会判断参数列表中是否包含有“..”和"*"之类的通配符;
最后就是解析表达式中是否包含可选的异常抛出表达式,异常抛出表达式都是使用throws进行修饰的,而throws之后则是接的一系列的类型列表,这里很明显也是可以使用递归进行类型列表的解析的。如下使用throws表达式的一个示例:
@Around("execution(public void chapter7.eg9.Dog.*(..) throws java.lang.Exception)")
4. 小结
本文主要讲解了Spring Aop是如何使用递归对切点表达式进行解析的,在解析过程中Spring Aop将其分为了两种过程,一种是对注解类型的切点表达式的解析,一种是对一般类型的切点表达式的解析。而在解析过程中也支持逻辑表达式&&,||和!的使用,对于这些表达式,则是通过AndPointcut,OrPointcut和NotPointcut等进行封装的。解析的最终结果就是整个切点表达式都会封装到一个Pointcut对象中,而标签中的每一部分都是该Pointcut对象的一个子节点,整体类似于一种树状结构。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Spring Cloud中Hystrix仪表盘与Turbine集群监控
Hystrix仪表盘,就像汽车的仪表盘实时显示汽车的各项数据一样,Hystrix仪表盘主要用来监控Hystrix的实时运行状态,通过它我们可以看到Hystrix的各项指标信息,从而快速发现系统中存在的问题进而解决它,接下来就让我们就来看看Hystrix仪表盘要怎么使用。 本文将从两个方面来看Hystrix仪表盘的使用,一方面是监控单体应用,另一方面则整合Turbine,对集群进行监控。 监控单体应用 监控环境搭建 不管是监控单体应用还是Turbine集群监控,我们都需要一个Hystrix Dashboard,当然我们可以在要监控的单体应用上继续添加功能,让它也具备仪表盘的功能,但是这样并不符合我们微服务的思想,所以,Hystrix仪表盘我还是单独创建一个新的工程专门用来做Hystrix Dashboard。OK,在Spring Cloud中创建一个Hystrix Dashboard非常简单,如下: 第一步:创建一个普通的Spring Boot工程 创建一个Spring Boot工程这个比较简单,直接创建一个名为hystrix-dashboard的Spring Boot工程。 第二步:添...
- 下一篇
Spring Boot基础教程3-配置文件详解:Properties和YAML
Spring Boot基础教程1-Spring Tool Suite工具的安装 Spring Boot基础教程2-RESTful API简单项目的快速搭建 Spring Boot基础教程3-配置文件详解:Properties和YAML Spring Boot基础教程4-配置文件-多环境配置 Spring Boot基础教程5-日志配置-logback和log4j2 视频教程:http://www.roncoo.com/course/view/c99516ea604d4053908c1768d6deee3d 源码地址:https://github.com/roncoo/spring-boot-demo 一.配置文件的生效顺序,会对值进行覆盖: 1. @TestPropertySource 注解 2. 命令行参数 3. Java系统属性(System.getProperties()) 4. 操作系统环境变量 5. 只有在random.*里包含的属性会产生一个RandomValuePropertySource 6. 在打包的jar外的应用程序配置文件(application.propertie...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
-
Docker使用Oracle官方镜像安装(12C,18C,19C)
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS8编译安装MySQL8.0.19
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
推荐阅读
最新文章
- SpringBoot2整合Redis,开启缓存,提高访问速度
- Windows10,CentOS7,CentOS8安装Nodejs环境
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- CentOS7设置SWAP分区,小内存服务器的救世主
- Docker安装Oracle12C,快速搭建Oracle学习环境
- Hadoop3单机部署,实现最简伪集群