精华内容
下载资源
问答
  • 字节码编程,提供了字节码的基础知识和操作系统等知识,学习完成后可能对字节码有一个很好的认识,在java中很多的一些也能用到字节码的知识
  • 小傅哥的《字节码编程》- 文章涉及源码 前言 初识字节码编程是从使用非入侵的全链路监控开始,在这之前我所了解的如果需要监控系统的运行状况,通常需要硬编码埋点或者AOP的方式采集方法执行信息;耗时、异常、出入...
  • 市面上以及网络搜索中都基本很少有成体系的关于字节码编程的知识,这主要由于大部分开发人员其实很少接触这部分内容,包括;ASM、Javassist、Byte-buddy以及JavaAgent,没有很大的市场也就没有很多的资料。但大家...
  • 字节码编程插桩这种技术常与 Javaagent 技术结合用在系统的非入侵监控中,这样就可以替代在方法中进行硬编码操作。比如,你需要监控一个方法,包括;方法信息、执行耗时、出入参数、执行链路以及异常等。那么就非常...


    作者:小傅哥
    博客:https://bugstack.cn

    沉淀、分享、成长,让自己和他人都能有所收获!

    一、前言

    字节码编程插桩这种技术常与 Javaagent 技术结合用在系统的非入侵监控中,这样就可以替代在方法中进行硬编码操作。比如,你需要监控一个方法,包括;方法信息、执行耗时、出入参数、执行链路以及异常等。那么就非常适合使用这样的技术手段进行处理。

    为了能让这部分最核心的内容体现出来,本文会只使用 Javassist 技术对一段方法字节码进行插桩操作,最终输出这段方法的执行信息,如下;

    方法 - 测试方法用于后续进行字节码增强操作

    public Integer strToInt(String str01, String str02) {
        return Integer.parseInt(str01);
    }
    

    监控 - 对一段方法进行字节码增强后,输出监控信息

    监控 - Begin
    方法:org.itstack.demo.javassist.ApiTest.strToInt
    入参:["str01","str02"] 入参[类型]["java.lang.String","java.lang.String"] 入数[]["1","2"]
    出参:java.lang.Integer 出参[]1
    耗时:59(s)
    监控 - End
    

    有了这样的监控方案,基本我们可以输出方法执行过程中的全部信息。再通过后期的完善将监控信息展示到界面,实时报警。既提升了系统的监控质量,也方便了研发排查并定位问题。

    好!那么接下来我们开始一步步使用 javassist 进行字节码插桩,已达到我们的监控效果。

    二、开发环境

    1. JDK 1.8.0
    2. javassist 3.12.1.GA
    3. 本章涉及源码在:itstack-demo-bytecode-1-04,可以关注公众号bugstack虫洞栈,回复源码下载获取。你会获得一个下载链接列表,打开后里面的第17个「因为我有好多开源代码」,记得给个Star

    三、技术实现

    1. 获取方法基础信息

    1.1 获取类

    ClassPool pool = ClassPool.getDefault();
    // 获取类
    CtClass ctClass = pool.get(org.itstack.demo.javassist.ApiTest.class.getName());
    ctClass.replaceClassName("ApiTest", "ApiTest02");
    String clazzName = ctClass.getName();
    

    通过类名获取类的信息,同时这里可以把类名进行替换。它也包括类里面一些其他获取属性的操作,比如;ctClass.getSimpleName()ctClass.getAnnotations() 等。

    1.2 获取方法

    CtMethod ctMethod = ctClass.getDeclaredMethod("strToInt");
    String methodName = ctMethod.getName();
    

    通过 getDeclaredMethod 获取方法的 CtMethod 的内容。之后就可以获取方法的名称等信息。

    1.3 方法信息

    MethodInfo methodInfo = ctMethod.getMethodInfo();
    

    MethodInfo 中包括了方法的信息;名称、类型等内容。

    1.4 方法类型

    boolean isStatic = (methodInfo.getAccessFlags() & AccessFlag.STATIC) != 0;
    

    通过 methodInfo.getAccessFlags() 获取方法的标识,之后通过 与运算AccessFlag.STATIC,判断方法是否为静态方法。因为静态方法会影响后续的参数名称获取,静态方法第一个参数是 this ,需要排除。

    1.5 方法:入参信息{名称和类型}

    CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
    LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
    CtClass[] parameterTypes = ctMethod.getParameterTypes();
    
    • LocalVariableAttribute,获取方法的入参的名称。
    • parameterTypes,获取方法入参的类型。

    1.6 方法;出参信息

    CtClass returnType = ctMethod.getReturnType();
    String returnTypeName = returnType.getName();
    

    对于方法的出参信息,只需要获取出参类型。

    1.7 输出所有获取的信息

    System.out.println("类名:" + clazzName);
    System.out.println("方法:" + methodName);
    System.out.println("类型:" + (isStatic ? "静态方法" : "非静态方法"));
    System.out.println("描述:" + methodInfo.getDescriptor());
    System.out.println("入参[名称]:" + attr.variableName(1) + "," + attr.variableName(2));
    System.out.println("入参[类型]:" + parameterTypes[0].getName() + "," + parameterTypes[1].getName());
    System.out.println("出参[类型]:" + returnTypeName);
    

    输出结果

    类名:org.itstack.demo.javassist.ApiTest
    方法:strToInt
    类型:非静态方法
    描述:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Integer;
    入参[名称]:str01,str02
    入参[类型]:java.lang.String,java.lang.String
    出参[类型]:java.lang.Integer
    

    以上,所输出信息,都在为监控方法在做准备。从上面可以记录方法的基本描述以及入参个数等。尤其是入参个数,因为在后续还需要使用 $1,来获取没有给入参的值。

    2. 方法字节码插桩

    一段需会被字节码插桩改变的原始方法;

    public class ApiTest {
    
        public Integer strToInt(String str01, String str02) {
            return Integer.parseInt(str01);
        }
    
    }
    

    2.1 先给基础属性打标

    在监控的适合,不可能每一次调用都把所有方法信息汇总输出出来。这样做不只是性能问题,而是这些都是固定不变的信息,没有必要让每一次方法执行都输出。

    好!那么在方法编译时候,给每一个方法都生成一个唯一ID,用ID关联上方法的固定信息。也就可以把监控数据通过ID传递到外面。

    // 方法:生成方法唯一标识ID
    int idx = Monitor.generateMethodId(clazzName, methodName, parameterNameList, parameterTypeList, returnTypeName);
    

    生成ID的过程

    public static final int MAX_NUM = 1024 * 32;
    private final static AtomicInteger index = new AtomicInteger(0);
    private final static AtomicReferenceArray<MethodDescription> methodTagArr = new AtomicReferenceArray<>(MAX_NUM);   
    
    public static int generateMethodId(String clazzName, String methodName, List<String> parameterNameList, List<String> parameterTypeList, String returnType) {
        MethodDescription methodDescription = new MethodDescription();
        methodDescription.setClazzName(clazzName);
        methodDescription.setMethodName(methodName);
        methodDescription.setParameterNameList(parameterNameList);
        methodDescription.setParameterTypeList(parameterTypeList);
        methodDescription.setReturnType(returnType); 
    
        int methodId = index.getAndIncrement();
        if (methodId > MAX_NUM) return -1;
        methodTagArr.set(methodId, methodDescription);
        return methodId;
    }
    

    2.2 字节码插桩添加进入方法时间

    // 定义属性
    ctMethod.addLocalVariable("startNanos", CtClass.longType);
    // 方法前加强
    ctMethod.insertBefore("{ startNanos = System.nanoTime(); }");
    
    • 定义一个 long 类型的属性,startNanos。并通过 insertBefore 插入到方法内容的开始处。

    最终 class 类方法

    public class ApiTest {     
    
        public Integer strToInt(String str01, String str02) {
            long startNanos = System.nanoTime();
            return Integer.parseInt(str01);
        }
    }
    
    • 此时已经有了一个方法的开始时间,有了开始时间在加上后续的结尾时间。就可以很方便的统计一个方法的执行耗时。

    2.3 字节码插桩添加入参输出

    // 定义属性
    ctMethod.addLocalVariable("parameterValues", pool.get(Object[].class.getName()));
    // 方法前加强
    ctMethod.insertBefore("{ parameterValues = new Object[]{" + parameters.toString() + "}; }");
    
    • 这里定义一个数组类型的属性,Object[],用于记录入参信息。

    最终 class 类方法

    public Integer strToInt(String str01, String str02) {
        Object[] var10000 = new Object[]{str01, str02};
        long startNanos = System.nanoTime();
        return Integer.parseInt(str01);
    }
    
    • 两个参数可以通过一条 insertBefore 进行插入,这里是为了更加清晰的向你展示字节码插桩的过程。现在我们就有了进入方法的时间和参数集合,方便后续输出。

    2.4 定义监控方法

    因为我们需要将监控信息,输出给外部。那么我们这里会定义一个静态方法,让字节码增强后的方法去调用,输出监控信息。

    public static void point(final int methodId, final long startNanos, Object[] parameterValues, Object returnValues) {
        MethodDescription method = methodTagArr.get(methodId);
        System.out.println("监控 - Begin");
        System.out.println("方法:" + method.getClazzName() + "." + method.getMethodName());
        System.out.println("入参:" + JSON.toJSONString(method.getParameterNameList()) + " 入参[类型]:" + JSON.toJSONString(method.getParameterTypeList()) + " 入数[值]:" + JSON.toJSONString(parameterValues));
        System.out.println("出参:" + method.getReturnType() + " 出参[值]:" + JSON.toJSONString(returnValues));
        System.out.println("耗时:" + (System.nanoTime() - startNanos) / 1000000 + "(s)");
        System.out.println("监控 - End\r\n");
    }     
    
    public static void point(final int methodId, Throwable throwable) {
        MethodDescription method = methodTagArr.get(methodId);
        System.out.println("监控 - Begin");
        System.out.println("方法:" + method.getClazzName() + "." + method.getMethodName());
        System.out.println("异常:" + throwable.getMessage());
        System.out.println("监控 - End\r\n");
    }
    
    • 这里一共有两个方法,一个用于记录正常情况下的监控信息。另外一个用于记录异常时候的信息。如果是实际的业务场景中,就可以通过这样的方法使用 MQ 将监控信息发送给服务端记录起来并做展示。

    2.5 字节码插桩调用监控方法

    // 方法后加强
    ctMethod.insertAfter("{ org.itstack.demo.javassist.Monitor.point(" + idx + ", startNanos, parameterValues, $_);}", false); // 如果返回类型非对象类型,$_ 需要进行类型转换
    
    • 这里通过静态方法将监控参数传递给外部;idxstartNanosparameterValues$_出参值

    最终 class 类方法

    public Integer strToInt(String str01, String str02) {
        Object[] parameterValues = new Object[]{str01, str02};
        long startNanos = System.nanoTime();
        Integer var7 = Integer.parseInt(str01);
        Monitor.point(0, startNanos, parameterValues, var7);
        return var7;
    }
    
    • 现在已经可以将基本的监控信息传递给外部。对于一个普通的监控,如果不需要追踪链路,基本已经可以满足需求了。

    2.6 字节码插桩给方法添加TryCatch

    以上插桩内容,如果只是正常调用还是没问题的。但是如果方法抛出异常,那么这个时候就不能做到收集监控信息了。所以还需要给方法添加上 TryCatch

    // 方法;添加TryCatch
    ctMethod.addCatch("{ org.itstack.demo.javassist.Monitor.point(" + idx + ", $e); throw $e; }", ClassPool.getDefault().get("java.lang.Exception"));   // 添加异常捕获
    
    • 这里通过 addCatch 将方法包装在 TryCatch 里面。
    • 再通过在 catch 中调用外部方法,将异常信息输出。
    • 同时有一个点需要注意,$e,用于获取抛出异常的内容。

    最终 class 类方法

    public Integer strToInt(String str01, String str02) {
        try {
            Object[] parameterValues = new Object[]{str01, str02};
            long startNanos = System.nanoTime();
            Integer var7 = Integer.parseInt(str01);
            Monitor.point(0, startNanos, parameterValues, var7);
            return var7;
        } catch (Exception var9) {
            Monitor.point(0, var9);
            throw var9;
        }
    }
    
    • 那么现在就可以非常完整的收录方法执行的信息,包括它的正常执行以及异常情况。

    四、测试结果

    接下来就是执行我们的调用测试被修改后的方法字节码。通过不同的入参,来验证监控结果;

    // 测试调用
    byte[] bytes = ctClass.toBytecode();
    Class<?> clazzNew = new GenerateClazzMethod().defineClass("org.itstack.demo.javassist.ApiTest", bytes, 0, bytes.length);          
    
    // 反射获取 main 方法
    Method method = clazzNew.getMethod("strToInt", String.class, String.class);
    Object obj_01 = method.invoke(clazzNew.newInstance(), "1", "2");
    System.out.println("正确入参:" + obj_01);             
    
    Object obj_02 = method.invoke(clazzNew.newInstance(), "a", "b");
    System.out.println("异常入参:" + obj_02);
    
    • 这里首先会使用 ClassLoader 加载字节码,之后生成新的类。
    • 接下来通过获取方法并传入正确和错误的入参。

    测试结果

    监控 - Begin
    方法:org.itstack.demo.javassist.ApiTest.strToInt
    入参:["str01","str02"] 入参[类型]["java.lang.String","java.lang.String"] 入数[]["1","2"]
    出参:java.lang.Integer 出参[]1
    耗时:63(s)
    监控 - End
    
    正确入参:1   
    
    监控 - Begin
    方法:org.itstack.demo.javassist.ApiTest.strToInt
    异常:For input string: "a"
    监控 - End
    
    • 截至到这我们已经将监控中最核心之一展示出来了,也就是监控方法的全部信息。后续就是需要将这样的监控信息填充到统一监控中心,进行做展示相关的计算操作。

    五、总结

    • 基于 Javassist 字节码操作框架可以非常方便的去进行字节码增强,也不需要考虑纯字节码编程下的指令码控制。但如果考虑性能以及更加细致的改变,还是需要使用到 ASM

    • 这里包括一些字节码操作的知识点,如下;

      • methodInfo.getDescriptor(),可以输出方法描述信息。(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Integer;,其实就是方法的出入参和返回值。
      • $1 $2 ... 用于获取不同位置的参数。$$ 可以获取全部入参,但是不太适合用在数值传递中。
      • 获取方法的入参需要判断方法的类型,静态类型的方法还包含了 this 参数。AccessFlag.STATIC
      • addCatch 最开始执行就包裹原有方法内的内容,最后执行就包括所有内容。它依赖于顺序操作,其他的方法也是这样;insertBeforeinsertAfter
    展开全文
  • 到本章为止已经写了四篇关于字节码编程的内容,涉及了大部分的API方法。整体来说对 Javassist 已经有一个基本的使用认知。那么在 Javassist 中不仅提供了高级 API 用于创建和修改类、方法,还提供了低级 API 控制...

    在这里插入图片描述

    作者:小傅哥
    博客:https://bugstack.cn

    沉淀、分享、成长,让自己和他人都能有所收获!

    一、前言

    到本章为止已经写了四篇关于字节码编程的内容,涉及了大部分的API方法。整体来说对 Javassist 已经有一个基本的使用认知。那么在 Javassist 中不仅提供了高级 API 用于创建和修改类、方法,还提供了低级 API 控制字节码指令的方式进行操作类、方法。

    有了这样的 javassist API 在一些特殊场景下就可以使用字节码指令控制方法。

    接下来我们通过字节码指令模拟一段含有自定义注解的方法修改和生成。在修改的过程中会将原有方法计算息费的返回值替换成 0,最后我们使用这样的技术去生成一段计算息费的方法。通过这样的练习学会字节码操作。

    二、开发环境

    1. JDK 1.8.0
    2. javassist 3.12.1.GA
    3. 本章涉及源码在:itstack-demo-bytecode-1-05,可以关注公众号bugstack虫洞栈,回复源码下载获取。你会获得一个下载链接列表,打开后里面的第17个「因为我有好多开源代码」,记得给个Star

    三、案例目标

    1. 使用指令码修改原有方法返回值
    2. 使用指令码生成一样的方法

    测试方法

    @RpcGatewayClazz(clazzDesc = "用户信息查询服务", alias = "api", timeOut = 500)
    public class ApiTest {
    
        @RpcGatewayMethod(methodDesc = "查询息费", methodName = "interestFee")
        public double queryInterestFee(String uId){
            return BigDecimal.TEN.doubleValue();  // 模拟息费计算返回
        }
    
    }
    
    • 这里使用的注解是测试中自定义的,模拟一个相当于网关接口的暴漏。

    四、技术实现

    1. 读取类自定义注解

    ClassPool pool = ClassPool.getDefault();
    // 类、注解
    CtClass ctClass = pool.get(ApiTest.class.getName());
    // 通过集合获取自定义注解
    Object[] clazzAnnotations = ctClass.getAnnotations();
    RpcGatewayClazz rpcGatewayClazz = (RpcGatewayClazz) clazzAnnotations[0];
    System.out.println("RpcGatewayClazz.clazzDesc:" + rpcGatewayClazz.clazzDesc());
    System.out.println("RpcGatewayClazz.alias:" + rpcGatewayClazz.alias());
    System.out.println("RpcGatewayClazz.timeOut:" + rpcGatewayClazz.timeOut());
    
    • ctClass.getAnnotations(),可以获取所有的注解,进行操作

    输出结果:

    RpcGatewayClazz.clazzDesc:用户信息查询服务
    RpcGatewayClazz.alias:api
    RpcGatewayClazz.timeOut:500
    

    2. 读取方法的自定义注解

    CtMethod ctMethod = ctClass.getDeclaredMethod("queryInterestFee");
    RpcGatewayMethod rpcGatewayMethod = (RpcGatewayMethod) ctMethod.getAnnotation(RpcGatewayMethod.class);
    System.out.println("RpcGatewayMethod.methodName:" + rpcGatewayMethod.methodName());
    System.out.println("RpcGatewayMethod.methodDesc:" + rpcGatewayMethod.methodDesc());
    
    • 在读取方法自定义注解时,通过的是注解的 class 获取的,这样按照名称可以只获取最需要的注解名称。

    输出结果:

    RpcGatewayMethod.methodName:interestFee
    RpcGatewayMethod.methodDesc:查询息费
    

    3. 读取方法指令码

    MethodInfo methodInfo = ctMethod.getMethodInfo();
    CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
    CodeIterator iterator = codeAttribute.iterator();
    while (iterator.hasNext()) {
        int idx = iterator.next();
        int code = iterator.byteAt(idx);
        System.out.println("指令码:" + idx + " > " + Mnemonic.OPCODE[code]);
    }
    
    • 这里的指令码就是一个方法编译后在 JVM 执行的操作流程。

    输出结果:

    指令码:0 > getstatic
    指令码:3 > invokevirtual
    指令码:6 > dreturn
    

    4. 通过指令修改方法

    ConstPool cp = methodInfo.getConstPool();
    Bytecode bytecode = new Bytecode(cp);
    bytecode.addDconst(0);
    bytecode.addReturn(CtClass.doubleType);
    methodInfo.setCodeAttribute(bytecode.toCodeAttribute());
    
    • addDconst,将 double 型0推送至栈顶
    • addReturn,返回 double 类型的结果

    此时的方法的返回值已经被修改,下面的是新的 class 类;

    @RpcGatewayClazz(
        clazzDesc = "用户信息查询服务",
        alias = "api",
        timeOut = 500L
    )
    public class ApiTest {
        public ApiTest() {
        }
    
        @RpcGatewayMethod(
            methodDesc = "查询息费",
            methodName = "interestFee"
        )
        public double queryInterestFee(String var1) {
            return 0.0D;
        }
    }
    
    • 可以看到查询息费的返回结果已经是 0.0D。如果你的程序被这样操作,那么还是很危险的。所以有时候会进行一些混淆编译,降低破解风险。

    5. 使用指令码生成方法

    5.1 创建基础方法信息

    ClassPool pool = ClassPool.getDefault();
    // 创建类信息
    CtClass ctClass = pool.makeClass("org.itstack.demo.javassist.HelloWorld");
    // 添加方法
    CtMethod mainMethod = new CtMethod(CtClass.doubleType, "queryInterestFee", new CtClass[]{pool.get(String.class.getName())}, ctClass);
    mainMethod.setModifiers(Modifier.PUBLIC);
    MethodInfo methodInfo = mainMethod.getMethodInfo();
    ConstPool cp = methodInfo.getConstPool();
    
    • 创建类和方法的信息在我们几个章节中也经常使用,主要是创建方法的时候需要传递;返回类型、方法名称、入参类型,以及最终标记方法的可访问量。

    5.2 创建类使用注解

    // 类添加注解
    AnnotationsAttribute clazzAnnotationsAttribute = new AnnotationsAttribute(cp, AnnotationsAttribute.visibleTag);
    Annotation clazzAnnotation = new Annotation("org/itstack/demo/javassist/RpcGatewayClazz", cp);
    clazzAnnotation.addMemberValue("clazzDesc", new StringMemberValue("用户信息查询服务", cp));
    clazzAnnotation.addMemberValue("alias", new StringMemberValue("api", cp));
    clazzAnnotation.addMemberValue("timeOut", new LongMemberValue(500L, cp));
    clazzAnnotationsAttribute.setAnnotation(clazzAnnotation);
    ctClass.getClassFile().addAttribute(clazzAnnotationsAttribute);
    
    • AnnotationsAttribute,创建自定义注解标签
    • Annotation,创建实际需要的自定义注解,这里需要传递自定义注解的类路径
    • addMemberValue,用于添加自定义注解中的值。需要注意不同类型的值 XxxMemberValue 前缀不一样;StringMemberValueLongMemberValue
    • setAnnotation,最终设置自定义注解。如果不设置,是不能生效的。

    5.3 创建方法注解

    // 方法添加注解
    AnnotationsAttribute methodAnnotationsAttribute = new AnnotationsAttribute(cp, AnnotationsAttribute.visibleTag);
    Annotation methodAnnotation = new Annotation("org/itstack/demo/javassist/RpcGatewayMethod", cp);
    methodAnnotation.addMemberValue("methodName", new StringMemberValue("查询息费", cp));
    methodAnnotation.addMemberValue("methodDesc", new StringMemberValue("interestFee", cp));
    methodAnnotationsAttribute.setAnnotation(methodAnnotation);
    methodInfo.addAttribute(methodAnnotationsAttribute);
    
    • 设置类的注解与设置方法的注解,前面的内容都是一样的。唯独需要注意的是方法的注解,需要设置到方法的;addAttribute 上。

    5.4 字节码编写方法快

    // 指令控制
    Bytecode bytecode = new Bytecode(cp);
    bytecode.addGetstatic("java/math/BigDecimal", "TEN", "Ljava/math/BigDecimal;");
    bytecode.addInvokevirtual("java/math/BigDecimal", "doubleValue", "()D");
    bytecode.addReturn(CtClass.doubleType);
    methodInfo.setCodeAttribute(bytecode.toCodeAttribute());
    
    • Javassist 中的指令码通过,Bytecode 的方式进行添加。基本所有的指令你都可以在这里使用,它有非常强大的 API
    • addGetstatic,获取指定类的静态域, 并将其压入栈顶
    • addInvokevirtual,调用实例方法
    • addReturn,从当前方法返回double
    • 最终讲字节码添加到方法中,也就是会变成方法体。

    5.5 添加方法信息并输出

    // 添加方法
    ctClass.addMethod(mainMethod);
     
    // 输出类信息到文件夹下
    ctClass.writeFile();
    
    • 这部分内容就比较简单了,也是我们做 Javassist 字节码开发常用的内容。添加方法和输出字节码编程后的类信息。

    5.6 最终创建的类方法

    @RpcGatewayClazz(
        clazzDesc = "用户信息查询服务",
        alias = "api",
        timeOut = 500L
    )
    public class HelloWorld {
        @RpcGatewayMethod(
            methodName = "查询息费",
            methodDesc = "interestFee"
        )
        public double queryInterestFee(String var1) {
            return BigDecimal.TEN.doubleValue();
        }
    
        public HelloWorld() {
        }
    }
    

    字节码生成含有注解的类和方法

    五、总结

    • 本章节我们看到字节码编程不只可以像以前使用强大的api去直接编写代码,还可以向方法中添加指令,控制方法。这样就可以非常方便的处理一些特殊场景。例如 TryCatch 中的开始位置。
    • 关于 javassist 字节码编程本身常用的方法基本已经覆盖完成,后续会集合 JavaAgent 做一些案例汇总,将知识点与实际场景进行串联。
    • 学习终究还是要成体系的系统化深入学习,只言片语有的内容不能很好的形成一个技术栈的闭环,也不利于在项目中实战。
    展开全文
  • 作者:小傅哥 ... 沉淀、分享、成长,让自己和他人都能有所收获!...讲道理,市面上以及网络搜索中都基本很少有成体系的关于字节码编程的知识,这主要由于大部分开发人员其实很少接触这部分内容,包括;ASM、Javassi


    作者:小傅哥
    博客:https://bugstack.cn - 汇总系列原创专题文章

    沉淀、分享、成长,让自己和他人都能有所收获!😄

    让人怪不好意思的,说是出书有点膨胀💥,毕竟这不是走出版社的流程,选题、组稿、编著、审读、加工到出版发行。

    但全书共计107页,11万7千字,20个章节涵盖三个字节码框架和JavaAgent使用并附带整套案例源码!

    讲道理,市面上以及网络搜索中都基本很少有成体系的关于字节码编程的知识,这主要由于大部分开发人员其实很少接触这部分内容,包括;ASMJavassistByte-buddy以及JavaAgent,没有很大的市场也就没有很多的资料。但大家其实已经从其他的框架或者中间件中使用到,就像你用到的;Cglib、混沌工程、非入侵的全链路监控以及你是否使用过jetbrains-agent.jar做了某项实验?

    所以这样的技术栈一直都萦绕在你身边,只是你还没有发现!当有一天面试问到了,那时你已经170斤工作五年。

    蹭个车告诉你这个知识的重要性,阿里云的挑战赛!

    读不在三更五鼓,功只怕一曝十寒!,不一定一本书中就能读出个黄金屋,但脚下路的用什么垫都是自己日积月累。

    就这本书他出炉了

    介绍

    初识字节码编程是从使用非入侵的全链路监控开始,在这之前我所了解的如果需要监控系统的运行状况,通常需要硬编码埋点或者AOP的方式采集方法执行信息;耗时、异常、出入参等来监控一个系统的运行健康度。而这样的监控方式在大量的系统中去改造非常耗时且不好维护,更不要说去监控一个业务流程的调用链路。

    在2010年的时候,谷歌发布一篇名为《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》的论文,在文中介绍了谷歌生产环境中大规模分布式系统下的跟踪系统Dapper的设计和使用经验。

    这样的监控系统采用 Javaagent 与字节码操作框架结合使用,在应用系统加载时对需要监控的方法进行字节码增强也叫插桩。对方法处理后的结果就和你之前硬编码类似,但这样就可以减轻认为操作,同时可以对多个系统之间定义调用链路ID进行串联业务流程关系。最终,极大减轻了监控成本也提高了线上问题的快速定位和处理。

    这里面监控系统核心知识也主要是 Javaagent和字节码操作,在字节码操作中目前有三个比较常用的框架;ASMJavassistByte Buddy,这几个框架都能进行字节码操作,其中ASM 更偏向于底层,需要了解字节码指令以及操作数栈等知识,最好学习过《Java虚拟机规范》等书籍,另外两个框架是对 ASM 的封装,提供更加高级的API去操作字节码。

    在本书中小傅哥会分别讲解这三种字节码框架的使用,以及最终与Javagent结合完成全链路监控的案例。通过这样的学习让你可以从有抓手的从案例开始,把枯燥的字节码编程融入场景,深化理解和实操应用。也能让你忙于CRUD开发的同时提升自己的知识栈,拓展技术视野。也许不久以后这项技术也能为你带来一些有价值的收获!

    作者

    作者小傅哥多年从事一线互联网 Java 开发,热衷于对学习历程做技术汇总,侧重点更倾向编写 Java 核心内容。旨在为大家提供一个清晰详细的学习教程也帮助自己不断沉淀。所以投入时间学习、整理、编写相关的资料,如果我的文章或书籍能为您提供帮助,请给予支持(关注、点赞、分享)!

    如何支持:

    如果这些都做了!记得加我微信(fustack),交个朋友!

    下载

    内容包括

    1. 小傅哥的《字节码编程》
    2. 一整套书中对应的源码
      字节码编程附带源码

    下载方式;

    1. 打开外链分享链接下载书籍: http://book.bugstack.cn/#s/51Es_z_Q
    2. 书中的源码部分在公众号:bugstack虫洞栈,回复源码下载即可获取
    3. 如果链接失效关注公众号:bugstack虫洞栈,回复PDF,获取新的链接下载
    4. 添加作者微信(fustack)获取书籍和源码,也方便做相关技术交流
    5. 公众号用户,直接点击下方阅读原文下载

    由于网络兼容直接在线阅读可能有字体错位问题,请下载阅读,体验更好!

    收个尾

    头一次把系列文章写成书,虽然免费发布,但也可能在获取书籍下载以及学习过程中发现我写错字以及写错某个名称想喷我🤮,如果你有此冲动!请添加小傅哥微信(公众号:bugstack虫洞栈获取),我会用我的技术魅力和爆炸人品感化你,并修改我的书籍内容,📝记录你的功绩到:https://github.com/fuzhengwei/CodeGuide/wiki

    如果你在阅读本书的过程中有些地方不是很容易理解,不要担心一定作者没有描述的更加清楚。很多知识或者系统建设并不难,只是有时候被描述的麻烦了。所以我也非常愿意与你一起去学习这部分知识,在讨论中不断把问题梳理的更加清晰,用更易懂的方式剖析问题的本质。

    最后,希望同好编程开发的你不只是CRUD的工具人,多多拓展技术栈夯实基本功。共勉!加油!

    彩蛋

    CodeGuide | 程序员编码指南 Go!

    本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总,旨在为大家提供一个清晰详细的学习教程,侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助,请给予支持(关注、点赞、分享)!

    CodeGuide | 程序员编码指南

    推荐阅读

    展开全文
  • 相对于小傅哥之前编写的字节码编程; ASM、Javassist 系列,Byte Buddy 玩法上更加高级,你可以完全不需要了解一个类和方法块是如何通过 指令码 LDC、LOAD、STORE、IRETURN… 生成出来的。就像它的官网介绍; Byte ...


    作者:小傅哥
    博客:https://bugstack.cn

    沉淀、分享、成长,让自己和他人都能有所收获!

    一、前言

    相对于小傅哥之前编写的字节码编程; ASMJavassist 系列,Byte Buddy 玩法上更加高级,你可以完全不需要了解一个类和方法块是如何通过 指令码 LDC、LOAD、STORE、IRETURN… 生成出来的。就像它的官网介绍;

    Byte Buddy 是一个代码生成和操作库,用于在 Java 应用程序运行时创建和修改 Java 类,而无需编译器的帮助。除了 Java 类库附带的代码生成实用程序外,Byte Buddy 还允许创建任意类,并且不限于实现用于创建运行时代理的接口。此外,Byte Buddy 提供了一种方便的 API,可以使用 Java 代理或在构建过程中手动更改类。

    • 无需理解字节码指令,即可使用简单的 API 就能很容易操作字节码,控制类和方法。
    • 已支持Java 11,库轻量,仅取决于Java字节代码解析器库ASM的访问者API,它本身不需要任何其他依赖项。
    • 比起JDK动态代理、cglib、Javassist,Byte Buddy在性能上具有一定的优势。

    2015年10月,Byte Buddy被 Oracle 授予了 Duke’s Choice大奖。该奖项对Byte Buddy的“ Java技术方面的巨大创新 ”表示赞赏。我们为获得此奖项感到非常荣幸,并感谢所有帮助Byte Buddy取得成功的用户以及其他所有人。我们真的很感激!

    除了这些简单的介绍外,还可以通过官网:https://bytebuddy.net,去了解更多关于 Byte Buddy 的内容。

    好! 那么接下来,我们开始从 HelloWorld 开始。深入了解一个技能前,先多多运行,这样总归能让找到学习的快乐。

    二、开发环境

    1. JDK 1.8.0
    2. byte-buddy 1.10.9
    3. byte-buddy-agent 1.10.9
    4. 本章涉及源码在:itstack-demo-bytecode-2-01,可以关注公众号bugstack虫洞栈,回复源码下载获取。你会获得一个下载链接列表,打开后里面的第17个「因为我有好多开源代码」,记得给个Star

    三、案例目标

    每一个程序员,都运行过 N 多个HelloWorld,就像很熟悉的 Java

    public class Hi {
    
        public static void main(String[] args) {
            System.out.println("Byte-buddy Hi HelloWorld By 小傅哥(bugstack.cn)");
        }
    
    }
    

    那么我们接下来就通过使用动态字节码生成的方式,来创建出可以输出 HelloWorld 的程序。

    新知识点的学习不要慌,最主要是找到一个可以入手的点,通过这样的一个点去慢慢解开整个程序的面纱。

    四、技术实现

    1. 官网经典例子

    在我们看官网文档中,从它的介绍了就已经提供了一个非常简单的例子,用于输出 HelloWorld,我们在这展示并讲解下。

    案例代码:

    String helloWorld = new ByteBuddy()
                .subclass(Object.class)
                .method(named("toString"))
                .intercept(FixedValue.value("Hello World!"))
                .make()
                .load(getClass().getClassLoader())
                .getLoaded()
                .newInstance()
                .toString();    
    
    System.out.println(helloWorld);  // Hello World!
    

    他的运行结果就是一行,Hello World!,整个代码块核心功能就是通过 method(named("toString")),找到 toString 方法,再通过拦截 intercept,设定此方法的返回值。FixedValue.value("Hello World!")。到这里其实一个基本的方法就通过 Byte-buddy ,改造完成。

    接下来的这一段主要是用于加载生成后的 Class 和执行,以及调用方法 toString()。也就是最终我们输出了想要的结果。那么,如果你不能看到这样一段方法块,把我们的代码改造后的样子,心里还是有点虚。那么,我们通过字节码输出到文件,看下具体被改造后的样子,如下;

    编译后的Class文件ByteBuddyHelloWorld.class

    public class HelloWorld {
        public String toString() {
            return "Hello World!";
        }
    
        public HelloWorld() {
        }
    }
    

    在官网来看,这是一个非常简单并且能体现 Byte buddy 的例子。但是与我们平时想创建出来的 main 方法相比,还是有些差异。那么接下来,我们尝试使用字节码编程技术创建出这样一个方法。

    2. 字节码创建类和方法

    接下来的例子会通过一点点的增加代码梳理,不断的把一个方法完整的创建出来。

    2.1 定义输出字节码方法

    为了可以更加清晰的看到每一步对字节码编程后,所创建出来的方法样子(clazz),我们需要输出字节码生成 clazz。在Byte buddy中默认提供了一个 dynamicType.saveIn() 方法,我们暂时先不使用,而是通过字节码进行保存。

    private static void outputClazz(byte[] bytes) {
        FileOutputStream out = null;
        try {
            String pathName = ApiTest.class.getResource("/").getPath() + "ByteBuddyHelloWorld.class";
            out = new FileOutputStream(new File(pathName));
            System.out.println("类输出路径:" + pathName);
            out.write(bytes);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (null != out) try {
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    
    • 这个方法我们在之前也用到过,主要就是一个 Java 基础的内容,输出字节码到文件中。

    2.2 创建类信息

    DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
            .subclass(Object.class)
            .name("org.itstack.demo.bytebuddy.HelloWorld")
            .make();
    
    // 输出类字节码
    outputClazz(dynamicType.getBytes());
    
    • 创建类和定义类名,如果不写类名会自动生成要给类名。

    此时class文件:

    public class HelloWorld {
        public HelloWorld() {
        }
    }
    

    2.3 创建main方法

    DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
            .subclass(Object.class)
            .name("org.itstack.demo.bytebuddy.HelloWorld")
            .defineMethod("main", void.class, Modifier.PUBLIC + Modifier.STATIC)
            .withParameter(String[].class, "args")
            .intercept(FixedValue.value("Hello World!"))
            .make();
    

    与上面相比新增的代码片段;

    • defineMethod("main", void.class, Modifier.PUBLIC + Modifier.STATIC),定义方法;名称、返回类型、属性public static
    • withParameter(String[].class, "args"),定义参数;参数类型、参数名称
    • intercept(FixedValue.value("Hello World!")),拦截设置返回值,但此时还能满足我们的要求。

    这里有一个知识点,Modifier.PUBLIC + Modifier.STATIC,这是一个是二进制相加,每一个类型都在二进制中占有一位。例如 1 2 4 8 ... 对应的二进制占位 1111。所以可以执行相加运算,并又能保留原有单元的属性。

    此时class文件:

    public class HelloWorld {
        public static void main(String[] args) {
            String var10000 = "Hello World!";
        }
    
        public HelloWorld() {
        }
    }
    

    此时基本已经可以看到我们平常编写的 Hello World 影子了,但还能输出结果。

    2.4 委托函数使用

    为了能让我们使用字节码编程创建的方法去输出一段 Hello World ,那么这里需要使用到委托

    DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
            .subclass(Object.class)
            .name("org.itstack.demo.bytebuddy.HelloWorld")
            .defineMethod("main", void.class, Modifier.PUBLIC + Modifier.STATIC)
            .withParameter(String[].class, "args")
            .intercept(MethodDelegation.to(Hi.class))
            .make();
    
    • 整体来看变化并不大,只有 intercept(MethodDelegation.to(Hi.class)),使用了一段委托函数,真正去执行输出的是另外的函数方法。

      • MethodDelegation,需要是 public
      • 被委托的方法与需要与原方法有着一样的入参、出参、方法名,否则不能映射上

    此时class文件:

    public class HelloWorld {
        public static void main(String[] args) {
            Hi.main(var0);
        }
    
        public HelloWorld() {
        }
    }
    
    • 那么此时就可以输出我们需要的内容了,Hi.main 是定义出来的委托函数。也就是一个 HelloWorld

    五、测试结果

    为了可以让整个方法运行起来,我们需要添加字节码加载和反射调用的代码块,如下;

    // 加载类
    Class<?> clazz = dynamicType.load(GenerateClazzMethod.class.getClassLoader())
            .getLoaded();
    
    // 反射调用
    clazz.getMethod("main", String[].class).invoke(clazz.newInstance(), (Object) new String[1]);
    

    运行结果

    类输出路径:/User/xiaofuge/itstack/git/github.com/itstack-demo-bytecode/itstack-demo-bytecode-2-01/target/test-classes/ByteBuddyHelloWorld.class
    helloWorld
    
    Process finished with exit code 0
    

    效果图

    Byte buddy HelloWorld 效果图

    六、总结

    • 在本章节 Byte buddy 中,需要掌握几个关键信息;创建方法、定义属性、拦截委托、输出字节码,以及最终的运行。这样的一个简单过程,可以很快的了解到如何使用 Byte buddy
    • 本系列文章后续会继续更新,把常用的 Byte buddy 方法通过实际的案例去模拟建设,在这个过程中加强学习使用。一些基础知识也可以通过官方文档进行学习;https://bytebuddy.net
    • 在学习整理的过程中发现,关于字节码编程方面的资料并不是很全,主要源于大家平时的开发中基本是用不到的,谁也不可能总去修改字节码。但对于补全这样的成体系完善技术栈资料,却可以帮助很多需要的人。因此我也会持续输出类似这样空白的技术文章。

    七、彩蛋

    CodeGuide | 程序员编码指南 Go!
    本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总,旨在为大家提供一个清晰详细的学习教程,侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助,请给予支持(关注、点赞、分享)!

    CodeGuide | 程序员编码指南

    展开全文
  • 这在我们之前的字节码编程文章里也有所提到。 本文主要讲解关于 ASM 方式的字节码增强,接下来的案例会逐步讲解一个给方法添加 TryCatch 块,用于采集异常信息以及正常的出参结果的流程。 一步步向你展示通过指令码...
  • 就像是我们研究字节码编程最终是需要应用到实际场景中,例如:实现一款非入侵的全链路最终监控系统,那么这里就会包括一些基本的核心功能点;方法执行耗时、出入参获取、异常捕获、添加链路ID等等。而这些一个个的...
  • 字节码 描述 INVOKESTATIC 调用静态方法 LSTORE 将栈顶long类型值保存到局部变量indexbyte中 5.2 初始化入参装填数组 使用字节码的方式去初始化一个参数数量的数组 Object[] var6 = new Object[](x); 通过字节码的...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 240,625
精华内容 96,250
关键字:

字节码编程