Search code examples
aopaspectjpointcut

Does a set() field pointcut get invoked if field is set via reflection?


I have a field set pointcut, which seems to do as I expect. Its defined as follows

before(Object newval): set(@Serviced private * *.*) && args(newval)

The above is meant to capture: whenever a private field attribute, annotated with @Serviced, is set call my before advice.

Everything seems to work fine, except for the one case in my code that sets a variable matching the above via java reflection ( ie via java.lang.reflect.Field.set(....).

Any idea's how I can catch that "set" also?

Thanks


Solution

  • As you have noticed, the set() pointcut cannot intercept reflective field changes. But if you control (i.e. can weave aspects into) the code calling the Field.set*(..) methods, you can work around that issue by also using reflection. Here is a complete, compileable code sample illustrating the solution:

    Sample annotation:

    package de.scrum_master.app;
    
    import java.lang.annotation.*;
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.FIELD)
    public @interface Serviced {}
    

    Sample entity class with main method:

    package de.scrum_master.app;
    
    public class Person {
        @Serviced private int id;
        @Serviced private String name;
        private String country;
    
        public int getId() { return id; }
        public void setId(int id) { this.id = id; }
        public String getName() { return name; }
        public void setName(String name) { this.name = name; }
        public String getCountry() { return country; }
        public void setCountry(String country) { this.country = country; }
    
        public void setIdReflective(int id) throws Exception {
            Person.class.getDeclaredField("id").setInt(this, id);
        }
    
        public void setNameReflective(String name) throws Exception {
            Person.class.getDeclaredField("name").set(this, name);
        }
    
        public void setCountryReflective(String country) throws Exception {
            Person.class.getDeclaredField("country").set(this, country);
        }
    
        @Override
        public String toString() {
            return "Person [id=" + id + ", name=" + name + ", country=" + country + "]";
        }
    
        public static void main(String[] args) throws Exception {
            Person person = new Person();
            person.setId(11);
            person.setName("Tin Man");
            person.setCountry("Oz");
            System.out.println("Before reflective setters: " + person);
            person.setIdReflective(22);
            person.setNameReflective("Cowardly Lion");
            person.setCountryReflective("The Land of Oz");
            System.out.println("After reflective setters:  " + person);
        }
    }
    

    As you can see, only two out of three private fields have the @Serviced annotation. Setters are called for all three fields twice: once normally and once via reflection.

    Aspect intercepting both normal and reflective field changes:

    package de.scrum_master.aspect;
    
    import de.scrum_master.app.Serviced;
    import java.lang.reflect.Field;
    
    public aspect ServicedFieldChangeInterceptor {
        before(Object newValue):
            set(@Serviced private * *) && args(newValue)
        {
            System.out.println(thisJoinPointStaticPart + " -> " + newValue);
        }
    
        before(Object newValue, Field field):
            call(public void Field.set*(Object, *)) && args(*, newValue) && target(field)
        {
            if (field.getAnnotation(Serviced.class) == null)
                return;
            System.out.println(thisJoinPointStaticPart + " -> " + field + ", " + newValue);
        }
    }
    

    Sample output when running Person.main:

    set(int de.scrum_master.app.Person.id) -> 11
    set(String de.scrum_master.app.Person.name) -> Tin Man
    Before reflective setters: Person [id=11, name=Tin Man, country=Oz]
    call(void java.lang.reflect.Field.setInt(Object, int)) -> private int de.scrum_master.app.Person.id, 22
    call(void java.lang.reflect.Field.set(Object, Object)) -> private java.lang.String de.scrum_master.app.Person.name, Cowardly Lion
    After reflective setters:  Person [id=22, name=Cowardly Lion, country=The Land of Oz]
    

    The output clearly shows that both advice only "do something" (in this case print information to standard output) for fields annotated with @Serviced, whereas other fields are skipped. While the set() pointcut applies statically, the reflective one needs to determine if the target field has a matching annotation dynamically.