从利用Arthas排查线上Fastjson问题到Java动态字节码技术(下)

2023-09-17 08:34:16

上一篇从Arthas的源码引出了Java动态字节码技术,那么这一篇就从几种Java字节码技术出发,看看Arthas是如何通过动态字节码技术做到无侵入的源码增强;


Java大部分情况下都是解释执行的,也就是解释.class文件,所以如果我们想对原代码进行增强的话,直接接的手段便是从源文件.java入手,使用静态代理、动态代理、装饰器等设计模式进行功能增强。但很多时候我们作为第三方,没有机会、不方便拿到源码时,这条路就走不通了;此时如果还是想继续其进行功能增强的话,那么只剩一条路了,就是直接对.class文件下手。

但对于二进制的.class文件,还有多少人能分清魔数、标识符、各种区域表示的内容呢?所以直接操作字节码也似乎不太现实,那有没有什么抓手呢?

ASM

ASM 是 assembly 汇编 的简称,所以它更偏底层、更贴近底层字节码,所以也就更难使用、更晦涩。

整体思路上是通过Reader将.class文件读取后,通过一系列API来增强源码,之后通过Writer生成新的.class文件。

个人不太建议使用这种方式,学习和使用成本都比较高,不直观,太晦涩。

Javaassist

相比于ASM,理解起来不要太简单,不需要了解底层虚拟机指令和晦涩的API,只需要掌握四个核心类即可 ClassPool、CtClass、CtMethod、CtField:

CtClass(compile-time class):编译时 - 类对象,Java中万物皆对象,CtClass是Class在编译时的对象,可以通过类的全限定名来获取到这个CtClass;

与编译时(compile)类对象相似的,我们平时用反射获取到的是运行时(Runtime)类对象;

ClassPool:可以把它简单的理解为一个HashMap,类的全限定名作为key,可以从中获取到CtClass

CtMethod & CtField:类中的方法和属性

利用以上四个类,可以快速实现对源码的增强,比如当你想实现AOP,在原方法执行前和执行后添加功能的时候,只需要先通过ClassPool获取到CtClass,再获取到CtMethod后,就可以调用该方法的insertBeforeinsertAfter,插入具体代码块了。

Instrument

Instrument是JVM提供的一个对已加载的类进行修改的接口(Interface),打开java.lang.Instrumentation可以看到清晰的说明。

首先Instrument是基于 Java Agent技术的,也就是上一篇中提到的,用 agentmainpremain attache java进程生成的 agent,也就是为 Instrument 在运行的进程中开了一个后门。

instrument接口中最重要的方法是 通过ClassFileTransformer 对原class进行 retransform

ClassFileTransformer中实现想要对原class的增强,该接口只有一个需要被子类实现的方法,就是transform方法:

package java.lang.instrument;

public interface ClassFileTransformer {
    byte[]
    transform(  ClassLoader         loader,
                String              className,
                Class<?>            classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer)
        throws IllegalClassFormatException;
}

举一个直观的例子,福报厂的ttl threadpool

package com.alibaba.ttl.threadpool.agent;

public class TtlTransformer implements ClassFileTransformer {
	
	//类似于责任链,可以添加一系列的 ClassFileTransformer
	//值得注意的是,这里使用到了Javassist
    private final List<JavassistTransformlet> transformletList = new ArrayList<JavassistTransformlet>();
    TtlTransformer(List<? extends JavassistTransformlet> transformletList) {
        for (JavassistTransformlet transformlet : transformletList) {
            this.transformletList.add(transformlet);
            logger.info("[TtlTransformer] add Transformlet " + transformlet.getClass() + " success");
        }
    }

    @Override
    public final byte[] transform(@Nullable final ClassLoader loader, @Nullable final String classFile, final Class<?> classBeingRedefined,
                                  final ProtectionDomain protectionDomain, @NonNull final byte[] classFileBuffer) {
        final String className = toClassName(classFile);
		
		//通过classloader和classname加载了原class对象
        ClassInfo classInfo = new ClassInfo(className, classFileBuffer, loader);
		
		//对原class对象,依次应用 ClassFileTransformer,并返回transform后的新class对象
        for (JavassistTransformlet transformlet : transformletList) {
            transformlet.doTransform(classInfo);
            if (classInfo.isModified()) return classInfo.getCtClass().toBytecode();
        }
    }
}

这时就可以回到上一篇中Arthas的核心启动类ArthasBootstrap ==> transformerManager = new TransformerManager(instrumentation);

在TransformerManager中,有三类增强功能:watcher/tarce/其余的归为reTransformers

public TransformerManager(Instrumentation instrumentation) {
    this.instrumentation = instrumentation;

    classFileTransformer = new ClassFileTransformer() {

        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
            for (ClassFileTransformer classFileTransformer : reTransformers) {
                byte[] transformResult = classFileTransformer.transform(loader, className, classBeingRedefined,
                        protectionDomain, classfileBuffer);
                if (transformResult != null) {
                    classfileBuffer = transformResult;
                }
            }

            for (ClassFileTransformer classFileTransformer : watchTransformers) {
                byte[] transformResult = classFileTransformer.transform(loader, className, classBeingRedefined,
                        protectionDomain, classfileBuffer);
                if (transformResult != null) {
                    classfileBuffer = transformResult;
                }
            }

            for (ClassFileTransformer classFileTransformer : traceTransformers) {
                byte[] transformResult = classFileTransformer.transform(loader, className, classBeingRedefined,
                        protectionDomain, classfileBuffer);
                if (transformResult != null) {
                    classfileBuffer = transformResult;
                }
            }

            return classfileBuffer;
        }

    };
    instrumentation.addTransformer(classFileTransformer, true);
}

以其中的Watcher为例子,看下Arthas底层是如何增强源码的;

//继承java.lang.instrument包中的 ClassFileTransformer 接口
public class Enhancer implements ClassFileTransformer {
    @Override
    public byte[] transform(final ClassLoader inClassLoader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
    	//底层是ASM
        ClassNode classNode = new ClassNode(Opcodes.ASM9);
        //首先获取原class对象
        ClassReader classReader = AsmUtils.toClassNode(classfileBuffer, classNode);
        classNode = AsmUtils.removeJSRInstructions(classNode);

        List<MethodNode> matchedMethods = new ArrayList<MethodNode>();
        for (MethodNode methodNode : classNode.methods) {
            if (!isIgnore(methodNode, methodNameMatcher)) {
                matchedMethods.add(methodNode);
            }
        }

        for (MethodNode methodNode : matchedMethods) {
            if (AsmUtils.isNative(methodNode)) {
                continue; //过滤调native方法
            }
            if(AsmUtils.containsMethodInsnNode(methodNode, Type.getInternalName(SpyAPI.class), "atBeforeInvoke")) {
                ... //对class的方法添加watch功能
            }
        }
        return AsmUtils.toBytes(classNode, inClassLoader, classReader); //返回新的class对象
    }
}
更多推荐

Mysql存储-变量、函数、游标、判断、循环

存储过程(procedure)1、介绍:存储过程是事先经过编译并存储在数据库中的一段SQL语句的集合,调用存储过程可以,减少数据在数据库和应用服务器之间的传输,对于提高数据处理的效率是有好处的。存储过程思想上很简单,就是数据库SQL语言层面的代码封装与重用2、特点封装、复用可以接收参数,也可以返回数据减少网络交互,效率

TCP粘包拆包的原因及解决办法

TCP粘包拆包的原因及解决办法文章目录TCP粘包拆包的原因及解决办法TCP粘包拆包的原因如何解决如果你曾经亲自动手在实际项目中编写过TCP服务器或客户端,特别是涉及到高性能服务器的开发,那么你一定会对TCP的粘包和拆包问题有深刻的理解。这个问题在网络编程中是无法避免的,它源于TCP协议本身的特性和网络环境的复杂性。处理

【MySQL集群一】CentOS 7上搭建MySQL集群:一主一从、多主多从

CentOS7上搭建MySQL集群介绍一主一从步骤1:准备工作步骤2:安装MySQL步骤3:配置主服务器步骤4:创建复制用户步骤5:备份主服务器数据,如果没有数据则省略这一步步骤6:配置从服务器步骤7:配置主从复制步骤8:测试主从复制处理宕机情况处理Slave宕机处理Master宕机一主多从多主多从介绍MySQL集群允

【新版】系统架构设计师 - 案例分析 - 系统维护与设计模式

个人总结,仅供参考,欢迎加好友一起讨论文章目录架构-案例分析-系统维护与设计模式典型例题1典型例题2架构-案例分析-系统维护与设计模式典型例题1某企业两年前自主研发的消防集中控制软件系统在市场上取得了较好的业绩,目前已成功应用到国内外众多企业用户的消防管理控制系统中。该软件系统通过不同型号消防控制器连接各种消防器件,实

GPT学术优化 (GPT Academic):支持一键润色、一键中英互译、一键代码解释、chat分析报告生成、PDF论文全文翻译功能、互联网信息聚合+GPT等等

项目设计集合(人工智能方向):助力新人快速实战掌握技能、自主完成项目设计升级,提升自身的硬实力(不仅限NLP、知识图谱、计算机视觉等领域):汇总有意义的项目设计集合,助力新人快速实战掌握技能,助力用户更好利用CSDN平台,自主完成项目设计升级,提升自身的硬实力。专栏订阅:项目大全提升自身的硬实力[专栏详细介绍:项目设计

绘图(二)五子棋小游戏

AWT编程·语雀仓库:Java图形化界面:Java图形化界面学习demo与资料(gitee.com)处理位图如果仅仅绘制一些简单的几何图形,程序的图形效果依然比较单调。AWT也允许在组件上绘制位图,Graphics提供了drawlmage()方法用于绘制位图,该方法需要一个Image参数一一代表位图,通过该方法就可以绘

Android NDK 中有导出 sp智能指针吗?如果没有,可以用什么方法代替 android::sp 智能指针

AndroidNDK中有导出sp智能指针吗?如果没有,可以用什么方法代替android::sp智能指针Author:LycanNote:以下问题解答通过大模型生成,主要用于个人学习和备忘,仅供参考,若有错误或者侵权,请联系我修正,谢谢。问题AndroidNDK中有导出sp智能指针吗?如果没有,可以用什么方法代替andr

深入理解WebSocket,让你入门音视频

😄作者简介:小曾同学.com,一个致力于测试开发的博主⛽️,主要职责:测试开发、CI/CD如果文章知识点有错误的地方,还请大家指正,让我们一起学习,一起进步。😊座右铭:不想当开发的测试,不是一个好测试✌️。如果感觉博主的文章还不错的话,还请点赞、收藏哦!👍原文在这里https://testerhome.com/t

iPhone 15 与 iPhone 14:如何选择?

🌷🍁博主猫头虎(🐅🐾)带您GotoNewWorld✨🍁🐅🐾猫头虎建议程序员必备技术栈一览表📖:🛠️全栈技术FullStack:📚MERN/MEAN/MEVNStack|🌐Jamstack|🌍GraphQL|🔁RESTfulAPI|⚡WebSockets|🔄CI/CD|🌐Git&Versio

TypeScript基础内容(1)

目录一:TypeScript变量声明类型断言(TypeAssertion)TypeScript是怎么确定单个断言是否足够类型推断变量作用域二:TypeScript运算符算术运算符关系运算符逻辑运算符短路运算符(&&与||)位运算符赋值运算符三元运算符(?)类型运算符typeof运算符instanceof其他运算符负号运

小程序的数据驱动和Vue的双向绑定有何异同

引言在现代应用程序开发中,数据驱动和双向绑定是两个非常重要的概念。它们能够提供更好的用户体验和开发效率。本文将探讨小程序的数据驱动和Vue的双向绑定,并通过代码实例来说明它们的异同。让我们一起来了解吧!小程序的数据驱动小程序是一种轻量级的应用程序,可以在移动设备上运行。它采用了数据驱动的开发模式,将界面和数据分离。这种

热文推荐