SkyWalking 探针原理与手写 Java 探针 Demo

SkyWalking 探针原理深入分析

SkyWalking 探针的核心是基于字节码增强技术,通过 Java Agent 机制在类加载时修改字节码来收集监控数据。其主要工作原理如下:

1. Java Agent 机制

  • 加载时机:通过 JVM 的 -javaagent 参数在应用启动时加载
  • ** premain 方法**:代理 jar 包中的 premain 方法是入口点
  • Instrumentation API:提供修改字节码的能力

2. 字节码增强技术

  • ASM/ByteBuddy:SkyWalking 使用 ByteBuddy 库操作字节码
  • 拦截点:在特定方法前后插入监控代码
  • 上下文传播:通过修改方法参数或线程上下文传递追踪信息

3. 数据收集与上报

  • Trace 生成:为每个请求生成全局唯一的 Trace ID
  • Span 创建:记录方法调用的开始时间、结束时间和元数据
  • 异步上报:通过 gRPC/HTTP 将数据发送到 SkyWalking OAP 服务器

手写 Java 探针 Demo:项目创建指南

对于手写 Java 探针 Demo,您不需要创建 Spring 项目,一个标准的 Maven 项目就足够了。下面是在 IntelliJ IDEA 中创建项目的详细步骤。

项目结构规划

建议创建两个独立的 Maven 项目:

  1. agent-project - 探针项目(Java Agent)
  2. target-project - 目标应用项目(被增强的应用)

在 IntelliJ IDEA 中创建项目

第一步:创建探针项目(agent-project)

  1. 打开 IntelliJ IDEA,选择 “File” → “New” → “Project”

  2. 选择 “Maven” 作为项目类型,点击 “Next”

  3. 填写项目信息:

    • GroupId: com.demo
    • ArtifactId: agent-project
    • Version: 1.0-SNAPSHOT
  4. 点击 “Finish” 创建项目

  5. 修改 pom.xml 文件,添加 ByteBuddy 依赖和打包配置:

<?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.demo</groupId>
    <artifactId>agent-project</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <byte-buddy.version>1.14.8</byte-buddy.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>net.bytebuddy</groupId>
            <artifactId>byte-buddy</artifactId>
            <version>${byte-buddy.version}</version>
        </dependency>
        <dependency>
            <groupId>net.bytebuddy</groupId>
            <artifactId>byte-buddy-agent</artifactId>
            <version>${byte-buddy.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.2.2</version>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                        </manifest>
                        <manifestEntries>
                            <Premain-Class>com.demo.agent.Agent</Premain-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>3.3.0</version>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                        </manifest>
                        <manifestEntries>
                            <Premain-Class>com.demo.agent.Agent</Premain-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                </configuration>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>
  1. 创建包结构:右键 src/main/java → “New” → “Package”,命名为 com.demo.agent

  2. 在包中创建 Java 类:

    • Agent.java - 探针入口类
    • MethodTimerInterceptor.java - 方法拦截器

第二步:创建目标应用项目(target-project)

这里的目标应用就可以自己创建,只是需要启动时添加vm option :-javaagent:jar包路径

代码实现

Agent 项目代码

Agent.java:

package com.demo.agent;

import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.matcher.ElementMatchers;
import net.bytebuddy.utility.JavaModule;

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

import static net.bytebuddy.matcher.ElementMatchers.*;

public class Agent {
    public static void premain(String args, Instrumentation inst) {
        System.out.println("bzy111111111111 Demo APM Agent is running!");

        new AgentBuilder.Default()
                // 匹配需要增强的类(Controller, Service, Repository)
                .type(ElementMatchers.nameMatches(".*Controller")
                        .or(ElementMatchers.nameMatches(".*Service"))
                        .or(ElementMatchers.nameMatches(".*Repository")))
                .transform(new AgentBuilder.Transformer() {
                    @Override
                    public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription,
                                                            ClassLoader classLoader, JavaModule javaModule,
                                                            ProtectionDomain protectionDomain) {
                        // 根据类名选择拦截器
                        Class<?> interceptorClass = getInterceptorForClass(typeDescription.getSimpleName());
                        return builder.method(isPublic()
                                        .and(not(isStatic()))
                                        .and(not(isConstructor())))
                                .intercept(MethodDelegation.to(interceptorClass));
                    }
                })
                .installOn(inst); // 关键:安装到 Instrumentation
    }

    /**
     * 根据类名返回对应的拦截器类
     */
    private static Class<?> getInterceptorForClass(String className) {
        if (className.endsWith("Controller")) {
            return TraceInterceptor.class;
        } else if (className.endsWith("Service") || className.endsWith("Repository")) {
            return LoggingInterceptor.class;
        }
        // 默认使用 TraceInterceptor
        return null;
    }

    public static void agentmain(String args, Instrumentation inst) {
        premain(args, inst);
    }
}

LoggingInterceptor

package com.demo.agent;

import net.bytebuddy.implementation.bind.annotation.Origin;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.SuperCall;
import net.bytebuddy.implementation.bind.annotation.AllArguments;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.concurrent.Callable;

public class LoggingInterceptor {

    @RuntimeType
    public static Object intercept(@Origin Method method,
                                   @AllArguments Object[] allArguments,
                                   @SuperCall Callable<?> callable) throws Exception {
        String className = method.getDeclaringClass().getName();
        String methodName = method.getName();
        System.out.println("bzy111111111 intercept");
        System.out.println(method + "allArguments" + Arrays.toString(allArguments) + "call" + callable.toString());
        // 记录方法入参
        StringBuilder argsInfo = new StringBuilder();
        if (allArguments != null && allArguments.length > 0) {
            for (int i = 0; i < allArguments.length; i++) {
                if (i > 0) argsInfo.append(", ");
                argsInfo.append("arg").append(i).append("=");
                if (allArguments[i] != null) {
                    argsInfo.append(allArguments[i].toString());
                } else {
                    argsInfo.append("null");
                }
            }
        }

        System.out.printf("[LOG-INFO] traceId=%s, class=%s, method=%s, action=start, parameters=%s%n",
                TraceContext.getCurrentTraceId(), className, methodName, argsInfo.toString());

        long startTime = System.currentTimeMillis();
        try {
            Object result = callable.call();

            // 记录方法返回结果(简化处理,只记录基本类型和字符串)
            String resultInfo = result != null ? result.toString() : "null";
            if (resultInfo.length() > 100) {
                resultInfo = resultInfo.substring(0, 100) + "...";
            }

            long duration = System.currentTimeMillis() - startTime;
            System.out.printf("[LOG-INFO] traceId=%s, class=%s, method=%s, action=end, duration=%dms, result=%s%n",
                    TraceContext.getCurrentTraceId(), className, methodName, duration, resultInfo);

            return result;
        } catch (Exception e) {
            long duration = System.currentTimeMillis() - startTime;
            System.out.printf("[LOG-ERROR] traceId=%s, class=%s, method=%s, action=error, duration=%dms, error=%s%n",
                    TraceContext.getCurrentTraceId(), className, methodName, duration, e.getMessage());
            throw e;
        }
    }
}

TraceInterceptor

package com.demo.agent;

import net.bytebuddy.implementation.bind.annotation.Origin;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.SuperCall;

import java.lang.reflect.Method;
import java.util.concurrent.Callable;

public class TraceInterceptor {

    @RuntimeType
    public static Object intercept(@Origin Method method, @SuperCall Callable<?> callable) throws Exception {

        String className = method.getDeclaringClass().getName();
        String methodName = method.getName();
        String spanName = className + "." + methodName;

        TraceContext.startSpan(spanName, className, methodName);
        System.out.println("bzy111111111 TraceInterceptor");
        try {
            return callable.call();
        } catch (Exception e) {
            // 记录异常信息
            System.out.printf("[TRACE-ERROR] traceId=%s, spanId=%s, class=%s, method=%s, error=%s%n",
                    TraceContext.getCurrentTraceId(), TraceContext.getCurrentSpanId(),
                    className, methodName, e.getMessage());
            throw e;
        } finally {
            TraceContext.endSpan();
        }
    }
}

TraceContext

package com.demo.agent;

import com.demo.agent.utils.IdGenerator;

import java.util.Stack;

public class TraceContext {
    private static final ThreadLocal<Stack<TraceSpan>> traceStack = ThreadLocal.withInitial(Stack::new);
    private static final ThreadLocal<String> traceId = new ThreadLocal<>();

    public static void startSpan(String spanName, String className, String methodName) {
        TraceSpan span = new TraceSpan();
        span.spanId = IdGenerator.generateSpanId();
        span.spanName = spanName;
        span.className = className;
        span.methodName = methodName;
        span.startTime = System.currentTimeMillis();

        if (traceId.get() == null) {
            traceId.set(IdGenerator.generateTraceId());
        }

        span.traceId = traceId.get();

        if (!traceStack.get().isEmpty()) {
            TraceSpan parentSpan = traceStack.get().peek();
            span.parentSpanId = parentSpan.spanId;
        }

        traceStack.get().push(span);

        // 输出开始日志
        System.out.printf("[TRACE-START] traceId=%s, spanId=%s, parentSpanId=%s, spanName=%s, class=%s, method=%s%n",
                span.traceId, span.spanId, span.parentSpanId, span.spanName, className, methodName);
    }

    public static void endSpan() {
        if (traceStack.get().isEmpty()) {
            return;
        }

        TraceSpan span = traceStack.get().pop();
        span.endTime = System.currentTimeMillis();
        long duration = span.endTime - span.startTime;

        // 输出结束日志
        System.out.printf("[TRACE-END] traceId=%s, spanId=%s, duration=%dms, class=%s, method=%s%n",
                span.traceId, span.spanId, duration, span.className, span.methodName);

        // 如果是根span,清除traceId
        if (traceStack.get().isEmpty()) {
            traceId.remove();
        }
    }

    public static String getCurrentTraceId() {
        return traceId.get();
    }

    public static String getCurrentSpanId() {
        if (traceStack.get().isEmpty()) {
            return null;
        }
        return traceStack.get().peek().spanId;
    }

    static class TraceSpan {
        String traceId;
        String spanId;
        String parentSpanId;
        String spanName;
        String className;
        String methodName;
        long startTime;
        long endTime;
    }
}

MethodTimerInterceptor.java:

package com.demo.agent;

import net.bytebuddy.implementation.bind.annotation.Origin;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.SuperCall;

import java.lang.reflect.Method;
import java.util.concurrent.Callable;

public class MethodTimerInterceptor {

    @RuntimeType
    public static Object intercept(@Origin Method method, @SuperCall Callable<?> callable) throws Exception {
        System.out.println("bzy111111111 TraceInterceptor");
        System.out.println(method.toString() + "call" + callable.toString());
        long start = System.currentTimeMillis();
        try {
            return callable.call();
        } finally {
            long end = System.currentTimeMillis();
            System.out.println("bzy Method " + method.getName() + " executed in " + (end - start) + "ms");
        }
    }
}
``

**IdGenerator**
~~~
package com.demo.agent.utils;

import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;

public class IdGenerator {
    private static final AtomicInteger spanCounter = new AtomicInteger(1);

    public static String generateTraceId() {
        return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
    }

    public static String generateSpanId() {
        return Integer.toHexString(spanCounter.getAndIncrement());
    }
}
~~~

## 在 IDEA 中直接运行

1. 打开目标项目的运行配置:
   - 点击右上角的运行配置下拉菜单 → "Edit Configurations"

2. 添加新的 "Application" 配置:
   - Main class: `com.demo.app.DemoApplication`
   - VM options: `-javaagent:/path/to/agent-project/target/agent-project-1.0-SNAPSHOT-jar-with-dependencies.jar`
   - 将 `/path/to/` 替换为实际的 agent-project 路径

3. 点击 "Apply" 然后 "OK"

4. 现在可以直接在 IDEA 中运行目标应用,并看到探针的效果

# 对比 SkyWalking 探针

我们这个简单Demo实现了SkyWalking探针的基本原理,但实际SkyWalking要复杂得多:

1. **上下文传播**SkyWalking 能够跨线程、跨进程传播追踪上下文
2. **多种组件支持**:支持HTTP、数据库、消息队列等多种组件的自动增强
3. **数据上报**:通过gRPC将数据异步上报到收集器
4. **性能优化**:采用缓存、采样等策略降低性能开销
5. **插件体系**:支持通过插件扩展对各种框架的增强

## 总结

通过这个Demo,我们可以看到Java探针的基本工作原理:
1. 通过Java Agent机制在应用启动时加载
2. 使用字节码增强技术修改类的字节码
3. 在方法执行前后插入监控代码
4. 收集并输出监控数据

SkyWalkingAPM工具的核心原理与此类似,但实现了更复杂的功能和更完善的生态系统。理解这个基本原理有助于更好地使用和调试分布式追踪系统。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐