【字节码】Byte-buddy 监控方法执行耗时动态获取出入参类型 和值

在这里插入图片描述

1.概述

上一篇文章:【字节码】基于Byte Buddy语法创建的第一个 HelloWorld

转载:https://github.com/fuzhengwei/itstack-demo-bytecode

案例是剥去外衣包装展示出核心功能的最佳学习方式!

就像是我们研究字节码编程最终是需要应用到实际场景中,例如:实现一款非入侵的全链路最终监控系统,那么这里就会包括一些基本的核心功能点; 方法执行耗时 、 出入参获取 、 异常捕获 、 添加链路ID 等等。而这些一个个的功能点,最快的掌握方式就是去实现他最基本的功能验证,这个阶段基本也是技术选型的阶段,验证各项技术点是否可以满足你后续开发的需求。否则在后续开发中,如果已经走了很远的时候再发现不适合,那么到时候就很麻烦了。

在前面的 ASM 、 Javassist 章节中也有陆续实现过获取方法的出入参信息,但实现的方式还是偏向于字节码控制,尤其 ASM ,更是需要使用到字节码指令将入参信息压栈操作保存到局部变量用于输出,在这个过程中需要深入了解 Java虚拟机规范 ,否则很不好完成这一项的开发。但! ASM 也是性能最牛的

其他的字节码编程框架都是基于它所开发的。关于这部分系列文章可以访问链接进行专题系列的学习:
https://bugstack.cn/itstack/itstack-demo-bytecode.html

那么,本章节我们会使用 Byte-buddy 来实现这一功能,在接下来的操作中你会感受到这个字节码框架的魅力,它的API更加高级也更符合普遍易接受的操作方式进行处理

2.案例目标

在这里我们定义一个类并创建出等待被监控的方法,当方法执行时监控方法的各项信息; 执行耗时 、 出入参信息 等。

public class BizMethod {
    
    
    public String queryUserInfo(String uid, String token) throws
            InterruptedException {
    
    
        Thread.sleep(new Random().nextInt(500));
        return "德莱联盟,王牌工程师。小傅哥(公众号:bugstack虫洞栈),申请出栈!";
    }
}

我们这里模拟监控并没有使用 Javaagent 去做字节码加载时的增强,主要为了将最核心的内容体现出来。后续的章节会陆续讲解各个核心功能的组合使用,做出一套监控系统

3.技术实现

在技术实现的过程中,我会陆续的将需要监控的内容一步步完善。这样将一个总体的内容进行拆解后,方便学习和理解

扫描二维码关注公众号,回复: 14561182 查看本文章

3.1 创建监控主体类

 /**
     * 测试点:
     *
     * 运行结果
     * 方法耗时:465ms
     * @throws Exception
     */
    @Test
    public void queryUserInfo() throws Exception {
    
    
        DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
                .subclass(BizMethod.class)
                .method(ElementMatchers.named("queryUserInfo"))
                .intercept(MethodDelegation.to(MonitorDemo.class))
                .make();
        // 加载类
        Class<?> clazz = dynamicType.load(BizMethod.class.getClassLoader())
                .getLoaded();
        // 反射调用
        clazz.getMethod("queryUserInfo", String.class,
                String.class).invoke(clazz.newInstance(), "10001", "Adhl9dkl");

    }

这一部分是 Byte Buddy模版代码,定义需要被加载的类和方法;BizMethod.class、ElementMatchers.named("queryUserInfo"),这一步也就是让程序可以定位到你的被监控内容。

接下来就是最重要的一部分委托; MethodDelegation.to(MonitorDemo.class) ,最终所有的监控操作都会被 MonitorDemo.class 类中的方法进行处理。

最后就是类的加载和反射调用,这部分主要用于每次的测试验证。查找方法,传递对象和入参信息

3.2 监控方法耗时

如上一步所述这里主要需要使用到,委托类进行控制监控信息


import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.SuperCall;

import java.util.concurrent.Callable;

/**
 * @author lcc
 */
public class MonitorDemo {
    
    

    @RuntimeType
    public static Object intercept(@SuperCall Callable<?> callable) throws
            Exception {
    
    
        long start = System.currentTimeMillis();
        try {
    
    
            return callable.call();
        } finally {
    
    
            System.out.println("方法耗时:" + (System.currentTimeMillis() -
                    start) + "ms");
        }
    }
}

这里面包括几个核心的知识点; @RuntimeType :定义运行时的目标方法。 @SuperCall :用于调用父类版本的方法。

定义好方法后,下面有一个 callable.call(); 这个方法是调用原方法的内容,返回结果。而前后包装的。

最后在 finally 中,打印方法的执行耗时。 System.currentTimeMillis() - start

3.3 获取方法信息

获取方法信息的过程其实就是在获取方法的描述内容,也就是你编写的方法拆解为各个内容进行输出。
那么为了实现这样的功能我们需要使用到新的注解 @Origin Method method

public class MonitorDemoV2 {
    
    

    @RuntimeType
    public static Object intercept(@Origin Method method, @SuperCall Callable<?
            > callable) throws Exception {
    
    
        long start = System.currentTimeMillis();
        Object resObj = null;
        try {
    
    
            resObj = callable.call();
            return resObj;
        } finally {
    
    
            System.out.println("方法名称:" + method.getName());
            System.out.println("入参个数:" + method.getParameterCount());
            System.out.println("入参类型:" + method.getParameterTypes()
                    [0].getTypeName() + "、" + method.getParameterTypes()[1].getTypeName());
            System.out.println("出参类型:" + method.getReturnType().getName());
            System.out.println("出参结果:" + resObj);
            System.out.println("方法耗时:" + (System.currentTimeMillis() -
                    start) + "ms");
        }
    }

}

@Origin ,用于拦截原有方法,这样就可以获取到方法中的相关信息。

这一部分的信息相对来说比较全,尤其也获取到了参数的个数和类型,这样就可以在后续的处理参数时进行循环输出。

测试类如下

/**
     * 测试点:测试运行方法的信息
     *
     * 方法名称:queryUserInfo
     * 入参个数:2
     * 入参类型:java.lang.String、java.lang.String
     * 出参类型:java.lang.String
     * 出参结果:德莱联盟,王牌工程师。小傅哥(公众号:bugstack虫洞栈),申请出栈!
     * 方法耗时:316ms
     *
     * @throws Exception
     */
    @Test
    public void queryUserInfo2() throws Exception {
    
    
        DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
                .subclass(BizMethod.class)
                .method(ElementMatchers.named("queryUserInfo"))
                .intercept(MethodDelegation.to(MonitorDemoV2.class))
                .make();
        // 加载类
        Class<?> clazz = dynamicType.load(BizMethod.class.getClassLoader())
                .getLoaded();
        // 反射调用
        clazz.getMethod("queryUserInfo", String.class,
                String.class).invoke(clazz.newInstance(), "10001", "Adhl9dkl");

    }

3.4 获取入参内容

当我们能获取入参的基本描述以后,再者就是获取入参的内容。在一段方法执行的过程中,如果可以在必要的时候拿到当时入参的信息,那么就可以非常方便的进行排查异常快速定位问题。在这里我们会用到新的注解; @AllArguments 、 @Argument(0) ,一个用于获取全部参数,一个获取指定的参数

public class MonitorDemoV3 {
    
    

    @RuntimeType
    public static Object intercept(@Origin Method method, @AllArguments Object[] args, @Argument(0) Object arg0, @SuperCall Callable<?> callable) throws Exception {
    
    
        long start = System.currentTimeMillis();
        Object resObj = null;
        try {
    
    
            resObj = callable.call();
            return resObj;
        } finally {
    
    
            System.out.println("方法名称:" + method.getName());
            System.out.println("入参个数:" + method.getParameterCount());
            System.out.println("入参类型:" + method.getParameterTypes()[0].getTypeName() + "、" + method.getParameterTypes()[1].getTypeName());
            System.out.println("入参内容:" + arg0 + "、" + args[1]);
            System.out.println("出参类型:" + method.getReturnType().getName());
            System.out.println("出参结果:" + resObj);
            System.out.println("方法耗时:" + (System.currentTimeMillis() - start) + "ms");
        }
    }
}

与上面的代码块相比,多了参数的获取和打印。主要知道这个方法就可以很方便的获取入参的内
容。


    /**
     * 测试点:测试运行方法的信息
     *
     * 方法名称:queryUserInfo
     * 入参个数:2
     * 入参类型:java.lang.String、java.lang.String
     * 入参内容:10001、Adhl9dkl
     * 出参类型:java.lang.String
     * 出参结果:德莱联盟,王牌工程师。小傅哥(公众号:bugstack虫洞栈),申请出栈!
     * 方法耗时:293ms
     *
     * @throws Exception
     */
    @Test
    public void queryUserInfo3() throws Exception {
    
    
        DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
                .subclass(BizMethod.class)
                .method(ElementMatchers.named("queryUserInfo"))
                .intercept(MethodDelegation.to(MonitorDemoV3.class))
                .make();
        // 加载类
        Class<?> clazz = dynamicType.load(BizMethod.class.getClassLoader())
                .getLoaded();
        // 反射调用
        clazz.getMethod("queryUserInfo", String.class,
                String.class).invoke(clazz.newInstance(), "10001", "Adhl9dkl");

    }

4.其他注解汇总

除了以上为了获取方法的执行信息使用到的注解外, Byte Buddy 还提供了很多其他的注解。如下;

在这里插入图片描述

5.常用核心API

1. ByteBuddy

  1. 流式API方式的入口类
  2. 提供Subclassing/Redefining/Rebasing方式改写字节码
  3. 所有的操作依赖DynamicType.Builder进行,创建不可变的对象

2. ElementMatchers(ElementMatcher)

  1. 提供一系列的元素匹配的工具类(named/any/nameEndsWith等等)
  2. ElementMatcher(提供对类型、方法、字段、注解进行matches的方式,类似于Predicate)
  3. Junction对多个ElementMatcher进行了and/or操作

3. DynamicType (动态类型,所有字节码操作的开始,非常值得关注)

  1. Unloaded(动态创建的字节码还未加载进入到虚拟机,需要类加载器进行加载)
  2. Loaded(已加载到jvm中后,解析出Class表示)
  3. Default(DynamicType的默认实现,完成相关实际操作)

4. Implementation (用于提供动态方法的实现)

  1. FixedValue(方法调用返回固定值)
  2. MethodDelegation(方法调用委托,支持两种方式: Class的static方法调用、object的instance method方法调用)

5. Builder (用于创建DynamicType,相关接口以及实现后续待详解)

  1. MethodDefinition
  2. FieldDefinition
  3. AbstractBase

6.总结

在这一章节的实现过程来看,只要知道相关API就可以很方便的解决我们的监控方法信息的诉求,他所处理的方式非常易于使用。而在本章节中也要学会几个关键知识点;委托、方法注解、返回值注解以及入参注解。

当我们学会了监控的核心功能,在后续与 Javaagent 结合使用时就可以很容易扩展进去,而不是看到了陌生的代码。对于这一部分非入侵的入侵链路监控,也是目前比较热门的话题和需要探索的解决方案,就像最近阿里云也举办了类似的编程挑战赛。首届云原生编程挑战赛1:实现一个分布式统计和过滤的链路追踪

最佳的学习体验和方式是,在学习和探索的过程中不断的对知识进行深度的学习,通过一个个实践的方式让知识成结构化和体系的建设。

猜你喜欢

转载自blog.csdn.net/qq_21383435/article/details/125475552