Spring获取方法参数名称的分析

原创
2017/08/29 17:57
阅读数 3.2K

1.问题的开始

Spring AOP中args和arg-names的使用的最后我提到了在Spring AOP的注解方式中的@Pointcut的args配置时,对于args中的变量名必须匹配@Pointcut注解所在方法中的参数名的问题。代码如下:

@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方法改成

pointcut(String video, Date date)

就会抛出异常

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方法中的参数名称必须一致。因而就引发了下一个问题,它是怎么获取到方法中的参数名的?

关于java获取方法参数名,之前看过一些文章,观点基本是一致的。

即可以从字节码中获取方法的参数名,但是有限制,只有在编译时使用了-g或者-g:vars参数生成了调试信息,class文件中才会生成方法参数名信息(在本地变量表LocalVariableTable中),而不使用-g时编译的class文件中则会丢弃方法参数名信息。

通过javap反编译生成的class文件

javap -c -v AspectJAnnotationArgsBrowserAroundAdvice.class

反编译的结果:

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中应该是通过字节码中获取的参数名。

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
展开阅读全文
打赏
3
5 收藏
分享
加载中
更多评论
打赏
0 评论
5 收藏
3
分享
返回顶部
顶部