如何使用ByteBuddy

时间:2019-08-16 13:45:35

标签: java proxy aop byte-buddy

我想使用AOP自动将某些功能添加到带注释的类中。

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

  1. 创建实现接口StoredOnDatabase的bean的代理;
  2. 为setter添加拦截器,以便在修改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)。

我确定可以使用上面示例中的解决方案来实现所需的功能,但是看来我最终会写很多样板代码(请参见下面的答案)。

我想念什么吗?

3 个答案:

答案 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构造函数和方法。

IntelliJ IDEA project window

但是,如果您现在打开设置对话框并将IDE构建委托给Maven ...

IntelliJ IDEA Maven settings

...您可以从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>