我想使用AOP自动将某些功能添加到带注释的类中。
例如,假设存在一个接口(StoredOnDatabase),其中包含一些用于从数据库读取和写入Bean的有用方法。假设有一些类(POJO)没有实现此接口,并且用注解@Bean进行注解。当出现此注释时,我想:
我不想更改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)。
我确定可以使用上面示例中的解决方案来实现所需的功能,但是看来我最终会写很多样板代码(请参见下面的答案)。
我想念什么吗?
答案 0 :(得分:1)
我想出了以下解决方案。最后,它完成了我想要的一切,并且比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));
}
}
答案 1 :(得分:1)
这是一个AspectJ解决方案。我认为这比ByteBuddy版本更简单,更易读。让我们从与之前相同的Resource
类开始:
package de.scrum_master.app;
public class Resource {
private String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
现在,让我们通过AspectJ的ITD(内部类型定义)又称为Resource
类添加以下内容:
id
成员的构造函数toString()
方法equals(*)
方法package de.scrum_master.aspect;
import de.scrum_master.app.Resource;
public aspect MethodIntroductionAspect {
public Resource.new(String id) {
this();
setId(id);
}
public boolean Resource.equals(Object obj) {
if (!(obj instanceof Resource))
return false;
return getId().equals(((Resource) obj).getId());
}
public String Resource.toString() {
return "Resource[id=" + getId() + "]";
}
}
顺便说一句,如果声明方面privileged
,我们还可以直接访问私有id
成员,而不必使用getId()
和setId()
。但是重构将变得更加困难,所以让我们像上面一样保持它。
测试用例会检查所有3个新引入的方法/构造函数,但是由于我们这里没有代理,因此也没有委派模式,因此,我们不需要像ByteBuddy解决方案那样进行测试。
package de.scrum_master.app;
import static org.junit.Assert.*;
import org.junit.Test;
public class ResourceTest {
@Test
public void useConstructorWithArgument() {
assertNotEquals(null, new Resource("dummy"));
}
@Test
public void testToString() {
assertEquals("Resource[id=dummy]", new Resource("dummy").toString());
}
@Test
public void testEquals() {
assertEquals(new Resource("A"), new Resource("A"));
assertNotEquals(new Resource("A"), new Resource("B"));
}
}
Marco,也许我不能说服您这比您自己的解决方案要好,但是如果可以并且您需要一个Maven POM,请告诉我。
更新:
我刚刚为您创建了一个简单的Maven POM(单个模块项目):
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>de.scrum-master.stackoverflow</groupId>
<artifactId>aspectj-itd-example-57525767</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.source-target.version>8</java.source-target.version>
<aspectj.version>1.9.4</aspectj.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.3</version>
<configuration>
<source>${java.source-target.version}</source>
<target>${java.source-target.version}</target>
<!-- IMPORTANT -->
<useIncrementalCompilation>false</useIncrementalCompilation>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.11</version>
<configuration>
<!--<showWeaveInfo>true</showWeaveInfo>-->
<source>${java.source-target.version}</source>
<target>${java.source-target.version}</target>
<Xlint>ignore</Xlint>
<complianceLevel>${java.source-target.version}</complianceLevel>
<encoding>${project.build.sourceEncoding}</encoding>
<!--<verbose>true</verbose>-->
<!--<warn>constructorName,packageDefaultMethod,deprecation,maskedCatchBlocks,unusedLocals,unusedArguments,unusedImport</warn>-->
</configuration>
<executions>
<execution>
<!-- IMPORTANT -->
<phase>process-sources</phase>
<goals>
<goal>compile</goal>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>${aspectj.version}</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>${aspectj.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${aspectj.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
其次,仅出于测试目的,我停用了IntelliJ IDEA Ultimate中的AspectJ和Spring AOP插件,出于所有目的和目的,此处将我的IDE变成了关于AspectJ的社区版。当然,您不再需要针对AspectJ本机语法或方面交叉引用信息(在哪条建议中将方面代码编织到应用程序代码中?在哪条编织的建议)中突出显示特定的语法,但是对于ITD而言,支持仍然受到限制。例如,在单元测试中,您似乎会看到编译问题,因为IDE并不知道ITS构造函数和方法。
但是,如果您现在打开设置对话框并将IDE构建委托给Maven ...
...您可以从IntelliJ IDEA构建,通过用户界面等运行单元测试。当然,在右侧,您可以看到Maven视图,也可以运行Maven目标。顺便说一句,如果IDEA询问您是否要启用Maven自动导入,您应该接受。
我也将同样的Maven POM导入了一个新的Eclipse项目(安装了AJDT),它也运行得很好。 IDEA和Eclipse项目在一个项目目录中和平共处。
P.S .:在IDEA Ultimate中也必须委派Maven,以避免IDE中的编译错误,因为AspectJ ITD支持在IDEA中是如此糟糕。
P.P.S .:我仍然认为使用商业IDE的专业开发人员应该可以负担得起IDEA Ultimate许可证。但是,如果您是活跃的OSS(开源软件)提交者,并且仅将IDEA用于OSS工作,则无论如何都可以索取免费的Ultimate许可证。
答案 2 :(得分:1)
在您大量编辑问题之后,我没有再更新first answer here,而是决定为您现在描述的情况写一个新的答案。就像我说的那样,您的散文不构成有效的MCVE,因此,我需要在这里进行一些有根据的猜测。
致阅读此答案的任何人:请先阅读另一个答案,尽管关于代码和Maven的两个答案之间有多余之处,但我不想重复自己配置。
根据您的描述,对我来说情况如下:
Bean标记注释:
package de.scrum_master.app;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
@Retention(RUNTIME)
@Target(TYPE)
public @interface Bean {}
一些POJO,其中两个@Bean
,一个不是:
package de.scrum_master.app;
@Bean
public class Resource {
private String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
package de.scrum_master.app;
@Bean
public class Person {
private String firstName;
private String lastName;
private int age;
public Person(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
@Override
public String toString() {
return "Person[firstName=" + firstName + ", lastName=" + lastName + ", age=" + age + "]";
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
package de.scrum_master.app;
public class NoBeanResource {
private String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
每个@Bean
类的数据库存储接口都应实现:
我不得不在这里发明一些伪造的方法,因为您没有告诉我接口及其实现的真实外观。
package de.scrum_master.app;
public interface StoredOnDatabase {
void writeToDatabase();
void readFromDatabase();
}
向Resource
类介绍方法:
这与我的第一个答案相同,并在此处进行了描述,在此无需添加任何内容,只需重复代码即可:
package de.scrum_master.aspect;
import de.scrum_master.app.Resource;
public aspect MethodIntroducer {
public Resource.new(String id) {
this();
setId(id);
}
public boolean Resource.equals(Object obj) {
if (!(obj instanceof Resource))
return false;
return getId().equals(((Resource) obj).getId());
}
public String Resource.toString() {
return "Resource[id=" + getId() + "]";
}
}
方面拦截设置方法调用:
package de.scrum_master.aspect;
import de.scrum_master.app.Bean;
public aspect BeanSetterInterceptor {
before(Object newValue) : @within(Bean) && execution(public void set*(*)) && args(newValue) {
System.out.println(thisJoinPoint + " -> " + newValue);
}
}
在执行setter方法时,方面会打印出如下内容:
execution(void de.scrum_master.app.Resource.setId(String)) -> dummy
execution(void de.scrum_master.app.Resource.setId(String)) -> A
execution(void de.scrum_master.app.Resource.setId(String)) -> B
execution(void de.scrum_master.app.Person.setFirstName(String)) -> Jim
execution(void de.scrum_master.app.Person.setLastName(String)) -> Nobody
execution(void de.scrum_master.app.Person.setAge(int)) -> 99
顺便说一句,您也可以通过set()
切入点直接拦截字段写访问,而不是通过名称间接拦截setter方法。如何执行取决于您要实现的目标以及是否要停留在API级别(公共方法)还是要跟踪在setter方法内部/外部完成的内部字段分配。
使@Bean
的方面实现StoredOnDatabase
接口:
首先,该方面提供了接口的方法实现。其次,它声明所有@Bean
类都应实现此接口(并继承方法的实现)。请注意AspectJ如何直接在接口上声明方法实现。它甚至可以声明字段。在Java中没有接口默认方法之前,这也是可行的。无需声明实现接口的类并重写接口方法作为中介,它可以直接在接口上工作!
package de.scrum_master.aspect;
import de.scrum_master.app.StoredOnDatabase;
import de.scrum_master.app.Bean;
public aspect DatabaseStorageAspect {
public void StoredOnDatabase.writeToDatabase() {
System.out.println("Writing " + this + " to database");
}
public void StoredOnDatabase.readFromDatabase() {
System.out.println("Reading " + this + " from database");
}
declare parents: @Bean * implements StoredOnDatabase;
}
JUnit测试,展示了所有方面引入的功能:
请注意,以上类仅使用System.out.println()
,没有日志记录框架。因此,测试使用System.setOut(*)
来注入Mockito模拟,以验证预期的记录行为。
package de.scrum_master.app;
import org.junit.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import java.io.PrintStream;
public class BeanAspectsTest {
private PrintStream systemOut;
@Before
public void doBefore() {
systemOut = System.out;
System.setOut(mock(PrintStream.class));
}
@After
public void doAfter() {
System.setOut(systemOut);
}
@Test
public void canCallConstructorWithArgument() {
// Awkward way of verifying that no exception is thrown when calling this
// aspect-introduced constructor not present in the original class
assertNotEquals(null, new Resource("dummy"));
}
@Test
public void testToString() {
assertEquals("Resource[id=dummy]", new Resource("dummy").toString());
}
@Test
public void testEquals() {
assertEquals(new Resource("A"), new Resource("A"));
assertNotEquals(new Resource("A"), new Resource("B"));
// BeanSetterInterceptor should fire 4x because MethodIntroducer calls 'setId(*)' from
// ITD constructor. I.e. one aspect can intercept methods or constructors introduced
// by another one! :-)
verify(System.out, times(4)).println(anyString());
}
@Test
public void testPerson() {
Person person = new Person("John", "Doe", 30);
person.setFirstName("Jim");
person.setLastName("Nobody");
person.setAge(99);
// BeanSetterInterceptor should fire 3x
verify(System.out, times(3)).println(anyString());
}
@Test
public void testNoBeanResource() {
NoBeanResource noBeanResource = new NoBeanResource();
noBeanResource.setId("xxx");
// BeanSetterInterceptor should not fire because NoBeanResource has no @Bean annotation
verify(System.out, times(0)).println(anyString());
}
@Test
public void testDatabaseStorage() {
// DatabaseStorageAspect makes Resource implement interface StoredOnDatabase
StoredOnDatabase resource = (StoredOnDatabase) new Resource("dummy");
resource.writeToDatabase();
resource.readFromDatabase();
// DatabaseStorageAspect makes Person implement interface StoredOnDatabase
StoredOnDatabase person = (StoredOnDatabase) new Person("John", "Doe", 30);
person.writeToDatabase();
person.readFromDatabase();
// DatabaseStorageAspect does not affect non-@Bean class NoBeanResource
assertFalse(new NoBeanResource() instanceof StoredOnDatabase);
// We should have 2x2 log lines for StoredOnDatabase method calls
// plus 1 log line for setter called from Resource constructor
verify(System.out, times(5)).println(anyString());
}
}
Maven POM:
这与第一个答案几乎相同,我刚刚添加了Mockito。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>de.scrum-master.stackoverflow</groupId>
<artifactId>aspectj-itd-example-57525767</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.source-target.version>8</java.source-target.version>
<aspectj.version>1.9.4</aspectj.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.3</version>
<configuration>
<source>${java.source-target.version}</source>
<target>${java.source-target.version}</target>
<!-- IMPORTANT -->
<useIncrementalCompilation>false</useIncrementalCompilation>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.11</version>
<configuration>
<!--<showWeaveInfo>true</showWeaveInfo>-->
<source>${java.source-target.version}</source>
<target>${java.source-target.version}</target>
<Xlint>ignore</Xlint>
<complianceLevel>${java.source-target.version}</complianceLevel>
<encoding>${project.build.sourceEncoding}</encoding>
<!--<verbose>true</verbose>-->
<!--<warn>constructorName,packageDefaultMethod,deprecation,maskedCatchBlocks,unusedLocals,unusedArguments,unusedImport</warn>-->
</configuration>
<executions>
<execution>
<!-- IMPORTANT -->
<phase>process-sources</phase>
<goals>
<goal>compile</goal>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>${aspectj.version}</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>${aspectj.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${aspectj.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.0.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>