java动态代理——jvm指令集基本概念和方法字节码结构的进一步探究及proxy源码分析四
前文地址
本系列文章主要是博主在学习spring aop的过程中了解到其使用了java动态代理,本着究根问底的态度,于是对java动态代理的本质原理做了一些研究,于是便有了这个系列的文章
上一篇文章详细分析了class字节码结构中的field_info和method_info,以及对应的Proxy的源码。本文将会更详细的分析method_info中的方法执行体部分,也就是attributes中的Code
因为方法的字节码涉及到了jvm的操作指令,因此我们先做一个基础性的了解
原文地址:https://dzone.com/articles/introduction-to-java-bytecode
jvm指令文档:https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html
文中开始介绍的堆、栈、方法区等概念这里就不详细描述了,主要看它后面对一些简单方法的字节码的解析
首先我们定义一个简单的类
public class Test { public static void main(String[] args) { int a = 1; int b = 2; int c = a + b; } }
编译生成Test.class
javac Test.java
查看字节码结构
javap -v Test.class
我们关注其中的main方法部分
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=4, args_size=1 0: iconst_1 1: istore_1 2: iconst_2 3: istore_2 4: iload_1 5: iload_2 6: iadd 7: istore_3 8: return LineNumberTable: line 3: 0 line 4: 2 line 5: 4 line 6: 8
其中的Code正是方法的执行体,下面按照顺序图解具体操作
iconst_1:将常量1压入操作栈
istore_1:弹出栈顶的操作数,存入栈的本地变量数组的索引1,也就是变量a
iconst_2:将常量2压入操作栈
istore_2:弹出栈顶的操作数,存入栈的本地变量数组的索引2,也就是变量b
iload_1:从本地变量索引1种读取值,并压入操作栈
iload_2:从本地变量索引2种读取值,并压入操作栈
iadd:弹出栈顶的2个操作数,相加后将结果压入操作栈
istore_3:弹出栈顶的操作数,存入栈的本地变量数组的索引3,也就是变量c
return:从方法返回
如果我们在类中定义一个方法
public class Test { public static void main(String[] args) { int a = 1; int b = 2; int c = calc(a, b); } static int calc(int a, int b) { return (int) Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2)); } }
得到的字节码如下,这次我把部分Constant pool也展示在下面
Constant pool: #1 = Methodref #8.#19 // java/lang/Object."":()V #2 = Methodref #7.#20 // Test.calc:(II)I #3 = Double 2.0d #5 = Methodref #21.#22 // java/lang/Math.pow:(DD)D #6 = Methodref #21.#23 // java/lang/Math.sqrt:(D)D public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=4, args_size=1 0: iconst_1 1: istore_1 2: iconst_2 3: istore_2 4: iload_1 5: iload_2 6: invokestatic #2 // Method calc:(II)I 9: istore_3 10: return LineNumberTable: line 3: 0 line 4: 2 line 5: 4 line 6: 10 static int calc(int, int); descriptor: (II)I flags: ACC_STATIC Code: stack=6, locals=2, args_size=2 0: iload_0 1: i2d 2: ldc2_w #3 // double 2.0d 5: invokestatic #5 // Method java/lang/Math.pow:(DD)D 8: iload_1 9: i2d 10: ldc2_w #3 // double 2.0d 13: invokestatic #5 // Method java/lang/Math.pow:(DD)D 16: dadd 17: invokestatic #6 // Method java/lang/Math.sqrt:(D)D 20: d2i 21: ireturn LineNumberTable: line 8: 0
这里我们主要看一下一些新出现的操作指令
在main方法中,编号6
invokestatic #2:调用静态方法,方法在Constant Pool中索引为2,表示Test.calc方法(这里特别注意,调用的方法目标必须是常量池中的一个有效索引)
在cacl方法中
i2d:将int类型的转换成double类型的
ldc2_w:将long型或者double型(思考一下为何是这2种类型放在同一个操作指令中)从静态池中压入栈
dadd:将double相加
d2i:将double类型转换成int类型
ireturn:返回一个int
将上面的jvm指令结合java代码,就可以初步理解每一行java代码究竟是如何被jvm执行的了
接下去我们可以通过Proxy的代码结合实际来看看
方法还是generateClassFile()
在上一篇文章的第三部分字节与方法字节码的写入中,有提到
这里的第一行,正是写入构造器的字节码,这一部分因为涉及到jvm的执行指令,我们放到下篇文章再详细看,所以这里先跳过
this.methods.add(this.generateConstructor());
此时我们就可以详细看下generateConstructor方法究竟干了什么
特别注意的是,这里的var2表示的是方法的执行体部分,也就是在上一篇文章中,我们提到的方法attributes中的一个:Code
private ProxyGenerator.MethodInfo generateConstructor() throws IOException { ProxyGenerator.MethodInfo var1 = new ProxyGenerator.MethodInfo("", "(Ljava/lang/reflect/InvocationHandler;)V", 1); DataOutputStream var2 = new DataOutputStream(var1.code); this.code_aload(0, var2); this.code_aload(1, var2); var2.writeByte(183); var2.writeShort(this.cp.getMethodRef("java/lang/reflect/Proxy", " ", "(Ljava/lang/reflect/InvocationHandler;)V")); var2.writeByte(177); var1.maxStack = 10; var1.maxLocals = 2; var1.declaredExceptions = new short[0]; return var1; }
接下一行一行分析
初始化MethodInfo对象,3个参数分别是,方法名、方法描述、access_flag,1表示public(参见Modifier.java)
因为是构造函数,所以方法名为
方法的描述表示,该方法获取一个java.lang.reflect.InvocationHandler类型的参数,返回值为V(表示void)
方法的access_flag为1,表示public
ProxyGenerator.MethodInfo var1 = new ProxyGenerator.MethodInfo("", "(Ljava/lang/reflect/InvocationHandler;)V", 1);
在Code中写入aload_0和aload_1操作指令
this.code_aload(0, var2); this.code_aload(1, var2);
在Code中写入183号操作指令,查文档得:invokespecial
调用实例方法,特别用来处理父类的构造函数
var2.writeByte(183);
在Code中写入需要调用的方法名和方法的参数
注意,这里的方法是通过this.cp.getMethodRef方法得到的,也就是说,这里写入的最终数据,其实是一个符合该方法描述的常量池中的一个有效索引(这部分知识可以参看之前的3篇文章)
var2.writeShort(this.cp.getMethodRef("java/lang/reflect/Proxy", "", "(Ljava/lang/reflect/InvocationHandler;)V"));
在Code中写入177号指令,查文档得:return
返回void
var2.writeByte(177);
和上一篇文章中提到的一样,最后还需要写入栈深和本地变量数量,以及方法会抛出的异常数量,因为构造函数不主动抛出异常,所以异常数量直接为0
注意这里并非是直接writeByte,而是对MethodInfo的属性做了一个设置,这部分的字节码依然会在MethodInfo的write方法中写入,参见上一篇文章
var1.maxStack = 10; var1.maxLocals = 2; var1.declaredExceptions = new short[0];
到此,一个构造方法的结构就完成了
此时我们总结一下该构造函数的结构,当我们查看class文件的结构时,应当是下面这种结构
aload_0; aload_1; invokespecial #x //这里x对应Constant pool中构造函数的编号 return;
验证一下,我们建立一个类
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; public class Test extends Proxy { protected TestClass(InvocationHandler h) { super(h); } }
查看其字节码
protected Test(java.lang.reflect.InvocationHandler); descriptor: (Ljava/lang/reflect/InvocationHandler;)V flags: ACC_PROTECTED Code: stack=2, locals=2, args_size=2 0: aload_0 1: aload_1 2: invokespecial #1 // Method java/lang/reflect/Proxy."":(Ljava/lang/reflect/InvocationHandler;)V 5: return LineNumberTable: line 6: 0 line 7: 5
正和我们之前总结的一模一样
结合之前的一些jvm指令的基本描述,我们就可以对method_info的正题结构有了更深入的了解
本文中我们初步了解了方法执行体Code的结构,jvm指令的基本概念,那么在下一篇文章中,我们将会继续探究Proxy的最核心的部分,代理方法的Code部分的结构及其实际实现