前言/基本原理
我不建议使用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
通知。saveData
joinPoint.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]