打印Java方法参数首先描述一下具体的需求就是,能不能不需要手动添加代码就能打印Java方法所有的参数,这有些时候在我们调试代码的时候有很重要的帮助。
按照这个需求,我们可以想一下我们大体需要一下什么信息,方法的名称,方法参数类型,方法参数的名字,方法参数的值。
如何实现不写代码就能够实现动态的打印这些信息呢,了解Java的这时候就都会想到动态代理。
有了动态代理我们就可以不用写代码了,但是为了区分哪些方法需要打印,哪些方法不需要打印,我们这里还需要注解来辅助区分需要打印的方法。
如何获取需要打印的信息呢,这里我相信大家都会想到反射,但是反射这里有一个参数是拿不到的,哪个参数呢,方法参数的名字是拿不到的。
这里我们采用的是asm的方式来获取方法参数的名字。
到这里功能已经描述清楚,需要用到的技术也描述清楚,接下来就是具体怎么实现了。
首先,我们设计了一个注解类如下:import ng.annotation.ElementType;import ng.annotation.Retention;import ng.annotation.RetentionPolicy;import ng.annotation.Target;@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public@interface MethodLog {}接下来就是我们设计的最后要打印的数据的一个简单的封装类,如下:public class MethodInfo {private int index;//参数的索引private Object parameterType;//参数的类型private String parameterName;//参数的名称private Object parameterValue;//参数的值public MethodInfo(){}public MethodInfo(int index, Object parameterType, String parameterName, Object parameterValue) {super();this.index = index;this.parameterType = parameterType;this.parameterName = parameterName;this.parameterValue = parameterValue;}public int getIndex() {return index;}public void setIndex(int index) {this.index = index;}public Object getParameterType() {return parameterType;}public void setParameterType(Object parameterType) {this.parameterType = parameterType;}public String getParameterName() {return parameterName;}public void setParameterName(String parameterName) {this.parameterName = parameterName;}public Object getParameterValue() {return parameterValue;}public void setParameterValue(Object parameterValue) {this.parameterValue = parameterValue;}@Overridepublic String toString() {return System.getProperty("line.separator")+"index=" + index + ", parameterType=" + parameterType+ ", parameterName="+ parameterName+ ", parameterValue=" + parameterValue;}}比较简单,没什么可以描述的,接下来我们就可以写动态代理类了,这里做了一个简单的封装,如下:import ng.reflect.InvocationHandler;import ng.reflect.Proxy;public class ProxyUtil{public static <T> T getProxy(T obj,InvocationHandler h){ Class[] interfaces = null;if(obj.getClass().isInterface()){interfaces = new Class[1];interfaces[0] = obj.getClass();}else{interfaces = obj.getClass().getInterfaces();}T ins = (T)h);return ins;}}这里的参数中的obj要动态代理的目标类,为了避免传入的是接口,代码中多了对接口的判断,也比较简单。
接下来就是要编写InvocationHandler的实现类了,具体实现如下:import ng.annotation.Annotation;import ng.reflect.InvocationHandler;import ng.reflect.Method;import java.util.ArrayList;import java.util.List;import org.objectweb.asm.ClassReader;import org.objectweb.asm.tree.ClassNode;import org.objectweb.asm.tree.LocalVariableNode;import org.objectweb.asm.tree.MethodNode;public class MethodLogUtil implements InvocationHandler{ private Object target;public MethodLogUtil(){}public MethodLogUtil(Object target){this.target = target;}public Object getTarget() {return target;}public void setTarget(Object target) {this.target = target;}@Override@SuppressWarnings("unchecked")public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {Class clazz = target.getClass();Class[] params = method.getParameterTypes();Method m = clazz.getMethod(method.getName(), params);Annotation anno = m.getAnnotation(MethodLog.class);if(anno != null){ClassReader rc = newClassReader(target.getClass().getName());ClassNode cn = new ClassNode();rc.accept(cn, ClassReader.EXPAND_FRAMES);List<MethodNode> list = cn.methods;List<MethodInfo> methodInfos= new ArrayList<>(args.length);for(MethodNode e : list){if(.equals(method.getName())){List<LocalVariableNode> local = e.localVariables;for(int i=1;i<local.size() && i<=args.length;i++){MethodInfo info = newMethodInfo(i,local.get(i).desc,local.get(i).name,args[i-1]);methodInfos.add(info);}}}System.out.println("method:"+method.getName()+System.getProperty( "line.separator")+methodInfos);}return method.invoke(target, args);}}上面这段代码中还是有一些细节需要描述的,同时我针对我之前的一个文档《Spring AOP自动切换数据源+简单事务的封装》中的一个Bug进行修正描述一下:首先,描述一下上一篇文章中出现Bug的地方是在文章的最后,就是我们在动态代理中,通过Proxy中拿不到我们目标类的注解,当时我们通过调试发现,proxy中的方法上是没有目标类的注解的,而且两个的方法是不一样的。
于是当时我就想到了采用先获取所有的方法之后通过对方法名比较的方式来获取具体的需要获取注解的方法,这种方式的bug出现在方法重载的时候这里是有问题的,所以现在修改成了上面的实现方式,直接通过getMethod传入具体的方法名和调用参数的类型就可以获取具体的方法。
这里可以简单描述一下为什么代理的方法中是没有我们的目标类上的注解的,主要是由于jdk的代理是只能够代理接口的,在生成代理类的时候,代理类中的方法是来自于接口的,所以我们把注解放到目标类上,在代理类中是拿不到注解的,但是我们可以把注解写到接口中的方法上,此时就可以拿到注解了。
接下来的那部分代码就是通过asm来获取具体的参数名字等信息了,这里有一点,我们是通过获取局部变量的方式来获取方法的参数的,主要是因为我通过获取参数的方式直接拿参数的时候每次拿到的值总是null,不知道为什么,所以就变成了如上的实现方式,接下来就是进行测试了。