Skip to content

Commit

Permalink
Gracefully skip non-assignable lambda callbacks on Java 18.
Browse files Browse the repository at this point in the history
We now consider IllegalArgumentException as marker for incompatible lambda payload that was introduced with Java 18's reflection rewrite that uses method handles internally.

Closes #2583
  • Loading branch information
mp911de committed Mar 23, 2022
1 parent 3541820 commit f5ee277
Show file tree
Hide file tree
Showing 3 changed files with 40 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ public <T> T invokeCallback(EntityCallback<T> callback, T entity,
throw new IllegalArgumentException(
String.format("Callback invocation on %s returned null value for %s", callback.getClass(), entity));

} catch (ClassCastException ex) {
} catch (IllegalArgumentException | ClassCastException ex) {

var msg = ex.getMessage();
if (msg == null || EntityCallbackInvoker.matchesClassCastMessage(msg, entity.getClass())) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,26 @@ interface EntityCallbackInvoker {
<T> Object invokeCallback(EntityCallback<T> callback, T entity,
BiFunction<EntityCallback<T>, T, Object> callbackInvokerFunction);

static boolean matchesClassCastMessage(String classCastMessage, Class<?> eventClass) {
static boolean matchesClassCastMessage(String exceptionMessage, Class<?> eventClass) {

// On Java 8, the message starts with the class name: "java.lang.String cannot be cast..."
if (classCastMessage.startsWith(eventClass.getName())) {
if (exceptionMessage.startsWith(eventClass.getName())) {
return true;
}

// On Java 11, the message starts with "class ..." a.k.a. Class.toString()
if (classCastMessage.startsWith(eventClass.toString())) {
if (exceptionMessage.startsWith(eventClass.toString())) {
return true;
}

// On Java 9, the message used to contain the module name: "java.base/java.lang.String cannot be cast..."
var moduleSeparatorIndex = classCastMessage.indexOf('/');
if (moduleSeparatorIndex != -1 && classCastMessage.startsWith(eventClass.getName(), moduleSeparatorIndex + 1)) {
var moduleSeparatorIndex = exceptionMessage.indexOf('/');
if (moduleSeparatorIndex != -1 && exceptionMessage.startsWith(eventClass.getName(), moduleSeparatorIndex + 1)) {
return true;
}

// On Java 18, the message is "IllegalArgumentException: argument type mismatch"
if (exceptionMessage.equals("argument type mismatch")) {
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.springframework.core.Ordered;
import org.springframework.data.mapping.Person;
import org.springframework.data.mapping.PersonDocument;
import org.springframework.data.mapping.PersonNoId;
import org.springframework.data.mapping.callback.CapturingEntityCallback.FirstCallback;
import org.springframework.data.mapping.callback.CapturingEntityCallback.SecondCallback;
import org.springframework.data.mapping.callback.CapturingEntityCallback.ThirdCallback;
Expand Down Expand Up @@ -95,9 +96,8 @@ void invokeInvalidEvent() {
var callbacks = new DefaultEntityCallbacks();
callbacks.addEntityCallback(new InvalidEntityCallback() {});

assertThatIllegalStateException()
.isThrownBy(() -> callbacks.callback(InvalidEntityCallback.class, new PersonDocument(null, "Walter", null),
"agr0", Float.POSITIVE_INFINITY));
assertThatIllegalStateException().isThrownBy(() -> callbacks.callback(InvalidEntityCallback.class,
new PersonDocument(null, "Walter", null), "agr0", Float.POSITIVE_INFINITY));
}

@Test // DATACMNS-1467
Expand Down Expand Up @@ -126,8 +126,7 @@ void errorsOnNullEntity() {
var callbacks = new DefaultEntityCallbacks();
callbacks.addEntityCallback(new CapturingEntityCallback());

assertThatIllegalArgumentException()
.isThrownBy(() -> callbacks.callback(CapturingEntityCallback.class, null));
assertThatIllegalArgumentException().isThrownBy(() -> callbacks.callback(CapturingEntityCallback.class, null));
}

@Test // DATACMNS-1467
Expand All @@ -144,18 +143,31 @@ void errorsOnNullValueReturnedByCallbackEntity() {

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);
assertThat(third.capturedValues()).isEmpty();
}

@Test // GH-2583
void skipsInvocationUsingJava18ReflectiveTypeRejection() {

DefaultEntityCallbacks callbacks = new DefaultEntityCallbacks();
callbacks.addEntityCallback(new Java18ClassCastStyle());

Person person = new PersonNoId(42, "Walter", "White");

Person afterCallback = callbacks.callback(BeforeConvertCallback.class, person);

assertThat(afterCallback).isSameAs(person);
}

@Test // DATACMNS-1467
void detectsMultipleCallbacksWithinOneClass() {

var ctx = new AnnotationConfigApplicationContext(MultipleCallbacksInOneClassConfig.class);
var ctx = new AnnotationConfigApplicationContext(
MultipleCallbacksInOneClassConfig.class);

var callbacks = new DefaultEntityCallbacks(ctx);

Expand Down Expand Up @@ -288,4 +300,13 @@ public Person onBeforeSave(Person object) {
}
}

static class Java18ClassCastStyle implements BeforeConvertCallback<Person> {

@Override
public Person onBeforeConvert(Person object) {
throw new IllegalArgumentException("argument type mismatch");
}

}

}

0 comments on commit f5ee277

Please sign in to comment.