如何使用ByteBuddy代理一个现有的对象

4
我希望使用AOP自动为注解类添加一些功能。
例如,假设有一个接口(StoredOnDatabase),其中包含一些有用的方法来读写数据库中的bean。假设存在一些未实现此接口且带有注解@Bean的类(POJOs)。当存在此注解时,我想要:
1. 创建一个代理bean,实现接口StoredOnDatabase; 2. 添加拦截器以跟踪bean属性何时被修改的setter; 3. 使用通用的equals()和hashCode()方法,适用于所有这些bean。
我不想更改POJOs的类。一个简单的解决方案是在bean实例化之前使用ByteBuddy完成所有这些操作。这可能是一个解决方案,但我想知道是否可以将bean实例化为干净的POJO,并使用代理添加其他功能。
我正在尝试使用ByteBuddy,并且我认为我有一个可行的解决方案,但它似乎比我预期的要复杂。
如上所述,我需要代理类的实例以添加新接口,拦截对现有方法的调用并替换现有方法(主要是equals(),hashCode()和toString())。
以下示例似乎与我所需的接近(从ByteBuddy教程复制):
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)。
我确信我可以使用上面示例中的解决方案来实现我所需的功能,但似乎我最终会编写大量样板代码(请参见下面我的答案)。
我是否漏掉了什么?

你列出的目标可以使用AspectJ实现,而无需使用代理。你是否已经研究过这个选项了呢? - kriegaex
@kriegaex,AspectJ 的问题在于它需要 IDE 特定的插件支持。Intellij IDEA 仅在“旗舰版”中支持 AspectJ。 - Marco Altieri
作为开发人员,这就是你选择另一个工具的理由?认真的吗?我使用IntelliJ IDE Ultimate,并且可以告诉你它的AspectJ支持不完整,多年来没有任何改进,问题仍然存在。对于AspectJ项目,我使用Eclipse,因为AspectJ是Eclipse产品,那里的IDE支持不仅免费而且更好。如果你对ByteBuddy解决方案感到满意,那很好。但是,因为缺少IDE支持而做出重新发明轮子的决定仍然有点 - 嗯,令人惊讶。 - kriegaex
@kriegaex 这正是问题所在。我不想让我的框架依赖于特定的IDE。如果只是为了我自己,我可能会决定使用Eclipse(我每天在工作中都使用它),但是,考虑到我正在尝试实现将由其他开发人员使用的东西,我宁愿不引入与Eclipse的依赖关系。 - Marco Altieri
伙计,你可以用Notepad++或vi写你的Aspect代码,不管怎样。它永远不会依赖于IDE。顺便说一句,如果你作为专业开发人员无法负担IDEA Ultimate Edition,但又拒绝使用免费替代品Eclipse进行你的非常特殊的项目,我也无能为力。我只是认为,基于这种推理做出有利于ByteBuddy的设计决策,其晦涩难懂的代码并不是一个很聪明的想法。在IDEA中你仍然可以使用AspectJ,为什么不用Maven编译你的项目,然后在所有IDE中都可以工作呢?我就是这样做的。 - kriegaex
3个回答

3
我想到了以下解决方案。最终,它能够实现我想要的一切,且代码量较少(虽然有点晦涩),比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));
    }
}

对其他读者来说,如果您能稍微调整一下解决方案的结构并提取内部类,那将会很有帮助。目前这种写法看起来相当牵强。 - kriegaex
请您更新问题并更清楚地解释您想要实现的是什么,而不是谈论您认为应该如何完成。在我看来,您的问题遭受了XY问题。我想通过在AspectJ中提供一种替代方案来帮助您。如果您能展示一个原始类的行为,以及您想要修改的类会是什么样子(也可以是源代码),那将对我最有帮助。请还告诉我这是否影响单个类或多个类(模式是什么?)。 - kriegaex
再次挑战您解释您的问题,而不是您心中的解决方案。我承诺我将提供一个合适的AspectJ解决方案,以便您和其他读者可以自由决定哪种方式去。您甚至会免费获得Maven POM,以使您独立于IDE。 - kriegaex
我认为你从未真正尝试过使用AspectJ,否则你就不会认为它依赖于Spring或CGLIB。那是Spring AOP,而不是AspectJ。我每天都在使用不依赖于Spring的AspectJ。编译后,您仅有的依赖项是小的AspectJ运行时(118K)。此外,没有依赖关系的ByteBuddy JAR大小为3.2M。 - kriegaex
@kriegaex 你说得对,我从未尝试过使用AspectJ,我想我已经解释过原因了。我提到Spring+CGLIB是因为这是我已经实现的方式。最终,我不喜欢它,因为覆盖方法equals()和hashCode()变得很复杂(请参见https://github.com/cglib/cglib/issues/161)。 - Marco Altieri
显示剩余4条评论

1

我决定为你现在描述的情况编写一个新答案,而不是在你大量编辑问题后再次更新我的第一个答案。正如我所说,你的散文不构成有效的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方法被执行时,Aspect会打印类似于以下内容的输出:
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>

当你要求我提供更多信息并在你写第一个答案之前,我改变了我的问题。你的解决方案实现了我想要的功能,并且更易读。它正在改变资源的类(这就是为什么您可以将new Resource()强制转换为StoredOnDatabase),但这并不重要。我仍然认为使用ByteBuddy的解决方案是可接受的,因为它的DSL功能强大,并且如果您将其视为字节码操作而不是AOP,则很清晰。 - Marco Altieri
其实你甚至不需要进行强制类型转换,我只是这样做是因为IDE发出了警告,无法在测试代码中检测到ITD。即使没有强制转换和波浪线红线,它也可以正常工作。当然,ByteBuddy解决方案是可以接受的,它比我的方面解决方案少一些,但根据您更新的问题,它仍然能够满足所有您的需求。如果您对代理间接性感到满意,那没问题。我喜欢事情既简单又高效。 :-) - kriegaex
关于编辑时间,你是对的。我只检查了时间,没有检查日期。提示:如果您更新了一个问题,请在评论中通知读者,因为SO不会通知我问题的编辑,只会通知新评论。 - kriegaex

0
这里提供一个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(inter-type definition)即引入,向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转变为Community Edition,至于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中出现编译错误,因为IDEA对AspectJ ITD支持得不好。

P.P.S.:我仍然认为一个专业的开发者使用商业IDE应该能够负担得起IDEA Ultimate许可证。然而,如果你是一个活跃的开源软件贡献者,并且只在开源工作中使用IDEA,你可以免费申请Ultimate许可证。


非常感谢您抽出时间来实现这个AspectJ解决方案。当我说我不想依赖于IDE时,我并不是说我想在没有任何IDE的情况下工作,而只是想能够使用我喜欢的IDE。如果我错了,请纠正我,但是上面的代码在没有AspectJ插件的IDE中无法编译。 - Marco Altieri
IntelliJ IDEA具有非常好的Maven集成,您可以针对每个项目配置构建和运行操作以重定向到Maven。我没有尝试过,因为我使用的是IDEA Ultimate,但这也应该适用于IDEA免费版。我可以帮助您。至于Eclipse,只需安装AJDT(AspectJ开发工具),它们当然是免费的。顺便说一句,因为我使用两种IDE,对我来说Maven始终是主要的构建工具,并且我的IDE已配置为从Maven自动更新其构建配置。这使我真正摆脱了IDE,我可以在任何IDE或从CLI中构建。 - kriegaex
正如我所想,在IDEA中不需要AspectJ插件,可以正常工作,请查看我的答案更新。 - kriegaex
我在我的项目中使用Maven。我从未在IDE中使用过使用Maven编译的选项。您正在为基类Resource定义一个方面。正如我在我的问题中所描述的那样,我还需要拦截对用户类上定义的任何setter的调用。假设我框架的一个虚构用户定义了一个带有其getter和setter的新类,那么我认为可以拦截对setter的调用而无需为每个新类编写一个新的方面,这样想是否正确? - Marco Altieri
当然可以,一个切入点可以捕获它们所有。至于方法引入,它是特定于类的,因为对于toString()equals(),您需要访问类成员或方法。回答您的问题的问题是其中包含了大量散文,而代码更清晰地传达了您的问题,缺少MCVE 。您示例中的日志记录拦截器除了“在Resource上调用”之外没有打印任何有用的内容,因此我没有在我的答案中繁琐复制它。 - kriegaex
我刚刚注意到你在我回答后更新了你的问题。现在主要问题与之前完全不同,这让人感到烦琐。无论如何,我很乐意帮助你,但请提供我在先前评论中要求的MCVE。最好解释一下你所说的“通用equals()和hashCode()” - 通用是指什么?展示一下你会为此编写哪些代码。展示一下你所说的bean和interface类。给出一个应该被拦截并打印/记录的示例。你的文章中有太多开放性问题。也许一个新问题会更好。 - kriegaex

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接