Skip to content

Commit d5477d1

Browse files
committed
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().
1 parent 8d067e8 commit d5477d1

File tree

12 files changed

+118
-23
lines changed

12 files changed

+118
-23
lines changed

spring-modulith-events/spring-modulith-events-amqp/src/main/java/org/springframework/modulith/events/amqp/RabbitEventExternalizerConfiguration.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ DelegatingEventExternalizer rabbitEventExternalizer(EventExternalizationConfigur
6464
var routing = BrokerRouting.of(target, context);
6565
var headers = configuration.getHeadersFor(payload);
6666

67-
operations.convertAndSend(routing.getTarget(), routing.getKey(payload), payload, headers);
67+
operations.convertAndSend(routing.getTarget(payload), routing.getKey(payload), payload, headers);
6868

6969
return CompletableFuture.completedFuture(null);
7070
});

spring-modulith-events/spring-modulith-events-api/src/main/java/org/springframework/modulith/events/RoutingTarget.java

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
*/
3131
public class RoutingTarget {
3232

33+
private static final String EXPRESSION_PREFIX = "#{";
3334
private final String target;
3435
private final @Nullable String key;
3536

@@ -43,8 +44,8 @@ private RoutingTarget(String target, @Nullable String key) {
4344

4445
Assert.hasText(target, "Target must not be null or empty!");
4546

46-
this.target = target;
47-
this.key = key;
47+
this.target = target.trim();
48+
this.key = key == null ? null : key.trim();
4849
}
4950

5051
/**
@@ -58,8 +59,8 @@ static ParsedRoutingTarget parse(String source) {
5859
Assert.notNull(source, "Routing target source must not be null!");
5960

6061
var parts = source.split("::", 2);
61-
var target = parts[0].isBlank() ? null : parts[0];
62-
var key = parts.length == 2 ? parts[1] : null;
62+
var target = parts[0].isBlank() ? null : parts[0].trim();
63+
var key = parts.length == 2 ? parts[1].trim() : null;
6364

6465
return new ParsedRoutingTarget(target, key);
6566
}
@@ -93,7 +94,7 @@ private RoutingTargetBuilder(String target) {
9394

9495
Assert.hasText(target, "Target must not be null or empty!");
9596

96-
this.target = target;
97+
this.target = target.trim();
9798
}
9899

99100
/**
@@ -141,7 +142,17 @@ public String getKey() {
141142
* @return whether the routing key is a SpEL expression.
142143
*/
143144
public boolean hasKeyExpression() {
144-
return key != null && key.startsWith("#{");
145+
return key != null && key.startsWith(EXPRESSION_PREFIX);
146+
}
147+
148+
/**
149+
* Returns whether either the target or key is using a SpEL expression.
150+
*
151+
* @return whether the routing key is a SpEL expression.
152+
* @since 1.3
153+
*/
154+
public boolean hasExpression() {
155+
return hasKeyExpression() || target.startsWith(EXPRESSION_PREFIX);
145156
}
146157

147158
RoutingTarget withTarget(String target) {

spring-modulith-events/spring-modulith-events-api/src/test/java/org/springframework/modulith/events/RoutingTargetUnitTests.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,22 @@ void equalsAndHashCode() {
8282
assertThat(first.hashCode()).isEqualTo(second.hashCode());
8383
assertThat(first.hashCode()).isNotEqualTo(third);
8484
}
85+
86+
@Test // GH-881
87+
void trimsTargetAndKeyOnParsing() {
88+
89+
var target = RoutingTarget.parse(" target :: key ");
90+
91+
assertThat(target.getTarget()).isEqualTo("target");
92+
assertThat(target.getKey()).isEqualTo("key");
93+
}
94+
95+
@Test // GH-881
96+
void trimsTargetAndKeyOnBuilding() {
97+
98+
var target = RoutingTarget.forTarget(" target ").andKey(" key ");
99+
100+
assertThat(target.getTarget()).isEqualTo("target");
101+
assertThat(target.getKey()).isEqualTo("key");
102+
}
85103
}

spring-modulith-events/spring-modulith-events-aws-sns/src/main/java/org/springframework/modulith/events/aws/sns/SnsEventExternalizerConfiguration.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ DelegatingEventExternalizer snsEventExternalizer(EventExternalizationConfigurati
7878
builder.groupId(key);
7979
}
8080

81-
operations.sendNotification(routing.getTarget(), builder.build());
81+
operations.sendNotification(routing.getTarget(payload), builder.build());
8282

8383
return CompletableFuture.completedFuture(null);
8484
});

spring-modulith-events/spring-modulith-events-aws-sqs/src/main/java/org/springframework/modulith/events/aws/sqs/SqsEventExternalizerConfiguration.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ DelegatingEventExternalizer sqsEventExternalizer(EventExternalizationConfigurati
7272

7373
return CompletableFuture.completedFuture(operations.send(sqsSendOptions -> {
7474

75-
var options = sqsSendOptions.queue(routing.getTarget()).payload(payload);
75+
var options = sqsSendOptions.queue(routing.getTarget(payload)).payload(payload);
7676
var key = routing.getKey(payload);
7777

7878
if (key != null) {

spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/support/BrokerRouting.java

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,14 @@
2525

2626
/**
2727
* A {@link BrokerRouting} supports {@link RoutingTarget} instances that contain values matching the format
28-
* {@code $target::$key} for which the key can actually be a SpEL expression.
28+
* {@code $target::$key} for which both the target and key can be a SpEL expression.
2929
*
3030
* @author Oliver Drotbohm
3131
* @since 1.1
3232
*/
3333
public class BrokerRouting {
3434

35-
private final RoutingTarget target;
35+
protected final RoutingTarget target;
3636

3737
/**
3838
* Creates a new {@link BrokerRouting} for the given {@link RoutingTarget}.
@@ -54,18 +54,34 @@ private BrokerRouting(RoutingTarget target) {
5454
* @return will never be {@literal null}.
5555
*/
5656
public static BrokerRouting of(RoutingTarget target, EvaluationContext context) {
57-
return target.hasKeyExpression() ? new SpelBrokerRouting(target, context) : new BrokerRouting(target);
57+
58+
return target.hasExpression()
59+
? new SpelBrokerRouting(target, context)
60+
: new BrokerRouting(target);
5861
}
5962

6063
/**
6164
* Returns the actual routing target.
6265
*
6366
* @return will never be {@literal null}.
67+
* @deprecated since 1.3, call {@link #getTarget(Object)} instead.
6468
*/
69+
@Deprecated
6570
public String getTarget() {
6671
return target.getTarget();
6772
}
6873

74+
/**
75+
* Returns the actual routing target for the given event.
76+
*
77+
* @param event must not be {@literal null}.
78+
* @return will never be {@literal null}.
79+
* @since 1.3
80+
*/
81+
public String getTarget(Object event) {
82+
return getTarget();
83+
}
84+
6985
/**
7086
* Resolves the routing key against the given event. In case the original {@link RoutingTarget} contained an
7187
* expression, the event will be used as root object to evaluate that expression.
@@ -89,7 +105,8 @@ static class SpelBrokerRouting extends BrokerRouting {
89105
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
90106
private static final TemplateParserContext CONTEXT = new TemplateParserContext();
91107

92-
private final Expression expression;
108+
private final Expression targetExpression;
109+
private final @Nullable Expression keyExpression;
93110
private final EvaluationContext context;
94111

95112
/**
@@ -103,15 +120,39 @@ private SpelBrokerRouting(RoutingTarget target, EvaluationContext context) {
103120

104121
super(target);
105122

106-
var key = target.getKey();
107-
108-
Assert.notNull(target.getKey(), "Routing key must not be null!");
109123
Assert.notNull(context, "EvaluationContext must not be null!");
110124

111-
this.expression = PARSER.parseExpression(key, CONTEXT);
125+
this.keyExpression = target.getKey() == null ? null : PARSER.parseExpression(target.getKey(), CONTEXT);
126+
this.targetExpression = PARSER.parseExpression(target.getTarget(), CONTEXT);
112127
this.context = context;
113128
}
114129

130+
/*
131+
* (non-Javadoc)
132+
* @see org.springframework.modulith.events.support.BrokerRouting#getTarget()
133+
*/
134+
@Override
135+
public String getTarget() {
136+
return getTarget(null);
137+
}
138+
139+
/*
140+
* (non-Javadoc)
141+
* @see org.springframework.modulith.events.support.BrokerRouting#getTarget(java.lang.Object)
142+
*/
143+
@Override
144+
public String getTarget(@Nullable Object event) {
145+
146+
var result = targetExpression.getValue(context, event);
147+
148+
if (result == null) {
149+
throw new IllegalStateException(
150+
"Evaluation of target expression %s must not result in null!".formatted(targetExpression));
151+
}
152+
153+
return result.toString();
154+
}
155+
115156
/*
116157
* (non-Javadoc)
117158
* @see org.springframework.modulith.events.support.BrokerRouting#getKey(java.lang.Object)
@@ -120,6 +161,12 @@ private SpelBrokerRouting(RoutingTarget target, EvaluationContext context) {
120161
@Override
121162
public String getKey(Object event) {
122163

164+
var expression = keyExpression;
165+
166+
if (expression == null) {
167+
return target.getTarget();
168+
}
169+
123170
var result = expression.getValue(context, event);
124171

125172
return result == null ? null : result.toString();

spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/support/EventExternalizationSupport.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.springframework.modulith.events.EventExternalized;
2525
import org.springframework.modulith.events.RoutingTarget;
2626
import org.springframework.modulith.events.core.ConditionalEventListener;
27+
import org.springframework.transaction.annotation.Propagation;
2728
import org.springframework.util.Assert;
2829

2930
/**
@@ -66,7 +67,7 @@ public boolean supports(Object event) {
6667
* @param event must not be {@literal null}.
6768
* @return the externalization result, will never be {@literal null}.
6869
*/
69-
@ApplicationModuleListener
70+
@ApplicationModuleListener(propagation = Propagation.SUPPORTS)
7071
public CompletableFuture<?> externalize(Object event) {
7172

7273
Assert.notNull(event, "Object must not be null!");

spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/support/BrokerRoutingUnitTests.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,15 @@ void doesNotAccessEvaluationContextIfNoSpelExpression() {
5555
verifyNoInteractions(context);
5656
}
5757

58+
@Test // GH-881
59+
void evaluatesSpelExpressionForTarget() {
60+
61+
var target = RoutingTarget.forTarget("#{@bean.getKey(#this)}").withoutKey();
62+
var routing = BrokerRouting.of(target, getEvaluationContext());
63+
64+
assertThat(routing.getTarget(new TestEvent())).isEqualTo("foo");
65+
}
66+
5867
private static EvaluationContext getEvaluationContext() {
5968

6069
var evaluationContext = new StandardEvaluationContext();

spring-modulith-events/spring-modulith-events-jms/src/main/java/org/springframework/modulith/events/jms/JmsEventExternalizerConfiguration.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,19 @@
1919

2020
import org.slf4j.Logger;
2121
import org.slf4j.LoggerFactory;
22+
import org.springframework.beans.factory.BeanFactory;
2223
import org.springframework.boot.autoconfigure.AutoConfiguration;
2324
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
2425
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
2526
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
2627
import org.springframework.context.annotation.Bean;
28+
import org.springframework.context.expression.BeanFactoryResolver;
29+
import org.springframework.expression.spel.support.StandardEvaluationContext;
2730
import org.springframework.jms.core.JmsOperations;
2831
import org.springframework.modulith.events.EventExternalizationConfiguration;
2932
import org.springframework.modulith.events.config.EventExternalizationAutoConfiguration;
3033
import org.springframework.modulith.events.core.EventSerializer;
34+
import org.springframework.modulith.events.support.BrokerRouting;
3135
import org.springframework.modulith.events.support.DelegatingEventExternalizer;
3236

3337
/**
@@ -48,15 +52,19 @@ class JmsEventExternalizerConfiguration {
4852

4953
@Bean
5054
DelegatingEventExternalizer jmsEventExternalizer(EventExternalizationConfiguration configuration,
51-
JmsOperations operations, EventSerializer serializer) {
55+
JmsOperations operations, EventSerializer serializer, BeanFactory factory) {
5256

5357
logger.debug("Registering domain event externalization to JMS…");
5458

59+
var context = new StandardEvaluationContext();
60+
context.setBeanResolver(new BeanFactoryResolver(factory));
61+
5562
return new DelegatingEventExternalizer(configuration, (target, payload) -> {
5663

5764
var serialized = serializer.serialize(payload);
65+
var routing = BrokerRouting.of(target, context);
5866

59-
operations.send(target.getTarget(), session -> session.createTextMessage(serialized.toString()));
67+
operations.send(routing.getTarget(payload), session -> session.createTextMessage(serialized.toString()));
6068

6169
return CompletableFuture.completedFuture(null);
6270
});

spring-modulith-events/spring-modulith-events-kafka/src/main/java/org/springframework/modulith/events/kafka/KafkaEventExternalizerConfiguration.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ DelegatingEventExternalizer kafkaEventExternalizer(EventExternalizationConfigura
7171

7272
var message = builder
7373
.setHeaderIfAbsent(KafkaHeaders.KEY, routing.getKey(payload))
74-
.setHeaderIfAbsent(KafkaHeaders.TOPIC, routing.getTarget())
74+
.setHeaderIfAbsent(KafkaHeaders.TOPIC, routing.getTarget(payload))
7575
.build();
7676

7777
return operations.send(message);

spring-modulith-events/spring-modulith-events-messaging/src/main/java/org/springframework/modulith/events/messaging/SpringMessagingEventExternalizerConfiguration.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.springframework.messaging.support.MessageBuilder;
3232
import org.springframework.modulith.events.EventExternalizationConfiguration;
3333
import org.springframework.modulith.events.config.EventExternalizationAutoConfiguration;
34+
import org.springframework.modulith.events.support.BrokerRouting;
3435
import org.springframework.modulith.events.support.DelegatingEventExternalizer;
3536

3637
/**
@@ -62,7 +63,7 @@ DelegatingEventExternalizer springMessagingEventExternalizer(EventExternalizatio
6263

6364
return new DelegatingEventExternalizer(configuration, (target, payload) -> {
6465

65-
var targetChannel = target.getTarget();
66+
var targetChannel = BrokerRouting.of(target, context).getTarget(payload);
6667
var message = MessageBuilder
6768
.withPayload(payload)
6869
.setHeader(MODULITH_ROUTING_HEADER, target.toString())

src/docs/antora/modules/ROOT/pages/events.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ By default, no routing key is used.
404404
=== Annotation-based Event Externalization Configuration
405405

406406
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.
407-
The key can be a SpEL expression which will get the event instance configured as root object.
407+
Both the target and key can be a SpEL expression which will get the event instance configured as root object.
408408

409409
.Defining a dynamic routing key via SpEL expression
410410
[tabs]

0 commit comments

Comments
 (0)