Skip to content

Commit

Permalink
Provide service connection support for Hazelcast
Browse files Browse the repository at this point in the history
  • Loading branch information
nosan committed Sep 22, 2024
1 parent c7c73be commit 1430fb3
Show file tree
Hide file tree
Showing 19 changed files with 553 additions and 36 deletions.
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 @@ -18,6 +18,8 @@ dependencies {
dockerTestImplementation("org.awaitility:awaitility")
dockerTestImplementation("org.junit.jupiter:junit-jupiter")
dockerTestImplementation("org.testcontainers:testcontainers")
dockerTestImplementation("com.hazelcast:hazelcast")


dockerTestRuntimeOnly("com.microsoft.sqlserver:mssql-jdbc")
dockerTestRuntimeOnly("com.oracle.database.r2dbc:oracle-r2dbc")
Expand All @@ -34,6 +36,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,50 @@
/*
* 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 com.hazelcast.client.HazelcastClient;
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) {
HazelcastInstance hazelcastInstance = HazelcastClient.newHazelcastClient(connectionDetails.getClientConfig());
try {
IMap<Object, Object> map = hazelcastInstance.getMap("spring-boot");
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,5 @@
services:
hazelcast:
image: '{imageName}'
ports:
- '5701'
Loading

0 comments on commit 1430fb3

Please sign in to comment.