exposing previous value in AspectJ set-pointcut

6.2k Views Asked by At

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.

3

There are 3 best solutions below

2
On

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.

1
On

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.

8
On

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.