2
@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) 方法以返回某些内容,并且 onEvent(FlowableEvent event) 方法必须首先完全完成,那么我将如何将参数传递给 insertThing()?

4

2 回答 2

8

前言/基本原理

我不建议使用Daniil的解决方案,因为

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

主要问题在于 OP ( moesyzlack23 ) 的思维方式:他说,他想“将参数传递给方面”。这违反了基本的 AOP 原则,即切面应该知道如何添加横切行为,但应用程序代码应该与切面无关。

AspectJ 解决方案

我建议

  • 只需在类中添加一个负责计算结果的方法TaskCreateListener并从中调用它onEvent(..)
  • 如Spring 手册中所述,从 Spring AOP 切换到 AspectJ,并使用cflowbelow切入点和percflow切面实例化等特性,从而摆脱线程局部变量并使核心代码再次与 AOP 无关,
  • 可以选择Map<String, Object>使用常规 getter 方法将其转换为类型更安全的数据对象。如果该方面适用于许多要处理的数据集非常多样化的注释方法,这将是困难的。但在我看来,这方面似乎非常具体。

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

助手类:

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

内部Data类是可选的,您可以继续使用 aMap<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通知。saveDatajoinPoint.proceed()
  • @AfterReturning通知会在控制流下方的每个saveData方法执行myPointcut()触发并返回一个Data对象。返回其他内容或在指定控制流之外执行的方法将被忽略。通知确保被拦截的方法调用的结果被分配给一个私有Data变量,以后可以通过insertThing通知访问该变量。
  • 我正在添加execution(* *(..)) &&切入点,因为在 AspectJ 中还有其他连接点,例如方法call(),此外方法执行是 Spring AOP 中唯一支持的类型。所以你不需要那么具体,在 AspectJ 中你应该这样做。
  • 如果percflow(myPointcut())从注释中删除实例化@Aspect,则必须改为使私有Data成员 aThreadLocal<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在方面内部使用 a 而不是前者,而不是后者,您可以将计算分解到一个单独的 Spring 组件/bean 中,并确保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 {}

数据提供者组件:

这是我正在谈论的新组件/bean。Data同样,您可以使用 a而不是内部类,Map但这会不太安全。该决定取决于您需要解决方案的具体程度或通用程度。这里重要的是提供者本身是一个单例 bean,但在每次调用时都会提供一个新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 + "]";
    }
  }
}

监听器组件:

这也是一个普通的单例 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。针对两个不同组件的两个切入点/建议对的组合确保我们首先收集并保存正确的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]
于 2020-02-18T04:32:52.133 回答
3

根据您的问题,不可能更改 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);
}
于 2020-02-17T11:36:37.710 回答