java - 如何使用 ByteBuddy 代理现有对象

标签 java proxy aop byte-buddy

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

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

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

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

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

如上所述,我需要代理类的实例以向它们添加新接口(interface)、拦截对现有方法的调用并替换现有方法(主要是 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)。

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

我错过了什么吗?

最佳答案

我想出了以下解决方案。最后,它完成了我想要的一切,而且它比 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));
    }
}

关于java - 如何使用 ByteBuddy 代理现有对象,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57525767/

相关文章:

ssl - 将 https 客户端的 header 转发到后端应用程序

java - Spring AOP 启动慢

java - Aspectj 默认构造函数切入点

Java Object[] 强制转换为 long[][] 性能问题?

java - LWJGL .OBJ 文件读取器有时会导致文件变形或不渲染文件

linux - 如何通过本地代理允许 curl ?

java - Play 框架 - 具有 session 身份验证的代理请求

java - aspectj 中的 After()

java - Hibernate在使用@GenerateValue(strategy=GenerationType.AUTO)时不保存对象

java - 集成加载器和回收器 View 不起作用