在教程开始之前首先简单介绍下生成操作的字节码命令的原理。我们知道在java代码中我们最基本的运算就是操作,比如四则运算,方法调用等比如一下代码:
String a = "str";int i = 1 + 2;
i++;
System.out.println(a + i);
这里存在6种操作,让我们一句一句解析出操作具体如下:
1.String a = "str"; = : 赋值 2.int i = 1 + 2; + : 算数运算操作 = : 赋值 3.i++; ++ : 增量操作 4 System.out.println(a + i); _第二个"." : 方法调用 + : 字符串拼接操作
既然是操作就有操作因子,什么是操作因子?见下表:
代码 | 操作因子 |
String a = "str"; | "str" |
int i = 1 + 2; | 1, 2, 1+2 |
i++; | i |
System.out.println(a + i) | a, i, a + 1 |
总共存在10种操作,具体如下(这里只列举每种操作的其中一个,并不全部列举):
1. String a = "str";
= : 赋值
2. i++;
++ : 增量操作
3. int i = 1 + 2;
+ : 算数运算操作
= : 赋值
4. 9&7
& :位操作
5. a > 1
> : 关系运算操作
6. true && false
&& : 逻辑运算操作
7. k = i < 0 ? -i : i
_三元操作
8. a instanceof String
instanceof : instanceof操作
9. "Hello " + "asmsupport!"
+ : 字符串拼接操作
10. System.out.println(a + i);
_第二个"." : 方法调用操作
在我们的程序中我们将所有的操作都通过IBlockOperators中的方法调用实现。而所有的操作因子是通過方法参数传入的。并且有的操作同样能作为其他操作的操作因子,如上面的1+2就是作为赋值操作"="的操作因子。在asmsupport中只要是继承了jw.asmsupport.Parameterized接口的都可以是作为操作因子。其实在使用asmsupport的时候大部分只要在java代码中能作为操作因子的在asmsupport中同样也能作为操作因子。因为asmsupport保留了java代码编写的方式。这里是在使用ASMSupport的角度上去介绍的。
这里将简单的介绍这些操作在底层是如何实现的。在ASMSupport中我们将java操作都抽象成为类AbstractOperator.class,每个具体的操作都是继承自它,比如instanceof操作我们将其抽象成jw.asmsupport.operators.InstanceofOperator.AbstractOperator同时也继承了jw.asmsupport.Executeable接口,这个接口有两个方法。
每一个操作都相当于一个Executeable接口。ASMSupport会用一个List按照我们期望生成的代码的顺序将每个对应的Executeable存入到这个List中,我们称之为执行队列(执行队列中我们还会存储程序块jw.asmsupport.block.ProgramBlock),在我们将所有的操作都存入到执行队列之后先遍历所有操作,对所有操作执行prepare方法,然后再一次遍历执行队列,对每个元素执行execute方法。prepare方法的作用是整合和重新排列执行队列,execute方法是生成字节码指令。比如我们想要生成如下代码:
int value = 100 + 10; System.out.println(value + 1);
通过ASMSupport生成这段的代码如下:
LocalVariable value = createVariable("value", AClass.INT_ACLASS, false, this.add(Value.value(100), Value.value(10))); invoke(AClassFactory.getProductClass(System.class).getGlobalVariable("out"), "println", add(value, Value.value(1)));
我们可以看到我们分别将100,10,1通过Value.value()方法生成对应jw.asmsupport.definition.value.Value对象。而add方法则返回jw.asmsupport.operators.numerical.arithmetic.Addition对象,createVariable方法返回jw.asmsupport.definition.variable.LocalVariable对象,invoke方法返回jw.asmsupport.operators.method.MethodInvoker。我用列表的形式列出所有对象创建的顺序,顺序是最上面的最先创建。
序号 | ASMSupport代码 | 返回结果类型 | 对应java代码 |
1 | Value.value(100) | jw.asmsupport.definition.value.Value | 数值100 |
2 | Value.value(10) | jw.asmsupport.definition.value.Value | 数值10 |
3 | this.add() | jw.asmsupport.operators.numerical.arithmetic.Addition | 加法操作(100+10) |
4 | createVariable() | jw.asmsupport.definition.variable.LocalVariable | 创建变量,并且将上面的加法操作的结果存入到这个变量value。这里虽然返回的是LocalVariable,但是执行这段代码的时候在内部依然会创建一个jw.asmsupport.operators.variable.LocalVariableCreator对象和jw.asmsupport.operators.assign.Assigner对象,前者的作用是创建一个变量,后者的作用是在JVM层面将变量注册到方法的局部变量中。 |
5 | ...getGlobalVariable("out") | jw.asmsupport.definition.variable.GlobalVariable | 获取System.out |
6 | Value.value(1) | jw.asmsupport.definition.value.Value | 数值1 |
7 | add(value, Value.value(1)) | jw.asmsupport.operators.numerical.arithmetic.Addition | 加法操作:value+1 |
8 | invoke(...) | jw.asmsupport.operators.method.MethodInvoker | 方法调用操作,第一个参数是方法的拥有者,方法名为第二参数,剩下的参数表示所要调用的方法的参数,这里表示System.out.println(value + 1) |
当按照上面的顺序执行完之后,ASMSupport会用一个执行队列(List 类型)保存上面所创建出来的Executeable类型的对象(可见源码:jw.asmsupport.block.ProgramBlock的executeQueue),通过源码可以知道:所有的程序块ProgramBlock及其子类,所有的操作(AbstractOperator的子类),以及创建全局变量操作GlobalVariableCreator和方法的创建MethodCreator是实现了Executeable的方法的。所以我们将会得到如下的执行队列.
对象 | 别名 | 对应代码 |
Addition | add1 | 100 + 10 |
LocalVariableCreator | ||
Assigner | ass1 | |
Addition | add2 | |
MethodInvoker | mi | System.out.println(...) |
至于其他的对象呢,其实通过代码我们可以看到,都传入到相应的操作类当中去了,比如Value.value(100)和Value.value(10)传入到了add1当中去了。当所有对象都已就位之后逐一执行prepare方法,执行之后的执行队列如下:
对象 | 别名 | 对应代码 |
Assigner | ass1 | value = add1 |
MethodInvoker | mi | System.out.println(...); |
为什么执行了prepare之后执行队列会将add1,add2移除呢,是因为prepare遍历这个队列的时候对每个队列中的对象判断时候被其他对象引用,如果被其他的引用了,就将其移除,而将执行execute方法的控制权转交到被引用的对象上去。程序是如何判断是否是被其他操作引用了呢,通过Parameterized接口的asArgument()方法。通过代码可以看到this.add(Value.value(100), Value.value(10))这个代码的返回值是被createVariable方法所引用的。add(value, Value.value(1))是被invoke()方法引用的。
在执行完prepare之后我们再一次遍历这个列表,然后逐一执行execute方法,执行完execute方法之后就生成了字节码了。