|
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