【每天学习一点点:字节码增强】class字节码结构、ASM生成新的class字节码、javassist操作字节码、Instrument类库运行时加载类、Agent注入到JVM、JPDA接口attach

在这里插入图片描述

一、class字节码结构

我们都知道java是一个跨平台的开发语言,其主要核心在于class字节码,也就是java在编译后会生成对应的class字节码文件,会通过类加载器将class字节码加载到jvm运行空间中。

在编写一个如下java文件:

public class SimpleJava {


    public static void main(String[] args) {
        int a = 0;
        int b = 2;
        System.out.println(a + b);
    }
}

可以通过javap -c可以对class进行反汇编,其实class文件就是一个 十六进制的数据。
在这里插入图片描述

在class文件中主要存储了java类的魔数 cafebabe 以及对应的版本号,通过javap -c可以进行反汇编。

在这里插入图片描述
可以看到这里的信息,code就是方法区,我们的字节码增强也就是增强方法区的代码。

方法区里面都是一些指令。
iconst_0 也就是一个入栈指令,其作用是用来将int类型的数字、取值在-1到5之间的整数压入栈中。
istore_1 将数据存入到本地变量表中的第一个变量
iconst_2 将2进入入栈
istore_2 将2存入本地变量表中

在这里插入图片描述

二、ASM生成新的class字节码

ASM是一个Java字节码操作库。它允许Java开发人员在不借助Java编译器的情况下,直接以二进制形式操作Java类文件,并且可以动态生成或修改类的内容。ASM提供了一组API来访问和操作Java字节码指令。

import org.objectweb.asm.*;

public class MyClassGenerator {
  public static void main(String[] args) {
    String className = "com.example.MyClass";
    ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
    int access = Opcodes.ACC_PUBLIC;
    String superClassName = "java/lang/Object";
    String[] interfaces = new String[0];
    cw.visit(Opcodes.V1_8, access, className, null, superClassName, interfaces);

    cw.visitField(Opcodes.ACC_PRIVATE, "field", "Ljava/lang/String;", null, null).visitEnd();

    MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "myMethod", "()V", null, null);
    mv.visitCode();
    mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
    mv.visitLdcInsn("Hello, world!");
    mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
    mv.visitInsn(Opcodes.RETURN);
    mv.visitMaxs(0, 0);
    mv.visitEnd();

    byte[] code = cw.toByteArray();
    ClassLoader classLoader = MyClass.class.getClassLoader
    

上述代码就是使用asm进行生成对应的class文件,不过其需要掌握指令级别,难度有点大,因此有javassit框架来进行生成或修改class文件,通过源码的方式。

ASM能够更加精细操作字节码,而javassist提供了更好容易操作的api,如果要实现性能要求较高则需要使用asm框架。

三、javassist

Javassist(Java Programming Assistant)是一个开源的 Java 字节码编辑工具库,它可以动态地修改已有的类和生成新的类。与 ASM 等其它字节码操作库不同,Javassist 主要通过修改 Java 类的源代码来进行字节码操作,因此更加易于使用和学习。Javassist 提供了一组 API 来访问和操作 Java 类的属性、方法、构造函数等元素,并提供了丰富的模板功能来简化 Java 类的创建和修改过程。Javassist 通常被广泛应用于 Java 框架和应用程序中,例如 Hibernate、Spring、Tomcat 等。

3.1 核心对象

  1. CtClass: 代表了一个 Java 类的字节码。它提供了一组 API 来访问和修改 Java 类的结构和属性,并支持动态创建、修改和加载 Java 类。
  2. ClassPool : 用于管理 CtClass 对象,每个 ClassPool 实例都维护了一个字节码池,其中包含了所有已加载的 CtClass 对象。通过创建 ClassPool 实例,可以方便地访问和修改 CtClass 的属性和方法。
  3. CtMethod: 用来创建类的方法
  4. CtField: 用于创建字段的对象

3.2 创建一个类

public static void main(String[] args) throws NotFoundException, CannotCompileException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, IOException {
        // 1. 获得classPool 创建 ccClass
        ClassPool classPool = ClassPool.getDefault();
        CtClass ctClass = classPool.makeClass("aop.JavassistMakeClass");
        ctClass.setSuperclass(classPool.get("java.lang.Object"));
        // 2. 为ctClass 添加 sayHi方法
        CtMethod sayHiMethod = new CtMethod(CtClass.voidType, "sayHi", new CtClass[0], ctClass);
        sayHiMethod.setBody("{System.out.println(\" say hi \");}");
        ctClass.addMethod(sayHiMethod);
        // 3. 加载到jvm中
        ctClass.writeFile();
        Class<?> aClass = ctClass.toClass();
        Object o = aClass.newInstance();
        Method sayHi = aClass.getMethod("sayHi");
        sayHi.invoke(o);
    }

3.3 动态修改一个类

public static void main(String[] args) throws NotFoundException, CannotCompileException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, IOException {
        // 0. 先加载一个SimpleJava
        SimpleJava simpleJava = new SimpleJava();
        // 1. 获得classPool 获得  simpleJavaClass
        ClassPool classPool = ClassPool.getDefault();
        CtClass ctClass = classPool.getCtClass("aop.SimpleJava");

        // 2. 获得方法
        CtMethod sayHello = ctClass.getDeclaredMethod("sayHello");
        sayHello.insertBefore("System.out.println(\"say hi\");\r\n");

        ctClass.writeFile();
        ctClass.writeFile(); // 写入磁盘
        Class<?> clazz = ctClass.toClass(); // 动态加载类
        Object obj = clazz.newInstance();
        clazz.getMethod("sayHello").invoke(obj);

    }
  

但是这边运行的时候会出现一个错误。
Caused by: java.lang.ClassFormatError: loader (instance of sun/misc/Launcher$AppClassLoader): attempted duplicate class definition for name: “aop/SimpleJava”
也就是javassist不能这么简单地修改运行的class文件,不然在defineClass的时候,因为全路径是相同的,因此会出现重复定义了。

3.4 通过指令修改class

在3.3 中我们使用了beforeInsert ,就是在方法前进行插入,当然我们也可以修改对应的方法的代码。这样就需要利用到class字节码的javap -c中的code区域的指令集,也就是说我们方法在运行的时候,会被编译成指令进行操作,因此我们可以修改对应的指令来改变行为。

四、Instrument库动态修改class

Instrument 是 Java 中的一个 API,用于在运行时修改字节码并对 Java 类进行重定义、重新转换等操作,但它本身并不提供创建新类的方法。它提供了一组方法,可以让开发人员在不停止 JVM 的情况下修改已加载的类,并使得新的代码与老代码兼容。

使用 Instrument API 可以实现很多有趣的功能,例如:
在运行时对程序的字节码进行增强或修补,以增加程序的功能或改善性能;
在运行时添加或删除类的字段、方法或接口,以满足特定的需求;
在运行时动态生成新的类,并将其加载到 JVM 中

在实现instrument只需要在代理类实现ClassFileTransformer中的transform方法,并且返回新的class文件。

public class InstrumentTest implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws Exception {
    if(!className.equals("aop.SimpleJava")){
    	    return null;  // 不对其他类进行操作
    }
        // 1. 获得classPool 重载类
        ClassPool classPool = ClassPool.getDefault();
        CtClass ctClass = classPool.makeClass("aop.SimpleJava");
        ctClass.setSuperclass(classPool.get("java.lang.Object"));
        // 2. 为ctClass 添加 sayHi方法
        CtMethod sayHiMethod = new CtMethod(CtClass.voidType, "sayHi", new CtClass[0], ctClass);
        sayHiMethod.setBody("{System.out.println(\" say hi \");}");
        ctClass.addMethod(sayHiMethod);
        return ctClass.toBytecode();
    }
}

但是这里我们写出了Instrument的实现类,没有注入到jvm中,因此需要一个agent进行注入。

4.2 redefineClass 重新覆盖类

在这里插入图片描述

五、Agent注入Instrument

Agent被注入到jvm中,有两种方式,每一种方式的实现方式不一致,但是原理类似。

  1. 实现premain,在jvm启动时通过-javaagent 加载代理程序进行指定
  2. 实现agentmain , 在jvm运行时进行自动调用。

5.1 -agentlib方式

"javaagent"是Java提供的一种机制,用于在JVM启动时加载并运行Java代理程序。

具体来说,可以使用"-javaagent:agent.jar"命令行参数来指定要加载的Java代理程序,并在其中实现"premain"方法。在JVM启动时,会自动加载该代理程序,并调用其中的"premain"方法。在"premain"方法中,可以通过Instrumentation接口对应用程序的类进行重定义和增强等操作。

需要注意的是,通过"javaagent"加载的Java代理程序必须打包成一个独立的JAR文件,并且其中必须包含一个名为"premain"的公共静态方法,其签名必须为:

public static void premain(String agentArgs, Instrumentation inst);
  1. 实现instrument

public class LogInstrument  implements ClassFileTransformer {


    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {



        if(!className.equals("com/nt/auth/controller/openApi/A")){
            return null;
        }
        System.out.println("找到对应注入类,进行生成");
        try {
            // 使用指定的类加载器创建 ClassPath 对象
            ClassPath classPath = new LoaderClassPath(loader);
            ClassPool classPool = new ClassPool();
            classPool.insertClassPath(classPath);
            CtClass ctClass = classPool.get("com.nt.auth.controller.openApi.A");
            System.out.println("生成对应类:" + ctClass);
            CtMethod[] declaredMethods = ctClass.getDeclaredMethods();
            System.out.println("declaredMethods:" + declaredMethods.length);
            for(CtMethod ctMethod : declaredMethods){
                System.out.println("ctMethod" + ctMethod);
                // 判断是否有参数
                CtClass[] parameterTypes = ctMethod.getParameterTypes();
                System.out.println("parameterTypes" + parameterTypes);
                if(parameterTypes.length != 0) {
                    System.out.println("有参数");
                    int index = 1;
                    for(CtClass param : parameterTypes){
                        ctMethod.insertBefore("System.out.println(\"注入对应日志代理,解析传入参数信息:\" + " +  "$" + index + ".toString());\r\n");
                        index++;
                    }
                } else {
                    System.out.println("没有参数");
                    ctMethod.insertBefore("System.out.println(\"注入对应日志代理,当前参数为0\");");
                }
            }

            return ctClass.toBytecode();
        } catch (Exception e){
            return null;
        }

    }
}
  1. 实现preMain的agent
public class MyAgent {

    public static void premain(String agentArgs, Instrumentation inst){
        inst.addTransformer(new LogInstrument());
    }
}
  1. 实现MANIFEST.MF
Premain-Class: com.agent.test.MyAgent
Can-Redefine-Classes: true

ÅÅ

  1. 打成jar

在这里插入图片描述
5. 在目标应用中使用
-javaagent:agent.jar

在这里插入图片描述
6. 如果遇到错误,是使用maven打包,需要添加如下配置

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-jar-plugin</artifactId>
      <version>3.2.0</version>
      <configuration>
        <archive>
          <manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile>
        </archive>
      </configuration>
    </plugin>
  </plugins>
</build>

  1. 演示,我在注入了一个日志提醒,打印出入参参数
      ctMethod.insertBefore("System.out.println(\"注入对应日志代理,解析传入参数信息:\" + " +  "$" + index + ".toString());\r\n")

在这里插入图片描述
8. 注意点

  1. transform.方法的入参的className的形式为 A/b/c ,不是a.b.c,不然会找不到
  2. 在进行classpool的时候,通过get(a.b.c)的形式
  3. 如果修改了已存在的class的方法,则不需要再执行ctClass.addMethod
  4. 如果打包后,成功启动了应用,但是不显示对应的注入点的代码,那是因为你的javassist代码写错了,写错了不会抛出异常,只会不执行。比如 需要一个打印的功能
    ctMethod.insertBefore(“System.out.println(“注入对应日志代理,当前参数为0”)”);
    上述代码存在错误,因为没有加“;”导致,代码出现异常,没有注入成功,但是也不会抛出异常。

5.2 通过agentmain的形式,在运行时注入

agentmain 注入是一种 Java 技术,它允许在运行时通过Java Agent将字节码注入到正在运行的 JVM 中的方法中。它可以用于实现一些特定的功能,比如性能监控、调试、代码注入等。
在这里插入图片描述

5.2.1 实现方式

  1. 编写agentmain
public class MyAgentMain {

    public static void premain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException {
        inst.addTransformer(new LogInstrument());
    }
		// 入口方法
    public static void agentmain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException {
        premain(agentArgs, inst);
    }
}
  1. 在META-INF中METAFEST.MF

Manifest-Version: 1.0
Agent-Class: com.agent.test.MyAgentMain
Can-Retransform-Classes: true

  1. 打成jar

  2. 启动目标应用

  3. 获得应用进程
    jps -v

  4. 通过命令注入到jvm

jcmd <pid> java.lang.instrumentation.loadAgent /path/to/agent-loader.jar
  1. 通过java代码
   public static void main(String[] args) throws AttachNotSupportedException, IOException, AgentLoadException, AgentInitializationException {
        // 传入目标 JVM pid
        VirtualMachine vm = VirtualMachine.attach("6785");
        vm.loadAgent("/path/to/agent-loader.jar");
    }

5.4 注意事项

  1. java字节码修改重载是需要添加如下代码
    Class<?> aClass = Class.forName(“com.nt.auth.controller.openApi.xx”);
    inst.retransformClasses(aClass);
    这样就要人有问了,为啥premain不需要加,因为premain是使用-agentlib命令,在jvm启动的时候进行加载的,这样其他所有的class都是会去加载,而如果运行时注入对应的instrument则会对重新加载的class生效,如果想对之前的进行生效,则需要使用retransformClasses
  2. 需要再MAINFEST.MF添加: Can-Retransform-Classes: true
  3. 需要在目标应用开启JPDA功能:(本地运行不需要)
    -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值