|
17 | 17 |
|
18 | 18 | import java.lang.annotation.Annotation;
|
19 | 19 | import java.lang.reflect.Method;
|
| 20 | +import java.util.ArrayList; |
20 | 21 | import java.util.Collection;
|
21 | 22 | import java.util.Collections;
|
22 | 23 | import java.util.Map;
|
@@ -111,7 +112,7 @@ public Object invoke(MethodInvocation invocation) throws Throwable {
|
111 | 112 | return result;
|
112 | 113 | }
|
113 | 114 |
|
114 |
| - Iterable<?> arguments = asCollection(invocation.getArguments()[0], invocation.getMethod()); |
| 115 | + Iterable<?> arguments = asIterable(invocation.getArguments()[0], invocation.getMethod()); |
115 | 116 |
|
116 | 117 | eventMethod.publishEventsFrom(arguments, publisher);
|
117 | 118 |
|
@@ -144,6 +145,9 @@ static class EventPublishingMethod {
|
144 | 145 | private static Map<Class<?>, EventPublishingMethod> cache = new ConcurrentReferenceHashMap<>();
|
145 | 146 | private static @SuppressWarnings("null") EventPublishingMethod NONE = new EventPublishingMethod(Object.class, null,
|
146 | 147 | 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."; |
147 | 151 |
|
148 | 152 | private final Class<?> type;
|
149 | 153 | private final Method publishingMethod;
|
@@ -188,18 +192,33 @@ public static EventPublishingMethod of(Class<?> type) {
|
188 | 192 | * @param aggregates can be {@literal null}.
|
189 | 193 | * @param publisher must not be {@literal null}.
|
190 | 194 | */
|
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 | + } |
192 | 200 |
|
193 | 201 | for (Object aggregateRoot : aggregates) {
|
194 | 202 |
|
195 | 203 | if (!type.isInstance(aggregateRoot)) {
|
196 | 204 | continue;
|
197 | 205 | }
|
198 | 206 |
|
199 |
| - for (Object event : asCollection(ReflectionUtils.invokeMethod(publishingMethod, aggregateRoot), null)) { |
| 207 | + var events = asCollection(ReflectionUtils.invokeMethod(publishingMethod, aggregateRoot)); |
| 208 | + |
| 209 | + for (Object event : events) { |
200 | 210 | publisher.publishEvent(event);
|
201 | 211 | }
|
202 | 212 |
|
| 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 | + |
203 | 222 | if (clearingMethod != null) {
|
204 | 223 | ReflectionUtils.invokeMethod(clearingMethod, aggregateRoot);
|
205 | 224 | }
|
@@ -272,23 +291,34 @@ private static Method getClearingMethod(AnnotationDetectionMethodCallback<?> cle
|
272 | 291 | * one-element collection, {@literal null} will become an empty collection.
|
273 | 292 | *
|
274 | 293 | * @param source can be {@literal null}.
|
275 |
| - * @return |
| 294 | + * @return will never be {@literal null}. |
276 | 295 | */
|
277 | 296 | @SuppressWarnings("unchecked")
|
278 |
| - private static Iterable<Object> asCollection(@Nullable Object source, @Nullable Method method) { |
| 297 | + private static Collection<Object> asCollection(@Nullable Object source) { |
279 | 298 |
|
280 | 299 | if (source == null) {
|
281 | 300 | return Collections.emptyList();
|
282 | 301 | }
|
283 | 302 |
|
284 |
| - if (method != null && method.getName().startsWith("saveAll")) { |
285 |
| - return (Iterable<Object>) source; |
286 |
| - } |
287 |
| - |
288 | 303 | if (Collection.class.isInstance(source)) {
|
289 |
| - return (Collection<Object>) source; |
| 304 | + return new ArrayList<>((Collection<Object>) source); |
290 | 305 | }
|
291 | 306 |
|
292 | 307 | return Collections.singletonList(source);
|
293 | 308 | }
|
| 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 | + } |
294 | 324 | }
|
0 commit comments