Search code examples
javaaopaspectjpointcut

Anyway to create a pointcut to methods of a class' members?


Given a class with a bunch of members, each with their own getter/setter/etc methods, is there a way to design a pointcut that will trigger only on members' methods when contained within the parent class?

For example:

public MyClass{
   List myList = new ArrayList<String>();
}

If I want to create a pointcut to advise myList.add(), is there a way to do this? I do not wish to advise all ArrayList.add() calls. Only to Collections.add() that are members of MyClass.

I've tried playing around with within and cflow, but to no avail:

pointcut addPointcut() : cflow( execution( * *.getMyList() ) ) && call( * *.add(..));

but it does not seem to work. I presume that given that that the add() calls are not actually part of the get() control flow, it doesn't seem to trigger properly.

After some more playing around, I've noticed the following solution seems to work:

pointcut addPointcut(): within( MyClass ) && call( * *.add(..) );

Is this the correct implementation?

I've tried to limit the pointcut to only advise calls to add() when passing an @Entity object, but it does not work. Ex:

pointcut addEntityPointcut(): within( MyClass ) && call( * *.add(@javax.persistence.Entity *) );

and yet the addPointcut() works when called with an @Entity as a parameter.

Is the argument type based on the actual calling method, or based on the add() signature?

EDIT

I was too quick to jump to the wrong conclusion. After sleeping, I've come to recognize that my pointcut will not work.

public class FirstClass{
   List<String> strings = new ArrayList<>();
   // getters and setters
}

public class Execute{

    public main(){
      FirstClass fc = new FirstClass();
      fc.getStrings().add( "This call is advised" );   // <---- Is there any way to advise this add() method?

      List<String> l = new ArrayList<>();
      l.add( "This call is not advised" );   // <---- this one should not be advised
    }
}

I'm looking for a way to advise the add() method called from any class. However, I'm only looking to advise the add() method on the member List contained within FirstClass, even when called from outside FirstClass.


Solution

  • Is the argument type based on the actual calling method, or based on the add() signature?

    In AspectJ for the call() pointcut you need to specify method or constructor signatures. The add() method in this case does not have any parameters annotated by @Entity, thus what you are trying to do does not work. This is a workaround using reflection:

    Sample annotation:

    package de.scrum_master.app;
    
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Entity {}
    

    Sample entity:

    package de.scrum_master.app;
    
    @Entity
    public class MyEntity {}
    

    Driver application:

    package de.scrum_master.app;
    
    import java.util.ArrayList;
    import java.util.List;
    
    public class Application {
        List<Object> myList = new ArrayList<>();
    
        public static void main(String[] args) {
            Application application = new Application();
            application.myList.add("foo");
            application.myList.add(new MyEntity());
            application.myList.add("bar");
            application.myList.add(new MyEntity());
        }
    }
    

    Aspect:

    package de.scrum_master.aspect;
    
    import de.scrum_master.app.Application;
    import de.scrum_master.app.Entity;
    
    public aspect EntityAddInterceptor {
        pointcut addEntity(Object addedObject) :
            within(Application) && call(* *.add(*)) && args(addedObject);
    
        before(Object addedObject) : addEntity(addedObject) {
            if (addedObject.getClass().isAnnotationPresent(Entity.class))
                System.out.println(thisJoinPointStaticPart + " -> " + addedObject);
        }
    }
    

    Output:

    call(boolean java.util.List.add(Object)) -> de.scrum_master.app.MyEntity@19dc6592
    call(boolean java.util.List.add(Object)) -> de.scrum_master.app.MyEntity@54906181
    

    As for the control flow matching variant, I think from the naming perspective it makes sense to assume that getMyList() does not add anything, but just return a list. Probably you rather do something like application.getMyList().add("foo"), and in this case the add() is really outside (after) the control flow of getMyList() because it operates on its result.

    If OTOH you have a hypothetical method addToList(Object element) which really calls add() you can use cflow(). Let us modify the code sample:

    Modified driver application:

    package de.scrum_master.app;
    
    import java.util.ArrayList;
    import java.util.List;
    
    public class Application {
        List<Object> myList = new ArrayList<>();
    
        public void addToMyList(Object element) { reallyAddToMyList(element); }
        private void reallyAddToMyList(Object element) { myList.add(element); }
    
        public static void main(String[] args) {
            Application application = new Application();
            application.myList.add("foo");
            application.myList.add(new MyEntity());
            application.addToMyList("bar");
            application.addToMyList(new MyEntity());
        }
    }
    

    Modified aspect:

    package de.scrum_master.aspect;
    
    import de.scrum_master.app.Entity;
    
    public aspect EntityAddInterceptor {
        pointcut addEntity(Object addedObject) :
            cflow(execution(* *.addToMyList(*))) && (call(* *.add(*)) && args(addedObject));
    
        before(Object addedObject) : addEntity(addedObject) {
            if (addedObject.getClass().isAnnotationPresent(Entity.class))
                System.out.println(thisJoinPointStaticPart + " -> " + addedObject);
        }
    }
    

    New output:

    call(boolean java.util.List.add(Object)) -> de.scrum_master.app.MyEntity@323ba00
    

    As you can see, only one call is logged. It is the one from reallyAddToMyList(), not the one from main().


    Update 2014-07-21 - better aspect modification:

    Credits for this more elegant solution go to Andy Clement (AspectJ maintainer) who has mentioned it on the AspectJ mailing list. It shows both of my variants from above, but uses && @args(Entity) instead of if (addedObject.getClass().isAnnotationPresent(Entity.class)):

    package de.scrum_master.aspect;
    
    import de.scrum_master.app.Application;
    import de.scrum_master.app.Entity;
    
    public aspect EntityAddInterceptor {
        pointcut addEntity(Object addedObject) :
            within(Application) && call(* *.add(*)) && args(addedObject) && @args(Entity);
    
        before(Object addedObject) : addEntity(addedObject) {
            System.out.println(thisJoinPointStaticPart + " -> " + addedObject);
        }
    
        pointcut addEntitySpecial(Object addedObject) :
            cflow(execution(* *.addToMyList(*))) && (call(* *.add(*)) && args(addedObject))  && @args(Entity);
    
        before(Object addedObject) : addEntitySpecial(addedObject) {
            System.out.println(thisJoinPointStaticPart + " -> " + addedObject + " [special]");
        }
    }
    

    The output with both variants active looks like this:

    call(boolean java.util.List.add(Object)) -> de.scrum_master.app.MyEntity@229ff6d1
    call(boolean java.util.List.add(Object)) -> de.scrum_master.app.MyEntity@1976bf9e
    call(boolean java.util.List.add(Object)) -> de.scrum_master.app.MyEntity@1976bf9e [special]