Skip to content

Commit 58fd706

Browse files
authored
Load HTTP implementations from classpath based on priority order (#4774)
1 parent 39c8972 commit 58fd706

File tree

3 files changed

+103
-39
lines changed

3 files changed

+103
-39
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "feature",
3+
"category": "AWS SDK for Java v2",
4+
"contributor": "",
5+
"description": "If an HTTP client or HTTP client builder is not specified explicitly on the SDK client AND there are multiple HTTP client implementations on the classpath, the SDK will choose the one that has the highest priority instead of throwing exception. ApacheHttpClient and NettyNioAsyncHttpClient have the highest priority in sync and async SDK client respectively for now."
6+
}

core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/ClasspathSdkHttpServiceProvider.java

Lines changed: 55 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -15,84 +15,106 @@
1515

1616
package software.amazon.awssdk.core.internal.http.loader;
1717

18-
import java.util.List;
18+
import java.util.Comparator;
19+
import java.util.Map;
1920
import java.util.Optional;
21+
import java.util.PriorityQueue;
22+
import java.util.Queue;
2023
import java.util.ServiceLoader;
21-
import java.util.stream.Collectors;
22-
import java.util.stream.StreamSupport;
24+
import java.util.StringJoiner;
2325
import software.amazon.awssdk.annotations.SdkInternalApi;
2426
import software.amazon.awssdk.annotations.SdkTestInternalApi;
25-
import software.amazon.awssdk.core.SdkSystemSetting;
26-
import software.amazon.awssdk.core.exception.SdkClientException;
2727
import software.amazon.awssdk.http.SdkHttpService;
2828
import software.amazon.awssdk.http.async.SdkAsyncHttpService;
29-
import software.amazon.awssdk.utils.SystemSetting;
29+
import software.amazon.awssdk.utils.ImmutableMap;
30+
import software.amazon.awssdk.utils.Logger;
3031

3132
/**
3233
* {@link SdkHttpServiceProvider} implementation that uses {@link ServiceLoader} to find HTTP implementations on the
33-
* classpath. If more than one implementation is found on the classpath then an exception is thrown.
34+
* classpath. If more than one implementation is found on the classpath, then the SDK will choose based on priority order.
3435
*/
3536
@SdkInternalApi
3637
final class ClasspathSdkHttpServiceProvider<T> implements SdkHttpServiceProvider<T> {
3738

39+
static final Map<String, Integer> SYNC_HTTP_SERVICES_PRIORITY =
40+
ImmutableMap.<String, Integer>builder()
41+
.put("software.amazon.awssdk.http.apache.ApacheSdkHttpService", 1)
42+
.put("software.amazon.awssdk.http.crt.AwsCrtSdkHttpService", 2)
43+
.put("software.amazon.awssdk.http.urlconnection.UrlConnectionSdkHttpService", 3)
44+
.build();
45+
46+
static final Map<String, Integer> ASYNC_HTTP_SERVICES_PRIORITY =
47+
ImmutableMap.<String, Integer>builder()
48+
.put("software.amazon.awssdk.http.nio.netty.NettySdkAsyncHttpService", 1)
49+
.put("software.amazon.awssdk.http.crt.AwsCrtSdkHttpService", 2)
50+
.build();
51+
52+
private static final Logger log = Logger.loggerFor(ClasspathSdkHttpServiceProvider.class);
53+
54+
private final Map<String, Integer> httpServicesPriority;
55+
3856
private final SdkServiceLoader serviceLoader;
39-
private final SystemSetting implSystemProperty;
4057
private final Class<T> serviceClass;
4158

4259
@SdkTestInternalApi
43-
ClasspathSdkHttpServiceProvider(SdkServiceLoader serviceLoader, SystemSetting implSystemProperty, Class<T> serviceClass) {
60+
ClasspathSdkHttpServiceProvider(SdkServiceLoader serviceLoader,
61+
Class<T> serviceClass,
62+
Map<String, Integer> httpServicesPriority) {
4463
this.serviceLoader = serviceLoader;
45-
this.implSystemProperty = implSystemProperty;
4664
this.serviceClass = serviceClass;
65+
this.httpServicesPriority = httpServicesPriority;
4766
}
4867

4968
@Override
5069
public Optional<T> loadService() {
70+
Queue<T> impls = new PriorityQueue<>(Comparator.comparingInt(o -> httpServicesPriority.getOrDefault(o.getClass(),
71+
Integer.MAX_VALUE)));
5172
Iterable<T> iterable = () -> serviceLoader.loadServices(serviceClass);
52-
List<T> impls = StreamSupport
53-
.stream(iterable.spliterator(), false)
54-
.collect(Collectors.toList());
73+
iterable.forEach(impl -> impls.add(impl));
5574

5675
if (impls.isEmpty()) {
5776
return Optional.empty();
5877
}
5978

60-
if (impls.size() > 1) {
61-
62-
String implText =
63-
impls.stream()
64-
.map(clazz -> clazz.getClass().getName())
65-
.collect(Collectors.joining(",", "[", "]"));
79+
log.debug(() -> logServices(impls));
80+
return Optional.of(impls.poll());
81+
}
6682

67-
throw SdkClientException.builder().message(
68-
String.format(
69-
"Multiple HTTP implementations were found on the classpath. To avoid non-deterministic loading " +
70-
"implementations, please explicitly provide an HTTP client via the client builders, set the %s " +
71-
"system property with the FQCN of the HTTP service to use as the default, or remove all but one " +
72-
"HTTP implementation from the classpath. The multiple implementations found were: %s",
73-
implSystemProperty.property(), implText))
74-
.build();
83+
private String logServices(Queue<T> impls) {
84+
StringJoiner joiner = new StringJoiner(",", "[", "]");
85+
int count = 0;
86+
for (T clazz : impls) {
87+
String name = clazz.getClass().getName();
88+
joiner.add(name);
89+
count++;
7590
}
91+
String implText = joiner.toString();
92+
T impl = impls.peek();
93+
String message = count == 1 ? "The HTTP implementation loaded is " + impl :
94+
String.format(
95+
"Multiple HTTP implementations were found on the classpath. The SDK will use %s since it has the "
96+
+ "highest. The multiple implementations found were: %s",
97+
impl,
98+
implText);
7699

77-
return impls.stream().findFirst();
100+
return message;
78101
}
79102

80103
/**
81104
* @return ClasspathSdkHttpServiceProvider that loads an {@link SdkHttpService} (sync) from the classpath.
82105
*/
83106
static SdkHttpServiceProvider<SdkHttpService> syncProvider() {
84107
return new ClasspathSdkHttpServiceProvider<>(SdkServiceLoader.INSTANCE,
85-
SdkSystemSetting.SYNC_HTTP_SERVICE_IMPL,
86-
SdkHttpService.class);
108+
SdkHttpService.class,
109+
SYNC_HTTP_SERVICES_PRIORITY);
87110
}
88111

89112
/**
90113
* @return ClasspathSdkHttpServiceProvider that loads an {@link SdkAsyncHttpService} (async) from the classpath.
91114
*/
92115
static SdkHttpServiceProvider<SdkAsyncHttpService> asyncProvider() {
93116
return new ClasspathSdkHttpServiceProvider<>(SdkServiceLoader.INSTANCE,
94-
SdkSystemSetting.ASYNC_HTTP_SERVICE_IMPL,
95-
SdkAsyncHttpService.class);
117+
SdkAsyncHttpService.class,
118+
ASYNC_HTTP_SERVICES_PRIORITY);
96119
}
97-
98120
}

core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/ClasspathSdkHttpServiceProviderTest.java

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import static org.assertj.core.api.Assertions.assertThat;
2020
import static org.mockito.Mockito.mock;
2121
import static org.mockito.Mockito.when;
22+
import static software.amazon.awssdk.core.internal.http.loader.ClasspathSdkHttpServiceProvider.ASYNC_HTTP_SERVICES_PRIORITY;
23+
import static software.amazon.awssdk.core.internal.http.loader.ClasspathSdkHttpServiceProvider.SYNC_HTTP_SERVICES_PRIORITY;
2224

2325
import java.util.Arrays;
2426
import java.util.Iterator;
@@ -30,6 +32,9 @@
3032
import software.amazon.awssdk.core.SdkSystemSetting;
3133
import software.amazon.awssdk.core.exception.SdkClientException;
3234
import software.amazon.awssdk.http.SdkHttpService;
35+
import software.amazon.awssdk.http.apache.ApacheSdkHttpService;
36+
import software.amazon.awssdk.http.async.SdkAsyncHttpService;
37+
import software.amazon.awssdk.http.nio.netty.NettySdkAsyncHttpService;
3338

3439
@RunWith(MockitoJUnitRunner.class)
3540
public class ClasspathSdkHttpServiceProviderTest {
@@ -39,11 +44,17 @@ public class ClasspathSdkHttpServiceProviderTest {
3944

4045
private SdkHttpServiceProvider<SdkHttpService> provider;
4146

47+
private SdkHttpServiceProvider<SdkAsyncHttpService> asyncProvider;
48+
4249
@Before
4350
public void setup() {
4451
provider = new ClasspathSdkHttpServiceProvider<>(serviceLoader,
45-
SdkSystemSetting.SYNC_HTTP_SERVICE_IMPL,
46-
SdkHttpService.class);
52+
SdkHttpService.class,
53+
SYNC_HTTP_SERVICES_PRIORITY);
54+
55+
asyncProvider = new ClasspathSdkHttpServiceProvider<>(serviceLoader,
56+
SdkAsyncHttpService.class,
57+
ASYNC_HTTP_SERVICES_PRIORITY);
4758
}
4859

4960
@Test
@@ -60,11 +71,36 @@ public void oneImplementationsFound_ReturnsFulfilledOptional() {
6071
assertThat(provider.loadService()).isPresent();
6172
}
6273

63-
@Test(expected = SdkClientException.class)
64-
public void multipleImplementationsFound_ThrowsException() {
74+
@Test
75+
public void multipleSyncImplementationsFound_ReturnHighestPriority() {
76+
ApacheSdkHttpService apacheSdkHttpService = new ApacheSdkHttpService();
77+
SdkHttpService mock = mock(SdkHttpService.class);
78+
79+
when(serviceLoader.loadServices(SdkHttpService.class))
80+
.thenReturn(iteratorOf(apacheSdkHttpService, mock));
81+
assertThat(provider.loadService()).contains(apacheSdkHttpService);
82+
83+
SdkHttpService mock1 = mock(SdkHttpService.class);
84+
SdkHttpService mock2 = mock(SdkHttpService.class);
6585
when(serviceLoader.loadServices(SdkHttpService.class))
66-
.thenReturn(iteratorOf(mock(SdkHttpService.class), mock(SdkHttpService.class)));
67-
provider.loadService();
86+
.thenReturn(iteratorOf(mock1, mock2));
87+
assertThat(provider.loadService()).contains(mock1);
88+
}
89+
90+
@Test
91+
public void multipleAsyncImplementationsFound_ReturnHighestPriority() {
92+
NettySdkAsyncHttpService netty = new NettySdkAsyncHttpService();
93+
SdkAsyncHttpService mock = mock(SdkAsyncHttpService.class);
94+
95+
when(serviceLoader.loadServices(SdkAsyncHttpService.class))
96+
.thenReturn(iteratorOf(netty, mock));
97+
assertThat(asyncProvider.loadService()).contains(netty);
98+
99+
SdkAsyncHttpService mock1 = mock(SdkAsyncHttpService.class);
100+
SdkAsyncHttpService mock2 = mock(SdkAsyncHttpService.class);
101+
when(serviceLoader.loadServices(SdkAsyncHttpService.class))
102+
.thenReturn(iteratorOf(mock1, mock2));
103+
assertThat(asyncProvider.loadService()).contains(mock1);
68104
}
69105

70106
@SafeVarargs

0 commit comments

Comments
 (0)