利用ByteBuddy动态代理拦截美团API调用并注入调试上下文信息

利用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开发者团队,转载请注明出处!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值