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的配置中是否遗漏了什么?
解决方案
问题原因
- Mockito的mock机制差异:
- 私有方法:Mockito无法生成代理方法,只能通过反射直接调用原
Print类的私有方法,正好被ByteBuddy Agent拦截,参数拷贝逻辑生效。 - 公共方法:Mockito会生成
Print类的CGLIB代理子类,类名类似ByteBuddyTest$Print$MockitoMock$xxxx,原Agent仅匹配名称以Print结尾的类,未覆盖该代理子类,导致公共方法调用未被拦截。
- 私有方法:Mockito无法生成代理方法,只能通过反射直接调用原
- 拦截器调用方式问题:使用
superMethod.invoke(self, allArguments)调用原方法,在代理子类场景下会绕过Mockito的代理逻辑,改用@SuperCall提供的Callable才是兼容原类和代理子类的正确调用方式。
修改步骤
- 调整Agent的类型匹配器:
将类型匹配规则从ElementMatchers.nameEndsWith("Print")改为ElementMatchers.isSubTypeOf(Print.class),覆盖Print类本身及其所有子类(包括Mockito生成的代理类)。 - 修改拦截器的原方法调用逻辑:
把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
相关产品推荐
相关产品推荐

