在Spring AOP中args和arg-names的使用的最后我提到了在Spring AOP的注解方式中的@Pointcut的args配置时,对于args中的变量名必须匹配@Pointcut注解所在方法中的参数名的问题。代码以下:html
@Pointcut("execution(* com.lcifn.spring.aop.bean.ChromeBrowser.*(..)) && args(music,date)") private void pointcut(String music, Date date){}
即args(music,date)中的music和date必须同pointcut方法中的music和date一致,若是将pointcut方法改为java
pointcut(String video, Date date)
就会抛出异常spring
Caused by: java.lang.IllegalArgumentException: warning no match for this type name: music [Xlint:invalidAbsoluteTypeName] at org.aspectj.weaver.tools.PointcutParser.parsePointcutExpression(PointcutParser.java:301) at org.springframework.aop.aspectj.AspectJExpressionPointcut.buildPointcutExpression(AspectJExpressionPointcut.java:206) at org.springframework.aop.aspectj.AspectJExpressionPointcut.checkReadyToMatch(AspectJExpressionPointcut.java:192) at org.springframework.aop.aspectj.AspectJExpressionPointcut.getClassFilter(AspectJExpressionPointcut.java:169)
通过反复测试,证实args中的变量名称同pointcut方法中的参数名称必须一致。于是就引起了下一个问题,它是怎么获取到方法中的参数名的?apache
关于java获取方法参数名,以前看过一些文章,观点基本是一致的。api
便可以从字节码中获取方法的参数名,可是有限制,只有在编译时使用了-g或者-g:vars参数生成了调试信息,class文件中才会生成方法参数名信息(在本地变量表LocalVariableTable中),而不使用-g时编译的class文件中则会丢弃方法参数名信息。框架
经过javap反编译生成的class文件eclipse
javap -c -v AspectJAnnotationArgsBrowserAroundAdvice.class
反编译的结果:maven
Classfile /e:/exercise/workspace/spring-d/target/classes/com/lcifn/spring/aop/ad vice/AspectJAnnotationArgsBrowserAroundAdvice.class Last modified 2017-8-23; size 2133 bytes MD5 checksum dc8e53c7881db8fc8d0f7856bdaa378d Compiled from "AspectJAnnotationArgsBrowserAroundAdvice.java" public class com.lcifn.spring.aop.advice.AspectJAnnotationArgsBrowserAroundAdvic e minor version: 0 major version: 49 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Class #2 // com/lcifn/spring/aop/advice/AspectJ AnnotationArgsBrowserAroundAdvice #2 = Utf8 com/lcifn/spring/aop/advice/AspectJAnnotationArgsBrow serAroundAdvice ... public java.lang.Object aroundIntercept(org.aspectj.lang.ProceedingJoinPoint, java.lang.String, java.util.Date, java.lang.String) throws java.lang.Throwable; descriptor: (Lorg/aspectj/lang/ProceedingJoinPoint;Ljava/lang/String;Ljava/u til/Date;Ljava/lang/String;)Ljava/lang/Object; flags: ACC_PUBLIC Exceptions: throws java.lang.Throwable ... LocalVariableTable: Start Length Slot Name Signature 0 60 0 this Lcom/lcifn/spring/aop/advice/AspectJAnnotati onArgsBrowserAroundAdvice; 0 60 1 pjp Lorg/aspectj/lang/ProceedingJoinPoint; 0 60 2 music Ljava/lang/String; 0 60 3 date Ljava/util/Date; 53 7 5 retVal Ljava/lang/Object;
能够看到在最后确实有本地变量表LocalVariableTable,方法中的参数名都记录在内。那么推测Spring中应该是经过字节码中获取的参数名。ide
经过跟踪断点的方式,发现查询方法参数名的方法在AspectJ的aspectjweaver-1.8.7.jar中的org.aspectj.weaver.reflect.Java15ReflectionBasedReferenceTypeDelegate类中。学习
// for @AspectJ pointcuts compiled by javac only... private String[] tryToDiscoverParameterNames(Pointcut pcut) { Method[] ms = pcut.getDeclaringType().getJavaClass().getDeclaredMethods(); for (Method m : ms) { if (m.getName().equals(pcut.getName())) { return argNameFinder.getParameterNames(m); } } return null; }
从方法名称tryToDiscoverParameterNames能够明确是去寻找方法参数名,而其真正的执行在于
argNameFinder.getParameterNames(m);
argNameFinder是org.aspectj.weaver.reflect.Java15AnnotationFinder
public String[] getParameterNames(Member forMember) { if (!(forMember instanceof AccessibleObject)) return null; try { // 使用bcel框架读取class文件并加载成类字节码对象 JavaClass jc = bcelRepository.loadClass(forMember.getDeclaringClass()); LocalVariableTable lvt = null; int numVars = 0; if (forMember instanceof Method) { org.aspectj.apache.bcel.classfile.Method bcelMethod = jc.getMethod((Method) forMember); // 获取方法的本地变量表 lvt = bcelMethod.getLocalVariableTable(); numVars = bcelMethod.getArgumentTypes().length; } else if (forMember instanceof Constructor) { org.aspectj.apache.bcel.classfile.Method bcelCons = jc.getMethod((Constructor) forMember); lvt = bcelCons.getLocalVariableTable(); numVars = bcelCons.getArgumentTypes().length; } // 从本地变量表中提取参数名称 return getParameterNamesFromLVT(lvt, numVars); } catch (ClassNotFoundException cnfEx) { ; // no luck } return null; }
AspectJ中使用apache的bcel(Byte Code Engineering Library)字节码操做框架,经过读取class文件加载成本身的字节码对象JavaClass,不只获得这个类的字段和方法信息,还包括对类的内部信息的访问,其中就包括本地变量表。来简单看下它的实现:
public JavaClass loadClass(Class clazz) throws ClassNotFoundException { return loadClass(clazz.getName()); } private JavaClass loadJavaClass(String className) throws ClassNotFoundException { String classFile = className.replace('.', '/'); try { // 读取class文件的字节流 InputStream is = loaderRef.getClassLoader().getResourceAsStream(classFile + ".class"); if (is == null) { throw new ClassNotFoundException(className + " not found."); } // 使用ClassParse对字节流进行解析,生成字节码对象 ClassParser parser = new ClassParser(is, className); return parser.parse(); } catch (IOException e) { throw new ClassNotFoundException(e.toString()); } }
ClassParse解析class文件字节流的过程很是清晰
public JavaClass parse() throws IOException, ClassFormatException { /****************** Read headers ********************************/ // Check magic tag of class file readID(); // Get compiler version readVersion(); /****************** Read constant pool and related **************/ // Read constant pool entries readConstantPool(); // Get class information readClassInfo(); // Get interface information, i.e., implemented interfaces readInterfaces(); /****************** Read class fields and methods ***************/ // Read class fields, i.e., the variables of the class readFields(); // Read class methods, i.e., the functions in the class readMethods(); // Read class attributes readAttributes(); // Read everything of interest, so close the file file.close(); // Return the information we have gathered in a new object JavaClass jc= new JavaClass(classnameIndex, superclassnameIndex, filename, major, minor, accessflags, cpool, interfaceIndices, fields, methods, attributes); return jc; }
以上解析完成后便可拿到方法的本地变量表,从而拿到全部方法的参数名称。
经过bcel框架加载字节码对象从而获取参数名称咱们已经清楚了,如今还剩一个问题就是,class文件中记录本地变量表的前提是java编译时使用了-g或-g:vars参数。那么我在exclipse中测试的时候为何没有问题呢?由于eclipse默认设置了编译时就添加调试信息。
我把这个选项去掉,再次执行测试,直接报错,说明aspectJ中查询方法参数名称确实是从字节码文件中获取的。
但在生产环境下,咱们是经过maven打包的方式进行部署,这就意味着maven应该也是默认使用-g参数的。maven是经过其内置的Compiler插件来编译的,在maven官网的compiler插件的可选参数列表中有一个debug参数,它的定义就是设置编译时是否包含调试信息,而且默认为true,而另外一个参数debuglevel则是在debug为true时,能够设置-g的后缀,分别为lines,vars或sources。
经过命令行的方式执行mvn -X compile命令手动编译(-X表示maven日志级别为debug),输入的日志中记录了最终编译执行的命令参数(省略了classpath)。
[DEBUG] Command line options: [DEBUG] -d e:\exercise\workspace\spring-d\target\classes -classpath xxx -g -nowarn -target 1.5 -source 1.5 -encoding utf-8
日志也证明了maven编译时默认包含调试信息。
然后翻阅了maven的部分源码,发现其依赖了一个Codehaus Plexus的jar包,官网上显示其为maven使用的组件集。plexus-compiler组件即maven的compiler组件实际执行的地方。在子模块plexus-compiler-javac中的JavacCompiler类中,对maven-compiler的可选参数进行了装配。
if ( config.isDebug() ) { if ( StringUtils.isNotEmpty( config.getDebugLevel() ) ) { args.add( "-g:" + config.getDebugLevel() ); } else { args.add( "-g" ); } }
至此spring AOP中的@Pointcut注解的使用中,对方法参数名称的获取原理所有揭开了,同时涉及到java的编译参数,以及maven的编译实现。过程虽然漫长,可是结果却颇有成就感。不断追求,不断进步,不只是在技术的学习上,也应在人生的道路上。
参考文档: