在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的編譯實現。過程雖然漫長,可是結果卻頗有成就感。不斷追求,不斷進步,不只是在技術的學習上,也應在人生的道路上。
參考文檔: