利用ByteBuddy动态代理拦截美团API调用并注入调试上下文信息
在“吃喝不愁”App对接美团开放平台过程中,多个业务模块(如活动查询、资格校验、订单核销)均需调用美团API。为实现统一日志追踪、请求耗时监控及调试上下文(如用户ID、请求ID)注入,本文采用 ByteBuddy 动态生成代理类,在不侵入原始代码的前提下,对 MeituanApiClient 接口的所有方法调用进行拦截增强。
1. 引入ByteBuddy依赖
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.14.10</version>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>1.14.10</version>
</dependency>

2. 定义原始API接口与实现
package baodanbao.com.cn.meituan.client;
import baodanbao.com.cn.meituan.model.FreeMealActivityResponse;
public interface MeituanApiClient {
FreeMealActivityResponse getActivity(String activityId);
boolean checkEligibility(String userId, String activityId);
}
// 原始实现(无任何日志或上下文)
public class DefaultMeituanApiClient implements MeituanApiClient {
@Override
public FreeMealActivityResponse getActivity(String activityId) {
// 模拟HTTP调用
return new FreeMealActivityResponse(activityId, "active");
}
@Override
public boolean checkEligibility(String userId, String activityId) {
return true;
}
}
3. 实现MethodDelegation拦截器
package baodanbao.com.cn.meituan.interceptor;
import net.bytebuddy.implementation.bind.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Method;
import java.util.concurrent.Callable;
public class MeituanApiInterceptor {
private static final Logger log = LoggerFactory.getLogger(MeituanApiInterceptor.class);
@RuntimeType
public Object intercept(
@This Object proxy,
@Origin Method method,
@AllArguments Object[] args,
@SuperCall Callable<Object> callable) throws Exception {
// 从ThreadLocal获取调试上下文
String traceId = DebugContext.getTraceId();
String userId = DebugContext.getUserId();
long start = System.currentTimeMillis();
log.info("Meituan API call [{}] started. traceId={}, userId={}, args={}",
method.getName(), traceId, userId, args);
try {
Object result = callable.call(); // 执行原方法
long cost = System.currentTimeMillis() - start;
log.info("Meituan API call [{}] succeeded in {}ms. traceId={}",
method.getName(), cost, traceId);
return result;
} catch (Exception e) {
long cost = System.currentTimeMillis() - start;
log.error("Meituan API call [{}] failed after {}ms. traceId={}, error={}",
method.getName(), cost, traceId, e.getMessage(), e);
throw e;
}
}
}
4. 调试上下文管理器
package baodanbao.com.cn.meituan.interceptor;
public class DebugContext {
private static final ThreadLocal<String> TRACE_ID = new ThreadLocal<>();
private static final ThreadLocal<String> USER_ID = new ThreadLocal<>();
public static void setTraceId(String id) { TRACE_ID.set(id); }
public static String getTraceId() { return TRACE_ID.get(); }
public static void setUserId(String id) { USER_ID.set(id); }
public static String getUserId() { return USER_ID.get(); }
public static void clear() {
TRACE_ID.remove();
USER_ID.remove();
}
}
5. 动态创建代理实例
package baodanbao.com.cn.meituan.proxy;
import baodanbao.com.cn.meituan.client.MeituanApiClient;
import baodanbao.com.cn.meituan.client.DefaultMeituanApiClient;
import baodanbao.com.cn.meituan.interceptor.MeituanApiInterceptor;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.matcher.ElementMatchers;
public class MeituanApiClientProxyFactory {
public static MeituanApiClient createProxiedClient() {
MeituanApiClient target = new DefaultMeituanApiClient();
return new ByteBuddy()
.subclass(MeituanApiClient.class)
.method(ElementMatchers.any())
.intercept(MethodDelegation.to(new MeituanApiInterceptor()))
.make()
.load(MeituanApiClient.class.getClassLoader())
.getLoaded()
.getDeclaredConstructor()
.newInstance();
}
}
注意:上述方式创建的是空实现子类。若需委托至真实对象,应使用
@SuperCall+ 构造函数注入目标实例,或改用Advice字节码织入。更实用的做法如下:
改进版:代理委托真实实例
public static MeituanApiClient createProxiedClient(MeituanApiClient realClient) {
return new ByteBuddy()
.subclass(MeituanApiClient.class)
.defineField("target", MeituanApiClient.class, Visibility.PRIVATE)
.implement(MeituanApiClient.class)
.intercept(MethodDelegation.to(MeituanApiInterceptor.class))
.make()
.load(MeituanApiClient.class.getClassLoader())
.getLoaded()
.getDeclaredConstructor(MeituanApiClient.class)
.newInstance(realClient);
}
并在 MeituanApiInterceptor 中通过 @FieldValue("target") 获取真实对象调用。
但为简化,本文采用 Spring AOP 更常见;此处展示纯 ByteBuddy 方式,适用于无容器环境。
6. 在业务中使用代理客户端
package baodanbao.com.cn.bajie.service;
import baodanbao.com.cn.meituan.client.MeituanApiClient;
import baodanbao.com.cn.meituan.interceptor.DebugContext;
import baodanbao.com.cn.meituan.proxy.MeituanApiClientProxyFactory;
public class FreeMealApplyService {
private final MeituanApiClient client = MeituanApiClientProxyFactory.createProxiedClient();
public void apply(String userId, String activityId) {
try {
DebugContext.setTraceId(java.util.UUID.randomUUID().toString());
DebugContext.setUserId(userId);
boolean eligible = client.checkEligibility(userId, activityId);
if (!eligible) {
throw new RuntimeException("用户无参与资格");
}
// 继续业务...
} finally {
DebugContext.clear();
}
}
}
7. 日志输出示例
INFO Meituan API call [checkEligibility] started. traceId=abc123, userId=u_8888, args=[u_8888, act_999]
INFO Meituan API call [checkEligibility] succeeded in 42ms. traceId=abc123
该日志可直接用于链路追踪、性能分析与问题复现。
本文著作权归吃喝不愁app开发者团队,转载请注明出处!

被折叠的 条评论
为什么被折叠?



