Предисловие / Обоснование
Я бы не предложил использовать решение Даниил , поскольку
- добавляет сложность обработки локальная переменная потока для аспекта (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]