Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide service connection support for Hazelcast #42416

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2022 the original author or authors.
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,13 +16,8 @@

package org.springframework.boot.autoconfigure.hazelcast;

import java.io.IOException;
import java.net.URL;

import com.hazelcast.client.HazelcastClient;
import com.hazelcast.client.config.ClientConfig;
import com.hazelcast.client.config.XmlClientConfigBuilder;
import com.hazelcast.client.config.YamlClientConfigBuilder;
import com.hazelcast.core.HazelcastInstance;

import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
Expand All @@ -31,9 +26,8 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.context.annotation.Import;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.StringUtils;

/**
* Configuration for Hazelcast client.
Expand All @@ -44,49 +38,32 @@
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(HazelcastClient.class)
@ConditionalOnMissingBean(HazelcastInstance.class)
@Import(HazelcastClientInstanceConfiguration.class)
class HazelcastClientConfiguration {

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

private static HazelcastInstance getHazelcastInstance(ClientConfig config) {
if (StringUtils.hasText(config.getInstanceName())) {
return HazelcastClient.getOrCreateHazelcastClient(config);
}
return HazelcastClient.newHazelcastClient(config);
}

@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(ClientConfig.class)
@ConditionalOnMissingBean({ ClientConfig.class, HazelcastConnectionDetails.class })
@Conditional(HazelcastClientConfigAvailableCondition.class)
static class HazelcastClientConfigFileConfiguration {

@Bean
HazelcastInstance hazelcastInstance(HazelcastProperties properties, ResourceLoader resourceLoader)
throws IOException {
Resource configLocation = properties.resolveConfigLocation();
ClientConfig config = (configLocation != null) ? loadClientConfig(configLocation) : ClientConfig.load();
config.setClassLoader(resourceLoader.getClassLoader());
return getHazelcastInstance(config);
}

private ClientConfig loadClientConfig(Resource configLocation) throws IOException {
URL configUrl = configLocation.getURL();
String configFileName = configUrl.getPath();
if (configFileName.endsWith(".yaml") || configFileName.endsWith(".yml")) {
return new YamlClientConfigBuilder(configUrl).build();
}
return new XmlClientConfigBuilder(configUrl).build();
HazelcastConnectionDetails hazelcastConnectionDetails(HazelcastProperties properties,
ResourceLoader resourceLoader) {
return new PropertiesHazelcastConnectionDetails(properties, resourceLoader);
}

}

@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(HazelcastConnectionDetails.class)
@ConditionalOnSingleCandidate(ClientConfig.class)
static class HazelcastClientConfigConfiguration {

@Bean
HazelcastInstance hazelcastInstance(ClientConfig config) {
return getHazelcastInstance(config);
HazelcastConnectionDetails hazelcastConnectionDetails(ClientConfig config) {
return () -> config;
}

}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.boot.autoconfigure.hazelcast;

import com.hazelcast.client.HazelcastClient;
import com.hazelcast.client.config.ClientConfig;
import com.hazelcast.core.HazelcastInstance;

import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;

/**
* Configuration for Hazelcast client instance.
*
* @author Dmytro Nosan
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(HazelcastConnectionDetails.class)
class HazelcastClientInstanceConfiguration {

@Bean
HazelcastInstance hazelcastInstance(HazelcastConnectionDetails hazelcastConnectionDetails) {
ClientConfig config = hazelcastConnectionDetails.getClientConfig();
if (StringUtils.hasText(config.getInstanceName())) {
return HazelcastClient.getOrCreateHazelcastClient(config);
}
return HazelcastClient.newHazelcastClient(config);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.boot.autoconfigure.hazelcast;

import com.hazelcast.client.config.ClientConfig;

import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;

/**
* Details required to establish a client connection to a Hazelcast instance.
*
* @author Dmytro Nosan
* @since 3.4.0
*/
public interface HazelcastConnectionDetails extends ConnectionDetails {

/**
* The {@link ClientConfig} for Hazelcast client.
* @return the client config
*/
ClientConfig getClientConfig();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.boot.autoconfigure.hazelcast;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URL;

import com.hazelcast.client.config.ClientConfig;
import com.hazelcast.client.config.XmlClientConfigBuilder;
import com.hazelcast.client.config.YamlClientConfigBuilder;

import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;

/**
* Adapts {@link HazelcastProperties} to {@link HazelcastConnectionDetails}.
*
* @author Dmytro Nosan
*/
class PropertiesHazelcastConnectionDetails implements HazelcastConnectionDetails {

private final HazelcastProperties properties;

private final ResourceLoader resourceLoader;

PropertiesHazelcastConnectionDetails(HazelcastProperties properties, ResourceLoader resourceLoader) {
this.properties = properties;
this.resourceLoader = resourceLoader;
}

@Override
public ClientConfig getClientConfig() {
Resource configLocation = this.properties.resolveConfigLocation();
ClientConfig config = (configLocation != null) ? loadClientConfig(configLocation) : ClientConfig.load();
config.setClassLoader(this.resourceLoader.getClassLoader());
return config;
}

private ClientConfig loadClientConfig(Resource configLocation) {
try {
URL configUrl = configLocation.getURL();
String configFileName = configUrl.getPath();
if (configFileName.endsWith(".yaml") || configFileName.endsWith(".yml")) {
return new YamlClientConfigBuilder(configUrl).build();
}
return new XmlClientConfigBuilder(configUrl).build();
}
catch (IOException ex) {
throw new UncheckedIOException("Failed to load Hazelcast config", ex);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.util.Set;

import com.hazelcast.client.HazelcastClient;
import com.hazelcast.client.config.ClientConfig;
Expand Down Expand Up @@ -150,6 +151,21 @@ void clientConfigTakesPrecedence() {
.isInstanceOf(HazelcastClientProxy.class));
}

@Test
void connectionDetailsTakesPrecedenceOverConfigFile() {
this.contextRunner.withUserConfiguration(HazelcastConnectionDetailsConfig.class)
.withPropertyValues("spring.hazelcast.config=this-is-ignored.xml")
.run(assertSpecificHazelcastClient("connection-details"));
}

@Test
void connectionDetailsTakesPrecedenceOverUserDefinedClientConfig() {
this.contextRunner
.withUserConfiguration(HazelcastConnectionDetailsConfig.class, HazelcastServerAndClientConfig.class)
.withPropertyValues("spring.hazelcast.config=this-is-ignored.xml")
.run(assertSpecificHazelcastClient("connection-details"));
}

@Test
void clientConfigWithInstanceNameCreatesClientIfNecessary() throws MalformedURLException {
assertThat(HazelcastClient.getHazelcastClientByName("spring-boot")).isNull();
Expand Down Expand Up @@ -202,6 +218,20 @@ private File prepareConfiguration(String input) {
}
}

@Configuration(proxyBeanMethods = false)
static class HazelcastConnectionDetailsConfig {

@Bean
HazelcastConnectionDetails hazelcastConnectionDetails() {
ClientConfig config = new ClientConfig();
config.setLabels(Set.of("connection-details"));
config.getConnectionStrategyConfig().getConnectionRetryConfig().setClusterConnectTimeoutMillis(60000);
config.getNetworkConfig().getAddresses().add(endpointAddress);
return () -> config;
}

}

@Configuration(proxyBeanMethods = false)
static class HazelcastServerAndClientConfig {

Expand Down
3 changes: 3 additions & 0 deletions spring-boot-project/spring-boot-docker-compose/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ dependencies {
api(project(":spring-boot-project:spring-boot"))

dockerTestImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support-docker"))
dockerTestImplementation("com.hazelcast:hazelcast")
dockerTestImplementation("com.redis:testcontainers-redis")
dockerTestImplementation("org.assertj:assertj-core")
dockerTestImplementation("org.awaitility:awaitility")
dockerTestImplementation("org.junit.jupiter:junit-jupiter")
dockerTestImplementation("org.testcontainers:testcontainers")


dockerTestRuntimeOnly("com.microsoft.sqlserver:mssql-jdbc")
dockerTestRuntimeOnly("com.oracle.database.r2dbc:oracle-r2dbc")
dockerTestRuntimeOnly("io.r2dbc:r2dbc-mssql")
Expand All @@ -33,6 +35,7 @@ dependencies {
optional("org.mongodb:mongodb-driver-core")
optional("org.neo4j.driver:neo4j-java-driver")
optional("org.springframework.data:spring-data-r2dbc")
optional("com.hazelcast:hazelcast")

testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support"))
testImplementation(project(":spring-boot-project:spring-boot-test"))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.boot.docker.compose.service.connection.hazelcast;

import java.util.UUID;

import com.hazelcast.client.HazelcastClient;
import com.hazelcast.client.config.ClientConfig;
import com.hazelcast.config.Config;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.map.IMap;

import org.springframework.boot.autoconfigure.hazelcast.HazelcastConnectionDetails;
import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest;
import org.springframework.boot.testsupport.container.TestImage;

import static org.assertj.core.api.Assertions.assertThat;

/**
* Integration tests for {@link HazelcastDockerComposeConnectionDetailsFactory}.
*
* @author Dmytro Nosan
*/
class HazelcastDockerComposeConnectionDetailsFactoryIntegrationTests {

@DockerComposeTest(composeFile = "hazelcast-compose.yaml", image = TestImage.HAZELCAST)
void runCreatesConnectionDetails(HazelcastConnectionDetails connectionDetails) {
ClientConfig config = connectionDetails.getClientConfig();
assertThat(config.getClusterName()).isEqualTo(Config.DEFAULT_CLUSTER_NAME);
verifyConnection(config);
}

@DockerComposeTest(composeFile = "hazelcast-cluster-name-compose.yaml", image = TestImage.HAZELCAST)
void runCreatesConnectionDetailsCustomClusterName(HazelcastConnectionDetails connectionDetails) {
ClientConfig config = connectionDetails.getClientConfig();
assertThat(config.getClusterName()).isEqualTo("spring-boot");
verifyConnection(config);
}

private static void verifyConnection(ClientConfig config) {
HazelcastInstance hazelcastInstance = HazelcastClient.newHazelcastClient(config);
try {
IMap<String, String> map = hazelcastInstance.getMap(UUID.randomUUID().toString());
map.put("docker", "compose");
assertThat(map.get("docker")).isEqualTo("compose");
}
finally {
hazelcastInstance.shutdown();
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
services:
hazelcast:
image: '{imageName}'
environment:
HZ_CLUSTERNAME: "spring-boot"
ports:
- '5701'
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
services:
hazelcast:
image: '{imageName}'
ports:
- '5701'
Loading
Loading