GH-881 - Support for SpEL expression in routing targets.
We now support SpEL expressions in routing targets for events to be externalized. Introduced a BrokerRouting.getTarget(Object) overload to allow access to the event object in the SpEL expression. To support those, event externalizers will have to call that method where they previously called ….getTarget().
This commit is contained in:
@@ -64,7 +64,7 @@ class RabbitEventExternalizerConfiguration {
|
||||
var routing = BrokerRouting.of(target, context);
|
||||
var headers = configuration.getHeadersFor(payload);
|
||||
|
||||
operations.convertAndSend(routing.getTarget(), routing.getKey(payload), payload, headers);
|
||||
operations.convertAndSend(routing.getTarget(payload), routing.getKey(payload), payload, headers);
|
||||
|
||||
return CompletableFuture.completedFuture(null);
|
||||
});
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.springframework.util.StringUtils;
|
||||
*/
|
||||
public class RoutingTarget {
|
||||
|
||||
private static final String EXPRESSION_PREFIX = "#{";
|
||||
private final String target;
|
||||
private final @Nullable String key;
|
||||
|
||||
@@ -43,8 +44,8 @@ public class RoutingTarget {
|
||||
|
||||
Assert.hasText(target, "Target must not be null or empty!");
|
||||
|
||||
this.target = target;
|
||||
this.key = key;
|
||||
this.target = target.trim();
|
||||
this.key = key == null ? null : key.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,8 +59,8 @@ public class RoutingTarget {
|
||||
Assert.notNull(source, "Routing target source must not be null!");
|
||||
|
||||
var parts = source.split("::", 2);
|
||||
var target = parts[0].isBlank() ? null : parts[0];
|
||||
var key = parts.length == 2 ? parts[1] : null;
|
||||
var target = parts[0].isBlank() ? null : parts[0].trim();
|
||||
var key = parts.length == 2 ? parts[1].trim() : null;
|
||||
|
||||
return new ParsedRoutingTarget(target, key);
|
||||
}
|
||||
@@ -93,7 +94,7 @@ public class RoutingTarget {
|
||||
|
||||
Assert.hasText(target, "Target must not be null or empty!");
|
||||
|
||||
this.target = target;
|
||||
this.target = target.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,7 +142,17 @@ public class RoutingTarget {
|
||||
* @return whether the routing key is a SpEL expression.
|
||||
*/
|
||||
public boolean hasKeyExpression() {
|
||||
return key != null && key.startsWith("#{");
|
||||
return key != null && key.startsWith(EXPRESSION_PREFIX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether either the target or key is using a SpEL expression.
|
||||
*
|
||||
* @return whether the routing key is a SpEL expression.
|
||||
* @since 1.3
|
||||
*/
|
||||
public boolean hasExpression() {
|
||||
return hasKeyExpression() || target.startsWith(EXPRESSION_PREFIX);
|
||||
}
|
||||
|
||||
RoutingTarget withTarget(String target) {
|
||||
|
||||
@@ -82,4 +82,22 @@ class RoutingTargetUnitTests {
|
||||
assertThat(first.hashCode()).isEqualTo(second.hashCode());
|
||||
assertThat(first.hashCode()).isNotEqualTo(third);
|
||||
}
|
||||
|
||||
@Test // GH-881
|
||||
void trimsTargetAndKeyOnParsing() {
|
||||
|
||||
var target = RoutingTarget.parse(" target :: key ");
|
||||
|
||||
assertThat(target.getTarget()).isEqualTo("target");
|
||||
assertThat(target.getKey()).isEqualTo("key");
|
||||
}
|
||||
|
||||
@Test // GH-881
|
||||
void trimsTargetAndKeyOnBuilding() {
|
||||
|
||||
var target = RoutingTarget.forTarget(" target ").andKey(" key ");
|
||||
|
||||
assertThat(target.getTarget()).isEqualTo("target");
|
||||
assertThat(target.getKey()).isEqualTo("key");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ class SnsEventExternalizerConfiguration {
|
||||
builder.groupId(key);
|
||||
}
|
||||
|
||||
operations.sendNotification(routing.getTarget(), builder.build());
|
||||
operations.sendNotification(routing.getTarget(payload), builder.build());
|
||||
|
||||
return CompletableFuture.completedFuture(null);
|
||||
});
|
||||
|
||||
@@ -72,7 +72,7 @@ class SqsEventExternalizerConfiguration {
|
||||
|
||||
return CompletableFuture.completedFuture(operations.send(sqsSendOptions -> {
|
||||
|
||||
var options = sqsSendOptions.queue(routing.getTarget()).payload(payload);
|
||||
var options = sqsSendOptions.queue(routing.getTarget(payload)).payload(payload);
|
||||
var key = routing.getKey(payload);
|
||||
|
||||
if (key != null) {
|
||||
|
||||
@@ -25,14 +25,14 @@ import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A {@link BrokerRouting} supports {@link RoutingTarget} instances that contain values matching the format
|
||||
* {@code $target::$key} for which the key can actually be a SpEL expression.
|
||||
* {@code $target::$key} for which both the target and key can be a SpEL expression.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @since 1.1
|
||||
*/
|
||||
public class BrokerRouting {
|
||||
|
||||
private final RoutingTarget target;
|
||||
protected final RoutingTarget target;
|
||||
|
||||
/**
|
||||
* Creates a new {@link BrokerRouting} for the given {@link RoutingTarget}.
|
||||
@@ -54,18 +54,34 @@ public class BrokerRouting {
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public static BrokerRouting of(RoutingTarget target, EvaluationContext context) {
|
||||
return target.hasKeyExpression() ? new SpelBrokerRouting(target, context) : new BrokerRouting(target);
|
||||
|
||||
return target.hasExpression()
|
||||
? new SpelBrokerRouting(target, context)
|
||||
: new BrokerRouting(target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the actual routing target.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
* @deprecated since 1.3, call {@link #getTarget(Object)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public String getTarget() {
|
||||
return target.getTarget();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the actual routing target for the given event.
|
||||
*
|
||||
* @param event must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @since 1.3
|
||||
*/
|
||||
public String getTarget(Object event) {
|
||||
return getTarget();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the routing key against the given event. In case the original {@link RoutingTarget} contained an
|
||||
* expression, the event will be used as root object to evaluate that expression.
|
||||
@@ -89,7 +105,8 @@ public class BrokerRouting {
|
||||
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
|
||||
private static final TemplateParserContext CONTEXT = new TemplateParserContext();
|
||||
|
||||
private final Expression expression;
|
||||
private final Expression targetExpression;
|
||||
private final @Nullable Expression keyExpression;
|
||||
private final EvaluationContext context;
|
||||
|
||||
/**
|
||||
@@ -103,15 +120,39 @@ public class BrokerRouting {
|
||||
|
||||
super(target);
|
||||
|
||||
var key = target.getKey();
|
||||
|
||||
Assert.notNull(target.getKey(), "Routing key must not be null!");
|
||||
Assert.notNull(context, "EvaluationContext must not be null!");
|
||||
|
||||
this.expression = PARSER.parseExpression(key, CONTEXT);
|
||||
this.keyExpression = target.getKey() == null ? null : PARSER.parseExpression(target.getKey(), CONTEXT);
|
||||
this.targetExpression = PARSER.parseExpression(target.getTarget(), CONTEXT);
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.modulith.events.support.BrokerRouting#getTarget()
|
||||
*/
|
||||
@Override
|
||||
public String getTarget() {
|
||||
return getTarget(null);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.modulith.events.support.BrokerRouting#getTarget(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public String getTarget(@Nullable Object event) {
|
||||
|
||||
var result = targetExpression.getValue(context, event);
|
||||
|
||||
if (result == null) {
|
||||
throw new IllegalStateException(
|
||||
"Evaluation of target expression %s must not result in null!".formatted(targetExpression));
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.modulith.events.support.BrokerRouting#getKey(java.lang.Object)
|
||||
@@ -120,6 +161,12 @@ public class BrokerRouting {
|
||||
@Override
|
||||
public String getKey(Object event) {
|
||||
|
||||
var expression = keyExpression;
|
||||
|
||||
if (expression == null) {
|
||||
return target.getTarget();
|
||||
}
|
||||
|
||||
var result = expression.getValue(context, event);
|
||||
|
||||
return result == null ? null : result.toString();
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.springframework.modulith.events.EventExternalizationConfiguration;
|
||||
import org.springframework.modulith.events.EventExternalized;
|
||||
import org.springframework.modulith.events.RoutingTarget;
|
||||
import org.springframework.modulith.events.core.ConditionalEventListener;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -66,7 +67,7 @@ abstract class EventExternalizationSupport implements ConditionalEventListener {
|
||||
* @param event must not be {@literal null}.
|
||||
* @return the externalization result, will never be {@literal null}.
|
||||
*/
|
||||
@ApplicationModuleListener
|
||||
@ApplicationModuleListener(propagation = Propagation.SUPPORTS)
|
||||
public CompletableFuture<?> externalize(Object event) {
|
||||
|
||||
Assert.notNull(event, "Object must not be null!");
|
||||
|
||||
@@ -55,6 +55,15 @@ class BrokerRoutingUnitTests {
|
||||
verifyNoInteractions(context);
|
||||
}
|
||||
|
||||
@Test // GH-881
|
||||
void evaluatesSpelExpressionForTarget() {
|
||||
|
||||
var target = RoutingTarget.forTarget("#{@bean.getKey(#this)}").withoutKey();
|
||||
var routing = BrokerRouting.of(target, getEvaluationContext());
|
||||
|
||||
assertThat(routing.getTarget(new TestEvent())).isEqualTo("foo");
|
||||
}
|
||||
|
||||
private static EvaluationContext getEvaluationContext() {
|
||||
|
||||
var evaluationContext = new StandardEvaluationContext();
|
||||
|
||||
@@ -19,15 +19,19 @@ import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.expression.BeanFactoryResolver;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
import org.springframework.jms.core.JmsOperations;
|
||||
import org.springframework.modulith.events.EventExternalizationConfiguration;
|
||||
import org.springframework.modulith.events.config.EventExternalizationAutoConfiguration;
|
||||
import org.springframework.modulith.events.core.EventSerializer;
|
||||
import org.springframework.modulith.events.support.BrokerRouting;
|
||||
import org.springframework.modulith.events.support.DelegatingEventExternalizer;
|
||||
|
||||
/**
|
||||
@@ -48,15 +52,19 @@ class JmsEventExternalizerConfiguration {
|
||||
|
||||
@Bean
|
||||
DelegatingEventExternalizer jmsEventExternalizer(EventExternalizationConfiguration configuration,
|
||||
JmsOperations operations, EventSerializer serializer) {
|
||||
JmsOperations operations, EventSerializer serializer, BeanFactory factory) {
|
||||
|
||||
logger.debug("Registering domain event externalization to JMS…");
|
||||
|
||||
var context = new StandardEvaluationContext();
|
||||
context.setBeanResolver(new BeanFactoryResolver(factory));
|
||||
|
||||
return new DelegatingEventExternalizer(configuration, (target, payload) -> {
|
||||
|
||||
var serialized = serializer.serialize(payload);
|
||||
var routing = BrokerRouting.of(target, context);
|
||||
|
||||
operations.send(target.getTarget(), session -> session.createTextMessage(serialized.toString()));
|
||||
operations.send(routing.getTarget(payload), session -> session.createTextMessage(serialized.toString()));
|
||||
|
||||
return CompletableFuture.completedFuture(null);
|
||||
});
|
||||
|
||||
@@ -71,7 +71,7 @@ class KafkaEventExternalizerConfiguration {
|
||||
|
||||
var message = builder
|
||||
.setHeaderIfAbsent(KafkaHeaders.KEY, routing.getKey(payload))
|
||||
.setHeaderIfAbsent(KafkaHeaders.TOPIC, routing.getTarget())
|
||||
.setHeaderIfAbsent(KafkaHeaders.TOPIC, routing.getTarget(payload))
|
||||
.build();
|
||||
|
||||
return operations.send(message);
|
||||
|
||||
@@ -31,6 +31,7 @@ import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.modulith.events.EventExternalizationConfiguration;
|
||||
import org.springframework.modulith.events.config.EventExternalizationAutoConfiguration;
|
||||
import org.springframework.modulith.events.support.BrokerRouting;
|
||||
import org.springframework.modulith.events.support.DelegatingEventExternalizer;
|
||||
|
||||
/**
|
||||
@@ -62,7 +63,7 @@ class SpringMessagingEventExternalizerConfiguration {
|
||||
|
||||
return new DelegatingEventExternalizer(configuration, (target, payload) -> {
|
||||
|
||||
var targetChannel = target.getTarget();
|
||||
var targetChannel = BrokerRouting.of(target, context).getTarget(payload);
|
||||
var message = MessageBuilder
|
||||
.withPayload(payload)
|
||||
.setHeader(MODULITH_ROUTING_HEADER, target.toString())
|
||||
|
||||
@@ -404,7 +404,7 @@ By default, no routing key is used.
|
||||
=== Annotation-based Event Externalization Configuration
|
||||
|
||||
To define a custom routing key via the `@Externalized` annotations, a pattern of `$target::$key` can be used for the target/value attribute available in each of the particular annotations.
|
||||
The key can be a SpEL expression which will get the event instance configured as root object.
|
||||
Both the target and key can be a SpEL expression which will get the event instance configured as root object.
|
||||
|
||||
.Defining a dynamic routing key via SpEL expression
|
||||
[tabs]
|
||||
|
||||
Reference in New Issue
Block a user