这篇继续结合样例来深入了解下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. sc命令以及InstallUtil安装service

    1.安装 https://stackoverflow.com/questions/8164859/install-a-windows-service-using-a-windows-command-p ...

  2. Android View体系(十)自定义组合控件

    相关文章 Android View体系(一)视图坐标系 Android View体系(二)实现View滑动的六种方法 Android View体系(三)属性动画 Android View体系(四)从源 ...

  3. Python(九) Python的高级语法与用法

    本章节我们揭开Python进阶部分的高级特性,详细讲解枚举.闭包,并对函数式编程做出介绍 一. 枚举其实是一个类 from enum import Enum #枚举类 class VIP(Enum): ...

  4. MyEclipse的代码自动提示功能

     一般默认情况下,Eclipse ,MyEclipse的代码提示功能是比Microsoft Visual Studio的差很多的,主要是Eclipse ,MyEclipse本身有很多选项是默认关闭的, ...

  5. Svn备份与Bandizip压缩批处理程序

    目的:为了定时备份多个svn仓库数据,使用批处理程序进行备份并Bandizip进行压缩保存到指定位置,操作完成后弹出成功提示. 为了完成以上目标,需要了解以下几个方面: 批处理命令 Svn命令 Ban ...

  6. 第四次python作业——叶耀宗

    设计题2: 1.参考“三国演义”词频统计程序,实现对红楼梦出场人物的频次统计.2.(可选)将红楼梦出场人物的频次统计结果用词云显示. import jieba excludes = {"什么 ...

  7. CentOS下安装.net core环境并部署WebAPI

    1.安装CentOS 7 2.安装.net Core 2环境,参考官方文档:(建议采用SDK (tar.gz)安装) https://www.microsoft.com/net/download/li ...

  8. QT就是别人好心帮你做一些枯燥,并且很重复的代码编写工作,让你更好的把精力投入到你界面的逻辑和功能的实现的功能库(否则写了上万行代码了,才写出个BUG一大堆的毛坯)

    好了,现在开始记录我学习QT的学习历程 . 本人也不是计算机专业出来的,自学了一点,但还是不好找工作,于是参加了培训,虽然感觉没多学到什么 编程的学习生涯就是不断的看别人的源码,然后自己参考着写写自己 ...

  9. js---16继承

    123 instanceof Number;//false,要左边是对象右边是函数 typeof 123 ; //number new Number(123) instanceof Number; / ...

  10. js---10时间类

    <!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-Type" content ...