在开发中,我是一个极懒的人, 而且特别不喜欢写无用且烦锁的代码,但java的语言特性又使得我不得不写这些无用的代码,以前我研究过一段时间的python代码,发现python的语法确实简单得令人发指,有一段时间,如果有机会去开发python代码,都想放弃java代码开发项目的冲动 ,但现在是残酷的,因为生活所迫,不得不继续用java代码来谋求生活,就像有人据说中,生活就像被强间一样,既然不能改变,那就好好的享受吧,但是我们也不能一味的忍受,总要改变点什么 。
话不多说,先来看一个例子。
public static void main(String[] args) { int a = 1; int b = 2; int c = 3; System.out.println(response(a, b, c)); } public static Map<String, Object> response(Object... args) { Map<String, Object> map = new HashMap<>(); map.put("a", args[0]); map.put("b", args[1]); map.put("c", args[2]); return map; }
上面这个例子的意图是想调用response()方法,将传入response()方法作作为一个map返回。 但我不想传入map的key,而在response()方法中自动的获取传入参数名,如 response(a, b, c), 想在response()方法中获取a的变量名"a",b的变量名"b",c的变量名"c",如果在另外一个地方同样调用response(d),则会获取到d的变量名"d", 这能做到不? 按java语言的特点,是获取不到的,因为在编译时,为了节省内存空间,在编译时,会将复杂的变量名简单化,因此无从得到具体的变量名。
来看一下真实的应用场景 。
Spring Boot中,我们写了3个测试方法。 test0(),test01(),test02(),对于test0()方法,是我们经常遇到的情况,我们要返回一个Map 对象,但不得不写下面几行鸡肋的代码,索然无味,弃之不行的代码。
Map<String,Object> map = new HashMap(); map.put("a",1); map.put("b",2); map.put("dataDto",dataDto);
如果能像test01()和test02()方法一样,只需要将变量丢进result方法中,就可以得到一个Map对象,或得到一个具体的对象,那该多好啊。
如创建一个对象
@Data public class DataDto { private Integer a; private Integer b; private Integer c; }
调用如下方法。
就可以得到DataDto对象或修改参数的顺序,也能得到正确的DataDto对象。
如果能实现这一点,那该多好啊。 为什么我想这么做呢?因为python中有一种元组的语法,如下所示 。
python中的这种语法是不是很赞,但是想在java中实现这种语法,基本上不可能,之前在《Java编程思想》提到过一种语法,也是java的元组 。 但这种语法最多能像我这样实现。
上面语法的好处就是就是不需要返回多个参数时重复的创建对象,在Tuple中传入3个参数, 可以通过getFirst(),getSecond(),getThird()来获取传入参数的值,但这样抹除了原来参数的含义。 最后写代码的人都不知道first,second,third是什么东西了,如
如上图所示 ,
DataDto dto1 = R.result(a); DataDto dto2 = R.result(b, a); DataDto dto3 = R.result(c, b, a);
a,b,c 传入参数没有任何顺序可言,但结果输出DataDto对象的属性是正确的,这样做的好处,一方面我们写代码的风格更加自由,另外,当上面3种情况,我们不需要在DataDto中写3个构造方法,分别适应传入的参数。 只需要传入的参数名和DataDto的属性字段名对应即可,如果我们不想写构造函数,那我们至少也要调用setA() ,setB() ,setC()方法,因此上面这种写法至少了少掉了1~3行鸡肋代码,从输出结果上来看,竟然实现了此功能 。
{"a":1} {"a":1,"b":2} {"a":1,"b":2,"c":3}
来看看http://localhost:8080/test01执行效果
再来看看http://localhost:8080/test02执行效果
再来看看http://localhost:8080/test03的执行结果
是不是我们之前提出的疑问,在例子中都得到了解决,感觉不可能啊,在方法的内部根本拿不到调用者的方法参数名称 ,那这么神奇的代码,到底如何实现的呢?
先来简述一下,到底如何实现,通过常规操作是不可能实现了, 因此需要修改字节码的方式来实现, 这种技术很多都用到,如cglib代理,我们只需要声明变量时,将变量名和变量值构建一个Map加入到threadLocal中,当调用R.result()时,根据变量值取出变量名,再在R.result()方法内部获取使用R.result()所在方法的返回参数。
当然,目前只对返回参数是对象类型或Map类型做支持,当然如返回参数是基本数据类型或返回参数是字符串类型,或返回参数是List类型,这些可以根据实际的业务需要再去做扩展即可。 如果方法的返回参数是对象类型,则通过反射创建对象并实例化它,并通过反射为字段赋值,如果返回值是Map对象,则直接构建map对象返回即可,是不是原理也很简单,接下来看对象修改后的字节码 。 看test03()方法 。
public class LocalvariablesNamesEnhancer { static ThreadLocal<Stack<Map<String, Object>>> localVariables = new ThreadLocal<Stack<Map<String, Object>>>(); public static void checkEmpty() { if (localVariables.get() != null && localVariables.get().size() != 0) { logger.error("LocalVariablesNamesTracer.checkEmpty, constraint violated (%s)", localVariables.get().size()); } } public static void clear() { if (localVariables.get() != null) { localVariables.set(null); } } public static void enter() { if (localVariables.get() == null) { localVariables.set(new Stack<Map<String, Object>>()); } localVariables.get().push(new HashMap<String, Object>()); } public static void exit() { if (localVariables.get().isEmpty()) { return; } localVariables.get().pop().clear(); } public static Map<String, Object> locals() { if (localVariables.get() != null && !localVariables.get().empty()) { return localVariables.get().peek(); } return new HashMap<String, Object>(); } public static void addVariable(String name, Object o) { locals().put(name, o); } public static void addVariable(String name, boolean b) { locals().put(name, b); } public static void addVariable(String name, char c) { locals().put(name, c); } public static void addVariable(String name, byte b) { locals().put(name, b); } public static void addVariable(String name, double d) { locals().put(name, d); } public static void addVariable(String name, float f) { locals().put(name, f); } public static void addVariable(String name, int i) { locals().put(name, i); } public static void addVariable(String name, long l) { locals().put(name, l); } public static void addVariable(String name, short s) { locals().put(name, s); } public static Map<String, Object> getLocalVariables() { return locals(); } } }
在生成字节码中用到了下面3个方法
LocalVariablesNamesTracer.enter(); LocalVariablesNamesTracer.addVariable("a", a); LocalVariablesNamesTracer.exit();
每次enter方法时会向localVariables栈中添加一个Map对象,每一次调用exit()方法时,会将栈顶的Map对象移除掉,而调用addVariable()方法时,会将变量名和变量值存储于栈顶的map中。
看修改后的字节码
实现原理是什么呢?先看下图。
通过上图分析,我相信大家对执行过程有了一定的了然,当然,再来看result()的实现
public class R { public static Map<String, Object> getResParam(Object... args) { // 构建map Map<String, Object> result = new HashMap(); Object[] var6 = args; int var5 = args.length; for (int var4 = 0; var4 < var5; ++var4) { Object o = var6[var4]; List<String> names = LocalvariablesNamesEnhancer.LocalVariablesNamesTracer.getAllLocalVariableNames(o); Iterator var9 = names.iterator(); while (var9.hasNext()) { String name = (String) var9.next(); result.put(name, o); } } return result; } public static List<String> getAllLocalVariableNames(Object o) { List<String> allNames = new ArrayList<String>(); // 将R.result() 传入参数值与栈顶中的变量值比对, // 如果相等,则返回对象名 for (String variable : getLocalVariables().keySet()) { if (getLocalVariables().get(variable) == o) { allNames.add(variable); } if (o != null && o instanceof Number && o.equals(getLocalVariables().get(variable))) { allNames.add(variable); } } return allNames; } public static <T> T result(Object... args) { Object result = null; try { Throwable throwable = new Throwable(); StackTraceElement[] stackTraceElements = throwable.getStackTrace(); // 获取到调用result() 代码所在方法的类名,方法名,行号 String className = stackTraceElements[1].getClassName(); String methodName = stackTraceElements[1].getMethodName(); int line = stackTraceElements[1].getLineNumber(); // 通过参数值获取参数名和参数值的map对象 Map<String, Object> resp = getResParam(args); // 通过字节码读取方法的返回值 String returnType = ClassMethodUtils.getMethodReturnType(className, methodName, line); // 如果是map,则返回map对象 if("Ljava/util/Map;".equals(returnType)){ return (T)resp; } // 获取得 returnType = Lcom/example/thread_no_known/dto; // 如果需要反射调用,需要删除到开头的L和字符串结尾的; // 同时将字符串中 / 替换为 . returnType = returnType.substring(1); returnType = returnType.substring(0, returnType.length() - 1); returnType = returnType.replaceAll("/", "."); // 反射实例化对象 Class clazz = Class.forName(returnType); result = clazz.newInstance(); Field fields[] = clazz.getDeclaredFields(); if (fields != null && fields.length > 0) { // 为属性赋值 for (Field field : fields) { if(field.getName().startsWith("$")){ continue; } field.setAccessible(true); field.set(result, resp.get(field.getName())); } } return (T) result; } catch (Exception e) { log.error("异常",e); } return null; } }
对于result()方法,有两个比较重要的块,第一,从栈顶中获取Map,根据值获取key,并再封装成map返回即可。但有小伙伴肯定会觉得费解。
为什么会通过值获取到多个key呢? 因为这里存在一种这样的情况 ,如下。
从上面例子中,人只塞入两个参数,但map返回值却是3个,这难道有bug不?
反过来想,假如在getAllLocalVariableNames()方法中,取到一个变量值也传入值相等即返回。
将会出现你传入了a,b,c 3个参数,可能返回值只有a,c两个参数。
因此这就明显有逻辑上的错误了,因此,在这里使用宁可错杀,也不能错过的方式,只要值与之前的相等,则加入到返回的map中,这一点需要注意 。
像这样写,就明显的语言上的错误了。 我只要得到一个对象
DataDto,但对象DataDto的b属性我不想赋值,但返回结果却是b有值,明显语义不对嘛,但小伙伴也不用这么想,你想想,你都不用b 这个变量,你定义在这里做什么,而且还与其他变量值一样,所以这可能是这种运用产生的后遗证。 这点还需要注意 。
接下来看通过类名,方法名,行号获取方法的返回值。
Throwable throwable = new Throwable(); StackTraceElement[] stackTraceElements = throwable.getStackTrace(); String className = stackTraceElements[1].getClassName(); String methodName = stackTraceElements[1].getMethodName(); int line = stackTraceElements[1].getLineNumber();
通过Throwable,我们只能获取到类名,方法名,行号这些信息,如果想拿到方法的返回值,显然拿不到,那怎么办呢?
public static String getMethodReturnType(String className,String methodName ,int line) { try { String resourcePath = ClassUtils.convertClassNameToResourcePath(className) + ClassUtils.CLASS_FILE_SUFFIX; ClassLoader classLoader = ClassUtils.getDefaultClassLoader(); // 获取class的输入流 InputStream is = ClassParser.getInputStream(classLoader, resourcePath); // 解析class方法 ClassFile classFile = ClassFile.Parse(ClassReaderUtils.readClass(is)); // 遍历类中所有的方法 for (MemberInfo memberInfo : classFile.getMethods()) { MethodDescriptorParser parser = new MethodDescriptorParser(); // 解析方法,得到方法描述,如请求参数,返回参数,方法名等 MethodDescriptor parsedDescriptor = parser.parseMethodDescriptor(memberInfo.Descriptor()); StringBuilder parameterTypeStr = new StringBuilder(); String byteMethodName = memberInfo.Name(); // 获取字节码中方法名 if (!byteMethodName.equals(methodName)) { //如果方法名不相等,则重新查找 continue; } for (String parameterType : parsedDescriptor.parameterTypes) { String d = ClassNameHelper.toStandClassName(parameterType); parameterTypeStr.append(d); } AttributeInfo attributeInfos[] = memberInfo.getAttributes(); // 获取Code下的第一个LineNumberTable for (AttributeInfo codeAttribute : attributeInfos) { if (codeAttribute instanceof CodeAttribute) { AttributeInfo codeAttributeInfos[] = ((CodeAttribute) codeAttribute).getAttributes(); for (AttributeInfo attributeInfo : codeAttributeInfos) { // 获取行号表属性 if (attributeInfo instanceof LineNumberTableAttribute) { LineNumberTableEntry[] lineNumberTableEntries = ((LineNumberTableAttribute) attributeInfo).getLineNumberTables(); LineNumberTableEntry firstLine = lineNumberTableEntries[0]; LineNumberTableEntry lastLine = lineNumberTableEntries[lineNumberTableEntries.length - 1]; // 如果调用栈中的行号在字节码中方法的行号之间,则此方法就是我们要找的方法 // 获取此方法的返回值即可 if (line >= firstLine.getLineNumber().Value() && line <= lastLine.getLineNumber().Value()) { return parsedDescriptor.returnType; } } break; } break; } } } } catch (IOException e) { e.printStackTrace(); } return null; }
上面代码来源于另外一个jar包,感兴趣可去下载看看。
https://github.com/quyixiao/classparser.git 这个包的主要用途就是解析class字节码,得到类的信息如下
ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; }
上面代码获取方法的返回值,可能大家理解起来还是费解的,如
以test5()方法调用R.result()为例, 在public static T result(Object… args) {} 方法内部能拿到调用result()方法的类名为com.example.thread_no_known.contoller.TestController,方法名为test5() , 行号为126行,在解析字节码后,遍历类的所有方法,126 行刚好是test5()方法内部,因此test5()就是我们要找的方法,再通过字节码解析得到test5()方法的返回值即可,得到返回值,再通过反射创建对象,为对象属性赋值,就轻而易举了。
我相信大家看到这里觉得是那么回事了,但有小伙伴肯定会想。
如上图所示,你又是如何修改字节码的呢? 这涉及到另外一个包 。 代码也上传到github上了。
https://github.com/quyixiao/transmit-variable-local.git
mvn clean install 打包这个项目 。 在jar包启动时,作为启动参数添加进去,如下图所示 。
这个包做了哪些事情呢?在类加载时修改类字节码 。
我们着生来分析这个类。
public byte[] doTransform(String className, byte[] classFileBuffer, ClassLoader loader) throws IOException, NotFoundException, CannotCompileException { // 如果不符合条件,则对字节码不做修改 if (Utils.isNotNull(className) && Utils.validate(className)) { System.out.println("real doTransform class name :" + className); // 这里使用了javassist ,获取CtClass对象 final CtClass ctClass = Utils.getCtClass(classFileBuffer, loader); String temp = className.replaceAll("\\.","/"); File file = new File("/Users/quyixiao/github/Thread_NO_Known/origin" + getdir(className)); if(!file.exists()){ file.mkdirs(); } file = new File("/Users/quyixiao/github/Thread_NO_Known/out" + getdir(className)); if(!file.exists()){ file.mkdirs(); } for (CtMethod method : ctClass.getDeclaredMethods()) { // 如果类名中包含$ ,则表示代理方法,而不是开发者自己写的,对于这种方法,不需要修改字节码 if (method.getName().contains("$")) { // Generated method, skip continue; } // Signatures names // 获取字节码中所有的Code属性 CodeAttribute codeAttribute = (CodeAttribute) method.getMethodInfo().getAttribute("Code"); if (codeAttribute == null || javassist.Modifier.isAbstract(method.getModifiers())) { continue; } // 获取方法的本地变量表 LocalVariableAttribute localVariableAttribute = (LocalVariableAttribute) codeAttribute.getAttribute("LocalVariableTable"); List<T2<Integer, String>> parameterNames = new ArrayList<T2<Integer, String>>(); if (localVariableAttribute == null) { if (method.getParameterTypes().length > 0) continue; } else { if (localVariableAttribute.tableLength() < method.getParameterTypes().length + (javassist.Modifier.isStatic(method.getModifiers()) ? 0 : 1)) { logger.warn("weird: skipping method %s %s as its number of local variables is incorrect (lv=%s || lv.length=%s || params.length=%s || (isStatic? %s)", method.getReturnType().getName(), method.getLongName(), localVariableAttribute, localVariableAttribute != null ? localVariableAttribute.tableLength() : -1, method.getParameterTypes().length, javassist.Modifier.isStatic(method.getModifiers())); } for (int i = 0; i < localVariableAttribute.tableLength(); i++) { if (!localVariableAttribute.variableName(i).equals("__stackRecorder")) { parameterNames.add(new T2<Integer, String>(localVariableAttribute.startPc(i) + localVariableAttribute.index(i), localVariableAttribute.variableName(i))); } } Collections.sort(parameterNames, new Comparator<T2<Integer, String>>() { public int compare(T2<Integer, String> o1, T2<Integer, String> o2) { return o1._1.compareTo(o2._1); } }); } List<String> names = new ArrayList<String>(); // 下面这一块主要是 for (int i = 0; i < method.getParameterTypes().length + (javassist.Modifier.isStatic(method.getModifiers()) ? 0 : 1); i++) { if (localVariableAttribute == null) { continue; } try { String name = parameterNames.get(i)._2; // 排队掉this本地变量 if (!name.equals("this")) { names.add(name); } } catch (Exception e) { System.out.println("exception 97"); } } StringBuilder iv = new StringBuilder(); if (names.isEmpty()) { iv.append("new String[0];"); } else { iv.append("new String[] {"); for (Iterator<String> i = names.iterator(); i.hasNext(); ) { iv.append("\""); String aliasedName = i.next(); if (aliasedName.contains("$")) { aliasedName = aliasedName.substring(0, aliasedName.indexOf("$")); } iv.append(aliasedName); iv.append("\""); if (i.hasNext()) { iv.append(","); } } iv.append("};"); } String sigField = "$" + method.getName() + LocalvariablesNamesEnhancer.LocalVariablesNamesTracer.computeMethodHash(method.getParameterTypes()); try { // #1198 ctClass.getDeclaredField(sigField); } catch (NotFoundException nfe) { CtField signature = CtField.make("public static String[] " + sigField + " = " + iv.toString(), ctClass); ctClass.addField(signature); } if (localVariableAttribute == null) { continue; } // 上面这块代码主要是 如图10 所示 // public static String[] $test695092022 = new String[]{"param1", "param2"}; 属性生成 // OK. // Here after each local variable creation instruction, // we insert a call to com.linzi.utils.LocalVariables.addVariable('var', var) // without breaking everything... for (int i = 0; i < localVariableAttribute.tableLength(); i++) { // name of the local variable String name = localVariableAttribute.getConstPool().getUtf8Info(localVariableAttribute.nameIndex(i)); System.out.println("变量名="+name); // Normalize the variable name // For several reasons, both variables name and name$1 will be aliased to name String aliasedName = name; if (aliasedName.contains("$")) { aliasedName = aliasedName.substring(0, aliasedName.indexOf("$")); } if (name.equals("this")) { continue; } /* DEBUG IO.write(ctClass.toBytecode(), new File("/tmp/lv_"+applicationClass.name+".class")); ctClass.defrost(); */ try { // The instruction at which this local variable has been created Integer pc = localVariableAttribute.startPc(i); // Move to the next instruction (insertionPc) CodeIterator codeIterator = codeAttribute.iterator(); codeIterator.move(pc); pc = codeIterator.next(); Bytecode b = makeBytecodeForLVStore(method, localVariableAttribute.signature(i), name, localVariableAttribute.index(i)); codeIterator.insert(pc, b.get()); codeAttribute.setMaxStack(codeAttribute.computeMaxStack()); // Bon chaque instruction de cette méthode while (codeIterator.hasNext()) { int index = codeIterator.next(); int op = codeIterator.byteAt(index); String option = Factory.NewInstruction(op); System.out.println("op =" + op + ",option="+option); // DEBUG // printOp(op); int varNumber = -1; // The variable changes if (storeByCode.containsKey(op)) { varNumber = storeByCode.get(op); if (varNumber == -2) { varNumber = codeIterator.byteAt(index + 1); } } // Si c'est un store de la variable en cours d'examination // et que c'est dans la frame d'utilisation de cette variable on trace l'affectation. // (en fait la frame commence à localVariableAttribute.startPc(i)-1 qui est la première affectation // mais aussi l'initialisation de la variable qui est deja tracé plus haut, donc on commence à localVariableAttribute.startPc(i)) if (varNumber == localVariableAttribute.index(i) && index < localVariableAttribute.startPc(i) + localVariableAttribute.codeLength(i)) { b = makeBytecodeForLVStore(method, localVariableAttribute.signature(i), aliasedName, varNumber); codeIterator.insertEx(b.get()); codeAttribute.setMaxStack(codeAttribute.computeMaxStack()); } } } catch (Exception e) { // Well probably a compiled optimizer (I hope so) } } // init variable tracer method.insertBefore("com.linzi.classloading.enhancers.LocalvariablesNamesEnhancer.LocalVariablesNamesTracer.enter();"); method.insertAfter("com.linzi.classloading.enhancers.LocalvariablesNamesEnhancer.LocalVariablesNamesTracer.exit();", true); } // Done. byte [] result = ctClass.toBytecode(); IO.write(result, new File("/Users/quyixiao/github/Thread_NO_Known/out/"+temp+".class")); ctClass.defrost(); return result; } return null; } private final static Map<Integer, Integer> storeByCode = new HashMap<Integer, Integer>(); /** * Useful instructions */ static { storeByCode.put(CodeIterator.ASTORE_0, 0); storeByCode.put(CodeIterator.ASTORE_1, 1); storeByCode.put(CodeIterator.ASTORE_2, 2); storeByCode.put(CodeIterator.ASTORE_3, 3); storeByCode.put(CodeIterator.ASTORE, -2); storeByCode.put(CodeIterator.ISTORE_0, 0); storeByCode.put(CodeIterator.ISTORE_1, 1); storeByCode.put(CodeIterator.ISTORE_2, 2); storeByCode.put(CodeIterator.ISTORE_3, 3); storeByCode.put(CodeIterator.ISTORE, -2); storeByCode.put(CodeIterator.IINC, -2); storeByCode.put(CodeIterator.LSTORE_0, 0); storeByCode.put(CodeIterator.LSTORE_1, 1); storeByCode.put(CodeIterator.LSTORE_2, 2); storeByCode.put(CodeIterator.LSTORE_3, 3); storeByCode.put(CodeIterator.LSTORE, -2); storeByCode.put(CodeIterator.FSTORE_0, 0); storeByCode.put(CodeIterator.FSTORE_1, 1); storeByCode.put(CodeIterator.FSTORE_2, 2); storeByCode.put(CodeIterator.FSTORE_3, 3); storeByCode.put(CodeIterator.FSTORE, -2); storeByCode.put(CodeIterator.DSTORE_0, 0); storeByCode.put(CodeIterator.DSTORE_1, 1); storeByCode.put(CodeIterator.DSTORE_2, 2); storeByCode.put(CodeIterator.DSTORE_3, 3); storeByCode.put(CodeIterator.DSTORE, -2); }
重点看上面加粗代码
private static Bytecode makeBytecodeForLVStore(CtMethod method, String sig, String name, int slot) { Bytecode b = new Bytecode(method.getMethodInfo().getConstPool()); // 如果字符串在常量池中索引小等于255 ,用ldc 指令 // 如果字符串在常量号中索引大于255,则用ldc_w指令 b.addLdc(name); // 如果以像Short,boolean ,char , byte 类型,都用整形表示,则用iload指令 if ("I".equals(sig) || "B".equals(sig) || "C".equals(sig) || "S".equals(sig) || "Z".equals(sig)) b.addIload(slot); else if ("F".equals(sig)) // 如果是F 开头,则用fload指令 b.addFload(slot); else if ("J".equals(sig)) // 如果是J 开头,表示Long类型,则用lload指令 b.addLload(slot); else if ("D".equals(sig)) // 如果是double类型,则使用dload指令 b.addDload(slot); else // 如果是引用类型,则使用Aload指令 b.addAload(slot); String localVarDescriptor = sig; // 如果非基本数据类型,则本地变量用Object来描述 if (!"B".equals(sig) && !"C".equals(sig) && !"D".equals(sig) && !"F".equals(sig) && !"I".equals(sig) && !"J".equals(sig) && !"S".equals(sig) && !"Z".equals(sig)) localVarDescriptor = "Ljava/lang/Object;"; b.addInvokestatic("com.linzi.classloading.enhancers.LocalvariablesNamesEnhancer$LocalVariablesNamesTracer", "addVariable", "(Ljava/lang/String;" + localVarDescriptor + ")V"); return b; }
其实makeBytecodeForLVStore()方法写了那么多,就是加了LocalVariablesNamesTracer.addVariable(“a”, a);这一行代码 。
最后在方法调用前加com.linzi.classloading.enhancers.LocalvariablesNamesEnhancer.LocalVariablesNamesTracer.enter();方法。对应的代码如下。
method.insertBefore(“com.linzi.classloading.enhancers.LocalvariablesNamesEnhancer.LocalVariablesNamesTracer.enter();”);
最后在方法调用后加com.linzi.classloading.enhancers.LocalvariablesNamesEnhancer.LocalVariablesNamesTracer.exit();
method.insertAfter(“com.linzi.classloading.enhancers.LocalvariablesNamesEnhancer.LocalVariablesNamesTracer.exit();”, true);
test6()生成的字节码如下图所示
如图10
以test6()方法为例,看修改前和修改后字节码做了哪些修改。
从图中可以看出,红色部分为新增加的字节码 。我们解读一下下面这几条指令
-
0 invokestatic #71 <com/linzi/classloading/enhancers/LocalvariablesNamesEnhancer$LocalVariablesNamesTracer.enter> : 使用invokestatic指令调用常量池中指向71的静态方法。
-
3 ldc #62 <param2> : 将常量池指向62的变量推到操作数栈顶。
-
5 aload_2 :将本地变量槽2的引用变量推入操作数栈顶
-
6 invokestatic #61 :<com/linzi/classloading/enhancers/LocalvariablesNamesEnhancer$LocalVariablesNamesTracer.addVariable> :使用invokestatic指令调用指向常量池中61的静态方法 。
我相信大家看了跟没有看一样, 不知道我在说什么 ,这里我要科普一下, 对于普通方法,方法本地变量槽第0个位置肯定是this,而对于静态方法,变量槽的第0个位置可能是方法参数,也可能是内部声明的变量,而test6()方法肯定不是静态方法,而方法有两个参数,因此变量槽的第0个位置肯定是this,变量槽的第1个位置存储的是param1变量,变量槽的第2个位置存储的是param2, 而LocalVariablesNamesTracer.addVariable(“param2”, param2);这个调用需要两个参数,因此就出现了
- 3 ldc #62 <param2>
- 5 aload_2
这两步调用,实际上是将字符串param2推入操作数栈顶,再将param2的变量值推入操作数栈顶。然后再调用静态方法 。 之前也按照 《go 语言手写虚拟机》这本书,自己实现了一个Java 版本的虚拟机,接下来看里面的代码 。
通过invokestatic指令调用源码,我相信再来理解上面几条指令,你肯定已经理解了。 而上面截图的源码和相关博客在
自己动手写Java虚拟机 (Java核心技术系列)_java版 这篇博客中。
总结
我相信大家对我想要实现的功能及原理肯定有一定理解了, 但肯定有小伙伴会问 , method.insertBefore(“com.linzi.classloading.enhancers.LocalvariablesNamesEnhancer.LocalVariablesNamesTracer.enter();”); 这个的底层实现又是怎样的呢?是怎样通过一个字节一个字节的修改,最终得到我们想要的效果呢? 这一块是javassist 源码的解析了。本来我也想去研究一下,
我连源码都准备好了,但是由于个人时间不够,平常也有业务需求需要开发,同时还有tomcat , Netty , Dubbo , Nacos , ElasticSearch等更多的开源框架值得我去研究,因此这么底层的东西,只能放到后面有时间,有精力,有激情再来写博客了, 因为这种东西可能需要花几个星期时间才能研究明白。
这篇博客的内容也没有那么复杂,可能也只是我们没有想到而已,但是他提供的思想我觉得还是很有借鉴意义的。至少以另外一种方式,以不太美观的方式实现了java元组,如果应用于我们的WEB 开发,让我们繁锁的代码得到简化,使得业务逻辑变得更加清晰,我觉得还不错,希望对读者有所帮助,到这里,这一篇博客又告一段落了, 期侍下一篇博客再见。
本文相关的代码
https://github.com/quyixiao/Thread_NO_Known.git
https://github.com/quyixiao/transmit-variable-local.git