Как передать переменные в качестве параметров аспекту - PullRequest
1 голос
/ 15 февраля 2020
@Component("taskCreateListener")
public class TaskCreateListener implements FlowableEventListener { 

@LogMethod
    @DetermineCaseTypeOfWork
    @Override
    public void onEvent(FlowableEvent event) {

///Do stuff that results in variables I want to pass to aspect
//For example, get ids and details and set to variables that I want to pass to insertThing() method once onEvent(FlowableEvent event) is finished executing
//ex:
//String procInstId = "abc1234";
//String type = "case1";
}

Мне нужно, чтобы onEvent полностью завершил, а затем передал локальные переменные, заданные в onEvent (событие FlowableEvent), в мой аспект insertThing ():

@Aspect
@Component
public class DetermineCaseTypeOfWork {

@Transactional
@After(@annotation(path goes here))
public void insertThing() {
    //Do something here with the variables passed in from doSomething method
//if(procInstId.equals("abc1234") && type.equals("case1")) {
//do something
} else if(//other id and type) {
//do something else
} else {
//do something else
}
}

Я не могу изменить onEvent (Событие FlowableEvent) для возврата чего-либо, и метод onEvent (событие FlowableEvent) должен сначала полностью завершиться, так как бы я go о передаче параметров в insertThing ()?

Ответы [ 2 ]

2 голосов
/ 18 февраля 2020

Предисловие / Обоснование

Я бы не предложил использовать решение Даниил , поскольку

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

Основная проблема Это образ мышления ОП ( moesyzlack23 ): он сказал, что хочет «передать параметры аспекту». Это нарушает основополагающий принцип c AOP, согласно которому аспект должен знать, как добавить сквозное поведение, но код приложения должен соответствовать аспекту c.

Решение AspectJ

Я бы предложил

  • просто добавить метод, отвечающий за вычисление результатов в класс TaskCreateListener и вызывать его из onEvent(..),
  • переключается из Spring AOP в AspectJ, как описано в руководстве Spring , и использует такие функции, как cflowbelow pointcut и percflow аспектное создание экземпляров, таким образом избавляясь от локальных потоков и делая ядро снова в коде c AOP,
  • опционально преобразует Map<String, Object> в более безопасный для типов объект данных с обычными методами получения. Это будет трудно только в том случае, если этот аспект применяется ко многим аннотированным методам с очень разнообразным набором данных для обработки. Но мне кажется, что этот аспект довольно специфичен c.

Вот простой пример AspectJ (без Spring), который можно легко интегрировать в приложение Spring после включения полного AspectJ :

Вспомогательные классы:

package de.scrum_master.app;

public class FlowableEvent {}
package de.scrum_master.app;

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 DetermineCaseTypeOfWork {}

Аспект целевой класс:

package de.scrum_master.app;

public class TaskCreateListener {
  @DetermineCaseTypeOfWork
  public void onEvent(FlowableEvent event) {
    // Calculate values which might be dependent on 'event' or not
    Data data = calculateData(event);
    System.out.println("[" + Thread.currentThread().getId() + "] onEvent: " + data);
  }

  public Data calculateData(FlowableEvent event) {
    return new Data("thread-" + Thread.currentThread().getId(), "case1");
  }

  public static class Data {
    private String procInstId;
    private String type;

    public Data(String procInstId, String type) {
      this.procInstId = procInstId;
      this.type = type;
    }

    public String getProcInstId() {
      return procInstId;
    }

    public String getType() {
      return type;
    }

    @Override
    public String toString() {
      return "Data[procInstId=" + procInstId + ", type=" + type + "]";
    }
  }
}

Внутренний * Класс 1053 * является необязательным, вы можете просто продолжать использовать Map<String, Object> и изменить класс и аспект (см. Ниже), чтобы использовать карту вместо этого.

Приложение драйвера, запускающее несколько потоков:

package de.scrum_master.app;

public class Application {
  public static void main(String[] args) {
    for (int taskCount = 0; taskCount < 5; taskCount++) {
      new Thread(() -> new TaskCreateListener().onEvent(new FlowableEvent())).start();
    }
  }
}

Журнал консоли без аспекта:

[11] onEvent: Data[procInstId=thread-11, type=case1]
[12] onEvent: Data[procInstId=thread-12, type=case1]
[13] onEvent: Data[procInstId=thread-13, type=case1]
[10] onEvent: Data[procInstId=thread-10, type=case1]
[14] onEvent: Data[procInstId=thread-14, type=case1]

Пока все очень просто.

Аспект:

package de.scrum_master.aspect;

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

import de.scrum_master.app.TaskCreateListener.Data;

@Aspect("percflow(myPointcut())")
public class DetermineTypeOfWorkAspect {
  private Data data;

  @Pointcut("execution(* *(..)) && @annotation(de.scrum_master.app.DetermineCaseTypeOfWork)")
  private void myPointcut() {}

  @Around("myPointcut()")
  public void insertThing(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println("[" + Thread.currentThread().getId() + "] " + joinPoint);
    joinPoint.proceed();
    System.out.println("[" + Thread.currentThread().getId() + "] " + "insertThing: " + data);
  }

  @AfterReturning(pointcut = "execution(* *(..)) && cflowbelow(myPointcut())", returning = "result")
  public void saveData(JoinPoint joinPoint, Data result) throws Throwable {
    System.out.println("[" + Thread.currentThread().getId() + "] " + joinPoint);
    data = result;
  }
}

Обратите внимание:

  • @Aspect("percflow(myPointcut())") гарантирует, что аспект не является единичным, что будет значением по умолчанию. Вместо этого один экземпляр аспекта создается каждый раз, когда приложение входит в поток управления myPointcut(), т.е. каждый раз, когда выполняется метод, аннотированный DetermineCaseTypeOfWork.
  • Совет @Around insertThing основан на @AfterReturning рекомендация saveData выполняется во время ожидания возврата joinPoint.proceed().
  • @AfterReturning рекомендация saveData запускается при каждом выполнении метода ниже поток управления myPointcut() и возвращение Data объекта. Методы, возвращающие что-то еще или выполняемые вне указанного потока управления, будут игнорироваться. Этот совет гарантирует, что результат перехваченного вызова метода назначается закрытой переменной Data, к которой позже может обратиться совет insertThing.
  • Я добавляю execution(* *(..)) && к pointcuts, потому что в В AspectJ есть и другие точки соединения, такие как метод call(), кроме того, выполнение метода является единственным поддерживаемым типом в Spring AOP. Поэтому вам не нужно указывать c, в AspectJ вы должны это сделать.
  • Если вы удалите экземпляр percflow(myPointcut()) из аннотации @Aspect, вам придется сделать приватный Data член a ThreadLocal<Data> вместо того, чтобы снова сделать аспект потокобезопасным. Это также работает и по-прежнему сохраняет ядро ​​приложения свободным от локальной обработки потока, но сам аспект должен был бы иметь с ним дело.

Журнал консоли с активным аспектом:

[10] execution(void de.scrum_master.app.TaskCreateListener.onEvent(FlowableEvent))
[14] execution(void de.scrum_master.app.TaskCreateListener.onEvent(FlowableEvent))
[12] execution(void de.scrum_master.app.TaskCreateListener.onEvent(FlowableEvent))
[13] execution(void de.scrum_master.app.TaskCreateListener.onEvent(FlowableEvent))
[11] execution(void de.scrum_master.app.TaskCreateListener.onEvent(FlowableEvent))
[14] execution(TaskCreateListener.Data de.scrum_master.app.TaskCreateListener.calculateData(FlowableEvent))
[14] onEvent: Data[procInstId=thread-14, type=case1]
[14] insertThing: Data[procInstId=thread-14, type=case1]
[11] execution(TaskCreateListener.Data de.scrum_master.app.TaskCreateListener.calculateData(FlowableEvent))
[11] onEvent: Data[procInstId=thread-11, type=case1]
[10] execution(TaskCreateListener.Data de.scrum_master.app.TaskCreateListener.calculateData(FlowableEvent))
[12] execution(TaskCreateListener.Data de.scrum_master.app.TaskCreateListener.calculateData(FlowableEvent))
[12] onEvent: Data[procInstId=thread-12, type=case1]
[10] onEvent: Data[procInstId=thread-10, type=case1]
[10] insertThing: Data[procInstId=thread-10, type=case1]
[12] insertThing: Data[procInstId=thread-12, type=case1]
[11] insertThing: Data[procInstId=thread-11, type=case1]
[13] execution(TaskCreateListener.Data de.scrum_master.app.TaskCreateListener.calculateData(FlowableEvent))
[13] onEvent: Data[procInstId=thread-13, type=case1]
[13] insertThing: Data[procInstId=thread-13, type=case1]

Обратите внимание, как идентификаторы потоков в начале каждой строки журнала соответствуют значению procInstId. Это доказывает, что на самом деле он работает без локальных потоков из-за модели реализации аспекта percflow.

Решение Spring AOP

Альтернатива Spring AOP: Если вы хотите придерживаться Spring AOP, вы не можете использовать ни percflow instantiation, ни cflowbelow pointcuts, потому что Spring AOP просто делает не поддерживает эти функции. Таким образом, вместо первого вы все равно могли бы использовать ThreadLocal внутри аспекта, а вместо второго вы могли бы разделить вычисления на отдельный компонент / компонент Spring и убедиться, что совет saveData перехватывает этот. Поэтому, вероятно, цена отказа от использования AspectJ (если вы так склонны избегать его) все равно будет приемлемой: один локальный поток плюс один новый компонент. Пожалуйста, дайте мне знать, если вы заинтересованы в таком подходе.


Обновление:

Мне также было бы интересно увидеть ваш подход с использованием Spring AOP если вы не против поделиться.

Хорошо. Я опубликую завершенный MCVE снова с разными именами пакетов, чтобы устранить неоднозначность всех классов (некоторые с незначительными или существенными изменениями) из примера кода AspectJ.

Вспомогательные классы:

package de.scrum_master.spring.q60234800;

public class FlowableEvent {}
package de.scrum_master.spring.q60234800;

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 DetermineCaseTypeOfWork {}

Компонент поставщика данных:

Это новый компонент / компонент, о котором я говорил. Опять же, вместо внутреннего класса Data вы можете использовать Map, но это будет менее безопасно для типов. Решение зависит от того, насколько конкретно c или generi c вам нужно ваше решение. Здесь важно то, что сам провайдер является одноэлементным компонентом, но предоставляет новый экземпляр Data при каждом вызове calculateData(..). Поэтому вам нужно убедиться, что метод зависит только от его входных параметров, а не от полей класса, чтобы быть потокобезопасным.

package de.scrum_master.spring.q60234800;

import org.springframework.stereotype.Component;

@Component
public class DataProvider {
  public Data calculateData(FlowableEvent event) {
    return new Data("thread-" + Thread.currentThread().getId(), "event-" + event.hashCode());
  }

  public static class Data {
    private String procInstId;
    private String type;

    public Data(String procInstId, String type) {
      this.procInstId = procInstId;
      this.type = type;
    }

    @Override
    public String toString() {
      return "Data[procInstId=" + procInstId + ", type=" + type + "]";
    }
  }
}

Компонент слушателя:

Это также обычный синглтон-компонент, который автоматически вводит поставщик данных.

package de.scrum_master.spring.q60234800;

import de.scrum_master.spring.q60234800.DataProvider.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class TaskCreateListener {
  @Autowired
  DataProvider dataProvider;

  @DetermineCaseTypeOfWork
  public void onEvent(FlowableEvent event) {
    // Calculate values which might be dependent on 'event' or not
    Data data = dataProvider.calculateData(event);
    System.out.println("[" + Thread.currentThread().getId() + "] onEvent: " + data);
  }
}

Приложение драйвера:

Опять же, приложение создает несколько потоков и запускает TaskCreateListener.onEvent(..) для каждого из них.

package de.scrum_master.spring.q60234800;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

import java.util.stream.IntStream;

@SpringBootApplication
@Configuration
@EnableAspectJAutoProxy//(proxyTargetClass = true)
public class Application {
  public static void main(String[] args) {
    try (ConfigurableApplicationContext appContext = SpringApplication.run(Application.class, args)) {
      TaskCreateListener taskCreateListener = appContext.getBean(TaskCreateListener.class);
      IntStream.range(0, 5).forEach(i ->
        new Thread(() -> taskCreateListener.onEvent(new FlowableEvent())).start()
      );
    }
  }
}

Аспект Spring AOP:

Как описано ранее, нам необходимо поле ThreadLocal<Data> для обеспечения безопасности потока, потому что также аспект - одиночный боб. Комбинация двух пар pointcut / advice для двух разных компонентов гарантирует, что сначала мы соберем и сохраним права Data, а затем используем их в другом совете.

package de.scrum_master.spring.q60234800;

import de.scrum_master.spring.q60234800.DataProvider.Data;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class DetermineTypeOfWorkAspect {
  private ThreadLocal<Data> data = new ThreadLocal<>();

  @Around("@annotation(de.scrum_master.spring.q60234800.DetermineCaseTypeOfWork)")
  public void insertThing(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println("[" + Thread.currentThread().getId() + "] " + joinPoint);
    joinPoint.proceed();
    System.out.println("[" + Thread.currentThread().getId() + "] " + "insertThing: " + data.get());
  }

  @AfterReturning(pointcut = "execution(* calculateData(..))", returning = "result")
  public void saveData(JoinPoint joinPoint, Data result) throws Throwable {
    System.out.println("[" + Thread.currentThread().getId() + "] " + joinPoint);
    data.set(result);
  }
}

Журнал консоли:

Как и в решении AspectJ, идентификаторы потоков в начале строк журнала соответствуют идентификаторам, захваченным в Data объектах.

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.5.2.RELEASE)

(...)
2020-02-20 08:03:47.494  INFO 12864 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2020-02-20 08:03:47.498  INFO 12864 --- [           main] d.s.spring.q60234800.Application         : Started Application in 4.429 seconds (JVM running for 5.986)
[33] execution(void de.scrum_master.spring.q60234800.TaskCreateListener.onEvent(FlowableEvent))
[32] execution(void de.scrum_master.spring.q60234800.TaskCreateListener.onEvent(FlowableEvent))
[35] execution(void de.scrum_master.spring.q60234800.TaskCreateListener.onEvent(FlowableEvent))
[34] execution(void de.scrum_master.spring.q60234800.TaskCreateListener.onEvent(FlowableEvent))
[36] execution(void de.scrum_master.spring.q60234800.TaskCreateListener.onEvent(FlowableEvent))
[33] execution(Data de.scrum_master.spring.q60234800.DataProvider.calculateData(FlowableEvent))
[35] execution(Data de.scrum_master.spring.q60234800.DataProvider.calculateData(FlowableEvent))
[34] execution(Data de.scrum_master.spring.q60234800.DataProvider.calculateData(FlowableEvent))
[33] onEvent: Data[procInstId=thread-33, type=event-932577999]
[33] insertThing: Data[procInstId=thread-33, type=event-932577999]
[34] onEvent: Data[procInstId=thread-34, type=event-1335128372]
[34] insertThing: Data[procInstId=thread-34, type=event-1335128372]
[36] execution(Data de.scrum_master.spring.q60234800.DataProvider.calculateData(FlowableEvent))
[36] onEvent: Data[procInstId=thread-36, type=event-130476008]
[32] execution(Data de.scrum_master.spring.q60234800.DataProvider.calculateData(FlowableEvent))
[36] insertThing: Data[procInstId=thread-36, type=event-130476008]
[35] onEvent: Data[procInstId=thread-35, type=event-987686114]
[35] insertThing: Data[procInstId=thread-35, type=event-987686114]
[32] onEvent: Data[procInstId=thread-32, type=event-1849439251]
[32] insertThing: Data[procInstId=thread-32, type=event-1849439251]
2 голосов
/ 17 февраля 2020

Согласно вашему вопросу, нет возможности изменить подпись метода onEvent (), который должен обрабатываться аспектом. Вы можете попытаться создать контейнерный класс на основе ThreadLocal, который инициализируется в аспекте перед вызовом onEvent () и оценивается после завершения onEvent (). Но такой подход требует от вас возможности редактировать код onEvent () (но не требует изменения его возвращаемого типа). Вот некоторые подробности:

public class VariableContextHolder {

/**
 * ThreadLocal with map storing variables
 */
private final ThreadLocal<Map<String, Object>> threadlocal = new ThreadLocal<>();

private static VariableContextHolder instance;

private VariableContextHolder () {

}

public final static VariableContextHolder getInstance() {
    if (instance == null) {
        instance = new VariableContextHolder ();
    }
    return instance;
}

public Map<String, Object>get() {
    return threadlocal.get();
}

public void set(Map<String, Object>map) {
    threadlocal.set(map);
}

public void clear() {
    threadlocal.remove();
}
}

Класс аспекта:

@Aspect()
public class DetermineCaseTypeOfWork {

@Transactional
@Around(@annotation("path goes here"))
public void insertThing(ProceedingJoinPoint joinPoint) throws Throwable {

// save initialized map to threadlocal    
VariableContextHolder.getInstance().set(new HashMap<>());

// method onEvent() will be called
joinPoint.proceed();

// retrieve map from threadlocal
Map<String, Object> variablesMap = VariableContextHolder.getInstance().get();

// get variables by names and handle them
String procInstId = variablesMap.get("procInstId");

// clear threadlocal after using it
VariableContextHolder.getInstance().clear();
}

}

Изменения, которые необходимо сделать в методе onEvent ():

public void onEvent(FlowableEvent event) {

    // retrieve map from threadlocal
    Map<String, Object> variablesMap = VariableContextHolder.getInstance().get();
    String procInstId = "abc1234";
    String type = "case1";
    // save variables to map
    variablesMap.put("procInstId", procInstId);
    variablesMap.put("type", type);
}
...