You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ByteBuddy Agent无法拦截公共方法,私有方法正常的问题求助

问题

我尝试用ByteBuddy Agent实现方法参数深拷贝(在Mockito的ArgumentCaptor场景中很实用,因为仅对象引用不够,需要深拷贝)。使用版本1.11.19,发现目标类的私有方法可以正常生效,但公共方法无法被拦截。

以下是最小可复现示例(MRE),其中testPrivate()测试通过,但testPublic()测试失败,唯一区别是Print类中被拦截方法的访问修饰符:

import net.bytebuddy.agent.ByteBuddyAgent;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.bind.annotation.*;
import net.bytebuddy.matcher.ElementMatchers;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;

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

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.internal.verification.VerificationModeFactory.times;

public class ByteBuddyTest {

    public static class CopyInterceptor {
        @RuntimeType
        public static Object intercept(
            @AllArguments Object[] allArguments,
            @SuperMethod Method superMethod,
            @Origin Method method,
            @SuperCall Callable<?> callable,
            @This Object self
        ) throws Exception {
            if (allArguments.length == 1 && allArguments[0] instanceof Person person) {
                var copiedPerson = new Person(person.getAge());
                allArguments[0] = copiedPerson;
            }
            return superMethod.invoke(self, allArguments);
        }
    }

    void initAgent() {
        ByteBuddyAgent.install();
        new AgentBuilder.Default()
            .type(ElementMatchers.nameEndsWith("Print"))
            .transform((builder, type, classLoader, module, protectionDomain) ->
                builder
                    .method(ElementMatchers.any())
                    .intercept(MethodDelegation.to(CopyInterceptor.class))
            ).installOnByteBuddyAgent();
    }

    @Test
    void testPrivate() {
        initAgent();
        final ArgumentCaptor<Person> personCaptor =
            ArgumentCaptor.forClass(Person.class);

        var print = mock(Print.class);
        var person = new Person(0);

        int count = 10;

        for (int i = 0; i < count; i++) {
            person.setAge(i);
            print.privatePrint(person);
        }
        verify(print, times(count)).privatePrint(personCaptor.capture());

        for (int i = 0; i < count; i++) {
            var p = personCaptor.getAllValues().get(i);
            assertEquals(i, p.getAge());
        }
    }

    @Test
    void testPublic() {
        initAgent();
        final ArgumentCaptor<Person> personCaptor =
            ArgumentCaptor.forClass(Person.class);

        var print = mock(Print.class);
        var person = new Person(0);

        int count = 10;
        for (int i = 0; i < count; i++) {
            person.setAge(i);
            print.publicPrint(person);
        }
        verify(print, times(count)).publicPrint(personCaptor.capture());

        for (int i = 0; i < count; i++) {
            var p = personCaptor.getAllValues().get(i);
            assertEquals(i, p.getAge());
        }
    }

    static class Person {
        private int age;
        public Person (int age) {
            this.age = age;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }
    }

    static class Print {
        private void privatePrint(Person p) {
            System.out.println("private - " + p.getAge());
        }

        public void publicPrint(Person p) {
            System.out.println("public - " + p.getAge());
        }
    }
}

请问我在Agent的配置中是否遗漏了什么?

解决方案

问题原因

  1. Mockito的mock机制差异:
    • 私有方法:Mockito无法生成代理方法,只能通过反射直接调用原Print类的私有方法,正好被ByteBuddy Agent拦截,参数拷贝逻辑生效。
    • 公共方法:Mockito会生成Print类的CGLIB代理子类,类名类似ByteBuddyTest$Print$MockitoMock$xxxx,原Agent仅匹配名称以Print结尾的类,未覆盖该代理子类,导致公共方法调用未被拦截。
  2. 拦截器调用方式问题:使用superMethod.invoke(self, allArguments)调用原方法,在代理子类场景下会绕过Mockito的代理逻辑,改用@SuperCall提供的Callable才是兼容原类和代理子类的正确调用方式。

修改步骤

  1. 调整Agent的类型匹配器:
    将类型匹配规则从ElementMatchers.nameEndsWith("Print")改为ElementMatchers.isSubTypeOf(Print.class),覆盖Print类本身及其所有子类(包括Mockito生成的代理类)。
  2. 修改拦截器的原方法调用逻辑:
    把return superMethod.invoke(self, allArguments);替换为return callable.call();,利用@SuperCall的Callable安全调用原方法逻辑。

修改后的核心代码

调整后的initAgent方法

void initAgent() {
    ByteBuddyAgent.install();
    new AgentBuilder.Default()
        .type(ElementMatchers.isSubTypeOf(Print.class))
        .transform((builder, type, classLoader, module, protectionDomain) ->
            builder
                .method(ElementMatchers.any())
                .intercept(MethodDelegation.to(CopyInterceptor.class))
        ).installOnByteBuddyAgent();
}

调整后的CopyInterceptor拦截方法

@RuntimeType
public static Object intercept(
    @AllArguments Object[] allArguments,
    @SuperMethod Method superMethod,
    @Origin Method method,
    @SuperCall Callable<?> callable,
    @This Object self
) throws Exception {
    if (allArguments.length == 1 && allArguments[0] instanceof Person person) {
        var copiedPerson = new Person(person.getAge());
        allArguments[0] = copiedPerson;
    }
    return callable.call();
}

额外优化

如果仅需拦截Print类的公共方法,可给方法匹配器添加ElementMatchers.isPublic()缩小拦截范围:

.method(ElementMatchers.isPublic())

内容的提问来源于stack exchange,提问作者rgb1380

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 12:40:34