乐趣区

关于java:调研字节码插桩技术用于互联网分布式系统监控设计和实现

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

积淀、分享、成长,让本人和别人都能有所播种!😄

一、来自深夜的电话!

咋滴,你那上线的零碎是裸奔呢?

周末酣睡的深夜,忽然接到老板电话☎的督促。“连忙看微信、看微信,咋零碎出问题了,咱们都不晓得,还得用户反馈才晓得的!!!”深夜爬起来,关上电脑连上 VPN,打着哈欠、睁开朦胧的眼睛,查查系统日志,原来是零碎挂了,连忙重启复原!

尽管重启复原了零碎,也重置了老板扭曲的表情。但零碎是怎么挂的呢,因为没有一个监控零碎,也不晓得是流量太大导致,还是因为程序问题引起,通过一片片的日志,也仅能粗略预计出一些打着 如同的标签 给老板汇报。不过老板 也不傻,聊来聊去,让把所有的零碎运行状况都监控进去。

双手拖着困倦的脑袋,一时半会也想不出什么好办法,难道在每个办法上都硬编码上执行耗时计算。之后把信息在对立收集起来,展现到一个监控页面呢,监控页面应用阿帕奇的 echarts,别说要是这样显示了,还真能挺难看还好用。

  • 但这么硬编码也不叫玩意呀,这不把咱们部门搬砖的码农累岔气呀!再说了,这么干他们必定瞧不起我。啥架构师,要监控零碎,还得硬编码,傻了不是!!!
  • 这么一想整的没法睡觉,得找找材料,今天给老板汇报!

其实一套线上零碎是否稳固运行,取决于它的运行衰弱度,而这包含;调用量、可用率、影响时长以及服务器性能等各项指标的一个综合值。并且在零碎出现异常问题时,能够抓取整个业务办法执行链路并输入;过后的入参、出参、异样信息等等。当然还包含一些 JVM、Redis、Mysql 的各项性能指标,以用于疾速定位并解决问题。

那么要做到这样的事件有什么解决计划呢,其实做法还是比拟多的,比方;

  1. 最简略粗犷的就是硬编码在办法中,收取执行耗时以及出入参和异样信息。但这样的编码老本切实太大,而且硬编码完还须要大量回归测试,可能给零碎带来肯定的危险。万一谁手抖给复制粘贴错了呢!
  2. 能够抉择切面形式做一套对立监控的组件,相对来说还是好一些的。但也须要硬编码,比方写入注解,同时保护老本也不低。
  3. 其实市面上对于这样的监控其实是有整套的非入侵监控计划的,比方;Google Dapper、Zipkin 等都能够实现监控零碎需要,他们都是基于探针技术非入侵的采纳字节码加强的形式采集零碎运行信息进行剖析和监控运行状态。

好,那么本文就来带着大家来尝试下几种不同形式,监控零碎运行状态的实现思路。

二、筹备工作

本文会基于 AOP、字节码框架(ASMJavassistByte-Buddy),别离实现不同的监控实现代码。整个工程构造如下:

MonitorDesign
├── cn-bugstack-middleware-aop
├── cn-bugstack-middleware-asm
├── cn-bugstack-middleware-bytebuddy
├── cn-bugstack-middleware-javassist
├── cn-bugstack-middleware-test
└──    pom.xml
  • 源码地址:https://github.com/fuzhengwei/MonitorDesign
  • 简略介绍:aop、asm、bytebuddy、javassist,别离是四种不同的实现计划。test 是一个基于 SpringBoot 的简略测试工程。
  • 技术应用:SpringBoot、asm、byte-buddy、javassist

cn-bugstack-middleware-test

@RestController
public class UserController {private Logger logger = LoggerFactory.getLogger(UserController.class);

    /**
     * 测试:http://localhost:8081/api/queryUserInfo?userId=aaa
     */
    @RequestMapping(path = "/api/queryUserInfo", method = RequestMethod.GET)
    public UserInfo queryUserInfo(@RequestParam String userId) {logger.info("查问用户信息,userId:{}", userId);
        return new UserInfo("虫虫:" + userId, 19, "天津市东丽区万科赏溪苑 14-0000");
    }

}
  • 接下来的各类监控代码实现,都会以监控 UserController#queryUserInfo 的办法执行信息为主,看看各类技术都是怎么操作的。

三、应用 AOP 做个切面监控

1. 工程构造

cn-bugstack-middleware-aop
└── src
    ├── main
    │   └── java
    │       ├── cn.bugstack.middleware.monitor
    │       │   ├── annotation
    │       │   │   └── DoMonitor.java
    │       │   ├── config
    │       │   │   └── MonitorAutoConfigure.java
    │       │   └── DoJoinPoint.java
    │       └── resources
    │           └── META-INF 
    │               └── spring.factories
    └── test
        └── java
            └── cn.bugstack.middleware.monitor.test
                └── ApiTest.java

基于 AOP 实现的监控零碎,外围逻辑的以上工程并不简单,其外围点在于对切面的了解和使用,以及一些配置项须要依照 SpringBoot 中的实现形式进行开发。

  • DoMonitor,是一个自定义注解。它作用就是在须要应用到的办法监控接口上,增加此注解并配置必要的信息。
  • MonitorAutoConfigure,配置下是能够对 SpringBoot yml 文件的应用,能够解决一些 Bean 的初始化操作。
  • DoJoinPoint,是整个中间件的外围局部,它负责对所有增加自定义注解的办法进行拦挡和逻辑解决。

2. 定义监控注解

cn.bugstack.middleware.monitor.annotation.DoMonitor

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DoMonitor {String key() default "";
   String desc() default "";}
  • @Retention(RetentionPolicy.RUNTIME),Annotations are to be recorded in the class file by the compiler and retained by the VM at run time, so they may be read reflectively.
  • @Retention 是注解的注解,也称作元注解。这个注解外面有一个入参信息 RetentionPolicy.RUNTIME 在它的正文中有这样一段形容:Annotations are to be recorded in the class file by the compiler and retained by the VM at run time, so they may be read reflectively. 其实说的就是加了这个注解,它的信息会被带到 JVM 运行时,当你在调用办法时能够通过反射拿到注解信息。除此之外,RetentionPolicy 还有两个属性 SOURCECLASS,其实这三个枚举正式对应了 Java 代码的加载和运行程序,Java 源码文件 -> .class 文件 -> 内存字节码。并且后者范畴大于前者,所以个别状况下只须要应用 RetentionPolicy.RUNTIME 即可。
  • @Target 也是元注解起到标记作用,它的注解名称就是它的含意,指标 ,也就是咱们这个自定义注解 DoWhiteList 要放在类、接口还是办法上。 在 JDK1.8 中 ElementType 一共提供了 10 中指标枚举,TYPE、FIELD、METHOD、PARAMETER、CONSTRUCTOR、LOCAL_VARIABLE、ANNOTATION_TYPE、PACKAGE、TYPE_PARAMETER、TYPE_USE,能够参考本人的自定义注解作用域进行设置
  • 自定义注解 @DoMonitor 提供了监控的 key 和 desc 形容,这个次要记录你监控办法的为惟一值配置和对监控办法的文字描述。

3. 定义切面拦挡

cn.bugstack.middleware.monitor.DoJoinPoint

@Aspect
public class DoJoinPoint {@Pointcut("@annotation(cn.bugstack.middleware.monitor.annotation.DoMonitor)")
    public void aopPoint() {}

    @Around("aopPoint() && @annotation(doMonitor)")
    public Object doRouter(ProceedingJoinPoint jp, DoMonitor doMonitor) throws Throwable {long start = System.currentTimeMillis();
        Method method = getMethod(jp);
        try {return jp.proceed();
        } finally {System.out.println("监控 - Begin By AOP");
            System.out.println("监控索引:" + doMonitor.key());
            System.out.println("监控形容:" + doMonitor.desc());
            System.out.println("办法名称:" + method.getName());
            System.out.println("办法耗时:" + (System.currentTimeMillis() - start) + "ms");
            System.out.println("监控 - End\r\n");
        }
    }

    private Method getMethod(JoinPoint jp) throws NoSuchMethodException {Signature sig = jp.getSignature();
        MethodSignature methodSignature = (MethodSignature) sig;
        return jp.getTarget().getClass().getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
    }

}
  • 应用注解 @Aspect,定义切面类。这是一个十分罕用的切面定义形式。
  • @Pointcut("@annotation(cn.bugstack.middleware.monitor.annotation.DoMonitor)"),定义切点。在 Pointcut 中提供了很多的切点寻找形式,有指定办法名称的、有范畴筛选表达式的,也有咱们当初通过自定义注解形式的。个别在中间件开发中,自定义注解形式应用的比拟多,因为它能够更加灵便的使用到各个业务零碎中。
  • @Around("aopPoint() && @annotation(doMonitor)"),能够了解为是对办法加强的织入动作,有了这个注解的成果就是在你调用曾经加了自定义注解 @DoMonitor 的办法时,会先进入到此切点加强的办法。那么这个时候就你能够做一些对办法的操作动作了,比方咱们要做一些办法监控和日志打印等。
  • 最初在 doRouter 办法体中获取把办法执行 jp.proceed(); 应用 try finally 包装起来,并打印相干的监控信息。这些监控信息的获取最初都是能够通过异步音讯的形式发送给服务端,再由服务器进行解决监控数据和解决展现到监控页面。

4. 初始化切面类

cn.bugstack.middleware.monitor.config.MonitorAutoConfigure

@Configuration
public class MonitorAutoConfigure {

    @Bean
    @ConditionalOnMissingBean
    public DoJoinPoint point(){return new DoJoinPoint();
    }

}
  • @Configuration,能够算作是一个组件注解,在 SpringBoot 启动时能够进行加载创立出 Bean 文件。因为 @Configuration 注解有一个 @Component 注解
  • MonitorAutoConfigure 能够解决自定义在 yml 中的配置信息,也能够用于初始化 Bean 对象,比方在这里咱们实例化了 DoJoinPoint 切面对象。

5. 运行测试

5.1 引入 POM 配置

<!-- 监控形式:AOP -->
<dependency>
    <groupId>cn.bugstack.middleware</groupId>
    <artifactId>cn-bugstack-middleware-aop</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

5.2 办法上配置监控注册

@DoMonitor(key = "cn.bugstack.middleware.UserController.queryUserInfo", desc = "查问用户信息")
@RequestMapping(path = "/api/queryUserInfo", method = RequestMethod.GET)
public UserInfo queryUserInfo(@RequestParam String userId) {logger.info("查问用户信息,userId:{}", userId);
    return new UserInfo("虫虫:" + userId, 19, "天津市东丽区万科赏溪苑 14-0000");
}
  • 在通过 POM 引入本人的开发的组件后,就能够通过自定义的注解,拦挡办法获取监控信息。

5.3 测试后果

2021-07-04 23:21:10.710  INFO 19376 --- [nio-8081-exec-1] c.b.m.test.interfaces.UserController     : 查问用户信息,userId:aaa
监控 - Begin By AOP
监控索引:cn.bugstack.middleware.UserController.queryUserInfo
监控形容:查问用户信息
办法名称:queryUserInfo
办法耗时:6ms
监控 - End
  • 通过启动 SpringBoot 程序,在网页中关上 URL 地址:http://localhost:8081/api/queryUserInfo?userId=aaa,能够看到曾经能够把监控信息打印到控制台了。
  • 此种通过自定义注解的配置形式,能解决肯定的硬编码工作,但如果在办法上大量的增加注解,也是须要肯定的开发工作的。

接下来咱们开始介绍对于应用字节码插桩非入侵的形式进行系统监控,对于字节码插桩罕用的有三个组件,包含:ASM、Javassit、Byte-Buddy,接下来咱们别离介绍它们是如何应用的。

四、ASM

ASM 是一个 Java 字节码操控框架。它能被用来动静生成类或者加强既有类的性能。ASM 能够间接产生二进制 class 文件,也能够在类被加载入 Java 虚拟机之前动静扭转类行为。Java class 被存储在严格格局定义的 .class 文件里,这些类文件领有足够的元数据来解析类中的所有元素:类名称、办法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,可能扭转类行为,剖析类信息,甚至可能依据用户要求生成新类。

1. 先来个测试

cn.bugstack.middleware.monitor.test.ApiTest

private static byte[] generate() {ClassWriter classWriter = new ClassWriter(0);
    // 定义对象头;版本号、修饰符、全类名、签名、父类、实现的接口
    classWriter.visit(Opcodes.V1_7, Opcodes.ACC_PUBLIC, "cn/bugstack/demo/asm/AsmHelloWorld", null, "java/lang/Object", null);
    // 增加办法;修饰符、办法名、描述符、签名、异样
    MethodVisitor methodVisitor = classWriter.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
    // 执行指令;获取动态属性
    methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
    // 加载常量 load constant
    methodVisitor.visitLdcInsn("Hello World ASM!");
    // 调用办法
    methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
    // 返回
    methodVisitor.visitInsn(Opcodes.RETURN);
    // 设置操作数栈的深度和局部变量的大小
    methodVisitor.visitMaxs(2, 1);
    // 办法完结
    methodVisitor.visitEnd();
    // 类实现
    classWriter.visitEnd();
    // 生成字节数组
    return classWriter.toByteArray();}
  • 以上这段代码就是基于 ASM 编写的 HelloWorld,整个过程包含:定义一个类的生成 ClassWriter、设定版本、修饰符、全类名、签名、父类、实现的接口,其实也就是那句;public class HelloWorld
  • 类型描述符:

    | Java 类型 | 类型描述符 |
    | :——— | :——————- |
    | boolean | Z |
    | char | C |
    | byte | B |
    | short | S |
    | int | I |
    | float | F |
    | long | J |
    | double | D |
    | Object | Ljava/lang/Object; |
    | int[] | [I |
    | Object[][] | [[Ljava/lang/Object; |

  • 办法描述符:

    | 源文件中的办法申明 | 办法描述符 |
    | :———————– | :———————- |
    | void m(int i, float f) | (IF)V |
    | int m(Object o) | (Ljava/lang/Object;)I |
    | int[] m(int i, String s) | (ILjava/lang/String;)[I |
    | Object m(int[] i) | ([I)Ljava/lang/Object; |

  • 执行指令;获取动态属性。次要是取得 System.out
  • 加载常量 load constant,输入咱们的 HelloWorld methodVisitor.visitLdcInsn("Hello World");
  • 最初是调用输入办法并设置空返回,同时在结尾要设置操作数栈的深度和局部变量的大小。
  • 这样输入一个 HelloWorld 是不还是蛮有意思的,尽管你可能感觉这编码起来切实太难了吧,也十分难了解。不过你能够装置一个 ASM 在 IDEA 中的插件 ASM Bytecode Outline,能更加不便的查看一个一般的代码在应用 ASM 的形式该如何解决。
  • 另外以上这段代码的测试后果,次要是生成一个 class 文件和输入 Hello World ASM! 后果。

2. 监控设计工程构造

cn-bugstack-middleware-asm
└── src
    ├── main
    │   ├── java
    │   │   └── cn.bugstack.middleware.monitor
    │   │       ├── config
    │   │       │   ├── MethodInfo.java
    │   │       │   └── ProfilingFilter.java
    │   │       ├── probe
    │   │       │   ├── ProfilingAspect.java
    │   │       │   ├── ProfilingClassAdapter.java
    │   │       │   ├── ProfilingMethodVisitor.java
    │   │       │   └── ProfilingTransformer.java
    │   │       └── PreMain.java
    │   └── resources    
    │       └── META_INF
    │           └── MANIFEST.MF
    └── test
        └── java
            └── cn.bugstack.middleware.monitor.test
                └── ApiTest.java

以上工程构造是应用 ASM 框架给零碎办法做加强操作,也就是相当于通过框架实现硬编码写入办法前后的监控信息。不过这个过程转移到了 Java 程序启动时在 Javaagent#premain 进行解决。

  • MethodInfo 是办法的定义,次要是形容类名、办法名、形容、入参、出参信息。
  • ProfilingFilter 是监控的配置信息,次要是过滤一些不须要字节码加强操作的办法,比方 main、hashCode、javax/ 等
  • ProfilingAspect、ProfilingClassAdapter、ProfilingMethodVisitor、ProfilingTransformer,这四个类次要是实现字节码插装操作和输入监控后果的类。
  • PreMain 提供了 Javaagent 的入口,JVM 首先尝试在代理类上调用 premain 办法。
  • MANIFEST.MF 是配置信息,次要是找到 Premain-Class Premain-Class: cn.bugstack.middleware.monitor.PreMain

3. 监控类入口

cn.bugstack.middleware.monitor.PreMain

public class PreMain {

    //JVM 首先尝试在代理类上调用以下办法
    public static void premain(String agentArgs, Instrumentation inst) {inst.addTransformer(new ProfilingTransformer());
    }

    // 如果代理类没有实现下面的办法,那么 JVM 将尝试调用该办法
    public static void premain(String agentArgs) {}}
  • 这个是 Javaagent 技术的固定入口办法类,同时还须要把这个类的门路配置到 MANIFEST.MF 中。

4. 字节码办法解决

cn.bugstack.middleware.monitor.probe.ProfilingTransformer

public class ProfilingTransformer implements ClassFileTransformer {

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        try {if (ProfilingFilter.isNotNeedInject(className)) {return classfileBuffer;}
            return getBytes(loader, className, classfileBuffer);
        } catch (Throwable e) {System.out.println(e.getMessage());
        }
        return classfileBuffer;
    }

    private byte[] getBytes(ClassLoader loader, String className, byte[] classfileBuffer) {ClassReader cr = new ClassReader(classfileBuffer);
        ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
        ClassVisitor cv = new ProfilingClassAdapter(cw, className);
        cr.accept(cv, ClassReader.EXPAND_FRAMES);
        return cw.toByteArray();}

}
  • 应用 ASM 外围类 ClassReader、ClassWriter、ClassVisitor,解决传入进行的类加载器、类名、字节码等,负责字节码的加强操作。
  • 此处次要是对于 ASM 的操作类,ClassReader、ClassWriter、ClassVisitor,对于字节码编程的文章:ASM、Javassist、Byte-bu 系列文章

5. 字节码办法解析

cn.bugstack.middleware.monitor.probe.ProfilingMethodVisitor

public class ProfilingMethodVisitor extends AdviceAdapter {private List<String> parameterTypeList = new ArrayList<>();
    private int parameterTypeCount = 0;     // 参数个数
    private int startTimeIdentifier;        // 启动工夫标记
    private int parameterIdentifier;        // 入参内容标记
    private int methodId = -1;              // 办法全局惟一标记
    private int currentLocal = 0;           // 以后局部变量值
    private final boolean isStaticMethod;   // true;静态方法,false;非静态方法
    private final String className;

    protected ProfilingMethodVisitor(int access, String methodName, String desc, MethodVisitor mv, String className, String fullClassName, String simpleClassName) {super(ASM5, mv, access, methodName, desc);
        this.className = className;
        // 判断是否为静态方法,非静态方法中局部变量第一个值是 this,静态方法是第一个入参参数
        isStaticMethod = 0 != (access & ACC_STATIC);
        //(String var1,Object var2,String var3,int var4,long var5,int[] var6,Object[][] var7,Req var8)=="(Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;IJ[I[[Ljava/lang/Object;Lorg/itstack/test/Req;)V"
        Matcher matcher = Pattern.compile("(L.*?;|\\[{0,2}L.*?;|[ZCBSIFJD]|\\[{0,2}[ZCBSIFJD]{1})").matcher(desc.substring(0, desc.lastIndexOf(')') + 1));
        while (matcher.find()) {parameterTypeList.add(matcher.group(1));
        }
        parameterTypeCount = parameterTypeList.size();
        methodId = ProfilingAspect.generateMethodId(new MethodInfo(fullClassName, simpleClassName, methodName, desc, parameterTypeList, desc.substring(desc.lastIndexOf(')') + 1)));
    }     

    //... 一些字节码插桩操作 
}
  • 当程序启动加载的时候,每个类的每一个办法都会被监控到。类的名称、办法的名称、办法入参出参的形容等,都能够在这里获取。
  • 为了能够在后续监控解决不至于每一次都去传参(办法信息)节约耗费性能,个别这里都会给每个办法生产一个全局防重的 id,通过这个 id 就能够查问到对应的办法。
  • 另外从这里能够看到的办法的入参和出参被形容成一段指定的码,(II)Ljava/lang/String;,为了咱们后续对参数进行解析,那么须要将这段字符串进行拆解。

6. 运行测试

6.1 配置 VM 参数 Javaagent

-javaagent:E:\itstack\git\github.com\MonitorDesign\cn-bugstack-middleware-asm\target\cn-bugstack-middleware-asm.jar
  • IDEA 运行时候配置到 VM options 中,jar 包地址依照本人的门路进行配置。

6.2 测试后果

监控 - Begin By ASM
办法:cn.bugstack.middleware.test.interfaces.UserController$$EnhancerBySpringCGLIB$$8f5a18ca.queryUserInfo
入参:null 入参类型:["Ljava/lang/String;"] 入数[值]:["aaa"]
出参:Lcn/bugstack/middleware/test/interfaces/dto/UserInfo; 出参[值]:{"address":"天津市东丽区万科赏溪苑 14-0000","age":19,"code":"0000","info":"success","name":"虫虫:aaa"}
耗时:54(s)
监控 - End
  • 从运行测试后果能够看到,在应用 ASM 监控后,就不须要硬编码也不须要 AOP 的形式在代码中操作了。同时还能够监控到更残缺的办法执行信息,包含入参类型、入参值和出参信息、出参值。
  • 但可能大家会发现 ASM 操作起来还是挺麻烦的,尤其是一些很简单的编码逻辑中,可能会遇到各种各样问题,因而接下来咱们还会介绍一些基于 ASM 开发的组件,这些组件也能够实现同样的性能。

五、Javassist

Javassist 是一个开源的剖析、编辑和创立 Java 字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba(千叶 滋)所创立的。它已退出了凋谢源代码 JBoss 应用服务器我的项目,通过应用 Javassist 对字节码操作为 JBoss 实现动静 ”AOP” 框架。

1. 先来个测试

cn.bugstack.middleware.monitor.test.ApiTest

public class ApiTest {public static void main(String[] args) throws Exception {ClassPool pool = ClassPool.getDefault();

        CtClass ctClass = pool.makeClass("cn.bugstack.middleware.javassist.MathUtil");

        // 属性字段
        CtField ctField = new CtField(CtClass.doubleType, "π", ctClass);
        ctField.setModifiers(Modifier.PRIVATE + Modifier.STATIC + Modifier.FINAL);
        ctClass.addField(ctField, "3.14");

        // 办法:求圆面积
        CtMethod calculateCircularArea = new CtMethod(CtClass.doubleType, "calculateCircularArea", new CtClass[]{CtClass.doubleType}, ctClass);
        calculateCircularArea.setModifiers(Modifier.PUBLIC);
        calculateCircularArea.setBody("{return π * $1 * $1;}");
        ctClass.addMethod(calculateCircularArea);

        // 办法;两数之和
        CtMethod sumOfTwoNumbers = new CtMethod(pool.get(Double.class.getName()), "sumOfTwoNumbers", new CtClass[]{CtClass.doubleType, CtClass.doubleType}, ctClass);
        sumOfTwoNumbers.setModifiers(Modifier.PUBLIC);
        sumOfTwoNumbers.setBody("{return Double.valueOf($1 + $2);}");
        ctClass.addMethod(sumOfTwoNumbers);
        // 输入类的内容
        ctClass.writeFile();

        // 测试调用
        Class clazz = ctClass.toClass();
        Object obj = clazz.newInstance();

        Method method_calculateCircularArea = clazz.getDeclaredMethod("calculateCircularArea", double.class);
        Object obj_01 = method_calculateCircularArea.invoke(obj, 1.23);
        System.out.println("圆面积:" + obj_01);

        Method method_sumOfTwoNumbers = clazz.getDeclaredMethod("sumOfTwoNumbers", double.class, double.class);
        Object obj_02 = method_sumOfTwoNumbers.invoke(obj, 1, 2);
        System.out.println("两数和:" + obj_02);
    }

}
  • 这是一个应用 Javassist 生成的求圆面积和形象的类和办法并运行后果的过程,能够看到 Javassist 次要是 ClassPool、CtClass、CtField、CtMethod 等办法的应用。
  • 测试后果次要包含会生成一个指定门路下的类 cn.bugstack.middleware.javassist.MathUtil,同时还会在控制台输入后果。

生成的类

public class MathUtil {
  private static final double π = 3.14D;

  public double calculateCircularArea(double var1) {return 3.14D * var1 * var1;}

  public Double sumOfTwoNumbers(double var1, double var3) {return var1 + var3;}

  public MathUtil() {}
}

测试后果

圆面积:4.750506
两数和:3.0

Process finished with exit code 0

2. 监控设计工程构造

cn-bugstack-middleware-javassist
└── src
    ├── main
    │   ├── java
    │   │   └── cn.bugstack.middleware.monitor
    │   │       ├── config
    │   │       │   └── MethodDescription.java
    │   │       ├── probe
    │   │       │   ├── Monitor.java
    │   │       │   └── MyMonitorTransformer.java
    │   │       └── PreMain.java
    │   └── resources
    │       └── META_INF
    │           └── MANIFEST.MF
    └── test
        └── java
            └── cn.bugstack.middleware.monitor.test
                └── ApiTest.java
  • 整个应用 javassist 实现的监控框架来看,与 ASM 的构造十分类似,但大部分操作字节码的工作都交给了 javassist 框架来解决,所以整个代码构造看上去更简略了。

3. 监控办法插桩

cn.bugstack.middleware.monitor.probe.MyMonitorTransformer

public class MyMonitorTransformer implements ClassFileTransformer {private static final Set<String> classNameSet = new HashSet<>();

    static {classNameSet.add("cn.bugstack.middleware.test.interfaces.UserController");
    }

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
        try {String currentClassName = className.replaceAll("/", ".");
            if (!classNameSet.contains(currentClassName)) { // 晋升 classNameSet 中含有的类
                return null;
            }

            // 获取类
            CtClass ctClass = ClassPool.getDefault().get(currentClassName);
            String clazzName = ctClass.getName();

            // 获取办法
            CtMethod ctMethod = ctClass.getDeclaredMethod("queryUserInfo");
            String methodName = ctMethod.getName();

            // 办法信息:methodInfo.getDescriptor();
            MethodInfo methodInfo = ctMethod.getMethodInfo();

            // 办法:入参信息
            CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
            LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
            CtClass[] parameterTypes = ctMethod.getParameterTypes();

            boolean isStatic = (methodInfo.getAccessFlags() & AccessFlag.STATIC) != 0;  // 判断是否为静态方法
            int parameterSize = isStatic ? attr.tableLength() : attr.tableLength() - 1; // 动态类型取值
            List<String> parameterNameList = new ArrayList<>(parameterSize);            // 入参名称
            List<String> parameterTypeList = new ArrayList<>(parameterSize);            // 入参类型
            StringBuilder parameters = new StringBuilder();                             // 参数组装;$1、$2...,$$ 能够获取全副,然而不能放到数组初始化

            for (int i = 0; i < parameterSize; i++) {parameterNameList.add(attr.variableName(i + (isStatic ? 0 : 1))); // 动态类型去掉第一个 this 参数
                parameterTypeList.add(parameterTypes[i].getName());
                if (i + 1 == parameterSize) {parameters.append("$").append(i + 1);
                } else {parameters.append("$").append(i + 1).append(",");
                }
            }

            // 办法:出参信息
            CtClass returnType = ctMethod.getReturnType();
            String returnTypeName = returnType.getName();

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

            // 定义属性
            ctMethod.addLocalVariable("startNanos", CtClass.longType);
            ctMethod.addLocalVariable("parameterValues", ClassPool.getDefault().get(Object[].class.getName()));

            // 办法前增强
            ctMethod.insertBefore("{ startNanos = System.nanoTime(); parameterValues = new Object[]{" + parameters.toString() + "}; }");

            // 办法后增强
            ctMethod.insertAfter("{ cn.bugstack.middleware.monitor.probe.Monitor.point(" + idx + ", startNanos, parameterValues, $_);}", false); // 如果返回类型非对象类型,$_ 须要进行类型转换

            // 办法;增加 TryCatch
            ctMethod.addCatch("{ cn.bugstack.middleware.monitor.probe.Monitor.point(" + idx + ", $e); throw $e; }", ClassPool.getDefault().get("java.lang.Exception"));   // 增加异样捕捉

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

}
  • 与 ASM 实现相比,整体的监控办法都是相似的,所以这里只展现下不同的中央。
  • 通过 Javassist 的操作,次要是实现一个 ClassFileTransformer 接口的 transform 办法,在这个办法中获取字节码并进行相应的解决。
  • 处理过程包含:获取类、获取办法、获取入参信息、获取出参信息、给办法生成惟一 ID、之后开始进行办法的前后加强操作,这个加强也就是在办法块中增加监控代码。
  • 最初返回字节码信息 return ctClass.toBytecode(); 当初你新退出的字节码就曾经能够被程序加载解决了。

4. 运行测试

4.1 配置 VM 参数 Javaagent

-javaagent:E:\itstack\git\github.com\MonitorDesign\cn-bugstack-middleware-javassist\target\cn-bugstack-middleware-javassist.jar
  • IDEA 运行时候配置到 VM options 中,jar 包地址依照本人的门路进行配置。

4.2 测试后果

监控 -  Begin By Javassist
办法:cn.bugstack.middleware.test.interfaces.UserController$$EnhancerBySpringCGLIB$$8f5a18ca.queryUserInfo
入参:null 入参类型:["Ljava/lang/String;"] 入数[值]:["aaa"]
出参:Lcn/bugstack/middleware/test/interfaces/dto/UserInfo; 出参[值]:{"address":"天津市东丽区万科赏溪苑 14-0000","age":19,"code":"0000","info":"success","name":"虫虫:aaa"}
耗时:46(s)
监控 - End
  • 从测试后果来看与 ASM 做字节码插桩的成果是一样,都能够做到监控零碎执行信息。然而这样的框架会使开发流程更简略,也更容易管制。

六、Byte-Buddy

2015 年 10 月,Byte Buddy 被 Oracle 授予了 Duke’s Choice 大奖。该奖项对 Byte Buddy 的“Java 技术方面的微小翻新”示意赞叹。咱们为取得此奖项感到十分荣幸,并感激所有帮忙 Byte Buddy 取得成功的用户以及其余所有人。咱们真的很感谢!

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

  • 无需了解字节码指令,即可应用简略的 API 就能很容易操作字节码,管制类和办法。
  • 已反对 Java 11,库轻量,仅取决于 Java 字节代码解析器库 ASM 的访问者 API,它自身不须要任何其余依赖项。
  • 比起 JDK 动静代理、cglib、Javassist,Byte Buddy 在性能上具备肯定的劣势。

1. 先来个测试

cn.bugstack.middleware.monitor.test.ApiTest

public class ApiTest {public static void main(String[] args) throws IllegalAccessException, InstantiationException {String helloWorld = new ByteBuddy()
                .subclass(Object.class)
                .method(named("toString"))
                .intercept(FixedValue.value("Hello World!"))
                .make()
                .load(ApiTest.class.getClassLoader())
                .getLoaded()
                .newInstance()
                .toString();

        System.out.println(helloWorld);
    }

}
  • 这是一个应用 ByteBuddy 语法生成的 “Hello World!” 案例,他的运行后果就是一行,Hello World!,整个代码块外围性能就是通过 method(named("toString")),找到 toString 办法,再通过拦挡 intercept,设定此办法的返回值。FixedValue.value("Hello World!")。到这里其实一个根本的办法就通过 Byte-buddy,最初加载、初始化和调用输入。

测试后果

Hello World!

Process finished with exit code 0

2. 监控设计工程构造

cn-bugstack-middleware-bytebuddy
└── src
    ├── main
    │   ├── java
    │   │   └── cn.bugstack.middleware.monitor
    │   │       ├── MonitorMethod
    │   │       └── PreMain.java
    │   └── resources
    │       └── META_INF
    │           └── MANIFEST.MF
    └── test
        └── java
            └── cn.bugstack.middleware.monitor.test
                └── ApiTest.java
  • 这是我集体最喜爱的一个框架,因为它操作的方便性,能够像应用一般的业务代码一样应用字节码加强的操作。从当初的工程构造你能看得出来,代码类数量越来越少了。

3. 监控办法插桩

cn.bugstack.middleware.monitor.MonitorMethod

public class MonitorMethod {

    @RuntimeType
    public static Object intercept(@Origin Method method, @SuperCall Callable<?> callable, @AllArguments Object[] args) throws Exception {long start = System.currentTimeMillis();
        Object resObj = null;
        try {resObj = callable.call();
            return resObj;
        } finally {System.out.println("监控 - Begin By Byte-buddy");
            System.out.println("办法名称:" + method.getName());
            System.out.println("入参个数:" + method.getParameterCount());
            for (int i = 0; i < method.getParameterCount(); i++) {System.out.println("入参 Idx:" + (i + 1) + "类型:" + method.getParameterTypes()[i].getTypeName() + "内容:" + args[i]);
            }
            System.out.println("出参类型:" + method.getReturnType().getName());
            System.out.println("出参后果:" + resObj);
            System.out.println("办法耗时:" + (System.currentTimeMillis() - start) + "ms");
            System.out.println("监控 - End\r\n");
        }
    }

}
  • @Origin,用于拦挡原有办法,这样就能够获取到办法中的相干信息。
  • 这一部分的信息相对来说比拟全,尤其也获取到了参数的个数和类型,这样就能够在后续的解决参数时进行循环输入。

罕用注解阐明

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

注解 阐明
@Argument 绑定单个参数
@AllArguments 绑定所有参数的数组
@This 以后被拦挡的、动静生成的那个对象
@Super 以后被拦挡的、动静生成的那个对象的父类对象
@Origin 能够绑定到以下类型的参数:Method 被调用的原始办法 Constructor 被调用的原始结构器 Class 以后动态创建的类 MethodHandle MethodType String 动静类的 toString()的返回值 int 动静办法的修饰符
@DefaultCall 调用默认办法而非 super 的办法
@SuperCall 用于调用父类版本的办法
@Super 注入父类型对象,能够是接口,从而调用它的任何办法
@RuntimeType 能够用在返回值、参数上,提醒 ByteBuddy 禁用严格的类型查看
@Empty 注入参数的类型的默认值
@StubValue 注入一个存根值。对于返回援用、void 的办法,注入 null;对于返回原始类型的办法,注入 0
@FieldValue 注入被拦挡对象的一个字段的值
@Morph 相似于 @SuperCall,然而容许指定调用参数

罕用外围 API

  1. ByteBuddy

    • 流式 API 形式的入口类
    • 提供 Subclassing/Redefining/Rebasing 形式改写字节码
    • 所有的操作依赖 DynamicType.Builder 进行, 创立不可变的对象
  2. ElementMatchers(ElementMatcher)

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

    (动静类型, 所有字节码操作的开始, 十分值得关注)

    • Unloaded(动态创建的字节码还未加载进入到虚拟机, 须要类加载器进行加载)
    • Loaded(已加载到 jvm 中后, 解析出 Class 示意)
    • Default(DynamicType 的默认实现, 实现相干实际操作)
  4. `Implementation

    (用于提供动静办法的实现)

    • FixedValue(办法调用返回固定值)
    • MethodDelegation(办法调用委托, 反对两种形式: Class 的 static 办法调用、object 的 instance method 办法调用)
  5. Builder

    (用于创立 DynamicType, 相干接口以及实现后续待详解)

    • MethodDefinition
    • FieldDefinition
    • AbstractBase

4. 配置入口办法

cn.bugstack.middleware.monitor.PreMain

public class PreMain {

    //JVM 首先尝试在代理类上调用以下办法
    public static void premain(String agentArgs, Instrumentation inst) {AgentBuilder.Transformer transformer = (builder, typeDescription, classLoader, javaModule) -> {
            return builder
                    .method(ElementMatchers.named("queryUserInfo")) // 拦挡任意办法
                    .intercept(MethodDelegation.to(MonitorMethod.class)); // 委托
        };

        new AgentBuilder
                .Default()
                .type(ElementMatchers.nameStartsWith(agentArgs))  // 指定须要拦挡的类 "cn.bugstack.demo.test"
                .transform(transformer)
                .installOn(inst);
    }

    // 如果代理类没有实现下面的办法,那么 JVM 将尝试调用该办法
    public static void premain(String agentArgs) {}}
  • premain 办法中次要是对实现的 MonitorMethod 进行委托应用,同时还在 method 设置了拦挡的办法,这个拦挡办法还能够到类门路等。

5. 运行测试

5.1 配置 VM 参数 Javaagent

-javaagent:E:\itstack\git\github.com\MonitorDesign\cn-bugstack-middleware-bytebuddy\target\cn-bugstack-middleware-bytebuddy.jar
  • IDEA 运行时候配置到 VM options 中,jar 包地址依照本人的门路进行配置。

5.2 测试后果

监控 - Begin By Byte-buddy
办法名称:queryUserInfo
入参个数:1
入参 Idx:1 类型:java.lang.String 内容:aaa
出参类型:cn.bugstack.middleware.test.interfaces.dto.UserInfo
出参后果:cn.bugstack.middleware.test.interfaces.dto.@214b199c
办法耗时:1ms
监控 - End
  • Byte-buddy 是咱们整个测试过程的几个字节码框架中,操作起来最简略,最不便的,也非常容易扩容信息。整个过程就像最后应用 AOP 一样简略,但却满足了非入侵的监控需要。
  • 所以在应用字节码框架的时候,能够思考抉择应用 Byte-buddy 这个十分好用的字节码框架。

七、总结

  • ASM 这种字节码编程的利用是十分广的,但可能的确平时看不到的,因为他都是与其余框架联合一起作为撑持服务应用。像这样的技术还有很多,比方 javassit、Cglib、jacoco 等等。
  • 在一些全链路监控中的组件中 Javassist 的应用十分多,它即可应用编码的形式操作字节码加强,也能够像 ASM 那样进行解决。
  • Byte-buddy 是一个十分不便的框架,目前应用也越来越宽泛,并且上手应用的学习难度也是几个框架中最低的。除了本章节的案例应用介绍外,还能够通过官网:https://bytebuddy.net,去理解更多对于 Byte Buddy 的内容。
  • 本章节所有的源码曾经上传到 GitHub:https://github.com/fuzhengwei/MonitorDesign

八、系列举荐

  • 毕业前写了 20 万行代码,让我从成为同学眼里的面霸!
  • 《Spring 手撸专栏》开篇介绍,我要带你撸 Spring 啦!
  • 《SpringBoot 中间件设计和开发》,这次教你造火箭!
  • 一本互联网实战案例的技术书籍《重学 Java 设计模式》
  • 《Java 面经手册》PDF,全书 417 页 11.5 万字,完稿 & 发版!
退出移动版