Fix deprecations around ListenableFuture (#3865)

* Fix deprecations around ListenableFuture

SF has deprecated a `ListenableFuture` and API around it

* Migrate to `CompletableFuture` everywhere a `ListenableFuture` has been used
* Suppress a deprecation for `ListenableFuture` keeping the functionality until the next version
* Resolve deprecations nad removals from the latest Spring for Apache Kafka
* Fix documentation for the `ListenableFuture` in favor of `CompletableFuture`

NOTE: the AMQP module is left as is until `ListenableFuture` deprecation is resolved in Spring AMQP

* * Restore some `ListenableFuture` test for messaging gateway
This commit is contained in:
Artem Bilan
2022-07-28 09:32:01 -04:00
committed by GitHub
parent 2e9beada0b
commit 5572c2161d
23 changed files with 196 additions and 259 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 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.
@@ -26,7 +26,6 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
@@ -49,7 +48,6 @@ import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.core.task.AsyncListenableTaskExecutor;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.core.task.support.TaskExecutorAdapter;
@@ -145,10 +143,6 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint
private boolean asyncExecutorExplicitlySet;
private Class<?> asyncSubmitType;
private Class<?> asyncSubmitListenableType;
private volatile boolean initialized;
private Map<String, GatewayMethodMetadata> methodMetadataMap;
@@ -468,15 +462,6 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint
new ProxyFactory(this.serviceInterface, this);
gatewayProxyFactory.addAdvice(new DefaultMethodInvokingMethodInterceptor());
this.serviceProxy = gatewayProxyFactory.getProxy(this.beanClassLoader);
if (this.asyncExecutor != null) {
Callable<String> task = () -> null;
Future<String> submitType = this.asyncExecutor.submit(task);
this.asyncSubmitType = submitType.getClass();
if (this.asyncExecutor instanceof AsyncListenableTaskExecutor) {
submitType = ((AsyncListenableTaskExecutor) this.asyncExecutor).submitListenable(task);
this.asyncSubmitListenableType = submitType.getClass();
}
}
this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(beanFactory);
this.initialized = true;
}
@@ -511,6 +496,7 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint
@Override
@Nullable
@SuppressWarnings("deprecation")
public Object invoke(final MethodInvocation invocation) throws Throwable { // NOSONAR
final Class<?> returnType;
MethodInvocationGateway gateway = this.gatewayMap.get(invocation.getMethod());
@@ -522,15 +508,16 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint
}
if (this.asyncExecutor != null && !Object.class.equals(returnType)) {
Invoker invoker = new Invoker(invocation);
if (returnType.isAssignableFrom(this.asyncSubmitType)) {
if (Future.class.equals(returnType)) {
return this.asyncExecutor.submit(invoker::get);
}
else if (returnType.isAssignableFrom(this.asyncSubmitListenableType)) {
return ((AsyncListenableTaskExecutor) this.asyncExecutor).submitListenable(invoker::get);
}
else if (CompletableFuture.class.equals(returnType)) { // exact
return CompletableFuture.supplyAsync(invoker, this.asyncExecutor);
}
else if (org.springframework.util.concurrent.ListenableFuture.class.equals(returnType)) {
return ((org.springframework.core.task.AsyncListenableTaskExecutor) this.asyncExecutor)
.submitListenable(invoker::get);
}
else if (Future.class.isAssignableFrom(returnType)) {
logger.debug(() -> "AsyncTaskExecutor submit*() return types are incompatible with the method return " +
"type; running on calling thread; the downstream flow must return the required Future: "

View File

@@ -51,7 +51,6 @@ import org.springframework.messaging.support.ErrorMessage;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.concurrent.ListenableFuture;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -106,9 +105,9 @@ public abstract class AbstractMessageProducingHandler extends AbstractMessageHan
}
/**
* Allow async replies. If the handler reply is a {@link ListenableFuture}, send
* the output when it is satisfied rather than sending the future as the result.
* Ignored for return types other than {@link ListenableFuture}.
* Allow async replies. If the handler reply is a {@link CompletableFuture} or {@link Publisher},
* send the output when it is satisfied rather than sending the future as the result.
* Ignored for return types other than {@link CompletableFuture} or {@link Publisher}.
* @param async true to allow.
* @since 4.3
*/
@@ -299,6 +298,7 @@ public abstract class AbstractMessageProducingHandler extends AbstractMessageHan
return replyChannel;
}
@SuppressWarnings("deprecation")
private void doProduceOutput(Message<?> requestMessage, MessageHeaders requestHeaders, Object reply,
@Nullable Object replyChannelArg) {
@@ -307,7 +307,7 @@ public abstract class AbstractMessageProducingHandler extends AbstractMessageHan
replyChannel = getOutputChannel();
}
if (this.async && (reply instanceof ListenableFuture<?>
if (this.async && (reply instanceof org.springframework.util.concurrent.ListenableFuture<?>
|| reply instanceof CompletableFuture<?>
|| reply instanceof Publisher<?>)) {
@@ -351,13 +351,14 @@ public abstract class AbstractMessageProducingHandler extends AbstractMessageHan
return builder;
}
@SuppressWarnings("deprecation")
private void asyncNonReactiveReply(Message<?> requestMessage, Object reply, @Nullable Object replyChannel) {
CompletableFuture<?> future;
if (reply instanceof CompletableFuture<?>) {
future = (CompletableFuture<?>) reply;
}
else if (reply instanceof ListenableFuture<?>) {
future = ((ListenableFuture<?>) reply).completable();
else if (reply instanceof org.springframework.util.concurrent.ListenableFuture<?>) {
future = ((org.springframework.util.concurrent.ListenableFuture<?>) reply).completable();
}
else {
Mono<?> reactiveReply;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2022 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.
@@ -17,10 +17,12 @@
package org.springframework.integration.config.annotation;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.concurrent.CompletableFuture;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
@@ -33,9 +35,7 @@ import org.springframework.messaging.MessageDeliveryException;
import org.springframework.messaging.PollableChannel;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.SettableListenableFuture;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
/**
* @author Dave Syer
@@ -44,7 +44,7 @@ import org.springframework.util.concurrent.SettableListenableFuture;
* @author Artem Bilan
* @author Yilin Wei
*/
@RunWith(SpringRunner.class)
@SpringJUnitConfig
@DirtiesContext
public class AnnotatedEndpointActivationTests {
@@ -72,7 +72,7 @@ public class AnnotatedEndpointActivationTests {
// them will get the message.
private static volatile int count = 0;
@Before
@BeforeEach
public void resetCount() {
count = 0;
}
@@ -108,11 +108,12 @@ public class AnnotatedEndpointActivationTests {
assertThat(count).isEqualTo(1);
}
@Test(expected = MessageDeliveryException.class)
@Test
@DirtiesContext
public void stopContext() {
applicationContext.stop();
this.input.send(new GenericMessage<>("foo"));
assertThatExceptionOfType(MessageDeliveryException.class)
.isThrownBy(() -> this.input.send(new GenericMessage<>("foo")));
}
@Test
@@ -159,9 +160,9 @@ public class AnnotatedEndpointActivationTests {
private static class AnnotatedEndpoint3 {
@ServiceActivator(inputChannel = "inputAsync", outputChannel = "outputAsync", async = "true")
public ListenableFuture<String> process(String message) {
SettableListenableFuture<String> future = new SettableListenableFuture<>();
future.set(message);
public CompletableFuture<String> process(String message) {
CompletableFuture<String> future = new CompletableFuture<>();
future.complete(message);
return future;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 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.
@@ -59,7 +59,6 @@ import org.springframework.messaging.PollableChannel;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
@@ -403,7 +402,7 @@ public class GatewayParserTests {
.setCorrelationId(request.getHeaders().getId()).build();
Object payload = null;
if (request.getPayload().equals("futureSync")) {
payload = new AsyncResult<Message<?>>(reply);
payload = CompletableFuture.completedFuture(reply);
}
else if (request.getPayload().equals("flowCompletable")) {
payload = CompletableFuture.<String>completedFuture("SYNC_COMPLETABLE");
@@ -448,7 +447,7 @@ public class GatewayParserTests {
}
@Override
@SuppressWarnings({ "rawtypes", "unchecked" })
@SuppressWarnings("unchecked")
public <T> Future<T> submit(Callable<T> task) {
try {
Future<?> result = super.submit(task);
@@ -462,7 +461,8 @@ public class GatewayParserTests {
modifiedMessage = MessageBuilder.fromMessage(message)
.setHeader("executor", this.beanName).build();
}
return new AsyncResult(modifiedMessage);
return (Future<T>) CompletableFuture.completedFuture(modifiedMessage);
}
catch (Exception e) {
throw new IllegalStateException("unexpected exception in testExecutor", e);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2022 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.
@@ -21,6 +21,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.Mockito.mock;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
@@ -39,8 +40,6 @@ import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.PollableChannel;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureCallback;
import reactor.core.publisher.Mono;
@@ -105,22 +104,15 @@ public class AsyncGatewayTests {
proxyFactory.setBeanFactory(mock(BeanFactory.class));
proxyFactory.afterPropertiesSet();
TestEchoService service = (TestEchoService) proxyFactory.getObject();
ListenableFuture<Message<?>> f = service.returnMessageListenable("foo");
CompletableFuture<Message<?>> f = service.returnMessageListenable("foo");
long start = System.currentTimeMillis();
final AtomicReference<Message<?>> result = new AtomicReference<>();
final CountDownLatch latch = new CountDownLatch(1);
f.addCallback(new ListenableFutureCallback<Message<?>>() {
@Override
public void onSuccess(Message<?> msg) {
result.set(msg);
f.whenComplete((message, throwable) -> {
if (throwable == null) {
result.set(message);
latch.countDown();
}
@Override
public void onFailure(Throwable t) {
}
});
assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue();
long elapsed = System.currentTimeMillis() - start;
@@ -300,7 +292,7 @@ public class AsyncGatewayTests {
String header = (String) input.getHeaders().get("method");
if (header != null && header.startsWith("returnCustomFuture")) {
reply = MessageBuilder.withPayload(new CustomFuture(payload,
(Thread) input.getHeaders().get("thread")))
(Thread) input.getHeaders().get("thread")))
.copyHeaders(input.getHeaders())
.build();
}
@@ -317,7 +309,7 @@ public class AsyncGatewayTests {
Future<?> returnSomething(String s);
ListenableFuture<Message<?>> returnMessageListenable(String s);
CompletableFuture<Message<?>> returnMessageListenable(String s);
@Gateway(headers = @GatewayHeader(name = "method", expression = "#gatewayMethod.name"))
CustomFuture returnCustomFuture(String s);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 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.
@@ -30,11 +30,11 @@ import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
@@ -81,13 +81,10 @@ import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Component;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureCallback;
/**
* @author Oleg Zhurakousky
@@ -399,6 +396,7 @@ public class GatewayInterfaceTests {
* performed the send() on gatewayThreadChannel.
*/
@Test
@SuppressWarnings("deprecation")
public void testExecs() throws Exception {
assertThat(TestUtils.getPropertyValue(execGatewayFB, "asyncExecutor")).isSameAs(exec);
assertThat(TestUtils.getPropertyValue(noExecGatewayFB, "asyncExecutor")).isNull();
@@ -414,25 +412,23 @@ public class GatewayInterfaceTests {
result = this.noExecGateway.test1(Thread.currentThread());
assertThat(result.get()).isEqualTo(Thread.currentThread());
ListenableFuture<Thread> result2 = this.execGateway.test2(Thread.currentThread());
CompletableFuture<Thread> result2 = this.execGateway.test2(Thread.currentThread());
final CountDownLatch latch = new CountDownLatch(1);
final AtomicReference<Thread> thread = new AtomicReference<>();
result2.addCallback(new ListenableFutureCallback<>() {
@Override
public void onSuccess(Thread result) {
thread.set(result);
result2.whenComplete((currentThread, throwable) -> {
if (throwable == null) {
latch.countDown();
}
@Override
public void onFailure(Throwable t) {
}
});
assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue();
assertThat(result2.get().getName()).startsWith("exec-");
org.springframework.util.concurrent.ListenableFuture<Thread> result3 =
this.execGateway.test3(Thread.currentThread());
final CountDownLatch latch1 = new CountDownLatch(1);
result3.addCallback(data -> latch1.countDown(), ex -> { });
assertThat(latch1.await(10, TimeUnit.SECONDS)).isTrue();
assertThat(result3.get().getName()).startsWith("exec-");
/*
@IntegrationComponentScan(useDefaultFilters = false,
includeFilters = @ComponentScan.Filter(TestMessagingGateway.class))
@@ -615,7 +611,7 @@ public class GatewayInterfaceTests {
Object payload;
if (Thread.currentThread().equals(message.getPayload())) {
// running on calling thread - need to return a Future.
payload = new AsyncResult<>(Thread.currentThread());
payload = CompletableFuture.completedFuture(Thread.currentThread());
}
else {
payload = Thread.currentThread();
@@ -683,7 +679,11 @@ public class GatewayInterfaceTests {
Future<Thread> test1(Thread caller);
@Gateway(requestChannel = "gatewayThreadChannel")
ListenableFuture<Thread> test2(Thread caller);
CompletableFuture<Thread> test2(Thread caller);
@Gateway(requestChannel = "gatewayThreadChannel")
@SuppressWarnings("deprecation")
org.springframework.util.concurrent.ListenableFuture<Thread> test3(Thread caller);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016-2020 the original author or authors.
* Copyright 2016-2022 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.
@@ -22,6 +22,7 @@ import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -46,7 +47,6 @@ import org.springframework.messaging.MessagingException;
import org.springframework.messaging.core.DestinationResolutionException;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.util.concurrent.SettableListenableFuture;
/**
* @author Gary Russell
@@ -80,19 +80,14 @@ public class AsyncHandlerTests {
@Override
protected Object handleRequestMessage(Message<?> requestMessage) {
final SettableListenableFuture<String> future = new SettableListenableFuture<>();
CompletableFuture<String> future = new CompletableFuture<>();
AsyncHandlerTests.this.executor.execute(() -> {
try {
latch.await(10, TimeUnit.SECONDS);
switch (whichTest) {
case 0:
future.set("reply");
break;
case 1:
future.setException(new RuntimeException("foo"));
break;
case 2:
future.setException(new MessagingException(requestMessage));
case 0 -> future.complete("reply");
case 1 -> future.completeExceptionally(new RuntimeException("foo"));
case 2 -> future.completeExceptionally(new MessagingException(requestMessage));
}
}
catch (InterruptedException e) {

View File

@@ -19,6 +19,7 @@ package org.springframework.integration.handler;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
@@ -43,8 +44,6 @@ import org.springframework.messaging.MessagingException;
import org.springframework.messaging.PollableChannel;
import org.springframework.messaging.support.ErrorMessage;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.SettableListenableFuture;
/**
* See INT-1688 for background.
@@ -210,7 +209,7 @@ public class ServiceActivatorDefaultFrameworkMethodTests {
this.asyncIn.send(message);
Message<?> reply = replyChannel.receive(0);
assertThat(reply).isNull();
this.asyncService.future.set(this.asyncService.payload.toUpperCase());
this.asyncService.future.complete(this.asyncService.payload.toUpperCase());
reply = replyChannel.receive(0);
assertThat(reply).isNotNull();
assertThat(reply.getPayload()).isEqualTo("TEST");
@@ -225,7 +224,7 @@ public class ServiceActivatorDefaultFrameworkMethodTests {
Message<?> message = MessageBuilder.withPayload("testing").setReplyChannel(replyChannel).build();
this.asyncIn.send(message);
assertThat(reply.get()).isNull();
this.asyncService.future.set(this.asyncService.payload.toUpperCase());
this.asyncService.future.complete(this.asyncService.payload.toUpperCase());
assertThat(reply.get()).isNotNull();
assertThat(reply.get().getPayload()).isEqualTo("TESTING");
}
@@ -235,7 +234,7 @@ public class ServiceActivatorDefaultFrameworkMethodTests {
QueueChannel errorChannel = new QueueChannel();
Message<?> message = MessageBuilder.withPayload("test").setErrorChannel(errorChannel).build();
this.asyncIn.send(message);
this.asyncService.future.setException(new RuntimeException("intended"));
this.asyncService.future.completeExceptionally(new RuntimeException("intended"));
Message<?> error = errorChannel.receive(0);
assertThat(error).isNotNull();
assertThat(error).isInstanceOf(ErrorMessage.class);
@@ -249,7 +248,7 @@ public class ServiceActivatorDefaultFrameworkMethodTests {
public void testAsyncErrorNoHeader() {
Message<?> message = MessageBuilder.withPayload("test").build();
this.asyncIn.send(message);
this.asyncService.future.setException(new RuntimeException("intended"));
this.asyncService.future.completeExceptionally(new RuntimeException("intended"));
Message<?> error = this.errorChannel.receive(0);
assertThat(error).isNotNull();
assertThat(error).isInstanceOf(ErrorMessage.class);
@@ -326,13 +325,13 @@ public class ServiceActivatorDefaultFrameworkMethodTests {
private static class AsyncService {
private volatile SettableListenableFuture<String> future;
private volatile CompletableFuture<String> future;
private volatile String payload;
@SuppressWarnings("unused")
public ListenableFuture<String> process(String payload) {
this.future = new SettableListenableFuture<>();
public CompletableFuture<String> process(String payload) {
this.future = new CompletableFuture<>();
this.payload = payload;
return this.future;
}