字节码插桩
前言
文档来源于视频教学: b站(更多ub项目也在)
收获 -有逼格的技术 -面试吹b -能够操作class文件
-市面上教学内容杂乱不堪,无系统性
字节码插桩=操作class文件 -新增class文件 -修改class文件 -删除class文件
选择ASM库作为视频教学内容
字节码插桩库
ASM:central.sonatype.com/search?q=as…
ASM-原生字节码
尽量避免复杂的字节码相关 将另辟蹊径讲解如何利用ASM操作字节码
注:asm有俩个包: -jdk.internal.org.objectweb.asm jdk自己提供的,需要手动指定包路径,否则打包失败 -org.objectweb 引入asm pom即可
ASM
ASM中核心类:
ClassVisitor:用于操作class
ClassWriter:用于写出byte[]
ClassReader: 用于读class文件
工作流程
-如果只是创建class,则不需要 ClassReader
利用ASM编写简单Demo(提前了解写法) 预期目标 创建一个HelloWorld类并且调用hello方法
public class HelloWorld {
public void hello() {
System.out.println("Hello ASM");
}
}
实现 看不懂不要紧,混个眼熟,回到学jvm字节码的时候
public static void main(String[] args) throws Exception{
String path = "org/xhy/HelloWorld.class";
String filepath = FileUtils.getFilePath(path);
byte[] bytes = dump();
FileUtils.writeBytes(filepath, bytes);
}
public static byte[] dump() throws Exception {
// (1) 创建 ClassWriter 对象
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
// (2) 调用 visitXxx() 方法
cw.visit(V1_8, ACC_PUBLIC , "org/xhy/HelloWorld",
null, "java/lang/Object", new String[]{});
FieldVisitor fieldVisitor;
MethodVisitor methodVisitor;
{
methodVisitor = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
methodVisitor.visitCode();
methodVisitor.visitVarInsn(ALOAD, 0);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
methodVisitor.visitInsn(RETURN);
methodVisitor.visitMaxs(1, 1);
methodVisitor.visitEnd();
}
{
methodVisitor = cw.visitMethod(ACC_PUBLIC, "hello", "()V", null, null);
methodVisitor.visitCode();
methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
methodVisitor.visitLdcInsn("Hello ASM");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
methodVisitor.visitInsn(RETURN);
methodVisitor.visitMaxs(2, 1);
methodVisitor.visitEnd();
}
cw.visitEnd();
return cw.toByteArray();
}
输出字节码的方式
1.IDEA plugins : ASM Bytecode Outline
下载后右键会出现 show Bytecode Outline点击后即可出现
2.ASMPrint
通过ASM提供的API打印信息,暂且不关心该类
描述符
| Java 类型 | ClassFile 描述符 |
|---|---|
| boolean | Z(Z 表示 Zero,零表示 false,非零表示 true) |
| byte | B |
| char | C |
| double | D |
| float | F |
| int | I |
| long | J |
| short | S |
| void | V |
| non-array reference | L; |
| array reference | [ |
对字段描述符的举例:
- boolean flag: Z
- byte byteValue: B
- int intValue: I
- float floatValue: F
- double doubleValue: D
- String strValue: Ljava/lang/String;
- Object objValue: Ljava/lang/Object;
- byte[] bytes: [B
- String[] array: [Ljava/lang/String;
- Object[][] twoDimArray: [[Ljava/lang/Object;
对方法描述符的举例:
- int add(int a, int b): (II)I
- void test(int a, int b): (II)V
- boolean compare(Object obj): (Ljava/lang/Object;)Z
- void main(String[] args): ([Ljava/lang/String;)V
ClassVisitor
ClassVisitor类介绍 field
public abstract class ClassVisitor {
protected final int api; // 指定ASM版本
protected ClassVisitor cv; // 可连接多个ClassVisitor
}
method
常用的方法如下
public abstract class ClassVisitor {
// 访问类
public void visit(
final int version,
final int access,
final String name,
final String signature,
final String superName,
final String[] interfaces);
// 访问字段
public FieldVisitor visitField(
final int access,
final String name,
final String descriptor,
final String signature,
final Object value);
// 访问方法
public MethodVisitor visitMethod(
final int access,
final String name,
final String descriptor,
final String signature,
final String[] exceptions);
// 结束工作
public void visitEnd();
// ......
}
constructors
public abstract class ClassVisitor {
public ClassVisitor(final int api) {
this(api, null);
}
public ClassVisitor(final int api, final ClassVisitor classVisitor) {
this.api = api;
this.cv = classVisitor;
}
}
方法介绍 目标类
public class HelloWorld {
String xhy;
public void hello() {
System.out.println("Hello ASM");
}
}
visit()
- ClassVisitor.visit(int version, int access, String name, String signature, String superName, String[] interfaces)
- version: 修改当前 Class 版本的信息
- access: 修改当前类的访问标识(access flag)信息
- name: 修改当前类的名字
- signature: 修改当前类的泛型信息
- superName: 修改父类
- interfaces: 修改接口信息
visit(V1_8, ACC_PUBLIC , "org/xhy/HelloWorld",
null, "java/lang/Object", new String[]{});
visitField()
- ClassVisitor.visitField(int access, String name, String descriptor, String signature, Object value)
- access: 修改当前字段的访问标识(access flag)信息
- name: 修改当前字段的名字
- descriptor: 修改当前字段的描述符
- signature: 修改当前字段的泛型信息
- value: 修改当前字段的值,若access=0(static)则是常量
visitField(0, "xhy", "Ljava/lang/String;", null, null);
visitMethod()
用于构建方法头信息
- ClassVisitor.visitMethod(int access, String name, String descriptor, String signature, String[] exceptions)
- access: 修改当前方法的访问标识(access flag)信息
- name: 修改当前方法的名字
- descriptor: 修改当前方法的描述符。
- signature: 修改当前方法的泛型信息
- exceptions: 修改当前方法可以招出的异常信息
visitMethod(ACC_PUBLIC, "hello", "()V", null, null);
ClassVisitor中定义方法调用顺序
visit
[visitSource][visitModule][visitNestHost][visitPermittedSubclass][visitOuterClass]
(
visitAnnotation |
visitTypeAnnotation |
visitAttribute
)*
(
visitNestMember |
visitInnerClass |
visitRecordComponent |
visitField |
visitMethod
)*
visitEnd
- []: 表示最多调用一次,可以不调用,但最多调用一次。
- () 和 |: 表示在多个方法之间,可以选择任意一个,并且多个方法之间不分前后顺序。
- *: 表示方法可以调用 0 次或多次。
1.visit() 2.visitField() 3.visitMethod() 4.visitEnd()
MethodVisitor
public final MethodVisitor visitMethod(
final int access,
final String name,
final String descriptor,
final String signature,
final String[] exceptions) {
MethodWriter methodWriter =
new MethodWriter(symbolTable, access, name, descriptor, signature, exceptions, compute);
if (firstMethod == null) {
firstMethod = methodWriter;
} else {
lastMethod.mv = methodWriter;
}
return lastMethod = methodWriter;
}
ClassVisitor.visitorMethod()返回的对象,用于构建方法体
public abstract class MethodVisitor {
public void visitCode();
public void visitInsn(final int opcode);
public void visitIntInsn(final int opcode, final int operand);
public void visitVarInsn(final int opcode, final int var);
public void visitTypeInsn(final int opcode, final String type);
public void visitFieldInsn(final int opcode, final String owner, final String name, final String descriptor);
public void visitMethodInsn(final int opcode, final String owner, final String name, final String descriptor,
final boolean isInterface);
public void visitInvokeDynamicInsn(final String name, final String descriptor, final Handle bootstrapMethodHandle,
final Object... bootstrapMethodArguments);
public void visitJumpInsn(final int opcode, final Label label);
public void visitLabel(final Label label);
public void visitLdcInsn(final Object value);
public void visitIincInsn(final int var, final int increment);
public void visitTableSwitchInsn(final int min, final int max, final Label dflt, final Label... labels);
public void visitLookupSwitchInsn(final Label dflt, final int[] keys, final Label[] labels);
public void visitMultiANewArrayInsn(final String descriptor, final int numDimensions);
public void visitTryCatchBlock(final Label start, final Label end, final Label handler, final String type);
public void visitMaxs(final int maxStack, final int maxLocals);
public void visitEnd();
// ......
}
虽然有这么多方法,其实主要关心的核心方法已经方法调用顺序,因为在方法体当中需要声明变量,调用方法,传形参。
(visitParameter)*
[visitAnnotationDefault]
(visitAnnotation | visitAnnotableParameterCount | visitParameterAnnotation | visitTypeAnnotation | visitAttribute)*
[
visitCode
(
visitFrame |
visitXxxInsn |
visitLabel |
visitInsnAnnotation |
visitTryCatchBlock |
visitTryCatchAnnotation |
visitLocalVariable |
visitLocalVariableAnnotation |
visitLineNumber
)*
visitMaxs
]
visitEnd
方法调用顺序如下:
- 第一步,调用 visitCode() 方法,调用一次。
- 第二步,调用 visitXxxInsn() 方法,可以调用多次。对这些方法的调用,就是在构建方法的“方法体”。
- 第三步,调用 visitMaxs() 方法,调用一次。
- 第四步,调用 visitEnd() 方法,调用一次。
例子:
// 构建方法头
methodVisitor = cw.visitMethod(ACC_PUBLIC, "hello", "()V", null, null);
// 方法体...
methodVisitor.visitCode();
methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
methodVisitor.visitLdcInsn("Hello ASM");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
methodVisitor.visitInsn(RETURN);
// 方法体结束...
methodVisitor.visitMaxs(2, 1);
methodVisitor.visitEnd();
ClassWriter
ClassWriter 继承了 ClassVisitor ,本质也就拥有几个visitxxx方法,不同的是多了toByteArray(),用于将设置好的vist()输出
public class ClassWriter extends ClassVisitor {
/* A flag to automatically compute the maximum stack size and the maximum number of local variables of methods. */
public static final int COMPUTE_MAXS = 1;
/* A flag to automatically compute the stack map frames of methods from scratch. */
public static final int COMPUTE_FRAMES = 2;
// flags option can be used to modify the default behavior of this class.
// Must be zero or more of COMPUTE_MAXS and COMPUTE_FRAMES.
public ClassWriter(final int flags) {
this(null, flags);
}
}
flags可选值
- 0:ASM 不会自动计算 max stacks 和 max locals,也不会自动计算 stack map frames。
- ClassWriter.COMPUTE_MAXS:ASM 会自动计算 max stacks 和 max locals,但不会自动计算 stack map frames。
- ClassWriter.COMPUTE_FRAMES:ASM 会自动计算 max stacks 和 max locals,也会自动计算 stack map frames。
- Stack Map Frames:当前栈的状态以及局部变量表的状态
- Max Stacks: 操作数栈大小
- Max Locals:最大局部变量大小
ClassWriter作用是将调用后的visit()输出为byte[]数组
ClassReader
用于读取class文件
构造方法 最常用的:
- ClassReader(final byte[] classFile)
- ClassReader(final String className)
public ClassReader(final byte[] classFile) {
this(classFile, 0, classFile.length);
}
public ClassReader(
final byte[] classFileBuffer,
final int classFileOffset,
final int classFileLength) { // NOPMD(UnusedFormalParameter) used for backward compatibility.
this(classFileBuffer, classFileOffset, /* checkClassVersion = */ true);
}
public ClassReader(final String className) throws IOException {
this(
readStream(
ClassLoader.getSystemResourceAsStream(className.replace('.', '/') + ".class"), true));
}
public ClassReader(final InputStream inputStream) throws IOException {
this(readStream(inputStream, false));
}
常用方法: getxxx()用于获取class相关信息
public class ClassReader {
public int getAccess() {
return readUnsignedShort(header);
}
public String getClassName() {
// this_class is just after the access_flags field (using 2 bytes).
return readClass(header + 2, new char[maxStringLength]);
}
public String getSuperName() {
// super_class is after the access_flags and this_class fields (2 bytes each).
return readClass(header + 4, new char[maxStringLength]);
}
public String[] getInterfaces() {
// interfaces_count is after the access_flags, this_class and super_class fields (2 bytes each).
int currentOffset = header + 6;
int interfacesCount = readUnsignedShort(currentOffset);
String[] interfaces = new String[interfacesCount];
if (interfacesCount > 0) {
char[] charBuffer = new char[maxStringLength];
for (int i = 0; i < interfacesCount; ++i) {
currentOffset += 2;
interfaces[i] = readClass(currentOffset, charBuffer);
}
}
return interfaces;
}
}
accept() 接收一个 ClassVisitor 类型的参数,因此 accept() 方法是将 ClassReader 和 ClassVisitor 进行连接的“桥梁”。accept() 方法的代码逻辑就是按照一定的顺序来调用 ClassVisitor 当中的 visitXxx() 方法。
public class ClassReader {
// A flag to skip the Code attributes.
public static final int SKIP_CODE = 1;
// A flag to skip the SourceFile, SourceDebugExtension,
// LocalVariableTable, LocalVariableTypeTable,
// LineNumberTable and MethodParameters attributes.
public static final int SKIP_DEBUG = 2;
// A flag to skip the StackMap and StackMapTable attributes.
public static final int SKIP_FRAMES = 4;
// A flag to expand the stack map frames.
public static final int EXPAND_FRAMES = 8;
public void accept(final ClassVisitor classVisitor, final int parsingOptions) {
accept(classVisitor, new Attribute[0], parsingOptions);
}
public void accept(
final ClassVisitor classVisitor,
final Attribute[] attributePrototypes,
final int parsingOptions) {
/*
...
*/
}
}
parsingOptions 参数
在 ClassReader 类当中,accept() 方法接收一个 int 类型的 parsingOptions 参数。
public void accept(final ClassVisitor classVisitor, final int parsingOptions)
parsingOptions 参数可以选取的值有以下 5 个:
- 0
- ClassReader.SKIP_CODE
- ClassReader.SKIP_DEBUG
- ClassReader.SKIP_FRAMES
- ClassReader.EXPAND_FRAMES
推荐使用:
- 在调用 ClassReader.accept() 方法时,其中的 parsingOptions 参数,推荐使用 ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES。
- 在创建 ClassWriter 对象时,其中的 flags 参数,推荐使用 ClassWriter.COMPUTE_FRAMES。
示例代码如下:
ClassReader cr = new ClassReader(bytes);
int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
cr.accept(cv, parsingOptions);
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
为什么我们推荐使用 ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES 呢?因为使用这样的一个值,可以生成最少的 ASM 代码,但是又能实现完整的功能。
- 0:会生成所有的 ASM 代码,包括调试信息、frame 信息和代码信息。
- ClassReader.SKIP_CODE:会忽略代码信息,例如,会忽略对于 MethodVisitor.visitXxxInsn() 方法的调用。
- ClassReader.SKIP_DEBUG:会忽略调试信息,例如,会忽略对于 MethodVisitor.visitParameter()、MethodVisitor.visitLineNumber() 和 MethodVisitor.visitLocalVariable() 等方法的调用。
- ClassReader.SKIP_FRAMES:会忽略 frame 信息,例如,会忽略对于 MethodVisitor.visitFrame() 方法的调用。
- ClassReader.EXPAND_FRAMES:会对 frame 信息进行扩展,例如,会对 MethodVisitor.visitFrame() 方法的参数有影响。
简而言之,使用 ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES 的目的是功能完整、代码少、复杂度低,:
- 不使用 ClassReader.SKIP_CODE,使代码的功能保持完整。
- 使用 ClassReader.SKIP_DEBUG,减少不必要的调试信息,会使代码量减少。
- 使用 ClassReader.SKIP_FRAMES,降低代码的复杂度。
- 不使用 ClassReader.EXPAND_FRAMES,降低代码的复杂度
说这么多不如看俩例子: 这是输出class字节码,其中选择了ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG
public static void print(String className){
int parsingOptions = ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG;
Printer printer = new ASMifier();
PrintWriter printWriter = new PrintWriter(System.out, true);
TraceClassVisitor traceClassVisitor = new TraceClassVisitor(null, printer, printWriter);
try {
new ClassReader(className).accept(traceClassVisitor, parsingOptions);
} catch (IOException e) {
e.printStackTrace();
}
}
输出
去掉ClassReader.SKIP_DEBUG
public static void print(String className){
int parsingOptions = ClassReader.SKIP_FRAMES;
Printer printer = new ASMifier();
PrintWriter printWriter = new PrintWriter(System.out, true);
TraceClassVisitor traceClassVisitor = new TraceClassVisitor(null, printer, printWriter);
try {
new ClassReader(className).accept(traceClassVisitor, parsingOptions);
} catch (IOException e) {
e.printStackTrace();
}
}
平日用打印class的方式写字节码则用 ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG
accpet原理
修改类的信息
1.ClassReader 读取类信息 2.调用visitxxx填充字节码 3.ClassWriter输出byte[]
目标 使 HelloWorld 实现接口
public class HelloWorld{
public HelloWorld() {
}
public void hello() {
System.out.println("Hello ASM");
}
}
如何实现 ClassReader.accept()会接收一个ClassVisitor并且调用该类的visitor()构造class。因此只需要创建类继承ClassVisitor并在visitor中修改类的信息即可 核心代码
public class ClassUpdateInfoVisitor extends ClassVisitor {
private List<String> interfaces = new ArrayList<>();
public void addInterfaces(Class... interfaces) {
for (Class anInterface : interfaces) {
this.interfaces.add(ClassNameTrans.trans(anInterface.getName()));
}
}
public ClassUpdateInfoVisitor(int api, ClassVisitor cw) {
super(api, cw);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, getInterfaces());
}
public String[] getInterfaces() {
return interfaces.toArray(new String[interfaces.size()]);
}
}
全部代码
public static byte[] update() throws Exception {
// (1) 创建 ClassWriter 对象
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
// (2) 调用 visitXxx() 方法
cw.visit(V1_8, ACC_PUBLIC , "org/xhy/HelloWorld",
null, "java/lang/Object", new String[]{});
MethodVisitor methodVisitor;
{
methodVisitor = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
methodVisitor.visitCode();
methodVisitor.visitVarInsn(ALOAD, 0);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
methodVisitor.visitInsn(RETURN);
methodVisitor.visitMaxs(1, 1);
methodVisitor.visitEnd();
}
{
methodVisitor = cw.visitMethod(ACC_PUBLIC, "hello", "()V", null, null);
methodVisitor.visitCode();
methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
methodVisitor.visitLdcInsn("Hello ASM");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
methodVisitor.visitInsn(RETURN);
methodVisitor.visitMaxs(2, 1);
methodVisitor.visitEnd();
}
cw.visitEnd();
return cw.toByteArray();
}
ClassNameTrans 将java.lang.Object转换java/lang/Object
public class ClassNameTrans {
public static String trans(String name){
return name.replace(".","/");
}
}
添加字段
目标 使 HelloWorld 中添加String name 字段 如何实现 visitField是用于操作字段的 access:访问控制修饰符 name:变量名 descriptor:数据类型 signature:泛型 value:常量
@Override
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
return super.visitField(access, name, descriptor, signature, value);
}
核心代码 通过在visitEnd()进行添加字段
为什么不在visitField()添加? 因为这时的操作是在遍历所有的字段,可以在此对遍历到的字段进行操作,而并非添加行为
FieldInfo 只是将字段信息封装了一层,好进行存储
public class ClassUpdateInfoVisitor extends ClassVisitor {
private List<FieldInfo> fieldInfos = new ArrayList<>();
public ClassUpdateInfoVisitor(int api, ClassVisitor cw) {
super(api, cw);
}
@Override
public void visitEnd() {
if(fieldInfos.size() > 0){
for (FieldInfo fieldInfo : fieldInfos) {
super.visitField(fieldInfo.getAccess(),fieldInfo.getName(),fieldInfo.getDescriptor(),fieldInfo.getSignature(),fieldInfo.getValue());
}
}
super.visitEnd();
}
public void addFieldInfos(FieldInfo... fieldInfos) {
for (FieldInfo fieldInfo : fieldInfos) {
this.fieldInfos.add(fieldInfo);
}
}
}
删除字段
目标 删除HellodWorld中的xhy字段
如何实现 visit的工作原理: 依次调用visit进行传输,如果中途返回null,则数据终止 核心代码
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
if (delName.equales(name)){
return null;
}
return super.visitField(access, name, descriptor, signature, value);
}
实际代码 其中通过封装设计提高程序优雅 小tips:将不相关的代码删除提供可读性
public class ClassUpdateInfoVisitor extends ClassVisitor {
private Set<String> delField = new HashSet<>();
public ClassUpdateInfoVisitor(int api, ClassVisitor cw) {
super(api, cw);
}
@Override
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
if (containsDelField(name)){
return null;
}
return super.visitField(access, name, descriptor, signature, value);
}
private boolean containsDelField(String name){
return delField.contains(name);
}
public void addDelField(String... names){
for (String name : names) {
delField.add(name);
}
}
}
修改字段
和删除字段一样的操作,在visitFiled()判断字段如果存在则修改
添加方法
和字段一样
删除方法
和字段一样
修改方法
和字段一样