Javassist学习

Javassist学习

建议先看一下jvm class文件操作码

原文:从零学习Agent内存马 小菜鸡学web

彩笔跟着走一遍,并补充了一些细节。

运行完后,左边栏记得reload from disk

本文jdk版本jdk1.8.0_181
tomcat 8.5.82

1. ASM学习

这一节略读一下。

1. 概念

ASM是一个通用的Java字节码操作和分析框架。它可以直接以二进制形式用于修改现有类或动态生成类。

ASM 本质上是通过字节码修改 class 文件,再以 byte 流的形式写入到本地。

Asm 分为两个部分,Core API(事件模型) 以及 Tree API(对象模型) 。

Tree ApI 是以对象模型为基础进行封装的,顾名思义是以 树状图 来描述一个类,包含多个子节点,例如方法、字段节点等等

Core API 采用了访问者设计模式,其中最主要的有3个类 ClassReader、ClassVisitor、ClassWriter,通过调用 ClassReader 的 accept 方法接收一个访问者,然后通过它去访问类的各个组件,最后以 visitEnd 代表访问结束。ClassWriter 是负责将修改后的内容,重新组合生成新的.class文件。

首先需要学习ASM几个相关的API,主要是 ClassVisitor 、MethodVisitor、FieldVisitor、ClassWriter、ClassReader

在ASM中,通过实现ClassVisitor接口,可以定义自己的字节码操作,而不需要修改原始的类文件。

在这里插入图片描述

ClassVisitor:用于操作class

//这个方法的主要作用是在字节码级别构建或修改一个Java类的信息。
ClassVisitor.visit(int version, int access, String name, String signature, String superName, String[]interfaces)
version: 修改当前 Class 版本的信息
access: 修改当前类的访问标识(access flag)信息
name: 修改当前类的名字signature: 修改当前类的泛型信息
superName: 修改父类
interfaces: 修改接口信息
如:
cw.visit(V1_8, ACC_PUBLIC, "com/haozai/HelloWorld", null, "java/lang/Object", new String[]{});

ClassVisitor.visitField(int access, String name, String descriptor, String signature, Object value)
access: 修改当前字段的访问标识(access flag)信息
name: 修改当前字段的名字
descriptor: 修改当前字段的描述符
signature: 修改当前字段的泛型信息
value: 修改当前字段的值,若access=0(static)则是常量
如:
visitField(0, "num", "Ljava/lang/String;", null, null);

//用于构建方法头信息
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);

MethodVisitor

ClassVisitor.visitMethod(…) 这个方法会返回一个 MethodVisitor对象,利用这个对象可以操作字节码中具体的方法。

visitVarInsn
visitVarInsn方法用于访问局部变量指令,即加载或存储局部变量到操作数栈的指令。它的签名如下:
void visitVarInsn(int opcode, int var);
opcode:操作码,指定了要执行的具体操作,比如ALOADISTORE等。这些操作码对应着Java虚拟机规范中定义的字节码指令。
var:局部变量的索引。在方法内部,局部变量是按照它们被声明的顺序进行索引的,从0开始。对于实例方法,索引为0的变量总是this引用。

visitInsn
visitInsn方法用于访问无操作数指令,即那些不需要额外操作数的指令,比如返回指令。它的签名如下:
void visitInsn(int opcode);
opcode:操作码,指定了要执行的具体无操作数指令,比如RETURNNOP等。

visitMethodInsn
visitMethodInsn方法用于访问方法调用指令,即调用一个方法的字节码指令。它的签名如下:
void visitMethodInsn(int opcode, String owner, String name, String descriptor);
opcode:操作码,指定了方法调用的类型,比如INVOKEVIRTUALINVOKESPECIAL等。这些操作码决定了调用的具体语义,比如是调用实例方法还是静态方法,是调用接口方法还是具体类的方法等。
owner:方法所属类的内部名称。对于实例方法,这是方法所在类的名称;对于静态方法,这也是方法所在类的名称。内部名称使用斜杠/作为分隔符,并且不包含包名。
name:方法的名称。
descriptor:方法的描述符,用于描述方法的参数类型和返回类型。描述符是由类型字符组成的字符串,遵循Java虚拟机规范中定义的格式。

visitFieldInsn ,用于生成字段访问指令的字节码。这个方法用于在方法中访问类的字段,无论是读取字段的值还是写入新的值。
void visitFieldInsn(int opcode, String owner, String name, String descriptor);
opcode:操作码,指定了要执行的字段访问操作类型。常见的操作码有 GETFIELD(用于访问非静态字段)、PUTFIELD(用于设置非静态字段的值)、GETSTATIC(用于访问静态字段)和 PUTSTATIC(用于设置静态字段的值)。
owner:字段所在类的内部名称。内部名称使用斜杠 / 作为分隔符,且不包含包名。例如,java/lang/String 表示 String 类。
name:字段的名称。
descriptor:字段的描述符,用于描述字段的类型。描述符是由类型字符组成的字符串,遵循 Java 虚拟机规范中定义的格式。例如,I 表示 int 类型,Ljava/lang/String; 表示 String 类型。

FieldVisitor

FiedVisitor是用来在访问类的域字节码过程中创建域或者修改域字节码信息的;

visitAnnotation:访问域的注解
public AnnotationVisitor visitAnnotation(final String descriptor, final boolean visible) {  
    if (fv != null) {   
        return fv.visitAnnotation(descriptor, visible);  
    }  
    return null; 
}
其中
descriptor表示注解类的描述,即注解类的描述:如“Ljava/lang/JavaBean”等;
visible表示该注解运行时是否可见;

ClassWriter:用于写出byte[]

他是实现了ClassVisitor里面的方法,但是还可以将修改后的字节码以byte数组写出。

ClassReader: 用于读class文件

用于读取class文件构造方法 最常用的:
ClassReader(final byte[] classFile)
ClassReader(final String className)

accept() 接收一个 ClassVisitor 类型的参数,因此 accept() 方法是将 ClassReaderClassVisitor 进行连接的“桥梁”。
accept() 方法的代码逻辑就是按照一定的顺序来调用 ClassVisitor 当中的 visitXxx() 方法。
在 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.SKIP_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);

他们之间的工作流程:

在这里插入图片描述

2. 生成类

pom.xml添加

<dependency>
    <groupId>org.ow2.asm</groupId>
    <artifactId>asm</artifactId>
    <version>9.6</version>
</dependency>
<dependency>
    <groupId>asm</groupId>
    <artifactId>asm</artifactId>
    <version>3.3.1</version>
</dependency>

testAsmAdd.java

package com.jk.web;

public class testAsmAdd {
    private int num1;
    public testAsmAdd() {
        this.num1=100;
    }
    public int add(int var1, int var2) {
        return var1 + var2 + this.num1;
    }
}

file->settings->plugins下载asm bytecode outline rebooted插件

在这里插入图片描述

然后重启idea,右键->show bytecode outline

在这里插入图片描述

复制下来改造一下

package com.jk.web;
import org.objectweb.asm.*;
import java.io.FileOutputStream;
import java.io.IOException;

public class test1 {
    public static void main(String[] args) {
        try {
            // 调用dump方法生成字节码
            byte[] classBytes = dump();
            // 将字节码写入当前路径下的testAsmAdd.class文件
            FileOutputStream fos = new FileOutputStream("C:\\Users\\21609\\IdeaProjects\\maven1\\testAsmAdd.class");
            fos.write(classBytes);
            System.out.println("字节码已成功写入testAsmAdd.class文件");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 使用ASM库动态生成一个Java类的字节码,并返回该字节码数组。
     *
     * @return 生成的Java类的字节码数组。
     * @throws Exception 如果在生成字节码时发生错误。
     */
    public static byte[] dump() throws Exception {
        // 创建一个ClassWriter,用于生成类的字节码。
        ClassWriter classWriter = new ClassWriter(0);

        // 省略了FieldVisitor, MethodVisitor, AnnotationVisitor等变量的声明,因为它们在后续被直接赋值。

        // 访问类,并设置其版本、访问修饰符、类名、签名、父类名和接口。
        classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER, "com/jk/web/testAsmAdd", null, "java/lang/Object", null);

        // 设置源代码文件名。
        classWriter.visitSource("testAsmAdd.java", null);

        // 定义一个私有整型字段num1。
        {
            FieldVisitor fieldVisitor = classWriter.visitField(Opcodes.ACC_PRIVATE, "num1", "I", null, null);
            fieldVisitor.visitEnd(); // 结束字段的定义。
        }

        // 定义一个公共构造函数。
        {
            MethodVisitor methodVisitor = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
            methodVisitor.visitCode(); // 开始方法体的字节码生成。

            // 标记代码的开始位置,并设置对应的源代码行号。
            Label label0 = new Label();
            methodVisitor.visitLabel(label0);
            methodVisitor.visitLineNumber(5, label0);

            // 加载`this`引用,并调用父类的构造函数。
            methodVisitor.visitVarInsn(Opcodes.ALOAD, 0);
            methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);

            // 标记下一个代码位置。
            Label label1 = new Label();
            methodVisitor.visitLabel(label1);
            methodVisitor.visitLineNumber(6, label1);

            // 将字段num1初始化为100。
            methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); // 加载`this`引用。
            methodVisitor.visitIntInsn(Opcodes.BIPUSH, 100); // 将常量100压入栈。
            methodVisitor.visitFieldInsn(Opcodes.PUTFIELD, "com/jk/web/testAsmAdd", "num1", "I"); // 将100赋值给num1字段。

            // 标记方法体的结束位置。
            Label label2 = new Label();
            methodVisitor.visitLabel(label2);
            methodVisitor.visitLineNumber(7, label2);
            methodVisitor.visitInsn(Opcodes.RETURN); // 从构造函数返回。

            // 标记局部变量表,这对于调试很有用。
            Label label3 = new Label();
            methodVisitor.visitLabel(label3);
            methodVisitor.visitLocalVariable("this", "Lcom/jk/web/testAsmAdd;", null, label0, label3, 0);

            // 设置方法的最大栈深度和局部变量数。
            methodVisitor.visitMaxs(2, 1);
            methodVisitor.visitEnd(); // 结束方法的定义。
        }

        // 定义一个公共的add方法,它接受两个整型参数并返回它们的和加上num1字段的值。
        {
            MethodVisitor methodVisitor = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "add", "(II)I", null, null);
            methodVisitor.visitCode(); // 开始方法体的字节码生成。

            // 标记代码的开始位置,并设置对应的源代码行号。
            Label label0 = new Label();
            methodVisitor.visitLabel(label0);
            methodVisitor.visitLineNumber(9, label0);

            // 加载两个整型参数,并将它们相加。
            methodVisitor.visitVarInsn(Opcodes.ILOAD, 1); // 加载第一个参数。
            methodVisitor.visitVarInsn(Opcodes.ILOAD, 2); // 加载第二个参数。
            methodVisitor.visitInsn(Opcodes.IADD); // 将两个参数相加。

            // 加载`this`引用,并获取num1字段的值。
            methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); // 加载`this`引用。
            methodVisitor.visitFieldInsn(Opcodes.GETFIELD, "com/jk/web/testAsmAdd", "num1", "I"); // 获取num1字段的值。

            // 将前面相加的结果与num1的值再次相加,并返回结果。
            methodVisitor.visitInsn(Opcodes.IADD); // 再次相加。
            methodVisitor.visitInsn(Opcodes.IRETURN); // 返回结果。

            // 标记局部变量表。
            Label label1 = new Label();
            methodVisitor.visitLabel(label1);
            methodVisitor.visitLocalVariable("this", "Lcom/jk/web/testAsmAdd;", null, label0, label1, 0);
            methodVisitor.visitLocalVariable("var1", "I", null, label0, label1, 1); // 第一个参数。
            methodVisitor.visitLocalVariable("var2", "I", null, label0, label1, 2); // 第二个参数。

            // 设置方法的最大栈深度和局部变量数。
            methodVisitor.visitMaxs(2, 3);
            methodVisitor.visitEnd(); // 结束方法的定义。
        }

        // 结束类的定义。
        classWriter.visitEnd();

        // 返回生成的类的字节码数组。
        return classWriter.toByteArray();
    }
}

可以看到基本跟原来一致

在这里插入图片描述

javap -v testAsmAdd.class输出详细的字节码信息

在这里插入图片描述

3. 修改类(AOP实现)

其中ClassWriter是实现了ClassVisitor的,也就是说,这个类都是具体的访问者,和上篇中的顾客的角色是一样的。(b站一共四个视频讲解agent内存马)

ClassReader是读取字节码文件,相当于资源,也就是上面例子中的具体菜品,ClassReader有一个accept方法,接收ClassVisitor(具体的访问者),然后在accept方法中会调用具体的访问者的各种方法。所以在修改类的时候,我们需要做的几步 :

1、使用ClassReader读取具体的class文件

2、自定义实现一个类实现ClassVisitor接口,也就是我们自定义的访问者,然后重写各种方法,比如我们修改方法,我们可以重写visitMethod,然后自定义操作。其中这个类需要传入ClassWriter,然后调用super将cw传到父类使用。

3、使用ClassWriter写出字节码。

下面以实现AOP为例,展示下具体实现修改类的代码。

  1. 如下代码,需要实现在调用Process的时候打印start和end

Base.java编译成class文件

package com.jk.web;

public class Base {
    public void process(){
        System.out.println("process");
    }
}

在这里插入图片描述

  1. 先实现自定义的visitor

MyClassVisitor.java

package com.jk.web;
import org.objectweb.asm.*;

public class MyClassVisitor extends ClassVisitor implements Opcodes {
    public MyClassVisitor(ClassVisitor cv) {
        super(ASM5,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);
    }


    /**
     * access: 修改当前方法的访问标识(access flag)信息
     * name: 修改当前方法的名字
     * descriptor: 修改当前方法的描述符。
     * signature: 修改当前方法的泛型信息
     * exceptions: 修改当前方法可以招出的异常信息
     */
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature,
                exceptions);

        //Base类中有两个方法:无参构造以及process方法,这里不增强构造方法
        if (name.equals("process") && mv != null) {
            //非构造方法
            mv = new MyMethodVisitor(mv);
        }
        return mv;
    }

    class MyMethodVisitor extends MethodVisitor implements Opcodes {
        public MyMethodVisitor(MethodVisitor mv) {
            super( ASM5,mv);
        }

        /**
         * 重写`visitCode`方法,用于在访问类的代码之前执行一些操作。
         */
        @Override
        public void visitCode() {
            super.visitCode();
            // 获取静态字段:System.out
            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitLdcInsn("start"); // 在方法参数列表中添加一个常量:"start"
            //// 调用虚拟方法:System.out.println("start")
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");
        }
        /**
         * 重写`visitInsn`方法,用于在访问字节码指令之前执行一些操作。
         *
         * @param opcode 指令的操作码
         */
        @Override
        public void visitInsn(int opcode) {
            if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)
                    || opcode == Opcodes.ATHROW) {
                // 方法在返回之前,打印"end"
                mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                mv.visitLdcInsn("end");
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");
            }
            mv.visitInsn(opcode);
        }
    }
}
  1. 我们需要使用ClassReader读取字节码

Generator.java

package com.jk.web;
import org.objectweb.asm.*;

import java.io.File;
import java.io.FileOutputStream;

public class Generator {
    public static void main(String[] args) throws Exception {
        //读取
        ClassReader classReader = new ClassReader("com/jk/web/Base");
        ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        //处理
        ClassVisitor classVisitor = new MyClassVisitor(classWriter);
        classReader.accept(classVisitor, ClassReader.SKIP_DEBUG);
        byte[] data = classWriter.toByteArray();
        //输出
        File f = new File("C:\\Users\\21609\\IdeaProjects\\maven1\\target\\classes\\com\\jk\\web\\Base.class");
        FileOutputStream fout = new FileOutputStream(f);
        fout.write(data);
        fout.close();
        System.out.println("now generator cc success!!!!!");
    }
}
  1. 直接运行后,看下生成的字节码文件。

可以看到我们已经成功实现了需求

在这里插入图片描述

4. 添加cmd函数

尝试向Base.class中添加一个cmd函数

我们知道每次对函数操作完都需要调用cw.visitEnd,我们重写visitEnd,然后使用ASM添加cmd函数后,再调用super.visitEnd()。

重新编译一下Base.java

写一个cmd函数编译成class

package com.jk.web;
import java.io.IOException;

public class test2 {
    public void cmd() throws IOException {
            Runtime.getRuntime().exec("calc");
    }
}

使用asm bytecode outline rebooted插件,将Java代码转换成ASM字节码类型代码

package asm.com.jk.web;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.Attribute;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.ConstantDynamic;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.RecordComponentVisitor;
import org.objectweb.asm.Type;
import org.objectweb.asm.TypePath;

public class test2Dump {

    public static byte[] dump() throws Exception {

        ClassWriter classWriter = new ClassWriter(0);
        FieldVisitor fieldVisitor;
        RecordComponentVisitor recordComponentVisitor;
        MethodVisitor methodVisitor;
        AnnotationVisitor annotationVisitor0;

        classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER, "com/jk/web/test2", null, "java/lang/Object", null);

        classWriter.visitSource("test2.java", null);

        {
            methodVisitor = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
            methodVisitor.visitCode();
            Label label0 = new Label();
            methodVisitor.visitLabel(label0);
            methodVisitor.visitLineNumber(5, label0);
            methodVisitor.visitVarInsn(Opcodes.ALOAD, 0);
            methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
            methodVisitor.visitInsn(Opcodes.RETURN);
            Label label1 = new Label();
            methodVisitor.visitLabel(label1);
            methodVisitor.visitLocalVariable("this", "Lcom/jk/web/test2;", null, label0, label1, 0);
            methodVisitor.visitMaxs(1, 1);
            methodVisitor.visitEnd();
        }
        {
            //主要看这里
            methodVisitor = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "cmd", "()V", null, new String[]{"java/io/IOException"});
            methodVisitor.visitCode();
            Label label0 = new Label();
            methodVisitor.visitLabel(label0);
            methodVisitor.visitLineNumber(7, label0);
            methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Runtime", "getRuntime", "()Ljava/lang/Runtime;", false);
            methodVisitor.visitLdcInsn("calc");
            methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Runtime", "exec", "(Ljava/lang/String;)Ljava/lang/Process;", false);
            methodVisitor.visitInsn(Opcodes.POP);
            Label label1 = new Label();
            methodVisitor.visitLabel(label1);
            methodVisitor.visitLineNumber(8, label1);
            methodVisitor.visitInsn(Opcodes.RETURN);
            Label label2 = new Label();
            methodVisitor.visitLabel(label2);
            methodVisitor.visitLocalVariable("this", "Lcom/jk/web/test2;", null, label0, label2, 0);
            methodVisitor.visitMaxs(2, 1);
            methodVisitor.visitEnd();
        }
        classWriter.visitEnd();

        return classWriter.toByteArray();
    }
}

MyClassVisitor.javaalt+insert重写一下visitEnd方法

public void visitEnd() {
        MethodVisitor mv;
        // 在类结束时添加新方法
        mv = cv.visitMethod(ACC_PUBLIC, "cmd", "()V", null, new String[]{"java/io/IOException"});
        mv.visitCode();
        Label l0 = new Label();
        mv.visitLabel(l0);
        mv.visitLineNumber(7, l0);//l0标签源代码在第7行,便于调试定位源代码
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/Runtime", "getRuntime", "()Ljava/lang/Runtime;");
        mv.visitLdcInsn("calc");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Runtime", "exec", "(Ljava/lang/String;)Ljava/lang/Process;");
        mv.visitInsn(POP);
        Label l1 = new Label();
        mv.visitLabel(l1);
        mv.visitLineNumber(8, l1);
        mv.visitInsn(RETURN);
        Label l2 = new Label();
        mv.visitLabel(l2);
//      mv.visitLocalVariable("this", "Lcom/jk/web/test2;", null, l0, l2, 0);
        mv.visitMaxs(2, 1);
        mv.visitEnd();
        super.visitEnd();
}

修改visitMethod方法

@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
    MethodVisitor mv = cv.visitMethod(access, name, desc, signature,
                                      exceptions);

    //Base类中有两个方法:无参构造以及process方法,这里不增强构造方法
    if (name.equals("process") && mv != null) {
        //非构造方法
        mv = new MyMethodVisitor(mv);
        mv.visitVarInsn(ALOAD,0);
        mv.visitMethodInsn(INVOKEVIRTUAL,"com/jk/web/Base","cmd","()V");//
        //上面两行相当于this.cmd()
    }
    return mv;
}

运行一下,成功

在这里插入图片描述

2. Javassist学习

1. 概念

​ Javassist是一个Java库,用于在运行时操作字节码。它提供了一种简单的方法来创建新类,修改现有类和动态加载类。Javassist还提供了用于分析和修改Java字节码的API,以及用于在运行时修改Java类的能力。

在这里插入图片描述

​ 在Javassist中每个需要编辑的class都对应一个CtCLass实例,CtClass的含义是编译时的类(compile time class),这些类会存储在Class Pool中(Class pool是一个存储CtClass对象的容器)。CtClass中的CtField和CtMethod分别对应Java中的字段和方法。通过CtClass对象即可对类新增字段和修改方法等操作了。

在这里插入图片描述

重要的类:

ClassPool:

ClassPool是Javassist的类池,主要用于管理和操作字节码,可以加载.class文件或者是CtClass对象并进行转换。

常用方法:
getDefault(): 获取默认的ClassPool对象。
appendClassPath, insertClassPath : 将一个ClassPath加到类搜索路径的末尾位置 或 插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类的尴尬;
get(String className): 通过类名获取CtClass对象。
makeClass(String className): 创建新的CtClass对象。
makeInterface(String className): 创建新的接口CtClass对象。
makeAnnotation(String className): 创建新的注解CtClass对象。
toClass : 将修改后的CtClass加载至当前线程的上下文类加载器中,CtClass的toClass方法是通过调用本方法实现。

CtClass对象存储了指定类的所有信息,不仅能像反射一样修改属性信息,还能够动态的创造方法修改方法。
// 类库, jvm中所加载的class
ClassPool pool = ClassPool.getDefault();
// 加载一个已知的类, 注:参数必须为全量类名
CtClass ctClass = pool.get("com.test.Student");
// 创建一个新的类, 类名必须为全量类名
CtClass tClass = pool.makeClass("com.test.Calculator");

CtClass:

CtClass提供了类的操作,如在类中动态添加新字段、方法和构造函数、以及改变类、父类和接口的方法。

freeze : 冻结一个类,使其不可修改;
isFrozen : 判断一个类是否已被冻结;
prune : 删除类不必要的属性,以减少内存占用。调用该方法后,许多方法将无法正常使用,慎用;
defrost : 解冻一个类,使其可以被修改。如果事先知道一个类会被defrost, 则禁止调用 prune 方法;
detach : 将该classClassPool中删除;
writeFile : 根据CtClass生成 .class 文件;
toClass : 通过类加载器加载该CtClass

CtField:

类的属性,通过它可以给类创建新的属性,还可以修改已有的属性的类型,访问修饰符等

// 获取已知类的属性
CtField ctField = ctClass.getDeclaredField("name");
// 构建新的类的成员变量
CtField ctFieldNew = new CtField(CtClass.intType,"age",ctClass);
// 设置类的访问修饰符为
publicctFieldNew.setModifiers(Modifier.PUBLIC);
// 将属性添加到类中
ctClass.addField(ctFieldNew);

CtMethod:

​ 类中的方法,通过它可以给类创建新的方法,还可以修改返回类型,访问修饰符等, 甚至还可以修改方法体内容代码。

insertBefore : 在方法的起始位置插入代码;
insterAfter : 在方法的所有 return 语句前插入代码以确保语句能够被执行,除非遇到exception;
insertAt : 在指定的位置插入代码;
setBody : 将方法的内容设置为要写入的代码,当方法被 abstract修饰时,该修饰符被移除;
make : 创建一个新的方法。

setBody()的时候我们使用了一些符号: 
$0=this / $1,$2,$3... 代表方法参数 cons.setBody("{$0.name = $1;}");
// 获取已有方法
CtMethod ctMethod = ctClass.getDeclaredMethod("sayHello");

//创建新的方法, 参数1:方法的返回类型,参数2:名称,参数3:方法的参数,参数4:方法所属的类
CtMethod ctMethod = new CtMethod(CtClass.intType, "calc", new CtClass[]{CtClass.intType,CtClass.intType}, tClass); 
// 设置方法的访问修饰
ctMethod.setModifiers(Modifier.PUBLIC);
// 将新建的方法添加到类中
ctClass.addMethod(ctMethod);
//方法体内容代码 $1代表第一个参数,$2代表第二个参数
ctMethod.setBody("return $1 + $2;");
// 直接创建方法
CtMethod getMethod = CtNewMethod.make("public int getAge() { return this.age;}", ctClass); 
CtMethod setMethod = CtNewMethod.make("public void setAge(int age) { this.age = age;}", ctClass);
ctClass.addMethod(getMethod);
ctClass.addMethod(setMethod);

CtMethod中,会有一些特殊的参数,如我想要在在方法体中,操作参数,我该如何得到参数呢?这时会有一些特殊语法,如下:

$0,$1,$2,…方法参数$0表示this
$1 第一个参数
$2 第二个参数,以此类推
静态方法没有this,所以$0不存在
$args将参数,以Object数组的形式进行封装,相当于new Object[]{$1,$2,…}
$$表示所有实参,例如m($$)等价于m($1,$2,…)
$cflow(…)方法在递归调用时可读取其递归的层次
$r返回结果的类型,用于强制类型转换,如:Object result=…;return ($r)result;
$w用于将基础类型转换成包装类型,如Integer a1=($w)123;
$_设置返回结果$_s=($w)1;相当于return ($w)1;
$sig获取方法中所有参数类型,数组形式展现
$type获取方法结果的Class
$class获取当前方法所在类的class

2. 新增类

pom.xml添加

<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.28.0-GA</version>
</dependency>

​ 需要注意的是,新增完类后,这个类已经存在内存中了,尽管没有实际落地的文件,我们依然可以通过反射或者使用ClassPool对我们创建的类进行实例化。

JavassitTest.java

package com.jk.web;
import javassist.*;

public class JavassitTest {
    public static void main(String[] args) throws Exception
{
        // 获取ClassPool对象
        ClassPool pool = new ClassPool(true);
        // insertClassPath 方法是用来向当前 ClassPool 添加一个新的类路径,
        // 这样 ClassPool 就可以在指定的类路径中查找和读取类文件。
        pool.insertClassPath(new LoaderClassPath(JavassitTest.class.getClassLoader()));
        // 新增Class
        CtClass ctClass = pool.makeClass("Test");
        // 新增Interface
        ctClass.addInterface(pool.get(Test.class.getName()));
        // 要添加的方法的返回值类型
        CtClass type = pool.get(void.class.getName());
        // 方法名称
        String name = "SayHello";
        // 方法参数
        CtClass[] parameters = new CtClass[]{pool.get(String.class.getName())};
        // 方法体,$1是方法的第一个参数
        String body = "{" +
                "System.out.println(\"Hello \" + $1);" +
                "}";
        // 实现方法
        CtMethod ctMethod = new CtMethod(type, name, parameters, ctClass);
        // 设置方法体
        ctMethod.setBody(body);
        //添加方法
        ctClass.addMethod(ctMethod);
        //调用
        // ctClass.toClass()   编译并加载到当前ClassLoader
        Test o = (Test) ctClass.toClass().newInstance();
        o.SayHello("ok ,you are born !!!");
        //写入文件,这一步只是为了查看,生成类的代码
        ctClass.writeFile("C:\\Users\\21609\\IdeaProjects\\maven1");
        //将Test类写入maven1文件夹下
    }
    // 添加Test接口以便于Class的获取等一系列操作
    public interface Test
	{
        void SayHello(String str);
    }
}

在这里插入图片描述

3. 修改类

使用javassist会减少代码量

Base.java参考文章前面的,需要重新编译成class文件

在这里插入图片描述

package com.jk.web;

import com.jk.web.Base;
import javassist.*;

import java.io.IOException;


public class AOPTest {
    public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException {
        // 获取ClassPool对象
        ClassPool pool = new ClassPool(true);
        // insertClassPath 方法是用来向当前 ClassPool 添加一个新的类路径,
        // 这样 ClassPool 就可以在指定的类路径中查找和读取类文件。
        pool.insertClassPath(new LoaderClassPath(AOPTest.class.getClassLoader()));
        //拿到 需要修改的类
        CtClass ctClass = pool.get("com.jk.web.Base");
//        System.out.println(ctClass);
        // 获取已有方法
        CtMethod ctMethod = ctClass.getDeclaredMethod("process");

        //在起始位置插入代码
        String insertBodyBefore = "System.out.println(\"start .. \" );";

        ctMethod.insertBefore(insertBodyBefore);
        //在结束的时候添加代码
        String insertBodyAfter = "System.out.println(\"end .. \");";
        ctMethod.insertAfter(insertBodyAfter);
        ctClass.writeFile(
                "C:\\Users\\21609\\IdeaProjects\\maven1\\target\\classes");

        Base base = new Base();
        base.process();

    }

}

在这里插入图片描述

4. 添加cmd函数

重新编译一下Base.java

修改AOPTest.java

package com.jk.web;

import com.jk.web.Base;
import javassist.*;

import java.io.IOException;


public class AOPTest {
    public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException {
        // 获取ClassPool对象
        ClassPool pool = new ClassPool(true);
        // insertClassPath 方法是用来向当前 ClassPool 添加一个新的类路径,
        // 这样 ClassPool 就可以在指定的类路径中查找和读取类文件。
        pool.insertClassPath(new LoaderClassPath(AOPTest.class.getClassLoader()));
        //拿到 需要修改的类
        CtClass ctClass = pool.get("com.jk.web.Base");
        System.out.println(ctClass);
        addCmd(ctClass);

        // 获取已有方法
        CtMethod ctMethod = ctClass.getDeclaredMethod("process");

        //在起始位置插入代码
        String insertBodyBefore = "this.execCmd(\"calc\");System.out.println(\"start .. \" );";

        ctMethod.insertBefore(insertBodyBefore);
        //在结束的时候添加代码
        String insertBodyAfter = "System.out.println(\"end .. \");";
        ctMethod.insertAfter(insertBodyAfter);
        ctClass.writeFile(
                "C:\\Users\\21609\\IdeaProjects\\maven1\\target\\classes");

        Base base = new Base();
        base.process();

    }
    public static  void addCmd(CtClass ctClass)throws IOException,CannotCompileException{
        CtMethod ctMethod=CtNewMethod.make("public void execCmd(String cmd){" +
                    "Runtime.getRuntime().exec(\"cmd /c \"+cmd);" +
                "}",ctClass);
        ctClass.addMethod(ctMethod);
    }

}

成功!

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

cllsse

富✌您吉祥

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值