• 从利用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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    举一个直观的例子,福报厂的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();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    这时就可以回到上一篇中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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    以其中的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对象
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
  • 相关阅读:
    【游戏建模全流程】Maya制作赛博朋克机器人模型
    医院挂号网站(JSP+java+springmvc+mysql+MyBatis)
    java+springboot基于vue中小学生作业管理系统 ssm学习辅助系统
    lap库就该这么安装
    CSDN,毕业生有话说!在如此疯狂的年代如何寻找自己的方向?迈向社会的第一步
    java计算机毕业设计高校教师教学业绩考核系统2021源码+mysql数据库+系统+lw文档+部署
    react 使用 craco库 配置 @ 路径,以及 jsconfig.json或者tsconfig.json 配置智能提示
    ERP生产作业控制 金蝶
    LeetCode(力扣)96. 不同的二叉搜索树Python
    mysql高级语句
  • 原文地址:https://blog.csdn.net/ilfrost/article/details/118565805