Arthas原理-字节码增强技术

1,731 阅读4分钟

AOP

AOP(Aspect Oriented Programming,面向切面编程)的核心概念是以不改动源码为前提,通过前后“横切”的方式,动态为程序添加新功能,它的出现,最初是为了解决开发人员所面临的诸多耦合性问题,如图所示。

image.png

一般来说,一个成熟的系统中往往都会包含但不限于如下 6 点通用逻辑:

  • 日志记录;
  • 异常处理;
  • 事务处理;
  • 权限检查;
  • 性能统计;
  • 流量管控。

AOP 术语中我们把上述这些通用逻辑称之为切面(Aspect)。试想一下,如果在系统的各个业务模块中都充斥着上述这些与自身逻辑无毫瓜葛的共享代码会产生什么问题?很明显,当被依赖方发生改变时,避免不了需要修改程序中所有依赖方的逻辑代码,着实不利于维护。想要解决这个痛点,就必须将这些共享代码从逻辑代码中剥离出来,让其形成一个独立的模块,以一种声明式的、可插拔式的方式来应用到具体的逻辑代码中去,以此消除程序中那些不必要的依赖、提升开发效率和代码的可重用性,这就是我们使用 AOP 初衷。

AOP和具体的实现技术无关,只要是符合AOP的思想,我们都可以将其称之为 AOP 的实现。目前市面上 AOP 框架的实现方案通常都是基于如下 2 种形式:

  • 静态编织;
  • 动态编织。

静态编织选择在编译期就将 AOP 增强逻辑插入到目标类的方法中。而动态编织选择在运行期以动态代理的形式对目标类的方法进行 AOP 增强,诸如 Cglib、Javassist,以及 ASM 等字节码生成工具都可用于支撑这一方案的实现。

字节码增强 插桩技术

通过CGlib动态生成目前类的代理类,以动态代理方式实现AOP不在本文讨论的范围。本文讨论的是字节码增强这一技术。 动态编织的技术,首先我们可以尝试是不是能够运行时动态变更一个类。

package asm;

public class Base {
    public void process(){
        System.out.println("process");
    }
}

package asm;
import javassist.*;
import java.io.IOException;
public class Watch {
    public static void main(String[] args) throws NotFoundException, CannotCompileException, IllegalAccessException, InstantiationException, IOException {
        Base base = new Base();
        ClassPool cp = ClassPool.getDefault();
        CtClass cc = cp.get("asm.Base");
        CtMethod m = cc.getDeclaredMethod("process");
        m.insertBefore("{ System.out.println("start"); }");
        m.insertAfter("{ System.out.println("end"); }");
        Class c = cc.toClass();
        cc.writeFile("/Users/shanjunwei/Code/xiaohongshu/LearnByDo");
        Base h = (Base)c.newInstance();
        h.process();
    }
}

上面的代码我们去掉 Base base = new Base(); 这一行可以正常执行,但是有这一行我们看到如下报错。说明JVM是不允许在运行时动态重复加载一个类的,也就是热更换一个类。

Exception in thread "main" javassist.CannotCompileException: by java.lang.ClassFormatError: loader (instance of  sun/misc/Launcher$AppClassLoader): attempted  duplicate class definition for name: "asm/Base"
	at javassist.util.proxy.DefineClassHelper.toClass(DefineClassHelper.java:271)
	at javassist.ClassPool.toClass(ClassPool.java:1240)
	at javassist.ClassPool.toClass(ClassPool.java:1098)
	at javassist.ClassPool.toClass(ClassPool.java:1056)
	at javassist.CtClass.toClass(CtClass.java:1298)
	at asm.Watch.main(Watch.java:35)
Caused by: java.lang.ClassFormatError: loader (instance of  sun/misc/Launcher$AppClassLoader): attempted  duplicate class definition for name: "asm/Base"
	at javassist.util.proxy.DefineClassHelper$Java7.defineClass(DefineClassHelper.java:182)
	at javassist.util.proxy.DefineClassHelper.toClass(DefineClassHelper.java:260)
	... 5 more

既然此路不通,那么是否还有别的方式?

值得庆幸的是,从 JDK1.5 开始,Java 的设计者们在 java.lang.instrument 包下为开发人员提供了基于 JVMTI(Java Virtual Machine Tool Interface,Java 虚拟机工具接口)规范的 Instrumentation-API,使之能够使用 Instrumentation 来构建一个独立于应用的 Agent 程序,以便于监测和协助运行在JVM上的程序。当然最重要的是,使用Instrumentation 可以在运行期对类定义进行修改和替换,换句话来说,相当于我们可以动态对目标类的方法进行AOP增强。

实现的例子,ASM这块比较复杂,需要理解字节码,javassist实现相对简单点。

agent模块

image.png

pom文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>LearnByDo</artifactId>
        <groupId>org.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>agentmain</artifactId>
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.29.0-GA</version>
            <!--<type>bundle</type>-->
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>2.6</version>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                        </manifest>
                        <manifestEntries>
                            <Agent-Class>demo.AgentMain</Agent-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

AgentMain类。agentmain是agent进程执行的入口方法。

package demo;

import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;

public class AgentMain {

    public static void agentmain(String agentOps, Instrumentation instrumentation) throws UnmodifiableClassException,ClassNotFoundException {
        System.out.println("======> agentmain started: " + agentOps);
        instrumentation.addTransformer(new ClassTransformer(), true);
        instrumentation.retransformClasses(Class.forName(agentOps));
    }
}

增强的代理类,这里主要简单实现了一个统计耗时。

package demo;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.Modifier;

import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;

public class ClassTransformer implements ClassFileTransformer {
    public ClassTransformer() {
    }
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
        try {
            ClassPool classPool = ClassPool.getDefault();
            classPool.insertClassPath(className);
            CtClass ctClass = classPool.get(className);
            for (CtMethod ctMethod : ctClass.getDeclaredMethods()) {
                if (Modifier.isPublic(ctMethod.getModifiers()) && !ctMethod.getName().equals("main")) {
                    // 修改字节码
                    ctMethod.addLocalVariable("begin", CtClass.longType);
                    ctMethod.addLocalVariable("end", CtClass.longType);
                    ctMethod.insertBefore("begin = System.nanoTime();");
                    ctMethod.insertAfter("end = System.nanoTime();");
                    ctMethod.insertAfter("System.out.println("方法" + ctMethod.getName() + "耗时"+ (end - begin) +"ns");");
                }
            }
            ctClass.detach();
            return ctClass.toBytecode();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return classfileBuffer;
    }
}

通过 mvn isntall命令将agent打成本地jar包,打包后在工程target目录下可以看到agentmain-1.0-SNAPSHOT.jar

➜  target ls
agentmain-1.0-SNAPSHOT     agentmain-1.0-SNAPSHOT.jar classes                    generated-sources          maven-archiver             maven-status

字节码插桩的目标类和方法

package test;

import java.util.concurrent.TimeUnit;

public class BaseTask {
    public void run() {
        System.out.println("Running...");
    }

    public static void main(String[] args) throws InterruptedException {
        BaseTask task = new BaseTask();
        while (true) {
            task.run();
            TimeUnit.SECONDS.sleep(2);
        }
    }
}

运行BaseTask的mian方法,会看到两秒一次打印Running... image.png

agent attach代码,代码的作用是将agent类attach到目标JVM进程,对目标JVM进程在运行时进行字节码增强。

package test;

import com.sun.tools.attach.*;

import java.io.IOException;

public class AttachTest {

    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
        for (VirtualMachineDescriptor descriptor: VirtualMachine.list()) {
            if (descriptor.displayName().equals("test.BaseTask")) {
                VirtualMachine virtualMachine = VirtualMachine.attach(descriptor.id());
                virtualMachine.loadAgent("/agentmain/target/agentmain-1.0-SNAPSHOT.jar", "test.BaseTask");
                virtualMachine.detach();
            }
        }
    }
}

image.png

image.png

参考