Выставление предыдущего значения в AspectJ set-pointcut

Я должен обнаружить изменения значений полей. Я хочу сравнить предыдущее значение с новым. Я не знаю имя поля или его тип. (Более подробная информация здесь.) Для примера данного класса:

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();
    }
}

У меня есть этот аспект:

    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());
        }
}

Проблема в том, что args представляет значение, переданное в заданной точке соединения, а не текущее значение поля. В этой презентации на странице 27 я нашел:

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

но, похоже, он вообще не компилируется с моим кодом (аннотациями). Когда я попробовал:

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

Тогда я получил:

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

Это рабочее решение с использованием отражения:

@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();
}

Это решение с лучшей производительностью, чем отражение (я думаю). Но есть все еще большие издержки (дополнительное поле и привязка экземпляра аспекта к каждой цели).

    @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();
        }
    }

и вот решение с помощью объявить родителей:

@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();
    }

Повторное суммирование есть три альтернативы:

  • pertarget / perthis вокруг набора с картой значений полей
  • синглтон вокруг установлен с отражением
  • Singleton вокруг набора с объявить родителей и карту значений поля

Лучшим решением было бы получить предыдущее значение непосредственно из pointcut (без отражения или запоминания значений полей между pointcut). Является ли это возможным? Если нет, какая альтернатива имеет лучшую производительность?

Дополнительные примечания

Я нашел это обсуждение предыдущего значения в set pointcut, но оно довольно старое.

Весь этот механизм предназначен для обнаружения изменений внутреннего состояния компонента JSF в области сеанса - исправление для Google App Engine. Такой боб обычно имеет менее 100 полей. Все вызывается из одного потока.

3 ответа

К сожалению, в настоящее время в AspectJ нет встроенной функции для просмотра старого значения поля.

Два решения, которые вы уже получили, вполне стандартны, и, вероятно, лучшее в этом случае - отражение.

Другой вариант:

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);
    }
}

(Я написал этот код здесь, поэтому могут быть некоторые ошибки)

Но это создает карту для отслеживания каждого экземпляра и дополнительное поле в каждом экземпляре для хранения этой карты, так что это даст вам более или менее те же накладные расходы, что и для целевого аспекта.

В настоящее время я использую аспект, похожий на этот, но в этом случае мне нужна эта карта для сериализации json (она намного быстрее, чем при использовании отражения), а возможность видеть старые значения - это просто побочный эффект.

Есть лучшее решение. Он имеет лучшую производительность, чем отражение.

    @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;
        }
    }

Я знаю, что вы написали, что "не хотите запоминать значения полей между точечными вызовами". AFAIK другого пути нет.

Похоже, что этот слайд использует раннюю версию AspectJ. Руководство по портированию говорит, что удаление 's' на pointcuts необходимо для старых советов.

Вот совет из другого учебника, который не использует аннотации в 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();
      }
  }

Что касается вашего кода:

  • Прислушиваться к твоему совету после сета мне кажется немного странным. Применение совета как "до" кажется более понятным.
  • Новое значение является аргументом для точки соединения, а не для точки. Pointcut указывает старый аргумент. К сожалению, в этом примере и тип, и имя поля известны. Так что на него можно ссылаться в совете.

Надо связать

Из другого обсуждения кажется, что нет способа получить текущее значение устанавливаемого поля (или его тип) без привязки информации в сигнатуре точки соединения.

Другие вопросы по тегам