04-字节码增强技术分析与实践

简介

字节码增强技术相当于是一把打开运行时JVM的钥匙,利用它可以对现有字节码进行修改或者动态生成新的字节码,进而对运行中的程序做修改,实现热部署。也可以跟踪JVM运行中程序的状态,进行性能诊断等。

此外,我们平时使用的动态代理、AOP也与字节码增强密切相关,它们实质上还是利用各种手段生成符合规范的字节码文件。综上所述,掌握字节码增强后可以高效地定位并快速修复一些棘手的问题(如线上性能问题、方法出现不可控的出入参需要紧急加日志等问题),也可以在开发中减少冗余代码,大大提高开发效率。

ASM技术

概述

对于需要手动操纵字节码的需求,可以使用ASM,它可以直接生产 .class字节码文件,也可以在类被加载入JVM之前动态修改类行为。ASM的应用场景有AOP(Cglib就是基于ASM)、热部署、修改其他jar包中的类等。当然,涉及到如此底层的步骤,实现起来也比较麻烦。

API说明

ASM Core API不需要把这个类的整个结构读取进来,就可以用流式的方法来处理字节码文件。好处是非常节约内存,但是编程难度较大。然而出于性能考虑,一般情况下编程都使用Core API。在Core API中有以下几个关键类:

  1. ClassReader:用于读取已经编译好的.class文件。
  2. ClassWriter:用于重新构建编译后的类,如修改类名、属性以及方法,也可以生成新的类的字节码文件。
  3. ClassVisitor:ASM中对于字节码文件中不同的区域有不同的Visitor,比如用于访问方法的MethodVisitor、用于访问类变量的FieldVisitor、用于访问注解的AnnotationVisitor等。为了实现AOP,重点要使用的是MethodVisitor。

字节码增强

第一步:在项目中添加asm依赖,例如:

        <dependency>
            <groupId>org.ow2.asm</groupId>
            <artifactId>asm</artifactId>
            <version>9.1</version>
        </dependency>

第二步:定义需要被增强的ResourceService类:其中只包含一个handle()方法,方法内输出一行“hangle”。代码如下:

package com.java.jvm.bytecode.asm;

public class ResourceService {
    public void handle(){
        System.out.println("handle()");
    }
}

第三步:定义ResourceClassVisitor类,用于对字节码进行visit以及修改,实现字节码增强,增强后,我们期望的是,方法执行前输出“start”,之后输出”end”。例如:

package com.java.jvm.bytecode.asm;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class ResourceClassVisitor extends ClassVisitor implements Opcodes {

    public ResourceClassVisitor(ClassVisitor cv) {
        super(Opcodes.ASM9, cv);//ASM9表示你使用用的asm api的版本
    }
    /**访问类基本信息*/
    @Override
    public void visit(int version, int access, String name,
                      String signature, String superName, String[] interfaces) {
        this.cv.visit(version, access, name, signature, superName, interfaces);
    }

    /**访问方法基本信息*/
    @Override
    public MethodVisitor visitMethod(int access, String name,
                                     String desc, String signature, String[] exceptions) {
        MethodVisitor mv = this.cv.visitMethod(access, name, desc,
                signature, exceptions);
        //假如不是构造方法,我们构建方法的访问对象(MethodVisitor)
        if (!name.equals("<init>") && mv != null) {
            mv = new ResourceClassVisitor.MyMethodVisitor((MethodVisitor)mv);
        }

        return (MethodVisitor)mv;
    }

    /**自定义方法访问对象*/
    class MyMethodVisitor extends MethodVisitor implements Opcodes {

        public MyMethodVisitor(MethodVisitor mv) {
            super(Opcodes.ASM9, mv);
        }
        /**此方法会在方法执行之前执行*/
        @Override
        public void visitCode() {
            super.visitCode();
            this.mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out",
                    "Ljava/io/PrintStream;");
            this.mv.visitLdcInsn("start");
            this.mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream",
                    "println", "(Ljava/lang/String;)V", false);
        }
        /**对应方法体本身*/
        @Override
        public void visitInsn(int opcode) {
            //在方法return或异常之前,添加一个end输出
            if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) {
                this.mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out",
                        "Ljava/io/PrintStream;");
                this.mv.visitLdcInsn("end");
                this.mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream",
                        "println", "(Ljava/lang/String;)V", false);
            }
            this.mv.visitInsn(opcode);
        }
    }

}


第四步:执行字节码增强,例如:

package com.java.jvm.bytecode.asm;

import com.java.jvm.bytecode.service.ResourceService;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

/**
 * 类生成器对象,基于这个类将ClassReader,ClassVisitor,ClassWriter组装在一起,
 * 然后修改类,并生成新的类
 */
public class ResourceClassGenerator {
    public static void main(String[] args) throws Exception {
        //第一步:构建ClassReader对象,读取指定位置的class文件(默认是类路径-classpath)
        ClassReader classReader =
                new ClassReader("com/java/jvm/bytecode/service/ResourceService");
        //第二步:构建ClassWriter对象,基于此对象创建新的class文件

        //ClassWriter.COMPUTE_FRAMES 表示ASM会自动计算max stacks、max locals和stack map frame的具体内容。
        //ClassWriter.COMPUTE_MAXS 表示ASM会自动计算max stacks和max locals,但不会自动计算stack map frames。
        ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);//推荐使用COMPUTE_FRAMES

        //第三步:构建ClassVisitor对象,此对象用于接收ClassReader对象的数据,并将数据处理后传给ClassWriter对象
        ClassVisitor classVisitor = new ResourceClassVisitor(classWriter);

        //第四步:基于ClassReader读取class信息,并将数据传递给ClassVisitor对象
        //这里的参数ClassReader.SKIP_DEBUG表示跳过一些调试信息等,ASM代码看上去就会更简洁
        //这里的参数ClassReader.SKIP_FRAMES表示跳过一些方法中的部分栈帧信息,栈帧手动计算非常复杂,所以交给系统去做吧
        classReader.accept(classVisitor,
                ClassReader.SKIP_DEBUG|ClassReader.SKIP_FRAMES);//推荐用这两个参数

        //第五步:从ClassWriter拿到数据,并将数据写出到一个class文件中
        byte[] data = classWriter.toByteArray();

        //构建内存中的字节码对象
//      String className="com.java.jvm.bytecode.service.ResourceService";
//      Class aClass = new AsmClassLoader().defineClassFromByteCodes(className, data);
//      System.out.println(aClass);
//      Object obj= aClass.newInstance();
//      Method handle = aClass.getDeclaredMethod("handle");
//      handle.invoke(obj);

        //将字节码写入到磁盘的class文件
        File f = new File("01-java/target/classes/com/java/jvm/bytecode/service/ResourceService.class");
        FileOutputStream fout = new FileOutputStream(f);
        fout.write(data);
        fout.close();

        ResourceService rs = new ResourceService();
        rs.handle();//start,handle(),end
    }

    //自定义类加载器(可选)
    static class AsmClassLoader extends  ClassLoader{

        public Class defineClassFromByteCodes(String className,byte[] byteCodes){
            return  defineClass(className, byteCodes, 0, byteCodes.length);
        }
    }

}

Asm辅助工具

利用ASM手写字节码时,需要利用一系列visitXXXXInsn()方法来写对应的助记符,所以需要先将每一行源代码转化为一个个的助记符,然后通过ASM的语法转换为visitXXXXInsn()这种写法。第一步将源码转化为助记符就已经够麻烦了,不熟悉字节码操作集合的话,需要我们将代码编译后再反编译,才能得到源代码对应的助记符。第二步利用ASM写字节码时,如何传参也很令人头疼。ASM社区也知道这两个问题,所以提供了工具ASM ByteCode Outline。

安装后,选择java类,然后右键选择“Show Bytecode Outline”,在新标签页中选择“ASMified”这个tab,就可以看到这个类中的代码对应的ASM写法了。图中上下两个红框分别对应AOP中的前置逻辑于后置逻辑,将这两块直接复制到visitor中的visitMethod()以及visitInsn()方法中,就可以了。
在这里插入图片描述

课后练习

基于ASM技术在类路径下创建一个HelloJVMInterface接口对应的class文件,我们的目标
是创建一个如下接口对应的class,例如:

package com.java.jvm;
public interface HelloJVMInterface{
   public static final int a=-1;
   void doSay(Object obj);
}

基于ASM技术创建HelloJVMInterface.class,例如:

package com.java.jvm.bytecode.asm;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

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

/**
 *
 * 思考:
 * 1)所有class文件中的内容是什么? byte[]
 * 2)如何创建byte[]数组?ClassWriter
 * 3)字节数组byte[]中的内容是什么?接口信息、属性信息、方法信息。
 * 4)如何构建接口信息、属性信息、方法信息?FieldVisitor,MethodVisitor等对象
 */
public class HelloJVMInterfaceGenerator {
    public static void main(String[] args) throws Exception {
        // (1) 生成byte[]内容
        byte[] bytes = generatorBytes();
        // (2) 保存byte[]内容到文件(class文件)
        File f = new File("01-java/target/classes/com/java/jvm/HelloJVMInterface.class");
        FileOutputStream fout = new FileOutputStream(f);
        fout.write(bytes);
        fout.close();
        //(3)加载类获取类的字节码对象(可选,检测类是否可以被加载到内存,语法是否有问题)
        Class<?> clazz = Class.forName("com.java.jvm.HelloJVMInterface");
        System.out.println(clazz);
    }

    public static byte[] generatorBytes() throws Exception {
        // (1) 创建ClassWriter对象(此对象可以将类中信息写入到一个字节数组)
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        // (2) 调用visitXxx()方法(创建类或接口)
        //创建接口类型信息 (public interface com.java.jvm.HelloJVMInterface extends Cloneable{} )
        cw.visit(
                Opcodes.V1_8,                                        // version
                Opcodes.ACC_PUBLIC
                        +Opcodes. ACC_ABSTRACT + Opcodes.ACC_INTERFACE,   // access
                "com/java/jvm/HelloJVMInterface",                         // name
                null,                                        // signature
                "java/lang/Object",                          // superName
                new String[]{"java/lang/Cloneable"}                                         // interfaces
        );
        //创建接口内部属性 (public static final int a=-1)
        FieldVisitor fv1 = cw.visitField(Opcodes.ACC_PUBLIC +
                        Opcodes.ACC_FINAL + Opcodes.ACC_STATIC,
                "x", "I", null, -1);
        fv1.visitEnd();

        //创建接口内部方法(public abstract int say(Object obj);
        MethodVisitor mv1 = cw.visitMethod(Opcodes.ACC_PUBLIC +
                Opcodes.ACC_ABSTRACT, "say",
                "(Ljava/lang/Object;)V",
                null, null);
        mv1.visitEnd();
        cw.visitEnd();

        // (3) 调用toByteArray()方法
        return cw.toByteArray();
    }

}


Javassist技术

概述

Javassist是一个用于分析、编辑和创建Java字节码的类库,相比ASM在指令层次上操作字节码会更加简单直观。可以无须关注字节码刻板的结构,直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构或者动态生成类。

API说明

Javassist中最重要的是ClassPool、CtClass、CtMethod、CtField这四个类:

  1. CtClass(compile-time class):编译时类信息,它是一个class文件在代码中的抽象表现形式,可以通过一个类的全限定名来获取一个CtClass对象,用来表示这个类文件。基于CtClass可以实现对类的操作,如在类中动态添加新字段、方法和构造函数以及改变类、父类和接口的方法。

  2. ClassPool:ClassPool对象是一个CtClass对象的容器。一个CtClass对象被构建后,它被记录在ClassPool中。从开发视角来看,ClassPool是一张保存CtClass信息的HashTable,key为类名,value为类名对应的CtClass对象。当我们需要对某个类进行修改时,就是通过pool.getCtClass(“className”)方法从pool中获取到相应的CtClass。

  3. CtField:对应类的属性,通过它可以给类创建新的属性,还可以修改已有属性的类型,访问修饰符等。

  4. CtMethod:对应类中的方法,通过它可以给类创建新的方法,还可以修改返回类型,访问修饰符等,甚至还可以修改方法体内容。

ClassPool应用分析:

/ 类库, jvm中所加载的class
ClassPool pool = ClassPool.getDefault();
// 加载一个已知的类, 注:参数必须为全量类名
CtClass ctClass = pool.get("com.java.jvm.Point");
// 创建一个新的类, 类名必须为全量类名
CtClass tClass = pool.makeClass("com.java.jvm.HelloService");

CtField应用分析:

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

CtMethod应用分析:

 // 获取已有方法
 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);

CtConstructor 应用分析

获取已有构造方法,参数为构造方法的参数类型数组
CtConstructor ctConstructor=ctClass.getDeclaredConstructor(new CtClass[]{});
创建新的构造方法
CtConstructor ctConstructor=
new CtConstructor(new CtClass[]{CtClass.intType},ctClass)
ctConstructor.setModifiers(Modifier.PUBLIC)
ctConstructor.setBody(“this.age=$1;”)
ctClass.addConstructor(ctConstructor);
也可以直接创建构造函数
ctConstructor=CtNewConstructor.make(
“public Student(int age){this.age=age}”,ctClass);

字节码增强

第一步:添加Javassist依赖,例如:

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

第二步:代码增强,例如:

我们接下来写一个小案例,展示Javassist简单、快速的特点。我们依旧是对ResourceService中的handle()方法做增强,在方法调用前后分别输出”start”和”end”,实现代码如下。我们需要做的就是从pool中获取到相应的CtClass对象和其中的方法,然后执行method.insertBefore和insertAfter方法,参数为要插入的Java代码,再以字符串的形式传入即可。例如:

package com.java.jvm.bytecode.javassist;

import com.java.jvm.bytecode.asm.ResourceService;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

public class JavassistTests {
    public static void main(String[] args)throws Exception {
        //获取CtClass对象的容器
        ClassPool cp = ClassPool.getDefault();
        //从容器中获取ResourceService的类型对象
        CtClass cc =   
cp.get("com.java.jvm.bytecode.asm.ResourceService");
        //获取类型中的CtMethod对象
        CtMethod m = cc.getDeclaredMethod("handle");
        //在方法前插入代码
        m.insertBefore("{ System.out.println(\"start\"); }");
        //在方法后插入代码
        m.insertAfter("{ System.out.println(\"end\"); }");
        //获取类的字节码对象
        Class c = cc.toClass();
        //输出类的字节码文件(可选)
        cc.writeFile("d:/classes/");
        //基于字节码创建对象实例
        ResourceService rs = (ResourceService)c.newInstance();
        rs.handle();
    }
}

第三步:问题分析
我们在JavassistTests的main()方法中的第一行先添添加如下代码:

ResourceService rs=new ResourceService ();

也就是在增强前就先让JVM加载ResourceService类,然后在执行到c.toClass()方法,此时可能会抛出如下错误,例如:
在这里插入图片描述
错误中告诉我们,JVM默认是不允许在运行时动态重载(重新加载)一个类的。那么,如果只能在类加载前对类进行强化,那字节码增强技术的使用场景就变得很窄了。我们期望的效果是在一个持续运行,并已经加载了所有类的JVM中,还能利用字节码增强技术,对其中的类行为做替换并重新加载,这个过程我们有时通常称之为热替换。

Java Agent技术

简介

Java Agent是Java Instrumentation API的一部分,它提供了向现有已编译的Java类添加字节码的功能,相当于字节码插桩的入口。可以侵入运行在JVM上的应用程序,进而修改应用程序中各种类的字节码。

Java Agent 这个技术出现在 JDK1.5 之后,,我们平时用的很多工具,都是基于 Java Agent 实现的,例如常见的热部署 JRebel,各种线上诊断工具(Btrace, Greys),还有阿里开源的 Arthas。总之,在分布式链路追踪中为了获取服务之间调用链信息,采集器(也叫做探针)通常需要在方法的前后做埋点。而Java Agent技术就是实现埋点的一种方式。

Java Agent的启动方式和普通 Jar 包有所不同,对于普通的Jar包,通过指定类的 main 函数进行启动,但是 Java Agent 并不能单独启动,必须依附在一个 Java 应用程序运行。

我们可以使用 Agent 技术构建一个独立于应用程序的代理程序,用来协助监测、运行甚至替换其他 JVM 上的程序,使用它可以实现虚拟机级别的 AOP 功能。

API说明

Java Instrumentation API主要提供了两个接口:Instrumentation、ClassFileTransformer。

  1. Instrumentation:该类提供了向Java中插入代码的服务
  2. ClassFileTransformer:顾名思义,该类就是类文件转换器接口,Java Agent提供该接口的实现,用于转换类文件,接口中的transform()方法会在类文件被加载时调用,而在transform方法里,我们可以利用上文中的ASM或Javassist对传入的字节码进行改写或替换,生成新的字节码数组后返回。

java.lang.instrument包的具体实现,依赖于JVMTI(Java Virtual Machine Tool Interface),JVMTI是一套由Java虚拟机提供的,为JVM相关工具提供的本地变成接口集合。JVMTI 提供了一套”代理”程序机制,可以支持第三方工具程序以代理的方式连接和访问 JVM,并利用 JVMTI 提供的丰富的编程接口,完成很多跟 JVM 相关的功能。

字节码增强实践

Instrument以及Javassist对指定业务对象进行功能增强,例如:

第一步:创建业务service类,将此类作为字节码增强对象,例如:

package com.java.jvm.bytecode.service;

public class CycleService {
    public void doCycle(){
       System.out.println("doCycle()");
    }
}


第二步:创建Transformer对象,例如:

创建Transformer对象,用于对CycleService对象进行功能增强,例如:

package com.java.jvm.bytecode.instrument;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class DefaultClassTransformer implements ClassFileTransformer {
   
  @Override
    public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
 byte[] classfileBuffer) throws IllegalClassFormatException {
        System.out.println("Transforming " + className);
        try {
            ClassPool cp = ClassPool.getDefault();
            CtClass cc = cp.get("com.java.jvm.bytecode.service.CycleService");
            CtMethod m = cc.getDeclaredMethod("doCycle");
            m.insertBefore("{ System.out.println(\"start\"); }");
            m.insertAfter("{ System.out.println(\"end\"); }");
            byte[] bytes = cc.toBytecode();
            return bytes;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

第三步:创建Agent对象

创建Agent对象,用于调用DefaultClassTransformer对象执行字节码增强,在Agent中可定义两个方法进行不同时间点进行增强:

  1. premain方法,此方法在main方法执行之前执行。(方法声明固定写法)
  2. agentmain方法,此方法在main方法启动后,也就时候程序运行时进行执行。(方法声明固定写法)
package com.java.jvm.bytecode.instrument;

import com.java.jvm.bytecode.service.CycleService;

import java.lang.instrument.Instrumentation;

/**
 * 定义Agent对象
 */
public class DefaultAgent {
    /**假如你希望在main方法执行之前执行,就这样定义方法*/
    public static void premain(String args, Instrumentation inst){
        System.out.println("premain->"+args);
        inst.addTransformer(new DefaultClassTransformer(),true);
    }
    /**
     * 这种方式是要以attach的方式进行载入,然后在java程序启动后执行。
     * @param args
     * @param inst
     */
    public static void agentmain(String args, Instrumentation inst){
        System.out.println("agentmain->"+args);
        inst.addTransformer(new DefaultClassTransformer(),true);
        try {
            //指明哪些类需要重新加载
            inst.retransformClasses(CycleService.class);
        }catch (Exception e){
            System.out.println("agent error");
        }
    }
}



第四步:添加maven插件用于对项目进行打包,例如:

<plugins>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
            <source>8</source>
            <target>8</target>
        </configuration>
    </plugin>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <version>3.2.0</version>
        <configuration>
            <archive>
                <manifest>
                    <addClasspath>true</addClasspath>
                </manifest>
                <manifestEntries>
                    <Premain-Class>com.java.jvm.bytecode.instrument.DefaultAgent</Premain-Class>
                    <Agent-Class>com.java.jvm.bytecode.instrument.DefaultAgent</Agent-Class>
                    <Can-Redefine-Classes>true</Can-Redefine-Classes>
                    <Can-Retransform-Classes>true</Can-Retransform-Classes>
                </manifestEntries>
            </archive>
        </configuration>
    </plugin>
</plugins>

依赖中,Premain-Class和Agent-Class用于指定代理类,Can-Redefine-Classes 是否需要重新定义所有类,默认为false,可选。Can-Retransform-Classes 是否需要retransform,默认为false,可选。依赖添加后,先执行maven clean对原有类文件进行清除,然后执行maven package对项目进行打包。

说明,当一个代理jar包中的manifest文件中,既有Premain-Class又有Agent-Class时,如果以命令行方式在VM启动前指定代理jar,则使用Premain-Class;反之如果在VM启动后,动态添加代理jar,则使用Agent-Class。

第五步:创建CycleServiceTests类,对CycleService对象进行调用,也就是启动服务,假如需要在此服务启动之前进行增强,可以考虑在idea的vm参数中添加-javaagent:E:\TCGBIV\DEVCODES\CSDNCODES\cgb2202codes\01-java\target\01-java-1.0-SNAPSHOT.jar,这里的jar位

package com.java.jvm.bytecode;

import com.java.jvm.bytecode.service.CycleService;

import java.lang.management.ManagementFactory;

public class CycleServiceTests {
    public static void main(String[] args) {
        String name = ManagementFactory.getRuntimeMXBean().getName();
        String s = name.split("@")[0];
        //打印当前Pid
        System.out.println("pid:"+s);
        CycleService cs=new CycleService();
        while(true) {
            try {
                cs.doCycle();
                Thread.sleep(3000);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}

说明,假如此类的运行是命令行执行,可以通过java -javaagent:xxx.jar CycleServiceTests方式进行实现。

第六步:添加tools依赖

程序启动之后,通过VirtualMachine 的 attach api加载 Java Agent,这组 api 其实是 JVM 进程之间的的沟通桥梁,底层通过socket 进行通信,JVM A 可以发送一些指令给JVM B,B 收到指令之后,可以执行对应的逻辑,比如在命令行中经常使用的 jstack、jps 等,很多都是基于这种机制实现的。VirtualMachine 在jdk的lib下面的tools.jar中,如果不在classpath的话,要加进去。可通过添加依赖的方式进行tools的依赖配置。

<dependency>
    <groupId>com.sun</groupId>
    <artifactId>tools</artifactId>
    <version>1.8</version>
    <scope>system</scope>
    <systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>

第七步:创建AgentInstrumentTests类

package com.java.jvm.bytecode;

import com.sun.tools.attach.VirtualMachine;

import java.lang.management.ManagementFactory;

public class AgentInstrumentTests {
    public static void main(String[] args) throws Exception{
        //传入目标 JVM pid(这里的id为CycleServiceTests类中打印的进程id)
        VirtualMachine vm = VirtualMachine.attach("4328");
        vm.loadAgent("E:/TCGBIV/DEVCODES/CGB2112CODES/01-java/target/01-java-1.0-SNAPSHOT.jar");
    }
}


第八步:分别运行CycleServiceTests、AgentInstrumentTests类进行测试。

总结(Summary)

本章节重点讲解了为什么要进行字节码增强以及常用的字节码增强技术。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

青木编码

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值