这篇继续结合样例来深入了解下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. 洛谷P1586 四方定理

    题目描述 四方定理是众所周知的:任意一个正整数nn ,可以分解为不超过四个整数的平方和.例如:25=1^{2}+2^{2}+2^{2}+4^{2}25=12+22+22+42 ,当然还有其他的分解方案 ...

  2. Cisco交换机端口安全

    Cisco交换机端口安全       通过端口设置,可以限制允许访问交换机上某个端口的MAC地址以及IP(可选)来实现严格控制对该端口的输入,最终确保网络接入安全.配置网络安全时应该注意如下问题: 1 ...

  3. javaScript 原型与原型链学习笔记

    javaScript中,原型是常用到一种方式,它能降低储存占用,写出更高效的代码 原型常用到的则是prototype属性 JavaScript prototype 属性 定义和用法 prototype ...

  4. 三种new

    http://www.cnblogs.com/zhuyf87/archive/2013/03/23/2976714.html 第一种是正常的new,它是语言内建的,不能重载. new operator ...

  5. ThinkPHP数据分页Page.class.php

    获取分页类 ThinkPHP提供了数据分页的扩展类库Page,能够在http://www.thinkphp.cn/extend/241.html下载,或者下载官方的完整扩展包(http://www.t ...

  6. oculus rift 开发(一)

    我使用的是oculus rift DK2,也就是开发人员版.在開始使用前.要确保几件事: 一.准备工作 1.pc配置.配置越好执行越流畅.反之会使帧率低.伴随的体验就是眩晕恶心,甚至无法执行. 官方推 ...

  7. android framework 02

    Android底层开发1.安装Ubuntu系统2.Ubuntu配置开发环境: sudo apt-get install git-core gnupg flex bison gperf zip sudo ...

  8. adb-端口被占用解决办法(win)

    今天在window下使用adb的时候,提示我说5037端口被占用. * daemon not running. starting it now on port * ADB server didn't ...

  9. tomcat指定配置文件路径方法

    1.在catalina.sh 中设置JAVA_OPTS,例如: JAVA_OPTS='-server -Xms1024m -Xmx1024m -XX:NewSize=128m -XX:MaxPermS ...

  10. Kinect 开发 —— 全息图

    Kinect的另一个有趣的应用是伪全息图(pseudo-hologram).3D图像可以根据人物在Kinect前面的各种位置进行倾斜和移动.如果方法够好,可以营造出3D控件中3D图像的效果,这样可以用 ...