转自https://my.oschina.net/u/2377110/blog/1525716
1.问题的开始
在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的编译实现。过程虽然漫长,但是结果却很有成就感。不断追求,不断进步,不仅是在技术的学习上,也应在人生的道路上。
参考文档: