Skip to content

Commit 53e70e8

Browse files
authored
Switch to using the Java HTTP client (#255)
* Switch to using the Java HTTP client There are 2 reasons to make that change: 1. reduce the number of dependencies of the client, in order to remove the dependency on Netty 2. the JDK HTTP client doesn't need to be closed The 2d point deserves a bit more information. The test resources client factory is called by the test resources client, and in general, there should be at most a couple calls per build. In practice, this isn't the case: in Micronaut Data for example, there are many calls because tests spawn their own application context. One issue is that Micronaut Core will _not_ call `#close` on a `PropertySourceLoader` once an application context is closed, even if that loader implements `Closeable`. It is arguable if it's a bug or not, but in practice, it means that there is no way for Micronaut Test Resources to proactively close a client. Switching to the JDK HTTP Client solves this problem because there is no need to call the close method anymore. This PR doesn't change the communication protocol between the client and the server because core lacks some support to make it easily possible. The consequence is that we're still adding a dependency on `micronaut-json-core`, which also means that if there's no JSON mapper on classpath (either Jackson or Serde), then test resources won't work. I am unsure but I think that was already the case before, even with the dependency on HTTP Netty, unless that was bringing a mapper on classpath transparently. * Fix client String decoding
1 parent 7901e66 commit 53e70e8

File tree

8 files changed

+116
-73
lines changed

8 files changed

+116
-73
lines changed

config/accepted-api-changes.json

+5
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,10 @@
33
"type": "io.micronaut.testresources.redis.RedisTestResourceProvider",
44
"member": "Field REDIS_PORT",
55
"reason": "We're now using Redis URI"
6+
},
7+
{
8+
"type": "io.micronaut.testresources.client.DefaultTestResourcesClient",
9+
"member": "Constructor io.micronaut.testresources.client.DefaultTestResourcesClient(io.micronaut.http.client.HttpClient,java.lang.String)",
10+
"reason": "This type is internal API"
611
}
712
]

test-resources-client/build.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ and provide their value on demand.
99
"""
1010

1111
dependencies {
12-
api(mn.micronaut.http.client)
12+
api(mn.micronaut.json.core)
1313
api(project(':micronaut-test-resources-core'))
1414

1515
testRuntimeOnly(mn.micronaut.http.server.netty)

test-resources-client/src/main/java/io/micronaut/testresources/client/DefaultTestResourcesClient.java

+86-38
Original file line numberDiff line numberDiff line change
@@ -15,46 +15,57 @@
1515
*/
1616
package io.micronaut.testresources.client;
1717

18+
import io.micronaut.core.annotation.Internal;
1819
import io.micronaut.core.annotation.Nullable;
1920
import io.micronaut.core.type.Argument;
20-
import io.micronaut.http.HttpRequest;
21-
import io.micronaut.http.MutableHttpRequest;
22-
import io.micronaut.http.client.BlockingHttpClient;
23-
import io.micronaut.http.client.HttpClient;
24-
import io.micronaut.http.uri.UriBuilder;
21+
import io.micronaut.json.JsonMapper;
2522

23+
import java.io.IOException;
2624
import java.net.URI;
25+
import java.net.URISyntaxException;
26+
import java.net.http.HttpClient;
27+
import java.net.http.HttpRequest;
28+
import java.net.http.HttpResponse;
29+
import java.time.Duration;
2730
import java.util.Collection;
2831
import java.util.HashMap;
2932
import java.util.List;
3033
import java.util.Map;
3134
import java.util.Optional;
32-
33-
import static io.micronaut.http.HttpHeaders.ACCEPT;
34-
import static io.micronaut.http.HttpHeaders.USER_AGENT;
35+
import java.util.function.Consumer;
3536

3637
/**
3738
* A simple implementation of the test resources client.
3839
*/
3940
@SuppressWarnings("unchecked")
41+
@Internal
4042
public class DefaultTestResourcesClient implements TestResourcesClient {
4143
public static final String ACCESS_TOKEN = "Access-Token";
4244

43-
private static final URI RESOLVABLE_PROPERTIES_URI = UriBuilder.of("/").path("/list").build();
44-
private static final URI REQUIRED_PROPERTIES_URI = UriBuilder.of("/").path("/requirements/expr").build();
45-
private static final URI REQUIRED_PROPERTY_ENTRIES_URI = UriBuilder.of("/").path("/requirements/entries").build();
46-
private static final URI CLOSE_ALL_URI = UriBuilder.of("/").path("/close/all").build();
47-
private static final URI CLOSE_URI = UriBuilder.of("/").path("/close").build();
48-
private static final URI RESOLVE_URI = UriBuilder.of("/").path("/resolve").build();
49-
private static final Argument<List<String>> LIST_OF_STRING = Argument.listOf(String.class);
45+
private static final String RESOLVABLE_PROPERTIES_URI = "/list";
46+
private static final String REQUIRED_PROPERTIES_URI = "/requirements/expr";
47+
private static final String REQUIRED_PROPERTY_ENTRIES_URI = "/requirements/entries";
48+
private static final String CLOSE_ALL_URI = "/close/all";
49+
private static final String CLOSE_URI = "/close";
50+
private static final String RESOLVE_URI = "/resolve";
51+
private static final Argument<List<String>> LIST_OF_STRING = Argument.LIST_OF_STRING;
52+
private static final Argument<String> STRING = Argument.STRING;
53+
private static final Argument<Boolean> BOOLEAN = Argument.BOOLEAN;
5054

51-
private final BlockingHttpClient client;
55+
private final JsonMapper jsonMapper;
56+
private final String baseUri;
57+
private final HttpClient client;
5258

5359
private final String accessToken;
60+
private final Duration clientTimeout;
5461

55-
public DefaultTestResourcesClient(HttpClient client, String accessToken) {
56-
this.client = client.toBlocking();
62+
public DefaultTestResourcesClient(String baseUri, String accessToken, int clientReadTimeout) {
63+
this.baseUri = baseUri;
64+
clientTimeout = Duration.ofSeconds(clientReadTimeout);
65+
this.client = HttpClient.newBuilder()
66+
.build();
5767
this.accessToken = accessToken;
68+
this.jsonMapper = JsonMapper.createDefault();
5869
}
5970

6071
@Override
@@ -63,8 +74,9 @@ public List<String> getResolvableProperties(Map<String, Collection<String>> prop
6374
Map<String, Object> properties = new HashMap<>();
6475
properties.put("propertyEntries", propertyEntries);
6576
properties.put("testResourcesConfig", testResourcesConfig);
66-
HttpRequest<?> req = configure(HttpRequest.POST(RESOLVABLE_PROPERTIES_URI, properties));
67-
return client.retrieve(req, LIST_OF_STRING);
77+
return request(RESOLVABLE_PROPERTIES_URI, LIST_OF_STRING,
78+
r -> POST(r, properties)
79+
);
6880
}
6981

7082
@Override
@@ -73,45 +85,81 @@ public Optional<String> resolve(String name, Map<String, Object> properties, Map
7385
params.put("name", name);
7486
params.put("properties", properties);
7587
params.put("testResourcesConfig", testResourcesConfiguration);
76-
HttpRequest<?> req = configure(HttpRequest.POST(RESOLVE_URI, params));
77-
return Optional.ofNullable(client.retrieve(req));
88+
return Optional.ofNullable(request(RESOLVE_URI, STRING, r -> POST(r, params)));
7889
}
7990

8091
@Override
8192
public List<String> getRequiredProperties(String expression) {
82-
return doGet(REQUIRED_PROPERTIES_URI, LIST_OF_STRING, expression);
93+
return request(REQUIRED_PROPERTIES_URI + "/" + expression, LIST_OF_STRING, this::GET);
8394
}
8495

8596
@Override
8697
public List<String> getRequiredPropertyEntries() {
87-
return doGet(REQUIRED_PROPERTY_ENTRIES_URI, LIST_OF_STRING);
98+
return request(REQUIRED_PROPERTY_ENTRIES_URI, LIST_OF_STRING, this::GET);
8899
}
89100

90101
@Override
91102
public boolean closeAll() {
92-
return doGet(CLOSE_ALL_URI, Argument.BOOLEAN);
103+
return request(CLOSE_ALL_URI, BOOLEAN, this::GET);
93104
}
94105

95106
@Override
96107
public boolean closeScope(@Nullable String id) {
97-
return doGet(CLOSE_URI, Argument.BOOLEAN, id);
108+
return request(CLOSE_URI + "/" + id, BOOLEAN, this::GET);
98109
}
99110

100-
private <T> T doGet(URI uri, Argument<T> clazz, String... pathElements) {
101-
UriBuilder builder = UriBuilder.of(uri);
102-
for (String param : pathElements) {
103-
builder = builder.path(param);
104-
}
105-
MutableHttpRequest<?> req = configure(HttpRequest.GET(builder.build()));
106-
return client.retrieve(req, clazz);
111+
@SuppressWarnings({"java:S100", "checkstyle:MethodName"})
112+
private void POST(HttpRequest.Builder request, Object o) {
113+
request.POST(HttpRequest.BodyPublishers.ofByteArray(writeValueAsBytes(o)));
114+
}
115+
116+
@SuppressWarnings({"java:S100", "checkstyle:MethodName"})
117+
private void GET(HttpRequest.Builder request) {
118+
request.GET();
107119
}
108120

109-
private <T> MutableHttpRequest<T> configure(MutableHttpRequest<T> request) {
110-
MutableHttpRequest<T> result = request.header(USER_AGENT, "Micronaut HTTP Client")
111-
.header(ACCEPT, "application/json");
121+
private <T> T request(String path, Argument<T> type, Consumer<? super HttpRequest.Builder> config) {
122+
var request = HttpRequest.newBuilder()
123+
.uri(uri(path))
124+
.timeout(clientTimeout);
125+
request = request.header("User-Agent", "Micronaut Test Resources Client")
126+
.header("Content-Type", "application/json")
127+
.header("Accept", "application/json");
112128
if (accessToken != null) {
113-
result = result.header(ACCESS_TOKEN, accessToken);
129+
request = request.header(ACCESS_TOKEN, accessToken);
130+
}
131+
config.accept(request);
132+
try {
133+
var response = client.send(request.build(), HttpResponse.BodyHandlers.ofString());
134+
var body = response.body();
135+
if (response.statusCode() == 200) {
136+
if (STRING.equalsType(type)) {
137+
return (T) body;
138+
}
139+
return jsonMapper.readValue(body, type);
140+
}
141+
return null;
142+
} catch (IOException e) {
143+
throw new TestResourcesException(e);
144+
} catch (InterruptedException e) {
145+
Thread.currentThread().interrupt();
146+
throw new TestResourcesException(e);
147+
}
148+
}
149+
150+
private URI uri(String path) {
151+
try {
152+
return new URI(baseUri + path);
153+
} catch (URISyntaxException e) {
154+
throw new TestResourcesException(e);
155+
}
156+
}
157+
158+
private byte[] writeValueAsBytes(Object o) {
159+
try {
160+
return jsonMapper.writeValueAsBytes(o);
161+
} catch (IOException e) {
162+
throw new TestResourcesException(e);
114163
}
115-
return result;
116164
}
117165
}

test-resources-client/src/main/java/io/micronaut/testresources/client/TestResourcesClientFactory.java

+17-26
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,11 @@
1616
package io.micronaut.testresources.client;
1717

1818
import io.micronaut.context.ApplicationContext;
19-
import io.micronaut.http.client.DefaultHttpClientConfiguration;
20-
import io.micronaut.http.client.HttpClient;
21-
import io.micronaut.http.client.HttpClientConfiguration;
2219

2320
import java.io.IOException;
2421
import java.io.InputStream;
25-
import java.net.MalformedURLException;
22+
import java.lang.ref.WeakReference;
2623
import java.net.URL;
27-
import java.time.Duration;
28-
import java.time.temporal.ChronoUnit;
2924
import java.util.Optional;
3025
import java.util.Properties;
3126

@@ -39,12 +34,15 @@
3934
public final class TestResourcesClientFactory {
4035
private static final String DEFAULT_TIMEOUT_SECONDS = "60";
4136

37+
private static WeakReference<TestResourcesClient> cachedClient;
38+
4239
private TestResourcesClientFactory() {
4340

4441
}
4542

4643
/**
4744
* Creates a new test resources client configured via a properties file.
45+
*
4846
* @param configFile the URL to the configuration properties file.
4947
* @return a new test resources client.
5048
*/
@@ -55,44 +53,37 @@ public static TestResourcesClient configuredAt(URL configFile) {
5553
} catch (IOException e) {
5654
throw new TestResourcesException(e);
5755
}
58-
try {
59-
String serverUri = props.getProperty(TestResourcesClient.SERVER_URI);
60-
String accessToken = props.getProperty(TestResourcesClient.ACCESS_TOKEN);
61-
int clientReadTimeout = Integer.parseInt(props.getProperty(TestResourcesClient.CLIENT_READ_TIMEOUT, DEFAULT_TIMEOUT_SECONDS));
62-
HttpClientConfiguration config = new DefaultHttpClientConfiguration();
63-
config.setReadTimeout(Duration.of(clientReadTimeout, ChronoUnit.SECONDS));
64-
HttpClient client = HttpClient.create(new URL(serverUri), config);
65-
return new DefaultTestResourcesClient(client, accessToken);
66-
} catch (MalformedURLException e) {
67-
throw new TestResourcesException(e);
68-
}
56+
String serverUri = props.getProperty(TestResourcesClient.SERVER_URI);
57+
String accessToken = props.getProperty(TestResourcesClient.ACCESS_TOKEN);
58+
int clientReadTimeout = Integer.parseInt(props.getProperty(TestResourcesClient.CLIENT_READ_TIMEOUT, DEFAULT_TIMEOUT_SECONDS));
59+
return new DefaultTestResourcesClient(serverUri, accessToken, clientReadTimeout);
6960
}
7061

7162
/**
7263
* Creates a new test resources client configured via system properties.
64+
*
7365
* @return a new test resources client, if system properties were found.
7466
*/
7567
public static Optional<TestResourcesClient> fromSystemProperties() {
68+
var client = cachedClient != null ? cachedClient.get() : null;
69+
if (client != null) {
70+
return Optional.of(client);
71+
}
7672
String serverUri = System.getProperty(ConfigFinder.systemPropertyNameOf(TestResourcesClient.SERVER_URI));
7773
if (serverUri != null) {
7874
String accessToken = System.getProperty(ConfigFinder.systemPropertyNameOf(TestResourcesClient.ACCESS_TOKEN));
7975
String clientTimeoutString = System.getProperty(ConfigFinder.systemPropertyNameOf(TestResourcesClient.CLIENT_READ_TIMEOUT), DEFAULT_TIMEOUT_SECONDS);
8076
int clientReadTimeout = Integer.parseInt(clientTimeoutString);
81-
HttpClientConfiguration config = new DefaultHttpClientConfiguration();
82-
config.setReadTimeout(Duration.of(clientReadTimeout, ChronoUnit.SECONDS));
83-
HttpClient client;
84-
try {
85-
client = HttpClient.create(new URL(serverUri), config);
86-
} catch (MalformedURLException e) {
87-
return Optional.empty();
88-
}
89-
return Optional.of(new DefaultTestResourcesClient(client, accessToken));
77+
client = new DefaultTestResourcesClient(serverUri, accessToken, clientReadTimeout);
78+
cachedClient = new WeakReference<>(client);
79+
return Optional.of(client);
9080
}
9181
return Optional.empty();
9282
}
9383

9484
/**
9585
* Extracts the {@link TestResourcesClient} from the given {@link ApplicationContext}.
86+
*
9687
* @param context the application context
9788
* @return the test resources client
9889
*/

test-resources-client/src/main/java/io/micronaut/testresources/client/TestResourcesClientPropertyExpressionResolver.java

+1-7
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import io.micronaut.core.annotation.Nullable;
2121
import io.micronaut.core.convert.ConversionService;
2222
import io.micronaut.core.value.PropertyResolver;
23-
import io.micronaut.http.client.exceptions.HttpClientResponseException;
2423
import io.micronaut.testresources.core.LazyTestResourcesExpressionResolver;
2524
import io.micronaut.testresources.core.TestResourcesResolver;
2625
import org.slf4j.Logger;
@@ -116,12 +115,7 @@ public <T> Optional<T> resolve(PropertyResolver propertyResolver,
116115
}
117116

118117
private static Optional<String> callClient(String expression, TestResourcesClient client, Map<String, Object> props, Map<String, Object> properties) {
119-
try {
120-
return client.resolve(expression, props, properties);
121-
} catch (HttpClientResponseException ex) {
122-
LOGGER.debug("Test resources client failed to resolve expression '{}'", expression, ex);
123-
return Optional.empty();
124-
}
118+
return client.resolve(expression, props, properties);
125119
}
126120

127121
@Override

test-resources-client/src/main/java/io/micronaut/testresources/client/TestResourcesException.java

+4
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,8 @@ public class TestResourcesException extends RuntimeException {
2222
public TestResourcesException(Throwable cause) {
2323
super(cause);
2424
}
25+
26+
public TestResourcesException(String message) {
27+
super(message);
28+
}
2529
}

test-resources-client/src/test/groovy/io/micronaut/testresources/client/TestServer.groovy

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import io.micronaut.testresources.core.TestResourcesResolver
88

99
@Controller("/")
1010
@Requires(property = 'server', notEquals = 'false')
11-
class TestServer implements TestResourcesResolver {
11+
class TestServer implements TestResourcesResolver {
1212

1313
@Override
1414
@Post("/list")

test-resources-server/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ dependencies {
2424
runtimeOnly(mnSerde.micronaut.serde.jackson)
2525
runtimeOnly(mn.snakeyaml)
2626

27+
testImplementation(mn.micronaut.http.client)
2728
testImplementation(project(':micronaut-test-resources-client'))
2829
testRuntimeOnly(project(':micronaut-test-resources-kafka'))
2930

0 commit comments

Comments
 (0)