Как исключить перехват внутренних методов в AspectJ AOP? - PullRequest
1 голос
/ 04 июня 2019

Мы переносим приложение, использующее Jboss AOP (на основе прокси), в AspectJ AOP с переплетением времени компиляции.Однако мы не хотим, чтобы AspectJ перехватывал внутренние методы, но это похоже на поведение AspectJ по умолчанию.

В Spring AOP есть несколько сообщений о том, как перехватывать внутренние вызовы методов.Тем не менее, не удалось найти сообщения, связанные с исключением внутренних методов с использованием AspectJ.Мы хотим использовать AspectJ для компиляции времени компиляции для обещанных улучшений производительности во время выполнения.

Если метод другого класса вызывает какой-либо открытый метод в нижеприведенном классе TestService, вызов должен быть перехвачен.Однако внутренний вызов от method1 () к method2 () не должен быть перехвачен.Мы просто хотим, чтобы перехватчики перехватывали только один раз для каждого объекта.

public class TestService {
  public void method1() {
    …
    // We do not want the below internal call to be intercepted. 
    this.method2();
  }

  // If some other class's method calls this, intercept the call. But do not intercept the call from method1().
  public void method2() {
    ...     
  }
}

Пример Aspect:

@Aspect
public class ServiceAspectJHydrationInterceptor {
    @Pointcut("execution(public * com.companyname.service..impl.*ServiceImpl.*(..))")
    public void serviceLayerPublicMethods() {}

    @Pointcut("@annotation(com.companyname.core.annotation.SkipHydrationInterception)")
    public void skipHydrationInterception() {}

    @Around("serviceLayerPublicMethods() && !skipHydrationInterception()")
    public Object invoke(ProceedingJoinPoint pjp) throws Throwable {
        …
    }
}

Поведение исключения перехвата внутреннего вызова метода по умолчанию в Spring AOP по умолчаниюна основе прокси.Есть ли способ добиться исключения внутреннего перехвата метода с использованием AspectJ с переплетением времени компиляции?

Сведения о программном обеспечении: версия Spring: 3.2.14.Версия JDK: 1.8.Плагин maven codehaus «aspectj-maven-plugin» версии 1.7 используется для создания времени компиляции.

1 Ответ

0 голосов
/ 05 июня 2019

Вы можете использовать шаблон execution(...) && !cflowbelow(execution(...)).Это не хорошо для производительности, потому что путь выполнения (например, callstack) должен проверяться во время выполнения, а не во время компиляции, но он делает то, что вы хотите.Остерегайтесь некоторых ключевых отличий из-за непрокси-природы AspectJ и из-за более широкого набора точек соединения и точек доступа, доступных по сравнению с другими средами AOP, такими как перехват частных или статических методов.

Теперь вотНебольшой пример в соответствии с тем, что вы описали:

package de.scrum_master.core.annotation;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

@Retention(RUNTIME)
@Target(METHOD)
public @interface SkipHydrationInterception {}
package de.scrum_master.service.foo.bar.impl;

import de.scrum_master.core.annotation.SkipHydrationInterception;

public class MyServiceImpl {
  public void method1() {
    // We do not want the below internal call to be intercepted.
    method2();
  }

  public void method2() {
    // If some other class's method calls this, intercept the call. But do not
    // intercept the call from method1().
  }

  @SkipHydrationInterception
  public void method3() {
    // Always skip this method one due to the annotation.

    // Should this one be intercepted or not?
    // method1();
  }

  public static void main(String[] args) {
    MyServiceImpl service = new MyServiceImpl();
    service.method1();
    System.out.println("-----");
    service.method2();
    System.out.println("-----");
    service.method3();
  }
}
package de.scrum_master.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class ServiceAspectJHydrationInterceptor {
  @Pointcut("execution(public !static * de.scrum_master.service..impl.*ServiceImpl.*(..))")
  public void serviceLayerPublicMethods() {}

  @Pointcut("@annotation(de.scrum_master.core.annotation.SkipHydrationInterception)")
  public void skipHydrationInterception() {}

  @Pointcut("serviceLayerPublicMethods() && !skipHydrationInterception()")
  public void interceptMe() {}

  @Around("interceptMe() && !cflowbelow(interceptMe())")
  public Object invoke(ProceedingJoinPoint pjp) throws Throwable {
    System.out.println(pjp);
    return pjp.proceed();
  }
}

Теперь запустите приложение драйвера, и вы увидите этот журнал консоли:

execution(void de.scrum_master.service.foo.bar.impl.MyServiceImpl.method1())
-----
execution(void de.scrum_master.service.foo.bar.impl.MyServiceImpl.method2())
-----

Это точното, что ты хочешь.Все идет нормально.Также обратите внимание на квалификатор !static в точке выполнения, поскольку в противном случае static main(..) будет перехвачен.

Но теперь раскомментируйте вызов method1() внутри тела method3().Журнал консоли становится:

execution(void de.scrum_master.service.foo.bar.impl.MyServiceImpl.method1())
-----
execution(void de.scrum_master.service.foo.bar.impl.MyServiceImpl.method2())
-----
execution(void de.scrum_master.service.foo.bar.impl.MyServiceImpl.method1())

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

Также обратите внимание, что открытые методы, вызываемые из закрытых или защищенных методов того же класса, также будут перехватываться.Так что cflow() или cflowbelow() не заботятся о самопризыве, только о указанном потоке управления.

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

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

Так что мое решение, даже если мы настроим эти точки, чтобы покрытьВ нескольких ключевых случаях это не то же самое, что решение на основе прокси по своей природе.Если в вашей среде могут произойти такие угловые случаи, как описанные, вам действительно следует провести рефакторинг аспектов, чтобы вести бухгалтерский учет (сохранение состояния) и / или использовать другую модель создания экземпляров, такую ​​как percflowbelow (но не думал, чтоодин, потому что я не знаю ваших точных требований).Но ТАК не дискуссионный форум, и я не могу помочь вам здесь постепенно.Не стесняйтесь проверить контактные данные (например, Telegram) в моем профиле SO и нанять меня, если вам нужна более глубокая поддержка.Но, может быть, вы также можете взять его отсюда, я просто упоминаю об этом.


Обновление:

Хорошо, я придумал способ эмулировать проксиповедение на основе AOP через AspectJ.Мне это не нравится, и оно требует, чтобы вы переключились с execution() на call() pointcut, т.е. вам больше не нужно контролировать (аспектное переплетение) вызываемый объект (исполняемый код), а вызывать вызывающий объект (источник вызова методаперехвачено).

Вам также понадобится проверка во время выполнения между двумя объектами this() и target() из if() pointcut.Мне это тоже не нравится, потому что это замедляет ваш код и требует проверки во многих местах.Если вы все еще можете достичь своей цели повышения производительности по сравнению с прокси-решением, от которого вы хотите избавиться, вы должны проверить это самостоятельно.Помните, вы теперь эмулируете то, что хотите отменить, LOL.

Давайте добавим еще один класс, чтобы имитировать взаимодействие внешнего класса, вызывающего целевой класс, в дополнение к простому вызову его из статического метода, которыйнедостаточно для проверки.

package de.scrum_master.service.foo.bar.impl;

public class AnotherClass {
  public void doSomething() {
    MyServiceImpl service = new MyServiceImpl();
    service.method1();
    System.out.println("-----");
    service.method2();
    System.out.println("-----");
    service.method3();
    System.out.println("-----");
  }
}

Исходный класс MyServiceImpl мы немного расширили, регистрируя больше и также вызывая AnotherClass.doSomething().

package de.scrum_master.service.foo.bar.impl;

import de.scrum_master.core.annotation.SkipHydrationInterception;

public class MyServiceImpl {
  public void method1() {
    System.out.println("method1");
    method2();
  }

  public void method2() {
    System.out.println("method2");
  }

  @SkipHydrationInterception
  public void method3() {
    System.out.println("method3");
    method1();
  }

  public static void main(String[] args) {
    MyServiceImpl service = new MyServiceImpl();
    service.method1();
    System.out.println("-----");
    service.method2();
    System.out.println("-----");
    service.method3();
    System.out.println("-----");
    new AnotherClass().doSomething();
  }
}

Улучшенный аспект выглядит так:это:

package de.scrum_master.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class ServiceAspectJHydrationInterceptor {
  @Pointcut("call(public !static * de.scrum_master.service..impl.*ServiceImpl.*(..))")
  public void serviceLayerPublicMethods() {}

  @Pointcut("@annotation(de.scrum_master.core.annotation.SkipHydrationInterception)")
  public void skipHydrationInterception() {}

  @Pointcut("serviceLayerPublicMethods() && !skipHydrationInterception()")
  public void interceptMe() {}

  @Pointcut("if()")
  public static boolean noSelfInvocation(ProceedingJoinPoint thisJoinPoint) {
    return thisJoinPoint.getThis() != thisJoinPoint.getTarget();
  }

  @Around("interceptMe() && noSelfInvocation(thisJoinPoint)")
  public Object invoke(ProceedingJoinPoint thisJoinPoint, JoinPoint.EnclosingStaticPart thisEnclosingStaticPart) throws Throwable {
    System.out.println(thisJoinPoint);
    System.out.println("  called by: " + thisEnclosingStaticPart);
    return thisJoinPoint.proceed();
  }
}

А теперь журнал консоли выглядит так:

call(void de.scrum_master.service.foo.bar.impl.MyServiceImpl.method1())
  called by: execution(void de.scrum_master.service.foo.bar.impl.MyServiceImpl.main(String[]))
method1
method2
-----
call(void de.scrum_master.service.foo.bar.impl.MyServiceImpl.method2())
  called by: execution(void de.scrum_master.service.foo.bar.impl.MyServiceImpl.main(String[]))
method2
-----
method3
method1
method2
-----
call(void de.scrum_master.service.foo.bar.impl.MyServiceImpl.method1())
  called by: execution(void de.scrum_master.service.foo.bar.impl.AnotherClass.doSomething())
method1
method2
-----
call(void de.scrum_master.service.foo.bar.impl.MyServiceImpl.method2())
  called by: execution(void de.scrum_master.service.foo.bar.impl.AnotherClass.doSomething())
method2
-----
method3
method1
method2
-----

По моему мнению, именно так будет вести себя Spring AOP или JBoss AOP из-за своей природы прокси. Может быть, я что-то забыл, но я думаю, что я довольно хорошо раскрыл угловые случаи.

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

...