Search code examples
spring-bootaspectj

AspectJ member introduction doesn't build


I have a POJO:

class Test {
  private int i;
  void setI(int i) {
    this.i = i;
  }
}

this is what I have so far for the aspect:

public aspect t perthis(within(@Tracking *)){

  private Set<String> set = new HashSet<>();

  pointcut setterMethod() : execution(public void set*(..));

  after(Object o) returning() : setterMethod() && this(o) {
    set.add(thisJoinPoint.getSignature().getName());
    System.out.println(set);
  }

  public Set<String> go() {
    return set;
  }
}

I want a Set<String> set for any instance of ANY class that has @Tracking. I also want to add the go() method for any instance of ANY class that has @Tracking.

Can't figure out the syntax. The go() method doesn't get added. If I put Test.go() then the method get added, but then it crashes during runtime.


Solution

  • Marker interface:

    package de.scrum_master.tracking;
    
    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 Track {}
    

    Sample POJOs with/without marker annotation:

    We use two sample classes for positive/negative tests.

    package de.scrum_master.app;
    
    import de.scrum_master.tracking.Track;
    
    @Track
    class TestTracked {
      private int number;
      private String text;
    
      public void setNumber(int number) {
        this.number = number;
      }
    
      public void setText(String text) {
        this.text = text;
      }
    
      @Override
      public String toString() {
        return "TestTracked [number=" + number + ", text=" + text + "]";
      }
    }
    
    package de.scrum_master.app;
    
    class TestUntracked {
      private int number;
      private String text;
    
      public void setNumber(int number) {
        this.number = number;
      }
    
      public void setText(String text) {
        this.text = text;
      }
    
      @Override
      public String toString() {
        return "TestUntracked [number=" + number + ", text=" + text + "]";
      }
    }
    

    "Dirty tracker" interface:

    We want the aspect to implement the following interface for each class annotated by @Track by means of inter-type declaration (ITD).

    package de.scrum_master.tracking;
    
    import java.util.Set;
    
    public interface Trackable {
      Set<String> getDirty();
    }
    

    Driver application:

    Here, we are assuming that each POJO class annotated with the marker interface automagically implements the Trackable interface and therefore knows the getDirty() method, which the we call in order to verify that the aspect correctly tracks setter calls.

    package de.scrum_master.app;
    
    import de.scrum_master.tracking.Trackable;
    
    public class Application {
      public static void main(String[] args) {
        TestTracked testTracked = new TestTracked();
        testTracked.setNumber(11);
        testTracked.setText("foo");
        System.out.println(testTracked);
        if (testTracked instanceof Trackable)
          System.out.println("Dirty members: " + ((Trackable) testTracked).getDirty());
    
        TestUntracked testUntracked = new TestUntracked();
        testUntracked.setNumber(22);
        testUntracked.setText("bar");
        System.out.println(testUntracked);
        if (testUntracked instanceof Trackable)
          System.out.println("Dirty members: " + ((Trackable) testUntracked).getDirty());
      }
    }
    

    Aspect:

    This aspect makes each @Track-annotated class implement interface Trackable and provides both a private field storing tracking information and a getDirty() method implementation returning its value. Furthermore, the aspect makes sure to store the "dirty" information for each successfully executed setter.

    package de.scrum_master.tracking;
    
    import java.util.HashSet;
    import java.util.Set;
    
    public aspect TrackingAspect {
      private Set<String> Trackable.dirty = new HashSet<>();
    
      public Set<String> Trackable.getDirty() {
        return dirty;
      }
    
      declare parents : @Track * implements Trackable;
    
      pointcut setterMethod() : execution(public void set*(..));
    
      after(Trackable trackable) returning() : setterMethod() && this(trackable) {
        System.out.println(thisJoinPoint);
        trackable.dirty.add(thisJoinPoint.getSignature().getName().substring(3));
      }
    }
    

    Console log:

    You will see this when running the driver application:

    execution(void de.scrum_master.app.TestTracked.setNumber(int))
    execution(void de.scrum_master.app.TestTracked.setText(String))
    TestTracked [number=11, text=foo]
    Dirty members: [Number, Text]
    TestUntracked [number=22, text=bar]
    

    The reason why we do not need perthis or pertarget instantiation is that we store the "dirty" information right inside the original classes. Alternatively, we could use per* instantiation and keep all information inside the corresponding aspect instances instead of using ITD. In that case however, the "dirty" information would be unaccessible from outside the aspect, which might even be desirable with regard to encapsulation. But then, whatever action needs to be performed when storing the "dirty" instances, would also need to happen from inside the aspect. As you did not provide an MCVE and hence your question is lacking detail, I did not consider this functionality in the aspect. I can easily imagine how this could be done with both aspect variants - per* instantiation vs. ITD - but I hate to speculate and then be wrong.