这篇继续结合样例来深入了解下Method组件动态变更方法字节码的实现。通过前面一篇,知道ClassVisitor 的visitMethod()方法能够返回一个MethodVisitor的实例。

那么我们也基本能够知道,同ClassVisitor改变类成员一样,MethodVIsistor假设须要改变方法成员,注入逻辑,也能够通过继承MethodVisitor,来编写一个MethodXXXAdapter来实现对于方法逻辑的注入。通过以下的两个样例来介绍下无状态注入和有状态注入方法逻辑的实现。

样例主要參考官方文档介绍,大家依照这个思路能够扩展很多其它种场景的应用。

一、无状态注入

先看一个样例,也是比較常见的一种场景,我们须要给以下这个类的全部方法注入一个计时的逻辑。

源代码例如以下:

package asm.core.methord;

/**
* Created by yunshen.ljy on 2015/6/29.
*/
public class Time {
public void myCount() throws Exception {
int i = 5;
int j = 10;
System.out.println(j - i);
} public void myDeal() {
try {
int[] myInt = { 1, 2, 3, 4, 5 };
int f = myInt[10];
System.out.println(f);
} catch (ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
}
}
}

我们目标的class 字节码例如以下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
// package asm.core.methord; public class Time {
public static long timer; public Time() {
} public void myCount() throws Exception {
timer -= System.currentTimeMillis();
byte i = 5;
byte j = 10;
System.out.println(j - i);
timer += System.currentTimeMillis();
} public void myDeal() {
timer -= System.currentTimeMillis(); try {
int[] e = new int[]{1, 2, 3, 4, 5};
int f = e[10];
System.out.println(f);
} catch (ArrayIndexOutOfBoundsException var3) {
var3.printStackTrace();
} timer += System.currentTimeMillis();
}
}

通过查看字节码结构能够知道,首先我们须要添加一个field给Time类。然后在除了构造器以外的方法注入计时逻辑的字节码。我们先以第一个方法myCount()为例,用javap工具查看字节码信息例如以下:

public void myCount() throws java.lang.Exception;
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=7, locals=3, args_size=1
0: getstatic #18 // Field timer:J
3: invokestatic #24 // Method java/lang/System.currentTimeMillis:()J
6: lsub
7: putstatic #18 // Field timer:J
10: iconst_5
11: istore_1
12: bipush 10
14: istore_2
15: getstatic #28 // Field java/lang/System.out:Ljava/io/PrintStream;
18: iload_2
19: iload_1
20: isub
21: invokevirtual #34 // Method java/io/PrintStream.println:(I)V
24: getstatic #18 // Field timer:J
27: invokestatic #24 // Method java/lang/System.currentTimeMillis:()J
30: ladd
31: putstatic #18 // Field timer:J
34: return
LocalVariableTable:
Start Length Slot Name Signature
10 25 0 this Lasm/core/methord/Time;
12 23 1 i I
15 20 2 j I
LineNumberTable:
line 8: 10
line 9: 12
line 10: 15
line 11: 24
Exceptions:
throws java.lang.Exception

从方法的偏移量0 到 7 是我们的  timer -=System.currentTimeMillis();相应的字节码实现。24 到31 是timer += System.currentTimeMillis();的字节码实现。

基本能够判定,我们须要再方法刚进入的时候先生成timer -= System.currentTimeMillis();的字节码。然后在方法返回return 指令或者是athrow指令之前生成timer+= System.currentTimeMillis()的字节码。

timer +=System.currentTimeMillis()我们能够通过visitCode(方法開始是通过此方法的调用)方法中加入ASM提供的字节码指令生成的几个方法来实现:

@Override
public void visitCode() {
mv.visitCode();
mv.visitFieldInsn(Opcodes.GETSTATIC, owner, "timer", "J");
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitInsn(Opcodes.LSUB);
mv.visitFieldInsn(Opcodes.PUTSTATIC, owner, "timer", "J");
}

timer -=System.currentTimeMillis()须要通过visitInsn(int opcode)方法来完毕,遍历全部的操作码来推断我们当前的指令是否是return 或者athrow 。假设是那么前插入我们须要的指令。再继续调用下一层mv.visitInsn(opcode)。

代码例如以下:

@Override
public void visitInsn(int opcode) {
if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) {
mv.visitFieldInsn(Opcodes.GETSTATIC, owner, "timer", "J");
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitInsn(Opcodes.LADD);
mv.visitFieldInsn(Opcodes.PUTSTATIC, owner, "timer", "J");
}
mv.visitInsn(opcode);
}

那么最后还剩下。须要在class中生成一个timer的属性,如前面ClassVisitor的介绍一样,须要在ClassVisitor 的适配子类中的visitEnd()方法中插入我们的FieldVisitor。

@Override
public void visitEnd() {
if (!isInterface) {
FieldVisitor fv = cv.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "timer", "J", null, null);
if (fv != null) {
fv.visitEnd();
}
}
cv.visitEnd();
}

至此。我们的字节码已经创建和生成完成,为了健壮性考虑。我们仅仅要再加上是否是Interface的推断。由于接口是没有方法实现体的。而且还要推断,构造器方法中不加入timer计时逻辑。这里我们把须要注入逻辑的Class的name通过參数owner传递给MethodVisitor。总体Adapter方法例如以下:

package asm.core.methord;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes; /**
* Created by yunshen.ljy on 2015/6/29.
*/
public class AddTimerAdapter extends ClassVisitor {
private String owner;
private boolean isInterface; public AddTimerAdapter(ClassVisitor cv) {
super(Opcodes.ASM4, cv);
} @Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
cv.visit(version, access, name, signature, superName, interfaces);
owner = name;
isInterface = (access & Opcodes.ACC_INTERFACE) != 0;
} @Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
if (!isInterface && mv != null && !name.equals("<init>")) {
mv = new AddTimerMethodAdapter(mv);
}
return mv;
} @Override
public void visitEnd() {
if (!isInterface) {
FieldVisitor fv = cv.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "timer", "J", null, null);
if (fv != null) {
fv.visitEnd();
}
}
cv.visitEnd();
} class AddTimerMethodAdapter extends MethodVisitor {
public AddTimerMethodAdapter(MethodVisitor mv) {
super(Opcodes.ASM4, mv);
} @Override
public void visitCode() {
// mv.visitCode();
mv.visitFieldInsn(Opcodes.GETSTATIC, owner, "timer", "J");
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitInsn(Opcodes.LSUB);
mv.visitFieldInsn(Opcodes.PUTSTATIC, owner, "timer", "J");
} @Override
public void visitInsn(int opcode) {
if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) {
mv.visitFieldInsn(Opcodes.GETSTATIC, owner, "timer", "J");
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitInsn(Opcodes.LADD);
mv.visitFieldInsn(Opcodes.PUTSTATIC, owner, "timer", "J");
}
mv.visitInsn(opcode);
} @Override
public void visitMaxs(int maxStack, int maxLocals) {
// 手动挡须要计算栈空间,这里两个long型变量的操作须要4个slot
mv.visitMaxs(maxStack + 4, maxLocals);
} }
}

二、有状态注入

这里的有状态是相对于无状态来说的。刚才的样例中是对于方法绝对偏移量的一种逻辑注入。

简单来说就是注入的逻辑不依赖前一个指令的操作或者指令的參数。

对于Class文件的全部方法都是同样的逻辑注入。可是假设考虑一种情况,那就是当前须要注入的字节码指令依赖于前面指令的运行结果状态。那么我们就必须存储前面这个指令的状态。

以下这个样例来源于自官方文档中的举例。考虑例如以下方法:

public void myCount(){
int i = 5;
int j = 10;
System.out.println(j - i);
System.out.println(j + i);
System.out.println(j + 0);
}

这里我们知道j+0 或者j-0 的输出结果都是j。那么假设我们要让上面的代码去掉+0 以及-0这两种操作。也就是须要变成例如以下的方法:

public void myCount(){
byte i = 5;
byte j = 10;
System.out.println(j - i);
System.out.println(j + i);
System.out.println(j);
}

通过查看原方法的字节码信息例如以下:

  0: iconst_5
1: istore_1
2: bipush 10
4: istore_2
5: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
8: iload_2
9: iload_1
10: isub
11: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
14: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
17: iload_2
18: iload_1
19: iadd
20: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
23: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
26: iload_2
27: iconst_0
28: iadd
29: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
32: return

能够发现iadd 指令是iconst_0 的后置指令。可是我们不能单纯得推断当前字节码指令时iadd或者iconst_0 就直接remove。当然remove的实现方式MethodVisitor 同ClassVisitor的适配器实现方式相近,都是通过不继续调用mv.visitInsn(opcode);方法的方式。但这里我们须要标记iconst_0指令的状态。iconst_0指令运行时标记一个状态,在下一条指令运行的时候推断状态值,假设下一条命令是iadd那么就直接return掉方法来移除指令。官方实现很的优雅,这里加了一些凝视。方便理解实现。

  

package asm.core.methord;

import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes; /**
* Created by yunshen.ljy on 2015/7/1.
*/
public class RemoveAddZeroAdapter extends MethodVisitor {
private static int SEEN_ICONST_0 = 1;
protected final static int SEEN_NOTHING = 0;
protected int state;
public RemoveAddZeroAdapter(MethodVisitor mv) {
super(Opcodes.ASM4, mv);
} @Override
public void visitInsn(int opcode) {
// 是否检測到前一个指令是ICONST_0
if (state == SEEN_ICONST_0) {
// 而且当前指令时iadd
if (opcode == Opcodes.IADD) {
// 又一次初始化指令状态
state = SEEN_NOTHING;
// 移除指令序列
return;
}
}
visitInsn();
// 假设当前指令是ICONST_0 记录指令状态。而且直接返回(移除)
if (opcode == Opcodes.ICONST_0) {
state = SEEN_ICONST_0;
return;
}
// 继续訪问下一条指令
mv.visitInsn(opcode);
} protected void visitInsn() {
// 假设最后訪问的是SEEN_ICONST_0指令,那么还原指令(由于刚才被移除了)
if (state == SEEN_ICONST_0) {
mv.visitInsn(Opcodes.ICONST_0);
}
state = SEEN_NOTHING;
} }

这里再补充一下,我们不须要处理StacckMapFrame以及像前一部分须要计算局部变量表和操作数栈的size,那是由于我们没有添加额外的属性,而且演示样例中也没有无条件跳转语句等,须要验证的操作。但假设我们要实现更复杂的情况,还须要覆盖visitMaxs方法、visitFrame visitLable方法等。

(保证移除指令不会影响其它指令的正常跳转。须要调用visitInsn()方法)

事实上我个人认为。处理有状态的字节码指令移除、加入、转移还是须要注意各种字节码指令的情况。

字节码指令的顺序,上下文,栈信息对于编写一段健壮的ASM 逻辑注入代码都很关键。

有时候事实上还是建议先去把注入前后情况的class文件分析一遍,再进行编码。

ASM(四) 利用Method 组件动态注入方法逻辑的更多相关文章

  1. 利用java反射动态调用方法,生成grid数据

    项目中需要java后台查询并组装前台grid的数据,数据行数不定,数据行定义不定,开始用了最原始的方法,写了几百行,就是前台需要什么字段后台拼接什么字段,java代码冗余量非常大,并且不够灵活,一旦前 ...

  2. 利用Mono.Cecil动态修改程序集来破解商业组件(仅用于研究学习)

    原文 利用Mono.Cecil动态修改程序集来破解商业组件(仅用于研究学习) Mono.Cecil是一个强大的MSIL的注入工具,利用它可以实现动态创建程序集,也可以实现拦截器横向切入动态方法,甚至还 ...

  3. [ASP.NET MVC] 利用动态注入HTML的方式来设计复杂页面

    原文:[ASP.NET MVC] 利用动态注入HTML的方式来设计复杂页面 随着最终用户对用户体验需求的不断提高,实际上我们很多情况下已经在按照桌面应用的标准来设计Web应用,甚至很多Web页面本身就 ...

  4. {Django基础十之Form和ModelForm组件}一 Form介绍 二 Form常用字段和插件 三 From所有内置字段 四 字段校验 五 Hook钩子方法 六 进阶补充 七 ModelForm

    Django基础十之Form和ModelForm组件 本节目录 一 Form介绍 二 Form常用字段和插件 三 From所有内置字段 四 字段校验 五 Hook钩子方法 六 进阶补充 七 Model ...

  5. Java 扫描实现 Ioc 动态注入,过滤器根据访问url调用自定义注解标记的类及其方法

    扫描实现 Ioc 动态注入 参考: http://www.private-blog.com/2017/11/16/java-%e6%89%ab%e6%8f%8f%e5%ae%9e%e7%8e%b0-i ...

  6. C# 知识点笔记:IEnumerable<>的使用,利用反射动态调用方法

    IEnumerable<T>的使用 创建一个IEnumerable对象 List<string> fruits = new List<string> { " ...

  7. AX2012 XppCompiler create method动态创建方法并运行

    在用友系列的软件中经常可以看到可配置的计算公式,AX中其实也有可配置的公式,如call center呼叫中心的欺诈Fraud rule的配置,AX后台可以根据配置规则,变量,条件来动态产生方法并执行, ...

  8. iOS中动态注入JavaScript方法。动态给html标签添加事件

    项目中有这样一种需求,给html5网页中图片添加点击事件,并且弹出弹出点击的对应的图片,并且可以保持图片到本地 应对这样的需求你可能会想到很多方法来实现. 1. 最简单的方法就是在html5中添加图片 ...

  9. 反射中的一个问题点:利用Method执行main方法特殊的地方

    利用Method执行main方法 问题: 启动Java程序的main方法的参数是一个字符串数组,即public static void main(String[] args),通过反射方式来调用这个m ...

随机推荐

  1. Exception: Operation xx of contract xx specifies multiple request body parameters to be serialized without any wrapper elements.

    Operation 'CreateProductCodeStock' of contract 'IChileService' specifies multiple request body param ...

  2. Windows cannot find ". Make sure you typed the name correctly, and then try again

    https://answers.microsoft.com/en-us/windows/forum/windows_10-desktop/windows-10-explorerexe-error-wi ...

  3. Lesson 2 Building your first web page: Part 2

    Tag Diagram You may have noticed that HTML tags come in pairs; HTML has both an opening tag (<tag ...

  4. 37.微信跳一跳辅助开发(C语言+EasyX)

    一.开发环境 开发环境 使用语言:C/C++ IDE:VS2010+ 其他三方库 EasyX(http://www.easyx.cn/downloads/) ADB(链接:https://pan.ba ...

  5. OpenGL常见错误之——glut.h文件的函数无法正常连接

    glut.h文件的函数无法正常连接,典型的错误如下:------ 已启动生成: 项目: gears, 配置: Debug Win32 ------1>正在链接...1>GEARS.obj ...

  6. img下面的留白解决

    在做网页的时候经常会出现一个令人困惑的现象.那就是行内元素和块级元素之间会出现“留白”.就是块级元素中明明只有一个行内元素,但行内元素却不会铺满块级元素.像这个例子: “留白”出现的原因 行内元素默认 ...

  7. [Parcel] Bundle a React App with Parcel

    Parcel comes in as the new cool kid in the bundlers world. Unlike other bundlers which take lots of ...

  8. css3中关于伪类的使用

    目标: css中after伪类,last-child伪类的使用.以及部分css3的属性. 过程: 在制作导航时.常常会遇到在每个li后面加入一个切割符号,到最后一个元素的时候,切割符就会去掉的一种效果 ...

  9. BZOJ 1231 状压DP

    思路: f[i][j] i表示集合的组成 j表示选最后一个数 f[i][j]表示能选的方案数 f[i|(1<< k)][k]+=f[i][j]; k不属于i j属于i且符合题意 最后Σf[ ...

  10. svg(1) path路径

    注: 笔记来自于http://www.jb51.net/html5/72250.html  以及http://blog.csdn.net/u013291076/article/details/2707 ...