Spring獲取方法參數名稱的分析

1.問題的開始

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

2.Spring如何獲取方法參數名

經過跟蹤斷點的方式,發現查詢方法參數名的方法在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;
}

以上解析完成後便可拿到方法的本地變量表,從而拿到全部方法的參數名稱。

3.編譯時-g參數設置

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

參考文檔:

  1. java如何獲取方法參數名
  2. bcel官方文檔
  3. BCEL介紹
  4. maven compiler
相關文章
相關標籤/搜索