java agent 字节码增强demo实现,无修改增强服务
通过Java Agent机制在应用启动时加载使用字节码增强技术修改类的字节码在方法执行前后插入监控代码收集并输出监控数据SkyWalking等APM工具的核心原理与此类似,但实现了更复杂的功能和更完善的生态系统。理解这个基本原理有助于更好地使用和调试分布式追踪系统。// 待补充真实增强案例。
·
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 项目:
- agent-project - 探针项目(Java Agent)
- target-project - 目标应用项目(被增强的应用)
在 IntelliJ IDEA 中创建项目
第一步:创建探针项目(agent-project)
-
打开 IntelliJ IDEA,选择 “File” → “New” → “Project”
-
选择 “Maven” 作为项目类型,点击 “Next”
-
填写项目信息:
- GroupId:
com.demo
- ArtifactId:
agent-project
- Version:
1.0-SNAPSHOT
- GroupId:
-
点击 “Finish” 创建项目
-
修改
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>
-
创建包结构:右键
src/main/java
→ “New” → “Package”,命名为com.demo.agent
-
在包中创建 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. 收集并输出监控数据
SkyWalking等APM工具的核心原理与此类似,但实现了更复杂的功能和更完善的生态系统。理解这个基本原理有助于更好地使用和调试分布式追踪系统。
更多推荐
所有评论(0)