如何将变量作为参数传递给方面

How to pass variables as parameters to an Aspect

@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 event) 中设置的局部变量传递到我的方面 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 event) 方法修改为 return 某些东西并且 onEvent(FlowableEvent event) 方法必须首先完全完成,那么我将如何将参数传递给 insertThing() ?

根据您的问题,不可能更改 onEvent() 方法的签名,这应该由方面处理。您可以尝试创建一个基于 ThreadLocal 的容器 class,它在调用 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();
}
}

看点class:

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

前言/基本原理

我不建议使用 因为

  • 它增加了处理线程局部变量的复杂性到方面(好)和核心业务代码(我认为不好),
  • 作为映射的上下文持有者并不是特别安全的类型,
  • 核心业务代码在没有方面的情况下不再工作(它依赖于初始化上下文持有者的方面)并且与它有着千丝万缕的联系,这是一个糟糕的设计决策。

主要问题出在OP(moesyzlack23)的思维方式上:他说,他想"pass parameters to the aspect"。这违反了基本的 AOP 原则,即方面应该知道如何添加横切行为,但应用程序代码应该与方面无关。

AspectJ 解决方案

我建议

  • 只需将负责计算结果的方法添加到TaskCreateListener class 并从onEvent(..),
  • 调用它
  • 从 Spring AOP 切换到 AspectJ,如 Spring manual 中所述,并使用 cflowbelow 切入点和 percflow 方面实例化等功能,从而摆脱线程本地化并再次使核心代码与 AOP 无关,
  • 可选择使用常规 getter 方法将 Map<String, Object> 转换为类型更安全的数据对象。只有当该方面适用于许多带有要处理的非常多样化的数据集的注释方法时,这才会很困难。但对我来说,这方面看起来非常具体。

这是一个简单的 AspectJ 示例(没有 Spring),您可以在启用完整的 AspectJ 后轻松地将其集成到您的 Spring 应用程序中:

助手 classes:

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

方面目标class:

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 + "]";
    }
  }
}

内部Data class是可选的,你可以继续使用Map<String, Object>并重构class和方面(见下文)以便请改用地图。

启动多个线程的驱动程序应用程序:

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() 到 return 期间执行。
  • @AfterReturning 建议 saveData 每次方法执行都会触发 下面 [= 的控制流32=] 和 returning 一个 Data 对象。方法 return 执行其他操作或在指定控制流之外执行的方法将被忽略。该建议确保被截获的方法调用的结果被分配给一个私有 Data 变量,该变量稍后可以被 insertThing 建议访问。
  • 我将 execution(* *(..)) && 添加到切入点,因为在 AspectJ 中还有其他连接点,例如方法 call() 此外方法执行是 Spring AOP 中唯一受支持的类型。所以你不需要那么具体,在 AspectJ 中你应该这样做。
  • 如果从 @Aspect 注释中删除 percflow(myPointcut()) 实例化,则必须将私有 Data 成员改为 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]

请注意每个日志行开头的线程 ID 如何对应 procInstId 的值。由于 percflow 方面实例化模型,这证明它实际上可以在没有线程局部变量的情况下工作。

Spring AOP解决方案

替代 Spring AOP: 如果你想坚持使用 Spring AOP,你既不能使用 percflow 实例化也不能使用 cflowbelow 切入点,因为 Spring AOP 不支持这些功能。因此,您仍然可以在方面内使用 ThreadLocal 而不是前者,而不是后者,您可以将计算分解为单独的 Spring component/bean 并确保 saveData 建议拦截那个。因此,不必使用 AspectJ 的成本(如果您愿意避免使用它)可能仍然是可以接受的:一个本地线程加上一个新组件。如果您对这种方法感兴趣,请告诉我。


更新:

I would also be interested in seeing your approach using Spring AOP if you wouldn't mind sharing.

很好。我将 post 完整的 MCVE 再次使用不同的包名称,以便从 AspectJ 示例代码中消除所有 class 的歧义(一些有轻微或重大的变化)。

助手 classes:

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

数据提供者组件:

这就是我所说的新 component/bean。同样,您可以使用 Map 而不是内部 Data class ,但这会降低类型安全性。该决定取决于您需要解决方案的具体程度或通用程度。这里重要的是提供者本身是一个单例 bean,但在每个 calculateData(..) 调用时提供一个新的 Data 实例。因此,您需要确保该方法仅依赖于它的输入参数,而不依赖于 class 字段,以便线程安全。

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 + "]";
    }
  }
}

侦听器组件:

这也是一个自动注入数据提供程序的普通单例 bean。

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> 字段来确保线程安全,因为方面也是一个单例 bean。针对两个不同组件的两个 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 解决方案中一样,日志行开头的线程 ID 与 Data 对象中捕获的线程 ID 相对应。

  .   ____          _            __ _ _
 /\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: 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]