From b0cb728944ac6b8c258297ff20f19b0a61133ace Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Sun, 28 Jan 2018 23:55:25 -0800 Subject: [PATCH 1/2] Add general utility to deal with lambda callbacks Add `LambdaSafe` utility that provides a consistent way to deal with the problems that can occur when calling lambda based callbacks. See gh-11584 --- .../springframework/boot/util/LambdaSafe.java | 402 +++++++++++++++ .../boot/util/package-info.java | 20 + .../boot/util/LambdaSafeTests.java | 475 ++++++++++++++++++ 3 files changed, 897 insertions(+) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/util/LambdaSafe.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/util/package-info.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/util/LambdaSafeTests.java diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/util/LambdaSafe.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/util/LambdaSafe.java new file mode 100644 index 0000000000..1550da13b7 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/util/LambdaSafe.java @@ -0,0 +1,402 @@ +/* + * Copyright 2012-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.util; + +import java.util.Collection; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.ResolvableType; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Utility that can be used to invoke lambdas in a safe way. Primarily designed to help + * support generically typed callbacks where {@link ClassCastException class cast + * exceptions} need to be dealt with due to class erasure. + * + * @author Phillip Webb + * @since 2.0.0 + */ +public final class LambdaSafe { + + private LambdaSafe() { + } + + /** + * Start a call to a single callback instance, dealing with common generic type + * concerns and exceptions. + * @param callbackType the callback type (a {@link FunctionalInterface functional + * interface}) + * @param callbackInstance the callback instance (may be a lambda) + * @param argument the primary argument passed to the callback + * @param additionalArguments any additional argument passed to the callback + * @param the callback type + * @param the primary argument type + * @return a {@link Callback} instance that can be invoked. + */ + public static Callback callback(Class callbackType, + C callbackInstance, A argument, Object... additionalArguments) { + Assert.notNull(callbackType, "CallbackType must not be null"); + Assert.notNull(callbackInstance, "CallbackInstance must not be null"); + return new Callback<>(callbackType, callbackInstance, argument, + additionalArguments); + } + + /** + * Start a call to a single callback instance, dealing with common generic type + * concerns and exceptions. + * @param callbackType the callback type (a {@link FunctionalInterface functional + * interface}) + * @param callbackInstances the callback instances (elements may be lambdas) + * @param argument the primary argument passed to the callbacks + * @param additionalArguments any additional argument passed to the callbacks + * @param the callback type + * @param the primary argument type + * @return a {@link Callbacks} instance that can be invoked. + */ + public static Callbacks callbacks(Class callbackType, + Collection callbackInstances, A argument, + Object... additionalArguments) { + Assert.notNull(callbackType, "CallbackType must not be null"); + Assert.notNull(callbackInstances, "CallbackInstances must not be null"); + return new Callbacks(callbackType, callbackInstances, argument, + additionalArguments); + } + + /** + * Abstract base class for lambda safe callbacks. + */ + private static abstract class LambdaSafeCallback> { + + private final Class callbackType; + + private final A argument; + + private final Object[] additionalArguments; + + private Log logger; + + private Filter filter = new GenericTypeFilter(); + + protected LambdaSafeCallback(Class callbackType, A argument, + Object[] additionalArguments) { + this.callbackType = callbackType; + this.argument = argument; + this.additionalArguments = additionalArguments; + this.logger = LogFactory.getLog(callbackType); + } + + /** + * Use the specified logger source to report any lambda failures. + * @param loggerSource the logger source to use + * @return this instance + */ + public SELF withLogger(Class loggerSource) { + return withLogger(LogFactory.getLog(loggerSource)); + } + + /** + * Use the specified logger to report any lambda failures. + * @param logger the logger to use + * @return this instance + */ + public SELF withLogger(Log logger) { + Assert.notNull(logger, "Logger must not be null"); + this.logger = logger; + return self(); + } + + /** + * Use a specific filter to determine when a callback should apply. If not + * explicit filter is set filter will be attempted using the generic type on the + * callback type. + * @param filter the filter to use + * @return this instance + */ + public SELF withFilter(Filter filter) { + Assert.notNull(filter, "Filter must not be null"); + this.filter = filter; + return self(); + } + + protected final InvocationResult invoke(C callbackInstance, + Supplier supplier) { + if (this.filter.match(this.callbackType, callbackInstance, this.argument, + this.additionalArguments)) { + try { + return InvocationResult.of(supplier.get()); + } + catch (ClassCastException ex) { + if (!isLambdaGenericProblem(ex)) { + throw ex; + } + logNonMachingType(callbackInstance, ex); + } + } + return InvocationResult.noResult(); + } + + private boolean isLambdaGenericProblem(ClassCastException ex) { + return (ex.getMessage() == null + || startsWithArgumentClassName(ex.getMessage())); + } + + private boolean startsWithArgumentClassName(String message) { + Predicate startsWith = (argument) -> argument != null + && message.startsWith(argument.getClass().getName()); + return startsWith.test(this.argument) + || Stream.of(this.additionalArguments).anyMatch(startsWith); + } + + private void logNonMachingType(C callback, ClassCastException ex) { + if (this.logger.isDebugEnabled()) { + Class expectedType = ResolvableType.forClass(this.callbackType) + .resolveGeneric(); + String message = "Non-matching " + + (expectedType == null ? "type" + : ClassUtils.getShortName(expectedType) + " type") + + " for callback " + ClassUtils.getShortName(this.callbackType) + + ": " + callback; + this.logger.debug(message, ex); + } + } + + @SuppressWarnings("unchecked") + private SELF self() { + return (SELF) this; + } + + } + + /** + * Represents a single callback that can be invoked in a lambda safe way. + * + * @param the callback type + * @param the primary argument type + */ + public static final class Callback + extends LambdaSafeCallback> { + + private C callbackInstance; + + private Callback(Class callbackType, C callbackInstance, A argument, + Object[] additionalArguments) { + super(callbackType, argument, additionalArguments); + this.callbackInstance = callbackInstance; + } + + /** + * Invoke the callback instance where the callback method returns void. + * @param invoker the invoker used to invoke the callback + */ + public void invoke(Consumer invoker) { + invoke(this.callbackInstance, () -> { + invoker.accept(this.callbackInstance); + return null; + }); + } + + /** + * Invoke the callback instance where the callback method returns a result. + * @param invoker the invoker used to invoke the callback + * @param the result type + * @return the result of the invocation (may be {@link InvocationResult#noResult} + * if the callback was not invoked) + */ + public InvocationResult invokeAnd(Function invoker) { + return invoke(this.callbackInstance, + () -> invoker.apply(this.callbackInstance)); + } + + } + + /** + * Represents a collection of callbacks that can be invoked in a lambda safe way. + * + * @param the callback type + * @param the primary argument type + */ + public static final class Callbacks + extends LambdaSafeCallback> { + + private Collection callbackInstances; + + private Callbacks(Class callbackType, + Collection callbackInstances, A argument, + Object[] additionalArguments) { + super(callbackType, argument, additionalArguments); + this.callbackInstances = callbackInstances; + } + + /** + * Invoke the callback instances where the callback method returns void. + * @param invoker the invoker used to invoke the callback + */ + public void invoke(Consumer invoker) { + Function> mapper = (callbackInstance) -> invoke( + callbackInstance, () -> { + invoker.accept(callbackInstance); + return null; + }); + this.callbackInstances.stream().map(mapper).forEach((result) -> { + }); + } + + /** + * Invoke the callback instances where the callback method returns a result. + * @param invoker the invoker used to invoke the callback + * @param the result type + * @return the results of the invocation (may be an empty stream if not callbacks + * could be called) + */ + public Stream invokeAnd(Function invoker) { + Function> mapper = (callbackInstance) -> invoke( + callbackInstance, () -> invoker.apply(callbackInstance)); + return this.callbackInstances.stream().map(mapper) + .filter(InvocationResult::hasResult).map(InvocationResult::get); + } + + } + + /** + * A filter that can be used to restrict when a callback is used. + * + * @param the callback type + * @param the primary argument type + */ + @FunctionalInterface + interface Filter { + + /** + * Determine if the given callback matches and should be invoked. + * @param callbackType the callback type (the functional interface) + * @param callbackInstance the callback instance (the implementation) + * @param argument the primary argument + * @param additionalArguments any additional arguments + * @return if the callback matches and should be invoked + */ + boolean match(Class callbackType, C callbackInstance, A argument, + Object[] additionalArguments); + + /** + * Return a {@link Filter} that allows all callbacks to be invoked. + * @param the callback type + * @param the primary argument type + * @return an "allow all" filter + */ + static Filter allowAll() { + return (callbackType, callbackInstance, argument, + additionalArguments) -> true; + } + + } + + /** + * {@link Filter} that matches when the callback has a single generic and primary + * argument is an instance of it. + */ + private static class GenericTypeFilter implements Filter { + + @Override + public boolean match(Class callbackType, C callbackInstance, A argument, + Object[] additionalArguments) { + ResolvableType type = ResolvableType.forClass(callbackType, + callbackInstance.getClass()); + if (type.getGenerics().length == 1 && type.resolveGeneric() != null) { + return type.resolveGeneric().isInstance(argument); + } + + return true; + } + + } + + /** + * The result of a callback which may be a value, {@code null} or absent entirely if + * the callback wasn't suitable. Similar in design to {@link Optional} but allows for + * {@code null} as a valid value. + * @param The result type + */ + public final static class InvocationResult { + + private static final InvocationResult NONE = new InvocationResult( + null); + + private final R value; + + private InvocationResult(R value) { + this.value = value; + } + + /** + * Return true if a result in present. + * @return if a result is present + */ + public boolean hasResult() { + return this != NONE; + } + + /** + * Return the result of the invocation or {@code null} if the callback wasn't + * suitable. + * @return the result of the invocation or {@code null} + */ + public R get() { + return this.value; + } + + /** + * Return the result of the invocation or the given fallback if the callback + * wasn't suitable. + * @param fallback the fallback to use when there is no result + * @return the result of the invocation or the fallback + */ + public R get(R fallback) { + return (this == NONE ? fallback : this.value); + } + + /** + * Create a new {@link InvocationResult} instance with the specified value. + * @param value the value (may be {@code null}) + * @param the result type + * @return an {@link InvocationResult} + */ + public static InvocationResult of(R value) { + return new InvocationResult(value); + } + + /** + * Return an {@link InvocationResult} instance representing no result. + * @param the result type + * @return an {@link InvocationResult} + */ + @SuppressWarnings("unchecked") + public static InvocationResult noResult() { + return (InvocationResult) NONE; + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/util/package-info.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/util/package-info.java new file mode 100644 index 0000000000..a0c609e9ff --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/util/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Contains miscellaneous utility classes. + */ +package org.springframework.boot.util; diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/util/LambdaSafeTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/util/LambdaSafeTests.java new file mode 100644 index 0000000000..3bbcd2ba21 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/util/LambdaSafeTests.java @@ -0,0 +1,475 @@ +/* + * Copyright 2012-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +import org.apache.commons.logging.Log; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.boot.util.LambdaSafe.Filter; +import org.springframework.boot.util.LambdaSafe.InvocationResult; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.contains; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Tests for {@link LambdaSafe}. + * + * @author Phillip Webb + */ +public class LambdaSafeTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void callbackWhenCallbackTypeIsNullShouldThrowException() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("CallbackType must not be null"); + LambdaSafe.callback(null, new Object(), null); + } + + @Test + public void callbackWhenCallbackInstanceIsNullShouldThrowException() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("CallbackInstance must not be null"); + LambdaSafe.callback(Object.class, null, null); + } + + @Test + public void callbackInvokeWhenNoGenericShouldInvokeCallback() { + NonGenericCallback callbackInstance = mock(NonGenericCallback.class); + String argument = "foo"; + LambdaSafe.callback(NonGenericCallback.class, callbackInstance, argument) + .invoke((c) -> c.handle(argument)); + verify(callbackInstance).handle(argument); + } + + @Test + @SuppressWarnings("unchecked") + public void callbackInvokeWhenHasGenericShouldInvokeCallback() { + StringCallback callbackInstance = mock(StringCallback.class); + String argument = "foo"; + LambdaSafe.callback(GenericCallback.class, callbackInstance, argument) + .invoke((c) -> c.handle(argument)); + verify(callbackInstance).handle(argument); + } + + @Test + @SuppressWarnings("unchecked") + public void callbackInvokeWhenHasResolvableGenericMatchShouldInvokeCallback() { + StringBuilderCallback callbackInstance = mock(StringBuilderCallback.class); + StringBuilder argument = new StringBuilder("foo"); + LambdaSafe.callback(GenericCallback.class, callbackInstance, argument) + .invoke((c) -> c.handle(argument)); + verify(callbackInstance).handle(argument); + } + + @Test + @SuppressWarnings("unchecked") + public void callbackInvokeWhenHasResolvableGenericNonMatchShouldNotInvokeCallback() { + GenericCallback callbackInstance = mock(StringBuilderCallback.class); + String argument = "foo"; + LambdaSafe.callback(GenericCallback.class, callbackInstance, argument) + .invoke((c) -> c.handle(argument)); + verifyZeroInteractions(callbackInstance); + } + + @Test + @SuppressWarnings("unchecked") + public void callbackInvokeWhenLambdaMismatchShouldSwallowException() { + GenericCallback callbackInstance = (s) -> { + fail("Should not get here"); + }; + String argument = "foo"; + LambdaSafe.callback(GenericCallback.class, callbackInstance, argument) + .invoke((c) -> c.handle(argument)); + } + + @Test + @SuppressWarnings("unchecked") + public void callbackInvokeWhenLambdaMismatchOnDifferentArgumentShouldSwallowException() { + GenericMultiArgCallback callbackInstance = (n, s, b) -> { + fail("Should not get here"); + }; + String argument = "foo"; + LambdaSafe.callback(GenericMultiArgCallback.class, callbackInstance, argument) + .invoke((c) -> c.handle(1, argument, false)); + } + + @Test + public void callbackInvokeAndWhenNoGenericShouldReturnResult() { + NonGenericFactory callbackInstance = mock(NonGenericFactory.class); + String argument = "foo"; + given(callbackInstance.handle("foo")).willReturn(123); + InvocationResult result = LambdaSafe + .callback(NonGenericFactory.class, callbackInstance, argument) + .invokeAnd((c) -> c.handle(argument)); + assertThat(result.hasResult()).isTrue(); + assertThat(result.get()).isEqualTo(123); + } + + @Test + @SuppressWarnings("unchecked") + public void callbackInvokeAndWhenHasGenericShouldReturnResult() { + StringFactory callbackInstance = mock(StringFactory.class); + String argument = "foo"; + given(callbackInstance.handle("foo")).willReturn(123); + InvocationResult result = LambdaSafe + .callback(GenericFactory.class, callbackInstance, argument) + .invokeAnd((c) -> c.handle(argument)); + assertThat(result.hasResult()).isTrue(); + assertThat(result.get()).isEqualTo(123); + } + + @Test + @SuppressWarnings("unchecked") + public void callbackInvokeAndWhenReturnNullShouldReturnResult() { + StringFactory callbackInstance = mock(StringFactory.class); + String argument = "foo"; + given(callbackInstance.handle("foo")).willReturn(null); + InvocationResult result = LambdaSafe + .callback(GenericFactory.class, callbackInstance, argument) + .invokeAnd((c) -> c.handle(argument)); + assertThat(result.hasResult()).isTrue(); + assertThat(result.get()).isNull(); + } + + @Test + @SuppressWarnings("unchecked") + public void callbackInvokeAndWhenHasResolvableGenericMatchShouldReturnResult() { + StringBuilderFactory callbackInstance = mock(StringBuilderFactory.class); + StringBuilder argument = new StringBuilder("foo"); + given(callbackInstance.handle(any(StringBuilder.class))).willReturn(123); + InvocationResult result = LambdaSafe + .callback(GenericFactory.class, callbackInstance, argument) + .invokeAnd((c) -> c.handle(argument)); + verify(callbackInstance).handle(argument); + assertThat(result.hasResult()).isTrue(); + assertThat(result.get()).isEqualTo(123); + } + + @Test + @SuppressWarnings("unchecked") + public void callbackInvokeAndWhenHasResolvableGenericNonMatchShouldReturnNoResult() { + GenericFactory callbackInstance = mock(StringBuilderFactory.class); + String argument = "foo"; + InvocationResult result = LambdaSafe + .callback(GenericFactory.class, callbackInstance, argument) + .invokeAnd((c) -> c.handle(argument)); + assertThat(result.hasResult()).isFalse(); + verifyZeroInteractions(callbackInstance); + } + + @Test + @SuppressWarnings("unchecked") + public void callbackInvokeAndWhenLambdaMismatchShouldSwallowException() { + GenericFactory callbackInstance = (s) -> { + fail("Should not get here"); + return 123; + }; + String argument = "foo"; + InvocationResult result = LambdaSafe + .callback(GenericFactory.class, callbackInstance, argument) + .invokeAnd((c) -> c.handle(argument)); + assertThat(result.hasResult()).isFalse(); + } + + @Test + @SuppressWarnings("unchecked") + public void callbackInvokeAndWhenLambdaMismatchOnDifferentArgumentShouldSwallowException() { + GenericMultiArgFactory callbackInstance = (n, s, b) -> { + fail("Should not get here"); + return 123; + }; + String argument = "foo"; + InvocationResult result = LambdaSafe + .callback(GenericMultiArgFactory.class, callbackInstance, argument) + .invokeAnd((c) -> c.handle(1, argument, false)); + assertThat(result.hasResult()).isFalse(); + } + + @Test + public void callbacksInvokeWhenNoGenericShouldInvokeCallbacks() { + NonGenericCallback callbackInstance = mock(NonGenericCallback.class); + String argument = "foo"; + LambdaSafe + .callbacks(NonGenericCallback.class, + Collections.singleton(callbackInstance), argument) + .invoke((c) -> c.handle(argument)); + verify(callbackInstance).handle(argument); + } + + @Test + @SuppressWarnings("unchecked") + public void callbacksInvokeWhenHasGenericShouldInvokeCallback() { + StringCallback callbackInstance = mock(StringCallback.class); + String argument = "foo"; + LambdaSafe.callbacks(GenericCallback.class, + Collections.singleton(callbackInstance), argument) + .invoke((c) -> c.handle(argument)); + verify(callbackInstance).handle(argument); + } + + @Test + @SuppressWarnings("unchecked") + public void callbacksInvokeWhenHasResolvableGenericMatchShouldInvokeCallback() { + StringBuilderCallback callbackInstance = mock(StringBuilderCallback.class); + StringBuilder argument = new StringBuilder("foo"); + LambdaSafe.callbacks(GenericCallback.class, + Collections.singleton(callbackInstance), argument) + .invoke((c) -> c.handle(argument)); + verify(callbackInstance).handle(argument); + } + + @Test + @SuppressWarnings("unchecked") + public void callbacksInvokeWhenHasResolvableGenericNonMatchShouldNotInvokeCallback() { + GenericCallback callbackInstance = mock(StringBuilderCallback.class); + String argument = "foo"; + LambdaSafe.callbacks(GenericCallback.class, + Collections.singleton(callbackInstance), argument) + .invoke((c) -> c.handle(null)); + verifyZeroInteractions(callbackInstance); + } + + @Test + @SuppressWarnings("unchecked") + public void callbacksInvokeWhenLambdaMismatchShouldSwallowException() { + GenericCallback callbackInstance = (s) -> { + fail("Should not get here"); + }; + String argument = "foo"; + LambdaSafe.callbacks(GenericCallback.class, + Collections.singleton(callbackInstance), argument) + .invoke((c) -> c.handle(argument)); + } + + @Test + @SuppressWarnings("unchecked") + public void callbacksInvokeWhenLambdaMismatchOnDifferentArgumentShouldSwallowException() { + GenericMultiArgCallback callbackInstance = (n, s, b) -> { + fail("Should not get here"); + }; + String argument = "foo"; + LambdaSafe + .callbacks(GenericMultiArgCallback.class, + Collections.singleton(callbackInstance), argument) + .invoke((c) -> c.handle(1, argument, false)); + } + + @Test + public void callbacksInvokeAndWhenNoGenericShouldReturnResult() { + NonGenericFactory callbackInstance = mock(NonGenericFactory.class); + String argument = "foo"; + given(callbackInstance.handle("foo")).willReturn(123); + Stream result = LambdaSafe + .callbacks(NonGenericFactory.class, + Collections.singleton(callbackInstance), argument) + .invokeAnd((c) -> c.handle(argument)); + assertThat(result).containsExactly(123); + } + + @Test + @SuppressWarnings("unchecked") + public void callbacksInvokeAndWhenHasGenericShouldReturnResult() { + StringFactory callbackInstance = mock(StringFactory.class); + String argument = "foo"; + given(callbackInstance.handle("foo")).willReturn(123); + Stream result = LambdaSafe.callbacks(GenericFactory.class, + Collections.singleton(callbackInstance), argument) + .invokeAnd((c) -> c.handle(argument)); + assertThat(result).containsExactly(123); + } + + @Test + @SuppressWarnings("unchecked") + public void callbacksInvokeAndWhenReturnNullShouldReturnResult() { + StringFactory callbackInstance = mock(StringFactory.class); + String argument = "foo"; + given(callbackInstance.handle("foo")).willReturn(null); + Stream result = LambdaSafe.callbacks(GenericFactory.class, + Collections.singleton(callbackInstance), argument) + .invokeAnd((c) -> c.handle(argument)); + assertThat(result).containsExactly((Integer) null); + } + + @Test + @SuppressWarnings("unchecked") + public void callbacksInvokeAndWhenHasResolvableGenericMatchShouldReturnResult() { + StringBuilderFactory callbackInstance = mock(StringBuilderFactory.class); + StringBuilder argument = new StringBuilder("foo"); + given(callbackInstance.handle(any(StringBuilder.class))).willReturn(123); + Stream result = LambdaSafe.callbacks(GenericFactory.class, + Collections.singleton(callbackInstance), argument) + .invokeAnd((c) -> c.handle(argument)); + assertThat(result).containsExactly(123); + } + + @Test + @SuppressWarnings("unchecked") + public void callbacksInvokeAndWhenHasResolvableGenericNonMatchShouldReturnNoResult() { + GenericFactory callbackInstance = mock(StringBuilderFactory.class); + String argument = "foo"; + Stream result = LambdaSafe.callbacks(GenericFactory.class, + Collections.singleton(callbackInstance), argument) + .invokeAnd((c) -> c.handle(argument)); + assertThat(result).isEmpty(); + } + + @Test + @SuppressWarnings("unchecked") + public void callbacksInvokeAndWhenLambdaMismatchShouldSwallowException() { + GenericFactory callbackInstance = (s) -> { + fail("Should not get here"); + return 123; + }; + String argument = "foo"; + Stream result = LambdaSafe.callbacks(GenericFactory.class, + Collections.singleton(callbackInstance), argument) + .invokeAnd((c) -> (c).handle(argument)); + assertThat(result).isEmpty(); + } + + @Test + @SuppressWarnings("unchecked") + public void callbacksInvokeAndWhenLambdaMismatchOnDifferentArgumentShouldSwallowException() { + GenericMultiArgFactory callbackInstance = (n, s, b) -> { + fail("Should not get here"); + return 123; + }; + String argument = "foo"; + Stream result = LambdaSafe + .callbacks(GenericMultiArgFactory.class, + Collections.singleton(callbackInstance), argument) + .invokeAnd((c) -> c.handle(1, argument, false)); + assertThat(result).isEmpty(); + } + + @Test + @SuppressWarnings("unchecked") + public void callbacksInvokeWhenMultipleShouldInvokeSuitable() { + List> callbackInstances = new ArrayList<>(); + GenericFactory callback1 = (s) -> 1; + GenericFactory callback2 = (s) -> 2; + GenericFactory callback3 = (s) -> 3; + StringFactory callback4 = mock(StringFactory.class); + given(callback4.handle(any(String.class))).willReturn(4); + StringBuilderFactory callback5 = mock(StringBuilderFactory.class); + given(callback5.handle(any(StringBuilder.class))).willReturn(5); + callbackInstances.add(callback1); + callbackInstances.add(callback2); + callbackInstances.add(callback3); + callbackInstances.add(callback4); + callbackInstances.add(callback5); + String argument = "foo"; + Stream result = LambdaSafe + .callbacks(GenericFactory.class, callbackInstances, argument) + .invokeAnd((c) -> c.handle(argument)); + assertThat(result).containsExactly(1, 2, 4); + } + + @Test + @SuppressWarnings("unchecked") + public void callbackWithFilterShouldUseFilter() { + GenericCallback callbackInstance = mock(StringBuilderCallback.class); + String argument = "foo"; + LambdaSafe.callback(GenericCallback.class, callbackInstance, argument) + .withFilter(Filter.allowAll()).invoke((c) -> c.handle(null)); + verify(callbackInstance).handle(null); + } + + @Test + @SuppressWarnings("unchecked") + public void callbackWithLoggerShouldUseLogger() { + Log logger = mock(Log.class); + given(logger.isDebugEnabled()).willReturn(true); + GenericCallback callbackInstance = (s) -> { + fail("Should not get here"); + }; + String argument = "foo"; + LambdaSafe.callback(GenericCallback.class, callbackInstance, argument) + .withLogger(logger).invoke((c) -> c.handle(argument)); + verify(logger).debug(contains("Non-matching CharSequence type for callback " + + "LambdaSafeTests.GenericCallback"), any(Throwable.class)); + } + + interface NonGenericCallback { + + void handle(String argument); + + } + + interface GenericCallback { + + void handle(T argument); + + } + + interface StringCallback extends GenericCallback { + + } + + interface StringBuilderCallback extends GenericCallback { + + } + + interface GenericMultiArgCallback { + + void handle(Integer numner, T argument, Boolean bool); + + } + + interface NonGenericFactory { + + Integer handle(String argument); + + } + + interface GenericFactory { + + Integer handle(T argument); + + } + + interface StringFactory extends GenericFactory { + + } + + interface StringBuilderFactory extends GenericFactory { + + } + + interface GenericMultiArgFactory { + + Integer handle(Integer numner, T argument, Boolean bool); + + } + +} From 3a12f98babbc2937ed39d34b02482c148384b8e5 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Sun, 28 Jan 2018 23:55:37 -0800 Subject: [PATCH 2/2] Migrate callbacks to LambdaSafe util Migrate existing code to the new `LambaSafe` callback handler. Closes gh-11584 --- .../annotation/EndpointDiscoverer.java | 31 ++++------- .../metrics/cache/CacheMetricsRegistrar.java | 49 ++++------------- .../cache/CacheManagerCustomizers.java | 35 ++---------- .../TransactionManagerCustomizers.java | 45 ++++------------ ...verFactoryCustomizerBeanPostProcessor.java | 54 +++---------------- 5 files changed, 43 insertions(+), 171 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/EndpointDiscoverer.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/EndpointDiscoverer.java index 9f51497a6c..8648478e23 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/EndpointDiscoverer.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/EndpointDiscoverer.java @@ -26,12 +26,10 @@ import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.boot.actuate.endpoint.EndpointFilter; @@ -41,6 +39,7 @@ import org.springframework.boot.actuate.endpoint.Operation; import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker; import org.springframework.boot.actuate.endpoint.invoke.OperationInvokerAdvisor; import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper; +import org.springframework.boot.util.LambdaSafe; import org.springframework.context.ApplicationContext; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotatedElementUtils; @@ -66,8 +65,6 @@ import org.springframework.util.StringUtils; public abstract class EndpointDiscoverer, O extends Operation> implements EndpointsSupplier { - private static final Log logger = LogFactory.getLog(EndpointDiscoverer.class); - private final ApplicationContext applicationContext; private final Collection> filters; @@ -313,23 +310,15 @@ public abstract class EndpointDiscoverer, O exten return isFilterMatch(filter, getFilterEndpoint(endpointBean)); } + @SuppressWarnings("unchecked") private boolean isFilterMatch(EndpointFilter filter, E endpoint) { - try { - return filter.match(endpoint); - } - catch (ClassCastException ex) { - String msg = ex.getMessage(); - if (msg == null || msg.startsWith(endpoint.getClass().getName())) { - // Possibly a lambda-defined EndpointFilter which we could not resolve the - // generic EndpointInfo type for - if (logger.isDebugEnabled()) { - logger.debug("Non-matching Endpoint for EndpointFilter: " + filter, - ex); - } - return false; - } - throw ex; - } + return LambdaSafe.callback(EndpointFilter.class, filter, endpoint) + .withLogger(EndpointDiscoverer.class).invokeAnd((f) -> f.match(endpoint)) + .get(); + } + + public void doIt(Function x) { + } private E getFilterEndpoint(EndpointBean endpointBean) { diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/cache/CacheMetricsRegistrar.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/cache/CacheMetricsRegistrar.java index f2b71759a2..bf00ec205e 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/cache/CacheMetricsRegistrar.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/cache/CacheMetricsRegistrar.java @@ -17,16 +17,15 @@ package org.springframework.boot.actuate.metrics.cache; import java.util.Collection; +import java.util.Objects; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Tag; import io.micrometer.core.instrument.Tags; import io.micrometer.core.instrument.binder.MeterBinder; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; +import org.springframework.boot.util.LambdaSafe; import org.springframework.cache.Cache; -import org.springframework.core.ResolvableType; /** * Register supported {@link Cache} to a {@link MeterRegistry}. @@ -36,8 +35,6 @@ import org.springframework.core.ResolvableType; */ public class CacheMetricsRegistrar { - private static final Log logger = LogFactory.getLog(CacheMetricsRegistrar.class); - private final MeterRegistry registry; private final String metricName; @@ -74,41 +71,15 @@ public class CacheMetricsRegistrar { return false; } - @SuppressWarnings({ "unchecked", "rawtypes" }) + @SuppressWarnings({ "unchecked" }) private MeterBinder getMeterBinder(Cache cache, Tags tags) { - tags = tags.and(getAdditionalTags(cache)); - for (CacheMeterBinderProvider binderProvider : this.binderProviders) { - Class cacheType = ResolvableType - .forClass(CacheMeterBinderProvider.class, binderProvider.getClass()) - .resolveGeneric(); - if (cacheType.isInstance(cache)) { - try { - MeterBinder meterBinder = ((CacheMeterBinderProvider) binderProvider) - .getMeterBinder(cache, this.metricName, tags); - if (meterBinder != null) { - return meterBinder; - } - } - catch (ClassCastException ex) { - String msg = ex.getMessage(); - if (msg == null || msg.startsWith(cache.getClass().getName())) { - // Possibly a lambda-defined CacheMeterBinderProvider which we - // could not resolve the generic Cache type for - if (logger.isDebugEnabled()) { - logger.debug( - "Non-matching Cache type for CacheMeterBinderProvider: " - + binderProvider, - ex); - } - } - else { - throw ex; - } - } - } - - } - return null; + Tags cacheTags = tags.and(getAdditionalTags(cache)); + return LambdaSafe + .callbacks(CacheMeterBinderProvider.class, this.binderProviders, cache) + .withLogger(CacheMetricsRegistrar.class) + .invokeAnd((binderProvider) -> binderProvider.getMeterBinder(cache, + this.metricName, cacheTags)) + .filter(Objects::nonNull).findFirst().orElse(null); } /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheManagerCustomizers.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheManagerCustomizers.java index 489aebd600..acca87ddb9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheManagerCustomizers.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheManagerCustomizers.java @@ -20,11 +20,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - +import org.springframework.boot.util.LambdaSafe; import org.springframework.cache.CacheManager; -import org.springframework.core.ResolvableType; /** * Invokes the available {@link CacheManagerCustomizer} instances in the context for a @@ -35,8 +32,6 @@ import org.springframework.core.ResolvableType; */ public class CacheManagerCustomizers { - private static final Log logger = LogFactory.getLog(CacheManagerCustomizers.class); - private final List> customizers; public CacheManagerCustomizers( @@ -53,32 +48,12 @@ public class CacheManagerCustomizers { * @param cacheManager the cache manager to customize * @return the cache manager */ + @SuppressWarnings("unchecked") public T customize(T cacheManager) { - for (CacheManagerCustomizer customizer : this.customizers) { - Class generic = ResolvableType - .forClass(CacheManagerCustomizer.class, customizer.getClass()) - .resolveGeneric(); - if (generic.isInstance(cacheManager)) { - customize(cacheManager, customizer); - } - } + LambdaSafe.callbacks(CacheManagerCustomizer.class, this.customizers, cacheManager) + .withLogger(CacheManagerCustomizers.class) + .invoke((customizer) -> customizer.customize(cacheManager)); return cacheManager; } - @SuppressWarnings({ "unchecked", "rawtypes" }) - private void customize(CacheManager cacheManager, CacheManagerCustomizer customizer) { - try { - customizer.customize(cacheManager); - } - catch (ClassCastException ex) { - // Possibly a lambda-defined customizer which we could not resolve the generic - // cache manager type for - if (logger.isDebugEnabled()) { - logger.debug( - "Non-matching cache manager type for customizer: " + customizer, - ex); - } - } - } - } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizers.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizers.java index 65b54868b1..4108f62a68 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizers.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizers.java @@ -18,12 +18,10 @@ package org.springframework.boot.autoconfigure.transaction; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.core.ResolvableType; +import org.springframework.boot.util.LambdaSafe; import org.springframework.transaction.PlatformTransactionManager; /** @@ -34,44 +32,21 @@ import org.springframework.transaction.PlatformTransactionManager; */ public class TransactionManagerCustomizers { - private static final Log logger = LogFactory - .getLog(TransactionManagerCustomizers.class); - private final List> customizers; public TransactionManagerCustomizers( Collection> customizers) { - this.customizers = (customizers == null ? null : new ArrayList<>(customizers)); + this.customizers = (customizers == null ? Collections.emptyList() + : new ArrayList<>(customizers)); } + @SuppressWarnings("unchecked") public void customize(PlatformTransactionManager transactionManager) { - if (this.customizers != null) { - for (PlatformTransactionManagerCustomizer customizer : this.customizers) { - Class generic = ResolvableType - .forClass(PlatformTransactionManagerCustomizer.class, - customizer.getClass()) - .resolveGeneric(); - if (generic.isInstance(transactionManager)) { - customize(transactionManager, customizer); - } - } - } - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - private void customize(PlatformTransactionManager transactionManager, - PlatformTransactionManagerCustomizer customizer) { - try { - customizer.customize(transactionManager); - } - catch (ClassCastException ex) { - // Possibly a lambda-defined customizer which we could not resolve the generic - // transaction manager type for - if (logger.isDebugEnabled()) { - logger.debug("Non-matching transaction manager type for customizer: " - + customizer, ex); - } - } + LambdaSafe + .callbacks(PlatformTransactionManagerCustomizer.class, this.customizers, + transactionManager) + .withLogger(TransactionManagerCustomizers.class) + .invoke((customizer) -> customizer.customize(transactionManager)); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServerFactoryCustomizerBeanPostProcessor.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServerFactoryCustomizerBeanPostProcessor.java index 5986f91a04..2188450283 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServerFactoryCustomizerBeanPostProcessor.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServerFactoryCustomizerBeanPostProcessor.java @@ -21,15 +21,12 @@ import java.util.Collection; import java.util.Collections; import java.util.List; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.config.BeanPostProcessor; -import org.springframework.core.ResolvableType; +import org.springframework.boot.util.LambdaSafe; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.util.Assert; @@ -45,9 +42,6 @@ import org.springframework.util.Assert; public class WebServerFactoryCustomizerBeanPostProcessor implements BeanPostProcessor, BeanFactoryAware { - private static final Log logger = LogFactory - .getLog(WebServerFactoryCustomizerBeanPostProcessor.class); - private ListableBeanFactory beanFactory; private List> customizers; @@ -75,45 +69,13 @@ public class WebServerFactoryCustomizerBeanPostProcessor return bean; } - private void postProcessBeforeInitialization(WebServerFactory bean) { - for (WebServerFactoryCustomizer customizer : getCustomizers()) { - Class type = ResolvableType - .forClass(WebServerFactoryCustomizer.class, customizer.getClass()) - .getGeneric().resolve(WebServerFactory.class); - if (type.isInstance(bean)) { - invokeCustomizer(customizer, bean); - } - } - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - private void invokeCustomizer(WebServerFactoryCustomizer customizer, - WebServerFactory webServerFactory) { - try { - customizer.customize(webServerFactory); - } - catch (ClassCastException ex) { - String msg = ex.getMessage(); - if (msg == null || msg.startsWith(webServerFactory.getClass().getName())) { - // Possibly a lambda-defined WebServerFactoryCustomizer which we could not - // resolve the - // generic WebServerFactory type for - logLambdaDebug(customizer, ex); - } - else { - throw ex; - } - } - } - - private void logLambdaDebug(WebServerFactoryCustomizer customizer, - ClassCastException ex) { - if (logger.isDebugEnabled()) { - logger.debug( - "Non-matching WebServerFactory type for WebServerFactoryCustomizer: " - + customizer, - ex); - } + @SuppressWarnings("unchecked") + private void postProcessBeforeInitialization(WebServerFactory webServerFactory) { + LambdaSafe + .callbacks(WebServerFactoryCustomizer.class, getCustomizers(), + webServerFactory) + .withLogger(WebServerFactoryCustomizerBeanPostProcessor.class) + .invoke((customizer) -> customizer.customize(webServerFactory)); } private Collection> getCustomizers() {