天天看點

java位元組碼生成架構及使用示例1 位元組碼生成架構2. 使用代碼示例

目前Java位元組碼生成架構大緻有ASM、Javassist和byte buddy

BECL不常用,ByteKit是Arthas中使用的位元組碼工具集

1 位元組碼生成架構

1.1 ASM

1.1.1 介紹

ASM官網 介紹如下:

ASM是一個通用的Java位元組碼操作和分析架構。它能夠以二進制形式修改已有的類或是動态生成類。

ASM提供了一些常見的位元組碼轉換和分析算法,可以根據這些算法建構定制的複雜轉換和代碼分析工具。

ASM提供了與其他Java位元組碼架構類似的功能,但側重于性能。因為它的設計和實作是盡可能的小和快,是以它非常适合在動态系統中使用(當然也可以在靜态方式中使用,例如在編譯器中)

源碼 ,目前最新版本是9.2

1.1.2 典型案例

ASM在許多項目中使用,如實作動态代理的CGLIB,包括但不限于如下:

  • the OpenJDK, to generate the lambda call sites, and also in the Nashorn compiler,
  • the Groovy compiler and the Kotlin compiler,
  • Cobertura and Jacoco, to instrument classes in order to measure code coverage,
  • CGLIB, to dynamically generate proxy classes (which are used in other projects such as Mockito and EasyMock),
  • Gradle, to generate some classes at runtime.
  • Arthas(阿爾薩斯)Arthas增強功能的核心是Enhancer和AdviceWeaver這兩個類,對方法進行Aop織入,達到watch,trace等效果。

1.2 Javassist

javassist是jboss的一個子項目,其主要的優點,在于簡單,而且快速。

源碼, 目前最新版本是:3.28.0-GA

Javassist (Java程式設計助手)使Java位元組碼操作變得簡單。它是一個用于在Java中編輯位元組碼的類庫; 它使Java程式能夠在運作時定義一個新類,并在JVM加載類檔案時修改類檔案。

與其他類似的位元組碼編輯器不同,Javassist提供了兩個級别的API:

  • 源代碼級别: 可以編輯類檔案,而不需要了解Java位元組碼的規範。整個API直接使用java編碼的形式進行設計。甚至可以以源文本【source text】的形式指定插入的位元組碼;Javassist動态地編譯它。
  • 位元組碼級别:位元組碼級API允許使用者像其他編輯器一樣直接編輯類檔案。

1.3 bytebuddy

1.3.1 介紹

Byte Buddy是一個代碼生成和操作庫,用于在Java應用程式運作時建立和修改Java類,無需編譯器的幫助。除了Java類庫附帶的代碼生成實用程式外,Byte Buddy允許建立任意類,而且不限于實作建立運作時代理的接口。此外,Byte Buddy提供了一個友善的API,可以使用Java代理或在建構過程中手動更改類。

要使用Byte Buddy,不需要了解Java位元組代碼或類檔案格式。相比之下,Byte Buddy的API旨在編寫簡潔且易于了解的代碼。

API被設計成盡可能非侵入性,是以,Byte Buddy不會在它建立的類中留下任何痕迹。是以,生成的類可以存在,而不需要類路徑上的Byte Buddy。因為這個特性,Byte Buddy的吉祥物被選為幽靈,如下圖。

java位元組碼生成架構及使用示例1 位元組碼生成架構2. 使用代碼示例

Byte Buddy是用Java 5編寫的,但支援為任何Java版本生成類。Byte Buddy是一個輕量級的庫,隻依賴于Java位元組代碼解析器庫ASM的通路者API, ASM本身不需要任何進一步的依賴。

1.3.2 典型案例

bytebuddy在許多項目中使用,包括但不限于如下:

  • Apache SkyWalking: 分布式系統的應用程式性能監視工具,用來增強agent的監控點的類,實作性能監控名額。

1.4 BCEL

Commons BCEL

位元組代碼工程庫(Apache Commons BCEL™)旨在為使用者提供一種友善的方法來分析、建立和操作(二進制)Java類檔案(以.class結尾的檔案)。類由對象表示,對象包含給定類的所有符号資訊:特别是方法、字段和位元組代碼指令。

這些對象可以從現有檔案中讀取,由程式轉換(例如,運作時的類加載器),然後再次寫入檔案。一個更有趣的應用程式是在運作時從頭建立類。如果您想了解Java虛拟機(JVM)和Java .class檔案的格式,位元組代碼工程庫(BCEL)可能也很有用。

BCEL包含一個名為JustIce的位元組代碼驗證器,它通常提供比标準JVM消息更好的關于代碼錯誤的資訊。

目前的版本是:

<dependency>
    <groupId>org.apache.bcel</groupId>
    <artifactId>bcel</artifactId>
    <version>6.5.0</version>
</dependency>
           

1.5 ByteKit

ByteKit:bytecode kit for java,在Arthas中使用的位元組碼工具集,目标是:

  • 之前的Arthas裡的位元組碼增強,是通過asm來處理的,代碼邏輯不好修改,了解困難
  • 基于ASM提供更高層的位元組碼處理能力,面向診斷/APM領域,不是通用的位元組碼庫
  • ByteKit期望能提供一套簡潔的API,讓開發人員可以比較輕松的完成位元組碼增強
功能 函數Enter/Exit注入點 綁定資料 inline 防止重複增強 避免裝箱/拆箱開銷 origin調用替換

@ExceptionHandler

ByteKit

@AtEnter

@AtExit

@AtExceptionExit

@AtFieldAccess

@AtInvoke

@AtInvokeException

@AtLine

@AtSyncEnter

@AtSyncExit

@AtThrow

this/args/return/throw

field

locals

子調用入參/傳回值/子調用異常

line number

ByteBuddy

@OnMethodEnter

@OnMethodExit

@OnMethodExit#onThrowable()

this/args/return/throw

field

locals

傳統AOP

Enter

Exit

Exception

this/args/return/throw

2. 使用代碼示例

2.1 asm示例

實作後的效果僅僅是記錄方法的執行時間:

public static void show() {
        long l = System.currentTimeMillis();
        System.out.println("Hello World");
        long l2 = System.currentTimeMillis() - l;
        System.out.println("\u65b9\u6cd5\u6267\u884c\u8017\u65f6\u662f:" + l2 + " ms");
    }
           

執行結果是:

Hello World

方法執行耗時是:0 ms

源碼示例

import com.alibaba.bytekit.utils.AgentUtils;
import com.alibaba.bytekit.utils.Decompiler;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;

import java.io.IOException;
import java.lang.instrument.UnmodifiableClassException;

class ASMDemo {
    public static void main(String[] args) throws IOException, UnmodifiableClassException {
        ClassReader cr = new ClassReader(User.class.getName());
        ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
        ClassVisitor cv = new MyClassVisitor(cw);
        cr.accept(cv, Opcodes.ASM5);
        // 擷取生成的class檔案對應的二進制流
        byte[] bytes = cw.toByteArray();

        //将二進制流寫到out/下
//        FileOutputStream fos = new FileOutputStream("F:/tmp/User.class");
//        fos.write(code);
//        fos.close();

        //檢視增強後反編譯的結果
        System.out.println(Decompiler.decompile(bytes));

        // 通過 reTransform 增強類
        AgentUtils.reTransform(User.class, bytes);
        User.show();
    }
}

class User {
    public static void show(){
        System.out.println("Hello World");
    }
}

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

public class MyClassVisitor extends ClassVisitor {
    public MyClassVisitor(final ClassVisitor cv) {
        super(Opcodes.ASM9, cv);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        if (cv != null) {
            cv.visit(version, access, name, signature, superName, interfaces);
        }
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        //如果methodName是show,則傳回自定義的MyMethodVisitor 
        if ("show".equals(name)) {
            MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
            return new MyMethodVisitor(mv);
        }
        if (cv != null) {
            return cv.visitMethod(access, name, desc, signature, exceptions);
        }
        return null;
    }
}

import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

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

    @Override
    public void visitCode() {
        //方法體内開始時調用
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
        mv.visitVarInsn(LSTORE, 0);
        super.visitCode();
    }

    @Override
    public void visitInsn(int opcode) {
        //每執行一個指令都會調用
        if (opcode == Opcodes.RETURN) {
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitVarInsn(LLOAD, 0);
            mv.visitInsn(LSUB);
            mv.visitVarInsn(LSTORE, 2);
            Label l3 = new Label();
            mv.visitLabel(l3);
            mv.visitLineNumber(11, l3);
            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
            mv.visitInsn(DUP);
            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
            mv.visitLdcInsn("== method cost time = ");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitVarInsn(LLOAD, 2);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
            mv.visitLdcInsn(" ==");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

        }
        super.visitInsn(opcode);
    }
}
           

maven依賴

<asm.version>9.0</asm.version>

<dependency>
			<groupId>org.ow2.asm</groupId>
			<artifactId>asm</artifactId>
			<version>${asm.version}</version>
		</dependency>
		<dependency>
			<groupId>org.ow2.asm</groupId>
			<artifactId>asm-commons</artifactId>
			<version>${asm.version}</version>
		</dependency>
		<dependency>
			<groupId>org.ow2.asm</groupId>
			<artifactId>asm-util</artifactId>
			<version>${asm.version}</version>
		</dependency>
		<dependency>
			<groupId>org.ow2.asm</groupId>
			<artifactId>asm-analysis</artifactId>
			<version>${asm.version}</version>
		</dependency>
           

2.2 bytebuddy示例

maven依賴

<dependency>
			<groupId>net.bytebuddy</groupId>
			<artifactId>byte-buddy</artifactId>
			<version>1.10.18</version>
		</dependency>
           

源碼示例

class ByteBuddyTest {

    public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException {
        Class dynamicType = new ByteBuddy()
                .subclass(Dog.class)
                .method(ElementMatchers.named("say"))
                .intercept(MethodDelegation.to(MyServiceInterceptor.class))
                .make()
                .load(ByteBuddyTest.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
                .getLoaded();

        Object s = dynamicType.newInstance();
        System.out.println(((Dog)s).say(" 歡迎測試bytebuddy"));
    }
}

public class MyServiceInterceptor {
    @RuntimeType
    public static String intercept(
            //用于調用父類版本的方法
            @SuperCall Callable<String> zuper,
            @AllArguments Object[] allArguments,
            //被攔截的原始方法
            @Origin Method method
    ) throws Exception {
        System.out.println("intercept:攔截" + method.getName());
        StringBuffer sb = new StringBuffer();
        Arrays.stream(allArguments).forEach(arg -> sb.append(arg).append(","));
        System.out.println("參數是:" + sb.toString());
        String res = zuper.call();
        System.out.println("執行結果是:" + res);
        return res;
    }
}

public class Dog {
    public String say(String msg){
        return "汪汪汪"+msg;
    }
}
           

2.3 Bytekit示例

maven依賴

<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>bytekit-core</artifactId>
			<version>0.0.4</version>
		</dependency>
		<dependency>
			<groupId>net.bytebuddy</groupId>
			<artifactId>byte-buddy-agent</artifactId>
			<version>1.10.18</version>
		</dependency>
		<dependency>
			<groupId>org.benf</groupId>
			<artifactId>cfr</artifactId>
			<version>0.150</version>
		</dependency>
           

源碼示例

import java.util.List;
import java.util.concurrent.TimeUnit;

import com.alibaba.deps.org.objectweb.asm.tree.ClassNode;
import com.alibaba.deps.org.objectweb.asm.tree.MethodNode;
import com.alibaba.bytekit.asm.MethodProcessor;
import com.alibaba.bytekit.asm.binding.Binding;
import com.alibaba.bytekit.asm.interceptor.InterceptorProcessor;
import com.alibaba.bytekit.asm.interceptor.annotation.AtEnter;
import com.alibaba.bytekit.asm.interceptor.annotation.AtExceptionExit;
import com.alibaba.bytekit.asm.interceptor.annotation.AtExit;
import com.alibaba.bytekit.asm.interceptor.annotation.ExceptionHandler;
import com.alibaba.bytekit.asm.interceptor.parser.DefaultInterceptorClassParser;
import com.alibaba.bytekit.utils.AgentUtils;
import com.alibaba.bytekit.utils.AsmUtils;
import com.alibaba.bytekit.utils.Decompiler;

public class ByteKitDemo {

    public static class Sample {
        private int exceptionCount = 0;

        public String hello(String str, boolean exception) {
            if (exception) {
                exceptionCount++;
                throw new RuntimeException("test exception, str: " + str);
            }
            return "hello " + str;
        }
    }

    public static class PrintExceptionSuppressHandler {

        @ExceptionHandler(inline = true)
        public static void onSuppress(@Binding.Throwable Throwable e, @Binding.Class Object clazz) {
            System.out.println("exception handler: " + clazz);
            e.printStackTrace();
        }
    }

    public static class SampleInterceptor {

        @AtEnter(inline = true, suppress = RuntimeException.class, suppressHandler = PrintExceptionSuppressHandler.class)
        public static void atEnter(@Binding.This Object object,
                                   @Binding.Class Object clazz,
                                   @Binding.Args Object[] args,
                                   @Binding.MethodName String methodName,
                                   @Binding.MethodDesc String methodDesc) {
            System.out.println("atEnter, args[0]: " + args[0]);
        }

        @AtExit(inline = true)
        public static void atExit(@Binding.Return Object returnObject) {
            System.out.println("atExit, returnObject: " + returnObject);
        }

        @AtExceptionExit(inline = true, onException = RuntimeException.class)
        public static void atExceptionExit(@Binding.Throwable RuntimeException ex,
                                           @Binding.Field(name = "exceptionCount") int exceptionCount) {
            System.out.println("atExceptionExit, ex: " + ex.getMessage() + ", field exceptionCount: " + exceptionCount);
        }
    }

    public static void main(String[] args) throws Exception {
        AgentUtils.install();

        // 解析定義的 Interceptor類 和相關的注解
        DefaultInterceptorClassParser interceptorClassParser = new DefaultInterceptorClassParser();
        List<InterceptorProcessor> processors = interceptorClassParser.parse(SampleInterceptor.class);

        // 加載位元組碼
        ClassNode classNode = AsmUtils.loadClass(Sample.class);

        // 對加載到的位元組碼做增強處理
        for (MethodNode methodNode : classNode.methods) {
            if (methodNode.name.equals("hello")) {
                MethodProcessor methodProcessor = new MethodProcessor(classNode, methodNode);
                for (InterceptorProcessor interceptor : processors) {
                    interceptor.process(methodProcessor);
                }
            }
        }

        // 擷取增強後的位元組碼
        byte[] bytes = AsmUtils.toBytes(classNode);

        // 檢視反編譯結果
        System.out.println(Decompiler.decompile(bytes));

        // 通過 reTransform 增強類
        AgentUtils.reTransform(Sample.class, bytes);

        // 啟動Sample,不斷執行
        final Sample sample = new Sample();
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; ++i) {
                    try {
                        TimeUnit.SECONDS.sleep(3);
                        String result = sample.hello("" + i, (i % 3) == 0);
                        System.out.println("call hello result: " + result);
                    } catch (Throwable e) {
                        // ignore
                        System.out.println("call hello exception: " + e.getMessage());
                    }
                }
            }
        });
        t.start();
        System.in.read();
    }
}