Skip to content

Commit a976759

Browse files
committed
Explicitly reject invalid aggregate event registrations during publishing.
We now detect that the consumption of the events published during a persistence operation has produced new event instances that would go unpublished and raise an explaining exception. Previously such a scenario would've resulted in a ConcurrentModificationException. We primarily reject such a scenario as handling the additional event would extend our convenience mechanism over the publishing scope a direct 1:1 replacement with ApplicationEventPublisher would've achieved. Fixes GH-3116.
1 parent ce62224 commit a976759

File tree

2 files changed

+67
-10
lines changed

2 files changed

+67
-10
lines changed

Diff for: src/main/java/org/springframework/data/repository/core/support/EventPublishingRepositoryProxyPostProcessor.java

+40-10
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import java.lang.annotation.Annotation;
1919
import java.lang.reflect.Method;
20+
import java.util.ArrayList;
2021
import java.util.Collection;
2122
import java.util.Collections;
2223
import java.util.Map;
@@ -111,7 +112,7 @@ public Object invoke(MethodInvocation invocation) throws Throwable {
111112
return result;
112113
}
113114

114-
Iterable<?> arguments = asCollection(invocation.getArguments()[0], invocation.getMethod());
115+
Iterable<?> arguments = asIterable(invocation.getArguments()[0], invocation.getMethod());
115116

116117
eventMethod.publishEventsFrom(arguments, publisher);
117118

@@ -144,6 +145,9 @@ static class EventPublishingMethod {
144145
private static Map<Class<?>, EventPublishingMethod> cache = new ConcurrentReferenceHashMap<>();
145146
private static @SuppressWarnings("null") EventPublishingMethod NONE = new EventPublishingMethod(Object.class, null,
146147
null);
148+
private static String ILLEGAL_MODIFCATION = "Aggregate's events were modified during event publication. "
149+
+ "Make sure event listeners obtain a fresh instance of the aggregate before adding further events. "
150+
+ "Additional events found: %s.";
147151

148152
private final Class<?> type;
149153
private final Method publishingMethod;
@@ -188,18 +192,33 @@ public static EventPublishingMethod of(Class<?> type) {
188192
* @param aggregates can be {@literal null}.
189193
* @param publisher must not be {@literal null}.
190194
*/
191-
public void publishEventsFrom(Iterable<?> aggregates, ApplicationEventPublisher publisher) {
195+
public void publishEventsFrom(@Nullable Iterable<?> aggregates, ApplicationEventPublisher publisher) {
196+
197+
if (aggregates == null) {
198+
return;
199+
}
192200

193201
for (Object aggregateRoot : aggregates) {
194202

195203
if (!type.isInstance(aggregateRoot)) {
196204
continue;
197205
}
198206

199-
for (Object event : asCollection(ReflectionUtils.invokeMethod(publishingMethod, aggregateRoot), null)) {
207+
var events = asCollection(ReflectionUtils.invokeMethod(publishingMethod, aggregateRoot));
208+
209+
for (Object event : events) {
200210
publisher.publishEvent(event);
201211
}
202212

213+
var postPublication = asCollection(ReflectionUtils.invokeMethod(publishingMethod, aggregateRoot));
214+
215+
if (events.size() != postPublication.size()) {
216+
217+
postPublication.removeAll(events);
218+
219+
throw new IllegalStateException(ILLEGAL_MODIFCATION.formatted(postPublication));
220+
}
221+
203222
if (clearingMethod != null) {
204223
ReflectionUtils.invokeMethod(clearingMethod, aggregateRoot);
205224
}
@@ -272,23 +291,34 @@ private static Method getClearingMethod(AnnotationDetectionMethodCallback<?> cle
272291
* one-element collection, {@literal null} will become an empty collection.
273292
*
274293
* @param source can be {@literal null}.
275-
* @return
294+
* @return will never be {@literal null}.
276295
*/
277296
@SuppressWarnings("unchecked")
278-
private static Iterable<Object> asCollection(@Nullable Object source, @Nullable Method method) {
297+
private static Collection<Object> asCollection(@Nullable Object source) {
279298

280299
if (source == null) {
281300
return Collections.emptyList();
282301
}
283302

284-
if (method != null && method.getName().startsWith("saveAll")) {
285-
return (Iterable<Object>) source;
286-
}
287-
288303
if (Collection.class.isInstance(source)) {
289-
return (Collection<Object>) source;
304+
return new ArrayList<>((Collection<Object>) source);
290305
}
291306

292307
return Collections.singletonList(source);
293308
}
309+
310+
/**
311+
* Returns the given source object as {@link Iterable}.
312+
*
313+
* @param source can be {@literal null}.
314+
* @return will never be {@literal null}.
315+
*/
316+
@Nullable
317+
@SuppressWarnings("unchecked")
318+
private static Iterable<Object> asIterable(@Nullable Object source, @Nullable Method method) {
319+
320+
return method != null && method.getName().startsWith("saveAll")
321+
? (Iterable<Object>) source
322+
: asCollection(source);
323+
}
294324
}

Diff for: src/test/java/org/springframework/data/repository/core/support/EventPublishingRepositoryProxyPostProcessorUnitTests.java

+27
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static org.mockito.Mockito.*;
2121

2222
import java.lang.reflect.Method;
23+
import java.util.ArrayList;
2324
import java.util.Arrays;
2425
import java.util.Collection;
2526
import java.util.Collections;
@@ -324,6 +325,32 @@ void doesNotEmitEventsFromPrimitiveValue() throws Throwable {
324325
verify(publisher, never()).publishEvent(any());
325326
}
326327

328+
@Test // GH-3116
329+
void rejectsEventAddedDuringProcessing() throws Throwable {
330+
331+
var originalEvent = new SomeEvent();
332+
var eventToBeAdded = new SomeEvent();
333+
334+
var events = new ArrayList<Object>();
335+
events.add(originalEvent);
336+
337+
var aggregate = MultipleEvents.of(events);
338+
339+
doAnswer(invocation -> {
340+
341+
events.add(eventToBeAdded);
342+
return null;
343+
344+
}).when(publisher).publishEvent(any(Object.class));
345+
346+
var method = EventPublishingMethod.of(MultipleEvents.class);
347+
348+
assertThatIllegalStateException()
349+
.isThrownBy(() -> method.publishEventsFrom(List.of(aggregate), publisher))
350+
.withMessageContaining(eventToBeAdded.toString())
351+
.withMessageNotContaining(originalEvent.toString());
352+
}
353+
327354
private static void mockInvocation(MethodInvocation invocation, Method method, Object parameterAndReturnValue)
328355
throws Throwable {
329356

0 commit comments

Comments
 (0)