java - 如何使用 ByteBuddy 代理现有对象
问题描述
我想使用 AOP 自动向带注释的类添加一些功能。
例如,假设有一个接口(StoredOnDatabase),其中包含一些有用的方法来从数据库中读取和写入 bean。假设有一些类(POJO)没有实现这个接口,并且用注解@Bean 进行了注解。当此注释存在时,我想:
- 创建实现接口 StoredOnDatabase 的 bean 的代理;
- 为设置器添加拦截器,当 bean 的属性被修改时,我可以使用它来“跟踪”;
- 使用对所有这些 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)。
我确信我可以使用上面示例中的解决方案来实现我需要的东西,但似乎我最终会编写很多样板代码(请参阅下面的答案)。
我错过了什么吗?
解决方案
我想出了以下解决方案。最后,它完成了我想要的一切,而且它比 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));
}
}
推荐阅读
- android - 当 Room 数据库发生变化时,LiveData 会更新我的 observable
- windows - 使用 Apache24 在 Windows 上配置 phpmyadmin
- c# - dd/MM/yyyy 格式在 ASP.NET Core 中不起作用
- c# - 在服务器端使会话 cookie 无效
- join - 如何将单元格数据包含到文本字符串中?
- python - 在 scipy 中重现 sox 频谱图
- laravel - 控制器中更新和编辑的区别 - laravel
- r - 查找具有相同元素的行(不关心顺序)
- algorithm - 解决“玩具匹配难题”的最佳算法是什么?
- java - 无法将键发送到输入元素