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.
This commit is contained in:
Oliver Drotbohm
2023-04-01 16:24:48 +02:00
parent a257a0399a
commit 83162b25c7
5 changed files with 145 additions and 214 deletions

View File

@@ -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);

View File

@@ -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<EntityCallback<Person>> 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