Spring AOP: как исключить ненужное выполнение @Pointcut (@Around Advice) из-за выполнения других @Pointcuts/advices

Я работаю с:

  • Spring Framework 4.3.3
  • AspectJ 1.8.9

У меня есть следующий нормальный процесс:

  • @Controller -> @Service -> @Repository

У меня есть следующая пара о AOP:

  • PersonaServicePointcut
    • PersonaServiceAspect

Сценарий следующий:

Класс @Service имеет несколько методов, таких как: delete, save, update и findOneById. Они объявляются вместе в одном классе.

Для таких методов, как delete и с update по AOP, я использую совет @Before или @Around для вызова метода findOneById.

Причина в том, что не имеет смысла выполнять методы delete или update (рассмотрите сценарий Rest), если сущность не существует. Поэтому через это advice должно быть выброшено исключение, скажем, исключение A, оно должно быть обработано в @ControllerAdvice

Практически такой же подход был применен для метода save. Поэтому перед выполнением метода save выполняется другой совет @Before или @Around, снова вызывая метод findOneById. Если объект уже существует, должно быть выдано исключение, скажем, исключение B, оно должно быть обработано в @ControllerAdvice

Обратите внимание, что у меня есть 3 пункта/3 совета, которые используют метод findOneById. Это проверить, существует ли сущность или нет.

Например:

    @Pointcut(value=
    "execution(* mypackage.PersonaServiceImpl.saveOne(otherpackage.Persona)) 
    && args(persona)")
    public void saveOnePointcut(Persona persona){}

    @Pointcut(value=
    "execution(*  
    mypackage.PersonaServiceImpl.updateOne(otherpackage.Persona)) 
    && args(persona)")
    public void updateOnePointcut(Persona persona){}

    @Pointcut(value="execution(*  
    mypackage.PersonaServiceImpl.deleteOne(String)) && args(id)")
    public void deleteOnePointcut(String id){}

Еще раз: эти 3 совета используют или выполняют метод findOneById.

Проблема в том, что я добавляю новый pointcut, например:

@Pointcut(value="execution(*    
mypackage.PersonaServiceImpl.findOneById(String)) 
&& args(id)")
public void findOneByIdPointcut(String id){}

Я создал этот pointcut, чтобы проверить, существует ли объект уже или нет, если он не существует, он должен выдать исключение типа C (это для классического 404).

Кажется излишним выполнять метод findOneById через совет @Before или @Around для самого метода findOneById. Но мне это нужно для целей logging и audit, а также для создания исключения типа C. Это должно быть обработано каким-то @ControllerAdvice

Проблема заключается в том, что когда другие советы для методов delete/update/save выполняются (помните, что они также вызывают и выполняют метод findOneById), мой findOneByIdPointcut выполняется без необходимости .

Мне нужно изменить объявление pointcut, чтобы указать что-то вроде этого:

@Pointcut(Alpha)
public void findOneByIdPointcut(String id){}

Где Alpha:

выполнить совет до/вокруг для метода findOneById @Service, но никогда, если его вызов был выполнен из других советов из класса
PersonaServiceAspect.

Я пробовал много способов с комбинациями !execution и !within, но безрезультатно.

Даже когда я создал только один Pointcut, который перехватывает все методы @Service с соответствующим уникальным советом @Around, и через параметр ProceedingJoinPoint proceedingJoinPoint я могу проверить, какой метод был вызван, а затем выполнить соответствующие элементы управления. Но опять такое поведение происходит.

Это означает, через следующее:

@Around("PersonaServicePointcut.anyMethodPointcut()")
    public Object aroundAdviceAnyMethod(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{

где anyMethodPointcut равно execution(* mypackage.PersonaServiceImpl.*(..))

Возможен ли такой подход? Как?

Спасибо.


person Manuel Jordan    schedule 02.10.2016    source источник


Ответы (2)


Вы можете исключить pointcut из потока управления выражением pointcut с помощью

!cflow(<pointcut>)

В вашем примере вы хотите исключить те исполнения findOneById, где выполнение находится внутри потока управления вашего собственного совета. Если ваш совет применим к выражению pointcut

@Pointcut("saveOnePointcut() || updateOnePointcut() || deleteOnePointcut()")
public void combinedPointcut() {}

вы можете исключить это с помощью:

!cflow(combinedPointcut())

или просто исключить все потоки управления из потока управления всеми выполнениями рекомендаций, вы можете использовать:

!cflow(adviceexecution())

Ваш совет по поиску будет выглядеть так, основываясь на комбинированном выражении pointcut:

@Around("findOneByIdPointcut() && !cflow(combinedPointcut())")
public void aroundFind() {
    ....
}

Обратите внимание, что вы не можете комбинировать выражения pointcut в своем примере, потому что они связывают аргументы pointcut. Вам нужно удалить части && args(...) и переместить их непосредственно в выражения pointcut совета. Вы не можете комбинировать связывающие выражения pointcut, такие как args(paramName), с оператором || (или), поэтому вам нужно будет создать отдельные рекомендации для этих случаев. Вы по-прежнему можете делегировать большую часть работы из этих советов одному методу, если хотите. См. пример такого делегирования здесь.

person Nándor Előd Fekete    schedule 03.10.2016
comment
Большое спасибо за подробное объяснение. Позвольте мне провести тестирование рядом со мной. О binding pointcut arguments У меня были некоторые проблемы при микшировании. Так что вы правы. Поэтому следующее кажется невозможным но с параметрами, если возможно"> stackoverflow.com/questions/39730232/ - person Manuel Jordan; 03.10.2016
comment
Да, вы получите сообщение об ошибке inconsistent binding. Но это очень легко обойти, пример показан в моем ответе здесь. В моем ответе вы увидите примеры привязки выражений pointcut для @this() и @annotation(), но то же самое относится к любому другому выражению привязки pointcut, args() в вашем случае. - person Nándor Előd Fekete; 03.10.2016
comment
Я получаю org.aspectj.weaver.tools.UnsupportedPointcutPrimitiveException: это, потому что cflow не поддерживается Spring, как указано здесь: docs.spring.io/spring/docs/current/spring-framework-reference/. Проверяем прямо сейчас: docs.spring.io/spring/docs/current/spring-framework-reference/ - person Manuel Jordan; 03.10.2016
comment
Есть ли другой способ сделать это? Мое @AspectJ использование @Transactional. Так что даже если я настрою его на Load-time weaving, он не обрабатывается Spring. Даже больше, когда мой класс использует @Autowired - person Manuel Jordan; 03.10.2016
comment
Я смог решить эту проблему без cflow. Я сделал рефакторинг (там истинная проблема). Но просто любопытно, возможен мой предыдущий комментарий или нет. - person Manuel Jordan; 03.10.2016
comment
Вам нужно решить, хотите ли вы придерживаться Spring AOP (уменьшенная функциональность и другие ограничения, т.е. на основе прокси) или использовать полный AspectJ. Я добился большого успеха с использованием времени компиляции AspectJ в своих проектах, вам просто нужно соответствующим образом настроить свою сборку. Плетение во время загрузки тоже должно работать. Интегрировать AspectJ с Spring довольно просто, так что это не должно быть проблемой. - person Nándor Előd Fekete; 04.10.2016
comment
Спасибо за обновление. Я проведу более глубокое исследование. Не уверен, что Load-time weaving should work too будет работать с @Transactional - person Manuel Jordan; 04.10.2016
comment
Да, это было бы. Я использовал весеннее управление транзакциями как с переплетением времени загрузки, так и с переплетением времени компиляции. - person Nándor Előd Fekete; 04.10.2016
comment
Большое спасибо за подтверждение. Просто любопытно, есть ли у вас учебник, сочетающий эти два подхода, но через Spring Java Config. Кажется обязательным до сих пор использовать aop.xml в META-INF - person Manuel Jordan; 05.10.2016
comment
Я не могу сразу придумать учебник, но при включении управления транзакциями Spring с помощью AspectJ важно использовать атрибут mode @EnableTransactionManagement со значением ASPECTJ. Использование aop.xml не является обязательным, насколько мне известно, модуль spring-aspects, который содержит код управления транзакциями AspectJ, содержит его, хотя и для своих собственных аспектов. - person Nándor Előd Fekete; 05.10.2016
comment
Спасибо, проведу исследование. Об учебнике Я имею в виду, если вы знаете хороший учебник, через Google результаты за 2008 и 2010 годы. С уважением! - person Manuel Jordan; 06.10.2016

Если я правильно понимаю, вы хотите, чтобы findOneByIdPointcut не вызывался внутри вашего совета, но должен запускаться во всех других случаях.

Один из способов сделать это — использовать call в сочетании с within точками. call переместит точку соединения вверх к вызывающему методу, а within исключит рекомендации из целевых точек соединения.

Таким образом, ваш код аспекта может выглядеть так:

public class ControllerAspect {

    ...

    @Pointcut(value = "execution(* PersonaServiceImpl.deleteOne(String)) && args(id)")
    public void deleteOnePointcut(String id)
    {
    }

    @Pointcut(value = "call(* PersonaServiceImpl.findOneById(String)) && args(id)")
    public void findOneByIdPointcut(String id)
    {
    }

    @Before(value = "findOneByIdPointcut(id) && !within(ControllerAspect)")
    public void beforeFindOneByIdAdvice(String id)
    {
       //do some logic here
    }


    @Before(value = "deleteOnePointcut(id)")
    public void beforeDeleteOneAdvice(String id)
    {
        Persona persona = personaService.findOneById(id);
        //check that persona is not null
    }
}

Другой способ - использовать cflow pointcut, как описано в ответе @Nándor Előd Fekete, но вы должны отметить, что в этом случае все beforeFindOneByIdAdvice точки соединения ниже выполнения совета в стеке также будут исключены. Так что это может иметь некоторые неожиданные результаты для вас в будущем.

Вы можете увидеть, как работает cflow, в разделе "Аспектно-ориентированное программирование - что такое cflow?" ответ.

person Sergey Bespalov    schedule 04.10.2016
comment
Еще раз спасибо за вашу ценную поддержку. Я уже решил это, выполнив рефакторинг. Хитрость заключается в том, что вызов findOneById должен выполняться с использованием зависимости нижнего уровня. Это означает, что через @Repository. Поэтому советы по поводу save, delete, update не влияют косвенно и излишне на советы по find. Это была моя ошибка. Дело в том, что Aspect перехватывает любой вызов метода @Service. Оттуда каждый совет Aspect вызывает или выполняет метод findOneById через зависимость @Repository. - person Manuel Jordan; 04.10.2016