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

上一篇从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对象
    }
}

http://www.niftyadmin.cn/n/5037177.html

相关文章

微服务是个坏主意吗?

曾几何时&#xff0c;我记得我的手指疯狂地敲打键盘&#xff0c;与庞大而杂乱的代码库搏斗。那是巨石的时代&#xff0c;代码就像古老的城堡一样&#xff0c;由一块块石头砌成一个令人印象深刻的庞然大物。 几年过去了&#xff0c;时代变了。开发人员口中的流行语变成了“微服…

keytool工具生成JKS证书

生成证书 使用jdk keytool生成证书 自建证书不受CA信任&#xff0c;仅适合学习使用&#xff0c;如果需要用到服务中&#xff0c;建议使用由CA颁发的可信证书。如果仅是内部使用&#xff0c;也可以安装自己生成的证书到本机。 生成证书 keytool -genkey -alias jwt -keyalg RS…

SpringSecurity授权--前端进行访问控制

目录 &#xff08;1&#xff09;引入依赖 &#xff08;2&#xff09;页面 SpringSecurity可以在一些视图技术中进行控制显示效果。例如Thymeleaf中&#xff0c;只有登录用户拥有某些权限才会展示一些菜单 &#xff08;1&#xff09;引入依赖 <!--Spring Security整合Thyme…

SpringMVC学习|JSON讲解、Controller返回JSON数据、Jackson、JSON乱码处理、FastJson

JSON讲解 JSON(JavaScript Object Notation,JS 对象标记)是一种轻量级的数据交换格式&#xff0c;目前使用特别 广泛。 采用完全独立于编程语言的文本格式来存储和表示数据。 简洁和清晰的层次结构使得 JSON成为理想的数据交换语言。 易于人阅读和编写&#xff0c;同时也易于机…

javaScript:事件冒泡和事件捕获

目录 什么情况下需要考虑事件冒泡 事件冒泡的过程 事件捕获 相关代码 阻止事件冒泡的方法 什么情况下需要考虑事件冒泡 需要考虑事件冒泡的条件 1.同一个页面区域内&#xff0c;具有多个元素 2.这些元素相互构成父子关系 3.这些元素同时绑定了相同的事件 事件冒泡的过程…

微生物学检验试剂——博迈伦

微生物学检验试剂是用于微生物学实验室中进行微生物检测和分析的特定化学试剂。这些试剂通常用于培养、鉴定和检测微生物&#xff0c;以确定其类型、数量和特性。以下是关于微生物学检验试剂的一些重要信息&#xff1a; 1. 分类&#xff1a; - 培养基&#xff1a;微生物培养基是…

Android 自定义加解密播放音视频(m3u8独立加密)

文章目录 背景加密流程音视频解密音视频播放结语 背景 当涉及App内部视频的时候&#xff0c;我们不希望被别人以抓包的形式来爬取我们的视频大视频文件以文件方式整个加密的话需要完全下载后才能进行解密当前m3u8格式虽然支持加密&#xff0c;但是ts格式的小视频可以独立播放的…

ubuntu 安装docker-compose

下载docker-compose uname -suname -asunyuhuasunyuhua-HKF-WXX:~$ uname -s Linux sunyuhuasunyuhua-HKF-WXX:~$ uname -m x86_64检查自己的linux 版本下载对应的包 https://github.com/docker/compose/releases docker-compose-linux-x86_64建立软连 sudo ln -s /home/sun…