Java字节码操纵框架ASM快速入门
ASM 是一个 Java 字节码操纵框架。它可以直接以二进制形式动态地生成 stub 类或其他代理类,或者在装载时动态地修改类。ASM 提供类似于 BCEL 和 SERP 之类的工具包的功能,但是被设计得更小巧、更快速,这使它适用于实时代码插装。
本篇内容使用ASM动态生成java类和方法
在阅读本文之前,需要对JVM有所了解,class文件格式,JVM指令等等
先加入ASM的依赖
-
<dependency>
-
<groupId>org.ow2.asm</groupId>
-
<artifactId>asm-all</artifactId>
-
<version>5.1</version>
-
</dependency>
示例一:
这里打算用ASM动态生成如下的类
-
package com.agent.my3;
-
public class Tester
-
{
-
public void run()
-
{
-
System.out.println("This is my first ASM test");
-
}
-
}
通过javap -s -c Tester 可以看到,class的文件格式(部分)如下:
这里可以看到一些jvm指令,也就是说,jvm执行方法的流程
代码如下:
-
package com.agent.my3;
-
import org.objectweb.asm.ClassWriter;
-
import org.objectweb.asm.MethodVisitor;
-
import org.objectweb.asm.Opcodes;
-
public class ASMGettingStarted
-
{
-
/**
-
* 动态创建一个类,有一个无参数的构造函数
-
*/
-
static ClassWriter createClassWriter(String className)
-
{
-
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
-
//声明一个类,使用JDK1.8版本,public的类,父类是java.lang.Object,没有实现任何接口
-
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, className, null, "java/lang/Object", null);
-
//初始化一个无参的构造函数
-
MethodVisitor constructor = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
-
//这里请看截图
-
constructor.visitVarInsn(Opcodes.ALOAD, 0);
-
//执行父类的init初始化
-
constructor.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
-
//从当前方法返回void
-
constructor.visitInsn(Opcodes.RETURN);
-
constructor.visitMaxs(1, 1);
-
constructor.visitEnd();
-
return cw;
-
}
-
/**
-
* 创建一个run方法,里面只有一个输出
-
* public void run()
-
* {
-
* System.out.println(message);
-
* }
-
* @return
-
* @throws Exception
-
*/
-
static byte[] createVoidMethod(String className, String message) throws Exception
-
{
-
//注意,这里需要把classname里面的.改成/,如com.asm.Test改成com/asm/Test
-
ClassWriter cw = createClassWriter(className.replace('.', '/'));
-
//创建run方法
-
//()V表示函数,无参数,无返回值
-
MethodVisitor runMethod = cw.visitMethod(Opcodes.ACC_PUBLIC, "run", "()V", null, null);
-
//先获取一个java.io.PrintStream对象
-
runMethod.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
-
//将int, float或String型常量值从常量池中推送至栈顶 (此处将message字符串从常量池中推送至栈顶[输出的内容])
-
runMethod.visitLdcInsn(message);
-
//执行println方法(执行的是参数为字符串,无返回值的println函数)
-
runMethod.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
-
runMethod.visitInsn(Opcodes.RETURN);
-
runMethod.visitMaxs(1, 1);
-
runMethod.visitEnd();
-
return cw.toByteArray();
-
}
-
public static void main(String[] args) throws Exception
-
{
-
String className = "com.agent.my3.Tester";
-
byte[] classData = createVoidMethod(className, "This is my first ASM test");
-
Class<?> clazz = new MyClassLoader().defineClassForName(className, classData);
-
clazz.getMethods()[0].invoke(clazz.newInstance());
-
}
-
}
示例二:
这里打算用ASM动态生成如下的类
-
package com.agent.my3;
-
public class Tester
-
{
-
public Integer getIntVal()
-
{
-
return 10;
-
}
-
}
通过javap -s -c Tester 可以看到,class的文件格式(部分)如下:
代码如下:
-
package com.agent.my3;
-
import org.objectweb.asm.ClassWriter;
-
import org.objectweb.asm.MethodVisitor;
-
import org.objectweb.asm.Opcodes;
-
import org.objectweb.asm.Type;
-
public class ASMGettingStarted
-
{
-
/**
-
* 动态创建一个类,有一个无参数的构造函数
-
*/
-
static ClassWriter createClassWriter(String className)
-
{
-
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
-
//声明一个类,使用JDK1.8版本,public的类,父类是java.lang.Object,没有实现任何接口
-
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, className, null, Type.getInternalName(Object.class), null);
-
//初始化一个无参的构造函数
-
MethodVisitor constructor = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
-
//这里请看截图
-
constructor.visitVarInsn(Opcodes.ALOAD, 0);
-
//执行父类的init初始化
-
constructor.visitMethodInsn(Opcodes.INVOKESPECIAL, Type.getInternalName(Object.class), "<init>", "()V", false);
-
//从当前方法返回void
-
constructor.visitInsn(Opcodes.RETURN);
-
constructor.visitMaxs(1, 1);
-
constructor.visitEnd();
-
return cw;
-
}
-
/**
-
* 创建一个返回Integer=10的函数
-
* public Integer getIntVal()
-
* {
-
* return 10;
-
* }
-
* @return
-
* @throws Exception
-
*/
-
static byte[] createRetrurnMethod(String className, int returnValue) throws Exception
-
{
-
//注意,这里需要把classname里面的.改成/,如com.asm.Test改成com/asm/Test
-
ClassWriter cw = createClassWriter(className.replace('.', '/'));
-
//创建get方法
-
//()Ljava/lang/Integer;表示函数,无参数,返回值为:java.lang.Integer,注意最后面的分号,没有就会报错
-
MethodVisitor getMethod = cw.visitMethod(Opcodes.ACC_PUBLIC, "getIntVal", "()Ljava/lang/Integer;", null, null);
-
//将单字节的常量值(-128~127)推送至栈顶(如果不是-128~127之间的数字,则不能用bipush指令)
-
getMethod.visitIntInsn(Opcodes.BIPUSH, returnValue);
-
//调用Integer的静态方法valueOf把10转换成Integer对象
-
String methodDesc = Type.getMethodDescriptor(Integer.class.getMethod("valueOf", int.class));
-
getMethod.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Integer.class), "valueOf", methodDesc, false);
-
//从当前方法返回对象引用
-
getMethod.visitInsn(Opcodes.ARETURN);
-
getMethod.visitMaxs(1, 1);
-
getMethod.visitEnd();
-
return cw.toByteArray();
-
}
-
public static void main(String[] args) throws Exception
-
{
-
String className = "com.agent.my3.Tester";
-
/**
-
* 因为上面方法用的是Opcodes.BIPUSH指令【将单字节的常量值(-128~127)推送至栈顶(如果不是-128~127之间的数字,则不能用bipush指令】
-
* 所以,这里传入的int参数,只能是-127~128
-
* 如果要传入其他的int值,则需要使用其他的jvm指令
-
*/
-
byte[] classData = createRetrurnMethod(className, 10);
-
Class<?> clazz = new MyClassLoader().defineClassForName(className, classData);
-
Object value = clazz.getMethods()[0].invoke(clazz.newInstance());
-
System.out.println(value);
-
}
-
}
注意:示例二里面大量使用了Type类的方法,示例一里面没有使用
-
package com.agent.my3;
-
public class MyClassLoader extends ClassLoader
-
{
-
public MyClassLoader() {
-
super(Thread.currentThread().getContextClassLoader());
-
}
-
public Class<?> defineClassForName(String name, byte[] data) {
-
return this.defineClass(name, data, 0, data.length);
-
}
-
}