Skip to content

Commit d8171b4

Browse files
committed
spring-projectsGH-1419: Add RestTemplateNodeLocator
- also remove hard dependency on `spring-webflux` from `spring-rabbit-junit`.
1 parent c143c5b commit d8171b4

File tree

8 files changed

+204
-25
lines changed

8 files changed

+204
-25
lines changed

build.gradle

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ ext {
4444
assertkVersion = '0.24'
4545
awaitilityVersion = '4.2.0'
4646
commonsCompressVersion = '1.20'
47+
commonsHttpClientVersion = '5.1.3'
4748
commonsPoolVersion = '2.11.1'
4849
googleJsr305Version = '3.0.2'
4950
hamcrestVersion = '2.2'
@@ -387,6 +388,7 @@ project('spring-rabbit') {
387388
api 'org.springframework:spring-messaging'
388389
api 'org.springframework:spring-tx'
389390
optionalApi 'org.springframework:spring-webflux'
391+
optionalApi "org.apache.httpcomponents.client5:httpclient5:$commonsHttpClientVersion"
390392
optionalApi 'io.projectreactor:reactor-core'
391393
optionalApi "ch.qos.logback:logback-classic:$logbackVersion"
392394
optionalApi 'org.apache.logging.log4j:log4j-core'
@@ -473,6 +475,7 @@ project('spring-rabbit-stream') {
473475
testRuntimeOnly "com.github.luben:zstd-jni:$zstdJniVersion"
474476
testImplementation "org.testcontainers:rabbitmq:1.17.3"
475477
testImplementation "org.apache.logging.log4j:log4j-slf4j-impl:$log4jVersion"
478+
testImplementation 'org.springframework:spring-webflux'
476479
}
477480

478481
}
@@ -488,14 +491,12 @@ project('spring-rabbit-junit') {
488491
exclude group: 'org.hamcrest', module: 'hamcrest-core'
489492
}
490493
api "com.rabbitmq:amqp-client:$rabbitmqVersion"
491-
api 'org.springframework:spring-webflux'
494+
api 'org.springframework:spring-web'
492495
api 'org.junit.jupiter:junit-jupiter-api'
493496
api "org.assertj:assertj-core:$assertjVersion"
494497
optionalApi "ch.qos.logback:logback-classic:$logbackVersion"
495498
optionalApi 'org.apache.logging.log4j:log4j-core'
496499
compileOnly 'org.apiguardian:apiguardian-api:1.0.0'
497-
testRuntimeOnly 'com.fasterxml.jackson.core:jackson-core'
498-
testRuntimeOnly 'com.fasterxml.jackson.core:jackson-databind'
499500
}
500501

501502
}

spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunningSupport.java

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,16 @@
1717
package org.springframework.amqp.rabbit.junit;
1818

1919
import java.io.IOException;
20+
import java.net.Authenticator;
21+
import java.net.PasswordAuthentication;
2022
import java.net.URI;
2123
import java.net.URISyntaxException;
24+
import java.net.http.HttpClient;
25+
import java.net.http.HttpRequest;
26+
import java.net.http.HttpResponse;
27+
import java.net.http.HttpResponse.BodyHandlers;
2228
import java.nio.ByteBuffer;
2329
import java.nio.charset.StandardCharsets;
24-
import java.time.Duration;
2530
import java.util.ArrayList;
2631
import java.util.Arrays;
2732
import java.util.HashMap;
@@ -33,12 +38,8 @@
3338
import org.apache.commons.logging.Log;
3439
import org.apache.commons.logging.LogFactory;
3540

36-
import org.springframework.core.ParameterizedTypeReference;
37-
import org.springframework.http.MediaType;
3841
import org.springframework.util.Base64Utils;
3942
import org.springframework.util.StringUtils;
40-
import org.springframework.web.reactive.function.client.ExchangeFilterFunctions;
41-
import org.springframework.web.reactive.function.client.WebClient;
4243
import org.springframework.web.util.UriUtils;
4344

4445
import com.rabbitmq.client.Channel;
@@ -388,22 +389,35 @@ private Channel createQueues(Connection connection) throws IOException, URISynta
388389
}
389390

390391
private boolean alivenessTest() throws URISyntaxException {
391-
WebClient client = WebClient.builder()
392-
.filter(ExchangeFilterFunctions.basicAuthentication(this.adminUser, this.adminPassword))
392+
HttpClient client = HttpClient.newBuilder()
393+
.authenticator(new Authenticator() {
394+
395+
@Override
396+
protected PasswordAuthentication getPasswordAuthentication() {
397+
return new PasswordAuthentication(getAdminUser(), getAdminPassword().toCharArray());
398+
}
399+
400+
})
393401
.build();
394402
URI uri = new URI(getAdminUri())
395403
.resolve("/api/aliveness-test/" + UriUtils.encodePathSegment("/", StandardCharsets.UTF_8));
396-
HashMap<String, String> result = client.get()
404+
HttpRequest request = HttpRequest.newBuilder()
405+
.GET()
397406
.uri(uri)
398-
.accept(MediaType.APPLICATION_JSON)
399-
.retrieve()
400-
.bodyToMono(new ParameterizedTypeReference<HashMap<String, String>>() {
401-
})
402-
.block(Duration.ofSeconds(10)); // NOSONAR magic#
403-
if (result != null) {
404-
return result.get("status").equals("ok");
407+
.build();
408+
HttpResponse<String> response;
409+
try {
410+
response = client.send(request, BodyHandlers.ofString());
411+
}
412+
catch (IOException ex) {
413+
LOGGER.error("Exception checking admin aliveness", ex);
414+
return false;
415+
}
416+
catch (InterruptedException ex) {
417+
Thread.currentThread().interrupt();
418+
return false;
405419
}
406-
return false;
420+
return response.body().contentEquals("{\"status\":\"ok\"}");
407421
}
408422

409423
public static boolean fatal() {

spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.springframework.core.log.LogAccessor;
3636
import org.springframework.lang.Nullable;
3737
import org.springframework.util.Assert;
38+
import org.springframework.util.ClassUtils;
3839

3940
/**
4041
* A {@link RoutingConnectionFactory} that determines the node on which a queue is located and
@@ -54,6 +55,13 @@
5455
*/
5556
public class LocalizedQueueConnectionFactory implements ConnectionFactory, RoutingConnectionFactory, DisposableBean {
5657

58+
private static final boolean USING_WEBFLUX;
59+
60+
static {
61+
USING_WEBFLUX = ClassUtils.isPresent("org.springframework.web.reactive.function.client.WebClient",
62+
LocalizedQueueConnectionFactory.class.getClassLoader());
63+
}
64+
5765
private final Log logger = LogFactory.getLog(getClass());
5866

5967
private final Map<String, ConnectionFactory> nodeFactories = new HashMap<String, ConnectionFactory>();
@@ -82,7 +90,7 @@ public class LocalizedQueueConnectionFactory implements ConnectionFactory, Routi
8290

8391
private final String trustStorePassPhrase;
8492

85-
private NodeLocator<?> nodeLocator = new WebFluxNodeLocator();
93+
private NodeLocator<?> nodeLocator;
8694

8795
/**
8896
* @param defaultConnectionFactory the fallback connection factory to use if the queue
@@ -190,6 +198,12 @@ private LocalizedQueueConnectionFactory(ConnectionFactory defaultConnectionFacto
190198
this.trustStore = trustStore;
191199
this.keyStorePassPhrase = keyStorePassPhrase;
192200
this.trustStorePassPhrase = trustStorePassPhrase;
201+
if (USING_WEBFLUX) {
202+
this.nodeLocator = new WebFluxNodeLocator();
203+
}
204+
else {
205+
this.nodeLocator = new RestTemplateNodeLocator();
206+
}
193207
}
194208

195209
private static Map<String, String> nodesAddressesToMap(String[] nodes, String[] addresses) {
@@ -206,6 +220,7 @@ private static Map<String, String> nodesAddressesToMap(String[] nodes, String[]
206220
* @since 2.4.8
207221
*/
208222
public void setNodeLocator(NodeLocator<?> nodeLocator) {
223+
Assert.notNull(nodeLocator, "'nodeLocator' cannot be null");
209224
this.nodeLocator = nodeLocator;
210225
}
211226

@@ -378,7 +393,7 @@ default ConnectionFactory locate(String[] adminUris, Map<String, String> nodeToA
378393
try {
379394
String uri = new URI(adminUri)
380395
.resolve("/api/queues/").toString();
381-
HashMap<String, Object> queueInfo = restCall(client, uri, vhost, queue);
396+
Map<String, Object> queueInfo = restCall(client, uri, vhost, queue);
382397
if (queueInfo != null) {
383398
String node = (String) queueInfo.get("node");
384399
if (node != null) {
@@ -429,7 +444,8 @@ default void close(T client) {
429444
* @return the map of queue properties.
430445
* @throws URISyntaxException if the syntax is bad.
431446
*/
432-
HashMap<String, Object> restCall(T client, String baseUri, String vhost, String queue)
447+
@Nullable
448+
Map<String, Object> restCall(T client, String baseUri, String vhost, String queue)
433449
throws URISyntaxException;
434450

435451
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.amqp.rabbit.connection;
18+
19+
import org.springframework.web.client.RestTemplate;
20+
21+
/**
22+
* Holder for a {@link RestTemplate} and credentials.
23+
*
24+
* @author Gary Russell
25+
* @since 2.4.8
26+
*
27+
*/
28+
class RestTemplateHolder {
29+
30+
final String userName; // NOSONAR
31+
32+
final String password; // NOSONAR
33+
34+
RestTemplate template; // NOSONAR
35+
36+
RestTemplateHolder(String userName, String password) {
37+
this.userName = userName;
38+
this.password = password;
39+
}
40+
41+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.amqp.rabbit.connection;
18+
19+
import java.io.IOException;
20+
import java.net.URI;
21+
import java.net.URISyntaxException;
22+
import java.nio.charset.StandardCharsets;
23+
import java.util.Map;
24+
25+
import org.apache.hc.client5.http.auth.AuthCache;
26+
import org.apache.hc.client5.http.impl.auth.BasicAuthCache;
27+
import org.apache.hc.client5.http.impl.auth.BasicScheme;
28+
import org.apache.hc.client5.http.protocol.HttpClientContext;
29+
import org.apache.hc.core5.http.HttpHost;
30+
import org.apache.hc.core5.http.protocol.BasicHttpContext;
31+
import org.apache.hc.core5.http.protocol.HttpContext;
32+
33+
import org.springframework.amqp.rabbit.connection.LocalizedQueueConnectionFactory.NodeLocator;
34+
import org.springframework.http.HttpMethod;
35+
import org.springframework.http.HttpStatus;
36+
import org.springframework.http.ResponseEntity;
37+
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
38+
import org.springframework.http.client.support.BasicAuthenticationInterceptor;
39+
import org.springframework.lang.Nullable;
40+
import org.springframework.web.client.RestTemplate;
41+
import org.springframework.web.util.UriUtils;
42+
43+
/**
44+
* A {@link NodeLocator} using the {@link RestTemplate}.
45+
*
46+
* @author Gary Russell
47+
* @since 3.0
48+
*
49+
*/
50+
public class RestTemplateNodeLocator implements NodeLocator<RestTemplateHolder> {
51+
52+
@Override
53+
public RestTemplateHolder createClient(String userName, String password) {
54+
return new RestTemplateHolder(userName, password);
55+
}
56+
57+
@SuppressWarnings({ "unchecked", "rawtypes" })
58+
@Override
59+
@Nullable
60+
public Map<String, Object> restCall(RestTemplateHolder client, String baseUri, String vhost, String queue)
61+
throws URISyntaxException {
62+
63+
if (client.template == null) {
64+
URI uri = new URI(baseUri);
65+
HttpHost host = new HttpHost(uri.getHost(), uri.getPort());
66+
client.template = new RestTemplate(new HttpComponentsClientHttpRequestFactory() {
67+
68+
@Override
69+
@Nullable
70+
protected HttpContext createHttpContext(HttpMethod httpMethod, URI uri) {
71+
AuthCache cache = new BasicAuthCache();
72+
BasicScheme scheme = new BasicScheme();
73+
cache.put(host, scheme);
74+
BasicHttpContext context = new BasicHttpContext();
75+
context.setAttribute(HttpClientContext.AUTH_CACHE, cache);
76+
return context;
77+
}
78+
79+
});
80+
client.template.getInterceptors().add(new BasicAuthenticationInterceptor(client.userName, client.password));
81+
}
82+
URI uri = new URI(baseUri)
83+
.resolve("/api/queues/" + UriUtils.encodePathSegment(vhost, StandardCharsets.UTF_8) + "/" + queue);
84+
ResponseEntity<Map> response = client.template.exchange(uri, HttpMethod.GET, null, Map.class);
85+
return response.getStatusCode().equals(HttpStatus.OK) ? response.getBody() : null;
86+
}
87+
88+
@Override
89+
public void close(RestTemplateHolder client) {
90+
try {
91+
client.template.close();
92+
}
93+
catch (IOException e) {
94+
}
95+
}
96+
97+
}

spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/WebFluxNodeLocator.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@
2121
import java.nio.charset.StandardCharsets;
2222
import java.time.Duration;
2323
import java.util.HashMap;
24+
import java.util.Map;
2425

2526
import org.springframework.amqp.rabbit.connection.LocalizedQueueConnectionFactory.NodeLocator;
2627
import org.springframework.core.ParameterizedTypeReference;
2728
import org.springframework.http.MediaType;
29+
import org.springframework.lang.Nullable;
2830
import org.springframework.web.reactive.function.client.ExchangeFilterFunctions;
2931
import org.springframework.web.reactive.function.client.WebClient;
3032
import org.springframework.web.util.UriUtils;
@@ -39,7 +41,8 @@
3941
public class WebFluxNodeLocator implements NodeLocator<WebClient> {
4042

4143
@Override
42-
public HashMap<String, Object> restCall(WebClient client, String baseUri, String vhost, String queue)
44+
@Nullable
45+
public Map<String, Object> restCall(WebClient client, String baseUri, String vhost, String queue)
4346
throws URISyntaxException {
4447

4548
URI uri = new URI(baseUri)

spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryIntegrationTests.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ void findLocal() {
8383
ConnectionFactory cf = lqcf.getTargetConnectionFactory("[local]");
8484
RabbitAdmin admin = new RabbitAdmin(cf);
8585
assertThat(admin.getQueueProperties("local")).isNotNull();
86+
lqcf.setNodeLocator(new RestTemplateNodeLocator());
87+
ConnectionFactory cf2 = lqcf.getTargetConnectionFactory("[local]");
88+
assertThat(cf2).isSameAs(cf);
8689
lqcf.destroy();
8790
}
8891

src/reference/asciidoc/amqp.adoc

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -796,7 +796,9 @@ Notice that the first three parameters are arrays of `addresses`, `adminUris`, a
796796
These are positional in that, when a container attempts to connect to a queue, it uses the admin API to determine which node is the lead for the queue and connects to the address in the same array position as that node.
797797

798798
IMPORTANT: Starting with version 3.0, the RabbitMQ `http-client` is no longer used to access the Rest API.
799-
Instead, by default, the `WebClient` from Spring Webflux is used; which is not added to the class path by default.
799+
Instead, by default, the `WebClient` from Spring Webflux is used if `spring-webflux` is on the class path; otherwise a `RestTemplate` is used.
800+
801+
To add `WebFlux` to the class path:
800802

801803
.Maven
802804
====
@@ -821,7 +823,7 @@ You can also use other REST technology by implementing `LocalizedQueueConnection
821823
====
822824
[source, java]
823825
----
824-
lqcf.setNodeLocator(new DefaultNodeLocator<MyClient>() {
826+
lqcf.setNodeLocator(new NodeLocator<MyClient>() {
825827
826828
@Override
827829
public MyClient createClient(String userName, String password) {
@@ -837,6 +839,8 @@ lqcf.setNodeLocator(new DefaultNodeLocator<MyClient>() {
837839
----
838840
====
839841

842+
The framework provides the `WebFluxNodeLocator` and `RestTemplateNodeLocator`, with the default as discussed above.
843+
840844
[[cf-pub-conf-ret]]
841845
===== Publisher Confirms and Returns
842846

0 commit comments

Comments
 (0)