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

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

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 вокруг набора с картой значений поля
  • синглтон вокруг набора с отражением
  • синглтон вокруг набора с объявленными родителями и картой значений полей

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

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

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

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


person Community    schedule 14.07.2011    source источник


Ответы (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);
    }
}

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

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

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

person Simone Gianni    schedule 21.07.2011
comment
Хорошая идея. Насколько я понимаю вашу концепцию, целевые объекты уже должны реализовывать интерфейс TrackingField? Кстати, с pertarget возникла проблема. Пример oldVal из примера Майка может содержать значения только для одного поля. Данная цель имеет неизвестное количество полей. Как вы предложили, я добавил карту. И вы уверены, что в настоящее время в AspectJ нет встроенной функции для просмотра старого значения поля? - person zacheusz; 22.07.2011
comment
Привет, Zacheusz, целевой объект не должен реализовывать интерфейс, аспект заставит их реализовать интерфейс, чтобы мы могли внедрить поле, содержащее карту. Я активно следил за AspectJ только в последние 3 года, но из того, что я могу понять, глядя на старые сообщения, кажется, что был способ сделать это, но он был удален вместе с рядом других подобных функций для улучшения производительности. и потому что это не было очень распространенным вариантом использования. Но опять же, это только то, что я думаю, что это произошло. - person Simone Gianni; 22.07.2011
comment
Большое спасибо - это отличный ответ. Итак, я собираюсь реализовать три решения (pertarget/perthis, вокруг с отражением, объявить) и проведу несколько тестов производительности. Вопрос пока остается открытым. Возможно, sb знает больше деталей о предыдущем значении в set-pointcut. ИМХО это будет лучшее решение. - person zacheusz; 22.07.2011
comment
Спасибо, что касается производительности, учтите, что интерфейс вводится во время волны, так что это не проблема производительности. Наличие указателя на хэш-карту — это еще одно поле, поэтому при выделении объекта это означает еще 32/64 бита. Карта по умолчанию имеет значение null и выделяется только тогда, когда необходимо сохранить значение поля. Если у вас есть сотни полей на отслеживаемых объектах, возможно, использование TreeMap может ускорить процесс. Если вы обращаетесь к этим полям несколькими потоками, вы должны написать какую-то синхронизацию потоков на карте (вероятно, лучшим решением является блокировка чтения и записи). - person Simone Gianni; 22.07.2011
comment
Хорошие моменты. Это предыстория: для обнаружение изменений внутреннего состояния компонента JSF в области сеанса — исправление для Google App Engine. Такой компонент обычно имеет менее 100 полей. Все вызывается из одного потока. - person zacheusz; 22.07.2011
comment
Я знаю, что это воскрешает старый пост, но как вы справляетесь со случаем изменения чего-то вроде коллекции? Вы не можете просто создать точку на сеттере; вам нужно создать pointcut для getter().add/getter().remove коллекции. У вас есть творческий подход к этому? - person Eric B.; 16.07.2014
comment
@ЭрикБ. вы должны поступать так, как это делает большинство систем персистентности: советовать геттеру и возвращать свою собственную реализацию коллекции (например, вашу реализацию List), которая обертывает исходную и ловушки добавляет, удаляет или что-то еще, что вам нужно. Помимо совета по геттеру, это не имеет прямого отношения к AspectJ, Hibernate, например, использует другие методы для консультирования геттеров и сеттеров сущностей и использует org.hibernate.collection.internal.AbstractPersistentCollection для замены коллекций своими собственными реализациями. - person Simone Gianni; 17.07.2014
comment
@Симоне Джанни. Спасибо; это в значительной степени то, что я планировал сделать для общих реализаций (например, List, Collection и т. д.). Но это не сработает для классов @Embedded, которым также необходимо сообщить об их методах getEmbedded().setField(), поскольку нет гарантии, что класс @Embedded реализует предопределенный интерфейс и т. д. - person Eric B.; 18.07.2014

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

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

Я знаю, что вы написали, что «не хотите запоминать значения полей между точками». АФАИК другого выхода нет.

person Mike    schedule 20.07.2011
comment
Большое спасибо, но вот одна проблема. oldVal может содержать значения для одного поля. Данная цель имеет неизвестное количество полей. Я обновил ваше решение картой из концепции SimoneGiannis. - person zacheusz; 22.07.2011

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

Вот совет из другого учебника, который не использует аннотации в 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. Pointcut указывает старый аргумент. К сожалению, в этом примере известны и тип, и имя поля. Так что на него можно ссылаться в совете.

Необходимо привязать

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

person Atreys    schedule 14.07.2011
comment
Большое спасибо. 1. Я исправил наборы на set и оставил синтаксис [] таким, как вы видите в моем листинге. 2. Значит без рефлексии никак? Если я не знаю имя поля или его тип, то как я могу выбрать правильное поле с помощью отражения? - person zacheusz; 14.07.2011
comment
Найдено решение с отражением. Но мне все же интересно, как этого добиться без размышлений? - person zacheusz; 14.07.2011