From 83162b25c7cbd02e1f1e84b3c55ed47dd2da36d3 Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Sat, 1 Apr 2023 16:24:48 +0200 Subject: [PATCH] Improve lambda-based EntityCallback detection. In case an EntityCallback is declared as lambda expression, the JVM does not expose any generics information about the target entity type the callback shall be applied to. This commit changes the callback lookup and processing so that in case the generics information is not detectable on the type, we fall back to the BeanDefinition's resolvable type (fed by the factory method's return type which carries the necessary reflection information). That generics information is then kept in the newly introduce EntityCallbackAdapter and the code inspecting the actual entity type for matches then uses the resolvable type held in that. Also, the actual callback invocation is done on the adapter's delegate. Removed the ability of the discoverer to register EntityCallbacks by bean name as that was not used in the public API at all and it avoids duplicating the bean definition type detection. A couple of minor additional cleanups (records for cache key, methods static where possible and with lower visibility etc.) Fixes #2812. --- .../callback/DefaultEntityCallbacks.java | 8 +- .../DefaultReactiveEntityCallbacks.java | 8 +- .../callback/EntityCallbackDiscoverer.java | 302 +++++++----------- .../DefaultEntityCallbacksUnitTests.java | 7 +- .../EntityCallbackDiscovererUnitTests.java | 34 +- 5 files changed, 145 insertions(+), 214 deletions(-) diff --git a/src/main/java/org/springframework/data/mapping/callback/DefaultEntityCallbacks.java b/src/main/java/org/springframework/data/mapping/callback/DefaultEntityCallbacks.java index 6bb3fc852..b844aa474 100644 --- a/src/main/java/org/springframework/data/mapping/callback/DefaultEntityCallbacks.java +++ b/src/main/java/org/springframework/data/mapping/callback/DefaultEntityCallbacks.java @@ -21,7 +21,6 @@ import java.util.function.BiFunction; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - import org.springframework.beans.factory.BeanFactory; import org.springframework.core.ResolvableType; import org.springframework.util.Assert; @@ -35,6 +34,7 @@ import org.springframework.util.ReflectionUtils; * * @author Mark Paluch * @author Christoph Strobl + * @author Oliver Drotbohm * @since 2.2 */ class DefaultEntityCallbacks implements EntityCallbacks { @@ -93,7 +93,8 @@ class DefaultEntityCallbacks implements EntityCallbacks { this.callbackDiscoverer.addEntityCallback(callback); } - static class SimpleEntityCallbackInvoker implements org.springframework.data.mapping.callback.EntityCallbackInvoker { + static class SimpleEntityCallbackInvoker + implements org.springframework.data.mapping.callback.EntityCallbackInvoker { @Override public T invokeCallback(EntityCallback callback, T entity, @@ -108,11 +109,12 @@ class DefaultEntityCallbacks implements EntityCallbacks { } throw new IllegalArgumentException( - String.format("Callback invocation on %s returned null value for %s", callback.getClass(), entity)); + "Callback invocation on %s returned null value for %s".formatted(callback.getClass(), entity)); } catch (IllegalArgumentException | ClassCastException ex) { String msg = ex.getMessage(); + if (msg == null || EntityCallbackInvoker.matchesClassCastMessage(msg, entity.getClass())) { // Possibly a lambda-defined listener which we could not resolve the generic event type for diff --git a/src/main/java/org/springframework/data/mapping/callback/DefaultReactiveEntityCallbacks.java b/src/main/java/org/springframework/data/mapping/callback/DefaultReactiveEntityCallbacks.java index f10665e30..abd59f592 100644 --- a/src/main/java/org/springframework/data/mapping/callback/DefaultReactiveEntityCallbacks.java +++ b/src/main/java/org/springframework/data/mapping/callback/DefaultReactiveEntityCallbacks.java @@ -24,7 +24,6 @@ import java.util.function.BiFunction; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; - import org.springframework.beans.factory.BeanFactory; import org.springframework.core.ResolvableType; import org.springframework.util.Assert; @@ -38,6 +37,7 @@ import org.springframework.util.ReflectionUtils; * @author Mark Paluch * @author Christoph Strobl * @author Michael J. Simons + * @author Oliver Drotbohm */ class DefaultReactiveEntityCallbacks implements ReactiveEntityCallbacks { @@ -63,11 +63,13 @@ class DefaultReactiveEntityCallbacks implements ReactiveEntityCallbacks { } @Override + @SuppressWarnings("unchecked") public Mono callback(Class callbackType, T entity, Object... args) { Assert.notNull(entity, "Entity must not be null"); - Class entityType = (Class) (entity != null ? ClassUtils.getUserClass(entity.getClass()) + Class entityType = (Class) (entity != null + ? ClassUtils.getUserClass(entity.getClass()) : callbackDiscoverer.resolveDeclaredEntityType(callbackType).getRawClass()); Method callbackMethod = callbackMethodCache.computeIfAbsent(callbackType, it -> { @@ -112,7 +114,7 @@ class DefaultReactiveEntityCallbacks implements ReactiveEntityCallbacks { } throw new IllegalArgumentException( - String.format("Callback invocation on %s returned null value for %s", callback.getClass(), entity)); + "Callback invocation on %s returned null value for %s".formatted(callback.getClass(), entity)); } catch (IllegalArgumentException | ClassCastException ex) { String msg = ex.getMessage(); diff --git a/src/main/java/org/springframework/data/mapping/callback/EntityCallbackDiscoverer.java b/src/main/java/org/springframework/data/mapping/callback/EntityCallbackDiscoverer.java index 7a699a673..f2a4780f5 100644 --- a/src/main/java/org/springframework/data/mapping/callback/EntityCallbackDiscoverer.java +++ b/src/main/java/org/springframework/data/mapping/callback/EntityCallbackDiscoverer.java @@ -28,15 +28,18 @@ import java.util.function.BiFunction; import org.springframework.aop.framework.AopProxyUtils; import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.core.ResolvableType; +import org.springframework.core.ResolvableTypeProvider; import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ConcurrentReferenceHashMap; -import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.comparator.Comparators; @@ -45,11 +48,12 @@ import org.springframework.util.comparator.Comparators; * @author Christoph Strobl * @author Myeonghyeon Lee * @author Johannes Englmeier + * @author Oliver Drotbohm * @since 2.2 */ class EntityCallbackDiscoverer { - private final CallbackRetriever defaultRetriever = new CallbackRetriever(false); + private final CallbackRetriever defaultRetriever = new CallbackRetriever(); private final Map retrieverCache = new ConcurrentHashMap<>(64); private final Map, ResolvableType> entityTypeCache = new ConcurrentReferenceHashMap<>(64); @@ -64,9 +68,8 @@ class EntityCallbackDiscoverer { EntityCallbackDiscoverer() {} /** - * Create a new {@link EntityCallback} instance. - *

- * Pre loads {@link EntityCallback} beans by scanning the {@link BeanFactory}. + * Create a new {@link EntityCallback} instance.

callback) { synchronized (this.retrievalMutex) { @@ -105,29 +102,12 @@ class EntityCallbackDiscoverer { } } - void removeEntityCallbackBean(String callbackBeanName) { - - synchronized (this.retrievalMutex) { - this.defaultRetriever.entityCallbackBeans.remove(callbackBeanName); - this.retrieverCache.clear(); - } - } - - void clear() { - - synchronized (this.retrievalMutex) { - this.defaultRetriever.entityCallbacks.clear(); - this.defaultRetriever.entityCallbackBeans.clear(); - this.retrieverCache.clear(); - } - } - /** * Return a {@link Collection} of all {@link EntityCallback}s matching the given entity type. Non-matching callbacks * get excluded early. * - * @param entity the entity to be called back for. Allows for excluding non-matching callbacks early, based on cached - * matching information. + * @param entity the entity to be called back for. Allows for excluding non-matching callbacks early, based on + * cached matching information. * @param callbackType the source callback type. * @return a {@link Collection} of {@link EntityCallback}s. * @see EntityCallback @@ -140,27 +120,27 @@ class EntityCallbackDiscoverer { // Quick check for existing entry on ConcurrentHashMap... CallbackRetriever retriever = this.retrieverCache.get(cacheKey); if (retriever != null) { - return (Collection>) (Collection) retriever.getEntityCallbacks(); + return (Collection) retriever.getEntityCallbacks(); } - if (this.beanClassLoader == null || (ClassUtils.isCacheSafe(entity.getClass(), this.beanClassLoader) - && (sourceType == null || ClassUtils.isCacheSafe(sourceType, this.beanClassLoader)))) { + if (this.beanClassLoader == null || ClassUtils.isCacheSafe(entity.getClass(), this.beanClassLoader) + && (sourceType == null || ClassUtils.isCacheSafe(sourceType, this.beanClassLoader))) { // Fully synchronized building and caching of a CallbackRetriever synchronized (this.retrievalMutex) { retriever = this.retrieverCache.get(cacheKey); if (retriever != null) { - return (Collection>) (Collection) retriever.getEntityCallbacks(); + return (Collection) retriever.getEntityCallbacks(); } - retriever = new CallbackRetriever(true); + retriever = new CallbackRetriever(); Collection> callbacks = retrieveEntityCallbacks(ResolvableType.forClass(sourceType), callbackType, retriever); this.retrieverCache.put(cacheKey, retriever); - return (Collection>) (Collection) callbacks; + return (Collection) callbacks; } } else { // No CallbackRetriever caching -> no synchronization necessary - return (Collection>) (Collection) retrieveEntityCallbacks(callbackType, callbackType, null); + return (Collection) retrieveEntityCallbacks(callbackType, callbackType, null); } } @@ -174,7 +154,7 @@ class EntityCallbackDiscoverer { entityTypeCache.put(callbackType, eventType); } - return (eventType != ResolvableType.NONE ? eventType : null); + return eventType != ResolvableType.NONE ? eventType : null; } /** @@ -185,55 +165,33 @@ class EntityCallbackDiscoverer { * @param retriever the {@link CallbackRetriever}, if supposed to populate one (for caching purposes) * @return the pre-filtered list of entity callbacks for the given entity and callback type. */ - private Collection> retrieveEntityCallbacks(ResolvableType entityType, ResolvableType callbackType, - @Nullable CallbackRetriever retriever) { + private Collection> retrieveEntityCallbacks(ResolvableType entityType, + ResolvableType callbackType, @Nullable CallbackRetriever retriever) { List> allCallbacks = new ArrayList<>(); Set> callbacks; - Set callbackBeans; synchronized (this.retrievalMutex) { callbacks = new LinkedHashSet<>(this.defaultRetriever.entityCallbacks); - callbackBeans = new LinkedHashSet<>(this.defaultRetriever.entityCallbackBeans); } for (EntityCallback callback : callbacks) { + if (supportsEvent(callback, entityType, callbackType)) { + + callback = callback instanceof EntityCallbackAdapter adapter ? adapter.delegate() : callback; + if (retriever != null) { retriever.getEntityCallbacks().add(callback); } - allCallbacks.add(callback); - } - } - if (!callbackBeans.isEmpty()) { - BeanFactory beanFactory = getRequiredBeanFactory(); - for (String callbackBeanName : callbackBeans) { - try { - Class callbackImplType = beanFactory.getType(callbackBeanName); - if (callbackImplType == null || supportsEvent(callbackImplType, entityType)) { - EntityCallback callback = beanFactory.getBean(callbackBeanName, EntityCallback.class); - if (!allCallbacks.contains(callback) && supportsEvent(callback, entityType, callbackType)) { - if (retriever != null) { - if (beanFactory.isSingleton(callbackBeanName)) { - retriever.entityCallbacks.add(callback); - } else { - retriever.entityCallbackBeans.add(callbackBeanName); - } - } - allCallbacks.add(callback); - } - } - } catch (NoSuchBeanDefinitionException ex) { - // Singleton callback instance (without backing bean definition) disappeared - - // probably in the middle of the destruction phase - } + allCallbacks.add(callback); } } AnnotationAwareOrderComparator.sort(allCallbacks); - if (retriever != null && retriever.entityCallbackBeans.isEmpty()) { + if (retriever != null) { retriever.entityCallbacks.clear(); retriever.entityCallbacks.addAll(allCallbacks); } @@ -242,43 +200,8 @@ class EntityCallbackDiscoverer { } /** - * Filter a callback early through checking its generically declared entity type before trying to instantiate it. - *

- * If this method returns {@literal true} for a given callback as a first pass, the callback instance will get - * retrieved and fully evaluated through a {@link #supportsEvent(EntityCallback, ResolvableType, ResolvableType)} call - * afterwards. - * - * @param callback the callback's type as determined by the BeanFactory. - * @param entityType the entity type to check. - * @return whether the given callback should be included in the candidates for the given callback type. - */ - protected boolean supportsEvent(Class callback, ResolvableType entityType) { - - ResolvableType declaredEventType = resolveDeclaredEntityType(callback); - return (declaredEventType == null || declaredEventType.isAssignableFrom(entityType)); - } - - /** - * Determine whether the given callback supports the given entity type and callback type. - * - * @param callback the target callback to check. - * @param entityType the entity type to check. - * @param callbackType the source type to check against. - * @return whether the given callback should be included in the candidates for the given callback type. - */ - protected boolean supportsEvent(EntityCallback callback, ResolvableType entityType, ResolvableType callbackType) { - - return supportsEvent(callback.getClass(), entityType) - && callbackType.isAssignableFrom(ResolvableType.forInstance(callback)); - } - - public void setBeanClassLoader(ClassLoader classLoader) { - this.beanClassLoader = classLoader; - } - - /** - * Set the {@link BeanFactory} and optionally {@link #setBeanClassLoader(ClassLoader) class loader} if not set. Pre - * loads {@link EntityCallback} beans by scanning the {@link BeanFactory}. + * Set the {@link BeanFactory} and optionally {@link #setBeanClassLoader(ClassLoader) class loader} if not set. + * Pre-loads {@link EntityCallback} beans by scanning the {@link BeanFactory}. * * @param beanFactory must not be {@literal null}. * @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(BeanFactory) @@ -288,13 +211,15 @@ class EntityCallbackDiscoverer { this.beanFactory = beanFactory; if (beanFactory instanceof ConfigurableBeanFactory cbf) { + if (this.beanClassLoader == null) { this.beanClassLoader = cbf.getBeanClassLoader(); } + this.retrievalMutex = cbf.getSingletonMutex(); } - defaultRetriever.discoverEntityCallbacks(this.beanFactory); + defaultRetriever.discoverEntityCallbacks(beanFactory); this.retrieverCache.clear(); } @@ -305,8 +230,10 @@ class EntityCallbackDiscoverer { ReflectionUtils.doWithMethods(callbackType, methods::add, method -> { - if (!Modifier.isPublic(method.getModifiers()) || method.getParameterCount() != args.length + 1 - || method.isBridge() || ReflectionUtils.isObjectMethod(method)) { + if (!Modifier.isPublic(method.getModifiers()) + || method.getParameterCount() != args.length + 1 + || method.isBridge() + || ReflectionUtils.isObjectMethod(method)) { return false; } @@ -318,7 +245,7 @@ class EntityCallbackDiscoverer { } throw new IllegalStateException( - String.format("%s does not define a callback method accepting %s and %s additional arguments", + "%s does not define a callback method accepting %s and %s additional arguments".formatted( ClassUtils.getShortName(callbackType), ClassUtils.getShortName(entityType), args.length)); } @@ -329,6 +256,7 @@ class EntityCallbackDiscoverer { Object[] invocationArgs = new Object[args.length + 1]; invocationArgs[0] = entity; + if (args.length > 0) { System.arraycopy(args, 0, invocationArgs, 1, args.length); } @@ -337,104 +265,108 @@ class EntityCallbackDiscoverer { }; } - private BeanFactory getRequiredBeanFactory() { + /** + * Filter a callback early through checking its generically declared entity type before trying to instantiate it. + *

+ * If this method returns {@literal true} for a given callback as a first pass, the callback instance will get + * retrieved and fully evaluated through a {@link #supportsEvent(EntityCallback, ResolvableType, ResolvableType)} + * call afterwards. + * + * @param callback the callback's type as determined by the BeanFactory. + * @param entityType the entity type to check. + * @return whether the given callback should be included in the candidates for the given callback type. + */ + static boolean supportsEvent(ResolvableType callbackType, ResolvableType entityType) { + return callbackType.as(EntityCallback.class).getGeneric(0).isAssignableFrom(entityType); + } - Assert.state(beanFactory != null, - "EntityCallbacks cannot retrieve callback beans because it is not associated with a BeanFactory"); - return beanFactory; + /** + * Determine whether the given callback supports the given entity type and callback type. + * + * @param callback the target callback to check. + * @param entityType the entity type to check. + * @param callbackType the source type to check against. + * @return whether the given callback should be included in the candidates for the given callback type. + */ + static boolean supportsEvent(EntityCallback callback, ResolvableType entityType, + ResolvableType callbackType) { + + ResolvableType callbackInstanceType = callback instanceof EntityCallbackAdapter provider + ? provider.getResolvableType() + : ResolvableType.forInstance(callback); + + return supportsEvent(callbackInstanceType, entityType) + && callbackType.isAssignableFrom(callbackInstanceType); } /** * Helper class that encapsulates a specific set of target {@link EntityCallback callbacks}, allowing for efficient * retrieval of pre-filtered callbacks. */ - class CallbackRetriever { + private static class CallbackRetriever { private final Set> entityCallbacks = new LinkedHashSet<>(); - private final Set entityCallbackBeans = new LinkedHashSet<>(); - - private final boolean preFiltered; - - CallbackRetriever(boolean preFiltered) { - this.preFiltered = preFiltered; - } - Collection> getEntityCallbacks() { - - List> allCallbacks = new ArrayList<>( - this.entityCallbacks.size() + this.entityCallbackBeans.size()); - allCallbacks.addAll(this.entityCallbacks); - - if (!this.entityCallbackBeans.isEmpty()) { - BeanFactory beanFactory = getRequiredBeanFactory(); - for (String callbackBeanName : this.entityCallbackBeans) { - try { - EntityCallback callback = beanFactory.getBean(callbackBeanName, EntityCallback.class); - if (this.preFiltered || !allCallbacks.contains(callback)) { - allCallbacks.add(callback); - } - } catch (NoSuchBeanDefinitionException ex) { - // Singleton callback instance (without backing bean definition) disappeared - - // probably in the middle of the destruction phase - } - } - } - - if (!this.preFiltered || !this.entityCallbackBeans.isEmpty()) { - AnnotationAwareOrderComparator.sort(allCallbacks); - } - - return allCallbacks; + return this.entityCallbacks; } void discoverEntityCallbacks(BeanFactory beanFactory) { - beanFactory.getBeanProvider(EntityCallback.class).stream().forEach(entityCallbacks::add); + + // We need both a ListableBeanFactory and BeanDefinitionRegistry here for advanced inspection. + // If we don't get that, use simple inspection. + if (!(beanFactory instanceof ListableBeanFactory && beanFactory instanceof BeanDefinitionRegistry)) { + beanFactory.getBeanProvider(EntityCallback.class).stream().forEach(entityCallbacks::add); + return; + } + + var bf = (ListableBeanFactory & BeanDefinitionRegistry) beanFactory; + + for (var beanName : bf.getBeanNamesForType(EntityCallback.class)) { + + EntityCallback bean = EntityCallback.class.cast(bf.getBean(beanName)); + + ResolvableType type = ResolvableType.forClass(EntityCallback.class, bean.getClass()); + ResolvableType entityType = type.getGeneric(0); + + if (entityType.resolve() != null) { + entityCallbacks.add(bean); + } else { + + BeanDefinition definition = bf.getBeanDefinition(beanName); + entityCallbacks.add(new EntityCallbackAdapter<>(bean, definition.getResolvableType())); + } + } + } + } + + /** + * A combination of an {@link EntityCallback} + * + * @author Oliver Drotbohm + */ + private static record EntityCallbackAdapter(EntityCallback delegate, ResolvableType type) + implements EntityCallback, ResolvableTypeProvider { + + @NonNull + @Override + public ResolvableType getResolvableType() { + return type; } } /** * Cache key for {@link EntityCallback}, based on event type and source type. */ - static final class CallbackCacheKey implements Comparable { - - private final ResolvableType callbackType; - - private final Class entityType; - - public CallbackCacheKey(ResolvableType callbackType, @Nullable Class entityType) { - - Assert.notNull(callbackType, "Callback type must not be null"); - Assert.notNull(entityType, "Entity type must not be null"); - - this.callbackType = callbackType; - this.entityType = entityType; - } - - @Override - public boolean equals(Object other) { - - if (this == other) { - return true; - } - - CallbackCacheKey otherKey = (CallbackCacheKey) other; - - return (this.callbackType.equals(otherKey.callbackType) - && ObjectUtils.nullSafeEquals(this.entityType, otherKey.entityType)); - } - - @Override - public int hashCode() { - return this.callbackType.hashCode() * 17 + ObjectUtils.nullSafeHashCode(this.entityType); - } + private static record CallbackCacheKey(ResolvableType callbackType, @Nullable Class entityType) + implements Comparable { @Override public int compareTo(CallbackCacheKey other) { - return Comparators. nullsHigh().thenComparing(it -> callbackType.toString()) + return Comparators. nullsHigh() + .thenComparing(it -> callbackType.toString()) .thenComparing(it -> entityType.getName()).compare(this, other); - } } diff --git a/src/test/java/org/springframework/data/mapping/callback/DefaultEntityCallbacksUnitTests.java b/src/test/java/org/springframework/data/mapping/callback/DefaultEntityCallbacksUnitTests.java index 48eac92d6..2c4dcc025 100644 --- a/src/test/java/org/springframework/data/mapping/callback/DefaultEntityCallbacksUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/callback/DefaultEntityCallbacksUnitTests.java @@ -65,6 +65,7 @@ class DefaultEntityCallbacksUnitTests { var afterCallback = callbacks.callback(BeforeSaveCallback.class, personDocument); assertThat(afterCallback).isSameAs(personDocument); + assertThat(afterCallback.getSsn()).isEqualTo(6); } @Test // DATACMNS-1467 @@ -73,7 +74,8 @@ class DefaultEntityCallbacksUnitTests { var callbacks = new DefaultEntityCallbacks(); callbacks.addEntityCallback(new GenericPersonCallback()); - Person afterCallback = callbacks.callback(GenericPersonCallback.class, new PersonDocument(null, "Walter", null)); + Person afterCallback = callbacks.callback(GenericPersonCallback.class, + new PersonDocument(null, "Walter", null)); assertThat(afterCallback.getSsn()).isEqualTo(6); } @@ -143,7 +145,8 @@ class DefaultEntityCallbacksUnitTests { var initial = new PersonDocument(null, "Walter", null); - assertThatIllegalArgumentException().isThrownBy(() -> callbacks.callback(CapturingEntityCallback.class, initial)); + assertThatIllegalArgumentException() + .isThrownBy(() -> callbacks.callback(CapturingEntityCallback.class, initial)); assertThat(first.capturedValue()).isSameAs(initial); assertThat(second.capturedValue()).isNotNull().isNotSameAs(initial); diff --git a/src/test/java/org/springframework/data/mapping/callback/EntityCallbackDiscovererUnitTests.java b/src/test/java/org/springframework/data/mapping/callback/EntityCallbackDiscovererUnitTests.java index 358c59bf1..7f3110cd7 100644 --- a/src/test/java/org/springframework/data/mapping/callback/EntityCallbackDiscovererUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/callback/EntityCallbackDiscovererUnitTests.java @@ -25,6 +25,7 @@ import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import org.assertj.core.api.AbstractBooleanAssert; import org.junit.jupiter.api.Test; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; @@ -43,6 +44,7 @@ import org.springframework.data.mapping.PersonDocument; * @author Christoph Strobl * @author Myeonghyeon Lee * @author Mark Paluch + * @author Oliver Drotbohm */ class EntityCallbackDiscovererUnitTests { @@ -94,33 +96,23 @@ class EntityCallbackDiscovererUnitTests { assertThat(exceptions).isEmpty(); } - @Test // DATACMNS-1467 - void shouldDiscoverCallbackTypeByName() { - - var ctx = new AnnotationConfigApplicationContext(MyConfig.class); - - var discoverer = new EntityCallbackDiscoverer(ctx); - discoverer.clear(); - discoverer.addEntityCallbackBean("namedCallback"); - - Collection> entityCallbacks = discoverer.getEntityCallbacks(PersonDocument.class, - ResolvableType.forType(BeforeSaveCallback.class)); - - assertThat(entityCallbacks).hasSize(1).element(0).isInstanceOf(MyOtherCallback.class); - } - @Test // DATACMNS-1467 void shouldSupportCallbackTypes() { var discoverer = new EntityCallbackDiscoverer(); - assertThat(discoverer.supportsEvent(MyBeforeSaveCallback.class, ResolvableType.forClass(Person.class))).isTrue(); - assertThat(discoverer.supportsEvent(MyBeforeSaveCallback.class, ResolvableType.forClass(Child.class))).isTrue(); - assertThat(discoverer.supportsEvent(BeforeSaveCallback.class, ResolvableType.forClass(PersonDocument.class))) - .isTrue(); + assertSupportsEvent(discoverer, MyBeforeSaveCallback.class, Person.class).isTrue(); + assertSupportsEvent(discoverer, MyBeforeSaveCallback.class, Child.class).isTrue(); + assertSupportsEvent(discoverer, BeforeSaveCallback.class, PersonDocument.class).isTrue(); - assertThat(discoverer.supportsEvent(MyBeforeSaveCallback.class, ResolvableType.forClass(Object.class))).isFalse(); - assertThat(discoverer.supportsEvent(MyBeforeSaveCallback.class, ResolvableType.forClass(User.class))).isFalse(); + assertSupportsEvent(discoverer, MyBeforeSaveCallback.class, Object.class).isFalse(); + assertSupportsEvent(discoverer, MyBeforeSaveCallback.class, User.class).isFalse(); + } + + private static AbstractBooleanAssert assertSupportsEvent(EntityCallbackDiscoverer discoverer, + Class callbackType, Class entityType) { + return assertThat( + discoverer.supportsEvent(ResolvableType.forClass(callbackType), ResolvableType.forClass(entityType))); } @Test // DATACMNS-1467