4

I would like to use AOP to automatically add some functionality to annotated classes.

Suppose, for example, that there is an interface (StoredOnDatabase) with some useful methods to read and write beans from a database. Suppose that there are classes (POJOs) that do not implement this interface, and that are annotated with the annotation @Bean. When this annotation is present, I would like to:

  1. Create a proxy of the bean that implement the interface StoredOnDatabase;
  2. Add interceptor for the setters that I can use to "trace" when properties of the bean are modified;
  3. Use a generic equals() and hashCode() methods that will be valid for all these beans.

I do not want to alter the class of the POJOs. A simple solution can be to use ByteBuddy to do all of this before the bean is instantiated. It can be a solution, but I am wondering if it could be possible to instantiate the bean as a clean POJO and add the other functionality with a proxy.

I am trying to use ByteBuddy and I think that I have a working solution, but it seems more complex than I was expecting.

As described above, I need to proxy instances of classes to add to them new interfaces, to intercept calls to existing methods and to replace existing methods (mostly equals(), hashCode() and toString()).

The example that seems close to what I need is the following (copied from 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");

I can see that the class generated by ByteBuddy is intercepting the method "hello" and replacing its implementation with a static method defined in Target. There are several problems with this and one of them is that you need to instantiate a new object by calling newInstance(). This is not what I need: the proxy object should wrap the existing instance. I can do this using Spring+CGLIB or java proxies, but they have other limitations (see override-equals-on-a-cglib-proxy).

I am sure that I can use the solution in the example above to implement what I need, but it seems that I would end up writing a lot of boilerplate code (see my answer below).

Am I missing something?

Marco Altieri
  • 3,726
  • 2
  • 33
  • 47
  • The goals you listed can be achieved without proxies using AspectJ. Have you looked into that option yet? – kriegaex Aug 17 '19 at 01:23
  • @kriegaex the problem with AspectJ is that it needs specific plugins for the IDE. Intellij supports it only in the "ultimate" edition. – Marco Altieri Aug 17 '19 at 16:57
  • That's your reason as a developer to select another tool? Seriously? I use IntelliJ IDE Ultimate and can tell you that its AspectJ support is incomplete, nothing has been improved in years, tickets are open. For AspectJ projects I use Eclipse because AspectJ is an Eclipse product and the IDE support there is not just free but also better. If you are happy with your ByteBuddy solution, fine. But to make the decision to re-invent the wheel because you miss IDE support is still kind of - well, surprising. – kriegaex Aug 18 '19 at 01:41
  • @kriegaex that's exactly the problem. I do not want my framework to depend on a specific IDE. If it was only for me, I would have probably decided to work with Eclipse (I use it every day at work), but, given that I am trying to implement something that will be used by other developers, I'd rather not introduce a dependency with Eclipse. – Marco Altieri Aug 18 '19 at 09:03
  • Buddy, you can write your aspect code with Notepad++ or vi, whatever. It will never **depend** on an IDE. And by the way, if you are unable to afford IDEA Ultimate Edition as a professional developer but then refuse to use the free alternative Eclipse for your very special project, I cannot help you. I just believe that making design decisions in favour of ByteBuddy with its obscure, unreadable code based on that kind of reasoning is not a very smart idea. In IDEA you can still work with AspectJ, why don't you compile your project with Maven, then it works in all IDEs. I do it like that. – kriegaex Aug 18 '19 at 13:08

3 Answers3

3

I came up with the following solution. At the end, it does everything I wanted and it is less code (yes, a bit cryptic) than 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));
    }
}
Marco Altieri
  • 3,726
  • 2
  • 33
  • 47
  • It would be helpful for other readers to structure your solution a bit and extract the inner classes. It looks pretty contrived like this. – kriegaex Aug 18 '19 at 01:45
  • I extracted all classes in order to get some more overview. I had to give `LoggerInterceptor` its own static logger and a setter for it, so I could inject the mock from the test. The test passes, even if I replace `.method(ElementMatchers.not(ElementMatchers.named("clone")))` by `.method(ElementMatchers.any())`. I cannot see your exception (also using IntelliJ IDEA). I have to say, the solution works and this is quite impressive - but to me it is completely unreadable. The code does not communicate what it is supposed to do. – kriegaex Aug 18 '19 at 02:21
  • Can you please update your question and explain more clearly **what** you want to achieve instead of talking about **how** you think it should be done? Your question suffers from the [XY problem](https://meta.stackexchange.com/a/66378/309898) IMO. I would like to help you by presenting an alternative solution implemented in AspectJ. It would help me the most if you would show the an original class the behaviour of which you like to modify and then what the modified class would look like (also in source code). Please also tell me if this affects a single class or many (what is the pattern?). – kriegaex Aug 18 '19 at 02:27
  • @kriegaex I agree with you that ByteBuddy, I think by design, is not the easiest tool to implement the usual AOP patterns. It is a runtime code generator and so what you see in the code is manipulation of bytecode and nothing is talking about aspects, advices and pointcuts (ElementMatchers looks like pointcuts, more flexible, but definitely harder to read). – Marco Altieri Aug 18 '19 at 09:17
  • @kriegaex Thanks for taking the time to test my code. You are right. It works even with any(). I do not know what was wrong with one of my previous attempts. – Marco Altieri Aug 18 '19 at 11:54
  • Again I am challenging you to explain your problem, not the solution you have in mind. I promise I will present a suitable AspectJ solution so you and other readers can decide freely which way to go. You will even get a Maven POM for free in order to make you IDE-independent. – kriegaex Aug 18 '19 at 13:10
  • And I think you never really tried working with AspectJ because then you wouldn't believe it depends on Spring or CGLIB. That's Spring AOP, not AspectJ. I use AspectJ every day without Spring. The only dependency you have after compilation is the small [AspectJ runtime](https://mvnrepository.com/artifact/org.aspectj/aspectjrt/1.9.4) (118K). Besides, the [ByteBuddy JAR](https://mvnrepository.com/artifact/net.bytebuddy/byte-buddy/1.10.1) without dependencies has a size of 3.2M. – kriegaex Aug 18 '19 at 13:16
  • @kriegaex You are right, I never tried working with AspectJ, I think I have already explained why. I mentioned Spring+CGLIB because this is how I have already implemented it. At the end, I did not like it because it has been complicated to override the method equals() and hashCode() (see https://github.com/cglib/cglib/issues/161). – Marco Altieri Aug 18 '19 at 15:00
  • I cannot explain what I need to do better than I have. I need to do exactly what I have done with the code above because I am writing a framework that works like an ORM (where the database is replaced with something else). Developers write just a POJO with some simple annotations and the framework manages all the logic to store, load, search, cache, json serialise and deserialise, and some extensible CRUD REST endpoints. To do this, I need (as a starting point) to be able to: 1. use some kind of "introduction" to add functionality to existing beans, intercept method calls and compare beans. – Marco Altieri Aug 18 '19 at 15:42
1

Instead of updating my first answer here yet another time after you massively edited your question, I have decided to write a new answer for the situation you now describe. As I said, your prose does not constitute a valid MCVE, so I need to make a few educated guesses here.

To anyone reading this answer: Please read the other one first, I don't want to repeat myself even though there is redundancy in between the two answers with respect to code and Maven configuration.

The situation to me looks like this according to your description:

Bean marker annotation:

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 {}

Some POJOs, two of them @Beans, one not:

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;
  }
}

Database storage interface each @Bean class should implement:

I had to invent some fake methods here because you did not tell me what the interface and its implementation really look like.

package de.scrum_master.app;

public interface StoredOnDatabase {
  void writeToDatabase();
  void readFromDatabase();
}

Aspect introducing methods to the Resource class:

This is the same as in my first answer and described there, nothing to add here, just repeating the code:

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() + "]";
  }
}

Aspect intercepting setter method calls:

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);
  }
}

The aspect prints something like this when setter methods are being executed:

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

BTW, you could alternatively also directly intercept field write access via set() pointcut instead of indirectly intercepting setter methods by name. How you do it depends on what you want to achieve and if you want to stay on API level (public methods) or also track internal field assignments done in-/outside of setter methods.

Aspect making @Beans implement the StoredOnDatabase interface:

Firstly, the aspect provides method implementations for the interface. Secondly it declares that all @Bean classes should implement this interface (and also inherit method implementations). Please note how AspectJ can directly declare method implementations on interfaces. It could even declare fields. This also worked before there were interface default methods in Java. There is no need to declare a class implementing the interface and overriding interface methods as an intermediary, it works directly on the interface!

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 test demonstrating all the aspect-introduced features:

Please note that the classes above just use System.out.println(), no logging framework. Thus the test uses System.setOut(*) for injecting a Mockito mock in order to verify the expected logging behaviour.

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:

This is almost the same as in the first answer, I just added 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>
kriegaex
  • 63,017
  • 15
  • 111
  • 202
  • I changed my question when you asked me to give more information and before you wrote your first answer. Your solution does what I wanted and it is more readable. It is changing the class of the resources (that's why you can cast new Resource() to StoredOnDatabase),but that's not a big deal. I still think that the solution with ByteBuddy is acceptable, because its DSL is powerful and it is clear if you consider it byte-code manipulation and not AOP. – Marco Altieri Aug 20 '19 at 08:02
  • Actually you don't even need to cast, I just did because the IDE complained, not detecting the ITD in the test code. Even without the cast and with the squiggly red lines it works just fine. And of course the ByteBuddy solution is acceptable, it just does less than my aspect solution which satisfies all your needs according to your updated question. If you are fine with the proxy indirection, no problem. I like things to be both simpler and more efficient. :-) – kriegaex Aug 20 '19 at 08:44
  • As for the editing time, you are right. I just checked the time, not the date. Hint: If you update a question, please notify readers in a comment because SO does not notify me of question edits, only of new comments. – kriegaex Aug 20 '19 at 08:49
0

Here is an AspectJ solution. I think this is a lot simpler and a more readable than the ByteBuddy version. Let us start with the same Resource class as before:

package de.scrum_master.app;

public class Resource {
  private String id;

  public String getId() {
    return id;
  }

  public void setId(String id) {
    this.id = id;
  }
}

Now let us add the following to the Resource class via AspectJ's ITD (inter-type definition) a.k.a. introduction:

  • a constructor directly initialising the id member
  • a toString() method
  • an equals(*) method
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() + "]";
  }
}

BTW, if declaring the aspect privileged we could also directly access the private id member and would not have to use getId() and setId(). But then refactoring would get more difficult, so let us keep it like above.

The test case checks all 3 newly introduced methods/constructors but because we have no proxy and thus no delegation pattern here, we do not need to test that like in the ByteBuddy solution, of course.

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, maybe I cannot convince you that this is better than your own solution, but if I could and you need a Maven POM, just let me know.


Update:

I just created simple Maven POM (single module project) for you:

<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>

Secondly, just for test purposes I deactivated the AspectJ and Spring AOP plugins in IntelliJ IDEA Ultimate, for all intents and purposes here turning my IDE into the Community Edition with regard to AspectJ. Of course you don't have specific syntax highlighting for AspectJ native syntax or aspect cross-reference information (which advice is woven where or where is aspect code woven into application code?) anymore, but with regard to ITD the support is limited anyway. For example, in the unit test you seemingly see compile problems because the ITS constructor and methods are not known by the IDE.

IntelliJ IDEA project window

But if you now open the settings dialogue and delegate the IDE build to Maven...

IntelliJ IDEA Maven settings

... you can build from IntelliJ IDEA, run the unit test via user interface etc. On the right hand side of course you have the Maven view and can run Maven targets, too. BTW, you should accept if IDEA asks you if you want to enable Maven auto-import.

I also imported the very same Maven POM into a new Eclipse project (with AJDT installed) and it also runs just fine. Both IDEA and Eclipse projects co-exist peacefully within one project directory.

P.S.: The delegation to Maven is also necessary in IDEA Ultimate in order to avoid compile errors in the IDE because AspectJ ITD support is so shitty in IDEA.

P.P.S.: I still think a professional developer using a commercial IDE should be able to afford an IDEA Ultimate licence. If, however, you are an active OSS (open source software) committer and only use IDEA for OSS work, you can claim a free Ultimate licence anyway.

kriegaex
  • 63,017
  • 15
  • 111
  • 202
  • Thank you very much for taking the time to implement this AspectJ solution. When I said that I did not want to depend on an IDE, I did not mean that I want to work without any IDE, but only that I want to be able to use the IDE that I prefer. Correct me if I am wrong, but the code above won't compile in an IDE without the AspectJ plugin. – Marco Altieri Aug 19 '19 at 05:34
  • IntelliJ IDEA has a very good Maven integration and you can configure your build and run actions to be redirected to Maven on a per-project basis. I have not tried because I use IDEA Ultimate, but this should also work with the free version of IDEA. I can help you with that. And as for Eclipse, just install AJDT (AspectJ Development Tools) which are of course free. BTW, because I work with both IDEs, for me Maven is always the leading build tool and my IDEs are configured to auto-update their build configuration from Maven. It makes me really free of IDEs, I can build in any IDE or from CLI. – kriegaex Aug 19 '19 at 06:10
  • As I thought, it works in IDEA without AspectJ plugin, see my answer update. – kriegaex Aug 19 '19 at 06:51
  • I use maven in my project. I've never used the option to compile using maven in the IDE. You are defining an aspect on the base class Resource. As I described in my question, I need also to intercept any calls to setters defined on the user class. Suppose that an hypothetical user of my framework defines a new class with its getters and setters, am I right thinking that it will be possible to intercept the calls to the setters without writing a new aspect for each new class? – Marco Altieri Aug 19 '19 at 17:43
  • Of course this is possible, one pointcut can capture them all. As for method introduction, it is class-specific because for `toString()` or `equals()` you need to access class members or methods. The problem in answering your question is that is contains a lot of prose where code would more clearly communicate your problem, an [MCVE](https://stackoverflow.com/help/mcve) is missing. The logger interceptor in your example does not print anything useful other than something was "called on `Resource`", so I did not bother replicating it in my answer. – kriegaex Aug 20 '19 at 00:56
  • I just noticed that you updated your question after I answered. The main question is now completely different than before. This is getting tedious. Anyway, I like to help you, but please do provide the MCVE I asked for in my previous comment. Ideally it explains what you mean by "generic `equals()` and `hashCode()`" - generic how? Show which code you would write for that. Show which bean and interface classes you are talking about. Give an example of what should be intercepted and printed/logged during interception. Too many open questions in your prose. Maybe a new question would be better. – kriegaex Aug 20 '19 at 01:13