|
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;
|
|
28 | 29 | import org.springframework.context.ApplicationEventPublisher;
|
29 | 30 | import org.springframework.data.domain.AfterDomainEventPublication;
|
30 | 31 | import org.springframework.data.domain.DomainEvents;
|
31 |
| -import org.springframework.data.repository.CrudRepository; |
32 | 32 | import org.springframework.data.repository.core.RepositoryInformation;
|
33 | 33 | import org.springframework.data.util.AnnotationDetectionMethodCallback;
|
34 | 34 | import org.springframework.lang.Nullable;
|
@@ -111,7 +111,7 @@ public Object invoke(MethodInvocation invocation) throws Throwable {
|
111 | 111 | return result;
|
112 | 112 | }
|
113 | 113 |
|
114 |
| - Iterable<?> arguments = asCollection(invocation.getArguments()[0], invocation.getMethod()); |
| 114 | + Iterable<?> arguments = asIterable(invocation.getArguments()[0], invocation.getMethod()); |
115 | 115 |
|
116 | 116 | eventMethod.publishEventsFrom(arguments, publisher);
|
117 | 117 |
|
@@ -144,6 +144,9 @@ static class EventPublishingMethod {
|
144 | 144 | private static Map<Class<?>, EventPublishingMethod> cache = new ConcurrentReferenceHashMap<>();
|
145 | 145 | private static @SuppressWarnings("null") EventPublishingMethod NONE = new EventPublishingMethod(Object.class, null,
|
146 | 146 | null);
|
| 147 | + private static String ILLEGAL_MODIFCATION = "Aggregate's events were modified during event publication. " |
| 148 | + + "Make sure event listeners obtain a fresh instance of the aggregate before adding further events. " |
| 149 | + + "Additional events found: %s."; |
147 | 150 |
|
148 | 151 | private final Class<?> type;
|
149 | 152 | private final Method publishingMethod;
|
@@ -188,18 +191,33 @@ public static EventPublishingMethod of(Class<?> type) {
|
188 | 191 | * @param aggregates can be {@literal null}.
|
189 | 192 | * @param publisher must not be {@literal null}.
|
190 | 193 | */
|
191 |
| - public void publishEventsFrom(Iterable<?> aggregates, ApplicationEventPublisher publisher) { |
| 194 | + public void publishEventsFrom(@Nullable Iterable<?> aggregates, ApplicationEventPublisher publisher) { |
| 195 | + |
| 196 | + if (aggregates == null) { |
| 197 | + return; |
| 198 | + } |
192 | 199 |
|
193 | 200 | for (Object aggregateRoot : aggregates) {
|
194 | 201 |
|
195 | 202 | if (!type.isInstance(aggregateRoot)) {
|
196 | 203 | continue;
|
197 | 204 | }
|
198 | 205 |
|
199 |
| - for (Object event : asCollection(ReflectionUtils.invokeMethod(publishingMethod, aggregateRoot), null)) { |
| 206 | + var events = asCollection(ReflectionUtils.invokeMethod(publishingMethod, aggregateRoot)); |
| 207 | + |
| 208 | + for (Object event : events) { |
200 | 209 | publisher.publishEvent(event);
|
201 | 210 | }
|
202 | 211 |
|
| 212 | + var postPublication = asCollection(ReflectionUtils.invokeMethod(publishingMethod, aggregateRoot)); |
| 213 | + |
| 214 | + if (events.size() != postPublication.size()) { |
| 215 | + |
| 216 | + postPublication.removeAll(events); |
| 217 | + |
| 218 | + throw new IllegalStateException(ILLEGAL_MODIFCATION.formatted(postPublication)); |
| 219 | + } |
| 220 | + |
203 | 221 | if (clearingMethod != null) {
|
204 | 222 | ReflectionUtils.invokeMethod(clearingMethod, aggregateRoot);
|
205 | 223 | }
|
@@ -272,23 +290,34 @@ private static Method getClearingMethod(AnnotationDetectionMethodCallback<?> cle
|
272 | 290 | * one-element collection, {@literal null} will become an empty collection.
|
273 | 291 | *
|
274 | 292 | * @param source can be {@literal null}.
|
275 |
| - * @return |
| 293 | + * @return will never be {@literal null}. |
276 | 294 | */
|
277 | 295 | @SuppressWarnings("unchecked")
|
278 |
| - private static Iterable<Object> asCollection(@Nullable Object source, @Nullable Method method) { |
| 296 | + private static Collection<Object> asCollection(@Nullable Object source) { |
279 | 297 |
|
280 | 298 | if (source == null) {
|
281 | 299 | return Collections.emptyList();
|
282 | 300 | }
|
283 | 301 |
|
284 |
| - if (method != null && method.getName().startsWith("saveAll")) { |
285 |
| - return (Iterable<Object>) source; |
286 |
| - } |
287 |
| - |
288 | 302 | if (Collection.class.isInstance(source)) {
|
289 |
| - return (Collection<Object>) source; |
| 303 | + return new ArrayList<>((Collection<Object>) source); |
290 | 304 | }
|
291 | 305 |
|
292 | 306 | return Collections.singletonList(source);
|
293 | 307 | }
|
| 308 | + |
| 309 | + /** |
| 310 | + * Returns the given source object as {@link Iterable}. |
| 311 | + * |
| 312 | + * @param source can be {@literal null}. |
| 313 | + * @return will never be {@literal null}. |
| 314 | + */ |
| 315 | + @Nullable |
| 316 | + @SuppressWarnings("unchecked") |
| 317 | + private static Iterable<Object> asIterable(@Nullable Object source, @Nullable Method method) { |
| 318 | + |
| 319 | + return method != null && method.getName().startsWith("saveAll") |
| 320 | + ? (Iterable<Object>) source |
| 321 | + : asCollection(source); |
| 322 | + } |
294 | 323 | }
|
0 commit comments