我想使用 AOP 自动向带注释的类添加一些功能。
例如,假设有一个接口(interface) (StoredOnDatabase),其中包含一些用于从数据库读取和写入 bean 的有用方法。假设有一些类 (POJO) 没有实现这个接口(interface),并且用注解 @Bean 进行了注解。当此注释存在时,我想:
- 创建实现接口(interface) StoredOnDatabase 的 bean 的代理;
- 为 setter 添加拦截器,当 bean 的属性被修改时,我可以使用它来“跟踪”;
- 使用对所有这些 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/