首页 > 解决方案 > 如何使用 ByteBuddy 代理现有对象

问题描述

我想使用 AOP 自动向带注释的类添加一些功能。

例如,假设有一个接口(StoredOnDatabase),其中包含一些有用的方法来从数据库中读取和写入 bean。假设有一些类(POJO)没有实现这个接口,并且用注解@Bean 进行了注解。当此注释存在时,我想:

  1. 创建实现接口 StoredOnDatabase 的 bean 的代理;
  2. 为设置器添加拦截器,当 bean 的属性被修改时,我可以使用它来“跟踪”;
  3. 使用对所有这些 bean 都有效的通用 equals() 和 hashCode() 方法。

我不想改变 POJO 的类。一个简单的解决方案是在实例化 bean 之前使用 ByteBuddy 完成所有这些工作。它可以是一个解决方案,但我想知道是否可以将 bean 实例化为一个干净的 POJO 并使用代理添加其他功能。

我正在尝试使用 ByteBuddy,我认为我有一个可行的解决方案,但它似乎比我预期的要复杂。

如上所述,我需要代理类实例以向它们添加新接口,拦截对现有方法的调用并替换现有方法(主要是 equals()、hashCode() 和 toString())。

似乎接近我需要的示例如下(从ByteBuddy Tutorial复制):

class Source {
  public String hello(String name) { return null; }
}

class Target {
  public static String hello(String name) {
    return "Hello " + name + "!";
  }
}

String helloWorld = new ByteBuddy()
  .subclass(Source.class)
  .method(named("hello")).intercept(MethodDelegation.to(Target.class))
  .make()
  .load(getClass().getClassLoader())
  .getLoaded()
  .newInstance()
  .hello("World");

我可以看到 ByteBuddy 生成的类正在拦截方法“hello”并将其实现替换为 Target 中定义的静态方法。这有几个问题,其中一个是您需要通过调用 newInstance() 来实例化一个新对象。这不是我需要的:代理对象应该包装现有的实例。我可以使用 Spring+CGLIB 或 java 代理来做到这一点,但它们有其他限制(请参阅override-equals-on-a-cglib-proxy)。

我确信我可以使用上面示例中的解决方案来实现我需要的东西,但似乎我最终会编写很多样板代码(请参阅下面的答案)。

我错过了什么吗?

标签: javaproxyaopbyte-buddy

解决方案


我想出了以下解决方案。最后,它完成了我想要的一切,而且它比 Spring AOP+CGLIB 的代码更少(是的,有点神秘):

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.implementation.FieldAccessor;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.bind.annotation.Origin;
import net.bytebuddy.implementation.bind.annotation.SuperCall;
import net.bytebuddy.implementation.bind.annotation.This;
import net.bytebuddy.matcher.ElementMatchers;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Method;

import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

public class ByteBuddyTest {
    private static final Logger logger = LoggerFactory.getLogger(ByteBuddyTest.class);
    private Logger mockedLogger;

    @Before
    public void setup() {
        mockedLogger = mock(Logger.class);
    }

    public interface ByteBuddyProxy {
        public Resource getTarget();
        public void setTarget(Resource target);
    }

    public class LoggerInterceptor {
        public void logger(@Origin Method method, @SuperCall Runnable zuper, @This ByteBuddyProxy self) {
            logger.debug("Method {}", method);
            logger.debug("Called on {} ", self.getTarget());
            mockedLogger.info("Called on {} ", self.getTarget());

            /* Proceed */
            zuper.run();
        }
    }

    public static class ResourceComparator {
        public static boolean equalBeans(Object that, @This ByteBuddyProxy self) {
            if (that == self) {
                return true;
            }
            if (!(that instanceof ByteBuddyProxy)) {
                return false;
            }
            Resource someBeanThis = (Resource)self;
            Resource someBeanThat = (Resource)that;
            logger.debug("someBeanThis: {}", someBeanThis.getId());
            logger.debug("someBeanThat: {}", someBeanThat.getId());

            return someBeanThis.getId().equals(someBeanThat.getId());
        }
    }

    public static class Resource {
        private String id;

        public String getId() {
            return id;
        }

        public void setId(String id) {
            this.id = id;
        }
    }

    @Test
    public void useTarget() throws IllegalAccessException, InstantiationException {
        Class<?> dynamicType = new ByteBuddy()
                .subclass(Resource.class)
                .defineField("target", Resource.class, Visibility.PRIVATE)
                .method(ElementMatchers.any())
                .intercept(MethodDelegation.to(new LoggerInterceptor())
                        .andThen(MethodDelegation.toField("target")))
                .implement(ByteBuddyProxy.class)
                .intercept(FieldAccessor.ofField("target"))
                .method(ElementMatchers.named("equals"))
                .intercept(MethodDelegation.to(ResourceComparator.class))
                .make()
                .load(getClass().getClassLoader())
                .getLoaded();

        Resource someBean = new Resource();
        someBean.setId("id-000");
        ByteBuddyProxy someBeanProxied = (ByteBuddyProxy)dynamicType.newInstance();
        someBeanProxied.setTarget(someBean);

        Resource sameBean = new Resource();
        sameBean.setId("id-000");
        ByteBuddyProxy sameBeanProxied = (ByteBuddyProxy)dynamicType.newInstance();
        sameBeanProxied.setTarget(sameBean);

        Resource someOtherBean = new Resource();
        someOtherBean.setId("id-001");
        ByteBuddyProxy someOtherBeanProxied = (ByteBuddyProxy)dynamicType.newInstance();
        someOtherBeanProxied.setTarget(someOtherBean);

        assertEquals("Target", someBean, someBeanProxied.getTarget());
        assertFalse("someBeanProxied is equal to sameBean", someBeanProxied.equals(sameBean));
        assertFalse("sameBean is equal to someBeanProxied", sameBean.equals(someBeanProxied));
        assertTrue("sameBeanProxied is not equal to someBeanProxied", someBeanProxied.equals(sameBeanProxied));
        assertFalse("someBeanProxied is equal to Some other bean", someBeanProxied.equals(someOtherBeanProxied));
        assertFalse("equals(null) returned true", someBeanProxied.equals(null));

        /* Reset counters */
        mockedLogger = mock(Logger.class);
        String id = ((Resource)someBeanProxied).getId();
        @SuppressWarnings("unused")
        String id2 = ((Resource)someBeanProxied).getId();
        @SuppressWarnings("unused")
        String id3 = ((Resource)someOtherBeanProxied).getId();
        assertEquals("Id", someBean.getId(), id);
        verify(mockedLogger, times(3)).info(any(String.class), any(Resource.class));
    }
}

推荐阅读