Skip to content

Commit cee7439

Browse files
nosanmhalbritter
authored andcommitted
Add service connection support for Hazelcast
See spring-projectsgh-42416
1 parent fb3dd68 commit cee7439

File tree

23 files changed

+756
-35
lines changed

23 files changed

+756
-35
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastClientConfiguration.java

+10-33
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2022 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,13 +16,8 @@
1616

1717
package org.springframework.boot.autoconfigure.hazelcast;
1818

19-
import java.io.IOException;
20-
import java.net.URL;
21-
2219
import com.hazelcast.client.HazelcastClient;
2320
import com.hazelcast.client.config.ClientConfig;
24-
import com.hazelcast.client.config.XmlClientConfigBuilder;
25-
import com.hazelcast.client.config.YamlClientConfigBuilder;
2621
import com.hazelcast.core.HazelcastInstance;
2722

2823
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@@ -31,9 +26,8 @@
3126
import org.springframework.context.annotation.Bean;
3227
import org.springframework.context.annotation.Conditional;
3328
import org.springframework.context.annotation.Configuration;
34-
import org.springframework.core.io.Resource;
29+
import org.springframework.context.annotation.Import;
3530
import org.springframework.core.io.ResourceLoader;
36-
import org.springframework.util.StringUtils;
3731

3832
/**
3933
* Configuration for Hazelcast client.
@@ -44,49 +38,32 @@
4438
@Configuration(proxyBeanMethods = false)
4539
@ConditionalOnClass(HazelcastClient.class)
4640
@ConditionalOnMissingBean(HazelcastInstance.class)
41+
@Import(HazelcastClientInstanceConfiguration.class)
4742
class HazelcastClientConfiguration {
4843

4944
static final String CONFIG_SYSTEM_PROPERTY = "hazelcast.client.config";
5045

51-
private static HazelcastInstance getHazelcastInstance(ClientConfig config) {
52-
if (StringUtils.hasText(config.getInstanceName())) {
53-
return HazelcastClient.getOrCreateHazelcastClient(config);
54-
}
55-
return HazelcastClient.newHazelcastClient(config);
56-
}
57-
5846
@Configuration(proxyBeanMethods = false)
59-
@ConditionalOnMissingBean(ClientConfig.class)
47+
@ConditionalOnMissingBean({ ClientConfig.class, HazelcastConnectionDetails.class })
6048
@Conditional(HazelcastClientConfigAvailableCondition.class)
6149
static class HazelcastClientConfigFileConfiguration {
6250

6351
@Bean
64-
HazelcastInstance hazelcastInstance(HazelcastProperties properties, ResourceLoader resourceLoader)
65-
throws IOException {
66-
Resource configLocation = properties.resolveConfigLocation();
67-
ClientConfig config = (configLocation != null) ? loadClientConfig(configLocation) : ClientConfig.load();
68-
config.setClassLoader(resourceLoader.getClassLoader());
69-
return getHazelcastInstance(config);
70-
}
71-
72-
private ClientConfig loadClientConfig(Resource configLocation) throws IOException {
73-
URL configUrl = configLocation.getURL();
74-
String configFileName = configUrl.getPath();
75-
if (configFileName.endsWith(".yaml") || configFileName.endsWith(".yml")) {
76-
return new YamlClientConfigBuilder(configUrl).build();
77-
}
78-
return new XmlClientConfigBuilder(configUrl).build();
52+
HazelcastConnectionDetails hazelcastConnectionDetails(HazelcastProperties properties,
53+
ResourceLoader resourceLoader) {
54+
return new PropertiesHazelcastConnectionDetails(properties, resourceLoader);
7955
}
8056

8157
}
8258

8359
@Configuration(proxyBeanMethods = false)
60+
@ConditionalOnMissingBean(HazelcastConnectionDetails.class)
8461
@ConditionalOnSingleCandidate(ClientConfig.class)
8562
static class HazelcastClientConfigConfiguration {
8663

8764
@Bean
88-
HazelcastInstance hazelcastInstance(ClientConfig config) {
89-
return getHazelcastInstance(config);
65+
HazelcastConnectionDetails hazelcastConnectionDetails(ClientConfig config) {
66+
return () -> config;
9067
}
9168

9269
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2012-2024 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.boot.autoconfigure.hazelcast;
18+
19+
import com.hazelcast.client.HazelcastClient;
20+
import com.hazelcast.client.config.ClientConfig;
21+
import com.hazelcast.core.HazelcastInstance;
22+
23+
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
24+
import org.springframework.context.annotation.Bean;
25+
import org.springframework.context.annotation.Configuration;
26+
import org.springframework.util.StringUtils;
27+
28+
/**
29+
* Configuration for Hazelcast client instance.
30+
*
31+
* @author Dmytro Nosan
32+
*/
33+
@Configuration(proxyBeanMethods = false)
34+
@ConditionalOnBean(HazelcastConnectionDetails.class)
35+
class HazelcastClientInstanceConfiguration {
36+
37+
@Bean
38+
HazelcastInstance hazelcastInstance(HazelcastConnectionDetails hazelcastConnectionDetails) {
39+
ClientConfig config = hazelcastConnectionDetails.getClientConfig();
40+
if (StringUtils.hasText(config.getInstanceName())) {
41+
return HazelcastClient.getOrCreateHazelcastClient(config);
42+
}
43+
return HazelcastClient.newHazelcastClient(config);
44+
}
45+
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2012-2024 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.boot.autoconfigure.hazelcast;
18+
19+
import com.hazelcast.client.config.ClientConfig;
20+
21+
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
22+
23+
/**
24+
* Details required to establish a client connection to a Hazelcast instance.
25+
*
26+
* @author Dmytro Nosan
27+
* @since 3.4.0
28+
*/
29+
public interface HazelcastConnectionDetails extends ConnectionDetails {
30+
31+
/**
32+
* The {@link ClientConfig} for Hazelcast client.
33+
* @return the client config
34+
*/
35+
ClientConfig getClientConfig();
36+
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright 2012-2024 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.boot.autoconfigure.hazelcast;
18+
19+
import java.io.IOException;
20+
import java.io.UncheckedIOException;
21+
import java.net.URL;
22+
23+
import com.hazelcast.client.config.ClientConfig;
24+
import com.hazelcast.client.config.XmlClientConfigBuilder;
25+
import com.hazelcast.client.config.YamlClientConfigBuilder;
26+
27+
import org.springframework.core.io.Resource;
28+
import org.springframework.core.io.ResourceLoader;
29+
30+
/**
31+
* Adapts {@link HazelcastProperties} to {@link HazelcastConnectionDetails}.
32+
*
33+
* @author Dmytro Nosan
34+
*/
35+
class PropertiesHazelcastConnectionDetails implements HazelcastConnectionDetails {
36+
37+
private final HazelcastProperties properties;
38+
39+
private final ResourceLoader resourceLoader;
40+
41+
PropertiesHazelcastConnectionDetails(HazelcastProperties properties, ResourceLoader resourceLoader) {
42+
this.properties = properties;
43+
this.resourceLoader = resourceLoader;
44+
}
45+
46+
@Override
47+
public ClientConfig getClientConfig() {
48+
Resource configLocation = this.properties.resolveConfigLocation();
49+
ClientConfig config = (configLocation != null) ? loadClientConfig(configLocation) : ClientConfig.load();
50+
config.setClassLoader(this.resourceLoader.getClassLoader());
51+
return config;
52+
}
53+
54+
private ClientConfig loadClientConfig(Resource configLocation) {
55+
try {
56+
URL configUrl = configLocation.getURL();
57+
String configFileName = configUrl.getPath();
58+
if (configFileName.endsWith(".yaml") || configFileName.endsWith(".yml")) {
59+
return new YamlClientConfigBuilder(configUrl).build();
60+
}
61+
return new XmlClientConfigBuilder(configUrl).build();
62+
}
63+
catch (IOException ex) {
64+
throw new UncheckedIOException("Failed to load Hazelcast config", ex);
65+
}
66+
}
67+
68+
}

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastAutoConfigurationClientTests.java

+30
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.net.InetSocketAddress;
2424
import java.net.MalformedURLException;
2525
import java.nio.file.Files;
26+
import java.util.Set;
2627

2728
import com.hazelcast.client.HazelcastClient;
2829
import com.hazelcast.client.config.ClientConfig;
@@ -150,6 +151,21 @@ void clientConfigTakesPrecedence() {
150151
.isInstanceOf(HazelcastClientProxy.class));
151152
}
152153

154+
@Test
155+
void connectionDetailsTakesPrecedenceOverConfigFile() {
156+
this.contextRunner.withUserConfiguration(HazelcastConnectionDetailsConfig.class)
157+
.withPropertyValues("spring.hazelcast.config=this-is-ignored.xml")
158+
.run(assertSpecificHazelcastClient("connection-details"));
159+
}
160+
161+
@Test
162+
void connectionDetailsTakesPrecedenceOverUserDefinedClientConfig() {
163+
this.contextRunner
164+
.withUserConfiguration(HazelcastConnectionDetailsConfig.class, HazelcastServerAndClientConfig.class)
165+
.withPropertyValues("spring.hazelcast.config=this-is-ignored.xml")
166+
.run(assertSpecificHazelcastClient("connection-details"));
167+
}
168+
153169
@Test
154170
void clientConfigWithInstanceNameCreatesClientIfNecessary() throws MalformedURLException {
155171
assertThat(HazelcastClient.getHazelcastClientByName("spring-boot")).isNull();
@@ -202,6 +218,20 @@ private File prepareConfiguration(String input) {
202218
}
203219
}
204220

221+
@Configuration(proxyBeanMethods = false)
222+
static class HazelcastConnectionDetailsConfig {
223+
224+
@Bean
225+
HazelcastConnectionDetails hazelcastConnectionDetails() {
226+
ClientConfig config = new ClientConfig();
227+
config.setLabels(Set.of("connection-details"));
228+
config.getConnectionStrategyConfig().getConnectionRetryConfig().setClusterConnectTimeoutMillis(60000);
229+
config.getNetworkConfig().getAddresses().add(endpointAddress);
230+
return () -> config;
231+
}
232+
233+
}
234+
205235
@Configuration(proxyBeanMethods = false)
206236
static class HazelcastServerAndClientConfig {
207237

spring-boot-project/spring-boot-docker-compose/build.gradle

+3
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@ dependencies {
1212
api(project(":spring-boot-project:spring-boot"))
1313

1414
dockerTestImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support-docker"))
15+
dockerTestImplementation("com.hazelcast:hazelcast")
1516
dockerTestImplementation("com.redis:testcontainers-redis")
1617
dockerTestImplementation("org.assertj:assertj-core")
1718
dockerTestImplementation("org.awaitility:awaitility")
1819
dockerTestImplementation("org.junit.jupiter:junit-jupiter")
1920
dockerTestImplementation("org.testcontainers:testcontainers")
2021

22+
2123
dockerTestRuntimeOnly("com.microsoft.sqlserver:mssql-jdbc")
2224
dockerTestRuntimeOnly("com.oracle.database.r2dbc:oracle-r2dbc")
2325
dockerTestRuntimeOnly("io.r2dbc:r2dbc-mssql")
@@ -33,6 +35,7 @@ dependencies {
3335
optional("org.mongodb:mongodb-driver-core")
3436
optional("org.neo4j.driver:neo4j-java-driver")
3537
optional("org.springframework.data:spring-data-r2dbc")
38+
optional("com.hazelcast:hazelcast")
3639

3740
testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support"))
3841
testImplementation(project(":spring-boot-project:spring-boot-test"))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2012-2024 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.boot.docker.compose.service.connection.hazelcast;
18+
19+
import java.util.UUID;
20+
21+
import com.hazelcast.client.HazelcastClient;
22+
import com.hazelcast.client.config.ClientConfig;
23+
import com.hazelcast.config.Config;
24+
import com.hazelcast.core.HazelcastInstance;
25+
import com.hazelcast.map.IMap;
26+
27+
import org.springframework.boot.autoconfigure.hazelcast.HazelcastConnectionDetails;
28+
import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest;
29+
import org.springframework.boot.testsupport.container.TestImage;
30+
31+
import static org.assertj.core.api.Assertions.assertThat;
32+
33+
/**
34+
* Integration tests for {@link HazelcastDockerComposeConnectionDetailsFactory}.
35+
*
36+
* @author Dmytro Nosan
37+
*/
38+
class HazelcastDockerComposeConnectionDetailsFactoryIntegrationTests {
39+
40+
@DockerComposeTest(composeFile = "hazelcast-compose.yaml", image = TestImage.HAZELCAST)
41+
void runCreatesConnectionDetails(HazelcastConnectionDetails connectionDetails) {
42+
ClientConfig config = connectionDetails.getClientConfig();
43+
assertThat(config.getClusterName()).isEqualTo(Config.DEFAULT_CLUSTER_NAME);
44+
verifyConnection(config);
45+
}
46+
47+
@DockerComposeTest(composeFile = "hazelcast-cluster-name-compose.yaml", image = TestImage.HAZELCAST)
48+
void runCreatesConnectionDetailsCustomClusterName(HazelcastConnectionDetails connectionDetails) {
49+
ClientConfig config = connectionDetails.getClientConfig();
50+
assertThat(config.getClusterName()).isEqualTo("spring-boot");
51+
verifyConnection(config);
52+
}
53+
54+
private static void verifyConnection(ClientConfig config) {
55+
HazelcastInstance hazelcastInstance = HazelcastClient.newHazelcastClient(config);
56+
try {
57+
IMap<String, String> map = hazelcastInstance.getMap(UUID.randomUUID().toString());
58+
map.put("docker", "compose");
59+
assertThat(map.get("docker")).isEqualTo("compose");
60+
}
61+
finally {
62+
hazelcastInstance.shutdown();
63+
}
64+
}
65+
66+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
services:
2+
hazelcast:
3+
image: '{imageName}'
4+
environment:
5+
HZ_CLUSTERNAME: "spring-boot"
6+
ports:
7+
- '5701'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
services:
2+
hazelcast:
3+
image: '{imageName}'
4+
ports:
5+
- '5701'

0 commit comments

Comments
 (0)