likes
comments
collection
share

战损版JavaAgent方法耗时统计工具实现

作者站长头像
站长
· 阅读数 17

新来的实习生妹妹故意刁难我,

说想让我实现一个方法耗时统计工具,

不能用切面,

这能难倒我嘛,Java Agent安排上。


前言

本篇文章将实现一个超绝战损版的基于Java Agent的方法耗时统计工具。

整体内容分为:

  1. Java Agent原理简析;
  2. 方法耗时统计工具实现;
  3. 方法耗时工具的Springbootstarter包实现。

正文

一. Java Agent原理简析

理解啥是Java Agent前,需要先介绍一下JVM TIJVM Tool Interface)。

JVM TIJVM提供的用于访问JVM各种状态的一套编程接口。基于JVM TI可以注册各种JVM事件钩子函数,当JVM事件发生时,触发钩子函数以对相应的JVM事件进行处理。关于JVM TI的详细文档,可以参考JVMTM Tool Interface

那么Java Agent可以理解为就是JVM TI的一种具体实现。关于Java Agent,可以概括其特性如下。

  1. 是一个jar包;
  2. 无法独立运行;
  3. JDK1.5)可以在程序运行前被加载,加载后会调用到Java Agent提供的入口函数premain(String agentArgs, Instrumentation inst)
  4. JDK1.6开始)可以在程序运行中被加载,加载后会调用到Java Agent提供的入口函数agentmain(String agentArgs, Instrumentation inst)

如果想要agentmain() 方法被调用,则需要将Agent程序attach到主进程的JVM上,这时就需要使用到com.sun.tools.attach包里提供的Attach APIAgentattachJVM后,agentagentmain() 方法就会被调用。

最后说明一下Java Agent的入口函数中的类型为Instrumentation的参数。InstrumentJVM提供的一套能够对Java代码进行插桩操作的服务能力,JDK1.5Instrument支持在JVM启动并加载类时修改类,InstrumentJDK1.6开始支持在程序运行时修改类。Instrument提供的重要方法如下所示。

public interface Instrumentation {

    // ...

    /**
     * JDK1.5提供,注册一个{@link ClassFileTransformer}。
     * 等同于addTransformer(transformer, false)。
     *
     * @param transformer {@link ClassFileTransformer}。
     */
    void addTransformer(ClassFileTransformer transformer);

    /**
     * JDK1.6提供,注册一个{@link ClassFileTransformer}。
     *
     * @param transformer {@link ClassFileTransformer}。
     * @param canRetransform false表示注册的{@link ClassFileTransformer}仅对首次加载的类生效,
     *                       即首次加载类时可以改变这个类的定义再完成加载;
     *                       true表示注册的{@link ClassFileTransformer}可对已加载的类生效,即
     *                       可对已加载的类进行重定义并重加载,重加载重定义的类时会覆盖已加载的类。
     */
    void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

    /**
     * JDK1.6提供,重定义并重加载传入的类。
     *
     * @param classes 需要重定义并重加载的类。
     * @throws UnmodifiableClassException 传入的类无法被修改时抛出。
     */
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

    // ...

}

也就是可以向Instrumentation注册ClassFileTransformer

JDK1.5时只能通过addTransformer(ClassFileTransformer) 方法注册ClassFileTransformer,此时每个类被加载到JVM中之前会调用到注册的ClassFileTransformertransform() 方法,并可以在其中先改变类定义后再将类加载到JVM中。

JDK1.6开始提供了addTransformer(ClassFileTransformer, boolean) 方法,当第二个参数传入false时,效果与addTransformer(ClassFileTransformer) 方法一样,当第二个参数传入true时,那么此时注册的ClassFileTransformer除了在类被加载到JVM中之前会调用到,还会在retransformClasses(Class<?>... classes) 方法调用时被调用到,也就是此时注册的ClassFileTransformer支持对通过retransformClasses(Class<?>... classes) 方法传入的类进行重定义然后再重加载到JVM中。

二. 整体构思

首先,因为是超绝战损版,所以我们的方法耗时统计,伪代码可以表示如下。

public class TestDemo {

    public void execute() {
        // 记录开始时间
        long beginTime = System.currentTimeMillis();

        // 原方法方法体
        // ...

        // 记录结束时间
        long endTime = System.currentTimeMillis();
        // 记录执行耗时
        long executeTime = endTime - beginTime;
        // 超绝战损版打印
        System.out.println(executeTime);
    }

}

其次,我们需要编写一个Java Agent,且希望能够在程序运行时加载这个Java Agent,所以编写的Java Agent需要提供入口函数agentmain(String agentArgs, Instrumentation inst),此时Java Agent需要通过com.sun.tools.attach包里提供的Attach API来加载并附加到主进程JVM上。

然后,在Java Agent中,我们需要初始化ClassFileTransformer,然后将ClassFileTransformer注册到Instrumentation,再然后获取到需要重定义的类并通过InstrumentationretransformClasses(Class<?>... classes) 方法将这些类传递到注册的ClassFileTransformer中。

接着,在我们自定义的ClassFileTransformer中,需要借助Javassist的能力,为相应的类添加方法耗时统计的代码片段,并完成重加载。

最后,还需要编写一个测试程序来验证我们的超绝战损版方法耗时打印工具的功能。

整体的一个流程示意图如下。

战损版JavaAgent方法耗时统计工具实现

三. 方法耗时统计工具实现

现在开始代码实现。首先创建一个Maven工程,命名为myagent-corePOM文件如下所示。

<?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">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.lee.learn.agent</groupId>
    <artifactId>myagent-core</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.12.1.GA</version>
        </dependency>
    </dependencies>

    <build>
        <finalName>myagent-core</finalName>
        <plugins>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
                    </archive>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

POM文件中主要就是引入必须的javassist的依赖,以及通过打包插件将依赖打入jar包。

然后需要创建src/main/resources/META-INF目录,然后在其中创建MANIFEST.MF文件,内容如下所示。

Agent-Class: com.lee.learn.agent.core.MethodAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true

特别注意最后有一个空行。

现在开始编写Java Agent的代码。首先是自定义一个转换器,命名为MTransformer,并实现ClassFileTransformer接口,代码现如下。

public class MTransformer implements ClassFileTransformer {

    /**
     * 目标类的类全限定名和类加载器的映射,用于筛选出需要重定义的类。
     * 映射类型是:Map[类全限定名, 类加载器]。
     */
    private final Map<String, ClassLoader> targetClassesMap = new HashMap<>();

    /**
     * 初始化时就需要传入目标类集合,并转换成映射关系。
     *
     * @param targetClasses 目标类的类对象集合。
     */
    public MTransformer(List<Class<?>> targetClasses) {
        targetClasses.forEach(targetClass ->
                targetClassesMap.put(
                        targetClass.getName(),
                        targetClass.getClassLoader()));
    }

    /**
     * 基于Javassist改造类方法,为每个方法添加打印执行耗时的逻辑。
     *
     * @param loader 需要改造的类的类加载器。
     * @param className 需要改造的类的类全限定名。
     * @param classBeingRedefined 有值时传入的就是正在被重定义的类的类对象,如果是类加载阶段那么传入为null。
     * @param protectionDomain 改造类的保护域。
     * @param classfileBuffer 类文件的输入字节缓冲区。
     * @return 改造后的类文件字节缓存区,如果未执行改造,返回null。
     */
    @Override
    public byte[] transform(ClassLoader loader, String className,
                            Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
                            byte[] classfileBuffer) {
        byte[] byteCode = classfileBuffer;
        // 类加载器+类路径才能完全确定一个类
        // 基于类加载器和类路径进行目标类筛选
        String targetClassName = className.replaceAll(PATH_SEP, REGEX_QUALIFIER);
        if (targetClassesMap.get(targetClassName) == null
                || !targetClassesMap.get(targetClassName).equals(loader)) {
            return byteCode;
        }
        try {
            ClassPool classPool = ClassPool.getDefault();
            CtClass ctClass = classPool.get(targetClassName);
            // 对目标类的所有方法都插入统计耗时逻辑
            CtMethod[] declaredMethods = ctClass.getDeclaredMethods();
            for (CtMethod ctMethod : declaredMethods) {
                // 插入统计耗时逻辑
                transformMethod(ctMethod);
            }
            byteCode = ctClass.toBytecode();
            ctClass.detach();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return byteCode;
    }

    /**
     * 为每个方法添加统计执行耗时的逻辑。
     *
     * @param ctMethod 详情见{@link CtMethod}。
     */
    private void transformMethod(CtMethod ctMethod) throws Exception {
        // 在方法内添加本地参数
        ctMethod.addLocalVariable("beginTime", CtClass.longType);
        ctMethod.addLocalVariable("endTime", CtClass.longType);
        ctMethod.addLocalVariable("executeTime", CtClass.longType);

        // 方法体之前添加统计开始时间的代码
        ctMethod.insertBefore("beginTime = System.currentTimeMillis();");

        // 方法体结束位置添加获取结束时间并计算执行耗时的代码
        String endCode = "endTime = System.currentTimeMillis();" +
                "executeTime = endTime - beginTime;" +
                "System.out.println(executeTime);";
        ctMethod.insertAfter(endCode);
    }

}

MTransformer的构造函数中,需要传入目标类的类对象的集合,目的就是做到动态的控制对哪些类添加方法耗时统计的逻辑。

最后定义Java Agent的主体类,命名为MethodAgent,代码如下所示。

public class MethodAgent {

    public static final String REGEX_QUALIFIER = "\\.";
    private static final String REGEX_CLASS_SUFFIX = "\\.class";

    private static final String QUALIFIER = ".";
    private static final String CLASS_SUFFIX = ".class";
    public static final String PATH_SEP = "/";

    private static final String SEP = ",";
    private static final String EMPTY = "";

    public static void agentmain(String agentArgs, Instrumentation instrumentation) {
        if (agentArgs == null) {
            return;
        }
        // 期望agentArgs传入的是一个以英文逗号分隔的多个路径
        String[] basePackages = agentArgs.split(SEP);
        List<Class<?>> targetClasses = new ArrayList<>();
        for (String basePackage : basePackages) {
            try {
                // 获取到传入路径下的所有类的类对象
                findClasses(basePackage, targetClasses, MethodAgent.class.getClassLoader());
            } catch (IOException | ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
        // 基于获取到的类的类对象集合创建MTransformer,并向Instrumentation注册MTransformer转换器
        // 需要指定canRetransform为true,否则下面调用的retransformClasses()方法会不生效
        instrumentation.addTransformer(new MTransformer(targetClasses), true);
        try {
            // 将所有目标类通过retransformClasses()方法传递到MTransformer转换器完成转换
            instrumentation.retransformClasses(targetClasses.toArray(new Class<?>[0]));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static void findClasses(String basePackage, List<Class<?>> clazzList, ClassLoader classLoader)
            throws IOException, ClassNotFoundException {
        Enumeration<URL> resources = classLoader.getResources(basePackage.replaceAll(REGEX_QUALIFIER, PATH_SEP));
        while (resources.hasMoreElements()) {
            URL url = resources.nextElement();
            String[] fileNames = new File(url.getPath()).list();
            if (fileNames == null || fileNames.length == 0) {
                return;
            }
            for (String fileName : fileNames) {
                if (!fileName.endsWith(CLASS_SUFFIX)) {
                    findClasses(basePackage + QUALIFIER + fileName, clazzList, classLoader);
                } else {
                    clazzList.add(Class.forName(basePackage + QUALIFIER + fileName.replaceAll(REGEX_CLASS_SUFFIX, EMPTY)));
                }
            }
        }
    }

}

至此Java Agent就编写完毕,整个的工程目录如下所示。

战损版JavaAgent方法耗时统计工具实现

可以先将Java Agent进行打包,并将得到的jar包放在磁盘的某个路径下,这里就放在D盘的根路径下(D:\myagent-core-jar-with-dependencies.jar)。

下面编写测试工程。首先创建Maven工程,命名为myagent-local-testPOM文件如下所示。

<?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">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.lee.learn.agent</groupId>
    <artifactId>myagent-local-test</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.sun</groupId>
            <artifactId>tools</artifactId>
            <version>1.8</version>
            <scope>system</scope>
            <systemPath>${java.home}/../lib/tools.jar</systemPath>
        </dependency>
    </dependencies>

</project>

主要就是引入sun的工具包。

然后创建两个测试目标类,InnerTask位于com.lee.learn.agent.test.inner包路径下,OuterTask位于com.lee.learn.agent.test.outter包路径下,实现如下所示。

public class InnerTask {

    public void execute() {
        System.out.println("Begin to execute inner task.");
        LockSupport.parkNanos(1000 * 1000 * 1000 * 2L);
    }

    public void close() {
        System.out.println("Begin to close inner task.");
        LockSupport.parkNanos(1000 * 1000 * 1000 * 3L);
    }

}

public class OuterTask {

    public void execute() {
        System.out.println("Start to execute outer task.");
        LockSupport.parkNanos(1000 * 1000 * 1000 * 2L);
    }

    public void close() {
        System.out.println("Start to close outer task.");
        LockSupport.parkNanos(1000 * 1000 * 1000 * 3L);
    }

}

然后就是主测试方法,如下所示。

public class MainTest {

    private static final String agentPath = "D:\\myagent-core-jar-with-dependencies.jar";

    public static void main(String[] args) {
        loadAgent();

        InnerTask innerTask = new InnerTask();
        innerTask.execute();
        innerTask.close();

        OuterTask outerTask = new OuterTask();
        outerTask.execute();
        outerTask.close();
    }

    private static void loadAgent() {
        try {
            // 获取主进程Id
            String jvmId = getJvmId();
            // Attach到主进程
            VirtualMachine virtualMachine = VirtualMachine.attach(jvmId);
            // 加载Java Agent,并指定包路径
            virtualMachine.loadAgent(agentPath, "com.lee.learn.agent.test.inner");
            virtualMachine.detach();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static String getJvmId() {
        RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
        String runtimeMXBeanName = runtimeMXBean.getName();
        return runtimeMXBeanName.split("@")[0];
    }

}

因为事先已经将Java Agentjar包放在了D盘根路径下,所以在测试程序中attach到主进程中后,直接通过jar包加载Java Agent

测试工程目录结构如下所示。

战损版JavaAgent方法耗时统计工具实现

运行测试程序,打印如下所示。

战损版JavaAgent方法耗时统计工具实现

四. 方法耗时统计工具的Springboot的starter包实现

第三节中的方法耗时统计工具,功能是实现了,并且主要就是依靠一个Java Agentjar包,但是实在是太简陋了,作为超绝战损版也完全不能看,缺点如下。

  1. Java Agentjar包需要通过某种手段才能让应用程序找得到。例如容器中的一个应用,要使用这个Java Agent,首先要做的事情就是下载jar包,然后拷贝到容器中的某个路径下;
  2. 对用户代码产生了侵入。在测试程序中,编写了代码并调用了com.sun.tools.attach包的VirtualMachine的相关API才实现了attach主进程以及加载Java Agent,这在实际使用中,大家肯定都是不愿意做这个事情的。

鉴于第三节中的做法实在是不优雅,所以本节会编写一个方法耗时统计的starter包,只需要在Springboot工程中引用这个包,然后做少量配置,就能够实现和第三节一样的方法耗时统计效果。

整体会创建三个工程,如下所示。

  1. myagent-package工程。该工程仅需要做一件事情,就是存放Java Agentjar包;
  2. myagent-starter工程。starter包,主要完成的事情就是完成Java Agent的加载;
  3. myagent-starter-test工程。测试工程。

主要的做法和部分代码,参考了ArthasSpringbootstarterarthas-spring-boot-starter的实现。

1. myagent-package工程

创建一个Maven工程,命名为myagent-package,然后将Java Agentjar包打成zip包并放在myagent-package工程的src/main/resources目录下,如下所示。

战损版JavaAgent方法耗时统计工具实现

最后将myagent-package通过install安装到本地仓库。

2. myagent-starter工程

创建一个Maven工程,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">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <artifactId>spring-boot-parent</artifactId>
        <groupId>org.springframework.boot</groupId>
        <version>2.7.6</version>
    </parent>

    <groupId>com.lee.learn.agent</groupId>
    <artifactId>myagent-starter</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>com.sun</groupId>
            <artifactId>tools</artifactId>
            <version>1.8</version>
            <scope>system</scope>
            <systemPath>${java.home}/../lib/tools.jar</systemPath>
        </dependency>

        <dependency>
            <groupId>com.lee.learn.agent.package</groupId>
            <artifactId>myagent-package</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>org.zeroturnaround</groupId>
            <artifactId>zt-zip</artifactId>
            <version>1.14</version>
        </dependency>
    </dependencies>

</project>

上述POM文件中引入的zt-zip是一个zip包工具,然后最关键的就是需要引入myagent-package的依赖,Java Agentjar包的压缩包就在这个依赖包中。

myagent-starter的核心思路就是基于SpringbootSPI机制注册一个ApplicationListener监听器,监听的事件是ApplicationEnvironmentPreparedEvent,也就是在外部配置加载完毕后就开始加载Java Agent

现在先创建src/main/resources/META-INF目录,然后创建spring.factories文件,内容如下所示。

org.springframework.context.ApplicationListener=\
        com.learn.agent.starter.MyAgentApplicationListener

自定义的事件监听器MyAgentApplicationListener实现如下所示。

public class MyAgentApplicationListener implements GenericApplicationListener {

    private static final Class<?>[] EVENT_TYPES = {ApplicationEnvironmentPreparedEvent.class};

    @Override
    public boolean supportsEventType(ResolvableType eventType) {
        return isAssignableFrom(eventType.getRawClass(), EVENT_TYPES);
    }

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof ApplicationEnvironmentPreparedEvent) {
            ConfigurableEnvironment environment = ((ApplicationEnvironmentPreparedEvent) event).getEnvironment();
            try {
                new MyAgentLoader(environment).load();
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }
        }
    }

    private boolean isAssignableFrom(Class<?> type, Class<?>... supportedTypes) {
        if (type != null) {
            for (Class<?> supportedType : supportedTypes) {
                if (supportedType.isAssignableFrom(type)) {
                    return true;
                }
            }
        }
        return false;
    }

}

监听到ApplicationEnvironmentPreparedEvent事件后,就会创建一个MyAgentLoader加载器并调用其load() 方法。Java Agent的加载器MyAgentLoader实现如下。

public class MyAgentLoader {

    private static final int TEMP_DIR_ATTEMPTS = 10000;

    private static final String AGENT_ZIP_NAME = "myagent-core-jar-with-dependencies.zip";
    private static final String AGENT_JAR_NAME = "myagent-core-jar-with-dependencies.jar";

    private final ConfigurableEnvironment environment;

    public MyAgentLoader(ConfigurableEnvironment environment) {
        this.environment = environment;
    }

    public void load() throws IOException {
        // 创建临时目录用于存放agent的jar包
        File tempDir = createTempDir();
        // 解压得到agent的jar包并放到临时目录
        URL agentJarUrl = this.getClass().getClassLoader().getResource(AGENT_ZIP_NAME);
        ZipUtil.unpack(agentJarUrl.openStream(), tempDir);

        // 拿到主进程Id
        String jvmId = getJvmId();
        String basePackage = environment.getProperty("myagent.basepackage");
        try {
            // Attach到主进程
            VirtualMachine virtualMachine = VirtualMachine.attach(jvmId);
            // 加载Java Agent,并传入包路径
            virtualMachine.loadAgent(new File(tempDir.getAbsolutePath(), AGENT_JAR_NAME).getAbsolutePath(), basePackage);
            virtualMachine.detach();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static File createTempDir() {
        File baseDir = new File(System.getProperty("java.io.tmpdir"));
        String baseName = "myagent-" + System.currentTimeMillis() + "-";

        for (int counter = 0; counter < TEMP_DIR_ATTEMPTS; counter++) {
            File tempDir = new File(baseDir, baseName + counter);
            if (tempDir.mkdir()) {
                return tempDir;
            }
        }
        throw new IllegalStateException("Failed to create directory within " + TEMP_DIR_ATTEMPTS + " attempts (tried "
                + baseName + "0 to " + baseName + (TEMP_DIR_ATTEMPTS - 1) + ')');
    }

    private static String getJvmId() {
        RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
        String runtimeMXBeanName = runtimeMXBean.getName();
        return runtimeMXBeanName.split("@")[0];
    }

}

MyAgentLoader#load方法的主要思路如下。

  1. 创建用于存放Java Agentjar包的临时目录;
  2. classpath下找到Java Agentzip包;
  3. Java Agentzip包解压到刚创建出来的临时目录中;
  4. 拿到主进程Id
  5. Environment中拿到配置的目标包路径;
  6. 基于VirtualMachine附加到主进程上;
  7. 加载Java Agent,并传入目标包路径。

至此starter包就编写完毕。myagent-starter工程的目录结构如下所示。

战损版JavaAgent方法耗时统计工具实现

最后还需要将myagent-starter通过install安装到本地仓库。

3. myagent-starter-test工程

现在开始编写测试工程并完成测试,测试工程的目录结构如下所示。

战损版JavaAgent方法耗时统计工具实现

是一个简单的三层架构,首先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">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-parent</artifactId>
        <version>2.7.6</version>
    </parent>

    <groupId>com.lee.learn.agent</groupId>
    <artifactId>myagent-starter-test</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>com.leanr.agent.starter</groupId>
            <artifactId>myagent-starter</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>

然后MyControllerMyServiceMyDao的实现如下。

@RestController
public class MyController {

    @Autowired
    private MyService myService;

    @GetMapping("/testagent")
    public String testAgent() {
        LockSupport.parkNanos(1000 * 1000 * 1000 * 2L);
        myService.testAgent();
        System.out.println("MyController executed.");
        return "Test Agent Success.";
    }

}

@Service
public class MyService {

    @Autowired
    private MyDao myDao;

    public void testAgent() {
        LockSupport.parkNanos(1000 * 1000 * 1000 * 2L);
        myDao.testAgent();
        System.out.println("MyService executed.");
    }

}

@Repository
public class MyDao {

    public void testAgent() {
        LockSupport.parkNanos(1000 * 1000 * 1000 * 2L);
        System.out.println("MyDao executed.");
    }

}

也就是模拟每个方法会耗时2秒。最后编写配置文件,如下所示。

myagent:
  basepackage: com.lee.learn.agent.startertest.controller,com.lee.learn.agent.startertest.dao

配置仅对controllerdao包下的类进行方法耗时统计。

最后启动Springboot程序,并调用MyController接口,打印如下。

战损版JavaAgent方法耗时统计工具实现

方法耗时统计确实只针对controllerdao包生效了,至此测试完毕。

总结

Java Agent就是一个无法独立运行的jar包,其加载时机可以是程序运行前程序运行中,也就是基于Java Agent可以实现在程序运行前程序运行中来动态的修改类。

方法耗时统计,简单的思路就是使用切面去切,首先想到的就是使用SpringAOP来切,但是SpringAOP都知道是基于动态代理,但是无论是JDK动态代理,还是CGLIB动态代理,都有其局限性(貌似AspectJ可行,但这不是本文的重点),不是所有类都能切,所以本文采取的思路就是基于Java Agent再结合Javassist的能力,完成向目标类的方法插入方法耗时统计的逻辑。

一个Java Agentjar包,是一个很精致的jar包,但是有些时候想要这个jar包被加载,还真有点头疼,主要是放哪里怎么解决,所以提供一个Springbootstarter包貌似是一个很好的解决思路,只需要在程序中引入提供的starter包,那么我们的程序最终无论是虚机部署,还是容器部署,我们都能拿到Java Agent并加载。

本文的方法耗时统计,之所以称为战损版,是因为仅仅做了耗时的一个打印,但是真正有用的是啥,那就是能够通过链路Id将方法调用链路以及耗时串起来,但是这也不是本文的重点。

如果觉得本篇文章对你有帮助,烦请点赞,收藏加关注。创作不易,感谢支持!