Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 40 additions & 12 deletions core/src/main/java/feign/AsyncFeign.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,23 +56,28 @@ public static <C> AsyncBuilder<C> asyncBuilder() {
return builder();
}

private static class LazyInitializedExecutorService {

private static final ExecutorService instance =
Executors.newCachedThreadPool(
r -> {
final Thread result = new Thread(r);
result.setDaemon(true);
return result;
});
/**
* Creates the default {@link ExecutorService} used when neither a custom {@link AsyncClient} nor
* a custom executor is provided. The executor is created per built client (instance scoped)
* rather than being held in a static singleton, so that it - together with its daemon threads -
* becomes eligible for garbage collection once the owning client is discarded. This avoids the
* {@code ClassLoader} leak that a static singleton causes on application redeployments inside
* servlet containers (see gh-3178).
*/
private static ExecutorService defaultExecutorService() {
return Executors.newCachedThreadPool(
r -> {
final Thread result = new Thread(r);
result.setDaemon(true);
return result;
});
}

public static class AsyncBuilder<C> extends BaseBuilder<AsyncBuilder<C>, AsyncFeign<C>> {

private AsyncContextSupplier<C> defaultContextSupplier = () -> null;
private AsyncClient<C> client =
new DefaultAsyncClient<>(
new DefaultClient(null, null), LazyInitializedExecutorService.instance);
private AsyncClient<C> client;
private ExecutorService executorService;
private MethodInfoResolver methodInfoResolver = MethodInfo::new;

@Deprecated
Expand All @@ -86,6 +91,28 @@ public AsyncBuilder<C> client(AsyncClient<C> client) {
return this;
}

/**
* Sets the {@link ExecutorService} used by the default {@link AsyncClient} to run the
* (blocking) underlying client calls. When provided, the caller owns the executor's lifecycle
* and is responsible for shutting it down. This is the recommended way to avoid the {@code
* ClassLoader} leak described in gh-3178: supply a managed, shut-downable executor instead of
* relying on the built-in default. Ignored when a custom {@link #client(AsyncClient)} is
* supplied.
*/
public AsyncBuilder<C> executorService(ExecutorService executorService) {
this.executorService = executorService;
return this;
}

private AsyncClient<C> resolveClient() {
if (client != null) {
return client;
}
final ExecutorService executor =
executorService != null ? executorService : defaultExecutorService();
return new DefaultAsyncClient<>(new DefaultClient(null, null), executor);
}

public AsyncBuilder<C> methodInfoResolver(MethodInfoResolver methodInfoResolver) {
this.methodInfoResolver = methodInfoResolver;
return this;
Expand Down Expand Up @@ -206,6 +233,7 @@ public AsyncBuilder<C> invocationHandlerFactory(

@Override
public AsyncFeign<C> internalBuild() {
final AsyncClient<C> client = resolveClient();
AsyncResponseHandler responseHandler =
(AsyncResponseHandler)
Capability.enrich(
Expand Down
37 changes: 37 additions & 0 deletions core/src/test/java/feign/AsyncFeignTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
import static org.assertj.core.data.MapEntry.entry;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
Expand Down Expand Up @@ -104,6 +108,39 @@ void postTemplateParamsResolve() throws Exception {
+ " \"password\"}");
}

@Test
void usesProvidedExecutorService() throws Exception {
server.enqueue(new MockResponse().setBody("foo"));

ExecutorService executorService = spy(Executors.newCachedThreadPool());
try {
TestInterfaceAsync api =
AsyncFeign.<Void>builder()
.decoder(new DefaultDecoder())
.executorService(executorService)
.target(TestInterfaceAsync.class, "http://localhost:" + server.getPort());

String result = api.post().join();

assertThat(result).isEqualTo("foo");
verify(executorService, atLeastOnce()).submit(any(Runnable.class));
} finally {
executorService.shutdownNow();
}
}

@Test
void defaultExecutorServiceStillExecutesRequests() throws Exception {
server.enqueue(new MockResponse().setBody("foo"));

TestInterfaceAsync api =
AsyncFeign.<Void>builder()
.decoder(new DefaultDecoder())
.target(TestInterfaceAsync.class, "http://localhost:" + server.getPort());

assertThat(api.post().join()).isEqualTo("foo");
}

@Test
void postFormParams() throws Exception {
server.enqueue(new MockResponse().setBody("foo"));
Expand Down