10

I have to detect fields value changes. I want to compare the previous value with the new one. I don't know the field name or its type. (More background here.) For sample given class:

package eu.zacheusz.aspectjtries;

@eu.zacheusz.aspectjtries.MyAnnotation
public class Sample {
    private String field;
    public void modify(){
        this.field = "new";
    }
    public static void main(String[] a){
        new Sample().modify();
    }
}

I have this aspect:

    package eu.zacheusz.aspectjtries.aspects;

    import org.aspectj.lang.annotation.After;
    import org.aspectj.lang.annotation.Aspect;

    @Aspect
    public class SampleAspect {

        @After(" set(!static !final !transient * (@eu.zacheusz.aspectjtries.MyAnnotation *) . *) && args(value) && target(m) ")
        public void afterSetField(Object m, Object value){
            System.out.println("After set field. value=" + value + " target=" + m.getClass());
        }
}

The problem is that the args is exposing the value passed at the field set joint point and not the current value of the field. In this presentation at page 27 I found:

sets(int p._x)[oldVal] [newVal]

but it doesn't seem to compile with my code (annotations) at all. When I tried:

@After(" set(!static !final !transient * (@eu.zacheusz.aspectjtries.MyAnnotation *) . *)[oldVal] [newVal] && target(m) ")
    public void afterSetField(Object m, Object oldVal, Object newVal){

Then I got :

Syntax error on token " set(!static !final !transient * (@eu.zacheusz.aspectjtries.MyAnnotation *) . *)[oldVal] [newVal] && target(m)", "unexpected pointcut element: '['@53:53" expected

This is working solution using reflection:

@Around(" set(!static !final !transient * (@eu.zacheusz.aspectjtries.MyAnnotation *) . *) && args(newVal) && target(t) ")
public void aroundSetField(ProceedingJoinPoint jp, Object t, Object newVal) throws Throwable{
    Signature signature = jp.getSignature();
    String fieldName = signature.getName();
    Field field = t.getClass().getDeclaredField(fieldName);
    field.setAccessible(true);
    Object oldVal = field.get(t);
    System.out.println("Before set field. "
            + "oldVal=" + oldVal + " newVal=" + newVal + " target.class=" + t.getClass());
    //TODO compare oldVal with newVal and do sth.
    jp.proceed();
}

This is solution with better performance than reflection (I think). But there is still large overhead (additional field, and binding aspect instance to each target).

    @Aspect("perthis(set(!static !final !transient * (@eu.zacheusz.aspectjtries.MyAnnotation *) . *))")
    public class SampleAspect {            
        private final Map<String, Object> values = new HashMap<String, Object>();            
        @Around(" set(!static !final !transient * (@eu.zacheusz.aspectjtries.MyAnnotation *) . *) && args(newVal) && target(t) ")
        public void beforeSetField(ProceedingJoinPoint jp, Object t, Object newVal) throws Throwable {
            String fieldName = jp.getSignature().getName();
            Object oldVal = this.values.get(fieldName);
            System.out.println("Before set field. "
                    + "oldVal=" + oldVal + " newVal=" + newVal + " target.class=" + t.getClass());
            //TODO compare oldVal with newVal and do sth.                
            this.values.put(fieldName, newVal);
            jp.proceed();
        }
    }

and here is solution using declare parents:

@Aspect
public class AspectC {

    public interface FieldTracker {

        Map<String, Object> getValues();
    }
    // this implementation can be outside of the aspect

    public static class FieldTrackerImpl implements FieldTracker {

        private transient Map<String, Object> values;

        @Override
        public Map<String, Object> getValues() {
            if (values == null) {
                values = new HashMap<String, Object>();
            }
            return values;
        }
    }
    // the field type must be the introduced interface. It can't be a class.
    @DeclareParents(value = "@eu.zacheusz.aspectjtries.MyAnnotation *", defaultImpl = FieldTrackerImpl.class)
    private FieldTracker implementedInterface;

    @Around("set(!static !final !transient * (@eu.zacheusz.aspectjtries.MyAnnotation *) . *) && args(newVal) && target(t)")
    public void beforeSetField(final ProceedingJoinPoint jp, final FieldTracker t, final Object newVal) throws Throwable{
        final Map<String, Object> values = t.getValues();
        final String fieldName = jp.getSignature().getName();
        final Object oldVal = values.get(fieldName);
        System.out.println("Before set field " + fieldName
                + " oldVal=" + oldVal + " newVal=" + newVal + " target.class=" + t.getClass());
        //TODO compare oldVal with newVal and do sth.
        values.put(fieldName, newVal);
        jp.proceed();
    }

Reasumming there are three alternatives:

  • pertarget/perthis around set with field values map
  • singleton around set with reflection
  • singleton around set with declare parents and field values map

The best solution would be getting the previous value directly from pointcut (without reflection or remembering field values between pointcuts). Is it possible? If not, which alternative has the best performance?

Additional notes

I found this discussion about previous value in set pointcut, but it's quite old.

All this mechanism is for detecting JSF session-scoped bean internal state changes - fix for Google App Engine. Such a bean usually have less than 100 fields. All is invoked from one thread.

zacheusz
  • 8,750
  • 3
  • 36
  • 60

3 Answers3

5

unfortunately there isn't currently a built-in feature in AspectJ to see the old value of the field.

The two solutions you already obtained are quite standard, and probably the reflection one is the best in this case.

Another option is :

public aspect FieldTracker {

    public interface TrackingField {};
    public Map<String,Object> TrackingField.fields;

    declare parents : @Deprecated * : implements TrackingField;

    void around(TrackingField t, Object val) :
        set(!static !final !transient * TrackingField.*) 
        && args(val) 
        && target(t) 
    {

        String fieldName = thisJoinPointStaticPart.getSignature().getName();
        Object oldVal = t.fields == null ? null : t.fields.get(fieldName);

        // do whatever

        if (val != null) {
            if (t.fields == null) t.fields = new HashMap<String,Object>();
            t.fields.put(fieldName, val);
        }
        proceed(t,val);
    }
}

(I have written this code here, so there could be some errors)

But this creates a map to track each instance, and an additional field in each instance to hold that map, so it will give you more or less the same overhead of the pertarget aspect.

I'm using an aspect similar to this one currently, but in that case I need that map for json serialization (it is much faster than using reflection), and the ability to see old values is just a side effect.

Simone Gianni
  • 11,426
  • 40
  • 49
  • Nice idea. As I understand your concept the target objects have to implement TrackingField interface already? BTW With pertarget there was a problem. The `oldVal` from Mikes' example can hold values only for one field. Given target has unknown number of fields. As you suggested I added a map. And are you sure that there isn't currently a built-in feature in AspectJ to see the old value of the field? – zacheusz Jul 21 '11 at 21:57
  • Hi Zacheusz, the target object needs not to implement the interface, the aspect will make them "implement the interface" so that we can inject the field holding the map. I've been actively following AspectJ only in the last 3 years, but from what i can understand looking at old posts it seems like there was a way to do it, but has been removed together with a number of other similar features to improve performances and because it was not a very common use case. But again, this is only what I think it happened. – Simone Gianni Jul 22 '11 at 12:16
  • Thanks a lot - this is a great answer. So I'm going to implement three solutions (pertarget/perthis, around with reflection, declare) and make some performance tests. The question still remain open. Maybe sb knows more details about previous value in set-pointcut. IMHO this wolud be the best solution. – zacheusz Jul 22 '11 at 12:24
  • Thanks, regarding performance consider that the interface is injected at wave time, so it's not a performance issue. Having a pointer to a hashmap is one more field, so it means 32/64 more bits when allocating the object. The map is by default null, and is allocated only when it needs to store the field value. If you have hundreds of fields on your tracked objects, maybe using a TreeMap could speed things up. If you are accessing those fields by multiple threads, you should write some kind of thread synchronization on the map (a read write lock is probably the best solution). – Simone Gianni Jul 22 '11 at 12:31
  • Good points. This is a background: It is for [detecting JSF session-scoped bean internal state changes](http://java.zacheusz.eu/google-app-engine-http-session-vs-jsf-en/394/#solution-3-authomatic) - fix for Google App Engine. Such a bean usually have less than 100 fields. All is invoked from one thread. – zacheusz Jul 22 '11 at 12:39
  • I know this is resurrecting an old post, but how do you handle the case of a change to something like a Collection? You cannot simply create a pointcut on the setter; you have to create a pointcut to the getter().add/getter().remove of the collection. Do you have a creative way of doing that? – Eric B. Jul 16 '14 at 19:55
  • @EricB. you should do how most persistence systems do : advice the getter and return your own implementation of the collection (your implementation of List for example) that wraps the original one and traps add, remove or whatever else you need. Apart from the advice on the getter, this is not directly AspectJ related, Hibernate for example uses other techniques to advice getters and setters of entities, and uses org.hibernate.collection.internal.AbstractPersistentCollection to replace collections with its own implementations. – Simone Gianni Jul 17 '14 at 12:02
  • @SimoneGianni. Thanks; that's pretty much what I was planning to do for the generic implementations (ex: List, Collection, etc). But it won't work for `@Embedded` classes, which also need to be advised on their getEmbedded().setField() methods, since there is no guarantee that an `@Embedded` class implements a predefined interface, etc. – Eric B. Jul 18 '14 at 01:42
3

There is better solution. It has better performance than reflection.

    @Aspect("pertarget(set(!static !final !transient * (@Deprecated *) . *))")
    public class SampleAspect {

        private Object oldVal;

        @Before(" set(!static !final !transient * (@Deprecated *) . *) && args(newVal) && target(t) ")
        public void beforeSetField(Object t, Object newVal) throws Throwable{
            System.out.println("Before set field. "
                    + "oldVal=" + oldVal + " newVal=" + newVal + " target.class=" + t.getClass());
            this.oldVal = newVal;
        }
    }

I know that you wrote that you "don't want to remember field values between pointcuts". AFAIK there is no other way.

Mike
  • 149
  • 4
  • Thanks a lot, but here is one problem. The `oldVal` can hold values for one field. Given target has unknown number of fields. I updated your solution with map from SimoneGiannis' concept. – zacheusz Jul 21 '11 at 21:55
2

It looks like that slide is using an early version of AspectJ. A porting guide tells that the removal of 's' on the pointcuts is necessary for older advice.

Here's a piece of advice from another tutorial that doesn't use the annotations in AspectJ:

  aspect GuardedX {
      static final int MAX_CHANGE = 100;
      before(int newval): set(static int T.x) && args(newval) {
      if (Math.abs(newval - T.x) > MAX_CHANGE)
          throw new RuntimeException();
      }
  }

Regarding your code:

  • Applying your advice after the set occurs feels a bit odd to me. Applying the advice as a 'before' seems more understandable.
  • The new value is an argument to the joinpoint, not the pointcut. The pointcut specifies the old argument. Unfortunately in this example both the type and the fieldname are known, though. So that it can be referenced int the advice.

Need to bind

From another discussion, it looks like there's not a way to get the current value of the field being set (or its type) without binding the information in the signature of the joinpoint.

zacheusz
  • 8,750
  • 3
  • 36
  • 60
Atreys
  • 3,741
  • 1
  • 17
  • 27
  • Thanks a lot. 1. I corrected sets to set and left [] syntax as you see in my listing. 2. So there is no way without reflection? If i don't know the field name or its type then how can I select proper field using reflection? – zacheusz Jul 14 '11 at 19:24
  • Found solution with reflection. But I still wonder how to achieve it without reflection? – zacheusz Jul 14 '11 at 19:39