Реентерабельные аспекты Spring AOP

Можно ли создавать реентерабельные аспекты с помощью Spring AOP (или AspectJ)?

Вот пример:

@Log
public int calcFibonacci(int n) {
    if(n <= 1) {
        return n;
    } else {
        return calcFibonacci(n - 1) + calcFibonacci(n - 2);
    }
}

И аспект:

@Aspect
public class LoggingAspect {

@Around("@annotation(log)")
public Object measure(ProceedingJoinPoint pjp, Log log) throws Throwable {
    // log some relevant information from log annotation and pjp
    ...
    return pjp.proceed();
}

}

Теперь я хотел бы знать, сколько раз вызывался calcFibonacci (считая повторяющиеся вызовы).

Есть ли способ сделать это?


person Sebastian Łaskawiec    schedule 20.06.2014    source источник
comment
В calcFibonacci() вам нужно, чтобы внутренний вызов был примерно таким: ((CalcFibonaciiInterface) AopContext.currentProxy()).calcFibonacci(). Вы не опубликовали полный класс, но я предположил, что ваш класс, содержащий calcFibonacci, реализует интерфейс (я назвал его CalcFibonacciInterface).   -  person Andrei Stefan    schedule 20.06.2014


Ответы (2)


Хорошо, вы не можете решить это элегантно в Spring AOP - см. Мое первое замечание к ответу Andrei Stefan. Если в АОП код приложения должен знать о существовании аспекта или даже вызывать код, связанный с аспектом, это плохой дизайн и анти-АОП. Таким образом, здесь у меня есть решение AspectJ для вас.

Прежде всего, в AspectJ есть больше, чем просто execution() pointcuts, например. call(). Таким образом, простой подсчет точек соединения, аннотированных @Log, даст результат, в два раза превышающий фактическое количество рекурсивных вызовов calcFibonacci(int). Из-за этого pointcut не должен быть просто

@annotation(log)

но

execution(* *(..)) && @annotation(log)

На самом деле, этого все еще недостаточно, потому что что, если несколько методов содержат аннотации @Log? Должны ли эти звонки учитываться? Нет, только те, что до calcFibonacci(int)! Таким образом, мы должны еще больше ограничить «счетчик вызовов Фибоначчи» чем-то вроде:

execution(* *..Application.calcFibonacci(int)) && @annotation(log)

Вот пример полностью компилируемого кода:

Аннотация:

package de.scrum_master.app;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface Log {}

Применение с рекурсивным методом Фибоначчи:

package de.scrum_master.app;

public class Application {
    public static void main(String[] args) {
        int fibonacciNumber = 6;
        System.out.println("Fibonacci #" + fibonacciNumber + " = " + new Application().calcFibonacci(fibonacciNumber));
    }

    @Log
    public int calcFibonacci(int n) {
        return n <= 1 ? n : calcFibonacci(n - 1) + calcFibonacci(n - 2);
    }
}

Аспект, версия 1:

package de.scrum_master.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

import de.scrum_master.app.Log;

@Aspect
public class LoggingAspect {
    int count = 0;

    @Before("execution(* *..Application.calcFibonacci(int)) && @annotation(log)")
    public void measure(JoinPoint thisJoinPoint, Log log) {
        System.out.println(thisJoinPoint + " - " + ++count);
    }
}

Вывод, версия 1:

execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 1
execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 2
(...)
execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 24
execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 25
Fibonacci #6 = 8

А что, если мы дважды вызовем метод Фибоначчи?

int fibonacciNumber = 6;
System.out.println("Fibonacci #" + fibonacciNumber + " = " + new Application().calcFibonacci(fibonacciNumber));
fibonacciNumber = 4;
System.out.println("Fibonacci #" + fibonacciNumber + " = " + new Application().calcFibonacci(fibonacciNumber));
execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 1
execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 2
(...)
execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 24
execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 25
Fibonacci #6 = 8
execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 26
execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 27
(...)
execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 33
execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 34
Fibonacci #4 = 3

Uh-oh!!!

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

Аспект, версия 2:

package de.scrum_master.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

import de.scrum_master.app.Log;

@Aspect("percflow(execution(* *.calcFibonacci(int)) && !cflowbelow(execution(* *.calcFibonacci(int))))")
public class LoggingAspect {
    int count = 0;

    @Before("execution(* *.calcFibonacci(int)) && @annotation(log)")
    public void measure(JoinPoint thisJoinPoint, Log log) {
        System.out.println(thisJoinPoint + " - " + ++count);
    }
}

Примечание:

  • Я сократил спецификацию пакета и класса до *, чтобы сделать исходный код более читабельным. Вы также можете использовать de.scrum_master.app.Application или любую его аббревиатуру, чтобы избежать конфликтов с похожими именами классов/методов.
  • В аннотации @Aspect теперь был параметр, который говорит: «Создать один экземпляр на выполнение метода Фибоначчи, но исключить рекурсивные».

Вывод, версия 2:

execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 1
execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 2
(..)
execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 24
execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 25
Fibonacci #6 = 8
execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 1
execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 2
(..)
execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 8
execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 9
Fibonacci #4 = 3

Теперь это выглядит намного лучше. :)))

Наслаждаться!

person kriegaex    schedule 23.06.2014
comment
Но это по-прежнему не будет работать с Spring AOP, поскольку в нем используется подход на основе прокси. Чтобы это работало, вам потребуется переплетение времени загрузки или компиляции. С простым Spring AOP это все равно не удастся. - person M. Deinum; 23.06.2014
comment
@M.Deinum: оригинальный постер просил решения Spring или AspectJ. Spring AOP с его динамическим прокси-подходом не только слишком ограничен, но и слишком медленный, на мой взгляд. Но это всего лишь личное мнение. Зачем использовать мушкет, если я могу использовать лучевое ружье? - person kriegaex; 23.06.2014
comment
Правильно, но Spring также может использовать аспекты AspectJ (см. справочное руководство :)). Однако, если вы ожидаете, что это будет работать с Spring, это не удастся из-за подхода на основе прокси. Основная проблема не в аспекте, а в способах применения этого аспекта. - person M. Deinum; 23.06.2014
comment
Нет, если вы используете полный AspectJ в Spring, он будет работать, потому что AspectJ не нужны прокси. Я говорю об AspectJ, а не только о AspectJ-совместимом синтаксисе Spring AOP. Кроме того, в моем вступительном комментарии четко сказано, что это решение AspectJ, и объясняется, почему. Spring AOP просто не создан для этого варианта использования. - person kriegaex; 23.06.2014
comment
Единственный способ, которым вы можете использовать полный AspectJ, - это переплетение времени загрузки или компиляции, и ИМХО вы забыли упомянуть об этом в своем ответе (который в остальном отличный, кстати). Я просто хотел прояснить это, и это улучшит ответ, особенно если вы добавите, почему это не работает с Spring AOP (прокси). - person M. Deinum; 23.06.2014
comment
+1, но я согласен с @M.Deinum, что вам нужно указать недостающие биты. - person Andrei Stefan; 23.06.2014
comment
kriegaex, M.Deinum - Спасибо за ваши комментарии и очень полезные ответы! Мне удалось реализовать окончательное решение на основе ваших ответов и сплетения аспектов времени компиляции (я предпочитаю улучшать код один раз и не использовать javaagents). - person Sebastian Łaskawiec; 23.06.2014

Вам нужно следующее:

<aop:aspectj-autoproxy expose-proxy="true"/>

и класс, который вычисляет значение:

@Component
public class CalcFibonacci implements CalcFibonacciInterface {

    @Log
    public int calcFibonacci(int n) {
        if(n <= 1) {
            return n;
        } else {
            return ((CalcFibonacciInterface) AopContext.currentProxy()).calcFibonacci(n - 1) 
                    + ((CalcFibonacciInterface) AopContext.currentProxy()).calcFibonacci(n - 2);
        }
    }
}

Соответствующий раздел документации: здесь.

person Andrei Stefan    schedule 20.06.2014
comment
Я нахожу довольно странным, что код, не относящийся к аспектам, должен знать и даже явно вызывать код, связанный с аспектами. Это противоречит всему, что касается АОП. - person kriegaex; 23.06.2014
comment
@kriegaex Это не странно, это уродливо, и ссылка на документацию Spring, которую я предоставил выше, явно рекомендует против этого. Единственное реальное решение вопроса — изменить код так, чтобы не было внутренних вызовов (или, в данном случае, рекурсивных вызовов методов). Существуют нерекурсивные решения Фибоначчи. алгоритм, но вопрос в этом посте был общим, а Фибоначчи был просто примером. - person Andrei Stefan; 23.06.2014
comment
Возник вопрос: можно ли создавать реентерабельные аспекты с помощью Spring AOP (или AspectJ)? Через некоторое время я собираюсь представить решение в AspectJ, мне просто нужно выполнить небольшое поручение перед этим. До скорого. - person kriegaex; 23.06.2014
comment
Это правда. С нетерпением жду вашего решения. - person Andrei Stefan; 23.06.2014
comment
@kriegaex Проблема не столько в АОП, сколько в подходе Spring к применению АОП. Он использует прокси, что означает, что только вызовы методов INTO объекта будут применяться к AOP. Внутренние вызовы методов не будут перехвачены, поэтому вам нужно получить фактический объект и снова вызвать метод. Это или использование времени загрузки или компиляции для применения аспектов. - person M. Deinum; 23.06.2014
comment
@M.Deinum: я полностью осведомлен о совместимости Spring AOP и AspectJ, различиях, ограничениях и возможностях для объединения обоих или замены Spring AOP гораздо более мощным AspectJ, который по-прежнему хорошо интегрируется со Spring. Вы можете использовать переплетение во время компиляции или во время загрузки, в зависимости от того, что вы предпочитаете. Не нужно читать мне лекции. :-) - person kriegaex; 23.06.2014